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, Transaction, TransactionEnvelope, TxReceipt,
10    transaction::{Either, Recovered},
11};
12use alloy_eips::{
13    Encodable2718, eip2935, eip4788,
14    eip7702::{RecoveredAuthority, RecoveredAuthorization},
15};
16use alloy_evm::{
17    Evm, FromRecoveredTx, FromTxWithEncoded, RecoveredTx,
18    block::{
19        BlockExecutionError, BlockExecutionResult, BlockExecutor, BlockValidationError,
20        ExecutableTx, GasOutput, OnStateHook, StateChangePreBlockSource, StateChangeSource,
21        StateDB, TxResult,
22    },
23    eth::{
24        EthTxResult,
25        receipt_builder::{ReceiptBuilder, ReceiptBuilderCtx},
26    },
27};
28use alloy_primitives::{Address, B256, Bytes, U256};
29use anvil_core::eth::transaction::{
30    MaybeImpersonatedTransaction, PendingTransaction, TransactionInfo,
31};
32use foundry_evm::core::{env::FoundryTransaction, evm::IntoInstructionResult};
33use foundry_primitives::{FoundryReceiptEnvelope, FoundryTxEnvelope, FoundryTxType};
34use revm::{
35    Database, DatabaseCommit,
36    context::Block as RevmBlock,
37    context_interface::result::{ExecutionResult, Output, ResultAndState},
38    interpreter::InstructionResult,
39    primitives::hardfork::SpecId,
40    state::AccountInfo,
41};
42use std::{fmt, fmt::Debug, mem::take, sync::Arc};
43
44/// Receipt builder for Foundry/Anvil that handles all transaction types
45#[derive(Debug, Default, Clone, Copy)]
46#[non_exhaustive]
47pub struct FoundryReceiptBuilder;
48
49impl ReceiptBuilder for FoundryReceiptBuilder {
50    type Transaction = FoundryTxEnvelope;
51    type Receipt = FoundryReceiptEnvelope;
52
53    fn build_receipt<E: Evm>(
54        &self,
55        ctx: ReceiptBuilderCtx<'_, FoundryTxType, E>,
56    ) -> FoundryReceiptEnvelope {
57        let receipt = alloy_consensus::Receipt {
58            status: Eip658Value::Eip658(ctx.result.is_success()),
59            cumulative_gas_used: ctx.cumulative_gas_used,
60            logs: ctx.result.into_logs(),
61        }
62        .with_bloom();
63
64        match ctx.tx_type {
65            FoundryTxType::Legacy => FoundryReceiptEnvelope::Legacy(receipt),
66            FoundryTxType::Eip2930 => FoundryReceiptEnvelope::Eip2930(receipt),
67            FoundryTxType::Eip1559 => FoundryReceiptEnvelope::Eip1559(receipt),
68            FoundryTxType::Eip4844 => FoundryReceiptEnvelope::Eip4844(receipt),
69            FoundryTxType::Eip7702 => FoundryReceiptEnvelope::Eip7702(receipt),
70            #[cfg(feature = "optimism")]
71            FoundryTxType::Deposit => {
72                unreachable!("deposit receipts are built in commit_transaction")
73            }
74            #[cfg(feature = "optimism")]
75            FoundryTxType::PostExec => FoundryReceiptEnvelope::PostExec(receipt),
76            FoundryTxType::Tempo => FoundryReceiptEnvelope::Tempo(receipt),
77        }
78    }
79}
80
81/// Result of executing a transaction in [`AnvilBlockExecutor`].
82///
83/// Wraps [`EthTxResult`] with the sender address, needed for deposit nonce resolution.
84#[derive(Debug)]
85pub struct AnvilTxResult<H> {
86    pub inner: EthTxResult<H, FoundryTxType>,
87    pub sender: Address,
88}
89
90impl<H: Send + 'static> TxResult for AnvilTxResult<H> {
91    type HaltReason = H;
92
93    fn result(&self) -> &ResultAndState<Self::HaltReason> {
94        self.inner.result()
95    }
96
97    fn into_result(self) -> ResultAndState<Self::HaltReason> {
98        self.inner.into_result()
99    }
100}
101
102/// Block executor for Anvil that implements [`BlockExecutor`].
103///
104/// Wraps an EVM instance and produces [`FoundryReceiptEnvelope`] receipts.
105/// Validation (gas limits, blob gas, transaction validity) is handled by the
106/// caller before transactions are fed to this executor.
107pub struct AnvilBlockExecutor<E> {
108    /// The EVM instance used for execution.
109    evm: E,
110    /// Parent block hash — needed for EIP-2935 system call.
111    parent_hash: B256,
112    /// The active spec id, used to gate hardfork-specific behavior.
113    spec_id: SpecId,
114    /// Receipt builder.
115    receipt_builder: FoundryReceiptBuilder,
116    /// Receipts of executed transactions.
117    receipts: Vec<FoundryReceiptEnvelope>,
118    /// Total gas used by transactions in this block.
119    gas_used: u64,
120    /// Blob gas used by the block.
121    blob_gas_used: u64,
122    /// Optional state change hook.
123    state_hook: Option<Box<dyn OnStateHook>>,
124}
125
126impl<E: fmt::Debug> fmt::Debug for AnvilBlockExecutor<E> {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        f.debug_struct("AnvilBlockExecutor")
129            .field("evm", &self.evm)
130            .field("parent_hash", &self.parent_hash)
131            .field("spec_id", &self.spec_id)
132            .field("gas_used", &self.gas_used)
133            .field("blob_gas_used", &self.blob_gas_used)
134            .field("receipts", &self.receipts.len())
135            .finish_non_exhaustive()
136    }
137}
138
139impl<E> AnvilBlockExecutor<E> {
140    /// Creates a new [`AnvilBlockExecutor`].
141    pub fn new(evm: E, parent_hash: B256, spec_id: SpecId) -> Self {
142        Self {
143            evm,
144            parent_hash,
145            spec_id,
146            receipt_builder: FoundryReceiptBuilder,
147            receipts: Vec::new(),
148            gas_used: 0,
149            blob_gas_used: 0,
150            state_hook: None,
151        }
152    }
153}
154
155impl<E> BlockExecutor for AnvilBlockExecutor<E>
156where
157    E: Evm<
158            DB: StateDB,
159            Tx: FromRecoveredTx<FoundryTxEnvelope> + FromTxWithEncoded<FoundryTxEnvelope>,
160        >,
161{
162    type Transaction = FoundryTxEnvelope;
163    type Receipt = FoundryReceiptEnvelope;
164    type Evm = E;
165    type Result = AnvilTxResult<E::HaltReason>;
166
167    fn apply_pre_execution_changes(&mut self) -> Result<(), BlockExecutionError> {
168        // EIP-2935: store parent block hash in history storage contract.
169        if self.spec_id >= SpecId::PRAGUE {
170            let result = self
171                .evm
172                .transact_system_call(
173                    eip4788::SYSTEM_ADDRESS,
174                    eip2935::HISTORY_STORAGE_ADDRESS,
175                    Bytes::copy_from_slice(self.parent_hash.as_slice()),
176                )
177                .map_err(BlockExecutionError::other)?;
178
179            if let Some(hook) = &mut self.state_hook {
180                hook.on_state(
181                    StateChangeSource::PreBlock(StateChangePreBlockSource::BlockHashesContract),
182                    &result.state,
183                );
184            }
185            self.evm.db_mut().commit(result.state);
186        }
187        Ok(())
188    }
189
190    fn execute_transaction_without_commit(
191        &mut self,
192        tx: impl ExecutableTx<Self>,
193    ) -> Result<Self::Result, BlockExecutionError> {
194        let (tx_env, tx) = tx.into_parts();
195
196        let block_available_gas = self.evm.block().gas_limit() - self.gas_used;
197        if tx.tx().gas_limit() > block_available_gas {
198            return Err(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
199                transaction_gas_limit: tx.tx().gas_limit(),
200                block_available_gas,
201            }
202            .into());
203        }
204
205        let sender = *tx.signer();
206
207        let result = self.evm.transact(tx_env).map_err(|err| {
208            let hash = tx.tx().trie_hash();
209            BlockExecutionError::evm(err, hash)
210        })?;
211
212        Ok(AnvilTxResult {
213            inner: EthTxResult {
214                result,
215                blob_gas_used: tx.tx().blob_gas_used().unwrap_or_default(),
216                tx_type: tx.tx().tx_type(),
217            },
218            sender,
219        })
220    }
221
222    fn commit_transaction(&mut self, output: Self::Result) -> GasOutput {
223        let AnvilTxResult {
224            inner: EthTxResult { result: ResultAndState { result, state }, blob_gas_used, tx_type },
225            #[cfg_attr(not(feature = "optimism"), allow(unused_variables))]
226            sender,
227        } = output;
228
229        if let Some(hook) = &mut self.state_hook {
230            hook.on_state(StateChangeSource::Transaction(self.receipts.len()), &state);
231        }
232
233        let gas_used = result.tx_gas_used();
234        self.gas_used += gas_used;
235
236        if self.spec_id >= SpecId::CANCUN {
237            self.blob_gas_used = self.blob_gas_used.saturating_add(blob_gas_used);
238        }
239
240        #[cfg(feature = "optimism")]
241        let receipt = if tx_type == FoundryTxType::Deposit {
242            let deposit_nonce = state.get(&sender).map(|acc| acc.info.nonce);
243            let receipt = alloy_consensus::Receipt {
244                status: Eip658Value::Eip658(result.is_success()),
245                cumulative_gas_used: self.gas_used,
246                logs: result.into_logs(),
247            }
248            .with_bloom();
249            FoundryReceiptEnvelope::Deposit(op_alloy_consensus::OpDepositReceiptWithBloom {
250                receipt: op_alloy_consensus::OpDepositReceipt {
251                    inner: receipt.receipt,
252                    deposit_nonce,
253                    deposit_receipt_version: deposit_nonce.map(|_| 1),
254                },
255                logs_bloom: receipt.logs_bloom,
256            })
257        } else {
258            self.receipt_builder.build_receipt(ReceiptBuilderCtx {
259                tx_type,
260                evm: &self.evm,
261                result,
262                state: &state,
263                cumulative_gas_used: self.gas_used,
264            })
265        };
266        #[cfg(not(feature = "optimism"))]
267        let receipt = self.receipt_builder.build_receipt(ReceiptBuilderCtx {
268            tx_type,
269            evm: &self.evm,
270            result,
271            state: &state,
272            cumulative_gas_used: self.gas_used,
273        });
274
275        self.receipts.push(receipt);
276        self.evm.db_mut().commit(state);
277
278        GasOutput::new(gas_used)
279    }
280
281    fn finish(
282        self,
283    ) -> Result<(Self::Evm, BlockExecutionResult<FoundryReceiptEnvelope>), BlockExecutionError>
284    {
285        Ok((
286            self.evm,
287            BlockExecutionResult {
288                receipts: self.receipts,
289                requests: Default::default(),
290                gas_used: self.gas_used,
291                blob_gas_used: self.blob_gas_used,
292            },
293        ))
294    }
295
296    fn set_state_hook(&mut self, hook: Option<Box<dyn OnStateHook>>) {
297        self.state_hook = hook;
298    }
299
300    fn evm_mut(&mut self) -> &mut Self::Evm {
301        &mut self.evm
302    }
303
304    fn evm(&self) -> &Self::Evm {
305        &self.evm
306    }
307
308    fn receipts(&self) -> &[FoundryReceiptEnvelope] {
309        &self.receipts
310    }
311}
312
313/// Result of executing pool transactions against a block executor.
314pub struct ExecutedPoolTransactions<T> {
315    /// Successfully included transactions.
316    pub included: Vec<Arc<PoolTransaction<T>>>,
317    /// Transactions that failed validation.
318    pub invalid: Vec<Arc<PoolTransaction<T>>>,
319    /// Transactions skipped because they're not yet valid (e.g., valid_after in the future).
320    /// These remain in the pool and should be retried later.
321    pub not_yet_valid: Vec<Arc<PoolTransaction<T>>>,
322    /// Per-transaction execution info.
323    pub tx_info: Vec<TransactionInfo>,
324    /// The raw pending transactions that were included (in order).
325    pub txs: Vec<MaybeImpersonatedTransaction<T>>,
326}
327
328/// Gas-related configuration for pool transaction execution.
329///
330/// Bundles parameters that cannot be derived from the generic `Evm` trait
331/// (which doesn't expose `cfg()`), so callers construct this from `EvmEnv`
332/// before calling [`execute_pool_transactions`].
333pub struct PoolTxGasConfig {
334    pub disable_block_gas_limit: bool,
335    pub tx_gas_limit_cap: Option<u64>,
336    pub tx_gas_limit_cap_resolved: u64,
337    pub max_blob_gas_per_block: u64,
338    pub is_cancun: bool,
339}
340
341/// Executes pool transactions against a block executor, handling validation,
342/// execution, commit, inspector drain, and result collection.
343///
344/// This is the shared core of `do_mine_block` and `with_pending_block`.
345#[allow(clippy::type_complexity)]
346pub fn execute_pool_transactions<B>(
347    executor: &mut B,
348    pool_transactions: &[Arc<PoolTransaction<B::Transaction>>],
349    gas_config: &PoolTxGasConfig,
350    inspector_config: &InspectorTxConfig,
351    cheats: &CheatsManager,
352    validator: &dyn Fn(
353        &PendingTransaction<B::Transaction>,
354        &AccountInfo,
355    ) -> Result<(), InvalidTransactionError>,
356) -> ExecutedPoolTransactions<B::Transaction>
357where
358    B: BlockExecutor<
359            Transaction = FoundryTxEnvelope,
360            Evm: Evm<DB: Database + Debug, Inspector = AnvilInspector>,
361        >,
362    B::Receipt: TxReceipt,
363    <B::Result as TxResult>::HaltReason: Clone + IntoInstructionResult,
364    <B::Evm as Evm>::Tx: FromTxWithEncoded<B::Transaction> + FoundryTransaction,
365{
366    let gas_limit = executor.evm().block().gas_limit();
367
368    let mut included = Vec::new();
369    let mut invalid = Vec::new();
370    let mut not_yet_valid = Vec::new();
371    let mut tx_info: Vec<TransactionInfo> = Vec::new();
372    let mut transactions = Vec::new();
373    let mut blob_gas_used = 0u64;
374
375    for pool_tx in pool_transactions {
376        let pending = &pool_tx.pending_transaction;
377        let sender = *pending.sender();
378        let block_timestamp = executor.evm().block().timestamp();
379
380        if let FoundryTxEnvelope::Tempo(aa_tx) = pending.transaction.as_ref()
381            && let Some(valid_after) = aa_tx.tx().valid_after
382            && U256::from(valid_after.get()) > block_timestamp
383        {
384            trace!(target: "backend", "[{:?}] transaction not valid yet, will retry later", pool_tx.hash());
385            not_yet_valid.push(pool_tx.clone());
386            continue;
387        }
388
389        let account = match executor.evm_mut().db_mut().basic(sender).map(|a| a.unwrap_or_default())
390        {
391            Ok(acc) => acc,
392            Err(err) => {
393                trace!(target: "backend", ?err, "db error for tx {:?}, skipping", pool_tx.hash());
394                continue;
395            }
396        };
397
398        let tx_env =
399            build_tx_env_for_pending::<B::Transaction, <B::Evm as Evm>::Tx>(pending, cheats);
400
401        // Gas limit checks
402        let cumulative_gas =
403            executor.receipts().last().map(|r| r.cumulative_gas_used()).unwrap_or(0);
404        let max_block_gas = cumulative_gas.saturating_add(pending.transaction.gas_limit());
405        if !gas_config.disable_block_gas_limit && max_block_gas > gas_limit {
406            trace!(target: "backend", tx_gas_limit = %pending.transaction.gas_limit(), ?pool_tx, "block gas limit exhausting, skipping transaction");
407            continue;
408        }
409
410        // Osaka EIP-7825 tx gas limit cap check
411        if gas_config.tx_gas_limit_cap.is_none()
412            && pending.transaction.gas_limit() > gas_config.tx_gas_limit_cap_resolved
413        {
414            trace!(target: "backend", tx_gas_limit = %pending.transaction.gas_limit(), ?pool_tx, "transaction gas limit exhausting, skipping transaction");
415            continue;
416        }
417
418        // Blob gas check
419        let tx_blob_gas = pending.transaction.blob_gas_used().unwrap_or(0);
420        if blob_gas_used.saturating_add(tx_blob_gas) > gas_config.max_blob_gas_per_block {
421            trace!(target: "backend", blob_gas = %tx_blob_gas, ?pool_tx, "block blob gas limit exhausting, skipping transaction");
422            continue;
423        }
424
425        // Validate
426        if let Err(err) = validator(pending, &account) {
427            warn!(target: "backend", "Skipping invalid tx execution [{:?}] {}", pool_tx.hash(), err);
428            invalid.push(pool_tx.clone());
429            continue;
430        }
431
432        let nonce = account.nonce;
433
434        let recovered = Recovered::new_unchecked(pending.transaction.as_ref().clone(), sender);
435        trace!(target: "backend", "[{:?}] executing", pool_tx.hash());
436        match executor.execute_transaction_without_commit((tx_env, recovered)) {
437            Ok(result) => {
438                let exec_result = result.result().result.clone();
439                let gas_used = result.result().result.tx_gas_used();
440
441                executor.commit_transaction(result);
442
443                let traces =
444                    executor.evm_mut().inspector_mut().finish_transaction(inspector_config);
445
446                if gas_config.is_cancun {
447                    blob_gas_used = blob_gas_used.saturating_add(tx_blob_gas);
448                }
449
450                let (exit_reason, out, _logs) = match exec_result {
451                    ExecutionResult::Success { reason, logs, output, .. } => {
452                        (reason.into(), Some(output), logs)
453                    }
454                    ExecutionResult::Revert { output, .. } => {
455                        (InstructionResult::Revert, Some(Output::Call(output)), Vec::new())
456                    }
457                    ExecutionResult::Halt { reason, .. } => {
458                        (reason.into_instruction_result(), None, Vec::new())
459                    }
460                };
461
462                if exit_reason == InstructionResult::OutOfGas {
463                    warn!(target: "backend", "[{:?}] executed with out of gas", pool_tx.hash());
464                }
465
466                trace!(target: "backend", ?exit_reason, ?gas_used, "[{:?}] executed with out={:?}", pool_tx.hash(), out);
467                trace!(target: "backend::executor", "transacted [{:?}], result: {:?} gas {}", pool_tx.hash(), exit_reason, gas_used);
468
469                let contract_address = pending.transaction.to().is_none().then(|| {
470                    let addr = sender.create(nonce);
471                    trace!(target: "backend", "Contract creation tx: computed address {:?}", addr);
472                    addr
473                });
474
475                // TODO: replace `TransactionInfo` with alloy receipt/transaction types
476                let transaction_index = tx_info.len() as u64;
477                let info = TransactionInfo {
478                    transaction_hash: pool_tx.hash(),
479                    transaction_index,
480                    from: sender,
481                    to: pending.transaction.to(),
482                    contract_address,
483                    traces,
484                    exit: exit_reason,
485                    out: out.map(Output::into_data),
486                    nonce,
487                    gas_used,
488                };
489
490                included.push(pool_tx.clone());
491                tx_info.push(info);
492                transactions.push(pending.transaction.clone());
493            }
494            Err(err) => {
495                if err.as_validation().is_some() {
496                    warn!(target: "backend", "Skipping invalid tx [{:?}]: {}", pool_tx.hash(), err);
497                    invalid.push(pool_tx.clone());
498                } else {
499                    trace!(target: "backend", ?err, "tx execution error, skipping {:?}", pool_tx.hash());
500                }
501            }
502        }
503    }
504
505    ExecutedPoolTransactions { included, invalid, not_yet_valid, tx_info, txs: transactions }
506}
507
508/// Builds the EVM transaction env from a pending pool transaction.
509pub fn build_tx_env_for_pending<Tx, T>(tx: &PendingTransaction<Tx>, cheats: &CheatsManager) -> T
510where
511    Tx: Transaction + Encodable2718,
512    T: FromTxWithEncoded<Tx> + FoundryTransaction,
513{
514    let encoded = tx.transaction.encoded_2718().into();
515    let mut tx_env: T =
516        FromTxWithEncoded::from_encoded_tx(tx.transaction.as_ref(), *tx.sender(), encoded);
517
518    if let Some(signed_auths) = tx.transaction.authorization_list()
519        && cheats.has_recover_overrides()
520    {
521        let auth_list = tx_env.authorization_list_mut();
522        let cheated_auths = signed_auths
523            .iter()
524            .zip(take(auth_list))
525            .map(|(signed_auth, either_auth)| {
526                either_auth.right_and_then(|recovered_auth| {
527                    if recovered_auth.authority().is_none()
528                        && let Ok(signature) = signed_auth.signature()
529                        && let Some(override_addr) =
530                            cheats.get_recover_override(&signature.as_bytes().into())
531                    {
532                        Either::Right(RecoveredAuthorization::new_unchecked(
533                            recovered_auth.into_parts().0,
534                            RecoveredAuthority::Valid(override_addr),
535                        ))
536                    } else {
537                        Either::Right(recovered_auth)
538                    }
539                })
540            })
541            .collect();
542        *tx_env.authorization_list_mut() = cheated_auths;
543    }
544
545    tx_env
546}