Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions crates/blockchain/blockchain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2303,14 +2303,40 @@ impl Blockchain {

let chain_config: ChainConfig = self.storage.get_chain_config();

// Cache block hashes for the full batch so we can access them during execution without having to store the blocks beforehand
let block_hash_cache = blocks.iter().map(|b| (b.header.number, b.hash())).collect();
// Cache block hashes for the full batch so we can access them during
// execution without having to store the blocks beforehand.
let mut block_hash_cache: BTreeMap<BlockNumber, BlockHash> =
blocks.iter().map(|b| (b.header.number, b.hash())).collect();

let parent_header = self
.storage
.get_block_header_by_hash(first_block_header.parent_hash)
.map_err(|e| (ChainError::StoreError(e), None))?
.ok_or((ChainError::ParentNotFound, None))?;

// Walk the parent chain to cache the last 256 block hashes so that
// BLOCKHASH can resolve references to blocks from previous batches
// (they may not be canonical yet during import).
block_hash_cache
.entry(parent_header.number)
.or_insert_with(|| parent_header.hash());
let mut hash = parent_header.parent_hash;
let mut number = parent_header.number.saturating_sub(1);
let lookback = first_block_header.number.saturating_sub(256);
while number > lookback {
block_hash_cache.entry(number).or_insert(hash);
match self.storage.get_block_header_by_hash(hash) {
Ok(Some(header)) => {
hash = header.parent_hash;
number = number.saturating_sub(1);
}
Ok(None) => break,
Err(e) => {
warn!("Failed to fetch block header by hash during BLOCKHASH cache walk: {e}");
break;
}
}
}
let vm_db = StoreVmDatabase::new_with_block_hash_cache(
self.storage.clone(),
parent_header,
Expand Down
156 changes: 156 additions & 0 deletions test/tests/blockchain/batch_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,162 @@ async fn batch_selfdestruct_created_account_no_spurious_state() {
);
}

/// Regression test for cross-batch BLOCKHASH resolution during import.
///
/// When importing blocks in batches, a block in batch N+1 may execute
/// BLOCKHASH for a block that was in batch N. Without the fix, the hash
/// wouldn't be found because batch N's blocks aren't canonical yet and
/// the block_hash_cache only covers the current batch.
///
/// This test builds 3 blocks:
/// - Block 1: deploy a contract that stores `blockhash(number - 1)` in storage
/// - Block 2: empty (just advances the chain)
/// - Block 3: call the contract (reads blockhash of block 2)
///
/// Then executes blocks 1-2 in one batch, and block 3 in a second batch.
/// Block 3 needs `blockhash(2)` which is only in the first batch.
#[tokio::test]
async fn batch_cross_batch_blockhash_regression() {
let sk = test_secret_key();
let sender = sender_from_key(&sk);
let signer: Signer = LocalSigner::new(sk).into();

let (store_a, chain_id) = setup_store(sender).await;
let blockchain_a = Blockchain::default_with_store(store_a.clone());
let genesis_header = store_a.get_block_header(0).unwrap().unwrap();

// Deploy contract: BLOCKHASH(NUMBER - 1) -> SSTORE(0)
// Bytecode: NUMBER PUSH1 1 SWAP1 SUB BLOCKHASH PUSH1 0 SSTORE STOP
// 43 60 01 90 03 40 60 00 55 00
let deploy_code = {
let runtime = vec![0x43, 0x60, 0x01, 0x90, 0x03, 0x40, 0x60, 0x00, 0x55, 0x00];
let rt_len = runtime.len();
// Init code: CODECOPY runtime into memory, then RETURN it.
// PUSH1 rt_len PUSH1 init_len PUSH1 0 CODECOPY PUSH1 rt_len PUSH1 0 RETURN
let init_len = 12;
// PUSH1 rt_len | PUSH1 init_code_len | PUSH1 0 | CODECOPY
// PUSH1 rt_len | PUSH1 0 | RETURN
let mut init = vec![
0x60,
rt_len as u8,
0x60,
init_len as u8,
0x60,
0x00,
0x39,
0x60,
rt_len as u8,
0x60,
0x00,
0xF3,
];
assert_eq!(init.len(), init_len as usize);
init.extend_from_slice(&runtime);
Bytes::from(init)
};

let contract_address = calculate_create_address(sender, 0);

// tx1: deploy the contract
let mut tx1 = Transaction::EIP1559Transaction(EIP1559Transaction {
chain_id,
nonce: 0,
max_priority_fee_per_gas: 0,
max_fee_per_gas: TEST_MAX_FEE_PER_GAS,
gas_limit: TEST_GAS_LIMIT,
to: TxKind::Create,
value: U256::zero(),
data: deploy_code,
..Default::default()
});
tx1.sign_inplace(&signer).await.unwrap();

blockchain_a
.add_transaction_to_pool(tx1)
.await
.expect("tx1 should enter pool");

// Build block 1 (deploys the contract)
let block1 = build_block(&store_a, &blockchain_a, &genesis_header).await;
assert!(
!block1.body.transactions.is_empty(),
"block1 must include tx"
);
blockchain_a
.add_block(block1.clone())
.expect("block1 valid");
store_a
.forkchoice_update(vec![], 1, block1.hash(), None, None)
.await
.unwrap();
blockchain_a
.remove_block_transactions_from_pool(&block1)
.unwrap();

// Build block 2 (empty, just advances the chain)
let block2 = build_block(&store_a, &blockchain_a, &block1.header).await;
blockchain_a
.add_block(block2.clone())
.expect("block2 valid");
store_a
.forkchoice_update(vec![], 2, block2.hash(), None, None)
.await
.unwrap();

// tx3: call the contract (triggers BLOCKHASH for block 2)
let mut tx3 = Transaction::EIP1559Transaction(EIP1559Transaction {
chain_id,
nonce: 1,
max_priority_fee_per_gas: 0,
max_fee_per_gas: TEST_MAX_FEE_PER_GAS,
gas_limit: TEST_GAS_LIMIT,
to: TxKind::Call(contract_address),
value: U256::zero(),
data: Bytes::new(),
..Default::default()
});
tx3.sign_inplace(&signer).await.unwrap();

blockchain_a
.add_transaction_to_pool(tx3)
.await
.expect("tx3 should enter pool");

// Build block 3 (calls contract, needs blockhash of block 2)
let block3 = build_block(&store_a, &blockchain_a, &block2.header).await;
assert!(
!block3.body.transactions.is_empty(),
"block3 must include tx"
);
blockchain_a
.add_block(block3.clone())
.expect("block3 valid");

// Now re-execute on a fresh store in TWO batches:
// Batch 1: blocks 1-2, Batch 2: block 3
// Block 3 needs blockhash(2) which is only in batch 1.
let (store_b, _) = setup_store(sender).await;
let blockchain_b = Blockchain::default_with_store(store_b);

let result1 = blockchain_b
.add_blocks_in_batch(vec![block1, block2], CancellationToken::new())
.await;
assert!(
result1.is_ok(),
"batch 1 should succeed — got error: {:?}",
result1.err()
);

let result2 = blockchain_b
.add_blocks_in_batch(vec![block3], CancellationToken::new())
.await;
assert!(
result2.is_ok(),
"batch 2 should succeed (needs blockhash from batch 1) — got error: {:?}",
result2.err()
);
}

/// Simpler variant: a single block with a self-destructing contract, executed
/// in batch. Ensures the basic batch path doesn't regress for single-block
/// batches containing selfdestruct.
Expand Down
Loading