1use super::{ordered_outputs::ordered_worker_outputs, worker, BalExecutionError};
18use alloy_eip7928::{
19 bal::{Bal as AlloyBal, DecodedBal},
20 compute_block_access_list_hash, BlockAccessList,
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 state::bal::Bal as RevmBal,
36};
37use std::sync::Arc;
38
39use crate::tree::payload_processor::receipt_root_task::IndexedReceipt;
40
41#[expect(clippy::too_many_arguments, clippy::type_complexity)]
43pub fn execute_block<'a, Evm, Tx, Err, DB, MakeDb>(
44 runtime: &Runtime,
45 evm_config: &'a Evm,
46 make_db: &'a MakeDb,
47 input_bal: Arc<DecodedBal>,
48 evm_env: EvmEnvFor<Evm>,
49 ctx: ExecutionCtxFor<'a, Evm>,
50 transaction_count: usize,
51 txs: Receiver<(usize, Result<Tx, Err>)>,
52 receipt_tx: Sender<IndexedReceipt<ReceiptTy<Evm::Primitives>>>,
53) -> Result<
54 (BlockExecutionOutput<ReceiptTy<Evm::Primitives>>, Vec<Address>, BlockAccessList),
55 BalExecutionError,
56>
57where
58 Evm: ConfigureEvm + 'static,
59 Tx: ExecutableTxFor<Evm> + Send + 'a,
60 Err: core::error::Error + Send + Sync + 'static,
61 DB: Database + Send + 'a,
62 MakeDb: Fn(bool) -> Result<DB, BalExecutionError> + Sync + 'a,
63 ReceiptTy<Evm::Primitives>: Clone,
64{
65 let worker_pool = runtime.bal_streaming_pool();
66 let worker_count = worker_pool.current_num_threads().max(1).min(transaction_count);
67
68 worker_pool.in_place_scope(|scope| {
69 execute_block_inner(
70 scope,
71 evm_config,
72 make_db,
73 input_bal,
74 evm_env,
75 ctx,
76 transaction_count,
77 txs,
78 receipt_tx,
79 worker_count,
80 )
81 })
82}
83
84#[expect(clippy::too_many_arguments, clippy::type_complexity)]
85fn execute_block_inner<'scope, Evm, Tx, Err, DB, MakeDb>(
86 scope: &rayon::Scope<'scope>,
87 evm_config: &'scope Evm,
88 make_db: &'scope MakeDb,
89 input_bal: Arc<DecodedBal>,
90 evm_env: EvmEnvFor<Evm>,
91 ctx: ExecutionCtxFor<'scope, Evm>,
92 transaction_count: usize,
93 txs: Receiver<(usize, Result<Tx, Err>)>,
94 receipt_tx: Sender<IndexedReceipt<ReceiptTy<Evm::Primitives>>>,
95 worker_count: usize,
96) -> Result<
97 (BlockExecutionOutput<ReceiptTy<Evm::Primitives>>, Vec<Address>, BlockAccessList),
98 BalExecutionError,
99>
100where
101 Evm: ConfigureEvm + 'scope,
102 Tx: ExecutableTxFor<Evm> + Send + 'scope,
103 Err: core::error::Error + Send + Sync + 'static,
104 DB: Database + Send + 'scope,
105 MakeDb: Fn(bool) -> Result<DB, BalExecutionError> + Sync + 'scope,
106 ReceiptTy<Evm::Primitives>: Clone,
107{
108 let bal = input_bal.as_bal();
109 let input_bal_revm = convert_alloy_to_revm_bal(bal)?;
110
111 let block_gas_limit = evm_env.block_env.gas_limit();
112 let enable_amsterdam_eip8037 = evm_env.cfg_env.enable_amsterdam_eip8037;
113 let tx_gas_limit_cap = evm_env.cfg_env.tx_gas_limit_cap;
114 let mut canonical_state = State::builder()
115 .with_database(make_db(false)?)
116 .with_bundle_update()
117 .with_bal_builder()
118 .build();
119
120 let (block_result, senders) = {
121 let (result_tx, result_rx) = crossbeam_channel::unbounded();
122 let (abort_guard, abort_rx) = AbortGuard::new();
123
124 for _ in 0..worker_count {
125 worker::spawn_worker(
126 scope,
127 txs.clone(),
128 abort_rx.clone(),
129 result_tx.clone(),
130 evm_config,
131 make_db,
132 Arc::clone(&input_bal_revm),
133 evm_env.clone(),
134 ctx.clone(),
135 );
136 }
137 drop(result_tx);
138
139 let mut gas_tracker =
140 BlockGasTracker::new(block_gas_limit, enable_amsterdam_eip8037, tx_gas_limit_cap);
141 let evm = evm_config.evm_with_env(&mut canonical_state, evm_env);
142 let mut canonical_executor = evm_config.create_executor_with_state(evm, ctx.clone());
143
144 canonical_executor.apply_pre_execution_changes()?;
145 let mut senders = Vec::with_capacity(transaction_count);
146 let mut last_sent_len = 0usize;
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 let built_bal = take_built_bal_and_log_divergence(&mut canonical_state, bal);
174
175 canonical_state.merge_transitions(BundleRetention::Reverts);
176 Ok((
177 BlockExecutionOutput { state: canonical_state.take_bundle(), result: block_result },
178 senders,
179 built_bal,
180 ))
181}
182
183fn convert_alloy_to_revm_bal(alloy_bal: &AlloyBal) -> Result<Arc<RevmBal>, BalExecutionError> {
184 let received_bal_revm = RevmBal::clone_from_alloy(alloy_bal.as_vec()).map_err(|e| {
197 BalExecutionError::Consensus(reth_consensus::ConsensusError::BlockAccessListInvalid(
198 format!("{e:?}"),
199 ))
200 })?;
201 Ok(Arc::new(received_bal_revm))
202}
203
204fn take_built_bal_and_log_divergence<DB>(
205 canonical_state: &mut State<DB>,
206 received_bal: &AlloyBal,
207) -> BlockAccessList
208where
209 DB: Database,
210{
211 let built_bal = canonical_state.take_built_alloy_bal().expect("with_bal_builder set");
212 if tracing::enabled!(target: "engine::tree::payload_processor::bal", tracing::Level::DEBUG) &&
213 built_bal.as_slice() != received_bal.as_slice()
214 {
215 let rebuilt = compute_block_access_list_hash(built_bal.as_slice());
216 let expected = compute_block_access_list_hash(received_bal.as_slice());
217 let div = received_bal.diff(built_bal.as_slice());
218 tracing::debug!(
219 target: "engine::tree::payload_processor::bal",
220 %rebuilt,
221 %expected,
222 %div,
223 "first BAL divergence",
224 );
225 }
226
227 built_bal
228}
229
230struct AbortGuard {
232 _tx: Sender<()>,
233}
234
235impl AbortGuard {
236 fn new() -> (Self, Receiver<()>) {
237 let (tx, rx) = crossbeam_channel::bounded(0);
238 (Self { _tx: tx }, rx)
239 }
240}
241
242#[derive(Debug)]
244struct BlockGasTracker {
245 block_gas_limit: u64,
246 enable_amsterdam_eip8037: bool,
247 tx_gas_limit_cap: Option<u64>,
248 cumulative_tx_gas_used: u64,
249 block_regular_gas_used: u64,
250 block_state_gas_used: u64,
251}
252
253impl BlockGasTracker {
254 const fn new(
255 block_gas_limit: u64,
256 enable_amsterdam_eip8037: bool,
257 tx_gas_limit_cap: Option<u64>,
258 ) -> Self {
259 Self {
260 block_gas_limit,
261 enable_amsterdam_eip8037,
262 tx_gas_limit_cap,
263 cumulative_tx_gas_used: 0,
264 block_regular_gas_used: 0,
265 block_state_gas_used: 0,
266 }
267 }
268
269 fn validate_tx_limit(&self, tx_gas_limit: u64) -> Result<(), BlockExecutionError> {
286 let block_gas_used = if self.enable_amsterdam_eip8037 {
287 self.block_regular_gas_used
288 } else {
289 self.cumulative_tx_gas_used
290 };
291 let block_available_gas = self.block_gas_limit.saturating_sub(block_gas_used);
292 let tx_min_gas_limit =
293 self.tx_gas_limit_cap.map_or(tx_gas_limit, |cap| tx_gas_limit.min(cap));
294
295 if tx_min_gas_limit > block_available_gas {
296 return Err(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
297 transaction_gas_limit: tx_gas_limit,
298 block_available_gas,
299 }
300 .into());
301 }
302
303 if self.enable_amsterdam_eip8037 {
304 let state_gas_available =
305 self.block_gas_limit.saturating_sub(self.block_state_gas_used);
306 if tx_gas_limit > state_gas_available {
307 return Err(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
308 transaction_gas_limit: tx_gas_limit,
309 block_available_gas: state_gas_available,
310 }
311 .into());
312 }
313 }
314
315 Ok(())
316 }
317
318 const fn record_result<H>(&mut self, result: &ResultAndState<H>) {
319 let gas = result.result.gas();
320 self.cumulative_tx_gas_used = self.cumulative_tx_gas_used.saturating_add(gas.tx_gas_used());
321 self.block_regular_gas_used =
322 self.block_regular_gas_used.saturating_add(gas.block_regular_gas_used());
323 self.block_state_gas_used =
324 self.block_state_gas_used.saturating_add(gas.block_state_gas_used());
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331 use alloy_consensus::{BlockHeader, Header};
332 use alloy_eip7928::{bal::Bal as AlloyBal, BlockAccessList};
333 use alloy_eips::{
334 eip2935::{HISTORY_STORAGE_ADDRESS, HISTORY_STORAGE_CODE},
335 eip4788::{BEACON_ROOTS_ADDRESS, BEACON_ROOTS_CODE},
336 eip7002::{WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, WITHDRAWAL_REQUEST_PREDEPLOY_CODE},
337 };
338 use alloy_primitives::{keccak256, B256, U256};
339 use reth_ethereum_primitives::{Block, BlockBody, Receipt, TransactionSigned};
340 use reth_evm_ethereum::EthEvmConfig;
341 use reth_primitives_traits::{Block as _, Recovered, SealedBlock};
342 use reth_revm::db::BundleState;
343 use reth_tasks::Runtime;
344 use revm::{
345 database::{CacheDB, EmptyDB},
346 state::{AccountInfo, Bytecode},
347 };
348 use std::convert::Infallible;
349
350 fn to_arc_decoded(bal: BlockAccessList) -> Arc<DecodedBal> {
352 let alloy_bal: AlloyBal = bal.into();
353 let raw = alloy_rlp::encode(&alloy_bal).into();
354 Arc::new(DecodedBal::new(alloy_bal, raw))
355 }
356
357 fn system_contracts_db() -> CacheDB<EmptyDB> {
361 let mut db = CacheDB::<EmptyDB>::new(Default::default());
362 db.insert_account_info(
363 BEACON_ROOTS_ADDRESS,
364 AccountInfo {
365 balance: U256::ZERO,
366 nonce: 1,
367 code_hash: keccak256(BEACON_ROOTS_CODE.clone()),
368 code: Some(Bytecode::new_raw(BEACON_ROOTS_CODE.clone())),
369 account_id: None,
370 },
371 );
372 db.insert_account_info(
373 WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS,
374 AccountInfo {
375 balance: U256::ZERO,
376 nonce: 1,
377 code_hash: keccak256(WITHDRAWAL_REQUEST_PREDEPLOY_CODE.clone()),
378 code: Some(Bytecode::new_raw(WITHDRAWAL_REQUEST_PREDEPLOY_CODE.clone())),
379 account_id: None,
380 },
381 );
382 db.insert_account_info(
383 HISTORY_STORAGE_ADDRESS,
384 AccountInfo {
385 balance: U256::ZERO,
386 nonce: 1,
387 code_hash: keccak256(HISTORY_STORAGE_CODE.clone()),
388 code: Some(Bytecode::new_raw(HISTORY_STORAGE_CODE.clone())),
389 account_id: None,
390 },
391 );
392 db
393 }
394
395 fn empty_amsterdam_block(header_bal_hash: B256) -> SealedBlock<Block> {
397 empty_amsterdam_block_with_gas_limit(header_bal_hash, 30_000_000)
398 }
399
400 fn empty_amsterdam_block_with_gas_limit(
401 header_bal_hash: B256,
402 gas_limit: u64,
403 ) -> SealedBlock<Block> {
404 let header = Header {
405 timestamp: 1,
406 number: 1,
407 gas_limit,
408 parent_beacon_block_root: Some(B256::ZERO),
409 withdrawals_root: Some(alloy_consensus::EMPTY_ROOT_HASH),
410 requests_hash: Some(alloy_eips::eip7685::EMPTY_REQUESTS_HASH),
411 excess_blob_gas: Some(0),
412 blob_gas_used: Some(0),
413 block_access_list_hash: Some(header_bal_hash),
414 ..Header::default()
415 };
416 let block = Block {
417 header,
418 body: BlockBody {
419 transactions: vec![],
420 ommers: vec![],
421 withdrawals: Some(vec![].into()),
422 },
423 };
424 block.seal_slow()
425 }
426
427 fn reference_bal_for_empty_block(evm_config: &EthEvmConfig) -> BlockAccessList {
434 use revm::database::State as RevmState;
435
436 let db = system_contracts_db();
437 let mut state =
438 RevmState::builder().with_database(db).with_bundle_update().with_bal_builder().build();
439
440 let block = empty_amsterdam_block(B256::ZERO);
442 {
443 let mut executor =
444 evm_config.executor_for_block(&mut state, &block).expect("build executor");
445 executor.apply_pre_execution_changes().expect("pre-exec");
446 executor.evm_mut().db_mut().bump_bal_index();
447 executor.apply_post_execution_changes().expect("post-exec");
448 }
449 state.take_built_alloy_bal().expect("with_bal_builder was set")
450 }
451
452 #[test]
453 fn empty_block_happy_path_round_trip() {
454 let evm_config = EthEvmConfig::mainnet();
460
461 let input_bal = reference_bal_for_empty_block(&evm_config);
462 let bal_hash = alloy_eip7928::compute_block_access_list_hash(&input_bal);
463 assert!(!input_bal.is_empty(), "empty BAL means system calls didn't record state");
465
466 let block = empty_amsterdam_block(bal_hash);
467
468 let result = run_execute_block(
469 &Runtime::test(),
470 evm_config,
471 db_factory(system_contracts_db()),
472 to_arc_decoded(input_bal),
473 &block,
474 Vec::<Recovered<TransactionSigned>>::new(),
475 );
476
477 match result {
478 Ok(output) => {
479 assert!(output.receipts.is_empty(), "empty block → no receipts");
480 }
481 Err(e) => panic!("expected success, got {e:?}"),
482 }
483 }
484
485 fn db_factory(
486 db: CacheDB<EmptyDB>,
487 ) -> impl Fn() -> Result<CacheDB<EmptyDB>, BalExecutionError> + Sync {
488 move || Ok(db.clone())
489 }
490
491 fn tx_stream<Tx>(txs: Vec<Tx>) -> Receiver<(usize, Result<Tx, Infallible>)> {
492 let (tx, rx) = crossbeam_channel::unbounded();
493 for (index, transaction) in txs.into_iter().enumerate() {
494 tx.send((index, Ok(transaction))).unwrap();
495 }
496 rx
497 }
498
499 fn run_execute_block<Tx, DB, MakeDb>(
500 runtime: &Runtime,
501 evm_config: EthEvmConfig,
502 make_db: MakeDb,
503 input_bal: Arc<DecodedBal>,
504 block: &SealedBlock<Block>,
505 txs: Vec<Tx>,
506 ) -> Result<BlockExecutionOutput<Receipt>, BalExecutionError>
507 where
508 Tx: ExecutableTxFor<EthEvmConfig> + Send,
509 DB: Database + Send,
510 MakeDb: Fn() -> Result<DB, BalExecutionError> + Sync,
511 {
512 run_execute_block_full(runtime, evm_config, make_db, input_bal, block, txs)
513 .map(|(output, _)| output)
514 }
515
516 fn run_execute_block_full<Tx, DB, MakeDb>(
517 runtime: &Runtime,
518 evm_config: EthEvmConfig,
519 make_db: MakeDb,
520 input_bal: Arc<DecodedBal>,
521 block: &SealedBlock<Block>,
522 txs: Vec<Tx>,
523 ) -> Result<(BlockExecutionOutput<Receipt>, BlockAccessList), BalExecutionError>
524 where
525 Tx: ExecutableTxFor<EthEvmConfig> + Send,
526 DB: Database + Send,
527 MakeDb: Fn() -> Result<DB, BalExecutionError> + Sync,
528 {
529 let transaction_count = txs.len();
530 let (receipt_tx, _receipt_rx) = crossbeam_channel::unbounded();
531 let evm_env = evm_config.evm_env(block.header()).unwrap();
532 let execution_ctx = evm_config.context_for_block(block).unwrap();
533 let make_db = |_: bool| make_db();
534 execute_block(
535 runtime,
536 &evm_config,
537 &make_db,
538 input_bal,
539 evm_env,
540 execution_ctx,
541 transaction_count,
542 tx_stream(txs),
543 receipt_tx,
544 )
545 .map(|(output, _, built_bal)| (output, built_bal))
546 }
547
548 fn insert_funded(db: &mut CacheDB<EmptyDB>, addr: alloy_primitives::Address, balance: U256) {
550 db.insert_account_info(
551 addr,
552 AccountInfo { nonce: 0, balance, code_hash: B256::ZERO, code: None, account_id: None },
553 );
554 }
555
556 fn reference_bal_for_block<Tx>(
559 evm_config: &EthEvmConfig,
560 mut db: CacheDB<EmptyDB>,
561 block: &SealedBlock<Block>,
562 txs: Vec<Tx>,
563 ) -> BlockAccessList
564 where
565 Tx: ExecutableTxFor<EthEvmConfig>,
566 {
567 use revm::database::State as RevmState;
568
569 let mut state = RevmState::builder()
570 .with_database(&mut db)
571 .with_bundle_update()
572 .with_bal_builder()
573 .build();
574
575 {
576 let mut executor =
577 evm_config.executor_for_block(&mut state, block).expect("build executor");
578 executor.apply_pre_execution_changes().expect("pre-exec");
579 for (i, tx) in txs.into_iter().enumerate() {
580 executor.evm_mut().db_mut().bump_bal_index();
581 executor
582 .execute_transaction(tx)
583 .unwrap_or_else(|e| panic!("tx {i} failed during reference build: {e:?}"));
584 }
585 executor.evm_mut().db_mut().bump_bal_index();
586 executor.apply_post_execution_changes().expect("post-exec");
587 }
588 state.take_built_alloy_bal().expect("with_bal_builder was set")
589 }
590
591 #[test]
592 fn multi_tx_happy_path_round_trip() {
593 use alloy_consensus::TxLegacy;
601 use alloy_primitives::TxKind;
602 use reth_chainspec::MAINNET;
603 use reth_ethereum_primitives::Transaction;
604 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
605 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
606
607 let evm_config = EthEvmConfig::mainnet();
608 let carol: alloy_primitives::Address = alloy_primitives::Address::from([0xCA; 20]);
609 let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
610
611 let alice_kp = generate_key(&mut rng());
613 let alice = public_key_to_address(alice_kp.public_key());
614 let bob_kp = generate_key(&mut rng());
615 let bob = public_key_to_address(bob_kp.public_key());
616
617 let mut pre_block_db = system_contracts_db();
619 insert_funded(&mut pre_block_db, alice, sender_balance);
620 insert_funded(&mut pre_block_db, bob, sender_balance);
621
622 let chain_id = MAINNET.chain.id();
624 let gas_price = 1u128; let tx1 = sign_tx_with_key_pair(
626 alice_kp,
627 Transaction::Legacy(TxLegacy {
628 chain_id: Some(chain_id),
629 nonce: 0,
630 gas_price,
631 gas_limit: 21_000,
632 to: TxKind::Call(carol),
633 value: U256::from(100u64),
634 input: Default::default(),
635 }),
636 );
637 let tx2 = sign_tx_with_key_pair(
638 bob_kp,
639 Transaction::Legacy(TxLegacy {
640 chain_id: Some(chain_id),
641 nonce: 0,
642 gas_price,
643 gas_limit: 21_000,
644 to: TxKind::Call(carol),
645 value: U256::from(200u64),
646 input: Default::default(),
647 }),
648 );
649 let recovered1 = Recovered::new_unchecked(tx1, alice);
650 let recovered2 = Recovered::new_unchecked(tx2, bob);
651
652 let block_for_ref = empty_amsterdam_block(B256::ZERO);
654 let reference_bal = reference_bal_for_block::<Recovered<TransactionSigned>>(
655 &evm_config,
656 {
657 let mut db = system_contracts_db();
659 db.insert_account_info(
660 alice,
661 AccountInfo {
662 nonce: 0,
663 balance: sender_balance,
664 code_hash: B256::ZERO,
665 code: None,
666 account_id: None,
667 },
668 );
669 db.insert_account_info(
670 bob,
671 AccountInfo {
672 nonce: 0,
673 balance: sender_balance,
674 code_hash: B256::ZERO,
675 code: None,
676 account_id: None,
677 },
678 );
679 db
680 },
681 &block_for_ref,
682 vec![recovered1.clone(), recovered2.clone()],
683 );
684 assert!(!reference_bal.is_empty(), "expected BAL entries from pre-exec + txs");
685
686 let bal_hash = alloy_eip7928::compute_block_access_list_hash(&reference_bal);
687 let block = empty_amsterdam_block(bal_hash);
688
689 let result = run_execute_block(
690 &Runtime::test(),
691 evm_config,
692 db_factory(pre_block_db),
693 to_arc_decoded(reference_bal),
694 &block,
695 vec![recovered1, recovered2],
696 );
697
698 match result {
699 Ok(output) => {
700 assert_eq!(output.receipts.len(), 2, "expected 2 receipts");
701 assert!(output.gas_used >= 2 * 21_000, "expected at least 42k gas used");
702 }
703 Err(e) => panic!("expected success, got {e:?}"),
704 }
705 }
706
707 #[derive(Debug)]
715 struct ShadowOutput {
716 bundle_state: BundleState,
717 receipts: Vec<reth_ethereum_primitives::Receipt>,
718 gas_used: u64,
719 requests: alloy_eips::eip7685::Requests,
720 }
721
722 fn run_serial_path(
727 evm_config: &EthEvmConfig,
728 canonical_db: CacheDB<EmptyDB>,
729 block: &SealedBlock<Block>,
730 txs: &[Recovered<TransactionSigned>],
731 ) -> (ShadowOutput, BlockAccessList) {
732 use revm::database::State as RevmState;
733
734 let mut state = RevmState::builder()
735 .with_database(canonical_db)
736 .with_bundle_update()
737 .with_bal_builder()
738 .build();
739
740 let block_result = {
741 let mut executor =
742 evm_config.executor_for_block(&mut state, block).expect("build serial executor");
743 executor.apply_pre_execution_changes().expect("serial pre-exec");
744 for (i, tx) in txs.iter().cloned().enumerate() {
745 executor.evm_mut().db_mut().bump_bal_index();
746 executor
747 .execute_transaction(tx)
748 .unwrap_or_else(|e| panic!("serial tx {i} failed: {e:?}"));
749 }
750 executor.evm_mut().db_mut().bump_bal_index();
751 executor.apply_post_execution_changes().expect("serial post-exec")
752 };
753
754 let bal = state.take_built_alloy_bal().expect("with_bal_builder was set");
755 state.merge_transitions(BundleRetention::Reverts);
756 let bundle_state = state.take_bundle();
757
758 (
759 ShadowOutput {
760 bundle_state,
761 receipts: block_result.receipts,
762 gas_used: block_result.gas_used,
763 requests: block_result.requests,
764 },
765 bal,
766 )
767 }
768
769 fn assert_shadow_equal(
771 evm_config: EthEvmConfig,
772 canonical_db_template: CacheDB<EmptyDB>,
773 block_header_only: SealedBlock<Block>,
774 txs: Vec<Recovered<TransactionSigned>>,
775 ) {
776 let (serial, reference_bal) =
778 run_serial_path(&evm_config, canonical_db_template.clone(), &block_header_only, &txs);
779
780 let bal_hash = alloy_eip7928::compute_block_access_list_hash(&reference_bal);
782 let block =
783 empty_amsterdam_block_with_gas_limit(bal_hash, block_header_only.header().gas_limit());
784
785 let bal_out = run_execute_block(
786 &Runtime::test(),
787 evm_config,
788 db_factory(canonical_db_template),
789 to_arc_decoded(reference_bal),
790 &block,
791 txs,
792 )
793 .unwrap_or_else(|e| panic!("BAL path failed: {e:?}"));
794
795 assert_eq!(
797 serial.receipts, bal_out.receipts,
798 "receipts diverge between serial and BAL paths",
799 );
800 assert_eq!(
801 serial.gas_used, bal_out.gas_used,
802 "gas_used differs: serial {} vs bal {}",
803 serial.gas_used, bal_out.gas_used,
804 );
805 assert_eq!(
806 serial.requests, bal_out.requests,
807 "requests (EIP-7685) diverge between serial and BAL paths",
808 );
809 assert_eq!(
810 serial.bundle_state, bal_out.state,
811 "bundle_state diverges — the canonical state transitions don't match",
812 );
813 }
814
815 #[test]
816 fn shadow_empty_block() {
817 assert_shadow_equal(
820 EthEvmConfig::mainnet(),
821 system_contracts_db(),
822 empty_amsterdam_block(B256::ZERO),
823 Vec::new(),
824 );
825 }
826
827 #[test]
828 fn shadow_multi_value_transfer() {
829 use alloy_consensus::TxLegacy;
832 use alloy_primitives::TxKind;
833 use reth_chainspec::MAINNET;
834 use reth_ethereum_primitives::Transaction;
835 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
836 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
837
838 let evm_config = EthEvmConfig::mainnet();
839 let carol: alloy_primitives::Address = alloy_primitives::Address::from([0xCA; 20]);
840 let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
841
842 let alice_kp = generate_key(&mut rng());
843 let alice = public_key_to_address(alice_kp.public_key());
844 let bob_kp = generate_key(&mut rng());
845 let bob = public_key_to_address(bob_kp.public_key());
846
847 let mut db = system_contracts_db();
848 insert_funded(&mut db, alice, sender_balance);
849 insert_funded(&mut db, bob, sender_balance);
850
851 let chain_id = MAINNET.chain.id();
852 let make_tx = |kp, to, value, nonce: u64| {
853 sign_tx_with_key_pair(
854 kp,
855 Transaction::Legacy(TxLegacy {
856 chain_id: Some(chain_id),
857 nonce,
858 gas_price: 1,
859 gas_limit: 21_000,
860 to: TxKind::Call(to),
861 value: U256::from(value),
862 input: Default::default(),
863 }),
864 )
865 };
866 let tx1 = Recovered::new_unchecked(make_tx(alice_kp, carol, 100u64, 0), alice);
867 let tx2 = Recovered::new_unchecked(make_tx(bob_kp, carol, 200u64, 0), bob);
868
869 assert_shadow_equal(evm_config, db, empty_amsterdam_block(B256::ZERO), vec![tx1, tx2]);
870 }
871
872 #[test]
873 fn rejects_tx_gas_limit_that_exceeds_remaining_block_gas() {
874 use alloy_consensus::TxLegacy;
878 use alloy_evm::block::BlockValidationError;
879 use alloy_primitives::TxKind;
880 use reth_chainspec::MAINNET;
881 use reth_ethereum_primitives::Transaction;
882 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
883 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
884
885 let evm_config = EthEvmConfig::mainnet();
886 let carol: alloy_primitives::Address = alloy_primitives::Address::from([0xCA; 20]);
887 let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
888 let block_gas_limit = 1_000_000;
889 let tx_gas_limit = 990_000;
890
891 let alice_kp = generate_key(&mut rng());
892 let alice = public_key_to_address(alice_kp.public_key());
893 let bob_kp = generate_key(&mut rng());
894 let bob = public_key_to_address(bob_kp.public_key());
895
896 let mut pre_block_db = system_contracts_db();
897 insert_funded(&mut pre_block_db, alice, sender_balance);
898 insert_funded(&mut pre_block_db, bob, sender_balance);
899
900 let chain_id = MAINNET.chain.id();
901 let make_tx = |kp, value| {
902 sign_tx_with_key_pair(
903 kp,
904 Transaction::Legacy(TxLegacy {
905 chain_id: Some(chain_id),
906 nonce: 0,
907 gas_price: 1,
908 gas_limit: tx_gas_limit,
909 to: TxKind::Call(carol),
910 value: U256::from(value),
911 input: Default::default(),
912 }),
913 )
914 };
915 let tx1 = Recovered::new_unchecked(make_tx(alice_kp, 100u64), alice);
916 let tx2 = Recovered::new_unchecked(make_tx(bob_kp, 200u64), bob);
917
918 let reference_block = empty_amsterdam_block(B256::ZERO);
922 let reference_bal = reference_bal_for_block(
923 &evm_config,
924 pre_block_db.clone(),
925 &reference_block,
926 vec![tx1.clone(), tx2.clone()],
927 );
928 let bal_hash = alloy_eip7928::compute_block_access_list_hash(&reference_bal);
929 let low_gas_block = empty_amsterdam_block_with_gas_limit(bal_hash, block_gas_limit);
930
931 let result = run_execute_block(
932 &Runtime::test(),
933 evm_config,
934 db_factory(pre_block_db),
935 to_arc_decoded(reference_bal),
936 &low_gas_block,
937 vec![tx1, tx2],
938 );
939
940 match result {
941 Err(BalExecutionError::Execution(err)) => assert!(matches!(
942 err.as_validation(),
943 Some(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas { .. })
944 )),
945 Err(err) => panic!("expected block gas validation error, got {err:?}"),
946 Ok(_) => panic!("expected block gas validation error, got Ok"),
947 }
948 }
949
950 #[test]
951 fn shadow_tx_with_revert() {
952 use alloy_consensus::TxLegacy;
958 use alloy_primitives::{keccak256, Bytes, TxKind};
959 use reth_chainspec::MAINNET;
960 use reth_ethereum_primitives::Transaction;
961 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
962 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
963
964 let evm_config = EthEvmConfig::mainnet();
965 let revert_contract: alloy_primitives::Address =
966 alloy_primitives::Address::from([0xDE; 20]);
967 let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
968
969 let alice_kp = generate_key(&mut rng());
970 let alice = public_key_to_address(alice_kp.public_key());
971
972 let revert_code: Bytes = Bytes::from_static(&[0x60, 0x00, 0x60, 0x00, 0xfd]);
974 let code_hash = keccak256(&revert_code);
975 let mut db = system_contracts_db();
976 insert_funded(&mut db, alice, sender_balance);
977 db.insert_account_info(
978 revert_contract,
979 AccountInfo {
980 nonce: 1,
981 balance: U256::ZERO,
982 code_hash,
983 code: Some(Bytecode::new_raw(revert_code)),
984 account_id: None,
985 },
986 );
987
988 let tx = Recovered::new_unchecked(
989 sign_tx_with_key_pair(
990 alice_kp,
991 Transaction::Legacy(TxLegacy {
992 chain_id: Some(MAINNET.chain.id()),
993 nonce: 0,
994 gas_price: 1,
995 gas_limit: 50_000,
996 to: TxKind::Call(revert_contract),
997 value: U256::ZERO,
998 input: Default::default(),
999 }),
1000 ),
1001 alice,
1002 );
1003
1004 assert_shadow_equal(evm_config, db, empty_amsterdam_block(B256::ZERO), vec![tx]);
1005 }
1006
1007 #[test]
1008 fn shadow_tx_with_sstore() {
1009 use alloy_consensus::TxLegacy;
1015 use alloy_primitives::{keccak256, Bytes, TxKind};
1016 use reth_chainspec::MAINNET;
1017 use reth_ethereum_primitives::Transaction;
1018 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
1019 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
1020
1021 let evm_config = EthEvmConfig::mainnet();
1022 let sstore_contract: alloy_primitives::Address =
1023 alloy_primitives::Address::from([0x55; 20]);
1024 let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
1025
1026 let alice_kp = generate_key(&mut rng());
1027 let alice = public_key_to_address(alice_kp.public_key());
1028
1029 let sstore_code: Bytes = Bytes::from_static(&[0x60, 0x42, 0x60, 0x00, 0x55, 0x00]);
1031 let code_hash = keccak256(&sstore_code);
1032 let mut db = system_contracts_db();
1033 insert_funded(&mut db, alice, sender_balance);
1034 db.insert_account_info(
1035 sstore_contract,
1036 AccountInfo {
1037 nonce: 1,
1038 balance: U256::ZERO,
1039 code_hash,
1040 code: Some(Bytecode::new_raw(sstore_code)),
1041 account_id: None,
1042 },
1043 );
1044
1045 let tx = Recovered::new_unchecked(
1046 sign_tx_with_key_pair(
1047 alice_kp,
1048 Transaction::Legacy(TxLegacy {
1049 chain_id: Some(MAINNET.chain.id()),
1050 nonce: 0,
1051 gas_price: 1,
1052 gas_limit: 100_000,
1053 to: TxKind::Call(sstore_contract),
1054 value: U256::ZERO,
1055 input: Default::default(),
1056 }),
1057 ),
1058 alice,
1059 );
1060
1061 assert_shadow_equal(evm_config, db, empty_amsterdam_block(B256::ZERO), vec![tx]);
1062 }
1063
1064 #[test]
1065 fn returns_built_bal_for_final_hash_mismatch() {
1066 use alloy_eip7928::AccountChanges;
1070
1071 let evm_config = EthEvmConfig::mainnet();
1072
1073 let real_bal = reference_bal_for_empty_block(&evm_config);
1075 assert!(!real_bal.is_empty(), "reference BAL must be non-empty");
1076
1077 let phantom = alloy_primitives::Address::from([0xFF; 20]);
1079 let mut tampered_entries: Vec<AccountChanges> = real_bal;
1080 tampered_entries.push(AccountChanges::new(phantom));
1081 let tampered_bal: alloy_eip7928::bal::Bal = alloy_eip7928::bal::Bal::new(tampered_entries);
1082
1083 let tampered_block_access_list: BlockAccessList = tampered_bal.clone().into();
1085 let tampered_hash =
1086 alloy_eip7928::compute_block_access_list_hash(&tampered_block_access_list);
1087 let block = empty_amsterdam_block(tampered_hash);
1088
1089 let received = {
1090 let raw = alloy_rlp::encode(&tampered_bal).into();
1091 Arc::new(DecodedBal::new(tampered_bal, raw))
1092 };
1093
1094 let result = run_execute_block_full(
1095 &Runtime::test(),
1096 evm_config,
1097 db_factory(system_contracts_db()),
1098 received,
1099 &block,
1100 Vec::<Recovered<TransactionSigned>>::new(),
1101 );
1102
1103 match result {
1104 Ok((_, built_bal)) => {
1105 let rebuilt = alloy_eip7928::compute_block_access_list_hash(&built_bal);
1106 assert_ne!(rebuilt, tampered_hash, "rebuilt and header hashes must differ");
1107 }
1108 Err(e) => panic!("expected success with rebuilt BAL, got {e:?}"),
1109 }
1110 }
1111
1112 #[test]
1113 fn canonical_make_db_failure() {
1114 let evm_config = EthEvmConfig::mainnet();
1117 let block = empty_amsterdam_block(B256::ZERO);
1118
1119 let failing_make_db = || -> Result<CacheDB<EmptyDB>, BalExecutionError> {
1120 Err(reth_provider::ProviderError::BestBlockNotFound.into())
1121 };
1122
1123 let result = run_execute_block(
1124 &Runtime::test(),
1125 evm_config,
1126 failing_make_db,
1127 to_arc_decoded(BlockAccessList::default()),
1128 &block,
1129 Vec::<Recovered<TransactionSigned>>::new(),
1130 );
1131
1132 assert!(
1133 matches!(result, Err(BalExecutionError::Provider(_))),
1134 "expected Provider error from canonical make_db failure, got {result:?}",
1135 );
1136 }
1137
1138 #[test]
1139 fn worker_tx_recovery_error_becomes_other_error() {
1140 let evm_config = EthEvmConfig::mainnet();
1144 let block = empty_amsterdam_block(B256::ZERO);
1145
1146 let (tx_tx, tx_rx) = crossbeam_channel::unbounded::<(
1147 usize,
1148 Result<Recovered<TransactionSigned>, std::io::Error>,
1149 )>();
1150 tx_tx.send((0, Err(std::io::Error::other("sig fail")))).unwrap();
1151 drop(tx_tx);
1152
1153 let (receipt_tx, _receipt_rx) = crossbeam_channel::unbounded();
1154 let evm_env = evm_config.evm_env(block.header()).unwrap();
1155 let execution_ctx = evm_config.context_for_block(&block).unwrap();
1156 let make_db = db_factory(system_contracts_db());
1157 let make_db = |_: bool| make_db();
1158
1159 let result = execute_block(
1160 &Runtime::test(),
1161 &evm_config,
1162 &make_db,
1163 to_arc_decoded(BlockAccessList::default()),
1164 evm_env,
1165 execution_ctx,
1166 1, tx_rx,
1168 receipt_tx,
1169 );
1170
1171 assert!(
1172 matches!(result, Err(BalExecutionError::Other(_))),
1173 "expected Other error from tx recovery failure, got {result:?}",
1174 );
1175 }
1176
1177 #[test]
1178 fn gas_tracker_non_amsterdam_uses_cumulative_gas() {
1179 use revm::{
1184 context::result::{
1185 ExecResultAndState, ExecutionResult, Output, ResultGas, SuccessReason,
1186 },
1187 state::EvmState,
1188 };
1189
1190 let block_gas_limit = 1_000_000u64;
1191 let first_tx_gas = 600_000u64;
1192 let second_tx_gas_limit = 500_000u64; let gas = ResultGas::new_with_state_gas(first_tx_gas, 0, 0, first_tx_gas / 2);
1195 let fake_result: ResultAndState<revm::context::result::HaltReason> =
1196 ExecResultAndState::new(
1197 ExecutionResult::Success {
1198 reason: SuccessReason::Return,
1199 gas,
1200 logs: vec![],
1201 output: Output::Call(Default::default()),
1202 },
1203 EvmState::default(),
1204 );
1205
1206 let mut non_amsterdam = BlockGasTracker::new(block_gas_limit, false, None);
1208 non_amsterdam.record_result(&fake_result);
1209 assert!(
1210 non_amsterdam.validate_tx_limit(second_tx_gas_limit).is_err(),
1211 "non-Amsterdam tracker must reject tx that exceeds remaining cumulative gas",
1212 );
1213
1214 let mut amsterdam = BlockGasTracker::new(block_gas_limit, true, None);
1216 amsterdam.record_result(&fake_result);
1217 assert!(
1218 amsterdam.validate_tx_limit(second_tx_gas_limit).is_ok(),
1219 "Amsterdam tracker must accept the same tx since per-dimension budgets still fit",
1220 );
1221 }
1222
1223 #[test]
1224 fn gas_tracker_amsterdam_enforces_state_gas_budget() {
1225 use revm::{
1229 context::result::{
1230 ExecResultAndState, ExecutionResult, Output, ResultGas, SuccessReason,
1231 },
1232 state::EvmState,
1233 };
1234
1235 let block_gas_limit = 1_000_000u64;
1236 let first_tx_state_gas = 600_000u64;
1237 let second_tx_gas_limit = 500_000u64; let gas = ResultGas::new_with_state_gas(first_tx_state_gas, 0, 0, first_tx_state_gas);
1240 let fake_result: ResultAndState<revm::context::result::HaltReason> =
1241 ExecResultAndState::new(
1242 ExecutionResult::Success {
1243 reason: SuccessReason::Return,
1244 gas,
1245 logs: vec![],
1246 output: Output::Call(Default::default()),
1247 },
1248 EvmState::default(),
1249 );
1250
1251 let mut amsterdam = BlockGasTracker::new(block_gas_limit, true, None);
1254 amsterdam.record_result(&fake_result);
1255 assert!(
1256 amsterdam.validate_tx_limit(second_tx_gas_limit).is_err(),
1257 "Amsterdam tracker must reject tx whose gas limit exceeds the remaining state budget",
1258 );
1259 assert!(
1260 amsterdam.validate_tx_limit(400_000).is_ok(),
1261 "Amsterdam tracker must accept tx whose gas limit exactly fits the state budget",
1262 );
1263
1264 let mut capped = BlockGasTracker::new(block_gas_limit, true, Some(400_000));
1267 capped.record_result(&fake_result);
1268 assert!(
1269 capped.validate_tx_limit(second_tx_gas_limit).is_err(),
1270 "tx_gas_limit_cap must not bound the state-gas admission check",
1271 );
1272 }
1273
1274 #[test]
1275 fn gas_tracker_caps_oversized_tx_gas_limit_at_tx_gas_limit_cap() {
1276 use revm::{
1279 context::result::{
1280 ExecResultAndState, ExecutionResult, Output, ResultGas, SuccessReason,
1281 },
1282 primitives::eip7825::TX_GAS_LIMIT_CAP,
1283 state::EvmState,
1284 };
1285
1286 let block_gas_limit = 30_000_000u64;
1287 let oversized = TX_GAS_LIMIT_CAP + 1_000_000; let tracker = BlockGasTracker::new(block_gas_limit, false, Some(TX_GAS_LIMIT_CAP));
1292 assert!(
1293 tracker.validate_tx_limit(oversized).is_ok(),
1294 "oversized tx must pass when capped limit fits in block gas",
1295 );
1296
1297 let prior_gas = 20_000_000u64;
1300 let gas = ResultGas::new_with_state_gas(prior_gas, 0, 0, prior_gas);
1301 let fake_result: ResultAndState<revm::context::result::HaltReason> =
1302 ExecResultAndState::new(
1303 ExecutionResult::Success {
1304 reason: SuccessReason::Return,
1305 gas,
1306 logs: vec![],
1307 output: Output::Call(Default::default()),
1308 },
1309 EvmState::default(),
1310 );
1311
1312 let mut tracker = BlockGasTracker::new(block_gas_limit, false, Some(TX_GAS_LIMIT_CAP));
1313 tracker.record_result(&fake_result);
1314 assert!(
1315 tracker.validate_tx_limit(oversized).is_err(),
1316 "oversized tx must be rejected when capped limit exceeds remaining block gas",
1317 );
1318 }
1319}