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