From a623423af487a535c35f1af296cfe8e582e8da03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Sun, 26 Jul 2026 11:51:20 +0200 Subject: [PATCH] state: Move Authorization into its own header The EIP-7702 authorization tuple has its own signing hash and signature recovery, which do not belong in transaction.hpp. Give it a home first. Order the fields as the tuple is specified: chain_id, address, nonce, y_parity, r, s; and put signer, which is not part of it, at the end. y_parity becomes uint8_t: EIP-7702 bounds it to one byte, and a value above 1 invalidates the tuple, not the transaction, so the field must still be able to hold it. The state test loader now rejects a wider one. --- test/state/CMakeLists.txt | 1 + test/state/authorization.hpp | 35 +++++++++++++++++++ test/state/state.cpp | 2 +- test/state/transaction.hpp | 15 +------- .../state_transition_eip7702_test.cpp | 2 +- test/utils/rlp_encode.cpp | 2 +- test/utils/statetest_export.cpp | 2 +- test/utils/statetest_loader.cpp | 2 +- 8 files changed, 42 insertions(+), 19 deletions(-) create mode 100644 test/state/authorization.hpp diff --git a/test/state/CMakeLists.txt b/test/state/CMakeLists.txt index 7e3eb4a35d..281b1425ec 100644 --- a/test/state/CMakeLists.txt +++ b/test/state/CMakeLists.txt @@ -12,6 +12,7 @@ target_include_directories(evmone-state PUBLIC ${PROJECT_SOURCE_DIR}) target_sources( evmone-state PRIVATE account.hpp + authorization.hpp blob_params.hpp block.hpp block.cpp diff --git a/test/state/authorization.hpp b/test/state/authorization.hpp new file mode 100644 index 0000000000..ed5a6482a1 --- /dev/null +++ b/test/state/authorization.hpp @@ -0,0 +1,35 @@ +// evmone: Fast Ethereum Virtual Machine implementation +// Copyright 2025 The evmone Authors. +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include +#include +#include + +namespace evmone::state +{ +using evmc::address; +using intx::uint256; + +/// The set-code transaction authorization tuple (EIP-7702). +struct Authorization +{ + uint256 chain_id; + address addr; + uint64_t nonce = 0; + + /// The signature's y_parity. Valid values are 0 and 1, but an out-of-range value only + /// invalidates the authorization, not the transaction. + uint8_t y_parity = 0; + + uint256 r; + uint256 s; + + /// The signer address. Empty if it cannot be recovered from the signature (y_parity, r, s). + std::optional
signer; +}; + +using AuthorizationList = std::vector; +} // namespace evmone::state diff --git a/test/state/state.cpp b/test/state/state.cpp index 97b69d39bf..9004ba65a1 100644 --- a/test/state/state.cpp +++ b/test/state/state.cpp @@ -131,7 +131,7 @@ int64_t process_authorization_list( // 3. Verify if the signer has been successfully recovered from the signature. // authority = ecrecover(...) // y_parity must be 0 or 1 for EIP-7702/2930 signatures. - if (auth.v > 1) + if (auth.y_parity > 1) continue; // TODO: We actually only do "partial" verification by assuming the signature is valid // when the test has the signer specified. diff --git a/test/state/transaction.hpp b/test/state/transaction.hpp index 27ead99759..5944d7fa6b 100644 --- a/test/state/transaction.hpp +++ b/test/state/transaction.hpp @@ -4,6 +4,7 @@ #pragma once +#include "authorization.hpp" #include "blob_params.hpp" #include "bloom_filter.hpp" #include "state_diff.hpp" @@ -18,20 +19,6 @@ constexpr auto MAX_TX_GAS_LIMIT = 0x1000000; // 2**24 using AccessList = std::vector>>; -struct Authorization -{ - intx::uint256 chain_id; - address addr; - uint64_t nonce = 0; - /// Signer is empty if it cannot be ecrecovered from r, s, v. - std::optional
signer; - intx::uint256 r; - intx::uint256 s; - intx::uint256 v; -}; - -using AuthorizationList = std::vector; - struct Transaction { /// The type of the transaction. diff --git a/test/unittests/state_transition_eip7702_test.cpp b/test/unittests/state_transition_eip7702_test.cpp index 75c6fa6319..63f0c0e709 100644 --- a/test/unittests/state_transition_eip7702_test.cpp +++ b/test/unittests/state_transition_eip7702_test.cpp @@ -91,7 +91,7 @@ TEST_F(state_transition, eip7702_set_code_transaction_invalid_y_parity) pre[authority] = {.nonce = 1}; tx.to = To; tx.type = Transaction::Type::set_code; - tx.authorization_list = {{.addr = delegate, .nonce = 1, .signer = authority, .v = 2}}; + tx.authorization_list = {{.addr = delegate, .nonce = 1, .y_parity = 2, .signer = authority}}; pre[To] = {.code = ret(0)}; expect.post[authority].nonce = 1; diff --git a/test/utils/rlp_encode.cpp b/test/utils/rlp_encode.cpp index 2e92dd8468..d94bfcb029 100644 --- a/test/utils/rlp_encode.cpp +++ b/test/utils/rlp_encode.cpp @@ -98,7 +98,7 @@ namespace evmone::state [[nodiscard]] bytes rlp_encode(const Authorization& authorization) { return rlp::encode_tuple(authorization.chain_id, authorization.addr, authorization.nonce, - authorization.v, authorization.r, authorization.s); + authorization.y_parity, authorization.r, authorization.s); } [[nodiscard]] bytes rlp_encode(const Withdrawal& withdrawal) diff --git a/test/utils/statetest_export.cpp b/test/utils/statetest_export.cpp index eb5dfd8c74..f59c6fb8dd 100644 --- a/test/utils/statetest_export.cpp +++ b/test/utils/statetest_export.cpp @@ -168,7 +168,7 @@ json::json to_state_test(std::string_view test_name, const state::BlockInfo& blo if (!tx.authorization_list.empty()) { auto& ja = jtx["authorizationList"]; - for (const auto& [chain_id, addr, nonce, signer, r, s, y_parity] : tx.authorization_list) + for (const auto& [chain_id, addr, nonce, y_parity, r, s, signer] : tx.authorization_list) { json::json je; je["chainId"] = hex0x(chain_id); diff --git a/test/utils/statetest_loader.cpp b/test/utils/statetest_loader.cpp index 06a8419eb9..af20239901 100644 --- a/test/utils/statetest_loader.cpp +++ b/test/utils/statetest_loader.cpp @@ -159,7 +159,7 @@ state::AuthorizationList from_json(const json::json& j authorization.signer = from_json
(a["signer"]); authorization.r = from_json(a.at("r")); authorization.s = from_json(a.at("s")); - authorization.v = from_json(a.at("v")); + authorization.y_parity = from_json(a.at("v")); o.emplace_back(authorization); } return o;