Skip to content

chore: upgrade to reth v2.3.0 / revm 40 / alloy 2.0.5 (mega-evm revm-40 branch) - #184

Open
flyq wants to merge 14 commits into
mainfrom
liquan/chore/upgrade-reth-2.3
Open

chore: upgrade to reth v2.3.0 / revm 40 / alloy 2.0.5 (mega-evm revm-40 branch)#184
flyq wants to merge 14 commits into
mainfrom
liquan/chore/upgrade-reth-2.3

Conversation

@flyq

@flyq flyq commented Aug 10, 2026

Copy link
Copy Markdown
Member

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::Client and must match mega-reth, whose upstream/reth workspace is on 0.13), leaving a single reqwest in the lockfile, and mega-evm tag v1.7.0 → branch cz/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 or OpTxEnvelope / receipt / hardfork types split into incompatible copies. alloy-op-evm, alloy-op-hardforks, op-alloy-consensus/network/rpc-types, and reth-optimism-chainspec (which moved out of paradigmxyz/reth into the monorepo) are all pinned there, grouped under # op in the manifests; the monorepo's own reth deps use tag = "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 into alloy_trie::HashBuilder — the same primitive reth's eth_getProof generator is built on — as a sorted stream: add_leaf for surviving/updated leaves (leaf paths are the hashed slots and never change) and add_branch for 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 reproduce storage_root bit-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. The reth-trie-common / reth-trie-sparse dependencies 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 and returnValue gain 0x prefixes — post-upgrade mega-reth serializes the prefix too, so the old strip-shim is removed. Gas values are unchanged (tx_gas_used() equals the old gas_used() with EIP-8037 pinned off). The new alloy builtin erc7562Tracer is not implemented: RequestShape classifies it as Unsupported and classify_and_gate rejects it with -32602 before any block data is fetched, the executor arms remaining as unreachable backstops. Per-tx EvmState clones the revm-40 API made avoidable are dropped, and tracing inspectors are taken by mem::replace instead 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 at MuxInspector construction rather than by serde; the error stays request-attributable with the same tracerConfig wording (#169's defense-in-depth test keeps its contract).

Other API adaptations

AccountInfo gains account_id: None (witness state is address-keyed), BlockEnv.slot_num projects from header.slot_number like every other field (EIP-7843; absent until Amsterdam), BundleState::state uses the AddressMap fixed-bytes hasher, and mega-evm's run_transaction outcome carries result_and_state instead 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, and cargo sort --check are clean. Latest main (#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/reth branch pins the Optimism monorepo at op-reth/v2.3.3 (2063657) rather than f67d87cd — if mega-reth ends up embedding stateless-core, the repos must converge on one monorepo rev or the OP types will not unify.

flyq added 4 commits August 10, 2026 15:31
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.
@mega-maxwell

mega-maxwell Bot commented Aug 10, 2026

Copy link
Copy Markdown

Claude review status

Living comment — rewritten in place. The review workflow keeps this single comment up to date instead of posting a new one each round, so it always describes the latest reviewed commit and the earlier text is intentionally gone. No reply is needed here; reply to a finding in its own review thread, and answer an open question in a reply on this PR. The next review round reconciles your answer.

🛠️ Review did not finish

Attempted head 99485a5d · updated 2026-08-11T01:01:57+00:00

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

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.84615% with 16 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.2%. Comparing base (73a8827) to head (99485a5).

Files with missing lines Patch % Lines
crates/stateless-core/src/withdrawals.rs 93.6% 16 Missing ⚠️

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

flyq added 5 commits August 10, 2026 16:44
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.
@flyq flyq changed the title chore: upgrade to reth v2.3.0 stack (revm 40, alloy 2.0.5) chore: upgrade to reth v2.3.0 / revm 40 / alloy 2.0.5 (mega-evm revm-40 branch) Aug 10, 2026
flyq added 2 commits August 10, 2026 18:19
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.
@flyq
flyq marked this pull request as ready for review August 10, 2026 10:43
flyq added 2 commits August 10, 2026 19:03
- 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.
Comment thread Cargo.toml
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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +367 to +371
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 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

Comment on lines +367 to +371
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 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

@vincent-k2026

Copy link
Copy Markdown
Collaborator

Best-engineered PR of the three currently open, and two things still have to land before it can merge.

Genuinely good: the OP single-source-identity argument is right and verified in the lockfile — that's the usual failure mode of this upgrade (OpTxEnvelope / receipt / hardfork types splitting into two incompatible copies) handled head-on. And the self-validating CONTRACTS rows are the part I liked most: the revm-bytecode 6.2.2 → 11.x variant-order flip makes old rows either fail to decode or silently decode into an EIP-7702 delegation to a garbage address, and both fixtures assert up front that they really do fail / really do silently succeed under the current layout. That's a real positive control, and old data dirs degrade to a one-time re-fetch instead of bricking.

Blocking

1. mega-evm is pinned to a branch. The lockfile holds today's rev, but any cargo update or force-push silently swaps the consensus execution engine, and tag pins are the convention everywhere else in this manifest (salt v1.0.5, reth v2.3.0). You note this yourself — flagging it as blocking rather than a note: needs megaeth-labs/mega-evm#365 merged and tagged first.

2. Withdrawals MPT verification is now a bespoke Linearizer (+433/−99) covered by 8 hand-built trie shapes. I walked the algorithm: emission order is monotonic across the leaf / extension / branch arms, split_at_prefix is right, the "sole survivor is an unwitnessed subtree" collapse guard fails closed, and the no-update pre-pass self-checking the same walk is a nice property. The motivation is real too — V2 sparse tries can't represent an extension whose child branch is absent, and the R2 witnesses are immutable. But this is consensus-critical and the only other coverage is 31 mainnet fixtures that all sit below block 15,412,411.

Before merge I'd want a randomized differential test: random storage trie → reference root from HashBuilder over the full state → prune to a minimal witness → random insert / overwrite / remove / remove-absent update set → assert compute_root matches the reference, or returns TrieOperationFailed when the witness can't prove it. Never a plausible-but-wrong root.

Two smaller ones in the same file:

  • compute_root returns Ok(EMPTY_ROOT_HASH) when the root node is absent. That's fail-open in shape — it's only safe because the caller always runs the pre-state pass first. Err(trie_error("witness lacks the root node")) says what happened and doesn't owe a debt to call order.
  • The pre-state pass runs on unauthenticated witness bytes, and HashBuilder::add_branch is an assert! on key ordering (alloy-trie 0.9.5, src/hash_builder/mod.rs:164). I believe it's unreachable for any decodable witness because the emitted keys are structurally monotonic, but that argument should be written down in verify's doc. Separately emit_subtree recurses and an empty-key extension consumes no depth, so a large enough malformed witness could blow the stack — fine to just note if witness size is bounded upstream.

3. Conflicting, and the conflict has semantics. main has since landed the R2 custom-domain / h2 / --r2-connections work that crates/stateless-r2/Cargo.toml here still says to "revisit alongside". The rebase has to re-settle reqwest 0.13's renamed features (rustls-tlsrustls), http2 now being opt-in, and whether stateless-common's http1_only pin still holds under it. Full suite needs re-running after the rebase — the 15-day-old CI doesn't cover any of that.

Needs evidence (otherwise blocking)

4. The 0x-prefix strip shim on struct-logger returnValue is gone, on the grounds that post-upgrade mega-reth serializes the prefix too. That's a consumer-visible wire change: if production mega-reth is still on alloy 1.x, the trace server diverges from the node it mirrors. Please paste a same-tx raw-JSON comparison. The return type also went from GethTrace::JS(frame_value) back to typed GethTrace::from(DefaultFrame), so the diff surface is wider than returnValue ("refund": 0, memory-word prefixes) — cover those in the same run.

5. result.gas_used()result.tx_gas_used() lands in callTracer / struct-logger gasUsed. "Equal with EIP-8037 pinned off" needs a source (the two revm 40 definitions) or a measured diff, not just the assertion.

6. Preceding txs now replay under TracingInspectorConfig::none(). I believe inspectors don't affect execution semantics, but it's a hot-path change riding a perf cleanup — one "same block, same tx, byte-identical trace before/after" run closes it.

Non-blocking

  • write_add_contracts still accepts a (hash, bytecode) pair whose hash doesn't match — hash_mismatched_contract_row_is_a_miss relies on exactly that to plant the row. The read side catches it, but the write site is where the invariant belongs: debug_assert_eq!(bytecode.hash_slow(), *hash).
  • hash_slow() on every disk hit — the in-memory tier should absorb it, but it's a per-block path; worth a number.
  • rust-version 1.95 → 1.94 in AGENTS.md reads like an MSRV regression until you notice Cargo.toml went 1.91 → 1.94. One line in the body would help.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants