Skip to content

feat(l1): implement noopTracer for debug trace endpoints#6694

Closed
azteca1998 wants to merge 7 commits into
mainfrom
feat/noop-tracer
Closed

feat(l1): implement noopTracer for debug trace endpoints#6694
azteca1998 wants to merge 7 commits into
mainfrom
feat/noop-tracer

Conversation

@azteca1998
Copy link
Copy Markdown
Contributor

@azteca1998 azteca1998 commented May 21, 2026

Summary

  • Adds noopTracer variant to the tracer type enum
  • Returns {} for debug_traceTransaction, and {txHash, result: {}} per tx for debug_traceBlockByNumber
  • Useful for benchmarking raw execution overhead without tracing cost
  • Matches geth's noopTracer

Closes part of #6572

Test plan

  • debug_traceTransaction with {"tracer": "noopTracer"} returns {}
  • debug_traceBlockByNumber with {"tracer": "noopTracer"} returns array of {txHash, result: {}}

Closes #6645

@azteca1998 azteca1998 requested a review from a team as a code owner May 21, 2026 10:45
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 21, 2026

⚠️ Known Issues — intentionally skipped tests

Source: docs/known_issues.md

Known Issues

Tests intentionally excluded from CI. Source of truth for the Known
Issues
section the L1 workflow appends to each ef-tests job summary
and posts as a sticky PR comment.

EF Tests — Stateless coverage narrowed to EIP-8025 optional-proofs

make -C tooling/ef_tests/blockchain test calls test-stateless-zkevm
instead of test-stateless. The zkevm@v0.3.3 fixtures are filled against
bal@v5.6.1, out of sync with current bal spec; the broad target trips ~549
fixtures. Re-broaden once the zkevm bundle is regenerated.

Why and resolution path

PR #6527 broadened
test-stateless to extract the entire for_amsterdam/ tree from the
zkevm bundle and run all of it under --features stateless; combined with
this branch's bal-devnet-7 semantics that scope produces ~549
GasUsedMismatch / ReceiptsRootMismatch /
BlockAccessListHashMismatch failures.

test-stateless-zkevm filters cargo to the eip8025_optional_proofs
suite, which still validates the stateless harness without the bal-version
mismatch.

Re-broaden by switching test: back to test-stateless in
tooling/ef_tests/blockchain/Makefile once the zkevm bundle is regenerated
against the current bal spec.

@github-actions github-actions Bot added the L1 Ethereum client label May 21, 2026
@github-actions
Copy link
Copy Markdown

🤖 Kimi Code Review

The PR adds a NoopTracer variant for benchmarking execution overhead without tracing costs. The implementation is correct and follows existing patterns, with minor optimization opportunities.

