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