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#[derive(Parser, Debug)]
38pub struct Cmd {
39 #[arg(value_name = "TX_HASH")]
41 pub tx_hash: B256,
42
43 #[command(flatten)]
45 pub rpc_args: super::RpcArgs,
46
47 #[command(flatten)]
49 pub ext_args: run::ExtEnvArgs,
50
51 #[command(flatten)]
53 pub dump_args: run::StateDumpArgs,
54
55 #[command(flatten)]
57 pub trace_args: run::TraceArgs,
58
59 #[arg(long = "override.spec", value_name = "SPEC")]
61 pub spec_override: Option<String>,
62
63 #[command(flatten)]
65 pub tx_override_args: TxOverrideArgs,
66
67 #[command(flatten)]
69 pub output_args: run::OutputArgs,
70}
71
72struct ProviderContext {
75 provider: crate::common::OpProvider,
76 cache_store: RpcCacheStore,
77 external_env: Option<ExternalEnvSnapshot>,
78 chain_id: u64,
79}
80
81#[allow(dead_code)]
83pub(super) struct ReplayOutcome {
84 pub outcome: EvmeOutcome,
86 pub original_tx: Transaction,
88 pub receipt: OpTxReceipt,
90}
91
92struct 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 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 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 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 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 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 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 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 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 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 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 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 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
475fn 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}