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_engine_primitives::BlockAccessListDecodeError;
29use reth_evm::{execute::ExecutableTxFor, ConfigureEvm, Database, EvmEnvFor, ExecutionCtxFor};
30use reth_primitives_traits::ReceiptTy;
31use reth_provider::BlockExecutionOutput;
32use reth_tasks::Runtime;
33use revm::{
34 context::{result::ResultAndState, Block},
35 database::{states::bundle_state::BundleRetention, State},
36 state::bal::Bal as RevmBal,
37};
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 canonical_state
121 .bal_state
122 .bal_builder
123 .as_mut()
124 .expect("with_bal_builder set")
125 .accounts
126 .reserve(bal.len());
127
128 let (block_result, senders) = {
129 let (result_tx, result_rx) = crossbeam_channel::unbounded();
130 let (abort_guard, abort_rx) = AbortGuard::new();
131
132 for _ in 0..worker_count {
133 worker::spawn_worker(
134 scope,
135 txs.clone(),
136 abort_rx.clone(),
137 result_tx.clone(),
138 evm_config,
139 make_db,
140 Arc::clone(&input_bal_revm),
141 evm_env.clone(),
142 ctx.clone(),
143 );
144 }
145 drop(result_tx);
146
147 let mut gas_tracker =
148 BlockGasTracker::new(block_gas_limit, enable_amsterdam_eip8037, tx_gas_limit_cap);
149 let evm = evm_config.evm_with_env(&mut canonical_state, evm_env);
150 let mut canonical_executor = evm_config.create_executor_with_state(evm, ctx.clone());
151
152 canonical_executor.apply_pre_execution_changes()?;
153 let mut senders = Vec::with_capacity(transaction_count);
154 let mut last_sent_len = 0usize;
155 for output in ordered_worker_outputs(&result_rx, transaction_count) {
156 let output = output?;
157
158 gas_tracker.validate_tx_limit(output.tx_gas_limit)?;
159 gas_tracker.record_result(output.result.result());
160 canonical_executor.evm_mut().db_mut().bump_bal_index();
161
162 let _ = canonical_executor.commit_transaction(output.result);
163 senders.push(output.signer);
164
165 let current_len = canonical_executor.receipts().len();
166 if current_len > last_sent_len {
167 last_sent_len = current_len;
168 if let Some(receipt) = canonical_executor.receipts().last() {
169 let tx_index = current_len - 1;
170 let _ = receipt_tx.send(IndexedReceipt::new(tx_index, receipt.clone()));
171 }
172 }
173 }
174 drop(abort_guard);
175
176 canonical_executor.evm_mut().db_mut().bump_bal_index();
177 let block_result = canonical_executor.apply_post_execution_changes()?;
178 (block_result, senders)
179 };
180
181 let built_bal = take_built_bal_and_log_divergence(&mut canonical_state, bal);
182
183 canonical_state.merge_transitions(BundleRetention::Reverts);
184 Ok((
185 BlockExecutionOutput { state: canonical_state.take_bundle(), result: block_result },
186 senders,
187 built_bal,
188 ))
189}
190
191fn convert_alloy_to_revm_bal(alloy_bal: &AlloyBal) -> Result<Arc<RevmBal>, BalExecutionError> {
192 let received_bal_revm =
205 RevmBal::clone_from_alloy(alloy_bal.as_vec()).map_err(BlockAccessListDecodeError::new)?;
206 Ok(Arc::new(received_bal_revm))
207}
208
209fn take_built_bal_and_log_divergence<DB>(
210 canonical_state: &mut State<DB>,
211 received_bal: &AlloyBal,
212) -> BlockAccessList
213where
214 DB: Database,
215{
216 let built_bal = canonical_state.take_built_alloy_bal().expect("with_bal_builder set");
217 if tracing::enabled!(target: "engine::tree::payload_processor::bal", tracing::Level::DEBUG) &&
218 built_bal.as_slice() != received_bal.as_slice()
219 {
220 let rebuilt = compute_block_access_list_hash(built_bal.as_slice());
221 let expected = compute_block_access_list_hash(received_bal.as_slice());
222 let div = received_bal.diff(built_bal.as_slice());
223 tracing::debug!(
224 target: "engine::tree::payload_processor::bal",
225 %rebuilt,
226 %expected,
227 %div,
228 "first BAL divergence",
229 );
230 }
231
232 built_bal
233}
234
235struct AbortGuard {
237 _tx: Sender<()>,
238}
239
240impl AbortGuard {
241 fn new() -> (Self, Receiver<()>) {
242 let (tx, rx) = crossbeam_channel::bounded(0);
243 (Self { _tx: tx }, rx)
244 }
245}
246
247#[derive(Debug)]
249struct BlockGasTracker {
250 block_gas_limit: u64,
251 enable_amsterdam_eip8037: bool,
252 tx_gas_limit_cap: Option<u64>,
253 cumulative_tx_gas_used: u64,
254 block_regular_gas_used: u64,
255 block_state_gas_used: u64,
256}
257
258impl BlockGasTracker {
259 const fn new(
260 block_gas_limit: u64,
261 enable_amsterdam_eip8037: bool,
262 tx_gas_limit_cap: Option<u64>,
263 ) -> Self {
264 Self {
265 block_gas_limit,
266 enable_amsterdam_eip8037,
267 tx_gas_limit_cap,
268 cumulative_tx_gas_used: 0,
269 block_regular_gas_used: 0,
270 block_state_gas_used: 0,
271 }
272 }
273
274 fn validate_tx_limit(&self, tx_gas_limit: u64) -> Result<(), BlockExecutionError> {
291 let block_gas_used = if self.enable_amsterdam_eip8037 {
292 self.block_regular_gas_used
293 } else {
294 self.cumulative_tx_gas_used
295 };
296 let block_available_gas = self.block_gas_limit.saturating_sub(block_gas_used);
297 let tx_min_gas_limit =
298 self.tx_gas_limit_cap.map_or(tx_gas_limit, |cap| tx_gas_limit.min(cap));
299
300 if tx_min_gas_limit > block_available_gas {
301 return Err(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
302 transaction_gas_limit: tx_gas_limit,
303 block_available_gas,
304 }
305 .into());
306 }
307
308 if self.enable_amsterdam_eip8037 {
309 let state_gas_available =
310 self.block_gas_limit.saturating_sub(self.block_state_gas_used);
311 if tx_gas_limit > state_gas_available {
312 return Err(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
313 transaction_gas_limit: tx_gas_limit,
314 block_available_gas: state_gas_available,
315 }
316 .into());
317 }
318 }
319
320 Ok(())
321 }
322
323 const fn record_result<H>(&mut self, result: &ResultAndState<H>) {
324 let gas = result.result.gas();
325 self.cumulative_tx_gas_used = self.cumulative_tx_gas_used.saturating_add(gas.tx_gas_used());
326 self.block_regular_gas_used =
327 self.block_regular_gas_used.saturating_add(gas.block_regular_gas_used());
328 self.block_state_gas_used =
329 self.block_state_gas_used.saturating_add(gas.block_state_gas_used());
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336 use crate::tree::error::{InsertBlockErrorKind, InsertBlockValidationError};
337 use alloy_consensus::{BlockHeader, Header};
338 use alloy_eip7928::{
339 bal::Bal as AlloyBal, AccountChanges, BlockAccessIndex, BlockAccessList, CodeChange,
340 };
341 use alloy_eips::{
342 eip2935::{HISTORY_STORAGE_ADDRESS, HISTORY_STORAGE_CODE},
343 eip4788::{BEACON_ROOTS_ADDRESS, BEACON_ROOTS_CODE},
344 eip7002::{WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, WITHDRAWAL_REQUEST_PREDEPLOY_CODE},
345 };
346 use alloy_primitives::{keccak256, B256, U256};
347 use reth_ethereum_primitives::{Block, BlockBody, Receipt, TransactionSigned};
348 use reth_evm_ethereum::EthEvmConfig;
349 use reth_primitives_traits::{Block as _, Recovered, SealedBlock};
350 use reth_revm::db::BundleState;
351 use reth_tasks::Runtime;
352 use revm::{
353 database::{CacheDB, EmptyDB},
354 state::{AccountInfo, Bytecode},
355 };
356 use std::convert::Infallible;
357
358 fn to_arc_decoded(bal: BlockAccessList) -> Arc<DecodedBal> {
360 let alloy_bal: AlloyBal = bal.into();
361 let raw = alloy_rlp::encode(&alloy_bal).into();
362 Arc::new(DecodedBal::new(alloy_bal, raw))
363 }
364
365 fn system_contracts_db() -> CacheDB<EmptyDB> {
369 let mut db = CacheDB::<EmptyDB>::new(Default::default());
370 db.insert_account_info(
371 BEACON_ROOTS_ADDRESS,
372 AccountInfo {
373 balance: U256::ZERO,
374 nonce: 1,
375 code_hash: keccak256(BEACON_ROOTS_CODE.clone()),
376 code: Some(Bytecode::new_raw(BEACON_ROOTS_CODE.clone())),
377 account_id: None,
378 },
379 );
380 db.insert_account_info(
381 WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS,
382 AccountInfo {
383 balance: U256::ZERO,
384 nonce: 1,
385 code_hash: keccak256(WITHDRAWAL_REQUEST_PREDEPLOY_CODE.clone()),
386 code: Some(Bytecode::new_raw(WITHDRAWAL_REQUEST_PREDEPLOY_CODE.clone())),
387 account_id: None,
388 },
389 );
390 db.insert_account_info(
391 HISTORY_STORAGE_ADDRESS,
392 AccountInfo {
393 balance: U256::ZERO,
394 nonce: 1,
395 code_hash: keccak256(HISTORY_STORAGE_CODE.clone()),
396 code: Some(Bytecode::new_raw(HISTORY_STORAGE_CODE.clone())),
397 account_id: None,
398 },
399 );
400 db
401 }
402
403 fn empty_amsterdam_block(header_bal_hash: B256) -> SealedBlock<Block> {
405 empty_amsterdam_block_with_gas_limit(header_bal_hash, 30_000_000)
406 }
407
408 fn empty_amsterdam_block_with_gas_limit(
409 header_bal_hash: B256,
410 gas_limit: u64,
411 ) -> SealedBlock<Block> {
412 let header = Header {
413 timestamp: 1,
414 number: 1,
415 gas_limit,
416 parent_beacon_block_root: Some(B256::ZERO),
417 withdrawals_root: Some(alloy_consensus::EMPTY_ROOT_HASH),
418 requests_hash: Some(alloy_eips::eip7685::EMPTY_REQUESTS_HASH),
419 excess_blob_gas: Some(0),
420 blob_gas_used: Some(0),
421 block_access_list_hash: Some(header_bal_hash),
422 ..Header::default()
423 };
424 let block = Block {
425 header,
426 body: BlockBody {
427 transactions: vec![],
428 ommers: vec![],
429 withdrawals: Some(vec![].into()),
430 },
431 };
432 block.seal_slow()
433 }
434
435 fn reference_bal_for_empty_block(evm_config: &EthEvmConfig) -> BlockAccessList {
442 use revm::database::State as RevmState;
443
444 let db = system_contracts_db();
445 let mut state =
446 RevmState::builder().with_database(db).with_bundle_update().with_bal_builder().build();
447
448 let block = empty_amsterdam_block(B256::ZERO);
450 {
451 let mut executor =
452 evm_config.executor_for_block(&mut state, &block).expect("build executor");
453 executor.apply_pre_execution_changes().expect("pre-exec");
454 executor.evm_mut().db_mut().bump_bal_index();
455 executor.apply_post_execution_changes().expect("post-exec");
456 }
457 state.take_built_alloy_bal().expect("with_bal_builder was set")
458 }
459
460 #[test]
461 fn invalid_bal_bytecode_is_validation_error() {
462 let alloy_bal = vec![AccountChanges {
463 address: Address::ZERO,
464 code_changes: vec![CodeChange::new(
465 BlockAccessIndex::new(1),
466 vec![0xef, 0x01, 0xde].into(),
467 )],
468 ..Default::default()
469 }]
470 .into();
471
472 let error = convert_alloy_to_revm_bal(&alloy_bal).unwrap_err();
473 assert!(matches!(&error, BalExecutionError::BlockAccessListDecode(_)));
474 let error = InsertBlockErrorKind::from(error);
475 assert!(matches!(&error, InsertBlockErrorKind::BlockAccessListDecode(_)));
476 assert!(matches!(
477 error.ensure_validation_error(),
478 Ok(InsertBlockValidationError::BlockAccessListDecode(_))
479 ));
480 }
481
482 #[test]
483 fn empty_block_happy_path_round_trip() {
484 let evm_config = EthEvmConfig::mainnet();
490
491 let input_bal = reference_bal_for_empty_block(&evm_config);
492 let bal_hash = alloy_eip7928::compute_block_access_list_hash(&input_bal);
493 assert!(!input_bal.is_empty(), "empty BAL means system calls didn't record state");
495
496 let block = empty_amsterdam_block(bal_hash);
497
498 let result = run_execute_block(
499 &Runtime::test(),
500 evm_config,
501 db_factory(system_contracts_db()),
502 to_arc_decoded(input_bal),
503 &block,
504 Vec::<Recovered<TransactionSigned>>::new(),
505 );
506
507 match result {
508 Ok(output) => {
509 assert!(output.receipts.is_empty(), "empty block → no receipts");
510 }
511 Err(e) => panic!("expected success, got {e:?}"),
512 }
513 }
514
515 fn db_factory(
516 db: CacheDB<EmptyDB>,
517 ) -> impl Fn() -> Result<CacheDB<EmptyDB>, BalExecutionError> + Sync {
518 move || Ok(db.clone())
519 }
520
521 fn tx_stream<Tx>(txs: Vec<Tx>) -> Receiver<(usize, Result<Tx, Infallible>)> {
522 let (tx, rx) = crossbeam_channel::unbounded();
523 for (index, transaction) in txs.into_iter().enumerate() {
524 tx.send((index, Ok(transaction))).unwrap();
525 }
526 rx
527 }
528
529 fn run_execute_block<Tx, DB, MakeDb>(
530 runtime: &Runtime,
531 evm_config: EthEvmConfig,
532 make_db: MakeDb,
533 input_bal: Arc<DecodedBal>,
534 block: &SealedBlock<Block>,
535 txs: Vec<Tx>,
536 ) -> Result<BlockExecutionOutput<Receipt>, BalExecutionError>
537 where
538 Tx: ExecutableTxFor<EthEvmConfig> + Send,
539 DB: Database + Send,
540 MakeDb: Fn() -> Result<DB, BalExecutionError> + Sync,
541 {
542 run_execute_block_full(runtime, evm_config, make_db, input_bal, block, txs)
543 .map(|(output, _)| output)
544 }
545
546 fn run_execute_block_full<Tx, DB, MakeDb>(
547 runtime: &Runtime,
548 evm_config: EthEvmConfig,
549 make_db: MakeDb,
550 input_bal: Arc<DecodedBal>,
551 block: &SealedBlock<Block>,
552 txs: Vec<Tx>,
553 ) -> Result<(BlockExecutionOutput<Receipt>, BlockAccessList), BalExecutionError>
554 where
555 Tx: ExecutableTxFor<EthEvmConfig> + Send,
556 DB: Database + Send,
557 MakeDb: Fn() -> Result<DB, BalExecutionError> + Sync,
558 {
559 let transaction_count = txs.len();
560 let (receipt_tx, _receipt_rx) = crossbeam_channel::unbounded();
561 let evm_env = evm_config.evm_env(block.header()).unwrap();
562 let execution_ctx = evm_config.context_for_block(block).unwrap();
563 let make_db = |_: bool| make_db();
564 execute_block(
565 runtime,
566 &evm_config,
567 &make_db,
568 input_bal,
569 evm_env,
570 execution_ctx,
571 transaction_count,
572 tx_stream(txs),
573 receipt_tx,
574 )
575 .map(|(output, _, built_bal)| (output, built_bal))
576 }
577
578 fn insert_funded(db: &mut CacheDB<EmptyDB>, addr: alloy_primitives::Address, balance: U256) {
580 db.insert_account_info(
581 addr,
582 AccountInfo { nonce: 0, balance, code_hash: B256::ZERO, code: None, account_id: None },
583 );
584 }
585
586 fn reference_bal_for_block<Tx>(
589 evm_config: &EthEvmConfig,
590 mut db: CacheDB<EmptyDB>,
591 block: &SealedBlock<Block>,
592 txs: Vec<Tx>,
593 ) -> BlockAccessList
594 where
595 Tx: ExecutableTxFor<EthEvmConfig>,
596 {
597 use revm::database::State as RevmState;
598
599 let mut state = RevmState::builder()
600 .with_database(&mut db)
601 .with_bundle_update()
602 .with_bal_builder()
603 .build();
604
605 {
606 let mut executor =
607 evm_config.executor_for_block(&mut state, block).expect("build executor");
608 executor.apply_pre_execution_changes().expect("pre-exec");
609 for (i, tx) in txs.into_iter().enumerate() {
610 executor.evm_mut().db_mut().bump_bal_index();
611 executor
612 .execute_transaction(tx)
613 .unwrap_or_else(|e| panic!("tx {i} failed during reference build: {e:?}"));
614 }
615 executor.evm_mut().db_mut().bump_bal_index();
616 executor.apply_post_execution_changes().expect("post-exec");
617 }
618 state.take_built_alloy_bal().expect("with_bal_builder was set")
619 }
620
621 #[test]
622 fn multi_tx_happy_path_round_trip() {
623 use alloy_consensus::TxLegacy;
631 use alloy_primitives::TxKind;
632 use reth_chainspec::MAINNET;
633 use reth_ethereum_primitives::Transaction;
634 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
635 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
636
637 let evm_config = EthEvmConfig::mainnet();
638 let carol: alloy_primitives::Address = alloy_primitives::Address::from([0xCA; 20]);
639 let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
640
641 let alice_kp = generate_key(&mut rng());
643 let alice = public_key_to_address(alice_kp.public_key());
644 let bob_kp = generate_key(&mut rng());
645 let bob = public_key_to_address(bob_kp.public_key());
646
647 let mut pre_block_db = system_contracts_db();
649 insert_funded(&mut pre_block_db, alice, sender_balance);
650 insert_funded(&mut pre_block_db, bob, sender_balance);
651
652 let chain_id = MAINNET.chain.id();
654 let gas_price = 1u128; let tx1 = sign_tx_with_key_pair(
656 alice_kp,
657 Transaction::Legacy(TxLegacy {
658 chain_id: Some(chain_id),
659 nonce: 0,
660 gas_price,
661 gas_limit: 21_000,
662 to: TxKind::Call(carol),
663 value: U256::from(100u64),
664 input: Default::default(),
665 }),
666 );
667 let tx2 = sign_tx_with_key_pair(
668 bob_kp,
669 Transaction::Legacy(TxLegacy {
670 chain_id: Some(chain_id),
671 nonce: 0,
672 gas_price,
673 gas_limit: 21_000,
674 to: TxKind::Call(carol),
675 value: U256::from(200u64),
676 input: Default::default(),
677 }),
678 );
679 let recovered1 = Recovered::new_unchecked(tx1, alice);
680 let recovered2 = Recovered::new_unchecked(tx2, bob);
681
682 let block_for_ref = empty_amsterdam_block(B256::ZERO);
684 let reference_bal = reference_bal_for_block::<Recovered<TransactionSigned>>(
685 &evm_config,
686 {
687 let mut db = system_contracts_db();
689 db.insert_account_info(
690 alice,
691 AccountInfo {
692 nonce: 0,
693 balance: sender_balance,
694 code_hash: B256::ZERO,
695 code: None,
696 account_id: None,
697 },
698 );
699 db.insert_account_info(
700 bob,
701 AccountInfo {
702 nonce: 0,
703 balance: sender_balance,
704 code_hash: B256::ZERO,
705 code: None,
706 account_id: None,
707 },
708 );
709 db
710 },
711 &block_for_ref,
712 vec![recovered1.clone(), recovered2.clone()],
713 );
714 assert!(!reference_bal.is_empty(), "expected BAL entries from pre-exec + txs");
715
716 let bal_hash = alloy_eip7928::compute_block_access_list_hash(&reference_bal);
717 let block = empty_amsterdam_block(bal_hash);
718
719 let result = run_execute_block(
720 &Runtime::test(),
721 evm_config,
722 db_factory(pre_block_db),
723 to_arc_decoded(reference_bal),
724 &block,
725 vec![recovered1, recovered2],
726 );
727
728 match result {
729 Ok(output) => {
730 assert_eq!(output.receipts.len(), 2, "expected 2 receipts");
731 assert!(output.gas_used >= 2 * 21_000, "expected at least 42k gas used");
732 }
733 Err(e) => panic!("expected success, got {e:?}"),
734 }
735 }
736
737 #[derive(Debug)]
745 struct ShadowOutput {
746 bundle_state: BundleState,
747 receipts: Vec<reth_ethereum_primitives::Receipt>,
748 gas_used: u64,
749 requests: alloy_eips::eip7685::Requests,
750 }
751
752 fn run_serial_path(
757 evm_config: &EthEvmConfig,
758 canonical_db: CacheDB<EmptyDB>,
759 block: &SealedBlock<Block>,
760 txs: &[Recovered<TransactionSigned>],
761 ) -> (ShadowOutput, BlockAccessList) {
762 use revm::database::State as RevmState;
763
764 let mut state = RevmState::builder()
765 .with_database(canonical_db)
766 .with_bundle_update()
767 .with_bal_builder()
768 .build();
769
770 let block_result = {
771 let mut executor =
772 evm_config.executor_for_block(&mut state, block).expect("build serial executor");
773 executor.apply_pre_execution_changes().expect("serial pre-exec");
774 for (i, tx) in txs.iter().cloned().enumerate() {
775 executor.evm_mut().db_mut().bump_bal_index();
776 executor
777 .execute_transaction(tx)
778 .unwrap_or_else(|e| panic!("serial tx {i} failed: {e:?}"));
779 }
780 executor.evm_mut().db_mut().bump_bal_index();
781 executor.apply_post_execution_changes().expect("serial post-exec")
782 };
783
784 let bal = state.take_built_alloy_bal().expect("with_bal_builder was set");
785 state.merge_transitions(BundleRetention::Reverts);
786 let bundle_state = state.take_bundle();
787
788 (
789 ShadowOutput {
790 bundle_state,
791 receipts: block_result.receipts,
792 gas_used: block_result.gas_used,
793 requests: block_result.requests,
794 },
795 bal,
796 )
797 }
798
799 fn assert_shadow_equal(
801 evm_config: EthEvmConfig,
802 canonical_db_template: CacheDB<EmptyDB>,
803 block_header_only: SealedBlock<Block>,
804 txs: Vec<Recovered<TransactionSigned>>,
805 ) {
806 let (serial, reference_bal) =
808 run_serial_path(&evm_config, canonical_db_template.clone(), &block_header_only, &txs);
809
810 let bal_hash = alloy_eip7928::compute_block_access_list_hash(&reference_bal);
812 let block =
813 empty_amsterdam_block_with_gas_limit(bal_hash, block_header_only.header().gas_limit());
814
815 let bal_out = run_execute_block(
816 &Runtime::test(),
817 evm_config,
818 db_factory(canonical_db_template),
819 to_arc_decoded(reference_bal),
820 &block,
821 txs,
822 )
823 .unwrap_or_else(|e| panic!("BAL path failed: {e:?}"));
824
825 assert_eq!(
827 serial.receipts, bal_out.receipts,
828 "receipts diverge between serial and BAL paths",
829 );
830 assert_eq!(
831 serial.gas_used, bal_out.gas_used,
832 "gas_used differs: serial {} vs bal {}",
833 serial.gas_used, bal_out.gas_used,
834 );
835 assert_eq!(
836 serial.requests, bal_out.requests,
837 "requests (EIP-7685) diverge between serial and BAL paths",
838 );
839 assert_eq!(
840 serial.bundle_state, bal_out.state,
841 "bundle_state diverges — the canonical state transitions don't match",
842 );
843 }
844
845 #[test]
846 fn shadow_empty_block() {
847 assert_shadow_equal(
850 EthEvmConfig::mainnet(),
851 system_contracts_db(),
852 empty_amsterdam_block(B256::ZERO),
853 Vec::new(),
854 );
855 }
856
857 #[test]
858 fn shadow_multi_value_transfer() {
859 use alloy_consensus::TxLegacy;
862 use alloy_primitives::TxKind;
863 use reth_chainspec::MAINNET;
864 use reth_ethereum_primitives::Transaction;
865 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
866 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
867
868 let evm_config = EthEvmConfig::mainnet();
869 let carol: alloy_primitives::Address = alloy_primitives::Address::from([0xCA; 20]);
870 let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
871
872 let alice_kp = generate_key(&mut rng());
873 let alice = public_key_to_address(alice_kp.public_key());
874 let bob_kp = generate_key(&mut rng());
875 let bob = public_key_to_address(bob_kp.public_key());
876
877 let mut db = system_contracts_db();
878 insert_funded(&mut db, alice, sender_balance);
879 insert_funded(&mut db, bob, sender_balance);
880
881 let chain_id = MAINNET.chain.id();
882 let make_tx = |kp, to, value, nonce: u64| {
883 sign_tx_with_key_pair(
884 kp,
885 Transaction::Legacy(TxLegacy {
886 chain_id: Some(chain_id),
887 nonce,
888 gas_price: 1,
889 gas_limit: 21_000,
890 to: TxKind::Call(to),
891 value: U256::from(value),
892 input: Default::default(),
893 }),
894 )
895 };
896 let tx1 = Recovered::new_unchecked(make_tx(alice_kp, carol, 100u64, 0), alice);
897 let tx2 = Recovered::new_unchecked(make_tx(bob_kp, carol, 200u64, 0), bob);
898
899 assert_shadow_equal(evm_config, db, empty_amsterdam_block(B256::ZERO), vec![tx1, tx2]);
900 }
901
902 #[test]
903 fn rejects_tx_gas_limit_that_exceeds_remaining_block_gas() {
904 use alloy_consensus::TxLegacy;
908 use alloy_evm::block::BlockValidationError;
909 use alloy_primitives::TxKind;
910 use reth_chainspec::MAINNET;
911 use reth_ethereum_primitives::Transaction;
912 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
913 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
914
915 let evm_config = EthEvmConfig::mainnet();
916 let carol: alloy_primitives::Address = alloy_primitives::Address::from([0xCA; 20]);
917 let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
918 let block_gas_limit = 1_000_000;
919 let tx_gas_limit = 990_000;
920
921 let alice_kp = generate_key(&mut rng());
922 let alice = public_key_to_address(alice_kp.public_key());
923 let bob_kp = generate_key(&mut rng());
924 let bob = public_key_to_address(bob_kp.public_key());
925
926 let mut pre_block_db = system_contracts_db();
927 insert_funded(&mut pre_block_db, alice, sender_balance);
928 insert_funded(&mut pre_block_db, bob, sender_balance);
929
930 let chain_id = MAINNET.chain.id();
931 let make_tx = |kp, value| {
932 sign_tx_with_key_pair(
933 kp,
934 Transaction::Legacy(TxLegacy {
935 chain_id: Some(chain_id),
936 nonce: 0,
937 gas_price: 1,
938 gas_limit: tx_gas_limit,
939 to: TxKind::Call(carol),
940 value: U256::from(value),
941 input: Default::default(),
942 }),
943 )
944 };
945 let tx1 = Recovered::new_unchecked(make_tx(alice_kp, 100u64), alice);
946 let tx2 = Recovered::new_unchecked(make_tx(bob_kp, 200u64), bob);
947
948 let reference_block = empty_amsterdam_block(B256::ZERO);
952 let reference_bal = reference_bal_for_block(
953 &evm_config,
954 pre_block_db.clone(),
955 &reference_block,
956 vec![tx1.clone(), tx2.clone()],
957 );
958 let bal_hash = alloy_eip7928::compute_block_access_list_hash(&reference_bal);
959 let low_gas_block = empty_amsterdam_block_with_gas_limit(bal_hash, block_gas_limit);
960
961 let result = run_execute_block(
962 &Runtime::test(),
963 evm_config,
964 db_factory(pre_block_db),
965 to_arc_decoded(reference_bal),
966 &low_gas_block,
967 vec![tx1, tx2],
968 );
969
970 match result {
971 Err(BalExecutionError::Execution(err)) => assert!(matches!(
972 err.as_validation(),
973 Some(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas { .. })
974 )),
975 Err(err) => panic!("expected block gas validation error, got {err:?}"),
976 Ok(_) => panic!("expected block gas validation error, got Ok"),
977 }
978 }
979
980 #[test]
981 fn shadow_tx_with_revert() {
982 use alloy_consensus::TxLegacy;
988 use alloy_primitives::{keccak256, Bytes, TxKind};
989 use reth_chainspec::MAINNET;
990 use reth_ethereum_primitives::Transaction;
991 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
992 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
993
994 let evm_config = EthEvmConfig::mainnet();
995 let revert_contract: alloy_primitives::Address =
996 alloy_primitives::Address::from([0xDE; 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 revert_code: Bytes = Bytes::from_static(&[0x60, 0x00, 0x60, 0x00, 0xfd]);
1004 let code_hash = keccak256(&revert_code);
1005 let mut db = system_contracts_db();
1006 insert_funded(&mut db, alice, sender_balance);
1007 db.insert_account_info(
1008 revert_contract,
1009 AccountInfo {
1010 nonce: 1,
1011 balance: U256::ZERO,
1012 code_hash,
1013 code: Some(Bytecode::new_raw(revert_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: 50_000,
1026 to: TxKind::Call(revert_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 shadow_tx_with_sstore() {
1039 use alloy_consensus::TxLegacy;
1045 use alloy_primitives::{keccak256, Bytes, TxKind};
1046 use reth_chainspec::MAINNET;
1047 use reth_ethereum_primitives::Transaction;
1048 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
1049 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
1050
1051 let evm_config = EthEvmConfig::mainnet();
1052 let sstore_contract: alloy_primitives::Address =
1053 alloy_primitives::Address::from([0x55; 20]);
1054 let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
1055
1056 let alice_kp = generate_key(&mut rng());
1057 let alice = public_key_to_address(alice_kp.public_key());
1058
1059 let sstore_code: Bytes = Bytes::from_static(&[0x60, 0x42, 0x60, 0x00, 0x55, 0x00]);
1061 let code_hash = keccak256(&sstore_code);
1062 let mut db = system_contracts_db();
1063 insert_funded(&mut db, alice, sender_balance);
1064 db.insert_account_info(
1065 sstore_contract,
1066 AccountInfo {
1067 nonce: 1,
1068 balance: U256::ZERO,
1069 code_hash,
1070 code: Some(Bytecode::new_raw(sstore_code)),
1071 account_id: None,
1072 },
1073 );
1074
1075 let tx = Recovered::new_unchecked(
1076 sign_tx_with_key_pair(
1077 alice_kp,
1078 Transaction::Legacy(TxLegacy {
1079 chain_id: Some(MAINNET.chain.id()),
1080 nonce: 0,
1081 gas_price: 1,
1082 gas_limit: 100_000,
1083 to: TxKind::Call(sstore_contract),
1084 value: U256::ZERO,
1085 input: Default::default(),
1086 }),
1087 ),
1088 alice,
1089 );
1090
1091 assert_shadow_equal(evm_config, db, empty_amsterdam_block(B256::ZERO), vec![tx]);
1092 }
1093
1094 #[test]
1095 fn returns_built_bal_for_final_hash_mismatch() {
1096 use alloy_eip7928::AccountChanges;
1100
1101 let evm_config = EthEvmConfig::mainnet();
1102
1103 let real_bal = reference_bal_for_empty_block(&evm_config);
1105 assert!(!real_bal.is_empty(), "reference BAL must be non-empty");
1106
1107 let phantom = alloy_primitives::Address::from([0xFF; 20]);
1109 let mut tampered_entries: Vec<AccountChanges> = real_bal;
1110 tampered_entries.push(AccountChanges::new(phantom));
1111 let tampered_bal: alloy_eip7928::bal::Bal = alloy_eip7928::bal::Bal::new(tampered_entries);
1112
1113 let tampered_block_access_list: BlockAccessList = tampered_bal.clone().into();
1115 let tampered_hash =
1116 alloy_eip7928::compute_block_access_list_hash(&tampered_block_access_list);
1117 let block = empty_amsterdam_block(tampered_hash);
1118
1119 let received = {
1120 let raw = alloy_rlp::encode(&tampered_bal).into();
1121 Arc::new(DecodedBal::new(tampered_bal, raw))
1122 };
1123
1124 let result = run_execute_block_full(
1125 &Runtime::test(),
1126 evm_config,
1127 db_factory(system_contracts_db()),
1128 received,
1129 &block,
1130 Vec::<Recovered<TransactionSigned>>::new(),
1131 );
1132
1133 match result {
1134 Ok((_, built_bal)) => {
1135 let rebuilt = alloy_eip7928::compute_block_access_list_hash(&built_bal);
1136 assert_ne!(rebuilt, tampered_hash, "rebuilt and header hashes must differ");
1137 }
1138 Err(e) => panic!("expected success with rebuilt BAL, got {e:?}"),
1139 }
1140 }
1141
1142 #[test]
1143 fn canonical_make_db_failure() {
1144 let evm_config = EthEvmConfig::mainnet();
1147 let block = empty_amsterdam_block(B256::ZERO);
1148
1149 let failing_make_db = || -> Result<CacheDB<EmptyDB>, BalExecutionError> {
1150 Err(reth_provider::ProviderError::BestBlockNotFound.into())
1151 };
1152
1153 let result = run_execute_block(
1154 &Runtime::test(),
1155 evm_config,
1156 failing_make_db,
1157 to_arc_decoded(BlockAccessList::default()),
1158 &block,
1159 Vec::<Recovered<TransactionSigned>>::new(),
1160 );
1161
1162 assert!(
1163 matches!(result, Err(BalExecutionError::Provider(_))),
1164 "expected Provider error from canonical make_db failure, got {result:?}",
1165 );
1166 }
1167
1168 #[test]
1169 fn worker_tx_recovery_error_becomes_validation_error() {
1170 let evm_config = EthEvmConfig::mainnet();
1174 let block = empty_amsterdam_block(B256::ZERO);
1175
1176 let (tx_tx, tx_rx) = crossbeam_channel::unbounded::<(
1177 usize,
1178 Result<Recovered<TransactionSigned>, std::io::Error>,
1179 )>();
1180 tx_tx.send((0, Err(std::io::Error::other("sig fail")))).unwrap();
1181 drop(tx_tx);
1182
1183 let (receipt_tx, _receipt_rx) = crossbeam_channel::unbounded();
1184 let evm_env = evm_config.evm_env(block.header()).unwrap();
1185 let execution_ctx = evm_config.context_for_block(&block).unwrap();
1186 let make_db = db_factory(system_contracts_db());
1187 let make_db = |_: bool| make_db();
1188
1189 let result = execute_block(
1190 &Runtime::test(),
1191 &evm_config,
1192 &make_db,
1193 to_arc_decoded(BlockAccessList::default()),
1194 evm_env,
1195 execution_ctx,
1196 1, tx_rx,
1198 receipt_tx,
1199 );
1200
1201 assert!(
1202 matches!(result, Err(BalExecutionError::Execution(BlockExecutionError::Validation(_)))),
1203 "expected validation error from tx recovery failure, got {result:?}",
1204 );
1205 }
1206
1207 #[test]
1208 fn gas_tracker_non_amsterdam_uses_cumulative_gas() {
1209 use revm::{
1214 context::result::{
1215 ExecResultAndState, ExecutionResult, Output, ResultGas, SuccessReason,
1216 },
1217 state::EvmState,
1218 };
1219
1220 let block_gas_limit = 1_000_000u64;
1221 let first_tx_gas = 600_000u64;
1222 let second_tx_gas_limit = 500_000u64; let gas = ResultGas::new_with_state_gas(first_tx_gas, 0, 0, first_tx_gas / 2);
1226 let fake_result: ResultAndState<revm::context::result::HaltReason> =
1227 ExecResultAndState::new(
1228 ExecutionResult::Success {
1229 reason: SuccessReason::Return,
1230 gas,
1231 logs: vec![],
1232 output: Output::Call(Default::default()),
1233 },
1234 EvmState::default(),
1235 );
1236
1237 let mut non_amsterdam = BlockGasTracker::new(block_gas_limit, false, None);
1239 non_amsterdam.record_result(&fake_result);
1240 assert!(
1241 non_amsterdam.validate_tx_limit(second_tx_gas_limit).is_err(),
1242 "non-Amsterdam tracker must reject tx that exceeds remaining cumulative gas",
1243 );
1244
1245 let mut amsterdam = BlockGasTracker::new(block_gas_limit, true, None);
1247 amsterdam.record_result(&fake_result);
1248 assert!(
1249 amsterdam.validate_tx_limit(second_tx_gas_limit).is_ok(),
1250 "Amsterdam tracker must accept the same tx since per-dimension budgets still fit",
1251 );
1252 }
1253
1254 #[test]
1255 fn gas_tracker_amsterdam_enforces_state_gas_budget() {
1256 use revm::{
1260 context::result::{
1261 ExecResultAndState, ExecutionResult, Output, ResultGas, SuccessReason,
1262 },
1263 state::EvmState,
1264 };
1265
1266 let block_gas_limit = 1_000_000u64;
1267 let first_tx_state_gas = 600_000u64;
1268 let second_tx_gas_limit = 500_000u64; let gas = ResultGas::new_with_state_gas(first_tx_state_gas, 0, 0, first_tx_state_gas);
1271 let fake_result: ResultAndState<revm::context::result::HaltReason> =
1272 ExecResultAndState::new(
1273 ExecutionResult::Success {
1274 reason: SuccessReason::Return,
1275 gas,
1276 logs: vec![],
1277 output: Output::Call(Default::default()),
1278 },
1279 EvmState::default(),
1280 );
1281
1282 let mut amsterdam = BlockGasTracker::new(block_gas_limit, true, None);
1285 amsterdam.record_result(&fake_result);
1286 assert!(
1287 amsterdam.validate_tx_limit(second_tx_gas_limit).is_err(),
1288 "Amsterdam tracker must reject tx whose gas limit exceeds the remaining state budget",
1289 );
1290 assert!(
1291 amsterdam.validate_tx_limit(400_000).is_ok(),
1292 "Amsterdam tracker must accept tx whose gas limit exactly fits the state budget",
1293 );
1294
1295 let mut capped = BlockGasTracker::new(block_gas_limit, true, Some(400_000));
1298 capped.record_result(&fake_result);
1299 assert!(
1300 capped.validate_tx_limit(second_tx_gas_limit).is_err(),
1301 "tx_gas_limit_cap must not bound the state-gas admission check",
1302 );
1303 }
1304
1305 #[test]
1306 fn gas_tracker_caps_oversized_tx_gas_limit_at_tx_gas_limit_cap() {
1307 use revm::{
1310 context::result::{
1311 ExecResultAndState, ExecutionResult, Output, ResultGas, SuccessReason,
1312 },
1313 primitives::eip7825::TX_GAS_LIMIT_CAP,
1314 state::EvmState,
1315 };
1316
1317 let block_gas_limit = 30_000_000u64;
1318 let oversized = TX_GAS_LIMIT_CAP + 1_000_000; let tracker = BlockGasTracker::new(block_gas_limit, false, Some(TX_GAS_LIMIT_CAP));
1323 assert!(
1324 tracker.validate_tx_limit(oversized).is_ok(),
1325 "oversized tx must pass when capped limit fits in block gas",
1326 );
1327
1328 let prior_gas = 20_000_000u64;
1331 let gas = ResultGas::new_with_state_gas(prior_gas, 0, 0, prior_gas);
1332 let fake_result: ResultAndState<revm::context::result::HaltReason> =
1333 ExecResultAndState::new(
1334 ExecutionResult::Success {
1335 reason: SuccessReason::Return,
1336 gas,
1337 logs: vec![],
1338 output: Output::Call(Default::default()),
1339 },
1340 EvmState::default(),
1341 );
1342
1343 let mut tracker = BlockGasTracker::new(block_gas_limit, false, Some(TX_GAS_LIMIT_CAP));
1344 tracker.record_result(&fake_result);
1345 assert!(
1346 tracker.validate_tx_limit(oversized).is_err(),
1347 "oversized tx must be rejected when capped limit exceeds remaining block gas",
1348 );
1349 }
1350}