From e928eaae6e58bde670fda076a33af8d6adca730e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Mon, 27 Jul 2026 14:27:38 +0200 Subject: [PATCH] test: Check the transaction codec against every fixture encoding decode_transaction() is meant to be the inverse of rlp_encode(), but nothing held it to that. The state test runner dropped the input bytes once it had decoded them, and the blockchain runner never looked at a block's serialization at all -- it took the transactions from the fixture's JSON. Re-encode each decoded transaction and compare it with the bytes it came from: the state test's txbytes, and every transaction sliced out of a valid block's RLP. A legacy transaction is a list item there, a typed one an RLP string wrapping the EIP-2718 envelope. Invalid blocks are skipped, their transactions need not decode at all. Keeping the block's serialization for that also replaces TestBlock's rlp_size, which was all the runner needed from it so far. The hand-written eip7778_block_gas fixture had only a placeholder there, since nothing but its length was ever read; it now carries the encoding of the block it describes. --- test/blockchaintest/blockchaintest_runner.cpp | 38 ++++++++++++++++++- .../blockchaintest/eip7778_block_gas.json | 2 +- test/statetest/statetest_runner.cpp | 6 +++ test/utils/blockchaintest.hpp | 2 +- test/utils/blockchaintest_loader.cpp | 4 +- 5 files changed, 47 insertions(+), 5 deletions(-) diff --git a/test/blockchaintest/blockchaintest_runner.cpp b/test/blockchaintest/blockchaintest_runner.cpp index abbfb226b5..1447cc0226 100644 --- a/test/blockchaintest/blockchaintest_runner.cpp +++ b/test/blockchaintest/blockchaintest_runner.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -119,12 +120,43 @@ std::error_code validate_block(evmc_revision rev, state::BlobParams blob_params, if (!test_block.withdrawals_parse_success) return make_error_code(INCORRECT_BLOCK_FORMAT); - if (rev >= EVMC_OSAKA && test_block.rlp_size > MAX_RLP_BLOCK_SIZE) + if (rev >= EVMC_OSAKA && test_block.rlp.size() > MAX_RLP_BLOCK_SIZE) return make_error_code(RLP_BLOCK_LIMIT_EXCEEDED); return {}; } +/// Checks the transaction codec against a block's own serialization: every transaction in it must +/// decode, and encode back to the very same bytes. +void expect_transactions_round_trip(bytes_view block_rlp) +{ + bytes_view body; // A block is [header, transactions, ...]. + ASSERT_TRUE(rlp::take_list_payload(block_rlp, body)); + ASSERT_TRUE(block_rlp.empty()) << "trailing bytes after the block"; + bytes_view block_header; + ASSERT_TRUE(rlp::take_list_payload(body, block_header)); // Skipped over. + bytes_view txs; + ASSERT_TRUE(rlp::take_list_payload(body, txs)); + + while (!txs.empty()) + { + const auto item = txs; + rlp::Header h; + ASSERT_TRUE(rlp::decode_header(txs, h)); // Advances txs to the item's payload. + const auto header_size = item.size() - txs.size(); + txs.remove_prefix(h.payload_length); + + // A legacy transaction is an RLP list here, a typed one an RLP string wrapping the + // EIP-2718 envelope; the envelope alone is the transaction. + const auto tx_bytes = h.is_list ? item.substr(0, header_size + h.payload_length) : + item.substr(header_size, h.payload_length); + + const auto tx = state::decode_transaction(tx_bytes); + ASSERT_TRUE(tx.has_value()) << hex(tx_bytes); + EXPECT_EQ(rlp::encode(*tx), tx_bytes); + } +} + std::optional mining_reward(evmc_revision rev) noexcept { if (rev < EVMC_BYZANTIUM) @@ -218,6 +250,10 @@ void run_blockchain_tests(std::span tests, evmc::VM& vm) SCOPED_TRACE(std::string{evmc::to_string(rev)} + '/' + std::to_string(case_index) + '/' + c.name + '/' + std::to_string(test_block.block_info.number)); + // Invalid blocks are skipped: they may carry transactions that do not even decode. + if (test_block.expected_exception.empty()) + expect_transactions_round_trip(test_block.rlp); + const auto block_error = validate_block(rev, blob_params, test_block, parent_header, parent_has_ommers); diff --git a/test/integration/blockchaintest/eip7778_block_gas.json b/test/integration/blockchaintest/eip7778_block_gas.json index f510401487..64d45acdad 100644 --- a/test/integration/blockchaintest/eip7778_block_gas.json +++ b/test/integration/blockchaintest/eip7778_block_gas.json @@ -52,7 +52,7 @@ }, "blocks": [ { - "rlp": "0x00", + "rlp": "0xf90285f90259a00000000000000000000000000000000000000000000000000000000000000001a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794000000000000000000000000000000000000c014a09906207bb91a552f2c9036ff75ffebd15d6262fe1f1dc40afa6e047158ee25e5a06e1d1e299aa4bca4b32c05a8bd8cf631d97c7d9b54b1401c373860db01a38efea0c220348cfca03fd761c800a2af15752cee5ba04f836e61f798c4c6222c0cab02b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800184010000008265960c80a000000000000000000000000000000000000000000000000000000000000000008800000000000000000ea056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b4218080a00000000000000000000000000000000000000000000000000000000000000000a0e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855e6a502e30180800e830186a094000000000000000000000000000000000000c0de8080c0800101c0c0", "blockHeader": { "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000001", "uncleHash": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", diff --git a/test/statetest/statetest_runner.cpp b/test/statetest/statetest_runner.cpp index cb6765461e..3b9592c7ed 100644 --- a/test/statetest/statetest_runner.cpp +++ b/test/statetest/statetest_runner.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include namespace evmone::test @@ -34,7 +35,12 @@ void run_state_test(const StateTransitionTest& test, evmc::VM& vm, bool trace_su { tx = state::decode_transaction(*expected.txbytes); if (tx.has_value()) + { + // Decoding is the inverse of encoding: what decoded must encode back exactly. + EXPECT_EQ(rlp::encode(*tx), *expected.txbytes); + tx->sender = template_tx.sender; // No recovery yet, take sender from JSON. + } } const auto res = diff --git a/test/utils/blockchaintest.hpp b/test/utils/blockchaintest.hpp index aeb081709d..fb2301a135 100644 --- a/test/utils/blockchaintest.hpp +++ b/test/utils/blockchaintest.hpp @@ -49,7 +49,7 @@ struct TestBlock { state::BlockInfo block_info; std::vector transactions; - size_t rlp_size = 0; + bytes rlp; ///< The block's complete serialization. bool withdrawals_parse_success = true; std::string expected_exception; ///< Empty for valid blocks. diff --git a/test/utils/blockchaintest_loader.cpp b/test/utils/blockchaintest_loader.cpp index 76f345fca9..fc29036e33 100644 --- a/test/utils/blockchaintest_loader.cpp +++ b/test/utils/blockchaintest_loader.cpp @@ -198,13 +198,13 @@ BlockchainTest load_blockchain_test_case(const std::string& name, const json::js auto test_block = load_test_block(el.at("rlp_decoded"), bt.network, bt.blob_schedule); test_block.expected_exception = map_legacy_block_exception(it->get()); - test_block.rlp_size = from_json(el.at("rlp")).size(); + test_block.rlp = from_json(el.at("rlp")); bt.test_blocks.emplace_back(test_block); } else { auto test_block = load_test_block(el, bt.network, bt.blob_schedule); - test_block.rlp_size = from_json(el.at("rlp")).size(); + test_block.rlp = from_json(el.at("rlp")); bt.test_blocks.emplace_back(test_block); } }