Skip to main content

anvil/eth/backend/
executor.rs

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