chore: upgrade to reth v2.3.0 / revm 40 / alloy 2.0.5 (mega-evm revm-40 branch) - #184
chore: upgrade to reth v2.3.0 / revm 40 / alloy 2.0.5 (mega-evm revm-40 branch)#184flyq wants to merge 14 commits into
Conversation
… cz/chore/upgrade-revm-40)
reth v2.3.0's V2 sparse tries merge extension nodes into their child branches and silently drop extensions whose child branch is absent from the witness — a legal minimal exclusion proof that the frozen MegaETH witness format produces, and whose split-on-insert never needs the child. Vendors the proven v1.6.0 serial sparse trie into stateless-core, reveals the storage trie directly (dropping the synthesized account-leaf wrapper reveal_witness required), and adds a regression test for the extension-split shape.
…update docs - Drop the returnValue 0x-strip shim: alloy-rpc-types-trace 2.x serializes the prefix on both mega-reth and this server, so emitting the frame as-is is what now matches. - rust-version 1.94 (mega-evm and the optimism monorepo crates require it). - Document the vendored withdrawal sparse trie and erc7562Tracer handling.
Claude review status
🛠️ Review did not finish Attempted head This round did not publish: MODEL_ACTION_FAILED in phase review_retry. Anything listed below is from the last round that did. Re-run the workflow or push a new commit to try again. |
Codecov Report❌ Patch coverage is
☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The reth section now holds only paradigmxyz/reth tag v2.3.0 crates; alloy-op-evm, alloy-op-hardforks, and reth-optimism-chainspec move next to the op-alloy crates they share a source identity (and pin comment) with.
Replaces the vendored reth-1.6 serial sparse trie (~1.9k lines) with a ~250-line linearization of the witnessed storage trie into alloy_trie::HashBuilder — the primitive reth's own eth_getProof generator is built on. Surviving and updated leaves stream as add_leaf (leaf paths are the hashed slots and never change); unwitnessed subtrees stream as add_branch (position and hash invariant under edits elsewhere); every structural change — extension splits, branch collapses, new branches — falls out of the stream shape inside the builder, so the module carries no trie mutation code at all. A pre-pass with no updates must reproduce storage_root exactly, self-checking the walk on every witness before its post-state root is trusted. Updates the witness cannot prove (descent into an unwitnessed subtree, a collapse adopting an unwitnessed node) fail closed, matching reth-1.6 behavior. Drops the reth-trie-common, smallvec, and auto_impl dependencies; adds tests for collapse merging, unwitnessed-survivor collapse, unwitnessed-region descent, and diverging-removal no-ops.
- withdrawals: skip the post-pass when a block carries no withdrawal updates; factor the branch-child partition into child_regions/ChildRegion shared by emission and survival walks; drop the dead empty-region arm; slice-pattern the sole-survivor check; add_insert_leaves helper for the four insert-only loops; flatten withdrawals/ back to withdrawals.rs; dedupe test fixtures (TrieNode::rlp, SLOT_B, ext_with_absent_branch). - debug-trace-server: reject the unsupported erc7562Tracer at the RequestShape::classify gate (-32602 before any block data is fetched), the executor arms staying as unreachable backstops; drop per-tx EvmState clones the revm-40 API made avoidable (block/tx prestate, mux); take tracing inspectors by mem::replace instead of cloning their recorded trace arenas. - executor: project slot_num from header.slot_number like every other BlockEnv field instead of pinning 0.
Beyond the textual Cargo.toml conflict (alloy family stays 2.0.5; #172's new alloy-rpc-client added at 2.0.5): - workspace reqwest 0.12 -> 0.13: #172 hands our tuned client to alloy's transport, which rides reqwest 0.13 in the 2.x era; stateless-r2 keeps its own 0.12 pin (API-coupled to mega-reth, no type crossing). - #169's builtin_tracer_kind gains the alloy-2.x Erc7562Tracer arm in its not-routed-here group. - mux rejection wording: since alloy 2.x an unknown tracer key in a mux config parses as a JS tracer name and is rejected by MuxInspector construction rather than serde; the error context stays 'invalid muxTracer tracerConfig' so #169's defense-in-depth test keeps its contract.
mega-reth's upstream/reth workspace — the branch this upgrade wave targets — is on reqwest 0.13, so the API-coupled pin follows (rustls-tls feature is named rustls since 0.13; http2 note stays valid, the feature is opt-in now). The lockfile drops to a single reqwest 0.13.4.
The no_std build (--no-default-features) has no prelude format!; import it explicitly like evm_database.rs does. Fixes the no-std and coverage CI jobs, which both compile stateless-core without default features.
- tracing_executor: extract the four TracerKind frame builders into one extract_trace_frame! macro shared by the block and tx paths (the twin matches this PR had to edit in lockstep six times); the FlatCall take now really is the per-tx reset (guarded tail); mux inspectors reset from a pristine clone instead of re-cloning + re-parsing the config each tx (also removes the only dispatcher unwrap); erc7562 rejection has one home in builtin_tracer_kind and the dispatchers route it there; preceding-tx replay in the tx-level and parity paths seeds a none-config inspector instead of recording every step just to discard it; drop a dead duplicate binding. - rpc_service: classify_and_gate dedups its rejection tail behind an exhaustive match (a future RequestShape variant must make an explicit gate decision); new test pins the -32602 for unsupported tracers. - response_cache: the label-conformance test now covers the Unsupported variant. - withdrawals: the leaf arm reuses sortedness instead of collect+sort; the split_at_prefix upper bound is a second partition_point.
revm-bytecode 11 reordered Bytecode's serde enum, so rows written by pre-upgrade binaries no longer decode (or, for 19-byte codes, silently mis-decode into EIP-7702 delegations to garbage addresses). Treat decode failures and keccak mismatches as cache misses: both binaries re-fetch misses through the hash-verified RPC tier and overwrite the stale row, so an in-place upgrade degrades to a one-time re-fetch instead of halting the node. Regression tests pin real revm-bytecode 6.2.2 row encodings for both failure shapes.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2f90a7b3ed
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| for (idx, child) in branch.as_ref().children() { | ||
| let split = rest.partition_point(|(p, _)| p.get_unchecked(path.len()) <= idx); | ||
| let (region, tail) = rest.split_at(split); | ||
| rest = tail; | ||
| regions.push(ChildRegion { idx, child: child.cloned(), updates: region }); |
There was a problem hiding this comment.
Emit updates for empty branch slots
When a withdrawal write targets an empty branch child whose nibble is greater than the branch's last populated child, this loop never creates a ChildRegion for it: rest still contains the insert after the loop, and release builds ignore the debug_assert!, so the new leaf is never streamed into HashBuilder. A valid block that adds such a withdrawal slot computes the old withdrawal root and is rejected; the branch walk needs to represent all 16 child slots or otherwise drain the remaining updates.
AGENTS.md reference: AGENTS.md:L132-L132
Useful? React with 👍 / 👎.
| for (idx, child) in branch.as_ref().children() { | ||
| let split = rest.partition_point(|(p, _)| p.get_unchecked(path.len()) <= idx); | ||
| let (region, tail) = rest.split_at(split); | ||
| rest = tail; | ||
| regions.push(ChildRegion { idx, child: child.cloned(), updates: region }); |
There was a problem hiding this comment.
Emit updates for empty branch slots
When a withdrawal write targets an empty branch child whose nibble is greater than the branch's last populated child, this loop never creates a ChildRegion for it: rest still contains the insert after the loop, and release builds ignore the debug_assert!, so the new leaf is never streamed into HashBuilder. A valid block that adds such a withdrawal slot computes the old withdrawal root and is rejected; the branch walk needs to represent all 16 child slots or otherwise drain the remaining updates.
Useful? React with 👍 / 👎.
Summary
Switches the workspace from the reth v1.6.0 era to the reth v2.3.0 stack, following mega-evm's revm-40 upgrade (megaeth-labs/mega-evm#365): reth git tags v1.6.0 → v2.3.0, revm 27.1.0 → 40.0.3, revm-inspectors 0.27.3 → 0.40.1, alloy 1.0.23 → 2.0.5, alloy-evm 0.15.0 → 0.36.0, alloy-hardforks 0.2.7 → 0.4.7, alloy-primitives 1.3.0 → 1.6.0, alloy-trie 0.9.0 → 0.9.4, op-alloy 0.18.12 → 2.0.0, reqwest 0.12 → 0.13 across the workspace including
stateless-r2's explicit pin (its API exposes&reqwest::Clientand must match mega-reth, whoseupstream/rethworkspace is on 0.13), leaving a single reqwest in the lockfile, and mega-evm tag v1.7.0 → branchcz/chore/upgrade-revm-40(locked at 8d1fc07). salt stays at v1.0.5. MSRV moves to 1.94.OP crates: one source identity
crates.io has no op-revm / alloy-op-evm release built against revm 40, so mega-evm pins the whole OP family to the Optimism monorepo at rev
f67d87cd; this workspace must share that exact source orOpTxEnvelope/ receipt / hardfork types split into incompatible copies.alloy-op-evm,alloy-op-hardforks,op-alloy-consensus/network/rpc-types, andreth-optimism-chainspec(which moved out of paradigmxyz/reth into the monorepo) are all pinned there, grouped under# opin the manifests; the monorepo's own reth deps usetag = "v2.3.0", the same source as our direct reth pins, so the graph unifies with a single copy of every crate (verified in Cargo.lock).Withdrawal MPT verification reimplemented on alloy-trie
reth v2.3.0's sparse tries moved to a V2 node model that merges extension nodes into their child branches. An extension whose child branch is absent from the witness — a legal minimal exclusion proof the frozen MegaETH witness generator emits, whose split-on-insert never reads the child — cannot be represented: every reveal path silently drops it and the insert then fails on a blind node (caught by the synthetic fixture at block 360; both v2.3.0 trie impls also panic on standalone extensions, and reth's own flat-witness entry point shares the gap with no in-tree users). Upstream sidesteps this by making its internal proof producers over-retain extension children (
AddedRemovedKeys), a contract our frozen witness format — and the immutable historical witnesses in R2 — can never satisfy.MptWitness::verify(crates/stateless-core/src/withdrawals.rs) now linearizes the witnessed storage trie intoalloy_trie::HashBuilder— the same primitive reth'seth_getProofgenerator is built on — as a sorted stream:add_leaffor surviving/updated leaves (leaf paths are the hashed slots and never change) andadd_branchfor unwitnessed subtrees (position and hash invariant under edits elsewhere). All structural changes — extension splits, branch collapses, new branches — fall out of the stream shape inside the builder, so the module contains no trie mutation code; this also returns to the storage-trie-only design of the original implementation (#16), dropping the synthesized account-leaf wrapper #119 introduced. A pre-pass with no updates must reproducestorage_rootbit-for-bit before the post-pass output is trusted (skipped entirely when a block carries no withdrawal updates); updates the witness cannot prove — descent into an unwitnessed subtree, a collapse adopting an unwitnessed node — fail closed, matching the previous sparse-trie behavior. Thereth-trie-common/reth-trie-sparsedependencies are gone. New tests cover the extension-split regression, collapse merging, unwitnessed-survivor and unwitnessed-descent fail-closed paths, and diverging-removal no-ops.Trace server: output drift and request gating
Struct-logger traces drift in serialization only (execution is byte-identical per mega-evm's replay gate): logs gain
"refund": 0, memory words andreturnValuegain0xprefixes — post-upgrade mega-reth serializes the prefix too, so the old strip-shim is removed. Gas values are unchanged (tx_gas_used()equals the oldgas_used()with EIP-8037 pinned off). The new alloy builtinerc7562Traceris not implemented:RequestShapeclassifies it asUnsupportedandclassify_and_gaterejects it with-32602before any block data is fetched, the executor arms remaining as unreachable backstops. Per-txEvmStateclones the revm-40 API made avoidable are dropped, and tracing inspectors are taken bymem::replaceinstead of cloning their recorded trace arenas. Since alloy 2.x, an unknown tracer name inside a mux config parses as a JS-tracer key and is rejected atMuxInspectorconstruction rather than by serde; the error stays request-attributable with the sametracerConfigwording (#169's defense-in-depth test keeps its contract).Other API adaptations
AccountInfogainsaccount_id: None(witness state is address-keyed),BlockEnv.slot_numprojects fromheader.slot_numberlike every other field (EIP-7843; absent until Amsterdam),BundleState::stateuses theAddressMapfixed-bytes hasher, and mega-evm'srun_transactionoutcome carriesresult_and_stateinstead of separately movable fields.Testing
cargo test --workspace: 373 passed / 0 failed (the ignored suites are the pre-existing env-gated live-server tests), including the mainnet single-block fixtures, the synthetic-chain pipeline test that exposed the trie regression, and the new withdrawal edge-case tests.cargo clippy --workspace --all-targets --all-features,cargo fmt --all --check, andcargo sort --checkare clean. Latestmain(#169, #172) is merged in.Notes
mega-evm is pinned to a branch while megaeth-labs/mega-evm#365 is open; re-pin to a tag once it lands. mega-reth's
upstream/rethbranch pins the Optimism monorepo atop-reth/v2.3.3(2063657) rather thanf67d87cd— if mega-reth ends up embedding stateless-core, the repos must converge on one monorepo rev or the OP types will not unify.