1use super::{ordered_outputs::ordered_worker_outputs, worker, BalExecutionError, RejectReason};
18use alloy_eip7928::{
19 bal::{Bal as AlloyBal, DecodedBal},
20 compute_block_access_list_hash,
21};
22use alloy_evm::{
23 block::{BlockExecutionError, BlockExecutor, BlockValidationError, TxResult},
24 Evm,
25};
26use alloy_primitives::Address;
27use crossbeam_channel::{Receiver, Sender};
28use reth_evm::{execute::ExecutableTxFor, ConfigureEvm, Database, EvmEnvFor, ExecutionCtxFor};
29use reth_primitives_traits::ReceiptTy;
30use reth_provider::BlockExecutionOutput;
31use reth_tasks::Runtime;
32use revm::{
33 context::{result::ResultAndState, Block},
34 database::{states::bundle_state::BundleRetention, State},
35 primitives::{eip7825::TX_GAS_LIMIT_CAP, hardfork::SpecId},
36};
37use revm_state::bal::Bal as RevmBal;
38use std::sync::Arc;
39
40use crate::tree::payload_processor::receipt_root_task::IndexedReceipt;
41
42#[expect(clippy::too_many_arguments, clippy::type_complexity)]
44pub fn execute_block<'a, Evm, Tx, Err, DB, MakeDb>(
45 runtime: &Runtime,
46 evm_config: &'a Evm,
47 make_db: &'a MakeDb,
48 input_bal: Arc<DecodedBal>,
49 evm_env: EvmEnvFor<Evm>,
50 ctx: ExecutionCtxFor<'a, Evm>,
51 transaction_count: usize,
52 txs: Receiver<(usize, Result<Tx, Err>)>,
53 receipt_tx: Sender<IndexedReceipt<ReceiptTy<Evm::Primitives>>>,
54) -> Result<(BlockExecutionOutput<ReceiptTy<Evm::Primitives>>, Vec<Address>), BalExecutionError>
55where
56 Evm: ConfigureEvm + 'static,
57 Tx: ExecutableTxFor<Evm> + Send + 'a,
58 Err: core::error::Error + Send + Sync + 'static,
59 DB: Database + Send + 'a,
60 MakeDb: Fn() -> Result<DB, BalExecutionError> + Sync + 'a,
61 ReceiptTy<Evm::Primitives>: Clone,
62{
63 let worker_pool = runtime.bal_streaming_pool();
64 let worker_count = worker_pool.current_num_threads().max(1).min(transaction_count);
65
66 worker_pool.in_place_scope(|scope| {
67 execute_block_inner(
68 scope,
69 evm_config,
70 make_db,
71 input_bal,
72 evm_env,
73 ctx,
74 transaction_count,
75 txs,
76 receipt_tx,
77 worker_count,
78 )
79 })
80}
81
82#[expect(clippy::too_many_arguments, clippy::type_complexity)]
83fn execute_block_inner<'scope, Evm, Tx, Err, DB, MakeDb>(
84 scope: &rayon::Scope<'scope>,
85 evm_config: &'scope Evm,
86 make_db: &'scope MakeDb,
87 input_bal: Arc<DecodedBal>,
88 evm_env: EvmEnvFor<Evm>,
89 ctx: ExecutionCtxFor<'scope, Evm>,
90 transaction_count: usize,
91 txs: Receiver<(usize, Result<Tx, Err>)>,
92 receipt_tx: Sender<IndexedReceipt<ReceiptTy<Evm::Primitives>>>,
93 worker_count: usize,
94) -> Result<(BlockExecutionOutput<ReceiptTy<Evm::Primitives>>, Vec<Address>), BalExecutionError>
95where
96 Evm: ConfigureEvm + 'scope,
97 Tx: ExecutableTxFor<Evm> + Send + 'scope,
98 Err: core::error::Error + Send + Sync + 'static,
99 DB: Database + Send + 'scope,
100 MakeDb: Fn() -> Result<DB, BalExecutionError> + Sync + 'scope,
101 ReceiptTy<Evm::Primitives>: Clone,
102{
103 let bal = input_bal.as_bal();
104 let input_bal_revm: Arc<RevmBal> = Arc::new(
105 RevmBal::try_from(Vec::<_>::from(bal.clone()))
106 .map_err(|e| BalExecutionError::BalConversion(format!("{e:?}")))?,
107 );
108
109 let is_amsterdam = evm_env.cfg_env.spec.into().is_enabled_in(SpecId::AMSTERDAM);
112 let block_gas_limit = evm_env.block_env.gas_limit();
113 let mut canonical_state =
114 State::builder().with_database(make_db()?).with_bundle_update().with_bal_builder().build();
115 load_bal_accounts(&mut canonical_state, bal)?;
116
117 let (block_result, senders) = {
118 let (result_tx, result_rx) = crossbeam_channel::unbounded();
119 let (abort_guard, abort_rx) = AbortGuard::new();
120
121 for _ in 0..worker_count {
122 worker::spawn_worker(
123 scope,
124 txs.clone(),
125 abort_rx.clone(),
126 result_tx.clone(),
127 evm_config,
128 make_db,
129 Arc::clone(&input_bal_revm),
130 evm_env.clone(),
131 ctx.clone(),
132 );
133 }
134 drop(result_tx);
135
136 let mut gas_tracker = BlockGasTracker::new(block_gas_limit, is_amsterdam);
137 let evm = evm_config.evm_with_env(&mut canonical_state, evm_env);
138 let mut canonical_executor = evm_config.create_executor_with_state(evm, ctx.clone());
139
140 canonical_executor.apply_pre_execution_changes()?;
141 let mut senders = Vec::with_capacity(transaction_count);
142 let mut last_sent_len = 0usize;
143 let _ = input_bal.hash();
147 for output in ordered_worker_outputs(&result_rx, transaction_count) {
148 let output = output?;
149
150 gas_tracker.validate_tx_limit(output.tx_gas_limit)?;
151 gas_tracker.record_result(output.result.result());
152 canonical_executor.evm_mut().db_mut().bump_bal_index();
153
154 let _ = canonical_executor.commit_transaction(output.result);
155 senders.push(output.signer);
156
157 let current_len = canonical_executor.receipts().len();
158 if current_len > last_sent_len {
159 last_sent_len = current_len;
160 if let Some(receipt) = canonical_executor.receipts().last() {
161 let tx_index = current_len - 1;
162 let _ = receipt_tx.send(IndexedReceipt::new(tx_index, receipt.clone()));
163 }
164 }
165 }
166 drop(abort_guard);
167
168 canonical_executor.evm_mut().db_mut().bump_bal_index();
169 let block_result = canonical_executor.apply_post_execution_changes()?;
170 (block_result, senders)
171 };
172
173 validate_bal(&mut canonical_state, input_bal.as_ref())?;
174
175 canonical_state.merge_transitions(BundleRetention::Reverts);
176 Ok((
177 BlockExecutionOutput { state: canonical_state.take_bundle(), result: block_result },
178 senders,
179 ))
180}
181
182fn load_bal_accounts<DB>(
183 canonical_state: &mut State<DB>,
184 bal: &AlloyBal,
185) -> Result<(), BalExecutionError>
186where
187 DB: Database,
188{
189 for account_changes in bal {
197 canonical_state
198 .load_cache_account(account_changes.address)
199 .map_err(|e| BalExecutionError::Evm(BlockExecutionError::other(e)))?;
200 }
201
202 Ok(())
203}
204
205pub(crate) fn validate_bal<DB>(
210 canonical_state: &mut State<DB>,
211 input_bal: &DecodedBal,
212) -> Result<(), BalExecutionError>
213where
214 DB: Database,
215{
216 let composed_alloy = canonical_state.take_built_alloy_bal().expect("with_bal_builder set");
217 let input_bal_entries = input_bal.as_bal();
218 if composed_alloy == input_bal_entries.as_slice() {
219 return Ok(());
220 }
221 let rebuilt = compute_block_access_list_hash(&composed_alloy);
222 let expected_bal_hash = input_bal.hash();
223
224 if tracing::enabled!(
225 target: "engine::tree::payload_processor::bal",
226 tracing::Level::DEBUG
227 ) {
228 let div = input_bal_entries.diff(&composed_alloy);
229 tracing::debug!(
230 target: "engine::tree::payload_processor::bal",
231 %rebuilt,
232 expected = %expected_bal_hash,
233 %div,
234 "first BAL divergence",
235 );
236 }
237
238 Err(BalExecutionError::Reject(RejectReason::FinalHashMismatch {
239 rebuilt,
240 expected: expected_bal_hash,
241 }))
242}
243
244struct AbortGuard {
246 _tx: Sender<()>,
247}
248
249impl AbortGuard {
250 fn new() -> (Self, Receiver<()>) {
251 let (tx, rx) = crossbeam_channel::bounded(0);
252 (Self { _tx: tx }, rx)
253 }
254}
255
256#[derive(Debug)]
258struct BlockGasTracker {
259 block_gas_limit: u64,
260 is_amsterdam: bool,
261 cumulative_tx_gas_used: u64,
262 block_regular_gas_used: u64,
263}
264
265impl BlockGasTracker {
266 const fn new(block_gas_limit: u64, is_amsterdam: bool) -> Self {
267 Self { block_gas_limit, is_amsterdam, cumulative_tx_gas_used: 0, block_regular_gas_used: 0 }
268 }
269
270 fn validate_tx_limit(&self, tx_gas_limit: u64) -> Result<(), BlockExecutionError> {
271 let block_gas_used = if self.is_amsterdam {
272 self.block_regular_gas_used
273 } else {
274 self.cumulative_tx_gas_used
275 };
276 let block_available_gas = self.block_gas_limit.saturating_sub(block_gas_used);
277 let tx_min_gas_limit = tx_gas_limit.min(TX_GAS_LIMIT_CAP);
278
279 if tx_min_gas_limit > block_available_gas {
280 return Err(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
281 transaction_gas_limit: tx_gas_limit,
282 block_available_gas,
283 }
284 .into());
285 }
286
287 Ok(())
288 }
289
290 fn record_result<H>(&mut self, result: &ResultAndState<H>) {
291 let gas = result.result.gas();
292 self.cumulative_tx_gas_used = self.cumulative_tx_gas_used.saturating_add(gas.tx_gas_used());
293 self.block_regular_gas_used =
294 self.block_regular_gas_used.saturating_add(gas.block_regular_gas_used());
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301 use alloy_consensus::{BlockHeader, EthereumReceipt, Header};
302 use alloy_eip7928::{bal::Bal as AlloyBal, BlockAccessList};
303 use alloy_eips::{
304 eip2935::{HISTORY_STORAGE_ADDRESS, HISTORY_STORAGE_CODE},
305 eip4788::{BEACON_ROOTS_ADDRESS, BEACON_ROOTS_CODE},
306 eip7002::{WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, WITHDRAWAL_REQUEST_PREDEPLOY_CODE},
307 };
308 use alloy_primitives::{keccak256, B256, U256};
309 use reth_ethereum_primitives::{Block, BlockBody, TransactionSigned};
310 use reth_evm_ethereum::EthEvmConfig;
311 use reth_primitives_traits::{Block as _, Recovered, SealedBlock};
312 use reth_revm::db::BundleState;
313 use reth_tasks::Runtime;
314 use revm::{
315 database::{CacheDB, EmptyDB},
316 state::{AccountInfo, Bytecode},
317 };
318 use std::convert::Infallible;
319
320 fn to_arc_decoded(bal: BlockAccessList) -> Arc<DecodedBal> {
322 let alloy_bal: AlloyBal = bal.into();
323 let raw = alloy_rlp::encode(&alloy_bal).into();
324 Arc::new(DecodedBal::new(alloy_bal, raw))
325 }
326
327 fn system_contracts_db() -> CacheDB<EmptyDB> {
331 let mut db = CacheDB::<EmptyDB>::new(Default::default());
332 db.insert_account_info(
333 BEACON_ROOTS_ADDRESS,
334 AccountInfo {
335 balance: U256::ZERO,
336 nonce: 1,
337 code_hash: keccak256(BEACON_ROOTS_CODE.clone()),
338 code: Some(Bytecode::new_raw(BEACON_ROOTS_CODE.clone())),
339 account_id: None,
340 },
341 );
342 db.insert_account_info(
343 WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS,
344 AccountInfo {
345 balance: U256::ZERO,
346 nonce: 1,
347 code_hash: keccak256(WITHDRAWAL_REQUEST_PREDEPLOY_CODE.clone()),
348 code: Some(Bytecode::new_raw(WITHDRAWAL_REQUEST_PREDEPLOY_CODE.clone())),
349 account_id: None,
350 },
351 );
352 db.insert_account_info(
353 HISTORY_STORAGE_ADDRESS,
354 AccountInfo {
355 balance: U256::ZERO,
356 nonce: 1,
357 code_hash: keccak256(HISTORY_STORAGE_CODE.clone()),
358 code: Some(Bytecode::new_raw(HISTORY_STORAGE_CODE.clone())),
359 account_id: None,
360 },
361 );
362 db
363 }
364
365 fn empty_amsterdam_block(header_bal_hash: B256) -> SealedBlock<Block> {
367 empty_amsterdam_block_with_gas_limit(header_bal_hash, 30_000_000)
368 }
369
370 fn empty_amsterdam_block_with_gas_limit(
371 header_bal_hash: B256,
372 gas_limit: u64,
373 ) -> SealedBlock<Block> {
374 let header = Header {
375 timestamp: 1,
376 number: 1,
377 gas_limit,
378 parent_beacon_block_root: Some(B256::ZERO),
379 withdrawals_root: Some(alloy_consensus::EMPTY_ROOT_HASH),
380 requests_hash: Some(alloy_eips::eip7685::EMPTY_REQUESTS_HASH),
381 excess_blob_gas: Some(0),
382 blob_gas_used: Some(0),
383 block_access_list_hash: Some(header_bal_hash),
384 ..Header::default()
385 };
386 let block = Block {
387 header,
388 body: BlockBody {
389 transactions: vec![],
390 ommers: vec![],
391 withdrawals: Some(vec![].into()),
392 },
393 };
394 block.seal_slow()
395 }
396
397 fn reference_bal_for_empty_block(evm_config: &EthEvmConfig) -> BlockAccessList {
404 use revm::database::State as RevmState;
405
406 let db = system_contracts_db();
407 let mut state =
408 RevmState::builder().with_database(db).with_bundle_update().with_bal_builder().build();
409
410 let block = empty_amsterdam_block(B256::ZERO);
412 {
413 let mut executor =
414 evm_config.executor_for_block(&mut state, &block).expect("build executor");
415 executor.apply_pre_execution_changes().expect("pre-exec");
416 executor.evm_mut().db_mut().bump_bal_index();
417 executor.apply_post_execution_changes().expect("post-exec");
418 }
419 state.take_built_alloy_bal().expect("with_bal_builder was set")
420 }
421
422 #[test]
423 fn empty_block_happy_path_round_trip() {
424 let evm_config = EthEvmConfig::mainnet();
430
431 let input_bal = reference_bal_for_empty_block(&evm_config);
432 let bal_hash = alloy_eip7928::compute_block_access_list_hash(&input_bal);
433 assert!(!input_bal.is_empty(), "empty BAL means system calls didn't record state");
435
436 let block = empty_amsterdam_block(bal_hash);
437
438 let result = run_execute_block(
439 &Runtime::test(),
440 evm_config,
441 db_factory(system_contracts_db()),
442 to_arc_decoded(input_bal),
443 &block,
444 Vec::<Recovered<TransactionSigned>>::new(),
445 );
446
447 match result {
448 Ok(output) => {
449 assert!(output.receipts.is_empty(), "empty block → no receipts");
450 }
451 Err(e) => panic!("expected success, got {e:?}"),
452 }
453 }
454
455 fn db_factory(
456 db: CacheDB<EmptyDB>,
457 ) -> impl Fn() -> Result<CacheDB<EmptyDB>, BalExecutionError> + Sync {
458 move || Ok(db.clone())
459 }
460
461 fn tx_stream<Tx>(txs: Vec<Tx>) -> Receiver<(usize, Result<Tx, Infallible>)> {
462 let (tx, rx) = crossbeam_channel::unbounded();
463 for (index, transaction) in txs.into_iter().enumerate() {
464 tx.send((index, Ok(transaction))).unwrap();
465 }
466 rx
467 }
468
469 fn run_execute_block<Tx, DB, MakeDb>(
470 runtime: &Runtime,
471 evm_config: EthEvmConfig,
472 make_db: MakeDb,
473 input_bal: Arc<DecodedBal>,
474 block: &SealedBlock<Block>,
475 txs: Vec<Tx>,
476 ) -> Result<BlockExecutionOutput<EthereumReceipt>, BalExecutionError>
477 where
478 Tx: ExecutableTxFor<EthEvmConfig> + Send,
479 DB: Database + Send,
480 MakeDb: Fn() -> Result<DB, BalExecutionError> + Sync,
481 {
482 let transaction_count = txs.len();
483 let (receipt_tx, _receipt_rx) = crossbeam_channel::unbounded();
484 let evm_env = evm_config.evm_env(block.header()).unwrap();
485 let execution_ctx = evm_config.context_for_block(block).unwrap();
486 execute_block(
487 runtime,
488 &evm_config,
489 &make_db,
490 input_bal,
491 evm_env,
492 execution_ctx,
493 transaction_count,
494 tx_stream(txs),
495 receipt_tx,
496 )
497 .map(|(output, _)| output)
498 }
499
500 fn insert_funded(db: &mut CacheDB<EmptyDB>, addr: alloy_primitives::Address, balance: U256) {
502 db.insert_account_info(
503 addr,
504 AccountInfo { nonce: 0, balance, code_hash: B256::ZERO, code: None, account_id: None },
505 );
506 }
507
508 fn reference_bal_for_block<Tx>(
511 evm_config: &EthEvmConfig,
512 mut db: CacheDB<EmptyDB>,
513 block: &SealedBlock<Block>,
514 txs: Vec<Tx>,
515 ) -> BlockAccessList
516 where
517 Tx: ExecutableTxFor<EthEvmConfig>,
518 {
519 use revm::database::State as RevmState;
520
521 let mut state = RevmState::builder()
522 .with_database(&mut db)
523 .with_bundle_update()
524 .with_bal_builder()
525 .build();
526
527 {
528 let mut executor =
529 evm_config.executor_for_block(&mut state, block).expect("build executor");
530 executor.apply_pre_execution_changes().expect("pre-exec");
531 for (i, tx) in txs.into_iter().enumerate() {
532 executor.evm_mut().db_mut().bump_bal_index();
533 executor
534 .execute_transaction(tx)
535 .unwrap_or_else(|e| panic!("tx {i} failed during reference build: {e:?}"));
536 }
537 executor.evm_mut().db_mut().bump_bal_index();
538 executor.apply_post_execution_changes().expect("post-exec");
539 }
540 state.take_built_alloy_bal().expect("with_bal_builder was set")
541 }
542
543 #[test]
544 fn multi_tx_happy_path_round_trip() {
545 use alloy_consensus::TxLegacy;
553 use alloy_primitives::TxKind;
554 use reth_chainspec::MAINNET;
555 use reth_ethereum_primitives::Transaction;
556 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
557 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
558
559 let evm_config = EthEvmConfig::mainnet();
560 let carol: alloy_primitives::Address = alloy_primitives::Address::from([0xCA; 20]);
561 let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
562
563 let alice_kp = generate_key(&mut rng());
565 let alice = public_key_to_address(alice_kp.public_key());
566 let bob_kp = generate_key(&mut rng());
567 let bob = public_key_to_address(bob_kp.public_key());
568
569 let mut pre_block_db = system_contracts_db();
571 insert_funded(&mut pre_block_db, alice, sender_balance);
572 insert_funded(&mut pre_block_db, bob, sender_balance);
573
574 let chain_id = MAINNET.chain.id();
576 let gas_price = 1u128; let tx1 = sign_tx_with_key_pair(
578 alice_kp,
579 Transaction::Legacy(TxLegacy {
580 chain_id: Some(chain_id),
581 nonce: 0,
582 gas_price,
583 gas_limit: 21_000,
584 to: TxKind::Call(carol),
585 value: U256::from(100u64),
586 input: Default::default(),
587 }),
588 );
589 let tx2 = sign_tx_with_key_pair(
590 bob_kp,
591 Transaction::Legacy(TxLegacy {
592 chain_id: Some(chain_id),
593 nonce: 0,
594 gas_price,
595 gas_limit: 21_000,
596 to: TxKind::Call(carol),
597 value: U256::from(200u64),
598 input: Default::default(),
599 }),
600 );
601 let recovered1 = Recovered::new_unchecked(tx1, alice);
602 let recovered2 = Recovered::new_unchecked(tx2, bob);
603
604 let block_for_ref = empty_amsterdam_block(B256::ZERO);
606 let reference_bal = reference_bal_for_block::<Recovered<TransactionSigned>>(
607 &evm_config,
608 {
609 let mut db = system_contracts_db();
611 db.insert_account_info(
612 alice,
613 AccountInfo {
614 nonce: 0,
615 balance: sender_balance,
616 code_hash: B256::ZERO,
617 code: None,
618 account_id: None,
619 },
620 );
621 db.insert_account_info(
622 bob,
623 AccountInfo {
624 nonce: 0,
625 balance: sender_balance,
626 code_hash: B256::ZERO,
627 code: None,
628 account_id: None,
629 },
630 );
631 db
632 },
633 &block_for_ref,
634 vec![recovered1.clone(), recovered2.clone()],
635 );
636 assert!(!reference_bal.is_empty(), "expected BAL entries from pre-exec + txs");
637
638 let bal_hash = alloy_eip7928::compute_block_access_list_hash(&reference_bal);
639 let block = empty_amsterdam_block(bal_hash);
640
641 let result = run_execute_block(
642 &Runtime::test(),
643 evm_config,
644 db_factory(pre_block_db),
645 to_arc_decoded(reference_bal),
646 &block,
647 vec![recovered1, recovered2],
648 );
649
650 match result {
651 Ok(output) => {
652 assert_eq!(output.receipts.len(), 2, "expected 2 receipts");
653 assert!(output.gas_used >= 2 * 21_000, "expected at least 42k gas used");
654 }
655 Err(e) => panic!("expected success, got {e:?}"),
656 }
657 }
658
659 #[derive(Debug)]
667 struct ShadowOutput {
668 bundle_state: BundleState,
669 receipts: Vec<reth_ethereum_primitives::Receipt>,
670 gas_used: u64,
671 requests: alloy_eips::eip7685::Requests,
672 }
673
674 fn run_serial_path(
679 evm_config: &EthEvmConfig,
680 canonical_db: CacheDB<EmptyDB>,
681 block: &SealedBlock<Block>,
682 txs: &[Recovered<TransactionSigned>],
683 ) -> (ShadowOutput, BlockAccessList) {
684 use revm::database::State as RevmState;
685
686 let mut state = RevmState::builder()
687 .with_database(canonical_db)
688 .with_bundle_update()
689 .with_bal_builder()
690 .build();
691
692 let block_result = {
693 let mut executor =
694 evm_config.executor_for_block(&mut state, block).expect("build serial executor");
695 executor.apply_pre_execution_changes().expect("serial pre-exec");
696 for (i, tx) in txs.iter().cloned().enumerate() {
697 executor.evm_mut().db_mut().bump_bal_index();
698 executor
699 .execute_transaction(tx)
700 .unwrap_or_else(|e| panic!("serial tx {i} failed: {e:?}"));
701 }
702 executor.evm_mut().db_mut().bump_bal_index();
703 executor.apply_post_execution_changes().expect("serial post-exec")
704 };
705
706 let bal = state.take_built_alloy_bal().expect("with_bal_builder was set");
707 state.merge_transitions(BundleRetention::Reverts);
708 let bundle_state = state.take_bundle();
709
710 (
711 ShadowOutput {
712 bundle_state,
713 receipts: block_result.receipts,
714 gas_used: block_result.gas_used,
715 requests: block_result.requests,
716 },
717 bal,
718 )
719 }
720
721 fn assert_shadow_equal(
723 evm_config: EthEvmConfig,
724 canonical_db_template: CacheDB<EmptyDB>,
725 block_header_only: SealedBlock<Block>,
726 txs: Vec<Recovered<TransactionSigned>>,
727 ) {
728 let (serial, reference_bal) =
730 run_serial_path(&evm_config, canonical_db_template.clone(), &block_header_only, &txs);
731
732 let bal_hash = alloy_eip7928::compute_block_access_list_hash(&reference_bal);
734 let block =
735 empty_amsterdam_block_with_gas_limit(bal_hash, block_header_only.header().gas_limit());
736
737 let bal_out = run_execute_block(
738 &Runtime::test(),
739 evm_config,
740 db_factory(canonical_db_template),
741 to_arc_decoded(reference_bal),
742 &block,
743 txs,
744 )
745 .unwrap_or_else(|e| panic!("BAL path failed: {e:?}"));
746
747 assert_eq!(
749 serial.receipts, bal_out.receipts,
750 "receipts diverge between serial and BAL paths",
751 );
752 assert_eq!(
753 serial.gas_used, bal_out.gas_used,
754 "gas_used differs: serial {} vs bal {}",
755 serial.gas_used, bal_out.gas_used,
756 );
757 assert_eq!(
758 serial.requests, bal_out.requests,
759 "requests (EIP-7685) diverge between serial and BAL paths",
760 );
761 assert_eq!(
762 serial.bundle_state, bal_out.state,
763 "bundle_state diverges — the canonical state transitions don't match",
764 );
765 }
766
767 #[test]
768 fn shadow_empty_block() {
769 assert_shadow_equal(
772 EthEvmConfig::mainnet(),
773 system_contracts_db(),
774 empty_amsterdam_block(B256::ZERO),
775 Vec::new(),
776 );
777 }
778
779 #[test]
780 fn shadow_multi_value_transfer() {
781 use alloy_consensus::TxLegacy;
784 use alloy_primitives::TxKind;
785 use reth_chainspec::MAINNET;
786 use reth_ethereum_primitives::Transaction;
787 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
788 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
789
790 let evm_config = EthEvmConfig::mainnet();
791 let carol: alloy_primitives::Address = alloy_primitives::Address::from([0xCA; 20]);
792 let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
793
794 let alice_kp = generate_key(&mut rng());
795 let alice = public_key_to_address(alice_kp.public_key());
796 let bob_kp = generate_key(&mut rng());
797 let bob = public_key_to_address(bob_kp.public_key());
798
799 let mut db = system_contracts_db();
800 insert_funded(&mut db, alice, sender_balance);
801 insert_funded(&mut db, bob, sender_balance);
802
803 let chain_id = MAINNET.chain.id();
804 let make_tx = |kp, to, value, nonce: u64| {
805 sign_tx_with_key_pair(
806 kp,
807 Transaction::Legacy(TxLegacy {
808 chain_id: Some(chain_id),
809 nonce,
810 gas_price: 1,
811 gas_limit: 21_000,
812 to: TxKind::Call(to),
813 value: U256::from(value),
814 input: Default::default(),
815 }),
816 )
817 };
818 let tx1 = Recovered::new_unchecked(make_tx(alice_kp, carol, 100u64, 0), alice);
819 let tx2 = Recovered::new_unchecked(make_tx(bob_kp, carol, 200u64, 0), bob);
820
821 assert_shadow_equal(evm_config, db, empty_amsterdam_block(B256::ZERO), vec![tx1, tx2]);
822 }
823
824 #[test]
825 fn rejects_tx_gas_limit_that_exceeds_remaining_block_gas() {
826 use alloy_consensus::TxLegacy;
830 use alloy_evm::block::BlockValidationError;
831 use alloy_primitives::TxKind;
832 use reth_chainspec::MAINNET;
833 use reth_ethereum_primitives::Transaction;
834 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
835 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
836
837 let evm_config = EthEvmConfig::mainnet();
838 let carol: alloy_primitives::Address = alloy_primitives::Address::from([0xCA; 20]);
839 let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
840 let block_gas_limit = 1_000_000;
841 let tx_gas_limit = 990_000;
842
843 let alice_kp = generate_key(&mut rng());
844 let alice = public_key_to_address(alice_kp.public_key());
845 let bob_kp = generate_key(&mut rng());
846 let bob = public_key_to_address(bob_kp.public_key());
847
848 let mut pre_block_db = system_contracts_db();
849 insert_funded(&mut pre_block_db, alice, sender_balance);
850 insert_funded(&mut pre_block_db, bob, sender_balance);
851
852 let chain_id = MAINNET.chain.id();
853 let make_tx = |kp, value| {
854 sign_tx_with_key_pair(
855 kp,
856 Transaction::Legacy(TxLegacy {
857 chain_id: Some(chain_id),
858 nonce: 0,
859 gas_price: 1,
860 gas_limit: tx_gas_limit,
861 to: TxKind::Call(carol),
862 value: U256::from(value),
863 input: Default::default(),
864 }),
865 )
866 };
867 let tx1 = Recovered::new_unchecked(make_tx(alice_kp, 100u64), alice);
868 let tx2 = Recovered::new_unchecked(make_tx(bob_kp, 200u64), bob);
869
870 let reference_block = empty_amsterdam_block(B256::ZERO);
874 let reference_bal = reference_bal_for_block(
875 &evm_config,
876 pre_block_db.clone(),
877 &reference_block,
878 vec![tx1.clone(), tx2.clone()],
879 );
880 let bal_hash = alloy_eip7928::compute_block_access_list_hash(&reference_bal);
881 let low_gas_block = empty_amsterdam_block_with_gas_limit(bal_hash, block_gas_limit);
882
883 let result = run_execute_block(
884 &Runtime::test(),
885 evm_config,
886 db_factory(pre_block_db),
887 to_arc_decoded(reference_bal),
888 &low_gas_block,
889 vec![tx1, tx2],
890 );
891
892 match result {
893 Err(BalExecutionError::Evm(err)) => assert!(matches!(
894 err.as_validation(),
895 Some(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas { .. })
896 )),
897 Err(err) => panic!("expected block gas validation error, got {err:?}"),
898 Ok(_) => panic!("expected block gas validation error, got Ok"),
899 }
900 }
901
902 #[test]
903 fn shadow_tx_with_revert() {
904 use alloy_consensus::TxLegacy;
910 use alloy_primitives::{Bytes, TxKind};
911 use reth_chainspec::MAINNET;
912 use reth_ethereum_primitives::Transaction;
913 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
914 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
915 use revm::primitives::keccak256;
916
917 let evm_config = EthEvmConfig::mainnet();
918 let revert_contract: alloy_primitives::Address =
919 alloy_primitives::Address::from([0xDE; 20]);
920 let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
921
922 let alice_kp = generate_key(&mut rng());
923 let alice = public_key_to_address(alice_kp.public_key());
924
925 let revert_code: Bytes = Bytes::from_static(&[0x60, 0x00, 0x60, 0x00, 0xfd]);
927 let code_hash = keccak256(&revert_code);
928 let mut db = system_contracts_db();
929 insert_funded(&mut db, alice, sender_balance);
930 db.insert_account_info(
931 revert_contract,
932 AccountInfo {
933 nonce: 1,
934 balance: U256::ZERO,
935 code_hash,
936 code: Some(Bytecode::new_raw(revert_code)),
937 account_id: None,
938 },
939 );
940
941 let tx = Recovered::new_unchecked(
942 sign_tx_with_key_pair(
943 alice_kp,
944 Transaction::Legacy(TxLegacy {
945 chain_id: Some(MAINNET.chain.id()),
946 nonce: 0,
947 gas_price: 1,
948 gas_limit: 50_000,
949 to: TxKind::Call(revert_contract),
950 value: U256::ZERO,
951 input: Default::default(),
952 }),
953 ),
954 alice,
955 );
956
957 assert_shadow_equal(evm_config, db, empty_amsterdam_block(B256::ZERO), vec![tx]);
958 }
959
960 #[test]
961 fn shadow_tx_with_sstore() {
962 use alloy_consensus::TxLegacy;
970 use alloy_primitives::{Bytes, TxKind};
971 use reth_chainspec::MAINNET;
972 use reth_ethereum_primitives::Transaction;
973 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
974 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
975 use revm::primitives::keccak256;
976
977 let evm_config = EthEvmConfig::mainnet();
978 let sstore_contract: alloy_primitives::Address =
979 alloy_primitives::Address::from([0x55; 20]);
980 let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
981
982 let alice_kp = generate_key(&mut rng());
983 let alice = public_key_to_address(alice_kp.public_key());
984
985 let sstore_code: Bytes = Bytes::from_static(&[0x60, 0x42, 0x60, 0x00, 0x55, 0x00]);
987 let code_hash = keccak256(&sstore_code);
988 let mut db = system_contracts_db();
989 insert_funded(&mut db, alice, sender_balance);
990 db.insert_account_info(
991 sstore_contract,
992 AccountInfo {
993 nonce: 1,
994 balance: U256::ZERO,
995 code_hash,
996 code: Some(Bytecode::new_raw(sstore_code)),
997 account_id: None,
998 },
999 );
1000
1001 let tx = Recovered::new_unchecked(
1002 sign_tx_with_key_pair(
1003 alice_kp,
1004 Transaction::Legacy(TxLegacy {
1005 chain_id: Some(MAINNET.chain.id()),
1006 nonce: 0,
1007 gas_price: 1,
1008 gas_limit: 100_000,
1009 to: TxKind::Call(sstore_contract),
1010 value: U256::ZERO,
1011 input: Default::default(),
1012 }),
1013 ),
1014 alice,
1015 );
1016
1017 assert_shadow_equal(evm_config, db, empty_amsterdam_block(B256::ZERO), vec![tx]);
1018 }
1019
1020 #[test]
1021 fn rejects_final_hash_mismatch() {
1022 use alloy_eip7928::AccountChanges;
1026
1027 let evm_config = EthEvmConfig::mainnet();
1028
1029 let real_bal = reference_bal_for_empty_block(&evm_config);
1031 assert!(!real_bal.is_empty(), "reference BAL must be non-empty");
1032
1033 let phantom = alloy_primitives::Address::from([0xFF; 20]);
1035 let mut tampered_entries: Vec<AccountChanges> = real_bal;
1036 tampered_entries.push(AccountChanges::new(phantom));
1037 let tampered_bal: alloy_eip7928::bal::Bal = alloy_eip7928::bal::Bal::new(tampered_entries);
1038
1039 let tampered_block_access_list: BlockAccessList = tampered_bal.clone().into();
1041 let tampered_hash =
1042 alloy_eip7928::compute_block_access_list_hash(&tampered_block_access_list);
1043 let block = empty_amsterdam_block(tampered_hash);
1044
1045 let received = {
1046 let raw = alloy_rlp::encode(&tampered_bal).into();
1047 Arc::new(DecodedBal::new(tampered_bal, raw))
1048 };
1049
1050 let result = run_execute_block(
1051 &Runtime::test(),
1052 evm_config,
1053 db_factory(system_contracts_db()),
1054 received,
1055 &block,
1056 Vec::<Recovered<TransactionSigned>>::new(),
1057 );
1058
1059 match result {
1060 Err(BalExecutionError::Reject(RejectReason::FinalHashMismatch {
1061 rebuilt,
1062 expected,
1063 })) => {
1064 assert_ne!(rebuilt, expected, "rebuilt and expected hashes must differ");
1065 }
1066 Err(e) => panic!("expected FinalHashMismatch, got {e:?}"),
1067 Ok(_) => panic!("expected FinalHashMismatch, got Ok"),
1068 }
1069 }
1070
1071 #[test]
1072 fn canonical_make_db_failure() {
1073 let evm_config = EthEvmConfig::mainnet();
1076 let block = empty_amsterdam_block(B256::ZERO);
1077
1078 let failing_make_db = || -> Result<CacheDB<EmptyDB>, BalExecutionError> {
1079 Err(reth_provider::ProviderError::BestBlockNotFound.into())
1080 };
1081
1082 let result = run_execute_block(
1083 &Runtime::test(),
1084 evm_config,
1085 failing_make_db,
1086 to_arc_decoded(BlockAccessList::default()),
1087 &block,
1088 Vec::<Recovered<TransactionSigned>>::new(),
1089 );
1090
1091 assert!(
1092 matches!(result, Err(BalExecutionError::Provider(_))),
1093 "expected Provider error from canonical make_db failure, got {result:?}",
1094 );
1095 }
1096
1097 #[test]
1098 fn worker_tx_recovery_error_becomes_evm_error() {
1099 let evm_config = EthEvmConfig::mainnet();
1103 let block = empty_amsterdam_block(B256::ZERO);
1104
1105 let (tx_tx, tx_rx) = crossbeam_channel::unbounded::<(
1106 usize,
1107 Result<Recovered<TransactionSigned>, std::io::Error>,
1108 )>();
1109 tx_tx.send((0, Err(std::io::Error::other("sig fail")))).unwrap();
1110 drop(tx_tx);
1111
1112 let (receipt_tx, _receipt_rx) = crossbeam_channel::unbounded();
1113 let evm_env = evm_config.evm_env(block.header()).unwrap();
1114 let execution_ctx = evm_config.context_for_block(&block).unwrap();
1115
1116 let result = execute_block(
1117 &Runtime::test(),
1118 &evm_config,
1119 &db_factory(system_contracts_db()),
1120 to_arc_decoded(BlockAccessList::default()),
1121 evm_env,
1122 execution_ctx,
1123 1, tx_rx,
1125 receipt_tx,
1126 );
1127
1128 assert!(
1129 matches!(result, Err(BalExecutionError::Evm(_))),
1130 "expected Evm error from tx recovery failure, got {result:?}",
1131 );
1132 }
1133
1134 #[test]
1135 fn gas_tracker_non_amsterdam_uses_cumulative_gas() {
1136 use revm::context::result::{
1140 ExecResultAndState, ExecutionResult, Output, ResultGas, SuccessReason,
1141 };
1142 use revm_state::EvmState;
1143
1144 let block_gas_limit = 1_000_000u64;
1145 let first_tx_gas = 600_000u64;
1146 let second_tx_gas_limit = 500_000u64; let gas = ResultGas::new_with_state_gas(first_tx_gas, 0, 0, first_tx_gas);
1149 let fake_result: ResultAndState<revm::context::result::HaltReason> =
1150 ExecResultAndState::new(
1151 ExecutionResult::Success {
1152 reason: SuccessReason::Return,
1153 gas,
1154 logs: vec![],
1155 output: Output::Call(Default::default()),
1156 },
1157 EvmState::default(),
1158 );
1159
1160 let mut non_amsterdam = BlockGasTracker::new(block_gas_limit, false);
1162 non_amsterdam.record_result(&fake_result);
1163 assert!(
1164 non_amsterdam.validate_tx_limit(second_tx_gas_limit).is_err(),
1165 "non-Amsterdam tracker must reject tx that exceeds remaining cumulative gas",
1166 );
1167
1168 let mut amsterdam = BlockGasTracker::new(block_gas_limit, true);
1170 amsterdam.record_result(&fake_result);
1171 assert!(
1172 amsterdam.validate_tx_limit(second_tx_gas_limit).is_ok(),
1173 "Amsterdam tracker must accept the same tx since block_regular_gas_used stays 0",
1174 );
1175 }
1176
1177 #[test]
1178 fn gas_tracker_caps_oversized_tx_gas_limit_at_tx_gas_limit_cap() {
1179 use revm::context::result::{
1182 ExecResultAndState, ExecutionResult, Output, ResultGas, SuccessReason,
1183 };
1184 use revm_state::EvmState;
1185
1186 let block_gas_limit = 30_000_000u64;
1187 let oversized = TX_GAS_LIMIT_CAP + 1_000_000; let tracker = BlockGasTracker::new(block_gas_limit, false);
1192 assert!(
1193 tracker.validate_tx_limit(oversized).is_ok(),
1194 "oversized tx must pass when capped limit fits in block gas",
1195 );
1196
1197 let prior_gas = 20_000_000u64;
1200 let gas = ResultGas::new_with_state_gas(prior_gas, 0, 0, prior_gas);
1201 let fake_result: ResultAndState<revm::context::result::HaltReason> =
1202 ExecResultAndState::new(
1203 ExecutionResult::Success {
1204 reason: SuccessReason::Return,
1205 gas,
1206 logs: vec![],
1207 output: Output::Call(Default::default()),
1208 },
1209 EvmState::default(),
1210 );
1211
1212 let mut tracker = BlockGasTracker::new(block_gas_limit, false);
1213 tracker.record_result(&fake_result);
1214 assert!(
1215 tracker.validate_tx_limit(oversized).is_err(),
1216 "oversized tx must be rejected when capped limit exceeds remaining block gas",
1217 );
1218 }
1219}