crates/networking/rpc/tracing.rs

  1. Iterator efficiency (minor) - Lines 366-377: The block tracing implementation allocates an intermediate Vec<H256> unnecessarily. You can fuse the operations to avoid the temporary allocation:

    let block_trace: BlockTrace<Value> = block
        .body
        .transactions
        .iter()
        .map(|tx| (tx.hash(), serde_json::json!({})).into())
        .collect();
  2. Documentation consistency - Line 62-65: The comment correctly documents geth compatibility. Consider adding a note that this tracer ignores all tracerConfig options (if that's the case), similar to how other tracers might document their configuration behavior.

  3. Type safety - Line 372: The .into() conversion assumes the target type implements From<(H256, Value)>. Verify this matches the BlockTrace definition used by other tracers (appears consistent with the pattern).

Security & Correctness

  • No vulnerabilities detected. The tracer returns constant empty JSON objects with no side effects on EVM execution or state.
  • Gas accounting and consensus logic are unaffected (this is RPC-layer only).

Verdict
Approach is sound. Consider applying the iterator fusion suggestion in Point 1 for slightly better memory efficiency with large blocks, though this is non-blocking given the tracer's benchmarking purpose.


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

@azteca1998 azteca1998 marked this pull request as draft May 21, 2026 10:46
@ethrex-project-sync ethrex-project-sync Bot moved this to In Progress in ethrex_l1 May 21, 2026
@github-actions
Copy link
Copy Markdown

🤖 Codex Code Review

  1. noopTracer currently changes the semantics of both RPCs by skipping replay entirely instead of replaying with tracing disabled. In crates/networking/rpc/tracing.rs:216 and crates/networking/rpc/tracing.rs:363, the new branch returns {} / [{txHash,result:{}}] without calling into blockchain at all. That means:

    • debug_traceTransaction now succeeds for any existing-looking request shape even if the tx is missing, the parent state cannot be rebuilt within reexec, sender recovery fails, or execution itself would error.
    • debug_traceBlockByNumber ignores replay/execution failures and timeout, so it reports success for blocks that the normal tracers cannot actually replay.
    • The stated benchmarking use case is not met, because this measures “skip all execution” overhead, not “execute without trace collection”.

    Suggested fix: keep the RPC output empty, but add a real no-op replay path in blockchain/vm that still rebuilds state and executes the tx/block, then discard the collected result.

I didn’t find other issues in the small diff, but this one is significant enough that I would not merge as-is. There’s also no coverage for the new tracer path, so once the replay semantics are fixed, add RPC tests for success and failure cases.


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

@github-actions
Copy link
Copy Markdown

🤖 Claude Code Review

Here is the review:


PR Review: feat(l1): implement noopTracer for debug trace endpoints

Summary

Clean, focused implementation matching geth's noopTracer semantics. No security issues, no correctness bugs, and error handling is consistent with the existing tracer branches. Two minor points below.


Findings

1. Unnecessary intermediate Vec<H256> allocation (lines 366–375)

The tx_hashes intermediate collection is created only to be immediately consumed. The two iterator chains can be collapsed into one:

// current
let tx_hashes: Vec<H256> = block
    .body
    .transactions
    .iter()
    .map(|tx| tx.hash())
    .collect();
let block_trace: BlockTrace<Value> = tx_hashes
    .into_iter()
    .map(|hash| (hash, serde_json::json!({})).into())
    .collect();

// simpler
let block_trace: BlockTrace<Value> = block
    .body
    .transactions
    .iter()
    .map(|tx| (tx.hash(), serde_json::json!({})).into())
    .collect();

Since this tracer is specifically described as useful for benchmarking raw execution overhead, eliminating the extra heap allocation is a small but meaningful consistency win.


2. Dead reexec / timeout computation for NoopTracer (lines 257–258)

let reexec = self.trace_config.reexec.unwrap_or(DEFAULT_REEXEC);
let timeout = self.trace_config.timeout.unwrap_or(DEFAULT_TIMEOUT);

These are computed unconditionally but are never read in the NoopTracer arm. This is harmless today (both are just integer unwraps), but it's a slight inconsistency since NoopTracer skips execution entirely. A broader refactor could move these declarations inside the arms that actually use them — though that's a pre-existing pattern affecting all non-noop branches, so it's out of scope here.


What's correct

  • #[serde(rename_all = "camelCase")] correctly maps NoopTracer"noopTracer" on the wire.
  • Returning serde_json::json!({}) for debug_traceTransaction matches geth's noopTracer output.
  • Wrapping results in BlockTrace<Value> with (hash, {}).into() for debug_traceBlockByNumber correctly produces the [{txHash, result: {}}] shape geth returns.
  • No execution or state access is performed for NoopTracer — the block is fetched only to enumerate transaction hashes, which is the minimum required for the response shape.
  • The #[allow(clippy::enum_variant_names)] suppression correctly covers the new variant without requiring a separate allow at the variant level.

Overall: Approve pending the intermediate allocation cleanup in point 1. Point 2 is a nit, not a blocker.


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

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 21, 2026

Lines of code report

Total lines added: 124
Total lines removed: 0
Total lines changed: 124

Detailed view
+-----------------------------------------+-------+------+
| File                                    | Lines | Diff |
+-----------------------------------------+-------+------+
| ethrex/crates/blockchain/tracing.rs     | 279   | +38  |
+-----------------------------------------+-------+------+
| ethrex/crates/networking/rpc/tracing.rs | 401   | +86  |
+-----------------------------------------+-------+------+

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 21, 2026

Greptile Summary

This PR adds a noopTracer variant to the debug trace infrastructure, correctly implementing it as a full EVM re-execution with no tracer attached — matching geth's intent as a raw execution benchmark. The RPC layer now calls dedicated blockchain methods (noop_trace_transaction, noop_trace_block) that validate transaction/block existence and replay through the EVM before returning empty JSON objects.

  • crates/blockchain/tracing.rs gains noop_trace_transaction and noop_trace_block, both using rebuild_parent_state + rerun_block (with and without a stop index) to execute through the EVM without any tracer overhead.
  • crates/networking/rpc/tracing.rs adds the NoopTracer match arm for both TraceTransactionRequest and TraceBlockByNumberRequest, delegating to the new blockchain methods and returning {} per transaction.
  • test/tests/rpc/debug_trace_tests.rs introduces integration tests covering the happy path, unknown-hash error case, and block-level tracing for noopTracer.

Confidence Score: 5/5

Safe to merge. Both new code paths correctly validate inputs, delegate to the EVM for execution, and return the expected empty-object responses.

Transaction existence is validated before returning a result, the EVM is executed to match geth's benchmarking intent, and integration tests confirm both the happy path and the error path for an unknown transaction hash.

No files require special attention.

Important Files Changed

Filename Overview
crates/blockchain/tracing.rs Adds noop_trace_transaction and noop_trace_block; both correctly validate existence, rebuild parent state, and rerun via the EVM with no tracer attached.
crates/networking/rpc/tracing.rs Adds NoopTracer enum variant and match arms for both trace endpoints; delegates to blockchain layer, returns {} per tx. Unit tests for parsing and deserialization added inline.
test/tests/rpc/debug_trace_tests.rs New integration tests cover noopTracer happy path for both transaction and block tracing, plus the unknown-hash error case.
test/tests/rpc/mod.rs Trivial module registration of the new debug_trace_tests file.

Sequence Diagram

sequenceDiagram
    participant Client
    participant RPC as RPC Handler
    participant BC as Blockchain
    participant EVM as EVM (rerun_block)
    participant Storage

    Client->>RPC: debug_traceTransaction
    RPC->>BC: noop_trace_transaction(txHash, reexec, timeout)
    BC->>Storage: get_transaction_location(txHash)
    Storage-->>BC: location or None
    alt tx not found
        BC-->>RPC: Err(Transaction not Found)
        RPC-->>Client: RpcErr
    end
    BC->>BC: rebuild_parent_state
    BC->>EVM: rerun_block(block, Some(tx_index+1))
    EVM-->>BC: Ok(())
    BC-->>RPC: Ok(())
    RPC-->>Client: "{}"

    Client->>RPC: debug_traceBlockByNumber
    RPC->>BC: noop_trace_block(block, reexec, timeout)
    BC->>BC: rebuild_parent_state
    BC->>EVM: rerun_block(block, None)
    EVM-->>BC: Ok(())
    BC-->>RPC: Ok([hashes])
    RPC-->>Client: "[{txHash, result:{}}]"
Loading

Reviews (2): Last reviewed commit: "fix(rpc): noopTracer replays through EVM..." | Re-trigger Greptile

Comment thread crates/networking/rpc/tracing.rs Outdated
Comment thread crates/networking/rpc/tracing.rs
@azteca1998 azteca1998 marked this pull request as ready for review May 26, 2026 13:33
@ethrex-project-sync ethrex-project-sync Bot moved this from In Progress to In Review in ethrex_l1 May 26, 2026
@github-actions
Copy link
Copy Markdown

🤖 Kimi Code Review

The PR introduces noopTracer support for debug_traceTransaction and debug_traceBlockByNumber, matching geth's behavior for benchmarking execution overhead without trace collection. The implementation is correct and follows the existing patterns well.

Minor Issues:

  1. Inconsistent error message capitalization (crates/blockchain/tracing.rs:232, 237)

    • "Transaction not Found" and "Block not Found" use title case inconsistently with typical Rust error messages. Consider "Transaction not found" and "Block not found", or better yet, use structured error variants instead of ChainError::Custom strings.
  2. Potential crate layering violation in tests (test/tests/rpc/debug_trace_tests.rs:10)

    • The test imports ethrex_l2_rpc::signer for transaction signing in what appears to be an L1 RPC test suite. If LocalSigner is only available in the L2 crate, consider moving the test to an L2-specific test directory or exposing signing utilities from a more appropriate crate (e.g., ethrex_blockchain or ethrex_common).
  3. Unnecessary Arc clone (crates/blockchain/tracing.rs:268-269)

    let block = Arc::new(block);
    let block_for_exec = block.clone();

    The clone is redundant; you can pass block.as_ref() directly to rerun_block since the closure takes ownership of the Arc. However, this is harmless.

  4. Missing trailing newline (crates/networking/rpc/tracing.rs)

    • The file ends without a trailing newline after the test module closing brace.

Correctness Verification:

  • The stop = tx_index.saturating_add(1) logic correctly includes the target transaction when calling rerun_block(&block, Some(stop)), assuming rerun_block treats the bound as exclusive (standard Rust range convention).
  • Proper use of timeout_trace_operation prevents DoS via long-running trace operations.
  • The reexec parameter propagation to rebuild_parent_state correctly limits historical block re-execution depth.

Testing:

The integration tests comprehensively cover:

  • Successful noop tracing of transactions and blocks
  • Error handling for unknown transaction hashes
  • Correct JSON response format ({} for single tx, array of {"txHash", "result": {}} for blocks)

Recommendation: Address the error message capitalization and consider the crate layering concern, otherwise LGTM.


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

@github-actions
Copy link
Copy Markdown

🤖 Codex Code Review

  1. noop_trace_block replays more than just transactions. In crates/blockchain/tracing.rs, it calls vm.rerun_block(..., None), and rerun_block(None) explicitly processes withdrawals after the tx loop (crates/vm/backends/levm/tracing.rs). That diverges from the method’s own docstring and from the other debug_traceBlockByNumber tracers, which only model per-tx execution state. Practical impact: noopTracer can fail on withdrawal handling that the other tracers never touch, and it benchmarks extra work instead of “raw tx execution only”. This should stop at Some(block.body.transactions.len()) rather than None.

No other correctness or security issues stood out in the diff. The transaction path looks consistent with the existing prestate/call/opcode replay model, and the RPC shape matches the intended {} output.

I couldn’t run the tests here because cargo/rustup failed on a read-only temp path in this environment.


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

@github-actions
Copy link
Copy Markdown

🤖 Claude Code Review

PR Review: feat(l1): implement noopTracer for debug trace endpoints

Overview

This PR adds a noopTracer variant matching geth's behavior: re-executes transactions through the EVM with no tracer attached, then returns empty JSON objects. It touches 4 files — two production files and two test files — with zero deletions.

The implementation is correct and the feature is well-scoped.


Correctness

noop_trace_block unnecessary Arc (crates/blockchain/tracing.rs, lines 258–275)

The Arc is introduced solely to share block between the closure and the outer Ok(...) return. There's a simpler pattern: collect the hashes before moving the block into the closure:

pub async fn noop_trace_block(
    &self,
    block: Block,
    reexec: u32,
    timeout: Duration,
) -> Result<Vec<H256>, ChainError> {
    let tx_hashes: Vec<H256> = block.body.transactions.iter().map(|tx| tx.hash()).collect();
    let mut vm = self.rebuild_parent_state(block.header.parent_hash, reexec).await?;
    timeout_trace_operation(timeout, move || vm.rerun_block(&block, None)).await?;
    Ok(tx_hashes)
}

This avoids the heap allocation and the distracting block_for_exec alias.


Error Message Consistency

noop_trace_transaction (tracing.rs:234,238) uses:

  • "Transaction not Found" — capital "F"
  • "Block not Found" — capital "F"

Check the existing ChainError messages in the codebase for casing convention (likely lowercase "not found"). Inconsistent error strings make string-matching tests fragile, as seen in the integration test:

assert!(msg.contains("Transaction not Found"), ...);

If the message ever gets normalized to lowercase, the test silently breaks.


Test Quality

parse_trace_tx_too_many_params (rpc/tracing.rs, test module)

let params = Some(vec![json!("0x01"), json!({}), json!("extra")]);
assert!(TraceTransactionRequest::parse(&params).is_err());

"0x01" is not a valid 32-byte hash. If the parser validates hash length before checking param count, the error reason is wrong — the test passes for the wrong reason. Use a full 66-char hex string to make the "too many params" path unambiguous.


Positive Notes

  • saturating_add(1) for the stop index is correct (avoids theoretical usize overflow).
  • timeout_trace_operation is correctly applied to both new paths.
  • The ? after timeout_trace_operation in noop_trace_block ensures hashes are only returned on success — no silent partial results.
  • The TracerType::NoopTracer variant correctly relies on the existing #[serde(rename_all = "camelCase")] on the enum, so "noopTracer" round-trips without an explicit rename attribute.
  • Integration tests cover the happy path, the unknown-hash error case, and the block-level response shape. The separation of rpc_call (panics on error) and direct map_http_requests (for error-path tests) is the right pattern.
  • Comments in the RPC handlers explaining the geth alignment and the "why execute at all" rationale are valuable.

Summary

Two actionable items: simplify noop_trace_block to avoid the unnecessary Arc, and fix error message capitalization (and the corresponding test assertion). Everything else is sound.


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

azteca1998 added a commit that referenced this pull request May 26, 2026
The handler iterated the entire state trie synchronously inside an async
RPC method via `iter_accounts`, which opens four long-lived locked DB
transactions (see `BackendTrieDBLocked`) and walks the trie node by
node. On any non-trivial state this stalls the tokio worker and pins DB
locks for the duration. Snap-sync already wraps the same call in
`spawn_blocking` for exactly this reason.

Move the traversal onto a blocking thread and add geth-style pagination:
`debug_dumpBlock` now accepts an optional `{start, maxResults}` config
object and emits a `next` cursor when the result is truncated. The
default `maxResults` is 100_000 — a divergence from geth's unbounded
default, documented on the type, that protects the RPC server from
mainnet-scale dumps. Also documents that `code`/`storage` are not
emitted and that `iter_accounts` silently truncates on decode errors.

Test setup helpers move out of the single test file into a shared
`rpc::helpers` module so #6694 and any future endpoint can reuse them
instead of duplicating ~140 lines per feature. The misnamed
`debug_trace_tests.rs` becomes `debug_dump_block_tests.rs` and gains
coverage for: block-not-found, `latest` tag resolution, sender appears
in the dump with expected nonce/balance, and end-to-end pagination
across two pages via `start`/`maxResults`/`next`.
azteca1998 added a commit that referenced this pull request May 26, 2026
The previous implementation was structurally wrong for two of the three
flat-frame variants:

- `create` actions emitted `input` and a result of `{gasUsed, output}`.
  geth's flatCallTracer (and Parity's spec) require `init` in the action
  and `{address, code, gasUsed}` in the result so callers can recover the
  deployed contract's address and runtime bytecode.
