From dad71b91bbd52dc22f5a379d2158416fd13f51fa Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Mon, 26 Jan 2026 19:17:20 +0200 Subject: [PATCH 1/6] feat: :sparkles: add encoded length methods to transactions --- src/enveloped.rs | 14 ++++++++++++++ src/transaction/eip1559.rs | 5 +++++ src/transaction/eip2930.rs | 5 +++++ src/transaction/eip7702.rs | 5 +++++ src/transaction/legacy.rs | 5 +++++ 5 files changed, 34 insertions(+) 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 2e009c6..e7d79d1 100644 --- a/src/transaction/eip1559.rs +++ b/src/transaction/eip1559.rs @@ -120,6 +120,11 @@ impl EIP1559TransactionMessage { out[1..].copy_from_slice(&encoded); H256::from_slice(Keccak256::digest(&out).as_slice()) } + + /// Returns the RLP-encoded length of this unsigned message. + pub fn encoded_len(&self) -> usize { + rlp::encode(self).len() + } } impl rlp::Encodable for EIP1559TransactionMessage { diff --git a/src/transaction/eip2930.rs b/src/transaction/eip2930.rs index d23162b..d87a613 100644 --- a/src/transaction/eip2930.rs +++ b/src/transaction/eip2930.rs @@ -260,6 +260,11 @@ impl EIP2930TransactionMessage { out[1..].copy_from_slice(&encoded); H256::from_slice(Keccak256::digest(&out).as_slice()) } + + /// Returns the RLP-encoded length of this unsigned message. + pub fn encoded_len(&self) -> usize { + rlp::encode(self).len() + } } impl rlp::Encodable for EIP2930TransactionMessage { diff --git a/src/transaction/eip7702.rs b/src/transaction/eip7702.rs index ab13b26..3284802 100644 --- a/src/transaction/eip7702.rs +++ b/src/transaction/eip7702.rs @@ -284,6 +284,11 @@ impl EIP7702TransactionMessage { out[1..].copy_from_slice(&encoded); H256::from_slice(Keccak256::digest(&out).as_slice()) } + + /// Returns the RLP-encoded length of this unsigned message. + pub fn encoded_len(&self) -> usize { + rlp::encode(self).len() + } } impl rlp::Encodable for EIP7702TransactionMessage { diff --git a/src/transaction/legacy.rs b/src/transaction/legacy.rs index 6ed5704..8080474 100644 --- a/src/transaction/legacy.rs +++ b/src/transaction/legacy.rs @@ -297,6 +297,11 @@ impl LegacyTransactionMessage { pub fn hash(&self) -> H256 { H256::from_slice(Keccak256::digest(rlp::encode(self)).as_slice()) } + + /// Returns the RLP-encoded length of this unsigned message. + pub fn encoded_len(&self) -> usize { + rlp::encode(self).len() + } } impl rlp::Encodable for LegacyTransactionMessage { From 6bc420fc8fbb7683b0a49d1511070b70cec99a60 Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Wed, 25 Feb 2026 15:54:56 +0200 Subject: [PATCH 2/6] refactor: :rotating_light: clippy --- src/block.rs | 2 +- src/header.rs | 2 +- src/transaction/eip1559.rs | 4 ++-- src/transaction/eip2930.rs | 4 ++-- src/transaction/eip7702.rs | 8 ++++---- src/transaction/legacy.rs | 4 ++-- src/util.rs | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/block.rs b/src/block.rs index 031468d..8e76d1f 100644 --- a/src/block.rs +++ b/src/block.rs @@ -58,7 +58,7 @@ impl rlp::Decodable for Block { impl Block { pub fn new(partial_header: PartialHeader, transactions: Vec, ommers: Vec
) -> Self { let ommers_hash = - H256::from_slice(Keccak256::digest(&rlp::encode_list(&ommers)[..]).as_slice()); + H256::from_slice(Keccak256::digest(&rlp::encode_list(&ommers)[..]).as_ref()); let transactions_root = ordered_trie_root( transactions .iter() diff --git a/src/header.rs b/src/header.rs index a684f85..302e048 100644 --- a/src/header.rs +++ b/src/header.rs @@ -53,7 +53,7 @@ impl Header { #[must_use] pub fn hash(&self) -> H256 { - H256::from_slice(Keccak256::digest(rlp::encode(self)).as_slice()) + H256::from_slice(Keccak256::digest(rlp::encode(self)).as_ref()) } } diff --git a/src/transaction/eip1559.rs b/src/transaction/eip1559.rs index e7d79d1..0762bc6 100644 --- a/src/transaction/eip1559.rs +++ b/src/transaction/eip1559.rs @@ -36,7 +36,7 @@ impl EIP1559Transaction { let mut out = alloc::vec![0; 1 + encoded.len()]; out[0] = 2; out[1..].copy_from_slice(&encoded); - H256::from_slice(Keccak256::digest(&out).as_slice()) + H256::from_slice(Keccak256::digest(&out).as_ref()) } pub fn to_message(self) -> EIP1559TransactionMessage { @@ -118,7 +118,7 @@ impl EIP1559TransactionMessage { let mut out = alloc::vec![0; 1 + encoded.len()]; out[0] = 2; out[1..].copy_from_slice(&encoded); - H256::from_slice(Keccak256::digest(&out).as_slice()) + H256::from_slice(Keccak256::digest(&out).as_ref()) } /// Returns the RLP-encoded length of this unsigned message. diff --git a/src/transaction/eip2930.rs b/src/transaction/eip2930.rs index d87a613..d17930a 100644 --- a/src/transaction/eip2930.rs +++ b/src/transaction/eip2930.rs @@ -180,7 +180,7 @@ impl EIP2930Transaction { let mut out = alloc::vec![0; 1 + encoded.len()]; out[0] = 1; out[1..].copy_from_slice(&encoded); - H256::from_slice(Keccak256::digest(&out).as_slice()) + H256::from_slice(Keccak256::digest(&out).as_ref()) } pub fn to_message(self) -> EIP2930TransactionMessage { @@ -258,7 +258,7 @@ impl EIP2930TransactionMessage { let mut out = alloc::vec![0; 1 + encoded.len()]; out[0] = 1; out[1..].copy_from_slice(&encoded); - H256::from_slice(Keccak256::digest(&out).as_slice()) + H256::from_slice(Keccak256::digest(&out).as_ref()) } /// Returns the RLP-encoded length of this unsigned message. diff --git a/src/transaction/eip7702.rs b/src/transaction/eip7702.rs index 3284802..1605afb 100644 --- a/src/transaction/eip7702.rs +++ b/src/transaction/eip7702.rs @@ -139,7 +139,7 @@ impl AuthorizationListItem { message.extend_from_slice(&rlp_stream.out()); // Return keccak256 hash of the complete message - H256::from_slice(Keccak256::digest(&message).as_slice()) + H256::from_slice(Keccak256::digest(&message).as_ref()) } /// Convert VerifyingKey to Ethereum address @@ -196,7 +196,7 @@ impl EIP7702Transaction { let mut out = alloc::vec![0; 1 + encoded.len()]; out[0] = SET_CODE_TX_TYPE; out[1..].copy_from_slice(&encoded); - H256::from_slice(Keccak256::digest(&out).as_slice()) + H256::from_slice(Keccak256::digest(&out).as_ref()) } pub fn to_message(self) -> EIP7702TransactionMessage { @@ -282,7 +282,7 @@ impl EIP7702TransactionMessage { let mut out = alloc::vec![0; 1 + encoded.len()]; out[0] = SET_CODE_TX_TYPE; out[1..].copy_from_slice(&encoded); - H256::from_slice(Keccak256::digest(&out).as_slice()) + H256::from_slice(Keccak256::digest(&out).as_ref()) } /// Returns the RLP-encoded length of this unsigned message. @@ -347,7 +347,7 @@ mod tests { rlp_stream.append(&address); rlp_stream.append(&nonce); message.extend_from_slice(&rlp_stream.out()); - let message_hash = H256::from_slice(Keccak256::digest(&message).as_slice()); + let message_hash = H256::from_slice(Keccak256::digest(&message).as_ref()); // Sign the message hash let (signature, recovery_id) = signing_key diff --git a/src/transaction/legacy.rs b/src/transaction/legacy.rs index 8080474..ac6be38 100644 --- a/src/transaction/legacy.rs +++ b/src/transaction/legacy.rs @@ -227,7 +227,7 @@ pub struct LegacyTransaction { impl LegacyTransaction { pub fn hash(&self) -> H256 { - H256::from_slice(Keccak256::digest(rlp::encode(self)).as_slice()) + H256::from_slice(Keccak256::digest(rlp::encode(self)).as_ref()) } pub fn to_message(self) -> LegacyTransactionMessage { @@ -295,7 +295,7 @@ pub struct LegacyTransactionMessage { impl LegacyTransactionMessage { pub fn hash(&self) -> H256 { - H256::from_slice(Keccak256::digest(rlp::encode(self)).as_slice()) + H256::from_slice(Keccak256::digest(rlp::encode(self)).as_ref()) } /// Returns the RLP-encoded length of this unsigned message. diff --git a/src/util.rs b/src/util.rs index fa33ae5..398fb17 100644 --- a/src/util.rs +++ b/src/util.rs @@ -17,7 +17,7 @@ impl Hasher for KeccakHasher { const LENGTH: usize = 32; fn hash(x: &[u8]) -> Self::Out { - H256::from_slice(Keccak256::digest(x).as_slice()) + H256::from_slice(Keccak256::digest(x).as_ref()) } } @@ -178,7 +178,7 @@ mod tests { const LENGTH: usize = 32; fn hash(x: &[u8]) -> Self::Out { - H256::from_slice(Keccak256::digest(x).as_slice()) + H256::from_slice(Keccak256::digest(x).as_ref()) } } From 0d9c2e4c8183cf8c15e82d0e6711877c1102f4a8 Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Wed, 25 Feb 2026 17:41:05 +0200 Subject: [PATCH 3/6] test: :white_check_mark: add unit tests --- src/transaction/mod.rs | 314 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 283 insertions(+), 31 deletions(-) diff --git a/src/transaction/mod.rs b/src/transaction/mod.rs index 64f0a39..59da3e1 100644 --- a/src/transaction/mod.rs +++ b/src/transaction/mod.rs @@ -345,9 +345,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(), @@ -357,17 +356,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(), @@ -398,17 +391,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(), @@ -440,17 +427,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(), @@ -486,11 +467,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); + } } From ee70e836ccb374670686d65e65a665ca8fe33acb Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Fri, 27 Feb 2026 16:06:14 +0200 Subject: [PATCH 4/6] feat: :sparkles: add RlpEncodableLen trait --- src/transaction/eip1559.rs | 32 ++++- src/transaction/eip2930.rs | 55 ++++++- src/transaction/eip7702.rs | 52 ++++++- src/transaction/legacy.rs | 52 ++++++- src/transaction/mod.rs | 28 ++++ src/transaction/rlp_len.rs | 284 +++++++++++++++++++++++++++++++++++++ 6 files changed, 487 insertions(+), 16 deletions(-) create mode 100644 src/transaction/rlp_len.rs diff --git a/src/transaction/eip1559.rs b/src/transaction/eip1559.rs index 0762bc6..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 { @@ -121,9 +139,19 @@ impl EIP1559TransactionMessage { H256::from_slice(Keccak256::digest(&out).as_ref()) } - /// Returns the RLP-encoded length of this unsigned message. + /// Returns the RLP-encoded length of this unsigned message without + /// allocating. pub fn encoded_len(&self) -> usize { - rlp::encode(self).len() + 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) } } diff --git a/src/transaction/eip2930.rs b/src/transaction/eip2930.rs index d17930a..c08cacb 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 crate::Bytes; pub use super::legacy::TransactionAction; @@ -109,10 +110,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")) } } @@ -149,6 +148,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)] @@ -195,6 +213,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 { @@ -261,9 +295,18 @@ impl EIP2930TransactionMessage { H256::from_slice(Keccak256::digest(&out).as_ref()) } - /// Returns the RLP-encoded length of this unsigned message. + /// Returns the RLP-encoded length of this unsigned message without + /// allocating. pub fn encoded_len(&self) -> usize { - rlp::encode(self).len() + 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) } } diff --git a/src/transaction/eip7702.rs b/src/transaction/eip7702.rs index 1605afb..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 { @@ -285,9 +322,20 @@ impl EIP7702TransactionMessage { H256::from_slice(Keccak256::digest(&out).as_ref()) } - /// Returns the RLP-encoded length of this unsigned message. + /// Returns the RLP-encoded length of this unsigned message without + /// allocating. pub fn encoded_len(&self) -> usize { - rlp::encode(self).len() + 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) } } diff --git a/src/transaction/legacy.rs b/src/transaction/legacy.rs index ac6be38..3d188d4 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 crate::Bytes; #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -47,6 +48,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", @@ -197,10 +212,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")) } } @@ -241,6 +254,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 { @@ -298,9 +325,22 @@ impl LegacyTransactionMessage { H256::from_slice(Keccak256::digest(rlp::encode(self)).as_ref()) } - /// Returns the RLP-encoded length of this unsigned message. + /// Returns the RLP-encoded length of this unsigned message without + /// allocating. pub fn encoded_len(&self) -> usize { - rlp::encode(self).len() + 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) } } diff --git a/src/transaction/mod.rs b/src/transaction/mod.rs index 59da3e1..43f958e 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; use bytes::BytesMut; use ethereum_types::H256; @@ -26,6 +27,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 { @@ -76,6 +80,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 { @@ -153,6 +164,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 { @@ -260,6 +279,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 { diff --git a/src/transaction/rlp_len.rs b/src/transaction/rlp_len.rs new file mode 100644 index 0000000..7912bc6 --- /dev/null +++ b/src/transaction/rlp_len.rs @@ -0,0 +1,284 @@ +//! 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). +#[inline] +fn length_of_length(len: usize) -> usize { + if len < 0x100 { + 1 + } else if len < 0x1_0000 { + 2 + } else if len < 0x100_0000 { + 3 + } else { + // Sufficient for any practical RLP payload (up to 4 GB). + 4 + } +} + +/// 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); + } +} From d813d5db648f846627a9bcb3d0f933aea325006e Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Mon, 2 Mar 2026 16:55:41 +0200 Subject: [PATCH 5/6] fix: uncap length_of_length to handle payloads above 4 GiB on 64-bit Replace the hard-coded if/else chain (capped at 4) with a bit-width formula matching alloy-rlp's approach. This ensures rlp_list_header_len, byte-string rlp_len, and all downstream encoded_len calculations are correct for arbitrary usize values. Also mark the function const fn and add boundary tests for length_of_length, rlp_list_header_len, and byte-string rlp_len at 0xFF, 0xFFFF, 0xFFFFFF, 0xFFFFFFFF, and above on 64-bit targets. --- src/transaction/rlp_len.rs | 129 ++++++++++++++++++++++++++++++++++--- 1 file changed, 119 insertions(+), 10 deletions(-) diff --git a/src/transaction/rlp_len.rs b/src/transaction/rlp_len.rs index 7912bc6..dde4635 100644 --- a/src/transaction/rlp_len.rs +++ b/src/transaction/rlp_len.rs @@ -24,18 +24,19 @@ pub(crate) trait RlpEncodableLen { /// 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] -fn length_of_length(len: usize) -> usize { - if len < 0x100 { - 1 - } else if len < 0x1_0000 { - 2 - } else if len < 0x100_0000 { - 3 - } else { - // Sufficient for any practical RLP payload (up to 4 GB). - 4 +const fn length_of_length(len: usize) -> usize { + if len == 0 { + return 1; } + // Equivalent to alloy-rlp's approach: + // (usize::BITS as usize / 8) - (len.leading_zeros() as usize / 8) + // which counts the minimal big-endian bytes needed for `len`. + (usize::BITS as usize / 8) - (len.leading_zeros() as usize / 8) } /// Total RLP-encoded length of a *list* whose item payloads occupy @@ -281,4 +282,112 @@ mod tests { // 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()); + } } From f797674b28f913bc1963de63a2ad77206d9c6492 Mon Sep 17 00:00:00 2001 From: Manuel Mauro Date: Mon, 2 Mar 2026 16:58:12 +0200 Subject: [PATCH 6/6] refactor: simplify length_of_length comment --- src/transaction/rlp_len.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/transaction/rlp_len.rs b/src/transaction/rlp_len.rs index dde4635..9ac8681 100644 --- a/src/transaction/rlp_len.rs +++ b/src/transaction/rlp_len.rs @@ -33,9 +33,7 @@ const fn length_of_length(len: usize) -> usize { if len == 0 { return 1; } - // Equivalent to alloy-rlp's approach: - // (usize::BITS as usize / 8) - (len.leading_zeros() as usize / 8) - // which counts the minimal big-endian bytes needed for `len`. + // Minimal big-endian bytes needed to represent `len`. (usize::BITS as usize / 8) - (len.leading_zeros() as usize / 8) }