Conversation
Deletes the second sources of truth and the dead parameters/accessors the /simplify review found, with no observable change on canonical-block paths. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CHVpMMX9N69sUNbuKgpBVY
Deletes the `writer` parameter threaded through `validate_block`, `validate_block_deriving_updates`, `replay_block` and `verify_and_replay`, and with it EIP-3155 trace output. A feature deletion, not a cleanup. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CHVpMMX9N69sUNbuKgpBVY
…s_used from the executor Both are definitional rewrites of security-critical derivations, argued in the PR body: `trie_hash()` is `keccak256(encoded_2718())`, and mega-evm's `gas_used` is the last receipt's cumulative gas. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CHVpMMX9N69sUNbuKgpBVY
Block integrity verification becomes the RPC retry loop's finalize step and runs on the blocking pool; the advancer's pre-advance hooks and store commit run there too. Panic semantics are preserved and pinned by tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CHVpMMX9N69sUNbuKgpBVY
Claude review status
🛠️ Review did not finish Attempted This round did not publish: PRIOR_QUESTION_INVALID in phase compile. Anything listed below is from the last round that did. Re-run the workflow or push a new commit to try again. |
Resolves what main's squash-merges of #194, #195 and #196 left conflicting against this branch's own copies of that work. Every file outside this PR's two commits is taken from main verbatim, so the branch keeps only the reviewed form of those refactors and the PR diff narrows to the perf work. - verify_block_integrity: main's #196 form wins outright. It is this branch's "encode each transaction once" rewrite plus the per-transaction keccak check and its forged-hash test, so the PR no longer carries that hunk at all. The blocking-pool wrapper around it is unchanged. - executor.rs: main's inlined replay body, with this branch's `execution_result.gas_used` + `debug_assert_eq!` re-applied on top; the `execute_transactions` extraction is dropped, as it was during #194's review. - rpc_client.rs test imports: union of main's `TestFixtures` and this branch's `consistent_header`. The merged tree is origin/main plus exactly the deltas of e62e3e1 and 432e550. Cargo.lock is untouched; check, clippy, fmt, sort and the full test suite are green.
Codecov Report✅ All modified and coverable lines are covered by tests. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
❓ Review complete — 1 open question(s)
Reviewed head c24d6f32.
Reviewed the two blocking-work-off-runtime moves (block verify as round_robin_with_backoff finalize, chain-advancer commit via spawn_blocking), the extracted next_data_rr_start, new tests, and README/AGENTS.md doc updates.
Open questions — answer them in a reply on this PR. Each one is marked answered here once a later review round confirms the answer, so this list stays current:
✅ **Answered** — flyq confirmed in the conversation the gas_used change is intentional, from commit e62e3e1, and added a '## 3.' section with the mega-evm citation to the PR description. This round's test refactor (using verify_and_replay) reinforces the claim.
- The executor.rs change in
replay_block— switchinggas_usedto theBlockExecutionResult::gas_usedfield (guarded by adebug_assert_eq!and pinned by a newreplayed_gas_used_matches_the_mainnet_headertest) — isn't mentioned in the PR description. Is this refactor intentionally in this PR, or did it slip in from another slice of the #170 split? - Why it matters: Only the
debug_assert_eq!enforcesgas_used == receipts.last().cumulative_gas_used()in debug/CI; release builds trust whatever mega-evm produces. A divergence would surface loudly (output.gas_used != header.gas_usedwould reject blocks), but the change alters the source of a consensus-critical value with no description, making the intent hard to attribute later. - How to verify: Confirm with the author whether this hunk was meant to ride along with the perf changes; if not, move it to its own PR (or the correct slice of the #170 split) with its own description.
`replayed_gas_used_matches_the_mainnet_header` was inserted between `validate_block_deriving_updates_mainnet_fixtures` and the doc comment describing it, so that comment documented the new test and the older one lost its own. Moving the comment back down restores both pairings; the change is a pure reorder, no line edited.
|
Intentional, and not from another slice — but you are right that the description never covered it. The hunk comes from |
Quality-only cleanups on this PR's own diff; no behavior change. Full suite 484 passed / 0 failed, clippy `--all-features` clean, fmt and sort green. - `rpc_client.rs`: `get_block_with_deadline` reached the retry loop's finalize seam by hand-copying eight of `call_with_deadline_at`'s twelve arguments, giving the data path two definitions of its own provider-rotation policy. The seam now lives on the funnel: `call_with_finish` owns the data-path wiring, `call_with_deadline_at` delegates with the identity finish it used to write inline, and `get_block_with_deadline` passes method plus two closures. `round_robin_with_backoff` is back to two call sites, which makes its own signature-justifying comment true again. - `executor.rs`: `replayed_gas_used_matches_the_mainnet_header` hand-rolled the witness-verify -> replay sequence that `verify_and_replay` owns — the front half `validate_block` actually runs. It calls the helper now, so the pinned value cannot drift from the production one. - `advancer.rs`: the processed batch is freed inside the blocking closure rather than handed back for the async task to drop. In the trace server each item owns a block plus its witness, so that drop was the work this hop exists to avoid, landing back on the runtime. Clearing keeps the allocation, so the buffer is still reused. - `pipeline/tests.rs`: the panic-payload downcast had an unreachable `String` branch and an `unwrap_or_default()` that turned a payload-type mismatch into an empty string — i.e. into "the message was lost". It asserts the `&str` payload directly. - `rpc_client.rs` tests: the two nine-line mock servers differed by one expression; `start_counting_block_rpc` follows the file's existing `start_counting_block_number_rpc` shape.
`TraceHooks::pre_advance` cloned every block and every witness purely to reshape a slice for `store_block_data`, which only ever borrows: both fields go straight into `encode_block_to_vec` / `encode_to_vec`. The witness clone is the expensive half — a node-by-node `BTreeMap` copy of `kvs` plus the `levels` map — and it ran once per block, per advance batch, on the same blocking hop this PR just introduced. Taking `&[(&Block<Transaction>, &LightWitness)]` makes it a 16-bytes-per-item Vec of references instead. That matters for the sentence this PR added to AGENTS.md: `pre_advance` is described as synchronous multi-millisecond disk work, and a meaningful slice of those milliseconds was this memcpy rather than disk. Mechanical elsewhere: the trait declaration, the inherent and trait impls, the test stub, and five call sites. `test_server_db_store_and_get_block_and_witness` drops a `block.clone()` that only existed to feed the owned slice.
RealiCZ
left a comment
There was a problem hiding this comment.
Reviewed the full diff and verified the claims it rests on:
round_robin_with_backoff'sfinishseam classifies afinisherror as that provider'sError(rotates) and afinishthat outruns the deadline asDeadlineClamped, so §1's "a tampered block rotates like a transport failure" holds and verification no longer sits inside the per-attempt window.- §3: mega-evm v1.7.0 (
30ce038)finish()atcrates/mega-evm/src/block/executor.rs:712setsgas_usedtoreceipts.last().cumulative_gas_used(), so reading the field is definitional on the pinned version. - §2:
run_with_signalsonly awaits the pipeline handle with a drain timeout and never aborts it, and a runtime drop waits for running blocking tasks, so a commit in flight on the blocking pool is never dropped mid-way.resume_unwindkeeps a store/hook panic fatal. chain_advancerispub(crate),run_pipelinealready tookArcs, andBlockStoreis trace-server-local, so nothing mega-reth consumes changes shape.- Merges cleanly onto
mainand alongside #221. - Ran locally:
test_chain_advancer_propagates_hook_panics,replayed_gas_used_matches_the_mainnet_header,block_verification_failure_rotates_to_the_next_provider, and theserver_dbtests all pass.
Two non-blocking notes inline. Approving.
|
|
||
| /// Deadline-aware counterpart of [`Self::get_block`]. | ||
| /// | ||
| /// Verification is the retry loop's *finalize* step, not part of the attempt window: it |
There was a problem hiding this comment.
Non-blocking, and pre-existing since #182 rather than introduced here: round_robin_with_backoff drops the concurrency permit after finish, so --data-max-concurrent-requests now also bounds how many ECDSA verifications run in parallel on the blocking pool, and attempt_elapsed (the on_rpc_attempt histogram) includes the verification CPU time. Consistent with how the witness decode already behaves, so fine for this PR; a follow-up could release the permit before finish so the cap counts only what is actually in flight against the gateway.
| /// | ||
| /// A failure here is an integrity failure from this provider — the retry loop records it as | ||
| /// that provider's `Error` and rotates, exactly like a transport error. | ||
| async fn verify_block_on_blocking_pool(block: Block<Transaction>) -> Result<Block<Transaction>> { |
There was a problem hiding this comment.
Worth one sentence in this doc: a panic inside verify_block_integrity becomes this provider's Error and rotates (with deadline = None it retries forever), whereas the advancer half of this PR deliberately re-raises panics. The asymmetry is right (a store panic is a corrupted persistence layer, a verify panic is bad provider data) and matches decode_witness_wire, but the PR description only states the advancer half, so a reader may assume both hops preserve panics.
Summary
PR 4/6 of the #170 split. #196 has merged and
mainis merged in as ofc24d6f3, so this now targetsmaindirectly.Two pieces of synchronous, multi-millisecond work move off the async runtime (§1, §2) — and the one that #170 got wrong is fixed here rather than shipped. §4 is that same argument one level down: a deep copy that was riding along inside §2, doing nothing. §3 is the one rider: an executor-side
gas_usedderivation, the surviving half of a commit whose other half went up as #196.vincent's review of #170 named this PR's whole reason for existing: "Moving
verify_block_integrityintospawn_blockingis a good change on its own, but it also moved verification insideround_robin_with_backoff's per-attempt timeout, so a healthy provider serving a large block can be classified as stalled, retried, and leave the blocking task running behind it. As a standalone three-line PR that consequence is visible on sight; at position 2 of 6 in a 30-file cleanup it is not."1. Block verification becomes the retry loop's finalize step
round_robin_with_backoffalready splits each attempt intof(run under the per-attempt window) andfinish(run after it, bounded by the caller's deadline alone). #182 introduced that seam for exactly this problem on the witness path: CPU-bound decode must neither burn the rotation reserve nor read as a provider stall, while a corrupt payload still rotates as that provider's error.get_block_with_deadlinenow uses the same seam instead ofcall_with_deadline's identityfinish:f=do_get_block_unchecked— the transport, timed by the attempt window.finish=verify_block_integrityon the blocking pool — per-transaction ECDSA recovery plus a re-encode of every envelope, outside the window.So the timeout interaction #170 introduced never exists: a healthy provider serving a large block cannot be classified as stalled by its own verification, and no abandoned blocking task runs behind a retry. A verification failure is still
RpcAttemptOutcome::Error, so a tampered block rotates exactly as a transport failure does — pinned byblock_verification_failure_rotates_to_the_next_provider, which serves a wrong-hash block from provider 1 and a good one from provider 2 and asserts both were hit.The seam is reached through the data path's own funnel rather than around it:
call_with_finishowns the provider-rotation wiring — providers, labels, concurrency, backoff policy, attempt cap, and the per-callrr_startrotation now inRpcClient::next_data_rr_start— andcall_with_deadline_atdelegates to it with the identityfinishit used to write inline.round_robin_with_backoffkeeps exactly two call sites, so the data path has one definition of its rotation policy rather than two.2. The advancer commits on the blocking pool
chain_advancertakesArc<S>/Arc<H>and runshooks.pre_advance+store.advance_chaininsidespawn_blocking. These are redb commits — and, in the trace server, multi-MB block-data writes — on a runtime that also serves RPC handlers.Panic semantics are preserved exactly: a panic in the store or the hooks comes back as a
JoinError, andtry_into_panic→resume_unwindre-raises it on this task, so a corrupted persistence layer still takes the process down the way it did inline. vincent called this handling "careful and correct" in #170;test_chain_advancer_propagates_hook_panicsnow pins it — aPipelineHookswhosepre_advancepanics must make the advancer future panic, not returnErr.The
metaslockstep vector is gone with it: the batch is moved into the blocking closure and the metas are built there from the items themselves, so the two can no longer drift.3.
gas_usedis read from the executor's own resultreplay_blockderived the header check'sgas_usedby re-deriving mega-evm's own expression,receipts.last().cumulative_gas_used(), rather than reading theBlockExecutionResult::gas_usedfield that mega-evm'sfinish()sets from that exact expression (v1.7.030ce038,crates/mega-evm/src/block/executor.rs:712). It now reads the field (crates/stateless-core/src/executor.rs:501). Definitional, not behavioural: on the pinned mega-evm the two cannot disagree. What it buys is that a future mega-evm accounting gas outside the receipt chain changes one derivation instead of silently diverging from a copy of it.Three things hold that claim down. The
debug_assert_eq!at the derivation site pins the upstream definition on every debug/CI replay;replayed_gas_used_matches_the_mainnet_header(executor.rs:1096) pins the surviving value against what real mainnet headers claim; and in release the value still feeds the header check atexecutor.rs:659, so a divergence rejects the block rather than passing quietly.This rode in on
e62e3e1, whose other half — theverify_block_integrityre-encode — was upstreamed as #196 and is now inmain; mergingmainin (c24d6f3) left only this half in the diff.4.
pre_advancestores block data by referenceMoving
pre_advanceonto the blocking pool surfaced work inside it that should not be there at all.TraceHooks::pre_advancecloned every block and every witness purely to reshape a slice forstore_block_data, which only ever borrows — both fields go straight intoencode_block_to_vec/encode_to_vec(bin/debug-trace-server/src/server_db.rs:86-87). The witness clone is the expensive half: a node-by-nodeBTreeMapcopy ofkvsplus thelevelsmap, once per block, per advance batch.BlockStore::store_block_datanow takes&[(&Block<Transaction>, &LightWitness)], sopre_advancebuilds a 16-bytes-per-item Vec of references instead (bin/debug-trace-server/src/chain_sync.rs:238). This is §2's argument one level down: §2 moved the work off the runtime, this removes a piece of it that never needed doing. It also keeps the AGENTS.md sentence honest —pre_advanceis described there as synchronous multi-millisecond disk work, and a meaningful slice of those milliseconds was memcpy, not disk.Mechanical elsewhere: the trait declaration, the inherent and trait impls, the test stub, and five call sites. No behavioural change, and the existing
server_dbstorage tests exercise the new signature unchanged.Testing
cargo fmt --all --check,cargo clippy --workspace --all-targets --all-features(0 warnings),cargo sort --check, full workspace suite 484 passed / 0 failed,cargo test -p stateless-core --no-default-features --lib --no-runclean.Both new tests were mutation-checked: forcing
verify = falsekills the rotation test, and replacingresume_unwindwith anErrreturn kills the panic test.Notes
§1 and §2 are the same argument applied to the two places blocking work sits on this runtime, and §4 is that argument one level down, so they are kept together — happy to split them apart if you would rather review them separately. AGENTS.md and README are updated for §1 and §2.
A quality pass over this PR's own diff landed in
333c283:get_block_with_deadlinereaches the finalize seam through the data path's funnel instead of copying its argument list, §3's test callsverify_and_replayrather than hand-rolling it, the advancer frees its batch on the blocking pool instead of handing it back to the runtime, and two test helpers were deduplicated. Behaviour-preserving; the numbers above are from after it.