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