diff --git a/crates/blockchain/blockchain.rs b/crates/blockchain/blockchain.rs index 078e12a87a9..87b34cc29ed 100644 --- a/crates/blockchain/blockchain.rs +++ b/crates/blockchain/blockchain.rs @@ -1436,6 +1436,9 @@ impl Blockchain { block_headers_bytes.push(current_header.encode_to_vec()); } + // EELS expects ascending order; we walked the chain backward, so reverse. + // Ref: https://github.com/ethereum/execution-specs/blob/projects/zkevm/src/ethereum/forks/amsterdam/stateless.py#L171-L191 + block_headers_bytes.reverse(); // Get initial state trie root and embed the rest of the trie into it let nodes: BTreeMap = used_trie_nodes @@ -1670,6 +1673,9 @@ impl Blockchain { block_headers_bytes.push(current_header.encode_to_vec()); } + // EELS expects ascending order; we walked the chain backward, so reverse. + // Ref: https://github.com/ethereum/execution-specs/blob/projects/zkevm/src/ethereum/forks/amsterdam/stateless.py#L171-L191 + block_headers_bytes.reverse(); // Get initial state trie root and embed the rest of the trie into it let nodes: BTreeMap = used_trie_nodes diff --git a/crates/common/types/block_execution_witness.rs b/crates/common/types/block_execution_witness.rs index 71071764fab..4363b3b32e2 100644 --- a/crates/common/types/block_execution_witness.rs +++ b/crates/common/types/block_execution_witness.rs @@ -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 - )) - })?; + // Skip headers that fail to decode, then pick parent by number. + 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 entries that don't decode. They can't be looked up by + // hash anyway; if execution needs them, the trie walk fails there. + // Ref: https://github.com/ethereum/execution-specs/blob/projects/zkevm/src/ethereum/forks/amsterdam/witness_state.py#L37-L42 let nodes: BTreeMap = 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::>()?; + .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,28 @@ impl GuestProgramState { value: ExecutionWitness, crypto: &dyn Crypto, ) -> Result { - let block_headers: BTreeMap = value - .block_headers_bytes - .into_iter() - .map(|bytes| BlockHeader::decode(bytes.as_ref())) - .collect::, _>>() - .map_err(|e| { - GuestProgramStateError::Custom(format!("Failed to decode block headers: {}", e)) - })? - .into_iter() - .map(|header| (header.number, header)) - .collect(); + // EIP-8025: headers must form a contiguous chain in list order (each + // `parent_hash` matches keccak of the previous header bytes). Reordered + // or fragmented chains are invalid even if by-number lookup would resolve. + // Ref: https://github.com/ethereum/execution-specs/blob/projects/zkevm/src/ethereum/forks/amsterdam/stateless.py#L171-L191 + let mut block_headers: BTreeMap = BTreeMap::new(); + let mut prev_hash: Option = None; + for bytes in &value.block_headers_bytes { + 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; + }; + if let Some(expected_parent) = prev_hash + && header.parent_hash != expected_parent + { + return Err(GuestProgramStateError::Custom( + "witness headers are not contiguous".to_string(), + )); + } + prev_hash = Some(H256(crypto.keccak256(bytes))); + block_headers.insert(header.number, header); + } let parent_number = value @@ -588,28 +594,22 @@ 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 { 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 {} in witness", + hex::encode(code_hash) + )) + }) } - /// 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 +620,17 @@ 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.", + }) + .ok_or_else(|| { + GuestProgramStateError::Database(format!( + "missing bytecode for hash {} in witness", hex::encode(code_hash) - ); - Ok(CodeMetadata { length: 0 }) - } - } + )) + }) } /// 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. diff --git a/tooling/ef_tests/blockchain/tests/all.rs b/tooling/ef_tests/blockchain/tests/all.rs index c9513b2bf96..f2443a3c919 100644 --- a/tooling/ef_tests/blockchain/tests/all.rs +++ b/tooling/ef_tests/blockchain/tests/all.rs @@ -28,28 +28,7 @@ const EXTRA_SKIPS: &[&str] = &[ "Return50000", "static_Call1MB1024Calldepth", ]; -#[cfg(feature = "stateless")] -const EXTRA_SKIPS: &[&str] = &[ - // zkevm@v0.3.3 tolerance tests: the fixture's `statelessOutputBytes` declares `valid = 1` - // because the executed path does not actually consume the malformed/extra/missing witness - // entry, but our RpcExecutionWitness conversion eagerly validates the full witness and - // rejects it. Re-enable once the witness conversion is lazy per EIP-8025 §Tolerance. - "validation_headers_malformed_rlp_header", - "validation_headers_missing_oldest_blockhash_ancestor", - "validation_headers_missing_parent_header", - "validation_state_extra_unused_trie_node", - // zkevm@v0.3.3 rejection tests: `statelessOutputBytes` declares `valid = 0` so the guest - // program must reject the deliberately-incomplete witness, but our stateless path runs - // to completion instead of detecting the missing entry. Re-enable once the witness - // completeness checks land (missing delegation/external-code bytecodes, non-contiguous - // header chain detection). - "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", - "validation_headers_non_contiguous_chain", -]; -#[cfg(not(any(feature = "sp1", feature = "stateless")))] +#[cfg(not(feature = "sp1"))] const EXTRA_SKIPS: &[&str] = &[]; // Select backend