From 451b48001732c6e90ee7ac6a4fe28b32398b5b3b Mon Sep 17 00:00:00 2001 From: Feliphe Date: Fri, 28 Aug 2026 14:17:29 -0300 Subject: [PATCH 1/9] feat: decode rlp directly from tx_input --- .../types/transaction/transaction_input.rs | 170 +++++++++++++++--- 1 file changed, 144 insertions(+), 26 deletions(-) diff --git a/src/eth/types/transaction/transaction_input.rs b/src/eth/types/transaction/transaction_input.rs index 7aede9f8d..221995f5e 100644 --- a/src/eth/types/transaction/transaction_input.rs +++ b/src/eth/types/transaction/transaction_input.rs @@ -1,3 +1,4 @@ +use alloy_consensus::SignableTransaction; use alloy_consensus::Signed; use alloy_consensus::Transaction; use alloy_consensus::TxEip1559; @@ -9,6 +10,7 @@ use alloy_consensus::TxEnvelope; use alloy_consensus::TxLegacy; use alloy_consensus::transaction::Recovered; use alloy_eips::eip2718::Decodable2718; +use alloy_primitives::B256; use alloy_primitives::Signature as AlloySignature; use alloy_primitives::TxKind; use alloy_primitives::U64; @@ -143,10 +145,86 @@ impl TransactionInput { } } + /// Computes the transaction signature hash from the fields stored in this input. + /// + /// This avoids reconstructing a full `TxEnvelope` just to obtain the signing hash. + fn signature_hash(&self) -> B256 { + match self.transaction_info.tx_type.map(|t| t.as_u64()).unwrap_or(0) { + // EIP-2930 + 1 => TxEip2930 { + chain_id: self.execution_info.chain_id.unwrap_or_default().into(), + nonce: self.execution_info.nonce.into(), + gas_price: self.execution_info.gas_price, + gas_limit: self.execution_info.gas_limit.into(), + to: TxKind::from(self.execution_info.to.map(Into::into)), + value: self.execution_info.value.into(), + input: self.execution_info.input.clone().into(), + access_list: AccessList::default(), + } + .signature_hash(), + + // EIP-1559 + 2 => TxEip1559 { + chain_id: self.execution_info.chain_id.unwrap_or_default().into(), + nonce: self.execution_info.nonce.into(), + max_fee_per_gas: self.execution_info.gas_price, + max_priority_fee_per_gas: self.execution_info.gas_price, + gas_limit: self.execution_info.gas_limit.into(), + to: TxKind::from(self.execution_info.to.map(Into::into)), + value: self.execution_info.value.into(), + input: self.execution_info.input.clone().into(), + access_list: AccessList::default(), + } + .signature_hash(), + + // EIP-4844 + 3 => TxEip4844 { + chain_id: self.execution_info.chain_id.unwrap_or_default().into(), + nonce: self.execution_info.nonce.into(), + max_fee_per_gas: self.execution_info.gas_price, + max_priority_fee_per_gas: self.execution_info.gas_price, + gas_limit: self.execution_info.gas_limit.into(), + to: self.execution_info.to.map(Into::into).unwrap_or_default(), + value: self.execution_info.value.into(), + input: self.execution_info.input.clone().into(), + access_list: AccessList::default(), + blob_versioned_hashes: Vec::default(), + max_fee_per_blob_gas: 0, + } + .signature_hash(), + + // EIP-7702 + 4 => TxEip7702 { + chain_id: self.execution_info.chain_id.unwrap_or_default().into(), + nonce: self.execution_info.nonce.into(), + gas_limit: self.execution_info.gas_limit.into(), + max_fee_per_gas: self.execution_info.gas_price, + max_priority_fee_per_gas: self.execution_info.gas_price, + to: self.execution_info.to.map(Into::into).unwrap_or_default(), + value: self.execution_info.value.into(), + input: self.execution_info.input.clone().into(), + access_list: AccessList::default(), + authorization_list: Vec::default(), + } + .signature_hash(), + + // Legacy (default) + _ => TxLegacy { + chain_id: self.execution_info.chain_id.map(Into::into), + nonce: self.execution_info.nonce.into(), + gas_price: self.execution_info.gas_price, + gas_limit: self.execution_info.gas_limit.into(), + to: TxKind::from(self.execution_info.to.map(Into::into)), + value: self.execution_info.value.into(), + input: self.execution_info.input.clone().into(), + } + .signature_hash(), + } + } + /// Recovers the signer address from the transaction fields already stored in this input. fn recover_signer_address(&self) -> anyhow::Result
{ - let inner = self.to_tx_envelope(); - let prehash = inner.signature_hash(); + let prehash = self.signature_hash(); let signature: AlloySignature = self.signature.into(); let signer = signature .recover_address_from_prehash(&prehash) @@ -255,17 +333,7 @@ impl TransactionInput { impl Decodable for TransactionInput { fn decode(rlp: &rlp::Rlp) -> Result { fn convert_tx(envelope: TxEnvelope) -> Result { - let tx_input = try_from_alloy_transaction(alloy_rpc_types_eth::Transaction { - inner: Recovered::new_unchecked(envelope, alloy_primitives::Address::ZERO), - block_hash: None, - block_number: None, - block_timestamp: None, - transaction_index: None, - effective_gas_price: None, - }) - .map_err(|_| rlp::DecoderError::Custom("failed to convert transaction"))?; - - Ok(tx_input) + build_transaction_input_from_envelope(&envelope).map_err(|_| rlp::DecoderError::Custom("failed to convert transaction")) } let raw_bytes = rlp.as_raw(); @@ -310,9 +378,9 @@ impl TryFrom for TransactionInput { } } -fn try_from_alloy_transaction(value: alloy_rpc_types_eth::Transaction) -> anyhow::Result { +fn build_transaction_input_from_envelope(envelope: &TxEnvelope) -> anyhow::Result { // Get signature components from the envelope - let signature = value.inner.signature(); + let signature = envelope.signature(); let signature = Signature { r: signature.r(), s: signature.s(), @@ -321,36 +389,40 @@ fn try_from_alloy_transaction(value: alloy_rpc_types_eth::Transaction) -> anyhow // Build the TransactionInput from the fields we currently support, leaving the // signer unrecovered. We intentionally ignore any signer that may - // already be present in the source AlloyTransaction so that the leader and the follower always derive the same address + // already be present in the source transaction so that the leader and the follower always derive the same address // from the same set of saved fields. let mut tx_input = TransactionInput { transaction_info: TransactionInfo { - tx_type: Some(U64::from(value.inner.tx_type() as u8)), - hash: Hash::from(*value.inner.tx_hash()), + tx_type: Some(U64::from(envelope.tx_type() as u8)), + hash: Hash::from(*envelope.tx_hash()), }, execution_info: ExecutionInfo { - chain_id: value.inner.chain_id().map(Into::into), - nonce: Nonce::from(value.inner.nonce()), + chain_id: envelope.chain_id().map(Into::into), + nonce: Nonce::from(envelope.nonce()), signer: Signer::Unrecovered, - to: match value.inner.kind() { + to: match envelope.kind() { TxKind::Call(addr) => Some(Address::from(addr)), TxKind::Create => None, }, - value: Wei::from(value.inner.value()), - input: Bytes::from(value.inner.input().clone()), - gas_limit: Gas::from(value.inner.gas_limit()), - gas_price: value.inner.max_fee_per_gas(), + value: Wei::from(envelope.value()), + input: Bytes::from(envelope.input().clone()), + gas_limit: Gas::from(envelope.gas_limit()), + gas_price: envelope.max_fee_per_gas(), }, signature, }; - // Recover the signer from the envelope reconstructed using the saved fields. + // Recover the signer directly from the saved fields. let recovered_signer = tx_input.recover_signer_address()?; tx_input.execution_info.signer = Signer::Recovered(recovered_signer); Ok(tx_input) } +fn try_from_alloy_transaction(value: alloy_rpc_types_eth::Transaction) -> anyhow::Result { + build_transaction_input_from_envelope(value.inner.inner()) +} + impl From for ExecutionInfo { fn from(value: TransactionExecutionInput) -> Self { Self { @@ -385,3 +457,49 @@ impl From for AlloyTransaction { } } } + +// ----------------------------------------------------------------------------- +// Tests +// ----------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use alloy_primitives::U64; + use alloy_primitives::U256; + + use super::*; + + fn dummy_transaction_input(tx_type: u64) -> TransactionInput { + TransactionInput { + transaction_info: TransactionInfo { + tx_type: Some(U64::from(tx_type)), + hash: Hash::default(), + }, + execution_info: ExecutionInfo { + chain_id: Some(ChainId::from(1u64)), + nonce: Nonce::from(1u64), + signer: Signer::Unrecovered, + to: Some(Address::default()), + value: Wei::from(100u64), + input: Bytes::default(), + gas_limit: Gas::from(21000u64), + gas_price: 1_000_000_000, + }, + signature: Signature { + v: U64::ZERO, + r: U256::from(1u64), + s: U256::from(2u64), + }, + } + } + + #[test] + fn signature_hash_matches_to_tx_envelope() { + for tx_type in [0, 1, 2, 3, 4] { + let tx_input = dummy_transaction_input(tx_type); + let from_signature_hash = tx_input.signature_hash(); + let from_envelope = tx_input.to_tx_envelope().signature_hash(); + assert_eq!(from_signature_hash, from_envelope, "signature_hash mismatch for tx_type {tx_type}"); + } + } +} From fbf1163b40300734522402d142d0ebd3bada348c Mon Sep 17 00:00:00 2001 From: Feliphe Date: Wed, 2 Sep 2026 11:52:27 -0300 Subject: [PATCH 2/9] decode rlp into tx_input without using alloy types --- Cargo.lock | 14 +- Cargo.toml | 2 +- src/eth/rpc/parser.rs | 5 +- .../types/transaction/transaction_input.rs | 590 +++++++++++++++--- 4 files changed, 517 insertions(+), 94 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fe52b482d..fa526851f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5265,16 +5265,6 @@ dependencies = [ "rustc-hex", ] -[[package]] -name = "rlp" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa24e92bb2a83198bb76d661a71df9f7076b8c420b8696e4d3d97d50d94479e3" -dependencies = [ - "bytes", - "rustc-hex", -] - [[package]] name = "rocksdb" version = "0.24.0" @@ -5312,7 +5302,7 @@ dependencies = [ "proptest", "rand 0.8.5", "rand 0.9.4", - "rlp 0.5.2", + "rlp", "ruint-macro", "serde_core", "valuable", @@ -6189,6 +6179,7 @@ dependencies = [ "alloy-eips", "alloy-json-abi", "alloy-primitives", + "alloy-rlp", "alloy-rpc-types-eth", "alloy-rpc-types-trace", "alloy-sol-types", @@ -6249,7 +6240,6 @@ dependencies = [ "revm-inspectors", "revm-state", "ring", - "rlp 0.6.1", "rocksdb", "rustc-hash", "sasl2-sys", diff --git a/Cargo.toml b/Cargo.toml index e19deca79..4342f2c2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,7 +83,7 @@ alloy-dyn-abi = "=1.6.1" alloy-json-abi = "=1.6.1" revm-inspectors = { version = "=0.42.0", features = ["js-tracer"] } revm-state = "42.0.0" -rlp = "=0.6.1" +alloy-rlp = "=0.3.16" # network jsonrpsee = { version = "=0.26.0", features = ["server", "client"] } diff --git a/src/eth/rpc/parser.rs b/src/eth/rpc/parser.rs index b5f6ec07f..d0cef5e23 100644 --- a/src/eth/rpc/parser.rs +++ b/src/eth/rpc/parser.rs @@ -1,8 +1,8 @@ //! Helper functions for parsing RPC requests and responses. +use alloy_rlp::Decodable; use jsonrpsee::Extensions; use jsonrpsee::types::ParamsSequence; -use rlp::Decodable; use tracing::Span; use super::middleware::Authentication; @@ -77,7 +77,8 @@ where /// /// https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp pub fn parse_rpc_rlp(value: &[u8]) -> Result { - match rlp::decode::(value) { + let mut buf = value; + match T::decode(&mut buf) { Ok(trx) => Ok(trx), Err(e) => Err(RpcError::TransactionInvalid { decode_error: e.to_string() }), } diff --git a/src/eth/types/transaction/transaction_input.rs b/src/eth/types/transaction/transaction_input.rs index 221995f5e..9d65d826b 100644 --- a/src/eth/types/transaction/transaction_input.rs +++ b/src/eth/types/transaction/transaction_input.rs @@ -1,4 +1,3 @@ -use alloy_consensus::SignableTransaction; use alloy_consensus::Signed; use alloy_consensus::Transaction; use alloy_consensus::TxEip1559; @@ -9,16 +8,21 @@ use alloy_consensus::TxEip7702; use alloy_consensus::TxEnvelope; use alloy_consensus::TxLegacy; use alloy_consensus::transaction::Recovered; -use alloy_eips::eip2718::Decodable2718; +use alloy_primitives::keccak256; +use alloy_primitives::Address as AlloyAddress; use alloy_primitives::B256; +use alloy_primitives::Bytes as AlloyBytes; use alloy_primitives::Signature as AlloySignature; use alloy_primitives::TxKind; use alloy_primitives::U64; use alloy_primitives::U256; use alloy_rpc_types_eth::AccessList; +use alloy_rlp::Decodable as RlpDecodable; +use alloy_rlp::Encodable as RlpEncodable; +use alloy_rlp::Header as RlpHeader; +use alloy_rlp::length_of_length; use anyhow::Context; use display_json::DebugAsJson; -use rlp::Decodable; use crate::alias::AlloyTransaction; use crate::eth::executor::TransactionExecutionInput; @@ -145,80 +149,146 @@ impl TransactionInput { } } + /// Encodes a list of RLP items and returns the encoded bytes. + fn encode_rlp_list(items: &[&dyn RlpEncodable]) -> Vec { + let payload_length: usize = items.iter().map(|item| item.length()).sum(); + let mut out = Vec::with_capacity(payload_length + length_of_length(payload_length)); + RlpHeader { list: true, payload_length }.encode(&mut out); + for item in items { + item.encode(&mut out); + } + out + } + + /// Returns the RLP encoding of the `to` field: empty bytes for contract creation, + /// or the 20-byte address for a call. + fn encode_to(&self) -> Vec { + self.execution_info.to.map(|addr| addr.0.to_vec()).unwrap_or_default() + } + + /// Returns the RLP encoding of the transaction `input` data. + fn encode_input(&self) -> Vec { + self.execution_info.input.0.to_vec() + } + /// Computes the transaction signature hash from the fields stored in this input. /// - /// This avoids reconstructing a full `TxEnvelope` just to obtain the signing hash. + /// Encodes the unsigned transaction directly via RLP. fn signature_hash(&self) -> B256 { + let chain_id = self.execution_info.chain_id.map(|c| c.0.as_u64()).unwrap_or_default(); + let nonce = self.execution_info.nonce.as_u64(); + let gas_limit = self.execution_info.gas_limit.as_u64(); + let gas_price = self.execution_info.gas_price; + let value = self.execution_info.value.0; + let to = self.encode_to(); + let input = self.encode_input(); + match self.transaction_info.tx_type.map(|t| t.as_u64()).unwrap_or(0) { // EIP-2930 - 1 => TxEip2930 { - chain_id: self.execution_info.chain_id.unwrap_or_default().into(), - nonce: self.execution_info.nonce.into(), - gas_price: self.execution_info.gas_price, - gas_limit: self.execution_info.gas_limit.into(), - to: TxKind::from(self.execution_info.to.map(Into::into)), - value: self.execution_info.value.into(), - input: self.execution_info.input.clone().into(), - access_list: AccessList::default(), + 1 => { + let encoded = Self::encode_rlp_list(&[ + &chain_id, + &nonce, + &gas_price, + &gas_limit, + &to.as_slice(), + &value, + &input.as_slice(), + &AccessList::default(), + ]); + let mut out = Vec::with_capacity(1 + encoded.len()); + out.push(0x01); + out.extend_from_slice(&encoded); + B256::from(keccak256(out)) } - .signature_hash(), // EIP-1559 - 2 => TxEip1559 { - chain_id: self.execution_info.chain_id.unwrap_or_default().into(), - nonce: self.execution_info.nonce.into(), - max_fee_per_gas: self.execution_info.gas_price, - max_priority_fee_per_gas: self.execution_info.gas_price, - gas_limit: self.execution_info.gas_limit.into(), - to: TxKind::from(self.execution_info.to.map(Into::into)), - value: self.execution_info.value.into(), - input: self.execution_info.input.clone().into(), - access_list: AccessList::default(), + 2 => { + let encoded = Self::encode_rlp_list(&[ + &chain_id, + &nonce, + &gas_price, // max_priority_fee_per_gas + &gas_price, // max_fee_per_gas + &gas_limit, + &to.as_slice(), + &value, + &input.as_slice(), + &AccessList::default(), + ]); + let mut out = Vec::with_capacity(1 + encoded.len()); + out.push(0x02); + out.extend_from_slice(&encoded); + B256::from(keccak256(out)) } - .signature_hash(), // EIP-4844 - 3 => TxEip4844 { - chain_id: self.execution_info.chain_id.unwrap_or_default().into(), - nonce: self.execution_info.nonce.into(), - max_fee_per_gas: self.execution_info.gas_price, - max_priority_fee_per_gas: self.execution_info.gas_price, - gas_limit: self.execution_info.gas_limit.into(), - to: self.execution_info.to.map(Into::into).unwrap_or_default(), - value: self.execution_info.value.into(), - input: self.execution_info.input.clone().into(), - access_list: AccessList::default(), - blob_versioned_hashes: Vec::default(), - max_fee_per_blob_gas: 0, + 3 => { + let encoded = Self::encode_rlp_list(&[ + &chain_id, + &nonce, + &gas_price, // max_priority_fee_per_gas + &gas_price, // max_fee_per_gas + &gas_limit, + &to.as_slice(), + &value, + &input.as_slice(), + &AccessList::default(), + &0u128, // max_fee_per_blob_gas + &Vec::::new(), // blob_versioned_hashes + ]); + let mut out = Vec::with_capacity(1 + encoded.len()); + out.push(0x03); + out.extend_from_slice(&encoded); + B256::from(keccak256(out)) } - .signature_hash(), // EIP-7702 - 4 => TxEip7702 { - chain_id: self.execution_info.chain_id.unwrap_or_default().into(), - nonce: self.execution_info.nonce.into(), - gas_limit: self.execution_info.gas_limit.into(), - max_fee_per_gas: self.execution_info.gas_price, - max_priority_fee_per_gas: self.execution_info.gas_price, - to: self.execution_info.to.map(Into::into).unwrap_or_default(), - value: self.execution_info.value.into(), - input: self.execution_info.input.clone().into(), - access_list: AccessList::default(), - authorization_list: Vec::default(), + 4 => { + let encoded = Self::encode_rlp_list(&[ + &chain_id, + &nonce, + &gas_price, // max_priority_fee_per_gas + &gas_price, // max_fee_per_gas + &gas_limit, + &to.as_slice(), + &value, + &input.as_slice(), + &AccessList::default(), + &Vec::::new(), // authorization list placeholder + ]); + let mut out = Vec::with_capacity(1 + encoded.len()); + out.push(0x04); + out.extend_from_slice(&encoded); + B256::from(keccak256(out)) } - .signature_hash(), // Legacy (default) - _ => TxLegacy { - chain_id: self.execution_info.chain_id.map(Into::into), - nonce: self.execution_info.nonce.into(), - gas_price: self.execution_info.gas_price, - gas_limit: self.execution_info.gas_limit.into(), - to: TxKind::from(self.execution_info.to.map(Into::into)), - value: self.execution_info.value.into(), - input: self.execution_info.input.clone().into(), + _ => { + if self.execution_info.chain_id.is_some() { + let encoded = Self::encode_rlp_list(&[ + &nonce, + &gas_price, + &gas_limit, + &to.as_slice(), + &value, + &input.as_slice(), + &chain_id, + &0u8, + &0u8, + ]); + B256::from(keccak256(encoded)) + } else { + let encoded = Self::encode_rlp_list(&[ + &nonce, + &gas_price, + &gas_limit, + &to.as_slice(), + &value, + &input.as_slice(), + ]); + B256::from(keccak256(encoded)) + } } - .signature_hash(), } } @@ -330,32 +400,247 @@ impl TransactionInput { // Serialization / Deserialization // ----------------------------------------------------------------------------- -impl Decodable for TransactionInput { - fn decode(rlp: &rlp::Rlp) -> Result { - fn convert_tx(envelope: TxEnvelope) -> Result { - build_transaction_input_from_envelope(&envelope).map_err(|_| rlp::DecoderError::Custom("failed to convert transaction")) +impl TransactionInput { + /// Decodes the `to` field from RLP bytes: empty means contract creation, + /// 20 bytes means a call to that address. + fn decode_to(bytes: &[u8]) -> alloy_rlp::Result> { + if bytes.is_empty() { + Ok(None) + } else { + let array = <[u8; 20]>::try_from(bytes).map_err(|_| alloy_rlp::Error::UnexpectedLength)?; + Ok(Some(Address::from(array))) } + } - let raw_bytes = rlp.as_raw(); + /// Derives the chain id and signature parity from a legacy `v` value. + fn decode_legacy_v(v: u64) -> alloy_rlp::Result<(Option, u64)> { + match v { + 27 => Ok((None, 0)), + 28 => Ok((None, 1)), + v if v >= 35 => { + let chain_id = (v - 35) / 2; + let parity = (v - 27) % 2; + Ok((Some(ChainId::from(chain_id)), parity)) + } + _ => Err(alloy_rlp::Error::Custom("invalid legacy v value")), + } + } - if raw_bytes.is_empty() { - return Err(rlp::DecoderError::Custom("empty transaction bytes")); + /// Builds a `TransactionInput` from decoded legacy fields and recovers the signer. + #[allow(clippy::too_many_arguments)] + fn build_legacy( + nonce: u64, + gas_price: u128, + gas_limit: u64, + to: Option
, + value: U256, + input: Vec, + v: u64, + r: U256, + s: U256, + hash: Hash, + ) -> anyhow::Result { + let (chain_id, parity) = Self::decode_legacy_v(v)?; + + let mut tx = Self { + transaction_info: TransactionInfo { + tx_type: None, + hash, + }, + execution_info: ExecutionInfo { + chain_id, + nonce: Nonce::from(nonce), + signer: Signer::Unrecovered, + to, + value: Wei::from(value), + input: Bytes::from(input), + gas_limit: Gas::from(gas_limit), + gas_price, + }, + signature: Signature { + v: U64::from(parity), + r, + s, + }, + }; + + let signer = tx.recover_signer_address()?; + tx.execution_info.signer = Signer::Recovered(signer); + + Ok(tx) + } + + /// Decodes a legacy transaction from raw RLP bytes. + fn decode_legacy(raw_bytes: &[u8]) -> alloy_rlp::Result { + let mut rlp = alloy_rlp::Rlp::new(raw_bytes)?; + + let nonce: u64 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing nonce"))?; + let gas_price: u128 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing gasPrice"))?; + let gas_limit: u64 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing gasLimit"))?; + let to_bytes: AlloyBytes = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing to"))?; + let value: U256 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing value"))?; + let input: AlloyBytes = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing input"))?; + let v: u64 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing v"))?; + let r: U256 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing r"))?; + let s: U256 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing s"))?; + + if rlp.get_next::()?.is_some() { + return Err(alloy_rlp::Error::Custom("legacy transaction has extra fields")); } - if rlp.is_list() { - // Legacy transaction - let mut bytes = raw_bytes; - TxEnvelope::fallback_decode(&mut bytes) - .map_err(|_| rlp::DecoderError::Custom("failed to decode legacy transaction")) - .and_then(convert_tx) - } else { - // Typed transaction (EIP-2718) - let first_byte = raw_bytes[0]; - let mut remaining_bytes = &raw_bytes[1..]; - TxEnvelope::typed_decode(first_byte, &mut remaining_bytes) - .map_err(|_| rlp::DecoderError::Custom("failed to decode transaction envelope")) - .and_then(convert_tx) + let to = Self::decode_to(&to_bytes)?; + let hash = Hash::from(keccak256(raw_bytes)); + + Self::build_legacy(nonce, gas_price, gas_limit, to, value, input.to_vec(), v, r, s, hash).map_err(|_| alloy_rlp::Error::Custom("failed to recover legacy signer")) + } + + /// Decodes a typed transaction (EIP-2718) from raw bytes. + fn decode_typed(tx_type: u8, payload: &[u8], raw_bytes: &[u8]) -> alloy_rlp::Result { + let mut rlp = alloy_rlp::Rlp::new(payload)?; + + let chain_id: u64 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing chainId"))?; + let nonce: u64 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing nonce"))?; + let gas_price: u128; + let gas_limit: u64; + let to: Option
; + let value: U256; + let input: Vec; + let v: u64; + let r: U256; + let s: U256; + + match tx_type { + // EIP-2930 + 1 => { + gas_price = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing gasPrice"))?; + gas_limit = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing gasLimit"))?; + let to_bytes: AlloyBytes = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing to"))?; + to = Self::decode_to(&to_bytes)?; + value = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing value"))?; + let input_bytes: AlloyBytes = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing input"))?; + input = input_bytes.to_vec(); + let _: AccessList = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing accessList"))?; + v = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing v"))?; + r = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing r"))?; + s = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing s"))?; + } + + // EIP-1559 + 2 => { + let max_priority_fee_per_gas: u128 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing maxPriorityFeePerGas"))?; + let max_fee_per_gas: u128 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing maxFeePerGas"))?; + gas_price = max_fee_per_gas; + let _ = max_priority_fee_per_gas; + gas_limit = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing gasLimit"))?; + let to_bytes: AlloyBytes = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing to"))?; + to = Self::decode_to(&to_bytes)?; + value = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing value"))?; + let input_bytes: AlloyBytes = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing input"))?; + input = input_bytes.to_vec(); + let _: AccessList = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing accessList"))?; + v = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing v"))?; + r = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing r"))?; + s = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing s"))?; + } + + // EIP-4844 + 3 => { + let max_priority_fee_per_gas: u128 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing maxPriorityFeePerGas"))?; + let max_fee_per_gas: u128 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing maxFeePerGas"))?; + gas_price = max_fee_per_gas; + let _ = max_priority_fee_per_gas; + gas_limit = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing gasLimit"))?; + let to_addr: AlloyAddress = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing to"))?; + to = Some(Address::from(to_addr.0)); + value = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing value"))?; + let input_bytes: AlloyBytes = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing input"))?; + input = input_bytes.to_vec(); + let _: AccessList = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing accessList"))?; + let _: u128 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing maxFeePerBlobGas"))?; + let _: Vec = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing blobVersionedHashes"))?; + v = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing v"))?; + r = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing r"))?; + s = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing s"))?; + } + + // EIP-7702 + 4 => { + let max_priority_fee_per_gas: u128 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing maxPriorityFeePerGas"))?; + let max_fee_per_gas: u128 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing maxFeePerGas"))?; + gas_price = max_fee_per_gas; + let _ = max_priority_fee_per_gas; + gas_limit = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing gasLimit"))?; + let to_addr: AlloyAddress = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing to"))?; + to = Some(Address::from(to_addr.0)); + value = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing value"))?; + let input_bytes: AlloyBytes = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing input"))?; + input = input_bytes.to_vec(); + let _: AccessList = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing accessList"))?; + let _: Vec = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing authorizationList"))?; + v = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing v"))?; + r = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing r"))?; + s = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing s"))?; + } + + _ => return Err(alloy_rlp::Error::Custom("unsupported transaction type")), + } + + if rlp.get_next::()?.is_some() { + return Err(alloy_rlp::Error::Custom("typed transaction has extra fields")); } + + let hash = Hash::from(keccak256(raw_bytes)); + + let mut tx = Self { + transaction_info: TransactionInfo { + tx_type: Some(U64::from(tx_type)), + hash, + }, + execution_info: ExecutionInfo { + chain_id: Some(ChainId::from(chain_id)), + nonce: Nonce::from(nonce), + signer: Signer::Unrecovered, + to, + value: Wei::from(value), + input: Bytes::from(input), + gas_limit: Gas::from(gas_limit), + gas_price, + }, + signature: Signature { + v: U64::from(v), + r, + s, + }, + }; + + let signer = tx.recover_signer_address().map_err(|_| alloy_rlp::Error::Custom("failed to recover typed signer"))?; + tx.execution_info.signer = Signer::Recovered(signer); + + Ok(tx) + } +} + +impl RlpDecodable for TransactionInput { + fn decode(buf: &mut &[u8]) -> alloy_rlp::Result { + let raw_bytes = *buf; + + if raw_bytes.is_empty() { + return Err(alloy_rlp::Error::Custom("empty transaction bytes")); + } + + let tx = match raw_bytes[0] { + byte if byte >= 0xc0 => Self::decode_legacy(raw_bytes)?, + byte if byte <= 0x7f => { + let tx_type = byte; + let payload = &raw_bytes[1..]; + Self::decode_typed(tx_type, payload, raw_bytes)? + } + _ => return Err(alloy_rlp::Error::Custom("invalid transaction type byte")), + }; + + // A raw transaction occupies the entire buffer. + *buf = &[]; + Ok(tx) } } @@ -464,6 +749,19 @@ impl From for AlloyTransaction { #[cfg(test)] mod tests { + use alloy_consensus::SignableTransaction; + use alloy_consensus::Signed; + use alloy_consensus::TxEip1559; + use alloy_consensus::TxEip2930; + use alloy_consensus::TxEip4844; + use alloy_consensus::TxEip7702; + use alloy_consensus::TxLegacy; + use alloy_eips::eip2718::Encodable2718; + use alloy_primitives::keccak256; + use alloy_primitives::Address as AlloyAddress; + use alloy_primitives::Bytes as AlloyBytes; + use alloy_primitives::Signature as AlloySignature; + use alloy_primitives::TxKind; use alloy_primitives::U64; use alloy_primitives::U256; @@ -502,4 +800,138 @@ mod tests { assert_eq!(from_signature_hash, from_envelope, "signature_hash mismatch for tx_type {tx_type}"); } } + + /// Builds raw EIP-2718 bytes for a transaction and decodes them directly into a + /// `TransactionInput`, comparing the recovered fields and signer with the source transaction. + fn assert_direct_decode(tx: T, tx_type: u8) + where + T: alloy_consensus::SignableTransaction + + alloy_consensus::transaction::RlpEcdsaEncodableTx + + alloy_eips::Typed2718, + { + let signature = AlloySignature::test_signature(); + let signing_hash = tx.signature_hash(); + let expected_signer = signature.recover_address_from_prehash(&signing_hash).expect("test signature should be recoverable"); + + let signed = Signed::new_unchecked(tx, signature, Default::default()); + + let mut raw_bytes = Vec::new(); + signed.encode_2718(&mut raw_bytes); + let expected_hash = Hash::from(keccak256(&raw_bytes)); + + let decoded = TransactionInput::decode(&mut raw_bytes.as_slice()).expect("direct RLP decode should succeed"); + + assert_eq!(decoded.transaction_info.tx_type, Some(U64::from(tx_type))); + assert_eq!(decoded.transaction_info.hash, expected_hash); + assert_eq!(decoded.execution_info.chain_id, Some(ChainId::from(1u64))); + assert_eq!(decoded.execution_info.nonce, Nonce::from(1u64)); + assert_eq!(decoded.execution_info.gas_price, 1_000_000_000); + assert_eq!(decoded.execution_info.gas_limit, Gas::from(21000u64)); + assert_eq!(decoded.execution_info.to, Some(Address::default())); + assert_eq!(decoded.execution_info.value, Wei::from(100u64)); + assert_eq!(decoded.execution_info.input, Bytes::default()); + assert_eq!(decoded.signer(), Address::from(expected_signer)); + } + + #[test] + fn decode_legacy_from_raw_bytes() { + let tx = TxLegacy { + chain_id: Some(1), + nonce: 1, + gas_price: 1_000_000_000, + gas_limit: 21000, + to: TxKind::Call(AlloyAddress::default()), + value: U256::from(100), + input: AlloyBytes::new(), + }; + + let signature = AlloySignature::test_signature(); + let signing_hash = tx.signature_hash(); + let expected_signer = signature.recover_address_from_prehash(&signing_hash).expect("test signature should be recoverable"); + + let signed = Signed::new_unchecked(tx, signature, Default::default()); + + let mut raw_bytes = Vec::new(); + signed.encode_2718(&mut raw_bytes); + let expected_hash = Hash::from(keccak256(&raw_bytes)); + + let decoded = TransactionInput::decode(&mut raw_bytes.as_slice()).expect("direct RLP decode should succeed"); + + assert_eq!(decoded.transaction_info.tx_type, None); + assert_eq!(decoded.transaction_info.hash, expected_hash); + assert_eq!(decoded.execution_info.chain_id, Some(ChainId::from(1u64))); + assert_eq!(decoded.execution_info.nonce, Nonce::from(1u64)); + assert_eq!(decoded.execution_info.gas_price, 1_000_000_000); + assert_eq!(decoded.execution_info.gas_limit, Gas::from(21000u64)); + assert_eq!(decoded.execution_info.to, Some(Address::default())); + assert_eq!(decoded.execution_info.value, Wei::from(100u64)); + assert_eq!(decoded.execution_info.input, Bytes::default()); + assert_eq!(decoded.signer(), Address::from(expected_signer)); + } + + #[test] + fn decode_eip2930_from_raw_bytes() { + let tx = TxEip2930 { + chain_id: 1, + nonce: 1, + gas_price: 1_000_000_000, + gas_limit: 21000, + to: TxKind::Call(AlloyAddress::default()), + value: U256::from(100), + input: AlloyBytes::new(), + access_list: Default::default(), + }; + assert_direct_decode(tx, 1); + } + + #[test] + fn decode_eip1559_from_raw_bytes() { + let tx = TxEip1559 { + chain_id: 1, + nonce: 1, + max_priority_fee_per_gas: 1_000_000_000, + max_fee_per_gas: 1_000_000_000, + gas_limit: 21000, + to: TxKind::Call(AlloyAddress::default()), + value: U256::from(100), + input: AlloyBytes::new(), + access_list: Default::default(), + }; + assert_direct_decode(tx, 2); + } + + #[test] + fn decode_eip4844_from_raw_bytes() { + let tx = TxEip4844 { + chain_id: 1, + nonce: 1, + max_priority_fee_per_gas: 1_000_000_000, + max_fee_per_gas: 1_000_000_000, + gas_limit: 21000, + to: AlloyAddress::default(), + value: U256::from(100), + input: AlloyBytes::new(), + access_list: Default::default(), + blob_versioned_hashes: Vec::new(), + max_fee_per_blob_gas: 0, + }; + assert_direct_decode(tx, 3); + } + + #[test] + fn decode_eip7702_from_raw_bytes() { + let tx = TxEip7702 { + chain_id: 1, + nonce: 1, + max_priority_fee_per_gas: 1_000_000_000, + max_fee_per_gas: 1_000_000_000, + gas_limit: 21000, + to: AlloyAddress::default(), + value: U256::from(100), + input: AlloyBytes::new(), + access_list: Default::default(), + authorization_list: Vec::new(), + }; + assert_direct_decode(tx, 4); + } } From 41c71bdc94cbaf26e5ad55bfd28522ef9578238c Mon Sep 17 00:00:00 2001 From: Feliphe Date: Wed, 2 Sep 2026 11:55:29 -0300 Subject: [PATCH 3/9] fmt --- .../types/transaction/transaction_input.rs | 65 ++++++------------- 1 file changed, 21 insertions(+), 44 deletions(-) diff --git a/src/eth/types/transaction/transaction_input.rs b/src/eth/types/transaction/transaction_input.rs index 9d65d826b..db2d87dfe 100644 --- a/src/eth/types/transaction/transaction_input.rs +++ b/src/eth/types/transaction/transaction_input.rs @@ -8,7 +8,6 @@ use alloy_consensus::TxEip7702; use alloy_consensus::TxEnvelope; use alloy_consensus::TxLegacy; use alloy_consensus::transaction::Recovered; -use alloy_primitives::keccak256; use alloy_primitives::Address as AlloyAddress; use alloy_primitives::B256; use alloy_primitives::Bytes as AlloyBytes; @@ -16,11 +15,12 @@ use alloy_primitives::Signature as AlloySignature; use alloy_primitives::TxKind; use alloy_primitives::U64; use alloy_primitives::U256; -use alloy_rpc_types_eth::AccessList; +use alloy_primitives::keccak256; use alloy_rlp::Decodable as RlpDecodable; use alloy_rlp::Encodable as RlpEncodable; use alloy_rlp::Header as RlpHeader; use alloy_rlp::length_of_length; +use alloy_rpc_types_eth::AccessList; use anyhow::Context; use display_json::DebugAsJson; @@ -233,7 +233,7 @@ impl TransactionInput { &value, &input.as_slice(), &AccessList::default(), - &0u128, // max_fee_per_blob_gas + &0u128, // max_fee_per_blob_gas &Vec::::new(), // blob_versioned_hashes ]); let mut out = Vec::with_capacity(1 + encoded.len()); @@ -265,27 +265,10 @@ impl TransactionInput { // Legacy (default) _ => { if self.execution_info.chain_id.is_some() { - let encoded = Self::encode_rlp_list(&[ - &nonce, - &gas_price, - &gas_limit, - &to.as_slice(), - &value, - &input.as_slice(), - &chain_id, - &0u8, - &0u8, - ]); + let encoded = Self::encode_rlp_list(&[&nonce, &gas_price, &gas_limit, &to.as_slice(), &value, &input.as_slice(), &chain_id, &0u8, &0u8]); B256::from(keccak256(encoded)) } else { - let encoded = Self::encode_rlp_list(&[ - &nonce, - &gas_price, - &gas_limit, - &to.as_slice(), - &value, - &input.as_slice(), - ]); + let encoded = Self::encode_rlp_list(&[&nonce, &gas_price, &gas_limit, &to.as_slice(), &value, &input.as_slice()]); B256::from(keccak256(encoded)) } } @@ -443,10 +426,7 @@ impl TransactionInput { let (chain_id, parity) = Self::decode_legacy_v(v)?; let mut tx = Self { - transaction_info: TransactionInfo { - tx_type: None, - hash, - }, + transaction_info: TransactionInfo { tx_type: None, hash }, execution_info: ExecutionInfo { chain_id, nonce: Nonce::from(nonce), @@ -457,11 +437,7 @@ impl TransactionInput { gas_limit: Gas::from(gas_limit), gas_price, }, - signature: Signature { - v: U64::from(parity), - r, - s, - }, + signature: Signature { v: U64::from(parity), r, s }, }; let signer = tx.recover_signer_address()?; @@ -491,7 +467,8 @@ impl TransactionInput { let to = Self::decode_to(&to_bytes)?; let hash = Hash::from(keccak256(raw_bytes)); - Self::build_legacy(nonce, gas_price, gas_limit, to, value, input.to_vec(), v, r, s, hash).map_err(|_| alloy_rlp::Error::Custom("failed to recover legacy signer")) + Self::build_legacy(nonce, gas_price, gas_limit, to, value, input.to_vec(), v, r, s, hash) + .map_err(|_| alloy_rlp::Error::Custom("failed to recover legacy signer")) } /// Decodes a typed transaction (EIP-2718) from raw bytes. @@ -606,14 +583,12 @@ impl TransactionInput { gas_limit: Gas::from(gas_limit), gas_price, }, - signature: Signature { - v: U64::from(v), - r, - s, - }, + signature: Signature { v: U64::from(v), r, s }, }; - let signer = tx.recover_signer_address().map_err(|_| alloy_rlp::Error::Custom("failed to recover typed signer"))?; + let signer = tx + .recover_signer_address() + .map_err(|_| alloy_rlp::Error::Custom("failed to recover typed signer"))?; tx.execution_info.signer = Signer::Recovered(signer); Ok(tx) @@ -757,13 +732,13 @@ mod tests { use alloy_consensus::TxEip7702; use alloy_consensus::TxLegacy; use alloy_eips::eip2718::Encodable2718; - use alloy_primitives::keccak256; use alloy_primitives::Address as AlloyAddress; use alloy_primitives::Bytes as AlloyBytes; use alloy_primitives::Signature as AlloySignature; use alloy_primitives::TxKind; use alloy_primitives::U64; use alloy_primitives::U256; + use alloy_primitives::keccak256; use super::*; @@ -805,13 +780,13 @@ mod tests { /// `TransactionInput`, comparing the recovered fields and signer with the source transaction. fn assert_direct_decode(tx: T, tx_type: u8) where - T: alloy_consensus::SignableTransaction - + alloy_consensus::transaction::RlpEcdsaEncodableTx - + alloy_eips::Typed2718, + T: alloy_consensus::SignableTransaction + alloy_consensus::transaction::RlpEcdsaEncodableTx + alloy_eips::Typed2718, { let signature = AlloySignature::test_signature(); let signing_hash = tx.signature_hash(); - let expected_signer = signature.recover_address_from_prehash(&signing_hash).expect("test signature should be recoverable"); + let expected_signer = signature + .recover_address_from_prehash(&signing_hash) + .expect("test signature should be recoverable"); let signed = Signed::new_unchecked(tx, signature, Default::default()); @@ -847,7 +822,9 @@ mod tests { let signature = AlloySignature::test_signature(); let signing_hash = tx.signature_hash(); - let expected_signer = signature.recover_address_from_prehash(&signing_hash).expect("test signature should be recoverable"); + let expected_signer = signature + .recover_address_from_prehash(&signing_hash) + .expect("test signature should be recoverable"); let signed = Signed::new_unchecked(tx, signature, Default::default()); From 443f0b8048e4c812b892f372a6e8ad5a757eea78 Mon Sep 17 00:00:00 2001 From: Feliphe Date: Wed, 2 Sep 2026 12:12:54 -0300 Subject: [PATCH 4/9] fix auth list --- src/eth/types/transaction/transaction_input.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/eth/types/transaction/transaction_input.rs b/src/eth/types/transaction/transaction_input.rs index db2d87dfe..d8e18a778 100644 --- a/src/eth/types/transaction/transaction_input.rs +++ b/src/eth/types/transaction/transaction_input.rs @@ -8,6 +8,7 @@ use alloy_consensus::TxEip7702; use alloy_consensus::TxEnvelope; use alloy_consensus::TxLegacy; use alloy_consensus::transaction::Recovered; +use alloy_eips::eip7702::SignedAuthorization; use alloy_primitives::Address as AlloyAddress; use alloy_primitives::B256; use alloy_primitives::Bytes as AlloyBytes; @@ -254,7 +255,7 @@ impl TransactionInput { &value, &input.as_slice(), &AccessList::default(), - &Vec::::new(), // authorization list placeholder + &Vec::::new(), // authorization list placeholder ]); let mut out = Vec::with_capacity(1 + encoded.len()); out.push(0x04); @@ -263,15 +264,14 @@ impl TransactionInput { } // Legacy (default) - _ => { + _ => if self.execution_info.chain_id.is_some() { let encoded = Self::encode_rlp_list(&[&nonce, &gas_price, &gas_limit, &to.as_slice(), &value, &input.as_slice(), &chain_id, &0u8, &0u8]); B256::from(keccak256(encoded)) } else { let encoded = Self::encode_rlp_list(&[&nonce, &gas_price, &gas_limit, &to.as_slice(), &value, &input.as_slice()]); B256::from(keccak256(encoded)) - } - } + }, } } @@ -553,7 +553,7 @@ impl TransactionInput { let input_bytes: AlloyBytes = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing input"))?; input = input_bytes.to_vec(); let _: AccessList = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing accessList"))?; - let _: Vec = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing authorizationList"))?; + let _: Vec = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing authorizationList"))?; v = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing v"))?; r = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing r"))?; s = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing s"))?; From e02e3c6808dcee0e33d268baf3355837ac38a93b Mon Sep 17 00:00:00 2001 From: Feliphe Date: Wed, 2 Sep 2026 12:37:49 -0300 Subject: [PATCH 5/9] fix access_list default --- src/eth/types/transaction/transaction_input.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/eth/types/transaction/transaction_input.rs b/src/eth/types/transaction/transaction_input.rs index d8e18a778..073bbbe14 100644 --- a/src/eth/types/transaction/transaction_input.rs +++ b/src/eth/types/transaction/transaction_input.rs @@ -788,7 +788,7 @@ mod tests { .recover_address_from_prehash(&signing_hash) .expect("test signature should be recoverable"); - let signed = Signed::new_unchecked(tx, signature, Default::default()); + let signed = Signed::new_unchecked(tx, signature, B256::default()); let mut raw_bytes = Vec::new(); signed.encode_2718(&mut raw_bytes); @@ -826,7 +826,7 @@ mod tests { .recover_address_from_prehash(&signing_hash) .expect("test signature should be recoverable"); - let signed = Signed::new_unchecked(tx, signature, Default::default()); + let signed = Signed::new_unchecked(tx, signature, B256::default()); let mut raw_bytes = Vec::new(); signed.encode_2718(&mut raw_bytes); @@ -856,7 +856,7 @@ mod tests { to: TxKind::Call(AlloyAddress::default()), value: U256::from(100), input: AlloyBytes::new(), - access_list: Default::default(), + access_list: AccessList::default(), }; assert_direct_decode(tx, 1); } @@ -872,7 +872,7 @@ mod tests { to: TxKind::Call(AlloyAddress::default()), value: U256::from(100), input: AlloyBytes::new(), - access_list: Default::default(), + access_list: AccessList::default(), }; assert_direct_decode(tx, 2); } @@ -888,7 +888,7 @@ mod tests { to: AlloyAddress::default(), value: U256::from(100), input: AlloyBytes::new(), - access_list: Default::default(), + access_list: AccessList::default(), blob_versioned_hashes: Vec::new(), max_fee_per_blob_gas: 0, }; @@ -906,7 +906,7 @@ mod tests { to: AlloyAddress::default(), value: U256::from(100), input: AlloyBytes::new(), - access_list: Default::default(), + access_list: AccessList::default(), authorization_list: Vec::new(), }; assert_direct_decode(tx, 4); From 21f6a34edd9af256f8f08d6c21e82b7b3317fbe3 Mon Sep 17 00:00:00 2001 From: Feliphe Date: Fri, 4 Sep 2026 15:52:35 -0300 Subject: [PATCH 6/9] implement decodable for alloy_rlp --- Cargo.toml | 2 +- src/eth/rpc/mod.rs | 1 + src/eth/rpc/parser.rs | 5 +- src/eth/rpc/server.rs | 3 +- src/eth/rpc/types/error.rs | 35 +- src/eth/rpc/types/mod.rs | 1 + src/eth/types/error.rs | 2 +- src/eth/types/primitives/address.rs | 2 +- src/eth/types/primitives/bytes.rs | 2 +- src/eth/types/primitives/chain_id.rs | 2 +- src/eth/types/primitives/gas.rs | 14 +- src/eth/types/primitives/nonce.rs | 2 +- src/eth/types/primitives/wei.rs | 16 +- .../types/transaction/transaction_input.rs | 376 ++++++++++-------- 14 files changed, 281 insertions(+), 182 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4342f2c2b..25ed156ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,7 +83,7 @@ alloy-dyn-abi = "=1.6.1" alloy-json-abi = "=1.6.1" revm-inspectors = { version = "=0.42.0", features = ["js-tracer"] } revm-state = "42.0.0" -alloy-rlp = "=0.3.16" +alloy-rlp = { version = "=0.3.16", features = ["derive"] } # network jsonrpsee = { version = "=0.26.0", features = ["server", "client"] } diff --git a/src/eth/rpc/mod.rs b/src/eth/rpc/mod.rs index 407924e89..a8eb95655 100644 --- a/src/eth/rpc/mod.rs +++ b/src/eth/rpc/mod.rs @@ -28,3 +28,4 @@ pub use types::LogFilterInputTopic; pub use types::MulticallError; pub use types::RpcClientApp; pub use types::RpcError; +pub use types::TransactionDecodeError; diff --git a/src/eth/rpc/parser.rs b/src/eth/rpc/parser.rs index d0cef5e23..3cc595cc7 100644 --- a/src/eth/rpc/parser.rs +++ b/src/eth/rpc/parser.rs @@ -8,6 +8,7 @@ use tracing::Span; use super::middleware::Authentication; use crate::eth::rpc::RpcClientApp; use crate::eth::rpc::RpcError; +use crate::eth::rpc::TransactionDecodeError; use crate::ext::type_basename; use crate::infra::tracing::EnteredWrap; @@ -80,6 +81,8 @@ pub fn parse_rpc_rlp(value: &[u8]) -> Result { let mut buf = value; match T::decode(&mut buf) { Ok(trx) => Ok(trx), - Err(e) => Err(RpcError::TransactionInvalid { decode_error: e.to_string() }), + Err(e) => Err(RpcError::TransactionInvalid { + decode_error: TransactionDecodeError::Custom(e.to_string()), + }), } } diff --git a/src/eth/rpc/server.rs b/src/eth/rpc/server.rs index 5c5d476a3..61981afb3 100644 --- a/src/eth/rpc/server.rs +++ b/src/eth/rpc/server.rs @@ -67,6 +67,7 @@ use crate::eth::rpc::RpcHttpMiddleware; use crate::eth::rpc::RpcMiddleware; use crate::eth::rpc::RpcServerConfig; use crate::eth::rpc::RpcSubscriptions; +use crate::eth::rpc::TransactionDecodeError; use crate::eth::rpc::middleware::decode_input_arguments; use crate::eth::rpc::next_rpc_param; use crate::eth::rpc::next_rpc_param_or_default; @@ -1320,7 +1321,7 @@ fn eth_send_raw_transaction(_: Params<'_>, ctx: Arc, ext: Extensions _ => { tracing::error!("failed to execute eth_sendRawTransaction because transaction input is not available"); return Err(RpcError::TransactionInvalid { - decode_error: "transaction input is not available".to_string(), + decode_error: TransactionDecodeError::Custom("transaction input is not available".to_string()), } .into()); } diff --git a/src/eth/rpc/types/error.rs b/src/eth/rpc/types/error.rs index 0e0d1ced8..31c2c680d 100644 --- a/src/eth/rpc/types/error.rs +++ b/src/eth/rpc/types/error.rs @@ -3,6 +3,37 @@ use stratus_macros::ErrorCode; use crate::eth::rpc::BlockFilter; use crate::eth::types::ErrorCode; +/// Errors that can occur while decoding a raw transaction. +#[derive(Debug, thiserror::Error)] +pub enum TransactionDecodeError { + #[error("missing field: {0}")] + MissingField(&'static str), + + #[error("invalid to field")] + InvalidTo, + + #[error("failed to recover signer")] + SignerRecovery, + + #[error("unsupported transaction type")] + UnsupportedType, + + #[error("typed transaction has extra fields")] + ExtraFields, + + #[error("invalid transaction type byte")] + InvalidTypeByte, + + #[error("empty transaction bytes")] + EmptyBytes, + + #[error("legacy transaction type is not typed")] + LegacyNotTyped, + + #[error("{0}")] + Custom(String), +} + #[derive(Debug, thiserror::Error, strum::EnumProperty, strum::IntoStaticStr, ErrorCode)] #[major_error_code = 1000] pub enum RpcError { @@ -38,9 +69,9 @@ pub enum RpcError { #[error_code = 7] SubscriptionLimit { max: u32 }, - #[error("failed to decode transaction RLP data.")] + #[error("failed to decode transaction RLP data: {decode_error}")] #[error_code = 8] - TransactionInvalid { decode_error: String }, + TransactionInvalid { decode_error: TransactionDecodeError }, #[error("miner mode param is invalid.")] #[error_code = 9] diff --git a/src/eth/rpc/types/mod.rs b/src/eth/rpc/types/mod.rs index 52bbeac38..673b94ed3 100644 --- a/src/eth/rpc/types/mod.rs +++ b/src/eth/rpc/types/mod.rs @@ -8,6 +8,7 @@ mod timestamp_filter; pub use block_filter::BlockFilter; pub use error::MulticallError; pub use error::RpcError; +pub use error::TransactionDecodeError; pub use log_filter::LogFilter; pub use log_filter_input::LogFilterInput; pub use log_filter_input::LogFilterInputTopic; diff --git a/src/eth/types/error.rs b/src/eth/types/error.rs index 2e355c3d5..24fe4ec6c 100644 --- a/src/eth/types/error.rs +++ b/src/eth/types/error.rs @@ -143,7 +143,7 @@ impl StratusError { Self::RPC(RpcError::ClientBlocked { client }) => to_json_value(client), // Transaction - Self::RPC(RpcError::TransactionInvalid { decode_error }) => to_json_value(decode_error), + Self::RPC(RpcError::TransactionInvalid { decode_error }) => to_json_value(decode_error.to_string()), Self::Executor(ExecutorError::EvmFailed(e)) => JsonValue::String(e.to_string()), Self::Executor(ExecutorError::RevertedCall { output }) => to_json_value(output), Self::Executor(ExecutorError::RevertedCallWithReason { reason }) => to_json_value(reason), diff --git a/src/eth/types/primitives/address.rs b/src/eth/types/primitives/address.rs index c6a0b823c..4e2282b7a 100644 --- a/src/eth/types/primitives/address.rs +++ b/src/eth/types/primitives/address.rs @@ -15,7 +15,7 @@ use crate::alias::RevmAddress; use crate::eth::types::LogTopic; /// Address of an Ethereum account (wallet or contract). -#[derive(DebugAsJson, Clone, Copy, Default, Eq, PartialEq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)] +#[derive(DebugAsJson, Clone, Copy, Default, Eq, PartialEq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize, alloy_rlp::RlpDecodableWrapper)] pub struct Address(pub FixedBytes<20>); impl Address { diff --git a/src/eth/types/primitives/bytes.rs b/src/eth/types/primitives/bytes.rs index ee761d366..dd0010478 100644 --- a/src/eth/types/primitives/bytes.rs +++ b/src/eth/types/primitives/bytes.rs @@ -6,7 +6,7 @@ use display_json::DebugAsJson; use crate::alias::RevmBytes; use crate::alias::RevmOutput; -#[derive(DebugAsJson, Clone, Default, Eq, PartialEq)] +#[derive(DebugAsJson, Clone, Default, Eq, PartialEq, alloy_rlp::RlpDecodableWrapper)] pub struct Bytes(pub bytes::Bytes); impl Display for Bytes { diff --git a/src/eth/types/primitives/chain_id.rs b/src/eth/types/primitives/chain_id.rs index 131a1aaaf..dcc504ffe 100644 --- a/src/eth/types/primitives/chain_id.rs +++ b/src/eth/types/primitives/chain_id.rs @@ -10,7 +10,7 @@ use fake::Faker; use crate::ext::RuintExt; -#[derive(DebugAsJson, derive_more::Display, Clone, Copy, Default, Eq, PartialEq, serde::Serialize)] +#[derive(DebugAsJson, derive_more::Display, Clone, Copy, Default, Eq, PartialEq, serde::Serialize, alloy_rlp::RlpDecodableWrapper)] #[cfg_attr(test, derive(serde::Deserialize))] pub struct ChainId(pub U64); diff --git a/src/eth/types/primitives/gas.rs b/src/eth/types/primitives/gas.rs index afe568a57..2594eb2b0 100644 --- a/src/eth/types/primitives/gas.rs +++ b/src/eth/types/primitives/gas.rs @@ -9,7 +9,19 @@ use revm::context::result::ResultGas; use crate::ext::RuintExt; -#[derive(DebugAsJson, derive_more::Display, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, derive_more::Add, derive_more::AddAssign)] +#[derive( + DebugAsJson, + derive_more::Display, + Clone, + Copy, + Default, + PartialEq, + Eq, + serde::Serialize, + derive_more::Add, + derive_more::AddAssign, + alloy_rlp::RlpDecodableWrapper, +)] #[serde(transparent)] #[cfg_attr(test, derive(serde::Deserialize))] pub struct Gas(U64); diff --git a/src/eth/types/primitives/nonce.rs b/src/eth/types/primitives/nonce.rs index 0cc3fe100..909ef7a68 100644 --- a/src/eth/types/primitives/nonce.rs +++ b/src/eth/types/primitives/nonce.rs @@ -9,7 +9,7 @@ use fake::Faker; use crate::ext::RuintExt; -#[derive(DebugAsJson, derive_more::Display, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(DebugAsJson, derive_more::Display, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, alloy_rlp::RlpDecodableWrapper)] pub struct Nonce(U64); impl Nonce { diff --git a/src/eth/types/primitives/wei.rs b/src/eth/types/primitives/wei.rs index 65a0aa880..6dc185db7 100644 --- a/src/eth/types/primitives/wei.rs +++ b/src/eth/types/primitives/wei.rs @@ -7,7 +7,21 @@ use fake::Dummy; use fake::Faker; /// Native token amount in wei. -#[derive(DebugAsJson, derive_more::Display, Clone, Copy, Default, PartialOrd, Ord, PartialEq, Eq, derive_more::Sub, serde::Serialize, serde::Deserialize)] +#[derive( + DebugAsJson, + derive_more::Display, + Clone, + Copy, + Default, + PartialOrd, + Ord, + PartialEq, + Eq, + derive_more::Sub, + serde::Serialize, + serde::Deserialize, + alloy_rlp::RlpDecodableWrapper, +)] pub struct Wei(pub U256); impl Wei { diff --git a/src/eth/types/transaction/transaction_input.rs b/src/eth/types/transaction/transaction_input.rs index 073bbbe14..e36b02714 100644 --- a/src/eth/types/transaction/transaction_input.rs +++ b/src/eth/types/transaction/transaction_input.rs @@ -7,6 +7,7 @@ use alloy_consensus::TxEip4844Variant; use alloy_consensus::TxEip7702; use alloy_consensus::TxEnvelope; use alloy_consensus::TxLegacy; +use alloy_consensus::TxType; use alloy_consensus::transaction::Recovered; use alloy_eips::eip7702::SignedAuthorization; use alloy_primitives::Address as AlloyAddress; @@ -27,6 +28,7 @@ use display_json::DebugAsJson; use crate::alias::AlloyTransaction; use crate::eth::executor::TransactionExecutionInput; +use crate::eth::rpc::TransactionDecodeError; use crate::eth::types::Address; use crate::eth::types::Bytes; use crate::eth::types::ChainId; @@ -38,6 +40,28 @@ use crate::eth::types::SignatureComponent; use crate::eth::types::Wei; use crate::ext::RuintExt; +/// Legacy transaction `v` value constants (EIP-155). +const LEGACY_V_UNPROTECTED_EVEN: u64 = 27; +const LEGACY_V_UNPROTECTED_ODD: u64 = 28; +const EIP155_V_OFFSET: u64 = 35; + +impl From for alloy_rlp::Error { + fn from(value: TransactionDecodeError) -> Self { + // Leak the formatted message to satisfy the `&'static str` requirement of `alloy_rlp::Error::Custom`. + // This only happens on error paths. + let message = Box::leak(value.to_string().into_boxed_str()); + alloy_rlp::Error::Custom(message) + } +} + +/// Common fields shared by EIP-2930, EIP-1559, EIP-4844, and EIP-7702 transactions. +struct AccessListTxFields { + gas_limit: Gas, + to: Option
, + value: Wei, + input: Bytes, +} + #[derive(DebugAsJson, Clone, Copy, Default, PartialEq, Eq, serde::Serialize)] #[cfg_attr(test, derive(serde::Deserialize, fake::Dummy))] pub struct TransactionInfo { @@ -161,6 +185,15 @@ impl TransactionInput { out } + /// Encodes the fields of a typed transaction, prefixes the type byte, and hashes the result. + fn encode_typed_transaction(tx_type: TxType, fields: &[&dyn RlpEncodable]) -> B256 { + let encoded = Self::encode_rlp_list(fields); + let mut out = Vec::with_capacity(1 + encoded.len()); + out.push(tx_type as u8); + out.extend_from_slice(&encoded); + B256::from(keccak256(out)) + } + /// Returns the RLP encoding of the `to` field: empty bytes for contract creation, /// or the 20-byte address for a call. fn encode_to(&self) -> Vec { @@ -184,10 +217,16 @@ impl TransactionInput { let to = self.encode_to(); let input = self.encode_input(); - match self.transaction_info.tx_type.map(|t| t.as_u64()).unwrap_or(0) { - // EIP-2930 - 1 => { - let encoded = Self::encode_rlp_list(&[ + let tx_type = self + .transaction_info + .tx_type + .and_then(|t| TxType::try_from(t.as_u64()).ok()) + .unwrap_or(TxType::Legacy); + + match tx_type { + TxType::Eip2930 => Self::encode_typed_transaction( + TxType::Eip2930, + &[ &chain_id, &nonce, &gas_price, @@ -196,16 +235,12 @@ impl TransactionInput { &value, &input.as_slice(), &AccessList::default(), - ]); - let mut out = Vec::with_capacity(1 + encoded.len()); - out.push(0x01); - out.extend_from_slice(&encoded); - B256::from(keccak256(out)) - } + ], + ), - // EIP-1559 - 2 => { - let encoded = Self::encode_rlp_list(&[ + TxType::Eip1559 => Self::encode_typed_transaction( + TxType::Eip1559, + &[ &chain_id, &nonce, &gas_price, // max_priority_fee_per_gas @@ -215,16 +250,12 @@ impl TransactionInput { &value, &input.as_slice(), &AccessList::default(), - ]); - let mut out = Vec::with_capacity(1 + encoded.len()); - out.push(0x02); - out.extend_from_slice(&encoded); - B256::from(keccak256(out)) - } + ], + ), - // EIP-4844 - 3 => { - let encoded = Self::encode_rlp_list(&[ + TxType::Eip4844 => Self::encode_typed_transaction( + TxType::Eip4844, + &[ &chain_id, &nonce, &gas_price, // max_priority_fee_per_gas @@ -236,16 +267,12 @@ impl TransactionInput { &AccessList::default(), &0u128, // max_fee_per_blob_gas &Vec::::new(), // blob_versioned_hashes - ]); - let mut out = Vec::with_capacity(1 + encoded.len()); - out.push(0x03); - out.extend_from_slice(&encoded); - B256::from(keccak256(out)) - } + ], + ), - // EIP-7702 - 4 => { - let encoded = Self::encode_rlp_list(&[ + TxType::Eip7702 => Self::encode_typed_transaction( + TxType::Eip7702, + &[ &chain_id, &nonce, &gas_price, // max_priority_fee_per_gas @@ -256,15 +283,10 @@ impl TransactionInput { &input.as_slice(), &AccessList::default(), &Vec::::new(), // authorization list placeholder - ]); - let mut out = Vec::with_capacity(1 + encoded.len()); - out.push(0x04); - out.extend_from_slice(&encoded); - B256::from(keccak256(out)) - } + ], + ), - // Legacy (default) - _ => + TxType::Legacy => if self.execution_info.chain_id.is_some() { let encoded = Self::encode_rlp_list(&[&nonce, &gas_price, &gas_limit, &to.as_slice(), &value, &input.as_slice(), &chain_id, &0u8, &0u8]); B256::from(keccak256(encoded)) @@ -290,9 +312,14 @@ impl TransactionInput { let signature: AlloySignature = self.signature.into(); let tx_hash = self.transaction_info.hash.into(); - match self.transaction_info.tx_type.map(|t| t.as_u64()).unwrap_or(0) { - // EIP-2930 - 1 => TxEnvelope::Eip2930(Signed::new_unchecked( + let tx_type = self + .transaction_info + .tx_type + .and_then(|t| TxType::try_from(t.as_u64()).ok()) + .unwrap_or(TxType::Legacy); + + match tx_type { + TxType::Eip2930 => TxEnvelope::Eip2930(Signed::new_unchecked( TxEip2930 { chain_id: self.execution_info.chain_id.unwrap_or_default().into(), nonce: self.execution_info.nonce.into(), @@ -307,8 +334,7 @@ impl TransactionInput { tx_hash, )), - // EIP-1559 - 2 => TxEnvelope::Eip1559(Signed::new_unchecked( + TxType::Eip1559 => TxEnvelope::Eip1559(Signed::new_unchecked( TxEip1559 { chain_id: self.execution_info.chain_id.unwrap_or_default().into(), nonce: self.execution_info.nonce.into(), @@ -324,8 +350,7 @@ impl TransactionInput { tx_hash, )), - // EIP-4844 - 3 => TxEnvelope::Eip4844(Signed::new_unchecked( + TxType::Eip4844 => TxEnvelope::Eip4844(Signed::new_unchecked( TxEip4844Variant::TxEip4844(TxEip4844 { chain_id: self.execution_info.chain_id.unwrap_or_default().into(), nonce: self.execution_info.nonce.into(), @@ -343,8 +368,7 @@ impl TransactionInput { tx_hash, )), - // EIP-7702 - 4 => TxEnvelope::Eip7702(Signed::new_unchecked( + TxType::Eip7702 => TxEnvelope::Eip7702(Signed::new_unchecked( TxEip7702 { chain_id: self.execution_info.chain_id.unwrap_or_default().into(), nonce: self.execution_info.nonce.into(), @@ -361,8 +385,7 @@ impl TransactionInput { tx_hash, )), - // Legacy (default) - _ => TxEnvelope::Legacy(Signed::new_unchecked( + TxType::Legacy => TxEnvelope::Legacy(Signed::new_unchecked( TxLegacy { chain_id: self.execution_info.chain_id.map(Into::into), nonce: self.execution_info.nonce.into(), @@ -398,11 +421,11 @@ impl TransactionInput { /// Derives the chain id and signature parity from a legacy `v` value. fn decode_legacy_v(v: u64) -> alloy_rlp::Result<(Option, u64)> { match v { - 27 => Ok((None, 0)), - 28 => Ok((None, 1)), - v if v >= 35 => { - let chain_id = (v - 35) / 2; - let parity = (v - 27) % 2; + LEGACY_V_UNPROTECTED_EVEN => Ok((None, 0)), + LEGACY_V_UNPROTECTED_ODD => Ok((None, 1)), + v if v >= EIP155_V_OFFSET => { + let chain_id = (v - EIP155_V_OFFSET) / 2; + let parity = (v - LEGACY_V_UNPROTECTED_EVEN) % 2; Ok((Some(ChainId::from(chain_id)), parity)) } _ => Err(alloy_rlp::Error::Custom("invalid legacy v value")), @@ -412,12 +435,12 @@ impl TransactionInput { /// Builds a `TransactionInput` from decoded legacy fields and recovers the signer. #[allow(clippy::too_many_arguments)] fn build_legacy( - nonce: u64, + nonce: Nonce, gas_price: u128, - gas_limit: u64, + gas_limit: Gas, to: Option
, - value: U256, - input: Vec, + value: Wei, + input: Bytes, v: u64, r: U256, s: U256, @@ -429,12 +452,12 @@ impl TransactionInput { transaction_info: TransactionInfo { tx_type: None, hash }, execution_info: ExecutionInfo { chain_id, - nonce: Nonce::from(nonce), + nonce, signer: Signer::Unrecovered, to, - value: Wei::from(value), - input: Bytes::from(input), - gas_limit: Gas::from(gas_limit), + value, + input, + gas_limit, gas_price, }, signature: Signature { v: U64::from(parity), r, s }, @@ -447,148 +470,161 @@ impl TransactionInput { } /// Decodes a legacy transaction from raw RLP bytes. - fn decode_legacy(raw_bytes: &[u8]) -> alloy_rlp::Result { + fn decode_legacy(raw_bytes: &[u8]) -> anyhow::Result { let mut rlp = alloy_rlp::Rlp::new(raw_bytes)?; - let nonce: u64 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing nonce"))?; - let gas_price: u128 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing gasPrice"))?; - let gas_limit: u64 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing gasLimit"))?; - let to_bytes: AlloyBytes = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing to"))?; - let value: U256 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing value"))?; - let input: AlloyBytes = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing input"))?; - let v: u64 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing v"))?; - let r: U256 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing r"))?; - let s: U256 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing s"))?; + let nonce: Nonce = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("nonce"))?; + let gas_price: u128 = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("gasPrice"))?; + let gas_limit: Gas = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("gasLimit"))?; + let to_bytes: AlloyBytes = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("to"))?; + let value: Wei = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("value"))?; + let input: Bytes = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("input"))?; + let v: u64 = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("v"))?; + let r: U256 = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("r"))?; + let s: U256 = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("s"))?; if rlp.get_next::()?.is_some() { - return Err(alloy_rlp::Error::Custom("legacy transaction has extra fields")); + return Err(TransactionDecodeError::ExtraFields.into()); } - let to = Self::decode_to(&to_bytes)?; + let to = Self::decode_to(&to_bytes).map_err(|_| TransactionDecodeError::InvalidTo)?; let hash = Hash::from(keccak256(raw_bytes)); - Self::build_legacy(nonce, gas_price, gas_limit, to, value, input.to_vec(), v, r, s, hash) - .map_err(|_| alloy_rlp::Error::Custom("failed to recover legacy signer")) + Ok(Self::build_legacy(nonce, gas_price, gas_limit, to, value, input, v, r, s, hash).map_err(|_| TransactionDecodeError::SignerRecovery)?) + } + + /// Decodes the common fields shared by access-list transaction types (EIP-2930, EIP-1559, EIP-4844, EIP-7702). + /// The `to` field is decoded via the provided closure because its RLP encoding varies by type. + fn decode_access_list_fields(rlp: &mut alloy_rlp::Rlp<'_>, decode_to: F) -> anyhow::Result + where + F: FnOnce(&mut alloy_rlp::Rlp<'_>) -> anyhow::Result>, + { + let gas_limit: Gas = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("gasLimit"))?; + let to = decode_to(rlp)?; + let value: Wei = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("value"))?; + let input: Bytes = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("input"))?; + let _: AccessList = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("accessList"))?; + + Ok(AccessListTxFields { gas_limit, to, value, input }) + } + + /// Decodes the dynamic-fee gas price fields (`maxPriorityFeePerGas` and `maxFeePerGas`) + /// and returns `maxFeePerGas` as the effective gas price. + fn decode_dynamic_fee_gas_price(rlp: &mut alloy_rlp::Rlp<'_>) -> anyhow::Result { + let max_priority_fee_per_gas: u128 = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("maxPriorityFeePerGas"))?; + let max_fee_per_gas: u128 = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("maxFeePerGas"))?; + let _ = max_priority_fee_per_gas; + Ok(max_fee_per_gas) + } + + /// Decodes the transaction signature fields (`v`, `r`, `s`). + fn decode_signature(rlp: &mut alloy_rlp::Rlp<'_>) -> anyhow::Result<(u64, U256, U256)> { + let v = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("v"))?; + let r = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("r"))?; + let s = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("s"))?; + Ok((v, r, s)) } /// Decodes a typed transaction (EIP-2718) from raw bytes. - fn decode_typed(tx_type: u8, payload: &[u8], raw_bytes: &[u8]) -> alloy_rlp::Result { + fn decode_typed(tx_type: u8, payload: &[u8], raw_bytes: &[u8]) -> anyhow::Result { + let tx_type = TxType::try_from(tx_type).map_err(|_| TransactionDecodeError::UnsupportedType)?; let mut rlp = alloy_rlp::Rlp::new(payload)?; - let chain_id: u64 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing chainId"))?; - let nonce: u64 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing nonce"))?; + let chain_id: ChainId = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("chainId"))?; + let nonce: Nonce = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("nonce"))?; let gas_price: u128; - let gas_limit: u64; + let gas_limit: Gas; let to: Option
; - let value: U256; - let input: Vec; + let value: Wei; + let input: Bytes; let v: u64; let r: U256; let s: U256; match tx_type { - // EIP-2930 - 1 => { - gas_price = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing gasPrice"))?; - gas_limit = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing gasLimit"))?; - let to_bytes: AlloyBytes = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing to"))?; - to = Self::decode_to(&to_bytes)?; - value = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing value"))?; - let input_bytes: AlloyBytes = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing input"))?; - input = input_bytes.to_vec(); - let _: AccessList = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing accessList"))?; - v = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing v"))?; - r = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing r"))?; - s = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing s"))?; + TxType::Eip2930 => { + gas_price = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("gasPrice"))?; + let fields = Self::decode_access_list_fields(&mut rlp, |rlp| { + let to_bytes: AlloyBytes = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("to"))?; + Self::decode_to(&to_bytes).map_err(|_| TransactionDecodeError::InvalidTo.into()) + })?; + gas_limit = fields.gas_limit; + to = fields.to; + value = fields.value; + input = fields.input; + (v, r, s) = Self::decode_signature(&mut rlp)?; } - // EIP-1559 - 2 => { - let max_priority_fee_per_gas: u128 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing maxPriorityFeePerGas"))?; - let max_fee_per_gas: u128 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing maxFeePerGas"))?; - gas_price = max_fee_per_gas; - let _ = max_priority_fee_per_gas; - gas_limit = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing gasLimit"))?; - let to_bytes: AlloyBytes = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing to"))?; - to = Self::decode_to(&to_bytes)?; - value = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing value"))?; - let input_bytes: AlloyBytes = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing input"))?; - input = input_bytes.to_vec(); - let _: AccessList = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing accessList"))?; - v = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing v"))?; - r = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing r"))?; - s = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing s"))?; + TxType::Eip1559 => { + gas_price = Self::decode_dynamic_fee_gas_price(&mut rlp)?; + let fields = Self::decode_access_list_fields(&mut rlp, |rlp| { + let to_bytes: AlloyBytes = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("to"))?; + Self::decode_to(&to_bytes).map_err(|_| TransactionDecodeError::InvalidTo.into()) + })?; + gas_limit = fields.gas_limit; + to = fields.to; + value = fields.value; + input = fields.input; + (v, r, s) = Self::decode_signature(&mut rlp)?; } - // EIP-4844 - 3 => { - let max_priority_fee_per_gas: u128 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing maxPriorityFeePerGas"))?; - let max_fee_per_gas: u128 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing maxFeePerGas"))?; - gas_price = max_fee_per_gas; - let _ = max_priority_fee_per_gas; - gas_limit = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing gasLimit"))?; - let to_addr: AlloyAddress = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing to"))?; - to = Some(Address::from(to_addr.0)); - value = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing value"))?; - let input_bytes: AlloyBytes = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing input"))?; - input = input_bytes.to_vec(); - let _: AccessList = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing accessList"))?; - let _: u128 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing maxFeePerBlobGas"))?; - let _: Vec = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing blobVersionedHashes"))?; - v = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing v"))?; - r = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing r"))?; - s = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing s"))?; + TxType::Eip4844 => { + gas_price = Self::decode_dynamic_fee_gas_price(&mut rlp)?; + let fields = Self::decode_access_list_fields(&mut rlp, |rlp| { + let to_addr: AlloyAddress = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("to"))?; + Ok(Some(Address::from(to_addr.0))) + })?; + gas_limit = fields.gas_limit; + to = fields.to; + value = fields.value; + input = fields.input; + let _: u128 = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("maxFeePerBlobGas"))?; + let _: Vec = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("blobVersionedHashes"))?; + (v, r, s) = Self::decode_signature(&mut rlp)?; } - // EIP-7702 - 4 => { - let max_priority_fee_per_gas: u128 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing maxPriorityFeePerGas"))?; - let max_fee_per_gas: u128 = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing maxFeePerGas"))?; - gas_price = max_fee_per_gas; - let _ = max_priority_fee_per_gas; - gas_limit = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing gasLimit"))?; - let to_addr: AlloyAddress = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing to"))?; - to = Some(Address::from(to_addr.0)); - value = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing value"))?; - let input_bytes: AlloyBytes = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing input"))?; - input = input_bytes.to_vec(); - let _: AccessList = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing accessList"))?; - let _: Vec = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing authorizationList"))?; - v = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing v"))?; - r = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing r"))?; - s = rlp.get_next()?.ok_or(alloy_rlp::Error::Custom("missing s"))?; + TxType::Eip7702 => { + gas_price = Self::decode_dynamic_fee_gas_price(&mut rlp)?; + let fields = Self::decode_access_list_fields(&mut rlp, |rlp| { + let to_addr: AlloyAddress = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("to"))?; + Ok(Some(Address::from(to_addr.0))) + })?; + gas_limit = fields.gas_limit; + to = fields.to; + value = fields.value; + input = fields.input; + let _: Vec = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("authorizationList"))?; + (v, r, s) = Self::decode_signature(&mut rlp)?; } - _ => return Err(alloy_rlp::Error::Custom("unsupported transaction type")), + TxType::Legacy => return Err(TransactionDecodeError::LegacyNotTyped.into()), } if rlp.get_next::()?.is_some() { - return Err(alloy_rlp::Error::Custom("typed transaction has extra fields")); + return Err(TransactionDecodeError::ExtraFields.into()); } let hash = Hash::from(keccak256(raw_bytes)); let mut tx = Self { transaction_info: TransactionInfo { - tx_type: Some(U64::from(tx_type)), + tx_type: Some(U64::from(tx_type as u8)), hash, }, execution_info: ExecutionInfo { - chain_id: Some(ChainId::from(chain_id)), - nonce: Nonce::from(nonce), + chain_id: Some(chain_id), + nonce, signer: Signer::Unrecovered, to, - value: Wei::from(value), - input: Bytes::from(input), - gas_limit: Gas::from(gas_limit), + value, + input, + gas_limit, gas_price, }, signature: Signature { v: U64::from(v), r, s }, }; - let signer = tx - .recover_signer_address() - .map_err(|_| alloy_rlp::Error::Custom("failed to recover typed signer"))?; + let signer = tx.recover_signer_address().map_err(|_| TransactionDecodeError::SignerRecovery)?; tx.execution_info.signer = Signer::Recovered(signer); Ok(tx) @@ -600,17 +636,17 @@ impl RlpDecodable for TransactionInput { let raw_bytes = *buf; if raw_bytes.is_empty() { - return Err(alloy_rlp::Error::Custom("empty transaction bytes")); + return Err(TransactionDecodeError::EmptyBytes.into()); } let tx = match raw_bytes[0] { - byte if byte >= 0xc0 => Self::decode_legacy(raw_bytes)?, + byte if byte >= 0xc0 => Self::decode_legacy(raw_bytes).map_err(decode_error_to_rlp)?, byte if byte <= 0x7f => { let tx_type = byte; let payload = &raw_bytes[1..]; - Self::decode_typed(tx_type, payload, raw_bytes)? + Self::decode_typed(tx_type, payload, raw_bytes).map_err(decode_error_to_rlp)? } - _ => return Err(alloy_rlp::Error::Custom("invalid transaction type byte")), + _ => return Err(TransactionDecodeError::InvalidTypeByte.into()), }; // A raw transaction occupies the entire buffer. @@ -619,6 +655,14 @@ impl RlpDecodable for TransactionInput { } } +/// Converts a transaction decode error into an `alloy_rlp::Error` for the `RlpDecodable` boundary. +fn decode_error_to_rlp(error: anyhow::Error) -> alloy_rlp::Error { + match error.downcast::() { + Ok(decode_error) => decode_error.into(), + Err(_) => alloy_rlp::Error::Custom("failed to decode transaction"), + } +} + // ----------------------------------------------------------------------------- // Conversion: Other -> Self // ----------------------------------------------------------------------------- @@ -630,14 +674,6 @@ impl TryFrom for TransactionInput { } } -impl TryFrom for TransactionInput { - type Error = anyhow::Error; - - fn try_from(value: AlloyTransaction) -> anyhow::Result { - try_from_alloy_transaction(value) - } -} - fn build_transaction_input_from_envelope(envelope: &TxEnvelope) -> anyhow::Result { // Get signature components from the envelope let signature = envelope.signature(); From c30b074b42998a6acd9bdc6e8b1b3ff94e5bc62b Mon Sep 17 00:00:00 2001 From: Feliphe Date: Fri, 4 Sep 2026 17:28:17 -0300 Subject: [PATCH 7/9] debug --- src/eth/rpc/parser.rs | 9 ++++++--- src/eth/rpc/server.rs | 12 ------------ 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/eth/rpc/parser.rs b/src/eth/rpc/parser.rs index 3cc595cc7..9a1430512 100644 --- a/src/eth/rpc/parser.rs +++ b/src/eth/rpc/parser.rs @@ -81,8 +81,11 @@ pub fn parse_rpc_rlp(value: &[u8]) -> Result { let mut buf = value; match T::decode(&mut buf) { Ok(trx) => Ok(trx), - Err(e) => Err(RpcError::TransactionInvalid { - decode_error: TransactionDecodeError::Custom(e.to_string()), - }), + Err(e) => { + tracing::error!(reason = %e, raw_bytes = %const_hex::encode_prefixed(value), "failed to decode raw transaction RLP"); + Err(RpcError::TransactionInvalid { + decode_error: TransactionDecodeError::Custom(e.to_string()), + }) + } } } diff --git a/src/eth/rpc/server.rs b/src/eth/rpc/server.rs index 79c04e7e1..05150a368 100644 --- a/src/eth/rpc/server.rs +++ b/src/eth/rpc/server.rs @@ -74,7 +74,6 @@ use crate::eth::rpc::RpcHttpMiddleware; use crate::eth::rpc::RpcMiddleware; use crate::eth::rpc::RpcServerConfig; use crate::eth::rpc::RpcSubscriptions; -use crate::eth::rpc::TransactionDecodeError; use crate::eth::rpc::middleware::TransactionTracingIdentifiers; use crate::eth::rpc::middleware::decode_input_arguments; use crate::eth::rpc::next_rpc_param; @@ -1386,17 +1385,6 @@ fn _eth_send_raw_transaction_impl( ) .entered(); - // get the pre-decoded transaction from extensions - let (tx, tx_data) = match (ext.get::(), ext.get::()) { - (Some(tx), Some(data)) => (tx.clone(), data.clone()), - _ => { - tracing::error!("failed to execute eth_sendRawTransaction because transaction input is not available"); - return Err(RpcError::TransactionInvalid { - decode_error: TransactionDecodeError::Custom("transaction input is not available".to_string()), - } - .into()); - } - }; let tx_hash = tx.transaction_info.hash; // track From 2a55223ae0dc1479613f2661b9a9c11524bccb33 Mon Sep 17 00:00:00 2001 From: Feliphe Date: Fri, 4 Sep 2026 17:46:07 -0300 Subject: [PATCH 8/9] remove debug --- src/eth/rpc/parser.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/eth/rpc/parser.rs b/src/eth/rpc/parser.rs index 9a1430512..3cc595cc7 100644 --- a/src/eth/rpc/parser.rs +++ b/src/eth/rpc/parser.rs @@ -81,11 +81,8 @@ pub fn parse_rpc_rlp(value: &[u8]) -> Result { let mut buf = value; match T::decode(&mut buf) { Ok(trx) => Ok(trx), - Err(e) => { - tracing::error!(reason = %e, raw_bytes = %const_hex::encode_prefixed(value), "failed to decode raw transaction RLP"); - Err(RpcError::TransactionInvalid { - decode_error: TransactionDecodeError::Custom(e.to_string()), - }) - } + Err(e) => Err(RpcError::TransactionInvalid { + decode_error: TransactionDecodeError::Custom(e.to_string()), + }), } } From a5934ed0d8410ec7dfc3091525b72ed6c55ac2da Mon Sep 17 00:00:00 2001 From: Feliphe Date: Wed, 9 Sep 2026 11:51:22 -0300 Subject: [PATCH 9/9] refact: generic decode 'to' field --- src/eth/rpc/types/error.rs | 25 ++ src/eth/types/primitives/address.rs | 26 +- src/eth/types/primitives/bytes.rs | 2 +- src/eth/types/primitives/chain_id.rs | 4 +- src/eth/types/primitives/gas.rs | 1 + src/eth/types/primitives/nonce.rs | 14 +- src/eth/types/primitives/wei.rs | 1 + .../types/transaction/transaction_input.rs | 227 ++++++++---------- 8 files changed, 173 insertions(+), 127 deletions(-) diff --git a/src/eth/rpc/types/error.rs b/src/eth/rpc/types/error.rs index 31c2c680d..220db3de9 100644 --- a/src/eth/rpc/types/error.rs +++ b/src/eth/rpc/types/error.rs @@ -30,10 +30,35 @@ pub enum TransactionDecodeError { #[error("legacy transaction type is not typed")] LegacyNotTyped, + #[error("invalid legacy v value")] + InvalidLegacyV, + + #[error("rlp decode error: {0}")] + RlpError(String), + #[error("{0}")] Custom(String), } +impl From for alloy_rlp::Error { + fn from(value: TransactionDecodeError) -> Self { + let message = match value { + TransactionDecodeError::MissingField(_) => "missing field", + TransactionDecodeError::InvalidTo => "invalid to field", + TransactionDecodeError::SignerRecovery => "failed to recover signer", + TransactionDecodeError::UnsupportedType => "unsupported transaction type", + TransactionDecodeError::ExtraFields => "typed transaction has extra fields", + TransactionDecodeError::InvalidTypeByte => "invalid transaction type byte", + TransactionDecodeError::EmptyBytes => "empty transaction bytes", + TransactionDecodeError::LegacyNotTyped => "legacy transaction type is not typed", + TransactionDecodeError::InvalidLegacyV => "invalid legacy v value", + TransactionDecodeError::RlpError(_) => "rlp decode error", + TransactionDecodeError::Custom(_) => "failed to decode transaction", + }; + alloy_rlp::Error::Custom(message) + } +} + #[derive(Debug, thiserror::Error, strum::EnumProperty, strum::IntoStaticStr, ErrorCode)] #[major_error_code = 1000] pub enum RpcError { diff --git a/src/eth/types/primitives/address.rs b/src/eth/types/primitives/address.rs index 4e2282b7a..d4e9976b0 100644 --- a/src/eth/types/primitives/address.rs +++ b/src/eth/types/primitives/address.rs @@ -12,10 +12,25 @@ use fake::Faker; use hex_literal::hex; use crate::alias::RevmAddress; +use crate::eth::rpc::TransactionDecodeError; use crate::eth::types::LogTopic; /// Address of an Ethereum account (wallet or contract). -#[derive(DebugAsJson, Clone, Copy, Default, Eq, PartialEq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize, alloy_rlp::RlpDecodableWrapper)] +#[derive( + DebugAsJson, + Clone, + Copy, + Default, + Eq, + PartialEq, + PartialOrd, + Ord, + Hash, + serde::Serialize, + serde::Deserialize, + alloy_rlp::RlpDecodableWrapper, + alloy_rlp::RlpEncodableWrapper, +)] pub struct Address(pub FixedBytes<20>); impl Address { @@ -109,6 +124,15 @@ impl TryFrom> for Address { } } +impl TryFrom<&[u8]> for Address { + type Error = TransactionDecodeError; + + fn try_from(value: &[u8]) -> Result { + let array = <[u8; 20]>::try_from(value).map_err(|_| TransactionDecodeError::InvalidTo)?; + Ok(Self(FixedBytes::from(array))) + } +} + // ----------------------------------------------------------------------------- // Conversions: Self -> Other // ----------------------------------------------------------------------------- diff --git a/src/eth/types/primitives/bytes.rs b/src/eth/types/primitives/bytes.rs index dd0010478..c56e68626 100644 --- a/src/eth/types/primitives/bytes.rs +++ b/src/eth/types/primitives/bytes.rs @@ -6,7 +6,7 @@ use display_json::DebugAsJson; use crate::alias::RevmBytes; use crate::alias::RevmOutput; -#[derive(DebugAsJson, Clone, Default, Eq, PartialEq, alloy_rlp::RlpDecodableWrapper)] +#[derive(DebugAsJson, Clone, Default, Eq, PartialEq, alloy_rlp::RlpDecodableWrapper, alloy_rlp::RlpEncodableWrapper)] pub struct Bytes(pub bytes::Bytes); impl Display for Bytes { diff --git a/src/eth/types/primitives/chain_id.rs b/src/eth/types/primitives/chain_id.rs index dcc504ffe..a6177c55e 100644 --- a/src/eth/types/primitives/chain_id.rs +++ b/src/eth/types/primitives/chain_id.rs @@ -10,7 +10,9 @@ use fake::Faker; use crate::ext::RuintExt; -#[derive(DebugAsJson, derive_more::Display, Clone, Copy, Default, Eq, PartialEq, serde::Serialize, alloy_rlp::RlpDecodableWrapper)] +#[derive( + DebugAsJson, derive_more::Display, Clone, Copy, Default, Eq, PartialEq, serde::Serialize, alloy_rlp::RlpDecodableWrapper, alloy_rlp::RlpEncodableWrapper, +)] #[cfg_attr(test, derive(serde::Deserialize))] pub struct ChainId(pub U64); diff --git a/src/eth/types/primitives/gas.rs b/src/eth/types/primitives/gas.rs index 2594eb2b0..ebc30e62a 100644 --- a/src/eth/types/primitives/gas.rs +++ b/src/eth/types/primitives/gas.rs @@ -21,6 +21,7 @@ use crate::ext::RuintExt; derive_more::Add, derive_more::AddAssign, alloy_rlp::RlpDecodableWrapper, + alloy_rlp::RlpEncodableWrapper, )] #[serde(transparent)] #[cfg_attr(test, derive(serde::Deserialize))] diff --git a/src/eth/types/primitives/nonce.rs b/src/eth/types/primitives/nonce.rs index 909ef7a68..ee43af8b4 100644 --- a/src/eth/types/primitives/nonce.rs +++ b/src/eth/types/primitives/nonce.rs @@ -9,7 +9,19 @@ use fake::Faker; use crate::ext::RuintExt; -#[derive(DebugAsJson, derive_more::Display, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, alloy_rlp::RlpDecodableWrapper)] +#[derive( + DebugAsJson, + derive_more::Display, + Clone, + Copy, + Default, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + alloy_rlp::RlpDecodableWrapper, + alloy_rlp::RlpEncodableWrapper, +)] pub struct Nonce(U64); impl Nonce { diff --git a/src/eth/types/primitives/wei.rs b/src/eth/types/primitives/wei.rs index 6dc185db7..30bd3ab8a 100644 --- a/src/eth/types/primitives/wei.rs +++ b/src/eth/types/primitives/wei.rs @@ -21,6 +21,7 @@ use fake::Faker; serde::Serialize, serde::Deserialize, alloy_rlp::RlpDecodableWrapper, + alloy_rlp::RlpEncodableWrapper, )] pub struct Wei(pub U256); diff --git a/src/eth/types/transaction/transaction_input.rs b/src/eth/types/transaction/transaction_input.rs index e36b02714..cf0263f15 100644 --- a/src/eth/types/transaction/transaction_input.rs +++ b/src/eth/types/transaction/transaction_input.rs @@ -10,9 +10,7 @@ use alloy_consensus::TxLegacy; use alloy_consensus::TxType; use alloy_consensus::transaction::Recovered; use alloy_eips::eip7702::SignedAuthorization; -use alloy_primitives::Address as AlloyAddress; use alloy_primitives::B256; -use alloy_primitives::Bytes as AlloyBytes; use alloy_primitives::Signature as AlloySignature; use alloy_primitives::TxKind; use alloy_primitives::U64; @@ -45,17 +43,25 @@ const LEGACY_V_UNPROTECTED_EVEN: u64 = 27; const LEGACY_V_UNPROTECTED_ODD: u64 = 28; const EIP155_V_OFFSET: u64 = 35; -impl From for alloy_rlp::Error { - fn from(value: TransactionDecodeError) -> Self { - // Leak the formatted message to satisfy the `&'static str` requirement of `alloy_rlp::Error::Custom`. - // This only happens on error paths. - let message = Box::leak(value.to_string().into_boxed_str()); - alloy_rlp::Error::Custom(message) +/// Decodes the next RLP value, mapping a missing value to `MissingField` and an RLP error to `RlpError`. +fn decode_next(rlp: &mut alloy_rlp::Rlp<'_>, field: &'static str) -> Result { + rlp.get_next() + .map_err(|e| TransactionDecodeError::RlpError(e.to_string()))? + .ok_or(TransactionDecodeError::MissingField(field)) +} + +/// Decodes the `to` field: empty bytes mean contract creation, otherwise a 20-byte address. +fn decode_to_field(rlp: &mut alloy_rlp::Rlp<'_>) -> Result, TransactionDecodeError> { + let to_bytes = decode_next::(rlp, "to")?; + if to_bytes.is_empty() { + Ok(None) + } else { + Ok(Some(Address::try_from(to_bytes.as_ref())?)) } } /// Common fields shared by EIP-2930, EIP-1559, EIP-4844, and EIP-7702 transactions. -struct AccessListTxFields { +struct TypedTxCommonFields { gas_limit: Gas, to: Option
, value: Wei, @@ -185,13 +191,22 @@ impl TransactionInput { out } - /// Encodes the fields of a typed transaction, prefixes the type byte, and hashes the result. - fn encode_typed_transaction(tx_type: TxType, fields: &[&dyn RlpEncodable]) -> B256 { + /// Encodes a transaction for signature hash computation. + /// + /// For typed transactions, the type byte is prepended to the RLP list. + /// For legacy transactions, only the RLP list is encoded. + fn encode_transaction(tx_type: Option, fields: &[&dyn RlpEncodable]) -> B256 { let encoded = Self::encode_rlp_list(fields); - let mut out = Vec::with_capacity(1 + encoded.len()); - out.push(tx_type as u8); - out.extend_from_slice(&encoded); - B256::from(keccak256(out)) + let hash_input = match tx_type { + Some(tx_type) => { + let mut out = Vec::with_capacity(1 + encoded.len()); + out.push(tx_type as u8); + out.extend_from_slice(&encoded); + out + } + None => encoded, + }; + B256::from(keccak256(hash_input)) } /// Returns the RLP encoding of the `to` field: empty bytes for contract creation, @@ -224,8 +239,8 @@ impl TransactionInput { .unwrap_or(TxType::Legacy); match tx_type { - TxType::Eip2930 => Self::encode_typed_transaction( - TxType::Eip2930, + TxType::Eip2930 => Self::encode_transaction( + Some(TxType::Eip2930), &[ &chain_id, &nonce, @@ -238,8 +253,8 @@ impl TransactionInput { ], ), - TxType::Eip1559 => Self::encode_typed_transaction( - TxType::Eip1559, + TxType::Eip1559 => Self::encode_transaction( + Some(TxType::Eip1559), &[ &chain_id, &nonce, @@ -253,8 +268,8 @@ impl TransactionInput { ], ), - TxType::Eip4844 => Self::encode_typed_transaction( - TxType::Eip4844, + TxType::Eip4844 => Self::encode_transaction( + Some(TxType::Eip4844), &[ &chain_id, &nonce, @@ -270,8 +285,8 @@ impl TransactionInput { ], ), - TxType::Eip7702 => Self::encode_typed_transaction( - TxType::Eip7702, + TxType::Eip7702 => Self::encode_transaction( + Some(TxType::Eip7702), &[ &chain_id, &nonce, @@ -288,11 +303,12 @@ impl TransactionInput { TxType::Legacy => if self.execution_info.chain_id.is_some() { - let encoded = Self::encode_rlp_list(&[&nonce, &gas_price, &gas_limit, &to.as_slice(), &value, &input.as_slice(), &chain_id, &0u8, &0u8]); - B256::from(keccak256(encoded)) + Self::encode_transaction( + None, + &[&nonce, &gas_price, &gas_limit, &to.as_slice(), &value, &input.as_slice(), &chain_id, &0u8, &0u8], + ) } else { - let encoded = Self::encode_rlp_list(&[&nonce, &gas_price, &gas_limit, &to.as_slice(), &value, &input.as_slice()]); - B256::from(keccak256(encoded)) + Self::encode_transaction(None, &[&nonce, &gas_price, &gas_limit, &to.as_slice(), &value, &input.as_slice()]) }, } } @@ -407,28 +423,18 @@ impl TransactionInput { // ----------------------------------------------------------------------------- impl TransactionInput { - /// Decodes the `to` field from RLP bytes: empty means contract creation, - /// 20 bytes means a call to that address. - fn decode_to(bytes: &[u8]) -> alloy_rlp::Result> { - if bytes.is_empty() { - Ok(None) - } else { - let array = <[u8; 20]>::try_from(bytes).map_err(|_| alloy_rlp::Error::UnexpectedLength)?; - Ok(Some(Address::from(array))) - } - } - /// Derives the chain id and signature parity from a legacy `v` value. - fn decode_legacy_v(v: u64) -> alloy_rlp::Result<(Option, u64)> { - match v { - LEGACY_V_UNPROTECTED_EVEN => Ok((None, 0)), - LEGACY_V_UNPROTECTED_ODD => Ok((None, 1)), + fn decode_legacy_v(v: U64) -> Result<(Option, U64), TransactionDecodeError> { + let v_raw = v.as_u64(); + match v_raw { + LEGACY_V_UNPROTECTED_EVEN => Ok((None, U64::ZERO)), + LEGACY_V_UNPROTECTED_ODD => Ok((None, U64::ONE)), v if v >= EIP155_V_OFFSET => { let chain_id = (v - EIP155_V_OFFSET) / 2; let parity = (v - LEGACY_V_UNPROTECTED_EVEN) % 2; - Ok((Some(ChainId::from(chain_id)), parity)) + Ok((Some(ChainId::from(chain_id)), U64::from(parity))) } - _ => Err(alloy_rlp::Error::Custom("invalid legacy v value")), + _ => Err(TransactionDecodeError::InvalidLegacyV), } } @@ -441,11 +447,11 @@ impl TransactionInput { to: Option
, value: Wei, input: Bytes, - v: u64, + v: U64, r: U256, s: U256, hash: Hash, - ) -> anyhow::Result { + ) -> Result { let (chain_id, parity) = Self::decode_legacy_v(v)?; let mut tx = Self { @@ -460,94 +466,86 @@ impl TransactionInput { gas_limit, gas_price, }, - signature: Signature { v: U64::from(parity), r, s }, + signature: Signature { v: parity, r, s }, }; - let signer = tx.recover_signer_address()?; + let signer = tx.recover_signer_address().map_err(|_| TransactionDecodeError::SignerRecovery)?; tx.execution_info.signer = Signer::Recovered(signer); Ok(tx) } /// Decodes a legacy transaction from raw RLP bytes. - fn decode_legacy(raw_bytes: &[u8]) -> anyhow::Result { - let mut rlp = alloy_rlp::Rlp::new(raw_bytes)?; - - let nonce: Nonce = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("nonce"))?; - let gas_price: u128 = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("gasPrice"))?; - let gas_limit: Gas = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("gasLimit"))?; - let to_bytes: AlloyBytes = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("to"))?; - let value: Wei = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("value"))?; - let input: Bytes = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("input"))?; - let v: u64 = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("v"))?; - let r: U256 = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("r"))?; - let s: U256 = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("s"))?; - - if rlp.get_next::()?.is_some() { - return Err(TransactionDecodeError::ExtraFields.into()); + fn decode_legacy(raw_bytes: &[u8]) -> Result { + let mut rlp = alloy_rlp::Rlp::new(raw_bytes).map_err(|e| TransactionDecodeError::RlpError(e.to_string()))?; + + let nonce = decode_next::(&mut rlp, "nonce")?; + let gas_price = decode_next::(&mut rlp, "gasPrice")?; + let gas_limit = decode_next::(&mut rlp, "gasLimit")?; + let to = decode_to_field(&mut rlp)?; + let value = decode_next::(&mut rlp, "value")?; + let input = decode_next::(&mut rlp, "input")?; + let v = decode_next::(&mut rlp, "v")?; + let r = decode_next::(&mut rlp, "r")?; + let s = decode_next::(&mut rlp, "s")?; + + if rlp.get_next::().map_err(|e| TransactionDecodeError::RlpError(e.to_string()))?.is_some() { + return Err(TransactionDecodeError::ExtraFields); } - let to = Self::decode_to(&to_bytes).map_err(|_| TransactionDecodeError::InvalidTo)?; let hash = Hash::from(keccak256(raw_bytes)); - Ok(Self::build_legacy(nonce, gas_price, gas_limit, to, value, input, v, r, s, hash).map_err(|_| TransactionDecodeError::SignerRecovery)?) + Self::build_legacy(nonce, gas_price, gas_limit, to, value, input, v, r, s, hash) } - /// Decodes the common fields shared by access-list transaction types (EIP-2930, EIP-1559, EIP-4844, EIP-7702). - /// The `to` field is decoded via the provided closure because its RLP encoding varies by type. - fn decode_access_list_fields(rlp: &mut alloy_rlp::Rlp<'_>, decode_to: F) -> anyhow::Result - where - F: FnOnce(&mut alloy_rlp::Rlp<'_>) -> anyhow::Result>, - { - let gas_limit: Gas = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("gasLimit"))?; - let to = decode_to(rlp)?; - let value: Wei = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("value"))?; - let input: Bytes = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("input"))?; - let _: AccessList = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("accessList"))?; + /// Decodes the common fields shared by access-list transaction types (EIP-2930, EIP-1559, EIP-4844, and EIP-7702). + fn decode_access_list_fields(rlp: &mut alloy_rlp::Rlp<'_>) -> Result { + let gas_limit = decode_next::(rlp, "gasLimit")?; + let to = decode_to_field(rlp)?; + let value = decode_next::(rlp, "value")?; + let input = decode_next::(rlp, "input")?; + let _: AccessList = decode_next(rlp, "accessList")?; - Ok(AccessListTxFields { gas_limit, to, value, input }) + Ok(TypedTxCommonFields { gas_limit, to, value, input }) } /// Decodes the dynamic-fee gas price fields (`maxPriorityFeePerGas` and `maxFeePerGas`) /// and returns `maxFeePerGas` as the effective gas price. - fn decode_dynamic_fee_gas_price(rlp: &mut alloy_rlp::Rlp<'_>) -> anyhow::Result { - let max_priority_fee_per_gas: u128 = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("maxPriorityFeePerGas"))?; - let max_fee_per_gas: u128 = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("maxFeePerGas"))?; + fn decode_dynamic_fee_gas_price(rlp: &mut alloy_rlp::Rlp<'_>) -> Result { + let max_priority_fee_per_gas = decode_next::(rlp, "maxPriorityFeePerGas")?; + let max_fee_per_gas = decode_next::(rlp, "maxFeePerGas")?; let _ = max_priority_fee_per_gas; Ok(max_fee_per_gas) } /// Decodes the transaction signature fields (`v`, `r`, `s`). - fn decode_signature(rlp: &mut alloy_rlp::Rlp<'_>) -> anyhow::Result<(u64, U256, U256)> { - let v = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("v"))?; - let r = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("r"))?; - let s = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("s"))?; + fn decode_signature(rlp: &mut alloy_rlp::Rlp<'_>) -> Result<(U64, U256, U256), TransactionDecodeError> { + let v = decode_next::(rlp, "v")?; + let r = decode_next::(rlp, "r")?; + let s = decode_next::(rlp, "s")?; Ok((v, r, s)) } /// Decodes a typed transaction (EIP-2718) from raw bytes. - fn decode_typed(tx_type: u8, payload: &[u8], raw_bytes: &[u8]) -> anyhow::Result { + fn decode_typed(tx_type: u8, payload: &[u8], raw_bytes: &[u8]) -> Result { let tx_type = TxType::try_from(tx_type).map_err(|_| TransactionDecodeError::UnsupportedType)?; - let mut rlp = alloy_rlp::Rlp::new(payload)?; + let mut rlp = alloy_rlp::Rlp::new(payload).map_err(|e| TransactionDecodeError::RlpError(e.to_string()))?; - let chain_id: ChainId = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("chainId"))?; - let nonce: Nonce = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("nonce"))?; + let chain_id = decode_next::(&mut rlp, "chainId")?; + let nonce = decode_next::(&mut rlp, "nonce")?; let gas_price: u128; let gas_limit: Gas; let to: Option
; let value: Wei; let input: Bytes; - let v: u64; + let v: U64; let r: U256; let s: U256; match tx_type { TxType::Eip2930 => { - gas_price = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("gasPrice"))?; - let fields = Self::decode_access_list_fields(&mut rlp, |rlp| { - let to_bytes: AlloyBytes = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("to"))?; - Self::decode_to(&to_bytes).map_err(|_| TransactionDecodeError::InvalidTo.into()) - })?; + gas_price = decode_next::(&mut rlp, "gasPrice")?; + let fields = Self::decode_access_list_fields(&mut rlp)?; gas_limit = fields.gas_limit; to = fields.to; value = fields.value; @@ -557,10 +555,7 @@ impl TransactionInput { TxType::Eip1559 => { gas_price = Self::decode_dynamic_fee_gas_price(&mut rlp)?; - let fields = Self::decode_access_list_fields(&mut rlp, |rlp| { - let to_bytes: AlloyBytes = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("to"))?; - Self::decode_to(&to_bytes).map_err(|_| TransactionDecodeError::InvalidTo.into()) - })?; + let fields = Self::decode_access_list_fields(&mut rlp)?; gas_limit = fields.gas_limit; to = fields.to; value = fields.value; @@ -570,38 +565,32 @@ impl TransactionInput { TxType::Eip4844 => { gas_price = Self::decode_dynamic_fee_gas_price(&mut rlp)?; - let fields = Self::decode_access_list_fields(&mut rlp, |rlp| { - let to_addr: AlloyAddress = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("to"))?; - Ok(Some(Address::from(to_addr.0))) - })?; + let fields = Self::decode_access_list_fields(&mut rlp)?; gas_limit = fields.gas_limit; to = fields.to; value = fields.value; input = fields.input; - let _: u128 = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("maxFeePerBlobGas"))?; - let _: Vec = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("blobVersionedHashes"))?; + let _: u128 = decode_next(&mut rlp, "maxFeePerBlobGas")?; + let _: Vec = decode_next(&mut rlp, "blobVersionedHashes")?; (v, r, s) = Self::decode_signature(&mut rlp)?; } TxType::Eip7702 => { gas_price = Self::decode_dynamic_fee_gas_price(&mut rlp)?; - let fields = Self::decode_access_list_fields(&mut rlp, |rlp| { - let to_addr: AlloyAddress = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("to"))?; - Ok(Some(Address::from(to_addr.0))) - })?; + let fields = Self::decode_access_list_fields(&mut rlp)?; gas_limit = fields.gas_limit; to = fields.to; value = fields.value; input = fields.input; - let _: Vec = rlp.get_next()?.ok_or(TransactionDecodeError::MissingField("authorizationList"))?; + let _: Vec = decode_next(&mut rlp, "authorizationList")?; (v, r, s) = Self::decode_signature(&mut rlp)?; } - TxType::Legacy => return Err(TransactionDecodeError::LegacyNotTyped.into()), + TxType::Legacy => return Err(TransactionDecodeError::LegacyNotTyped), } - if rlp.get_next::()?.is_some() { - return Err(TransactionDecodeError::ExtraFields.into()); + if rlp.get_next::().map_err(|e| TransactionDecodeError::RlpError(e.to_string()))?.is_some() { + return Err(TransactionDecodeError::ExtraFields); } let hash = Hash::from(keccak256(raw_bytes)); @@ -621,7 +610,7 @@ impl TransactionInput { gas_limit, gas_price, }, - signature: Signature { v: U64::from(v), r, s }, + signature: Signature { v, r, s }, }; let signer = tx.recover_signer_address().map_err(|_| TransactionDecodeError::SignerRecovery)?; @@ -640,11 +629,11 @@ impl RlpDecodable for TransactionInput { } let tx = match raw_bytes[0] { - byte if byte >= 0xc0 => Self::decode_legacy(raw_bytes).map_err(decode_error_to_rlp)?, + byte if byte >= 0xc0 => Self::decode_legacy(raw_bytes).map_err(alloy_rlp::Error::from)?, byte if byte <= 0x7f => { let tx_type = byte; let payload = &raw_bytes[1..]; - Self::decode_typed(tx_type, payload, raw_bytes).map_err(decode_error_to_rlp)? + Self::decode_typed(tx_type, payload, raw_bytes).map_err(alloy_rlp::Error::from)? } _ => return Err(TransactionDecodeError::InvalidTypeByte.into()), }; @@ -655,14 +644,6 @@ impl RlpDecodable for TransactionInput { } } -/// Converts a transaction decode error into an `alloy_rlp::Error` for the `RlpDecodable` boundary. -fn decode_error_to_rlp(error: anyhow::Error) -> alloy_rlp::Error { - match error.downcast::() { - Ok(decode_error) => decode_error.into(), - Err(_) => alloy_rlp::Error::Custom("failed to decode transaction"), - } -} - // ----------------------------------------------------------------------------- // Conversion: Other -> Self // -----------------------------------------------------------------------------