Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changelog/cast-raw-tx-unknown-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
cast: patch
---

`cast tx --raw` and `cast tx --lane` no longer panic on a transaction type Foundry does not model, such as the `ArbitrumInternalTx` (`0x6a`) that opens every Arbitrum and Orbit rollup block. They now report "Cannot EIP-2718 encode transaction type 0x6a" instead.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/cast/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ tempo-primitives.workspace = true
op-alloy-consensus = { workspace = true, features = ["k256"], optional = true }
op-alloy-flz = { workspace = true, optional = true }
op-alloy-network = { workspace = true, optional = true }
op-alloy-rpc-types = { workspace = true, optional = true }

chrono.workspace = true
eyre.workspace = true
Expand Down Expand Up @@ -143,6 +144,7 @@ optimism = [
"dep:op-alloy-flz",
"dep:op-alloy-consensus",
"dep:op-alloy-network",
"dep:op-alloy-rpc-types",
"foundry-common/optimism",
"foundry-evm-networks/optimism",
"foundry-evm/optimism",
Expand Down
58 changes: 50 additions & 8 deletions crates/cast/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,19 @@ extern crate foundry_common;
extern crate tracing;

use alloy_consensus::{
BlockHeader,
BlockHeader, Typed2718,
transaction::{Recovered, SignerRecoverable},
};
use alloy_dyn_abi::{DynSolType, DynSolValue, FunctionExt, Specifier};
use alloy_eips::Encodable2718;
use alloy_ens::NameOrAddress;
use alloy_json_abi::Function;
use alloy_json_rpc::RpcError;
use alloy_network::{AnyNetwork, BlockResponse, Network, TransactionBuilder};
use alloy_network::{
AnyNetwork, AnyRpcTransaction, AnyTxEnvelope, BlockResponse, Network, TransactionBuilder,
};
use alloy_primitives::{
Address, B256, I256, Keccak256, LogData, Selector, TxHash, U64, U256, hex,
Address, B256, Bytes, I256, Keccak256, LogData, Selector, TxHash, U64, U256, hex,
utils::{ParseUnits, Unit, keccak256},
};
use alloy_provider::{PendingTransactionBuilder, Provider, network::eip2718::Decodable2718};
Expand Down Expand Up @@ -1227,7 +1229,10 @@ where
raw: bool,
to_request: bool,
lane: bool,
) -> Result<String> {
) -> Result<String>
where
N::TransactionResponse: EncodeRpc2718,
{
let tx = if let Some(tx_hash) = tx_hash {
let tx_hash = TxHash::from_str(&tx_hash).wrap_err("invalid tx hash")?;
self.provider
Expand All @@ -1253,11 +1258,10 @@ where
};

Ok(if raw {
let encoded = tx.as_ref().encoded_2718();
format!("0x{}", hex::encode(encoded))
format!("0x{}", hex::encode(tx.encode_rpc_2718()?))
} else if lane {
let encoded = tx.as_ref().encoded_2718();
FoundryTxEnvelope::decode_2718(&mut encoded.as_slice())
let encoded = tx.encode_rpc_2718()?;
FoundryTxEnvelope::decode_2718(&mut encoded.as_ref())
.wrap_err("failed to decode transaction for lane classification")?;
crate::args::format_lane_classification(&classify_payment_lane(&encoded))?
} else if let Some(ref field) = field {
Expand Down Expand Up @@ -1285,6 +1289,44 @@ where
}
}

/// EIP-2718 encodes a transaction held in its JSON-RPC form.
///
/// [`AnyTxEnvelope`] panics rather than encode a transaction type alloy does not model, which is
/// every type minted by a chain Foundry can talk to but not execute, such as Arbitrum and its
/// Orbit rollups. The [`AnyNetwork`] response is therefore routed through [`FoundryTxEnvelope`],
/// which knows the types Foundry supports and reports the rest as an error. Networks whose own
/// envelope covers everything they serve encode straight from it.
pub trait EncodeRpc2718 {
/// Returns the EIP-2718 encoding of this transaction.
fn encode_rpc_2718(&self) -> Result<Bytes>;
}

impl EncodeRpc2718 for AnyRpcTransaction {
fn encode_rpc_2718(&self) -> Result<Bytes> {
if let AnyTxEnvelope::Ethereum(envelope) = self.as_ref() {
return Ok(envelope.encoded_2718().into());
}

let envelope = FoundryTxEnvelope::try_from(self.clone()).wrap_err_with(|| {
format!("Cannot EIP-2718 encode transaction type 0x{:x}", self.ty())
})?;
Ok(envelope.encoded_2718().into())
}
}

impl<T: Encodable2718> EncodeRpc2718 for alloy_rpc_types::Transaction<T> {
fn encode_rpc_2718(&self) -> Result<Bytes> {
Ok(self.as_ref().encoded_2718().into())
}
}

#[cfg(feature = "optimism")]
impl EncodeRpc2718 for op_alloy_rpc_types::Transaction {
fn encode_rpc_2718(&self) -> Result<Bytes> {
Ok(self.as_ref().encoded_2718().into())
}
}

pub struct SimpleCast;

impl SimpleCast {
Expand Down
64 changes: 64 additions & 0 deletions crates/cast/tests/cli/read_networks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
//! profile runs it with retries. These endpoints are free, unauthenticated and rate limited, so
//! an outage must not fail a normal CI run.

use alloy_primitives::{hex, keccak256};
use foundry_test_utils::TestCommand;

/// How far behind the tip to start looking.
Expand All @@ -23,6 +24,15 @@ const TIP_CONFIRMATIONS: u64 = 3;
/// them, so a single block is not enough to test against.
const BLOCK_SCAN_DEPTH: u64 = 24;

/// The highest EIP-2718 type byte every chain here is expected to encode.
///
/// `cast tx --raw` re-encodes the transaction rather than reformatting the response, so unlike
/// the other read commands it needs a consensus encoding for the type. Foundry's envelope covers
/// the standard Ethereum types everywhere, and a few chain-specific ones where the RPC form
/// carries enough to rebuild them. Above this sit the types a chain can serve that Foundry does
/// not model at all, such as Arbitrum's `ArbitrumInternalTx` (`0x6a`).
const MAX_STANDARD_TX_TYPE: u64 = 0x04;

/// A public endpoint to exercise the read commands against.
struct Network {
name: &'static str,
Expand Down Expand Up @@ -93,6 +103,8 @@ fn assert_read_commands_work(cmd: &mut TestCommand, network: &Network) {
network.name
);

assert_raw_encoding(cmd, network, &tx, &tx_hash);

// The receipt decodes through a separate path from the transaction, so agreeing on the block
// is a real cross-check rather than a restatement.
let receipt = json_output(cmd, &["receipt", &tx_hash, "--rpc-url", network.rpc_url])
Expand Down Expand Up @@ -149,6 +161,58 @@ fn assert_read_commands_work(cmd: &mut TestCommand, network: &Network) {
}
}

/// Asserts `cast tx --raw` either encodes the transaction or reports why it cannot.
///
/// Alloy panics rather than invent an encoding for a type it does not model, so the type that
/// cannot be encoded has to be reported rather than reached.
fn assert_raw_encoding(
cmd: &mut TestCommand,
network: &Network,
tx: &serde_json::Value,
tx_hash: &str,
) {
let ty = hex_field(tx, "type")
.unwrap_or_else(|| panic!("{}: {tx_hash} reports no transaction type", network.name));
let output =
cmd.cast_fuse().args(["tx", tx_hash, "--raw", "--rpc-url", network.rpc_url]).execute();
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);

// Asserted on both outcomes, because the failure being guarded against is a crash rather than
// an exit code.
assert!(
!stderr.contains("panicked"),
"{}: `cast tx --raw` panicked on {tx_hash} (type 0x{ty:x}): {stderr}",
network.name
);

if !output.status.success() {
assert!(
ty > MAX_STANDARD_TX_TYPE,
"{}: `cast tx --raw` failed for {tx_hash} (type 0x{ty:x}): {stderr}",
network.name
);
assert!(
stderr.contains(&format!("Cannot EIP-2718 encode transaction type 0x{ty:x}")),
"{}: `cast tx --raw` on type 0x{ty:x} failed without naming the type: {stderr}",
network.name
);
return;
}

// Hashing the encoding checks the bytes rather than just that something hex-shaped was
// printed, since a transaction hash is the keccak of its EIP-2718 encoding.
let raw = hex::decode(stdout.trim()).unwrap_or_else(|err| {
panic!("{}: `cast tx --raw` printed no hex for {tx_hash}: {err}", network.name)
});
assert_eq!(
keccak256(&raw).to_string(),
tx_hash,
"{}: re-encoding {tx_hash} did not reproduce it",
network.name
);
}

/// Fetches a block, optionally with full transaction bodies.
fn block(
cmd: &mut TestCommand,
Expand Down
Loading