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