Skip to content

Add transaction and RLP decoding to the state library - #1581

Merged
chfast merged 1 commit into
masterfrom
state/decode-transaction
Jul 26, 2026
Merged

Add transaction and RLP decoding to the state library#1581
chfast merged 1 commit into
masterfrom
state/decode-transaction

Conversation

@chfast

@chfast chfast commented Jul 7, 2026

Copy link
Copy Markdown
Member

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.

@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.82332% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 97.48%. Comparing base (0b7069a) to head (78d9fb4).

Files with missing lines Patch % Lines
test/unittests/state_rlp_decode_test.cpp 99.75% 0 Missing and 1 partial ⚠️
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     
Flag Coverage Δ
eest-develop 89.41% <ø> (ø)
eest-develop-gmp 25.02% <0.00%> (-0.96%) ⬇️
eest-legacy 16.75% <0.00%> (-0.65%) ⬇️
eest-libsecp256k1 27.15% <0.00%> (-1.04%) ⬇️
eest-stable 89.37% <ø> (ø)
evmone-unittests 93.07% <99.82%> (+0.31%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
core 96.03% <100.00%> (+0.13%) ⬆️
tooling 90.36% <ø> (ø)
tests 99.80% <99.75%> (-0.01%) ⬇️
Files with missing lines Coverage Δ
test/state/rlp_decode.cpp 100.00% <100.00%> (ø)
test/state/rlp_decode.hpp 100.00% <100.00%> (ø)
test/state/transaction.cpp 100.00% <100.00%> (ø)
test/state/transaction.hpp 100.00% <ø> (ø)
test/unittests/state_rlp_decode_test.cpp 99.75% <99.75%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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_rlp unit 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.

Comment thread test/state/transaction.cpp Outdated
Comment thread test/state/rlp_decode.hpp
@chfast
chfast force-pushed the state/decode-transaction branch 3 times, most recently from 72d44f2 to dcc7adb Compare July 7, 2026 10:23
@chfast
chfast requested a review from Copilot July 7, 2026 10:24

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Comment thread test/state/transaction.cpp
Comment thread test/state/transaction.hpp Outdated
Comment thread test/state/transaction.cpp
@chfast
chfast force-pushed the state/decode-transaction branch from dcc7adb to dd6bed2 Compare July 7, 2026 10:30
@chfast chfast changed the title test: Add transaction decoding to the state library Add transaction decoding to the state library Jul 7, 2026
@chfast
chfast force-pushed the state/decode-transaction branch 3 times, most recently from 3060cb0 to aaeddfc Compare July 7, 2026 15:42
@chfast
chfast requested a review from Copilot July 7, 2026 16:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

@chfast
chfast force-pushed the state/decode-transaction branch from 98cdda2 to b0183cb Compare July 24, 2026 17:31
@chfast
chfast requested a review from Copilot July 24, 2026 17:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 (legacy v is converted to y_parity 0/1 and chain_id is extracted). This is important because rlp_encode() for legacy writes tx.v verbatim, so callers might assume decode_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() narrows list_len (uint64_t) into Header::payload_length (uint32_t) without a range check. With very large inputs this can truncate the declared payload length and break list-boundary enforcement. Rejecting list_len > UINT32_MAX avoids 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;

Comment thread test/state/rlp_decode.cpp Outdated
@chfast
chfast force-pushed the state/decode-transaction branch from e0b35fa to 23c3b92 Compare July 26, 2026 19:21
@chfast
chfast requested a review from Copilot July 26, 2026 19:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 an assert() to ensure payload_len fits into Header::payload_length (uint32_t). In release builds the assert is compiled out, so a crafted RLP header with payload_len > UINT32_MAX would silently truncate during the static_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;

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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() constructs Transaction 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 returned Transaction may carry uninitialized data (and reading it is UB). Value-initialize the transaction to guarantee zeroed scalars and a zero sender when 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 leaves from advanced past its header, but that’s not true for the single-byte form where decode_header() leaves from unchanged. 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.

Comment on lines +22 to +26
/// 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;
Comment thread test/state/rlp_decode.hpp Outdated
Comment on lines +51 to +54
/// 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;
@chfast chfast changed the title Add transaction decoding to the state library Add transaction and RLP decoding to the state library Jul 26, 2026
@chfast
chfast force-pushed the state/decode-transaction branch from 1175942 to 66fe20d Compare July 26, 2026 21:01
@chfast
chfast requested a review from Copilot July 26, 2026 21:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Comment thread test/state/transaction.cpp Outdated
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>
@chfast
chfast force-pushed the state/decode-transaction branch from 66fe20d to 78d9fb4 Compare July 26, 2026 21:10
@chfast
chfast requested a review from Copilot July 26, 2026 21:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

@chfast
chfast merged commit 7686df8 into master Jul 26, 2026
24 checks passed
@chfast
chfast deleted the state/decode-transaction branch July 26, 2026 21:41
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