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, DecodedBal},
31    compute_block_access_list_hash, BlockAccessList,
32};
33use alloy_primitives::Address;
34use crossbeam_channel::{Receiver, Sender};
35use evm2::evm::Bal as EvmBal;
36use reth_engine_primitives::BlockAccessListDecodeError;
37use reth_evm::{
38    BlockExecutionOutput, BlockExecutor, BlockExecutorFactory, BlockExecutorFor, ConfigureEvm,
39    Database, EvmEnvFor, ExecutableTxFor, ExecutionCtxFor,
40};
41use reth_primitives_traits::ReceiptTy;
42use reth_tasks::Runtime;
43use std::sync::Arc;
44
45use crate::tree::payload_processor::receipt_root_task::IndexedReceipt;
46
47/// Executes one block on the BAL path using the runtime's persistent BAL worker pool.
48///
49/// Returns the execution output, the recovered senders, the BAL rebuilt from this execution, and
50/// the received BAL in the evm2 representation the workers consumed.
51#[expect(clippy::too_many_arguments, clippy::type_complexity)]
52pub fn execute_block<'a, Evm, Tx, Err, DB, MakeDb>(
53    runtime: &Runtime,
54    evm_config: &'a Evm,
55    make_db: &'a MakeDb,
56    input_bal: Arc<DecodedBal>,
57    evm_env: EvmEnvFor<Evm>,
58    ctx: ExecutionCtxFor<'a, Evm>,
59    transaction_count: usize,
60    txs: Receiver<(usize, Result<Tx, Err>)>,
61    receipt_tx: Sender<IndexedReceipt<ReceiptTy<Evm::Primitives>>>,
62) -> Result<
63    (BlockExecutionOutput<ReceiptTy<Evm::Primitives>>, Vec<Address>, BlockAccessList, Arc<EvmBal>),
64    BalExecutionError,
65>
66where
67    Evm: ConfigureEvm + 'static,
68    Tx: ExecutableTxFor<Evm> + Send + 'a,
69    Err: core::error::Error + Send + Sync + 'static,
70    DB: Database + Send + 'a,
71    MakeDb: Fn(bool) -> Result<DB, BalExecutionError> + Sync + 'a,
72{
73    let worker_pool = runtime.bal_streaming_pool();
74    let worker_count = worker_pool.current_num_threads().max(1).min(transaction_count);
75
76    worker_pool.in_place_scope(|scope| {
77        execute_block_inner(
78            scope,
79            evm_config,
80            make_db,
81            input_bal,
82            evm_env,
83            ctx,
84            transaction_count,
85            txs,
86            receipt_tx,
87            worker_count,
88        )
89    })
90}
91
92#[expect(clippy::too_many_arguments, clippy::type_complexity)]
93fn execute_block_inner<'scope, Evm, Tx, Err, DB, MakeDb>(
94    scope: &rayon::Scope<'scope>,
95    evm_config: &'scope Evm,
96    make_db: &'scope MakeDb,
97    input_bal: Arc<DecodedBal>,
98    evm_env: EvmEnvFor<Evm>,
99    ctx: ExecutionCtxFor<'scope, Evm>,
100    transaction_count: usize,
101    txs: Receiver<(usize, Result<Tx, Err>)>,
102    receipt_tx: Sender<IndexedReceipt<ReceiptTy<Evm::Primitives>>>,
103    worker_count: usize,
104) -> Result<
105    (BlockExecutionOutput<ReceiptTy<Evm::Primitives>>, Vec<Address>, BlockAccessList, Arc<EvmBal>),
106    BalExecutionError,
107>
108where
109    Evm: ConfigureEvm + 'scope,
110    Tx: ExecutableTxFor<Evm> + Send + 'scope,
111    Err: core::error::Error + Send + Sync + 'static,
112    DB: Database + Send + 'scope,
113    MakeDb: Fn(bool) -> Result<DB, BalExecutionError> + Sync + 'scope,
114{
115    // Convert the BAL from alloy to a BAL that can be consumed by evm2, that is more amenable
116    // for state lookups.
117    //
118    // This is failable.
119    //
120    // This is due to bytecodes. A transaction can attempt to deploy illegal bytecodes, e.g. due to
121    // EIP-3541 or more specifically due to EIP-7702.
122    //
123    // During serial execution this check happens before the bytecode is deployed and if the check
124    // is triggered then the execution is reverted, and as such no actual code change event takes
125    // place. Therefore, if we do observe such a bytecode in a BAL then that means the BAL is
126    // invalid as no legal execution should've led to this bytecode deployment.
127    let received_bal = Arc::new(
128        <BlockExecutorFor<'scope, Evm> as BlockExecutor>::convert_block_access_list(
129            input_bal.as_bal().as_vec(),
130        )
131        .map_err(BlockAccessListDecodeError::new)?,
132    );
133    let (result_tx, result_rx) = crossbeam_channel::unbounded();
134    let (abort_guard, abort_rx) = AbortGuard::new();
135
136    for _ in 0..worker_count {
137        worker::spawn_worker(
138            scope,
139            txs.clone(),
140            abort_rx.clone(),
141            result_tx.clone(),
142            evm_config,
143            make_db,
144            Arc::clone(&received_bal),
145            evm_env.clone(),
146            ctx.clone(),
147        );
148    }
149    drop(result_tx);
150
151    let evm = evm_config.evm_with_env(make_db(false)?, evm_env);
152    let mut canonical_executor =
153        evm_config.block_executor_factory().create_executor(evm, ctx.clone());
154    canonical_executor.enable_block_access_list_builder();
155    canonical_executor.apply_pre_execution_changes()?;
156
157    let mut senders = Vec::with_capacity(transaction_count);
158    let mut last_sent_len = 0;
159    for output in ordered_worker_outputs(&result_rx, transaction_count) {
160        let output = match output {
161            Ok(output) => output,
162            Err(OrderedWorkerOutputError::Worker(worker::BalWorkerError::Execution {
163                tx_index,
164                tx_gas_limit,
165                source,
166            })) => {
167                // Block-gas admission is adjudicated in transaction order and takes
168                // precedence over the failure at this slot.
169                canonical_executor.validate_transaction_gas_limit(tx_gas_limit)?;
170
171                // The transaction is admitted, so its execution failure is the block's
172                // verdict: a BAL miss proves the received BAL diverges from this execution.
173                return Err(
174                    worker::BalWorkerError::Execution { tx_index, tx_gas_limit, source }.into()
175                );
176            }
177            Err(error) => return Err(error.into()),
178        };
179        canonical_executor.validate_transaction_gas_limit(output.tx_gas_limit)?;
180        canonical_executor.commit_transaction(output.result)?;
181        senders.push(output.signer);
182
183        let current_len = canonical_executor.receipts().len();
184        if current_len > last_sent_len {
185            last_sent_len = current_len;
186            if let Some(receipt) = canonical_executor.receipts().last() {
187                let _ = receipt_tx.send(IndexedReceipt::new(current_len - 1, receipt.clone()));
188            }
189        }
190    }
191    drop(abort_guard);
192
193    let (output, built_bal) = canonical_executor.finish_with_block_access_list()?;
194    let built_bal = built_bal.expect("BAL builder was enabled for parallel execution");
195    log_bal_divergence(&built_bal, input_bal.as_bal());
196    Ok((output, senders, built_bal, received_bal))
197}
198
199fn log_bal_divergence(built_bal: &BlockAccessList, received_bal: &Bal) {
200    if tracing::enabled!(target: "engine::tree::payload_processor::bal", tracing::Level::DEBUG) &&
201        built_bal.as_slice() != received_bal.as_slice()
202    {
203        let rebuilt = compute_block_access_list_hash(built_bal);
204        let expected = compute_block_access_list_hash(received_bal.as_slice());
205        let div = received_bal.diff(built_bal.as_slice());
206        tracing::debug!(
207            target: "engine::tree::payload_processor::bal",
208            %rebuilt,
209            %expected,
210            %div,
211            "first BAL divergence",
212        );
213    }
214}
215
216/// Closes the abort channel on drop, waking scoped workers before the scope exits.
217struct AbortGuard {
218    _tx: Sender<()>,
219}
220
221impl AbortGuard {
222    fn new() -> (Self, Receiver<()>) {
223        let (tx, rx) = crossbeam_channel::bounded(0);
224        (Self { _tx: tx }, rx)
225    }
226}
227
228#[cfg(test)]
229mod tests {
230
231    use super::*;
232    use alloy_consensus::{BlockHeader, Header, TxLegacy};
233    use alloy_eip7928::{
234        bal::Bal as AlloyBal, AccountChanges, BlockAccessIndex, BlockAccessList, CodeChange,
235    };
236    use alloy_eips::{
237        eip2935::{HISTORY_STORAGE_ADDRESS, HISTORY_STORAGE_CODE},
238        eip4788::{BEACON_ROOTS_ADDRESS, BEACON_ROOTS_CODE},
239        eip7002::{WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS, WITHDRAWAL_REQUEST_PREDEPLOY_CODE},
240        eip7251::{CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS, CONSOLIDATION_REQUEST_PREDEPLOY_CODE},
241    };
242    use alloy_primitives::{Bytes, B256, U256};
243    use evm2::evm::{
244        CacheDB, EmptyDB, BUILDER_DEPOSIT_REQUEST_ADDRESS, BUILDER_EXIT_REQUEST_ADDRESS,
245    };
246    use reth_chainspec::{ChainSpecBuilder, MAINNET};
247    use reth_engine_primitives::{InsertBlockErrorKind, InsertBlockValidationError};
248    use reth_ethereum_primitives::{Block, BlockBody, Receipt, Transaction, TransactionSigned};
249    use reth_evm::{
250        cached::{AccountInfo, Bytecode},
251        BlockExecutionOutput, BlockValidationError,
252    };
253    use reth_evm_ethereum::EthEvmConfig;
254    use reth_primitives_traits::{Block as _, Recovered, SealedBlock};
255    use reth_testing_utils::generators::{generate_key, rng, sign_tx_with_key_pair};
256    use std::{
257        convert::Infallible,
258        sync::atomic::{AtomicUsize, Ordering},
259    };
260
261    fn test_evm_config() -> EthEvmConfig {
262        EthEvmConfig::new(Arc::new(ChainSpecBuilder::mainnet().amsterdam_activated().build()))
263    }
264
265    fn non_amsterdam_evm_config() -> EthEvmConfig {
266        EthEvmConfig::new(Arc::new(ChainSpecBuilder::mainnet().osaka_activated().build()))
267    }
268
269    /// Wraps a `BlockAccessList` into an `Arc<DecodedBal>` by RLP-encoding the BAL.
270    fn to_arc_decoded(bal: BlockAccessList) -> Arc<DecodedBal> {
271        let alloy_bal: AlloyBal = bal.into();
272        let raw = alloy_rlp::encode(&alloy_bal).into();
273        Arc::new(DecodedBal::new(alloy_bal, raw))
274    }
275
276    /// Builds an in-memory canonical DB pre-populated with the system contracts that block
277    /// execution calls: beacon roots (EIP-4788), historical block hashes (EIP-2935), the
278    /// withdrawal (EIP-7002) and consolidation (EIP-7251) request contracts, and no-op builder
279    /// request contracts (EIP-8282).
280    fn system_contracts_db() -> CacheDB<EmptyDB> {
281        let mut db = CacheDB::<EmptyDB>::default();
282        db.insert_account_info(
283            &BEACON_ROOTS_ADDRESS,
284            AccountInfo::default()
285                .with_nonce(1)
286                .with_code(Bytecode::new_raw(BEACON_ROOTS_CODE.clone())),
287        );
288        db.insert_account_info(
289            &WITHDRAWAL_REQUEST_PREDEPLOY_ADDRESS,
290            AccountInfo::default()
291                .with_nonce(1)
292                .with_code(Bytecode::new_raw(WITHDRAWAL_REQUEST_PREDEPLOY_CODE.clone())),
293        );
294        db.insert_account_info(
295            &CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS,
296            AccountInfo::default()
297                .with_nonce(1)
298                .with_code(Bytecode::new_raw(CONSOLIDATION_REQUEST_PREDEPLOY_CODE.clone())),
299        );
300        for address in [BUILDER_DEPOSIT_REQUEST_ADDRESS, BUILDER_EXIT_REQUEST_ADDRESS] {
301            db.insert_account_info(
302                &address,
303                AccountInfo::default()
304                    .with_nonce(1)
305                    .with_code(Bytecode::new_legacy(Bytes::from_static(&[0x00]))),
306            );
307        }
308        db.insert_account_info(
309            &HISTORY_STORAGE_ADDRESS,
310            AccountInfo::default()
311                .with_nonce(1)
312                .with_code(Bytecode::new_raw(HISTORY_STORAGE_CODE.clone())),
313        );
314        db
315    }
316
317    /// Builds a minimal sealed block (empty body, Amsterdam-ready header) for tests.
318    fn empty_amsterdam_block(header_bal_hash: B256) -> SealedBlock<Block> {
319        empty_amsterdam_block_with_gas_limit(header_bal_hash, 30_000_000)
320    }
321
322    fn empty_amsterdam_block_with_gas_limit(
323        header_bal_hash: B256,
324        gas_limit: u64,
325    ) -> SealedBlock<Block> {
326        let header = Header {
327            timestamp: 1,
328            number: 1,
329            gas_limit,
330            parent_beacon_block_root: Some(B256::ZERO),
331            withdrawals_root: Some(alloy_consensus::EMPTY_ROOT_HASH),
332            requests_hash: Some(alloy_eips::eip7685::EMPTY_REQUESTS_HASH),
333            excess_blob_gas: Some(0),
334            blob_gas_used: Some(0),
335            block_access_list_hash: Some(header_bal_hash),
336            slot_number: Some(0),
337            ..Header::default()
338        };
339        Block {
340            header,
341            body: BlockBody {
342                transactions: vec![],
343                ommers: vec![],
344                withdrawals: Some(vec![].into()),
345            },
346        }
347        .seal_slow()
348    }
349
350    fn db_factory(
351        db: CacheDB<EmptyDB>,
352    ) -> impl Fn(bool) -> Result<CacheDB<EmptyDB>, BalExecutionError> + Sync {
353        move |_| Ok(db.clone())
354    }
355
356    fn tx_stream<Tx>(txs: Vec<Tx>) -> Receiver<(usize, Result<Tx, Infallible>)> {
357        let (tx, rx) = crossbeam_channel::unbounded();
358        for (index, transaction) in txs.into_iter().enumerate() {
359            tx.send((index, Ok(transaction))).unwrap();
360        }
361        rx
362    }
363
364    fn run_execute_block<Tx, DB, MakeDb>(
365        runtime: &Runtime,
366        evm_config: EthEvmConfig,
367        make_db: MakeDb,
368        input_bal: Arc<DecodedBal>,
369        block: &SealedBlock<Block>,
370        txs: Vec<Tx>,
371    ) -> Result<BlockExecutionOutput<Receipt>, BalExecutionError>
372    where
373        Tx: ExecutableTxFor<EthEvmConfig> + Send,
374        DB: Database + Send,
375        MakeDb: Fn(bool) -> Result<DB, BalExecutionError> + Sync,
376    {
377        run_execute_block_full(runtime, evm_config, make_db, input_bal, block, txs)
378            .map(|(output, _)| output)
379    }
380
381    fn run_execute_block_full<Tx, DB, MakeDb>(
382        runtime: &Runtime,
383        evm_config: EthEvmConfig,
384        make_db: MakeDb,
385        input_bal: Arc<DecodedBal>,
386        block: &SealedBlock<Block>,
387        txs: Vec<Tx>,
388    ) -> Result<(BlockExecutionOutput<Receipt>, BlockAccessList), BalExecutionError>
389    where
390        Tx: ExecutableTxFor<EthEvmConfig> + Send,
391        DB: Database + Send,
392        MakeDb: Fn(bool) -> Result<DB, BalExecutionError> + Sync,
393    {
394        let transaction_count = txs.len();
395        let (receipt_tx, _receipt_rx) = crossbeam_channel::unbounded();
396        let evm_env = evm_config.evm_env(block.header()).unwrap();
397        let execution_ctx = evm_config.context_for_block(block).unwrap();
398        execute_block(
399            runtime,
400            &evm_config,
401            &make_db,
402            input_bal,
403            evm_env,
404            execution_ctx,
405            transaction_count,
406            tx_stream(txs),
407            receipt_tx,
408        )
409        .map(|(output, _, built_bal, _)| (output, built_bal))
410    }
411
412    /// Inserts `AccountInfo { nonce: 0, balance }` for `addr` into the canonical DB.
413    fn insert_funded(db: &mut CacheDB<EmptyDB>, address: Address, balance: U256) {
414        db.insert_account_info(&address, AccountInfo::default().with_balance(balance));
415    }
416
417    /// Runs the block through the serial executor and captures its full output together with the
418    /// composed BAL it built, which is the input of the BAL path.
419    fn run_serial_path(
420        evm_config: &EthEvmConfig,
421        canonical_db: CacheDB<EmptyDB>,
422        block: &SealedBlock<Block>,
423        txs: &[Recovered<TransactionSigned>],
424    ) -> (BlockExecutionOutput<Receipt>, BlockAccessList) {
425        let mut executor =
426            evm_config.executor_for_block(canonical_db, block).expect("build serial executor");
427        executor.enable_block_access_list_builder();
428        executor.apply_pre_execution_changes().expect("serial pre-exec");
429        for (index, tx) in txs.iter().cloned().enumerate() {
430            executor
431                .execute_transaction(tx)
432                .unwrap_or_else(|err| panic!("serial tx {index} failed: {err:?}"));
433        }
434        let (output, bal) = executor.finish_with_block_access_list().expect("serial post-exec");
435        (output, bal.expect("BAL builder was enabled"))
436    }
437
438    /// Runs only the canonical phases (pre-exec → post-exec, no txs) against a fresh
439    /// `system_contracts_db()` to compute the composed BAL a block produces. Used to build
440    /// the "reference" received BAL for the happy-path test below.
441    ///
442    /// This intentionally mirrors what `execute_block` does internally,
443    /// but without any hash check — the output is the BAL itself, not a pass/fail signal.
444    fn reference_bal_for_empty_block(evm_config: &EthEvmConfig) -> BlockAccessList {
445        run_serial_path(evm_config, system_contracts_db(), &empty_amsterdam_block(B256::ZERO), &[])
446            .1
447    }
448
449    /// Runs the canonical path on a block with real txs (no hash check) and returns the
450    /// composed BAL. Used to build the reference BAL for happy-path multi-tx tests.
451    fn reference_bal_for_block(
452        evm_config: &EthEvmConfig,
453        db: CacheDB<EmptyDB>,
454        block: &SealedBlock<Block>,
455        txs: &[Recovered<TransactionSigned>],
456    ) -> BlockAccessList {
457        run_serial_path(evm_config, db, block, txs).1
458    }
459
460    fn transfer(to: Address, value: u64, nonce: u64, gas_limit: u64) -> Transaction {
461        Transaction::Legacy(TxLegacy {
462            chain_id: Some(MAINNET.chain.id()),
463            nonce,
464            gas_price: 1,
465            gas_limit,
466            to: alloy_primitives::TxKind::Call(to),
467            value: U256::from(value),
468            input: Default::default(),
469        })
470    }
471
472    macro_rules! signed_transfer {
473        ($key:expr, $signer:expr, $to:expr, $value:expr, $nonce:expr, $gas_limit:expr) => {
474            Recovered::new_unchecked(
475                sign_tx_with_key_pair($key, transfer($to, $value, $nonce, $gas_limit)),
476                $signer,
477            )
478        };
479    }
480
481    // ============================================================================
482    // Shadow-mode harness — runs a block through the serial block executor
483    // and the BAL path, asserts byte-equal outputs.
484    // ============================================================================
485
486    /// Shadow harness. Runs the block through both paths; asserts byte-equal outputs.
487    fn assert_shadow_equal(
488        evm_config: EthEvmConfig,
489        canonical_db_template: CacheDB<EmptyDB>,
490        block_header_only: SealedBlock<Block>,
491        txs: Vec<Recovered<TransactionSigned>>,
492    ) {
493        // Serial run: also produces the reference BAL we'll feed to the BAL path.
494        let (mut serial, reference_bal) =
495            run_serial_path(&evm_config, canonical_db_template.clone(), &block_header_only, &txs);
496        // BAL path: stamp the hash of the reference BAL onto the header.
497        let block = empty_amsterdam_block_with_gas_limit(
498            compute_block_access_list_hash(&reference_bal),
499            block_header_only.header().gas_limit(),
500        );
501        let (mut bal_out, built_bal) = run_execute_block_full(
502            &Runtime::test(),
503            evm_config,
504            db_factory(canonical_db_template),
505            to_arc_decoded(reference_bal.clone()),
506            &block,
507            txs,
508        )
509        .unwrap_or_else(|e| panic!("BAL path failed: {e:?}"));
510
511        // Byte-equal assertions. Any divergence surfaces the specific field that broke.
512        assert_eq!(reference_bal, built_bal, "rebuilt BAL diverged");
513        assert_eq!(serial.receipts, bal_out.receipts, "receipts diverged");
514        assert_eq!(serial.gas_used, bal_out.gas_used, "gas used diverged");
515        assert_eq!(serial.requests, bal_out.requests, "requests diverged");
516        serial.state.reverts.sort();
517        bal_out.state.reverts.sort();
518        assert_eq!(serial.state, bal_out.state, "execution state diverged");
519    }
520
521    #[test]
522    fn invalid_bal_bytecode_is_validation_error() {
523        let bal = vec![AccountChanges {
524            address: Address::ZERO,
525            code_changes: vec![CodeChange::new(
526                BlockAccessIndex::new(1),
527                vec![0xef, 0x01, 0xde].into(),
528            )],
529            ..Default::default()
530        }];
531        let error =
532            <BlockExecutorFor<'_, EthEvmConfig> as BlockExecutor>::convert_block_access_list(&bal)
533                .unwrap_err();
534        let error = BalExecutionError::from(BlockAccessListDecodeError::new(error));
535        let error = InsertBlockErrorKind::from(error);
536        assert!(matches!(
537            error.ensure_validation_error(),
538            Ok(InsertBlockValidationError::BlockAccessListDecode(_))
539        ));
540    }
541
542    #[test]
543    fn empty_block_happy_path_round_trip() {
544        // Two-pass end-to-end:
545        //   1. Build the canonical BAL an empty Amsterdam block produces (via
546        //      `reference_bal_for_empty_block`).
547        //   2. Hash it, stamp the header, and run `execute_block` with that BAL. Every check must
548        //      pass.
549        let evm_config = test_evm_config();
550        let input_bal = reference_bal_for_empty_block(&evm_config);
551        // Sanity: reference BAL is non-empty (system calls populated it).
552        assert!(!input_bal.is_empty(), "system calls should populate the BAL");
553        let block = empty_amsterdam_block(compute_block_access_list_hash(&input_bal));
554
555        let output = run_execute_block(
556            &Runtime::test(),
557            evm_config,
558            db_factory(system_contracts_db()),
559            to_arc_decoded(input_bal),
560            &block,
561            Vec::<Recovered<TransactionSigned>>::new(),
562        )
563        .expect("empty BAL block should execute");
564        assert!(output.receipts.is_empty());
565    }
566
567    #[test]
568    fn multi_tx_happy_path_round_trip() {
569        // End-to-end with two value transfers from distinct senders to the same recipient.
570        //
571        // 1. Fund alice and bob in a fresh canonical DB.
572        // 2. Sign tx1 (alice → carol, 100 wei) and tx2 (bob → carol, 200 wei).
573        // 3. Build the reference BAL by running the block through a canonical executor with the BAL
574        //    builder enabled.
575        // 4. Feed that BAL into `execute_block` and assert 2 receipts + no rejections.
576        let evm_config = test_evm_config();
577        let carol = Address::repeat_byte(0xca);
578        let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
579
580        // Generate keypairs + derive sender addresses.
581        let alice_kp = generate_key(&mut rng());
582        let alice =
583            Address::from_raw_public_key(&alice_kp.public_key().serialize_uncompressed()[1..]);
584        let bob_kp = generate_key(&mut rng());
585        let bob = Address::from_raw_public_key(&bob_kp.public_key().serialize_uncompressed()[1..]);
586
587        // Sign txs.
588        let txs = vec![
589            signed_transfer!(alice_kp, alice, carol, 100, 0, 21_000),
590            signed_transfer!(bob_kp, bob, carol, 200, 0, 21_000),
591        ];
592
593        // Pre-block DB: system contracts + funded senders.
594        let mut db = system_contracts_db();
595        insert_funded(&mut db, alice, sender_balance);
596        insert_funded(&mut db, bob, sender_balance);
597
598        // Reference BAL: run the block through a separate canonical executor.
599        let reference_bal = reference_bal_for_block(
600            &evm_config,
601            db.clone(),
602            &empty_amsterdam_block(B256::ZERO),
603            &txs,
604        );
605        let block = empty_amsterdam_block(compute_block_access_list_hash(&reference_bal));
606
607        let output = run_execute_block(
608            &Runtime::test(),
609            evm_config,
610            db_factory(db),
611            to_arc_decoded(reference_bal),
612            &block,
613            txs,
614        )
615        .expect("multi-transaction BAL block should execute");
616        assert_eq!(output.receipts.len(), 2);
617        assert!(output.gas_used >= 42_000);
618    }
619
620    #[test]
621    fn shadow_empty_block() {
622        // System calls only — no txs. Both paths should produce identical system-call
623        // side effects in their execution state (beacon roots storage, history storage, etc.).
624        assert_shadow_equal(
625            test_evm_config(),
626            system_contracts_db(),
627            empty_amsterdam_block(B256::ZERO),
628            Vec::new(),
629        );
630    }
631
632    #[test]
633    fn shadow_multi_value_transfer() {
634        // Two senders → same recipient. Byte-equal across paths means: worker-produced
635        // diffs commit identically to a directly-executed serial path.
636        let evm_config = test_evm_config();
637        let carol = Address::repeat_byte(0xca);
638        let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
639        let alice_kp = generate_key(&mut rng());
640        let alice =
641            Address::from_raw_public_key(&alice_kp.public_key().serialize_uncompressed()[1..]);
642        let bob_kp = generate_key(&mut rng());
643        let bob = Address::from_raw_public_key(&bob_kp.public_key().serialize_uncompressed()[1..]);
644        let mut db = system_contracts_db();
645        insert_funded(&mut db, alice, sender_balance);
646        insert_funded(&mut db, bob, sender_balance);
647        let txs = vec![
648            signed_transfer!(alice_kp, alice, carol, 100, 0, 21_000),
649            signed_transfer!(bob_kp, bob, carol, 200, 0, 21_000),
650        ];
651        assert_shadow_equal(evm_config, db, empty_amsterdam_block(B256::ZERO), txs);
652    }
653
654    #[test]
655    fn rejects_tx_gas_limit_that_exceeds_remaining_block_gas() {
656        // Each worker sees an empty block, so both transactions fit individually. The ordered
657        // commit loop must still reject tx2 because tx1's committed gas leaves too little
658        // block gas for tx2's gas limit.
659        let evm_config = test_evm_config();
660        let carol = Address::repeat_byte(0xca);
661        let sender_balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
662        let alice_kp = generate_key(&mut rng());
663        let alice =
664            Address::from_raw_public_key(&alice_kp.public_key().serialize_uncompressed()[1..]);
665        let bob_kp = generate_key(&mut rng());
666        let bob = Address::from_raw_public_key(&bob_kp.public_key().serialize_uncompressed()[1..]);
667        let txs = vec![
668            signed_transfer!(alice_kp, alice, carol, 100, 0, 990_000),
669            signed_transfer!(bob_kp, bob, carol, 200, 0, 990_000),
670        ];
671        let mut pre_block_db = system_contracts_db();
672        insert_funded(&mut pre_block_db, alice, sender_balance);
673        insert_funded(&mut pre_block_db, bob, sender_balance);
674        // Build the reference BAL under a generous gas limit so both workers can execute.
675        // Replaying the same BAL under the low block gas limit below should reject in the
676        // ordered commit loop before tx2 is committed.
677        let reference_bal = reference_bal_for_block(
678            &evm_config,
679            pre_block_db.clone(),
680            &empty_amsterdam_block(B256::ZERO),
681            &txs,
682        );
683        let block = empty_amsterdam_block_with_gas_limit(
684            compute_block_access_list_hash(&reference_bal),
685            1_000_000,
686        );
687
688        let result = run_execute_block(
689            &Runtime::test(),
690            evm_config,
691            db_factory(pre_block_db),
692            to_arc_decoded(reference_bal),
693            &block,
694            txs,
695        );
696        match result {
697            Err(BalExecutionError::Execution(err)) => assert!(matches!(
698                err.as_validation(),
699                Some(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas { .. })
700            )),
701            other => panic!("expected block gas validation error, got {other:?}"),
702        }
703    }
704
705    #[test]
706    fn shadow_tx_with_revert() {
707        // A tx that reverts in a deployed contract. Both paths must produce identical receipts
708        // (success = false, gas charged, state rolled back except for gas payment + nonce bump).
709        //
710        // Deploys `0x60006000fd` (PUSH1 0 PUSH1 0 REVERT) at `contract`. Sender calls it; the call
711        // reverts; fees + nonce still apply.
712        let evm_config = test_evm_config();
713        let contract = Address::repeat_byte(0xde);
714        let key = generate_key(&mut rng());
715        let signer = Address::from_raw_public_key(&key.public_key().serialize_uncompressed()[1..]);
716        let mut db = system_contracts_db();
717        insert_funded(&mut db, signer, U256::from(alloy_consensus::constants::ETH_TO_WEI));
718        db.insert_account_info(
719            &contract,
720            AccountInfo::default()
721                .with_nonce(1)
722                .with_code(Bytecode::new_raw(Bytes::from_static(&[0x60, 0x00, 0x60, 0x00, 0xfd]))),
723        );
724        let tx = signed_transfer!(key, signer, contract, 0, 0, 50_000);
725        assert_shadow_equal(evm_config, db, empty_amsterdam_block(B256::ZERO), vec![tx]);
726    }
727
728    #[test]
729    fn shadow_tx_with_sstore() {
730        // Tx calls a deployed contract that does `SSTORE(0, 0x42)`. The storage write must
731        // commit identically across serial and BAL paths even though the canonical state applies
732        // a diff produced by a worker EVM.
733        //
734        // Bytecode: PUSH1 0x42, PUSH1 0x00, SSTORE, STOP → `0x60 0x42 0x60 0x00 0x55 0x00`.
735        let evm_config = test_evm_config();
736        let contract = Address::repeat_byte(0x55);
737        let key = generate_key(&mut rng());
738        let signer = Address::from_raw_public_key(&key.public_key().serialize_uncompressed()[1..]);
739        let mut db = system_contracts_db();
740        insert_funded(&mut db, signer, U256::from(alloy_consensus::constants::ETH_TO_WEI));
741        db.insert_account_info(
742            &contract,
743            AccountInfo::default().with_nonce(1).with_code(Bytecode::new_raw(Bytes::from_static(
744                &[0x60, 0x42, 0x60, 0x00, 0x55, 0x00],
745            ))),
746        );
747        let tx = signed_transfer!(key, signer, contract, 0, 0, 100_000);
748        assert_shadow_equal(evm_config, db, empty_amsterdam_block(B256::ZERO), vec![tx]);
749    }
750
751    #[test]
752    fn returns_built_bal_for_final_hash_mismatch() {
753        // Build the BAL an empty block actually produces, then append a phantom address
754        // that execution never touches. The rebuilt BAL omits it, and the outer consensus
755        // validator is responsible for comparing that rebuilt hash to the header commitment.
756
757        let evm_config = test_evm_config();
758
759        // Real BAL the block would produce.
760        let real_bal = reference_bal_for_empty_block(&evm_config);
761
762        // Tamper: append a phantom address not accessed during execution.
763        let mut tampered_entries: Vec<AccountChanges> = real_bal;
764        tampered_entries.push(AccountChanges::new(Address::repeat_byte(0xff)));
765        let tampered_bal = AlloyBal::new(tampered_entries);
766
767        // Stamp the tampered BAL's hash on the block header.
768        let tampered_access_list: BlockAccessList = tampered_bal.clone().into();
769        let tampered_hash = compute_block_access_list_hash(&tampered_access_list);
770        let block = empty_amsterdam_block(tampered_hash);
771        let received = Arc::new(DecodedBal::new(
772            tampered_bal.clone(),
773            alloy_rlp::encode(&tampered_bal).into(),
774        ));
775
776        let (_, built_bal) = run_execute_block_full(
777            &Runtime::test(),
778            evm_config,
779            db_factory(system_contracts_db()),
780            received,
781            &block,
782            Vec::<Recovered<TransactionSigned>>::new(),
783        )
784        .expect("BAL execution should return the rebuilt list");
785        assert_ne!(compute_block_access_list_hash(&built_bal), tampered_hash);
786    }
787
788    #[test]
789    fn canonical_make_db_failure() {
790        // A make_db that always fails must surface as Provider.
791        let block = empty_amsterdam_block(B256::ZERO);
792        let failing_make_db = |_: bool| -> Result<CacheDB<EmptyDB>, BalExecutionError> {
793            Err(reth_provider::ProviderError::BestBlockNotFound.into())
794        };
795        let result = run_execute_block(
796            &Runtime::test(),
797            test_evm_config(),
798            failing_make_db,
799            to_arc_decoded(BlockAccessList::default()),
800            &block,
801            Vec::<Recovered<TransactionSigned>>::new(),
802        );
803        assert!(matches!(result, Err(BalExecutionError::Provider(_))));
804    }
805
806    #[test]
807    fn worker_tx_recovery_error_becomes_validation_error() {
808        // A tx recovery failure fed into the worker channel must surface as
809        // a block validation error. Uses execute_block directly since tx_stream hardcodes
810        // Infallible and cannot inject errors.
811        let evm_config = test_evm_config();
812        let block = empty_amsterdam_block(B256::ZERO);
813        let (tx_tx, tx_rx) = crossbeam_channel::unbounded::<(
814            usize,
815            Result<Recovered<TransactionSigned>, std::io::Error>,
816        )>();
817        tx_tx.send((0, Err(std::io::Error::other("sig fail")))).unwrap();
818        drop(tx_tx);
819        let (receipt_tx, _receipt_rx) = crossbeam_channel::unbounded();
820        let evm_env = evm_config.evm_env(block.header()).unwrap();
821        let execution_ctx = evm_config.context_for_block(&block).unwrap();
822
823        let result = execute_block(
824            &Runtime::test(),
825            &evm_config,
826            &db_factory(system_contracts_db()),
827            to_arc_decoded(BlockAccessList::default()),
828            evm_env,
829            execution_ctx,
830            1, // transaction_count = 1 → exactly one worker spawned
831            tx_rx,
832            receipt_tx,
833        );
834        assert!(matches!(
835            result,
836            Err(BalExecutionError::Execution(reth_evm::BlockExecutionError::Validation(_)))
837        ));
838    }
839
840    #[test]
841    fn gas_tracker_non_amsterdam_uses_cumulative_gas() {
842        // tx2's gas limit fits the block gas limit on its own but not the gas left after tx1, so
843        // non-Amsterdam admission, which budgets by cumulative gas, must reject it.
844        let evm_config = non_amsterdam_evm_config();
845        let recipient = Address::repeat_byte(0xca);
846        let balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
847        let alice_key = generate_key(&mut rng());
848        let alice =
849            Address::from_raw_public_key(&alice_key.public_key().serialize_uncompressed()[1..]);
850        let bob_key = generate_key(&mut rng());
851        let bob = Address::from_raw_public_key(&bob_key.public_key().serialize_uncompressed()[1..]);
852        let txs = vec![
853            signed_transfer!(alice_key, alice, recipient, 1, 0, 21_000),
854            signed_transfer!(bob_key, bob, recipient, 1, 0, 980_000),
855        ];
856        let mut db = system_contracts_db();
857        insert_funded(&mut db, alice, balance);
858        insert_funded(&mut db, bob, balance);
859        let reference_bal = reference_bal_for_block(
860            &evm_config,
861            db.clone(),
862            &empty_amsterdam_block(B256::ZERO),
863            &txs,
864        );
865        let block = empty_amsterdam_block_with_gas_limit(
866            compute_block_access_list_hash(&reference_bal),
867            1_000_000,
868        );
869
870        let result = run_execute_block(
871            &Runtime::test(),
872            evm_config,
873            db_factory(db),
874            to_arc_decoded(reference_bal),
875            &block,
876            txs,
877        );
878        assert!(matches!(result, Err(BalExecutionError::Execution(_))));
879    }
880
881    #[test]
882    fn gas_tracker_caps_oversized_tx_gas_limit_at_tx_gas_limit_cap() {
883        // A tx with gas_limit above the tx gas limit cap (EIP-7825) is admitted when the
884        // capped value fits in the remaining block gas.
885        const TX_GAS_LIMIT_CAP: u64 = 1 << 24;
886
887        let evm_config = test_evm_config();
888        let loop_contract = Address::repeat_byte(0x44);
889        let recipient = Address::repeat_byte(0xca);
890        let key = generate_key(&mut rng());
891        let signer = Address::from_raw_public_key(&key.public_key().serialize_uncompressed()[1..]);
892        let mut db = system_contracts_db();
893        insert_funded(&mut db, signer, U256::from(alloy_consensus::constants::ETH_TO_WEI));
894        db.insert_account_info(
895            &loop_contract,
896            AccountInfo::default()
897                .with_nonce(1)
898                .with_code(Bytecode::new_raw(Bytes::from_static(&[0x5b, 0x60, 0x00, 0x56]))),
899        );
900        let txs = vec![
901            signed_transfer!(key, signer, loop_contract, 0, 0, 13_100_000),
902            signed_transfer!(key, signer, recipient, 1, 1, TX_GAS_LIMIT_CAP + 1_000_000),
903        ];
904        let reference_block = empty_amsterdam_block(B256::ZERO);
905        let reference_bal =
906            reference_bal_for_block(&evm_config, db.clone(), &reference_block, &txs);
907        let block = empty_amsterdam_block(compute_block_access_list_hash(&reference_bal));
908
909        let output = run_execute_block(
910            &Runtime::test(),
911            evm_config,
912            db_factory(db),
913            to_arc_decoded(reference_bal),
914            &block,
915            txs,
916        )
917        .expect("the capped regular gas limit should fit the remaining block gas");
918        assert_eq!(output.receipts.len(), 2);
919    }
920
921    /// Two funded senders each transferring to the same recipient, which is funded only if
922    /// `fund_recipient` is set.
923    fn two_transfers(
924        tx1_gas_limit: u64,
925        tx2_gas_limit: u64,
926        fund_recipient: bool,
927    ) -> (CacheDB<EmptyDB>, Recovered<TransactionSigned>, Recovered<TransactionSigned>) {
928        let recipient = Address::repeat_byte(0xca);
929        let balance = U256::from(alloy_consensus::constants::ETH_TO_WEI);
930
931        let alice_kp = generate_key(&mut rng());
932        let alice =
933            Address::from_raw_public_key(&alice_kp.public_key().serialize_uncompressed()[1..]);
934        let bob_kp = generate_key(&mut rng());
935        let bob = Address::from_raw_public_key(&bob_kp.public_key().serialize_uncompressed()[1..]);
936
937        let mut pre_block_db = system_contracts_db();
938        insert_funded(&mut pre_block_db, alice, balance);
939        insert_funded(&mut pre_block_db, bob, balance);
940        if fund_recipient {
941            insert_funded(&mut pre_block_db, recipient, balance);
942        }
943
944        let chain_id = MAINNET.chain.id();
945        let make_tx = |kp, gas_limit, value| {
946            sign_tx_with_key_pair(
947                kp,
948                Transaction::Legacy(TxLegacy {
949                    chain_id: Some(chain_id),
950                    nonce: 0,
951                    gas_price: 1,
952                    gas_limit,
953                    to: alloy_primitives::TxKind::Call(recipient),
954                    value: U256::from(value),
955                    input: Default::default(),
956                }),
957            )
958        };
959        let tx1 = Recovered::new_unchecked(make_tx(alice_kp, tx1_gas_limit, 100u64), alice);
960        let tx2 = Recovered::new_unchecked(make_tx(bob_kp, tx2_gas_limit, 200u64), bob);
961        (pre_block_db, tx1, tx2)
962    }
963
964    /// [`two_transfers`] to a fresh recipient, plus the reference BAL of a block containing only
965    /// the covered subset of those transfers.
966    fn two_transfers_with_reference_bal(
967        evm_config: &EthEvmConfig,
968        tx_gas_limit: u64,
969        bal_covers_first: bool,
970    ) -> (
971        CacheDB<EmptyDB>,
972        BlockAccessList,
973        Recovered<reth_ethereum_primitives::TransactionSigned>,
974        Recovered<reth_ethereum_primitives::TransactionSigned>,
975    ) {
976        let (pre_block_db, tx1, tx2) = two_transfers(tx_gas_limit, tx_gas_limit, false);
977        let reference_block = empty_amsterdam_block(B256::ZERO);
978        let covered = if bal_covers_first { vec![tx1.clone()] } else { vec![] };
979        let reference_bal =
980            reference_bal_for_block(evm_config, pre_block_db.clone(), &reference_block, &covered);
981        (pre_block_db, reference_bal, tx1, tx2)
982    }
983
984    #[test]
985    fn default_tx_gas_limit_cap_matches_serial_execution() {
986        let evm_config = test_evm_config();
987        let block_gas_limit = 20_000_000;
988        // The uncapped second limit exceeds the remaining regular budget. EIP-7825's
989        // default cap makes its regular part fit; the funded recipient consumes no state gas.
990        let (db, tx1, tx2) = two_transfers(100_000, 19_990_000, true);
991        let txs = vec![tx1, tx2];
992        let (serial, bal) = run_serial_path(
993            &evm_config,
994            db.clone(),
995            &empty_amsterdam_block_with_gas_limit(B256::ZERO, block_gas_limit),
996            &txs,
997        );
998        let block = empty_amsterdam_block_with_gas_limit(
999            compute_block_access_list_hash(&bal),
1000            block_gas_limit,
1001        );
1002        let parallel = run_execute_block(
1003            &Runtime::test(),
1004            evm_config,
1005            db_factory(db),
1006            to_arc_decoded(bal),
1007            &block,
1008            txs,
1009        )
1010        .expect("the default regular cap must match serial execution");
1011        assert_eq!(parallel.receipts, serial.receipts);
1012    }
1013
1014    #[test]
1015    fn propagates_bal_miss_of_admitted_transaction_as_block_verdict() {
1016        // The BAL covers neither transaction: the first slot's speculative failure is
1017        // adjudicated in block order and becomes the block's verdict.
1018        let (pre_block_db, reference_bal, tx1, tx2) =
1019            two_transfers_with_reference_bal(&test_evm_config(), 100_000, false);
1020        let bal_hash = alloy_eip7928::compute_block_access_list_hash(&reference_bal);
1021        let block = empty_amsterdam_block(bal_hash);
1022
1023        let result = run_execute_block(
1024            &Runtime::test(),
1025            test_evm_config(),
1026            db_factory(pre_block_db),
1027            to_arc_decoded(reference_bal),
1028            &block,
1029            vec![tx1, tx2],
1030        );
1031
1032        match result {
1033            Err(BalExecutionError::Execution(err)) => {
1034                let Some(BlockValidationError::EVM { error, .. }) = err.as_validation() else {
1035                    panic!("expected EVM validation error, got {err:?}");
1036                };
1037                let Some(evm2::registry::HandlerError::Database(database_error)) =
1038                    error.downcast_ref::<evm2::registry::HandlerError>()
1039                else {
1040                    panic!("expected owned database error, got {error:?}");
1041                };
1042                assert!(!database_error.is_fatal());
1043                assert!(database_error.downcast_ref::<evm2::evm::bal::BalError>().is_some());
1044            }
1045            Err(err) => panic!("expected BAL execution error, got {err:?}"),
1046            Ok(_) => panic!("expected BAL execution error, got Ok"),
1047        }
1048    }
1049
1050    #[test]
1051    fn admission_rejects_before_speculative_worker_failure_surfaces() {
1052        // tx2 exceeds the remaining block gas AND misses the BAL; block-gas admission is
1053        // adjudicated first, so the verdict is the gas error, not tx2's BAL failure.
1054        let (pre_block_db, reference_bal, tx1, tx2) =
1055            two_transfers_with_reference_bal(&test_evm_config(), 990_000, true);
1056        let bal_hash = alloy_eip7928::compute_block_access_list_hash(&reference_bal);
1057        let low_gas_block = empty_amsterdam_block_with_gas_limit(bal_hash, 1_000_000);
1058
1059        let result = run_execute_block(
1060            &Runtime::test(),
1061            test_evm_config(),
1062            db_factory(pre_block_db),
1063            to_arc_decoded(reference_bal),
1064            &low_gas_block,
1065            vec![tx1, tx2],
1066        );
1067
1068        match result {
1069            Err(BalExecutionError::Execution(err)) => assert!(
1070                matches!(
1071                    err.as_validation(),
1072                    Some(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas { .. })
1073                ),
1074                "expected block gas validation error, got {err:?}"
1075            ),
1076            Err(err) => panic!("expected block gas validation error, got {err:?}"),
1077            Ok(_) => panic!("expected block gas validation error, got Ok"),
1078        }
1079    }
1080
1081    #[test]
1082    fn worker_reuses_provider_after_failure_and_finishes_earlier_transactions() {
1083        let evm_config = test_evm_config();
1084        let (pre_block_db, reference_bal, tx1, tx2) =
1085            two_transfers_with_reference_bal(&evm_config, 100_000, true);
1086        let block = empty_amsterdam_block(B256::ZERO);
1087        let received_bal = Arc::new(
1088            <BlockExecutorFor<'_, EthEvmConfig> as BlockExecutor>::convert_block_access_list(
1089                &reference_bal,
1090            )
1091            .unwrap(),
1092        );
1093        let setups = AtomicUsize::new(0);
1094        let make_db = |_: bool| {
1095            // Reopening the provider after a speculative error must not introduce an
1096            // unindexed setup failure that overtakes the remaining transaction slots.
1097            if setups.fetch_add(1, Ordering::Relaxed) != 0 {
1098                return Err(BalExecutionError::Provider(
1099                    reth_errors::ProviderError::HeaderNotFound(B256::ZERO.into()),
1100                ));
1101            }
1102            Ok(pre_block_db.clone())
1103        };
1104        let (tx_tx, tx_rx) = crossbeam_channel::unbounded();
1105        // Recovery can finish out of order. A single worker must continue after both kinds
1106        // of error to execute slot 0, without reusing the failed slot 1 executor.
1107        tx_tx.send((2, Err(std::io::Error::other("sig fail")))).unwrap();
1108        tx_tx.send((1, Ok(tx2))).unwrap();
1109        tx_tx.send((0, Ok(tx1.clone()))).unwrap();
1110        drop(tx_tx);
1111        let (_abort_guard, abort_rx) = AbortGuard::new();
1112        let (result_tx, result_rx) = crossbeam_channel::unbounded();
1113        let runtime = Runtime::test();
1114        runtime.bal_streaming_pool().in_place_scope(|scope| {
1115            worker::spawn_worker(
1116                scope,
1117                tx_rx,
1118                abort_rx,
1119                result_tx,
1120                &evm_config,
1121                &make_db,
1122                received_bal,
1123                evm_config.evm_env(block.header()).unwrap(),
1124                evm_config.context_for_block(&block).unwrap(),
1125            );
1126        });
1127        let mut outputs = ordered_worker_outputs(&result_rx, 3);
1128        let output = outputs.next().unwrap().unwrap();
1129        assert_eq!(output.index, 0);
1130        assert_eq!(output.signer, tx1.signer());
1131        let mut serial = evm_config.executor_for_block(pre_block_db, &block).unwrap();
1132        serial.apply_pre_execution_changes().unwrap();
1133        let expected = serial.execute_transaction_without_commit(tx1).unwrap();
1134        assert_eq!(output.result.result(), expected.result());
1135        assert!(matches!(
1136            outputs.next().unwrap(),
1137            Err(OrderedWorkerOutputError::Worker(worker::BalWorkerError::Execution {
1138                tx_index: 1,
1139                ..
1140            }))
1141        ));
1142        let error = outputs.next().unwrap().err().unwrap();
1143        assert!(matches!(
1144            error,
1145            OrderedWorkerOutputError::Worker(worker::BalWorkerError::Transaction {
1146                tx_index: 2,
1147                ..
1148            })
1149        ));
1150        assert!(matches!(
1151            BalExecutionError::from(error),
1152            BalExecutionError::Execution(reth_evm::BlockExecutionError::Validation(_))
1153        ));
1154        assert!(outputs.next().is_none());
1155        assert_eq!(setups.load(Ordering::Relaxed), 1);
1156    }
1157
1158    #[test]
1159    fn amsterdam_state_gas_admission_matches_serial_execution() {
1160        let evm_config = EthEvmConfig::new(Arc::new(
1161            reth_chainspec::ChainSpecBuilder::mainnet().amsterdam_activated().build(),
1162        ));
1163        for (tx_gas_limit, complete_bal) in [(500_000, true), (990_000, true), (990_000, false)] {
1164            let (pre_block_db, partial_bal, tx1, tx2) =
1165                two_transfers_with_reference_bal(&evm_config, tx_gas_limit, true);
1166            let reference_block = empty_amsterdam_block(B256::ZERO);
1167            let reference_bal = if complete_bal {
1168                reference_bal_for_block(
1169                    &evm_config,
1170                    pre_block_db.clone(),
1171                    &reference_block,
1172                    &[tx1.clone(), tx2.clone()],
1173                )
1174            } else {
1175                // State-gas admission must also precede a speculative BAL miss at this slot.
1176                partial_bal
1177            };
1178            let block = empty_amsterdam_block_with_gas_limit(
1179                compute_block_access_list_hash(&reference_bal),
1180                1_000_000,
1181            );
1182            let mut evm_env = evm_config.evm_env(block.header()).unwrap();
1183            assert!(evm_env.version.feature(evm2::EvmFeatures::EIP8037));
1184            // Both capped regular limits fit. The full limit of the second transaction
1185            // must still fit the state budget consumed by the first account creation.
1186            evm_env.version.tx_gas_limit_cap = 400_000;
1187            let ctx = evm_config.context_for_block(&block).unwrap();
1188            let evm = evm_config.evm_with_env(pre_block_db.clone(), evm_env.clone());
1189            let mut serial = evm_config.block_executor_factory().create_executor(evm, ctx.clone());
1190            serial.apply_pre_execution_changes().unwrap();
1191            serial.execute_transaction(tx1.clone()).unwrap();
1192            let serial_result = serial.execute_transaction(tx2.clone());
1193
1194            let (receipt_tx, _receipt_rx) = crossbeam_channel::unbounded();
1195            let parallel_result = execute_block(
1196                &Runtime::test(),
1197                &evm_config,
1198                &|_: bool| Ok(pre_block_db.clone()),
1199                to_arc_decoded(reference_bal),
1200                evm_env,
1201                ctx,
1202                2,
1203                tx_stream(vec![tx1, tx2]),
1204                receipt_tx,
1205            );
1206            if tx_gas_limit == 500_000 {
1207                serial_result.unwrap();
1208                assert_eq!(parallel_result.unwrap().0.receipts, serial.receipts());
1209            } else {
1210                let serial_error = serial_result.unwrap_err();
1211                let Some(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
1212                    block_available_gas,
1213                    ..
1214                }) = serial_error.as_validation()
1215                else {
1216                    panic!("expected serial state-gas admission error, got {serial_error:?}");
1217                };
1218                assert!(*block_available_gas < 990_000);
1219                let Err(BalExecutionError::Execution(parallel_error)) = parallel_result else {
1220                    panic!("expected parallel state-gas admission error");
1221                };
1222                assert_eq!(parallel_error.to_string(), serial_error.to_string());
1223            }
1224        }
1225    }
1226}