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