From d4938e535c41f0de82d09b10e4f8b5c450b94540 Mon Sep 17 00:00:00 2001 From: zexoverz Date: Sat, 26 Sep 2026 23:14:36 +0900 Subject: [PATCH 1/2] test: Take the state test sender from secretKey when not given --- test/unittests/statetest_runner_test.cpp | 45 ++++++++++++++++++++++++ test/utils/statetest_loader.cpp | 20 +++++++++++ 2 files changed, 65 insertions(+) diff --git a/test/unittests/statetest_runner_test.cpp b/test/unittests/statetest_runner_test.cpp index a6acfdabd5..f83d1535cf 100644 --- a/test/unittests/statetest_runner_test.cpp +++ b/test/unittests/statetest_runner_test.cpp @@ -108,6 +108,46 @@ constexpr std::string_view FAILING_TX = R"({"failing_tx": { } }})"; +/// The reproducer from #1490: a value transfer signed by `secretKey`, with no `sender` given. +/// The expected hash is go-ethereum's. +constexpr std::string_view SECRET_KEY_ONLY = R"({"secretkey_sender_recovery": { + "env": { + "currentCoinbase": "0x8888f1f195afa192cfee860698584c030f4c9db1", + "currentNumber": "0x01", + "currentTimestamp": "0x54c99069", + "currentGasLimit": "0x2fefd8", + "currentDifficulty": "0x0", + "currentRandom": "0x0000000000000000000000000000000000000000000000000000000000000001", + "parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "currentBaseFee": "0x1", + "currentExcessBlobGas": "0x0", + "withdrawals": [] + }, + "pre": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "code": "0x", "nonce": "0x00", "balance": "0xde0b6b3a7640000", "storage": {} + }, + "0x095e7baea6a6c7c4c2dfeb977efac326af552d87": { + "code": "0x", "nonce": "0x00", "balance": "0x00", "storage": {} + } + }, + "transaction": { + "data": ["0x"], + "gasLimit": ["0x5208"], + "value": ["0x0"], + "to": "0x095e7baea6a6c7c4c2dfeb977efac326af552d87", + "nonce": "0x0", + "chainId": "0x1", + "gasPrice": "0x1", + "secretKey": "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" + }, + "post": {"Cancun": [{ + "indexes": {"data": 0, "gas": 0, "value": 0}, + "hash": "0x5a605f957d4aa4145411d63287ddf1bae2202c30cc6ec86ae003423bb6343778", + "logs": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347" + }]} +}})"; + /// A case given as raw txbytes, which the runner decodes and recovers the sender of, in a fixture /// that expects it to succeed. The `transaction` beside it is what the loader wants, not what runs. std::string txbytes_case(std::string_view txbytes) @@ -276,3 +316,8 @@ TEST(statetest_runner, txbytes_invalid_encoding) ASSERT_EQ(failures.size(), 1u); EXPECT_EQ(failures[0].detail, "unexpected invalid transaction: invalid transaction encoding"); } + +TEST(statetest_runner, sender_from_secret_key) +{ + EXPECT_THAT(run(SECRET_KEY_ONLY).failures, testing::IsEmpty()); +} diff --git a/test/utils/statetest_loader.cpp b/test/utils/statetest_loader.cpp index 2d282e54d7..ebadfd883c 100644 --- a/test/utils/statetest_loader.cpp +++ b/test/utils/statetest_loader.cpp @@ -7,6 +7,7 @@ #include "stdx/utility.hpp" #include "utils.hpp" #include +#include #include #include @@ -380,6 +381,21 @@ static void from_json_tx_common(const json::json& j, state::Transaction& o) } } +/// Derives the address of the secret key a state test signs its transaction with. +/// The scalar multiplication is not constant-time, which is fine for public test keys. +static address secret_key_to_address(const json::json& j_secret_key) +{ + using namespace crypto::secp256k1; + static constexpr AffinePoint G{ + 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798_u256, + 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8_u256}; + + const auto secret_key = from_json(j_secret_key); + if (secret_key == 0 || secret_key >= Curve::ORDER) + throw std::invalid_argument("invalid secretKey: " + j_secret_key.get()); + return to_address(crypto::ecc::to_affine(crypto::ecc::mul(G, secret_key))); +} + template <> state::Transaction from_json(const json::json& j) { @@ -425,6 +441,10 @@ static void from_json(const json::json& j, TestMultiTransaction& o) { from_json_tx_common(j, o); + // Without `sender`, the sender is the owner of `secretKey`, as in go-ethereum. + if (const auto it = j.find("secretKey"); it != j.end() && !j.contains("sender")) + o.sender = secret_key_to_address(*it); + for (const auto& j_data : j.at("data")) o.inputs.emplace_back(from_json(j_data)); From f340a1d551d3e9603bb09866963b75c36819a5b4 Mon Sep 17 00:00:00 2001 From: zexoverz Date: Sat, 26 Sep 2026 23:14:36 +0900 Subject: [PATCH 2/2] test: Pin the secretKey sender precedence and key range --- test/unittests/statetest_loader_test.cpp | 34 ++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/unittests/statetest_loader_test.cpp b/test/unittests/statetest_loader_test.cpp index cbe8f05212..7aeec420bb 100644 --- a/test/unittests/statetest_loader_test.cpp +++ b/test/unittests/statetest_loader_test.cpp @@ -162,6 +162,40 @@ TEST(statetest_loader, load_minimal_test) EXPECT_EQ(st.input_labels.size(), 0); } +TEST(statetest_loader, sender_from_secret_key) +{ + // The first account of ethereum/tests and its secret key. + static constexpr auto KEY = + "0x45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8"; + static constexpr auto KEY_OWNER = 0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b_address; + + const auto load_sender = [](const std::string& sender_fields) { + std::istringstream s{R"({"test": { + "pre": {}, + "transaction": {"gasPrice": "", "to": "", "data": null, "gasLimit": "0", + "value": null, "nonce": "0", )" + + sender_fields + R"(}, + "post": {}, + "env": {"currentNumber": "0", "currentTimestamp": "0", "currentGasLimit": "0", + "currentCoinbase": ""} + }})"}; + return load_state_tests(s).at(0).multi_tx.sender; + }; + + EXPECT_EQ(load_sender(R"("secretKey": ")" + std::string{KEY} + '"'), KEY_OWNER); + // `sender` takes precedence, as in go-ethereum. + EXPECT_EQ(load_sender(R"("sender": "0xa0a1", "secretKey": ")" + std::string{KEY} + '"'), + 0xa0a1_address); + EXPECT_THAT([&] { load_sender(R"("secretKey": "0x00")"); }, + ThrowsMessage("invalid secretKey: 0x00")); + EXPECT_THAT( + [&] { + load_sender( + R"("secretKey": "0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141")"); + }, + ThrowsMessage(HasSubstr("invalid secretKey"))); +} + TEST(statetest_loader, validate_state_zero_storage_slot) { TestState state{{0xadd4_address, {.storage = {{0x01_bytes32, 0x00_bytes32}}}}};