Skip to main content

reth_engine_tree/tree/payload_processor/bal/
execute.rs

1//! BAL executor.
2//!
3//! Read `execute_block` as two execution paths over the same parent state.
4//!
5//! Worker states run transactions speculatively. Each worker gets one fresh cache-filling database
6//! from `make_db(true)`, installs the received BAL, sets the transaction BAL index for each
7//! streamed transaction, and returns uncommitted results. Execution errors rebuild the State,
8//! EVM, and executor while retaining the worker's database.
9//!
10//! The canonical state owns block effects. It runs the normal pre/post block hooks, commits
11//! worker results in transaction order, tracks block gas admission, and builds the BAL that this
12//! execution actually produced.
13//!
14//! Recovery and speculative execution failures are deferred to their transaction slot and
15//! adjudicated in block order: block-gas admission at an earlier slot takes precedence, otherwise
16//! the first failure decides the block's verdict. Workers trust the received BAL before its hash is
17//! validated against the rebuilt BAL. An undeclared access therefore short-circuits execution
18//! at that slot, and its error may differ from serial execution against canonical state (which
19//! can instead fail nonce, balance, or later block checks). Failed transactions are not replayed.
20//!
21//! The rebuilt BAL is returned to the outer payload validator for consensus post-execution
22//! validation. This module only logs the first divergence between the received BAL and the BAL
23//! rebuilt from canonical execution.
24
25use 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/// Executes one block on the BAL path using the runtime's persistent BAL worker pool.
54///
55/// The ordered commit loop applies Ethereum block-level gas admission. Executors with different
56/// admission rules, such as segment-scoped gas budgets, must align those checks before using it.
57///
58/// Returns the execution output, the recovered senders, the BAL rebuilt from this execution, and
59/// the received BAL in the revm representation the workers consumed.
60#[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                    // Block-gas admission is adjudicated in transaction order and takes
179                    // precedence over the failure at this slot.
180                    gas_tracker.validate_tx_limit(tx_gas_limit)?;
181
182                    // The transaction is admitted, so its execution failure is the block's
183                    // verdict: a BAL miss proves the received BAL diverges from this execution.
184                    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    // Convert the BAL from alloy to a BAL that can be consumed by revm, that is more amenable
231    // for state lookups.
232    //
233    // This is failable.
234    //
235    // This is due to bytecodes. A transaction can attempt to deploy illegal bytecodes, e.g. due to
236    // EIP-3541 or more specifically due to EIP-7702.
237    //
238    // During serial execution this check happens before the bytecode is deployed and if the check
239    // is triggered then the execution is reverted, and as such no actual code change event takes
240    // place. Therefore, if we do observe such a bytecode in a BAL then that means the BAL is
241    // invalid as no legal execution should've led to this bytecode deployment.
242    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
273/// Closes the abort channel on drop, waking scoped workers before the scope exits.
274struct 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/// Mirrors `EthBlockExecutor`'s gas admission checks in the ordered BAL commit loop.
286#[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    /// Verifies that the transaction's gas limit fits the block's remaining gas budget(s): the
313    /// admission check `EthBlockExecutor::execute_transaction_without_commit` performs before
314    /// executing a transaction.
315    ///
316    /// The commit loop never calls that entry point — workers execute speculatively and their
317    /// results are committed directly via `commit_transaction` — so the check must be replayed
318    /// here for BAL and serial execution to reach the same block validity verdict.
319    ///
320    /// Pre-Amsterdam there is one budget: the tx gas limit, capped by `tx_gas_limit_cap`
321    /// (EIP-7825), must fit `block_gas_limit - cumulative_tx_gas_used`.
322    ///
323    /// Amsterdam (EIP-8037) splits gas into two lanes, each budgeted at `block_gas_limit`:
324    /// - regular: the capped tx gas limit must fit the remaining regular budget
325    /// - state: the full, uncapped tx gas limit must fit the remaining state budget, since state
326    ///   gas is drawn from the reservoir above `tx_gas_limit_cap` (execution-specs
327    ///   `check_block_gas_capacity`)
328    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    /// Wraps a `BlockAccessList` into an `Arc<DecodedBal>` by RLP-encoding the BAL.
398    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    /// Builds an in-memory canonical DB pre-populated with the post-Cancun system contracts
405    /// that `apply_pre_execution_changes` calls: beacon roots (EIP-4788), withdrawal requests
406    /// (EIP-7002), and historical block hashes (EIP-2935).
407    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    /// Builds a minimal sealed block (empty body, Amsterdam-ready header) for tests.
427    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    /// Runs only the canonical phases (pre-exec → post-exec, no txs) against a fresh
459    /// `system_contracts_db()` to compute the composed BAL a block produces. Used to build
460    /// the "reference" received BAL for the happy-path test below.
461    ///
462    /// This intentionally mirrors what `execute_block` does internally,
463    /// but without any hash check — the output is the BAL itself, not a pass/fail signal.
464    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        // Any header_bal_hash on the reference block is fine — we don't check it here.
472        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        // Two-pass end-to-end:
508        //   1. Build the canonical BAL an empty Amsterdam block produces (via
509        //      `reference_bal_for_empty_block`).
510        //   2. Hash it, stamp the header, and run `execute_block` with that BAL. Every check must
511        //      pass (A, B, D, F).
512        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        // Sanity: reference BAL is non-empty (system calls populated it).
517        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    /// Inserts `AccountInfo { nonce: 0, balance }` for `addr` into the canonical DB.
600    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    /// Runs the canonical path on a block with real txs (no hash check) and returns the
608    /// composed BAL. Used to build the reference BAL for happy-path multi-tx tests.
609    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        // End-to-end with two value transfers from distinct senders to the same recipient.
645        //
646        // 1. Fund alice and bob in a fresh canonical DB.
647        // 2. Sign tx1 (alice → carol, 100 wei) and tx2 (bob → carol, 200 wei).
648        // 3. Build the reference BAL by running the block through a canonical executor with
649        //    `with_bal_builder`.
650        // 4. Feed that BAL into `execute_block` and assert 2 receipts + no rejections.
651        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        // Generate keypairs + derive sender addresses.
656        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        // Pre-block DB: system contracts + funded senders.
662        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        // Sign txs.
667        let chain_id = MAINNET.chain.id();
668        let gas_price = 1u128; // flat low price; block has no base fee in our test header.
669        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        // Reference BAL: run the block canonically through a separate executor.
697        let block_for_ref = empty_amsterdam_block(B256::ZERO);
698        let reference_bal = reference_bal_for_block::<Recovered<TransactionSigned>>(
699            &evm_config,
700            {
701                // Separate fresh DB for the reference run so we don't pollute canonical_db.
702                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    // ============================================================================
750    // Shadow-mode harness — runs a block through the serial `BasicBlockExecutor`
751    // and the BAL path, asserts byte-equal outputs.
752    // ============================================================================
753
754    /// Output of one path in a shadow run. Both serial and BAL paths produce this shape so
755    /// the harness can compare field-by-field.
756    #[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    /// Runs the block through the serial path and captures its full output.
765    ///
766    /// Uses a manual state + executor (not `BasicBlockExecutor::execute_one`) so we can both
767    /// (a) capture the composed BAL for the BAL-path input and (b) pull the bundle out after.
768    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    /// Shadow harness. Runs the block through both paths; asserts byte-equal outputs.
812    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        // Serial run: also produces the reference BAL we'll feed to the BAL path.
819        let (serial, reference_bal) =
820            run_serial_path(&evm_config, canonical_db_template.clone(), &block_header_only, &txs);
821
822        // BAL path: stamp the hash of the reference BAL onto the header.
823        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        // Byte-equal assertions. Any divergence surfaces the specific field that broke.
838        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        // System calls only — no txs. Both paths should produce identical system-call
860        // side effects in their BundleState (beacon roots storage, history storage, etc.).
861        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        // Two senders → same recipient. Byte-equal across paths means: worker-produced
872        // diffs commit identically to a directly-executed serial path.
873        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        // Each worker sees an empty block, so both transactions fit individually. The ordered
910        // commit loop must still reject tx2 because tx1's committed gas leaves too little
911        // block gas for tx2's gas limit.
912        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        // Build the reference BAL under a generous gas limit so both workers can execute.
946        // Replaying the same BAL under `block_gas_limit` below should reject in the ordered
947        // commit loop before tx2 is committed.
948        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    /// Two funded senders each transferring to the same recipient, which is funded only if
978    /// `fund_recipient` is set.
979    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    /// [`two_transfers`] to a fresh recipient, plus the reference BAL of a block containing only
1020    /// the covered subset of those transfers.
1021    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        // The BAL covers neither transaction: the first slot's speculative failure is
1042        // adjudicated in block order and becomes the block's verdict.
1043        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        // tx2 exceeds the remaining block gas AND misses the BAL; block-gas admission is
1070        // adjudicated first, so the verdict is the gas error, not tx2's BAL failure.
1071        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            // Reopening the provider after a speculative error must not introduce an
1110            // unindexed setup failure that overtakes the remaining transaction slots.
1111            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        // Recovery can finish out of order. A single worker must continue after both kinds
1120        // of error to execute slot 0, without reusing the failed slot 1 executor.
1121        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                // State-gas admission must also precede a speculative BAL miss at this slot.
1191                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            // Both capped regular limits fit. The full limit of the second transaction
1200            // must still fit the state budget consumed by the first account creation.
1201            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        // The second gas limit exceeds the regular gas left after the first transfer, but its
1252        // regular part, capped at the EIP-7825 default, fits. The funded recipient keeps both
1253        // transfers off the state-gas budget.
1254        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        // An unset cap means the spec default.
1267        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        // A tx that reverts in a deployed contract. Both paths must produce identical receipts
1296        // (success = false, gas charged, state rolled back except for gas payment + nonce bump).
1297        //
1298        // Deploys `0x60006000fd` (PUSH1 0 PUSH1 0 REVERT) at `revert_contract`. Sender calls
1299        // it; the call reverts; fees + nonce still apply.
1300        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        // Deploy the revert contract bytecode.
1308        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        // Tx calls a deployed contract that does `SSTORE(0, 0x42)`. The storage write must
1338        // commit identically across serial and BAL paths even though the canonical state applies
1339        // a diff produced by a worker EVM.
1340        //
1341        // Bytecode: PUSH1 0x42, PUSH1 0x00, SSTORE, STOP → `0x60 0x42 0x60 0x00 0x55 0x00`.
1342        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        // Deploy the SSTORE contract.
1350        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        // Build the BAL an empty block actually produces, then append a phantom address
1380        // that execution never touches. The rebuilt BAL omits it, and the outer consensus
1381        // validator is responsible for comparing that rebuilt hash to the header commitment.
1382        use alloy_eip7928::AccountChanges;
1383
1384        let evm_config = EthEvmConfig::mainnet();
1385
1386        // Real BAL the block would produce.
1387        let real_bal = reference_bal_for_empty_block(&evm_config);
1388        assert!(!real_bal.is_empty(), "reference BAL must be non-empty");
1389
1390        // Tamper: append a phantom address not accessed during execution.
1391        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        // Stamp the tampered BAL's hash on the block header.
1397        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        // A make_db that always fails must surface as Provider before any workers are
1428        // spawned or the BAL is processed.
1429        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        // A tx recovery failure fed into the worker channel must surface as
1454        // a block validation error. Uses execute_block directly since tx_stream hardcodes
1455        // Infallible and cannot inject errors.
1456        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, // transaction_count = 1 → exactly one worker spawned
1480            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        // A half-state-gas result keeps both Amsterdam budgets (regular and state) at
1493        // 300_000 used while cumulative_tx_gas_used is 600_000, so a second tx that fits
1494        // within the block limit but not the remaining cumulative budget proves that
1495        // non-Amsterdam reads cumulative_tx_gas_used while Amsterdam does not.
1496        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; // fits in total limit but not after cumulative
1506                                              // deduction
1507
1508        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        // Non-Amsterdam: block_available_gas = 1_000_000 - 600_000 = 400_000 → reject 500_000.
1521        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        // Amsterdam: both regular and state budgets have 700_000 left → accept 500_000.
1529        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        // An all-state-gas result leaves the regular budget untouched, so only the
1540        // state-gas admission check can reject the second transaction. The tx's full gas
1541        // limit counts against the state budget — tx_gas_limit_cap does not bound it.
1542        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; // exceeds the remaining 400_000 state budget
1552
1553        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        // Regular budget is full (block_regular_gas_used = 0) but the state budget has
1566        // only 400_000 left → reject 500_000.
1567        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        // With a cap of 400_000 the capped regular check passes, but the full 500_000
1579        // limit still counts against the state budget → reject.
1580        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        // A tx with gas_limit above TX_GAS_LIMIT_CAP (EIP-7825) is admitted when the
1591        // capped value fits in the remaining block gas and rejected when it does not.
1592        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; // 17_777_216 — above the cap
1602
1603        // Case 1: fresh block, no prior gas consumed.
1604        // tx_min_gas_limit = TX_GAS_LIMIT_CAP (16_777_216) ≤ block_available_gas (30M) → Ok.
1605        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        // Case 2: prior tx consumed 20M, leaving 10M available.
1612        // tx_min_gas_limit = TX_GAS_LIMIT_CAP (16_777_216) > block_available_gas (10M) → Err.
1613        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}