- `suicide` (SELFDESTRUCT) frames emitted the call-shape action and a
  bogus `{gasUsed, output}` result. The Parity shape is
  `{address, balance, refundAddress}` with no result at all.

Split `FlatCallAction` / `FlatCallResult` into per-variant enums (untagged
so the outer `type` field continues to discriminate), map the LEVM call
tracer's SELFDESTRUCT frame fields onto the suicide shape (`from` →
destructed contract, `to` → refund target, `value` → balance), and emit
`init`/`address`/`code` for create frames. Use `&'static str` for the
static label fields to avoid per-frame allocations.

Unit tests now assert against the serialized JSON projection — the wire
shape clients actually receive — and cover create/create2, selfdestruct,
and failed-frame `result` omission. Integration tests gain a CREATE
end-to-end (deploys a tiny init code and verifies the action/result
shape), a `debug_traceBlockByNumber` test, and a missing-tx failure case
that Codex's review flagged as a gap.

Test setup boilerplate moves out of the misnamed `debug_trace_tests.rs`
into a shared `rpc::helpers` module so #6694 and #6701 can rebase to
re-use it instead of duplicating ~140 lines per feature. The file is
renamed to `debug_flat_call_tracer_tests.rs`.
azteca1998 added a commit that referenced this pull request May 26, 2026
Three correctness bugs versus geth's built-in 4byteTracer
(eth/tracers/native/4byte.go), each of which would cause clients matching
against geth output to mismatch every entry:

