Add transaction and RLP decoding to the state library - #1581
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1581 +/- ##
==========================================
+ Coverage 97.40% 97.48% +0.08%
==========================================
Files 166 170 +4
Lines 14810 15376 +566
Branches 3412 3591 +179
==========================================
+ Hits 14425 14990 +565
Misses 281 281
- Partials 104 105 +1
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR adds exception-free transaction decoding to the test/state library by implementing state::decode_transaction() (legacy + EIP-2718 typed envelopes) and introducing supporting RLP decoding primitives, with unit tests validating round-trips and malformed-input rejection.
Changes:
- Add
state::decode_transaction(bytes_view) -> std::optional<Transaction>and its implementation for legacy/EIP-2718 typed transactions. - Introduce
test/state/rlp_decode.{hpp,cpp}with overflow-safe/canonical RLP header decoding and basic decode helpers. - Extend
state_rlpunit tests with transaction decode round-trips and regression cases for malformed RLP / invalid transaction encodings.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| test/unittests/state_rlp_test.cpp | Adds unit tests for transaction decoding and RLP regression cases. |
| test/state/transaction.hpp | Declares decode_transaction() API and documents behavior. |
| test/state/transaction.cpp | Implements transaction decoding logic for legacy + typed transactions. |
| test/state/rlp_decode.hpp | Adds header-only RLP decoding primitives (templated decoders). |
| test/state/rlp_decode.cpp | Implements RLP header decoding and byte/address decoding. |
| test/state/CMakeLists.txt | Wires new decoder sources into the test/state target. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
72d44f2 to
dcc7adb
Compare
dcc7adb to
dd6bed2
Compare
3060cb0 to
aaeddfc
Compare
98cdda2 to
b0183cb
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (2)
test/state/transaction.hpp:93
- The public
decode_transaction()declaration doesn’t document that legacy transactions are normalized (legacyvis converted to y_parity 0/1 andchain_idis extracted). This is important becauserlp_encode()for legacy writestx.vverbatim, so callers might assumedecode_transaction()is a strict inverse and attempt to re-encode the decoded legacy transaction, producing different / invalid bytes.
/// Decodes a transaction from its complete serialization @p data.
///
/// Handles the legacy RLP list and the EIP-2718 typed envelope (type byte followed by an RLP list).
[[nodiscard]] std::optional<Transaction> decode_transaction(bytes_view data) noexcept;
test/state/rlp_decode.cpp:97
decode_header()narrowslist_len(uint64_t) intoHeader::payload_length(uint32_t) without a range check. With very large inputs this can truncate the declared payload length and break list-boundary enforcement. Rejectinglist_len > UINT32_MAXavoids silent truncation.
input.remove_prefix(1 + len_of_list_len);
out = {static_cast<uint32_t>(list_len), true}; // Fits: list_len < input_len < 4 GiB.
return true;
e0b35fa to
23c3b92
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
test/state/rlp_decode.cpp:40
decode_long_header()relies on anassert()to ensurepayload_lenfits intoHeader::payload_length(uint32_t). In release builds the assert is compiled out, so a crafted RLP header withpayload_len > UINT32_MAXwould silently truncate during thestatic_cast<uint32_t>, potentially causing incorrect parsing/acceptance of malformed inputs. Add a runtime check and reject oversized payload lengths instead of asserting.
input.remove_prefix(1 + len_of_len);
assert(payload_len <= std::numeric_limits<uint32_t>::max()); // Inputs stay well below 4 GiB.
out = {static_cast<uint32_t>(payload_len), IsList};
return true;
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (2)
test/state/transaction.cpp:156
decode_transaction()constructsTransaction tx;, which default-initializes and leaves several fields indeterminate (sender,max_gas_price,max_priority_gas_price,max_blob_gas_price,value,r,s, etc.). On successful decode these can remain unread/unused today, but the returnedTransactionmay carry uninitialized data (and reading it is UB). Value-initialize the transaction to guarantee zeroed scalars and a zerosenderwhen it is not recovered by the decoder.
Transaction tx;
test/state/rlp_decode.hpp:57
take_list_payload()claims that rejecting a non-list item leavesfromadvanced past its header, but that’s not true for the single-byte form wheredecode_header()leavesfromunchanged. Tighten the comment to avoid implying stronger advancement guarantees than the implementation provides.
/// Reads an RLP list header, advances @p from past the list, and returns its bounded payload.
/// Rejecting a non-list item leaves @p from advanced past its header.
| /// Decodes an EIP-7702 authorization. | ||
| /// A malformed tuple leaves @p from past it and @p to partially assigned. | ||
| /// | ||
| /// Declared here (not file-local) so the generic rlp::decode(std::vector<T>&) finds it by ADL. | ||
| [[nodiscard]] bool decode(bytes_view& from, Authorization& to) noexcept; |
| /// Decodes the RLP header, advancing @p input past it. Returns false on malformed input. | ||
| /// On success the payload fits the advanced input: out.payload_length <= input.size(). | ||
| /// The only decoder that leaves @p input unchanged on failure. | ||
| [[nodiscard]] bool decode_header(bytes_view& input, Header& out) noexcept; |
1175942 to
66fe20d
Compare
The state library can encode a transaction but not decode one: t8n and the
state tests assemble transactions from JSON fields, so the serialized form
a fixture carries is never parsed and never checked.
Add state::decode_transaction(), the inverse of the existing RLP encoder.
It takes a legacy RLP list or an EIP-2718 typed envelope and returns
std::nullopt on malformed input, without throwing.
The RLP primitives it needs are new. rlp_decode.{hpp,cpp} decodes a header
and enforces canonicality (no leading-zero lengths or integers, the long
form only above 55 bytes, a single byte below 0x80 unwrapped), bounds a
scalar by the destination width, requires an exact width for addresses and
hashes, and provides generic list, pair and vector decoders. The access
list and the EIP-7702 authorization list go through the vector decoder,
the latter via an ADL-visible decode(Authorization&).
Decoding is not a strict inverse of the encoder: the legacy wire v is
normalized into (chain_id, y_parity), so re-encoding a decoded legacy
transaction need not reproduce its bytes, and v=35/36 collapses onto the
pre-EIP-155 v=27/28 form. The declaration says so and a TODO records the
fix.
The unit tests give both new files 100% line and MC/DC coverage (clang
source-based); the only regions left uncovered are the failure path of the
payload-length assert, which cannot be reached.
Co-authored-by: rodiazet <radek.zagorowicz@gmail.com>
66fe20d to
78d9fb4
Compare
Add state::decode_transaction, the inverse of the existing RLP encoder: it
decodes a legacy or EIP-2718 typed transaction from its network serialization
into a state::Transaction, rejecting malformed input (exception-free, returns
std::nullopt). Covered by state_rlp unit tests.