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#[derive(Parser, Debug)]
39pub struct Cmd {
40 #[arg(value_name = "TX_HASH")]
42 pub tx_hash: B256,
43
44 #[command(flatten)]
46 pub rpc_args: super::RpcArgs,
47
48 #[command(flatten)]
50 pub ext_args: run::ExtEnvArgs,
51
52 #[command(flatten)]
54 pub dump_args: run::StateDumpArgs,
55
56 #[command(flatten)]
58 pub trace_args: run::TraceArgs,
59
60 #[arg(long = "override.spec", value_name = "SPEC")]
62 pub spec_override: Option<String>,
63
64 #[command(flatten)]
66 pub tx_override_args: TxOverrideArgs,
67
68 #[command(flatten)]
70 pub output_args: run::OutputArgs,
71
72 #[arg(long = "dump-fixture", value_name = "FILE")]
83 pub dump_fixture: Option<std::path::PathBuf>,
84}
85
86struct ProviderContext {
89 provider: crate::common::OpProvider,
90 cache_store: RpcCacheStore,
91 external_env: Option<ExternalEnvSnapshot>,
92 chain_id: u64,
93}
94
95pub(super) struct ReplayOutcome {
97 pub outcome: EvmeOutcome,
99 pub receipt: OpTxReceipt,
101 pub fixture: Option<super::fixture::FixtureDraft>,
103}
104
105struct 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 pub async fn run(&self) -> Result<()> {
117 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 let run_result = self.execute_and_report(&pctx.provider, &rctx, external_envs).await;
148
149 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 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 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 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 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 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 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 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 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 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 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 let fixture_inputs = if self.dump_fixture.is_some() {
383 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 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 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 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 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 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 block_executor.clear_accessed_block_hashes();
508
509 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 let fixture = match fixture_inputs {
555 Some((mega_env, anchor)) => {
556 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 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
663fn 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}