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