diff --git a/src/enveloped.rs b/src/enveloped.rs index 9543f0b..d49ddb5 100644 --- a/src/enveloped.rs +++ b/src/enveloped.rs @@ -29,11 +29,25 @@ pub trait EnvelopedEncodable { out } + /// Returns the length of the encoded transaction. + /// + /// This is the EIP-2718 encoded length: type_id (1 byte for typed txs) + RLP payload. + /// Matches geth's `tx.Size()` and reth/alloy's `encoded_length()`. + fn encoded_len(&self) -> usize { + let type_id_len = if self.type_id().is_some() { 1 } else { 0 }; + type_id_len + self.payload_len() + } + /// Type Id of the transaction. fn type_id(&self) -> Option; /// Encode inner payload. fn encode_payload(&self) -> BytesMut; + + /// Returns the length of the RLP-encoded payload without the type byte. + fn payload_len(&self) -> usize { + self.encode_payload().len() + } } /// Decodable typed transactions. diff --git a/src/transaction/eip1559.rs b/src/transaction/eip1559.rs index aca4343..11f071c 100644 --- a/src/transaction/eip1559.rs +++ b/src/transaction/eip1559.rs @@ -2,6 +2,7 @@ use ethereum_types::{H256, U256}; use rlp::{DecoderError, Rlp, RlpStream}; use sha3::{Digest, Keccak256}; +use super::rlp_len::{rlp_h256_as_u256_len, rlp_list_len, RlpEncodableLen}; use crate::Bytes; pub use super::eip2930::{AccessList, TransactionAction, TransactionSignature}; @@ -52,6 +53,23 @@ impl EIP1559Transaction { access_list: self.access_list, } } + + /// Non-allocating RLP-encoded length of this signed transaction. + pub fn rlp_len(&self) -> usize { + let payload = self.chain_id.rlp_len() + + self.nonce.rlp_len() + + self.max_priority_fee_per_gas.rlp_len() + + self.max_fee_per_gas.rlp_len() + + self.gas_limit.rlp_len() + + self.action.rlp_len() + + self.value.rlp_len() + + self.input.rlp_len() + + self.access_list.rlp_len() + + self.signature.odd_y_parity().rlp_len() + + rlp_h256_as_u256_len(self.signature.r()) + + rlp_h256_as_u256_len(self.signature.s()); + rlp_list_len(payload) + } } impl rlp::Encodable for EIP1559Transaction { @@ -120,6 +138,21 @@ impl EIP1559TransactionMessage { out[1..].copy_from_slice(&encoded); H256::from_slice(Keccak256::digest(&out).as_ref()) } + + /// Returns the RLP-encoded length of this unsigned message without + /// allocating. + pub fn encoded_len(&self) -> usize { + let payload = self.chain_id.rlp_len() + + self.nonce.rlp_len() + + self.max_priority_fee_per_gas.rlp_len() + + self.max_fee_per_gas.rlp_len() + + self.gas_limit.rlp_len() + + self.action.rlp_len() + + self.value.rlp_len() + + self.input.rlp_len() + + self.access_list.rlp_len(); + rlp_list_len(payload) + } } impl rlp::Encodable for EIP1559TransactionMessage { diff --git a/src/transaction/eip2930.rs b/src/transaction/eip2930.rs index 6223ead..5851438 100644 --- a/src/transaction/eip2930.rs +++ b/src/transaction/eip2930.rs @@ -4,6 +4,7 @@ use ethereum_types::{Address, H256, U256}; use rlp::{DecoderError, Rlp, RlpStream}; use sha3::{Digest, Keccak256}; +use super::rlp_len::{rlp_h256_as_u256_len, rlp_list_len, RlpEncodableLen}; use super::signature; use crate::Bytes; @@ -94,10 +95,8 @@ impl<'de> serde::Deserialize<'de> for TransactionSignature { D: serde::de::Deserializer<'de>, { let unchecked = MalleableTransactionSignature::deserialize(deserializer)?; - Ok( - TransactionSignature::new(unchecked.odd_y_parity, unchecked.r, unchecked.s) - .ok_or(serde::de::Error::custom("invalid signature"))?, - ) + TransactionSignature::new(unchecked.odd_y_parity, unchecked.r, unchecked.s) + .ok_or(serde::de::Error::custom("invalid signature")) } } @@ -134,6 +133,25 @@ impl rlp::Decodable for AccessListItem { } } +impl AccessListItem { + /// Non-allocating RLP-encoded length. + /// + /// Layout: `RLP_LIST(address‖RLP_LIST(storage_keys…))` + /// * address (H160) is always 21 bytes (1-byte prefix + 20 data). + /// * each storage key (H256) is always 33 bytes (1-byte prefix + 32 data). + pub fn rlp_len(&self) -> usize { + let payload = self.address.rlp_len() + self.storage_keys.rlp_len(); + rlp_list_len(payload) + } +} + +impl RlpEncodableLen for [AccessListItem] { + fn rlp_len(&self) -> usize { + let items_len: usize = self.iter().map(|item| item.rlp_len()).sum(); + rlp_list_len(items_len) + } +} + pub type AccessList = Vec; #[derive(Clone, Debug, PartialEq, Eq)] @@ -180,6 +198,22 @@ impl EIP2930Transaction { access_list: self.access_list, } } + + /// Non-allocating RLP-encoded length of this signed transaction. + pub fn rlp_len(&self) -> usize { + let payload = self.chain_id.rlp_len() + + self.nonce.rlp_len() + + self.gas_price.rlp_len() + + self.gas_limit.rlp_len() + + self.action.rlp_len() + + self.value.rlp_len() + + self.input.rlp_len() + + self.access_list.rlp_len() + + self.signature.odd_y_parity().rlp_len() + + rlp_h256_as_u256_len(self.signature.r()) + + rlp_h256_as_u256_len(self.signature.s()); + rlp_list_len(payload) + } } impl rlp::Encodable for EIP2930Transaction { @@ -245,6 +279,20 @@ impl EIP2930TransactionMessage { out[1..].copy_from_slice(&encoded); H256::from_slice(Keccak256::digest(&out).as_ref()) } + + /// Returns the RLP-encoded length of this unsigned message without + /// allocating. + pub fn encoded_len(&self) -> usize { + let payload = self.chain_id.rlp_len() + + self.nonce.rlp_len() + + self.gas_price.rlp_len() + + self.gas_limit.rlp_len() + + self.action.rlp_len() + + self.value.rlp_len() + + self.input.rlp_len() + + self.access_list.rlp_len(); + rlp_list_len(payload) + } } impl rlp::Encodable for EIP2930TransactionMessage { diff --git a/src/transaction/eip7702.rs b/src/transaction/eip7702.rs index 273ccbf..8472f00 100644 --- a/src/transaction/eip7702.rs +++ b/src/transaction/eip7702.rs @@ -5,6 +5,7 @@ use k256::ecdsa::{RecoveryId, Signature, VerifyingKey}; use rlp::{DecoderError, Rlp, RlpStream}; use sha3::{Digest, Keccak256}; +use super::rlp_len::{rlp_h256_as_u256_len, rlp_list_len, RlpEncodableLen}; use crate::Bytes; pub use super::eip2930::{ @@ -161,6 +162,24 @@ impl AuthorizationListItem { Err(AuthorizationError::InvalidPublicKey) } } + + /// Non-allocating RLP-encoded length of this authorization item. + pub fn rlp_len(&self) -> usize { + let payload = self.chain_id.rlp_len() + + self.address.rlp_len() + + self.nonce.rlp_len() + + self.signature.odd_y_parity.rlp_len() + + rlp_h256_as_u256_len(&self.signature.r) + + rlp_h256_as_u256_len(&self.signature.s); + rlp_list_len(payload) + } +} + +impl RlpEncodableLen for [AuthorizationListItem] { + fn rlp_len(&self) -> usize { + let items_len: usize = self.iter().map(|item| item.rlp_len()).sum(); + rlp_list_len(items_len) + } } pub type AuthorizationList = Vec; @@ -213,6 +232,24 @@ impl EIP7702Transaction { authorization_list: self.authorization_list, } } + + /// Non-allocating RLP-encoded length of this signed transaction. + pub fn rlp_len(&self) -> usize { + let payload = self.chain_id.rlp_len() + + self.nonce.rlp_len() + + self.max_priority_fee_per_gas.rlp_len() + + self.max_fee_per_gas.rlp_len() + + self.gas_limit.rlp_len() + + self.destination.rlp_len() + + self.value.rlp_len() + + self.data.rlp_len() + + self.access_list.rlp_len() + + self.authorization_list.rlp_len() + + self.signature.odd_y_parity().rlp_len() + + rlp_h256_as_u256_len(self.signature.r()) + + rlp_h256_as_u256_len(self.signature.s()); + rlp_list_len(payload) + } } impl rlp::Encodable for EIP7702Transaction { @@ -284,6 +321,22 @@ impl EIP7702TransactionMessage { out[1..].copy_from_slice(&encoded); H256::from_slice(Keccak256::digest(&out).as_ref()) } + + /// Returns the RLP-encoded length of this unsigned message without + /// allocating. + pub fn encoded_len(&self) -> usize { + let payload = self.chain_id.rlp_len() + + self.nonce.rlp_len() + + self.max_priority_fee_per_gas.rlp_len() + + self.max_fee_per_gas.rlp_len() + + self.gas_limit.rlp_len() + + self.destination.rlp_len() + + self.value.rlp_len() + + self.data.rlp_len() + + self.access_list.rlp_len() + + self.authorization_list.rlp_len(); + rlp_list_len(payload) + } } impl rlp::Encodable for EIP7702TransactionMessage { diff --git a/src/transaction/legacy.rs b/src/transaction/legacy.rs index c37890d..eff908b 100644 --- a/src/transaction/legacy.rs +++ b/src/transaction/legacy.rs @@ -4,6 +4,7 @@ use ethereum_types::{H160, H256, U256}; use rlp::{DecoderError, Rlp, RlpStream}; use sha3::{Digest, Keccak256}; +use super::rlp_len::{rlp_h256_as_u256_len, rlp_list_len, RlpEncodableLen}; use super::signature; use crate::Bytes; @@ -48,6 +49,20 @@ impl rlp::Decodable for TransactionAction { } } +impl TransactionAction { + /// Non-allocating RLP-encoded length. + /// + /// `Call(address)` encodes as a 20-byte string (1-byte prefix + 20 data + /// bytes); `Create` encodes as the empty string (`0x80`, 1 byte). + #[inline] + pub fn rlp_len(&self) -> usize { + match self { + Self::Call(_) => 1 + 20, + Self::Create => 1, + } + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[cfg_attr( feature = "with-scale", @@ -183,10 +198,8 @@ impl<'de> serde::Deserialize<'de> for TransactionSignature { { let unchecked = TransactionSignatureUnchecked::deserialize(deserializer)?; - Ok( - TransactionSignature::new(unchecked.v, unchecked.r, unchecked.s) - .ok_or(serde::de::Error::custom("invalid signature"))?, - ) + TransactionSignature::new(unchecked.v, unchecked.r, unchecked.s) + .ok_or(serde::de::Error::custom("invalid signature")) } } @@ -227,6 +240,20 @@ impl LegacyTransaction { chain_id: self.signature.chain_id(), } } + + /// Non-allocating RLP-encoded length of this signed transaction. + pub fn rlp_len(&self) -> usize { + let payload = self.nonce.rlp_len() + + self.gas_price.rlp_len() + + self.gas_limit.rlp_len() + + self.action.rlp_len() + + self.value.rlp_len() + + self.input.rlp_len() + + self.signature.v().rlp_len() + + rlp_h256_as_u256_len(self.signature.r()) + + rlp_h256_as_u256_len(self.signature.s()); + rlp_list_len(payload) + } } impl rlp::Encodable for LegacyTransaction { @@ -283,6 +310,24 @@ impl LegacyTransactionMessage { pub fn hash(&self) -> H256 { H256::from_slice(Keccak256::digest(rlp::encode(self)).as_ref()) } + + /// Returns the RLP-encoded length of this unsigned message without + /// allocating. + pub fn encoded_len(&self) -> usize { + let common = self.nonce.rlp_len() + + self.gas_price.rlp_len() + + self.gas_limit.rlp_len() + + self.action.rlp_len() + + self.value.rlp_len() + + self.input.rlp_len(); + let payload = if let Some(chain_id) = self.chain_id { + // EIP-155: 6 common fields + chain_id + two zero-bytes + common + chain_id.rlp_len() + 1 + 1 + } else { + common + }; + rlp_list_len(payload) + } } impl rlp::Encodable for LegacyTransactionMessage { diff --git a/src/transaction/mod.rs b/src/transaction/mod.rs index 68264bc..b247be1 100644 --- a/src/transaction/mod.rs +++ b/src/transaction/mod.rs @@ -2,6 +2,7 @@ pub mod eip1559; pub mod eip2930; pub mod eip7702; pub mod legacy; +mod rlp_len; mod signature; use bytes::BytesMut; @@ -27,6 +28,9 @@ impl EnvelopedEncodable for TransactionV0 { fn encode_payload(&self) -> BytesMut { rlp::encode(self) } + fn payload_len(&self) -> usize { + self.rlp_len() + } } impl EnvelopedDecodable for TransactionV0 { @@ -77,6 +81,13 @@ impl EnvelopedEncodable for TransactionV1 { Self::EIP2930(tx) => rlp::encode(tx), } } + + fn payload_len(&self) -> usize { + match self { + Self::Legacy(tx) => tx.rlp_len(), + Self::EIP2930(tx) => tx.rlp_len(), + } + } } impl EnvelopedDecodable for TransactionV1 { @@ -154,6 +165,14 @@ impl EnvelopedEncodable for TransactionV2 { Self::EIP1559(tx) => rlp::encode(tx), } } + + fn payload_len(&self) -> usize { + match self { + Self::Legacy(tx) => tx.rlp_len(), + Self::EIP2930(tx) => tx.rlp_len(), + Self::EIP1559(tx) => tx.rlp_len(), + } + } } impl EnvelopedDecodable for TransactionV2 { @@ -261,6 +280,15 @@ impl EnvelopedEncodable for TransactionV3 { Self::EIP7702(tx) => rlp::encode(tx), } } + + fn payload_len(&self) -> usize { + match self { + Self::Legacy(tx) => tx.rlp_len(), + Self::EIP2930(tx) => tx.rlp_len(), + Self::EIP1559(tx) => tx.rlp_len(), + Self::EIP7702(tx) => tx.rlp_len(), + } + } } impl EnvelopedDecodable for TransactionV3 { @@ -346,9 +374,8 @@ mod tests { ::decode(&bytes).unwrap(); } - #[test] - fn transaction_v0() { - let tx = TransactionV0 { + fn make_legacy_tx() -> TransactionV0 { + TransactionV0 { nonce: 12.into(), gas_price: 20_000_000_000_u64.into(), gas_limit: 21000.into(), @@ -358,17 +385,11 @@ mod tests { value: U256::from(10) * 1_000_000_000 * 1_000_000_000, input: hex!("a9059cbb000000000213ed0f886efd100b67c7e4ec0a85a7d20dc971600000000000000000000015af1d78b58c4000").into(), signature: legacy::TransactionSignature::new(38, hex!("be67e0a07db67da8d446f76add590e54b6e92cb6b8f9835aeb67540579a27717").into(), hex!("2d690516512020171c1ec870f6ff45398cc8609250326be89915fb538e7bd718").into()).unwrap(), - }; - - assert_eq!( - tx, - ::decode(&tx.encode()).unwrap() - ); + } } - #[test] - fn transaction_v1() { - let tx = TransactionV1::EIP2930(EIP2930Transaction { + fn make_eip2930_tx() -> EIP2930Transaction { + EIP2930Transaction { chain_id: 5, nonce: 7.into(), gas_price: 30_000_000_000_u64.into(), @@ -399,17 +420,11 @@ mod tests { hex!("5edcc541b4741c5cc6dd347c5ed9577ef293a62787b4510465fadbfe39ee4094").into(), ) .unwrap(), - }); - - assert_eq!( - tx, - ::decode(&tx.encode()).unwrap() - ); + } } - #[test] - fn transaction_v2() { - let tx = TransactionV2::EIP1559(EIP1559Transaction { + fn make_eip1559_tx() -> EIP1559Transaction { + EIP1559Transaction { chain_id: 5, nonce: 7.into(), max_priority_fee_per_gas: 10_000_000_000_u64.into(), @@ -441,17 +456,11 @@ mod tests { hex!("5edcc541b4741c5cc6dd347c5ed9577ef293a62787b4510465fadbfe39ee4094").into(), ) .unwrap(), - }); - - assert_eq!( - tx, - ::decode(&tx.encode()).unwrap() - ); + } } - #[test] - fn transaction_v3() { - let tx = TransactionV3::EIP7702(EIP7702Transaction { + fn make_eip7702_tx() -> EIP7702Transaction { + EIP7702Transaction { chain_id: 5, nonce: 7.into(), max_priority_fee_per_gas: 10_000_000_000_u64.into(), @@ -487,11 +496,282 @@ mod tests { hex!("5edcc541b4741c5cc6dd347c5ed9577ef293a62787b4510465fadbfe39ee4094").into(), ) .unwrap(), - }); + } + } + + #[test] + fn transaction_v0() { + let tx = make_legacy_tx(); + + assert_eq!( + tx, + ::decode(&tx.encode()).unwrap() + ); + } + + #[test] + fn transaction_v1() { + let tx = TransactionV1::EIP2930(make_eip2930_tx()); + + assert_eq!( + tx, + ::decode(&tx.encode()).unwrap() + ); + } + + #[test] + fn transaction_v2() { + let tx = TransactionV2::EIP1559(make_eip1559_tx()); + + assert_eq!( + tx, + ::decode(&tx.encode()).unwrap() + ); + } + + #[test] + fn transaction_v3() { + let tx = TransactionV3::EIP7702(make_eip7702_tx()); assert_eq!( tx, ::decode(&tx.encode()).unwrap() ); } + + #[test] + fn encoded_len_matches_encode_for_legacy() { + let tx = make_legacy_tx(); + assert_eq!(tx.encoded_len(), tx.encode().len()); + } + + #[test] + fn encoded_len_matches_encode_for_eip2930() { + let tx = TransactionV1::EIP2930(make_eip2930_tx()); + assert_eq!(tx.encoded_len(), tx.encode().len()); + } + + #[test] + fn encoded_len_matches_encode_for_eip1559() { + let tx = TransactionV2::EIP1559(make_eip1559_tx()); + assert_eq!(tx.encoded_len(), tx.encode().len()); + } + + #[test] + fn encoded_len_matches_encode_for_eip7702() { + let tx = TransactionV3::EIP7702(make_eip7702_tx()); + assert_eq!(tx.encoded_len(), tx.encode().len()); + } + + #[test] + fn payload_len_equals_encoded_len_for_legacy() { + let tx = make_legacy_tx(); + // Legacy has no type byte, so encoded_len == payload_len + assert_eq!(tx.type_id(), None); + assert_eq!(tx.encoded_len(), tx.payload_len()); + } + + #[test] + fn payload_len_plus_type_byte_equals_encoded_len_for_typed_txs() { + let eip2930 = TransactionV1::EIP2930(make_eip2930_tx()); + assert_eq!(eip2930.encoded_len(), 1 + eip2930.payload_len()); + + let eip1559 = TransactionV2::EIP1559(make_eip1559_tx()); + assert_eq!(eip1559.encoded_len(), 1 + eip1559.payload_len()); + + let eip7702 = TransactionV3::EIP7702(make_eip7702_tx()); + assert_eq!(eip7702.encoded_len(), 1 + eip7702.payload_len()); + } + + #[test] + fn transaction_message_encoded_len() { + let legacy_msg = make_legacy_tx().to_message(); + assert_eq!(legacy_msg.encoded_len(), rlp::encode(&legacy_msg).len()); + + let eip2930_msg = make_eip2930_tx().to_message(); + assert_eq!(eip2930_msg.encoded_len(), rlp::encode(&eip2930_msg).len()); + + let eip1559_msg = make_eip1559_tx().to_message(); + assert_eq!(eip1559_msg.encoded_len(), rlp::encode(&eip1559_msg).len()); + + let eip7702_msg = make_eip7702_tx().to_message(); + assert_eq!(eip7702_msg.encoded_len(), rlp::encode(&eip7702_msg).len()); + } + + #[test] + fn payload_len_matches_encode_payload() { + let legacy = make_legacy_tx(); + assert_eq!(legacy.payload_len(), legacy.encode_payload().len()); + + let eip2930 = TransactionV1::EIP2930(make_eip2930_tx()); + assert_eq!(eip2930.payload_len(), eip2930.encode_payload().len()); + + let eip1559 = TransactionV2::EIP1559(make_eip1559_tx()); + assert_eq!(eip1559.payload_len(), eip1559.encode_payload().len()); + + let eip7702 = TransactionV3::EIP7702(make_eip7702_tx()); + assert_eq!(eip7702.payload_len(), eip7702.encode_payload().len()); + } + + #[test] + fn encoded_len_grows_with_larger_input() { + let mut tx = make_legacy_tx(); + let small_len = tx.encoded_len(); + + tx.input = vec![0xab; 1024].into(); + let large_len = tx.encoded_len(); + assert!( + large_len > small_len, + "larger input should increase encoded_len" + ); + assert_eq!(large_len, tx.encode().len()); + } + + #[test] + fn encoded_len_grows_with_larger_access_list() { + let mut tx = make_eip1559_tx(); + let small_len = TransactionV2::EIP1559(tx.clone()).encoded_len(); + + tx.access_list.extend((0..10).map(|i| AccessListItem { + address: hex!("de0b295669a9fd93d5f28d9ec85e40f4cb697bae").into(), + storage_keys: vec![ + ethereum_types::H256::from_low_u64_be(i), + ethereum_types::H256::from_low_u64_be(i + 100), + ], + })); + let large = TransactionV2::EIP1559(tx); + let large_len = large.encoded_len(); + assert!( + large_len > small_len, + "larger access_list should increase encoded_len" + ); + assert_eq!(large_len, large.encode().len()); + } + + #[test] + fn encoded_len_grows_with_larger_authorization_list() { + let mut tx = make_eip7702_tx(); + let small_len = TransactionV3::EIP7702(tx.clone()).encoded_len(); + + tx.authorization_list + .extend((0..5).map(|i| { + AuthorizationListItem { + chain_id: 5, + address: hex!("de0b295669a9fd93d5f28d9ec85e40f4cb697bae").into(), + nonce: (i + 10).into(), + signature: eip2930::MalleableTransactionSignature { + odd_y_parity: false, + r: hex!("36b241b061a36a32ab7fe86c7aa9eb592dd59018cd0443adc0903590c16b02b0") + .into(), + s: hex!("5edcc541b4741c5cc6dd347c5ed9577ef293a62787b4510465fadbfe39ee4094") + .into(), + }, + } + })); + let large = TransactionV3::EIP7702(tx); + let large_len = large.encoded_len(); + assert!( + large_len > small_len, + "larger authorization_list should increase encoded_len" + ); + assert_eq!(large_len, large.encode().len()); + } + + #[test] + fn large_payload_encoded_len_matches() { + // ~128KB data field, matching reth's DEFAULT_MAX_TX_INPUT_BYTES + let large_data: Vec = vec![0xff; 128 * 1024]; + + let mut legacy = make_legacy_tx(); + legacy.input = large_data.clone().into(); + assert_eq!(legacy.encoded_len(), legacy.encode().len()); + assert_eq!(legacy.payload_len(), legacy.encode_payload().len()); + + let mut eip1559 = make_eip1559_tx(); + eip1559.input = large_data.clone().into(); + let eip1559_v2 = TransactionV2::EIP1559(eip1559); + assert_eq!(eip1559_v2.encoded_len(), eip1559_v2.encode().len()); + assert_eq!(eip1559_v2.payload_len(), eip1559_v2.encode_payload().len()); + + let mut eip7702 = make_eip7702_tx(); + eip7702.data = large_data.into(); + let eip7702_v3 = TransactionV3::EIP7702(eip7702); + assert_eq!(eip7702_v3.encoded_len(), eip7702_v3.encode().len()); + assert_eq!(eip7702_v3.payload_len(), eip7702_v3.encode_payload().len()); + } + + #[test] + fn encoded_len_is_nonzero() { + assert!(make_legacy_tx().encoded_len() > 0); + assert!(TransactionV1::EIP2930(make_eip2930_tx()).encoded_len() > 0); + assert!(TransactionV2::EIP1559(make_eip1559_tx()).encoded_len() > 0); + assert!(TransactionV3::EIP7702(make_eip7702_tx()).encoded_len() > 0); + } + + #[test] + fn v3_encoded_len_for_all_variants() { + let legacy = TransactionV3::Legacy(make_legacy_tx()); + assert_eq!(legacy.encoded_len(), legacy.encode().len()); + assert_eq!(legacy.type_id(), None); + + let eip2930 = TransactionV3::EIP2930(make_eip2930_tx()); + assert_eq!(eip2930.encoded_len(), eip2930.encode().len()); + assert_eq!(eip2930.type_id(), Some(1)); + + let eip1559 = TransactionV3::EIP1559(make_eip1559_tx()); + assert_eq!(eip1559.encoded_len(), eip1559.encode().len()); + assert_eq!(eip1559.type_id(), Some(2)); + + let eip7702 = TransactionV3::EIP7702(make_eip7702_tx()); + assert_eq!(eip7702.encoded_len(), eip7702.encode().len()); + assert_eq!(eip7702.type_id(), Some(4)); + } + + #[test] + fn roundtrip_preserves_encoded_len() { + // Legacy + let legacy = make_legacy_tx(); + let encoded = legacy.encode(); + let decoded = ::decode(&encoded).unwrap(); + assert_eq!(decoded.encoded_len(), encoded.len()); + + // EIP-2930 + let eip2930 = TransactionV1::EIP2930(make_eip2930_tx()); + let encoded = eip2930.encode(); + let decoded = ::decode(&encoded).unwrap(); + assert_eq!(decoded.encoded_len(), encoded.len()); + + // EIP-1559 + let eip1559 = TransactionV2::EIP1559(make_eip1559_tx()); + let encoded = eip1559.encode(); + let decoded = ::decode(&encoded).unwrap(); + assert_eq!(decoded.encoded_len(), encoded.len()); + + // EIP-7702 + let eip7702 = TransactionV3::EIP7702(make_eip7702_tx()); + let encoded = eip7702.encode(); + let decoded = ::decode(&encoded).unwrap(); + assert_eq!(decoded.encoded_len(), encoded.len()); + } + + #[test] + fn message_encoded_len_less_than_signed_tx() { + // Unsigned messages should be smaller than signed transactions + // because they lack signature fields (v, r, s). + let legacy = make_legacy_tx(); + let legacy_signed_len = legacy.encoded_len(); + assert!(legacy.to_message().encoded_len() < legacy_signed_len); + + let eip2930 = make_eip2930_tx(); + let eip2930_signed_len = rlp::encode(&eip2930).len(); + assert!(eip2930.to_message().encoded_len() < eip2930_signed_len); + + let eip1559 = make_eip1559_tx(); + let eip1559_signed_len = rlp::encode(&eip1559).len(); + assert!(eip1559.to_message().encoded_len() < eip1559_signed_len); + + let eip7702 = make_eip7702_tx(); + let eip7702_signed_len = rlp::encode(&eip7702).len(); + assert!(eip7702.to_message().encoded_len() < eip7702_signed_len); + } } diff --git a/src/transaction/rlp_len.rs b/src/transaction/rlp_len.rs new file mode 100644 index 0000000..9ac8681 --- /dev/null +++ b/src/transaction/rlp_len.rs @@ -0,0 +1,391 @@ +//! Non-allocating RLP length computation helpers. +//! +//! Mirrors alloy-rlp's `Encodable::length()` via an extension trait +//! [`RlpEncodableLen`]. Since parity's `rlp::Encodable` does not carry a +//! `length()` method, we bolt one on for every type that appears inside an +//! Ethereum transaction. + +use ethereum_types::{H160, H256, U256}; + +// --------------------------------------------------------------------------- +// Extension trait +// --------------------------------------------------------------------------- + +/// Non-allocating RLP-encoded length, analogous to +/// `alloy_rlp::Encodable::length()`. +pub(crate) trait RlpEncodableLen { + /// Exact number of bytes this value occupies when RLP-encoded. + fn rlp_len(&self) -> usize; +} + +// --------------------------------------------------------------------------- +// Standalone helpers (list framing, special encodings) +// --------------------------------------------------------------------------- + +/// Number of bytes required to RLP-encode a length value (used in long +/// string / list headers). +/// +/// Returns the minimal number of big-endian bytes needed to represent `len`. +/// Callers only invoke this for `len > 55`, so `len` is never zero here; +/// the guard is kept for defensive correctness. +#[inline] +const fn length_of_length(len: usize) -> usize { + if len == 0 { + return 1; + } + // Minimal big-endian bytes needed to represent `len`. + (usize::BITS as usize / 8) - (len.leading_zeros() as usize / 8) +} + +/// Total RLP-encoded length of a *list* whose item payloads occupy +/// `payload_len` bytes in total. +#[inline] +pub(crate) fn rlp_list_len(payload_len: usize) -> usize { + rlp_list_header_len(payload_len) + payload_len +} + +/// Length of just the RLP list header for a given payload size. +#[inline] +pub(crate) fn rlp_list_header_len(payload_len: usize) -> usize { + if payload_len <= 55 { + 1 + } else { + 1 + length_of_length(payload_len) + } +} + +/// RLP-encoded length of an [`H256`] that is serialised as a big-endian +/// [`U256`] (leading zero bytes stripped). +/// +/// This is the encoding used for ECDSA signature `r` and `s` components, +/// which are stored as `H256` but RLP-encoded via +/// `U256::from_big_endian(…)`. +#[inline] +pub(crate) fn rlp_h256_as_u256_len(h: &H256) -> usize { + let bytes = h.as_bytes(); + let start = bytes.iter().position(|b| *b != 0).unwrap_or(32); + let trimmed_len = 32 - start; + + if trimmed_len == 0 || (trimmed_len == 1 && bytes[start] < 0x80) { + 1 // zero (0x80) or single byte < 128 (encodes directly) + } else { + 1 + trimmed_len + } +} + +// --------------------------------------------------------------------------- +// Trait impls — primitive / foreign types +// --------------------------------------------------------------------------- + +impl RlpEncodableLen for U256 { + /// `U256` is serialised as a big-endian byte string with leading zeros + /// stripped, so the encoded size depends on the numeric magnitude. + #[inline] + fn rlp_len(&self) -> usize { + if self.is_zero() { + return 1; // 0x80 (empty string) + } + let byte_len = self.bits().div_ceil(8); + // A single byte < 0x80 is its own RLP encoding (no prefix). + if byte_len == 1 && self.low_u64() < 0x80 { + 1 + } else { + 1 + byte_len // length-prefix byte + data + } + } +} + +impl RlpEncodableLen for u64 { + #[inline] + fn rlp_len(&self) -> usize { + if *self == 0 { + return 1; // 0x80 + } + let byte_len = 8 - (*self).leading_zeros() as usize / 8; + if byte_len == 1 && *self < 0x80 { + 1 + } else { + 1 + byte_len + } + } +} + +impl RlpEncodableLen for bool { + /// `bool` is encoded as `u8` (`false → 0x80`, `true → 0x01`); always 1 + /// byte. + #[inline] + fn rlp_len(&self) -> usize { + 1 + } +} + +impl RlpEncodableLen for [u8] { + /// Byte-string encoding. + #[inline] + fn rlp_len(&self) -> usize { + match self.len() { + 0 => 1, + 1 if self[0] < 0x80 => 1, + len @ 1..=55 => 1 + len, + len => 1 + length_of_length(len) + len, + } + } +} + +impl RlpEncodableLen for H160 { + /// Fixed 20-byte hash → always `1 + 20 = 21` bytes. + #[inline] + fn rlp_len(&self) -> usize { + 1 + 20 + } +} + +impl RlpEncodableLen for H256 { + /// Fixed 32-byte hash → always `1 + 32 = 33` bytes. + #[inline] + fn rlp_len(&self) -> usize { + 1 + 32 + } +} + +impl RlpEncodableLen for [H256] { + /// RLP list of fixed 32-byte hashes (e.g. storage keys). + #[inline] + fn rlp_len(&self) -> usize { + rlp_list_len(self.len() * 33) + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use ethereum_types::{H256, U256}; + + // --- primitive sanity checks --- + + #[test] + fn u256_len_zero() { + assert_eq!(U256::zero().rlp_len(), 1); + assert_eq!(rlp::encode(&U256::zero()).len(), 1); + } + + #[test] + fn u256_len_one() { + let v = U256::from(1); + assert_eq!(v.rlp_len(), rlp::encode(&v).len()); + } + + #[test] + fn u256_len_boundary_0x7f() { + let v = U256::from(0x7fu64); + assert_eq!(v.rlp_len(), 1); + assert_eq!(rlp::encode(&v).len(), 1); + } + + #[test] + fn u256_len_boundary_0x80() { + let v = U256::from(0x80u64); + assert_eq!(v.rlp_len(), 2); + assert_eq!(rlp::encode(&v).len(), 2); + } + + #[test] + fn u256_len_large() { + let v = U256::MAX; + assert_eq!(v.rlp_len(), rlp::encode(&v).len()); + } + + #[test] + fn u64_len_zero() { + assert_eq!(0u64.rlp_len(), 1); + assert_eq!(rlp::encode(&0u64).len(), 1); + } + + #[test] + fn u64_len_small() { + for v in [1u64, 0x7f, 0x80, 0xff, 0x100, u64::MAX] { + assert_eq!(v.rlp_len(), rlp::encode(&v).len(), "mismatch for {v}"); + } + } + + #[test] + fn h256_as_u256_len_zero() { + assert_eq!(rlp_h256_as_u256_len(&H256::zero()), 1); + } + + #[test] + fn h256_as_u256_len_matches_u256() { + let h = H256::from_slice(&hex_literal::hex!( + "36b241b061a36a32ab7fe86c7aa9eb592dd59018cd0443adc0903590c16b02b0" + )); + let u = U256::from_big_endian(h.as_bytes()); + assert_eq!(rlp_h256_as_u256_len(&h), rlp::encode(&u).len()); + } + + #[test] + fn bytes_len_empty() { + let v: Vec = vec![]; + assert_eq!(v.rlp_len(), rlp::encode(&v).len()); + } + + #[test] + fn bytes_len_single_low() { + let v = vec![0x42u8]; + assert_eq!(v.rlp_len(), 1); + assert_eq!(rlp::encode(&v).len(), 1); + } + + #[test] + fn bytes_len_single_high() { + let v = vec![0x80u8]; + assert_eq!(v.rlp_len(), 2); + assert_eq!(rlp::encode(&v).len(), 2); + } + + #[test] + fn bytes_len_short() { + let v = vec![0xab; 55]; + assert_eq!(v.rlp_len(), rlp::encode(&v).len()); + } + + #[test] + fn bytes_len_long() { + let v = vec![0xab; 56]; + assert_eq!(v.rlp_len(), rlp::encode(&v).len()); + } + + #[test] + fn bytes_len_very_long() { + let v = vec![0xff; 128 * 1024]; + assert_eq!(v.rlp_len(), rlp::encode(&v).len()); + } + + #[test] + fn list_len_empty() { + // Empty list encodes as 0xc0 → 1 byte + assert_eq!(rlp_list_len(0), 1); + } + + #[test] + fn list_len_short() { + assert_eq!(rlp_list_len(55), 1 + 55); + } + + #[test] + fn list_len_long() { + // 56-byte payload needs 1-byte length → header = 2 bytes + assert_eq!(rlp_list_len(56), 2 + 56); + } + + // --- length_of_length boundary tests --- + + #[test] + fn length_of_length_boundaries() { + // 1-byte range: 1..=0xFF + assert_eq!(length_of_length(1), 1); + assert_eq!(length_of_length(55), 1); + assert_eq!(length_of_length(0xFF), 1); + + // 2-byte range: 0x100..=0xFFFF + assert_eq!(length_of_length(0x100), 2); + assert_eq!(length_of_length(0xFFFF), 2); + + // 3-byte range: 0x1_0000..=0xFF_FFFF + assert_eq!(length_of_length(0x1_0000), 3); + assert_eq!(length_of_length(0xFF_FFFF), 3); + + // 4-byte range: 0x100_0000..=0xFFFF_FFFF + assert_eq!(length_of_length(0x100_0000), 4); + assert_eq!(length_of_length(0xFFFF_FFFF_usize), 4); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn length_of_length_above_4gib() { + // 5-byte range: 0x1_0000_0000..=0xFF_FFFF_FFFF + assert_eq!(length_of_length(0x1_0000_0000_usize), 5); + assert_eq!(length_of_length(0xFF_FFFF_FFFF_usize), 5); + + // 6-byte range + assert_eq!(length_of_length(0x100_0000_0000_usize), 6); + assert_eq!(length_of_length(0xFFFF_FFFF_FFFF_usize), 6); + + // 7-byte range + assert_eq!(length_of_length(0x1_0000_0000_0000_usize), 7); + assert_eq!(length_of_length(0xFF_FFFF_FFFF_FFFF_usize), 7); + + // 8-byte range + assert_eq!(length_of_length(0x100_0000_0000_0000_usize), 8); + assert_eq!(length_of_length(usize::MAX), 8); + } + + // --- rlp_list_header_len boundary tests --- + + #[test] + fn list_header_len_boundaries() { + // Short list: payload ≤ 55 → header is 1 byte. + assert_eq!(rlp_list_header_len(0), 1); + assert_eq!(rlp_list_header_len(55), 1); + + // Long list, 1-byte length: payload 56..=0xFF → header is 2 bytes. + assert_eq!(rlp_list_header_len(56), 2); + assert_eq!(rlp_list_header_len(0xFF), 2); + + // Long list, 2-byte length: payload 0x100..=0xFFFF → header is 3 bytes. + assert_eq!(rlp_list_header_len(0x100), 3); + assert_eq!(rlp_list_header_len(0xFFFF), 3); + + // Long list, 3-byte length → header is 4 bytes. + assert_eq!(rlp_list_header_len(0x1_0000), 4); + assert_eq!(rlp_list_header_len(0xFF_FFFF), 4); + + // Long list, 4-byte length → header is 5 bytes. + assert_eq!(rlp_list_header_len(0x100_0000), 5); + assert_eq!(rlp_list_header_len(0xFFFF_FFFF_usize), 5); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn list_header_len_above_4gib() { + // 5-byte length → header is 6 bytes. + assert_eq!(rlp_list_header_len(0x1_0000_0000_usize), 6); + assert_eq!(rlp_list_header_len(0xFF_FFFF_FFFF_usize), 6); + + // 8-byte length → header is 9 bytes. + assert_eq!(rlp_list_header_len(usize::MAX), 9); + } + + // --- byte-string rlp_len boundary tests --- + + #[test] + fn bytes_len_boundary_0xff() { + let v = vec![0xab; 0xFF]; + assert_eq!(v.as_slice().rlp_len(), 2 + 0xFF); // 1 prefix + 1 length byte + data + assert_eq!(v.as_slice().rlp_len(), rlp::encode(&v).len()); + } + + #[test] + fn bytes_len_boundary_0x100() { + let v = vec![0xab; 0x100]; + assert_eq!(v.as_slice().rlp_len(), 3 + 0x100); // 1 prefix + 2 length bytes + data + assert_eq!(v.as_slice().rlp_len(), rlp::encode(&v).len()); + } + + #[test] + fn bytes_len_boundary_0xffff() { + let v = vec![0xab; 0xFFFF]; + assert_eq!(v.as_slice().rlp_len(), 3 + 0xFFFF); // 1 prefix + 2 length bytes + data + assert_eq!(v.as_slice().rlp_len(), rlp::encode(&v).len()); + } + + #[test] + fn bytes_len_boundary_0x1_0000() { + let v = vec![0xab; 0x1_0000]; + assert_eq!(v.as_slice().rlp_len(), 4 + 0x1_0000); // 1 prefix + 3 length bytes + data + assert_eq!(v.as_slice().rlp_len(), rlp::encode(&v).len()); + } +}