From cf41dbcf0813d78f3639f778fcea90489d8fcbcc Mon Sep 17 00:00:00 2001 From: zexoverz Date: Tue, 15 Sep 2026 14:08:49 +0700 Subject: [PATCH] t8n: Accept yParity as a typed transaction's v go-ethereum's transaction JSON names a typed transaction's signature recovery id yParity, and the evm t8n tool accepts it without v. The evmone loaders required v unconditionally, so such a txs.json failed to load. Take yParity for typed transactions when it is given, and reject a v alongside it that disagrees, the rule go-ethereum applies. Legacy transactions keep v, which also carries the chain id. Fixes #1488 --- .../integration/evmone-cli/t8n/CMakeLists.txt | 34 ++++++++++++++++++ .../evmone-cli/t8n/cancun_y_parity/alloc.json | 8 +++++ .../evmone-cli/t8n/cancun_y_parity/env.json | 11 ++++++ .../evmone-cli/t8n/cancun_y_parity/txs.json | 19 ++++++++++ test/unittests/statetest_loader_tx_test.cpp | 35 +++++++++++++++++++ test/utils/statetest_loader.cpp | 27 ++++++++++++-- 6 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 test/integration/evmone-cli/t8n/cancun_y_parity/alloc.json create mode 100644 test/integration/evmone-cli/t8n/cancun_y_parity/env.json create mode 100644 test/integration/evmone-cli/t8n/cancun_y_parity/txs.json diff --git a/test/integration/evmone-cli/t8n/CMakeLists.txt b/test/integration/evmone-cli/t8n/CMakeLists.txt index 4ab1e75cdc..808ff6164d 100644 --- a/test/integration/evmone-cli/t8n/CMakeLists.txt +++ b/test/integration/evmone-cli/t8n/CMakeLists.txt @@ -13,6 +13,7 @@ add_test(NAME ${PREFIX}/clean_output_dirs COMMAND ${CMAKE_COMMAND} -E rm -rf ${CMAKE_CURRENT_BINARY_DIR}/berlin_base_fee ${CMAKE_CURRENT_BINARY_DIR}/cancun_create_tx ${CMAKE_CURRENT_BINARY_DIR}/cancun_create_tx_trace + ${CMAKE_CURRENT_BINARY_DIR}/cancun_y_parity ${CMAKE_CURRENT_BINARY_DIR}/prague_empty_requests ${CMAKE_CURRENT_BINARY_DIR}/prague_pre_state_only ${CMAKE_CURRENT_BINARY_DIR}/osaka_blob_schedule @@ -257,6 +258,39 @@ set_tests_properties( ) +set(TEST_CASE cancun_y_parity) + +add_test( + NAME ${PREFIX}/${TEST_CASE} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${TEST_CASE} + COMMAND + evmone-cli t8n + --state.fork Cancun + --state.reward 0 + --state.chainid 1 + --input.alloc alloc.json + --input.txs txs.json + --input.env env.json + --output.basedir ${CMAKE_CURRENT_BINARY_DIR}/${TEST_CASE} + --output.result out.json +) +set_tests_properties( + ${PREFIX}/${TEST_CASE} PROPERTIES FIXTURES_REQUIRED "${TEST_CASE};outputs") + +add_test( + NAME ${PREFIX}/${TEST_CASE}/out.json + COMMAND ${CMAKE_COMMAND} -E cat ${CMAKE_CURRENT_BINARY_DIR}/${TEST_CASE}/out.json +) +set_tests_properties( + ${PREFIX}/${TEST_CASE}/out.json PROPERTIES + FIXTURES_CLEANUP ${TEST_CASE} + # The EIP-1559 transaction names its signature's y-parity as `yParity` and carries no `v`, as + # go-ethereum's JSON does. Loading it is the run above; this is the transaction executing. + PASS_REGULAR_EXPRESSION [=["gasUsed": "0x5208"]=] + FAIL_REGULAR_EXPRESSION [=["error"]=] +) + + add_test(NAME ${PREFIX}/bad_fork COMMAND evmone-cli t8n --state.fork NoSuchRev) set_tests_properties( ${PREFIX}/bad_fork PROPERTIES PASS_REGULAR_EXPRESSION "unknown revision") diff --git a/test/integration/evmone-cli/t8n/cancun_y_parity/alloc.json b/test/integration/evmone-cli/t8n/cancun_y_parity/alloc.json new file mode 100644 index 0000000000..f18840bef3 --- /dev/null +++ b/test/integration/evmone-cli/t8n/cancun_y_parity/alloc.json @@ -0,0 +1,8 @@ +{ + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "code": "0x", + "nonce": "0x0", + "balance": "0xde0b6b3a7640000", + "storage": {} + } +} diff --git a/test/integration/evmone-cli/t8n/cancun_y_parity/env.json b/test/integration/evmone-cli/t8n/cancun_y_parity/env.json new file mode 100644 index 0000000000..3ee66c4ff5 --- /dev/null +++ b/test/integration/evmone-cli/t8n/cancun_y_parity/env.json @@ -0,0 +1,11 @@ +{ + "currentCoinbase": "0x8888f1f195afa192cfee860698584c030f4c9db1", + "currentNumber": "0x01", + "currentTimestamp": "0x54c99069", + "currentGasLimit": "0x2fefd8", + "currentRandom": "0x0000000000000000000000000000000000000000000000000000000000000001", + "parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "currentBaseFee": "0x1", + "currentExcessBlobGas": "0x0", + "withdrawals": [] +} diff --git a/test/integration/evmone-cli/t8n/cancun_y_parity/txs.json b/test/integration/evmone-cli/t8n/cancun_y_parity/txs.json new file mode 100644 index 0000000000..42a7b856f3 --- /dev/null +++ b/test/integration/evmone-cli/t8n/cancun_y_parity/txs.json @@ -0,0 +1,19 @@ +[ + { + "accessList": [], + "chainId": "0x1", + "gas": "0x5208", + "hash": "0x3a01c573d8ea83d8872a79d7afe20bd009a61039da1347b64f0b3972ebc6a911", + "input": "0x", + "maxFeePerGas": "0x1", + "maxPriorityFeePerGas": "0x0", + "nonce": "0x0", + "r": "0x1043c2860f2cc884d4ce573c89fd36e7b68ff41480a17693f1e1349a5837e9fe", + "s": "0x78b469d7feac811d12dffc12bfefafc27e36e893042cb11c5c24a2e3e35b94a4", + "sender": "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "to": "0x095E7BAea6a6c7c4c2DfeB977eFac326aF552d87", + "type": "0x2", + "value": "0x0", + "yParity": "0x1" + } +] diff --git a/test/unittests/statetest_loader_tx_test.cpp b/test/unittests/statetest_loader_tx_test.cpp index 3576f0ae3e..66932299fe 100644 --- a/test/unittests/statetest_loader_tx_test.cpp +++ b/test/unittests/statetest_loader_tx_test.cpp @@ -155,6 +155,41 @@ TEST(statetest_loader, tx_eip1559) EXPECT_EQ(tx.v, 1); } +TEST(statetest_loader, tx_y_parity) +{ + // go-ethereum names a typed transaction's `v` as `yParity`; the two must agree when both are + // given. A legacy transaction's `v` also carries the chain id, so `yParity` cannot name it. + constexpr std::string_view typed = R"({ + "input": "", "gas": "0x5208", "value": "0", "sender": "a0a1", "to": "c0c1", + "maxFeePerGas": "1", "maxPriorityFeePerGas": "0", "accessList": [], "nonce": "0", + "r": "0x1111111111111111111111111111111111111111111111111111111111111111", + "s": "0x2222222222222222222222222222222222222222222222222222222222222222", + "type": "2")"; + constexpr std::string_view legacy = R"({ + "input": "", "gas": "0x5208", "value": "0", "sender": "a0a1", "to": "c0c1", + "gasPrice": "1", "nonce": "0", + "r": "0x1111111111111111111111111111111111111111111111111111111111111111", + "s": "0x2222222222222222222222222222222222222222222222222222222222222222")"; + + const auto load = [](std::string_view head, std::string_view signature) { + return test::from_json( + json::json::parse(std::string{head} + ", " + std::string{signature} + "}")); + }; + + EXPECT_EQ(load(typed, R"("yParity": "0x1")").v, 1); + EXPECT_EQ(load(typed, R"("yParity": "0x0")").v, 0); + EXPECT_EQ(load(typed, R"("v": "0x1")").v, 1); + EXPECT_EQ(load(typed, R"("v": "0x1", "yParity": "0x1")").v, 1); + + EXPECT_THAT([&] { load(typed, R"("v": "0x0", "yParity": "0x1")"); }, + ThrowsMessage("invalid transaction: v and yParity do not match")); + EXPECT_THAT([&] { load(typed, R"("yParity": "0x2")"); }, + ThrowsMessage("invalid transaction: yParity must be 0 or 1")); + + EXPECT_EQ(load(legacy, R"("v": "0x1b", "yParity": "0x0")").v, 27); // yParity is ignored. + EXPECT_THROW(load(legacy, R"("yParity": "0x1")"), json::json::out_of_range); +} + TEST(statetest_loader, tx_access_list) { constexpr std::string_view input = R"({ diff --git a/test/utils/statetest_loader.cpp b/test/utils/statetest_loader.cpp index 2d282e54d7..bc33f0d275 100644 --- a/test/utils/statetest_loader.cpp +++ b/test/utils/statetest_loader.cpp @@ -332,6 +332,23 @@ TestState from_json(const json::json& j) return o; } +/// Loads the `yParity` of a typed transaction, the name go-ethereum's JSON gives its signature's +/// `v`. Returns nothing for a legacy transaction, whose `v` also carries the chain id, or when the +/// key is absent; a `v` given alongside must agree with it. +static std::optional load_y_parity(const json::json& j, state::Transaction::Type type) +{ + if (type == state::Transaction::Type::legacy) + return std::nullopt; + const auto y_parity = load_optional(j, "yParity"); + if (!y_parity.has_value()) + return std::nullopt; + if (*y_parity > 1) + throw std::invalid_argument("invalid transaction: yParity must be 0 or 1"); + if (const auto v = load_optional(j, "v"); v.has_value() && *v != *y_parity) + throw std::invalid_argument("invalid transaction: v and yParity do not match"); + return y_parity; +} + /// Load common parts of Transaction or TestMultiTransaction. static void from_json_tx_common(const json::json& j, state::Transaction& o) { @@ -416,7 +433,10 @@ state::Transaction from_json(const json::json& j) o.r = from_json(j.at("r")); o.s = from_json(j.at("s")); - o.v = from_json(j.at("v")); + if (const auto y_parity = load_y_parity(j, o.type)) + o.v = *y_parity; + else + o.v = from_json(j.at("v")); return o; } @@ -442,7 +462,10 @@ static void from_json(const json::json& j, TestMultiTransaction& o) for (const auto& j_value : j.at("value")) o.values.emplace_back(from_json(j_value)); - o.v = load_or(j, "v", 0); + if (const auto y_parity = load_y_parity(j, o.type)) + o.v = *y_parity; + else + o.v = load_or(j, "v", 0); } static void from_json(const json::json& j, TestMultiTransaction::Indexes& o)