1use 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#[derive(Clone, Debug)]
32pub(crate) struct HistoricalReplayTransaction {
33 pub(crate) transaction: Recovered<FoundryTxEnvelope>,
34 pub(crate) source_index: usize,
39}
40
41pub(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 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
70pub(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
78pub(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 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
165pub(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
189pub(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 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 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 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}