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},
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    ReceiptTy<Evm::Primitives>: Clone,
82{
83    let worker_pool = runtime.bal_streaming_pool();
84    let worker_count = worker_pool.current_num_threads().max(1).min(transaction_count);
85
86    worker_pool.in_place_scope(|scope| {
87        execute_block_inner(
88            scope,
89            evm_config,
90            make_db,
91            input_bal,
92            evm_env,
93            ctx,
94            transaction_count,
95            txs,
96            receipt_tx,
97            worker_count,
98        )
99    })
100}
101
102#[expect(clippy::too_many_arguments, clippy::type_complexity)]
103fn execute_block_inner<'scope, Evm, Tx, Err, DB, MakeDb>(
104    scope: &rayon::Scope<'scope>,
105    evm_config: &'scope Evm,
106    make_db: &'scope MakeDb,
107    input_bal: Arc<DecodedBal>,
108    evm_env: EvmEnvFor<Evm>,
109    ctx: ExecutionCtxFor<'scope, Evm>,
110    transaction_count: usize,
111    txs: Receiver<(usize, Result<Tx, Err>)>,
112    receipt_tx: Sender<IndexedReceipt<ReceiptTy<Evm::Primitives>>>,
113    worker_count: usize,
114) -> Result<
115    (BlockExecutionOutput<ReceiptTy<Evm::Primitives>>, Vec<Address>, BlockAccessList, Arc<RevmBal>),
116    BalExecutionError,
117>
118where
119    Evm: ConfigureEvm + 'scope,
120    Tx: ExecutableTxFor<Evm> + Send + 'scope,
121    Err: core::error::Error + Send + Sync + 'static,
122    DB: Database + Send + 'scope,
123    MakeDb: Fn(bool) -> Result<DB, BalExecutionError> + Sync + 'scope,
124    ReceiptTy<Evm::Primitives>: Clone,
125{
126    let bal = input_bal.as_bal();
127    let input_bal_revm = convert_alloy_to_revm_bal(bal)?;
128
129    let block_gas_limit = evm_env.block_env.gas_limit();
130    let enable_amsterdam_eip8037 = evm_env.cfg_env.enable_amsterdam_eip8037;
131    let tx_gas_limit_cap = evm_env.cfg_env.tx_gas_limit_cap;
132    let mut canonical_state = State::builder()
133        .with_database(make_db(false)?)
134        .with_bundle_update()
135        .with_bal_builder()
136        .build();
137    canonical_state
138        .bal_state
139        .bal_builder
140        .as_mut()
141        .expect("with_bal_builder set")
142        .accounts
143        .reserve(bal.len());
144
145    let (block_result, senders) = {
146        let (result_tx, result_rx) = crossbeam_channel::unbounded();
147        let (abort_guard, abort_rx) = AbortGuard::new();
148
149        for _ in 0..worker_count {
150            worker::spawn_worker(
151                scope,
152                txs.clone(),
153                abort_rx.clone(),
154                result_tx.clone(),
155                evm_config,
156                make_db,
157                Arc::clone(&input_bal_revm),
158                evm_env.clone(),
159                ctx.clone(),
160            );
161        }
162        drop(result_tx);
163
164        let mut gas_tracker =
165            BlockGasTracker::new(block_gas_limit, enable_amsterdam_eip8037, tx_gas_limit_cap);
166        let evm = evm_config.evm_with_env(&mut canonical_state, evm_env);
167        let mut canonical_executor = evm_config.create_executor_with_state(evm, ctx.clone());
168
169        canonical_executor.apply_pre_execution_changes()?;
170        let mut senders = Vec::with_capacity(transaction_count);
171        let mut last_sent_len = 0usize;
172        for output in ordered_worker_outputs(&result_rx, transaction_count) {
173            let output = match output {
174                Ok(output) => output,
175                Err(OrderedWorkerOutputError::Worker(worker::BalWorkerError::Execution {
176                    tx_index,
177                    tx_gas_limit,
178                    source,
179                })) => {
180                    // Block-gas admission is adjudicated in transaction order and takes
181                    // precedence over the failure at this slot.
182                    gas_tracker.validate_tx_limit(tx_gas_limit)?;
183
184                    // The transaction is admitted, so its execution failure is the block's
185                    // verdict: a BAL miss proves the received BAL diverges from this execution.
186                    tracing::debug!(
187                        target: "engine::tree::payload_processor::bal",
188                        tx_index,
189                        err = %source,
190                        "Speculative BAL execution failed; rejecting block in transaction order"
191                    );
192                    return Err(BalExecutionError::Execution(source));
193                }
194                Err(err) => return Err(err.into()),
195            };
196
197            gas_tracker.validate_tx_limit(output.tx_gas_limit)?;
198            gas_tracker.record_result(output.result.result());
199            canonical_executor.evm_mut().db_mut().bump_bal_index();
200
201            let _ = canonical_executor.commit_transaction(output.result);
202            senders.push(output.signer);
203
204            let current_len = canonical_executor.receipts().len();
205            if current_len > last_sent_len {
206                last_sent_len = current_len;
207                if let Some(receipt) = canonical_executor.receipts().last() {
208                    let tx_index = current_len - 1;
209                    let _ = receipt_tx.send(IndexedReceipt::new(tx_index, receipt.clone()));
210                }
211            }
212        }
213        drop(abort_guard);
214
215        canonical_executor.evm_mut().db_mut().bump_bal_index();
216        let block_result = canonical_executor.apply_post_execution_changes()?;
217        (block_result, senders)
218    };
219
220    let built_bal = take_built_bal_and_log_divergence(&mut canonical_state, bal);
221
222    canonical_state.merge_transitions(BundleRetention::Reverts);
223    Ok((
224        BlockExecutionOutput { state: canonical_state.take_bundle(), result: block_result },
225        senders,
226        built_bal,
227        input_bal_revm,
228    ))
229}
230
231fn convert_alloy_to_revm_bal(alloy_bal: &AlloyBal) -> Result<Arc<RevmBal>, BalExecutionError> {
232    // Convert the BAL from alloy to a BAL that can be consumed by revm, that is more amenable
233    // for state lookups.
234    //
235    // This is failable.
236    //
237    // This is due to bytecodes. A transaction can attempt to deploy illegal bytecodes, e.g. due to
238    // EIP-3541 or more specifically due to EIP-7702.
239    //
240    // During serial execution this check happens before the bytecode is deployed and if the check
241    // is triggered then the execution is reverted, and as such no actual code change event takes
242    // place. Therefore, if we do observe such a bytecode in a BAL then that means the BAL is
243    // invalid as no legal execution should've led to this bytecode deployment.
244    let received_bal_revm =
245        RevmBal::clone_from_alloy(alloy_bal.as_vec()).map_err(BlockAccessListDecodeError::new)?;
246    Ok(Arc::new(received_bal_revm))
247}
248
249fn take_built_bal_and_log_divergence<DB>(
250    canonical_state: &mut State<DB>,
251    received_bal: &AlloyBal,
252) -> BlockAccessList
253where
254    DB: Database,
255{
256    let built_bal = canonical_state.take_built_alloy_bal().expect("with_bal_builder set");
257    if tracing::enabled!(target: "engine::tree::payload_processor::bal", tracing::Level::DEBUG) &&
258        built_bal.as_slice() != received_bal.as_slice()
259    {
260        let rebuilt = compute_block_access_list_hash(built_bal.as_slice());
261        let expected = compute_block_access_list_hash(received_bal.as_slice());
262        let div = received_bal.diff(built_bal.as_slice());
263        tracing::debug!(
264            target: "engine::tree::payload_processor::bal",
265            %rebuilt,
266            %expected,
267            %div,
268            "first BAL divergence",
269        );
270    }
271
272    built_bal
273}
274
275/// Closes the abort channel on drop, waking scoped workers before the scope exits.
276struct AbortGuard {
277    _tx: Sender<()>,
278}
279
280impl AbortGuard {
281    fn new() -> (Self, Receiver<()>) {
282        let (tx, rx) = crossbeam_channel::bounded(0);
283        (Self { _tx: tx }, rx)
284    }
285}
286
287/// Mirrors `EthBlockExecutor`'s gas admission checks in the ordered BAL commit loop.
288#[derive(Debug)]
289struct BlockGasTracker {
290    block_gas_limit: u64,
291    enable_amsterdam_eip8037: bool,
292    tx_gas_limit_cap: Option<u64>,
293    cumulative_tx_gas_used: u64,
294    block_regular_gas_used: u64,
295    block_state_gas_used: u64,
296}
297
298impl BlockGasTracker {
299    const fn new(
300        block_gas_limit: u64,
301        enable_amsterdam_eip8037: bool,
302        tx_gas_limit_cap: Option<u64>,
303    ) -> Self {
304        Self {
305            block_gas_limit,
306            enable_amsterdam_eip8037,
307            tx_gas_limit_cap,
308            cumulative_tx_gas_used: 0,
309            block_regular_gas_used: 0,
310            block_state_gas_used: 0,
311        }
312    }
313
314    /// Verifies that the transaction's gas limit fits the block's remaining gas budget(s): the
315    /// admission check `EthBlockExecutor::execute_transaction_without_commit` performs before
316    /// executing a transaction.
317    ///
318    /// The commit loop never calls that entry point — workers execute speculatively and their
319    /// results are committed directly via `commit_transaction` — so the check must be replayed
320    /// here for BAL and serial execution to reach the same block validity verdict.
321    ///
322    /// Pre-Amsterdam there is one budget: the tx gas limit, capped by `tx_gas_limit_cap`
323    /// (EIP-7825), must fit `block_gas_limit - cumulative_tx_gas_used`.
324    ///
325    /// Amsterdam (EIP-8037) splits gas into two lanes, each budgeted at `block_gas_limit`:
326    /// - regular: the capped tx gas limit must fit the remaining regular budget
327    /// - state: the full, uncapped tx gas limit must fit the remaining state budget, since state
328    ///   gas is drawn from the reservoir above `tx_gas_limit_cap` (execution-specs
329    ///   `check_block_gas_capacity`)
330    fn validate_tx_limit(&self, tx_gas_limit: u64) -> Result<(), BlockExecutionError> {
331        let block_gas_used = if self.enable_amsterdam_eip8037 {
332            self.block_regular_gas_used
333        } else {
334            self.cumulative_tx_gas_used
335        };
336        let block_available_gas = self.block_gas_limit.saturating_sub(block_gas_used);
337        let tx_min_gas_limit =
338            self.tx_gas_limit_cap.map_or(tx_gas_limit, |cap| tx_gas_limit.min(cap));
339
340        if tx_min_gas_limit > block_available_gas {
341            return Err(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
342                transaction_gas_limit: tx_gas_limit,
343                block_available_gas,
344            }
345            .into());
346        }
347
348        if self.enable_amsterdam_eip8037 {
349            let state_gas_available =
350                self.block_gas_limit.saturating_sub(self.block_state_gas_used);
351            if tx_gas_limit > state_gas_available {
352                return Err(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
353                    transaction_gas_limit: tx_gas_limit,
354                    block_available_gas: state_gas_available,
355                }
356                .into());
357            }
358        }
359
360        Ok(())
361    }
362
363    const fn record_result<H>(&mut self, result: &ResultAndState<H>) {
364        let gas = result.result.gas();
365        self.cumulative_tx_gas_used = self.cumulative_tx_gas_used.saturating_add(gas.tx_gas_used());
366        self.block_regular_gas_used =
367            self.block_regular_gas_used.saturating_add(gas.block_regular_gas_used());
368        self.block_state_gas_used =
369            self.block_state_gas_used.saturating_add(gas.block_state_gas_used());
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376    use crate::tree::error::{InsertBlockErrorKind, InsertBlockValidationError};
377    use alloy_consensus::{BlockHeader, Header};
378    use alloy_eip7928::{
379        bal::Bal as AlloyBal, AccountChanges, BlockAccessIndex, BlockAccessList, CodeChange,
380    };
381    use alloy_eips::{
382        eip2935::{HISTORY_STORAGE_ADDRESS, HISTORY_STORAGE_CODE},
383        eip4788::{BEACON_ROOTS_ADDRESS, BEACON_ROOTS_CODE},
384        eip7002::{WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, WITHDRAWAL_REQUEST_PREDEPLOY_CODE},
385    };
386    use alloy_primitives::{B256, U256};
387    use reth_ethereum_primitives::{Block, BlockBody, Receipt, TransactionSigned};
388    use reth_evm_ethereum::EthEvmConfig;
389    use reth_primitives_traits::{Block as _, Recovered, SealedBlock};
390    use reth_revm::db::BundleState;
391    use reth_tasks::Runtime;
392    use revm::{
393        database::{CacheDB, EmptyDB},
394        state::{AccountInfo, Bytecode},
395    };
396    use std::convert::Infallible;
397
398    /// Wraps a `BlockAccessList` into an `Arc<DecodedBal>` by RLP-encoding the BAL.
399    fn to_arc_decoded(bal: BlockAccessList) -> Arc<DecodedBal> {
400        let alloy_bal: AlloyBal = bal.into();
401        let raw = alloy_rlp::encode(&alloy_bal).into();
402        Arc::new(DecodedBal::new(alloy_bal, raw))
403    }
404
405    /// Builds an in-memory canonical DB pre-populated with the post-Cancun system contracts
406    /// that `apply_pre_execution_changes` calls: beacon roots (EIP-4788), withdrawal requests
407    /// (EIP-7002), and historical block hashes (EIP-2935).
408    fn system_contracts_db() -> CacheDB<EmptyDB> {
409        let mut db = CacheDB::<EmptyDB>::new(Default::default());
410        db.insert_account_info(
411            BEACON_ROOTS_ADDRESS,
412            AccountInfo::from_bytecode(Bytecode::new_raw(BEACON_ROOTS_CODE.clone())),
413        );
414        db.insert_account_info(
415            WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS,
416            AccountInfo::from_bytecode(Bytecode::new_raw(
417                WITHDRAWAL_REQUEST_PREDEPLOY_CODE.clone(),
418            )),
419        );
420        db.insert_account_info(
421            HISTORY_STORAGE_ADDRESS,
422            AccountInfo::from_bytecode(Bytecode::new_raw(HISTORY_STORAGE_CODE.clone())),
423        );
424        db
425    }
426
427    /// Builds a minimal sealed block (empty body, Amsterdam-ready header) for tests.
428    fn empty_amsterdam_block(header_bal_hash: B256) -> SealedBlock<Block> {
429        empty_amsterdam_block_with_gas_limit(header_bal_hash, 30_000_000)
430    }
431
432    fn empty_amsterdam_block_with_gas_limit(
433        header_bal_hash: B256,
434        gas_limit: u64,
435    ) -> SealedBlock<Block> {
436        let header = Header {
437            timestamp: 1,
438            number: 1,
439            gas_limit,
440            parent_beacon_block_root: Some(B256::ZERO),
441            withdrawals_root: Some(alloy_consensus::EMPTY_ROOT_HASH),
442            requests_hash: Some(alloy_eips::eip7685::EMPTY_REQUESTS_HASH),
443            excess_blob_gas: Some(0),
444            blob_gas_used: Some(0),
445            block_access_list_hash: Some(header_bal_hash),
446            ..Header::default()
447        };
448        let block = Block {
449            header,
450            body: BlockBody {
451                transactions: vec![],
452                ommers: vec![],
453                withdrawals: Some(vec![].into()),
454            },
455        };
456        block.seal_slow()
457    }
458
459    /// Runs only the canonical phases (pre-exec → post-exec, no txs) against a fresh
460    /// `system_contracts_db()` to compute the composed BAL a block produces. Used to build
461    /// the "reference" received BAL for the happy-path test below.
462    ///
463    /// This intentionally mirrors what `execute_block` does internally,
464    /// but without any hash check — the output is the BAL itself, not a pass/fail signal.
465    fn reference_bal_for_empty_block(evm_config: &EthEvmConfig) -> BlockAccessList {
466        use revm::database::State as RevmState;
467
468        let db = system_contracts_db();
469        let mut state =
470            RevmState::builder().with_database(db).with_bundle_update().with_bal_builder().build();
471
472        // Any header_bal_hash on the reference block is fine — we don't check it here.
473        let block = empty_amsterdam_block(B256::ZERO);
474        {
475            let mut executor =
476                evm_config.executor_for_block(&mut state, &block).expect("build executor");
477            executor.apply_pre_execution_changes().expect("pre-exec");
478            executor.evm_mut().db_mut().bump_bal_index();
479            executor.apply_post_execution_changes().expect("post-exec");
480        }
481        state.take_built_alloy_bal().expect("with_bal_builder was set")
482    }
483
484    #[test]
485    fn invalid_bal_bytecode_is_validation_error() {
486        let alloy_bal = vec![AccountChanges {
487            address: Address::ZERO,
488            code_changes: vec![CodeChange::new(
489                BlockAccessIndex::new(1),
490                vec![0xef, 0x01, 0xde].into(),
491            )],
492            ..Default::default()
493        }]
494        .into();
495
496        let error = convert_alloy_to_revm_bal(&alloy_bal).unwrap_err();
497        assert!(matches!(&error, BalExecutionError::BlockAccessListDecode(_)));
498        let error = InsertBlockErrorKind::from(error);
499        assert!(matches!(&error, InsertBlockErrorKind::BlockAccessListDecode(_)));
500        assert!(matches!(
501            error.ensure_validation_error(),
502            Ok(InsertBlockValidationError::BlockAccessListDecode(_))
503        ));
504    }
505
506    #[test]
507    fn empty_block_happy_path_round_trip() {
508        // Two-pass end-to-end:
509        //   1. Build the canonical BAL an empty Amsterdam block produces (via
510        //      `reference_bal_for_empty_block`).
511        //   2. Hash it, stamp the header, and run `execute_block` with that BAL. Every check must
512        //      pass (A, B, D, F).
513        let evm_config = EthEvmConfig::mainnet();
514
515        let input_bal = reference_bal_for_empty_block(&evm_config);
516        let bal_hash = alloy_eip7928::compute_block_access_list_hash(&input_bal);
517        // Sanity: reference BAL is non-empty (system calls populated it).
518        assert!(!input_bal.is_empty(), "empty BAL means system calls didn't record state");
519
520        let block = empty_amsterdam_block(bal_hash);
521
522        let result = run_execute_block(
523            &Runtime::test(),
524            evm_config,
525            db_factory(system_contracts_db()),
526            to_arc_decoded(input_bal),
527            &block,
528            Vec::<Recovered<TransactionSigned>>::new(),
529        );
530
531        match result {
532            Ok(output) => {
533                assert!(output.receipts.is_empty(), "empty block → no receipts");
534            }
535            Err(e) => panic!("expected success, got {e:?}"),
536        }
537    }
538
539    fn db_factory(
540        db: CacheDB<EmptyDB>,
541    ) -> impl Fn() -> Result<CacheDB<EmptyDB>, BalExecutionError> + Sync {
542        move || Ok(db.clone())
543    }
544
545    fn tx_stream<Tx>(txs: Vec<Tx>) -> Receiver<(usize, Result<Tx, Infallible>)> {
546        let (tx, rx) = crossbeam_channel::unbounded();
547        for (index, transaction) in txs.into_iter().enumerate() {
548            tx.send((index, Ok(transaction))).unwrap();
549        }
550        rx
551    }
552
553    fn run_execute_block<Tx, DB, MakeDb>(
554        runtime: &Runtime,
555        evm_config: EthEvmConfig,
556        make_db: MakeDb,
557        input_bal: Arc<DecodedBal>,
558        block: &SealedBlock<Block>,
559        txs: Vec<Tx>,
560    ) -> Result<BlockExecutionOutput<Receipt>, BalExecutionError>
561    where
562        Tx: ExecutableTxFor<EthEvmConfig> + Send,
563        DB: Database + Send,
564        MakeDb: Fn() -> Result<DB, BalExecutionError> + Sync,
565    {
566        run_execute_block_full(runtime, evm_config, make_db, input_bal, block, txs)
567            .map(|(output, _)| output)
568    }
569
570    fn run_execute_block_full<Tx, DB, MakeDb>(
571        runtime: &Runtime,
572        evm_config: EthEvmConfig,
573        make_db: MakeDb,
574        input_bal: Arc<DecodedBal>,
575        block: &SealedBlock<Block>,
576        txs: Vec<Tx>,
577    ) -> Result<(BlockExecutionOutput<Receipt>, BlockAccessList), BalExecutionError>
578    where
579        Tx: ExecutableTxFor<EthEvmConfig> + Send,
580        DB: Database + Send,
581        MakeDb: Fn() -> Result<DB, BalExecutionError> + Sync,
582    {
583        let transaction_count = txs.len();
584        let (receipt_tx, _receipt_rx) = crossbeam_channel::unbounded();
585        let evm_env = evm_config.evm_env(block.header()).unwrap();
586        let execution_ctx = evm_config.context_for_block(block).unwrap();
587        let make_db = |_: bool| make_db();
588        execute_block(
589            runtime,
590            &evm_config,
591            &make_db,
592            input_bal,
593            evm_env,
594            execution_ctx,
595            transaction_count,
596            tx_stream(txs),
597            receipt_tx,
598        )
599        .map(|(output, _, built_bal, _)| (output, built_bal))
600    }
601
602    /// Inserts `AccountInfo { nonce: 0, balance }` for `addr` into the canonical DB.
603    fn insert_funded(db: &mut CacheDB<EmptyDB>, addr: alloy_primitives::Address, balance: U256) {
604        db.insert_account_info(
605            addr,
606            AccountInfo { balance, code_hash: B256::ZERO, code: None, ..Default::default() },
607        );
608    }
609
610    /// Runs the canonical path on a block with real txs (no hash check) and returns the
611    /// composed BAL. Used to build the reference BAL for happy-path multi-tx tests.
612    fn reference_bal_for_block<Tx>(
613        evm_config: &EthEvmConfig,
614        mut db: CacheDB<EmptyDB>,
615        block: &SealedBlock<Block>,
616        txs: Vec<Tx>,
617    ) -> BlockAccessList
618    where
619        Tx: ExecutableTxFor<EthEvmConfig>,
620    {
621        use revm::database::State as RevmState;
622
623        let mut state = RevmState::builder()
624            .with_database(&mut db)
625            .with_bundle_update()
626            .with_bal_builder()
627            .build();
628
629        {
630            let mut executor =
631                evm_config.executor_for_block(&mut state, block).expect("build executor");
632            executor.apply_pre_execution_changes().expect("pre-exec");
633            for (i, tx) in txs.into_iter().enumerate() {
634                executor.evm_mut().db_mut().bump_bal_index();
635                executor
636                    .execute_transaction(tx)
637                    .unwrap_or_else(|e| panic!("tx {i} failed during reference build: {e:?}"));
638            }
639            executor.evm_mut().db_mut().bump_bal_index();
640            executor.apply_post_execution_changes().expect("post-exec");
641        }
642        state.take_built_alloy_bal().expect("with_bal_builder was set")
643    }
644
645    #[test]
646    fn multi_tx_happy_path_round_trip() {
647        // End-to-end with two value transfers from distinct senders to the same recipient.
648        //
649        // 1. Fund alice and bob in a fresh canonical DB.
650        // 2. Sign tx1 (alice → carol, 100 wei) and tx2 (bob → carol, 200 wei).
651        // 3. Build the reference BAL by running the block through a canonical executor with
652        //    `with_bal_builder`.
653        // 4. Feed that BAL into `execute_block` and assert 2 receipts + no rejections.
654        use alloy_consensus::TxLegacy;
655        use alloy_primitives::TxKind;
656        use reth_chainspec::MAINNET;
657        use reth_ethereum_primitives::Transaction;
658        use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
659        use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
660
661        let evm_config = EthEvmConfig::mainnet();
662        let carol: alloy_primitives::Address = alloy_primitives::Address::from([0xCA; 20]);
663        let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
664
665        // Generate keypairs + derive sender addresses.
666        let alice_kp = generate_key(&mut rng());
667        let alice = public_key_to_address(alice_kp.public_key());
668        let bob_kp = generate_key(&mut rng());
669        let bob = public_key_to_address(bob_kp.public_key());
670
671        // Pre-block DB: system contracts + funded senders.
672        let mut pre_block_db = system_contracts_db();
673        insert_funded(&mut pre_block_db, alice, sender_balance);
674        insert_funded(&mut pre_block_db, bob, sender_balance);
675
676        // Sign txs.
677        let chain_id = MAINNET.chain.id();
678        let gas_price = 1u128; // flat low price; block has no base fee in our test header.
679        let tx1 = sign_tx_with_key_pair(
680            alice_kp,
681            Transaction::Legacy(TxLegacy {
682                chain_id: Some(chain_id),
683                nonce: 0,
684                gas_price,
685                gas_limit: 21_000,
686                to: TxKind::Call(carol),
687                value: U256::from(100u64),
688                input: Default::default(),
689            }),
690        );
691        let tx2 = sign_tx_with_key_pair(
692            bob_kp,
693            Transaction::Legacy(TxLegacy {
694                chain_id: Some(chain_id),
695                nonce: 0,
696                gas_price,
697                gas_limit: 21_000,
698                to: TxKind::Call(carol),
699                value: U256::from(200u64),
700                input: Default::default(),
701            }),
702        );
703        let recovered1 = Recovered::new_unchecked(tx1, alice);
704        let recovered2 = Recovered::new_unchecked(tx2, bob);
705
706        // Reference BAL: run the block canonically through a separate executor.
707        let block_for_ref = empty_amsterdam_block(B256::ZERO);
708        let reference_bal = reference_bal_for_block::<Recovered<TransactionSigned>>(
709            &evm_config,
710            {
711                // Separate fresh DB for the reference run so we don't pollute canonical_db.
712                let mut db = system_contracts_db();
713                db.insert_account_info(
714                    alice,
715                    AccountInfo {
716                        balance: sender_balance,
717                        code_hash: B256::ZERO,
718                        code: None,
719                        ..Default::default()
720                    },
721                );
722                db.insert_account_info(
723                    bob,
724                    AccountInfo {
725                        balance: sender_balance,
726                        code_hash: B256::ZERO,
727                        code: None,
728                        ..Default::default()
729                    },
730                );
731                db
732            },
733            &block_for_ref,
734            vec![recovered1.clone(), recovered2.clone()],
735        );
736        assert!(!reference_bal.is_empty(), "expected BAL entries from pre-exec + txs");
737
738        let bal_hash = alloy_eip7928::compute_block_access_list_hash(&reference_bal);
739        let block = empty_amsterdam_block(bal_hash);
740
741        let result = run_execute_block(
742            &Runtime::test(),
743            evm_config,
744            db_factory(pre_block_db),
745            to_arc_decoded(reference_bal),
746            &block,
747            vec![recovered1, recovered2],
748        );
749
750        match result {
751            Ok(output) => {
752                assert_eq!(output.receipts.len(), 2, "expected 2 receipts");
753                assert!(output.gas_used >= 2 * 21_000, "expected at least 42k gas used");
754            }
755            Err(e) => panic!("expected success, got {e:?}"),
756        }
757    }
758
759    // ============================================================================
760    // Shadow-mode harness — runs a block through the serial `BasicBlockExecutor`
761    // and the BAL path, asserts byte-equal outputs.
762    // ============================================================================
763
764    /// Output of one path in a shadow run. Both serial and BAL paths produce this shape so
765    /// the harness can compare field-by-field.
766    #[derive(Debug)]
767    struct ShadowOutput {
768        bundle_state: BundleState,
769        receipts: Vec<reth_ethereum_primitives::Receipt>,
770        gas_used: u64,
771        requests: alloy_eips::eip7685::Requests,
772    }
773
774    /// Runs the block through the serial path and captures its full output.
775    ///
776    /// Uses a manual state + executor (not `BasicBlockExecutor::execute_one`) so we can both
777    /// (a) capture the composed BAL for the BAL-path input and (b) pull the bundle out after.
778    fn run_serial_path(
779        evm_config: &EthEvmConfig,
780        canonical_db: CacheDB<EmptyDB>,
781        block: &SealedBlock<Block>,
782        txs: &[Recovered<TransactionSigned>],
783    ) -> (ShadowOutput, BlockAccessList) {
784        use revm::database::State as RevmState;
785
786        let mut state = RevmState::builder()
787            .with_database(canonical_db)
788            .with_bundle_update()
789            .with_bal_builder()
790            .build();
791
792        let block_result = {
793            let mut executor =
794                evm_config.executor_for_block(&mut state, block).expect("build serial executor");
795            executor.apply_pre_execution_changes().expect("serial pre-exec");
796            for (i, tx) in txs.iter().cloned().enumerate() {
797                executor.evm_mut().db_mut().bump_bal_index();
798                executor
799                    .execute_transaction(tx)
800                    .unwrap_or_else(|e| panic!("serial tx {i} failed: {e:?}"));
801            }
802            executor.evm_mut().db_mut().bump_bal_index();
803            executor.apply_post_execution_changes().expect("serial post-exec")
804        };
805
806        let bal = state.take_built_alloy_bal().expect("with_bal_builder was set");
807        state.merge_transitions(BundleRetention::Reverts);
808        let bundle_state = state.take_bundle();
809
810        (
811            ShadowOutput {
812                bundle_state,
813                receipts: block_result.receipts,
814                gas_used: block_result.gas_used,
815                requests: block_result.requests,
816            },
817            bal,
818        )
819    }
820
821    /// Shadow harness. Runs the block through both paths; asserts byte-equal outputs.
822    fn assert_shadow_equal(
823        evm_config: EthEvmConfig,
824        canonical_db_template: CacheDB<EmptyDB>,
825        block_header_only: SealedBlock<Block>,
826        txs: Vec<Recovered<TransactionSigned>>,
827    ) {
828        // Serial run: also produces the reference BAL we'll feed to the BAL path.
829        let (serial, reference_bal) =
830            run_serial_path(&evm_config, canonical_db_template.clone(), &block_header_only, &txs);
831
832        // BAL path: stamp the hash of the reference BAL onto the header.
833        let bal_hash = alloy_eip7928::compute_block_access_list_hash(&reference_bal);
834        let block =
835            empty_amsterdam_block_with_gas_limit(bal_hash, block_header_only.header().gas_limit());
836
837        let bal_out = run_execute_block(
838            &Runtime::test(),
839            evm_config,
840            db_factory(canonical_db_template),
841            to_arc_decoded(reference_bal),
842            &block,
843            txs,
844        )
845        .unwrap_or_else(|e| panic!("BAL path failed: {e:?}"));
846
847        // Byte-equal assertions. Any divergence surfaces the specific field that broke.
848        assert_eq!(
849            serial.receipts, bal_out.receipts,
850            "receipts diverge between serial and BAL paths",
851        );
852        assert_eq!(
853            serial.gas_used, bal_out.gas_used,
854            "gas_used differs: serial {} vs bal {}",
855            serial.gas_used, bal_out.gas_used,
856        );
857        assert_eq!(
858            serial.requests, bal_out.requests,
859            "requests (EIP-7685) diverge between serial and BAL paths",
860        );
861        assert_eq!(
862            serial.bundle_state, bal_out.state,
863            "bundle_state diverges — the canonical state transitions don't match",
864        );
865    }
866
867    #[test]
868    fn shadow_empty_block() {
869        // System calls only — no txs. Both paths should produce identical system-call
870        // side effects in their BundleState (beacon roots storage, history storage, etc.).
871        assert_shadow_equal(
872            EthEvmConfig::mainnet(),
873            system_contracts_db(),
874            empty_amsterdam_block(B256::ZERO),
875            Vec::new(),
876        );
877    }
878
879    #[test]
880    fn shadow_multi_value_transfer() {
881        // Two senders → same recipient. Byte-equal across paths means: worker-produced
882        // diffs commit identically to a directly-executed serial path.
883        use alloy_consensus::TxLegacy;
884        use alloy_primitives::TxKind;
885        use reth_chainspec::MAINNET;
886        use reth_ethereum_primitives::Transaction;
887        use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
888        use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
889
890        let evm_config = EthEvmConfig::mainnet();
891        let carol: alloy_primitives::Address = alloy_primitives::Address::from([0xCA; 20]);
892        let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
893
894        let alice_kp = generate_key(&mut rng());
895        let alice = public_key_to_address(alice_kp.public_key());
896        let bob_kp = generate_key(&mut rng());
897        let bob = public_key_to_address(bob_kp.public_key());
898
899        let mut db = system_contracts_db();
900        insert_funded(&mut db, alice, sender_balance);
901        insert_funded(&mut db, bob, sender_balance);
902
903        let chain_id = MAINNET.chain.id();
904        let make_tx = |kp, to, value, nonce: u64| {
905            sign_tx_with_key_pair(
906                kp,
907                Transaction::Legacy(TxLegacy {
908                    chain_id: Some(chain_id),
909                    nonce,
910                    gas_price: 1,
911                    gas_limit: 21_000,
912                    to: TxKind::Call(to),
913                    value: U256::from(value),
914                    input: Default::default(),
915                }),
916            )
917        };
918        let tx1 = Recovered::new_unchecked(make_tx(alice_kp, carol, 100u64, 0), alice);
919        let tx2 = Recovered::new_unchecked(make_tx(bob_kp, carol, 200u64, 0), bob);
920
921        assert_shadow_equal(evm_config, db, empty_amsterdam_block(B256::ZERO), vec![tx1, tx2]);
922    }
923
924    #[test]
925    fn rejects_tx_gas_limit_that_exceeds_remaining_block_gas() {
926        // Each worker sees an empty block, so both transactions fit individually. The ordered
927        // commit loop must still reject tx2 because tx1's committed gas leaves too little
928        // block gas for tx2's gas limit.
929        use alloy_consensus::TxLegacy;
930        use alloy_evm::block::BlockValidationError;
931        use alloy_primitives::TxKind;
932        use reth_chainspec::MAINNET;
933        use reth_ethereum_primitives::Transaction;
934        use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
935        use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
936
937        let evm_config = EthEvmConfig::mainnet();
938        let carol: alloy_primitives::Address = alloy_primitives::Address::from([0xCA; 20]);
939        let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
940        let block_gas_limit = 1_000_000;
941        let tx_gas_limit = 990_000;
942
943        let alice_kp = generate_key(&mut rng());
944        let alice = public_key_to_address(alice_kp.public_key());
945        let bob_kp = generate_key(&mut rng());
946        let bob = public_key_to_address(bob_kp.public_key());
947
948        let mut pre_block_db = system_contracts_db();
949        insert_funded(&mut pre_block_db, alice, sender_balance);
950        insert_funded(&mut pre_block_db, bob, sender_balance);
951
952        let chain_id = MAINNET.chain.id();
953        let make_tx = |kp, value| {
954            sign_tx_with_key_pair(
955                kp,
956                Transaction::Legacy(TxLegacy {
957                    chain_id: Some(chain_id),
958                    nonce: 0,
959                    gas_price: 1,
960                    gas_limit: tx_gas_limit,
961                    to: TxKind::Call(carol),
962                    value: U256::from(value),
963                    input: Default::default(),
964                }),
965            )
966        };
967        let tx1 = Recovered::new_unchecked(make_tx(alice_kp, 100u64), alice);
968        let tx2 = Recovered::new_unchecked(make_tx(bob_kp, 200u64), bob);
969
970        // Build the reference BAL under a generous gas limit so both workers can execute.
971        // Replaying the same BAL under `block_gas_limit` below should reject in the ordered
972        // commit loop before tx2 is committed.
973        let reference_block = empty_amsterdam_block(B256::ZERO);
974        let reference_bal = reference_bal_for_block(
975            &evm_config,
976            pre_block_db.clone(),
977            &reference_block,
978            vec![tx1.clone(), tx2.clone()],
979        );
980        let bal_hash = alloy_eip7928::compute_block_access_list_hash(&reference_bal);
981        let low_gas_block = empty_amsterdam_block_with_gas_limit(bal_hash, block_gas_limit);
982
983        let result = run_execute_block(
984            &Runtime::test(),
985            evm_config,
986            db_factory(pre_block_db),
987            to_arc_decoded(reference_bal),
988            &low_gas_block,
989            vec![tx1, tx2],
990        );
991
992        match result {
993            Err(BalExecutionError::Execution(err)) => assert!(matches!(
994                err.as_validation(),
995                Some(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas { .. })
996            )),
997            Err(err) => panic!("expected block gas validation error, got {err:?}"),
998            Ok(_) => panic!("expected block gas validation error, got Ok"),
999        }
1000    }
1001
1002    /// Two funded senders each transferring to a fresh recipient, plus the reference BAL of a
1003    /// block containing only the covered subset of those transfers.
1004    fn two_transfers_with_reference_bal(
1005        evm_config: &EthEvmConfig,
1006        tx_gas_limit: u64,
1007        bal_covers_first: bool,
1008    ) -> (
1009        CacheDB<EmptyDB>,
1010        BlockAccessList,
1011        Recovered<reth_ethereum_primitives::TransactionSigned>,
1012        Recovered<reth_ethereum_primitives::TransactionSigned>,
1013    ) {
1014        use alloy_consensus::TxLegacy;
1015        use alloy_primitives::TxKind;
1016        use reth_chainspec::MAINNET;
1017        use reth_ethereum_primitives::Transaction;
1018        use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
1019        use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
1020
1021        let recipient = alloy_primitives::Address::from([0xCA; 20]);
1022        let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
1023
1024        let alice_kp = generate_key(&mut rng());
1025        let alice = public_key_to_address(alice_kp.public_key());
1026        let bob_kp = generate_key(&mut rng());
1027        let bob = public_key_to_address(bob_kp.public_key());
1028
1029        let mut pre_block_db = system_contracts_db();
1030        insert_funded(&mut pre_block_db, alice, sender_balance);
1031        insert_funded(&mut pre_block_db, bob, sender_balance);
1032
1033        let chain_id = MAINNET.chain.id();
1034        let make_tx = |kp, value| {
1035            sign_tx_with_key_pair(
1036                kp,
1037                Transaction::Legacy(TxLegacy {
1038                    chain_id: Some(chain_id),
1039                    nonce: 0,
1040                    gas_price: 1,
1041                    gas_limit: tx_gas_limit,
1042                    to: TxKind::Call(recipient),
1043                    value: U256::from(value),
1044                    input: Default::default(),
1045                }),
1046            )
1047        };
1048        let tx1 = Recovered::new_unchecked(make_tx(alice_kp, 100u64), alice);
1049        let tx2 = Recovered::new_unchecked(make_tx(bob_kp, 200u64), bob);
1050
1051        let reference_block = empty_amsterdam_block(B256::ZERO);
1052        let covered = if bal_covers_first { vec![tx1.clone()] } else { vec![] };
1053        let reference_bal =
1054            reference_bal_for_block(evm_config, pre_block_db.clone(), &reference_block, covered);
1055        (pre_block_db, reference_bal, tx1, tx2)
1056    }
1057
1058    #[test]
1059    fn propagates_bal_miss_of_admitted_transaction_as_block_verdict() {
1060        // The BAL covers neither transaction: the first slot's speculative failure is
1061        // adjudicated in block order and becomes the block's verdict.
1062        let (pre_block_db, reference_bal, tx1, tx2) =
1063            two_transfers_with_reference_bal(&EthEvmConfig::mainnet(), 100_000, false);
1064        let bal_hash = alloy_eip7928::compute_block_access_list_hash(&reference_bal);
1065        let block = empty_amsterdam_block(bal_hash);
1066
1067        let result = run_execute_block(
1068            &Runtime::test(),
1069            EthEvmConfig::mainnet(),
1070            db_factory(pre_block_db),
1071            to_arc_decoded(reference_bal),
1072            &block,
1073            vec![tx1, tx2],
1074        );
1075
1076        match result {
1077            Err(BalExecutionError::Execution(err)) => {
1078                let msg = err.to_string();
1079                assert!(msg.contains("not found in BAL"), "expected BAL miss error, got {msg}");
1080            }
1081            Err(err) => panic!("expected BAL execution error, got {err:?}"),
1082            Ok(_) => panic!("expected BAL execution error, got Ok"),
1083        }
1084    }
1085
1086    #[test]
1087    fn admission_rejects_before_speculative_worker_failure_surfaces() {
1088        // tx2 exceeds the remaining block gas AND misses the BAL; block-gas admission is
1089        // adjudicated first, so the verdict is the gas error, not tx2's BAL failure.
1090        let (pre_block_db, reference_bal, tx1, tx2) =
1091            two_transfers_with_reference_bal(&EthEvmConfig::mainnet(), 990_000, true);
1092        let bal_hash = alloy_eip7928::compute_block_access_list_hash(&reference_bal);
1093        let low_gas_block = empty_amsterdam_block_with_gas_limit(bal_hash, 1_000_000);
1094
1095        let result = run_execute_block(
1096            &Runtime::test(),
1097            EthEvmConfig::mainnet(),
1098            db_factory(pre_block_db),
1099            to_arc_decoded(reference_bal),
1100            &low_gas_block,
1101            vec![tx1, tx2],
1102        );
1103
1104        match result {
1105            Err(BalExecutionError::Execution(err)) => assert!(
1106                matches!(
1107                    err.as_validation(),
1108                    Some(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas { .. })
1109                ),
1110                "expected block gas validation error, got {err:?}"
1111            ),
1112            Err(err) => panic!("expected block gas validation error, got {err:?}"),
1113            Ok(_) => panic!("expected block gas validation error, got Ok"),
1114        }
1115    }
1116
1117    #[test]
1118    fn worker_reuses_provider_after_failure_and_finishes_earlier_transactions() {
1119        use std::sync::atomic::{AtomicUsize, Ordering};
1120
1121        let evm_config = EthEvmConfig::mainnet();
1122        let (pre_block_db, reference_bal, tx1, tx2) =
1123            two_transfers_with_reference_bal(&evm_config, 100_000, true);
1124        let block = empty_amsterdam_block(B256::ZERO);
1125        let received_bal = convert_alloy_to_revm_bal(&reference_bal.into()).unwrap();
1126        let setups = AtomicUsize::new(0);
1127        let make_db = |_: bool| {
1128            // Reopening the provider after a speculative error must not introduce an
1129            // unindexed setup failure that overtakes the remaining transaction slots.
1130            if setups.fetch_add(1, Ordering::Relaxed) != 0 {
1131                return Err(BalExecutionError::Provider(
1132                    reth_errors::ProviderError::HeaderNotFound(B256::ZERO.into()),
1133                ));
1134            }
1135            Ok(pre_block_db.clone())
1136        };
1137        let (tx_tx, tx_rx) = crossbeam_channel::unbounded();
1138        // Recovery can finish out of order. A single worker must continue after both kinds
1139        // of error to execute slot 0, without reusing the failed slot 1 executor.
1140        tx_tx.send((2, Err(std::io::Error::other("sig fail")))).unwrap();
1141        tx_tx.send((1, Ok(tx2))).unwrap();
1142        tx_tx.send((0, Ok(tx1.clone()))).unwrap();
1143        drop(tx_tx);
1144        let (_abort_guard, abort_rx) = AbortGuard::new();
1145        let (result_tx, result_rx) = crossbeam_channel::unbounded();
1146        let runtime = Runtime::test();
1147        runtime.bal_streaming_pool().in_place_scope(|scope| {
1148            worker::spawn_worker(
1149                scope,
1150                tx_rx,
1151                abort_rx,
1152                result_tx,
1153                &evm_config,
1154                &make_db,
1155                received_bal,
1156                evm_config.evm_env(block.header()).unwrap(),
1157                evm_config.context_for_block(&block).unwrap(),
1158            );
1159        });
1160        let mut outputs = ordered_worker_outputs(&result_rx, 3);
1161        let output = outputs.next().unwrap().unwrap();
1162        assert_eq!(output.index, 0);
1163        assert_eq!(output.signer, tx1.signer());
1164        let mut state = State::builder().with_database(pre_block_db).build();
1165        let mut serial = evm_config.executor_for_block(&mut state, &block).unwrap();
1166        serial.apply_pre_execution_changes().unwrap();
1167        let expected = serial.execute_transaction_without_commit(tx1).unwrap();
1168        assert_eq!(output.result.result(), expected.result());
1169        assert!(matches!(
1170            outputs.next().unwrap(),
1171            Err(OrderedWorkerOutputError::Worker(worker::BalWorkerError::Execution {
1172                tx_index: 1,
1173                ..
1174            }))
1175        ));
1176        let error = outputs.next().unwrap().err().unwrap();
1177        assert!(matches!(
1178            error,
1179            OrderedWorkerOutputError::Worker(worker::BalWorkerError::Transaction {
1180                tx_index: 2,
1181                ..
1182            })
1183        ));
1184        assert!(matches!(
1185            BalExecutionError::from(error),
1186            BalExecutionError::Execution(BlockExecutionError::Validation(_))
1187        ));
1188        assert!(outputs.next().is_none());
1189        assert_eq!(setups.load(Ordering::Relaxed), 1);
1190    }
1191
1192    #[test]
1193    fn amsterdam_state_gas_admission_matches_serial_execution() {
1194        let evm_config = EthEvmConfig::new(Arc::new(
1195            reth_chainspec::ChainSpecBuilder::mainnet().amsterdam_activated().build(),
1196        ));
1197        for (tx_gas_limit, complete_bal) in [(500_000, true), (990_000, true), (990_000, false)] {
1198            let (pre_block_db, partial_bal, tx1, tx2) =
1199                two_transfers_with_reference_bal(&evm_config, tx_gas_limit, true);
1200            let reference_block = empty_amsterdam_block(B256::ZERO);
1201            let reference_bal = if complete_bal {
1202                reference_bal_for_block(
1203                    &evm_config,
1204                    pre_block_db.clone(),
1205                    &reference_block,
1206                    vec![tx1.clone(), tx2.clone()],
1207                )
1208            } else {
1209                // State-gas admission must also precede a speculative BAL miss at this slot.
1210                partial_bal
1211            };
1212            let block = empty_amsterdam_block_with_gas_limit(
1213                compute_block_access_list_hash(&reference_bal),
1214                1_000_000,
1215            );
1216            let mut evm_env = evm_config.evm_env(block.header()).unwrap();
1217            assert!(evm_env.cfg_env.enable_amsterdam_eip8037);
1218            // Both capped regular limits fit. The full limit of the second transaction
1219            // must still fit the state budget consumed by the first account creation.
1220            evm_env.cfg_env.tx_gas_limit_cap = Some(400_000);
1221            let ctx = evm_config.context_for_block(&block).unwrap();
1222            let mut state = State::builder().with_database(pre_block_db.clone()).build();
1223            let evm = evm_config.evm_with_env(&mut state, evm_env.clone());
1224            let mut serial = evm_config.create_executor_with_state(evm, ctx.clone());
1225            serial.apply_pre_execution_changes().unwrap();
1226            serial.execute_transaction(tx1.clone()).unwrap();
1227            assert!(serial.block_state_gas_used > 10_000);
1228            assert!(serial.block_regular_gas_used + 400_000 < 1_000_000);
1229            let serial_result = serial.execute_transaction(tx2.clone());
1230
1231            let (receipt_tx, _receipt_rx) = crossbeam_channel::unbounded();
1232            let parallel_result = execute_block(
1233                &Runtime::test(),
1234                &evm_config,
1235                &|_: bool| Ok(pre_block_db.clone()),
1236                to_arc_decoded(reference_bal),
1237                evm_env,
1238                ctx,
1239                2,
1240                tx_stream(vec![tx1, tx2]),
1241                receipt_tx,
1242            );
1243            if tx_gas_limit == 500_000 {
1244                serial_result.unwrap();
1245                assert_eq!(parallel_result.unwrap().0.receipts, serial.receipts());
1246            } else {
1247                let serial_error = serial_result.unwrap_err();
1248                let Some(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
1249                    block_available_gas,
1250                    ..
1251                }) = serial_error.as_validation()
1252                else {
1253                    panic!("expected serial state-gas admission error, got {serial_error:?}");
1254                };
1255                assert_eq!(*block_available_gas, 1_000_000 - serial.block_state_gas_used);
1256                let Err(BalExecutionError::Execution(parallel_error)) = parallel_result else {
1257                    panic!("expected parallel state-gas admission error");
1258                };
1259                assert_eq!(parallel_error.to_string(), serial_error.to_string());
1260            }
1261        }
1262    }
1263
1264    #[test]
1265    fn shadow_tx_with_revert() {
1266        // A tx that reverts in a deployed contract. Both paths must produce identical receipts
1267        // (success = false, gas charged, state rolled back except for gas payment + nonce bump).
1268        //
1269        // Deploys `0x60006000fd` (PUSH1 0 PUSH1 0 REVERT) at `revert_contract`. Sender calls
1270        // it; the call reverts; fees + nonce still apply.
1271        use alloy_consensus::TxLegacy;
1272        use alloy_primitives::{Bytes, TxKind};
1273        use reth_chainspec::MAINNET;
1274        use reth_ethereum_primitives::Transaction;
1275        use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
1276        use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
1277
1278        let evm_config = EthEvmConfig::mainnet();
1279        let revert_contract: alloy_primitives::Address =
1280            alloy_primitives::Address::from([0xDE; 20]);
1281        let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
1282
1283        let alice_kp = generate_key(&mut rng());
1284        let alice = public_key_to_address(alice_kp.public_key());
1285
1286        // Deploy the revert contract bytecode.
1287        let revert_code: Bytes = Bytes::from_static(&[0x60, 0x00, 0x60, 0x00, 0xfd]);
1288        let mut db = system_contracts_db();
1289        insert_funded(&mut db, alice, sender_balance);
1290        db.insert_account_info(
1291            revert_contract,
1292            AccountInfo::from_bytecode(Bytecode::new_raw(revert_code)),
1293        );
1294
1295        let tx = Recovered::new_unchecked(
1296            sign_tx_with_key_pair(
1297                alice_kp,
1298                Transaction::Legacy(TxLegacy {
1299                    chain_id: Some(MAINNET.chain.id()),
1300                    nonce: 0,
1301                    gas_price: 1,
1302                    gas_limit: 50_000,
1303                    to: TxKind::Call(revert_contract),
1304                    value: U256::ZERO,
1305                    input: Default::default(),
1306                }),
1307            ),
1308            alice,
1309        );
1310
1311        assert_shadow_equal(evm_config, db, empty_amsterdam_block(B256::ZERO), vec![tx]);
1312    }
1313
1314    #[test]
1315    fn shadow_tx_with_sstore() {
1316        // Tx calls a deployed contract that does `SSTORE(0, 0x42)`. The storage write must
1317        // commit identically across serial and BAL paths even though the canonical state applies
1318        // a diff produced by a worker EVM.
1319        //
1320        // Bytecode: PUSH1 0x42, PUSH1 0x00, SSTORE, STOP → `0x60 0x42 0x60 0x00 0x55 0x00`.
1321        use alloy_consensus::TxLegacy;
1322        use alloy_primitives::{Bytes, TxKind};
1323        use reth_chainspec::MAINNET;
1324        use reth_ethereum_primitives::Transaction;
1325        use reth_primitives_traits::crypto::secp256k1::public_key_to_address;
1326        use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
1327
1328        let evm_config = EthEvmConfig::mainnet();
1329        let sstore_contract: alloy_primitives::Address =
1330            alloy_primitives::Address::from([0x55; 20]);
1331        let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
1332
1333        let alice_kp = generate_key(&mut rng());
1334        let alice = public_key_to_address(alice_kp.public_key());
1335
1336        // Deploy the SSTORE contract.
1337        let sstore_code: Bytes = Bytes::from_static(&[0x60, 0x42, 0x60, 0x00, 0x55, 0x00]);
1338        let mut db = system_contracts_db();
1339        insert_funded(&mut db, alice, sender_balance);
1340        db.insert_account_info(
1341            sstore_contract,
1342            AccountInfo::from_bytecode(Bytecode::new_raw(sstore_code)),
1343        );
1344
1345        let tx = Recovered::new_unchecked(
1346            sign_tx_with_key_pair(
1347                alice_kp,
1348                Transaction::Legacy(TxLegacy {
1349                    chain_id: Some(MAINNET.chain.id()),
1350                    nonce: 0,
1351                    gas_price: 1,
1352                    gas_limit: 100_000,
1353                    to: TxKind::Call(sstore_contract),
1354                    value: U256::ZERO,
1355                    input: Default::default(),
1356                }),
1357            ),
1358            alice,
1359        );
1360
1361        assert_shadow_equal(evm_config, db, empty_amsterdam_block(B256::ZERO), vec![tx]);
1362    }
1363
1364    #[test]
1365    fn returns_built_bal_for_final_hash_mismatch() {
1366        // Build the BAL an empty block actually produces, then append a phantom address
1367        // that execution never touches. The rebuilt BAL omits it, and the outer consensus
1368        // validator is responsible for comparing that rebuilt hash to the header commitment.
1369        use alloy_eip7928::AccountChanges;
1370
1371        let evm_config = EthEvmConfig::mainnet();
1372
1373        // Real BAL the block would produce.
1374        let real_bal = reference_bal_for_empty_block(&evm_config);
1375        assert!(!real_bal.is_empty(), "reference BAL must be non-empty");
1376
1377        // Tamper: append a phantom address not accessed during execution.
1378        let phantom = alloy_primitives::Address::from([0xFF; 20]);
1379        let mut tampered_entries: Vec<AccountChanges> = real_bal;
1380        tampered_entries.push(AccountChanges::new(phantom));
1381        let tampered_bal: alloy_eip7928::bal::Bal = alloy_eip7928::bal::Bal::new(tampered_entries);
1382
1383        // Stamp the tampered BAL's hash on the block header.
1384        let tampered_block_access_list: BlockAccessList = tampered_bal.clone().into();
1385        let tampered_hash =
1386            alloy_eip7928::compute_block_access_list_hash(&tampered_block_access_list);
1387        let block = empty_amsterdam_block(tampered_hash);
1388
1389        let received = {
1390            let raw = alloy_rlp::encode(&tampered_bal).into();
1391            Arc::new(DecodedBal::new(tampered_bal, raw))
1392        };
1393
1394        let result = run_execute_block_full(
1395            &Runtime::test(),
1396            evm_config,
1397            db_factory(system_contracts_db()),
1398            received,
1399            &block,
1400            Vec::<Recovered<TransactionSigned>>::new(),
1401        );
1402
1403        match result {
1404            Ok((_, built_bal)) => {
1405                let rebuilt = alloy_eip7928::compute_block_access_list_hash(&built_bal);
1406                assert_ne!(rebuilt, tampered_hash, "rebuilt and header hashes must differ");
1407            }
1408            Err(e) => panic!("expected success with rebuilt BAL, got {e:?}"),
1409        }
1410    }
1411
1412    #[test]
1413    fn canonical_make_db_failure() {
1414        // A make_db that always fails must surface as Provider before any workers are
1415        // spawned or the BAL is processed.
1416        let evm_config = EthEvmConfig::mainnet();
1417        let block = empty_amsterdam_block(B256::ZERO);
1418
1419        let failing_make_db = || -> Result<CacheDB<EmptyDB>, BalExecutionError> {
1420            Err(reth_provider::ProviderError::BestBlockNotFound.into())
1421        };
1422
1423        let result = run_execute_block(
1424            &Runtime::test(),
1425            evm_config,
1426            failing_make_db,
1427            to_arc_decoded(BlockAccessList::default()),
1428            &block,
1429            Vec::<Recovered<TransactionSigned>>::new(),
1430        );
1431
1432        assert!(
1433            matches!(result, Err(BalExecutionError::Provider(_))),
1434            "expected Provider error from canonical make_db failure, got {result:?}",
1435        );
1436    }
1437
1438    #[test]
1439    fn worker_tx_recovery_error_becomes_validation_error() {
1440        // A tx recovery failure fed into the worker channel must surface as
1441        // a block validation error. Uses execute_block directly since tx_stream hardcodes
1442        // Infallible and cannot inject errors.
1443        let evm_config = EthEvmConfig::mainnet();
1444        let block = empty_amsterdam_block(B256::ZERO);
1445
1446        let (tx_tx, tx_rx) = crossbeam_channel::unbounded::<(
1447            usize,
1448            Result<Recovered<TransactionSigned>, std::io::Error>,
1449        )>();
1450        tx_tx.send((0, Err(std::io::Error::other("sig fail")))).unwrap();
1451        drop(tx_tx);
1452
1453        let (receipt_tx, _receipt_rx) = crossbeam_channel::unbounded();
1454        let evm_env = evm_config.evm_env(block.header()).unwrap();
1455        let execution_ctx = evm_config.context_for_block(&block).unwrap();
1456        let make_db = db_factory(system_contracts_db());
1457        let make_db = |_: bool| make_db();
1458
1459        let result = execute_block(
1460            &Runtime::test(),
1461            &evm_config,
1462            &make_db,
1463            to_arc_decoded(BlockAccessList::default()),
1464            evm_env,
1465            execution_ctx,
1466            1, // transaction_count = 1 → exactly one worker spawned
1467            tx_rx,
1468            receipt_tx,
1469        );
1470
1471        assert!(
1472            matches!(result, Err(BalExecutionError::Execution(BlockExecutionError::Validation(_)))),
1473            "expected validation error from tx recovery failure, got {result:?}",
1474        );
1475    }
1476
1477    #[test]
1478    fn gas_tracker_non_amsterdam_uses_cumulative_gas() {
1479        // A half-state-gas result keeps both Amsterdam budgets (regular and state) at
1480        // 300_000 used while cumulative_tx_gas_used is 600_000, so a second tx that fits
1481        // within the block limit but not the remaining cumulative budget proves that
1482        // non-Amsterdam reads cumulative_tx_gas_used while Amsterdam does not.
1483        use revm::{
1484            context::result::{
1485                ExecResultAndState, ExecutionResult, Output, ResultGas, SuccessReason,
1486            },
1487            state::EvmState,
1488        };
1489
1490        let block_gas_limit = 1_000_000u64;
1491        let first_tx_gas = 600_000u64;
1492        let second_tx_gas_limit = 500_000u64; // fits in total limit but not after cumulative
1493                                              // deduction
1494
1495        let gas = ResultGas::new_with_state_gas(first_tx_gas, 0, 0, first_tx_gas / 2);
1496        let fake_result: ResultAndState<revm::context::result::HaltReason> =
1497            ExecResultAndState::new(
1498                ExecutionResult::Success {
1499                    reason: SuccessReason::Return,
1500                    gas,
1501                    logs: vec![],
1502                    output: Output::Call(Default::default()),
1503                },
1504                EvmState::default(),
1505            );
1506
1507        // Non-Amsterdam: block_available_gas = 1_000_000 - 600_000 = 400_000 → reject 500_000.
1508        let mut non_amsterdam = BlockGasTracker::new(block_gas_limit, false, None);
1509        non_amsterdam.record_result(&fake_result);
1510        assert!(
1511            non_amsterdam.validate_tx_limit(second_tx_gas_limit).is_err(),
1512            "non-Amsterdam tracker must reject tx that exceeds remaining cumulative gas",
1513        );
1514
1515        // Amsterdam: both regular and state budgets have 700_000 left → accept 500_000.
1516        let mut amsterdam = BlockGasTracker::new(block_gas_limit, true, None);
1517        amsterdam.record_result(&fake_result);
1518        assert!(
1519            amsterdam.validate_tx_limit(second_tx_gas_limit).is_ok(),
1520            "Amsterdam tracker must accept the same tx since per-dimension budgets still fit",
1521        );
1522    }
1523
1524    #[test]
1525    fn gas_tracker_amsterdam_enforces_state_gas_budget() {
1526        // An all-state-gas result leaves the regular budget untouched, so only the
1527        // state-gas admission check can reject the second transaction. The tx's full gas
1528        // limit counts against the state budget — tx_gas_limit_cap does not bound it.
1529        use revm::{
1530            context::result::{
1531                ExecResultAndState, ExecutionResult, Output, ResultGas, SuccessReason,
1532            },
1533            state::EvmState,
1534        };
1535
1536        let block_gas_limit = 1_000_000u64;
1537        let first_tx_state_gas = 600_000u64;
1538        let second_tx_gas_limit = 500_000u64; // exceeds the remaining 400_000 state budget
1539
1540        let gas = ResultGas::new_with_state_gas(first_tx_state_gas, 0, 0, first_tx_state_gas);
1541        let fake_result: ResultAndState<revm::context::result::HaltReason> =
1542            ExecResultAndState::new(
1543                ExecutionResult::Success {
1544                    reason: SuccessReason::Return,
1545                    gas,
1546                    logs: vec![],
1547                    output: Output::Call(Default::default()),
1548                },
1549                EvmState::default(),
1550            );
1551
1552        // Regular budget is full (block_regular_gas_used = 0) but the state budget has
1553        // only 400_000 left → reject 500_000.
1554        let mut amsterdam = BlockGasTracker::new(block_gas_limit, true, None);
1555        amsterdam.record_result(&fake_result);
1556        assert!(
1557            amsterdam.validate_tx_limit(second_tx_gas_limit).is_err(),
1558            "Amsterdam tracker must reject tx whose gas limit exceeds the remaining state budget",
1559        );
1560        assert!(
1561            amsterdam.validate_tx_limit(400_000).is_ok(),
1562            "Amsterdam tracker must accept tx whose gas limit exactly fits the state budget",
1563        );
1564
1565        // With a cap of 400_000 the capped regular check passes, but the full 500_000
1566        // limit still counts against the state budget → reject.
1567        let mut capped = BlockGasTracker::new(block_gas_limit, true, Some(400_000));
1568        capped.record_result(&fake_result);
1569        assert!(
1570            capped.validate_tx_limit(second_tx_gas_limit).is_err(),
1571            "tx_gas_limit_cap must not bound the state-gas admission check",
1572        );
1573    }
1574
1575    #[test]
1576    fn gas_tracker_caps_oversized_tx_gas_limit_at_tx_gas_limit_cap() {
1577        // A tx with gas_limit above TX_GAS_LIMIT_CAP (EIP-7825) is admitted when the
1578        // capped value fits in the remaining block gas and rejected when it does not.
1579        use revm::{
1580            context::result::{
1581                ExecResultAndState, ExecutionResult, Output, ResultGas, SuccessReason,
1582            },
1583            primitives::eip7825::TX_GAS_LIMIT_CAP,
1584            state::EvmState,
1585        };
1586
1587        let block_gas_limit = 30_000_000u64;
1588        let oversized = TX_GAS_LIMIT_CAP + 1_000_000; // 17_777_216 — above the cap
1589
1590        // Case 1: fresh block, no prior gas consumed.
1591        // tx_min_gas_limit = TX_GAS_LIMIT_CAP (16_777_216) ≤ block_available_gas (30M) → Ok.
1592        let tracker = BlockGasTracker::new(block_gas_limit, false, Some(TX_GAS_LIMIT_CAP));
1593        assert!(
1594            tracker.validate_tx_limit(oversized).is_ok(),
1595            "oversized tx must pass when capped limit fits in block gas",
1596        );
1597
1598        // Case 2: prior tx consumed 20M, leaving 10M available.
1599        // tx_min_gas_limit = TX_GAS_LIMIT_CAP (16_777_216) > block_available_gas (10M) → Err.
1600        let prior_gas = 20_000_000u64;
1601        let gas = ResultGas::new_with_state_gas(prior_gas, 0, 0, prior_gas);
1602        let fake_result: ResultAndState<revm::context::result::HaltReason> =
1603            ExecResultAndState::new(
1604                ExecutionResult::Success {
1605                    reason: SuccessReason::Return,
1606                    gas,
1607                    logs: vec![],
1608                    output: Output::Call(Default::default()),
1609                },
1610                EvmState::default(),
1611            );
1612
1613        let mut tracker = BlockGasTracker::new(block_gas_limit, false, Some(TX_GAS_LIMIT_CAP));
1614        tracker.record_result(&fake_result);
1615        assert!(
1616            tracker.validate_tx_limit(oversized).is_err(),
1617            "oversized tx must be rejected when capped limit exceeds remaining block gas",
1618        );
1619    }
1620}