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