- The reported size was `len(calldata)` instead of `len(calldata) - 4`.
  Geth's key shape is `"0xSELECTOR-N"` where N is the argument-bytes
  length excluding the selector itself.
- The top-level transaction call was being counted. Geth skips depth 0
  and only records nested invocations.
- Every CallType was counted, including STATICCALL, CALLCODE, CREATE,
  CREATE2, and SELFDESTRUCT. Geth filters on the opcode and only counts
  CALL and DELEGATECALL — for create variants and selfdestruct the input
  isn't a function selector, and the geth tracer doesn't surface
  STATICCALL or CALLCODE either.

Also skip precompile-targeted invocations (addresses 0x01..=0x11 plus
P256VERIFY at 0x100) — matching geth's per-fork `isPrecompiled` check
fork-agnostically, since any precompile address is a precompile once its
fork activates.

Unit tests now lock in each rule: top-frame skip, short-input skip,
call-type filter, precompile skip, arg-size key, duplicate counting.
Integration tests gain block-level coverage and a missing-tx failure
case. Variant doc clarifies the size semantics and skipped categories.

Test setup boilerplate moves out of the misnamed `debug_trace_tests.rs`
into a shared `rpc::helpers` module so #6694, #6701, #6705, #6704 can
all rebase to drop their duplicate copies; the file is renamed to
`debug_four_byte_tracer_tests.rs` to fit its contents.
Add noopTracer variant that returns an empty JSON object {} per
transaction. Useful for benchmarking execution overhead without
tracing cost. Supported in both debug_traceTransaction and
debug_traceBlockByNumber.

