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::{
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#[derive(Clone, Debug)]
36pub(crate) struct HistoricalReplayTransaction {
37 pub(crate) transaction: Recovered<FoundryTxEnvelope>,
38 pub(crate) source_index: usize,
43}
44
45pub(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 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
75pub(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
83pub(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 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
179pub(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
204pub(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 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 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 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}