edb_common/
forking.rs

1// EDB - Ethereum Debugger
2// Copyright (C) 2024 Zhuo Zhang and Wuqi Zhang
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17//! Chain forking and transaction replay utilities
18//!
19//! This module provides ACTUAL REVM TRANSACTION EXECUTION with transact_commit()
20
21use 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    // Use re-exported primitives from revm
40    context::result::ExecutionResult,
41    database_interface::WrapDatabaseAsync,
42    primitives::hardfork::SpecId,
43};
44
45/// Arbitrum L1 sender address of the first transaction in every block.
46/// `0x00000000000000000000000000000000000a4b05`
47pub const ARBITRUM_SENDER: Address = address!("0x00000000000000000000000000000000000a4b05");
48
49/// The system address, the sender of the first transaction in every block:
50/// `0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001`
51///
52/// See also <https://github.com/ethereum-optimism/optimism/blob/65ec61dde94ffa93342728d324fecf474d228e1f/specs/deposits.md#l1-attributes-deposited-transaction>
53pub const OPTIMISM_SYSTEM_ADDRESS: Address = address!("0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001");
54
55/// Fork configuration details
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct ForkInfo {
58    /// Block number that was forked
59    pub block_number: u64,
60    /// Block hash
61    pub block_hash: B256,
62    /// Timestamp of the block
63    pub timestamp: u64,
64    /// Chain ID
65    pub chain_id: u64,
66    /// Spec ID for the hardfork
67    pub spec_id: SpecId,
68}
69
70/// Result of forking operation containing comprehensive replay information
71pub 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    /// Fork information
78    pub fork_info: ForkInfo,
79    /// Revm context with executed state
80    pub context: EdbContext<DB>,
81    /// Transaction environment for the target transaction
82    pub target_tx_env: TxEnv,
83    /// Target transaction hash
84    pub target_tx_hash: TxHash,
85}
86
87/// Get chain id by querying RPC
88pub 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
94/// Fork the chain and ACTUALLY EXECUTE preceding transactions with revm.transact_commit()
95///
96/// This function:
97/// 1. Creates revm database and environment
98/// 2. Actually executes each preceding transaction with revm (unless quick mode is enabled)
99/// 3. Commits each transaction
100/// 4. Returns forked state ready for target transaction
101pub 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    // Get the target transaction to find which block it's in
121    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    // check if the tx is a system transaction
127    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    // Get the full block with transactions
141    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    // Get the transactions in the block
148    let transactions = block.transactions.as_transactions().unwrap_or_default();
149
150    // Find target transaction index
151    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    // Get all transactions before the target
157    let preceding_txs: Vec<&Transaction> = transactions.iter().take(target_index).collect();
158
159    // Get the spec ID for the block using our mainnet mapping
160    let spec_id = get_mainnet_spec_id(target_block_number);
161    info!("Block {} is under {:?} hardfork", target_block_number, spec_id);
162
163    // Create fork info
164    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    // Create revm database: we start with AlloyDB.
173    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            // Note: blob_excess_gas_and_price might not be available in older blocks
189            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; // Disable nonce check in quick mode
198        });
199
200    let mut evm = ctx.build_mainnet();
201    info!("The evm verision is {}", evm.cfg().spec);
202
203    // Skip replaying preceding transactions if quick mode is enabled
204    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        // Actually execute each transaction with revm
213        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            // System transactions such as on L2s don't contain any pricing info so
223            // we skip them otherwise this would cause
224            // reverts
225            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]; // Skip 0x, take 8 chars
231            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            // Actually execute the transaction with commit
243            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    // Get the target transaction environment
294    let target_tx_env = get_tx_env_from_tx(&target_tx, chain_id)?;
295
296    // Extract the context from the EVM
297    evm.finalize();
298    let context = evm.ctx;
299
300    Ok(ForkResult { fork_info, context, target_tx_env, target_tx_hash })
301}
302
303/// Get the transaction environment from the transaction.
304pub 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    // Fees
321    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    // EIP-4844
328    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    // EIP-7702 (post-Pectra)
336    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}