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