feat(l1): implement debug_traceCall RPC endpoint#6695
Conversation
Add debug_traceCall which traces an arbitrary call (GenericTransaction) at a given block state without creating a transaction on-chain. Supports all existing tracers: callTracer, prestateTracer, opcodeTracer. Adds trace-call infrastructure at each layer: - LEVM: trace_call_calls, trace_call_prestate, trace_call_opcodes - Evm: wrapper methods for generic-tx tracing - Blockchain: async trace_call_* methods with timeout support - RPC: TraceCallRequest handler with geth-compatible param parsing Part of #6572
|
Lines of code reportTotal lines added: Detailed view |
Set up a funded account, then call debug_traceCall with a simulated transfer. Assert the callTracer response shape.
Tests as shipped exercised one path: callTracer, "latest" block, against a genesis-only state. That left the three tracer variants asymmetric on coverage and didn't validate any of the items on the PR's own test plan. Added integration tests covering: prestateTracer + opcodeTracer paths, block-by-number, block-by-hash (EIP-1898 form), default-to-latest when the block parameter is omitted, unknown-block error, and empty-params error. The post-block tests pin `nonce` in the call object because ethrex's LEVM enforces nonce equality even on simulated calls — a pre-existing constraint shared with `eth_call`, documented inline so the next person doesn't repeat the diagnosis. Documented two ergonomic gaps on `TraceCallRequest` itself: - `stateOverrides` / `blockOverrides` from the third parameter are silently ignored. Real geth-compat support requires plumbing overrides down to the VM, so this is called out as a known divergence rather than fixed in-flight. - Pruned nodes that don't have the target block's state will surface the storage layer's "state root missing" error as a generic Internal. Test setup boilerplate moves out of the misnamed `debug_trace_tests.rs` into the shared `rpc::helpers` module introduced in #6701; the file is renamed to `debug_trace_call_tests.rs`. Also polished the handler's default-block fallback (one allocation instead of an `Option::clone`) and switched the not-found error from "Block not Found" to "Block not found" to match the casing every other endpoint uses.
🤖 Kimi Code ReviewThe PR implements Code Quality & Correctness
let mut env = env_from_generic(tx, block_header, db, vm_type)?;
env.block_gas_limit = i64::MAX as u64;
adjust_disabled_base_fee(&mut env);Consider extracting this into a
let default_block = BlockIdentifierOrHash::Identifier(BlockIdentifier::default());Verify Security & ConsensusState Isolation — The implementation correctly uses Timeout Protection — All three trace methods in Gas Price Handling — The TestingThe integration tests in
Test Helper Safety — DocumentationThe doc comment on Minor Suggestions
VerdictThe code is correct, secure, and well-tested. The implementation follows Ethereum RPC specifications appropriately and includes proper safeguards against resource exhaustion. No blocking issues identified. Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt |
🤖 Codex Code Review
Assumption: Item 1 depends on the store retaining non-canonical blocks, but I couldn’t run the Rust tests here because Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
Greptile SummaryThis PR implements the
Confidence Score: 4/5Safe to merge. The new endpoint follows established patterns for all three tracers and the test coverage is solid. Two minor code-quality notes but no functional defects. The implementation correctly threads through all layers and closely mirrors the existing transaction-tracer code paths. The only notable gap is that
|
| Filename | Overview |
|---|---|
| crates/vm/backends/levm/mod.rs | Exposes env_from_generic and adjust_disabled_base_fee as pub(crate), and adds vm_from_generic_with_tracer — a near-identical copy of vm_from_generic with an extra tracer parameter. The duplication is benign today but the original is not refactored to delegate. |
| crates/vm/backends/levm/tracing.rs | Adds three trace_call_* methods on LEVM that mirror the existing trace_tx_* methods. Pattern is consistent: build env, override block_gas_limit to i64::MAX, build VM with tracer, execute, return result. Logic is sound. |
| crates/blockchain/tracing.rs | Adds three async wrapper methods that follow the exact same timeout_trace_operation pattern used by the existing transaction tracers. No issues found. |
| crates/networking/rpc/tracing.rs | New TraceCallRequest implements parse/handle following the existing TraceTransactionRequest pattern. The reexec field from TraceConfig is silently ignored without documentation. Good inline unit tests added. |
| crates/networking/rpc/rpc.rs | One-line addition routing debug_traceCall to TraceCallRequest::call. Correct placement in the debug namespace dispatcher. |
| crates/vm/tracing.rs | Adds three thin delegating methods on Evm that forward to LEVM::trace_call_*. Correctly plumbed with vm_type and crypto passthrough. |
| test/tests/rpc/debug_trace_call_tests.rs | Good coverage: default tracer, omitted block param, by-block-number, by-block-hash (EIP-1898), prestateTracer, opcodeTracer, unknown block error, and empty-params error. |
| test/tests/rpc/helpers.rs | New shared test helpers file: setup_store, setup_genesis_only, setup_single_transfer_block, rpc_call, and rpc_call_expect_err. Well-structured and reusable. |
Sequence Diagram
sequenceDiagram
participant Client
participant RPC as TraceCallRequest (rpc/tracing.rs)
participant BC as Blockchain (blockchain/tracing.rs)
participant EVM as Evm (vm/tracing.rs)
participant LEVM as LEVM (vm/backends/levm/tracing.rs)
participant VM as VM (levm)
Client->>RPC: debug_traceCall([callObj, blockId?, traceConfig?])
RPC->>RPC: parse() — GenericTransaction + BlockIdentifierOrHash + TraceConfig
RPC->>BC: resolve block header
BC-->>RPC: BlockHeader
RPC->>BC: "trace_call_calls/prestate/opcodes(&header, &tx, timeout, ...)"
BC->>BC: StoreVmDatabase::new(storage, header)
BC->>BC: new_evm(vm_db)
BC->>BC: timeout_trace_operation(timeout, closure)
BC->>EVM: "trace_call_calls/prestate/opcodes(&header, &tx, ...)"
EVM->>LEVM: trace_call_calls/prestate/opcodes(db, header, tx, ...)
LEVM->>LEVM: env_from_generic(tx, header, db)
LEVM->>LEVM: "env.block_gas_limit = i64::MAX"
LEVM->>LEVM: "adjust_disabled_base_fee(&mut env)"
LEVM->>LEVM: vm_from_generic_with_tracer(tx, env, db, tracer)
LEVM->>VM: VM::new(env, db, tx, tracer, vm_type, crypto)
VM-->>LEVM: vm
LEVM->>VM: vm.execute()
VM-->>LEVM: Ok(())
LEVM->>VM: get_trace_result / opcode_tracer.take_result
VM-->>LEVM: CallTrace / PrestateResult / OpcodeTraceResult
LEVM-->>EVM: Result
EVM-->>BC: Result
BC-->>RPC: Result
RPC->>RPC: serialize result
RPC-->>Client: JSON response
Comments Outside Diff (1)
-
crates/vm/backends/levm/mod.rs, line 2838-2881 (link)vm_from_genericis now an exact duplicate ofvm_from_generic_with_tracerexcept for the hardcodedLevmCallTracer::disabled(). If the Transaction-building logic ever diverges (e.g. a new tx type is added), one copy will silently lag behind the other. The original can simply delegate to eliminate the duplication.Prompt To Fix With AI
This is a comment left during a code review. Path: crates/vm/backends/levm/mod.rs Line: 2838-2881 Comment: `vm_from_generic` is now an exact duplicate of `vm_from_generic_with_tracer` except for the hardcoded `LevmCallTracer::disabled()`. If the Transaction-building logic ever diverges (e.g. a new tx type is added), one copy will silently lag behind the other. The original can simply delegate to eliminate the duplication. How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
crates/vm/backends/levm/mod.rs:2838-2881
`vm_from_generic` is now an exact duplicate of `vm_from_generic_with_tracer` except for the hardcoded `LevmCallTracer::disabled()`. If the Transaction-building logic ever diverges (e.g. a new tx type is added), one copy will silently lag behind the other. The original can simply delegate to eliminate the duplication.
```suggestion
fn vm_from_generic<'a>(
tx: &GenericTransaction,
env: Environment,
db: &'a mut GeneralizedDatabase,
vm_type: VMType,
crypto: &'a dyn Crypto,
) -> Result<VM<'a>, VMError> {
vm_from_generic_with_tracer(tx, env, db, vm_type, crypto, LevmCallTracer::disabled())
}
```
### Issue 2 of 2
crates/networking/rpc/tracing.rs:43-46
The `reexec` field is deserialized from the caller's `traceConfig` but is never read in `TraceCallRequest::handle`. For `debug_traceTransaction` and `debug_traceBlockByNumber`, `reexec` controls how far back to re-execute blocks to rebuild missing trie state. That concept doesn't apply here (state comes directly from the block header's state root), so ignoring it is correct — but unlike `stateOverrides`/`blockOverrides`, this divergence is not mentioned in the doc comment, which may surprise callers who pass it expecting an effect.
```suggestion
/// **Known divergence from geth**: geth's `traceConfig` accepts
/// `stateOverrides` and `blockOverrides` for hypothetical-state debugging.
/// ethrex does not yet honour either — they are silently ignored. Pass-through
/// support requires applying overrides at the VM layer before execution.
///
/// The `reexec` field in `traceConfig` is also accepted but has no effect:
/// state is read directly from the block header's state root rather than
/// being rebuilt by re-executing ancestor blocks.
```
Reviews (1): Last reviewed commit: "fix(rpc): broaden debug_traceCall test c..." | Re-trigger Greptile
| /// **Known divergence from geth**: geth's `traceConfig` accepts | ||
| /// `stateOverrides` and `blockOverrides` for hypothetical-state debugging. | ||
| /// ethrex does not yet honour either — they are silently ignored. Pass-through | ||
| /// support requires applying overrides at the VM layer before execution. |
There was a problem hiding this comment.
The
reexec field is deserialized from the caller's traceConfig but is never read in TraceCallRequest::handle. For debug_traceTransaction and debug_traceBlockByNumber, reexec controls how far back to re-execute blocks to rebuild missing trie state. That concept doesn't apply here (state comes directly from the block header's state root), so ignoring it is correct — but unlike stateOverrides/blockOverrides, this divergence is not mentioned in the doc comment, which may surprise callers who pass it expecting an effect.
| /// **Known divergence from geth**: geth's `traceConfig` accepts | |
| /// `stateOverrides` and `blockOverrides` for hypothetical-state debugging. | |
| /// ethrex does not yet honour either — they are silently ignored. Pass-through | |
| /// support requires applying overrides at the VM layer before execution. | |
| /// **Known divergence from geth**: geth's `traceConfig` accepts | |
| /// `stateOverrides` and `blockOverrides` for hypothetical-state debugging. | |
| /// ethrex does not yet honour either — they are silently ignored. Pass-through | |
| /// support requires applying overrides at the VM layer before execution. | |
| /// | |
| /// The `reexec` field in `traceConfig` is also accepted but has no effect: | |
| /// state is read directly from the block header's state root rather than | |
| /// being rebuilt by re-executing ancestor blocks. |
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/networking/rpc/tracing.rs
Line: 43-46
Comment:
The `reexec` field is deserialized from the caller's `traceConfig` but is never read in `TraceCallRequest::handle`. For `debug_traceTransaction` and `debug_traceBlockByNumber`, `reexec` controls how far back to re-execute blocks to rebuild missing trie state. That concept doesn't apply here (state comes directly from the block header's state root), so ignoring it is correct — but unlike `stateOverrides`/`blockOverrides`, this divergence is not mentioned in the doc comment, which may surprise callers who pass it expecting an effect.
```suggestion
/// **Known divergence from geth**: geth's `traceConfig` accepts
/// `stateOverrides` and `blockOverrides` for hypothetical-state debugging.
/// ethrex does not yet honour either — they are silently ignored. Pass-through
/// support requires applying overrides at the VM layer before execution.
///
/// The `reexec` field in `traceConfig` is also accepted but has no effect:
/// state is read directly from the block header's state root rather than
/// being rebuilt by re-executing ancestor blocks.
```
How can I resolve this? If you propose a fix, please make it concise.
🤖 Claude Code ReviewNow I have everything needed for a thorough review. Let me write it up. Review:
|
Summary
debug_traceCallwhich traces an arbitrary call at a given block state without creating a transaction on-chaincallTracer,prestateTracer,opcodeTracereth_callplus an optional trace configCloses part of #6572
Test plan
debug_traceCallwith callTracer returns nested call framesdebug_traceCallwith prestateTracer returns pre/post statedebug_traceCallwith opcodeTracer returns EIP-3155 step trace