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};
36use revm_state::bal::Bal as RevmBal;
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}
251
252impl BlockGasTracker {
253 const fn new(
254 block_gas_limit: u64,
255 enable_amsterdam_eip8037: bool,
256 tx_gas_limit_cap: Option<u64>,
257 ) -> Self {
258 Self {
259 block_gas_limit,
260 enable_amsterdam_eip8037,
261 tx_gas_limit_cap,
262 cumulative_tx_gas_used: 0,
263 block_regular_gas_used: 0,
264 }
265 }
266
267 fn validate_tx_limit(&self, tx_gas_limit: u64) -> Result<(), BlockExecutionError> {
268 let block_gas_used = if self.enable_amsterdam_eip8037 {
269 self.block_regular_gas_used
270 } else {
271 self.cumulative_tx_gas_used
272 };
273 let block_available_gas = self.block_gas_limit.saturating_sub(block_gas_used);
274 let tx_min_gas_limit =
275 self.tx_gas_limit_cap.map_or(tx_gas_limit, |cap| tx_gas_limit.min(cap));
276
277 if tx_min_gas_limit > block_available_gas {
278 return Err(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
279 transaction_gas_limit: tx_gas_limit,
280 block_available_gas,
281 }
282 .into());
283 }
284
285 Ok(())
286 }
287
288 const fn record_result<H>(&mut self, result: &ResultAndState<H>) {
289 let gas = result.result.gas();
290 self.cumulative_tx_gas_used = self.cumulative_tx_gas_used.saturating_add(gas.tx_gas_used());
291 self.block_regular_gas_used =
292 self.block_regular_gas_used.saturating_add(gas.block_regular_gas_used());
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use alloy_consensus::{BlockHeader, Header};
300 use alloy_eip7928::{bal::Bal as AlloyBal, BlockAccessList};
301 use alloy_eips::{
302 eip2935::{HISTORY_STORAGE_ADDRESS, HISTORY_STORAGE_CODE},
303 eip4788::{BEACON_ROOTS_ADDRESS, BEACON_ROOTS_CODE},
304 eip7002::{WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, WITHDRAWAL_REQUEST_PREDEPLOY_CODE},
305 };
306 use alloy_primitives::{keccak256, B256, U256};
307 use reth_ethereum_primitives::{Block, BlockBody, Receipt, TransactionSigned};
308 use reth_evm_ethereum::EthEvmConfig;
309 use reth_primitives_traits::{Block as _, Recovered, SealedBlock};
310 use reth_revm::db::BundleState;
311 use reth_tasks::Runtime;
312 use revm::{
313 database::{CacheDB, EmptyDB},
314 state::{AccountInfo, Bytecode},
315 };
316 use std::convert::Infallible;
317
318 fn to_arc_decoded(bal: BlockAccessList) -> Arc<DecodedBal> {
320 let alloy_bal: AlloyBal = bal.into();
321 let raw = alloy_rlp::encode(&alloy_bal).into();
322 Arc::new(DecodedBal::new(alloy_bal, raw))
323 }
324
325 fn system_contracts_db() -> CacheDB<EmptyDB> {
329 let mut db = CacheDB::<EmptyDB>::new(Default::default());
330 db.insert_account_info(
331 BEACON_ROOTS_ADDRESS,
332 AccountInfo {
333 balance: U256::ZERO,
334 nonce: 1,
335 code_hash: keccak256(BEACON_ROOTS_CODE.clone()),
336 code: Some(Bytecode::new_raw(BEACON_ROOTS_CODE.clone())),
337 account_id: None,
338 },
339 );
340 db.insert_account_info(
341 WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS,
342 AccountInfo {
343 balance: U256::ZERO,
344 nonce: 1,
345 code_hash: keccak256(WITHDRAWAL_REQUEST_PREDEPLOY_CODE.clone()),
346 code: Some(Bytecode::new_raw(WITHDRAWAL_REQUEST_PREDEPLOY_CODE.clone())),
347 account_id: None,
348 },
349 );
350 db.insert_account_info(
351 HISTORY_STORAGE_ADDRESS,
352 AccountInfo {
353 balance: U256::ZERO,
354 nonce: 1,
355 code_hash: keccak256(HISTORY_STORAGE_CODE.clone()),
356 code: Some(Bytecode::new_raw(HISTORY_STORAGE_CODE.clone())),
357 account_id: None,
358 },
359 );
360 db
361 }
362
363 fn empty_amsterdam_block(header_bal_hash: B256) -> SealedBlock<Block> {
365 empty_amsterdam_block_with_gas_limit(header_bal_hash, 30_000_000)
366 }
367
368 fn empty_amsterdam_block_with_gas_limit(
369 header_bal_hash: B256,
370 gas_limit: u64,
371 ) -> SealedBlock<Block> {
372 let header = Header {
373 timestamp: 1,
374 number: 1,
375 gas_limit,
376 parent_beacon_block_root: Some(B256::ZERO),
377 withdrawals_root: Some(alloy_consensus::EMPTY_ROOT_HASH),
378 requests_hash: Some(alloy_eips::eip7685::EMPTY_REQUESTS_HASH),
379 excess_blob_gas: Some(0),
380 blob_gas_used: Some(0),
381 block_access_list_hash: Some(header_bal_hash),
382 ..Header::default()
383 };
384 let block = Block {
385 header,
386 body: BlockBody {
387 transactions: vec![],
388 ommers: vec![],
389 withdrawals: Some(vec![].into()),
390 },
391 };
392 block.seal_slow()
393 }
394
395 fn reference_bal_for_empty_block(evm_config: &EthEvmConfig) -> BlockAccessList {
402 use revm::database::State as RevmState;
403
404 let db = system_contracts_db();
405 let mut state =
406 RevmState::builder().with_database(db).with_bundle_update().with_bal_builder().build();
407
408 let block = empty_amsterdam_block(B256::ZERO);
410 {
411 let mut executor =
412 evm_config.executor_for_block(&mut state, &block).expect("build executor");
413 executor.apply_pre_execution_changes().expect("pre-exec");
414 executor.evm_mut().db_mut().bump_bal_index();
415 executor.apply_post_execution_changes().expect("post-exec");
416 }
417 state.take_built_alloy_bal().expect("with_bal_builder was set")
418 }
419
420 #[test]
421 fn empty_block_happy_path_round_trip() {
422 let evm_config = EthEvmConfig::mainnet();
428
429 let input_bal = reference_bal_for_empty_block(&evm_config);
430 let bal_hash = alloy_eip7928::compute_block_access_list_hash(&input_bal);
431 assert!(!input_bal.is_empty(), "empty BAL means system calls didn't record state");
433
434 let block = empty_amsterdam_block(bal_hash);
435
436 let result = run_execute_block(
437 &Runtime::test(),
438 evm_config,
439 db_factory(system_contracts_db()),
440 to_arc_decoded(input_bal),
441 &block,
442 Vec::<Recovered<TransactionSigned>>::new(),
443 );
444
445 match result {
446 Ok(output) => {
447 assert!(output.receipts.is_empty(), "empty block → no receipts");
448 }
449 Err(e) => panic!("expected success, got {e:?}"),
450 }
451 }
452
453 fn db_factory(
454 db: CacheDB<EmptyDB>,
455 ) -> impl Fn() -> Result<CacheDB<EmptyDB>, BalExecutionError> + Sync {
456 move || Ok(db.clone())
457 }
458
459 fn tx_stream<Tx>(txs: Vec<Tx>) -> Receiver<(usize, Result<Tx, Infallible>)> {
460 let (tx, rx) = crossbeam_channel::unbounded();
461 for (index, transaction) in txs.into_iter().enumerate() {
462 tx.send((index, Ok(transaction))).unwrap();
463 }
464 rx
465 }
466
467 fn run_execute_block<Tx, DB, MakeDb>(
468 runtime: &Runtime,
469 evm_config: EthEvmConfig,
470 make_db: MakeDb,
471 input_bal: Arc<DecodedBal>,
472 block: &SealedBlock<Block>,
473 txs: Vec<Tx>,
474 ) -> Result<BlockExecutionOutput<Receipt>, BalExecutionError>
475 where
476 Tx: ExecutableTxFor<EthEvmConfig> + Send,
477 DB: Database + Send,
478 MakeDb: Fn() -> Result<DB, BalExecutionError> + Sync,
479 {
480 run_execute_block_full(runtime, evm_config, make_db, input_bal, block, txs)
481 .map(|(output, _)| output)
482 }
483
484 fn run_execute_block_full<Tx, DB, MakeDb>(
485 runtime: &Runtime,
486 evm_config: EthEvmConfig,
487 make_db: MakeDb,
488 input_bal: Arc<DecodedBal>,
489 block: &SealedBlock<Block>,
490 txs: Vec<Tx>,
491 ) -> Result<(BlockExecutionOutput<Receipt>, BlockAccessList), BalExecutionError>
492 where
493 Tx: ExecutableTxFor<EthEvmConfig> + Send,
494 DB: Database + Send,
495 MakeDb: Fn() -> Result<DB, BalExecutionError> + Sync,
496 {
497 let transaction_count = txs.len();
498 let (receipt_tx, _receipt_rx) = crossbeam_channel::unbounded();
499 let evm_env = evm_config.evm_env(block.header()).unwrap();
500 let execution_ctx = evm_config.context_for_block(block).unwrap();
501 let make_db = |_: bool| make_db();
502 execute_block(
503 runtime,
504 &evm_config,
505 &make_db,
506 input_bal,
507 evm_env,
508 execution_ctx,
509 transaction_count,
510 tx_stream(txs),
511 receipt_tx,
512 )
513 .map(|(output, _, built_bal)| (output, built_bal))
514 }
515
516 fn insert_funded(db: &mut CacheDB<EmptyDB>, addr: alloy_primitives::Address, balance: U256) {
518 db.insert_account_info(
519 addr,
520 AccountInfo { nonce: 0, balance, code_hash: B256::ZERO, code: None, account_id: None },
521 );
522 }
523
524 fn reference_bal_for_block<Tx>(
527 evm_config: &EthEvmConfig,
528 mut db: CacheDB<EmptyDB>,
529 block: &SealedBlock<Block>,
530 txs: Vec<Tx>,
531 ) -> BlockAccessList
532 where
533 Tx: ExecutableTxFor<EthEvmConfig>,
534 {
535 use revm::database::State as RevmState;
536
537 let mut state = RevmState::builder()
538 .with_database(&mut db)
539 .with_bundle_update()
540 .with_bal_builder()
541 .build();
542
543 {
544 let mut executor =
545 evm_config.executor_for_block(&mut state, block).expect("build executor");
546 executor.apply_pre_execution_changes().expect("pre-exec");
547 for (i, tx) in txs.into_iter().enumerate() {
548 executor.evm_mut().db_mut().bump_bal_index();
549 executor
550 .execute_transaction(tx)
551 .unwrap_or_else(|e| panic!("tx {i} failed during reference build: {e:?}"));
552 }
553 executor.evm_mut().db_mut().bump_bal_index();
554 executor.apply_post_execution_changes().expect("post-exec");
555 }
556 state.take_built_alloy_bal().expect("with_bal_builder was set")
557 }
558
559 #[test]
560 fn multi_tx_happy_path_round_trip() {
561 use alloy_consensus::TxLegacy;
569 use alloy_primitives::TxKind;
570 use reth_chainspec::MAINNET;
571 use reth_ethereum_primitives::Transaction;
572 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
573 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
574
575 let evm_config = EthEvmConfig::mainnet();
576 let carol: alloy_primitives::Address = alloy_primitives::Address::from([0xCA; 20]);
577 let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
578
579 let alice_kp = generate_key(&mut rng());
581 let alice = public_key_to_address(alice_kp.public_key());
582 let bob_kp = generate_key(&mut rng());
583 let bob = public_key_to_address(bob_kp.public_key());
584
585 let mut pre_block_db = system_contracts_db();
587 insert_funded(&mut pre_block_db, alice, sender_balance);
588 insert_funded(&mut pre_block_db, bob, sender_balance);
589
590 let chain_id = MAINNET.chain.id();
592 let gas_price = 1u128; let tx1 = sign_tx_with_key_pair(
594 alice_kp,
595 Transaction::Legacy(TxLegacy {
596 chain_id: Some(chain_id),
597 nonce: 0,
598 gas_price,
599 gas_limit: 21_000,
600 to: TxKind::Call(carol),
601 value: U256::from(100u64),
602 input: Default::default(),
603 }),
604 );
605 let tx2 = sign_tx_with_key_pair(
606 bob_kp,
607 Transaction::Legacy(TxLegacy {
608 chain_id: Some(chain_id),
609 nonce: 0,
610 gas_price,
611 gas_limit: 21_000,
612 to: TxKind::Call(carol),
613 value: U256::from(200u64),
614 input: Default::default(),
615 }),
616 );
617 let recovered1 = Recovered::new_unchecked(tx1, alice);
618 let recovered2 = Recovered::new_unchecked(tx2, bob);
619
620 let block_for_ref = empty_amsterdam_block(B256::ZERO);
622 let reference_bal = reference_bal_for_block::<Recovered<TransactionSigned>>(
623 &evm_config,
624 {
625 let mut db = system_contracts_db();
627 db.insert_account_info(
628 alice,
629 AccountInfo {
630 nonce: 0,
631 balance: sender_balance,
632 code_hash: B256::ZERO,
633 code: None,
634 account_id: None,
635 },
636 );
637 db.insert_account_info(
638 bob,
639 AccountInfo {
640 nonce: 0,
641 balance: sender_balance,
642 code_hash: B256::ZERO,
643 code: None,
644 account_id: None,
645 },
646 );
647 db
648 },
649 &block_for_ref,
650 vec![recovered1.clone(), recovered2.clone()],
651 );
652 assert!(!reference_bal.is_empty(), "expected BAL entries from pre-exec + txs");
653
654 let bal_hash = alloy_eip7928::compute_block_access_list_hash(&reference_bal);
655 let block = empty_amsterdam_block(bal_hash);
656
657 let result = run_execute_block(
658 &Runtime::test(),
659 evm_config,
660 db_factory(pre_block_db),
661 to_arc_decoded(reference_bal),
662 &block,
663 vec![recovered1, recovered2],
664 );
665
666 match result {
667 Ok(output) => {
668 assert_eq!(output.receipts.len(), 2, "expected 2 receipts");
669 assert!(output.gas_used >= 2 * 21_000, "expected at least 42k gas used");
670 }
671 Err(e) => panic!("expected success, got {e:?}"),
672 }
673 }
674
675 #[derive(Debug)]
683 struct ShadowOutput {
684 bundle_state: BundleState,
685 receipts: Vec<reth_ethereum_primitives::Receipt>,
686 gas_used: u64,
687 requests: alloy_eips::eip7685::Requests,
688 }
689
690 fn run_serial_path(
695 evm_config: &EthEvmConfig,
696 canonical_db: CacheDB<EmptyDB>,
697 block: &SealedBlock<Block>,
698 txs: &[Recovered<TransactionSigned>],
699 ) -> (ShadowOutput, BlockAccessList) {
700 use revm::database::State as RevmState;
701
702 let mut state = RevmState::builder()
703 .with_database(canonical_db)
704 .with_bundle_update()
705 .with_bal_builder()
706 .build();
707
708 let block_result = {
709 let mut executor =
710 evm_config.executor_for_block(&mut state, block).expect("build serial executor");
711 executor.apply_pre_execution_changes().expect("serial pre-exec");
712 for (i, tx) in txs.iter().cloned().enumerate() {
713 executor.evm_mut().db_mut().bump_bal_index();
714 executor
715 .execute_transaction(tx)
716 .unwrap_or_else(|e| panic!("serial tx {i} failed: {e:?}"));
717 }
718 executor.evm_mut().db_mut().bump_bal_index();
719 executor.apply_post_execution_changes().expect("serial post-exec")
720 };
721
722 let bal = state.take_built_alloy_bal().expect("with_bal_builder was set");
723 state.merge_transitions(BundleRetention::Reverts);
724 let bundle_state = state.take_bundle();
725
726 (
727 ShadowOutput {
728 bundle_state,
729 receipts: block_result.receipts,
730 gas_used: block_result.gas_used,
731 requests: block_result.requests,
732 },
733 bal,
734 )
735 }
736
737 fn assert_shadow_equal(
739 evm_config: EthEvmConfig,
740 canonical_db_template: CacheDB<EmptyDB>,
741 block_header_only: SealedBlock<Block>,
742 txs: Vec<Recovered<TransactionSigned>>,
743 ) {
744 let (serial, reference_bal) =
746 run_serial_path(&evm_config, canonical_db_template.clone(), &block_header_only, &txs);
747
748 let bal_hash = alloy_eip7928::compute_block_access_list_hash(&reference_bal);
750 let block =
751 empty_amsterdam_block_with_gas_limit(bal_hash, block_header_only.header().gas_limit());
752
753 let bal_out = run_execute_block(
754 &Runtime::test(),
755 evm_config,
756 db_factory(canonical_db_template),
757 to_arc_decoded(reference_bal),
758 &block,
759 txs,
760 )
761 .unwrap_or_else(|e| panic!("BAL path failed: {e:?}"));
762
763 assert_eq!(
765 serial.receipts, bal_out.receipts,
766 "receipts diverge between serial and BAL paths",
767 );
768 assert_eq!(
769 serial.gas_used, bal_out.gas_used,
770 "gas_used differs: serial {} vs bal {}",
771 serial.gas_used, bal_out.gas_used,
772 );
773 assert_eq!(
774 serial.requests, bal_out.requests,
775 "requests (EIP-7685) diverge between serial and BAL paths",
776 );
777 assert_eq!(
778 serial.bundle_state, bal_out.state,
779 "bundle_state diverges — the canonical state transitions don't match",
780 );
781 }
782
783 #[test]
784 fn shadow_empty_block() {
785 assert_shadow_equal(
788 EthEvmConfig::mainnet(),
789 system_contracts_db(),
790 empty_amsterdam_block(B256::ZERO),
791 Vec::new(),
792 );
793 }
794
795 #[test]
796 fn shadow_multi_value_transfer() {
797 use alloy_consensus::TxLegacy;
800 use alloy_primitives::TxKind;
801 use reth_chainspec::MAINNET;
802 use reth_ethereum_primitives::Transaction;
803 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
804 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
805
806 let evm_config = EthEvmConfig::mainnet();
807 let carol: alloy_primitives::Address = alloy_primitives::Address::from([0xCA; 20]);
808 let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
809
810 let alice_kp = generate_key(&mut rng());
811 let alice = public_key_to_address(alice_kp.public_key());
812 let bob_kp = generate_key(&mut rng());
813 let bob = public_key_to_address(bob_kp.public_key());
814
815 let mut db = system_contracts_db();
816 insert_funded(&mut db, alice, sender_balance);
817 insert_funded(&mut db, bob, sender_balance);
818
819 let chain_id = MAINNET.chain.id();
820 let make_tx = |kp, to, value, nonce: u64| {
821 sign_tx_with_key_pair(
822 kp,
823 Transaction::Legacy(TxLegacy {
824 chain_id: Some(chain_id),
825 nonce,
826 gas_price: 1,
827 gas_limit: 21_000,
828 to: TxKind::Call(to),
829 value: U256::from(value),
830 input: Default::default(),
831 }),
832 )
833 };
834 let tx1 = Recovered::new_unchecked(make_tx(alice_kp, carol, 100u64, 0), alice);
835 let tx2 = Recovered::new_unchecked(make_tx(bob_kp, carol, 200u64, 0), bob);
836
837 assert_shadow_equal(evm_config, db, empty_amsterdam_block(B256::ZERO), vec![tx1, tx2]);
838 }
839
840 #[test]
841 fn rejects_tx_gas_limit_that_exceeds_remaining_block_gas() {
842 use alloy_consensus::TxLegacy;
846 use alloy_evm::block::BlockValidationError;
847 use alloy_primitives::TxKind;
848 use reth_chainspec::MAINNET;
849 use reth_ethereum_primitives::Transaction;
850 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
851 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
852
853 let evm_config = EthEvmConfig::mainnet();
854 let carol: alloy_primitives::Address = alloy_primitives::Address::from([0xCA; 20]);
855 let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
856 let block_gas_limit = 1_000_000;
857 let tx_gas_limit = 990_000;
858
859 let alice_kp = generate_key(&mut rng());
860 let alice = public_key_to_address(alice_kp.public_key());
861 let bob_kp = generate_key(&mut rng());
862 let bob = public_key_to_address(bob_kp.public_key());
863
864 let mut pre_block_db = system_contracts_db();
865 insert_funded(&mut pre_block_db, alice, sender_balance);
866 insert_funded(&mut pre_block_db, bob, sender_balance);
867
868 let chain_id = MAINNET.chain.id();
869 let make_tx = |kp, value| {
870 sign_tx_with_key_pair(
871 kp,
872 Transaction::Legacy(TxLegacy {
873 chain_id: Some(chain_id),
874 nonce: 0,
875 gas_price: 1,
876 gas_limit: tx_gas_limit,
877 to: TxKind::Call(carol),
878 value: U256::from(value),
879 input: Default::default(),
880 }),
881 )
882 };
883 let tx1 = Recovered::new_unchecked(make_tx(alice_kp, 100u64), alice);
884 let tx2 = Recovered::new_unchecked(make_tx(bob_kp, 200u64), bob);
885
886 let reference_block = empty_amsterdam_block(B256::ZERO);
890 let reference_bal = reference_bal_for_block(
891 &evm_config,
892 pre_block_db.clone(),
893 &reference_block,
894 vec![tx1.clone(), tx2.clone()],
895 );
896 let bal_hash = alloy_eip7928::compute_block_access_list_hash(&reference_bal);
897 let low_gas_block = empty_amsterdam_block_with_gas_limit(bal_hash, block_gas_limit);
898
899 let result = run_execute_block(
900 &Runtime::test(),
901 evm_config,
902 db_factory(pre_block_db),
903 to_arc_decoded(reference_bal),
904 &low_gas_block,
905 vec![tx1, tx2],
906 );
907
908 match result {
909 Err(BalExecutionError::Execution(err)) => assert!(matches!(
910 err.as_validation(),
911 Some(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas { .. })
912 )),
913 Err(err) => panic!("expected block gas validation error, got {err:?}"),
914 Ok(_) => panic!("expected block gas validation error, got Ok"),
915 }
916 }
917
918 #[test]
919 fn shadow_tx_with_revert() {
920 use alloy_consensus::TxLegacy;
926 use alloy_primitives::{Bytes, TxKind};
927 use reth_chainspec::MAINNET;
928 use reth_ethereum_primitives::Transaction;
929 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
930 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
931 use revm::primitives::keccak256;
932
933 let evm_config = EthEvmConfig::mainnet();
934 let revert_contract: alloy_primitives::Address =
935 alloy_primitives::Address::from([0xDE; 20]);
936 let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
937
938 let alice_kp = generate_key(&mut rng());
939 let alice = public_key_to_address(alice_kp.public_key());
940
941 let revert_code: Bytes = Bytes::from_static(&[0x60, 0x00, 0x60, 0x00, 0xfd]);
943 let code_hash = keccak256(&revert_code);
944 let mut db = system_contracts_db();
945 insert_funded(&mut db, alice, sender_balance);
946 db.insert_account_info(
947 revert_contract,
948 AccountInfo {
949 nonce: 1,
950 balance: U256::ZERO,
951 code_hash,
952 code: Some(Bytecode::new_raw(revert_code)),
953 account_id: None,
954 },
955 );
956
957 let tx = Recovered::new_unchecked(
958 sign_tx_with_key_pair(
959 alice_kp,
960 Transaction::Legacy(TxLegacy {
961 chain_id: Some(MAINNET.chain.id()),
962 nonce: 0,
963 gas_price: 1,
964 gas_limit: 50_000,
965 to: TxKind::Call(revert_contract),
966 value: U256::ZERO,
967 input: Default::default(),
968 }),
969 ),
970 alice,
971 );
972
973 assert_shadow_equal(evm_config, db, empty_amsterdam_block(B256::ZERO), vec![tx]);
974 }
975
976 #[test]
977 fn shadow_tx_with_sstore() {
978 use alloy_consensus::TxLegacy;
984 use alloy_primitives::{Bytes, TxKind};
985 use reth_chainspec::MAINNET;
986 use reth_ethereum_primitives::Transaction;
987 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
988 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
989 use revm::primitives::keccak256;
990
991 let evm_config = EthEvmConfig::mainnet();
992 let sstore_contract: alloy_primitives::Address =
993 alloy_primitives::Address::from([0x55; 20]);
994 let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
995
996 let alice_kp = generate_key(&mut rng());
997 let alice = public_key_to_address(alice_kp.public_key());
998
999 let sstore_code: Bytes = Bytes::from_static(&[0x60, 0x42, 0x60, 0x00, 0x55, 0x00]);
1001 let code_hash = keccak256(&sstore_code);
1002 let mut db = system_contracts_db();
1003 insert_funded(&mut db, alice, sender_balance);
1004 db.insert_account_info(
1005 sstore_contract,
1006 AccountInfo {
1007 nonce: 1,
1008 balance: U256::ZERO,
1009 code_hash,
1010 code: Some(Bytecode::new_raw(sstore_code)),
1011 account_id: None,
1012 },
1013 );
1014
1015 let tx = Recovered::new_unchecked(
1016 sign_tx_with_key_pair(
1017 alice_kp,
1018 Transaction::Legacy(TxLegacy {
1019 chain_id: Some(MAINNET.chain.id()),
1020 nonce: 0,
1021 gas_price: 1,
1022 gas_limit: 100_000,
1023 to: TxKind::Call(sstore_contract),
1024 value: U256::ZERO,
1025 input: Default::default(),
1026 }),
1027 ),
1028 alice,
1029 );
1030
1031 assert_shadow_equal(evm_config, db, empty_amsterdam_block(B256::ZERO), vec![tx]);
1032 }
1033
1034 #[test]
1035 fn returns_built_bal_for_final_hash_mismatch() {
1036 use alloy_eip7928::AccountChanges;
1040
1041 let evm_config = EthEvmConfig::mainnet();
1042
1043 let real_bal = reference_bal_for_empty_block(&evm_config);
1045 assert!(!real_bal.is_empty(), "reference BAL must be non-empty");
1046
1047 let phantom = alloy_primitives::Address::from([0xFF; 20]);
1049 let mut tampered_entries: Vec<AccountChanges> = real_bal;
1050 tampered_entries.push(AccountChanges::new(phantom));
1051 let tampered_bal: alloy_eip7928::bal::Bal = alloy_eip7928::bal::Bal::new(tampered_entries);
1052
1053 let tampered_block_access_list: BlockAccessList = tampered_bal.clone().into();
1055 let tampered_hash =
1056 alloy_eip7928::compute_block_access_list_hash(&tampered_block_access_list);
1057 let block = empty_amsterdam_block(tampered_hash);
1058
1059 let received = {
1060 let raw = alloy_rlp::encode(&tampered_bal).into();
1061 Arc::new(DecodedBal::new(tampered_bal, raw))
1062 };
1063
1064 let result = run_execute_block_full(
1065 &Runtime::test(),
1066 evm_config,
1067 db_factory(system_contracts_db()),
1068 received,
1069 &block,
1070 Vec::<Recovered<TransactionSigned>>::new(),
1071 );
1072
1073 match result {
1074 Ok((_, built_bal)) => {
1075 let rebuilt = alloy_eip7928::compute_block_access_list_hash(&built_bal);
1076 assert_ne!(rebuilt, tampered_hash, "rebuilt and header hashes must differ");
1077 }
1078 Err(e) => panic!("expected success with rebuilt BAL, got {e:?}"),
1079 }
1080 }
1081
1082 #[test]
1083 fn canonical_make_db_failure() {
1084 let evm_config = EthEvmConfig::mainnet();
1087 let block = empty_amsterdam_block(B256::ZERO);
1088
1089 let failing_make_db = || -> Result<CacheDB<EmptyDB>, BalExecutionError> {
1090 Err(reth_provider::ProviderError::BestBlockNotFound.into())
1091 };
1092
1093 let result = run_execute_block(
1094 &Runtime::test(),
1095 evm_config,
1096 failing_make_db,
1097 to_arc_decoded(BlockAccessList::default()),
1098 &block,
1099 Vec::<Recovered<TransactionSigned>>::new(),
1100 );
1101
1102 assert!(
1103 matches!(result, Err(BalExecutionError::Provider(_))),
1104 "expected Provider error from canonical make_db failure, got {result:?}",
1105 );
1106 }
1107
1108 #[test]
1109 fn worker_tx_recovery_error_becomes_other_error() {
1110 let evm_config = EthEvmConfig::mainnet();
1114 let block = empty_amsterdam_block(B256::ZERO);
1115
1116 let (tx_tx, tx_rx) = crossbeam_channel::unbounded::<(
1117 usize,
1118 Result<Recovered<TransactionSigned>, std::io::Error>,
1119 )>();
1120 tx_tx.send((0, Err(std::io::Error::other("sig fail")))).unwrap();
1121 drop(tx_tx);
1122
1123 let (receipt_tx, _receipt_rx) = crossbeam_channel::unbounded();
1124 let evm_env = evm_config.evm_env(block.header()).unwrap();
1125 let execution_ctx = evm_config.context_for_block(&block).unwrap();
1126 let make_db = db_factory(system_contracts_db());
1127 let make_db = |_: bool| make_db();
1128
1129 let result = execute_block(
1130 &Runtime::test(),
1131 &evm_config,
1132 &make_db,
1133 to_arc_decoded(BlockAccessList::default()),
1134 evm_env,
1135 execution_ctx,
1136 1, tx_rx,
1138 receipt_tx,
1139 );
1140
1141 assert!(
1142 matches!(result, Err(BalExecutionError::Other(_))),
1143 "expected Other error from tx recovery failure, got {result:?}",
1144 );
1145 }
1146
1147 #[test]
1148 fn gas_tracker_non_amsterdam_uses_cumulative_gas() {
1149 use revm::context::result::{
1153 ExecResultAndState, ExecutionResult, Output, ResultGas, SuccessReason,
1154 };
1155 use revm_state::EvmState;
1156
1157 let block_gas_limit = 1_000_000u64;
1158 let first_tx_gas = 600_000u64;
1159 let second_tx_gas_limit = 500_000u64; let gas = ResultGas::new_with_state_gas(first_tx_gas, 0, 0, first_tx_gas);
1162 let fake_result: ResultAndState<revm::context::result::HaltReason> =
1163 ExecResultAndState::new(
1164 ExecutionResult::Success {
1165 reason: SuccessReason::Return,
1166 gas,
1167 logs: vec![],
1168 output: Output::Call(Default::default()),
1169 },
1170 EvmState::default(),
1171 );
1172
1173 let mut non_amsterdam = BlockGasTracker::new(block_gas_limit, false, None);
1175 non_amsterdam.record_result(&fake_result);
1176 assert!(
1177 non_amsterdam.validate_tx_limit(second_tx_gas_limit).is_err(),
1178 "non-Amsterdam tracker must reject tx that exceeds remaining cumulative gas",
1179 );
1180
1181 let mut amsterdam = BlockGasTracker::new(block_gas_limit, true, None);
1183 amsterdam.record_result(&fake_result);
1184 assert!(
1185 amsterdam.validate_tx_limit(second_tx_gas_limit).is_ok(),
1186 "Amsterdam tracker must accept the same tx since block_regular_gas_used stays 0",
1187 );
1188 }
1189
1190 #[test]
1191 fn gas_tracker_caps_oversized_tx_gas_limit_at_tx_gas_limit_cap() {
1192 use revm::{
1195 context::result::{
1196 ExecResultAndState, ExecutionResult, Output, ResultGas, SuccessReason,
1197 },
1198 primitives::eip7825::TX_GAS_LIMIT_CAP,
1199 };
1200 use revm_state::EvmState;
1201
1202 let block_gas_limit = 30_000_000u64;
1203 let oversized = TX_GAS_LIMIT_CAP + 1_000_000; let tracker = BlockGasTracker::new(block_gas_limit, false, Some(TX_GAS_LIMIT_CAP));
1208 assert!(
1209 tracker.validate_tx_limit(oversized).is_ok(),
1210 "oversized tx must pass when capped limit fits in block gas",
1211 );
1212
1213 let prior_gas = 20_000_000u64;
1216 let gas = ResultGas::new_with_state_gas(prior_gas, 0, 0, prior_gas);
1217 let fake_result: ResultAndState<revm::context::result::HaltReason> =
1218 ExecResultAndState::new(
1219 ExecutionResult::Success {
1220 reason: SuccessReason::Return,
1221 gas,
1222 logs: vec![],
1223 output: Output::Call(Default::default()),
1224 },
1225 EvmState::default(),
1226 );
1227
1228 let mut tracker = BlockGasTracker::new(block_gas_limit, false, Some(TX_GAS_LIMIT_CAP));
1229 tracker.record_result(&fake_result);
1230 assert!(
1231 tracker.validate_tx_limit(oversized).is_err(),
1232 "oversized tx must be rejected when capped limit exceeds remaining block gas",
1233 );
1234 }
1235}