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, OnStateHook, StateChangePreBlockSource, StateChangeSource, StateDB, TxResult,
21    },
22    eth::{
23        EthTxResult,
24        receipt_builder::{ReceiptBuilder, ReceiptBuilderCtx},
25    },
26};
27use alloy_primitives::{Address, B256, Bytes};
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            FoundryTxType::Deposit => {
70                unreachable!("deposit receipts are built in commit_transaction")
71            }
72            FoundryTxType::Tempo => FoundryReceiptEnvelope::Tempo(receipt),
73        }
74    }
75}
76
77/// Result of executing a transaction in [`AnvilBlockExecutor`].
78///
79/// Wraps [`EthTxResult`] with the sender address, needed for deposit nonce resolution.
80#[derive(Debug)]
81pub struct AnvilTxResult<H> {
82    pub inner: EthTxResult<H, FoundryTxType>,
83    pub sender: Address,
84}
85
86impl<H> TxResult for AnvilTxResult<H> {
87    type HaltReason = H;
88
89    fn result(&self) -> &ResultAndState<Self::HaltReason> {
90        self.inner.result()
91    }
92}
93
94/// Block executor for Anvil that implements [`BlockExecutor`].
95///
96/// Wraps an EVM instance and produces [`FoundryReceiptEnvelope`] receipts.
97/// Validation (gas limits, blob gas, transaction validity) is handled by the
98/// caller before transactions are fed to this executor.
99pub struct AnvilBlockExecutor<E> {
100    /// The EVM instance used for execution.
101    evm: E,
102    /// Parent block hash — needed for EIP-2935 system call.
103    parent_hash: B256,
104    /// The active spec id, used to gate hardfork-specific behavior.
105    spec_id: SpecId,
106    /// Receipt builder.
107    receipt_builder: FoundryReceiptBuilder,
108    /// Receipts of executed transactions.
109    receipts: Vec<FoundryReceiptEnvelope>,
110    /// Total gas used by transactions in this block.
111    gas_used: u64,
112    /// Blob gas used by the block.
113    blob_gas_used: u64,
114    /// Optional state change hook.
115    state_hook: Option<Box<dyn OnStateHook>>,
116}
117
118impl<E: fmt::Debug> fmt::Debug for AnvilBlockExecutor<E> {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        f.debug_struct("AnvilBlockExecutor")
121            .field("evm", &self.evm)
122            .field("parent_hash", &self.parent_hash)
123            .field("spec_id", &self.spec_id)
124            .field("gas_used", &self.gas_used)
125            .field("blob_gas_used", &self.blob_gas_used)
126            .field("receipts", &self.receipts.len())
127            .finish_non_exhaustive()
128    }
129}
130
131impl<E> AnvilBlockExecutor<E> {
132    /// Creates a new [`AnvilBlockExecutor`].
133    pub fn new(evm: E, parent_hash: B256, spec_id: SpecId) -> Self {
134        Self {
135            evm,
136            parent_hash,
137            spec_id,
138            receipt_builder: FoundryReceiptBuilder,
139            receipts: Vec::new(),
140            gas_used: 0,
141            blob_gas_used: 0,
142            state_hook: None,
143        }
144    }
145}
146
147impl<E> BlockExecutor for AnvilBlockExecutor<E>
148where
149    E: Evm<
150            DB: StateDB,
151            Tx: FromRecoveredTx<FoundryTxEnvelope> + FromTxWithEncoded<FoundryTxEnvelope>,
152        >,
153{
154    type Transaction = FoundryTxEnvelope;
155    type Receipt = FoundryReceiptEnvelope;
156    type Evm = E;
157    type Result = AnvilTxResult<E::HaltReason>;
158
159    fn apply_pre_execution_changes(&mut self) -> Result<(), BlockExecutionError> {
160        // EIP-2935: store parent block hash in history storage contract.
161        if self.spec_id >= SpecId::PRAGUE {
162            let result = self
163                .evm
164                .transact_system_call(
165                    eip4788::SYSTEM_ADDRESS,
166                    eip2935::HISTORY_STORAGE_ADDRESS,
167                    Bytes::copy_from_slice(self.parent_hash.as_slice()),
168                )
169                .map_err(BlockExecutionError::other)?;
170
171            if let Some(hook) = &mut self.state_hook {
172                hook.on_state(
173                    StateChangeSource::PreBlock(StateChangePreBlockSource::BlockHashesContract),
174                    &result.state,
175                );
176            }
177            self.evm.db_mut().commit(result.state);
178        }
179        Ok(())
180    }
181
182    fn execute_transaction_without_commit(
183        &mut self,
184        tx: impl ExecutableTx<Self>,
185    ) -> Result<Self::Result, BlockExecutionError> {
186        let (tx_env, tx) = tx.into_parts();
187
188        let block_available_gas = self.evm.block().gas_limit() - self.gas_used;
189        if tx.tx().gas_limit() > block_available_gas {
190            return Err(BlockValidationError::TransactionGasLimitMoreThanAvailableBlockGas {
191                transaction_gas_limit: tx.tx().gas_limit(),
192                block_available_gas,
193            }
194            .into());
195        }
196
197        let sender = *tx.signer();
198
199        let result = self.evm.transact(tx_env).map_err(|err| {
200            let hash = tx.tx().trie_hash();
201            BlockExecutionError::evm(err, hash)
202        })?;
203
204        Ok(AnvilTxResult {
205            inner: EthTxResult {
206                result,
207                blob_gas_used: tx.tx().blob_gas_used().unwrap_or_default(),
208                tx_type: tx.tx().tx_type(),
209            },
210            sender,
211        })
212    }
213
214    fn commit_transaction(&mut self, output: Self::Result) -> Result<u64, BlockExecutionError> {
215        let AnvilTxResult {
216            inner: EthTxResult { result: ResultAndState { result, state }, blob_gas_used, tx_type },
217            sender,
218        } = output;
219
220        if let Some(hook) = &mut self.state_hook {
221            hook.on_state(StateChangeSource::Transaction(self.receipts.len()), &state);
222        }
223
224        let gas_used = result.gas_used();
225        self.gas_used += gas_used;
226
227        if self.spec_id >= SpecId::CANCUN {
228            self.blob_gas_used = self.blob_gas_used.saturating_add(blob_gas_used);
229        }
230
231        let receipt = if tx_type == FoundryTxType::Deposit {
232            let deposit_nonce = state.get(&sender).map(|acc| acc.info.nonce);
233            let receipt = alloy_consensus::Receipt {
234                status: Eip658Value::Eip658(result.is_success()),
235                cumulative_gas_used: self.gas_used,
236                logs: result.into_logs(),
237            }
238            .with_bloom();
239            FoundryReceiptEnvelope::Deposit(op_alloy_consensus::OpDepositReceiptWithBloom {
240                receipt: op_alloy_consensus::OpDepositReceipt {
241                    inner: receipt.receipt,
242                    deposit_nonce,
243                    deposit_receipt_version: deposit_nonce.map(|_| 1),
244                },
245                logs_bloom: receipt.logs_bloom,
246            })
247        } else {
248            self.receipt_builder.build_receipt(ReceiptBuilderCtx {
249                tx_type,
250                evm: &self.evm,
251                result,
252                state: &state,
253                cumulative_gas_used: self.gas_used,
254            })
255        };
256
257        self.receipts.push(receipt);
258        self.evm.db_mut().commit(state);
259
260        Ok(gas_used)
261    }
262
263    fn finish(
264        self,
265    ) -> Result<(Self::Evm, BlockExecutionResult<FoundryReceiptEnvelope>), BlockExecutionError>
266    {
267        Ok((
268            self.evm,
269            BlockExecutionResult {
270                receipts: self.receipts,
271                requests: Default::default(),
272                gas_used: self.gas_used,
273                blob_gas_used: self.blob_gas_used,
274            },
275        ))
276    }
277
278    fn set_state_hook(&mut self, hook: Option<Box<dyn OnStateHook>>) {
279        self.state_hook = hook;
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<Evm: Evm<DB: Database + Debug, Inspector = AnvilInspector>>,
341    B::Transaction: Transaction + Encodable2718 + Clone,
342    B::Receipt: TxReceipt,
343    <B::Result as TxResult>::HaltReason: Clone + IntoInstructionResult,
344    <B::Evm as Evm>::Tx: FromTxWithEncoded<B::Transaction> + FoundryTransaction,
345{
346    let gas_limit = executor.evm().block().gas_limit();
347
348    let mut included = Vec::new();
349    let mut invalid = Vec::new();
350    let mut not_yet_valid = Vec::new();
351    let mut tx_info: Vec<TransactionInfo> = Vec::new();
352    let mut transactions = Vec::new();
353    let mut blob_gas_used = 0u64;
354
355    for pool_tx in pool_transactions {
356        let pending = &pool_tx.pending_transaction;
357        let sender = *pending.sender();
358
359        let account = match executor.evm_mut().db_mut().basic(sender).map(|a| a.unwrap_or_default())
360        {
361            Ok(acc) => acc,
362            Err(err) => {
363                trace!(target: "backend", ?err, "db error for tx {:?}, skipping", pool_tx.hash());
364                continue;
365            }
366        };
367
368        let tx_env =
369            build_tx_env_for_pending::<B::Transaction, <B::Evm as Evm>::Tx>(pending, cheats);
370
371        // Gas limit checks
372        let cumulative_gas =
373            executor.receipts().last().map(|r| r.cumulative_gas_used()).unwrap_or(0);
374        let max_block_gas = cumulative_gas.saturating_add(pending.transaction.gas_limit());
375        if !gas_config.disable_block_gas_limit && max_block_gas > gas_limit {
376            trace!(target: "backend", tx_gas_limit = %pending.transaction.gas_limit(), ?pool_tx, "block gas limit exhausting, skipping transaction");
377            continue;
378        }
379
380        // Osaka EIP-7825 tx gas limit cap check
381        if gas_config.tx_gas_limit_cap.is_none()
382            && pending.transaction.gas_limit() > gas_config.tx_gas_limit_cap_resolved
383        {
384            trace!(target: "backend", tx_gas_limit = %pending.transaction.gas_limit(), ?pool_tx, "transaction gas limit exhausting, skipping transaction");
385            continue;
386        }
387
388        // Blob gas check
389        let tx_blob_gas = pending.transaction.blob_gas_used().unwrap_or(0);
390        if blob_gas_used.saturating_add(tx_blob_gas) > gas_config.max_blob_gas_per_block {
391            trace!(target: "backend", blob_gas = %tx_blob_gas, ?pool_tx, "block blob gas limit exhausting, skipping transaction");
392            continue;
393        }
394
395        // Validate
396        if let Err(err) = validator(pending, &account) {
397            warn!(target: "backend", "Skipping invalid tx execution [{:?}] {}", pool_tx.hash(), err);
398            invalid.push(pool_tx.clone());
399            continue;
400        }
401
402        let nonce = account.nonce;
403
404        let recovered = Recovered::new_unchecked(pending.transaction.as_ref().clone(), sender);
405        trace!(target: "backend", "[{:?}] executing", pool_tx.hash());
406        match executor.execute_transaction_without_commit((tx_env, recovered)) {
407            Ok(result) => {
408                let exec_result = result.result().result.clone();
409                let gas_used = result.result().result.gas_used();
410
411                executor.commit_transaction(result).expect("commit failed");
412
413                let traces =
414                    executor.evm_mut().inspector_mut().finish_transaction(inspector_config);
415
416                if gas_config.is_cancun {
417                    blob_gas_used = blob_gas_used.saturating_add(tx_blob_gas);
418                }
419
420                let (exit_reason, out, _logs) = match exec_result {
421                    ExecutionResult::Success { reason, logs, output, .. } => {
422                        (reason.into(), Some(output), logs)
423                    }
424                    ExecutionResult::Revert { output, .. } => {
425                        (InstructionResult::Revert, Some(Output::Call(output)), Vec::new())
426                    }
427                    ExecutionResult::Halt { reason, .. } => {
428                        (reason.into_instruction_result(), None, Vec::new())
429                    }
430                };
431
432                if exit_reason == InstructionResult::OutOfGas {
433                    warn!(target: "backend", "[{:?}] executed with out of gas", pool_tx.hash());
434                }
435
436                trace!(target: "backend", ?exit_reason, ?gas_used, "[{:?}] executed with out={:?}", pool_tx.hash(), out);
437                trace!(target: "backend::executor", "transacted [{:?}], result: {:?} gas {}", pool_tx.hash(), exit_reason, gas_used);
438
439                let contract_address = pending.transaction.to().is_none().then(|| {
440                    let addr = sender.create(nonce);
441                    trace!(target: "backend", "Contract creation tx: computed address {:?}", addr);
442                    addr
443                });
444
445                // TODO: replace `TransactionInfo` with alloy receipt/transaction types
446                let transaction_index = tx_info.len() as u64;
447                let info = TransactionInfo {
448                    transaction_hash: pool_tx.hash(),
449                    transaction_index,
450                    from: sender,
451                    to: pending.transaction.to(),
452                    contract_address,
453                    traces,
454                    exit: exit_reason,
455                    out: out.map(Output::into_data),
456                    nonce,
457                    gas_used,
458                };
459
460                included.push(pool_tx.clone());
461                tx_info.push(info);
462                transactions.push(pending.transaction.clone());
463            }
464            Err(err) => {
465                let err_str = err.to_string();
466                if err_str.contains("not valid yet") {
467                    trace!(target: "backend", "[{:?}] transaction not valid yet, will retry later", pool_tx.hash());
468                    not_yet_valid.push(pool_tx.clone());
469                } else if err.as_validation().is_some() {
470                    warn!(target: "backend", "Skipping invalid tx [{:?}]: {}", pool_tx.hash(), err);
471                    invalid.push(pool_tx.clone());
472                } else {
473                    trace!(target: "backend", ?err, "tx execution error, skipping {:?}", pool_tx.hash());
474                }
475            }
476        }
477    }
478
479    ExecutedPoolTransactions { included, invalid, not_yet_valid, tx_info, txs: transactions }
480}
481
482/// Builds the EVM transaction env from a pending pool transaction.
483pub fn build_tx_env_for_pending<Tx, T>(tx: &PendingTransaction<Tx>, cheats: &CheatsManager) -> T
484where
485    Tx: Transaction + Encodable2718,
486    T: FromTxWithEncoded<Tx> + FoundryTransaction,
487{
488    let encoded = tx.transaction.encoded_2718().into();
489    let mut tx_env: T =
490        FromTxWithEncoded::from_encoded_tx(tx.transaction.as_ref(), *tx.sender(), encoded);
491
492    if let Some(signed_auths) = tx.transaction.authorization_list()
493        && cheats.has_recover_overrides()
494    {
495        let auth_list = tx_env.authorization_list_mut();
496        let cheated_auths = signed_auths
497            .iter()
498            .zip(take(auth_list))
499            .map(|(signed_auth, either_auth)| {
500                either_auth.right_and_then(|recovered_auth| {
501                    if recovered_auth.authority().is_none()
502                        && let Ok(signature) = signed_auth.signature()
503                        && let Some(override_addr) =
504                            cheats.get_recover_override(&signature.as_bytes().into())
505                    {
506                        Either::Right(RecoveredAuthorization::new_unchecked(
507                            recovered_auth.into_parts().0,
508                            RecoveredAuthority::Valid(override_addr),
509                        ))
510                    } else {
511                        Either::Right(recovered_auth)
512                    }
513                })
514            })
515            .collect();
516        *tx_env.authorization_list_mut() = cheated_auths;
517    }
518
519    tx_env
520}