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 replay system transactions.
114    #[arg(long, alias = "sys")]
115    replay_system_txes: bool,
116
117    /// Use debug_traceTransaction to fetch the prestate instead of replaying the block.
118    ///
119    /// This is significantly faster than replaying all previous transactions in the block, but
120    /// requires the node to expose the `debug_` namespace (most public RPCs don't). If the call
121    /// or response can't be used, cast silently falls back to the block access list, then to
122    /// replaying the block.
123    #[arg(long, default_value_t = false)]
124    prestate_tracer: bool,
125
126    /// Fetch the transaction's trace from the node via `debug_traceTransaction` (callTracer) and
127    /// render it, instead of re-executing the transaction locally.
128    ///
129    /// This skips the block replay entirely, so it is fast and reflects exactly what happened
130    /// on-chain, including chain-specific EVM behavior a local replay may not reproduce, but it
131    /// requires the node to expose the `debug_` namespace. The result is a call-tree view:
132    /// nested calls, value, gas, emitted logs and revert data. It does not provide the
133    /// opcode-level detail of a local run, so the local-execution-only flags (`--debug`,
134    /// `--decode-internal`, `--trace-printer`, `--quick`, `--no-bal`, `--prestate-tracer`,
135    /// `--evm-version`) do not apply.
136    #[arg(
137        long,
138        default_value_t = false,
139        conflicts_with_all = ["debug", "decode_internal", "trace_printer", "quick", "no_bal", "prestate_tracer", "evm_version"]
140    )]
141    debug_trace_transaction: bool,
142
143    #[command(flatten)]
144    tracing: TracingArgs,
145
146    /// Deprecated short alias for `--labels`.
147    #[arg(short = 'l', value_name = "ADDRESS:LABEL", hide = true)]
148    legacy_labels: Vec<String>,
149
150    #[command(flatten)]
151    etherscan: EtherscanOpts,
152
153    #[command(flatten)]
154    rpc: RpcOpts,
155
156    /// The EVM version to use.
157    ///
158    /// Overrides the version specified in the config.
159    #[arg(long)]
160    evm_version: Option<EvmVersion>,
161
162    /// Use current project artifacts for trace decoding.
163    #[arg(long, visible_alias = "la")]
164    pub with_local_artifacts: bool,
165
166    /// Disable block gas limit check.
167    ///
168    /// Always implied: a mined transaction already passed its chain's own check.
169    #[arg(long)]
170    pub disable_block_gas_limit: bool,
171
172    /// Enable the tx gas limit checks as imposed by Osaka (EIP-7825).
173    #[arg(long)]
174    pub enable_tx_gas_limit: bool,
175}
176
177/// Target transaction resolved up front by [`RunArgs::fetch_target`], before any per-network
178/// preparation.
179struct TargetFetch {
180    tx: AnyRpcTransaction,
181    provider: RetryProvider,
182    compute_units_per_second: Option<u64>,
183}
184
185/// Fields only needed by the Monad-specific `execute_monad` path.
186#[cfg(feature = "monad")]
187struct MonadPrepared {
188    tx_block_number: u64,
189    compute_units_per_second: Option<u64>,
190}
191
192/// Chain-specific gas limits for transactions replayed by `cast run`.
193enum ReplayGasLimits {
194    Unchanged,
195    Nitro(B256HashMap<u64>),
196}
197
198/// State assembled by [`RunArgs::prepare`] and consumed by the network-specific `execute_*`
199/// methods below.
200struct PreparedRun<FEN: FoundryEvmNetwork> {
201    args: RunArgs,
202    config: Box<Config>,
203    tracing: TracingConfig,
204    tx: AnyRpcTransaction,
205    block: Option<AnyRpcBlock>,
206    evm_env: EvmEnvFor<FEN>,
207    executor: TracingExecutor<FEN>,
208    trace_context: TraceContext,
209    prestate_applied: bool,
210    /// The block access list of the target's block, when the node serves one.
211    block_access_list: Option<Arc<Bal>>,
212    replay_gas_limits: ReplayGasLimits,
213    /// The target's receipt, which the replay is checked against.
214    receipt: Option<AnyTransactionReceipt>,
215    #[cfg(feature = "monad")]
216    monad: MonadPrepared,
217}
218
219impl RunArgs {
220    fn resolve_tracing(&self, config: &TracingConfig, verbosity: u8) -> TracingConfig {
221        if self.debug_trace_transaction {
222            self.tracing.resolve_call_tracer(config, verbosity)
223        } else {
224            self.tracing.resolve(config, verbosity)
225        }
226    }
227
228    /// Applies the network and tracing options to `config` and returns the resolved tracing
229    /// config.
230    fn configure_tracing(&mut self, config: &mut Config, evm_opts: &EvmOpts) -> TracingConfig {
231        config.networks = evm_opts.networks;
232        self.tracing.labels.append(&mut self.legacy_labels);
233        config.tracing = self.resolve_tracing(&config.tracing, shell::verbosity());
234        config.tracing.clone()
235    }
236
237    /// Executes the transaction by replaying it
238    ///
239    /// This replays the entire block the transaction was mined in unless `quick` is set to true
240    /// or the node serves a block access list for the block.
241    ///
242    /// Note: This executes the transaction(s) as is: Cheatcodes are disabled
243    pub async fn run(self) -> Result<()> {
244        let figment = self.rpc.clone().into_figment(self.with_local_artifacts).merge(&self);
245        let (mut config, mut evm_opts) = super::load_cast_config_and_evm_opts(figment)?;
246        if config.eth_rpc_url.is_none()
247            && let Some(chain) = self.etherscan.chain
248        {
249            let alias = chain.to_string();
250            if config.rpc_endpoints.contains_key(&alias) {
251                config.eth_rpc_url = Some(alias);
252            }
253        }
254        evm_opts.fork_url = Some(config.get_rpc_url_or_localhost_http()?.into_owned());
255
256        // Auto-detect network from fork chain ID when not explicitly configured.
257        evm_opts.infer_network_from_fork().await?;
258
259        if self.debug_trace_transaction {
260            return self.remote_trace(config, evm_opts).await;
261        }
262
263        self.replay(config, evm_opts).await.map_err(|err| {
264            if err.chain().any(|cause| is_missing_state_message(&cause.to_string())) {
265                err.wrap_err(
266                    "the RPC endpoint does not have the historical state for the transaction's \
267                     block; use an archive endpoint",
268                )
269            } else {
270                err
271            }
272        })
273    }
274
275    /// Replays the transaction locally with the EVM of its network.
276    async fn replay(self, config: Box<Config>, evm_opts: EvmOpts) -> Result<()> {
277        match evm_opts.networks.execution_network() {
278            NetworkVariant::Tempo => {
279                self.run_with_evm(config, evm_opts, ExecutorBuilder::<TempoEvmNetwork>::new()).await
280            }
281            #[cfg(feature = "base")]
282            NetworkVariant::Base => {
283                self.run_with_evm(config, evm_opts, ExecutorBuilder::<BaseEvmNetwork>::new()).await
284            }
285            #[cfg(feature = "monad")]
286            NetworkVariant::Monad => {
287                let target = self.fetch_target(&config).await?;
288                let mut run = self
289                    .prepare::<MonadEvmNetwork>(
290                        config,
291                        evm_opts,
292                        target,
293                        ExecutorBuilder::<MonadEvmNetwork>::new(),
294                    )
295                    .await?;
296                let result = run.execute_monad().await?;
297                run.finish(result).await
298            }
299            #[cfg(feature = "optimism")]
300            NetworkVariant::Optimism => {
301                self.run_with_evm(config, evm_opts, ExecutorBuilder::<OpEvmNetwork>::new()).await
302            }
303            NetworkVariant::Ethereum => {
304                self.run_with_evm(config, evm_opts, ExecutorBuilder::<EthEvmNetwork>::new()).await
305            }
306        }
307    }
308
309    async fn run_with_evm<FEN: FoundryEvmNetwork>(
310        self,
311        config: Box<Config>,
312        evm_opts: EvmOpts,
313        executor_builder: ExecutorBuilder<FEN>,
314    ) -> Result<()> {
315        let target = self.fetch_target(&config).await?;
316        let endpoint_is_anvil = evm_opts
317            .fork_endpoint
318            .as_ref()
319            .is_some_and(|identity| identity.reported_hardfork.is_some());
320        if let Some(chain) = target.tx.chain_id().map(alloy_chains::Chain::from_id)
321            && chain.is_elastic()
322            && !endpoint_is_anvil
323        {
324            eyre::bail!(
325                "{chain} executes EraVM bytecode, which cannot be replayed locally; \
326                 {REMOTE_TRACE_HINT}"
327            );
328        }
329        if is_system_transaction(&target.tx) && !self.replay_system_txes {
330            eyre::bail!(
331                "{:?} is a system transaction.\nReplaying system transactions is currently not supported.",
332                target.tx.tx_hash()
333            );
334        }
335        let mut run = self.prepare::<FEN>(config, evm_opts, target, executor_builder).await?;
336        let result = run.execute_ordinary()?;
337        run.finish(result).await
338    }
339
340    /// `AnyNetwork` rather than `FEN::Network`: chains such as Arbitrum, Celo and the OP-stack
341    /// forks Foundry does not route to a dedicated network put transaction types the strict
342    /// Ethereum envelope cannot decode into every block, which would fail the full block fetch in
343    /// `prepare` for the whole chain. Execution still uses `FEN`.
344    async fn fetch_target(&self, config: &Config) -> Result<TargetFetch> {
345        let compute_units_per_second = if self.rpc.common.no_rpc_rate_limit {
346            Some(u64::MAX)
347        } else {
348            self.rpc.common.compute_units_per_second
349        };
350        let provider = ProviderBuilder::<AnyNetwork>::from_config(config)?
351            .compute_units_per_second_opt(compute_units_per_second)
352            .build()?;
353        let tx_hash = self.tx_hash.parse().wrap_err("invalid tx hash")?;
354        let tx = provider
355            .get_transaction_by_hash(tx_hash)
356            .await
357            .wrap_err_with(|| format!("tx not found: {tx_hash:?}"))?
358            .ok_or_else(|| eyre::eyre!("tx not found: {tx_hash:?}"))?;
359        ensure_requested_transaction(tx_hash, &tx)?;
360        Ok(TargetFetch { tx, provider, compute_units_per_second })
361    }
362
363    /// Fetches the trace from the node via `debug_traceTransaction` (callTracer) instead of
364    /// re-executing the transaction locally. The node already holds the transaction's exact
365    /// pre-state and EVM rules, so this needs no block replay and no local executor; it also
366    /// handles system transactions.
367    async fn remote_trace(mut self, mut config: Box<Config>, evm_opts: EvmOpts) -> Result<()> {
368        let TargetFetch { tx, provider, .. } = self.fetch_target(&config).await?;
369        let tx_hash = tx.tx_hash();
370        let tracing = self.configure_tracing(&mut config, &evm_opts);
371        let with_local_artifacts = self.with_local_artifacts;
372
373        let endpoint_identity = evm_opts.fork_endpoint_identity().await?;
374        let tx_inclusion = tx
375            .block_hash_num()
376            .ok_or_else(|| eyre::eyre!("tx may still be pending: {:?}", tx_hash))?;
377
378        let frame = call_tracer_frame(
379            provider
380                .debug_trace_transaction(
381                    tx_hash,
382                    GethDebugTracingOptions::call_tracer(call_tracer_config()),
383                )
384                .await,
385            "debug_traceTransaction",
386            "drop `--debug-trace-transaction` to re-execute the transaction locally",
387            "the transaction's block; use an archive endpoint",
388        )?;
389
390        let receipt = provider
391            .get_transaction_receipt(tx_hash)
392            .await?
393            .ok_or_else(|| eyre::eyre!("tx receipt not found: {:?}", tx_hash))?;
394        ensure_remote_transaction_inclusion(
395            tx_hash,
396            tx_inclusion,
397            receipt.block_hash_num(),
398            "transaction receipt",
399        )?;
400
401        let transaction_block = provider.get_block_by_hash(tx_inclusion.hash).await?;
402        ensure_remote_transaction_inclusion(
403            tx_hash,
404            tx_inclusion,
405            transaction_block.as_ref().map(block_num_hash),
406            "block fetched by hash",
407        )?;
408        let Some(transaction_block) = transaction_block else {
409            unreachable!("ensure_remote_transaction_inclusion errors when the block is missing")
410        };
411
412        let root_create_address = Transaction::to(&tx)
413            .is_none()
414            .then(|| receipt.contract_address().unwrap_or_else(|| tx.from().create(tx.nonce())));
415        let arena = SparsedTraceArena {
416            arena: call_frame_to_arena(&frame, root_create_address),
417            ignored: Default::default(),
418            diagnostics: Default::default(),
419        };
420        let result = TraceResult {
421            success: receipt.status(),
422            traces: Some(vec![(TraceKind::Execution, arena)]),
423            gas_used: receipt.gas_used(),
424        };
425
426        // Local-artifact labeling matches deployed runtime bytecode against the project
427        // artifacts. There is no local executor on this path, so fetch the code over RPC for the
428        // addresses in the trace, at the transaction's block. Skip the extra round-trips unless
429        // local artifacts were requested.
430        let contracts_bytecode = if with_local_artifacts {
431            fetch_transaction_contracts_bytecode_via_rpc(
432                &provider,
433                &result,
434                tx_hash,
435                BlockId::hash(tx_inclusion.hash),
436            )
437            .await
438        } else {
439            Default::default()
440        };
441
442        // The remote node executed this trace, so its reported family is authoritative for
443        // decoding even when the caller selected a compatible local EVM implementation.
444        let chain = alloy_chains::Chain::from_id(endpoint_identity.source_chain_id);
445        let resolved_hardfork = resolve_remote_trace_hardfork(
446            config.hardfork,
447            &endpoint_identity,
448            Some(transaction_block.header().timestamp()),
449        );
450        let final_endpoint_identity = evm_opts.fork_endpoint_identity().await?;
451        ensure_remote_trace_context_unchanged(&endpoint_identity, &final_endpoint_identity)?;
452
453        let current_tx = provider.get_transaction_by_hash(tx_hash).await?;
454        if let Some(current_tx) = &current_tx {
455            ensure_requested_transaction(tx_hash, current_tx)?;
456        }
457        ensure_remote_transaction_inclusion(
458            tx_hash,
459            tx_inclusion,
460            current_tx.and_then(|tx| tx.block_hash_num()),
461            "transaction lookup",
462        )?;
463        let canonical_block = provider.get_block_by_number(tx_inclusion.number.into()).await?;
464        ensure_remote_transaction_inclusion(
465            tx_hash,
466            tx_inclusion,
467            canonical_block.as_ref().map(block_num_hash),
468            "canonical block lookup",
469        )?;
470        handle_traces(
471            result,
472            &config,
473            {
474                let context =
475                    TraceContext::new(chain, endpoint_identity.network_profile, resolved_hardfork);
476                context.with_hardfork(context.decoding_hardfork(&config))
477            },
478            &contracts_bytecode,
479            &tracing,
480            with_local_artifacts,
481            false,
482        )
483        .await
484    }
485
486    async fn prepare<FEN: FoundryEvmNetwork>(
487        mut self,
488        mut config: Box<Config>,
489        evm_opts: EvmOpts,
490        target: TargetFetch,
491        executor_builder: ExecutorBuilder<FEN>,
492    ) -> Result<PreparedRun<FEN>> {
493        #[cfg_attr(not(feature = "monad"), allow(unused_variables))]
494        let TargetFetch { tx, provider, compute_units_per_second } = target;
495        let tx_hash = tx.tx_hash();
496        let tracing = self.configure_tracing(&mut config, &evm_opts);
497
498        let tx_block_number = tx
499            .block_number()
500            .ok_or_else(|| eyre::eyre!("tx may still be pending: {:?}", tx_hash))?;
501
502        // we need to fork off the parent block
503        config.fork_block_number = Some(tx_block_number - 1);
504
505        let create2_deployer = evm_opts.create2_deployer;
506        let (block, receipt, mut fork) = tokio::try_join!(
507            // fetch the block the transaction was mined in
508            provider.get_block(tx_block_number.into()).full().into_future().map_err(Into::into),
509            // The receipt only backs the check after the replay, so a failed lookup skips that
510            // check instead of failing the run.
511            async { Ok(provider.get_transaction_receipt(tx_hash).await.ok().flatten()) },
512            TracingExecutor::<FEN>::get_fork(&mut config, evm_opts)
513        )?;
514        let chain = fork.context().chain();
515        let networks = fork.context().networks();
516
517        let mut evm_version = self.evm_version;
518        // Mined transactions already passed the block gas limit check their chain applies, and
519        // some chains admit transactions whose gas limit exceeds it: BSC validator transactions
520        // carry a gas limit of `i64::MAX`. Re-applying the check can only reject a transaction
521        // the chain accepted.
522        fork.evm_env.cfg_env.disable_block_gas_limit = true;
523
524        // By default do not enforce transaction gas limits imposed by Osaka (EIP-7825).
525        // Users can opt-in to enable these limits by setting `enable_tx_gas_limit` to true.
526        if !self.enable_tx_gas_limit {
527            fork.evm_env.cfg_env.tx_gas_limit_cap = Some(u64::MAX);
528        }
529
530        fork.evm_env.cfg_env.limit_contract_code_size = None;
531        fork.evm_env.block_env.set_number(U256::from(tx_block_number));
532
533        let mut parent_beacon_block_root = None;
534        if let Some(block) = &block {
535            fork.evm_env.block_env = block_env_from_header(block.header());
536            parent_beacon_block_root = block.header().parent_beacon_block_root();
537
538            // Unless explicitly configured, resolve the correct spec for the block using the same
539            // approach as reth: walk known chain activation conditions to find the latest active
540            // fork. For unknown chains, probe the node for the features it executes at the block,
541            // falling back to a blob-gas heuristic if the node rejects the probe.
542            if evm_version.is_none()
543                && config.hardfork.is_none()
544                && FoundryHardfork::from_chain_and_timestamp(chain.id(), block.header().timestamp())
545                    .is_none()
546            {
547                let probed = probe_evm_version(&provider, BlockId::number(tx_block_number)).await;
548                evm_version = probed.or_else(|| {
549                    block.header().excess_blob_gas().is_some().then_some(EvmVersion::Cancun)
550                });
551            }
552            apply_chain_and_block_specific_env_changes_for_chain::<AnyNetwork, _, _>(
553                &mut fork.evm_env,
554                block,
555                chain.id(),
556                config.networks,
557            );
558        }
559        fork.resolve_spec(&config, evm_version);
560        fork.extend_precompile_labels(&mut config);
561
562        apply_chain_specific_tx_replay_env_changes_for_chain(&mut fork.evm_env, chain.id());
563
564        let trace_context = fork.context();
565        let mut evm_env = fork.evm_env.clone();
566
567        let mut executor = fork.into_executor(
568            executor_builder,
569            TraceRequirements::none(),
570            create2_deployer,
571            None,
572        )?;
573        // The fork is pinned to the parent block, but the replayed transactions execute in the
574        // target's block, which block queries such as Arbitrum's `ArbSys.arbBlockNumber()` must
575        // report.
576        executor.backend_mut().set_fork_block_number_override(tx_block_number);
577
578        evm_env.cfg_env.set_spec_and_mainnet_gas_params(executor.spec_id());
579
580        let spec_id = (*evm_env.cfg_env.spec()).into();
581
582        if let Some(parent_beacon_block_root) =
583            parent_beacon_block_root_for_network(networks, spec_id, parent_beacon_block_root)
584        {
585            executor.apply_beacon_root(parent_beacon_block_root)?;
586        }
587
588        // Set the state to the moment right before the transaction.
589        //
590        // When `--prestate-tracer` is set, opportunistically try to fetch the prestate directly
591        // via `debug_traceTransaction` (much faster than replaying the block). This requires the
592        // `debug_` namespace, which most nodes don't expose, so it is opt-in and silently falls
593        // back to the block access list, then to replaying previous transactions in the block,
594        // if the call or parsing fails.
595        let mut prestate_applied = false;
596        if !self.quick && self.prestate_tracer {
597            trace!(?tx_hash, "attempting to fetch prestate via debug_traceTransaction");
598            match provider
599                .debug_trace_transaction(
600                    tx_hash,
601                    GethDebugTracingOptions::prestate_tracer(PreStateConfig::default()),
602                )
603                .await
604            {
605                Ok(trace) => match trace.try_into_pre_state_frame() {
606                    Ok(pre_state_frame) => {
607                        executor.apply_prestate_trace(pre_state_frame.into_pre_state())?;
608                        prestate_applied = true;
609                        trace!("prestate trace applied successfully, skipping block replay");
610                    }
611                    Err(err) => {
612                        trace!(%err, "failed to parse prestate trace response");
613                    }
614                },
615                Err(err) => {
616                    trace!(?err, "debug_traceTransaction failed, falling back to block replay");
617                }
618            }
619        }
620
621        // A block access list (BAL) records every state write of the block by transaction index,
622        // so the target's prestate can be read from it instead of replaying the earlier
623        // transactions. Before Cancun, SELFDESTRUCT wipes storage the list does not enumerate.
624        // Explicit execution rules must replay the prefix: canonical writes may differ under
625        // the requested EVM version or hardfork.
626        let block_access_list = if !self.quick
627            && !self.no_bal
628            && !prestate_applied
629            && self.evm_version.is_none()
630            && config.hardfork.is_none()
631            && spec_id.is_enabled_in(SpecId::CANCUN)
632            && let Some(block) = &block
633        {
634            fetch_block_access_list(&provider, block).await?
635        } else {
636            None
637        };
638
639        let mut replay_tx_hashes = vec![tx_hash];
640        if !self.quick
641            && !prestate_applied
642            && block_access_list.is_none()
643            && let Some(block) = &block
644        {
645            replay_tx_hashes.extend(
646                full_transactions(block)?
647                    .iter()
648                    .take_while(|tx| tx.tx_hash() != tx_hash)
649                    .filter(|tx| !is_system_transaction(tx) || self.replay_system_txes)
650                    .map(TransactionResponse::tx_hash),
651            );
652        }
653        let replay_gas_limits =
654            ReplayGasLimits::fetch(chain.id(), &provider, replay_tx_hashes).await?;
655
656        Ok(PreparedRun {
657            args: self,
658            config,
659            tracing,
660            tx,
661            block,
662            evm_env,
663            executor,
664            trace_context,
665            prestate_applied,
666            block_access_list,
667            replay_gas_limits,
668            receipt,
669            #[cfg(feature = "monad")]
670            monad: MonadPrepared { tx_block_number, compute_units_per_second },
671        })
672    }
673}
674
675/// Fetches the block access list of `block`, if the node serves one that matches the header.
676async fn fetch_block_access_list(
677    provider: &RetryProvider,
678    block: &AnyRpcBlock,
679) -> Result<Option<Arc<Bal>>> {
680    let header = block.header();
681    let bal = match provider.get_block_access_list(BlockId::hash(header.hash)).await {
682        Ok(Some(bal)) => bal,
683        Ok(None) => return Ok(None),
684        Err(err) => {
685            trace!(%err, "block access list unavailable, falling back to block replay");
686            return Ok(None);
687        }
688    };
689    if let Some(hash) = header.block_access_list_hash()
690        && compute_block_access_list_hash(&bal) != hash
691    {
692        sh_warn!(
693            "block access list of block {} does not match its header, replaying the block instead",
694            header.number()
695        )?;
696        return Ok(None);
697    }
698    Ok(Some(Arc::new(Bal::try_from_alloy(bal).wrap_err("invalid block access list")?)))
699}
700
701impl<FEN: FoundryEvmNetwork> PreparedRun<FEN> {
702    fn transaction_env(&self, tx: &AnyRpcTransaction) -> Result<TxEnvFor<FEN>> {
703        let mut tx_env = TxEnvFor::<FEN>::from_any_rpc_transaction(tx)?;
704        self.replay_gas_limits.apply::<FEN>(tx, &mut tx_env)?;
705        Ok(tx_env)
706    }
707
708    /// Prepares the executor for the target transaction: enables tracing and, when the sender
709    /// was forged, disables the balance check.
710    fn prepare_target(&mut self) {
711        let requirements = TraceRequirements::none()
712            .with_calls(true)
713            .with_debug(self.args.debug)
714            .with_decode_internal(if self.tracing.decode_internal {
715                InternalTraceMode::Full
716            } else {
717                InternalTraceMode::None
718            })
719            .with_state_changes(self.tracing.verbosity > 4);
720        self.executor.set_trace_requirements(requirements);
721        self.executor.set_trace_printer(self.args.trace_printer);
722
723        let sender_is_forged = match &*self.tx.inner.inner {
724            AnyTxEnvelope::Ethereum(inner) => {
725                inner.recover_signer().is_ok_and(|signer| signer != self.tx.from())
726            }
727            AnyTxEnvelope::Unknown(_) => true,
728        };
729        if sender_is_forged {
730            self.evm_env.cfg_env.disable_balance_check = true;
731        }
732    }
733
734    /// Returns the index of the target transaction in its block.
735    fn target_index(&self) -> Result<usize> {
736        let Some(block) = &self.block else { return Ok(0) };
737        full_transactions(block)?
738            .iter()
739            .position(|candidate| candidate.tx_hash() == self.tx.tx_hash())
740            .ok_or_else(|| {
741                eyre::eyre!("transaction {:?} is missing from its block", self.tx.tx_hash())
742            })
743    }
744
745    /// Calls `f` for every transaction mined before the target, unless the block replay was
746    /// skipped (`--quick` or an applied prestate).
747    fn for_each_prefix_transaction(
748        &self,
749        target_index: usize,
750        mut f: impl FnMut(usize, &AnyRpcTransaction) -> Result<()>,
751    ) -> Result<()> {
752        if self.args.quick || self.prestate_applied {
753            return Ok(());
754        }
755        sh_status!("Executing previous transactions from the block.")?;
756        let Some(block) = &self.block else { return Ok(()) };
757        let txs = full_transactions(block)?;
758        let pb = init_progress(txs.len() as u64, "tx");
759        for (index, tx) in txs.iter().take(target_index).enumerate() {
760            if let Some(to) = Transaction::to(tx) {
761                trace!(tx=?tx.tx_hash(), ?to, "preparing previous call transaction");
762            } else {
763                trace!(tx=?tx.tx_hash(), "preparing previous create transaction");
764            }
765            f(index, tx)?;
766            pb.set_position((index + 1) as u64);
767        }
768        Ok(())
769    }
770
771    fn trace_kind(&self) -> TraceKind {
772        if let Some(to) = Transaction::to(&self.tx) {
773            trace!(tx=?self.tx.tx_hash(), ?to, "executing call transaction");
774            TraceKind::Execution
775        } else {
776            trace!(tx=?self.tx.tx_hash(), "executing create transaction");
777            TraceKind::Deployment
778        }
779    }
780
781    fn execute_ordinary(&mut self) -> Result<TraceResult> {
782        // Decode the target transaction before replaying the block: an envelope this build
783        // can't decode should fail fast.
784        let target_tx_env = self.transaction_env(&self.tx).wrap_err_with(|| {
785            format!(
786                "cannot replay transaction {:?} locally; {REMOTE_TRACE_HINT}",
787                self.tx.tx_hash()
788            )
789        })?;
790        let target_index = self.target_index()?;
791        self.prepare_target();
792
793        let mut replay = Vec::new();
794        if let Some(bal) = self.block_access_list.take() {
795            // Index 0 holds the pre-block system writes and transaction `i` is index `i + 1`, so
796            // reads positioned at the target's own index see exactly the earlier writes.
797            trace!("reading prestate from block access list, skipping block replay");
798            self.executor
799                .backend_mut()
800                .set_bal(bal, BlockAccessIndex::new(target_index as u64 + 1));
801        } else {
802            let block_number = self.evm_env.block_env.number();
803            let replay_system_txes = self.args.replay_system_txes;
804            self.for_each_prefix_transaction(target_index, |_, tx| {
805                if !is_system_transaction(tx) || replay_system_txes {
806                    let tx_env = self.transaction_env(tx).wrap_err_with(|| {
807                        format!(
808                            "Failed to prepare transaction: {:?} in block {}; `--quick` skips the \
809                             transactions before the target, and {REMOTE_TRACE_HINT}",
810                            tx.tx_hash(),
811                            block_number
812                        )
813                    })?;
814                    replay.push((tx.tx_hash(), tx_env));
815                }
816                Ok(())
817            })?;
818        }
819        let result = self.executor.transact_with_ordinary_block_replay(
820            self.evm_env.clone(),
821            target_tx_env,
822            replay,
823        )?;
824        let trace_kind = self.trace_kind();
825        trace!(tx_hash=?self.tx.tx_hash(), "completed execution");
826        Ok(TraceResult::from_raw(result, trace_kind))
827    }
828
829    async fn finish(self, result: TraceResult) -> Result<()> {
830        let contracts_bytecode = fetch_contracts_bytecode_from_trace(&self.executor, &result)?;
831        let (success, gas_used) = (result.success, result.gas_used);
832        handle_traces(
833            result,
834            &self.config,
835            self.trace_context,
836            &contracts_bytecode,
837            &self.tracing,
838            self.args.with_local_artifacts,
839            self.args.debug,
840        )
841        .await?;
842        self.warn_on_receipt_mismatch(success, gas_used)
843    }
844
845    /// Warns when the replay does not reproduce the target's receipt.
846    ///
847    /// A replay diverges when the chain applies rules the local EVM does not model or the replay
848    /// starts from different state, and the trace alone gives no sign of it.
849    fn warn_on_receipt_mismatch(&self, success: bool, gas_used: u64) -> Result<()> {
850        let Some(receipt) = &self.receipt else { return Ok(()) };
851        let outcome = |success: bool| if success { "succeeded" } else { "reverted" };
852        let mut differences = Vec::new();
853        if receipt.status() != success {
854            differences.push(format!(
855                "it {} on-chain but {} in the replay",
856                outcome(receipt.status()),
857                outcome(success)
858            ));
859        }
860        if let Some(expected) = self.expected_replay_gas(receipt)
861            && expected != gas_used
862        {
863            differences
864                .push(format!("it used {expected} gas on-chain but {gas_used} in the replay"));
865        }
866        if differences.is_empty() {
867            return Ok(());
868        }
869        let hint = if self.args.quick {
870            "`--quick` skips the transactions before it in the block, which can change the result; \
871             run without it to replay them first"
872        } else {
873            "The chain may apply rules the replay does not model; `--debug-trace-transaction` \
874             shows the node's own trace if it exposes the `debug` namespace"
875        };
876        sh_warn!(
877            "the replay does not match the transaction's receipt: {}. {hint}.",
878            differences.join(", and ")
879        )
880    }
881
882    /// Returns the gas the replay should report for `receipt`, or `None` if the receipt does not
883    /// record the gas the transaction executed.
884    fn expected_replay_gas(&self, receipt: &AnyTransactionReceipt) -> Option<u64> {
885        // Monad charges the full gas limit and reports it as the gas used.
886        if self.trace_context.networks().is_monad() {
887            return None;
888        }
889        let gas_used = receipt.gas_used();
890        if foundry_evm_networks::arbitrum::is_arbitrum_chain(self.trace_context.chain().id()) {
891            // Nitro folds the L1 posting cost into the receipt's gas used.
892            let l1_gas_used =
893                parse_nitro_l1_gas_used(receipt.other_fields().get("gasUsedForL1")).ok()?;
894            return gas_used.checked_sub(l1_gas_used);
895        }
896        Some(gas_used)
897    }
898}
899
900#[cfg(feature = "monad")]
901impl PreparedRun<MonadEvmNetwork> {
902    async fn execute_monad(&mut self) -> Result<TraceResult> {
903        // `BlockContext` is typed to `MonadEvmNetwork::Network` (`Ethereum`). Monad blocks only
904        // carry standard envelopes, so a typed provider can serve this path while the rest of the
905        // command stays on `AnyNetwork`.
906        let provider = ProviderBuilder::<alloy_network::Ethereum>::from_config(&self.config)?
907            .compute_units_per_second_opt(self.monad.compute_units_per_second)
908            .build()?;
909        let block =
910            provider.get_block(self.monad.tx_block_number.into()).full().await?.ok_or_else(
911                || {
912                    eyre::eyre!(
913                        "block {} is required to reconstruct transaction context",
914                        self.monad.tx_block_number
915                    )
916                },
917            )?;
918        let block_context = BlockContext::<MonadEvmNetwork>::fetch(&provider, &block).await?;
919        // Decode the target transaction before replaying the block: an envelope this build
920        // can't decode should fail fast, not after paying for the entire prior-transaction
921        // replay.
922        let target_tx_env = TxEnv::from_any_rpc_transaction(&self.tx)?;
923        let target_index = self.target_index()?;
924        self.prepare_target();
925
926        let mut replay = Vec::new();
927        self.for_each_prefix_transaction(target_index, |index, tx| {
928            let tx_env = TxEnv::from_any_rpc_transaction(tx)?;
929            let chain_context = block_context.transaction(index);
930            replay.push((tx.tx_hash(), tx_env, chain_context));
931            Ok(())
932        })?;
933        let result = self.executor.transact_with_monad_block_replay(
934            self.evm_env.clone(),
935            target_tx_env,
936            block_context.transaction(target_index),
937            replay,
938            self.args.replay_system_txes,
939        )?;
940        let Some((result, used_system_replay)) = result else {
941            eyre::bail!(
942                "{:?} is a system transaction.\nReplaying system transactions is currently not supported.",
943                self.tx.tx_hash()
944            );
945        };
946        if used_system_replay {
947            trace!(tx=?self.tx.tx_hash(), "executed canonical system transaction");
948        }
949        Ok(TraceResult::from_raw(result, self.trace_kind()))
950    }
951}
952
953fn is_system_transaction(tx: &AnyRpcTransaction) -> bool {
954    is_known_system_sender(tx.from()) || tx.transaction_type() == Some(SYSTEM_TRANSACTION_TYPE)
955}
956
957fn full_transactions(block: &AnyRpcBlock) -> Result<&[AnyRpcTransaction]> {
958    let BlockTransactions::Full(txs) = block.transactions() else {
959        eyre::bail!("Could not get block txs");
960    };
961    Ok(txs)
962}
963
964impl ReplayGasLimits {
965    async fn fetch(chain_id: u64, provider: &RetryProvider, tx_hashes: Vec<B256>) -> Result<Self> {
966        if !foundry_evm_networks::arbitrum::is_arbitrum_chain(chain_id) {
967            return Ok(Self::Unchanged);
968        }
969
970        let mut requests = futures::stream::iter(tx_hashes)
971            .map(|tx_hash| fetch_nitro_l1_gas_used(provider, tx_hash))
972            .buffer_unordered(MAX_CONCURRENT_RPC_REQUESTS);
973        let mut gas_used = B256HashMap::default();
974        while let Some(result) = requests.next().await {
975            let (tx_hash, l1_gas_used) = result?;
976            gas_used.insert(tx_hash, l1_gas_used);
977        }
978        Ok(Self::Nitro(gas_used))
979    }
980
981    fn apply<FEN: FoundryEvmNetwork>(
982        &self,
983        tx: &AnyRpcTransaction,
984        tx_env: &mut TxEnvFor<FEN>,
985    ) -> Result<()> {
986        let Self::Nitro(l1_gas_used) = self else { return Ok(()) };
987        let tx_hash = tx.tx_hash();
988        let l1_gas_used = l1_gas_used
989            .get(&tx_hash)
990            .ok_or_else(|| eyre::eyre!("receipt not found for Nitro transaction {tx_hash:?}"))?;
991        tx_env.set_gas_limit(nitro_execution_gas_limit(tx_hash, tx.gas_limit(), *l1_gas_used)?);
992        Ok(())
993    }
994}
995
996async fn fetch_nitro_l1_gas_used(provider: &RetryProvider, tx_hash: B256) -> Result<(B256, u64)> {
997    let receipt = provider
998        .get_transaction_receipt(tx_hash)
999        .await?
1000        .ok_or_else(|| eyre::eyre!("receipt not found for Nitro transaction {tx_hash:?}"))?;
1001    let l1_gas_used = parse_nitro_l1_gas_used(receipt.other_fields().get("gasUsedForL1"))
1002        .wrap_err_with(|| format!("invalid Nitro poster gas for transaction {tx_hash:?}"))?;
1003    Ok((tx_hash, l1_gas_used))
1004}
1005
1006fn parse_nitro_l1_gas_used(field: Option<&serde_json::Value>) -> Result<u64> {
1007    let field = field.ok_or_else(|| eyre::eyre!("missing `gasUsedForL1` receipt field"))?;
1008    let value = serde_json::from_value::<U256>(field.clone())
1009        .wrap_err("malformed `gasUsedForL1` receipt field")?;
1010    value.try_into().map_err(|_| eyre::eyre!("`gasUsedForL1` value {value} exceeds u64::MAX"))
1011}
1012
1013fn nitro_execution_gas_limit(tx_hash: B256, gas_limit: u64, l1_gas_used: u64) -> Result<u64> {
1014    gas_limit.checked_sub(l1_gas_used).ok_or_else(|| {
1015        eyre::eyre!(
1016            "Nitro poster gas {l1_gas_used} exceeds gas limit {gas_limit} for transaction {tx_hash:?}"
1017        )
1018    })
1019}
1020
1021/// Returns the number and hash of a fetched block.
1022pub(super) fn block_num_hash<B: BlockResponse>(block: &B) -> BlockNumHash
1023where
1024    B::Header: HeaderResponse,
1025{
1026    BlockNumHash::new(block.header().number(), block.header().hash())
1027}
1028
1029/// Extracts the `callTracer` frame from a `debug_trace*` response.
1030///
1031/// Two RPC rejections deserve an actionable hint instead of the raw transport error, and they
1032/// need different fixes: a disabled `debug` namespace, and missing historical state, hit
1033/// whenever a full node has pruned the traced block.
1034pub(super) fn call_tracer_frame(
1035    response: Result<GethTrace, TransportError>,
1036    method: &str,
1037    local_hint: &str,
1038    missing_state_hint: &str,
1039) -> Result<CallFrame> {
1040    let trace = response.map_err(|err| -> eyre::Report {
1041        if is_method_not_found_error(&err) {
1042            eyre::eyre!(
1043                "the RPC endpoint does not support `{method}` (method not found); use a node with the `debug` namespace enabled (e.g. a local anvil/reth or an archive endpoint), or {local_hint}"
1044            )
1045        } else if is_missing_state_error(&err) {
1046            eyre::eyre!(
1047                "the RPC endpoint does not have the historical state for {missing_state_hint}"
1048            )
1049        } else {
1050            err.into()
1051        }
1052    })?;
1053    let GethTrace::CallTracer(frame) = trace else {
1054        eyre::bail!(
1055            "`{method}` did not return a callTracer frame; the RPC endpoint may not support the `callTracer`"
1056        );
1057    };
1058    Ok(frame)
1059}
1060
1061fn ensure_remote_transaction_inclusion(
1062    tx_hash: B256,
1063    expected: BlockNumHash,
1064    actual: Option<BlockNumHash>,
1065    source: &str,
1066) -> Result<()> {
1067    let Some(actual) = actual else {
1068        eyre::bail!(
1069            "transaction {tx_hash} changed inclusion while collecting its remote trace: {source} no longer reports it as mined; retry the command"
1070        );
1071    };
1072    if actual != expected {
1073        eyre::bail!(
1074            "transaction {tx_hash} changed inclusion while collecting its remote trace: expected block {} at {}, but {source} reported block {} at {}; retry the command",
1075            expected.hash,
1076            expected.number,
1077            actual.hash,
1078            actual.number,
1079        );
1080    }
1081
1082    Ok(())
1083}
1084
1085/// Ensures the RPC answered `eth_getTransactionByHash` with the requested transaction.
1086fn ensure_requested_transaction(requested: B256, tx: &AnyRpcTransaction) -> Result<()> {
1087    let returned = tx.tx_hash();
1088    eyre::ensure!(
1089        returned == requested,
1090        "RPC returned transaction {returned:?} for requested {requested:?}"
1091    );
1092    Ok(())
1093}
1094
1095const fn parent_beacon_block_root_for_network(
1096    networks: NetworkConfigs,
1097    spec_id: SpecId,
1098    parent_beacon_block_root: Option<B256>,
1099) -> Option<B256> {
1100    if networks.is_monad() || !spec_id.is_enabled_in(SpecId::CANCUN) {
1101        return None;
1102    }
1103
1104    // Chains that run a Cancun or later EVM without Ethereum's beacon chain, such as Polygon and
1105    // Scroll, never populate this header field and never deploy the EIP-4788 contract, so there
1106    // is no root to apply. Requiring one makes their blocks unreplayable.
1107    parent_beacon_block_root
1108}
1109
1110pub fn fetch_contracts_bytecode_from_trace<FEN: FoundryEvmNetwork>(
1111    executor: &Executor<FEN>,
1112    result: &TraceResult,
1113) -> Result<AddressHashMap<Bytes>> {
1114    let contracts_bytecode = trace_addresses(result)
1115        .filter_map(|addr| {
1116            // All relevant bytecodes should already be cached in the executor.
1117            let code = executor
1118                .backend()
1119                .basic_ref(addr)
1120                .inspect_err(|e| _ = sh_warn!("Failed to fetch code for {addr}: {e}"))
1121                .ok()??
1122                .code?
1123                .bytes();
1124            (!code.is_empty()).then_some((addr, code))
1125        })
1126        .collect();
1127    Ok(contracts_bytecode)
1128}
1129
1130/// Fetches bytecode for a mined transaction at its exact transaction index.
1131///
1132/// The prestate tracer provides the code that existed immediately before the transaction, which
1133/// avoids reading end-of-block state for contracts changed or removed by later transactions. Any
1134/// address absent from the prestate (for example, a contract created by this transaction) falls
1135/// back to `eth_getCode` at the transaction's block.
1136async fn fetch_transaction_contracts_bytecode_via_rpc<N: Network, P: Provider<N>>(
1137    provider: &P,
1138    result: &TraceResult,
1139    tx_hash: B256,
1140    block: BlockId,
1141) -> AddressHashMap<Bytes> {
1142    let mut contracts_bytecode = AddressHashMap::default();
1143    let prestate_config = PreStateConfig { disable_storage: Some(true), ..Default::default() };
1144    match provider
1145        .debug_trace_transaction(tx_hash, GethDebugTracingOptions::prestate_tracer(prestate_config))
1146        .await
1147    {
1148        Ok(trace) => match trace.try_into_pre_state_frame() {
1149            Ok(prestate) => {
1150                for (&address, account) in prestate.pre_state() {
1151                    if let Some(code) = account.code.clone().filter(|code| !code.is_empty()) {
1152                        contracts_bytecode.insert(address, code);
1153                    }
1154                }
1155            }
1156            Err(err) => {
1157                let _ = sh_warn!("Failed to parse transaction prestate for local artifacts: {err}");
1158            }
1159        },
1160        Err(err) => {
1161            let _ = sh_warn!("Failed to fetch transaction prestate for local artifacts: {err}");
1162        }
1163    }
1164
1165    let missing_addresses = trace_addresses(result)
1166        .filter(|address| !contracts_bytecode.contains_key(address))
1167        .collect::<Vec<_>>();
1168    contracts_bytecode.extend(fetch_code_via_rpc(provider, missing_addresses, block).await);
1169    contracts_bytecode
1170}
1171
1172/// Returns the distinct non-zero addresses and callers seen in the traces of `result`.
1173pub(super) fn trace_addresses(result: &TraceResult) -> impl Iterator<Item = Address> {
1174    let mut addresses = AddressSet::default();
1175    for (_, trace) in result.traces.iter().flatten() {
1176        for node in trace.arena.nodes() {
1177            addresses.extend(
1178                [node.trace.address, node.trace.caller].into_iter().filter(|a| !a.is_zero()),
1179            );
1180        }
1181    }
1182    addresses.into_iter()
1183}
1184
1185impl figment::Provider for RunArgs {
1186    fn metadata(&self) -> Metadata {
1187        Metadata::named("RunArgs")
1188    }
1189
1190    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
1191        let mut map = Map::new();
1192
1193        if let Some(api_key) = &self.etherscan.key {
1194            map.insert("etherscan_api_key".into(), api_key.as_str().into());
1195        }
1196
1197        if let Some(evm_version) = self.evm_version {
1198            map.insert("evm_version".into(), figment::value::Value::serialize(evm_version)?);
1199        }
1200
1201        Ok(Map::from([(Config::selected_profile(), map)]))
1202    }
1203}
1204
1205#[cfg(test)]
1206mod tests {
1207
1208    use super::*;
1209
1210    #[test]
1211    fn http_wrapped_method_not_found_has_trace_guidance() {
1212        let error = alloy_transport::TransportErrorKind::http_error(
1213            403,
1214            r#"{"jsonrpc":"2.0","error":{"code":-32601,"message":"method disabled"}}"#.into(),
1215        );
1216        let error = call_tracer_frame(
1217            Err(error),
1218            "debug_traceTransaction",
1219            "replay locally",
1220            "this transaction",
1221        )
1222        .unwrap_err();
1223        assert_eq!(
1224            error.to_string(),
1225            "the RPC endpoint does not support `debug_traceTransaction` (method not found); use a node with the `debug` namespace enabled (e.g. a local anvil/reth or an archive endpoint), or replay locally"
1226        );
1227    }
1228
1229    #[test]
1230    fn parses_legacy_short_label_alias() {
1231        let address = Address::with_last_byte(1);
1232        let label = format!("{address}:alice");
1233        let args = RunArgs::parse_from(["cast run", "0x00", "-l", &label]);
1234
1235        assert_eq!(args.legacy_labels, vec![label]);
1236    }
1237
1238    #[test]
1239    fn debug_trace_transaction_accepts_label_and_render_flags() {
1240        let args = RunArgs::try_parse_from([
1241            "foundry-cli",
1242            "--debug-trace-transaction",
1243            "0x0000000000000000000000000000000000000000000000000000000000000000",
1244            "--label",
1245            "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045:vitalik.eth",
1246            "--disable-labels",
1247            "--trace-depth",
1248            "2",
1249            "--with-local-artifacts",
1250        ]);
1251        assert!(args.is_ok(), "--debug-trace-transaction must accept label/rendering flags");
1252    }
1253
1254    #[test]
1255    fn parent_beacon_block_root_is_applied_only_when_the_header_has_one() {
1256        let networks = NetworkConfigs::default();
1257        let root = Some(B256::repeat_byte(0x42));
1258        // Polygon and Scroll run a Cancun or later EVM without populating the header field.
1259        for (networks, spec_id, root, expected) in [
1260            (networks, SpecId::CANCUN, None, None),
1261            (networks, SpecId::CANCUN, root, root),
1262            (networks, SpecId::SHANGHAI, root, None),
1263            (networks, SpecId::SHANGHAI, None, None),
1264            #[cfg(feature = "monad")]
1265            (NetworkConfigs::with_monad(), SpecId::PRAGUE, root, None),
1266            #[cfg(feature = "monad")]
1267            (NetworkConfigs::with_monad(), SpecId::OSAKA, root, None),
1268            #[cfg(feature = "monad")]
1269            (NetworkConfigs::with_monad(), SpecId::PRAGUE, None, None),
1270            #[cfg(feature = "monad")]
1271            (NetworkConfigs::with_monad(), SpecId::OSAKA, None, None),
1272        ] {
1273            assert_eq!(parent_beacon_block_root_for_network(networks, spec_id, root), expected);
1274        }
1275    }
1276
1277    #[test]
1278    fn debug_trace_transaction_ignores_configured_internal_decoding() {
1279        let args = RunArgs::parse_from(["cast run", "0x00", "--debug-trace-transaction"]);
1280        let config = TracingConfig { decode_internal: true, ..Default::default() };
1281
1282        assert!(!args.resolve_tracing(&config, 0).decode_internal);
1283    }
1284
1285    #[test]
1286    fn parses_nitro_l1_gas_used() {
1287        let field = serde_json::json!("0x26ed52");
1288
1289        assert_eq!(parse_nitro_l1_gas_used(Some(&field)).unwrap(), 2_551_122);
1290        assert_eq!(nitro_execution_gas_limit(B256::ZERO, 2_733_748, 2_551_122).unwrap(), 182_626);
1291    }
1292
1293    #[test]
1294    fn rejects_invalid_nitro_gas() {
1295        assert_eq!(
1296            parse_nitro_l1_gas_used(None).unwrap_err().to_string(),
1297            "missing `gasUsedForL1` receipt field"
1298        );
1299
1300        let field = serde_json::json!("invalid");
1301        assert_eq!(
1302            parse_nitro_l1_gas_used(Some(&field)).unwrap_err().to_string(),
1303            "malformed `gasUsedForL1` receipt field"
1304        );
1305
1306        let field = serde_json::json!("0x10000000000000000");
1307        assert_eq!(
1308            parse_nitro_l1_gas_used(Some(&field)).unwrap_err().to_string(),
1309            "`gasUsedForL1` value 18446744073709551616 exceeds u64::MAX"
1310        );
1311
1312        let tx_hash = B256::repeat_byte(0x42);
1313        assert_eq!(
1314            nitro_execution_gas_limit(tx_hash, 100, 101).unwrap_err().to_string(),
1315            format!("Nitro poster gas 101 exceeds gas limit 100 for transaction {tx_hash:?}")
1316        );
1317    }
1318}