refactor(l1): remove keys dependency from ExecutionWitness#6356
refactor(l1): remove keys dependency from ExecutionWitness#6356avilagaston9 merged 6 commits intomainfrom
Conversation
to be keyed by H256 (keccak256 of address) instead of Address. Replace the keys-based loop in execution_witness_from_rpc_chain_config with a recursive state trie walker that discovers accounts and their storage roots directly from the trie leaves. This removes the dependency on the keys field which is being phased out of the RPC spec. Keep keys in RpcExecutionWitness with #[serde(default)] for interoperability but always emit it empty. Update GuestProgramState fields (storage_tries, verified_storage_roots, account_hashes_by_address) to use H256 keys consistently.
🤖 Kimi Code ReviewReview SummaryThis PR removes the Issues Found1. Critical Bug in
|
🤖 Codex Code Review
No obvious EVM opcode/gas-accounting regressions were introduced in this diff, and the shift to hashed-address keyed storage tries is internally consistent across producer/consumer paths. Automated review by OpenAI Codex · custom prompt |
🤖 Claude Code ReviewNow I have a thorough understanding of the code. Let me produce the review. Code Review: PR #6356 — Remove
|
…iggers so that changes to the witness type definition or RPC endpoint also run L2 tests
🤖 Kimi Code ReviewReview SummaryThe PR removes the Issues Found1. Potential Bug in
|
Lines of code reportTotal lines added: Detailed view |
🤖 Codex Code Review
Overall, the refactor direction (moving away from RPC Automated review by OpenAI Codex · custom prompt |
🤖 Claude Code ReviewHere is the review: Code Review: PR #6356 —
|
Greptile SummaryThis PR removes the Key changes:
Confidence Score: 1/5
|
| Filename | Overview |
|---|---|
| crates/networking/rpc/debug/execution_witness.rs | Replaces keys-based account discovery with a recursive state-trie walker, but the walker is initialized with Nibbles::from_bytes(&[]) which inserts a spurious leaf-terminator nibble causing all path length checks to fail — no storage trie roots are ever discovered. |
| crates/common/types/block_execution_witness.rs | Correctly migrates storage_tries, verified_storage_roots, and account_hashes_by_address from Address/Vec keys to H256; copy-semantics fix for hashed_address avoids borrow-checker conflicts; hash_address return type simplified to H256. |
| crates/blockchain/blockchain.rs | Removes the redundant keys Vec construction and replaces Address-keyed storage_trie_roots with H256 (hashed address) keys; logic is symmetric across both witness-generation functions. |
| .github/workflows/pr-main_l2.yaml | Adds the two changed source files to the L2 CI trigger list so the workflow runs on relevant file changes. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["execution_witness_from_rpc_chain_config(rpc_witness, ...)"] --> B["Build nodes: BTreeMap<H256, Node>"]
B --> C["get_embedded_root → state_trie_root: Option<Node>"]
C --> D{"state_trie_root\npresent?"}
D -- "Yes" --> E["collect_accounts_from_node(root, Nibbles::from_bytes(&[]), accounts)\n⚠️ initial path has stray leaf flag 16"]
D -- "No" --> H["storage_trie_roots = empty"]
E --> F["Walk Branch/Extension/Leaf nodes recursively"]
F --> G{"leaf path\nto_bytes().len()\n== 32?"}
G -- "NEVER TRUE\n(always 33 due to bug)" --> F
G -- "Should be true" --> I["Push (hashed_address, storage_root)"]
I --> J["Lookup storage root in nodes map"]
J --> K["Insert into storage_trie_roots"]
K --> L["Return ExecutionWitness { storage_trie_roots }"]
H --> L
style E fill:#f88,stroke:#c00
style G fill:#f88,stroke:#c00
Prompt To Fix All With AI
This is a comment left during a code review.
Path: crates/networking/rpc/debug/execution_witness.rs
Line: 71
Comment:
**Wrong initial `Nibbles` — trie walker never finds any account**
`Nibbles::from_bytes(&[])` calls `from_raw(&[], is_leaf=true)`, which appends a **leaf-terminator nibble `16`** to the data (see `nibbles.rs`). This means the initial path is `data = [16]` instead of `data = []`.
As the trie walk accumulates branch/extension nibbles via `append_new` and `concat`, that stray `16` ends up in the middle of the path. When a leaf is reached, `path.concat(&leaf.partial)` yields **65 nibbles** (1 spurious + 64 actual + 1 leaf flag), and after `to_bytes()` trims the trailing leaf flag and packs: ⌈65/2⌉ = **33 bytes**. The guard `if path_bytes.len() == 32` therefore **never** passes, no accounts are ever pushed, and `storage_trie_roots` is always empty.
This defeats the entire purpose of replacing the `keys`-based lookup and silently reproduces the same empty-storage-trie regression the PR was designed to fix.
The correct initial value is an empty nibble sequence with no leaf flag:
```suggestion
collect_accounts_from_node(state_trie_root, Nibbles::from_hex(vec![]), &mut accounts);
```
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: adf0711
| continue; // storage trie isn't relevant to this execution | ||
| if let Some(state_trie_root) = &state_trie_root { | ||
| let mut accounts = Vec::new(); | ||
| collect_accounts_from_node(state_trie_root, Nibbles::from_bytes(&[]), &mut accounts); |
There was a problem hiding this comment.
Wrong initial Nibbles — trie walker never finds any account
Nibbles::from_bytes(&[]) calls from_raw(&[], is_leaf=true), which appends a leaf-terminator nibble 16 to the data (see nibbles.rs). This means the initial path is data = [16] instead of data = [].
As the trie walk accumulates branch/extension nibbles via append_new and concat, that stray 16 ends up in the middle of the path. When a leaf is reached, path.concat(&leaf.partial) yields 65 nibbles (1 spurious + 64 actual + 1 leaf flag), and after to_bytes() trims the trailing leaf flag and packs: ⌈65/2⌉ = 33 bytes. The guard if path_bytes.len() == 32 therefore never passes, no accounts are ever pushed, and storage_trie_roots is always empty.
This defeats the entire purpose of replacing the keys-based lookup and silently reproduces the same empty-storage-trie regression the PR was designed to fix.
The correct initial value is an empty nibble sequence with no leaf flag:
| collect_accounts_from_node(state_trie_root, Nibbles::from_bytes(&[]), &mut accounts); | |
| collect_accounts_from_node(state_trie_root, Nibbles::from_hex(vec![]), &mut accounts); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/networking/rpc/debug/execution_witness.rs
Line: 71
Comment:
**Wrong initial `Nibbles` — trie walker never finds any account**
`Nibbles::from_bytes(&[])` calls `from_raw(&[], is_leaf=true)`, which appends a **leaf-terminator nibble `16`** to the data (see `nibbles.rs`). This means the initial path is `data = [16]` instead of `data = []`.
As the trie walk accumulates branch/extension nibbles via `append_new` and `concat`, that stray `16` ends up in the middle of the path. When a leaf is reached, `path.concat(&leaf.partial)` yields **65 nibbles** (1 spurious + 64 actual + 1 leaf flag), and after `to_bytes()` trims the trailing leaf flag and packs: ⌈65/2⌉ = **33 bytes**. The guard `if path_bytes.len() == 32` therefore **never** passes, no accounts are ever pushed, and `storage_trie_roots` is always empty.
This defeats the entire purpose of replacing the `keys`-based lookup and silently reproduces the same empty-storage-trie regression the PR was designed to fix.
The correct initial value is an empty nibble sequence with no leaf flag:
```suggestion
collect_accounts_from_node(state_trie_root, Nibbles::from_hex(vec![]), &mut accounts);
```
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Pull request overview
Refactors L1 execution witness handling to remove the deprecated keys dependency by deriving required storage trie roots directly from the embedded state trie, and aligns witness/state structures to key storage tries by keccak256(address) (H256).
Changes:
- Remove
keysfromExecutionWitnessand ensureRpcExecutionWitness.keysremains optional/defaulted but is always emitted empty. - Change
storage_trie_roots/ related guest state maps to be keyed by hashed address (H256) instead ofAddress. - Rebuild
storage_trie_rootsinexecution_witness_from_rpc_chain_configby walking the embedded state trie leaves to discover(hashed_address, storage_root)pairs.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
crates/networking/rpc/debug/execution_witness.rs |
Replaces keys-based reconstruction with a state-trie walker to find account storage roots. |
crates/common/types/block_execution_witness.rs |
Updates witness/guest-program state types to use H256 hashed-address keys; removes ExecutionWitness.keys; keeps RPC keys defaulted and always empty on output. |
crates/blockchain/blockchain.rs |
Stops building/emitting keys; stores storage_trie_roots keyed by hashed address (H256). |
.github/workflows/pr-main_l2.yaml |
Ensures L2 CI runs when execution-witness-related files change. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Node::Branch(branch) => { | ||
| for (i, child) in branch.choices.iter().enumerate() { | ||
| if let NodeRef::Node(child_node, _) = child { | ||
| collect_accounts_from_node(child_node, path.append_new(i as u8), accounts); | ||
| } | ||
| } |
| if let Ok(account_state) = AccountState::decode(&leaf.value) { | ||
| accounts.push((hashed_address, account_state.storage_root)); |
reconstructing storage_trie_roots from an RPC witness. Two bugs: (1) Nibbles::from_bytes(&[]) adds a leaf terminator flag, making all paths 33 bytes instead of 32 so no accounts were ever matched — use Nibbles::from_raw(&[], false) instead. (2) The walker only followed NodeRef::Node children, missing NodeRef::Hash references that weren't fully embedded by get_embedded_root — now resolves those via the flat nodes map.
leaf in collect_accounts_from_node, instead of silently skipping it. This aids diagnosis of corrupted or malformed witness data.
| Node::Leaf(leaf) => { | ||
| let full_path = path.concat(&leaf.partial); | ||
| let path_bytes = full_path.to_bytes(); | ||
| if path_bytes.len() == 32 { |
There was a problem hiding this comment.
nit: when path_bytes.len() != 32 the leaf is silently skipped. For consistency with the decode-failure log below (line 159), consider adding a debug! in the else branch — it would help diagnose corrupted or unexpected witness data:
} else {
debug!("Unexpected state trie leaf path length: {} (expected 32)", path_bytes.len());
}collect_accounts_from_node, instead of silently skipping it. This is consistent with the existing debug log for AccountState decode failures and helps diagnose corrupted or malformed witness data.
Update the zkevm execution-spec-tests fixtures from v0.2.0 to v0.3.0, which adds 117 new witness tests (bytecodes, headers, EIP-7702). Remove the is_zkevm / ZKEVM_AMSTERDAM_CONFIG workaround that was needed because zkevm v0.2.0 defined Amsterdam as Osaka + EIP-7928 only. Now that bal-devnet-3 is merged, all for_amsterdam tests run with the full Amsterdam config. Extract only the zkevm-specific eip8025_optional_proofs tests from the zkevm tarball instead of the entire for_amsterdam directory, avoiding overwrites of properly-filled amsterdam fixtures. Remove the keys workaround from run_stateless_from_fixture since PR #6356 replaced the keys-based storage trie discovery with a trie walker. Implement per-block stateless execution in run_stateless_from_fixture: each block is executed independently with its own executionWitness, matching the spec and how the guest program is designed to consume witnesses. Focus test-stateless-zkevm target on the 48 eip8025_optional_proofs witness tests rather than running the full test suite.
…ss#6356) ## Motivation The `keys` field in `ExecutionWitness` / `RpcExecutionWitness` is being phased out of the RPC spec. Our `execution_witness_from_rpc_chain_config` function (used by ethrex-replay) depended on `keys` to rebuild `storage_trie_roots`, which meant it would silently produce empty storage tries when `keys` was missing or empty. Follows up on lambdaclass#6338. ## Description - Remove `keys` from `ExecutionWitness`. Keep it in `RpcExecutionWitness` with `#[serde(default)]` for interoperability, but always emit it empty. - Change `storage_trie_roots` key from `Address` to `H256` (the keccak256 hash of the address). This aligns with how the state trie stores account paths. - Replace the `keys`-based loop in `execution_witness_from_rpc_chain_config` with a recursive state trie walker that discovers accounts and their storage roots directly from the trie leaves. - Update `GuestProgramState` fields (`storage_tries`, `verified_storage_roots`, `account_hashes_by_address`) to use `H256` keys consistently. ## How to Test - `cargo check --all-targets` - `cargo clippy --all-targets -- -D warnings` - Existing execution witness tests should pass unchanged ### End-to-end test with ethrex-replay 1. **Start an L1 dev node** (from the ethrex repo, on this branch): ```bash make -C crates/l2 init-l1 ``` 2. **Send a transaction** to produce a non-empty block: ```bash PRIVATE_KEY=0x850643a0224065ecce3882673c21f56bcf6eef86274cc21cadff15930b59fc8c rex send 0x0000000000000000000000000000000000000001 \ --value 1000000000000000000 \ -k $PRIVATE_KEY \ --rpc-url http://localhost:8545 ``` Note the block number from the receipt. 3. **Build ethrex-replay** (from [PR lambdaclass#68](lambdaclass/ethrex-replay#68)): ```bash cd ~/Documents/ethrex-replay git checkout fix/remove-keys-dependency cargo build --release ``` 4. **Run replay — no-zkvm path** (uses `Blockchain::add_block` directly): ```bash RUST_LOG=info ./target/release/ethrex-replay block <BLOCK_NUMBER> \ --rpc-url http://localhost:8545 \ --no-zkvm ``` 5. **Run replay — exec mode** (builds `ExecutionWitness` → `GuestProgramState`, runs guest program natively): ```bash RUST_LOG=info ./target/release/ethrex-replay block <BLOCK_NUMBER> \ --rpc-url http://localhost:8545 ``` Both should exit 0 with a successful replay.
Motivation
The
keysfield inExecutionWitness/RpcExecutionWitnessis being phased out of the RPC spec. Ourexecution_witness_from_rpc_chain_configfunction (used by ethrex-replay) depended onkeysto rebuildstorage_trie_roots, which meant it would silently produce empty storage tries whenkeyswas missing or empty.Follows up on #6338.
Description
keysfromExecutionWitness. Keep it inRpcExecutionWitnesswith#[serde(default)]for interoperability, but always emit it empty.storage_trie_rootskey fromAddresstoH256(the keccak256 hash of the address). This aligns with how the state trie stores account paths.keys-based loop inexecution_witness_from_rpc_chain_configwith a recursive state trie walker that discovers accounts and their storage roots directly from the trie leaves.GuestProgramStatefields (storage_tries,verified_storage_roots,account_hashes_by_address) to useH256keys consistently.How to Test
cargo check --all-targetscargo clippy --all-targets -- -D warningsEnd-to-end test with ethrex-replay
Start an L1 dev node (from the ethrex repo, on this branch):
Send a transaction to produce a non-empty block:
PRIVATE_KEY=0x850643a0224065ecce3882673c21f56bcf6eef86274cc21cadff15930b59fc8c rex send 0x0000000000000000000000000000000000000001 \ --value 1000000000000000000 \ -k $PRIVATE_KEY \ --rpc-url http://localhost:8545Note the block number from the receipt.
Build ethrex-replay (from PR #68):
Run replay — no-zkvm path (uses
Blockchain::add_blockdirectly):Run replay — exec mode (builds
ExecutionWitness→GuestProgramState, runs guest program natively):Both should exit 0 with a successful replay.