From d8958d81727918e18d793fd7741cddce625f7ec8 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Sat, 29 Aug 2026 04:01:48 +0200 Subject: [PATCH 1/2] fix(cast): decode replay blocks with AnyNetwork `cast run` built its provider from `FEN::Network`, which for every chain outside Optimism, Tempo and Monad is the strict Ethereum network. A block containing a transaction type that envelope cannot decode failed to deserialize as a whole: Error: deserialization error: data did not match any variant of untagged enum BlockTransactions Arbitrum puts its internal transaction (type `0x6a`) at index 0 of every block, so no Arbitrum transaction has ever been replayable. Celo (`0x7b`) and OP-stack forks Foundry does not route to the Optimism network, such as Mantle and Berachain (`0x7e`), fail the same way. `cast tx` and `cast block --full` already use `AnyNetwork` and were unaffected. RPC now uses `AnyNetwork` and transactions convert through `FromAnyRpcTransaction`, which execution already relies on in the fork backend. Transactions are classified as system transactions before being decoded, since a chain's reserved envelopes are exactly the ones this build may not decode, and Monad keeps a typed provider for its block context. `FromAnyRpcTransaction for OpTx` left `enveloped_tx` empty, which op-revm rejects for non-deposit transactions because the L1 data fee is charged off those bytes. It now carries the 2718 encoding, which keeps Optimism and Base exact through the new path. Verified against live nodes: Arbitrum transactions replay and match receipt gas exactly once `gasUsedForL1` is subtracted, and Ethereum, Optimism and Base still match receipt gas exactly. --- .changelog/cast-run-any-network.md | 6 +++ crates/cast/src/cmd/run.rs | 62 +++++++++++++++++++++++------- crates/evm/core/src/env.rs | 12 +++++- 3 files changed, 66 insertions(+), 14 deletions(-) create mode 100644 .changelog/cast-run-any-network.md diff --git a/.changelog/cast-run-any-network.md b/.changelog/cast-run-any-network.md new file mode 100644 index 0000000000000..845088637140c --- /dev/null +++ b/.changelog/cast-run-any-network.md @@ -0,0 +1,6 @@ +--- +cast: patch +foundry-evm-core: patch +--- + +Made `cast run` decode blocks with `AnyNetwork`, so chains whose blocks carry non-standard transaction types, such as Arbitrum, Celo and unrouted OP-stack forks, can be replayed at all. diff --git a/crates/cast/src/cmd/run.rs b/crates/cast/src/cmd/run.rs index 7b8d8fd020914..6b7d2d15cf76a 100644 --- a/crates/cast/src/cmd/run.rs +++ b/crates/cast/src/cmd/run.rs @@ -12,9 +12,9 @@ use crate::{ }; use alloy_consensus::{BlockHeader, Transaction, transaction::SignerRecoverable}; use alloy_eips::BlockNumHash; -use alloy_evm::FromRecoveredTx; use alloy_network::{ - BlockResponse, Network, ReceiptResponse, TransactionResponse, primitives::HeaderResponse, + AnyNetwork, AnyTxEnvelope, BlockResponse, Network, ReceiptResponse, TransactionResponse, + primitives::HeaderResponse, }; use alloy_primitives::{ Address, B256, Bytes, U256, @@ -47,6 +47,7 @@ use foundry_evm::core::evm::OpEvmNetwork; use foundry_evm::{ core::{ FoundryBlock as _, FoundryChain, + env::FromAnyRpcTransaction as _, evm::{ BlockContext, ChainFor, EthEvmNetwork, FoundryEvmNetwork, TempoEvmNetwork, TxEnvFor, }, @@ -200,7 +201,11 @@ impl RunArgs { self.rpc.common.compute_units_per_second }; - let provider = ProviderBuilder::::from_config(&config)? + // `AnyNetwork` rather than `FEN::Network`: chains such as Arbitrum, Celo and the + // OP-stack forks Foundry does not route to a dedicated network put transaction types the + // strict Ethereum envelope cannot decode into every block, which would fail the full + // block fetch below for the whole chain. Execution still uses `FEN`. + let provider = ProviderBuilder::::from_config(&config)? .compute_units_per_second_opt(compute_units_per_second) .build()?; @@ -373,9 +378,17 @@ impl RunArgs { return Ok(()); } - let target_tx_env = TxEnvFor::::from_recovered_tx(tx.as_ref(), tx.from()); let target_is_system = is_known_system_sender(tx.from()) || tx.transaction_type() == Some(SYSTEM_TRANSACTION_TYPE); + // Report an unsupported system transaction before decoding it: the envelopes a chain + // reserves for itself, such as Arbitrum's internal transaction, are exactly the ones this + // build may not be able to decode. + if target_is_system && !self.replay_system_txes && !evm_opts.networks.is_monad() { + return Err(eyre::eyre!( + "{tx_hash:?} is a system transaction.\nReplaying system transactions is currently not supported." + )); + } + let target_tx_env = TxEnvFor::::from_any_rpc_transaction(&tx)?; let tx_block_number = tx .block_number() @@ -421,7 +434,7 @@ impl RunArgs { // TODO: add glamsterdam header field checks in the future evm_version = Some(EvmVersion::Cancun); } - apply_chain_and_block_specific_env_changes_for_chain::( + apply_chain_and_block_specific_env_changes_for_chain::( &mut evm_env, block, chain.id(), @@ -439,12 +452,20 @@ impl RunArgs { TracingExecutor::::extend_precompile_labels(&mut config, networks, resolved_hardfork); let block_context = if networks.is_monad() { - let block = block.as_ref().ok_or_else(|| { - eyre::eyre!( - "block {tx_block_number} is required to reconstruct transaction context" - ) - })?; - Some(BlockContext::::fetch(&provider, block).await?) + // `BlockContext` is typed to `FEN::Network`. Monad blocks only carry standard + // envelopes, so a typed provider can serve this path while the rest of the command + // stays on `AnyNetwork`. + let typed_provider = ProviderBuilder::::from_config(&config)? + .compute_units_per_second_opt(compute_units_per_second) + .build()?; + let block = typed_provider.get_block(tx_block_number.into()).full().await?.ok_or_else( + || { + eyre::eyre!( + "block {tx_block_number} is required to reconstruct transaction context" + ) + }, + )?; + Some(BlockContext::::fetch(&typed_provider, &block).await?) } else { None }; @@ -519,9 +540,15 @@ impl RunArgs { break; } - let tx_env = TxEnvFor::::from_recovered_tx(tx.as_ref(), tx.from()); let is_system = is_known_system_sender(tx.from()) || tx.transaction_type() == Some(SYSTEM_TRANSACTION_TYPE); + // Classify before converting: a chain's own system envelopes are exactly the + // ones this build may not be able to decode, and they are skipped below. + if is_system && !self.replay_system_txes && !networks.is_monad() { + pb.set_position((index + 1) as u64); + continue; + } + let tx_env = TxEnvFor::::from_any_rpc_transaction(tx)?; let chain_context = block_context.as_ref().map_or_else( || ChainFor::::for_transaction(&tx_env), |context| context.transaction(index), @@ -635,7 +662,16 @@ impl RunArgs { |context| context.transaction(target_index), ); - if tx.as_ref().recover_signer().is_ok_and(|signer| signer != tx.from()) { + // A recovered signer that disagrees with the `from` the node reports marks a + // transaction the chain injected rather than one a key signed, such as a HyperCore + // credit. Envelopes this build cannot decode are in the same category. + let sender_is_forged = match &*tx.inner.inner { + AnyTxEnvelope::Ethereum(inner) => { + inner.recover_signer().is_ok_and(|signer| signer != tx.from()) + } + AnyTxEnvelope::Unknown(_) => true, + }; + if sender_is_forged { evm_env.cfg_env.disable_balance_check = true; } diff --git a/crates/evm/core/src/env.rs b/crates/evm/core/src/env.rs index 55a61cb63f155..2a5fea6d3eb83 100644 --- a/crates/evm/core/src/env.rs +++ b/crates/evm/core/src/env.rs @@ -723,6 +723,7 @@ impl FromAnyRpcTransaction for TempoTxEnv { #[cfg(feature = "optimism")] mod optimism { use super::*; + use alloy_eips::eip2718::Encodable2718; use alloy_op_evm::OpTx; use op_alloy_consensus::{DEPOSIT_TX_TYPE_ID, TxDeposit}; use op_revm::{OpTransaction, transaction::OpTxTr}; @@ -914,7 +915,9 @@ mod optimism { if let Some(envelope) = tx.as_envelope() { return Ok(Self(OpTransaction:: { base: TxEnv::from_recovered_tx(envelope, tx.from()), - enveloped_tx: None, + // The L1 data fee is charged off these bytes, and op-revm rejects a + // non-deposit transaction that arrives without them. + enveloped_tx: Some(envelope.encoded_2718().into()), deposit: Default::default(), })); } @@ -1209,6 +1212,7 @@ mod tests { mod optimism { use super::*; use alloy_consensus::Sealed; + use alloy_eips::eip2718::Encodable2718; use alloy_op_evm::{OpEvmFactory, OpTx}; use op_alloy_consensus::{OpTxEnvelope, TxDeposit, transaction::OpTransactionInfo}; use op_alloy_rpc_types::Transaction as OpRpcTransaction; @@ -1253,6 +1257,12 @@ mod tests { let op_tx_env = OpTx::from_any_rpc_transaction(&any_tx).unwrap(); assert_eq!(op_tx_env.base, expected_base); + // op-revm charges the L1 data fee off these bytes and rejects a non-deposit + // transaction that arrives without them. + assert_eq!( + op_tx_env.enveloped_tx, + Some(any_tx.as_envelope().unwrap().encoded_2718().into()) + ); } #[test] From d7477341d76ca1dd8da83e403fc1d80842d3caf2 Mon Sep 17 00:00:00 2001 From: steven Date: Fri, 28 Aug 2026 21:27:47 -0600 Subject: [PATCH 2/2] clippy --- Cargo.lock | 1 - crates/cast/Cargo.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c9bc6061fdb6..44e6b2127c0eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2960,7 +2960,6 @@ dependencies = [ "alloy-dyn-abi", "alloy-eips", "alloy-ens", - "alloy-evm", "alloy-hardforks", "alloy-json-abi", "alloy-json-rpc", diff --git a/crates/cast/Cargo.toml b/crates/cast/Cargo.toml index 5ee1086f05177..bd3244b67db2e 100644 --- a/crates/cast/Cargo.toml +++ b/crates/cast/Cargo.toml @@ -62,7 +62,6 @@ alloy-eips.workspace = true tempo-alloy.workspace = true tempo-contracts.workspace = true tempo-primitives.workspace = true -alloy-evm.workspace = true op-alloy-consensus = { workspace = true, features = ["k256"], optional = true } op-alloy-flz = { workspace = true, optional = true }