Skip to content

Add encoded length methods to transactions#2

Merged
librelois merged 7 commits intomoonbeam-polkadot-stable2506from
manuel/add-encoded-len-utils
Mar 2, 2026
Merged

Add encoded length methods to transactions#2
librelois merged 7 commits intomoonbeam-polkadot-stable2506from
manuel/add-encoded-len-utils

Conversation

@manuelmauro
Copy link

Summary

Add encoded_len() and payload_len() methods to compute transaction sizes without full encoding. This enables transaction size validation for DoS protection, matching reth/alloy's approach.

Motivation

Transaction pools need to enforce size limits for performance reasons and to prevent DoS attacks. Reth enforces a default maximum of 128 KB per transaction and validates transactions.

Alloy provides similar methods on transaction envelopes for this purpose.

Copy link

@librelois librelois left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add some unit tests, suggested scenarios:

  1. encoded_len matches actual encoded bytes: for TransactionV0..V3, assert tx.encoded_len() == tx.encode().len().

  2. payload_len matches payload encoding: assert tx.payload_len() == tx.encode_payload().len() for each tx variant.

  3. Typed vs legacy size rule: assert typed tx (2930/1559/7702) has encoded_len() == payload_len() + 1, and legacy has encoded_len() == payload_len().

  4. Unsigned message length methods: for Legacy/EIP2930/EIP1559/EIP7702 message structs, assert message.encoded_len() == rlp::encode(&message).len().

  5. Size growth sanity: mutate fields with variable length (large input/data, longer access_list, larger authorization_list) and assert encoded_len increases.

  6. Boundary-ish DoS shape case: construct a near-limit large tx payload (e.g., ~128KB data field) and verify length methods still exactly match full encoding length.

@manuelmauro
Copy link
Author

Please add some unit tests, suggested scenarios:

1. `encoded_len` matches actual encoded bytes: for `TransactionV0..V3`, assert `tx.encoded_len() == tx.encode().len()`.

2. `payload_len` matches payload encoding: assert `tx.payload_len() == tx.encode_payload().len()` for each tx variant.

3. Typed vs legacy size rule: assert typed tx (`2930/1559/7702`) has `encoded_len() == payload_len() + 1`, and legacy has `encoded_len() == payload_len()`.

4. Unsigned message length methods: for `Legacy/EIP2930/EIP1559/EIP7702` message structs, assert `message.encoded_len() == rlp::encode(&message).len()`.

5. Size growth sanity: mutate fields with variable length (large `input/data`, longer `access_list`, larger `authorization_list`) and assert `encoded_len` increases.

6. Boundary-ish DoS shape case: construct a near-limit large tx payload (e.g., ~128KB data field) and verify length methods still exactly match full encoding length.

Addressed in 0d9c2e4

Copy link

@librelois librelois left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

encoded_len()/payload_len() still perform full payload encoding and allocation, so they do not deliver the intended "size check without full encoding" DoS-hardening behavior.

Evidence:
src/enveloped.rs:48 uses self.encode_payload().len() for payload_len().
src/transaction/legacy.rs:288, src/transaction/eip2930.rs:250, src/transaction/eip1559.rs:125, src/transaction/eip7702.rs:289 each compute length via rlp::encode(self).len().
Impact: size validation remains allocation-heavy on untrusted inputs, which weakens the performance/DoS motivation.

Please implement true non-allocating length paths using RlpStream length accounting (begin_unbounded_list + append + finalize_unbounded_list) or explicit per-field RLP length math, and override payload_len() per transaction type.

@manuelmauro
Copy link
Author

encoded_len()/payload_len() still perform full payload encoding and allocation, so they do not deliver the intended "size check without full encoding" DoS-hardening behavior.

Evidence: src/enveloped.rs:48 uses self.encode_payload().len() for payload_len(). src/transaction/legacy.rs:288, src/transaction/eip2930.rs:250, src/transaction/eip1559.rs:125, src/transaction/eip7702.rs:289 each compute length via rlp::encode(self).len(). Impact: size validation remains allocation-heavy on untrusted inputs, which weakens the performance/DoS motivation.

Please implement true non-allocating length paths using RlpStream length accounting (begin_unbounded_list + append + finalize_unbounded_list) or explicit per-field RLP length math, and override payload_len() per transaction type.

Addressed in ee70e83

Copy link

@librelois librelois left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

length_of_length is capped at 4, so it undercounts RLP headers when payload length exceeds 0xFFFF_FFFF bytes (4 GiB). That propagates into list/string length math and can make encoded_len/rlp_len incorrect for very large payloads.
File: src/transaction/rlp_len.rs (line 28), src/transaction/rlp_len.rs (line 131)

Suggestions:

  1. Fix length_of_length to compute bytes needed for arbitrary usize (not capped at 4).
  2. Add unit tests around boundary values (0xFF, 0xFFFF, 0xFFFFFF, 0xFFFFFFFF, 0x1_0000_0000 on 64-bit) for rlp_list_header_len/byte-string length helpers.

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.
@manuelmauro
Copy link
Author

length_of_length is capped at 4, so it undercounts RLP headers when payload length exceeds 0xFFFF_FFFF bytes (4 GiB). That propagates into list/string length math and can make encoded_len/rlp_len incorrect for very large payloads. File: src/transaction/rlp_len.rs (line 28), src/transaction/rlp_len.rs (line 131)

Suggestions:

1. Fix length_of_length to compute bytes needed for arbitrary usize (not capped at 4).

2. Add unit tests around boundary values (0xFF, 0xFFFF, 0xFFFFFF, 0xFFFFFFFF, 0x1_0000_0000 on 64-bit) for `rlp_list_header_len`/byte-string length helpers.

Addressed in d813d5d

@librelois librelois merged commit 7f86ab4 into moonbeam-polkadot-stable2506 Mar 2, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants