1use super::{MAX_CONCURRENT_RPC_REQUESTS, fetch_code_via_rpc};
2use crate::{
3 debug::{ensure_remote_trace_context_unchanged, handle_traces, resolve_remote_trace_hardfork},
4 evm_version::probe_evm_version,
5 rpc_trace::{
6 call_frame_to_arena, call_tracer_config, is_method_not_found_error, is_missing_state_error,
7 is_missing_state_message,
8 },
9 traces::TraceKind,
10 utils::{
11 apply_chain_and_block_specific_env_changes_for_chain,
12 apply_chain_specific_tx_replay_env_changes_for_chain, block_env_from_header,
13 },
14};
15use alloy_consensus::{BlockHeader, Transaction, transaction::SignerRecoverable};
16use alloy_eips::{BlockNumHash, eip7928::compute_block_access_list_hash};
17use alloy_network::{
18 AnyNetwork, AnyRpcBlock, AnyRpcTransaction, AnyTransactionReceipt, AnyTxEnvelope,
19 BlockResponse, Network, ReceiptResponse, TransactionResponse, primitives::HeaderResponse,
20};
21use alloy_primitives::{
22 Address, B256, Bytes, U256,
23 map::{AddressHashMap, AddressSet, B256HashMap},
24};
25use alloy_provider::{Provider, ext::DebugApi};
26use alloy_rpc_types::{
27 BlockId, BlockTransactions,
28 trace::geth::{CallFrame, GethDebugTracingOptions, GethTrace, PreStateConfig},
29};
30use alloy_transport::TransportError;
31use clap::Parser;
32use eyre::{Result, WrapErr};
33use foundry_cli::{
34 opts::{EtherscanOpts, RpcOpts, TracingArgs},
35 utils::{TraceResult, init_progress},
36};
37use foundry_common::{
38 SYSTEM_TRANSACTION_TYPE, is_known_system_sender,
39 provider::{ProviderBuilder, RetryProvider},
40 shell,
41};
42use foundry_compilers::artifacts::EvmVersion;
43use foundry_config::{
44 Config, TracingConfig,
45 figment::{
46 self, Metadata, Profile,
47 value::{Dict, Map},
48 },
49};
50use foundry_evm::{
51 core::{
52 FoundryBlock as _, FoundryTransaction as _,
53 env::FromAnyRpcTransaction as _,
54 evm::{EthEvmNetwork, EvmEnvFor, FoundryEvmNetwork, TempoEvmNetwork, TxEnvFor},
55 },
56 executors::{Executor, ExecutorBuilder, TracingExecutor},
57 hardforks::FoundryHardfork,
58 opts::EvmOpts,
59 traces::{InternalTraceMode, SparsedTraceArena, TraceContext, TraceRequirements},
60};
61use foundry_evm_networks::{NetworkConfigs, NetworkVariant};
62use futures::{StreamExt, TryFutureExt};
63use revm::{
64 DatabaseRef,
65 context::Block,
66 primitives::hardfork::SpecId,
67 state::bal::{Bal, BlockAccessIndex},
68};
69use std::sync::Arc;
70
71#[cfg(feature = "base")]
72use foundry_evm::core::evm::BaseEvmNetwork;
73
74#[cfg(feature = "monad")]
75use foundry_evm::core::evm::{BlockContext, MonadEvmNetwork};
76#[cfg(feature = "monad")]
77use revm::context::TxEnv;
78
79#[cfg(feature = "optimism")]
80use foundry_evm::core::evm::OpEvmNetwork;
81
82const REMOTE_TRACE_HINT: &str = "`--debug-trace-transaction` renders the node's own trace instead";
84
85#[derive(Clone, Debug, Parser)]
87pub struct RunArgs {
88 tx_hash: String,
90
91 #[arg(long, short)]
93 debug: bool,
94
95 #[arg(long, short)]
97 trace_printer: bool,
98
99 #[arg(long)]
103 quick: bool,
104
105 #[arg(long)]
111 no_bal: bool,
112
113 #[arg(long, alias = "sys")]
115 replay_system_txes: bool,
116
117 #[arg(long, default_value_t = false)]
124 prestate_tracer: bool,
125
126 #[arg(
137 long,
138 default_value_t = false,
139 conflicts_with_all = ["debug", "decode_internal", "trace_printer", "quick", "no_bal", "prestate_tracer", "evm_version"]
140 )]
141 debug_trace_transaction: bool,
142
143 #[command(flatten)]
144 tracing: TracingArgs,
145
146 #[arg(short = 'l', value_name = "ADDRESS:LABEL", hide = true)]
148 legacy_labels: Vec<String>,
149
150 #[command(flatten)]
151 etherscan: EtherscanOpts,
152
153 #[command(flatten)]
154 rpc: RpcOpts,
155
156 #[arg(long)]
160 evm_version: Option<EvmVersion>,
161
162 #[arg(long, visible_alias = "la")]
164 pub with_local_artifacts: bool,
165
166 #[arg(long)]
170 pub disable_block_gas_limit: bool,
171
172 #[arg(long)]
174 pub enable_tx_gas_limit: bool,
175}
176
177struct TargetFetch {
180 tx: AnyRpcTransaction,
181 provider: RetryProvider,
182 compute_units_per_second: Option<u64>,
183}
184
185#[cfg(feature = "monad")]
187struct MonadPrepared {
188 tx_block_number: u64,
189 compute_units_per_second: Option<u64>,
190}
191
192enum ReplayGasLimits {
194 Unchanged,
195 Nitro(B256HashMap<u64>),
196}
197
198struct PreparedRun<FEN: FoundryEvmNetwork> {
201 args: RunArgs,
202 config: Box<Config>,
203 tracing: TracingConfig,
204 tx: AnyRpcTransaction,
205 block: Option<AnyRpcBlock>,
206 evm_env: EvmEnvFor<FEN>,
207 executor: TracingExecutor<FEN>,
208 trace_context: TraceContext,
209 prestate_applied: bool,
210 block_access_list: Option<Arc<Bal>>,
212 replay_gas_limits: ReplayGasLimits,
213 receipt: Option<AnyTransactionReceipt>,
215 #[cfg(feature = "monad")]
216 monad: MonadPrepared,
217}
218
219impl RunArgs {
220 fn resolve_tracing(&self, config: &TracingConfig, verbosity: u8) -> TracingConfig {
221 if self.debug_trace_transaction {
222 self.tracing.resolve_call_tracer(config, verbosity)
223 } else {
224 self.tracing.resolve(config, verbosity)
225 }
226 }
227
228 fn configure_tracing(&mut self, config: &mut Config, evm_opts: &EvmOpts) -> TracingConfig {
231 config.networks = evm_opts.networks;
232 self.tracing.labels.append(&mut self.legacy_labels);
233 config.tracing = self.resolve_tracing(&config.tracing, shell::verbosity());
234 config.tracing.clone()
235 }
236
237 pub async fn run(self) -> Result<()> {
244 let figment = self.rpc.clone().into_figment(self.with_local_artifacts).merge(&self);
245 let (mut config, mut evm_opts) = super::load_cast_config_and_evm_opts(figment)?;
246 if config.eth_rpc_url.is_none()
247 && let Some(chain) = self.etherscan.chain
248 {
249 let alias = chain.to_string();
250 if config.rpc_endpoints.contains_key(&alias) {
251 config.eth_rpc_url = Some(alias);
252 }
253 }
254 evm_opts.fork_url = Some(config.get_rpc_url_or_localhost_http()?.into_owned());
255
256 evm_opts.infer_network_from_fork().await?;
258
259 if self.debug_trace_transaction {
260 return self.remote_trace(config, evm_opts).await;
261 }
262
263 self.replay(config, evm_opts).await.map_err(|err| {
264 if err.chain().any(|cause| is_missing_state_message(&cause.to_string())) {
265 err.wrap_err(
266 "the RPC endpoint does not have the historical state for the transaction's \
267 block; use an archive endpoint",
268 )
269 } else {
270 err
271 }
272 })
273 }
274
275 async fn replay(self, config: Box<Config>, evm_opts: EvmOpts) -> Result<()> {
277 match evm_opts.networks.execution_network() {
278 NetworkVariant::Tempo => {
279 self.run_with_evm(config, evm_opts, ExecutorBuilder::<TempoEvmNetwork>::new()).await
280 }
281 #[cfg(feature = "base")]
282 NetworkVariant::Base => {
283 self.run_with_evm(config, evm_opts, ExecutorBuilder::<BaseEvmNetwork>::new()).await
284 }
285 #[cfg(feature = "monad")]
286 NetworkVariant::Monad => {
287 let target = self.fetch_target(&config).await?;
288 let mut run = self
289 .prepare::<MonadEvmNetwork>(
290 config,
291 evm_opts,
292 target,
293 ExecutorBuilder::<MonadEvmNetwork>::new(),
294 )
295 .await?;
296 let result = run.execute_monad().await?;
297 run.finish(result).await
298 }
299 #[cfg(feature = "optimism")]
300 NetworkVariant::Optimism => {
301 self.run_with_evm(config, evm_opts, ExecutorBuilder::<OpEvmNetwork>::new()).await
302 }
303 NetworkVariant::Ethereum => {
304 self.run_with_evm(config, evm_opts, ExecutorBuilder::<EthEvmNetwork>::new()).await
305 }
306 }
307 }
308
309 async fn run_with_evm<FEN: FoundryEvmNetwork>(
310 self,
311 config: Box<Config>,
312 evm_opts: EvmOpts,
313 executor_builder: ExecutorBuilder<FEN>,
314 ) -> Result<()> {
315 let target = self.fetch_target(&config).await?;
316 let endpoint_is_anvil = evm_opts
317 .fork_endpoint
318 .as_ref()
319 .is_some_and(|identity| identity.reported_hardfork.is_some());
320 if let Some(chain) = target.tx.chain_id().map(alloy_chains::Chain::from_id)
321 && chain.is_elastic()
322 && !endpoint_is_anvil
323 {
324 eyre::bail!(
325 "{chain} executes EraVM bytecode, which cannot be replayed locally; \
326 {REMOTE_TRACE_HINT}"
327 );
328 }
329 if is_system_transaction(&target.tx) && !self.replay_system_txes {
330 eyre::bail!(
331 "{:?} is a system transaction.\nReplaying system transactions is currently not supported.",
332 target.tx.tx_hash()
333 );
334 }
335 let mut run = self.prepare::<FEN>(config, evm_opts, target, executor_builder).await?;
336 let result = run.execute_ordinary()?;
337 run.finish(result).await
338 }
339
340 async fn fetch_target(&self, config: &Config) -> Result<TargetFetch> {
345 let compute_units_per_second = if self.rpc.common.no_rpc_rate_limit {
346 Some(u64::MAX)
347 } else {
348 self.rpc.common.compute_units_per_second
349 };
350 let provider = ProviderBuilder::<AnyNetwork>::from_config(config)?
351 .compute_units_per_second_opt(compute_units_per_second)
352 .build()?;
353 let tx_hash = self.tx_hash.parse().wrap_err("invalid tx hash")?;
354 let tx = provider
355 .get_transaction_by_hash(tx_hash)
356 .await
357 .wrap_err_with(|| format!("tx not found: {tx_hash:?}"))?
358 .ok_or_else(|| eyre::eyre!("tx not found: {tx_hash:?}"))?;
359 ensure_requested_transaction(tx_hash, &tx)?;
360 Ok(TargetFetch { tx, provider, compute_units_per_second })
361 }
362
363 async fn remote_trace(mut self, mut config: Box<Config>, evm_opts: EvmOpts) -> Result<()> {
368 let TargetFetch { tx, provider, .. } = self.fetch_target(&config).await?;
369 let tx_hash = tx.tx_hash();
370 let tracing = self.configure_tracing(&mut config, &evm_opts);
371 let with_local_artifacts = self.with_local_artifacts;
372
373 let endpoint_identity = evm_opts.fork_endpoint_identity().await?;
374 let tx_inclusion = tx
375 .block_hash_num()
376 .ok_or_else(|| eyre::eyre!("tx may still be pending: {:?}", tx_hash))?;
377
378 let frame = call_tracer_frame(
379 provider
380 .debug_trace_transaction(
381 tx_hash,
382 GethDebugTracingOptions::call_tracer(call_tracer_config()),
383 )
384 .await,
385 "debug_traceTransaction",
386 "drop `--debug-trace-transaction` to re-execute the transaction locally",
387 "the transaction's block; use an archive endpoint",
388 )?;
389
390 let receipt = provider
391 .get_transaction_receipt(tx_hash)
392 .await?
393 .ok_or_else(|| eyre::eyre!("tx receipt not found: {:?}", tx_hash))?;
394 ensure_remote_transaction_inclusion(
395 tx_hash,
396 tx_inclusion,
397 receipt.block_hash_num(),
398 "transaction receipt",
399 )?;
400
401 let transaction_block = provider.get_block_by_hash(tx_inclusion.hash).await?;
402 ensure_remote_transaction_inclusion(
403 tx_hash,
404 tx_inclusion,
405 transaction_block.as_ref().map(block_num_hash),
406 "block fetched by hash",
407 )?;
408 let Some(transaction_block) = transaction_block else {
409 unreachable!("ensure_remote_transaction_inclusion errors when the block is missing")
410 };
411
412 let root_create_address = Transaction::to(&tx)
413 .is_none()
414 .then(|| receipt.contract_address().unwrap_or_else(|| tx.from().create(tx.nonce())));
415 let arena = SparsedTraceArena {
416 arena: call_frame_to_arena(&frame, root_create_address),
417 ignored: Default::default(),
418 diagnostics: Default::default(),
419 };
420 let result = TraceResult {
421 success: receipt.status(),
422 traces: Some(vec![(TraceKind::Execution, arena)]),
423 gas_used: receipt.gas_used(),
424 };
425
426 let contracts_bytecode = if with_local_artifacts {
431 fetch_transaction_contracts_bytecode_via_rpc(
432 &provider,
433 &result,
434 tx_hash,
435 BlockId::hash(tx_inclusion.hash),
436 )
437 .await
438 } else {
439 Default::default()
440 };
441
442 let chain = alloy_chains::Chain::from_id(endpoint_identity.source_chain_id);
445 let resolved_hardfork = resolve_remote_trace_hardfork(
446 config.hardfork,
447 &endpoint_identity,
448 Some(transaction_block.header().timestamp()),
449 );
450 let final_endpoint_identity = evm_opts.fork_endpoint_identity().await?;
451 ensure_remote_trace_context_unchanged(&endpoint_identity, &final_endpoint_identity)?;
452
453 let current_tx = provider.get_transaction_by_hash(tx_hash).await?;
454 if let Some(current_tx) = ¤t_tx {
455 ensure_requested_transaction(tx_hash, current_tx)?;
456 }
457 ensure_remote_transaction_inclusion(
458 tx_hash,
459 tx_inclusion,
460 current_tx.and_then(|tx| tx.block_hash_num()),
461 "transaction lookup",
462 )?;
463 let canonical_block = provider.get_block_by_number(tx_inclusion.number.into()).await?;
464 ensure_remote_transaction_inclusion(
465 tx_hash,
466 tx_inclusion,
467 canonical_block.as_ref().map(block_num_hash),
468 "canonical block lookup",
469 )?;
470 handle_traces(
471 result,
472 &config,
473 {
474 let context =
475 TraceContext::new(chain, endpoint_identity.network_profile, resolved_hardfork);
476 context.with_hardfork(context.decoding_hardfork(&config))
477 },
478 &contracts_bytecode,
479 &tracing,
480 with_local_artifacts,
481 false,
482 )
483 .await
484 }
485
486 async fn prepare<FEN: FoundryEvmNetwork>(
487 mut self,
488 mut config: Box<Config>,
489 evm_opts: EvmOpts,
490 target: TargetFetch,
491 executor_builder: ExecutorBuilder<FEN>,
492 ) -> Result<PreparedRun<FEN>> {
493 #[cfg_attr(not(feature = "monad"), allow(unused_variables))]
494 let TargetFetch { tx, provider, compute_units_per_second } = target;
495 let tx_hash = tx.tx_hash();
496 let tracing = self.configure_tracing(&mut config, &evm_opts);
497
498 let tx_block_number = tx
499 .block_number()
500 .ok_or_else(|| eyre::eyre!("tx may still be pending: {:?}", tx_hash))?;
501
502 config.fork_block_number = Some(tx_block_number - 1);
504
505 let create2_deployer = evm_opts.create2_deployer;
506 let (block, receipt, mut fork) = tokio::try_join!(
507 provider.get_block(tx_block_number.into()).full().into_future().map_err(Into::into),
509 async { Ok(provider.get_transaction_receipt(tx_hash).await.ok().flatten()) },
512 TracingExecutor::<FEN>::get_fork(&mut config, evm_opts)
513 )?;
514 let chain = fork.context().chain();
515 let networks = fork.context().networks();
516
517 let mut evm_version = self.evm_version;
518 fork.evm_env.cfg_env.disable_block_gas_limit = true;
523
524 if !self.enable_tx_gas_limit {
527 fork.evm_env.cfg_env.tx_gas_limit_cap = Some(u64::MAX);
528 }
529
530 fork.evm_env.cfg_env.limit_contract_code_size = None;
531 fork.evm_env.block_env.set_number(U256::from(tx_block_number));
532
533 let mut parent_beacon_block_root = None;
534 if let Some(block) = &block {
535 fork.evm_env.block_env = block_env_from_header(block.header());
536 parent_beacon_block_root = block.header().parent_beacon_block_root();
537
538 if evm_version.is_none()
543 && config.hardfork.is_none()
544 && FoundryHardfork::from_chain_and_timestamp(chain.id(), block.header().timestamp())
545 .is_none()
546 {
547 let probed = probe_evm_version(&provider, BlockId::number(tx_block_number)).await;
548 evm_version = probed.or_else(|| {
549 block.header().excess_blob_gas().is_some().then_some(EvmVersion::Cancun)
550 });
551 }
552 apply_chain_and_block_specific_env_changes_for_chain::<AnyNetwork, _, _>(
553 &mut fork.evm_env,
554 block,
555 chain.id(),
556 config.networks,
557 );
558 }
559 fork.resolve_spec(&config, evm_version);
560 fork.extend_precompile_labels(&mut config);
561
562 apply_chain_specific_tx_replay_env_changes_for_chain(&mut fork.evm_env, chain.id());
563
564 let trace_context = fork.context();
565 let mut evm_env = fork.evm_env.clone();
566
567 let mut executor = fork.into_executor(
568 executor_builder,
569 TraceRequirements::none(),
570 create2_deployer,
571 None,
572 )?;
573 executor.backend_mut().set_fork_block_number_override(tx_block_number);
577
578 evm_env.cfg_env.set_spec_and_mainnet_gas_params(executor.spec_id());
579
580 let spec_id = (*evm_env.cfg_env.spec()).into();
581
582 if let Some(parent_beacon_block_root) =
583 parent_beacon_block_root_for_network(networks, spec_id, parent_beacon_block_root)
584 {
585 executor.apply_beacon_root(parent_beacon_block_root)?;
586 }
587
588 let mut prestate_applied = false;
596 if !self.quick && self.prestate_tracer {
597 trace!(?tx_hash, "attempting to fetch prestate via debug_traceTransaction");
598 match provider
599 .debug_trace_transaction(
600 tx_hash,
601 GethDebugTracingOptions::prestate_tracer(PreStateConfig::default()),
602 )
603 .await
604 {
605 Ok(trace) => match trace.try_into_pre_state_frame() {
606 Ok(pre_state_frame) => {
607 executor.apply_prestate_trace(pre_state_frame.into_pre_state())?;
608 prestate_applied = true;
609 trace!("prestate trace applied successfully, skipping block replay");
610 }
611 Err(err) => {
612 trace!(%err, "failed to parse prestate trace response");
613 }
614 },
615 Err(err) => {
616 trace!(?err, "debug_traceTransaction failed, falling back to block replay");
617 }
618 }
619 }
620
621 let block_access_list = if !self.quick
627 && !self.no_bal
628 && !prestate_applied
629 && self.evm_version.is_none()
630 && config.hardfork.is_none()
631 && spec_id.is_enabled_in(SpecId::CANCUN)
632 && let Some(block) = &block
633 {
634 fetch_block_access_list(&provider, block).await?
635 } else {
636 None
637 };
638
639 let mut replay_tx_hashes = vec![tx_hash];
640 if !self.quick
641 && !prestate_applied
642 && block_access_list.is_none()
643 && let Some(block) = &block
644 {
645 replay_tx_hashes.extend(
646 full_transactions(block)?
647 .iter()
648 .take_while(|tx| tx.tx_hash() != tx_hash)
649 .filter(|tx| !is_system_transaction(tx) || self.replay_system_txes)
650 .map(TransactionResponse::tx_hash),
651 );
652 }
653 let replay_gas_limits =
654 ReplayGasLimits::fetch(chain.id(), &provider, replay_tx_hashes).await?;
655
656 Ok(PreparedRun {
657 args: self,
658 config,
659 tracing,
660 tx,
661 block,
662 evm_env,
663 executor,
664 trace_context,
665 prestate_applied,
666 block_access_list,
667 replay_gas_limits,
668 receipt,
669 #[cfg(feature = "monad")]
670 monad: MonadPrepared { tx_block_number, compute_units_per_second },
671 })
672 }
673}
674
675async fn fetch_block_access_list(
677 provider: &RetryProvider,
678 block: &AnyRpcBlock,
679) -> Result<Option<Arc<Bal>>> {
680 let header = block.header();
681 let bal = match provider.get_block_access_list(BlockId::hash(header.hash)).await {
682 Ok(Some(bal)) => bal,
683 Ok(None) => return Ok(None),
684 Err(err) => {
685 trace!(%err, "block access list unavailable, falling back to block replay");
686 return Ok(None);
687 }
688 };
689 if let Some(hash) = header.block_access_list_hash()
690 && compute_block_access_list_hash(&bal) != hash
691 {
692 sh_warn!(
693 "block access list of block {} does not match its header, replaying the block instead",
694 header.number()
695 )?;
696 return Ok(None);
697 }
698 Ok(Some(Arc::new(Bal::try_from_alloy(bal).wrap_err("invalid block access list")?)))
699}
700
701impl<FEN: FoundryEvmNetwork> PreparedRun<FEN> {
702 fn transaction_env(&self, tx: &AnyRpcTransaction) -> Result<TxEnvFor<FEN>> {
703 let mut tx_env = TxEnvFor::<FEN>::from_any_rpc_transaction(tx)?;
704 self.replay_gas_limits.apply::<FEN>(tx, &mut tx_env)?;
705 Ok(tx_env)
706 }
707
708 fn prepare_target(&mut self) {
711 let requirements = TraceRequirements::none()
712 .with_calls(true)
713 .with_debug(self.args.debug)
714 .with_decode_internal(if self.tracing.decode_internal {
715 InternalTraceMode::Full
716 } else {
717 InternalTraceMode::None
718 })
719 .with_state_changes(self.tracing.verbosity > 4);
720 self.executor.set_trace_requirements(requirements);
721 self.executor.set_trace_printer(self.args.trace_printer);
722
723 let sender_is_forged = match &*self.tx.inner.inner {
724 AnyTxEnvelope::Ethereum(inner) => {
725 inner.recover_signer().is_ok_and(|signer| signer != self.tx.from())
726 }
727 AnyTxEnvelope::Unknown(_) => true,
728 };
729 if sender_is_forged {
730 self.evm_env.cfg_env.disable_balance_check = true;
731 }
732 }
733
734 fn target_index(&self) -> Result<usize> {
736 let Some(block) = &self.block else { return Ok(0) };
737 full_transactions(block)?
738 .iter()
739 .position(|candidate| candidate.tx_hash() == self.tx.tx_hash())
740 .ok_or_else(|| {
741 eyre::eyre!("transaction {:?} is missing from its block", self.tx.tx_hash())
742 })
743 }
744
745 fn for_each_prefix_transaction(
748 &self,
749 target_index: usize,
750 mut f: impl FnMut(usize, &AnyRpcTransaction) -> Result<()>,
751 ) -> Result<()> {
752 if self.args.quick || self.prestate_applied {
753 return Ok(());
754 }
755 sh_status!("Executing previous transactions from the block.")?;
756 let Some(block) = &self.block else { return Ok(()) };
757 let txs = full_transactions(block)?;
758 let pb = init_progress(txs.len() as u64, "tx");
759 for (index, tx) in txs.iter().take(target_index).enumerate() {
760 if let Some(to) = Transaction::to(tx) {
761 trace!(tx=?tx.tx_hash(), ?to, "preparing previous call transaction");
762 } else {
763 trace!(tx=?tx.tx_hash(), "preparing previous create transaction");
764 }
765 f(index, tx)?;
766 pb.set_position((index + 1) as u64);
767 }
768 Ok(())
769 }
770
771 fn trace_kind(&self) -> TraceKind {
772 if let Some(to) = Transaction::to(&self.tx) {
773 trace!(tx=?self.tx.tx_hash(), ?to, "executing call transaction");
774 TraceKind::Execution
775 } else {
776 trace!(tx=?self.tx.tx_hash(), "executing create transaction");
777 TraceKind::Deployment
778 }
779 }
780
781 fn execute_ordinary(&mut self) -> Result<TraceResult> {
782 let target_tx_env = self.transaction_env(&self.tx).wrap_err_with(|| {
785 format!(
786 "cannot replay transaction {:?} locally; {REMOTE_TRACE_HINT}",
787 self.tx.tx_hash()
788 )
789 })?;
790 let target_index = self.target_index()?;
791 self.prepare_target();
792
793 let mut replay = Vec::new();
794 if let Some(bal) = self.block_access_list.take() {
795 trace!("reading prestate from block access list, skipping block replay");
798 self.executor
799 .backend_mut()
800 .set_bal(bal, BlockAccessIndex::new(target_index as u64 + 1));
801 } else {
802 let block_number = self.evm_env.block_env.number();
803 let replay_system_txes = self.args.replay_system_txes;
804 self.for_each_prefix_transaction(target_index, |_, tx| {
805 if !is_system_transaction(tx) || replay_system_txes {
806 let tx_env = self.transaction_env(tx).wrap_err_with(|| {
807 format!(
808 "Failed to prepare transaction: {:?} in block {}; `--quick` skips the \
809 transactions before the target, and {REMOTE_TRACE_HINT}",
810 tx.tx_hash(),
811 block_number
812 )
813 })?;
814 replay.push((tx.tx_hash(), tx_env));
815 }
816 Ok(())
817 })?;
818 }
819 let result = self.executor.transact_with_ordinary_block_replay(
820 self.evm_env.clone(),
821 target_tx_env,
822 replay,
823 )?;
824 let trace_kind = self.trace_kind();
825 trace!(tx_hash=?self.tx.tx_hash(), "completed execution");
826 Ok(TraceResult::from_raw(result, trace_kind))
827 }
828
829 async fn finish(self, result: TraceResult) -> Result<()> {
830 let contracts_bytecode = fetch_contracts_bytecode_from_trace(&self.executor, &result)?;
831 let (success, gas_used) = (result.success, result.gas_used);
832 handle_traces(
833 result,
834 &self.config,
835 self.trace_context,
836 &contracts_bytecode,
837 &self.tracing,
838 self.args.with_local_artifacts,
839 self.args.debug,
840 )
841 .await?;
842 self.warn_on_receipt_mismatch(success, gas_used)
843 }
844
845 fn warn_on_receipt_mismatch(&self, success: bool, gas_used: u64) -> Result<()> {
850 let Some(receipt) = &self.receipt else { return Ok(()) };
851 let outcome = |success: bool| if success { "succeeded" } else { "reverted" };
852 let mut differences = Vec::new();
853 if receipt.status() != success {
854 differences.push(format!(
855 "it {} on-chain but {} in the replay",
856 outcome(receipt.status()),
857 outcome(success)
858 ));
859 }
860 if let Some(expected) = self.expected_replay_gas(receipt)
861 && expected != gas_used
862 {
863 differences
864 .push(format!("it used {expected} gas on-chain but {gas_used} in the replay"));
865 }
866 if differences.is_empty() {
867 return Ok(());
868 }
869 let hint = if self.args.quick {
870 "`--quick` skips the transactions before it in the block, which can change the result; \
871 run without it to replay them first"
872 } else {
873 "The chain may apply rules the replay does not model; `--debug-trace-transaction` \
874 shows the node's own trace if it exposes the `debug` namespace"
875 };
876 sh_warn!(
877 "the replay does not match the transaction's receipt: {}. {hint}.",
878 differences.join(", and ")
879 )
880 }
881
882 fn expected_replay_gas(&self, receipt: &AnyTransactionReceipt) -> Option<u64> {
885 if self.trace_context.networks().is_monad() {
887 return None;
888 }
889 let gas_used = receipt.gas_used();
890 if foundry_evm_networks::arbitrum::is_arbitrum_chain(self.trace_context.chain().id()) {
891 let l1_gas_used =
893 parse_nitro_l1_gas_used(receipt.other_fields().get("gasUsedForL1")).ok()?;
894 return gas_used.checked_sub(l1_gas_used);
895 }
896 Some(gas_used)
897 }
898}
899
900#[cfg(feature = "monad")]
901impl PreparedRun<MonadEvmNetwork> {
902 async fn execute_monad(&mut self) -> Result<TraceResult> {
903 let provider = ProviderBuilder::<alloy_network::Ethereum>::from_config(&self.config)?
907 .compute_units_per_second_opt(self.monad.compute_units_per_second)
908 .build()?;
909 let block =
910 provider.get_block(self.monad.tx_block_number.into()).full().await?.ok_or_else(
911 || {
912 eyre::eyre!(
913 "block {} is required to reconstruct transaction context",
914 self.monad.tx_block_number
915 )
916 },
917 )?;
918 let block_context = BlockContext::<MonadEvmNetwork>::fetch(&provider, &block).await?;
919 let target_tx_env = TxEnv::from_any_rpc_transaction(&self.tx)?;
923 let target_index = self.target_index()?;
924 self.prepare_target();
925
926 let mut replay = Vec::new();
927 self.for_each_prefix_transaction(target_index, |index, tx| {
928 let tx_env = TxEnv::from_any_rpc_transaction(tx)?;
929 let chain_context = block_context.transaction(index);
930 replay.push((tx.tx_hash(), tx_env, chain_context));
931 Ok(())
932 })?;
933 let result = self.executor.transact_with_monad_block_replay(
934 self.evm_env.clone(),
935 target_tx_env,
936 block_context.transaction(target_index),
937 replay,
938 self.args.replay_system_txes,
939 )?;
940 let Some((result, used_system_replay)) = result else {
941 eyre::bail!(
942 "{:?} is a system transaction.\nReplaying system transactions is currently not supported.",
943 self.tx.tx_hash()
944 );
945 };
946 if used_system_replay {
947 trace!(tx=?self.tx.tx_hash(), "executed canonical system transaction");
948 }
949 Ok(TraceResult::from_raw(result, self.trace_kind()))
950 }
951}
952
953fn is_system_transaction(tx: &AnyRpcTransaction) -> bool {
954 is_known_system_sender(tx.from()) || tx.transaction_type() == Some(SYSTEM_TRANSACTION_TYPE)
955}
956
957fn full_transactions(block: &AnyRpcBlock) -> Result<&[AnyRpcTransaction]> {
958 let BlockTransactions::Full(txs) = block.transactions() else {
959 eyre::bail!("Could not get block txs");
960 };
961 Ok(txs)
962}
963
964impl ReplayGasLimits {
965 async fn fetch(chain_id: u64, provider: &RetryProvider, tx_hashes: Vec<B256>) -> Result<Self> {
966 if !foundry_evm_networks::arbitrum::is_arbitrum_chain(chain_id) {
967 return Ok(Self::Unchanged);
968 }
969
970 let mut requests = futures::stream::iter(tx_hashes)
971 .map(|tx_hash| fetch_nitro_l1_gas_used(provider, tx_hash))
972 .buffer_unordered(MAX_CONCURRENT_RPC_REQUESTS);
973 let mut gas_used = B256HashMap::default();
974 while let Some(result) = requests.next().await {
975 let (tx_hash, l1_gas_used) = result?;
976 gas_used.insert(tx_hash, l1_gas_used);
977 }
978 Ok(Self::Nitro(gas_used))
979 }
980
981 fn apply<FEN: FoundryEvmNetwork>(
982 &self,
983 tx: &AnyRpcTransaction,
984 tx_env: &mut TxEnvFor<FEN>,
985 ) -> Result<()> {
986 let Self::Nitro(l1_gas_used) = self else { return Ok(()) };
987 let tx_hash = tx.tx_hash();
988 let l1_gas_used = l1_gas_used
989 .get(&tx_hash)
990 .ok_or_else(|| eyre::eyre!("receipt not found for Nitro transaction {tx_hash:?}"))?;
991 tx_env.set_gas_limit(nitro_execution_gas_limit(tx_hash, tx.gas_limit(), *l1_gas_used)?);
992 Ok(())
993 }
994}
995
996async fn fetch_nitro_l1_gas_used(provider: &RetryProvider, tx_hash: B256) -> Result<(B256, u64)> {
997 let receipt = provider
998 .get_transaction_receipt(tx_hash)
999 .await?
1000 .ok_or_else(|| eyre::eyre!("receipt not found for Nitro transaction {tx_hash:?}"))?;
1001 let l1_gas_used = parse_nitro_l1_gas_used(receipt.other_fields().get("gasUsedForL1"))
1002 .wrap_err_with(|| format!("invalid Nitro poster gas for transaction {tx_hash:?}"))?;
1003 Ok((tx_hash, l1_gas_used))
1004}
1005
1006fn parse_nitro_l1_gas_used(field: Option<&serde_json::Value>) -> Result<u64> {
1007 let field = field.ok_or_else(|| eyre::eyre!("missing `gasUsedForL1` receipt field"))?;
1008 let value = serde_json::from_value::<U256>(field.clone())
1009 .wrap_err("malformed `gasUsedForL1` receipt field")?;
1010 value.try_into().map_err(|_| eyre::eyre!("`gasUsedForL1` value {value} exceeds u64::MAX"))
1011}
1012
1013fn nitro_execution_gas_limit(tx_hash: B256, gas_limit: u64, l1_gas_used: u64) -> Result<u64> {
1014 gas_limit.checked_sub(l1_gas_used).ok_or_else(|| {
1015 eyre::eyre!(
1016 "Nitro poster gas {l1_gas_used} exceeds gas limit {gas_limit} for transaction {tx_hash:?}"
1017 )
1018 })
1019}
1020
1021pub(super) fn block_num_hash<B: BlockResponse>(block: &B) -> BlockNumHash
1023where
1024 B::Header: HeaderResponse,
1025{
1026 BlockNumHash::new(block.header().number(), block.header().hash())
1027}
1028
1029pub(super) fn call_tracer_frame(
1035 response: Result<GethTrace, TransportError>,
1036 method: &str,
1037 local_hint: &str,
1038 missing_state_hint: &str,
1039) -> Result<CallFrame> {
1040 let trace = response.map_err(|err| -> eyre::Report {
1041 if is_method_not_found_error(&err) {
1042 eyre::eyre!(
1043 "the RPC endpoint does not support `{method}` (method not found); use a node with the `debug` namespace enabled (e.g. a local anvil/reth or an archive endpoint), or {local_hint}"
1044 )
1045 } else if is_missing_state_error(&err) {
1046 eyre::eyre!(
1047 "the RPC endpoint does not have the historical state for {missing_state_hint}"
1048 )
1049 } else {
1050 err.into()
1051 }
1052 })?;
1053 let GethTrace::CallTracer(frame) = trace else {
1054 eyre::bail!(
1055 "`{method}` did not return a callTracer frame; the RPC endpoint may not support the `callTracer`"
1056 );
1057 };
1058 Ok(frame)
1059}
1060
1061fn ensure_remote_transaction_inclusion(
1062 tx_hash: B256,
1063 expected: BlockNumHash,
1064 actual: Option<BlockNumHash>,
1065 source: &str,
1066) -> Result<()> {
1067 let Some(actual) = actual else {
1068 eyre::bail!(
1069 "transaction {tx_hash} changed inclusion while collecting its remote trace: {source} no longer reports it as mined; retry the command"
1070 );
1071 };
1072 if actual != expected {
1073 eyre::bail!(
1074 "transaction {tx_hash} changed inclusion while collecting its remote trace: expected block {} at {}, but {source} reported block {} at {}; retry the command",
1075 expected.hash,
1076 expected.number,
1077 actual.hash,
1078 actual.number,
1079 );
1080 }
1081
1082 Ok(())
1083}
1084
1085fn ensure_requested_transaction(requested: B256, tx: &AnyRpcTransaction) -> Result<()> {
1087 let returned = tx.tx_hash();
1088 eyre::ensure!(
1089 returned == requested,
1090 "RPC returned transaction {returned:?} for requested {requested:?}"
1091 );
1092 Ok(())
1093}
1094
1095const fn parent_beacon_block_root_for_network(
1096 networks: NetworkConfigs,
1097 spec_id: SpecId,
1098 parent_beacon_block_root: Option<B256>,
1099) -> Option<B256> {
1100 if networks.is_monad() || !spec_id.is_enabled_in(SpecId::CANCUN) {
1101 return None;
1102 }
1103
1104 parent_beacon_block_root
1108}
1109
1110pub fn fetch_contracts_bytecode_from_trace<FEN: FoundryEvmNetwork>(
1111 executor: &Executor<FEN>,
1112 result: &TraceResult,
1113) -> Result<AddressHashMap<Bytes>> {
1114 let contracts_bytecode = trace_addresses(result)
1115 .filter_map(|addr| {
1116 let code = executor
1118 .backend()
1119 .basic_ref(addr)
1120 .inspect_err(|e| _ = sh_warn!("Failed to fetch code for {addr}: {e}"))
1121 .ok()??
1122 .code?
1123 .bytes();
1124 (!code.is_empty()).then_some((addr, code))
1125 })
1126 .collect();
1127 Ok(contracts_bytecode)
1128}
1129
1130async fn fetch_transaction_contracts_bytecode_via_rpc<N: Network, P: Provider<N>>(
1137 provider: &P,
1138 result: &TraceResult,
1139 tx_hash: B256,
1140 block: BlockId,
1141) -> AddressHashMap<Bytes> {
1142 let mut contracts_bytecode = AddressHashMap::default();
1143 let prestate_config = PreStateConfig { disable_storage: Some(true), ..Default::default() };
1144 match provider
1145 .debug_trace_transaction(tx_hash, GethDebugTracingOptions::prestate_tracer(prestate_config))
1146 .await
1147 {
1148 Ok(trace) => match trace.try_into_pre_state_frame() {
1149 Ok(prestate) => {
1150 for (&address, account) in prestate.pre_state() {
1151 if let Some(code) = account.code.clone().filter(|code| !code.is_empty()) {
1152 contracts_bytecode.insert(address, code);
1153 }
1154 }
1155 }
1156 Err(err) => {
1157 let _ = sh_warn!("Failed to parse transaction prestate for local artifacts: {err}");
1158 }
1159 },
1160 Err(err) => {
1161 let _ = sh_warn!("Failed to fetch transaction prestate for local artifacts: {err}");
1162 }
1163 }
1164
1165 let missing_addresses = trace_addresses(result)
1166 .filter(|address| !contracts_bytecode.contains_key(address))
1167 .collect::<Vec<_>>();
1168 contracts_bytecode.extend(fetch_code_via_rpc(provider, missing_addresses, block).await);
1169 contracts_bytecode
1170}
1171
1172pub(super) fn trace_addresses(result: &TraceResult) -> impl Iterator<Item = Address> {
1174 let mut addresses = AddressSet::default();
1175 for (_, trace) in result.traces.iter().flatten() {
1176 for node in trace.arena.nodes() {
1177 addresses.extend(
1178 [node.trace.address, node.trace.caller].into_iter().filter(|a| !a.is_zero()),
1179 );
1180 }
1181 }
1182 addresses.into_iter()
1183}
1184
1185impl figment::Provider for RunArgs {
1186 fn metadata(&self) -> Metadata {
1187 Metadata::named("RunArgs")
1188 }
1189
1190 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
1191 let mut map = Map::new();
1192
1193 if let Some(api_key) = &self.etherscan.key {
1194 map.insert("etherscan_api_key".into(), api_key.as_str().into());
1195 }
1196
1197 if let Some(evm_version) = self.evm_version {
1198 map.insert("evm_version".into(), figment::value::Value::serialize(evm_version)?);
1199 }
1200
1201 Ok(Map::from([(Config::selected_profile(), map)]))
1202 }
1203}
1204
1205#[cfg(test)]
1206mod tests {
1207
1208 use super::*;
1209
1210 #[test]
1211 fn http_wrapped_method_not_found_has_trace_guidance() {
1212 let error = alloy_transport::TransportErrorKind::http_error(
1213 403,
1214 r#"{"jsonrpc":"2.0","error":{"code":-32601,"message":"method disabled"}}"#.into(),
1215 );
1216 let error = call_tracer_frame(
1217 Err(error),
1218 "debug_traceTransaction",
1219 "replay locally",
1220 "this transaction",
1221 )
1222 .unwrap_err();
1223 assert_eq!(
1224 error.to_string(),
1225 "the RPC endpoint does not support `debug_traceTransaction` (method not found); use a node with the `debug` namespace enabled (e.g. a local anvil/reth or an archive endpoint), or replay locally"
1226 );
1227 }
1228
1229 #[test]
1230 fn parses_legacy_short_label_alias() {
1231 let address = Address::with_last_byte(1);
1232 let label = format!("{address}:alice");
1233 let args = RunArgs::parse_from(["cast run", "0x00", "-l", &label]);
1234
1235 assert_eq!(args.legacy_labels, vec![label]);
1236 }
1237
1238 #[test]
1239 fn debug_trace_transaction_accepts_label_and_render_flags() {
1240 let args = RunArgs::try_parse_from([
1241 "foundry-cli",
1242 "--debug-trace-transaction",
1243 "0x0000000000000000000000000000000000000000000000000000000000000000",
1244 "--label",
1245 "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045:vitalik.eth",
1246 "--disable-labels",
1247 "--trace-depth",
1248 "2",
1249 "--with-local-artifacts",
1250 ]);
1251 assert!(args.is_ok(), "--debug-trace-transaction must accept label/rendering flags");
1252 }
1253
1254 #[test]
1255 fn parent_beacon_block_root_is_applied_only_when_the_header_has_one() {
1256 let networks = NetworkConfigs::default();
1257 let root = Some(B256::repeat_byte(0x42));
1258 for (networks, spec_id, root, expected) in [
1260 (networks, SpecId::CANCUN, None, None),
1261 (networks, SpecId::CANCUN, root, root),
1262 (networks, SpecId::SHANGHAI, root, None),
1263 (networks, SpecId::SHANGHAI, None, None),
1264 #[cfg(feature = "monad")]
1265 (NetworkConfigs::with_monad(), SpecId::PRAGUE, root, None),
1266 #[cfg(feature = "monad")]
1267 (NetworkConfigs::with_monad(), SpecId::OSAKA, root, None),
1268 #[cfg(feature = "monad")]
1269 (NetworkConfigs::with_monad(), SpecId::PRAGUE, None, None),
1270 #[cfg(feature = "monad")]
1271 (NetworkConfigs::with_monad(), SpecId::OSAKA, None, None),
1272 ] {
1273 assert_eq!(parent_beacon_block_root_for_network(networks, spec_id, root), expected);
1274 }
1275 }
1276
1277 #[test]
1278 fn debug_trace_transaction_ignores_configured_internal_decoding() {
1279 let args = RunArgs::parse_from(["cast run", "0x00", "--debug-trace-transaction"]);
1280 let config = TracingConfig { decode_internal: true, ..Default::default() };
1281
1282 assert!(!args.resolve_tracing(&config, 0).decode_internal);
1283 }
1284
1285 #[test]
1286 fn parses_nitro_l1_gas_used() {
1287 let field = serde_json::json!("0x26ed52");
1288
1289 assert_eq!(parse_nitro_l1_gas_used(Some(&field)).unwrap(), 2_551_122);
1290 assert_eq!(nitro_execution_gas_limit(B256::ZERO, 2_733_748, 2_551_122).unwrap(), 182_626);
1291 }
1292
1293 #[test]
1294 fn rejects_invalid_nitro_gas() {
1295 assert_eq!(
1296 parse_nitro_l1_gas_used(None).unwrap_err().to_string(),
1297 "missing `gasUsedForL1` receipt field"
1298 );
1299
1300 let field = serde_json::json!("invalid");
1301 assert_eq!(
1302 parse_nitro_l1_gas_used(Some(&field)).unwrap_err().to_string(),
1303 "malformed `gasUsedForL1` receipt field"
1304 );
1305
1306 let field = serde_json::json!("0x10000000000000000");
1307 assert_eq!(
1308 parse_nitro_l1_gas_used(Some(&field)).unwrap_err().to_string(),
1309 "`gasUsedForL1` value 18446744073709551616 exceeds u64::MAX"
1310 );
1311
1312 let tx_hash = B256::repeat_byte(0x42);
1313 assert_eq!(
1314 nitro_execution_gas_limit(tx_hash, 100, 101).unwrap_err().to_string(),
1315 format!("Nitro poster gas 101 exceeds gas limit 100 for transaction {tx_hash:?}")
1316 );
1317 }
1318}