Add transaction decoding to the state library#1581
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #1581 +/- ##
==========================================
+ Coverage 97.41% 97.50% +0.09%
==========================================
Files 163 167 +4
Lines 14677 15209 +532
Branches 3404 3565 +161
==========================================
+ Hits 14297 14829 +532
Misses 280 280
Partials 100 100
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.
| else // Typed: type byte followed by the RLP list. | ||
| { | ||
| uint8_t t{}; | ||
| if (!rlp::decode(from, t)) | ||
| return false; | ||
| if (t > static_cast<uint8_t>(Transaction::Type::legacy) && | ||
| t <= static_cast<uint8_t>(Transaction::Type::set_code)) | ||
| to.type = static_cast<Transaction::Type>(t); | ||
| else | ||
| return false; | ||
|
|
||
| if (!take_list_payload(from, body) || !rlp::decode(body, to.chain_id)) | ||
| return false; | ||
| } |
There was a problem hiding this comment.
Addressed in the current revision: the EIP-2718 type byte is now read as a raw byte (const uint8_t t = from[0]), so the 0x81 0x02 string-wrapped form is rejected (type 129, outside {1..4}). Note it was already rejected before this change as well — decode_header enforces single-byte canonicality (0x81 0x0X with X < 0x80 is non-canonical) — so this is a clarity/robustness change rather than a bug fix.
| template <typename T> | ||
| [[nodiscard]] bool decode(bytes_view& from, std::vector<T>& to) noexcept | ||
| { | ||
| Header h; | ||
| if (!decode_header(from, h) || !h.is_list) | ||
| return false; | ||
|
|
||
| auto payload_view = from.substr(0, static_cast<size_t>(h.payload_length)); | ||
| while (!payload_view.empty()) | ||
| { | ||
| to.emplace_back(); | ||
| if (!decode(payload_view, to.back())) | ||
| return false; | ||
| } | ||
|
|
||
| from.remove_prefix(static_cast<size_t>(h.payload_length)); | ||
| return true; | ||
| } |
There was a problem hiding this comment.
Addressed in the current revision: elements are decoded into a local vector and std::moved into to only on success, so a failed decode leaves the output vector untouched.
72d44f2 to
dcc7adb
Compare
| to.v = (v_u256 - 35) % 2 == 0 ? 0 : 1; | ||
| const auto chain_id = (v_u256 - 35 - to.v) / 2; | ||
| if (chain_id > std::numeric_limits<uint64_t>::max()) | ||
| return false; // chain id must fit the 64-bit Transaction::chain_id. | ||
| to.chain_id = static_cast<uint64_t>(chain_id); |
dcc7adb to
dd6bed2
Compare
b698a8c to
3060cb0
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. Co-authored-by: rodiazet <radek.zagorowicz@gmail.com>
3060cb0 to
aaeddfc
Compare
Decode the "to" recipient with the strict rlp::decode(address) overload, which already requires exactly 20 bytes, rather than decoding a variable-length string and size-checking by hand; an empty string still selects CREATE. Narrow rlp::Header::payload_length to uint32_t so it widens to size_t without the per-call-site casts (it never exceeds the input size, well below 4 GiB). Mark which decode overloads are fixed-width vs variable-length.
Fold the three copies of the list-framing sequence (decode the header, check is_list, bound the payload, advance past it) into a single rlp::take_list_payload used by the vector and pair decoders and the transaction decoder. Decode the EIP-7702 authorization_list through the generic rlp::decode(std::vector<T>&) by giving Authorization an ADL-visible decode() overload, matching how access_list is already decoded, instead of a hand-rolled loop. Rename decode_fields to decode_multi.
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.