Skip to content

feat(l1): implement debug_dumpBlock endpoint#6701

Open
azteca1998 wants to merge 7 commits into
mainfrom
feat/debug-dump-block
Open

feat(l1): implement debug_dumpBlock endpoint#6701
azteca1998 wants to merge 7 commits into
mainfrom
feat/debug-dump-block

Conversation

@azteca1998
Copy link
Copy Markdown
Contributor

@azteca1998 azteca1998 commented May 21, 2026

Summary

  • Implement debug_dumpBlock RPC endpoint that dumps the entire state at a given block
  • Returns the state root and all accounts with their balance, nonce, storage root, and code hash
  • Uses iter_accounts to iterate the state trie at the block's state root

Closes part of #6572

Closes #6650

@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
Copy link
Copy Markdown

github-actions Bot commented May 21, 2026

Lines of code report

Total lines added: 155
Total lines removed: 0
Total lines changed: 155

Detailed view
+--------------------------------------------------+-------+------+
| File                                             | Lines | Diff |
+--------------------------------------------------+-------+------+
| ethrex/crates/networking/rpc/debug/dump_block.rs | 152   | +152 |
+--------------------------------------------------+-------+------+
| ethrex/crates/networking/rpc/debug/mod.rs        | 5     | +1   |
+--------------------------------------------------+-------+------+
| ethrex/crates/networking/rpc/rpc.rs              | 1365  | +2   |
+--------------------------------------------------+-------+------+

@azteca1998 azteca1998 changed the title feat(rpc): implement debug_dumpBlock endpoint feat(l1): implement debug_dumpBlock endpoint May 21, 2026
@github-actions github-actions Bot added the L1 Ethereum client label May 21, 2026
@azteca1998 azteca1998 marked this pull request as ready for review May 26, 2026 14:07
@azteca1998 azteca1998 requested a review from a team as a code owner May 26, 2026 14:07
@ethrex-project-sync ethrex-project-sync Bot moved this to In Review in ethrex_l1 May 26, 2026
@github-actions
Copy link
Copy Markdown

🤖 Claude Code Review

Here is my review of PR #6701:


PR #6701feat(l1): implement debug_dumpBlock endpoint

Overview

Implements the debug_dumpBlock JSON-RPC method, which dumps all accounts at a given block's state root. The implementation is intentionally bounded (default 100 000 accounts), uses spawn_blocking for the synchronous trie walk, and supports cursor-based pagination via start / maxResults. Tests cover the golden path, error cases, and pagination.


Correctness

Wrong error variant for missing block (dump_block.rs:85–89)

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

RpcErr::Internal maps to HTTP 500 / JSON-RPC internal error. A missing block is a client error (bad block identifier), not a server fault. It should use RpcErr::InvalidParam or a RpcErr::NotFound variant (whichever exists). Returning 500 here will confuse callers and break any tooling that distinguishes client vs server errors.

Pagination off-by-one when maxResults is 0 (dump_block.rs:101–112)

for (hashed_addr, account_state) in iter {
    if accounts.len() >= max_results {
        next = Some(hashed_addr);
        break;
    }
    accounts.insert(hashed_addr, ...);
}

When max_results = 0 the guard fires immediately on the first iteration, setting next without inserting anything. The caller gets an empty accounts map with next pointing to the first key — arguably valid, but unintuitive and untested. At minimum, document this, or clamp max_results to 1 with a returned error/warning for 0.

Trie truncation is silently ambiguous (acknowledged in docs)

The doc comment notes that iter_accounts_from stops silently on a node decode failure, making a truncated response indistinguishable from a complete one. This is an existing storage-layer bug, but the endpoint surfaces it opaquely. Consider adding a header field or a truncationWarning field to the response if the iterator terminates early without reaching max_results, once the storage layer can expose that signal.


Minor / Style

Redundant #[serde(default)] on Option<_> fields (dump_block.rs:46–52)

#[serde(default)]
start: Option<H256>,
#[serde(default)]
max_results: Option<usize>,

Option<T> already deserialises to None for absent fields by default in serde. The #[serde(default)] attributes are redundant and can be removed.

Two sequential storage reads for block number then header (dump_block.rs:84–90)

