Skip to main content

cast/cmd/
run.rs

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
82/// Points at the remote trace when a transaction cannot be replayed locally.
83const REMOTE_TRACE_HINT: &str = "`--debug-trace-transaction` renders the node's own trace instead";
84
85/// CLI arguments for `cast run`.
86#[derive(Clone, Debug, Parser)]
87pub struct RunArgs {
88    /// The transaction hash.
89    tx_hash: String,
90
91    /// Opens the transaction in the debugger.
92    #[arg(long, short)]
93    debug: bool,
94
95    /// Print out opcode traces.
96    #[arg(long, short)]
97    trace_printer: bool,
98
99    /// Executes the transaction only with the state from the previous block.
100    ///
101    /// May result in different results than the live execution!
102    #[arg(long)]
103    quick: bool,
104
105    /// Do not use the block access list (BAL) of the transaction's block.
106    ///
107    /// If the node serves an EIP-7928 block access list, cast reads the transaction's prestate
108    /// from it instead of replaying the earlier transactions of the block. With this flag, the
109    /// block is always replayed.
110    #[arg(long)]
111    no_bal: bool,
112
113    /// Whether to attempt local replay of system transactions.
114    ///
115    /// Without this flag, ordinary block replay leaves out system transactions mined before the
116    /// target and warns about each one. Replay support depends on the selected network. Canonical
117    /// Monad system transactions are replayed regardless of this flag.
118    #[arg(long, alias = "sys")]
119    replay_system_txes: bool,
120
121    /// Use debug_traceTransaction to fetch the prestate instead of replaying the block.
122    ///
123    /// This is significantly faster than replaying all previous transactions in the block, but
124    /// requires the node to expose the `debug_` namespace (most public RPCs don't). If the call
125    /// or response can't be used, cast silently falls back to the block access list, then to
126    /// replaying the block.
127    #[arg(long, default_value_t = false)]
128    prestate_tracer: bool,
129
130    /// Fetch the transaction's trace from the node via `debug_traceTransaction` (callTracer) and
131    /// render it, instead of re-executing the transaction locally.
132    ///
133    /// This skips the block replay entirely, so it is fast and reflects exactly what happened
134    /// on-chain, including chain-specific EVM behavior a local replay may not reproduce, but it
135    /// requires the node to expose the `debug_` namespace. The result is a call-tree view:
136    /// nested calls, value, gas, emitted logs and revert data. It does not provide the
137    /// opcode-level detail of a local run, so the local-execution-only flags (`--debug`,
138    /// `--decode-internal`, `--trace-printer`, `--quick`, `--no-bal`, `--prestate-tracer`,
139    /// `--evm-version`) do not apply.
140    #[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    /// Deprecated short alias for `--labels`.
151    #[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    /// The EVM version to use.
161    ///
162    /// Overrides the version specified in the config.
163    #[arg(long)]
164    evm_version: Option<EvmVersion>,
165
166    /// Use current project artifacts for trace decoding.
167    #[arg(long, visible_alias = "la")]
168    pub with_local_artifacts: bool,
169
170    /// Disable block gas limit check.
171    ///
172    /// Always implied: a mined transaction already passed its chain's own check.
173    #[arg(long)]
174    pub disable_block_gas_limit: bool,
175
176    /// Enable the tx gas limit checks as imposed by Osaka (EIP-7825).
177    #[arg(long)]
178    pub enable_tx_gas_limit: bool,
179}
180
181/// Target transaction resolved up front by [`RunArgs::fetch_target`], before any per-network
182/// preparation.
183struct TargetFetch {
184    tx: AnyRpcTransaction,
185    provider: RetryProvider,
186    compute_units_per_second: Option<u64>,
187}
188
189/// Fields only needed by the Monad-specific `execute_monad` path.
190#[cfg(feature = "monad")]
191struct MonadPrepared {
192    tx_block_number: u64,
193    compute_units_per_second: Option<u64>,
194}
195
196/// Chain-specific gas limits for transactions replayed by `cast run`.
197enum ReplayGasLimits {
198    Unchanged,
199    Nitro(B256HashMap<u64>),
200}
201
202/// State assembled by [`RunArgs::prepare`] and consumed by the network-specific `execute_*`
203/// methods below.
204struct 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    /// The block access list of the target's block, when the node serves one.
215    block_access_list: Option<Arc<Bal>>,
216    replay_gas_limits: ReplayGasLimits,
217    /// The target's receipt, which the replay is checked against.
218    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    /// Applies the network and tracing options to `config` and returns the resolved tracing
233    /// config.
234    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    /// Executes the transaction by replaying it
242    ///
243    /// This replays the entire block the transaction was mined in unless `quick` is set to true
244    /// or the node serves a block access list for the block.
245    ///
246    /// Note: This executes the transaction(s) as is: Cheatcodes are disabled
247    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        // Auto-detect network from fork chain ID when not explicitly configured.
261        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    /// Replays the transaction locally with the EVM of its network.
280    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    /// `AnyNetwork` rather than `FEN::Network`: chains such as Arbitrum, Celo and the OP-stack
345    /// forks Foundry does not route to a dedicated network put transaction types the strict
346    /// Ethereum envelope cannot decode into every block, which would fail the full block fetch in
347    /// `prepare` for the whole chain. Execution still uses `FEN`.
348    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    /// Fetches the trace from the node via `debug_traceTransaction` (callTracer) instead of
368    /// re-executing the transaction locally. The node already holds the transaction's exact
369    /// pre-state and EVM rules, so this needs no block replay and no local executor; it also
370    /// handles system transactions.
371    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        // Local-artifact labeling matches deployed runtime bytecode against the project
432        // artifacts. There is no local executor on this path, so fetch the code over RPC for the
433        // addresses in the trace, at the transaction's block. Skip the extra round-trips unless
434        // local artifacts were requested.
435        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        // The remote node executed this trace, so its reported family is authoritative for
448        // decoding even when the caller selected a compatible local EVM implementation.
449        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) = &current_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        // we need to fork off the parent block
508        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            // fetch the block the transaction was mined in
513            provider.get_block(tx_block_number.into()).full().into_future().map_err(Into::into),
514            // The receipt only backs the check after the replay, so a failed lookup skips that
515            // check instead of failing the run.
516            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        // Mined transactions already passed the block gas limit check their chain applies, and
524        // some chains admit transactions whose gas limit exceeds it: BSC validator transactions
525        // carry a gas limit of `i64::MAX`. Re-applying the check can only reject a transaction
526        // the chain accepted.
527        fork.evm_env.cfg_env.disable_block_gas_limit = true;
528
529        // By default do not enforce transaction gas limits imposed by Osaka (EIP-7825).
530        // Users can opt-in to enable these limits by setting `enable_tx_gas_limit` to true.
531        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            // Unless explicitly configured, resolve the correct spec for the block using the same
544            // approach as reth: walk known chain activation conditions to find the latest active
545            // fork. For unknown chains, probe the node for the features it executes at the block,
546            // falling back to a blob-gas heuristic if the node rejects the probe.
547            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        // The fork is pinned to the parent block, but the replayed transactions execute in the
579        // target's block, which block queries such as Arbitrum's `ArbSys.arbBlockNumber()` must
580        // report.
581        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        // Set the state to the moment right before the transaction.
594        //
595        // When `--prestate-tracer` is set, opportunistically try to fetch the prestate directly
596        // via `debug_traceTransaction` (much faster than replaying the block). This requires the
597        // `debug_` namespace, which most nodes don't expose, so it is opt-in and silently falls
598        // back to the block access list, then to replaying previous transactions in the block,
599        // if the call or parsing fails.
600        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        // A block access list (BAL) records every state write of the block by transaction index,
627        // so the target's prestate can be read from it instead of replaying the earlier
628        // transactions. Before Cancun, SELFDESTRUCT wipes storage the list does not enumerate.
629        // Explicit execution rules must replay the prefix: canonical writes may differ under
630        // the requested EVM version or hardfork.
631        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
680/// Fetches the block access list of `block`, if the node serves one that matches the header.
681async 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    /// Prepares the executor for the target transaction: enables tracing and, when the sender
714    /// was forged, disables the balance check.
715    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    /// Returns the index of the target transaction in its block.
740    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    /// Calls `f` for every transaction mined before the target, unless the block replay was
751    /// skipped (`--quick` or an applied prestate).
752    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        // Decode the target transaction before replaying the block: an envelope this build
788        // can't decode should fail fast.
789        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            // Index 0 holds the pre-block system writes and transaction `i` is index `i + 1`, so
801            // reads positioned at the target's own index see exactly the earlier writes.
802            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    /// Warns when the replay does not reproduce the target's receipt.
861    ///
862    /// A replay diverges when the chain applies rules the local EVM does not model or the replay
863    /// starts from different state, and the trace alone gives no sign of it.
864    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    /// Returns the gas the replay should report for `receipt`, or `None` if the receipt does not
898    /// record the gas the transaction executed.
899    fn expected_replay_gas(&self, receipt: &AnyTransactionReceipt) -> Option<u64> {
900        // Monad charges the full gas limit and reports it as the gas used.
901        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            // Nitro folds the L1 posting cost into the receipt's gas used.
907            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        // `BlockContext` is typed to `MonadEvmNetwork::Network` (`Ethereum`). Monad blocks only
919        // carry standard envelopes, so a typed provider can serve this path while the rest of the
920        // command stays on `AnyNetwork`.
921        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        // Decode the target transaction before replaying the block: an envelope this build
935        // can't decode should fail fast, not after paying for the entire prior-transaction
936        // replay.
937        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
1036/// Returns the number and hash of a fetched block.
1037pub(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
1044/// Extracts the `callTracer` frame from a `debug_trace*` response.
1045///
1046/// Two RPC rejections deserve an actionable hint instead of the raw transport error, and they
1047/// need different fixes: a disabled `debug` namespace, and missing historical state, hit
1048/// whenever a full node has pruned the traced block.
1049pub(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
1100/// Ensures the RPC answered `eth_getTransactionByHash` with the requested transaction.
1101fn 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    // Chains that run a Cancun or later EVM without Ethereum's beacon chain, such as Polygon and
1120    // Scroll, never populate this header field and never deploy the EIP-4788 contract, so there
1121    // is no root to apply. Requiring one makes their blocks unreplayable.
1122    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            // All relevant bytecodes should already be cached in the executor.
1132            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
1145/// Fetches bytecode for a mined transaction at its exact transaction index.
1146///
1147/// The prestate tracer provides the code that existed immediately before the transaction, which
1148/// avoids reading end-of-block state for contracts changed or removed by later transactions. Any
1149/// address absent from the prestate (for example, a contract created by this transaction) falls
1150/// back to `eth_getCode` at the transaction's block.
1151async 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
1187/// Returns the distinct non-zero addresses and callers seen in the traces of `result`.
1188pub(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        // Polygon and Scroll run a Cancun or later EVM without populating the header field.
1274        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}