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/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 } 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]