|
| 1 | +# Parity `trace_` Namespace |
| 2 | + |
| 3 | +Add the Parity/OpenEthereum `trace_` JSON-RPC namespace to signet-rpc for |
| 4 | +Blockscout and general tooling compatibility. Driven by ENG-1064 and ENG-1065. |
| 5 | + |
| 6 | +## Scope |
| 7 | + |
| 8 | +9 methods in a new `trace` namespace. No block/uncle reward traces — Signet |
| 9 | +is a post-merge L2. No `debug_storageRangeAt` — Geth's hashed-key pagination |
| 10 | +format doesn't match Signet's plain-key storage, and reth hasn't implemented |
| 11 | +it either. |
| 12 | + |
| 13 | +## Methods |
| 14 | + |
| 15 | +| Method | Input | Output | |
| 16 | +|--------|-------|--------| |
| 17 | +| `trace_block` | `BlockNumberOrTag` | `Option<Vec<LocalizedTransactionTrace>>` | |
| 18 | +| `trace_transaction` | `B256` | `Option<Vec<LocalizedTransactionTrace>>` | |
| 19 | +| `trace_replayBlockTransactions` | `BlockNumberOrTag, HashSet<TraceType>` | `Option<Vec<TraceResultsWithTransactionHash>>` | |
| 20 | +| `trace_replayTransaction` | `B256, HashSet<TraceType>` | `TraceResults` | |
| 21 | +| `trace_call` | `TransactionRequest, HashSet<TraceType>, Option<BlockId>` | `TraceResults` | |
| 22 | +| `trace_callMany` | `Vec<(TransactionRequest, HashSet<TraceType>)>, Option<BlockId>` | `Vec<TraceResults>` | |
| 23 | +| `trace_rawTransaction` | `Bytes, HashSet<TraceType>, Option<BlockId>` | `TraceResults` | |
| 24 | +| `trace_get` | `B256, Vec<usize>` | `Option<LocalizedTransactionTrace>` | |
| 25 | +| `trace_filter` | `TraceFilter` | `Vec<LocalizedTransactionTrace>` | |
| 26 | + |
| 27 | +All types from `alloy::rpc::types::trace::parity` and |
| 28 | +`alloy::rpc::types::trace::filter`. |
| 29 | + |
| 30 | +## Architecture |
| 31 | + |
| 32 | +### New files |
| 33 | + |
| 34 | +- `crates/rpc/src/trace/mod.rs` — router (9 routes) + re-exports |
| 35 | +- `crates/rpc/src/trace/endpoints.rs` — 9 handlers + 2 shared replay helpers |
| 36 | +- `crates/rpc/src/trace/error.rs` — `TraceError` with `IntoErrorPayload` |
| 37 | +- `crates/rpc/src/trace/types.rs` — param tuple structs for ajj positional |
| 38 | + param deserialization (following `debug/types.rs` pattern) |
| 39 | + |
| 40 | +### Modified files |
| 41 | + |
| 42 | +- `crates/rpc/src/debug/tracer.rs` — add 2 `pub(crate)` Parity tracer |
| 43 | + functions |
| 44 | +- `crates/rpc/src/config/rpc_config.rs` — add `max_trace_filter_blocks` |
| 45 | +- `crates/rpc/src/lib.rs` — add `mod trace`, export `TraceError`, nest router |
| 46 | + |
| 47 | +### No new dependencies |
| 48 | + |
| 49 | +`revm-inspectors` 0.34.2 already has `ParityTraceBuilder`. `alloy` 1.7.3 |
| 50 | +already has all Parity trace types. The existing `trace_flat_call` in |
| 51 | +`debug/tracer.rs` already uses `inspector.into_parity_builder()`. |
| 52 | + |
| 53 | +## Param Types |
| 54 | + |
| 55 | +Tuple structs in `trace/types.rs` for ajj positional param deserialization, |
| 56 | +following the pattern in `debug/types.rs`: |
| 57 | + |
| 58 | +``` |
| 59 | +TraceBlockParams(BlockNumberOrTag) |
| 60 | +TraceTransactionParams(B256) |
| 61 | +ReplayBlockParams(BlockNumberOrTag, HashSet<TraceType>) |
| 62 | +ReplayTransactionParams(B256, HashSet<TraceType>) |
| 63 | +TraceCallParams(TransactionRequest, HashSet<TraceType>, Option<BlockId>, |
| 64 | + Option<StateOverride>, Option<Box<BlockOverrides>>) |
| 65 | +TraceCallManyParams(Vec<(TransactionRequest, HashSet<TraceType>)>, |
| 66 | + Option<BlockId>) |
| 67 | +TraceRawTransactionParams(Bytes, HashSet<TraceType>, Option<BlockId>) |
| 68 | +TraceGetParams(B256, Vec<usize>) |
| 69 | +TraceFilterParams(TraceFilter) |
| 70 | +``` |
| 71 | + |
| 72 | +`trace_call` includes state and block override fields to support reth's |
| 73 | +`TraceCallRequest` semantics via positional params. |
| 74 | + |
| 75 | +## Parity Tracer Functions |
| 76 | + |
| 77 | +Two new `pub(crate)` functions in `debug/tracer.rs`: |
| 78 | + |
| 79 | +**`trace_parity_localized(trevm, tx_info)`** |
| 80 | +Returns `(Vec<LocalizedTransactionTrace>, EvmNeedsTx)`. Creates |
| 81 | +`TracingInspector` with `TracingInspectorConfig::default_parity()`, runs the |
| 82 | +tx via `try_with_inspector`, extracts `gas_used` from the result (matching |
| 83 | +`trace_flat_call` pattern), converts via |
| 84 | +`into_parity_builder().with_transaction_gas_used(gas).into_localized_transaction_traces(tx_info)`. |
| 85 | + |
| 86 | +Used by: `trace_block`, `trace_transaction`, `trace_get`, `trace_filter`. |
| 87 | + |
| 88 | +**`trace_parity_replay(trevm, trace_types)`** |
| 89 | +Returns `(TraceResults, EvmNeedsTx)`. Requires `Db: Database + DatabaseRef` |
| 90 | +(the `DatabaseRef` bound is needed for `populate_state_diff`). Creates |
| 91 | +`TracingInspector` with |
| 92 | +`TracingInspectorConfig::from_parity_config(&trace_types)`. Runs the tx, |
| 93 | +then: |
| 94 | + |
| 95 | +1. Takes result WITHOUT committing state (holds uncommitted state). |
| 96 | +2. Converts via `into_parity_builder().into_trace_results(&result, &trace_types)`. |
| 97 | +3. If `StateDiff` is requested: calls `populate_state_diff(&mut state_diff, &db, state.iter())` to enrich with pre-tx balance/nonce from the DB. |
| 98 | +4. Then commits state. |
| 99 | + |
| 100 | +This matches reth's `replayBlockTransactions` pattern. |
| 101 | + |
| 102 | +Used by: `trace_replayBlockTransactions`, `trace_call`, `trace_callMany`, |
| 103 | +`trace_rawTransaction`. |
| 104 | + |
| 105 | +**Exception:** `trace_replayTransaction` uses |
| 106 | +`into_trace_results_with_state(&res, &trace_types, &db)` instead. This |
| 107 | +method takes `&ResultAndState` (a different type) and handles state diff |
| 108 | +population internally. Matches reth's divergent pattern for single-tx replay. |
| 109 | +The `DB::Error` from this call must be mapped into `TraceError`. |
| 110 | + |
| 111 | +## Shared Block Replay Helpers |
| 112 | + |
| 113 | +Two inner functions in `trace/endpoints.rs`: |
| 114 | + |
| 115 | +**`trace_block_localized()`** — replays block txs, calls |
| 116 | +`trace_parity_localized` for each. Stops at the first magic-sig tx (using |
| 117 | +`peeking_take_while`, same as `debug::trace_block_inner`). Returns |
| 118 | +`Vec<LocalizedTransactionTrace>`. No reward traces (Signet is post-merge). |
| 119 | + |
| 120 | +**`trace_block_replay()`** — same replay loop but calls |
| 121 | +`trace_parity_replay` with the caller's `HashSet<TraceType>`. Returns |
| 122 | +`Vec<TraceResultsWithTransactionHash>`. |
| 123 | + |
| 124 | +Both follow the same EVM setup pattern as `debug::trace_block_inner`: |
| 125 | +`signet_evm::signet_evm()`, `fill_cfg`, `fill_block`, iterate txs with |
| 126 | +`peeking_take_while`. All handlers use `tracing::debug_span!` + |
| 127 | +`.instrument(span)` for instrumentation (matching existing debug endpoints). |
| 128 | + |
| 129 | +## Method Details |
| 130 | + |
| 131 | +### `trace_block` |
| 132 | + |
| 133 | +Semaphore-gated. Resolves block, delegates to `trace_block_localized`. |
| 134 | +Returns `None` if block not found. No reward traces. |
| 135 | + |
| 136 | +### `trace_transaction` |
| 137 | + |
| 138 | +Semaphore-gated. Finds tx by hash in cold storage, resolves containing block, |
| 139 | +replays preceding txs without tracing (using `run_tx` + `accept_state`), |
| 140 | +traces target tx with `trace_parity_localized`. Returns `None` if tx not |
| 141 | +found. |
| 142 | + |
| 143 | +### `trace_replayBlockTransactions` |
| 144 | + |
| 145 | +Semaphore-gated. Resolves block, delegates to `trace_block_replay` with the |
| 146 | +caller's `HashSet<TraceType>`. For each tx, wraps result in |
| 147 | +`TraceResultsWithTransactionHash`. Returns `None` if block not found. |
| 148 | + |
| 149 | +### `trace_replayTransaction` |
| 150 | + |
| 151 | +Semaphore-gated. Replays preceding txs, traces target tx. Uses |
| 152 | +`into_trace_results_with_state(&res, &trace_types, &db)` — different from |
| 153 | +`replayBlockTransactions` which uses `into_trace_results()` + |
| 154 | +`populate_state_diff()`. This matches reth's divergent pattern. Returns error |
| 155 | +(not `None`) if tx not found. |
| 156 | + |
| 157 | +### `trace_call` |
| 158 | + |
| 159 | +Semaphore-gated. Resolves EVM state at block via `resolve_evm_block`. |
| 160 | +Supports state overrides and block overrides (matching reth). Fills tx from |
| 161 | +`TransactionRequest`, traces with `trace_parity_replay`. Defaults to latest |
| 162 | +block if unspecified. |
| 163 | + |
| 164 | +### `trace_callMany` |
| 165 | + |
| 166 | +Semaphore-gated. Resolves EVM state once, then processes calls sequentially. |
| 167 | +Each call can have different `HashSet<TraceType>`. State is committed between |
| 168 | +calls via `db.commit(res.state)` — each subsequent call sees prior state |
| 169 | +changes. Last call's state is not committed. Defaults to `BlockId::pending()` |
| 170 | +if unspecified (matching reth). |
| 171 | + |
| 172 | +### `trace_rawTransaction` |
| 173 | + |
| 174 | +Semaphore-gated. Decodes RLP bytes into a transaction, recovers sender. Takes |
| 175 | +optional `block_id` (defaults to latest). Traces with `trace_parity_replay`. |
| 176 | + |
| 177 | +### `trace_get` |
| 178 | + |
| 179 | +Semaphore-gated. Returns `None` if `indices.len() != 1` (Erigon |
| 180 | +compatibility, matching reth). Delegates to `trace_transaction` to get all |
| 181 | +traces, then selects the trace at `indices[0]`. Returns `None` if index is |
| 182 | +out of bounds or tx not found. |
| 183 | + |
| 184 | +### `trace_filter` |
| 185 | + |
| 186 | +Semaphore-gated. Validates block range: |
| 187 | +- `from_block` defaults to 0, `to_block` defaults to latest |
| 188 | +- Both must be <= latest block |
| 189 | +- `from_block` must be <= `to_block` |
| 190 | +- Range must be <= `max_trace_filter_blocks` (default 100, configurable) |
| 191 | + |
| 192 | +Processes blocks sequentially. For each block, calls |
| 193 | +`trace_block_localized()`, filters results with |
| 194 | +`TraceFilter::matcher().matches(&trace.trace)`. Applies `after` (skip) and |
| 195 | +`count` (limit) pagination. No reward traces. |
| 196 | + |
| 197 | +## Error Type |
| 198 | + |
| 199 | +`TraceError` in `trace/error.rs`: |
| 200 | + |
| 201 | +``` |
| 202 | +Cold(ColdStorageError) — -32000 |
| 203 | +Hot(StorageError) — -32000 |
| 204 | +Resolve(ResolveError) — via resolve_error_code() |
| 205 | +EvmHalt { reason: String } — -32000 |
| 206 | +BlockNotFound(BlockId) — -32001 |
| 207 | +TransactionNotFound(B256) — -32001 |
| 208 | +RlpDecode(String) — -32602 |
| 209 | +SenderRecovery — -32000 |
| 210 | +BlockRangeExceeded — -32602 |
| 211 | +``` |
| 212 | + |
| 213 | +Implements `IntoErrorPayload`. Reuses `resolve_error_code` and |
| 214 | +`resolve_error_message` from `crate::eth::error`. Error messages are |
| 215 | +sanitized — storage/DB errors return `"server error"`, no internals leaked. |
| 216 | + |
| 217 | +## Configuration |
| 218 | + |
| 219 | +New field in `StorageRpcConfig`: |
| 220 | + |
| 221 | +- `max_trace_filter_blocks: u64` — default 100 |
| 222 | +- Env var: `SIGNET_RPC_MAX_TRACE_FILTER_BLOCKS` |
| 223 | +- Builder setter: `max_trace_filter_blocks(u64)` |
| 224 | + |
| 225 | +## Router |
| 226 | + |
| 227 | +``` |
| 228 | +router() |
| 229 | + |- eth::eth() (41 methods) |
| 230 | + |- debug::debug() (9 methods) |
| 231 | + |- signet::signet() (2 methods) |
| 232 | + |- web3::web3() (2 methods) |
| 233 | + |- net::net() (2 methods) |
| 234 | + +- trace::trace() (9 methods) |
| 235 | +``` |
| 236 | + |
| 237 | +65 total routes. |
| 238 | + |
| 239 | +## Ordering |
| 240 | + |
| 241 | +This work depends on PR #120 (namespace completeness) which depends on |
| 242 | +PR #119 (structured error codes). Branch off PR #120's head. |
0 commit comments