Skip to main content

anvil/eth/backend/
replay.rs

1//! Transaction-hash fork replay preparation and execution.
2
3use crate::{
4    config::ForkTransactionReplay,
5    eth::backend::executor::AnvilBlockExecutor,
6    mem::inspector::{AnvilInspector, InspectorTxConfig},
7};
8use alloy_consensus::{
9    BlockHeader, Transaction, Typed2718,
10    transaction::{Recovered, SignerRecoverable, TxHashRef},
11};
12use alloy_evm::{
13    Evm, FromRecoveredTx, FromTxWithEncoded, RecoveredTx,
14    block::{
15        BalIndexedDatabase, BlockExecutionError, BlockExecutionResult, BlockExecutor, StateDB,
16        TxResult,
17    },
18    precompiles::PrecompilesMap,
19};
20use alloy_network::{BlockResponse, TransactionResponse};
21use alloy_primitives::{Address, B256};
22use anvil_core::eth::transaction::{MaybeImpersonatedTransaction, TransactionInfo};
23use eyre::{Context, Result};
24use foundry_common::sh_warn;
25use foundry_evm::core::evm::IntoInstructionResult;
26use foundry_primitives::{FoundryReceiptEnvelope, FoundryTxEnvelope, FoundryTxType};
27use revm::{
28    Database,
29    context_interface::result::{ExecutionResult, Output},
30    interpreter::InstructionResult,
31    state::EvmState,
32};
33
34/// A source transaction prepared for direct historical execution.
35#[derive(Clone, Debug)]
36pub(crate) struct HistoricalReplayTransaction {
37    pub(crate) transaction: Recovered<FoundryTxEnvelope>,
38    /// Where this transaction sat in the source block, for diagnostics only.
39    ///
40    /// The replayed block holds just the transactions anvil executed, so this is not its index
41    /// there; storage keys receipts and traces by position in that block.
42    pub(crate) source_index: usize,
43}
44
45/// A validated transaction prefix together with its source block execution inputs.
46pub(crate) struct PreparedForkTransactionReplay {
47    pub(crate) transactions: Vec<HistoricalReplayTransaction>,
48    pub(crate) beneficiary: Address,
49    pub(crate) timestamp: u64,
50    pub(crate) parent_beacon_block_root: Option<B256>,
51}
52
53impl PreparedForkTransactionReplay {
54    /// Resolves the execution chain ID encoded by the source prefix.
55    ///
56    /// Unprotected legacy prefixes inherit the execution identity exposed by the endpoint.
57    pub(crate) fn execution_chain_id(&self, fallback: u64) -> Result<u64> {
58        let mut resolved = None;
59        for replay in &self.transactions {
60            let Some(chain_id) = replay.transaction.tx().chain_id() else { continue };
61            if let Some(expected) = resolved {
62                eyre::ensure!(
63                    chain_id == expected,
64                    "source transaction at index {} uses chain ID {chain_id}, expected {expected}",
65                    replay.source_index
66                );
67            } else {
68                resolved = Some(chain_id);
69            }
70        }
71        Ok(resolved.unwrap_or(fallback))
72    }
73}
74
75/// The complete result of executing a historical prefix against an overlay.
76pub(crate) struct ExecutedHistoricalReplay {
77    pub(crate) block_result: BlockExecutionResult<FoundryReceiptEnvelope>,
78    pub(crate) transactions: Vec<MaybeImpersonatedTransaction<FoundryTxEnvelope>>,
79    pub(crate) transaction_infos: Vec<TransactionInfo>,
80    pub(crate) state_changes: Vec<EvmState>,
81}
82
83/// Converts and validates every source-prefix transaction before database execution.
84pub(crate) fn prepare_fork_transaction_replay(
85    replay: ForkTransactionReplay,
86    #[cfg_attr(not(feature = "monad"), allow(unused_variables))] trust_monad_protocol_sender: bool,
87    allow_cip64: bool,
88) -> Result<PreparedForkTransactionReplay> {
89    let source_hash = replay.source_block.header().hash;
90    let source_number = replay.source_block.header().number;
91    let beneficiary = replay.source_block.header().beneficiary();
92    let timestamp = replay.source_block.header().timestamp();
93    let parent_beacon_block_root = replay.source_block.header().parent_beacon_block_root();
94    let source_transactions = replay
95        .source_block
96        .transactions()
97        .as_transactions()
98        .expect("full source block validated during resolution");
99
100    let target_index = replay.target_index;
101    let transactions = source_transactions
102        .iter()
103        .take(target_index.saturating_add(1))
104        .enumerate()
105        .map(|(source_index, source_transaction)| {
106            let source_transaction_hash = source_transaction.tx_hash();
107            // Chains anvil can fork but not execute, such as Arbitrum and its Orbit rollups, mint
108            // their own transaction types; Arbitrum opens every block with an `ArbitrumInternalTx`.
109            // Those carry no EVM semantics anvil could apply, so the prefix skips them instead of
110            // failing the whole replay. The requested transaction itself must still be executable,
111            // otherwise the resulting fork would not be the state the caller asked for.
112            if FoundryTxType::try_from(source_transaction.ty()).is_err()
113                || (source_transaction.ty() == foundry_primitives::CIP64_TX_TYPE && !allow_cip64)
114            {
115                eyre::ensure!(
116                    source_index != target_index,
117                    "fork transaction {source_transaction_hash} in block {source_hash} \
118                     ({source_number}) has type 0x{:x}, which anvil cannot execute",
119                    source_transaction.ty(),
120                );
121                sh_warn!(
122                    "skipping source transaction {source_transaction_hash} at index \
123                     {source_index} with unsupported type 0x{:x}; replayed state will not \
124                     include its effects",
125                    source_transaction.ty(),
126                )?;
127                return Ok(None);
128            }
129            let transaction = FoundryTxEnvelope::try_from(source_transaction.clone())
130                .wrap_err_with(|| {
131                    format!(
132                        "failed to convert source transaction {source_transaction_hash} at index \
133                         {source_index} in block {source_hash} ({source_number})"
134                    )
135                })?;
136            eyre::ensure!(
137                transaction.tx_hash() == &source_transaction_hash,
138                "converted source transaction at index {source_index} in block {source_hash} \
139                 ({source_number}) changed hash from {source_transaction_hash} to {}",
140                transaction.tx_hash()
141            );
142            #[cfg(feature = "monad")]
143            let sender = if trust_monad_protocol_sender
144                && source_transaction.from() == monad_revm::staking::constants::SYSTEM_ADDRESS
145            {
146                source_transaction.from()
147            } else {
148                transaction.recover_signer().wrap_err_with(|| {
149                    format!(
150                        "failed to recover sender for source transaction \
151                         {source_transaction_hash} at index {source_index} in block {source_hash} \
152                         ({source_number})"
153                    )
154                })?
155            };
156            #[cfg(not(feature = "monad"))]
157            let sender = transaction.recover_signer().wrap_err_with(|| {
158                format!(
159                    "failed to recover sender for source transaction {source_transaction_hash} at \
160                     index {source_index} in block {source_hash} ({source_number})"
161                )
162            })?;
163            Ok(Some(HistoricalReplayTransaction {
164                transaction: Recovered::new_unchecked(transaction, sender),
165                source_index,
166            }))
167        })
168        .filter_map(Result::transpose)
169        .collect::<Result<_>>()?;
170
171    Ok(PreparedForkTransactionReplay {
172        transactions,
173        beneficiary,
174        timestamp,
175        parent_beacon_block_root,
176    })
177}
178
179/// Executes a prepared prefix strictly and captures changesets for deferred publication.
180pub(crate) fn execute_historical_replay<E>(
181    executor: &mut AnvilBlockExecutor<E>,
182    transactions: &[HistoricalReplayTransaction],
183    inspector_config: &InspectorTxConfig,
184) -> Result<(Vec<MaybeImpersonatedTransaction<FoundryTxEnvelope>>, Vec<TransactionInfo>)>
185where
186    E: Evm<
187            DB: StateDB + BalIndexedDatabase,
188            Inspector = AnvilInspector,
189            Precompiles = PrecompilesMap,
190            Tx: FromRecoveredTx<FoundryTxEnvelope> + FromTxWithEncoded<FoundryTxEnvelope>,
191        >,
192    E::HaltReason: Clone + IntoInstructionResult,
193{
194    execute_historical_replay_with(
195        executor,
196        transactions,
197        inspector_config,
198        |evm, tx_env, transaction_hash| {
199            evm.transact(tx_env).map_err(|err| BlockExecutionError::evm(err, transaction_hash))
200        },
201    )
202}
203
204/// Executes a prepared prefix using a caller-selected network transaction entry point.
205pub(crate) fn execute_historical_replay_with<E, F>(
206    executor: &mut AnvilBlockExecutor<E>,
207    transactions: &[HistoricalReplayTransaction],
208    inspector_config: &InspectorTxConfig,
209    mut transact: F,
210) -> Result<(Vec<MaybeImpersonatedTransaction<FoundryTxEnvelope>>, Vec<TransactionInfo>)>
211where
212    E: Evm<
213            DB: StateDB + BalIndexedDatabase,
214            Inspector = AnvilInspector,
215            Precompiles = PrecompilesMap,
216            Tx: FromRecoveredTx<FoundryTxEnvelope> + FromTxWithEncoded<FoundryTxEnvelope>,
217        >,
218    E::HaltReason: Clone + IntoInstructionResult,
219    F: FnMut(
220        &mut E,
221        E::Tx,
222        B256,
223    ) -> Result<
224        revm::context_interface::result::ResultAndState<E::HaltReason>,
225        BlockExecutionError,
226    >,
227{
228    let mut stored_transactions = Vec::with_capacity(transactions.len());
229    let mut transaction_infos = Vec::with_capacity(transactions.len());
230
231    for (execution_index, replay) in transactions.iter().enumerate() {
232        let transaction = replay.transaction.tx();
233        let transaction_hash = *transaction.tx_hash();
234        let sender = replay.transaction.signer();
235        let nonce = executor
236            .evm_mut()
237            .db_mut()
238            .basic(sender)
239            .wrap_err_with(|| {
240                format!(
241                    "database error preparing source transaction {transaction_hash} at index {}",
242                    replay.source_index
243                )
244            })?
245            .unwrap_or_default()
246            .nonce;
247
248        let result = executor
249            .execute_transaction_without_commit_with(
250                replay.transaction.clone().into_encoded(),
251                &mut transact,
252            )
253            .map_err(|err| {
254                eyre::eyre!(
255                    "failed to execute source transaction {transaction_hash} at index {}: {err}",
256                    replay.source_index
257                )
258            })?;
259        let execution_result = result.result().result.clone();
260        let gas_used = execution_result.tx_gas_used();
261        executor.commit_transaction(result);
262
263        let (_, inspector, precompiles) = executor.evm_mut().components_mut();
264        let traces = inspector.finish_transaction(inspector_config, precompiles);
265        let (exit_reason, out) = match execution_result {
266            ExecutionResult::Success { reason, output, .. } => (reason.into(), Some(output)),
267            ExecutionResult::Revert { output, .. } => {
268                (InstructionResult::Revert, Some(Output::Call(output)))
269            }
270            ExecutionResult::Halt { reason, .. } => (reason.into_instruction_result(), None),
271        };
272        let contract_address = transaction.to().is_none().then(|| sender.create(nonce));
273        transaction_infos.push(TransactionInfo {
274            transaction_hash,
275            transaction_index: execution_index as u64,
276            from: sender,
277            to: transaction.to(),
278            contract_address,
279            traces,
280            exit: exit_reason,
281            out: out.map(Output::into_data),
282            nonce,
283            gas_used,
284        });
285        stored_transactions.push(MaybeImpersonatedTransaction::new(transaction.clone()));
286    }
287
288    Ok((stored_transactions, transaction_infos))
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use alloy_consensus::{SignableTransaction, TxEip1559};
295    use alloy_network::AnyRpcBlock;
296    use alloy_primitives::{Signature, U256};
297    use alloy_rpc_types::BlockTransactions;
298    use foundry_primitives::TxCip64;
299
300    /// A real signed legacy transaction, so signer recovery and the hash check pass.
301    const LEGACY_TX: &str = r#"{
302        "type": "0x0",
303        "chainId": "0x1",
304        "nonce": "0x0",
305        "gas": "0x5208",
306        "gasPrice": "0x1",
307        "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
308        "value": "0x1",
309        "input": "0x",
310        "r": "0x85c2794a580da137e24ccc823b45ae5cea99371ae23ee13860fcc6935f8305b0",
311        "s": "0x41de7fa4121dab284af4453d30928241208bafa90cdb701fe9bc7054759fe3cd",
312        "v": "0x1b",
313        "hash": "0x8c9b68e8947ace33028dba167354fde369ed7bbe34911b772d09b3c64b861515",
314        "from": "0xa1e4380a3b1f749673e270229993ee55f35663b4",
315        "blockHash": "0x1111111111111111111111111111111111111111111111111111111111111111",
316        "blockNumber": "0x1",
317        "transactionIndex": "0x1"
318    }"#;
319
320    /// An `ArbitrumInternalTx`, the type Arbitrum opens each block with.
321    const ARBITRUM_INTERNAL_TX: &str = r#"{
322        "type": "0x6a",
323        "chainId": "0xa4b1",
324        "nonce": "0x0",
325        "gas": "0x0",
326        "gasPrice": "0x0",
327        "to": "0x00000000000000000000000000000000000a4b05",
328        "value": "0x0",
329        "input": "0x6bf6a42d",
330        "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
331        "from": "0x00000000000000000000000000000000000a4b05",
332        "blockHash": "0x1111111111111111111111111111111111111111111111111111111111111111",
333        "blockNumber": "0x1",
334        "transactionIndex": "0x0"
335    }"#;
336
337    fn source_block() -> AnyRpcBlock {
338        let json = format!(
339            r#"{{
340                "hash": "0x1111111111111111111111111111111111111111111111111111111111111111",
341                "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
342                "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
343                "miner": "0x0000000000000000000000000000000000000000",
344                "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
345                "transactionsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
346                "receiptsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
347                "logsBloom": "0x{bloom}",
348                "difficulty": "0x0",
349                "number": "0x1",
350                "gasLimit": "0x1c9c380",
351                "gasUsed": "0x5208",
352                "timestamp": "0x64",
353                "extraData": "0x",
354                "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
355                "nonce": "0x0000000000000000",
356                "uncles": [],
357                "transactions": [{ARBITRUM_INTERNAL_TX}, {LEGACY_TX}]
358            }}"#,
359            bloom = "0".repeat(512),
360        );
361        serde_json::from_str(&json).unwrap()
362    }
363
364    fn replay_for(target_index: usize) -> ForkTransactionReplay {
365        ForkTransactionReplay { source_block: source_block(), target_index }
366    }
367
368    #[test]
369    fn skips_unsupported_prefix_transactions() {
370        let prepared = prepare_fork_transaction_replay(replay_for(1), false, false).unwrap();
371
372        // The Arbitrum-typed transaction at index 0 is dropped, and the standard one keeps its
373        // position in the source block.
374        assert_eq!(prepared.transactions.len(), 1);
375        assert_eq!(prepared.transactions[0].source_index, 1);
376    }
377
378    #[test]
379    fn rejects_unsupported_target_transaction() {
380        let Err(err) = prepare_fork_transaction_replay(replay_for(0), false, false) else {
381            panic!("expected the unsupported target transaction to be rejected");
382        };
383        assert!(err.to_string().contains("0x6a"), "unexpected error: {err}");
384    }
385
386    #[test]
387    fn fork_replay_cip64_requires_celo_mode() {
388        let envelope = FoundryTxEnvelope::Celo(
389            TxCip64 { inner: TxEip1559 { chain_id: 1, ..Default::default() }, fee_currency: None }
390                .into_signed(Signature::new(U256::ONE, U256::from(2), false)),
391        );
392        let mut transaction = serde_json::to_value(&envelope).unwrap();
393        transaction["from"] = serde_json::to_value(envelope.recover_signer().unwrap()).unwrap();
394        let mut block = source_block();
395        block.transactions = BlockTransactions::Full(vec![
396            serde_json::from_value(transaction).unwrap(),
397            serde_json::from_str(LEGACY_TX).unwrap(),
398        ]);
399        let replay =
400            |target_index| ForkTransactionReplay { source_block: block.clone(), target_index };
401        let prepared = prepare_fork_transaction_replay(replay(1), false, false).unwrap();
402        assert_eq!(prepared.transactions.len(), 1);
403        assert_eq!(prepared.transactions[0].source_index, 1);
404        assert!(prepare_fork_transaction_replay(replay(0), false, false).is_err());
405        let prepared = prepare_fork_transaction_replay(replay(0), false, true).unwrap();
406        assert_eq!(prepared.transactions[0].transaction.tx(), &envelope);
407    }
408}