Skip to main content

anvil/eth/backend/
executor.rs

1use crate::{
2    eth::{
3        backend::cheats::CheatsManager, error::InvalidTransactionError,
4        pool::transactions::PoolTransaction,
5    },
6    mem::inspector::{AnvilInspector, InspectorTxConfig},
7};
8use alloy_consensus::{
9    Eip658Value, Receipt, ReceiptWithBloom, Transaction, TransactionEnvelope, TxReceipt,
10    transaction::{Either, Recovered},
11};
12use alloy_eips::{
13    Encodable2718, eip2935, eip4788,
14    eip6110::DEPOSIT_REQUEST_TYPE,
15    eip7685::Requests,
16    eip7702::{RecoveredAuthority, RecoveredAuthorization},
17};
18use alloy_evm::{
19    Evm, FromRecoveredTx, FromTxWithEncoded, RecoveredTx,
20    block::{
21        BlockExecutionError, BlockExecutionResult, BlockExecutor, BlockValidationError,
22        ExecutableTx, GasOutput, StateDB, SystemCaller, TxResult,
23    },
24    eth::{
25        EthTxResult,
26        eip6110::parse_deposits_from_receipts,
27        receipt_builder::{ReceiptBuilder, ReceiptBuilderCtx},
28        spec::EthExecutorSpec,
29    },
30};
31use alloy_hardforks::{EthereumHardfork, EthereumHardforks, ForkCondition};
32use alloy_primitives::{Address, B256, Bytes, Log, U256};
33use anvil_core::eth::transaction::{
34    MaybeImpersonatedTransaction, PendingTransaction, TransactionInfo,
35};
36use foundry_evm::core::{env::FoundryTransaction, evm::IntoInstructionResult};
37use foundry_primitives::{FoundryReceiptEnvelope, FoundryTxEnvelope, FoundryTxType};
38use revm::{
39    Database, DatabaseCommit,
40    context::Block as RevmBlock,
41    context_interface::result::{ExecutionResult, Output, ResultAndState},
42    interpreter::InstructionResult,
43    primitives::hardfork::SpecId,
44    state::{AccountInfo, EvmState},
45};
46use std::{fmt, fmt::Debug, mem::take, sync::Arc};
47
48#[cfg(feature = "optimism")]
49pub(crate) mod optimism;
50
51/// Determines whether an executor produces a complete block or a historical transaction prefix.
52#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
53pub(crate) enum BlockExecutionKind {
54    /// Apply all pre- and post-block transitions.
55    #[default]
56    Complete,
57    /// Apply block-start transitions, but do not drain post-block request queues.
58    TransactionPrefix,
59}
60
61/// Ethereum-only consensus transition configuration for an Anvil block executor.
62#[derive(Clone, Copy, Debug)]
63pub(crate) struct EthereumBlockTransitions {
64    pub(crate) hardfork: EthereumHardfork,
65    pub(crate) deposit_contract_address: Address,
66    pub(crate) parent_beacon_block_root: Option<B256>,
67    pub(crate) execution_kind: BlockExecutionKind,
68}
69
70/// A hardfork specification whose configured Ethereum fork is active from genesis.
71#[derive(Clone, Copy, Debug)]
72struct ActiveEthereumSpec {
73    hardfork: EthereumHardfork,
74    deposit_contract_address: Address,
75}
76
77impl EthereumHardforks for ActiveEthereumSpec {
78    fn ethereum_fork_activation(&self, fork: EthereumHardfork) -> ForkCondition {
79        if fork <= self.hardfork { ForkCondition::ZERO_TIMESTAMP } else { ForkCondition::Never }
80    }
81}
82
83impl EthExecutorSpec for ActiveEthereumSpec {
84    fn deposit_contract_address(&self) -> Option<Address> {
85        Some(self.deposit_contract_address)
86    }
87}
88
89/// Applies canonical Ethereum block-start transitions in consensus order.
90pub(crate) fn apply_ethereum_pre_execution_changes<E>(
91    evm: &mut E,
92    parent_hash: B256,
93    transitions: EthereumBlockTransitions,
94) -> Result<(), BlockExecutionError>
95where
96    E: Evm<DB: DatabaseCommit>,
97{
98    let mut caller = SystemCaller::new(ActiveEthereumSpec {
99        hardfork: transitions.hardfork,
100        deposit_contract_address: transitions.deposit_contract_address,
101    });
102    caller.apply_blockhashes_contract_call(parent_hash, evm)?;
103    caller.apply_beacon_root_contract_call(transitions.parent_beacon_block_root, evm)
104}
105
106/// Collects deposits before draining the withdrawal and consolidation request queues.
107pub(crate) fn apply_ethereum_post_execution_changes<E>(
108    evm: &mut E,
109    transitions: EthereumBlockTransitions,
110    receipts: &[FoundryReceiptEnvelope],
111) -> Result<Requests, BlockExecutionError>
112where
113    E: Evm<DB: DatabaseCommit>,
114{
115    if transitions.hardfork < EthereumHardfork::Prague {
116        return Ok(Requests::default());
117    }
118
119    let spec = ActiveEthereumSpec {
120        hardfork: transitions.hardfork,
121        deposit_contract_address: transitions.deposit_contract_address,
122    };
123    let mut requests = Requests::default();
124    append_deposit_requests(spec, receipts, &mut requests)?;
125    SystemCaller::new(spec).append_post_execution_changes(evm, &mut requests)?;
126    Ok(requests)
127}
128
129fn append_deposit_requests(
130    spec: ActiveEthereumSpec,
131    receipts: &[FoundryReceiptEnvelope],
132    requests: &mut Requests,
133) -> Result<(), BlockExecutionError> {
134    let deposits = parse_deposits_from_receipts(spec, receipts)?;
135    if !deposits.is_empty() {
136        requests.push_request_with_type(DEPOSIT_REQUEST_TYPE, deposits);
137    }
138    Ok(())
139}
140
141/// Receipt builder for transaction types that do not require network-specific metadata.
142#[derive(Debug, Default, Clone, Copy)]
143#[non_exhaustive]
144pub struct FoundryReceiptBuilder;
145
146impl FoundryReceiptBuilder {
147    const fn wrap_receipt(
148        tx_type: FoundryTxType,
149        receipt: ReceiptWithBloom<Receipt>,
150    ) -> FoundryReceiptEnvelope {
151        match tx_type {
152            FoundryTxType::Legacy => FoundryReceiptEnvelope::Legacy(receipt),
153            FoundryTxType::Eip2930 => FoundryReceiptEnvelope::Eip2930(receipt),
154            FoundryTxType::Eip1559 => FoundryReceiptEnvelope::Eip1559(receipt),
155            FoundryTxType::Eip4844 => FoundryReceiptEnvelope::Eip4844(receipt),
156            FoundryTxType::Eip7702 => FoundryReceiptEnvelope::Eip7702(receipt),
157            #[cfg(feature = "optimism")]
158            FoundryTxType::Deposit => {
159                panic!("deposit receipts require fork-specific metadata")
160            }
161            #[cfg(feature = "optimism")]
162            FoundryTxType::PostExec => FoundryReceiptEnvelope::PostExec(receipt),
163            FoundryTxType::Tempo => FoundryReceiptEnvelope::Tempo(receipt),
164        }
165    }
166
167    /// Builds a typed receipt for an RPC-simulated transaction.
168    pub(crate) fn build_simulated_receipt(
169        tx_type: FoundryTxType,
170        result: &ExecutionResult,
171        logs: Vec<Log>,
172        cumulative_gas_used: u64,
173    ) -> FoundryReceiptEnvelope {
174        let receipt =
175            Receipt { status: Eip658Value::Eip658(result.is_success()), cumulative_gas_used, logs }
176                .with_bloom();
177        Self::wrap_receipt(tx_type, receipt)
178    }
179}
180
181impl ReceiptBuilder for FoundryReceiptBuilder {
182    type Transaction = FoundryTxEnvelope;
183    type Receipt = FoundryReceiptEnvelope;
184
185    fn build_receipt<E: Evm>(
186        &self,
187        ctx: ReceiptBuilderCtx<'_, FoundryTxType, E>,
188    ) -> FoundryReceiptEnvelope {
189        let receipt = Receipt {
190            status: Eip658Value::Eip658(ctx.result.is_success()),
191            cumulative_gas_used: ctx.cumulative_gas_used,
192            logs: ctx.result.into_logs(),
193        }
194        .with_bloom();
195        Self::wrap_receipt(ctx.tx_type, receipt)
196    }
197}
198
199/// Result of executing a transaction in [`AnvilBlockExecutor`].
200///
201/// Wraps [`EthTxResult`] with the sender address, needed for deposit nonce resolution.
202#[derive(Debug)]
203pub struct AnvilTxResult<H> {
204    pub inner: EthTxResult<H, FoundryTxType>,
205    pub sender: Address,
206}
207
208impl<H: Send + 'static> TxResult for AnvilTxResult<H> {
209    type HaltReason = H;
210
211    fn result(&self) -> &ResultAndState<Self::HaltReason> {
212        self.inner.result()
213    }
214
215    fn into_result(self) -> ResultAndState<Self::HaltReason> {
216        self.inner.into_result()
217    }
218}
219
220/// Block executor for Anvil that implements [`BlockExecutor`].
221///
222/// Wraps an EVM instance and produces [`FoundryReceiptEnvelope`] receipts.
223/// Validation (gas limits, blob gas, transaction validity) is handled by the
224/// caller before transactions are fed to this executor.
225pub struct AnvilBlockExecutor<E> {
226    /// The EVM instance used for execution.
227    evm: E,
228    /// Parent block hash — needed for EIP-2935 system call.
229    parent_hash: B256,
230    /// The active spec id, used to gate hardfork-specific behavior.
231    spec_id: SpecId,
232    /// Canonical Ethereum consensus transitions, disabled for other networks.
233    ethereum_transitions: Option<EthereumBlockTransitions>,
234    /// Receipt builder.
235    receipt_builder: FoundryReceiptBuilder,
236    /// Receipts of executed transactions.
237    receipts: Vec<FoundryReceiptEnvelope>,
238    /// Total gas used by transactions in this block.
239    gas_used: u64,
240    /// Blob gas used by the block.
241    blob_gas_used: u64,
242    /// Maximum blob gas available to transactions in this block.
243    max_blob_gas_per_block: u64,
244    /// Whether OP Jovian repurposes `blobGasUsed` for the DA footprint.
245    optimism_jovian: bool,
246    /// State changes captured for deferred publication.
247    state_changes: Option<Vec<EvmState>>,
248}
249
250impl<E: fmt::Debug> fmt::Debug for AnvilBlockExecutor<E> {
251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252        let mut debug = f.debug_struct("AnvilBlockExecutor");
253        debug
254            .field("evm", &self.evm)
255            .field("parent_hash", &self.parent_hash)
256            .field("spec_id", &self.spec_id)
257            .field("ethereum_transitions", &self.ethereum_transitions)
258            .field("gas_used", &self.gas_used)
259            .field("blob_gas_used", &self.blob_gas_used)
260            .field("max_blob_gas_per_block", &self.max_blob_gas_per_block)
261            .field("optimism_jovian", &self.optimism_jovian);
262        debug.field("receipts", &self.receipts.len()).finish_non_exhaustive()
263    }
264}
265
266impl<E> AnvilBlockExecutor<E> {
267    /// Creates a new [`AnvilBlockExecutor`].
268    pub(crate) const fn new(
269        evm: E,
270        parent_hash: B256,
271        spec_id: SpecId,
272        ethereum_transitions: Option<EthereumBlockTransitions>,
273    ) -> Self {
274        Self {
275            evm,
276            parent_hash,
277            spec_id,
278            ethereum_transitions,
279            receipt_builder: FoundryReceiptBuilder,
280            receipts: Vec::new(),
281            gas_used: 0,
282            blob_gas_used: 0,
283            max_blob_gas_per_block: u64::MAX,
284            optimism_jovian: false,
285            state_changes: None,
286        }
287    }
288
289    /// Captures every committed changeset so callers can publish them after block execution.
290    pub(crate) fn with_state_changes(mut self) -> Self {
291        self.state_changes = Some(Vec::new());
292        self
293    }
294
295    /// Applies the active block blob gas limit.
296    pub(crate) const fn with_max_blob_gas_per_block(mut self, limit: u64) -> Self {
297        self.max_blob_gas_per_block = limit;
298        self
299    }
300
301    /// Takes the captured changesets.
302    pub(crate) fn take_state_changes(&mut self) -> Vec<EvmState> {
303        self.state_changes.take().unwrap_or_default()
304    }
305}
306
307impl<E> AnvilBlockExecutor<E>
308where
309    E: Evm<
310            DB: StateDB,
311            Tx: FromRecoveredTx<FoundryTxEnvelope> + FromTxWithEncoded<FoundryTxEnvelope>,
312        >,
313{
314    /// Executes a transaction without committing it, using the supplied network-specific
315    /// transaction entry point.
316    pub(crate) fn execute_transaction_without_commit_with<T, F>(
317        &mut self,
318        tx: T,
319        transact: F,
320    ) -> Result<AnvilTxResult<E::HaltReason>, BlockExecutionError>
321    where
322        T: ExecutableTx<Self>,
323        F: FnOnce(
324            &mut E,
325            E::Tx,
326            B256,
327        ) -> Result<ResultAndState<E::HaltReason>, BlockExecutionError>,
328    {
329        let (tx_env, tx) = tx.into_parts();
330
331        let block_available_gas = self.evm.block().gas_limit() - self.gas_used;
332        if tx.tx().gas_limit() > block_available_gas {
333            return Err(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
334                transaction_gas_limit: tx.tx().gas_limit(),
335                block_available_gas,
336            }
337            .into());
338        }
339
340        let sender = *tx.signer();
341        let transaction_hash = tx.tx().trie_hash();
342        #[cfg(feature = "optimism")]
343        let blob_gas_used =
344            optimism::blob_gas_used(self.evm.db_mut(), tx.tx(), self.optimism_jovian)?;
345        #[cfg(not(feature = "optimism"))]
346        let blob_gas_used = tx.tx().blob_gas_used().unwrap_or_default();
347        let blob_gas_limit = block_blob_gas_limit(
348            self.optimism_jovian,
349            self.evm.block().gas_limit(),
350            self.max_blob_gas_per_block,
351        );
352        if self.blob_gas_used.saturating_add(blob_gas_used) > blob_gas_limit {
353            return Err(BlockExecutionError::msg("block blob gas limit exceeded"));
354        }
355        let result = transact(&mut self.evm, tx_env, transaction_hash)?;
356
357        Ok(AnvilTxResult {
358            inner: EthTxResult { result, blob_gas_used, tx_type: tx.tx().tx_type() },
359            sender,
360        })
361    }
362}
363
364impl<E> BlockExecutor for AnvilBlockExecutor<E>
365where
366    E: Evm<
367            DB: StateDB,
368            Tx: FromRecoveredTx<FoundryTxEnvelope> + FromTxWithEncoded<FoundryTxEnvelope>,
369        >,
370{
371    type Transaction = FoundryTxEnvelope;
372    type Receipt = FoundryReceiptEnvelope;
373    type Evm = E;
374    type Result = AnvilTxResult<E::HaltReason>;
375
376    fn apply_pre_execution_changes(&mut self) -> Result<(), BlockExecutionError> {
377        if let Some(transitions) = self.ethereum_transitions {
378            // Historical fork-prefix publication needs the individual changesets after executing
379            // against its disposable overlay. Preserve canonical ordering while capturing them.
380            if let Some(state_changes) = &mut self.state_changes {
381                if transitions.hardfork >= EthereumHardfork::Prague {
382                    let result = self
383                        .evm
384                        .transact_system_call(
385                            eip4788::SYSTEM_ADDRESS,
386                            eip2935::HISTORY_STORAGE_ADDRESS,
387                            Bytes::copy_from_slice(self.parent_hash.as_slice()),
388                        )
389                        .map_err(BlockExecutionError::other)?;
390                    state_changes.push(result.state.clone());
391                    self.evm.db_mut().commit(result.state);
392                }
393                if transitions.hardfork >= EthereumHardfork::Cancun {
394                    let parent_beacon_block_root = transitions
395                        .parent_beacon_block_root
396                        .ok_or(BlockValidationError::MissingParentBeaconBlockRoot)?;
397                    let result = self
398                        .evm
399                        .transact_system_call(
400                            eip4788::SYSTEM_ADDRESS,
401                            eip4788::BEACON_ROOTS_ADDRESS,
402                            Bytes::copy_from_slice(parent_beacon_block_root.as_slice()),
403                        )
404                        .map_err(BlockExecutionError::other)?;
405                    state_changes.push(result.state.clone());
406                    self.evm.db_mut().commit(result.state);
407                }
408                return Ok(());
409            }
410            apply_ethereum_pre_execution_changes(&mut self.evm, self.parent_hash, transitions)?;
411        }
412        Ok(())
413    }
414
415    fn execute_transaction_without_commit(
416        &mut self,
417        tx: impl ExecutableTx<Self>,
418    ) -> Result<Self::Result, BlockExecutionError> {
419        self.execute_transaction_without_commit_with(tx, |evm, tx_env, transaction_hash| {
420            evm.transact(tx_env).map_err(|err| BlockExecutionError::evm(err, transaction_hash))
421        })
422    }
423
424    fn commit_transaction(&mut self, output: Self::Result) -> GasOutput {
425        let AnvilTxResult {
426            inner: EthTxResult { result: ResultAndState { result, state }, blob_gas_used, tx_type },
427            #[cfg_attr(not(feature = "optimism"), allow(unused_variables))]
428            sender,
429        } = output;
430
431        let gas_used = result.tx_gas_used();
432        self.gas_used += gas_used;
433
434        if self.spec_id >= SpecId::CANCUN {
435            self.blob_gas_used = self.blob_gas_used.saturating_add(blob_gas_used);
436        }
437
438        #[cfg(feature = "optimism")]
439        let receipt = if tx_type == FoundryTxType::Deposit {
440            optimism::build_mined_deposit_receipt(result, &state, sender, self.gas_used)
441        } else {
442            self.receipt_builder.build_receipt(ReceiptBuilderCtx {
443                tx_type,
444                evm: &self.evm,
445                result,
446                state: &state,
447                cumulative_gas_used: self.gas_used,
448            })
449        };
450        #[cfg(not(feature = "optimism"))]
451        let receipt = self.receipt_builder.build_receipt(ReceiptBuilderCtx {
452            tx_type,
453            evm: &self.evm,
454            result,
455            state: &state,
456            cumulative_gas_used: self.gas_used,
457        });
458
459        if let Some(state_changes) = &mut self.state_changes {
460            state_changes.push(state.clone());
461        }
462        self.receipts.push(receipt);
463        self.evm.db_mut().commit(state);
464
465        GasOutput::new(gas_used)
466    }
467
468    fn finish(
469        mut self,
470    ) -> Result<(Self::Evm, BlockExecutionResult<FoundryReceiptEnvelope>), BlockExecutionError>
471    {
472        let requests = match self.ethereum_transitions {
473            Some(transitions) if transitions.execution_kind == BlockExecutionKind::Complete => {
474                apply_ethereum_post_execution_changes(&mut self.evm, transitions, &self.receipts)?
475            }
476            _ => Requests::default(),
477        };
478        Ok((
479            self.evm,
480            BlockExecutionResult {
481                receipts: self.receipts,
482                requests,
483                gas_used: self.gas_used,
484                blob_gas_used: self.blob_gas_used,
485            },
486        ))
487    }
488
489    fn evm_mut(&mut self) -> &mut Self::Evm {
490        &mut self.evm
491    }
492
493    fn evm(&self) -> &Self::Evm {
494        &self.evm
495    }
496
497    fn receipts(&self) -> &[FoundryReceiptEnvelope] {
498        &self.receipts
499    }
500}
501
502/// Result of executing pool transactions against a block executor.
503pub struct ExecutedPoolTransactions<T> {
504    /// Successfully included transactions.
505    pub included: Vec<Arc<PoolTransaction<T>>>,
506    /// Transactions that failed validation.
507    pub invalid: Vec<Arc<PoolTransaction<T>>>,
508    /// Transactions skipped because they're not yet valid (e.g., valid_after in the future).
509    /// These remain in the pool and should be retried later.
510    pub not_yet_valid: Vec<Arc<PoolTransaction<T>>>,
511    /// Per-transaction execution info.
512    pub tx_info: Vec<TransactionInfo>,
513    /// The raw pending transactions that were included (in order).
514    pub txs: Vec<MaybeImpersonatedTransaction<T>>,
515}
516
517/// Gas-related configuration for pool transaction execution.
518///
519/// Bundles parameters that cannot be derived from the generic `Evm` trait
520/// (which doesn't expose `cfg()`), so callers construct this from `EvmEnv`
521/// before calling [`execute_pool_transactions`].
522pub struct PoolTxGasConfig {
523    pub disable_block_gas_limit: bool,
524    pub tx_gas_limit_cap: Option<u64>,
525    pub tx_gas_limit_cap_resolved: u64,
526    pub max_blob_gas_per_block: u64,
527    pub is_cancun: bool,
528}
529
530/// Hooks invoked around each candidate transaction's execution.
531pub struct PoolTransactionHooks<BeforeTransaction, ExecuteTransaction, OnExecutionError> {
532    /// Runs after validation and immediately before execution.
533    pub before_transaction: BeforeTransaction,
534    /// Executes the candidate through the network-specific transaction entry point.
535    pub execute_transaction: ExecuteTransaction,
536    /// Runs when execution fails before the candidate can be included.
537    pub on_execution_error: OnExecutionError,
538}
539
540/// Executes a pool candidate through the block executor's ordinary transaction entry point.
541pub(crate) fn execute_pool_transaction<B>(
542    executor: &mut B,
543    tx_env: <B::Evm as Evm>::Tx,
544    recovered: Recovered<B::Transaction>,
545    _is_replay: bool,
546) -> Result<B::Result, BlockExecutionError>
547where
548    B: BlockExecutor,
549{
550    executor.execute_transaction_without_commit((tx_env, recovered))
551}
552
553/// Executes pool transactions against a block executor, handling validation,
554/// execution, commit, inspector drain, and result collection.
555///
556/// This is the shared core of `do_mine_block` and `with_pending_block`.
557#[allow(clippy::type_complexity)]
558pub fn execute_pool_transactions<B, BeforeTransaction, ExecuteTransaction, OnExecutionError>(
559    executor: &mut B,
560    pool_transactions: &[Arc<PoolTransaction<B::Transaction>>],
561    gas_config: &PoolTxGasConfig,
562    inspector_config: &InspectorTxConfig,
563    cheats: &CheatsManager,
564    validator: &dyn Fn(
565        &PoolTransaction<B::Transaction>,
566        &AccountInfo,
567    ) -> Result<(), InvalidTransactionError>,
568    hooks: &mut PoolTransactionHooks<BeforeTransaction, ExecuteTransaction, OnExecutionError>,
569) -> ExecutedPoolTransactions<B::Transaction>
570where
571    B: BlockExecutor<
572            Transaction = FoundryTxEnvelope,
573            Evm: Evm<DB: Database + Debug, Inspector = AnvilInspector>,
574        >,
575    B::Receipt: TxReceipt,
576    <B::Result as TxResult>::HaltReason: Clone + IntoInstructionResult,
577    <B::Evm as Evm>::Tx: FromTxWithEncoded<B::Transaction> + FoundryTransaction,
578    BeforeTransaction: FnMut(&mut B::Evm, &<B::Evm as Evm>::Tx),
579    ExecuteTransaction: FnMut(
580        &mut B,
581        <B::Evm as Evm>::Tx,
582        Recovered<B::Transaction>,
583        bool,
584    ) -> Result<B::Result, BlockExecutionError>,
585    OnExecutionError: FnMut(&mut B::Evm),
586{
587    let gas_limit = executor.evm().block().gas_limit();
588
589    let mut included = Vec::new();
590    let mut invalid = Vec::new();
591    let mut not_yet_valid = Vec::new();
592    let mut tx_info: Vec<TransactionInfo> = Vec::new();
593    let mut transactions = Vec::new();
594    let mut blob_gas_used = 0u64;
595
596    for pool_tx in pool_transactions {
597        let pending = &pool_tx.pending_transaction;
598        let sender = *pending.sender();
599        let block_timestamp = executor.evm().block().timestamp();
600
601        if let FoundryTxEnvelope::Tempo(aa_tx) = pending.transaction.as_ref()
602            && let Some(valid_after) = aa_tx.tx().valid_after
603            && U256::from(valid_after.get()) > block_timestamp
604        {
605            trace!(target: "backend", "[{:?}] transaction not valid yet, will retry later", pool_tx.hash());
606            not_yet_valid.push(pool_tx.clone());
607            continue;
608        }
609
610        let account = match executor.evm_mut().db_mut().basic(sender).map(|a| a.unwrap_or_default())
611        {
612            Ok(acc) => acc,
613            Err(err) => {
614                trace!(target: "backend", ?err, "db error for tx {:?}, skipping", pool_tx.hash());
615                continue;
616            }
617        };
618
619        let tx_env =
620            build_tx_env_for_pending::<B::Transaction, <B::Evm as Evm>::Tx>(pending, cheats);
621
622        // Gas limit checks
623        let cumulative_gas =
624            executor.receipts().last().map(|r| r.cumulative_gas_used()).unwrap_or(0);
625        let max_block_gas = cumulative_gas.saturating_add(pending.transaction.gas_limit());
626        if !gas_config.disable_block_gas_limit && max_block_gas > gas_limit {
627            trace!(target: "backend", tx_gas_limit = %pending.transaction.gas_limit(), ?pool_tx, "block gas limit exhausting, skipping transaction");
628            continue;
629        }
630
631        // Osaka EIP-7825 tx gas limit cap check
632        if gas_config.tx_gas_limit_cap.is_none()
633            && pending.transaction.gas_limit() > gas_config.tx_gas_limit_cap_resolved
634        {
635            trace!(target: "backend", tx_gas_limit = %pending.transaction.gas_limit(), ?pool_tx, "transaction gas limit exhausting, skipping transaction");
636            continue;
637        }
638
639        // Reject declared blob gas before execution. Network-specific accounting is checked again
640        // against the execution result below.
641        let declared_blob_gas = pending.transaction.blob_gas_used().unwrap_or(0);
642        if blob_gas_used.saturating_add(declared_blob_gas) > gas_config.max_blob_gas_per_block {
643            trace!(target: "backend", blob_gas = %declared_blob_gas, ?pool_tx, "block blob gas limit exhausting, skipping transaction");
644            continue;
645        }
646
647        // Validate
648        if let Err(err) = validator(pool_tx, &account) {
649            warn!(target: "backend", "Skipping invalid tx execution [{:?}] {}", pool_tx.hash(), err);
650            invalid.push(pool_tx.clone());
651            continue;
652        }
653
654        let nonce = account.nonce;
655
656        (hooks.before_transaction)(executor.evm_mut(), &tx_env);
657        let recovered = Recovered::new_unchecked(pending.transaction.as_ref().clone(), sender);
658        trace!(target: "backend", "[{:?}] executing", pool_tx.hash());
659        match (hooks.execute_transaction)(executor, tx_env, recovered, pool_tx.is_replay) {
660            Ok(result) => {
661                let exec_result = result.result().result.clone();
662                let gas_used = result.result().result.tx_gas_used();
663
664                executor.commit_transaction(result);
665
666                let traces =
667                    executor.evm_mut().inspector_mut().finish_transaction(inspector_config);
668
669                if gas_config.is_cancun {
670                    blob_gas_used = blob_gas_used.saturating_add(declared_blob_gas);
671                }
672
673                let (exit_reason, out, _logs) = match exec_result {
674                    ExecutionResult::Success { reason, logs, output, .. } => {
675                        (reason.into(), Some(output), logs)
676                    }
677                    ExecutionResult::Revert { output, .. } => {
678                        (InstructionResult::Revert, Some(Output::Call(output)), Vec::new())
679                    }
680                    ExecutionResult::Halt { reason, .. } => {
681                        (reason.into_instruction_result(), None, Vec::new())
682                    }
683                };
684
685                if exit_reason == InstructionResult::OutOfGas {
686                    warn!(target: "backend", "[{:?}] executed with out of gas", pool_tx.hash());
687                }
688
689                trace!(target: "backend", ?exit_reason, ?gas_used, "[{:?}] executed with out={:?}", pool_tx.hash(), out);
690                trace!(target: "backend::executor", "transacted [{:?}], result: {:?} gas {}", pool_tx.hash(), exit_reason, gas_used);
691
692                let contract_address = pending.transaction.to().is_none().then(|| {
693                    let addr = sender.create(nonce);
694                    trace!(target: "backend", "Contract creation tx: computed address {:?}", addr);
695                    addr
696                });
697
698                // TODO: replace `TransactionInfo` with alloy receipt/transaction types
699                let transaction_index = tx_info.len() as u64;
700                let info = TransactionInfo {
701                    transaction_hash: pool_tx.hash(),
702                    transaction_index,
703                    from: sender,
704                    to: pending.transaction.to(),
705                    contract_address,
706                    traces,
707                    exit: exit_reason,
708                    out: out.map(Output::into_data),
709                    nonce,
710                    gas_used,
711                };
712
713                included.push(pool_tx.clone());
714                tx_info.push(info);
715                transactions.push(pending.transaction.clone());
716            }
717            Err(err) => {
718                (hooks.on_execution_error)(executor.evm_mut());
719                executor.evm_mut().inspector_mut().discard_transaction(inspector_config);
720                if err.as_validation().is_some() {
721                    warn!(target: "backend", "Skipping invalid tx [{:?}]: {}", pool_tx.hash(), err);
722                    invalid.push(pool_tx.clone());
723                } else {
724                    trace!(target: "backend", ?err, "tx execution error, skipping {:?}", pool_tx.hash());
725                }
726            }
727        }
728    }
729
730    ExecutedPoolTransactions { included, invalid, not_yet_valid, tx_info, txs: transactions }
731}
732
733/// Builds the EVM transaction env from a pending pool transaction.
734pub fn build_tx_env_for_pending<Tx, T>(tx: &PendingTransaction<Tx>, cheats: &CheatsManager) -> T
735where
736    Tx: Transaction + Encodable2718,
737    T: FromTxWithEncoded<Tx> + FoundryTransaction,
738{
739    let encoded = tx.transaction.encoded_2718().into();
740    let mut tx_env: T =
741        FromTxWithEncoded::from_encoded_tx(tx.transaction.as_ref(), *tx.sender(), encoded);
742
743    if let Some(signed_auths) = tx.transaction.authorization_list()
744        && cheats.has_recover_overrides()
745    {
746        let auth_list = tx_env.authorization_list_mut();
747        let cheated_auths = signed_auths
748            .iter()
749            .zip(take(auth_list))
750            .map(|(signed_auth, either_auth)| {
751                either_auth.right_and_then(|recovered_auth| {
752                    if recovered_auth.authority().is_none()
753                        && let Ok(signature) = signed_auth.signature()
754                        && let Some(override_addr) =
755                            cheats.get_recover_override(&signature.as_bytes().into())
756                    {
757                        Either::Right(RecoveredAuthorization::new_unchecked(
758                            recovered_auth.into_parts().0,
759                            RecoveredAuthority::Valid(override_addr),
760                        ))
761                    } else {
762                        Either::Right(recovered_auth)
763                    }
764                })
765            })
766            .collect();
767        *tx_env.authorization_list_mut() = cheated_auths;
768    }
769
770    tx_env
771}
772
773/// Returns the block's blob gas budget.
774///
775/// OP Jovian repurposes the block gas limit as the DA-footprint budget. Every other execution
776/// profile uses the active EIP-4844 limit.
777pub(crate) const fn block_blob_gas_limit(
778    optimism_jovian: bool,
779    block_gas_limit: u64,
780    max_blob_gas_per_block: u64,
781) -> u64 {
782    if optimism_jovian { block_gas_limit } else { max_blob_gas_per_block }
783}
784
785#[cfg(test)]
786mod tests {
787    use super::*;
788    use alloy_eips::{
789        eip6110::MAINNET_DEPOSIT_CONTRACT_ADDRESS, eip7002::WITHDRAWAL_REQUEST_TYPE,
790        eip7251::CONSOLIDATION_REQUEST_TYPE,
791    };
792    use alloy_sol_types::{SolEvent, sol};
793
794    sol! {
795        event DepositEvent(
796            bytes pubkey,
797            bytes withdrawal_credentials,
798            bytes amount,
799            bytes signature,
800            bytes index
801        );
802    }
803
804    #[test]
805    fn prague_requests_use_consensus_order() {
806        let event = DepositEvent {
807            pubkey: Bytes::from(vec![0x11; 48]),
808            withdrawal_credentials: Bytes::from(vec![0x22; 32]),
809            amount: Bytes::from(vec![0x33; 8]),
810            signature: Bytes::from(vec![0x44; 96]),
811            index: Bytes::from(vec![0x55; 8]),
812        };
813        let log = DepositEvent::encode_log(&Log {
814            address: MAINNET_DEPOSIT_CONTRACT_ADDRESS,
815            data: event,
816        });
817        let receipt =
818            Receipt { status: Eip658Value::Eip658(true), cumulative_gas_used: 0, logs: vec![log] }
819                .with_bloom();
820        let receipts = [FoundryReceiptEnvelope::Legacy(receipt)];
821        let mut requests = Requests::default();
822
823        append_deposit_requests(
824            ActiveEthereumSpec {
825                hardfork: EthereumHardfork::Prague,
826                deposit_contract_address: MAINNET_DEPOSIT_CONTRACT_ADDRESS,
827            },
828            &receipts,
829            &mut requests,
830        )
831        .unwrap();
832        requests.push_request_with_type(WITHDRAWAL_REQUEST_TYPE, [0xaa]);
833        requests.push_request_with_type(CONSOLIDATION_REQUEST_TYPE, [0xbb]);
834
835        assert_eq!(
836            requests.iter().map(|request| request[0]).collect::<Vec<_>>(),
837            [DEPOSIT_REQUEST_TYPE, WITHDRAWAL_REQUEST_TYPE, CONSOLIDATION_REQUEST_TYPE]
838        );
839    }
840
841    #[test]
842    fn deposit_requests_use_configured_contract_address() {
843        let configured_address = Address::repeat_byte(0x42);
844        let event = DepositEvent {
845            pubkey: Bytes::from(vec![0x11; 48]),
846            withdrawal_credentials: Bytes::from(vec![0x22; 32]),
847            amount: Bytes::from(vec![0x33; 8]),
848            signature: Bytes::from(vec![0x44; 96]),
849            index: Bytes::from(vec![0x55; 8]),
850        };
851        let configured_log =
852            DepositEvent::encode_log(&Log { address: configured_address, data: event.clone() });
853        let mainnet_log = DepositEvent::encode_log(&Log {
854            address: MAINNET_DEPOSIT_CONTRACT_ADDRESS,
855            data: event,
856        });
857        let receipt = Receipt {
858            status: Eip658Value::Eip658(true),
859            cumulative_gas_used: 0,
860            logs: vec![mainnet_log, configured_log],
861        }
862        .with_bloom();
863        let receipts = [FoundryReceiptEnvelope::Legacy(receipt)];
864        let mut requests = Requests::default();
865
866        append_deposit_requests(
867            ActiveEthereumSpec {
868                hardfork: EthereumHardfork::Prague,
869                deposit_contract_address: configured_address,
870            },
871            &receipts,
872            &mut requests,
873        )
874        .unwrap();
875
876        let request = requests.first().expect("configured deposit should be collected");
877        assert_eq!(request[0], DEPOSIT_REQUEST_TYPE);
878        assert_eq!(request.len(), 1 + 48 + 32 + 8 + 96 + 8);
879    }
880}