fix(l1): close EIP-8025 stateless witness validation gaps (re-enable 9 zkevm tests)#6541
Conversation
so the resulting list satisfies `headers[i].parent_hash == keccak(headers[i-1])`, matching the EELS contract for `validate_headers`. The generator walked the chain backward (newest -> oldest), so reverse the byte list before returning. This is a no-op for current consumers because the headers are stored in a `BTreeMap<u64, BlockHeader>` keyed by number, but it makes the witness valid for any spec-conformant stateless verifier and is a precondition for adding the EIP-8025 contiguity check on the consumer side. Reference: https://github.com/ethereum/execution-specs/blob/projects/zkevm/src/ethereum/forks/amsterdam/stateless.py#L171-L191
…teness: - Tolerance: when decoding state nodes and ancestor headers in `into_execution_witness` and `from_witness`, drop entries that fail to RLP-decode instead of failing the whole conversion. A bad/extra entry cannot be looked up by hash anyway; if the trie walk or BLOCKHASH path actually requires the dropped entry, the lookup fails explicitly there. Mirrors EELS `witness_state.build_node_db` and geth `MakeHashDB`, which both store entries keyed by hash without pre-validation. - Header chain contiguity: in `from_witness`, walk the header byte list in order and reject when `headers[i].parent_hash != keccak(headers[i-1])`. A reordered or fragmented header chain is not a valid witness even if the by-number lookup would otherwise resolve to the right header. Mirrors EELS `stateless.validate_headers`. Malformed entries are treated as a chain break (subsequent headers won't satisfy the parent_hash check), preserving the tolerance behavior for blocks that do not actually request the bad header. - Codes completeness: `get_account_code` and `get_code_metadata` now error on missing bytecode instead of silently defaulting to empty code. EIP-8025 mandates that a stateless executor reaching a code lookup whose hash is not in the witness treat the witness as incomplete and reject. Matches EELS `witness_state.get_code` (raises KeyError on miss) and geth's documented "bytecode lookup will error on junk" model. References: - https://github.com/ethereum/execution-specs/blob/projects/zkevm/src/ethereum/forks/amsterdam/witness_state.py#L37-L42 - https://github.com/ethereum/execution-specs/blob/projects/zkevm/src/ethereum/forks/amsterdam/witness_state.py#L204-L212 - https://github.com/ethereum/execution-specs/blob/projects/zkevm/src/ethereum/forks/amsterdam/stateless.py#L171-L191 - https://github.com/ethereum/go-ethereum/blob/master/core/stateless/database.go#L26-L67
…ed under `feature = "stateless"`. They now pass with the witness-consumer alignment (tolerance + contiguity + codes-completeness) and the generator-side ascending header order: - validation_state_extra_unused_trie_node - validation_headers_malformed_rlp_header - validation_headers_missing_oldest_blockhash_ancestor - validation_headers_missing_parent_header - validation_headers_non_contiguous_chain - validation_codes_missing_delegated_code_on_insufficient_balance_call - validation_codes_missing_external_code_read_target - validation_codes_missing_redelegation_old_marker - validation_codes_missing_sender_delegation_marker `make test-stateless`: 8720 passed / 0 failed / 0 ignored (~155 s). `make test-levm`: unchanged.
…; keep the spec link but drop the prose. No behavior change.
Lines of code reportTotal lines added: Detailed view |
…nversion comments — keep the prose and the spec links, just remove the symbol-plus-word labels. Restore the `This is an optimized path for EXTCODESIZE opcode.` doc line on `get_code_metadata` that was dropped while trimming. No behavior change.
…ader.parent_hash != expected_parent { ... } }` in `from_witness` into a single let-chain `if`. Required to satisfy `clippy::collapsible_if` under `-D warnings` on rust 1.91 — the CI Lint and Lint L2 jobs were failing on this. Behavior unchanged.
Greptile SummaryThis PR closes four EIP-8025 stateless-witness validation gaps, re-enabling 9 previously-skipped Confidence Score: 4/5Safe to merge; only P2 style issues found, core logic is correct and all targeted tests pass. All findings are P2 (a misleading comment and using crates/common/types/block_execution_witness.rs — comment accuracy and error variant usage in
|
| Filename | Overview |
|---|---|
| crates/common/types/block_execution_witness.rs | Four EIP-8025 changes: tolerance for malformed trie nodes/headers, contiguous-chain validation in from_witness, and hard errors on missing bytecode. Minor: a misleading comment on the malformed-entry path and a Custom error where the typed NoncontiguousBlockHeaders variant already exists. |
| crates/blockchain/blockchain.rs | Adds block_headers_bytes.reverse() in both generate_witness_for_blocks code paths so the emitted ancestor list is ascending, satisfying the new contiguity check in from_witness. |
| tooling/ef_tests/blockchain/tests/all.rs | Removes the stateless-specific skip list (9 tests) and simplifies to a single #[cfg(not(feature = "sp1"))] empty slice, re-enabling all formerly-skipped validation tests. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["RpcExecutionWitness\n(from RPC / fixture)"] -->|"into_execution_witness()"| B["ExecutionWitness\n(block_headers_bytes in ascending order)"]
GEN["generate_witness_for_blocks\n(walks chain backward)"] -->|"reverse() → ascending"| B
B -->|"from_witness()"| C{For each header bytes}
C -->|"decode fails"| D["prev_hash = None\nskip (tolerance)"]
D --> C
C -->|"decode ok\nprev_hash != None AND\nheader.parent_hash != prev_hash"| E["❌ NoncontiguousBlockHeaders\nerror"]
C -->|"decode ok\nchain link valid"| F["insert into block_headers\nprev_hash = keccak(bytes)"]
F --> C
C -->|"done"| G["GuestProgramState"]
G --> H["get_account_code(hash)"]
H -->|"hash missing in codes_hashed"| I["❌ Database error\n(was: silent empty code)"]
H -->|"hash found"| J["✅ Return Code"]
Prompt To Fix All With AI
This is a comment left during a code review.
Path: crates/common/types/block_execution_witness.rs
Line: 330-334
Comment:
**Misleading inline comment — check is skipped, not failed**
The comment says "the next parent_hash check fails," but when `prev_hash` is set to `None`, the subsequent `if let Some(expected_parent) = prev_hash` guard in the next iteration simply does not fire — the check is bypassed, not triggered-and-failed. A validator reading this could mistake the skipping for active rejection.
```suggestion
let Ok(header) = BlockHeader::decode(bytes.as_ref()) else {
// Malformed entry: skip it and reset the chain cursor so the
// next decodable header is accepted without a parent-hash check.
prev_hash = None;
continue;
};
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: crates/common/types/block_execution_witness.rs
Line: 337-340
Comment:
**Use the existing `NoncontiguousBlockHeaders` error variant**
There is already a typed variant `GuestProgramStateError::NoncontiguousBlockHeaders` (defined a few lines above) with the message "Non-contiguous block headers (there's a gap in the block headers list)". Using `Custom(...)` here bypasses that variant and makes error matching on the call-site harder.
```suggestion
{
return Err(GuestProgramStateError::NoncontiguousBlockHeaders);
}
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "Collapse the nested `if let Some(expecte..." | Re-trigger Greptile
| let Ok(header) = BlockHeader::decode(bytes.as_ref()) else { | ||
| // Malformed entry is a chain break; the next parent_hash check fails. | ||
| prev_hash = None; | ||
| continue; | ||
| }; |
There was a problem hiding this comment.
Misleading inline comment — check is skipped, not failed
The comment says "the next parent_hash check fails," but when prev_hash is set to None, the subsequent if let Some(expected_parent) = prev_hash guard in the next iteration simply does not fire — the check is bypassed, not triggered-and-failed. A validator reading this could mistake the skipping for active rejection.
| let Ok(header) = BlockHeader::decode(bytes.as_ref()) else { | |
| // Malformed entry is a chain break; the next parent_hash check fails. | |
| prev_hash = None; | |
| continue; | |
| }; | |
| let Ok(header) = BlockHeader::decode(bytes.as_ref()) else { | |
| // Malformed entry: skip it and reset the chain cursor so the | |
| // next decodable header is accepted without a parent-hash check. | |
| prev_hash = None; | |
| continue; | |
| }; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/common/types/block_execution_witness.rs
Line: 330-334
Comment:
**Misleading inline comment — check is skipped, not failed**
The comment says "the next parent_hash check fails," but when `prev_hash` is set to `None`, the subsequent `if let Some(expected_parent) = prev_hash` guard in the next iteration simply does not fire — the check is bypassed, not triggered-and-failed. A validator reading this could mistake the skipping for active rejection.
```suggestion
let Ok(header) = BlockHeader::decode(bytes.as_ref()) else {
// Malformed entry: skip it and reset the chain cursor so the
// next decodable header is accepted without a parent-hash check.
prev_hash = None;
continue;
};
```
How can I resolve this? If you propose a fix, please make it concise.| { | ||
| return Err(GuestProgramStateError::Custom( | ||
| "witness headers are not contiguous".to_string(), | ||
| )); |
There was a problem hiding this comment.
Use the existing
NoncontiguousBlockHeaders error variant
There is already a typed variant GuestProgramStateError::NoncontiguousBlockHeaders (defined a few lines above) with the message "Non-contiguous block headers (there's a gap in the block headers list)". Using Custom(...) here bypasses that variant and makes error matching on the call-site harder.
| { | |
| return Err(GuestProgramStateError::Custom( | |
| "witness headers are not contiguous".to_string(), | |
| )); | |
| { | |
| return Err(GuestProgramStateError::NoncontiguousBlockHeaders); | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/common/types/block_execution_witness.rs
Line: 337-340
Comment:
**Use the existing `NoncontiguousBlockHeaders` error variant**
There is already a typed variant `GuestProgramStateError::NoncontiguousBlockHeaders` (defined a few lines above) with the message "Non-contiguous block headers (there's a gap in the block headers list)". Using `Custom(...)` here bypasses that variant and makes error matching on the call-site harder.
```suggestion
{
return Err(GuestProgramStateError::NoncontiguousBlockHeaders);
}
```
How can I resolve this? If you propose a fix, please make it concise.
🤖 Claude Code ReviewNow I have a full picture of the PR. Here is my review: PR #6541 Review:
|
| Area | Verdict |
|---|---|
| Spec alignment (EELS / geth) | Correct |
| EF test re-enablement | Correct |
| Ascending header order fix | Correct |
| Contiguity check logic | Correct, comment misleading |
| Tolerance (drop malformed entries) | Correct |
| Codes completeness (error on miss) | Correct |
| Comment clarity | One inaccurate comment (Point 1) |
| Error variant consistency | Minor inconsistency (Point 4) |
The logic is sound and the spec alignment looks right. The main ask before merge is fixing the comment in the malformed-header branch — it currently says the next check "fails" when it is actually skipped.
Automated review by Claude (Anthropic) · sonnet · custom prompt
🤖 Codex Code Review
Assumption: I’m reading the EIP-8025 intent here as “tolerate unused malformed entries, but require the usable header chain itself to be contiguous”. Under that interpretation, Item 1 is still a real validation gap. I couldn’t run Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
Motivation
PR #6527 widened stateless coverage to all
for_amsterdamfixtures and skipped 9validation_*tests that exposed gaps in ethrex's witness handling. This PRcloses those gaps so the 9 tests pass.
Description
Four spec-aligned changes (refs to EELS
execution-specs@projects/zkevmandgeth
core/stateless/database.goare inline in the code):into_execution_witnessandfrom_witnessdrop entries thatfail RLP-decode instead of failing the whole conversion. Bad/extra entries
can't be looked up by hash; if execution requires one, the trie or BLOCKHASH
walk errors there.
from_witnessrejects whenheaders[i].parent_hash != keccak(headers[i-1]). Mirrors EELSvalidate_headers.get_account_code/get_code_metadataerror onmissing bytecode instead of silently returning empty code.
generate_witness_for_blocksnow emits ancestors inascending order (was descending). Required so ethrex-generated witnesses
satisfy the contiguity check above.
How to test
Expected: 8720 passed / 0 failed / 0 ignored, ~155 s.
test-levmunaffected.Checklist
STORE_SCHEMA_VERSION— N/A