Skip to content

Commit 265352c

Browse files
JereSaloilitteri
authored andcommitted
fix(l1,l2): fix occasional error with debug_executionWitness (#5212)
**Motivation** <!-- Why does this pull request exist? What are its goals? --> - Sometimes when we want a witness ethrex returns an error, this aims to fix it. **Description** <!-- A clear and concise general description of the changes this PR introduces --> 1. There's a bug that assumes that we have some data in a map that we don't necessarily have. This is usually triggered when there's a call to the BLOCKHASH opcode during a block. The idea is to requests all the headers of intermediate blocks so that we can generate the witness correctly. 2. Now the Guest Program accepts blocks with hash already computed, it just recomputes it and compares the hash with the already set one (if any), returning an error in case they differ. This was suggested in eth-act/zkevm-benchmark-workload#177 and we liked the idea. Both changes are in the same PR because the goal was just to solve `1` but as I was hashing the blocks when generating the witness I decided to fix `2` elegantly so we can pass the Blockchain tests. Closes #5250 --------- Co-authored-by: Ivan Litteri <67517699+ilitteri@users.noreply.github.com>
1 parent 9cfd1f1 commit 265352c

File tree

3 files changed

+67
-79
lines changed

3 files changed

+67
-79
lines changed

crates/blockchain/blockchain.rs

Lines changed: 32 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -495,21 +495,6 @@ impl Blockchain {
495495
))?
496496
.header;
497497

498-
// Later on, we need to access block hashes by number. To avoid needing
499-
// to apply fork choice for each block, we cache them here.
500-
// The clone is not redundant, the hash function modifies the original block.
501-
#[allow(clippy::redundant_clone)]
502-
let mut block_hashes_map: BTreeMap<u64, H256> = blocks
503-
.iter()
504-
.cloned()
505-
.map(|block| (block.header.number, block.hash()))
506-
.collect();
507-
508-
block_hashes_map.insert(
509-
first_block_header.number.saturating_sub(1),
510-
first_block_header.parent_hash,
511-
);
512-
513498
// Get state at previous block
514499
let trie = self
515500
.storage
@@ -538,7 +523,7 @@ impl Blockchain {
538523
ChainError::WitnessGeneration("Failed to get root state node".to_string())
539524
})?;
540525

541-
let mut block_hashes = HashMap::new();
526+
let mut blockhash_opcode_references = HashMap::new();
542527
let mut codes = Vec::new();
543528

544529
for (i, block) in blocks.iter().enumerate() {
@@ -602,7 +587,7 @@ impl Blockchain {
602587
})?
603588
.clone();
604589

605-
block_hashes.extend(logger_block_hashes);
590+
blockhash_opcode_references.extend(logger_block_hashes);
606591