Part of #6572
Trace a real executed transaction with noopTracer and assert
the response is an empty JSON object.
noopTracer was returning {} without validating that the transaction
exists or re-executing it. This diverged from geth, which still runs
the EVM with no-op hooks. Now we call trace_transaction_calls /
trace_block_calls and discard the results, matching geth semantics
and properly erroring on invalid tx hashes.
Routing noopTracer through trace_transaction_calls / trace_block_calls
still constructed a LevmCallTracer that allocates a top-level callframe
and runs exit-context bookkeeping, so the PR's stated "benchmarking raw
execution overhead without tracing cost" goal was not actually met.

Add noop_trace_transaction / noop_trace_block on Blockchain that rebuild
parent state and use rerun_block with no tracer (LevmCallTracer::disabled
short-circuits every hook), and wire the RPC NoopTracer arms to call
them. Note in the NoopTracer doc that tracerConfig is ignored. Cover
missing-hash failure and block-level happy path with integration tests.
Copy link
Copy Markdown
Contributor

@ElFantasma ElFantasma left a comment

Choose a reason for hiding this comment

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

Mechanism differs from geth's noopTracer, which matters for the stated benchmarking goal. geth's noopTracer is a real tracer with empty hook implementations — the EVM runs with the tracer attached, so every opcode still dispatches into the (empty) hook. This PR instead runs rerun_block with no tracer at all, skipping hook dispatch entirely. The JSON output matches geth ({} per tx), but the measurement doesn't: geth's noopTracer number includes per-opcode hook-dispatch overhead, while ethrex's measures pure execution. For "benchmarking raw execution overhead without tracing cost" the no-hook approach is arguably the cleaner number — but anyone comparing ethrex-noop vs geth-noop side by side would be comparing different things. Worth a one-line doc clarification (inline).


