1use super::{
26 ordered_outputs::{ordered_worker_outputs, OrderedWorkerOutputError},
27 worker, BalExecutionError,
28};
29use alloy_eip7928::{
30 bal::{Bal as AlloyBal, DecodedBal},
31 compute_block_access_list_hash, BlockAccessList,
32};
33use alloy_evm::{
34 block::{BlockExecutionError, BlockExecutor, BlockValidationError, TxResult},
35 Evm,
36};
37use alloy_primitives::Address;
38use crossbeam_channel::{Receiver, Sender};
39use reth_engine_primitives::BlockAccessListDecodeError;
40use reth_evm::{execute::ExecutableTxFor, ConfigureEvm, Database, EvmEnvFor, ExecutionCtxFor};
41use reth_primitives_traits::ReceiptTy;
42use reth_provider::BlockExecutionOutput;
43use reth_tasks::Runtime;
44use revm::{
45 context::{result::ResultAndState, Block},
46 database::{states::bundle_state::BundleRetention, State},
47 state::bal::Bal as RevmBal,
48};
49use std::sync::Arc;
50
51use crate::tree::payload_processor::receipt_root_task::IndexedReceipt;
52
53#[expect(clippy::too_many_arguments, clippy::type_complexity)]
61pub fn execute_block<'a, Evm, Tx, Err, DB, MakeDb>(
62 runtime: &Runtime,
63 evm_config: &'a Evm,
64 make_db: &'a MakeDb,
65 input_bal: Arc<DecodedBal>,
66 evm_env: EvmEnvFor<Evm>,
67 ctx: ExecutionCtxFor<'a, Evm>,
68 transaction_count: usize,
69 txs: Receiver<(usize, Result<Tx, Err>)>,
70 receipt_tx: Sender<IndexedReceipt<ReceiptTy<Evm::Primitives>>>,
71) -> Result<
72 (BlockExecutionOutput<ReceiptTy<Evm::Primitives>>, Vec<Address>, BlockAccessList, Arc<RevmBal>),
73 BalExecutionError,
74>
75where
76 Evm: ConfigureEvm + 'static,
77 Tx: ExecutableTxFor<Evm> + Send + 'a,
78 Err: core::error::Error + Send + Sync + 'static,
79 DB: Database + Send + 'a,
80 MakeDb: Fn(bool) -> Result<DB, BalExecutionError> + Sync + 'a,
81 ReceiptTy<Evm::Primitives>: Clone,
82{
83 let worker_pool = runtime.bal_streaming_pool();
84 let worker_count = worker_pool.current_num_threads().max(1).min(transaction_count);
85
86 worker_pool.in_place_scope(|scope| {
87 execute_block_inner(
88 scope,
89 evm_config,
90 make_db,
91 input_bal,
92 evm_env,
93 ctx,
94 transaction_count,
95 txs,
96 receipt_tx,
97 worker_count,
98 )
99 })
100}
101
102#[expect(clippy::too_many_arguments, clippy::type_complexity)]
103fn execute_block_inner<'scope, Evm, Tx, Err, DB, MakeDb>(
104 scope: &rayon::Scope<'scope>,
105 evm_config: &'scope Evm,
106 make_db: &'scope MakeDb,
107 input_bal: Arc<DecodedBal>,
108 evm_env: EvmEnvFor<Evm>,
109 ctx: ExecutionCtxFor<'scope, Evm>,
110 transaction_count: usize,
111 txs: Receiver<(usize, Result<Tx, Err>)>,
112 receipt_tx: Sender<IndexedReceipt<ReceiptTy<Evm::Primitives>>>,
113 worker_count: usize,
114) -> Result<
115 (BlockExecutionOutput<ReceiptTy<Evm::Primitives>>, Vec<Address>, BlockAccessList, Arc<RevmBal>),
116 BalExecutionError,
117>
118where
119 Evm: ConfigureEvm + 'scope,
120 Tx: ExecutableTxFor<Evm> + Send + 'scope,
121 Err: core::error::Error + Send + Sync + 'static,
122 DB: Database + Send + 'scope,
123 MakeDb: Fn(bool) -> Result<DB, BalExecutionError> + Sync + 'scope,
124 ReceiptTy<Evm::Primitives>: Clone,
125{
126 let bal = input_bal.as_bal();
127 let input_bal_revm = convert_alloy_to_revm_bal(bal)?;
128
129 let block_gas_limit = evm_env.block_env.gas_limit();
130 let enable_amsterdam_eip8037 = evm_env.cfg_env.enable_amsterdam_eip8037;
131 let tx_gas_limit_cap = evm_env.cfg_env.tx_gas_limit_cap;
132 let mut canonical_state = State::builder()
133 .with_database(make_db(false)?)
134 .with_bundle_update()
135 .with_bal_builder()
136 .build();
137 canonical_state
138 .bal_state
139 .bal_builder
140 .as_mut()
141 .expect("with_bal_builder set")
142 .accounts
143 .reserve(bal.len());
144
145 let (block_result, senders) = {
146 let (result_tx, result_rx) = crossbeam_channel::unbounded();
147 let (abort_guard, abort_rx) = AbortGuard::new();
148
149 for _ in 0..worker_count {
150 worker::spawn_worker(
151 scope,
152 txs.clone(),
153 abort_rx.clone(),
154 result_tx.clone(),
155 evm_config,
156 make_db,
157 Arc::clone(&input_bal_revm),
158 evm_env.clone(),
159 ctx.clone(),
160 );
161 }
162 drop(result_tx);
163
164 let mut gas_tracker =
165 BlockGasTracker::new(block_gas_limit, enable_amsterdam_eip8037, tx_gas_limit_cap);
166 let evm = evm_config.evm_with_env(&mut canonical_state, evm_env);
167 let mut canonical_executor = evm_config.create_executor_with_state(evm, ctx.clone());
168
169 canonical_executor.apply_pre_execution_changes()?;
170 let mut senders = Vec::with_capacity(transaction_count);
171 let mut last_sent_len = 0usize;
172 for output in ordered_worker_outputs(&result_rx, transaction_count) {
173 let output = match output {
174 Ok(output) => output,
175 Err(OrderedWorkerOutputError::Worker(worker::BalWorkerError::Execution {
176 tx_index,
177 tx_gas_limit,
178 source,
179 })) => {
180 gas_tracker.validate_tx_limit(tx_gas_limit)?;
183
184 tracing::debug!(
187 target: "engine::tree::payload_processor::bal",
188 tx_index,
189 err = %source,
190 "Speculative BAL execution failed; rejecting block in transaction order"
191 );
192 return Err(BalExecutionError::Execution(source));
193 }
194 Err(err) => return Err(err.into()),
195 };
196
197 gas_tracker.validate_tx_limit(output.tx_gas_limit)?;
198 gas_tracker.record_result(output.result.result());
199 canonical_executor.evm_mut().db_mut().bump_bal_index();
200
201 let _ = canonical_executor.commit_transaction(output.result);
202 senders.push(output.signer);
203
204 let current_len = canonical_executor.receipts().len();
205 if current_len > last_sent_len {
206 last_sent_len = current_len;
207 if let Some(receipt) = canonical_executor.receipts().last() {
208 let tx_index = current_len - 1;
209 let _ = receipt_tx.send(IndexedReceipt::new(tx_index, receipt.clone()));
210 }
211 }
212 }
213 drop(abort_guard);
214
215 canonical_executor.evm_mut().db_mut().bump_bal_index();
216 let block_result = canonical_executor.apply_post_execution_changes()?;
217 (block_result, senders)
218 };
219
220 let built_bal = take_built_bal_and_log_divergence(&mut canonical_state, bal);
221
222 canonical_state.merge_transitions(BundleRetention::Reverts);
223 Ok((
224 BlockExecutionOutput { state: canonical_state.take_bundle(), result: block_result },
225 senders,
226 built_bal,
227 input_bal_revm,
228 ))
229}
230
231fn convert_alloy_to_revm_bal(alloy_bal: &AlloyBal) -> Result<Arc<RevmBal>, BalExecutionError> {
232 let received_bal_revm =
245 RevmBal::clone_from_alloy(alloy_bal.as_vec()).map_err(BlockAccessListDecodeError::new)?;
246 Ok(Arc::new(received_bal_revm))
247}
248
249fn take_built_bal_and_log_divergence<DB>(
250 canonical_state: &mut State<DB>,
251 received_bal: &AlloyBal,
252) -> BlockAccessList
253where
254 DB: Database,
255{
256 let built_bal = canonical_state.take_built_alloy_bal().expect("with_bal_builder set");
257 if tracing::enabled!(target: "engine::tree::payload_processor::bal", tracing::Level::DEBUG) &&
258 built_bal.as_slice() != received_bal.as_slice()
259 {
260 let rebuilt = compute_block_access_list_hash(built_bal.as_slice());
261 let expected = compute_block_access_list_hash(received_bal.as_slice());
262 let div = received_bal.diff(built_bal.as_slice());
263 tracing::debug!(
264 target: "engine::tree::payload_processor::bal",
265 %rebuilt,
266 %expected,
267 %div,
268 "first BAL divergence",
269 );
270 }
271
272 built_bal
273}
274
275struct AbortGuard {
277 _tx: Sender<()>,
278}
279
280impl AbortGuard {
281 fn new() -> (Self, Receiver<()>) {
282 let (tx, rx) = crossbeam_channel::bounded(0);
283 (Self { _tx: tx }, rx)
284 }
285}
286
287#[derive(Debug)]
289struct BlockGasTracker {
290 block_gas_limit: u64,
291 enable_amsterdam_eip8037: bool,
292 tx_gas_limit_cap: Option<u64>,
293 cumulative_tx_gas_used: u64,
294 block_regular_gas_used: u64,
295 block_state_gas_used: u64,
296}
297
298impl BlockGasTracker {
299 const fn new(
300 block_gas_limit: u64,
301 enable_amsterdam_eip8037: bool,
302 tx_gas_limit_cap: Option<u64>,
303 ) -> Self {
304 Self {
305 block_gas_limit,
306 enable_amsterdam_eip8037,
307 tx_gas_limit_cap,
308 cumulative_tx_gas_used: 0,
309 block_regular_gas_used: 0,
310 block_state_gas_used: 0,
311 }
312 }
313
314 fn validate_tx_limit(&self, tx_gas_limit: u64) -> Result<(), BlockExecutionError> {
331 let block_gas_used = if self.enable_amsterdam_eip8037 {
332 self.block_regular_gas_used
333 } else {
334 self.cumulative_tx_gas_used
335 };
336 let block_available_gas = self.block_gas_limit.saturating_sub(block_gas_used);
337 let tx_min_gas_limit =
338 self.tx_gas_limit_cap.map_or(tx_gas_limit, |cap| tx_gas_limit.min(cap));
339
340 if tx_min_gas_limit > block_available_gas {
341 return Err(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
342 transaction_gas_limit: tx_gas_limit,
343 block_available_gas,
344 }
345 .into());
346 }
347
348 if self.enable_amsterdam_eip8037 {
349 let state_gas_available =
350 self.block_gas_limit.saturating_sub(self.block_state_gas_used);
351 if tx_gas_limit > state_gas_available {
352 return Err(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
353 transaction_gas_limit: tx_gas_limit,
354 block_available_gas: state_gas_available,
355 }
356 .into());
357 }
358 }
359
360 Ok(())
361 }
362
363 const fn record_result<H>(&mut self, result: &ResultAndState<H>) {
364 let gas = result.result.gas();
365 self.cumulative_tx_gas_used = self.cumulative_tx_gas_used.saturating_add(gas.tx_gas_used());
366 self.block_regular_gas_used =
367 self.block_regular_gas_used.saturating_add(gas.block_regular_gas_used());
368 self.block_state_gas_used =
369 self.block_state_gas_used.saturating_add(gas.block_state_gas_used());
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376 use crate::tree::error::{InsertBlockErrorKind, InsertBlockValidationError};
377 use alloy_consensus::{BlockHeader, Header};
378 use alloy_eip7928::{
379 bal::Bal as AlloyBal, AccountChanges, BlockAccessIndex, BlockAccessList, CodeChange,
380 };
381 use alloy_eips::{
382 eip2935::{HISTORY_STORAGE_ADDRESS, HISTORY_STORAGE_CODE},
383 eip4788::{BEACON_ROOTS_ADDRESS, BEACON_ROOTS_CODE},
384 eip7002::{WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, WITHDRAWAL_REQUEST_PREDEPLOY_CODE},
385 };
386 use alloy_primitives::{B256, U256};
387 use reth_ethereum_primitives::{Block, BlockBody, Receipt, TransactionSigned};
388 use reth_evm_ethereum::EthEvmConfig;
389 use reth_primitives_traits::{Block as _, Recovered, SealedBlock};
390 use reth_revm::db::BundleState;
391 use reth_tasks::Runtime;
392 use revm::{
393 database::{CacheDB, EmptyDB},
394 state::{AccountInfo, Bytecode},
395 };
396 use std::convert::Infallible;
397
398 fn to_arc_decoded(bal: BlockAccessList) -> Arc<DecodedBal> {
400 let alloy_bal: AlloyBal = bal.into();
401 let raw = alloy_rlp::encode(&alloy_bal).into();
402 Arc::new(DecodedBal::new(alloy_bal, raw))
403 }
404
405 fn system_contracts_db() -> CacheDB<EmptyDB> {
409 let mut db = CacheDB::<EmptyDB>::new(Default::default());
410 db.insert_account_info(
411 BEACON_ROOTS_ADDRESS,
412 AccountInfo::from_bytecode(Bytecode::new_raw(BEACON_ROOTS_CODE.clone())),
413 );
414 db.insert_account_info(
415 WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS,
416 AccountInfo::from_bytecode(Bytecode::new_raw(
417 WITHDRAWAL_REQUEST_PREDEPLOY_CODE.clone(),
418 )),
419 );
420 db.insert_account_info(
421 HISTORY_STORAGE_ADDRESS,
422 AccountInfo::from_bytecode(Bytecode::new_raw(HISTORY_STORAGE_CODE.clone())),
423 );
424 db
425 }
426
427 fn empty_amsterdam_block(header_bal_hash: B256) -> SealedBlock<Block> {
429 empty_amsterdam_block_with_gas_limit(header_bal_hash, 30_000_000)
430 }
431
432 fn empty_amsterdam_block_with_gas_limit(
433 header_bal_hash: B256,
434 gas_limit: u64,
435 ) -> SealedBlock<Block> {
436 let header = Header {
437 timestamp: 1,
438 number: 1,
439 gas_limit,
440 parent_beacon_block_root: Some(B256::ZERO),
441 withdrawals_root: Some(alloy_consensus::EMPTY_ROOT_HASH),
442 requests_hash: Some(alloy_eips::eip7685::EMPTY_REQUESTS_HASH),
443 excess_blob_gas: Some(0),
444 blob_gas_used: Some(0),
445 block_access_list_hash: Some(header_bal_hash),
446 ..Header::default()
447 };
448 let block = Block {
449 header,
450 body: BlockBody {
451 transactions: vec![],
452 ommers: vec![],
453 withdrawals: Some(vec![].into()),
454 },
455 };
456 block.seal_slow()
457 }
458
459 fn reference_bal_for_empty_block(evm_config: &EthEvmConfig) -> BlockAccessList {
466 use revm::database::State as RevmState;
467
468 let db = system_contracts_db();
469 let mut state =
470 RevmState::builder().with_database(db).with_bundle_update().with_bal_builder().build();
471
472 let block = empty_amsterdam_block(B256::ZERO);
474 {
475 let mut executor =
476 evm_config.executor_for_block(&mut state, &block).expect("build executor");
477 executor.apply_pre_execution_changes().expect("pre-exec");
478 executor.evm_mut().db_mut().bump_bal_index();
479 executor.apply_post_execution_changes().expect("post-exec");
480 }
481 state.take_built_alloy_bal().expect("with_bal_builder was set")
482 }
483
484 #[test]
485 fn invalid_bal_bytecode_is_validation_error() {
486 let alloy_bal = vec![AccountChanges {
487 address: Address::ZERO,
488 code_changes: vec![CodeChange::new(
489 BlockAccessIndex::new(1),
490 vec![0xef, 0x01, 0xde].into(),
491 )],
492 ..Default::default()
493 }]
494 .into();
495
496 let error = convert_alloy_to_revm_bal(&alloy_bal).unwrap_err();
497 assert!(matches!(&error, BalExecutionError::BlockAccessListDecode(_)));
498 let error = InsertBlockErrorKind::from(error);
499 assert!(matches!(&error, InsertBlockErrorKind::BlockAccessListDecode(_)));
500 assert!(matches!(
501 error.ensure_validation_error(),
502 Ok(InsertBlockValidationError::BlockAccessListDecode(_))
503 ));
504 }
505
506 #[test]
507 fn empty_block_happy_path_round_trip() {
508 let evm_config = EthEvmConfig::mainnet();
514
515 let input_bal = reference_bal_for_empty_block(&evm_config);
516 let bal_hash = alloy_eip7928::compute_block_access_list_hash(&input_bal);
517 assert!(!input_bal.is_empty(), "empty BAL means system calls didn't record state");
519
520 let block = empty_amsterdam_block(bal_hash);
521
522 let result = run_execute_block(
523 &Runtime::test(),
524 evm_config,
525 db_factory(system_contracts_db()),
526 to_arc_decoded(input_bal),
527 &block,
528 Vec::<Recovered<TransactionSigned>>::new(),
529 );
530
531 match result {
532 Ok(output) => {
533 assert!(output.receipts.is_empty(), "empty block → no receipts");
534 }
535 Err(e) => panic!("expected success, got {e:?}"),
536 }
537 }
538
539 fn db_factory(
540 db: CacheDB<EmptyDB>,
541 ) -> impl Fn() -> Result<CacheDB<EmptyDB>, BalExecutionError> + Sync {
542 move || Ok(db.clone())
543 }
544
545 fn tx_stream<Tx>(txs: Vec<Tx>) -> Receiver<(usize, Result<Tx, Infallible>)> {
546 let (tx, rx) = crossbeam_channel::unbounded();
547 for (index, transaction) in txs.into_iter().enumerate() {
548 tx.send((index, Ok(transaction))).unwrap();
549 }
550 rx
551 }
552
553 fn run_execute_block<Tx, DB, MakeDb>(
554 runtime: &Runtime,
555 evm_config: EthEvmConfig,
556 make_db: MakeDb,
557 input_bal: Arc<DecodedBal>,
558 block: &SealedBlock<Block>,
559 txs: Vec<Tx>,
560 ) -> Result<BlockExecutionOutput<Receipt>, BalExecutionError>
561 where
562 Tx: ExecutableTxFor<EthEvmConfig> + Send,
563 DB: Database + Send,
564 MakeDb: Fn() -> Result<DB, BalExecutionError> + Sync,
565 {
566 run_execute_block_full(runtime, evm_config, make_db, input_bal, block, txs)
567 .map(|(output, _)| output)
568 }
569
570 fn run_execute_block_full<Tx, DB, MakeDb>(
571 runtime: &Runtime,
572 evm_config: EthEvmConfig,
573 make_db: MakeDb,
574 input_bal: Arc<DecodedBal>,
575 block: &SealedBlock<Block>,
576 txs: Vec<Tx>,
577 ) -> Result<(BlockExecutionOutput<Receipt>, BlockAccessList), BalExecutionError>
578 where
579 Tx: ExecutableTxFor<EthEvmConfig> + Send,
580 DB: Database + Send,
581 MakeDb: Fn() -> Result<DB, BalExecutionError> + Sync,
582 {
583 let transaction_count = txs.len();
584 let (receipt_tx, _receipt_rx) = crossbeam_channel::unbounded();
585 let evm_env = evm_config.evm_env(block.header()).unwrap();
586 let execution_ctx = evm_config.context_for_block(block).unwrap();
587 let make_db = |_: bool| make_db();
588 execute_block(
589 runtime,
590 &evm_config,
591 &make_db,
592 input_bal,
593 evm_env,
594 execution_ctx,
595 transaction_count,
596 tx_stream(txs),
597 receipt_tx,
598 )
599 .map(|(output, _, built_bal, _)| (output, built_bal))
600 }
601
602 fn insert_funded(db: &mut CacheDB<EmptyDB>, addr: alloy_primitives::Address, balance: U256) {
604 db.insert_account_info(
605 addr,
606 AccountInfo { balance, code_hash: B256::ZERO, code: None, ..Default::default() },
607 );
608 }
609
610 fn reference_bal_for_block<Tx>(
613 evm_config: &EthEvmConfig,
614 mut db: CacheDB<EmptyDB>,
615 block: &SealedBlock<Block>,
616 txs: Vec<Tx>,
617 ) -> BlockAccessList
618 where
619 Tx: ExecutableTxFor<EthEvmConfig>,
620 {
621 use revm::database::State as RevmState;
622
623 let mut state = RevmState::builder()
624 .with_database(&mut db)
625 .with_bundle_update()
626 .with_bal_builder()
627 .build();
628
629 {
630 let mut executor =
631 evm_config.executor_for_block(&mut state, block).expect("build executor");
632 executor.apply_pre_execution_changes().expect("pre-exec");
633 for (i, tx) in txs.into_iter().enumerate() {
634 executor.evm_mut().db_mut().bump_bal_index();
635 executor
636 .execute_transaction(tx)
637 .unwrap_or_else(|e| panic!("tx {i} failed during reference build: {e:?}"));
638 }
639 executor.evm_mut().db_mut().bump_bal_index();
640 executor.apply_post_execution_changes().expect("post-exec");
641 }
642 state.take_built_alloy_bal().expect("with_bal_builder was set")
643 }
644
645 #[test]
646 fn multi_tx_happy_path_round_trip() {
647 use alloy_consensus::TxLegacy;
655 use alloy_primitives::TxKind;
656 use reth_chainspec::MAINNET;
657 use reth_ethereum_primitives::Transaction;
658 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
659 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
660
661 let evm_config = EthEvmConfig::mainnet();
662 let carol: alloy_primitives::Address = alloy_primitives::Address::from([0xCA; 20]);
663 let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
664
665 let alice_kp = generate_key(&mut rng());
667 let alice = public_key_to_address(alice_kp.public_key());
668 let bob_kp = generate_key(&mut rng());
669 let bob = public_key_to_address(bob_kp.public_key());
670
671 let mut pre_block_db = system_contracts_db();
673 insert_funded(&mut pre_block_db, alice, sender_balance);
674 insert_funded(&mut pre_block_db, bob, sender_balance);
675
676 let chain_id = MAINNET.chain.id();
678 let gas_price = 1u128; let tx1 = sign_tx_with_key_pair(
680 alice_kp,
681 Transaction::Legacy(TxLegacy {
682 chain_id: Some(chain_id),
683 nonce: 0,
684 gas_price,
685 gas_limit: 21_000,
686 to: TxKind::Call(carol),
687 value: U256::from(100u64),
688 input: Default::default(),
689 }),
690 );
691 let tx2 = sign_tx_with_key_pair(
692 bob_kp,
693 Transaction::Legacy(TxLegacy {
694 chain_id: Some(chain_id),
695 nonce: 0,
696 gas_price,
697 gas_limit: 21_000,
698 to: TxKind::Call(carol),
699 value: U256::from(200u64),
700 input: Default::default(),
701 }),
702 );
703 let recovered1 = Recovered::new_unchecked(tx1, alice);
704 let recovered2 = Recovered::new_unchecked(tx2, bob);
705
706 let block_for_ref = empty_amsterdam_block(B256::ZERO);
708 let reference_bal = reference_bal_for_block::<Recovered<TransactionSigned>>(
709 &evm_config,
710 {
711 let mut db = system_contracts_db();
713 db.insert_account_info(
714 alice,
715 AccountInfo {
716 balance: sender_balance,
717 code_hash: B256::ZERO,
718 code: None,
719 ..Default::default()
720 },
721 );
722 db.insert_account_info(
723 bob,
724 AccountInfo {
725 balance: sender_balance,
726 code_hash: B256::ZERO,
727 code: None,
728 ..Default::default()
729 },
730 );
731 db
732 },
733 &block_for_ref,
734 vec![recovered1.clone(), recovered2.clone()],
735 );
736 assert!(!reference_bal.is_empty(), "expected BAL entries from pre-exec + txs");
737
738 let bal_hash = alloy_eip7928::compute_block_access_list_hash(&reference_bal);
739 let block = empty_amsterdam_block(bal_hash);
740
741 let result = run_execute_block(
742 &Runtime::test(),
743 evm_config,
744 db_factory(pre_block_db),
745 to_arc_decoded(reference_bal),
746 &block,
747 vec![recovered1, recovered2],
748 );
749
750 match result {
751 Ok(output) => {
752 assert_eq!(output.receipts.len(), 2, "expected 2 receipts");
753 assert!(output.gas_used >= 2 * 21_000, "expected at least 42k gas used");
754 }
755 Err(e) => panic!("expected success, got {e:?}"),
756 }
757 }
758
759 #[derive(Debug)]
767 struct ShadowOutput {
768 bundle_state: BundleState,
769 receipts: Vec<reth_ethereum_primitives::Receipt>,
770 gas_used: u64,
771 requests: alloy_eips::eip7685::Requests,
772 }
773
774 fn run_serial_path(
779 evm_config: &EthEvmConfig,
780 canonical_db: CacheDB<EmptyDB>,
781 block: &SealedBlock<Block>,
782 txs: &[Recovered<TransactionSigned>],
783 ) -> (ShadowOutput, BlockAccessList) {
784 use revm::database::State as RevmState;
785
786 let mut state = RevmState::builder()
787 .with_database(canonical_db)
788 .with_bundle_update()
789 .with_bal_builder()
790 .build();
791
792 let block_result = {
793 let mut executor =
794 evm_config.executor_for_block(&mut state, block).expect("build serial executor");
795 executor.apply_pre_execution_changes().expect("serial pre-exec");
796 for (i, tx) in txs.iter().cloned().enumerate() {
797 executor.evm_mut().db_mut().bump_bal_index();
798 executor
799 .execute_transaction(tx)
800 .unwrap_or_else(|e| panic!("serial tx {i} failed: {e:?}"));
801 }
802 executor.evm_mut().db_mut().bump_bal_index();
803 executor.apply_post_execution_changes().expect("serial post-exec")
804 };
805
806 let bal = state.take_built_alloy_bal().expect("with_bal_builder was set");
807 state.merge_transitions(BundleRetention::Reverts);
808 let bundle_state = state.take_bundle();
809
810 (
811 ShadowOutput {
812 bundle_state,
813 receipts: block_result.receipts,
814 gas_used: block_result.gas_used,
815 requests: block_result.requests,
816 },
817 bal,
818 )
819 }
820
821 fn assert_shadow_equal(
823 evm_config: EthEvmConfig,
824 canonical_db_template: CacheDB<EmptyDB>,
825 block_header_only: SealedBlock<Block>,
826 txs: Vec<Recovered<TransactionSigned>>,
827 ) {
828 let (serial, reference_bal) =
830 run_serial_path(&evm_config, canonical_db_template.clone(), &block_header_only, &txs);
831
832 let bal_hash = alloy_eip7928::compute_block_access_list_hash(&reference_bal);
834 let block =
835 empty_amsterdam_block_with_gas_limit(bal_hash, block_header_only.header().gas_limit());
836
837 let bal_out = run_execute_block(
838 &Runtime::test(),
839 evm_config,
840 db_factory(canonical_db_template),
841 to_arc_decoded(reference_bal),
842 &block,
843 txs,
844 )
845 .unwrap_or_else(|e| panic!("BAL path failed: {e:?}"));
846
847 assert_eq!(
849 serial.receipts, bal_out.receipts,
850 "receipts diverge between serial and BAL paths",
851 );
852 assert_eq!(
853 serial.gas_used, bal_out.gas_used,
854 "gas_used differs: serial {} vs bal {}",
855 serial.gas_used, bal_out.gas_used,
856 );
857 assert_eq!(
858 serial.requests, bal_out.requests,
859 "requests (EIP-7685) diverge between serial and BAL paths",
860 );
861 assert_eq!(
862 serial.bundle_state, bal_out.state,
863 "bundle_state diverges — the canonical state transitions don't match",
864 );
865 }
866
867 #[test]
868 fn shadow_empty_block() {
869 assert_shadow_equal(
872 EthEvmConfig::mainnet(),
873 system_contracts_db(),
874 empty_amsterdam_block(B256::ZERO),
875 Vec::new(),
876 );
877 }
878
879 #[test]
880 fn shadow_multi_value_transfer() {
881 use alloy_consensus::TxLegacy;
884 use alloy_primitives::TxKind;
885 use reth_chainspec::MAINNET;
886 use reth_ethereum_primitives::Transaction;
887 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
888 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
889
890 let evm_config = EthEvmConfig::mainnet();
891 let carol: alloy_primitives::Address = alloy_primitives::Address::from([0xCA; 20]);
892 let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
893
894 let alice_kp = generate_key(&mut rng());
895 let alice = public_key_to_address(alice_kp.public_key());
896 let bob_kp = generate_key(&mut rng());
897 let bob = public_key_to_address(bob_kp.public_key());
898
899 let mut db = system_contracts_db();
900 insert_funded(&mut db, alice, sender_balance);
901 insert_funded(&mut db, bob, sender_balance);
902
903 let chain_id = MAINNET.chain.id();
904 let make_tx = |kp, to, value, nonce: u64| {
905 sign_tx_with_key_pair(
906 kp,
907 Transaction::Legacy(TxLegacy {
908 chain_id: Some(chain_id),
909 nonce,
910 gas_price: 1,
911 gas_limit: 21_000,
912 to: TxKind::Call(to),
913 value: U256::from(value),
914 input: Default::default(),
915 }),
916 )
917 };
918 let tx1 = Recovered::new_unchecked(make_tx(alice_kp, carol, 100u64, 0), alice);
919 let tx2 = Recovered::new_unchecked(make_tx(bob_kp, carol, 200u64, 0), bob);
920
921 assert_shadow_equal(evm_config, db, empty_amsterdam_block(B256::ZERO), vec![tx1, tx2]);
922 }
923
924 #[test]
925 fn rejects_tx_gas_limit_that_exceeds_remaining_block_gas() {
926 use alloy_consensus::TxLegacy;
930 use alloy_evm::block::BlockValidationError;
931 use alloy_primitives::TxKind;
932 use reth_chainspec::MAINNET;
933 use reth_ethereum_primitives::Transaction;
934 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
935 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
936
937 let evm_config = EthEvmConfig::mainnet();
938 let carol: alloy_primitives::Address = alloy_primitives::Address::from([0xCA; 20]);
939 let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
940 let block_gas_limit = 1_000_000;
941 let tx_gas_limit = 990_000;
942
943 let alice_kp = generate_key(&mut rng());
944 let alice = public_key_to_address(alice_kp.public_key());
945 let bob_kp = generate_key(&mut rng());
946 let bob = public_key_to_address(bob_kp.public_key());
947
948 let mut pre_block_db = system_contracts_db();
949 insert_funded(&mut pre_block_db, alice, sender_balance);
950 insert_funded(&mut pre_block_db, bob, sender_balance);
951
952 let chain_id = MAINNET.chain.id();
953 let make_tx = |kp, value| {
954 sign_tx_with_key_pair(
955 kp,
956 Transaction::Legacy(TxLegacy {
957 chain_id: Some(chain_id),
958 nonce: 0,
959 gas_price: 1,
960 gas_limit: tx_gas_limit,
961 to: TxKind::Call(carol),
962 value: U256::from(value),
963 input: Default::default(),
964 }),
965 )
966 };
967 let tx1 = Recovered::new_unchecked(make_tx(alice_kp, 100u64), alice);
968 let tx2 = Recovered::new_unchecked(make_tx(bob_kp, 200u64), bob);
969
970 let reference_block = empty_amsterdam_block(B256::ZERO);
974 let reference_bal = reference_bal_for_block(
975 &evm_config,
976 pre_block_db.clone(),
977 &reference_block,
978 vec![tx1.clone(), tx2.clone()],
979 );
980 let bal_hash = alloy_eip7928::compute_block_access_list_hash(&reference_bal);
981 let low_gas_block = empty_amsterdam_block_with_gas_limit(bal_hash, block_gas_limit);
982
983 let result = run_execute_block(
984 &Runtime::test(),
985 evm_config,
986 db_factory(pre_block_db),
987 to_arc_decoded(reference_bal),
988 &low_gas_block,
989 vec![tx1, tx2],
990 );
991
992 match result {
993 Err(BalExecutionError::Execution(err)) => assert!(matches!(
994 err.as_validation(),
995 Some(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas { .. })
996 )),
997 Err(err) => panic!("expected block gas validation error, got {err:?}"),
998 Ok(_) => panic!("expected block gas validation error, got Ok"),
999 }
1000 }
1001
1002 fn two_transfers_with_reference_bal(
1005 evm_config: &EthEvmConfig,
1006 tx_gas_limit: u64,
1007 bal_covers_first: bool,
1008 ) -> (
1009 CacheDB<EmptyDB>,
1010 BlockAccessList,
1011 Recovered<reth_ethereum_primitives::TransactionSigned>,
1012 Recovered<reth_ethereum_primitives::TransactionSigned>,
1013 ) {
1014 use alloy_consensus::TxLegacy;
1015 use alloy_primitives::TxKind;
1016 use reth_chainspec::MAINNET;
1017 use reth_ethereum_primitives::Transaction;
1018 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
1019 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
1020
1021 let recipient = alloy_primitives::Address::from([0xCA; 20]);
1022 let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
1023
1024 let alice_kp = generate_key(&mut rng());
1025 let alice = public_key_to_address(alice_kp.public_key());
1026 let bob_kp = generate_key(&mut rng());
1027 let bob = public_key_to_address(bob_kp.public_key());
1028
1029 let mut pre_block_db = system_contracts_db();
1030 insert_funded(&mut pre_block_db, alice, sender_balance);
1031 insert_funded(&mut pre_block_db, bob, sender_balance);
1032
1033 let chain_id = MAINNET.chain.id();
1034 let make_tx = |kp, value| {
1035 sign_tx_with_key_pair(
1036 kp,
1037 Transaction::Legacy(TxLegacy {
1038 chain_id: Some(chain_id),
1039 nonce: 0,
1040 gas_price: 1,
1041 gas_limit: tx_gas_limit,
1042 to: TxKind::Call(recipient),
1043 value: U256::from(value),
1044 input: Default::default(),
1045 }),
1046 )
1047 };
1048 let tx1 = Recovered::new_unchecked(make_tx(alice_kp, 100u64), alice);
1049 let tx2 = Recovered::new_unchecked(make_tx(bob_kp, 200u64), bob);
1050
1051 let reference_block = empty_amsterdam_block(B256::ZERO);
1052 let covered = if bal_covers_first { vec![tx1.clone()] } else { vec![] };
1053 let reference_bal =
1054 reference_bal_for_block(evm_config, pre_block_db.clone(), &reference_block, covered);
1055 (pre_block_db, reference_bal, tx1, tx2)
1056 }
1057
1058 #[test]
1059 fn propagates_bal_miss_of_admitted_transaction_as_block_verdict() {
1060 let (pre_block_db, reference_bal, tx1, tx2) =
1063 two_transfers_with_reference_bal(&EthEvmConfig::mainnet(), 100_000, false);
1064 let bal_hash = alloy_eip7928::compute_block_access_list_hash(&reference_bal);
1065 let block = empty_amsterdam_block(bal_hash);
1066
1067 let result = run_execute_block(
1068 &Runtime::test(),
1069 EthEvmConfig::mainnet(),
1070 db_factory(pre_block_db),
1071 to_arc_decoded(reference_bal),
1072 &block,
1073 vec![tx1, tx2],
1074 );
1075
1076 match result {
1077 Err(BalExecutionError::Execution(err)) => {
1078 let msg = err.to_string();
1079 assert!(msg.contains("not found in BAL"), "expected BAL miss error, got {msg}");
1080 }
1081 Err(err) => panic!("expected BAL execution error, got {err:?}"),
1082 Ok(_) => panic!("expected BAL execution error, got Ok"),
1083 }
1084 }
1085
1086 #[test]
1087 fn admission_rejects_before_speculative_worker_failure_surfaces() {
1088 let (pre_block_db, reference_bal, tx1, tx2) =
1091 two_transfers_with_reference_bal(&EthEvmConfig::mainnet(), 990_000, true);
1092 let bal_hash = alloy_eip7928::compute_block_access_list_hash(&reference_bal);
1093 let low_gas_block = empty_amsterdam_block_with_gas_limit(bal_hash, 1_000_000);
1094
1095 let result = run_execute_block(
1096 &Runtime::test(),
1097 EthEvmConfig::mainnet(),
1098 db_factory(pre_block_db),
1099 to_arc_decoded(reference_bal),
1100 &low_gas_block,
1101 vec![tx1, tx2],
1102 );
1103
1104 match result {
1105 Err(BalExecutionError::Execution(err)) => assert!(
1106 matches!(
1107 err.as_validation(),
1108 Some(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas { .. })
1109 ),
1110 "expected block gas validation error, got {err:?}"
1111 ),
1112 Err(err) => panic!("expected block gas validation error, got {err:?}"),
1113 Ok(_) => panic!("expected block gas validation error, got Ok"),
1114 }
1115 }
1116
1117 #[test]
1118 fn worker_reuses_provider_after_failure_and_finishes_earlier_transactions() {
1119 use std::sync::atomic::{AtomicUsize, Ordering};
1120
1121 let evm_config = EthEvmConfig::mainnet();
1122 let (pre_block_db, reference_bal, tx1, tx2) =
1123 two_transfers_with_reference_bal(&evm_config, 100_000, true);
1124 let block = empty_amsterdam_block(B256::ZERO);
1125 let received_bal = convert_alloy_to_revm_bal(&reference_bal.into()).unwrap();
1126 let setups = AtomicUsize::new(0);
1127 let make_db = |_: bool| {
1128 if setups.fetch_add(1, Ordering::Relaxed) != 0 {
1131 return Err(BalExecutionError::Provider(
1132 reth_errors::ProviderError::HeaderNotFound(B256::ZERO.into()),
1133 ));
1134 }
1135 Ok(pre_block_db.clone())
1136 };
1137 let (tx_tx, tx_rx) = crossbeam_channel::unbounded();
1138 tx_tx.send((2, Err(std::io::Error::other("sig fail")))).unwrap();
1141 tx_tx.send((1, Ok(tx2))).unwrap();
1142 tx_tx.send((0, Ok(tx1.clone()))).unwrap();
1143 drop(tx_tx);
1144 let (_abort_guard, abort_rx) = AbortGuard::new();
1145 let (result_tx, result_rx) = crossbeam_channel::unbounded();
1146 let runtime = Runtime::test();
1147 runtime.bal_streaming_pool().in_place_scope(|scope| {
1148 worker::spawn_worker(
1149 scope,
1150 tx_rx,
1151 abort_rx,
1152 result_tx,
1153 &evm_config,
1154 &make_db,
1155 received_bal,
1156 evm_config.evm_env(block.header()).unwrap(),
1157 evm_config.context_for_block(&block).unwrap(),
1158 );
1159 });
1160 let mut outputs = ordered_worker_outputs(&result_rx, 3);
1161 let output = outputs.next().unwrap().unwrap();
1162 assert_eq!(output.index, 0);
1163 assert_eq!(output.signer, tx1.signer());
1164 let mut state = State::builder().with_database(pre_block_db).build();
1165 let mut serial = evm_config.executor_for_block(&mut state, &block).unwrap();
1166 serial.apply_pre_execution_changes().unwrap();
1167 let expected = serial.execute_transaction_without_commit(tx1).unwrap();
1168 assert_eq!(output.result.result(), expected.result());
1169 assert!(matches!(
1170 outputs.next().unwrap(),
1171 Err(OrderedWorkerOutputError::Worker(worker::BalWorkerError::Execution {
1172 tx_index: 1,
1173 ..
1174 }))
1175 ));
1176 let error = outputs.next().unwrap().err().unwrap();
1177 assert!(matches!(
1178 error,
1179 OrderedWorkerOutputError::Worker(worker::BalWorkerError::Transaction {
1180 tx_index: 2,
1181 ..
1182 })
1183 ));
1184 assert!(matches!(
1185 BalExecutionError::from(error),
1186 BalExecutionError::Execution(BlockExecutionError::Validation(_))
1187 ));
1188 assert!(outputs.next().is_none());
1189 assert_eq!(setups.load(Ordering::Relaxed), 1);
1190 }
1191
1192 #[test]
1193 fn amsterdam_state_gas_admission_matches_serial_execution() {
1194 let evm_config = EthEvmConfig::new(Arc::new(
1195 reth_chainspec::ChainSpecBuilder::mainnet().amsterdam_activated().build(),
1196 ));
1197 for (tx_gas_limit, complete_bal) in [(500_000, true), (990_000, true), (990_000, false)] {
1198 let (pre_block_db, partial_bal, tx1, tx2) =
1199 two_transfers_with_reference_bal(&evm_config, tx_gas_limit, true);
1200 let reference_block = empty_amsterdam_block(B256::ZERO);
1201 let reference_bal = if complete_bal {
1202 reference_bal_for_block(
1203 &evm_config,
1204 pre_block_db.clone(),
1205 &reference_block,
1206 vec![tx1.clone(), tx2.clone()],
1207 )
1208 } else {
1209 partial_bal
1211 };
1212 let block = empty_amsterdam_block_with_gas_limit(
1213 compute_block_access_list_hash(&reference_bal),
1214 1_000_000,
1215 );
1216 let mut evm_env = evm_config.evm_env(block.header()).unwrap();
1217 assert!(evm_env.cfg_env.enable_amsterdam_eip8037);
1218 evm_env.cfg_env.tx_gas_limit_cap = Some(400_000);
1221 let ctx = evm_config.context_for_block(&block).unwrap();
1222 let mut state = State::builder().with_database(pre_block_db.clone()).build();
1223 let evm = evm_config.evm_with_env(&mut state, evm_env.clone());
1224 let mut serial = evm_config.create_executor_with_state(evm, ctx.clone());
1225 serial.apply_pre_execution_changes().unwrap();
1226 serial.execute_transaction(tx1.clone()).unwrap();
1227 assert!(serial.block_state_gas_used > 10_000);
1228 assert!(serial.block_regular_gas_used + 400_000 < 1_000_000);
1229 let serial_result = serial.execute_transaction(tx2.clone());
1230
1231 let (receipt_tx, _receipt_rx) = crossbeam_channel::unbounded();
1232 let parallel_result = execute_block(
1233 &Runtime::test(),
1234 &evm_config,
1235 &|_: bool| Ok(pre_block_db.clone()),
1236 to_arc_decoded(reference_bal),
1237 evm_env,
1238 ctx,
1239 2,
1240 tx_stream(vec![tx1, tx2]),
1241 receipt_tx,
1242 );
1243 if tx_gas_limit == 500_000 {
1244 serial_result.unwrap();
1245 assert_eq!(parallel_result.unwrap().0.receipts, serial.receipts());
1246 } else {
1247 let serial_error = serial_result.unwrap_err();
1248 let Some(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
1249 block_available_gas,
1250 ..
1251 }) = serial_error.as_validation()
1252 else {
1253 panic!("expected serial state-gas admission error, got {serial_error:?}");
1254 };
1255 assert_eq!(*block_available_gas, 1_000_000 - serial.block_state_gas_used);
1256 let Err(BalExecutionError::Execution(parallel_error)) = parallel_result else {
1257 panic!("expected parallel state-gas admission error");
1258 };
1259 assert_eq!(parallel_error.to_string(), serial_error.to_string());
1260 }
1261 }
1262 }
1263
1264 #[test]
1265 fn shadow_tx_with_revert() {
1266 use alloy_consensus::TxLegacy;
1272 use alloy_primitives::{Bytes, TxKind};
1273 use reth_chainspec::MAINNET;
1274 use reth_ethereum_primitives::Transaction;
1275 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
1276 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
1277
1278 let evm_config = EthEvmConfig::mainnet();
1279 let revert_contract: alloy_primitives::Address =
1280 alloy_primitives::Address::from([0xDE; 20]);
1281 let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
1282
1283 let alice_kp = generate_key(&mut rng());
1284 let alice = public_key_to_address(alice_kp.public_key());
1285
1286 let revert_code: Bytes = Bytes::from_static(&[0x60, 0x00, 0x60, 0x00, 0xfd]);
1288 let mut db = system_contracts_db();
1289 insert_funded(&mut db, alice, sender_balance);
1290 db.insert_account_info(
1291 revert_contract,
1292 AccountInfo::from_bytecode(Bytecode::new_raw(revert_code)),
1293 );
1294
1295 let tx = Recovered::new_unchecked(
1296 sign_tx_with_key_pair(
1297 alice_kp,
1298 Transaction::Legacy(TxLegacy {
1299 chain_id: Some(MAINNET.chain.id()),
1300 nonce: 0,
1301 gas_price: 1,
1302 gas_limit: 50_000,
1303 to: TxKind::Call(revert_contract),
1304 value: U256::ZERO,
1305 input: Default::default(),
1306 }),
1307 ),
1308 alice,
1309 );
1310
1311 assert_shadow_equal(evm_config, db, empty_amsterdam_block(B256::ZERO), vec![tx]);
1312 }
1313
1314 #[test]
1315 fn shadow_tx_with_sstore() {
1316 use alloy_consensus::TxLegacy;
1322 use alloy_primitives::{Bytes, TxKind};
1323 use reth_chainspec::MAINNET;
1324 use reth_ethereum_primitives::Transaction;
1325 use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
1326 use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
1327
1328 let evm_config = EthEvmConfig::mainnet();
1329 let sstore_contract: alloy_primitives::Address =
1330 alloy_primitives::Address::from([0x55; 20]);
1331 let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
1332
1333 let alice_kp = generate_key(&mut rng());
1334 let alice = public_key_to_address(alice_kp.public_key());
1335
1336 let sstore_code: Bytes = Bytes::from_static(&[0x60, 0x42, 0x60, 0x00, 0x55, 0x00]);
1338 let mut db = system_contracts_db();
1339 insert_funded(&mut db, alice, sender_balance);
1340 db.insert_account_info(
1341 sstore_contract,
1342 AccountInfo::from_bytecode(Bytecode::new_raw(sstore_code)),
1343 );
1344
1345 let tx = Recovered::new_unchecked(
1346 sign_tx_with_key_pair(
1347 alice_kp,
1348 Transaction::Legacy(TxLegacy {
1349 chain_id: Some(MAINNET.chain.id()),
1350 nonce: 0,
1351 gas_price: 1,
1352 gas_limit: 100_000,
1353 to: TxKind::Call(sstore_contract),
1354 value: U256::ZERO,
1355 input: Default::default(),
1356 }),
1357 ),
1358 alice,
1359 );
1360
1361 assert_shadow_equal(evm_config, db, empty_amsterdam_block(B256::ZERO), vec![tx]);
1362 }
1363
1364 #[test]
1365 fn returns_built_bal_for_final_hash_mismatch() {
1366 use alloy_eip7928::AccountChanges;
1370
1371 let evm_config = EthEvmConfig::mainnet();
1372
1373 let real_bal = reference_bal_for_empty_block(&evm_config);
1375 assert!(!real_bal.is_empty(), "reference BAL must be non-empty");
1376
1377 let phantom = alloy_primitives::Address::from([0xFF; 20]);
1379 let mut tampered_entries: Vec<AccountChanges> = real_bal;
1380 tampered_entries.push(AccountChanges::new(phantom));
1381 let tampered_bal: alloy_eip7928::bal::Bal = alloy_eip7928::bal::Bal::new(tampered_entries);
1382
1383 let tampered_block_access_list: BlockAccessList = tampered_bal.clone().into();
1385 let tampered_hash =
1386 alloy_eip7928::compute_block_access_list_hash(&tampered_block_access_list);
1387 let block = empty_amsterdam_block(tampered_hash);
1388
1389 let received = {
1390 let raw = alloy_rlp::encode(&tampered_bal).into();
1391 Arc::new(DecodedBal::new(tampered_bal, raw))
1392 };
1393
1394 let result = run_execute_block_full(
1395 &Runtime::test(),
1396 evm_config,
1397 db_factory(system_contracts_db()),
1398 received,
1399 &block,
1400 Vec::<Recovered<TransactionSigned>>::new(),
1401 );
1402
1403 match result {
1404 Ok((_, built_bal)) => {
1405 let rebuilt = alloy_eip7928::compute_block_access_list_hash(&built_bal);
1406 assert_ne!(rebuilt, tampered_hash, "rebuilt and header hashes must differ");
1407 }
1408 Err(e) => panic!("expected success with rebuilt BAL, got {e:?}"),
1409 }
1410 }
1411
1412 #[test]
1413 fn canonical_make_db_failure() {
1414 let evm_config = EthEvmConfig::mainnet();
1417 let block = empty_amsterdam_block(B256::ZERO);
1418
1419 let failing_make_db = || -> Result<CacheDB<EmptyDB>, BalExecutionError> {
1420 Err(reth_provider::ProviderError::BestBlockNotFound.into())
1421 };
1422
1423 let result = run_execute_block(
1424 &Runtime::test(),
1425 evm_config,
1426 failing_make_db,
1427 to_arc_decoded(BlockAccessList::default()),
1428 &block,
1429 Vec::<Recovered<TransactionSigned>>::new(),
1430 );
1431
1432 assert!(
1433 matches!(result, Err(BalExecutionError::Provider(_))),
1434 "expected Provider error from canonical make_db failure, got {result:?}",
1435 );
1436 }
1437
1438 #[test]
1439 fn worker_tx_recovery_error_becomes_validation_error() {
1440 let evm_config = EthEvmConfig::mainnet();
1444 let block = empty_amsterdam_block(B256::ZERO);
1445
1446 let (tx_tx, tx_rx) = crossbeam_channel::unbounded::<(
1447 usize,
1448 Result<Recovered<TransactionSigned>, std::io::Error>,
1449 )>();
1450 tx_tx.send((0, Err(std::io::Error::other("sig fail")))).unwrap();
1451 drop(tx_tx);
1452
1453 let (receipt_tx, _receipt_rx) = crossbeam_channel::unbounded();
1454 let evm_env = evm_config.evm_env(block.header()).unwrap();
1455 let execution_ctx = evm_config.context_for_block(&block).unwrap();
1456 let make_db = db_factory(system_contracts_db());
1457 let make_db = |_: bool| make_db();
1458
1459 let result = execute_block(
1460 &Runtime::test(),
1461 &evm_config,
1462 &make_db,
1463 to_arc_decoded(BlockAccessList::default()),
1464 evm_env,
1465 execution_ctx,
1466 1, tx_rx,
1468 receipt_tx,
1469 );
1470
1471 assert!(
1472 matches!(result, Err(BalExecutionError::Execution(BlockExecutionError::Validation(_)))),
1473 "expected validation error from tx recovery failure, got {result:?}",
1474 );
1475 }
1476
1477 #[test]
1478 fn gas_tracker_non_amsterdam_uses_cumulative_gas() {
1479 use revm::{
1484 context::result::{
1485 ExecResultAndState, ExecutionResult, Output, ResultGas, SuccessReason,
1486 },
1487 state::EvmState,
1488 };
1489
1490 let block_gas_limit = 1_000_000u64;
1491 let first_tx_gas = 600_000u64;
1492 let second_tx_gas_limit = 500_000u64; let gas = ResultGas::new_with_state_gas(first_tx_gas, 0, 0, first_tx_gas / 2);
1496 let fake_result: ResultAndState<revm::context::result::HaltReason> =
1497 ExecResultAndState::new(
1498 ExecutionResult::Success {
1499 reason: SuccessReason::Return,
1500 gas,
1501 logs: vec![],
1502 output: Output::Call(Default::default()),
1503 },
1504 EvmState::default(),
1505 );
1506
1507 let mut non_amsterdam = BlockGasTracker::new(block_gas_limit, false, None);
1509 non_amsterdam.record_result(&fake_result);
1510 assert!(
1511 non_amsterdam.validate_tx_limit(second_tx_gas_limit).is_err(),
1512 "non-Amsterdam tracker must reject tx that exceeds remaining cumulative gas",
1513 );
1514
1515 let mut amsterdam = BlockGasTracker::new(block_gas_limit, true, None);
1517 amsterdam.record_result(&fake_result);
1518 assert!(
1519 amsterdam.validate_tx_limit(second_tx_gas_limit).is_ok(),
1520 "Amsterdam tracker must accept the same tx since per-dimension budgets still fit",
1521 );
1522 }
1523
1524 #[test]
1525 fn gas_tracker_amsterdam_enforces_state_gas_budget() {
1526 use revm::{
1530 context::result::{
1531 ExecResultAndState, ExecutionResult, Output, ResultGas, SuccessReason,
1532 },
1533 state::EvmState,
1534 };
1535
1536 let block_gas_limit = 1_000_000u64;
1537 let first_tx_state_gas = 600_000u64;
1538 let second_tx_gas_limit = 500_000u64; let gas = ResultGas::new_with_state_gas(first_tx_state_gas, 0, 0, first_tx_state_gas);
1541 let fake_result: ResultAndState<revm::context::result::HaltReason> =
1542 ExecResultAndState::new(
1543 ExecutionResult::Success {
1544 reason: SuccessReason::Return,
1545 gas,
1546 logs: vec![],
1547 output: Output::Call(Default::default()),
1548 },
1549 EvmState::default(),
1550 );
1551
1552 let mut amsterdam = BlockGasTracker::new(block_gas_limit, true, None);
1555 amsterdam.record_result(&fake_result);
1556 assert!(
1557 amsterdam.validate_tx_limit(second_tx_gas_limit).is_err(),
1558 "Amsterdam tracker must reject tx whose gas limit exceeds the remaining state budget",
1559 );
1560 assert!(
1561 amsterdam.validate_tx_limit(400_000).is_ok(),
1562 "Amsterdam tracker must accept tx whose gas limit exactly fits the state budget",
1563 );
1564
1565 let mut capped = BlockGasTracker::new(block_gas_limit, true, Some(400_000));
1568 capped.record_result(&fake_result);
1569 assert!(
1570 capped.validate_tx_limit(second_tx_gas_limit).is_err(),
1571 "tx_gas_limit_cap must not bound the state-gas admission check",
1572 );
1573 }
1574
1575 #[test]
1576 fn gas_tracker_caps_oversized_tx_gas_limit_at_tx_gas_limit_cap() {
1577 use revm::{
1580 context::result::{
1581 ExecResultAndState, ExecutionResult, Output, ResultGas, SuccessReason,
1582 },
1583 primitives::eip7825::TX_GAS_LIMIT_CAP,
1584 state::EvmState,
1585 };
1586
1587 let block_gas_limit = 30_000_000u64;
1588 let oversized = TX_GAS_LIMIT_CAP + 1_000_000; let tracker = BlockGasTracker::new(block_gas_limit, false, Some(TX_GAS_LIMIT_CAP));
1593 assert!(
1594 tracker.validate_tx_limit(oversized).is_ok(),
1595 "oversized tx must pass when capped limit fits in block gas",
1596 );
1597
1598 let prior_gas = 20_000_000u64;
1601 let gas = ResultGas::new_with_state_gas(prior_gas, 0, 0, prior_gas);
1602 let fake_result: ResultAndState<revm::context::result::HaltReason> =
1603 ExecResultAndState::new(
1604 ExecutionResult::Success {
1605 reason: SuccessReason::Return,
1606 gas,
1607 logs: vec![],
1608 output: Output::Call(Default::default()),
1609 },
1610 EvmState::default(),
1611 );
1612
1613 let mut tracker = BlockGasTracker::new(block_gas_limit, false, Some(TX_GAS_LIMIT_CAP));
1614 tracker.record_result(&fake_result);
1615 assert!(
1616 tracker.validate_tx_limit(oversized).is_err(),
1617 "oversized tx must be rejected when capped limit exceeds remaining block gas",
1618 );
1619 }
1620}