Skip to content

feat(l1): add debug_traceCall and debug_traceBlockByHash#6931

Open
edg-l wants to merge 11 commits into
mainfrom
feat/debug-trace-call-alt
Open

feat(l1): add debug_traceCall and debug_traceBlockByHash#6931
edg-l wants to merge 11 commits into
mainfrom
feat/debug-trace-call-alt

Conversation

@edg-l

@edg-l edg-l commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Adds two geth-style debug tracing endpoints and tidies up the block-trace path while doing so.

This started as debug_traceCall but grew to cover debug_traceBlockByHash too, since Dora calls that on every block on the devnet.

Note: this is an alternative take on the now-closed #6695. Same feature, independent implementation, with the block-trace perf work added.

What's new

debug_traceCall(callObject, blockNrOrHash, traceConfig)

  • All three tracers: callTracer, prestateTracer, opcodeTracer.
  • Block-or-hash plus EIP-1898 identifiers, defaults to latest.
  • Supports geth's txIndex config field (trace the call against the state just before tx i). Out-of-range txIndex is rejected to match geth instead of silently tracing against a bogus state.
  • stateOverrides / blockOverrides are not supported (documented in the config struct).
  • Reuses the existing eth_call machinery: the call is an unsigned GenericTransaction, so the sender comes from the from field with no signature recovery, and fees/base-fee are relaxed the same way simulate_tx_from_generic does.

debug_traceBlockByHash(blockHash, traceConfig)

  • We already had debug_traceBlockByNumber; this mirrors it for a block hash. The two now share a single trace_block dispatch helper.

Performance

Dora hits these on every block, so two things got optimized:

  1. debug_traceCall no longer re-executes the block. For the default case (no txIndex) it reads the block's committed post-state directly, like eth_call, instead of rebuilding the parent state and replaying every transaction. Falls back to rebuild + rerun for a specific txIndex or when the block's state isn't stored (archive/pruned gap). A default traceCall on an N-transaction block drops from ~N+1 EVM executions to 1.

  2. debug_traceBlock* traces the whole block in one pass. Replaced the per-transaction spawn_blocking + Arc<Mutex> loop with a single blocking pass, and the block-invariant EVMConfig is computed once instead of per transaction. Removes N-1 thread handoffs and N config recomputes per block trace. Side effect: the trace timeout now bounds the whole block trace rather than each transaction, which is the more sensible budget for this endpoint anyway.

Implementation notes

Layered the same way as the existing tracers:

  • crates/vm/backends/levm/tracing.rs: refactored the three trace_tx_* fns to share run_* cores, added trace_call_* (generic-tx) and trace_block_* (batch) variants on top.
  • crates/vm/tracing.rs: Evm wrappers.
  • crates/blockchain/tracing.rs: Blockchain::trace_call_* and the rewritten one-pass trace_block_*.
  • crates/networking/rpc/tracing.rs: TraceCallRequest, TraceBlockByHashRequest, shared trace_block helper.
  • crates/networking/rpc/rpc.rs: routes both new methods.

Tests

Three VM-level tests in test/tests/levm/trace_call_tests.rs covering the call / opcode / prestate-diff paths, pinning the from-field sender behaviour. The existing opcode and prestate tracer tests still pass after the core refactor.

Possible follow-up (not in this PR)

On Amsterdam the block carries a BAL (EIP-7928), which records per-transaction state changes. That makes it possible to reconstruct each transaction's pre-state from the parent state plus the BAL without replaying predecessors, which would let debug_traceBlock* trace transactions in parallel instead of sequentially. Out of scope here (correctness risk, needs the per-block BAL available at trace time), but worth a separate issue.

callTracer / debug_traceCall geth-compatibility fixes