Duplicate of #6657 — and one structural idea worth porting from it before this lands. There's an existing open PR for the exact same feature: #6657 (noopTracer, opened 2026-05-14 by @ElFantasma) implements the same TracerType::NoopTracer wired through the same two RPC methods. This PR is the stronger of the two — it ships real tests where #6657 ships none, and the whole-block rerun_block(None) path here is both simpler and a cleaner benchmark (no per-tx setup_env+VM::new reconstruction polluting the measurement). So the recommendation is to land this one and close #6657.

The one thing #6657 does that's worth salvaging: its single-tx path mirrors the sibling tracers (trace_tx_prestate / trace_tx_opcodes) by replaying [0, N-1] and then executing the target tx through a dedicated trace_tx_noop step, instead of folding the target into rerun_block(Some(N+1)). For a noop the observable result is identical, but matching the established replay-then-trace-target shape keeps the tracing module uniform and makes the next tracer that does collect per-tx data drop in without a special case. See the inline suggestion on noop_trace_transaction. (Non-blocking — adopt if you value the consistency; the current form is correct.)

Issue linkage: the description says "Closes part of #6572". #6572 is the umbrella debug-RPC tracker; the granular sub-issue for this exact feature is #6645 ("Implement noopTracer for debug_trace*"). Please change the body to Closes #6645 so GitHub auto-closes the specific sub-issue on merge — a bare "Closes part of #6572" closes nothing and leaves the umbrella issue to be reconciled by hand later.

Coordination: @azteca1998 — since #6657 is the duplicate and this PR supersedes it, suggest closing #6657 once this is finalized (Esteve to action). Flagging so two reviewers don't sink time into both.

Comment thread crates/networking/rpc/tracing.rs Outdated
/// `structLogger` wrapper shape (`{failed, gas, returnValue, structLogs}`).
/// Selected via `"tracer": "opcodeTracer"`.
OpcodeTracer,
/// No-op tracer that re-executes the transaction(s) through the EVM with no tracer
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.

nit: "matching geth's noopTracer" is true for the output shape but not the mechanism. geth's noopTracer attaches a tracer with empty hooks, so the EVM still dispatches into each (no-op) hook per opcode; this implementation runs with no tracer attached at all (rerun_block with no tracer), skipping hook dispatch entirely.

For the benchmarking goal this is arguably better (it isolates pure execution), but the doc could be precise so future readers don't assume the numbers are directly comparable to geth's noopTracer:

/// No-op tracer: re-executes the transaction(s) through the EVM with no tracer
/// attached and returns empty JSON objects. The output shape matches geth's
/// `noopTracer` (`{}` per tx), but note the mechanism differs — geth runs an
/// empty-hook tracer (paying per-opcode dispatch), whereas this skips tracer
/// hooks entirely, isolating raw execution cost. Any `tracerConfig` is ignored.
/// Selected via `"tracer": "noopTracer"`.

Non-blocking — just so a future benchmark comparison isn't misread.

.await?;
// Run every tx up to AND including the target — no tracer attached.
let stop = tx_index.saturating_add(1);
timeout_trace_operation(timeout, move || vm.rerun_block(&block, Some(stop))).await
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.

Optional consistency port from the duplicate #6657. Here the target tx is folded into the replay by stopping at tx_index + 1. The sibling tracers (trace_tx_prestate, trace_tx_opcodes) instead replay [0, tx_index) and then run the target tx through a dedicated step — and #6657's noop path follows that same shape (rerun_block(Some(tx_index)) + a separate trace_tx_noop(target)).

For a noop the result is identical either way, so this is purely about keeping the module uniform:

// replay preceding txs, then run the target through the same dedicated
// step the other tracers use — keeps the tracing module's shape uniform.
vm.rerun_block(&block, Some(tx_index))?;
timeout_trace_operation(timeout, move || vm.trace_tx_noop(&block, tx_index)).await

Non-blocking; adopt only if you want parity with the prestate/opcode paths. The current Some(tx_index + 1) form is correct as-is.

azteca1998 added a commit that referenced this pull request May 27, 2026
The handler iterated the entire state trie synchronously inside an async
RPC method via `iter_accounts`, which opens four long-lived locked DB
transactions (see `BackendTrieDBLocked`) and walks the trie node by
node. On any non-trivial state this stalls the tokio worker and pins DB
locks for the duration. Snap-sync already wraps the same call in
`spawn_blocking` for exactly this reason.

Move the traversal onto a blocking thread and add geth-style pagination:
`debug_dumpBlock` now accepts an optional `{start, maxResults}` config
object and emits a `next` cursor when the result is truncated. The
default `maxResults` is 100_000 — a divergence from geth's unbounded
default, documented on the type, that protects the RPC server from
mainnet-scale dumps. Also documents that `code`/`storage` are not
emitted and that `iter_accounts` silently truncates on decode errors.

