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 alloy_network::ReceiptResponse;
23use op_alloy_rpc_types::Transaction;
24
25use crate::{
26    common::{
27        op_receipt_to_tx_receipt, parse_bucket_capacity, print_execution_summary,
28        print_execution_trace, print_receipt, BuildProviderOutput, EvmeExternalEnvs, EvmeOutcome,
29        ExecutionSummary, ExternalEnvSnapshot, OpTxReceipt, RpcCacheStore, TxOverrideArgs,
30    },
31    replay::get_hardfork_config,
32    run, ChainArgs, EvmeState,
33};
34
35use super::{ReplayError, Result};
36
37/// Replay a transaction from RPC
38#[derive(Parser, Debug)]
39pub struct Cmd {
40    /// Transaction hash to replay
41    #[arg(value_name = "TX_HASH")]
42    pub tx_hash: B256,
43
44    /// RPC configuration
45    #[command(flatten)]
46    pub rpc_args: super::RpcArgs,
47
48    /// External environment configuration (bucket capacities)
49    #[command(flatten)]
50    pub ext_args: run::ExtEnvArgs,
51
52    /// State dump configuration
53    #[command(flatten)]
54    pub dump_args: run::StateDumpArgs,
55
56    /// Trace configuration
57    #[command(flatten)]
58    pub trace_args: run::TraceArgs,
59
60    /// Override the spec to use (default: auto-detect from chain ID and block timestamp)
61    #[arg(long = "override.spec", value_name = "SPEC")]
62    pub spec_override: Option<String>,
63
64    /// Transaction override configuration
65    #[command(flatten)]
66    pub tx_override_args: TxOverrideArgs,
67
68    /// Output format configuration
69    #[command(flatten)]
70    pub output_args: run::OutputArgs,
71
72    /// Dump a self-validating EEST state-test fixture for the replayed
73    /// transaction to the given file.
74    ///
75    /// The fixture captures the pre-state read closure, block environment,
76    /// transaction, and `MegaETH` external environment, and records `post`
77    /// expectations (state/logs roots, gas, status) computed by the state-test
78    /// runner. Re-running the file through `state-test` self-validates the
79    /// replay, and `state-test --bench` benchmarks it. The dump is rejected
80    /// unless the local replay reproduces the on-chain receipt's gas and success
81    /// status. Incompatible with transaction overrides and `--override.spec`.
82    #[arg(long = "dump-fixture", value_name = "FILE")]
83    pub dump_fixture: Option<std::path::PathBuf>,
84}
85
86/// Resolved provider and associated metadata from `--rpc` / `--rpc.capture-file` /
87/// `--rpc.replay-file` flags.
88struct ProviderContext {
89    provider: crate::common::OpProvider,
90    cache_store: RpcCacheStore,
91    external_env: Option<ExternalEnvSnapshot>,
92    chain_id: u64,
93}
94
95/// Replay-specific execution outcome
96pub(super) struct ReplayOutcome {
97    /// Common execution outcome
98    pub outcome: EvmeOutcome,
99    /// The transaction receipt
100    pub receipt: OpTxReceipt,
101    /// Self-validating fixture draft, present iff `--dump-fixture` was given.
102    pub fixture: Option<super::fixture::FixtureDraft>,
103}
104
105/// Intermediate context fetched from RPC before execution.
106struct ReplayContext {
107    target_tx: Transaction,
108    parent_block: Block<Transaction>,
109    block: Block<Transaction>,
110    chain_id: u64,
111    preceding_tx_hashes: Vec<B256>,
112}
113
114impl Cmd {
115    /// Replay a historical transaction.
116    pub async fn run(&self) -> Result<()> {
117        // Pure input validation — reject before any network/state work. A dumped
118        // fixture must represent the on-chain transaction, so it can neither apply
119        // transaction overrides nor force a spec: both would make the recorded
120        // execution a what-if, not the on-chain one.
121        if self.dump_fixture.is_some() {
122            if self.tx_override_args.has_overrides() {
123                return Err(ReplayError::Other(
124                    "--dump-fixture cannot be combined with transaction overrides (the \
125                     isolated execution would not represent the on-chain transaction)"
126                        .to_string(),
127                ));
128            }
129            if self.spec_override.is_some() {
130                return Err(ReplayError::Other(
131                    "--dump-fixture cannot be combined with --override.spec (the fixture \
132                     must record the spec auto-detected for the on-chain block, not a \
133                     manually forced one)"
134                        .to_string(),
135                ));
136            }
137        }
138
139        let mut pctx = self.resolve_provider().await?;
140        let rctx = self.fetch_replay_context(&pctx.provider, pctx.chain_id).await?;
141        let (external_envs, env_snapshot) = self.resolve_external_envs(&pctx)?;
142
143        // Execute, report, and (for --dump-fixture) finalize/write — but defer
144        // error propagation until the cache store has persisted: in capture mode
145        // an execution or dump-gate failure is exactly the case you'd want to
146        // debug offline, so the captured RPC responses must not be discarded.
147        let run_result = self.execute_and_report(&pctx.provider, &rctx, external_envs).await;
148
149        // Hand the effective external-env snapshot to the store before the final
150        // persist; no-op unless this is a fixture-capture store.
151        if let Some(snapshot) = env_snapshot {
152            pctx.cache_store.set_external_env(snapshot);
153        }
154        let persist_result = pctx.cache_store.persist();
155        match run_result {
156            Ok(()) => Ok(persist_result?),
157            Err(run_err) => {
158                // Surface the original error; a persist failure on top of it is
159                // logged, not propagated, so it cannot mask the root cause.
160                if let Err(persist_err) = persist_result {
161                    warn!(
162                        error = %persist_err,
163                        "Failed to persist RPC cache while handling an earlier error",
164                    );
165                }
166                Err(run_err)
167            }
168        }
169    }
170
171    /// Execute the replay, print the results, and (for `--dump-fixture`)
172    /// finalize and write the fixture.
173    ///
174    /// Split out of [`Self::run`] so the caller can persist the RPC cache store
175    /// regardless of which of these steps fails.
176    async fn execute_and_report<P>(
177        &self,
178        provider: &P,
179        rctx: &ReplayContext,
180        external_envs: EvmeExternalEnvs,
181    ) -> Result<()>
182    where
183        P: Provider<op_alloy_network::Optimism> + Clone + std::fmt::Debug,
184    {
185        let result = self.execute(provider, rctx, external_envs).await?;
186        self.output_results(&result)?;
187        // Write the self-validating fixture (re-executes the isolated unit through
188        // state-test and cross-checks it against the replay before writing).
189        if let (Some(path), Some(draft)) = (&self.dump_fixture, result.fixture) {
190            super::fixture::finalize_and_write(draft, path)?;
191            info!(path = %path.display(), "Wrote self-validating fixture");
192        }
193        Ok(())
194    }
195
196    /// Select the right provider based on `--rpc`, `--rpc.capture-file`, and
197    /// `--rpc.replay-file` flags.
198    async fn resolve_provider(&self) -> Result<ProviderContext> {
199        let output = if let Some(path) = &self.rpc_args.capture_file {
200            info!(path = %path.display(), "Provider mode: capture to cache file");
201            self.rpc_args.build_capture_provider().await?
202        } else if let Some(path) = &self.rpc_args.replay_file {
203            if !self.ext_args.bucket_capacity.is_empty() {
204                return Err(ReplayError::Other(
205                    "'--bucket-capacity' cannot be used in offline replay mode \
206                         (bucket capacities come from the fixture envelope)"
207                        .to_string(),
208                ));
209            }
210            info!(path = %path.display(), "Provider mode: offline replay from cache file");
211            self.rpc_args.build_replay_provider().await?
212        } else if let Some(rpc) = &self.rpc_args.rpc_url {
213            info!(rpc = %rpc, "Provider mode: online RPC");
214            self.rpc_args.build_provider().await?
215        } else {
216            return Err(ReplayError::Other(
217                "'mega-evme replay' requires '--rpc <URL>', '--rpc.capture-file <PATH>', \
218                 or '--rpc.replay-file <PATH>'"
219                    .to_string(),
220            ));
221        };
222
223        let BuildProviderOutput { provider, cache_store, chain_id, external_env } = output;
224        Ok(ProviderContext { provider, cache_store, external_env, chain_id })
225    }
226
227    /// Fetch the transaction, its block, and preceding transaction hashes from the provider.
228    async fn fetch_replay_context<P>(&self, provider: &P, chain_id: u64) -> Result<ReplayContext>
229    where
230        P: Provider<op_alloy_network::Optimism>,
231    {
232        info!(tx_hash = %self.tx_hash, "Fetching transaction");
233        let target_tx = provider
234            .get_transaction_by_hash(self.tx_hash)
235            .await
236            .map_err(|e| ReplayError::RpcError(format!("Failed to fetch transaction: {e}")))?
237            .ok_or_else(|| ReplayError::TransactionNotFound(self.tx_hash))?;
238        debug!(block_number = ?target_tx.block_number, "Transaction found");
239
240        let (state_base_block, block_number, is_pending) = if let Some(n) = target_tx.block_number {
241            (n - 1, n, false)
242        } else {
243            let latest = provider
244                .get_block_number()
245                .await
246                .map_err(|e| ReplayError::RpcError(format!("RPC transport error: {e}")))?;
247            (latest, latest, true)
248        };
249        debug!(
250            state_base_block = state_base_block,
251            block = block_number,
252            is_pending,
253            "Block numbers determined",
254        );
255
256        let parent_block = provider
257            .get_block_by_number(state_base_block.into())
258            .await
259            .map_err(|e| ReplayError::RpcError(format!("RPC transport error: {e}")))?
260            .ok_or(ReplayError::BlockNotFound(state_base_block))?;
261        let block = provider
262            .get_block_by_number(block_number.into())
263            .await
264            .map_err(|e| ReplayError::RpcError(format!("RPC transport error: {e}")))?
265            .ok_or(ReplayError::BlockNotFound(block_number))?;
266
267        let mut preceding_tx_hashes = vec![];
268        if !is_pending {
269            for hash in block.transactions.hashes() {
270                if hash == self.tx_hash {
271                    break;
272                }
273                preceding_tx_hashes.push(hash);
274            }
275        }
276
277        debug!(chain_id, preceding_count = preceding_tx_hashes.len(), "Replay context ready");
278
279        Ok(ReplayContext { target_tx, parent_block, block, chain_id, preceding_tx_hashes })
280    }
281
282    /// Build the external environment and (for capture mode) the envelope snapshot.
283    ///
284    /// Parses `--bucket-capacity` exactly once: the parsed values feed both the
285    /// runtime `EvmeExternalEnvs` and the `ExternalEnvSnapshot` for envelope persistence.
286    fn resolve_external_envs(
287        &self,
288        pctx: &ProviderContext,
289    ) -> Result<(EvmeExternalEnvs, Option<ExternalEnvSnapshot>)> {
290        if self.rpc_args.replay_file.is_some() {
291            let mut envs = EvmeExternalEnvs::new();
292            if let Some(snapshot) = &pctx.external_env {
293                debug!(
294                    bucket_count = snapshot.bucket_capacities.len(),
295                    "Using bucket capacities from replay envelope",
296                );
297                for &(bucket_id, capacity) in &snapshot.bucket_capacities {
298                    envs = envs.with_bucket_capacity(bucket_id, capacity);
299                }
300            }
301            return Ok((envs, None));
302        }
303
304        // Online / capture: parse bucket capacities once.
305        let parsed: Vec<(u32, u64)> = self
306            .ext_args
307            .bucket_capacity
308            .iter()
309            .map(|s| parse_bucket_capacity(s))
310            .collect::<std::result::Result<_, _>>()?;
311
312        // Determine the effective capacities: CLI values take precedence,
313        // then the previous envelope's values (refresh without --bucket-capacity),
314        // then empty (defaults to MIN_BUCKET_SIZE).
315        let effective = if !parsed.is_empty() {
316            parsed
317        } else if let Some(prev) = &pctx.external_env {
318            prev.bucket_capacities.clone()
319        } else {
320            vec![]
321        };
322
323        let mut envs = EvmeExternalEnvs::new();
324        for &(id, cap) in &effective {
325            envs = envs.with_bucket_capacity(id, cap);
326        }
327        debug!(
328            bucket_count = effective.len(),
329            from_cli = !self.ext_args.bucket_capacity.is_empty(),
330            "Resolved bucket capacities for online/capture mode",
331        );
332
333        // Build the envelope snapshot only in capture mode.
334        let snapshot = self
335            .rpc_args
336            .capture_file
337            .is_some()
338            .then_some(ExternalEnvSnapshot { bucket_capacities: effective });
339
340        Ok((envs, snapshot))
341    }
342
343    /// Execute the target transaction (with preceding transactions) and return the outcome.
344    async fn execute<P>(
345        &self,
346        provider: &P,
347        ctx: &ReplayContext,
348        external_envs: EvmeExternalEnvs,
349    ) -> Result<ReplayOutcome>
350    where
351        P: Provider<op_alloy_network::Optimism> + Clone + std::fmt::Debug,
352    {
353        let hardforks = get_hardfork_config(ctx.chain_id);
354        let spec = hardforks.spec_id(ctx.block.header.timestamp());
355        let chain_args = ChainArgs { chain_id: ctx.chain_id, spec: spec.to_string() };
356        debug!(chain_id = ctx.chain_id, spec = %spec, "Chain configuration");
357
358        info!(fork_block = ctx.parent_block.header.number(), "Forking state from parent block",);
359        let mut database = EvmeState::new_forked(
360            provider.clone(),
361            Some(ctx.parent_block.header.number()),
362            Default::default(),
363            Default::default(),
364        )
365        .await?;
366
367        let block_env = retrieve_block_env(&ctx.block)?;
368        trace!(?block_env, "Block environment built");
369        let mut evm_env = EvmEnv::new(chain_args.create_cfg_env()?, block_env);
370
371        // For `--dump-fixture`, snapshot the two inputs a fixture
372        // needs before the external env is moved into the factory: the effective
373        // MegaETH external environment, and the on-chain receipt gas used as the
374        // fidelity anchor. They live or die together (kept in one `Option`), so the
375        // fixture builder never has to assume one without the other.
376        //
377        // The receipt is fetched here (before the executor borrows the database) so
378        // it is captured by `--rpc.capture-file`. A fixture/benchmark is only
379        // meaningful if the local replay reproduces the receipt's gas and success
380        // status — a mismatch means a wrong spec or hardfork config, which
381        // self-validation alone cannot catch.
382        let fixture_inputs = if self.dump_fixture.is_some() {
383            // A pending transaction has no receipt yet, so the fidelity gate cannot
384            // run; fail clearly instead of surfacing the receipt lookup's confusing
385            // `TransactionNotFound`.
386            if ctx.target_tx.block_number.is_none() {
387                return Err(ReplayError::Other(
388                    "--dump-fixture does not support pending transactions: the fidelity \
389                     gate needs the on-chain receipt, which does not exist yet"
390                        .to_string(),
391                ));
392            }
393            // Sort the accessed buckets/oracle slots so the dumped fixture is
394            // byte-reproducible: these come from hash-map iteration, whose order
395            // is otherwise non-deterministic across runs (noisy diffs, and an
396            // online dump would not byte-match an offline re-dump).
397            let mut bucket_capacities = external_envs.bucket_capacities();
398            bucket_capacities.sort_unstable();
399            let mut oracle_storage = external_envs.oracle_storage();
400            oracle_storage.sort_unstable();
401            let mega_env = state_test::types::MegaEnv { bucket_capacities, oracle_storage };
402            let receipt = provider
403                .get_transaction_receipt(self.tx_hash)
404                .await
405                .map_err(|e| ReplayError::RpcError(format!("RPC transport error: {e}")))?
406                .ok_or(ReplayError::TransactionNotFound(self.tx_hash))?;
407            // Anchor the receipt to the replayed block: across a reorg or a
408            // load-balanced endpoint serving divergent views, the receipt can
409            // describe a different inclusion than the block fetched earlier,
410            // and the fidelity gate would then compare the replay against the
411            // wrong on-chain execution.
412            if let Some(receipt_block_hash) = receipt.block_hash() {
413                let replayed_block_hash = ctx.block.hash();
414                if receipt_block_hash != replayed_block_hash {
415                    return Err(ReplayError::Other(format!(
416                        "receipt block hash {receipt_block_hash} != replayed block hash \
417                         {replayed_block_hash}: the receipt describes a different inclusion \
418                         than the fetched block (reorg in progress, or a load-balanced \
419                         endpoint serving divergent views); retry the dump once the chain \
420                         settles"
421                    )));
422                }
423            }
424            // RLP-hash the receipt's logs with the same helper the state-test
425            // runner uses for `logsRoot`, so the dump can check the replay's logs
426            // against the chain (the rich RPC logs' `inner` is the consensus log).
427            let receipt_logs: Vec<_> =
428                receipt.inner.logs().iter().map(|log| log.inner.clone()).collect();
429            let anchor = super::fixture::OnchainAnchor {
430                gas_used: receipt.gas_used(),
431                success: receipt.inner.status(),
432                logs_root: state_test::utils::log_rlp_hash(&receipt_logs),
433            };
434            Some((mega_env, anchor))
435        } else {
436            None
437        };
438
439        let evm_factory = MegaEvmFactory::new().with_external_env_factory(external_envs);
440        let block_executor_factory = MegaBlockExecutorFactory::new(
441            &hardforks,
442            evm_factory,
443            OpAlloyReceiptBuilder::default(),
444        );
445        let mut block_limits = BlockLimits::from_hardfork_and_block_gas_limit(
446            hardforks.hardfork(ctx.block.header.timestamp()).ok_or(ReplayError::Other(format!(
447                "No `MegaHardfork` active at block timestamp: {}",
448                ctx.block.header.timestamp()
449            )))?,
450            ctx.block.header.gas_limit(),
451        );
452
453        if let Some(spec_override) = &self.spec_override {
454            info!(spec_override = %spec_override, "Overriding EVM spec");
455            let spec = MegaSpecId::from_str(spec_override)
456                .map_err(|e| ReplayError::Other(format!("Invalid spec: {e:?}")))?;
457            evm_env.cfg_env.spec = spec;
458            block_limits = block_limits.with_tx_runtime_limits(EvmTxRuntimeLimits::from_spec(spec));
459        }
460
461        // The spec the target transaction will execute under (after any override),
462        // captured before `evm_env` is moved into the executor.
463        let executed_spec = evm_env.cfg_env.spec;
464
465        let block_ctx = MegaBlockExecutionCtx::new(
466            ctx.parent_block.hash(),
467            ctx.block.header.parent_beacon_block_root(),
468            ctx.block.header.extra_data().clone(),
469            block_limits,
470        );
471
472        let start = Instant::now();
473        let mut inspector = self.trace_args.create_inspector();
474        let mut state =
475            StateBuilder::new().with_database(&mut database).with_bundle_update().build();
476        let mut block_executor = block_executor_factory.create_executor_with_inspector(
477            &mut state,
478            block_ctx,
479            evm_env,
480            &mut inspector,
481        );
482
483        block_executor
484            .apply_pre_execution_changes()
485            .map_err(|e| ReplayError::Other(format!("Block execution error: {e}")))?;
486
487        // Execute preceding transactions
488        info!(preceding_count = ctx.preceding_tx_hashes.len(), "Executing preceding transactions",);
489        for tx_hash in &ctx.preceding_tx_hashes {
490            debug!(tx_hash = %tx_hash, "Executing preceding transaction");
491            let tx = provider
492                .get_transaction_by_hash(*tx_hash)
493                .await
494                .map_err(|e| ReplayError::RpcError(format!("RPC transport error: {e}")))?
495                .ok_or(ReplayError::TransactionNotFound(*tx_hash))?;
496            let outcome = block_executor
497                .run_transaction(tx.as_recovered())
498                .map_err(|e| ReplayError::Other(format!("Block execution error: {e}")))?;
499            trace!(tx_hash = %tx_hash, ?outcome, "Preceding transaction executed");
500            block_executor
501                .commit_transaction_outcome(outcome)
502                .map_err(|e| ReplayError::Other(format!("Block execution error: {e}")))?;
503        }
504
505        // Clear block hash reads accumulated by the preceding transactions so the
506        // fixture gate below sees only the target transaction's BLOCKHASH reads.
507        block_executor.clear_accessed_block_hashes();
508
509        // Execute target transaction. Override-incompatibility with
510        // --dump-fixture is validated up front in `run()`.
511        info!("Executing target transaction");
512        if self.tx_override_args.has_overrides() {
513            info!(overrides = ?self.tx_override_args, "Applying transaction overrides");
514        }
515        let wrapped_tx = self.tx_override_args.wrap(ctx.target_tx.as_recovered())?;
516        let pre_execution_nonce = block_executor
517            .evm()
518            .db_ref()
519            .basic_ref(wrapped_tx.inner().signer())?
520            .map(|acc| acc.nonce)
521            .unwrap_or(0);
522
523        block_executor.inspector_mut().fuse();
524        let outcome = block_executor
525            .run_transaction(wrapped_tx)
526            .map_err(|e| ReplayError::Other(format!("Block execution error: {e}")))?;
527        trace!(tx_hash = %ctx.target_tx.inner.inner.tx_hash(), ?outcome, "Target transaction executed");
528        let exec_result = outcome.inner.result.clone();
529        let evm_state = outcome.inner.state.clone();
530
531        match &exec_result {
532            ExecutionResult::Success { gas_used, .. } => info!(gas_used, "Execution succeeded"),
533            ExecutionResult::Revert { gas_used, .. } => warn!(gas_used, "Execution reverted"),
534            ExecutionResult::Halt { reason, gas_used } => {
535                warn!(?reason, gas_used, "Execution halted")
536            }
537        }
538
539        let result_and_state = mega_evm::revm::context::result::ResultAndState {
540            result: exec_result.clone(),
541            state: evm_state.clone(),
542        };
543
544        let trace_data = self.trace_args.is_tracing_enabled().then(|| {
545            self.trace_args.generate_trace(
546                block_executor.inspector(),
547                &result_and_state,
548                block_executor.evm().db_ref(),
549            )
550        });
551
552        // Build the self-validating fixture draft while the database still reflects
553        // the pre-target-transaction state (preceding txs committed, target not yet).
554        let fixture = match fixture_inputs {
555            Some((mega_env, anchor)) => {
556                // A dumped fixture cannot faithfully reproduce BLOCKHASH: the
557                // state-test runner does not seed block hashes, so the isolated
558                // re-execution would read default hashes instead of the ones this
559                // replay observed. The access record is cleared after the preceding
560                // transactions, so it holds exactly the target transaction's reads;
561                // if the target read any block hash, refuse to dump rather than
562                // write a fixture that self-validates against the wrong roots.
563                let accessed_block_hashes = block_executor.get_accessed_block_hashes();
564                if !accessed_block_hashes.is_empty() {
565                    return Err(ReplayError::Other(format!(
566                        "--dump-fixture does not support transactions that read block \
567                         hashes (BLOCKHASH): {} block hash(es) were accessed and the \
568                         fixture cannot faithfully reproduce them",
569                        accessed_block_hashes.len()
570                    )));
571                }
572                Some(super::fixture::build_draft(
573                    block_executor.evm().db_ref(),
574                    &evm_state,
575                    ctx.chain_id,
576                    executed_spec,
577                    &ctx.block,
578                    &ctx.target_tx,
579                    super::fixture::FixtureInputs { mega_env, result: &exec_result, anchor },
580                )?)
581            }
582            None => None,
583        };
584
585        let gas_used = block_executor
586            .commit_transaction_outcome(outcome)
587            .map_err(|e| ReplayError::Other(format!("Block execution error: {e}")))?;
588        let duration = start.elapsed();
589
590        let (evm, block_result) = block_executor
591            .finish()
592            .map_err(|e| ReplayError::Other(format!("Block execution error: {e}")))?;
593        let (db, _) = evm.finish();
594        db.merge_transitions(BundleRetention::Reverts);
595        let receipt_envelope = block_result.receipts.last().unwrap().clone();
596        trace!(?receipt_envelope, "Receipt envelope obtained");
597
598        let from = ctx.target_tx.inner.inner.signer();
599        let to = ctx.target_tx.inner.inner.to();
600        let contract_address = (to.is_none() && receipt_envelope.is_success())
601            .then(|| from.create(pre_execution_nonce));
602        let receipt = op_receipt_to_tx_receipt(
603            &receipt_envelope,
604            ctx.block.number(),
605            ctx.block.header.timestamp(),
606            from,
607            to,
608            contract_address,
609            ctx.target_tx.inner.effective_gas_price.unwrap_or(0),
610            gas_used,
611            Some(ctx.target_tx.inner.inner.tx_hash()),
612            Some(ctx.block.hash()),
613            ctx.preceding_tx_hashes.len() as u64,
614        );
615
616        Ok(ReplayOutcome {
617            outcome: EvmeOutcome {
618                pre_execution_nonce,
619                exec_result,
620                state: evm_state,
621                exec_time: duration,
622                trace_data,
623            },
624            receipt,
625            fixture,
626        })
627    }
628
629    /// Print execution results as JSON (`--json`) or human-readable text.
630    fn output_results(&self, result: &ReplayOutcome) -> Result<()> {
631        trace!("Writing output results");
632        if self.output_args.json {
633            let mut summary = ExecutionSummary::from_result(
634                &result.outcome.exec_result,
635                result.receipt.contract_address,
636            );
637            summary.fill_trace_and_dump(&result.outcome, &self.trace_args, &self.dump_args)?;
638            summary.receipt =
639                Some(serde_json::to_value(&result.receipt).expect("failed to serialize receipt"));
640            println!(
641                "{}",
642                serde_json::to_string_pretty(&summary).expect("failed to serialize output")
643            );
644        } else {
645            print_execution_summary(
646                &result.outcome.exec_result,
647                result.receipt.contract_address,
648                result.outcome.exec_time,
649            );
650            print_receipt(&result.receipt);
651            print_execution_trace(
652                result.outcome.trace_data.as_deref(),
653                self.trace_args.trace_output_file.as_deref(),
654            )?;
655            if self.dump_args.dump {
656                self.dump_args.dump_evm_state(&result.outcome.state)?;
657            }
658        }
659        Ok(())
660    }
661}
662
663/// Build a [`BlockEnv`] from the RPC block header.
664///
665/// Reads `excess_blob_gas` directly from the header rather than using a
666/// hardcoded default, so blob-fee-sensitive opcodes (e.g. `BLOBBASEFEE`)
667/// match on-chain semantics during replay.
668fn retrieve_block_env(block: &Block<Transaction>) -> Result<BlockEnv> {
669    let mut block_env = BlockEnv {
670        number: U256::from(block.number()),
671        beneficiary: block.header.beneficiary(),
672        timestamp: U256::from(block.header.timestamp()),
673        gas_limit: block.header.gas_limit(),
674        basefee: block.header.base_fee_per_gas().unwrap_or_default(),
675        difficulty: block.header.difficulty(),
676        prevrandao: block.header.mix_hash(),
677        blob_excess_gas_and_price: None,
678    };
679
680    let excess_blob_gas = block.header.excess_blob_gas().ok_or_else(|| {
681        ReplayError::Other(format!(
682            "block header missing excess_blob_gas (block {})",
683            block.number()
684        ))
685    })?;
686    block_env.set_blob_excess_gas_and_price(
687        excess_blob_gas,
688        eip4844::BLOB_BASE_FEE_UPDATE_FRACTION_CANCUN,
689    );
690
691    trace!(block_env = ?block_env, "Block environment retrieved");
692    Ok(block_env)
693}
694
695#[cfg(test)]
696mod tests {
697    use super::*;
698    use alloy_consensus::Header as ConsensusHeader;
699    use alloy_rpc_types_eth::Header as RpcHeader;
700    use mega_evm::revm::context_interface::block::BlobExcessGasAndPrice;
701
702    fn make_block(excess_blob_gas: Option<u64>) -> Block<Transaction> {
703        let inner = ConsensusHeader { excess_blob_gas, ..Default::default() };
704        Block::empty(RpcHeader::new(inner))
705    }
706
707    #[test]
708    fn test_retrieve_block_env_sets_blob_fee_from_header() {
709        let excess_blob_gas: u64 = 786_432;
710        let block = make_block(Some(excess_blob_gas));
711
712        let env = retrieve_block_env(&block).expect("should build block env");
713
714        let expected = BlobExcessGasAndPrice::new(
715            excess_blob_gas,
716            eip4844::BLOB_BASE_FEE_UPDATE_FRACTION_CANCUN,
717        );
718        assert_eq!(env.blob_excess_gas_and_price, Some(expected));
719    }
720
721    #[test]
722    fn test_retrieve_block_env_zero_excess_blob_gas_yields_min_price() {
723        let block = make_block(Some(0));
724
725        let env = retrieve_block_env(&block).expect("should build block env");
726
727        let blob = env.blob_excess_gas_and_price.expect("blob fields populated");
728        assert_eq!(blob.excess_blob_gas, 0);
729        assert_eq!(blob.blob_gasprice, u128::from(eip4844::MIN_BLOB_GASPRICE));
730    }
731
732    #[test]
733    fn test_retrieve_block_env_missing_excess_blob_gas_errors() {
734        let block = make_block(None);
735
736        let err = retrieve_block_env(&block).expect_err("should reject pre-Cancun header");
737        match err {
738            ReplayError::Other(msg) => assert!(
739                msg.contains("excess_blob_gas"),
740                "error should mention missing field, got: {msg}"
741            ),
742            other => panic!("unexpected error variant: {other:?}"),
743        }
744    }
745}