-
Notifications
You must be signed in to change notification settings - Fork 207
fix(l1): close EIP-8025 stateless witness validation gaps (re-enable 9 zkevm tests) #6541
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
avilagaston9
wants to merge
14
commits into
main
Choose a base branch
from
fix/l1-stateless-witness-validation-gaps
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+196
−127
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
9d48e68
Bump the zkevm EF-tests fixture pin from v0.3.0 to v0.3.3 (same bal@v…
c10ae6b
Emit execution-witness ancestor headers in ascending block-number order
4a4a696
Align execution-witness consumer with EIP-8025 §Tolerance and §Comple…
3ecf85c
Re-enable the 9 EIP-8025 stateless validation_* tests that were skipp…
e72f5ea
Trim verbose comments in the witness conversion and generator changes…
22d4e39
Drop the §Tolerance/§Completeness section markers from the witness-co…
3de476f
Collapse the nested `if let Some(expected_parent) = prev_hash { if he…
b531dab
Reject malformed headers and non-contiguous chains in `from_witness` …
9e39377
Format the missing-bytecode hash via `{code_hash:x}` instead of `hex:…
29c80c2
Extract the chain-walk + reverse logic from `generate_witness_for_blo…
3fe0dfc
Merge origin/main into fix/l1-stateless-witness-validation-gaps. Brin…
0f78f21
Use `checked_sub(1)` for `current_number` in `build_ascending_ancesto…
f9f0fdc
Replace the `windows(2).all(|w| w[0] < w[1])` assertion in `generated…
ff37e7d
Restore the cross-client `0x80` (RLP-null) explainer in the `into_exe…
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -151,36 +151,31 @@ impl RpcExecutionWitness { | |
| )); | ||
| } | ||
|
|
||
| let mut initial_state_root = None; | ||
|
|
||
| for h in &self.headers { | ||
| let header = BlockHeader::decode(h)?; | ||
| if header.number == first_block_number - 1 { | ||
| initial_state_root = Some(header.state_root); | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| let initial_state_root = initial_state_root.ok_or_else(|| { | ||
| GuestProgramStateError::Custom(format!( | ||
| "header for block {} not found", | ||
| first_block_number - 1 | ||
| )) | ||
| })?; | ||
| // Local lookup only — malformed entries are still carried into | ||
| // `block_headers_bytes` and will be rejected by `from_witness`. | ||
| let initial_state_root = self | ||
| .headers | ||
| .iter() | ||
| .filter_map(|h| BlockHeader::decode(h).ok()) | ||
| .find(|header| header.number == first_block_number - 1) | ||
| .map(|header| header.state_root) | ||
| .ok_or_else(|| { | ||
| GuestProgramStateError::Custom(format!( | ||
| "header for block {} not found", | ||
| first_block_number - 1 | ||
| )) | ||
| })?; | ||
|
|
||
| // EIP-8025: drop undecodable entries (e.g. RLP-null `0x80` from some clients). | ||
| // Ref: https://github.com/ethereum/execution-specs/blob/projects/zkevm/src/ethereum/forks/amsterdam/witness_state.py#L37-L42 | ||
| let nodes: BTreeMap<H256, Node> = self | ||
| .state | ||
| .into_iter() | ||
| .filter_map(|b| { | ||
| if b == Bytes::from_static(&[0x80]) { | ||
| // other implementations of debug_executionWitness allow for a `Null` node, | ||
| // which would fail to decode in ours | ||
| return None; | ||
| } | ||
| let hash = keccak(&b); | ||
| Some(Node::decode(&b).map(|node| (hash, node))) | ||
| let node = Node::decode(&b).ok()?; | ||
| Some((keccak(&b), node)) | ||
| }) | ||
| .collect::<Result<_, RLPDecodeError>>()?; | ||
| .collect(); | ||
|
|
||
| // get state trie root and embed the rest of the trie into it | ||
| let state_trie_root = if let NodeRef::Node(state_trie_root, _) = | ||
|
|
@@ -325,17 +320,20 @@ impl GuestProgramState { | |
| value: ExecutionWitness, | ||
| crypto: &dyn Crypto, | ||
| ) -> Result<Self, GuestProgramStateError> { | ||
| let block_headers: BTreeMap<u64, BlockHeader> = value | ||
| .block_headers_bytes | ||
| .into_iter() | ||
| .map(|bytes| BlockHeader::decode(bytes.as_ref())) | ||
| .collect::<Result<Vec<_>, _>>() | ||
| .map_err(|e| { | ||
| GuestProgramStateError::Custom(format!("Failed to decode block headers: {}", e)) | ||
| })? | ||
| .into_iter() | ||
| .map(|header| (header.number, header)) | ||
| .collect(); | ||
| // Headers must decode and form a contiguous chain in list order. | ||
| // Ref: https://github.com/ethereum/execution-specs/blob/projects/zkevm/src/ethereum/forks/amsterdam/stateless.py#L171-L191 | ||
| let mut block_headers: BTreeMap<u64, BlockHeader> = BTreeMap::new(); | ||
| let mut prev_hash: Option<H256> = None; | ||
| for bytes in &value.block_headers_bytes { | ||
| let header = BlockHeader::decode(bytes.as_ref())?; | ||
| if let Some(expected_parent) = prev_hash | ||
| && header.parent_hash != expected_parent | ||
| { | ||
| return Err(GuestProgramStateError::NoncontiguousBlockHeaders); | ||
|
Comment on lines
+323
to
+332
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stale — |
||
| } | ||
| prev_hash = Some(H256(crypto.keccak256(bytes))); | ||
| block_headers.insert(header.number, header); | ||
| } | ||
|
|
||
| let parent_number = | ||
| value | ||
|
|
@@ -588,28 +586,21 @@ impl GuestProgramState { | |
| Ok(self.chain_config) | ||
| } | ||
|
|
||
| /// Retrieves the account code for a specific account. | ||
| /// Returns an Err if the code is not found. | ||
| /// Retrieves bytecode by code hash. Errors if missing — EIP-8025. | ||
| /// | ||
| /// Ref: https://github.com/ethereum/execution-specs/blob/projects/zkevm/src/ethereum/forks/amsterdam/witness_state.py#L204-L212 | ||
| pub fn get_account_code(&self, code_hash: H256) -> Result<Code, GuestProgramStateError> { | ||
| if code_hash == *EMPTY_KECCACK_HASH { | ||
| return Ok(Code::default()); | ||
| } | ||
| match self.codes_hashed.get(&code_hash) { | ||
| Some(code) => Ok(code.clone()), | ||
| None => { | ||
| // We do this because what usually happens is that the Witness doesn't have the code we asked for but it is because it isn't relevant for that particular case. | ||
| // In client implementations there are differences and it's natural for some clients to access more/less information in some edge cases. | ||
| // Sidenote: logger doesn't work inside SP1, that's why we use println! | ||
| println!( | ||
| "Missing bytecode for hash {} in witness. Defaulting to empty code.", // If there's a state root mismatch and this prints we have to see if it's the cause or not. | ||
| hex::encode(code_hash) | ||
| ); | ||
| Ok(Code::default()) | ||
| } | ||
| } | ||
| self.codes_hashed.get(&code_hash).cloned().ok_or_else(|| { | ||
| GuestProgramStateError::Database(format!( | ||
| "missing bytecode for hash {code_hash:x} in witness" | ||
| )) | ||
| }) | ||
| } | ||
|
|
||
| /// Retrieves code metadata (length) for a specific code hash. | ||
| /// Code length by hash. Errors on miss, like `get_account_code`. | ||
| /// This is an optimized path for EXTCODESIZE opcode. | ||
| pub fn get_code_metadata( | ||
| &self, | ||
|
|
@@ -620,19 +611,16 @@ impl GuestProgramState { | |
| if code_hash == *EMPTY_KECCACK_HASH { | ||
| return Ok(CodeMetadata { length: 0 }); | ||
| } | ||
| match self.codes_hashed.get(&code_hash) { | ||
| Some(code) => Ok(CodeMetadata { | ||
| self.codes_hashed | ||
| .get(&code_hash) | ||
| .map(|code| CodeMetadata { | ||
| length: code.bytecode.len() as u64, | ||
| }), | ||
| None => { | ||
| // Same as get_account_code - default to empty for missing bytecode | ||
| println!( | ||
| "Missing bytecode for hash {} in witness. Defaulting to empty code metadata.", | ||
| hex::encode(code_hash) | ||
| ); | ||
| Ok(CodeMetadata { length: 0 }) | ||
| } | ||
| } | ||
| }) | ||
| .ok_or_else(|| { | ||
| GuestProgramStateError::Database(format!( | ||
| "missing bytecode for hash {code_hash:x} in witness" | ||
| )) | ||
| }) | ||
| } | ||
|
|
||
| /// When executing multiple blocks in the L2 it happens that the headers in block_headers correspond to the same block headers that we have in the blocks array. The main goal is to hash these only once and set them in both places. | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| mod batch_tests; | ||
| mod mempool_tests; | ||
| mod smoke_tests; | ||
| mod witness_tests; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| use std::{fs::File, io::BufReader, path::PathBuf}; | ||
|
|
||
| use bytes::Bytes; | ||
| use ethrex_blockchain::{ | ||
| Blockchain, | ||
| payload::{BuildPayloadArgs, create_payload}, | ||
| }; | ||
| use ethrex_common::{ | ||
| H160, H256, | ||
| types::{Block, BlockHeader, DEFAULT_BUILDER_GAS_CEIL, ELASTICITY_MULTIPLIER}, | ||
| }; | ||
| use ethrex_rlp::decode::RLPDecode; | ||
| use ethrex_storage::{EngineType, Store}; | ||
|
|
||
| #[tokio::test] | ||
| async fn generated_witness_has_ancestor_headers_in_ascending_order() { | ||
| let store = test_store().await; | ||
| let genesis_header = store.get_block_header(0).unwrap().unwrap(); | ||
| let blockchain = Blockchain::default_with_store(store.clone()); | ||
|
|
||
| let block_1 = new_block(&store, &genesis_header).await; | ||
| blockchain.add_block(block_1.clone()).unwrap(); | ||
| let block_2 = new_block(&store, &block_1.header).await; | ||
| blockchain.add_block(block_2.clone()).unwrap(); | ||
| let block_3 = new_block(&store, &block_2.header).await; | ||
| blockchain.add_block(block_3.clone()).unwrap(); | ||
|
|
||
| let witness = blockchain | ||
| .generate_witness_for_blocks(&[block_2.clone(), block_3.clone()]) | ||
| .await | ||
| .unwrap(); | ||
|
|
||
| let numbers: Vec<u64> = witness | ||
| .block_headers_bytes | ||
| .iter() | ||
| .map(|b| BlockHeader::decode(b).unwrap().number) | ||
| .collect(); | ||
|
|
||
| assert_eq!( | ||
| numbers, | ||
| vec![block_1.header.number, block_2.header.number], | ||
| "expected ancestor headers [block_1, block_2] in ascending order, got {numbers:?}" | ||
| ); | ||
| } | ||
|
|
||
| async fn new_block(store: &Store, parent: &BlockHeader) -> Block { | ||
| let args = BuildPayloadArgs { | ||
| parent: parent.hash(), | ||
| timestamp: parent.timestamp + 12, | ||
| fee_recipient: H160::random(), | ||
| random: H256::random(), | ||
| withdrawals: Some(Vec::new()), | ||
| beacon_root: Some(H256::random()), | ||
| slot_number: None, | ||
| version: 1, | ||
| elasticity_multiplier: ELASTICITY_MULTIPLIER, | ||
| gas_ceil: DEFAULT_BUILDER_GAS_CEIL, | ||
| }; | ||
| let blockchain = Blockchain::default_with_store(store.clone()); | ||
| let block = create_payload(&args, store, Bytes::new()).unwrap(); | ||
| blockchain.build_payload(block).unwrap().payload | ||
| } | ||
|
|
||
| fn workspace_root() -> PathBuf { | ||
| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..") | ||
| } | ||
|
|
||
| async fn test_store() -> Store { | ||
| let file = File::open(workspace_root().join("fixtures/genesis/execution-api.json")) | ||
| .expect("Failed to open genesis file"); | ||
| let reader = BufReader::new(file); | ||
| let genesis = serde_json::from_reader(reader).expect("Failed to deserialize genesis file"); | ||
| let mut store = | ||
| Store::new("store.db", EngineType::InMemory).expect("Failed to build DB for testing"); | ||
| store | ||
| .add_initial_state(genesis) | ||
| .await | ||
| .expect("Failed to add genesis state"); | ||
| store | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lenient
.ok()on RPC side is intentional per EIP-8025 — the witness can carry extra entries that don't decode as headers. That's a different choice from the guest-side strict parsing at line 327+ (which now contiguity-checks each header).This split is correct in shape (RPC = lenient input, guest = strict invariant). Worth a short comment explaining the asymmetry — currently a future maintainer reading just one of these blocks would wonder why the other is different.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in b531dab.