Follow-up on a glamsterdam-devnet-6 report: execution was consensus-correct, but the callTracer reporting layer diverged from geth's de-facto output. Fixed by comparing against go-ethereum (eth/tracers/native/call.go):

  • gasUsed (correctness bug): the top-level frame now reports post-refund gas — matching the receipt and geth's callstack[0].GasUsed = receipt.GasUsed — instead of the pre-refund / EIP-7778 block-accounting value. Previously overstated gas on refund txs (e.g. by the full EIP-3529 refund).
  • debug_traceCall nonce: fills the nonce from account state when omitted (matching geth's ToMessage and our own eth_estimateGas) instead of rejecting the call on a nonce mismatch against a default of 0.
  • revertReason: decodes the ABI Error(string) payload (geth's abi.UnpackRevert) instead of raw UTF-8, which produced null for standard revert("...").
  • error strings: revert maps to "execution reverted"; static halts map to geth's wording (out of gas, write protection, contract address collision, max code size exceeded, …).
  • JSON shape: omit empty to/value/output/error/revertReason/calls like geth's omitempty; drop to on a failed CREATE; omit value for STATICCALL.
  • logs: clear logs of any frame whose ancestor reverted (geth's clearFailedLogs), and emit a block-absolute log index (geth's log.Index) for withLog traces.

Scope / compatibility: these changes touch only the callTracer (CallTraceFrame / CallLog / LevmCallTracer). The opcodeTracer (EIP-3155 Eip3155Step, consumed by the goevmlab statetest subcommand from #6663 / #6595), the prestateTracer, and VMError's Display (which feeds the execution-spec-tests / hive error mapping) are all left untouched — verified against branch history, so goevmlab output is byte-for-byte unchanged.

Adds 9 VM-level tests in test/tests/levm/trace_call_tests.rs covering post-refund gas, revert-reason decode, geth error mapping, omit-empty shape, and block-absolute log index.

@github-actions github-actions Bot added the L1 Ethereum client label Jun 29, 2026
@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown

Lines of code report

Total lines added: 684
Total lines removed: 0
Total lines changed: 684

Detailed view
+-------------------------------------------+-------+------+
| File                                      | Lines | Diff |
+-------------------------------------------+-------+------+
| ethrex/crates/blockchain/tracing.rs       | 293   | +51  |
+-------------------------------------------+-------+------+
| ethrex/crates/common/tracing.rs           | 504   | +9   |
+-------------------------------------------+-------+------+
| ethrex/crates/networking/rpc/rpc.rs       | 1406  | +4   |
+-------------------------------------------+-------+------+
| ethrex/crates/networking/rpc/tracing.rs   | 523   | +208 |
+-------------------------------------------+-------+------+
| ethrex/crates/vm/backends/levm/mod.rs     | 2844  | +2   |
+-------------------------------------------+-------+------+
| ethrex/crates/vm/backends/levm/tracing.rs | 562   | +233 |
+-------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/environment.rs  | 99    | +1   |
+-------------------------------------------+-------+------+
| ethrex/crates/vm/levm/src/tracing.rs      | 246   | +85  |
+-------------------------------------------+-------+------+
| ethrex/crates/vm/tracing.rs               | 181   | +91  |
+-------------------------------------------+-------+------+

@edg-l edg-l marked this pull request as ready for review June 29, 2026 11:50
@edg-l edg-l requested a review from a team as a code owner June 29, 2026 11:50
@edg-l edg-l moved this to In Review in ethrex_l1 Jun 29, 2026
@github-actions

Copy link
Copy Markdown

🤖 Kimi Code Review

Review of PR #6931 (adding debug_traceCall support):

Issues Found

1. Integer truncation risk (crates/networking/rpc/tracing.rs:433)

let tx_index = self.trace_config.tx_index.map(|i| i as usize);

Converting u64 to usize truncates on 32-bit systems. While Ethereum won't realistically have >4B transactions in a block, use try_into() for correctness:

let tx_index = match self.trace_config.tx_index {
    Some(i) => Some(i.try_into().map_err(|_| RpcErr::BadParams("tx_index out of range".to_owned()))?),
    None => None,
};

2. Magic number for disabled gas limit (crates/vm/backends/levm/tracing.rs:277)

env.block_gas_limit = i64::MAX as u64; // disable block gas limit

Prefer u64::MAX to avoid the cast and make intent clearer:

env.block_gas_limit = u64::MAX;

Security & Correctness

  • State isolation: Correctly rebuilds parent state and reruns block transactions before tracing, preventing state pollution between calls.
  • Timeout handling: Properly wraps execution in timeout_trace_operation to prevent DoS via long-running trace calls.
  • Transaction validation: The RPC layer correctly parses GenericTransaction without requiring a signature (using from field directly), matching eth_call semantics.
  • Error handling: Returns proper JSON-RPC errors rather than panicking on empty traces or missing blocks.

Code Quality

  • Good: Clean separation between RPC request handling (TraceCallRequest), blockchain state management (trace_call_* methods), and VM execution (LEVM::trace_call_*).
  • Good: Comprehensive tests covering callTracer, opcodeTracer, and prestateTracer with the new entry points.
  • Minor: In crates/networking/rpc/tracing.rs:417-418, "Block not Found" returns RpcErr::Internal when it should be a client error (resource not found), but this matches existing codebase patterns.

Recommendations

  1. Fix the u64 to usize conversion at line 433 in tracing.rs using try_into().
  2. Consider using u64::MAX instead of i64::MAX as u64 for disabled gas limits.
  3. Future: Add validation for tx_index against the actual block transaction count to fail fast with a clear error message.

Overall: Approved with minor fixes. The implementation correctly follows geth's debug_traceCall semantics and properly handles the unsigned transaction format.


Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt

@github-actions

Copy link
Copy Markdown

🤖 Codex Code Review

  1. High: crates/networking/rpc/tracing.rs resolves BlockIdentifierOrHash to a block number and then loads the block with get_block_by_number. That drops the original blockHash selection from EIP-1898, so a hash pointing to a non-canonical fork can end up tracing the canonical block at the same height instead of the requested block. BlockAccessListRequest handles hash-vs-number explicitly in crates/networking/rpc/eth/block_access_list.rs; debug_traceCall needs the same treatment.

  2. Medium: crates/networking/rpc/tracing.rs forwards txIndex without validating it against block.body.transactions.len(), and crates/vm/backends/levm/tracing.rs only stops on exact equality. For any txIndex >= tx_count, the code silently executes all block transactions and, because stop_index is still Some, skips withdrawals at crates/vm/backends/levm/tracing.rs. That produces a wrong trace state instead of returning BadParams.

  3. Medium: The new debug_traceCall LEVM paths construct VM::new directly in crates/vm/backends/levm/tracing.rs, crates/vm/backends/levm/tracing.rs, and crates/vm/backends/levm/tracing.rs after prepare_call_env at crates/vm/backends/levm/tracing.rs. That bypasses vm_from_generic used by simulate_tx_from_generic in crates/vm/backends/levm/mod.rs, which applies adjust_disabled_l2_fees at crates/vm/backends/levm/mod.rs. On L2, zero-priced debug_traceCall requests can therefore still hit operator/L1 fee deductions and fail even when eth_call semantics should relax them.

Aside from those points, the refactor is straightforward and the VM-level tests are useful. I’d add RPC coverage for blockHash, txIndex bounds, and an L2 zero-fee trace case.


Automated review by OpenAI Codex · gpt-5.4 · custom prompt

@greptile-apps

greptile-apps Bot commented Jun 29, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds geth-style debug_traceCall support. The main changes are:

  • New debug RPC routing and request parsing for debug_traceCall.
  • Blockchain-level trace-call methods that rebuild block state and run synthetic calls.
  • VM and LEVM trace-call entry points for call, prestate, and opcode tracers.
  • VM-level tests for sender handling and tracer output.

Confidence Score: 4/5

The new RPC path can return incorrect traces or reject compatible requests.

  • txIndex is not bounded before rebuilding block state.
  • Explicit null optional params are rejected instead of using defaults.
  • Omitted trace config returns the call-tracer shape instead of the geth default opcode trace shape.

crates/networking/rpc/tracing.rs

Important Files Changed

Filename Overview
crates/networking/rpc/tracing.rs Adds TraceCallRequest, config parsing, block resolution, tracer dispatch, and response shaping; the RPC edge cases need fixes.
crates/blockchain/tracing.rs Adds blockchain trace-call methods that rebuild parent state, rerun the target block, and invoke VM tracing.
crates/vm/backends/levm/tracing.rs Refactors shared tracer execution and adds LEVM call-style tracing through generic transaction environment setup.
crates/vm/tracing.rs Adds straightforward EVM trace-call wrappers for the LEVM backend.
crates/networking/rpc/rpc.rs Registers debug_traceCall in the debug namespace.
test/tests/levm/trace_call_tests.rs Adds VM-level tests for call tracer sender behavior, opcode tracing, and prestate diff tracing.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant RPC as TraceCallRequest
    participant BC as Blockchain
    participant EVM as Evm/LEVM

    Client->>RPC: debug_traceCall(callObject, block, traceConfig)
    RPC->>RPC: parse block, tracer config, txIndex
    RPC->>BC: "trace_call_* with GenericTransaction"
    BC->>BC: rebuild_parent_state(parent_hash, reexec)
    BC->>EVM: rerun_block(block, txIndex)
    EVM->>EVM: prepare_call_env(GenericTransaction)
    EVM->>EVM: execute synthetic call with tracer
    EVM-->>RPC: trace result
    RPC-->>Client: call frame / prestate / structLogs
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant RPC as TraceCallRequest
    participant BC as Blockchain
    participant EVM as Evm/LEVM

    Client->>RPC: debug_traceCall(callObject, block, traceConfig)
    RPC->>RPC: parse block, tracer config, txIndex
    RPC->>BC: "trace_call_* with GenericTransaction"
    BC->>BC: rebuild_parent_state(parent_hash, reexec)
    BC->>EVM: rerun_block(block, txIndex)
    EVM->>EVM: prepare_call_env(GenericTransaction)
    EVM->>EVM: execute synthetic call with tracer
    EVM-->>RPC: trace result
    RPC-->>Client: call frame / prestate / structLogs
Loading
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
crates/networking/rpc/tracing.rs:431
**Out-Of-Range Index Builds Impossible State**

When `txIndex` is greater than the selected block's transaction count, `rerun_block` never reaches the stop index, so it executes every transaction but skips withdrawals because the stop index is still `Some`. The trace then runs on a post-transaction, pre-withdrawal state that never existed on chain, so callers can receive incorrect trace output instead of a bounds error.

### Issue 2 of 3
crates/networking/rpc/tracing.rs:397-405
**Null Optional Params Are Rejected**

When a geth-compatible caller sends `debug_traceCall(callObject, null, {"tracer":"callTracer"})`, `params.get(1)` is present and `BlockIdentifierOrHash::parse` receives JSON `null` instead of defaulting to `latest`. The request returns a bad-params error, and a null third parameter fails the same way, even though these are optional RPC arguments.

### Issue 3 of 3
crates/networking/rpc/tracing.rs:404-405
**Default Tracer Returns Wrong Shape**

When `traceConfig` is omitted, `TraceCallConfig::default()` uses the shared `CallTracer` default, but geth's `debug_traceCall` default is the opcode struct logger. A caller that sends only `debug_traceCall(callObject)` receives a call-frame object instead of the expected `structLogs` response, which breaks clients that parse the geth default shape.

Reviews (1): Last reviewed commit: "feat(l1): add debug_traceCall RPC method" | Re-trigger Greptile


// `None` traces on top of the full block (geth's default); `Some(i)` runs the
// call against the state just before the block's transaction `i`.
let tx_index = self.trace_config.tx_index.map(|i| i as usize);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Out-Of-Range Index Builds Impossible State

When txIndex is greater than the selected block's transaction count, rerun_block never reaches the stop index, so it executes every transaction but skips withdrawals because the stop index is still Some. The trace then runs on a post-transaction, pre-withdrawal state that never existed on chain, so callers can receive incorrect trace output instead of a bounds error.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/networking/rpc/tracing.rs
Line: 431

Comment:
**Out-Of-Range Index Builds Impossible State**

When `txIndex` is greater than the selected block's transaction count, `rerun_block` never reaches the stop index, so it executes every transaction but skips withdrawals because the stop index is still `Some`. The trace then runs on a post-transaction, pre-withdrawal state that never existed on chain, so callers can receive incorrect trace output instead of a bounds error.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +397 to +405
let block = match params.get(1) {
Some(value) => BlockIdentifierOrHash::parse(value.clone(), 1)?,
None => BlockIdentifierOrHash::Identifier(BlockIdentifier::default()),
};

let trace_config = match params.get(2) {
Some(value) => serde_json::from_value(value.clone())?,
None => TraceCallConfig::default(),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Null Optional Params Are Rejected

When a geth-compatible caller sends debug_traceCall(callObject, null, {"tracer":"callTracer"}), params.get(1) is present and BlockIdentifierOrHash::parse receives JSON null instead of defaulting to latest. The request returns a bad-params error, and a null third parameter fails the same way, even though these are optional RPC arguments.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/networking/rpc/tracing.rs
Line: 397-405

Comment:
**Null Optional Params Are Rejected**

When a geth-compatible caller sends `debug_traceCall(callObject, null, {"tracer":"callTracer"})`, `params.get(1)` is present and `BlockIdentifierOrHash::parse` receives JSON `null` instead of defaulting to `latest`. The request returns a bad-params error, and a null third parameter fails the same way, even though these are optional RPC arguments.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread crates/networking/rpc/tracing.rs Outdated
Comment on lines +404 to +405
None => TraceCallConfig::default(),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Default Tracer Returns Wrong Shape

When traceConfig is omitted, TraceCallConfig::default() uses the shared CallTracer default, but geth's debug_traceCall default is the opcode struct logger. A caller that sends only debug_traceCall(callObject) receives a call-frame object instead of the expected structLogs response, which breaks clients that parse the geth default shape.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/networking/rpc/tracing.rs
Line: 404-405

Comment:
**Default Tracer Returns Wrong Shape**

When `traceConfig` is omitted, `TraceCallConfig::default()` uses the shared `CallTracer` default, but geth's `debug_traceCall` default is the opcode struct logger. A caller that sends only `debug_traceCall(callObject)` receives a call-frame object instead of the expected `structLogs` response, which breaks clients that parse the geth default shape.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@github-actions

Copy link
Copy Markdown

🤖 Claude Code Review

Now I have all angles covered. Let me synthesize the final review from the surviving findings.


Review: feat(l1): add debug_traceCall RPC method

This PR adds debug_traceCall to ethrex's JSON-RPC debug namespace, routing through the existing tracer infrastructure (callTracer, prestateTracer, opcodeTracer). The core refactoring — extracting shared run_call_trace/run_opcode_trace/run_prestate_trace helpers — is clean and the snapshot-timing concern (moving pre_snapshot into run_prestate_trace) is safe because both setup_env and prepare_call_env take db: &GeneralizedDatabase (immutable borrow). Three VM-level tests cover the key from-field sender behaviour. A few correctness and compatibility issues remain.


Findings

crates/networking/rpc/tracing.rs:431 — Out-of-range txIndex causes silent wrong-state trace

let tx_index = self.trace_config.tx_index.map(|i| i as usize);

This value is forwarded directly to vm.rerun_block(&block, tx_index). rerun_block's loop checks stop == index and simply exhausts all transactions when stop_index >= block.body.transactions.len() — no error is returned. A client sending { "txIndex": 9999 } against a 3-transaction block receives a trace against fully-executed-block state with a successful JSON-RPC response. The debug_traceTransaction path is properly guarded (it calls block.body.transactions.get(tx_index).ok_or(...) before proceeding); debug_traceCall should validate the same way. Consider adding a bounds check in handle() after the block is fetched:

if let Some(idx) = tx_index {
    if idx > block.body.transactions.len() {
        return Err(RpcErr::BadParams(format!("txIndex {idx} out of range")));
    }
}

crates/vm/backends/levm/tracing.rs:278GASLIMIT opcode returns i64::MAX during debug_traceCall

env.block_gas_limit = i64::MAX as u64; // disable block gas limit

prepare_call_env correctly copies the simulate_tx_from_generic sentinel to bypass the block-gas-limit validation check. However, the GASLIMIT opcode pushes env.block_gas_limit directly onto the stack. Any contract that branches on block.gaslimit (e.g. capacity estimation, require(gasleft() < block.gaslimit)) will observe 9_223_372_036_854_775_807 instead of the real limit, potentially following a different code path than actual execution. Geth makes the same trade-off (math.MaxInt64) but documents it explicitly in its source. This site has no comment at all. A note here — and ideally a named constant (e.g. DISABLED_BLOCK_GAS_LIMIT) shared with the same sentinel in mod.rs — would prevent future confusion.

There is also a forward-looking correctness risk: when EIP-8037's cost_per_state_byte dynamic formula is switched from its current stub (constant 1530) to the formula that depends on block_gas_limit, passing i64::MAX as u64 will make cpsb astronomically large, OOG-ing any SSTORE-heavy call traced via this path.


crates/networking/rpc/tracing.rs:422–427RpcErr::Internal for a missing block diverges from geth semantics

.ok_or(RpcErr::Internal("Block not Found".to_string()))?;

The comparable eth_call handler returns Ok(Value::Null) when the block is not found, matching geth's behaviour for unknown or not-yet-synced blocks. debug_traceCall returns a JSON-RPC -32603 internal error instead. Clients (e.g. Blockscout, indexers) that distinguish "block not found" (null result / specific code) from a genuine server fault will misclassify this. debug_traceTransaction and debug_traceBlockByNumber have the same pattern — but since this is a new method, aligning it with geth here costs nothing. Either return Ok(Value::Null) or add a dedicated RpcErr::BlockNotFound variant.


crates/networking/rpc/tracing.rs:497–500 — Hardcoded refund: false suppresses EVM refund counters, diverging from geth

let emit = StructLoggerEmit {
    mem_size: cfg.enable_memory,
    return_data: cfg.enable_return_data,
    refund: false,
};

Geth emits refundCounter in struct-log entries whenever the accumulated refund is non-zero, regardless of any config flag. Hardcoding refund: false unconditionally suppresses this field even for SSTORE-heavy contracts, silently diverging from geth's output for the same trace. The library-level OpcodeTracerConfig already supports refund: true (demonstrated in test/tests/levm/opcode_tracer_tests.rs). The RPC layer simply doesn't wire it through. The same issue exists in TraceTransactionRequest and TraceBlockByNumberRequest, but since this new handler is the one under review, it is the right place to fix it (or explicitly document the divergence in a comment).


crates/networking/rpc/tracing.rs:184, 304, 459 — Dead-code guard masks a missing invariant assertion

let top_frame = call_trace
    .into_iter()
    .next()
    .ok_or(RpcErr::Internal("Empty call trace".to_string()))?;

run_call_trace always returns Ok(vec![frame]) — it never returns an empty Vec (failures surface as Err). The .ok_or branch is unreachable today. If a future refactor changes this invariant (e.g. returns Ok(vec![]) for a reverted CREATE with no code deployed), the caller receives a generic -32603 with no diagnostic information. Replace with a debug_assert!(!call_trace.is_empty()) plus an unwrap_or_else(|| unreachable!()), or add a comment explaining the invariant.


crates/networking/rpc/tracing.rs:269, 274, 422, 427 — Inconsistent error message capitalisation

All four new "Block not Found" strings (capital F) differ from the established codebase convention of "Block not found" (lowercase) used in eth/block.rs:211, debug/execution_witness_by_hash.rs:40, and others. Log-scraping, integration-test assertions, and monitoring rules that match the canonical lowercase form will silently miss these occurrences.


crates/blockchain/tracing.rs:228–293 — Three trace_call_* methods share an identical 4-line setup skeleton

trace_call_calls, trace_call_prestate, and trace_call_opcodes each execute the same sequence: rebuild_parent_statererun_blocklet header = block.headertimeout_trace_operation. Only the inner dispatch call differs. If the state-preparation order ever needs to change (e.g. a new preparation step for a future fork), all three must be updated in lockstep. A small helper — async fn prepare_trace_call_state(&self, block: Block, tx_index: Option<usize>, reexec: u32) -> Result<(Evm, BlockHeader), ChainError> — would collapse the shared setup into one auditable place.


Automated review by Claude (Anthropic) · sonnet · custom prompt

edg-l added 4 commits June 29, 2026 13:56
Read the block's committed post-state directly (like eth_call) when no
txIndex is given, instead of rebuilding parent state and re-running every
transaction in the block. Falls back to the rebuild+rerun path for a
specific txIndex or when the block's state isn't stored.
Mirror debug_traceBlockByNumber for a block hash. Extract the shared
tracer dispatch into a trace_block helper used by both handlers.
Replace the per-transaction spawn_blocking + Arc<Mutex> loop with a single
blocking pass over the block, and compute the block-invariant EVM config once
instead of per transaction. Removes N-1 thread handoffs and N config recomputes
per block trace. The trace timeout now bounds the whole block trace rather than
each transaction.
@edg-l edg-l changed the title feat(l1): add debug_traceCall RPC method feat(l1): add debug_traceCall and debug_traceBlockByHash Jun 29, 2026
- callTracer top-frame gasUsed now reports post-refund gas (matches receipt)
- debug_traceCall fills nonce from account state when omitted
- decode Error(string) revertReason; map revert error to "execution reverted"
- omit empty fields (to/value/output/error/revertReason/calls) like geth
- drop `to` on failed CREATE; omit value for STATICCALL
- propagate parent-failure when clearing reverted logs
- emit block-absolute log index for withLog traces
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

Benchmark Results Comparison

No significant difference was registered for any benchmark run.

Detailed Results

Benchmark Results: BubbleSort

Command Mean [s] Min [s] Max [s] Relative
main_revm_BubbleSort 2.797 ± 0.019 2.781 2.848 1.11 ± 0.01
main_levm_BubbleSort 2.536 ± 0.016 2.522 2.566 1.00 ± 0.01
pr_revm_BubbleSort 2.794 ± 0.009 2.786 2.809 1.11 ± 0.00
pr_levm_BubbleSort 2.529 ± 0.007 2.518 2.540 1.00

Benchmark Results: ERC20Approval

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Approval 911.7 ± 3.0 908.5 916.7 1.00
main_levm_ERC20Approval 959.5 ± 27.9 942.1 1018.3 1.05 ± 0.03
pr_revm_ERC20Approval 915.1 ± 5.9 910.1 926.9 1.00 ± 0.01
pr_levm_ERC20Approval 949.2 ± 4.0 943.9 957.5 1.04 ± 0.01

Benchmark Results: ERC20Mint

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Mint 120.6 ± 0.5 120.1 121.8 1.00
main_levm_ERC20Mint 145.5 ± 1.5 144.2 148.2 1.21 ± 0.01
pr_revm_ERC20Mint 121.0 ± 0.6 120.2 122.2 1.00 ± 0.01
pr_levm_ERC20Mint 144.5 ± 1.1 143.8 147.5 1.20 ± 0.01

Benchmark Results: ERC20Transfer

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Transfer 217.5 ± 1.4 216.3 221.2 1.00 ± 0.01
main_levm_ERC20Transfer 241.8 ± 4.8 239.1 255.0 1.11 ± 0.02
pr_revm_ERC20Transfer 217.3 ± 1.2 216.1 219.7 1.00
pr_levm_ERC20Transfer 241.6 ± 2.7 239.5 246.7 1.11 ± 0.01

Benchmark Results: Factorial

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Factorial 184.8 ± 3.3 183.0 194.0 1.00 ± 0.02
main_levm_Factorial 214.4 ± 14.0 207.1 252.3 1.16 ± 0.08
pr_revm_Factorial 184.2 ± 0.8 182.5 185.3 1.00
pr_levm_Factorial 212.1 ± 5.2 207.5 224.1 1.15 ± 0.03

Benchmark Results: FactorialRecursive

Command Mean [s] Min [s] Max [s] Relative
main_revm_FactorialRecursive 1.313 ± 0.022 1.280 1.338 1.00
main_levm_FactorialRecursive 7.157 ± 0.035 7.099 7.217 5.45 ± 0.10
pr_revm_FactorialRecursive 1.319 ± 0.025 1.279 1.351 1.00 ± 0.03
pr_levm_FactorialRecursive 7.174 ± 0.070 7.091 7.274 5.46 ± 0.11

Benchmark Results: Fibonacci

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Fibonacci 153.3 ± 5.1 151.3 167.9 1.00
main_levm_Fibonacci 184.9 ± 3.6 180.1 190.4 1.21 ± 0.05
pr_revm_Fibonacci 153.7 ± 2.8 151.1 160.4 1.00 ± 0.04
pr_levm_Fibonacci 182.3 ± 4.5 178.8 193.3 1.19 ± 0.05

Benchmark Results: FibonacciRecursive

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_FibonacciRecursive 702.4 ± 6.1 689.6 712.0 1.17 ± 0.02
main_levm_FibonacciRecursive 605.0 ± 6.6 596.3 614.5 1.00 ± 0.01
pr_revm_FibonacciRecursive 698.2 ± 5.3 692.5 707.1 1.16 ± 0.01
pr_levm_FibonacciRecursive 602.0 ± 6.1 594.4 613.3 1.00

Benchmark Results: ManyHashes

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ManyHashes 7.9 ± 0.4 7.7 9.1 1.01 ± 0.06
main_levm_ManyHashes 8.5 ± 0.1 8.4 8.6 1.10 ± 0.01
pr_revm_ManyHashes 7.8 ± 0.1 7.6 8.0 1.00
pr_levm_ManyHashes 8.5 ± 0.0 8.4 8.6 1.09 ± 0.01

Benchmark Results: MstoreBench

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_MstoreBench 258.7 ± 0.9 257.7 260.1 1.49 ± 0.02
main_levm_MstoreBench 173.7 ± 2.8 171.3 180.5 1.00
pr_revm_MstoreBench 259.4 ± 1.7 257.7 263.1 1.49 ± 0.03
pr_levm_MstoreBench 173.9 ± 2.4 171.1 178.6 1.00 ± 0.02

Benchmark Results: Push

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Push 242.0 ± 1.6 240.3 245.1 1.25 ± 0.01
main_levm_Push 193.8 ± 1.9 191.9 198.3 1.00
pr_revm_Push 240.9 ± 1.0 239.6 242.7 1.24 ± 0.01
pr_levm_Push 196.5 ± 7.6 192.7 217.9 1.01 ± 0.04

Benchmark Results: SstoreBench_no_opt

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_SstoreBench_no_opt 143.6 ± 3.6 141.8 153.8 1.58 ± 0.04
main_levm_SstoreBench_no_opt 92.3 ± 2.3 90.5 97.9 1.02 ± 0.02
pr_revm_SstoreBench_no_opt 142.1 ± 0.2 141.8 142.5 1.56 ± 0.00
pr_levm_SstoreBench_no_opt 91.0 ± 0.2 90.7 91.4 1.00

None => {
let nonce = context
.storage
.get_nonce_by_account_address(block_number, self.transaction.from)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering the post-state will lead the transaction to fail due to a nonce mismatch when tx_index is set (ie it isn't added on top of the block).

This isn't a problem in geth because they skip the nonce check.

@github-project-automation github-project-automation Bot moved this from In Review to In Progress in ethrex_l1 Jul 2, 2026
#[serde(flatten)]
base: TraceConfig,
#[serde(default)]
tx_index: Option<u64>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc above says stateOverrides/blockOverrides "are not supported", but with #[serde(flatten)] on base and no deny_unknown_fields on TraceCallConfig, a client that sends either field gets its trace run without the overrides — silently, with no error. Geth's TraceCallConfig includes both, so this is a fairly high-probability compat mismatch (a client relying on geth semantics gets a trace against unexpected state). Two loud-fail options: #[serde(deny_unknown_fields)] on the whole struct, or explicit state_overrides: Option<Value> / block_overrides: Option<Value> fields that reject with BadParams("stateOverrides not supported") when Some. The explicit form has the advantage of surviving geth adding other new fields in the future without breaking us.

Comment thread crates/vm/backends/levm/tracing.rs Outdated
edg-l added 4 commits July 6, 2026 15:35
- skip nonce check in debug_traceCall (geth parity): a call traced on top
  of a mid-block state (txIndex) no longer fails on a nonce mismatch
- treat JSON null block/traceConfig params as omitted (accept
  traceCall(call, null, {...}))
- reject stateOverrides/blockOverrides explicitly instead of silently
  ignoring them
- split two glued docstrings in levm tracing
- VM-level: trace_call skips the sender nonce check (mismatched nonce)
- RPC parse: null block/traceConfig params accepted, txIndex parses,
  stateOverrides/blockOverrides rejected
- accept geth-style hex-string txIndex (hexutil.Uint) in addition to a
  JSON number
- expose the rpc tracing module so its handlers can be unit-tested
…-alt

# Conflicts:
#	crates/networking/rpc/lib.rs
#	crates/networking/rpc/rpc.rs
@edg-l edg-l requested a review from iovoid July 7, 2026 08:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

L1 Ethereum client

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants