Skip to content

refactor(l1): shared trie abstraction#6471

Draft
edg-l wants to merge 2 commits intomainfrom
shared-trie
Draft

refactor(l1): shared trie abstraction#6471
edg-l wants to merge 2 commits intomainfrom
shared-trie

Conversation

@edg-l
Copy link
Copy Markdown
Contributor

@edg-l edg-l commented Apr 14, 2026

Summary

Introduces a trie-agnostic abstraction layer so ethrex can support both MPT and a future binary trie (EIP-7864) without backend-specific types leaking across crate boundaries.

Spec and plan

What changed

New crates/modules:

  • `ethrex-state-backend`: StateReader, StateCommitter traits; MerkleOutput, NodeUpdates, AccountStateInfo, StateError
  • `MptBackend` in ethrex-trie: implements StateReader/StateCommitter (DB-backed + in-memory modes unified)
  • `MptMerkleizer` in ethrex-trie: 16-shard parallel merkleizer (extracted from blockchain.rs)
  • `StateBackend`/`Merkleizer` enums in ethrex-storage: enum dispatch to backends
  • `mpt_wiring.rs` in ethrex-storage: all MPT-specific wiring (trie opening, hash functions, genesis, proofs, FKV generator, snap sync, witness helpers)

Key architectural changes:

  • `store.rs` has zero MPT-specific code; all trie logic in `mpt_wiring.rs`
  • `blockchain.rs` has zero MPT imports (no Node, NodeRef, Trie, TrieLogger, mpt_hash_address, mpt_hash_key)
  • `vm.rs` has zero `StateBackend::Mpt` downcasts; reads through `StateReader` trait
  • Pipeline uses `Merkleizer::feed_updates`/`finalize` instead of inline `handle_merkleization`
  • State reads go through `StateBackend` via `Store::new_state_reader`
  • State writes go through `StateCommitter`; `commit()` returns `MerkleOutput`
  • Genesis uses `StateCommitter` trait methods (backend-agnostic)
  • Witness generation fully encapsulated in `StateBackend` methods (init_witness, record_witness_accesses, apply_updates_with_witness_state, advance_witness_to, finalize_witness)
  • `ExecutionWitness` is backend-agnostic: `state_proof: Vec<Vec>` replaces MPT-specific `Node` fields
  • `VmDatabase`/LEVM `Database` traits return `AccountStateInfo` (no `storage_root` leak)
  • `MptBackend` unified: no two-mode design (storage_opener/code_reader always present, no-op impls for genesis/witness)
  • SLOAD cache internalized in `MptBackend` via `Mutex`
  • `GuestProgramState` holds `MptBackend` instead of raw Trie fields
  • `AccountProof` uses `AccountInfo` + `storage_root` instead of `AccountState`
  • Dependency inversion: `ethrex-common` has no `ethrex-trie` dependency

Adding a new backend requires:

  1. Implement `StateReader`/`StateCommitter` traits
  2. Add enum arms to `StateBackend`, `Merkleizer`, `NodeUpdates`
  3. Add `*_wiring.rs` in ethrex-storage
  4. Zero changes to blockchain.rs, vm.rs, payload.rs, store.rs

Checklist

  • Break `ethrex-common` -> `ethrex-trie` dependency
  • Create `ethrex-state-backend` crate
  • Implement MptBackend + MptMerkleizer
  • Create StateBackend/Merkleizer enums
  • Migrate pipeline (write path)
  • Route read path through StateBackend
  • Purge all MPT code from store.rs
  • Wire genesis through StateCommitter
  • All 8675 EF blockchain tests pass
  • `cargo test --workspace` passes
  • Remove MPT downcasts from vm.rs
  • VmDatabase/Database traits return AccountStateInfo (not AccountState)
  • Extract witness generation from blockchain.rs into StateBackend
  • Make ExecutionWitness backend-agnostic (state_proof bytes)
  • Port GuestProgramState to MptBackend
  • Replace AccountProof.account with AccountInfo
  • Add backend guide documentation
  • Benchmark regression <= 2%

TODOs (follow-up PRs)

  • FKV generator abstraction (each backend provides its own; documented)
  • Snap sync abstraction (per-backend sync; documented in adding-a-backend.md)
  • Verify zkVM compilation (sp1, risc0 targets) with GuestProgramState changes

@github-actions github-actions bot added the L1 Ethereum client label Apr 14, 2026
@github-actions
Copy link
Copy Markdown

🤖 Kimi Code Review

This is a substantial and well-architected refactoring that introduces a clean abstraction layer (ethrex-state-backend) for supporting multiple state trie backends (MPT now, binary trie later). The design correctly isolates MPT-specific details while maintaining performance-critical paths.

Critical Issues:

  1. Error swallowing in MptBackend::hash() (`crates/common/trie/mpt_backend.rs:~520-530)
    The method silently ignores trie insertion errors when updating account storage roots:

    let _ = self
        .state_trie
        .insert(hashed.as_bytes().to_vec(), acc_state.encode_to_vec());

    If the underlying DB fails here, the method returns an incorrect state root. Change to propagate errors or panic explicitly on DB corruption.

  2. Inefficient O(n²) lookup in witness reconstruction (crates/storage/store.rs:~1950-1970) The nested findloop for reconstructingStorageTriesis quadratic in the number of updated accounts. Build aHashMap<H256, Address>` first:

    let addr_by_hash: HashMap<_, _> = account_updates
        .iter()
        .map(|u| (hash_address_fixed(&u.address), u.address))
        .collect();

Security/Consensus Considerations:

  1. RLP encoding compatibility (crates/common/trie/mpt_backend.rs:~70-110) MptAccountStateduplicatesethrex_common::types::AccountState` to avoid circular deps. Verify field ordering matches exactly:

    • nonce (u64)
    • balance (U256)
    • storage_root (H256)
    • code_hash (H256)

    Any deviation breaks consensus. Add a test asserting round-trip equality with the canonical type.

  2. Storage slot hashing (mpt_backend.rs:~350) Confirmed correct: keccak_hash(slot.as_bytes())` for 32-byte slot keys.

Performance Issues:

  1. Unnecessary bytecode cloning (mpt_backend.rs:~580, store.rs:~1800) CodeMutcontainsOption<Vec>and callers dobytes.clone(). For large contract deployments, this doubles memory. Consider using Arc<Vec>or havingCodeMuttake ownership viaVec` without cloning.

  2. HashMap iteration non-determinism (mpt_backend.rs:~430-440) dirty_accountsiteration order affects the sequence of storage trie updates (though not the final root). For debugging reproducibility, considerBTreeMap` or documenting that this is intentional.

Code Quality:

  1. Missing code_size validation (state-backend/src/lib.rs:~28) The code_sizefield inAccountMutis documented as "MUST be populated" but the MPT backend ignores it. Adddebug_assert!` or validation to catch caller errors early.

  2. Error type ergonomics (state-backend/src/lib.rs:~55-62) StateErrorusesString` variants which lose structured error context. Consider:

    #[error("trie error: {0}")]
    Trie(#[from] TrieError),

    (Requires making ethrex-trie a dependency or using Box<dyn Error>).

Documentation:

  1. Spec vs. Implementation mismatch
    The spec document mentions a Merkleizer trait for streaming, but this PR only implements StateCommitter. Clarify that Merkleizer is deferred to a follow-up PR.

Positive Notes:

  • The StorageTrieOpener trait correctly inverts dependencies between ethrex-trie and ethrex-storage.
  • LoggingStorageTrieOpener wrapper is a clean way to capture witnesses without polluting the core logic.
  • The separation of commit_full() vs commit_full_with_tries() provides good flexibility for witness extraction.
  • Genesis initialization using the new backend ensures code path unification.

Recommendation: Address the error swallowing in hash() before merging. The O(n²) lookup should be fixed if this path is used in hot consensus code (block execution), but is acceptable if only used in RPC/debug paths.


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

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 14, 2026

Lines of code report

Total lines added: 3705
Total lines removed: 3338
Total lines changed: 7043

Detailed view
+-----------------------------------------------------------------+-------+-------+
| File                                                            | Lines | Diff  |
+-----------------------------------------------------------------+-------+-------+
| ethrex/cmd/ethrex/cli.rs                                        | 985   | +1    |
+-----------------------------------------------------------------+-------+-------+
| ethrex/cmd/ethrex/l2/deployer.rs                                | 1533  | +1    |
+-----------------------------------------------------------------+-------+-------+
| ethrex/cmd/ethrex/utils.rs                                      | 167   | +1    |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/blockchain/blockchain.rs                          | 1313  | -1174 |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/blockchain/vm.rs                                  | 216   | +1    |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/common/common.rs                                  | 21    | -1    |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/common/config/networks.rs                         | 229   | +1    |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/common/rkyv_utils.rs                              | 221   | +30   |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/common/state-backend/src/lib.rs                   | 47    | +47   |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/common/trie/compute_roots.rs                      | 87    | +87   |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/common/trie/execution_witness.rs                  | 47    | +47   |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/common/trie/execution_witness_ext.rs              | 37    | +37   |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/common/trie/genesis_ext.rs                        | 99    | +99   |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/common/trie/guest_program_state.rs                | 364   | +364  |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/common/trie/mpt_backend.rs                        | 754   | +754  |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/common/trie/mpt_merkleizer.rs                     | 1053  | +1053 |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/common/trie/trie.rs                               | 561   | +18   |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/common/types/account.rs                           | 333   | +6    |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/common/types/block.rs                             | 862   | -91   |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/common/types/block_execution_witness.rs           | 26    | -544  |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/common/types/fork_id.rs                           | 215   | -322  |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/common/types/genesis.rs                           | 850   | -168  |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/common/types/transaction.rs                       | 3315  | -39   |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/common/validation.rs                              | 232   | -13   |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/guest-program/src/common/execution.rs             | 136   | +1    |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/guest-program/src/l1/program.rs                   | 175   | -2    |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/guest-program/src/l2/input.rs                     | 51    | -1    |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/l2/common/src/prover.rs                           | 28    | -1    |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/l2/networking/rpc/l2/execution_witness.rs         | 70    | -1    |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/networking/p2p/peer_handler.rs                    | 552   | +1    |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/networking/p2p/sync/snap_sync.rs                  | 1023  | +4    |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/networking/rpc/debug/execution_witness.rs         | 89    | -1    |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/networking/rpc/debug/execution_witness_by_hash.rs | 50    | -1    |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/networking/rpc/eth/account.rs                     | 260   | -1    |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/storage/error.rs                                  | 52    | +3    |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/storage/layering.rs                               | 203   | +2    |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/storage/lib.rs                                    | 17    | +2    |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/storage/merkleizer.rs                             | 52    | +52   |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/storage/mpt_wiring.rs                             | 879   | +879  |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/storage/state_backend.rs                          | 212   | +212  |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/storage/store.rs                                  | 1617  | -975  |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/vm/backends/levm/mod.rs                           | 2049  | +2    |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/vm/levm/src/account.rs                            | 87    | -1    |
+-----------------------------------------------------------------+-------+-------+
| ethrex/crates/vm/witness_db.rs                                  | 93    | -2    |
+-----------------------------------------------------------------+-------+-------+

@github-actions
Copy link
Copy Markdown

🤖 Claude Code Review

Now I have enough information to write a thorough review. Let me compile it.


PR Review: refactor(l1): shared trie abstraction (StateBackend + MptBackend)

This is a solid architectural refactor with clear goals and good documentation. The design correctly decouples trie internals from the storage layer and lays the groundwork for EIP-7864. That said, there are a few bugs and design issues worth addressing before this lands.


Critical Issues

1. StateCommitter::hash() signature suppresses errors, and the implementation silently discards them

crates/common/state-backend/src/lib.rs:137:

fn hash(&mut self) -> H256;

The infallible return type forces MptBackend::hash() to swallow trie errors. In mpt_backend.rs:680-688:

if let Ok(Some(encoded)) = self.state_trie.get(hashed.as_bytes())
    && let Ok(mut acc_state) = MptAccountState::decode(&encoded)
{
    acc_state.storage_root = new_storage_root;
    let _ = self.state_trie.insert(hashed.as_bytes().to_vec(), acc_state.encode_to_vec());
}

A DB error on get(), a decode failure, or an insert failure all silently no-op. The state trie returns an incorrect root without any indication of failure. This is a correctness bug: callers use hash() to check provisional state roots before full commit.

The signature should be fn hash(&mut self) -> Result<H256, StateError>, or the error propagation path must be redesigned. Given this affects block processing correctness, this should be fixed before merge.


Medium Issues

2. commit_full and commit_full_with_tries are near-identical copy-paste

mpt_backend.rs:333-498. The two methods share roughly 50 lines of duplicated logic (the dirty-account iteration, storage trie hashing, state trie insertion loop). commit_full should simply delegate:

pub fn commit_full(self) -> Result<MptCommitOutput, StateError> {
    self.commit_full_with_tries().map(|(output, _)| output)
}

3. Parallel arrays in update_accounts silently truncate on length mismatch

state-backend/src/lib.rs:134:

fn update_accounts(&mut self, addrs: &[Address], muts: &[AccountMut]) -> Result<(), StateError>;

The implementation at mpt_backend.rs:581 uses .zip() which will silently ignore tail elements if the slices have different lengths. This is a footgun — use Vec<(Address, AccountMut)> or assert addrs.len() == muts.len() at the top of the implementation. The current call sites always pass matching lengths, but the API permits silent data loss.

4. StoreTrieOpener::open_storage_trie opens a new read transaction per call

crates/storage/trie.rs:1527-1530:

let read_view = self.backend.begin_read()...;

The original code reused a single read transaction across all storage trie accesses within a block. Each call to open_storage_trie now starts a fresh read transaction. For backends like LMDB, this significantly increases overhead — each begin_read acquires a reader slot and potentially takes a lock. In a block with 100+ accounts touching storage, this is a performance regression. Consider capturing a StorageReadView in StoreTrieOpener at construction time (the snapshot semantics are already consistent since we want to read from the pre-block state).

5. Reverse-lookup O(n²) in apply_account_updates_from_trie_with_witness

store.rs:1242-1255:

for (hashed_addr, trie) in returned_tries {
    if let Some((addr, witness)) = preloaded_witnesses
        .iter()
        .find(|(a, _)| hash_address_fixed(a) == hashed_addr)  // O(n) per hashed_addr

Build a HashMap<H256, Address> from preloaded addresses during the drain loop instead:

let mut hashed_to_addr: HashMap<H256, Address> = HashMap::new();
for (addr, (witness, trie)) in storage_tries.drain() {
    let hashed = hash_address_fixed(&addr);
    backend.preload_storage_trie(hashed, trie);
    preloaded_witnesses.insert(addr, witness);
    hashed_to_addr.insert(hashed, addr);
}

Low-Priority / Design Observations

6. get_or_open_storage_trie does a double lookup

mpt_backend.rs:419-423:

if !self.storage_tries.contains_key(&hashed) {
    let trie = self.trie_opener.open_storage_trie(hashed, storage_root)?;
    self.storage_tries.insert(hashed, trie);
}
Ok(self.storage_tries.get_mut(&hashed).expect("just inserted"))

Use the Entry API:

Ok(match self.storage_tries.entry(hashed) {
    Entry::Occupied(e) => e.into_mut(),
    Entry::Vacant(e) => {
        let trie = self.trie_opener.open_storage_trie(hashed, storage_root)?;
        e.insert(trie)
    }
})

7. CommitOutput.storage_roots is an MPT-specific concept in a backend-agnostic type

state-backend/src/lib.rs:120-125:

pub struct CommitOutput {
    pub root: H256,
    /// Per-account (post, pre) storage roots. Populated only by MPT;
    /// empty map for single-tree backends.
    pub storage_roots: HashMap<Address, (H256, H256)>,
}

The comment "Populated only by MPT" signals that this field has leaked an MPT-specific concept into the shared abstraction. Either move storage_roots to MptCommitOutput only (it's already there anyway) or accept that callers of CommitOutput who care about storage roots must downcast to MptCommitOutput. The current design with a half-populated CommitOutput is confusing.

8. MptCommitOutput.code_updates is populated but unused in apply_account_updates_batch

store.rs:1008-1012: The store builds a local code_updates: Vec<(H256, Code)> list and also passes raw bytes into backend.pending_code via CodeMut. After commit_full(), output.code_updates (the raw bytes) is discarded in favour of the local list. This means pending_code / MptCommitOutput.code_updates is dead weight in that call path. Either drop the CodeMut passing at line 1012 for this call site, or unify the two paths.

9. Test InMemoryStorageOpener uses a single shared BTreeMap

mpt_backend.rs:711-732: All storage tries opened by InMemoryStorageOpener share the same underlying BTreeMap. Two different addresses whose storage nodes happen to produce the same Keccak hash would collide at the node level. In practice, unit tests are unlikely to hit this, but using a separate DB per address (keyed by hashed_address) would make the test helper more faithful to the real implementation and avoid potential interference.

10. code_size: 0 is hardcoded everywhere

AccountMut.code_size is always zero across all call sites in this PR. The field is documented as "for binary trie (PR 2)". This is fine for now, but it's worth adding a #[allow(dead_code)] or a comment at each construction site so future readers aren't confused about whether the 0 is intentional or an oversight.


Positive Observations

  • The spec in docs/shared-trie/spec.md is thorough and clearly articulates the design rationale, dependency rules, and migration plan. This kind of documentation significantly lowers the risk of future regressions.
  • StorageTrieOpener is a clean dependency-inversion point. The LoggingStorageTrieOpener composition pattern for witness generation is elegant.
  • The dirty_accounts tracking correctly captures the pre-block storage root even when clear_storageupdate_accounts ordering is used at call sites.
  • The 5 unit tests cover the core happy-path scenarios well (round-trip, deletion, empty hash, commit output).

Summary

The two issues that should block merge (or be explicitly acknowledged) are Point 1 (silent error discard in hash()) and Point 4 (per-call read transaction in StoreTrieOpener). The others are good-to-have cleanups for code quality.


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

@github-actions
Copy link
Copy Markdown

🤖 Codex Code Review

  1. removed_storage now drops preloaded storage witnesses for selfdestruct/recreate cases. In apply_account_updates_from_trie_with_witness, preloaded logger-wrapped tries are drained into MptBackend (store.rs:1956); clear_storage then removes that cached trie (mpt_backend.rs:265); and reconstruction only walks returned_tries from commit_full_with_tries (store.rs:2029). That means any storage trie that was accessed for witness purposes and then cleared is omitted from storage_tries_after_update, so the final execution witness can miss the pre-state storage nodes for touched selfdestructed accounts.

  2. The new batched committer API can silently drop mutations on length mismatch. StateCommitter::update_accounts takes parallel slices (lib.rs:47), but the implementation just does addrs.iter().zip(muts.iter()) (mpt_backend.rs:414). If a caller passes mismatched lengths, the tail is ignored with no error, which is a bad failure mode for consensus-critical state transitions. This should hard-fail or at least assert.

  3. MptBackend::hash() suppresses trie/decoding/write failures and still returns a root. The new trait makes hash() part of the public backend surface (lib.rs:50), but the implementation ignores failures from state_trie.get, RLP decode, and insert via if let Ok(...) / let _ = ... (mpt_backend.rs:513, mpt_backend.rs:517). On corruption or DB errors, callers can get a root computed from partially updated state with no signal that anything went wrong.

I couldn’t run the Rust tests in this environment because cargo needs network/write access for dependency/toolchain state.


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

@edg-l edg-l force-pushed the shared-trie branch 2 times, most recently from 1c2396c to 2c412a4 Compare April 14, 2026 12:05
@edg-l edg-l moved this to In Progress in ethrex_l1 Apr 14, 2026
@edg-l edg-l force-pushed the shared-trie branch 3 times, most recently from 6761248 to e870154 Compare April 14, 2026 14:32
@edg-l edg-l changed the title refactor(l1): shared trie abstraction (StateBackend + MptBackend) refactor(l1): shared trie abstraction Apr 14, 2026
@edg-l edg-l force-pushed the shared-trie branch 7 times, most recently from 0ce1702 to e6061f8 Compare April 16, 2026 15:09
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 16, 2026

Benchmark Results Comparison

No significant difference was registered for any benchmark run.

Detailed Results

Benchmark Results: BubbleSort

Command Mean [s] Min [s] Max [s] Relative
main_revm_BubbleSort 2.834 ± 0.033 2.813 2.918 1.11 ± 0.01
main_levm_BubbleSort 2.566 ± 0.040 2.538 2.675 1.01 ± 0.02
pr_revm_BubbleSort 2.833 ± 0.013 2.820 2.858 1.11 ± 0.01
pr_levm_BubbleSort 2.544 ± 0.007 2.536 2.560 1.00

Benchmark Results: ERC20Approval

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Approval 912.0 ± 8.6 903.0 930.7 1.00
main_levm_ERC20Approval 1003.7 ± 17.9 975.0 1032.8 1.10 ± 0.02
pr_revm_ERC20Approval 914.7 ± 12.5 900.1 939.5 1.00 ± 0.02
pr_levm_ERC20Approval 996.1 ± 15.4 977.3 1018.2 1.09 ± 0.02

Benchmark Results: ERC20Mint

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Mint 120.6 ± 1.4 119.6 124.3 1.00 ± 0.02
main_levm_ERC20Mint 143.9 ± 1.1 142.6 145.5 1.19 ± 0.02
pr_revm_ERC20Mint 120.5 ± 1.4 119.6 124.1 1.00
pr_levm_ERC20Mint 144.2 ± 2.0 142.7 148.2 1.20 ± 0.02

Benchmark Results: ERC20Transfer

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ERC20Transfer 213.4 ± 1.7 212.0 217.3 1.00 ± 0.01
main_levm_ERC20Transfer 247.8 ± 4.8 241.6 254.7 1.16 ± 0.03
pr_revm_ERC20Transfer 213.4 ± 2.2 212.3 219.4 1.00
pr_levm_ERC20Transfer 247.2 ± 4.5 240.1 253.4 1.16 ± 0.02

Benchmark Results: Factorial

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Factorial 185.7 ± 2.0 183.6 189.2 1.01 ± 0.01
main_levm_Factorial 210.8 ± 4.1 206.9 217.2 1.14 ± 0.02
pr_revm_Factorial 184.3 ± 1.1 182.7 186.3 1.00
pr_levm_Factorial 210.8 ± 4.8 207.3 223.1 1.14 ± 0.03

Benchmark Results: FactorialRecursive

Command Mean [s] Min [s] Max [s] Relative
main_revm_FactorialRecursive 1.304 ± 0.028 1.278 1.364 1.00
main_levm_FactorialRecursive 7.063 ± 0.036 7.013 7.123 5.41 ± 0.12
pr_revm_FactorialRecursive 1.323 ± 0.029 1.280 1.354 1.01 ± 0.03
pr_levm_FactorialRecursive 7.081 ± 0.046 6.999 7.144 5.43 ± 0.12

Benchmark Results: Fibonacci

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Fibonacci 153.2 ± 0.7 152.5 154.6 1.00
main_levm_Fibonacci 185.5 ± 0.8 184.6 187.2 1.21 ± 0.01
pr_revm_Fibonacci 154.5 ± 4.9 152.0 167.5 1.01 ± 0.03
pr_levm_Fibonacci 185.5 ± 1.5 184.2 189.2 1.21 ± 0.01

Benchmark Results: FibonacciRecursive

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_FibonacciRecursive 706.9 ± 7.6 697.4 725.7 1.22 ± 0.02
main_levm_FibonacciRecursive 577.4 ± 4.0 571.3 584.8 1.00
pr_revm_FibonacciRecursive 708.0 ± 14.8 691.1 741.5 1.23 ± 0.03
pr_levm_FibonacciRecursive 577.5 ± 7.0 571.7 595.9 1.00 ± 0.01

Benchmark Results: ManyHashes

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_ManyHashes 8.3 ± 0.3 7.9 9.0 1.06 ± 0.05
main_levm_ManyHashes 10.1 ± 0.7 9.3 11.4 1.29 ± 0.09
pr_revm_ManyHashes 7.8 ± 0.1 7.7 8.0 1.00
pr_levm_ManyHashes 9.2 ± 0.2 9.0 9.5 1.18 ± 0.03

Benchmark Results: MstoreBench

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_MstoreBench 259.4 ± 2.0 257.8 264.6 1.23 ± 0.01
main_levm_MstoreBench 210.8 ± 1.6 208.0 212.3 1.00
pr_revm_MstoreBench 258.9 ± 0.6 257.9 259.7 1.23 ± 0.01
pr_levm_MstoreBench 211.9 ± 2.0 209.2 215.2 1.01 ± 0.01

Benchmark Results: Push

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_Push 243.5 ± 3.8 240.9 252.9 1.01 ± 0.02
main_levm_Push 240.9 ± 4.1 237.4 251.3 1.00
pr_revm_Push 242.0 ± 2.0 240.7 247.6 1.00 ± 0.02
pr_levm_Push 242.2 ± 5.7 238.1 254.4 1.01 ± 0.03

Benchmark Results: SstoreBench_no_opt

Command Mean [ms] Min [ms] Max [ms] Relative
main_revm_SstoreBench_no_opt 143.5 ± 2.1 142.1 149.1 1.54 ± 0.03
main_levm_SstoreBench_no_opt 93.2 ± 1.0 92.4 95.8 1.00
pr_revm_SstoreBench_no_opt 143.3 ± 2.0 141.8 148.7 1.54 ± 0.03
pr_levm_SstoreBench_no_opt 93.2 ± 0.9 92.5 95.4 1.00 ± 0.01

…6486)

## Summary
- The Hive consume-engine Amsterdam tests for EIP-7778 and EIP-8037 were
failing because ethrex's per-tx gas limit checks were incompatible with
Amsterdam's new gas accounting rules.
- **EIP-7778** uses pre-refund gas for block accounting, so cumulative
pre-refund gas can exceed the block gas limit even when a block builder
correctly included all transactions.
- **EIP-8037** introduces 2D gas accounting (`block_gas = max(regular,
state)`), meaning cumulative total gas (regular + state) can legally
exceed the block gas limit.
- The fix skips the per-tx cumulative gas check for Amsterdam and adds a
**post-execution** block-level overflow check using `max(sum_regular,
sum_state)` in all three execution paths (sequential, pipeline,
parallel).

## Local test results
- **200/201** EIP-7778 + EIP-8037 Hive consume-engine tests pass
- **105/105** EIP-7778 + EIP-8037 EF blockchain tests pass (4 + 101)
- The single remaining Hive failure
(`test_block_regular_gas_limit[exceed=True]`) expects
`TransactionException.GAS_ALLOWANCE_EXCEEDED` but we return
`BlockException.GAS_USED_OVERFLOW` — the block is correctly rejected,
just with a different error classification.

## Test plan
- [x] All EIP-7778 EF blockchain tests pass locally
- [x] All EIP-8037 EF blockchain tests pass locally
- [x] 200/201 Hive consume-engine Amsterdam tests pass locally
- [ ] Full CI Amsterdam Hive suite passes

---------

Co-authored-by: Claude Sonnet 4.5 <[email protected]>
@edg-l edg-l force-pushed the shared-trie branch 2 times, most recently from 5657628 to 7acb4ac Compare April 17, 2026 14:56
Trie-agnostic abstraction layer so ethrex can support both MPT and
a future binary trie (EIP-7864) without backend-specific types leaking
across crate boundaries. Enum dispatch, not dyn. Adding a new backend:
implement traits, add enum arms, add wiring module, zero changes to
shared code.

MptBackend::update_accounts with account=None drops storage_tries and
storage_root_cache entries, preventing flush_storage_roots and commit
from resurrecting removed accounts with a default AccountState on
SELFDESTRUCT. flush_storage_roots and commit debug_assert the invariant.
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 Progress

Development

Successfully merging this pull request may close these issues.

2 participants