607592
// Access all the accounts needed for withdrawals
608593
if let Some(withdrawals) = block.body.withdrawals.as_ref() {
@@ -696,14 +681,7 @@ impl Blockchain {
696681

697682
let (new_state_trie_witness, updated_trie) = TrieLogger::open_trie(
698683
self.storage
699-
.state_trie(
700-
block_hashes_map
701-
.get(&block.header.number)
702-
.ok_or(ChainError::WitnessGeneration(
703-
"Block hash not found for witness generation".to_string(),
704-
))?
705-
.to_owned(),
706-
)
684+
.state_trie(block.header.hash())
707685
.map_err(|_| ChainError::ParentStateNotFound)?
708686
.ok_or(ChainError::ParentStateNotFound)?,
709687
);
@@ -734,39 +712,41 @@ impl Blockchain {
734712
used_trie_nodes.push(root.encode_to_vec());
735713
}
736714

737-
let mut needed_block_numbers = block_hashes.keys().collect::<Vec<_>>();
738-
739-
needed_block_numbers.sort();
715+
// - We now need necessary block headers, these go from the first block referenced (via BLOCKHASH or just the first block to execute) up to the parent of the last block to execute.
716+
let mut block_headers_bytes = Vec::new();
740717

741-
// Last needed block header for the witness is the parent of the last block we need to execute
742-
let last_needed_block_number = blocks
718+
let first_blockhash_opcode_number = blockhash_opcode_references.keys().min();
719+
let first_needed_block_hash = first_blockhash_opcode_number
720+
.and_then(|n| {
721+
(*n < first_block_header.number.saturating_sub(1))
722+
.then(|| blockhash_opcode_references.get(n))?
723+
.copied()
724+
})
725+
.unwrap_or(first_block_header.parent_hash);
726+
727+
// At the beginning this is the header of the last block to execute.
728+
let mut current_header = blocks
743729
.last()
744-
.ok_or(ChainError::WitnessGeneration("Empty batch".to_string()))?
730+
.ok_or_else(|| ChainError::WitnessGeneration("Empty batch".to_string()))?
745731
.header
746-
.number
747-
.saturating_sub(1);
748-
749-
// The first block number we need is either the parent of the first block number or the earliest block number used by BLOCKHASH
750-
let mut first_needed_block_number = first_block_header.number.saturating_sub(1);
732+
.clone();
751733

752-
if let Some(block_number_from_logger) = needed_block_numbers.first()
753-
&& **block_number_from_logger < first_needed_block_number
754-
{
755-
first_needed_block_number = **block_number_from_logger;
756-
}
734+
// Headers from latest - 1 until we reach first block header we need.
735+
// We do it this way because we want to fetch headers by hash, not by number
736+
while current_header.hash() != first_needed_block_hash {
737+
let parent_hash = current_header.parent_hash;
738+
let current_number = current_header.number - 1;
757739

758-
let mut block_headers_bytes = Vec::new();
740+
current_header = self
741+
.storage
742+
.get_block_header_by_hash(parent_hash)?
743+
.ok_or_else(|| {
744+
ChainError::WitnessGeneration(format!(
745+
"Failed to get block {current_number} header"
746+
))
747+
})?;
759748

760-
for block_number in first_needed_block_number..=last_needed_block_number {
761-
let hash = block_hashes_map
762-
.get(&block_number)
763-
.ok_or(ChainError::WitnessGeneration(format!(
764-
"Failed to get block {block_number} hash"
765-
)))?;
766-
let header = self.storage.get_block_header_by_hash(*hash)?.ok_or(
767-
ChainError::WitnessGeneration(format!("Failed to get block {block_number} header")),
768-
)?;
769-
block_headers_bytes.push(header.encode_to_vec());
749+
block_headers_bytes.push(current_header.encode_to_vec());
770750
}
771751

772752
let chain_config = self.storage.get_chain_config();

crates/common/types/block.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ impl RLPDecode for BlockBody {
317317
}
318318

319319
impl BlockHeader {
320-
fn compute_block_hash(&self) -> H256 {
320+
pub fn compute_block_hash(&self) -> H256 {
321321
let mut buf = vec![];
322322
self.encode(&mut buf);
323323
keccak(buf)

crates/common/types/block_execution_witness.rs

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::collections::BTreeMap;
1+
use std::collections::{BTreeMap, BTreeSet};
22
use std::fmt;
33
use std::str::FromStr;
44

@@ -469,40 +469,33 @@ impl GuestProgramState {
469469
}
470470
}
471471

472-
/// Hashes headers in witness and in blocks only once if they are repeated to avoid double hashing.
472+
/// 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.
473+
/// We also initialize the remaining block headers hashes. If they are set, we check their validity.
473474
pub fn initialize_block_header_hashes(
474475
&self,
475476
blocks: &[Block],
476477
) -> Result<(), GuestProgramStateError> {
477-
// First we need to ensure that the block headers are initialized not before the guest program is executed
478-
for header in self.block_headers.values() {
479-
if header.hash.get().is_some() {
480-
return Err(GuestProgramStateError::Custom(format!(
481-
"Block header hash is already set for {}",
482-
header.number
483-
)));
478+
let mut block_numbers_in_common = BTreeSet::new();
479+
for block in blocks {
480+
let hash = block.header.compute_block_hash();
481+
set_hash_or_validate(&block.header, hash)?;
482+
483+
let number = block.header.number;
484+
if let Some(header) = self.block_headers.get(&number) {
485+
block_numbers_in_common.insert(number);
486+
set_hash_or_validate(header, hash)?;
484487
}
485488
}
486489

487-
// Now we initialize the block_headers hashes and check the remaining blocks hashes
488-
for block in blocks {
489-
// Verify each block's header hash is uninitialized
490-
if block.header.hash.get().is_some() {
491-
return Err(GuestProgramStateError::Custom(format!(
492-
"Block header hash is already set for {}",
493-
block.header.number
494-
)));
490+
for header in self.block_headers.values() {
491+
if block_numbers_in_common.contains(&header.number) {
492+
// We have already set this hash in the previous step
493+
continue;
495494
}
496-
let header = self
497-
.block_headers
498-
.get(&block.header.number)
499-
.unwrap_or(&block.header);
500-
501-
let hash = header.hash();
502-
// this returns err if it's already set, so we drop the Result as we don't
503-
// care if it was already initialized.
504-
let _ = block.header.hash.set(hash);
495+
let hash = header.compute_block_hash();
496+
set_hash_or_validate(header, hash)?;
505497
}
498+
506499
Ok(())
507500
}
508501
}
@@ -581,3 +574,18 @@ pub fn hash_key(key: &H256) -> Vec<u8> {
581574
.finalize()
582575
.to_vec()
583576
}
577+
578+
/// Initializes hash of header or validates the hash is correct in case it's already set
579+
/// Note that header doesn't need to be mutable because the hash is a OnceCell
580+
fn set_hash_or_validate(header: &BlockHeader, hash: H256) -> Result<(), GuestProgramStateError> {
581+
// If it's already set the .set() method will return the current value
582+
if let Err(prev_hash) = header.hash.set(hash)
583+
&& prev_hash != hash
584+
{
585+
return Err(GuestProgramStateError::Custom(format!(
586+
"Block header hash was previously set for {} with the wrong value. It should be set correctly or left unset.",
587+
header.number
588+
)));
589+
}
590+
Ok(())
591+
}

0 commit comments

Comments
 (0)