Skip to main content

mega_evme/replay/
cmd.rs

1use std::{str::FromStr, time::Instant};
2
3use alloy_consensus::{BlockHeader, Transaction as _};
4use alloy_primitives::{B256, U256};
5use alloy_provider::Provider;
6use alloy_rpc_types_eth::Block;
7use clap::Parser;
8use mega_evm::{
9    alloy_evm::{block::BlockExecutor, Evm, EvmEnv},
10    alloy_op_evm::block::OpAlloyReceiptBuilder,
11    revm::{
12        context::{result::ExecutionResult, BlockEnv, ContextTr},
13        database::{states::bundle_state::BundleRetention, StateBuilder},
14        primitives::eip4844,
15        DatabaseRef,
16    },
17    BlockLimits, EvmTxRuntimeLimits, MegaBlockExecutionCtx, MegaBlockExecutorFactory,
18    MegaEvmFactory, MegaHardforks, MegaSpecId,
19};
20use tracing::{debug, info, trace, warn};
21
22use op_alloy_rpc_types::Transaction;
23
24use crate::{
25    common::{
26        op_receipt_to_tx_receipt, parse_bucket_capacity, print_execution_summary,
27        print_execution_trace, print_receipt, BuildProviderOutput, EvmeExternalEnvs, EvmeOutcome,
28        ExecutionSummary, ExternalEnvSnapshot, OpTxReceipt, RpcCacheStore, TxOverrideArgs,
29    },
30    replay::get_hardfork_config,
31    run, ChainArgs, EvmeState,
32};
33
34use super::{ReplayError, Result};
35
36/// Replay a transaction from RPC
37#[derive(Parser, Debug)]
38pub struct Cmd {
39    /// Transaction hash to replay
40    #[arg(value_name = "TX_HASH")]
41    pub tx_hash: B256,
42
43    /// RPC configuration
44    #[command(flatten)]
45    pub rpc_args: super::RpcArgs,
46
47    /// External environment configuration (bucket capacities)
48    #[command(flatten)]
49    pub ext_args: run::ExtEnvArgs,
50
51    /// State dump configuration
52    #[command(flatten)]
53    pub dump_args: run::StateDumpArgs,
54
55    /// Trace configuration
56    #[command(flatten)]
57    pub trace_args: run::TraceArgs,
58
59    /// Override the spec to use (default: auto-detect from chain ID and block timestamp)
60    #[arg(long = "override.spec", value_name = "SPEC")]
61    pub spec_override: Option<String>,
62
63    /// Transaction override configuration
64    #[command(flatten)]
65    pub tx_override_args: TxOverrideArgs,
66
67    /// Output format configuration
68    #[command(flatten)]
69    pub output_args: run::OutputArgs,
70}
71
72/// Resolved provider and associated metadata from `--rpc` / `--rpc.capture-file` /
73/// `--rpc.replay-file` flags.
74struct ProviderContext {
75    provider: crate::common::OpProvider,
76    cache_store: RpcCacheStore,
77    external_env: Option<ExternalEnvSnapshot>,
78    chain_id: u64,
79}
80
81/// Replay-specific execution outcome
82#[allow(dead_code)]
83pub(super) struct ReplayOutcome {
84    /// Common execution outcome
85    pub outcome: EvmeOutcome,
86    /// The original transaction that was replayed
87    pub original_tx: Transaction,
88    /// The transaction receipt
89    pub receipt: OpTxReceipt,
90}
91
92/// Intermediate context fetched from RPC before execution.
93struct ReplayContext {
94    target_tx: Transaction,
95    parent_block: Block<Transaction>,
96    block: Block<Transaction>,
97    chain_id: u64,
98    preceding_tx_hashes: Vec<B256>,
99}
100
101impl Cmd {
102    /// Replay a historical transaction.
103    pub async fn run(&self) -> Result<()> {
104        let mut pctx = self.resolve_provider().await?;
105        let rctx = self.fetch_replay_context(&pctx.provider, pctx.chain_id).await?;
106        let (external_envs, env_snapshot) = self.resolve_external_envs(&pctx)?;
107        let result = self.execute(&pctx.provider, &rctx, external_envs).await?;
108        self.output_results(&result)?;
109        // Hand the effective external-env snapshot to the store before the final
110        // persist; no-op unless this is a fixture-capture store.
111        if let Some(snapshot) = env_snapshot {
112            pctx.cache_store.set_external_env(snapshot);
113        }
114        pctx.cache_store.persist()?;
115        Ok(())
116    }
117
118    /// Select the right provider based on `--rpc`, `--rpc.capture-file`, and
119    /// `--rpc.replay-file` flags.
120    async fn resolve_provider(&self) -> Result<ProviderContext> {
121        let output = if let Some(path) = &self.rpc_args.capture_file {
122            info!(path = %path.display(), "Provider mode: capture to cache file");
123            self.rpc_args.build_capture_provider().await?
124        } else if let Some(path) = &self.rpc_args.replay_file {
125            if !self.ext_args.bucket_capacity.is_empty() {
126                return Err(ReplayError::Other(
127                    "'--bucket-capacity' cannot be used in offline replay mode \
128                         (bucket capacities come from the fixture envelope)"
129                        .to_string(),
130                ));
131            }
132            info!(path = %path.display(), "Provider mode: offline replay from cache file");
133            self.rpc_args.build_replay_provider().await?
134        } else if let Some(rpc) = &self.rpc_args.rpc_url {
135            info!(rpc = %rpc, "Provider mode: online RPC");
136            self.rpc_args.build_provider().await?
137        } else {
138            return Err(ReplayError::Other(
139                "'mega-evme replay' requires '--rpc <URL>', '--rpc.capture-file <PATH>', \
140                 or '--rpc.replay-file <PATH>'"
141                    .to_string(),
142            ));
143        };
144
145        let BuildProviderOutput { provider, cache_store, chain_id, external_env } = output;
146        Ok(ProviderContext { provider, cache_store, external_env, chain_id })
147    }
148
149    /// Fetch the transaction, its block, and preceding transaction hashes from the provider.
150    async fn fetch_replay_context<P>(&self, provider: &P, chain_id: u64) -> Result<ReplayContext>
151    where
152        P: Provider<op_alloy_network::Optimism>,
153    {
154        info!(tx_hash = %self.tx_hash, "Fetching transaction");
155        let target_tx = provider
156            .get_transaction_by_hash(self.tx_hash)
157            .await
158            .map_err(|e| ReplayError::RpcError(format!("Failed to fetch transaction: {e}")))?
159            .ok_or_else(|| ReplayError::TransactionNotFound(self.tx_hash))?;
160        debug!(block_number = ?target_tx.block_number, "Transaction found");
161
162        let (state_base_block, block_number, is_pending) = if let Some(n) = target_tx.block_number {
163            (n - 1, n, false)
164        } else {
165            let latest = provider
166                .get_block_number()
167                .await
168                .map_err(|e| ReplayError::RpcError(format!("RPC transport error: {e}")))?;
169            (latest, latest, true)
170        };
171        debug!(
172            state_base_block = state_base_block,
173            block = block_number,
174            is_pending,
175            "Block numbers determined",
176        );
177
178        let parent_block = provider
179            .get_block_by_number(state_base_block.into())
180            .await
181            .map_err(|e| ReplayError::RpcError(format!("RPC transport error: {e}")))?
182            .ok_or(ReplayError::BlockNotFound(state_base_block))?;
183        let block = provider
184            .get_block_by_number(block_number.into())
185            .await
186            .map_err(|e| ReplayError::RpcError(format!("RPC transport error: {e}")))?
187            .ok_or(ReplayError::BlockNotFound(block_number))?;
188
189        let mut preceding_tx_hashes = vec![];
190        if !is_pending {
191            for hash in block.transactions.hashes() {
192                if hash == self.tx_hash {
193                    break;
194                }
195                preceding_tx_hashes.push(hash);
196            }
197        }
198
199        debug!(chain_id, preceding_count = preceding_tx_hashes.len(), "Replay context ready");
200
201        Ok(ReplayContext { target_tx, parent_block, block, chain_id, preceding_tx_hashes })
202    }
203
204    /// Build the external environment and (for capture mode) the envelope snapshot.
205    ///
206    /// Parses `--bucket-capacity` exactly once: the parsed values feed both the
207    /// runtime `EvmeExternalEnvs` and the `ExternalEnvSnapshot` for envelope persistence.
208    fn resolve_external_envs(
209        &self,
210        pctx: &ProviderContext,
211    ) -> Result<(EvmeExternalEnvs, Option<ExternalEnvSnapshot>)> {
212        if self.rpc_args.replay_file.is_some() {
213            let mut envs = EvmeExternalEnvs::new();
214            if let Some(snapshot) = &pctx.external_env {
215                debug!(
216                    bucket_count = snapshot.bucket_capacities.len(),
217                    "Using bucket capacities from replay envelope",
218                );
219                for &(bucket_id, capacity) in &snapshot.bucket_capacities {
220                    envs = envs.with_bucket_capacity(bucket_id, capacity);
221                }
222            }
223            return Ok((envs, None));
224        }
225
226        // Online / capture: parse bucket capacities once.
227        let parsed: Vec<(u32, u64)> = self
228            .ext_args
229            .bucket_capacity
230            .iter()
231            .map(|s| parse_bucket_capacity(s))
232            .collect::<std::result::Result<_, _>>()?;
233
234        // Determine the effective capacities: CLI values take precedence,
235        // then the previous envelope's values (refresh without --bucket-capacity),
236        // then empty (defaults to MIN_BUCKET_SIZE).
237        let effective = if !parsed.is_empty() {
238            parsed
239        } else if let Some(prev) = &pctx.external_env {
240            prev.bucket_capacities.clone()
241        } else {
242            vec![]
243        };
244
245        let mut envs = EvmeExternalEnvs::new();
246        for &(id, cap) in &effective {
247            envs = envs.with_bucket_capacity(id, cap);
248        }
249        debug!(
250            bucket_count = effective.len(),
251            from_cli = !self.ext_args.bucket_capacity.is_empty(),
252            "Resolved bucket capacities for online/capture mode",
253        );
254
255        // Build the envelope snapshot only in capture mode.
256        let snapshot = self
257            .rpc_args
258            .capture_file
259            .is_some()
260            .then_some(ExternalEnvSnapshot { bucket_capacities: effective });
261
262        Ok((envs, snapshot))
263    }
264
265    /// Execute the target transaction (with preceding transactions) and return the outcome.
266    async fn execute<P>(
267        &self,
268        provider: &P,
269        ctx: &ReplayContext,
270        external_envs: EvmeExternalEnvs,
271    ) -> Result<ReplayOutcome>
272    where
273        P: Provider<op_alloy_network::Optimism> + Clone + std::fmt::Debug,
274    {
275        let hardforks = get_hardfork_config(ctx.chain_id);
276        let spec = hardforks.spec_id(ctx.block.header.timestamp());
277        let chain_args = ChainArgs { chain_id: ctx.chain_id, spec: spec.to_string() };
278        debug!(chain_id = ctx.chain_id, spec = %spec, "Chain configuration");
279
280        info!(fork_block = ctx.parent_block.header.number(), "Forking state from parent block",);
281        let mut database = EvmeState::new_forked(
282            provider.clone(),
283            Some(ctx.parent_block.header.number()),
284            Default::default(),
285            Default::default(),
286        )
287        .await?;
288
289        let block_env = retrieve_block_env(&ctx.block)?;
290        trace!(?block_env, "Block environment built");
291        let mut evm_env = EvmEnv::new(chain_args.create_cfg_env()?, block_env);
292
293        let evm_factory = MegaEvmFactory::new().with_external_env_factory(external_envs);
294        let block_executor_factory = MegaBlockExecutorFactory::new(
295            &hardforks,
296            evm_factory,
297            OpAlloyReceiptBuilder::default(),
298        );
299        let mut block_limits = BlockLimits::from_hardfork_and_block_gas_limit(
300            hardforks.hardfork(ctx.block.header.timestamp()).ok_or(ReplayError::Other(format!(
301                "No `MegaHardfork` active at block timestamp: {}",
302                ctx.block.header.timestamp()
303            )))?,
304            ctx.block.header.gas_limit(),
305        );
306
307        if let Some(spec_override) = &self.spec_override {
308            info!(spec_override = %spec_override, "Overriding EVM spec");
309            let spec = MegaSpecId::from_str(spec_override)
310                .map_err(|e| ReplayError::Other(format!("Invalid spec: {e:?}")))?;
311            evm_env.cfg_env.spec = spec;
312            block_limits = block_limits.with_tx_runtime_limits(EvmTxRuntimeLimits::from_spec(spec));
313        }
314
315        let block_ctx = MegaBlockExecutionCtx::new(
316            ctx.parent_block.hash(),
317            ctx.block.header.parent_beacon_block_root(),
318            ctx.block.header.extra_data().clone(),
319            block_limits,
320        );
321
322        let start = Instant::now();
323        let mut inspector = self.trace_args.create_inspector();
324        let mut state =
325            StateBuilder::new().with_database(&mut database).with_bundle_update().build();
326        let mut block_executor = block_executor_factory.create_executor_with_inspector(
327            &mut state,
328            block_ctx,
329            evm_env,
330            &mut inspector,
331        );
332
333        block_executor
334            .apply_pre_execution_changes()
335            .map_err(|e| ReplayError::Other(format!("Block execution error: {e}")))?;
336
337        // Execute preceding transactions
338        info!(preceding_count = ctx.preceding_tx_hashes.len(), "Executing preceding transactions",);
339        for tx_hash in &ctx.preceding_tx_hashes {
340            debug!(tx_hash = %tx_hash, "Executing preceding transaction");
341            let tx = provider
342                .get_transaction_by_hash(*tx_hash)
343                .await
344                .map_err(|e| ReplayError::RpcError(format!("RPC transport error: {e}")))?
345                .ok_or(ReplayError::TransactionNotFound(*tx_hash))?;
346            let outcome = block_executor
347                .run_transaction(tx.as_recovered())
348                .map_err(|e| ReplayError::Other(format!("Block execution error: {e}")))?;
349            trace!(tx_hash = %tx_hash, ?outcome, "Preceding transaction executed");
350            block_executor
351                .commit_transaction_outcome(outcome)
352                .map_err(|e| ReplayError::Other(format!("Block execution error: {e}")))?;
353        }
354
355        // Execute target transaction
356        info!("Executing target transaction");
357        if self.tx_override_args.has_overrides() {
358            info!(overrides = ?self.tx_override_args, "Applying transaction overrides");
359        }
360        let wrapped_tx = self.tx_override_args.wrap(ctx.target_tx.as_recovered())?;
361        let pre_execution_nonce = block_executor
362            .evm()
363            .db_ref()
364            .basic_ref(wrapped_tx.inner().signer())?
365            .map(|acc| acc.nonce)
366            .unwrap_or(0);
367
368        block_executor.inspector_mut().fuse();
369        let outcome = block_executor
370            .run_transaction(wrapped_tx)
371            .map_err(|e| ReplayError::Other(format!("Block execution error: {e}")))?;
372        trace!(tx_hash = %ctx.target_tx.inner.inner.tx_hash(), ?outcome, "Target transaction executed");
373        let exec_result = outcome.inner.result.clone();
374        let evm_state = outcome.inner.state.clone();
375
376        match &exec_result {
377            ExecutionResult::Success { gas_used, .. } => info!(gas_used, "Execution succeeded"),
378            ExecutionResult::Revert { gas_used, .. } => warn!(gas_used, "Execution reverted"),
379            ExecutionResult::Halt { reason, gas_used } => {
380                warn!(?reason, gas_used, "Execution halted")
381            }
382        }
383
384        let result_and_state = mega_evm::revm::context::result::ResultAndState {
385            result: exec_result.clone(),
386            state: evm_state.clone(),
387        };
388
389        let trace_data = self.trace_args.is_tracing_enabled().then(|| {
390            self.trace_args.generate_trace(
391                block_executor.inspector(),
392                &result_and_state,
393                block_executor.evm().db_ref(),
394            )
395        });
396
397        let gas_used = block_executor
398            .commit_transaction_outcome(outcome)
399            .map_err(|e| ReplayError::Other(format!("Block execution error: {e}")))?;
400        let duration = start.elapsed();
401
402        let (evm, block_result) = block_executor
403            .finish()
404            .map_err(|e| ReplayError::Other(format!("Block execution error: {e}")))?;
405        let (db, _) = evm.finish();
406        db.merge_transitions(BundleRetention::Reverts);
407        let receipt_envelope = block_result.receipts.last().unwrap().clone();
408        trace!(?receipt_envelope, "Receipt envelope obtained");
409
410        let from = ctx.target_tx.inner.inner.signer();
411        let to = ctx.target_tx.inner.inner.to();
412        let contract_address = (to.is_none() && receipt_envelope.is_success())
413            .then(|| from.create(pre_execution_nonce));
414        let receipt = op_receipt_to_tx_receipt(
415            &receipt_envelope,
416            ctx.block.number(),
417            ctx.block.header.timestamp(),
418            from,
419            to,
420            contract_address,
421            ctx.target_tx.inner.effective_gas_price.unwrap_or(0),
422            gas_used,
423            Some(ctx.target_tx.inner.inner.tx_hash()),
424            Some(ctx.block.hash()),
425            ctx.preceding_tx_hashes.len() as u64,
426        );
427
428        Ok(ReplayOutcome {
429            outcome: EvmeOutcome {
430                pre_execution_nonce,
431                exec_result,
432                state: evm_state,
433                exec_time: duration,
434                trace_data,
435            },
436            original_tx: ctx.target_tx.clone(),
437            receipt,
438        })
439    }
440
441    /// Print execution results as JSON (`--json`) or human-readable text.
442    fn output_results(&self, result: &ReplayOutcome) -> Result<()> {
443        trace!("Writing output results");
444        if self.output_args.json {
445            let mut summary = ExecutionSummary::from_result(
446                &result.outcome.exec_result,
447                result.receipt.contract_address,
448            );
449            summary.fill_trace_and_dump(&result.outcome, &self.trace_args, &self.dump_args)?;
450            summary.receipt =
451                Some(serde_json::to_value(&result.receipt).expect("failed to serialize receipt"));
452            println!(
453                "{}",
454                serde_json::to_string_pretty(&summary).expect("failed to serialize output")
455            );
456        } else {
457            print_execution_summary(
458                &result.outcome.exec_result,
459                result.receipt.contract_address,
460                result.outcome.exec_time,
461            );
462            print_receipt(&result.receipt);
463            print_execution_trace(
464                result.outcome.trace_data.as_deref(),
465                self.trace_args.trace_output_file.as_deref(),
466            )?;
467            if self.dump_args.dump {
468                self.dump_args.dump_evm_state(&result.outcome.state)?;
469            }
470        }
471        Ok(())
472    }
473}
474
475/// Build a [`BlockEnv`] from the RPC block header.
476///
477/// Reads `excess_blob_gas` directly from the header rather than using a
478/// hardcoded default, so blob-fee-sensitive opcodes (e.g. `BLOBBASEFEE`)
479/// match on-chain semantics during replay.
480fn retrieve_block_env(block: &Block<Transaction>) -> Result<BlockEnv> {
481    let mut block_env = BlockEnv {
482        number: U256::from(block.number()),
483        beneficiary: block.header.beneficiary(),
484        timestamp: U256::from(block.header.timestamp()),
485        gas_limit: block.header.gas_limit(),
486        basefee: block.header.base_fee_per_gas().unwrap_or_default(),
487        difficulty: block.header.difficulty(),
488        prevrandao: block.header.mix_hash(),
489        blob_excess_gas_and_price: None,
490    };
491
492    let excess_blob_gas = block.header.excess_blob_gas().ok_or_else(|| {
493        ReplayError::Other(format!(
494            "block header missing excess_blob_gas (block {})",
495            block.number()
496        ))
497    })?;
498    block_env.set_blob_excess_gas_and_price(
499        excess_blob_gas,
500        eip4844::BLOB_BASE_FEE_UPDATE_FRACTION_CANCUN,
501    );
502
503    trace!(block_env = ?block_env, "Block environment retrieved");
504    Ok(block_env)
505}
506
507#[cfg(test)]
508mod tests {
509    use super::*;
510    use alloy_consensus::Header as ConsensusHeader;
511    use alloy_rpc_types_eth::Header as RpcHeader;
512    use mega_evm::revm::context_interface::block::BlobExcessGasAndPrice;
513
514    fn make_block(excess_blob_gas: Option<u64>) -> Block<Transaction> {
515        let inner = ConsensusHeader { excess_blob_gas, ..Default::default() };
516        Block::empty(RpcHeader::new(inner))
517    }
518
519    #[test]
520    fn test_retrieve_block_env_sets_blob_fee_from_header() {
521        let excess_blob_gas: u64 = 786_432;
522        let block = make_block(Some(excess_blob_gas));
523
524        let env = retrieve_block_env(&block).expect("should build block env");
525
526        let expected = BlobExcessGasAndPrice::new(
527            excess_blob_gas,
528            eip4844::BLOB_BASE_FEE_UPDATE_FRACTION_CANCUN,
529        );
530        assert_eq!(env.blob_excess_gas_and_price, Some(expected));
531    }
532
533    #[test]
534    fn test_retrieve_block_env_zero_excess_blob_gas_yields_min_price() {
535        let block = make_block(Some(0));
536
537        let env = retrieve_block_env(&block).expect("should build block env");
538
539        let blob = env.blob_excess_gas_and_price.expect("blob fields populated");
540        assert_eq!(blob.excess_blob_gas, 0);
541        assert_eq!(blob.blob_gasprice, u128::from(eip4844::MIN_BLOB_GASPRICE));
542    }
543
544    #[test]
545    fn test_retrieve_block_env_missing_excess_blob_gas_errors() {
546        let block = make_block(None);
547
548        let err = retrieve_block_env(&block).expect_err("should reject pre-Cancun header");
549        match err {
550            ReplayError::Other(msg) => assert!(
551                msg.contains("excess_blob_gas"),
552                "error should mention missing field, got: {msg}"
553            ),
554            other => panic!("unexpected error variant: {other:?}"),
555        }
556    }
557}