1use crate::{get_blob_base_fee_update_fraction_by_spec_id, get_mainnet_spec_id, EdbContext, EdbDB};
22use alloy_primitives::{address, Address, TxHash, TxKind, B256, U256};
23use alloy_provider::{Provider, ProviderBuilder};
24use alloy_rpc_types::{BlockNumberOrTag, Transaction, TransactionTrait};
25use eyre::Result;
26use indicatif::ProgressBar;
27use revm::{
28 context::{ContextTr, TxEnv},
29 context_interface::block::BlobExcessGasAndPrice,
30 database::{AlloyDB, CacheDB},
31 Context, Database, DatabaseCommit, DatabaseRef, ExecuteCommitEvm, ExecuteEvm, MainBuilder,
32 MainContext,
33};
34use serde::{Deserialize, Serialize};
35use std::sync::Arc;
36use tracing::{debug, error, info, warn};
37
38use revm::{
39 context::result::ExecutionResult,
41 database_interface::WrapDatabaseAsync,
42 primitives::hardfork::SpecId,
43};
44
45pub const ARBITRUM_SENDER: Address = address!("0x00000000000000000000000000000000000a4b05");
48
49pub const OPTIMISM_SYSTEM_ADDRESS: Address = address!("0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001");
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct ForkInfo {
58 pub block_number: u64,
60 pub block_hash: B256,
62 pub timestamp: u64,
64 pub chain_id: u64,
66 pub spec_id: SpecId,
68}
69
70pub struct ForkResult<DB>
72where
73 DB: Database + DatabaseCommit + DatabaseRef + Clone + Send + Sync + 'static,
74 <CacheDB<DB> as Database>::Error: Clone + Send + Sync,
75 <DB as Database>::Error: Clone + Send + Sync,
76{
77 pub fork_info: ForkInfo,
79 pub context: EdbContext<DB>,
81 pub target_tx_env: TxEnv,
83 pub target_tx_hash: TxHash,
85}
86
87pub async fn get_chain_id(rpc_url: &str) -> Result<u64> {
89 let provider = ProviderBuilder::new().connect(rpc_url).await?;
90 let chain_id = provider.get_chain_id().await?;
91 Ok(chain_id)
92}
93
94pub async fn fork_and_prepare(
102 rpc_url: &str,
103 target_tx_hash: TxHash,
104 quick: bool,
105) -> Result<
106 ForkResult<EdbDB<impl Clone + Database + DatabaseCommit + DatabaseRef + Send + Sync + 'static>>,
107> {
108 info!("forking chain and executing transactions with revm for {:?}", target_tx_hash);
109
110 let provider = ProviderBuilder::new().connect(rpc_url).await?;
111
112 let chain_id = provider
113 .get_chain_id()
114 .await
115 .map_err(|e| eyre::eyre!("Failed to get chain ID: {:?}", e))?;
116 if chain_id != 1 {
117 warn!("We currently only support mainnet (chain ID 1), got {chain_id}. Use it at your own risk.");
118 }
119
120 let target_tx = provider
122 .get_transaction_by_hash(target_tx_hash)
123 .await?
124 .ok_or_else(|| eyre::eyre!("Target transaction not found: {:?}", target_tx_hash))?;
125
126 if is_known_system_sender(target_tx.inner.signer()) {
128 return Err(eyre::eyre!(
129 "{:?} is a system transaction.\nReplaying system transactions is currently not supported.",
130 target_tx.inner.tx_hash()
131 ));
132 }
133
134 let target_block_number = target_tx
135 .block_number
136 .ok_or_else(|| eyre::eyre!("Target transaction not mined: {:?}", target_tx_hash))?;
137
138 info!("Target transaction is in block {}", target_block_number);
139
140 let block = provider
142 .get_block_by_number(BlockNumberOrTag::Number(target_block_number))
143 .full()
144 .await?
145 .ok_or_else(|| eyre::eyre!("Block {} not found", target_block_number))?;
146
147 let transactions = block.transactions.as_transactions().unwrap_or_default();
149
150 let target_index = transactions
152 .iter()
153 .position(|tx| *tx.inner.hash() == target_tx_hash)
154 .ok_or_else(|| eyre::eyre!("Target transaction not found in block"))?;
155
156 let preceding_txs: Vec<&Transaction> = transactions.iter().take(target_index).collect();
158
159 let spec_id = get_mainnet_spec_id(target_block_number);
161 info!("Block {} is under {:?} hardfork", target_block_number, spec_id);
162
163 let fork_info = ForkInfo {
165 block_number: target_block_number,
166 block_hash: block.header.hash,
167 timestamp: block.header.timestamp,
168 chain_id,
169 spec_id,
170 };
171
172 let alloy_db = AlloyDB::new(provider, (target_block_number - 1).into());
174 let state_db =
175 WrapDatabaseAsync::new(alloy_db).ok_or(eyre::eyre!("Failed to create AlloyDB"))?;
176 let debug_db = EdbDB::new(CacheDB::new(Arc::new(state_db)));
177 let cache_db: CacheDB<_> = CacheDB::new(debug_db);
178
179 let ctx = Context::mainnet()
180 .with_db(cache_db)
181 .modify_block_chained(|b| {
182 b.number = U256::from(target_block_number);
183 b.timestamp = U256::from(block.header.timestamp);
184 b.basefee = block.header.base_fee_per_gas.unwrap_or_default();
185 b.difficulty = block.header.difficulty;
186 b.gas_limit = block.header.gas_limit;
187 b.prevrandao = Some(block.header.mix_hash);
188 b.blob_excess_gas_and_price = block.header.excess_blob_gas.map(|g| {
190 BlobExcessGasAndPrice::new(g, get_blob_base_fee_update_fraction_by_spec_id(spec_id))
191 });
192 b.beneficiary = block.header.beneficiary;
193 })
194 .modify_cfg_chained(|c| {
195 c.chain_id = chain_id;
196 c.spec = spec_id;
197 c.disable_nonce_check = quick; });
199
200 let mut evm = ctx.build_mainnet();
201 info!("The evm verision is {}", evm.cfg().spec);
202
203 if quick {
205 info!(
206 "Quick mode enabled - skipping replay of {} preceding transactions",
207 preceding_txs.len()
208 );
209 } else {
210 debug!("Executing {} preceding transactions", preceding_txs.len());
211
212 let console_bar = Arc::new(ProgressBar::new(preceding_txs.len() as u64));
214 let template = format!("{{spinner:.green}} 🔮 Replaying blockchain history for {} [{{bar:40.cyan/blue}}] {{pos:>3}}/{{len:3}} ⛽ {{msg}}", &target_tx_hash.to_string()[2..10]);
215 console_bar.set_style(
216 indicatif::ProgressStyle::with_template(&template)?
217 .progress_chars("🟩🟦⬜")
218 .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"),
219 );
220
221 for (i, tx) in preceding_txs.iter().enumerate() {
222 if is_known_system_sender(tx.inner.signer()) {
226 console_bar.inc(1);
227 continue;
228 }
229
230 let short_hash = &tx.inner.hash().to_string()[2..10]; console_bar.set_message(format!("tx {}: 0x{}...", i + 1, short_hash));
232
233 debug!(
234 "Executing transaction {}/{}: {:?}",
235 i + 1,
236 preceding_txs.len(),
237 tx.inner.hash()
238 );
239
240 let tx_env = get_tx_env_from_tx(tx, chain_id)?;
241
242 match evm.transact_commit(tx_env.clone()) {
244 Ok(result) => match result {
245 ExecutionResult::Success { gas_used, .. } => {
246 console_bar.set_message(format!("✅ 0x{short_hash}... gas: {gas_used}"));
247 debug!(
248 "Transaction {} executed and committed successfully, gas used: {}",
249 i + 1,
250 gas_used
251 );
252 }
253 ExecutionResult::Revert { gas_used, output } => {
254 console_bar.set_message(format!("⚠️ 0x{short_hash}... reverted"));
255 debug!(
256 "Transaction {} reverted but committed, gas used: {}, output: {:?}",
257 i + 1,
258 gas_used,
259 output
260 );
261 }
262 ExecutionResult::Halt { reason, gas_used } => {
263 console_bar.set_message(format!("❌ 0x{short_hash}... halted"));
264 debug!(
265 "Transaction {} halted, gas used: {}, reason: {:?}",
266 i + 1,
267 gas_used,
268 reason
269 );
270 }
271 },
272 Err(e) => {
273 error!("Failed to execute transaction {}: {:?}", i + 1, e);
274 return Err(eyre::eyre!(
275 "Transaction execution failed at index {} ({}): {:?}",
276 i,
277 tx.inner.hash(),
278 e
279 ));
280 }
281 }
282
283 console_bar.inc(1);
284 }
285
286 console_bar.finish_with_message(format!(
287 "✨ Ready! Replayed {} transactions before {}",
288 preceding_txs.len(),
289 &target_tx_hash.to_string()[2..10]
290 ));
291 }
292
293 let target_tx_env = get_tx_env_from_tx(&target_tx, chain_id)?;
295
296 evm.finalize();
298 let context = evm.ctx;
299
300 Ok(ForkResult { fork_info, context, target_tx_env, target_tx_hash })
301}
302
303pub fn get_tx_env_from_tx(tx: &Transaction, chain_id: u64) -> Result<TxEnv> {
305 let mut b = TxEnv::builder()
306 .caller(tx.inner.signer())
307 .gas_limit(tx.gas_limit())
308 .gas_price(tx.gas_price().unwrap_or(tx.inner.max_fee_per_gas()))
309 .value(tx.value())
310 .data(tx.input().to_owned())
311 .gas_priority_fee(tx.max_priority_fee_per_gas())
312 .chain_id(Some(chain_id))
313 .nonce(tx.nonce())
314 .access_list(tx.access_list().cloned().unwrap_or_default())
315 .kind(match tx.to() {
316 Some(to) => TxKind::Call(to),
317 None => TxKind::Create,
318 });
319
320 if let Some(gp) = tx.gas_price() {
322 b = b.gas_price(gp);
323 } else {
324 b = b.gas_price(tx.inner.max_fee_per_gas()).gas_priority_fee(tx.max_priority_fee_per_gas());
325 }
326
327 if let Some(mfb) = tx.max_fee_per_blob_gas() {
329 b = b.max_fee_per_blob_gas(mfb);
330 }
331 if let Some(hashes) = tx.blob_versioned_hashes() {
332 b = b.blob_hashes(hashes.to_vec());
333 }
334
335 if let Some(authz) = tx.authorization_list() {
337 b = b.authorization_list_signed(authz.to_vec());
338 }
339
340 b.build().map_err(|e| eyre::eyre!("TxEnv build failed: {:?}", e))
341}
342
343fn is_known_system_sender(sender: Address) -> bool {
344 [ARBITRUM_SENDER, OPTIMISM_SYSTEM_ADDRESS, Address::ZERO].contains(&sender)
345}