Test setup helpers move out of the single test file into a shared
`rpc::helpers` module so #6694 and any future endpoint can reuse them
instead of duplicating ~140 lines per feature. The misnamed
`debug_trace_tests.rs` becomes `debug_dump_block_tests.rs` and gains
coverage for: block-not-found, `latest` tag resolution, sender appears
in the dump with expected nonce/balance, and end-to-end pagination
across two pages via `start`/`maxResults`/`next`.
azteca1998 added a commit that referenced this pull request May 27, 2026
Three correctness bugs versus geth's built-in 4byteTracer
(eth/tracers/native/4byte.go), each of which would cause clients matching
against geth output to mismatch every entry:

- The reported size was `len(calldata)` instead of `len(calldata) - 4`.
  Geth's key shape is `"0xSELECTOR-N"` where N is the argument-bytes
  length excluding the selector itself.
- The top-level transaction call was being counted. Geth skips depth 0
  and only records nested invocations.
- Every CallType was counted, including STATICCALL, CALLCODE, CREATE,
  CREATE2, and SELFDESTRUCT. Geth filters on the opcode and only counts
  CALL and DELEGATECALL — for create variants and selfdestruct the input
  isn't a function selector, and the geth tracer doesn't surface
  STATICCALL or CALLCODE either.

Also skip precompile-targeted invocations (addresses 0x01..=0x11 plus
P256VERIFY at 0x100) — matching geth's per-fork `isPrecompiled` check
fork-agnostically, since any precompile address is a precompile once its
fork activates.

Unit tests now lock in each rule: top-frame skip, short-input skip,
call-type filter, precompile skip, arg-size key, duplicate counting.
Integration tests gain block-level coverage and a missing-tx failure
case. Variant doc clarifies the size semantics and skipped categories.

Test setup boilerplate moves out of the misnamed `debug_trace_tests.rs`
into a shared `rpc::helpers` module so #6694, #6701, #6705, #6704 can
all rebase to drop their duplicate copies; the file is renamed to
`debug_four_byte_tracer_tests.rs` to fit its contents.
azteca1998 added a commit that referenced this pull request May 27, 2026
The previous implementation was structurally wrong for two of the three
flat-frame variants:

- `create` actions emitted `input` and a result of `{gasUsed, output}`.
  geth's flatCallTracer (and Parity's spec) require `init` in the action
  and `{address, code, gasUsed}` in the result so callers can recover the
  deployed contract's address and runtime bytecode.
- `suicide` (SELFDESTRUCT) frames emitted the call-shape action and a
  bogus `{gasUsed, output}` result. The Parity shape is
  `{address, balance, refundAddress}` with no result at all.

Split `FlatCallAction` / `FlatCallResult` into per-variant enums (untagged
so the outer `type` field continues to discriminate), map the LEVM call
tracer's SELFDESTRUCT frame fields onto the suicide shape (`from` →
destructed contract, `to` → refund target, `value` → balance), and emit
`init`/`address`/`code` for create frames. Use `&'static str` for the
static label fields to avoid per-frame allocations.

Unit tests now assert against the serialized JSON projection — the wire
shape clients actually receive — and cover create/create2, selfdestruct,
and failed-frame `result` omission. Integration tests gain a CREATE
end-to-end (deploys a tiny init code and verifies the action/result
shape), a `debug_traceBlockByNumber` test, and a missing-tx failure case
that Codex's review flagged as a gap.

Test setup boilerplate moves out of the misnamed `debug_trace_tests.rs`
into a shared `rpc::helpers` module so #6694 and #6701 can rebase to
re-use it instead of duplicating ~140 lines per feature. The file is
renamed to `debug_flat_call_tracer_tests.rs`.
The doc comment now explicitly notes that ethrex skips tracer hooks
entirely rather than running empty-hook dispatch like geth does, so
benchmark numbers are not directly comparable.
@azteca1998
Copy link
Copy Markdown
Contributor Author

Closing in favour of #6657 which implements the same noopTracer (sub-issue #6645).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

L1 Ethereum client

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Implement noopTracer for debug_trace*

2 participants