let block_number = self.block.resolve_block_number(&context.storage).await?...;
let header = context.storage.get_block_header(block_number)?...;

resolve_block_number internally fetches the header for named tags anyway. If the storage API allows it, fetching the header directly (or combining the two into one call) avoids a round-trip. Not critical, but worth noting.

block: BlockIdentifier field shadowed by local block in tests (helpers.rs)

build_and_execute_block returns a Block named block and the TestEnv::block field has the same name. No actual conflict, but it makes grepping for usages slightly confusing.


Geth Compatibility

The PR documents its divergences clearly in the doc comment:

  • Hashed keys only (no address or preimage)
  • No code or storage fields
  • Default maxResults cap

All are reasonable for safety. The hashed-key-only format is a notable incompatibility for any tooling that expects plain addresses; worth calling this out explicitly in whatever API documentation (or the issue tracker item for #6572) tracks full geth compatibility.


Tests

Coverage is good: parse round-trip, error on unknown block, latest tag resolution, and pagination. Two gaps worth adding:

  • maxResults: 0 — documents the edge-case behaviour.
  • start set to a hash that does not exist in the trie — verifies that the iterator skips gracefully to the next key rather than returning nothing or panicking.

Positive Notes

  • spawn_blocking usage is correct and well-commented.
  • The double-? on spawn_blocking(...).await.map_err(...)?? is correctly structured.
  • BTreeMap for the output ensures deterministic key ordering.
  • The skip_serializing_if = "Option::is_none" on next is the right pattern.
  • Test helpers are well-factored and reusable for future RPC tests.

Summary

The implementation is solid overall. The main actionable items before merge:

  1. Change RpcErr::Internal to the appropriate client-error variant for missing blocks.
  2. Remove the redundant #[serde(default)] on Option fields.
  3. Document (or guard against) maxResults: 0.

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

@github-actions
Copy link
Copy Markdown

🤖 Kimi Code Review

The implementation is solid and well-documented. A few specific observations:

Security/DoS Concerns

  1. Unbounded max_results (dump_block.rs:82): While the default is 100k, the endpoint accepts arbitrary usize values from users. A malicious caller could request usize::MAX, causing the node to attempt loading the entire state trie into memory (potentially millions of accounts), leading to OOM. Even for debug_* methods, consider enforcing an upper bound (e.g., 500k or 1M) to prevent accidental resource exhaustion.
    // Suggested addition after line 95:
    const MAX_MAX_RESULTS: usize = 500_000;
    let max_results = max_results.min(MAX_MAX_RESULTS);

Code Quality

  1. Error conversion assumption (dump_block.rs:82): The ? operator on serde_json::from_value assumes RpcErr: From<serde_json::Error>. Verify this impl exists; otherwise, map explicitly with .map_err(|e| RpcErr::BadParams(e.to_string()))?.

  2. Iterator caveat documentation (dump_block.rs:30-32): Good that this is documented, but the ambiguity between "complete dump" and "truncated due to decode error" is concerning for production use. Consider adding a warning field to the response when this occurs, or prioritizing the storage layer fix mentioned.

Testing

  1. Test isolation: The setup_single_transfer_block helper uses InMemory storage, which is good for parallel test execution. The store.db path (helpers.rs:95) is ignored for in-memory engines, but ensure this doesn't conflict if tests ever switch to disk-based backends.

  2. Hex parsing in tests (debug_dump_block_tests.rs:58): The manual trim_start_matches("0x") works, but consider using U256::from_str_radix(&balance_str[2..], 16) or the crate's hex utilities for consistency.

Nits

  1. Clone in spawn_blocking (dump_block.rs:96): The storage.clone() is necessary, but ensure Store is cheap to clone (Arc-wrapped internally), which it likely is.

  2. Inclusive bounds check (dump_block.rs:103): The >= max_results check means requesting maxResults: 0 returns an empty page with next set to the first key. This is valid for pagination but worth noting as behavior.

The RPC routing and module registration are correct. The pagination logic using hashed addresses is sound for trie iteration.


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

@github-actions
Copy link
Copy Markdown

🤖 Codex Code Review

  1. debug_dumpBlock is still effectively unbounded for callers, so the “protect the RPC server” claim does not hold. At dump_block.rs any user-supplied maxResults overrides the default cap, and the loop at dump_block.rs eagerly materializes the whole page into a BTreeMap before serializing it. A single debug RPC can therefore pin a blocking worker, walk a huge trie, and allocate/emit an arbitrarily large response. This should enforce a hard server-side upper bound and reject larger values with BadParams.

  2. The new RPC can silently return an incomplete dump as if it were complete. The handler in dump_block.rs relies on Store::iter_accounts_from, but that iterator drops decode failures via map_while at store.rs, and trie traversal itself converts node lookup errors into end-of-iteration at trie_iter.rs. In practice, a corrupted/inconsistent trie can produce a response with no error and no next, which makes state-audit tooling believe the dump finished cleanly. For a debug/state endpoint, that is a correctness bug; the iterator needs to propagate errors, or the RPC needs an explicit “truncated/corrupt” signal instead of pretending completion.

  3. maxResults = 0 creates a non-progressing pagination token. At dump_block.rs zero is accepted, and the guard at dump_block.rs fires before inserting anything, returning { accounts: {}, next: first_key }. Re-issuing the request with that next repeats the same empty page forever. Reject 0 up front or clamp to >= 1.

I did not review any EVM/gas-accounting path here; this PR is RPC-only. I also could not run the targeted test slice because cargo test failed in this environment when rustup tried to create temp files under read-only /home/runner/.rustup/tmp.


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

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 26, 2026

Greptile Summary

Implements the debug_dumpBlock RPC endpoint, which walks the state trie at a given block's state root via iter_accounts_from and returns all accounts with their balance, nonce, storage root, and code hash. The PR also adds shared RPC test helpers and four integration tests covering state dump correctness, unknown-block errors, the latest tag, and cursor-based pagination.

  • The pagination design (maxResults + next cursor) is a deliberate, well-documented extension over geth's unbounded behaviour; the inclusive-cursor logic is correct.
  • balance: U256 uses ethereum_types's default hex serialization ("0x...") rather than geth's decimal string format; this divergence is absent from the documented divergences list and could silently break consumers that expect the geth format.
  • User-supplied maxResults has no server-side upper bound, so a caller can bypass the DEFAULT_MAX_RESULTS = 100_000 protection by explicitly setting a very large value.

Confidence Score: 4/5

The core logic is sound and the pagination cursor works correctly; the two open items are a balance serialization format that silently diverges from geth without being documented, and no server-side cap when callers override maxResults explicitly.

The implementation is structurally correct — block resolution, trie iteration, spawn_blocking usage, and pagination cursor semantics are all handled properly. The balance field serializes as hex rather than the decimal string geth produces, and that divergence is missing from the documented list, which could quietly break any consumer relying on geth-format output. The uncapped maxResults is a minor resource concern for large state roots but is isolated to this debug endpoint.

crates/networking/rpc/debug/dump_block.rs warrants a second look for the balance serialization format and the maxResults bound.

Important Files Changed

Filename Overview
crates/networking/rpc/debug/dump_block.rs Core implementation of debug_dumpBlock; pagination logic is correct, but balance serializes as hex (undocumented geth divergence) and user-supplied maxResults has no upper-bound cap.
crates/networking/rpc/debug/mod.rs Trivial one-line addition exposing the new dump_block module; no issues.
crates/networking/rpc/rpc.rs Registers debug_dumpBlock route in the dispatch table; correctly placed and no issues.
test/tests/rpc/debug_dump_block_tests.rs Good integration test coverage for basic state dump, block-not-found error, latest tag, and pagination; tests expect hex-encoded balance which is consistent with ethrex's U256 serde but differs from geth.
test/tests/rpc/helpers.rs New shared RPC test helpers for building in-memory stores and executing blocks; well-structured with appropriate dead-code allowance.
test/tests/rpc/mod.rs Module registration for new test files; no issues.

Sequence Diagram

sequenceDiagram
    participant Client
    participant RPC as map_debug_requests
    participant Handler as DumpBlockRequest
    participant Storage as Store

    Client->>RPC: debug_dumpBlock(block, config?)
    RPC->>Handler: parse params
    Handler->>Storage: resolve_block_number(block)
    Storage-->>Handler: "Option<block_number>"
    Handler->>Storage: get_block_header(block_number)
    Storage-->>Handler: "Option<BlockHeader>"
    Handler->>Handler: spawn_blocking
    Handler->>Storage: iter_accounts_from(state_root, start)
    loop up to maxResults accounts
        Storage-->>Handler: (hashed_addr, AccountState)
    end
    Handler-->>Handler: "(accounts BTreeMap, next Option<H256>)"
    Handler-->>RPC: "DumpResult { root, accounts, next? }"
    RPC-->>Client: JSON response
Loading
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/networking/rpc/debug/dump_block.rs:63-70
**Undocumented balance format divergence from geth**

`balance: U256` with `ethereum_types`'s default serde implementation serializes as a hex string (`"0x0de0b6b3a7640000"`), but geth's `debug_dumpBlock` returns `balance` as a decimal string (`"1000000000000000000"`). The doc comment above lists three explicit divergences from geth but omits this one. Any tool parsing the balance field expecting geth's decimal format will silently misread the value. Either serialize balance as decimal to match geth, or add it to the documented divergences list.

### Issue 2 of 2
crates/networking/rpc/debug/dump_block.rs:108-120
**`maxResults` has no upper bound when explicitly provided**

The `DEFAULT_MAX_RESULTS = 100_000` cap only applies when `maxResults` is omitted from the config object. When a caller explicitly provides `{"maxResults": 18446744073709551615}`, the whole trie is iterated without any limit. On a large mainnet state (~200 M+ accounts) this could exhaust server memory or hold the blocking thread for minutes. Consider clamping the user-supplied value to a maximum (e.g., `max_results.min(MAX_ALLOWED_RESULTS)`) so the per-request bound holds regardless of what the caller sends.

Reviews (1): Last reviewed commit: "fix(rpc): make debug_dumpBlock non-block..." | Re-trigger Greptile

Comment on lines +63 to +70
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct DumpAccount {
balance: U256,
nonce: u64,
root: H256,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Undocumented balance format divergence from geth

balance: U256 with ethereum_types's default serde implementation serializes as a hex string ("0x0de0b6b3a7640000"), but geth's debug_dumpBlock returns balance as a decimal string ("1000000000000000000"). The doc comment above lists three explicit divergences from geth but omits this one. Any tool parsing the balance field expecting geth's decimal format will silently misread the value. Either serialize balance as decimal to match geth, or add it to the documented divergences list.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/networking/rpc/debug/dump_block.rs
Line: 63-70

Comment:
**Undocumented balance format divergence from geth**

`balance: U256` with `ethereum_types`'s default serde implementation serializes as a hex string (`"0x0de0b6b3a7640000"`), but geth's `debug_dumpBlock` returns `balance` as a decimal string (`"1000000000000000000"`). The doc comment above lists three explicit divergences from geth but omits this one. Any tool parsing the balance field expecting geth's decimal format will silently misread the value. Either serialize balance as decimal to match geth, or add it to the documented divergences list.

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

Comment on lines +108 to +120
let storage = context.storage.clone();

// The trie iterator opens long-lived locked DB transactions and walks
// the state synchronously — must run off the async runtime.
let (accounts, next) = tokio::task::spawn_blocking(move || {
let iter = storage.iter_accounts_from(state_root, start)?;
let mut accounts: BTreeMap<H256, DumpAccount> = BTreeMap::new();
let mut next = None;
for (hashed_addr, account_state) in iter {
if accounts.len() >= max_results {
next = Some(hashed_addr);
break;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 maxResults has no upper bound when explicitly provided

The DEFAULT_MAX_RESULTS = 100_000 cap only applies when maxResults is omitted from the config object. When a caller explicitly provides {"maxResults": 18446744073709551615}, the whole trie is iterated without any limit. On a large mainnet state (~200 M+ accounts) this could exhaust server memory or hold the blocking thread for minutes. Consider clamping the user-supplied value to a maximum (e.g., max_results.min(MAX_ALLOWED_RESULTS)) so the per-request bound holds regardless of what the caller sends.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/networking/rpc/debug/dump_block.rs
Line: 108-120

Comment:
**`maxResults` has no upper bound when explicitly provided**

The `DEFAULT_MAX_RESULTS = 100_000` cap only applies when `maxResults` is omitted from the config object. When a caller explicitly provides `{"maxResults": 18446744073709551615}`, the whole trie is iterated without any limit. On a large mainnet state (~200 M+ accounts) this could exhaust server memory or hold the blocking thread for minutes. Consider clamping the user-supplied value to a maximum (e.g., `max_results.min(MAX_ALLOWED_RESULTS)`) so the per-request bound holds regardless of what the caller sends.

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

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.
azteca1998 added a commit that referenced this pull request May 26, 2026
Three real issues with the original handler:

- Wrong return values. The handler emitted `Address::from(hash)`, which
  takes the low 20 bytes of the 32-byte hashed-address key. That value
  isn't the original address — without a preimage store (ethrex has
  none) you can't recover it. Switch the response type to `Vec<H256>`
  of hashed addresses and document the divergence from geth on the
  type. The new `*_includes_sender` integration test asserts the sender's
  `keccak256(address)` appears in the result; it would have failed
  against the original truncation.

- Blocking I/O on the async runtime. `Store::iter_accounts` opens four
  long-lived locked DB transactions and walks the trie synchronously
  (same reason snap-sync's validators use `spawn_blocking`). The handler
  now runs the diff on a blocking thread.

- Full state materialised twice. The original collected every account
  from both tries into `HashSet<(H256, Vec<u8>)>` keyed on a stringified
  debug-format value — O(state) memory and millions of `format!()`
  allocations on mainnet. Replace with a streaming merge of the two
  iterators (both yield in hashed-key order from the trie traversal),
  comparing `AccountState` by value. O(1) extra memory.

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_get_modified_accounts_tests.rs`. Integration coverage
now spans: sender-appears-in-diff, by-number/by-hash equivalence,
empty-range, start-after-end (both variants), and unknown-block.
azteca1998 added a commit that referenced this pull request May 26, 2026
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.
azteca1998 added a commit that referenced this pull request May 26, 2026
…the test plan

The handler iterated the state trie synchronously inside an async RPC
method via `iter_accounts_from`, which opens four long-lived locked DB
transactions and walks the trie node by node (same mechanics that
forced `debug_dumpBlock` onto `spawn_blocking`). Switch to a blocking
thread.

Widen the first parameter from a raw `H256` block hash to
`BlockIdentifierOrHash`, so clients can pass a block number, a tag
(`latest`, etc.), an EIP-1898 object, or a hash — matching geth's
documented `blockNrOrHash` signature for `debug_accountRange`.

Document the implementation gaps on the type rather than papering over
them: `txIndex` is parsed (and accepts geth's `-1` sentinel via `i64`)
but ignored — returning the state immediately after the Nth transaction
needs mid-block trie-root reconstruction. `code` / `storage` are not
emitted; `nocode` / `nostorage` / `incompletes` (geth params 5–7) are
therefore not accepted. `AccountEntry.key` is always `null` because
ethrex has no preimage store.

The PR shipped with zero integration coverage of its own test plan.
Test setup boilerplate moves into the shared `rpc::helpers` module from
#6701, the misnamed `debug_trace_tests.rs` becomes
`debug_account_range_tests.rs`, and the new tests check all four items:
returns account entries with the sender's hashed address visible,
paginates correctly (max=1 page then continue from `next`), reports
all-zero `next` on completion, and errors on both unknown block hash
and unknown block number. Also added parse tests for the new
block-number and tag inputs.
azteca1998 added a commit that referenced this pull request May 26, 2026
…fy content

Same shape of issues as the sibling debug_accountRange PR:

- The trie iteration ran inside `async fn handle`, blocking the tokio
  worker on `iter_storage_from`'s long-lived locked DB transactions.
  Wrapped in `tokio::task::spawn_blocking`.
- The first parameter was a raw `H256` block hash. Widened to
  `BlockIdentifierOrHash` so clients can pass a block number, tag, hash,
  or EIP-1898 object — matching geth's documented `blockNrOrHash`.
- `txIndex` was `usize`, refusing geth's `-1` sentinel at parse time;
  switched to `i64`. The limitation (txIndex is currently ignored — we
  always return state at the end of the block) is documented on the
  type rather than hidden in a one-line TODO.

The shipped integration test queried storage on the *sender* (an EOA
with no storage trie), so it only exercised the empty-result path. None
of the four items on the PR's own test plan had real coverage.

Replaced it with a deploy-based fixture: the new helper
`setup_block_with_storage_contract` deploys a tiny contract whose
constructor sets three storage slots (slot 0 = 0x11, 1 = 0x22, 2 = 0x33)
via SSTORE, computing the deployed address via `calculate_create_address`.
Tests now verify: the three slots come back with the right values keyed
on their `keccak256(slot_index)` hashes, pagination with `max=1` walks
all three slots via `nextKey`, an unknown address returns
`{storage:{}, nextKey:null}` (matches geth), an EOA's address behaves
identically (no error), block-by-number resolves the same state, and an
unknown block hash returns "Block not found".

Test setup boilerplate moves into the shared `rpc::helpers` module from
#6701 (with the new `create_deploy_tx` / `setup_block_with_storage_contract`
helpers); the misnamed `debug_trace_tests.rs` is renamed to
`debug_storage_range_at_tests.rs`.
azteca1998 added a commit that referenced this pull request May 26, 2026
…oots

The shipped integration test only asserted that the response is "an
array of hex strings" — a check any half-broken implementation would
pass. The four items on the PR's own test plan all lacked real coverage.

Three meaningful tests now lock in the algorithm's correctness:

- single-tx: the only intermediate root must equal `block.header.state_root`.
  For an empty-withdrawal block, state_after_last_tx == block.state_root,
  so this is the strongest single-block correctness check available
  without re-running execution in the test harness.
- multi-tx (3 transfers from the same sender via a new
  `setup_multi_transfer_block` helper): three roots are returned,
  consecutive ones differ (each tx mutates the sender's nonce/balance),
  and the final root equals `block.state_root`.
- unknown-block: returns "Block not found".

A fourth test covers the config-object form (`{timeout, reexec}`).

Test setup boilerplate moves into the shared `rpc::helpers` module from
#6701 (with the new `setup_multi_transfer_block` helper); the misnamed
`debug_trace_tests.rs` is renamed to `debug_intermediate_roots_tests.rs`.

Implementation review note: the handler currently rebuilds the full
state trie from `parent_state_root` on every iteration (open trie +
apply cumulative updates), which is O(N²) in the tx count. Correct and
fine for typical 200-tx mainnet blocks but worth optimising via
incremental updates if this endpoint ever sees serious use. The
quadratic cost was not changed here — the change scope is correctness
coverage.
azteca1998 added a commit that referenced this pull request May 26, 2026
This PR duplicated the same `collect_four_byte_selectors` body that
ships in the standalone 4byteTracer PR — including its three correctness
bugs against geth's reference (eth/tracers/native/4byte.go):

- size was `len(input)` instead of `len(input) - 4`,
- the top-level transaction call was counted (geth skips depth 0),
- every CallType was counted (geth only counts CALL + DELEGATECALL,
  skipping STATICCALL, CALLCODE, CREATE*, SELFDESTRUCT).

Fix all three here and add precompile-address skipping to match what
geth's `isPrecompiled` filter does on mainnet. Pre-existing unit tests
that asserted the buggy output are rewritten to lock in the corrected
semantics: top-frame skip, short-input skip, arg-size key, CALL/DELEGATECALL
filter, precompile skip, duplicate counting.

Two ergonomic fixes on the muxTracer handler itself:

- `debug_traceBlockByNumber` with muxTracer was surfacing as
  `RpcErr::Internal`; switched to `BadParams` since refusing the request
  is a user-input issue, not a server fault. The error message now also
  points the caller at the per-transaction form.
- Documented all three known divergences from geth on the `MuxTracer`
  variant: per-sub-tracer re-execution cost, no block-level support,
  and the closed set of routable sub-tracers (no flatCallTracer yet).

Integration coverage now actually proves the multiplexer is faithful:
three cross-validation tests assert that the mux output's `callTracer`,
`prestateTracer`, and combined-multi-tracer slots equal what each
sub-tracer returns when invoked standalone. Plus tests for the noop
shape (`{}`), unknown sub-tracer, missing `tracerConfig`, and the
block-level `BadParams` surface.

Test setup boilerplate moves into the shared `rpc::helpers` module from
#6701; the misnamed `debug_trace_tests.rs` is renamed to
`debug_mux_tracer_tests.rs`.
azteca1998 added a commit that referenced this pull request May 27, 2026
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.
azteca1998 added a commit that referenced this pull request May 27, 2026
Three real issues with the original handler:

- Wrong return values. The handler emitted `Address::from(hash)`, which
  takes the low 20 bytes of the 32-byte hashed-address key. That value
  isn't the original address — without a preimage store (ethrex has
  none) you can't recover it. Switch the response type to `Vec<H256>`
  of hashed addresses and document the divergence from geth on the
  type. The new `*_includes_sender` integration test asserts the sender's
  `keccak256(address)` appears in the result; it would have failed
  against the original truncation.

- Blocking I/O on the async runtime. `Store::iter_accounts` opens four
  long-lived locked DB transactions and walks the trie synchronously
  (same reason snap-sync's validators use `spawn_blocking`). The handler
  now runs the diff on a blocking thread.

- Full state materialised twice. The original collected every account
  from both tries into `HashSet<(H256, Vec<u8>)>` keyed on a stringified
  debug-format value — O(state) memory and millions of `format!()`
  allocations on mainnet. Replace with a streaming merge of the two
  iterators (both yield in hashed-key order from the trie traversal),
  comparing `AccountState` by value. O(1) extra memory.

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_get_modified_accounts_tests.rs`. Integration coverage
now spans: sender-appears-in-diff, by-number/by-hash equivalence,
empty-range, start-after-end (both variants), and unknown-block.
Add debug_dumpBlock which dumps the entire state at a given block,
returning the state root and all accounts with their balance, nonce,
storage root, and code hash.
Execute a block, then query debug_dumpBlock by number. Assert
the response has root and accounts fields with non-empty state.
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 azteca1998 force-pushed the feat/debug-dump-block branch from e959e66 to 8130d7c Compare May 27, 2026 15:23
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.

Caller-supplied maxResults bypasses the DoS protection the default is there to provide. The doc says DEFAULT_MAX_RESULTS exists "to protect the RPC server" against unbounded dumps — but self.config.max_results.unwrap_or(DEFAULT_MAX_RESULTS) lets a caller pass {"maxResults": 18446744073709551615} and dump the entire state into one un-paginated BTreeMap in memory, defeating that protection. If the protection is real, the value should be clamped to a hard ceiling rather than only used as a default — see inline. (Mitigating context: debug_* is typically gated behind an access-controlled namespace, so the exposure is limited to operators who deliberately enabled the debug RPC. Still worth a ceiling, since the stated intent is server protection.)


Also, PR should point out that it closes the sub-issue instead of the tracking issue


let state_root = header.state_root;
let start = self.config.start.unwrap_or_else(H256::zero);
let max_results = self.config.max_results.unwrap_or(DEFAULT_MAX_RESULTS);
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.

DEFAULT_MAX_RESULTS only acts as a default, not a ceiling — a caller passing {"maxResults": <huge>} overrides it entirely and dumps the full state into one in-memory BTreeMap, which is exactly the unbounded-response DoS the constant's doc says it's protecting against:

Picked to bound response size on large states ... ethrex diverges here to protect the RPC server.

If the protection is meant to hold regardless of caller input, clamp to a hard ceiling:

/// Absolute cap; callers cannot exceed this even via `maxResults`.
const MAX_RESULTS_CEILING: usize = 100_000;
...
let max_results = self
    .config
    .max_results
    .unwrap_or(DEFAULT_MAX_RESULTS)
    .min(MAX_RESULTS_CEILING);

Then large states are walked via the next cursor across multiple calls instead of one unbounded response. (If unbounded single-shot dumps are intentionally allowed for trusted debug operators, a one-line doc note saying so would resolve the apparent contradiction with the "protect the RPC server" rationale.)

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`.
…ug_dumpBlock

- Clamp user-supplied maxResults to DEFAULT_MAX_RESULTS so callers
  cannot bypass the response-size protection by passing a huge value.
- Document the balance hex-vs-decimal format divergence from geth.
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 Review

Development

Successfully merging this pull request may close these issues.

Implement debug_dumpBlock

2 participants