From feadd8a96bbc7ac457eeab38a7cb1d23c6240105 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Mon, 10 Aug 2026 08:12:57 +0200 Subject: [PATCH] state: Report rejections with the spec exception names Transaction errors carried prose messages ("nonce too high") while block errors already carried the execution-spec-tests exception names, and the state test exporter translated the prose into a third vocabulary, the retesteth TR_* names. Comparing a rejection against a fixture therefore worked one way for blocks and another for transactions. Every message is now the exception name the specs use for that rule, so the comparison is the same on both sides and get_invalid_tx_message() is gone. The error codes whose names diverged from the specs for no reason are renamed with them; the ones that describe the rule better than the spec name does (nonce too high/low, the type-3 and type-4 rules, the chain id) keep their name, as do the two rules the specs do not name at all. INVALID_BLOCK_PARENT is renamed to UNKNOWN_PARENT, the name the specs actually use: no fixture could ever have matched the old one. --- test/blockchaintest/blockchaintest_runner.cpp | 2 +- test/integration/statetest/CMakeLists.txt | 4 +- test/integration/t8n/CMakeLists.txt | 2 +- test/state/errors.hpp | 94 ++++++++++--------- test/state/state.cpp | 24 ++--- test/unittests/state_transition_tx_test.cpp | 2 +- test/unittests/state_tx_test.cpp | 16 ++-- test/utils/statetest.hpp | 3 - test/utils/statetest_export.cpp | 58 +----------- 9 files changed, 78 insertions(+), 127 deletions(-) diff --git a/test/blockchaintest/blockchaintest_runner.cpp b/test/blockchaintest/blockchaintest_runner.cpp index 1447cc0226..1908672191 100644 --- a/test/blockchaintest/blockchaintest_runner.cpp +++ b/test/blockchaintest/blockchaintest_runner.cpp @@ -37,7 +37,7 @@ std::error_code validate_block(evmc_revision rev, state::BlobParams blob_params, // Fail if parent header was not found: the block references a parent that is neither the // genesis nor any previously-accepted block (an unknown or rejected parent). if (parent_header == nullptr) - return make_error_code(INVALID_BLOCK_PARENT); + return make_error_code(UNKNOWN_PARENT); if (test_block.block_info.number != parent_header->block_number + 1) return make_error_code(INVALID_BLOCK_NUMBER); diff --git a/test/integration/statetest/CMakeLists.txt b/test/integration/statetest/CMakeLists.txt index e18125b287..1548a6c7fa 100644 --- a/test/integration/statetest/CMakeLists.txt +++ b/test/integration/statetest/CMakeLists.txt @@ -113,7 +113,7 @@ add_test( ) set_tests_properties( ${PREFIX}/tx_invalid_nonce PROPERTIES - PASS_REGULAR_EXPRESSION "unexpected invalid transaction: nonce too high" + PASS_REGULAR_EXPRESSION "unexpected invalid transaction: TransactionException.NONCE_MISMATCH_TOO_HIGH" ) add_test( @@ -122,7 +122,7 @@ add_test( ) set_tests_properties( ${PREFIX}/tx_invalid_signature PROPERTIES - PASS_REGULAR_EXPRESSION "unexpected invalid transaction: invalid transaction signature" + PASS_REGULAR_EXPRESSION "unexpected invalid transaction: TransactionException.INVALID_SIGNATURE_VRS" ) add_test( diff --git a/test/integration/t8n/CMakeLists.txt b/test/integration/t8n/CMakeLists.txt index d658e14573..ae83fe2aeb 100644 --- a/test/integration/t8n/CMakeLists.txt +++ b/test/integration/t8n/CMakeLists.txt @@ -97,7 +97,7 @@ add_test( string( JOIN ".*" EXPECTED_OUT # Create blob transaction should be rejected: - [=["error": "blob transaction must not be a create transaction"]=] + [=["error": "TransactionException.TYPE_3_TX_CONTRACT_CREATION"]=] ) set_tests_properties( ${PREFIX}/${TEST_CASE}/out.json PROPERTIES diff --git a/test/state/errors.hpp b/test/state/errors.hpp index 3054fac14b..2a66edd80b 100644 --- a/test/state/errors.hpp +++ b/test/state/errors.hpp @@ -9,28 +9,34 @@ namespace evmone::state { +/// The reasons a transaction or a block is rejected. +/// +/// The message of each is the execution-spec-tests exception name for the same rule, so a test can +/// compare a rejection against the `expectException` its fixture states. Where the specs name more +/// than one exception for a rule, the message is the canonical one and the test harness carries the +/// alternatives; the few rules the specs do not name at all keep a plain message. enum ErrorCode : int // NOLINT(*-use-enum-class) { SUCCESS = 0, INTRINSIC_GAS_TOO_LOW, - TX_TYPE_NOT_SUPPORTED, - INSUFFICIENT_FUNDS, - NONCE_HAS_MAX_VALUE, + TYPE_NOT_SUPPORTED, + INSUFFICIENT_ACCOUNT_FUNDS, + NONCE_IS_MAX, NONCE_TOO_HIGH, NONCE_TOO_LOW, - TIP_GT_FEE_CAP, - FEE_CAP_LESS_THAN_BLOCKS, - BLOB_FEE_CAP_LESS_THAN_BLOCKS, - GAS_LIMIT_REACHED, + PRIORITY_GREATER_THAN_MAX_FEE_PER_GAS, + INSUFFICIENT_MAX_FEE_PER_GAS, + INSUFFICIENT_MAX_FEE_PER_BLOB_GAS, + GAS_ALLOWANCE_EXCEEDED, SENDER_NOT_EOA, - INIT_CODE_SIZE_LIMIT_EXCEEDED, + INITCODE_SIZE_EXCEEDED, CREATE_BLOB_TX, EMPTY_BLOB_HASHES_LIST, INVALID_BLOB_HASH_VERSION, BLOB_GAS_LIMIT_EXCEEDED, CREATE_SET_CODE_TX, EMPTY_AUTHORIZATION_LIST, - MAX_GAS_LIMIT_EXCEEDED, + GAS_LIMIT_EXCEEDS_MAXIMUM, INVALID_CHAIN_ID, INVALID_ENCODING, INVALID_SIGNATURE, @@ -43,7 +49,7 @@ enum ErrorCode : int // NOLINT(*-use-enum-class) INCORRECT_EXCESS_BLOB_GAS, RLP_BLOCK_LIMIT_EXCEEDED, INVALID_BLOCK_TIMESTAMP_OLDER_THAN_PARENT, - INVALID_BLOCK_PARENT, + UNKNOWN_PARENT, INVALID_BLOCK_NUMBER, // Block requests collection (EIP-7685). @@ -66,51 +72,53 @@ inline const std::error_category& evmone_category() noexcept case SUCCESS: return ""; case INTRINSIC_GAS_TOO_LOW: - return "intrinsic gas too low"; - case TX_TYPE_NOT_SUPPORTED: - return "transaction type not supported"; - case INSUFFICIENT_FUNDS: - return "insufficient funds for gas * price + value"; - case NONCE_HAS_MAX_VALUE: - return "nonce has max value:"; + return "TransactionException.INTRINSIC_GAS_TOO_LOW"; + case TYPE_NOT_SUPPORTED: + return "TransactionException.TYPE_NOT_SUPPORTED"; + case INSUFFICIENT_ACCOUNT_FUNDS: + return "TransactionException.INSUFFICIENT_ACCOUNT_FUNDS"; + case NONCE_IS_MAX: + return "TransactionException.NONCE_IS_MAX"; case NONCE_TOO_HIGH: - return "nonce too high"; + return "TransactionException.NONCE_MISMATCH_TOO_HIGH"; case NONCE_TOO_LOW: - return "nonce too low"; - case TIP_GT_FEE_CAP: - return "max priority fee per gas higher than max fee per gas"; - case FEE_CAP_LESS_THAN_BLOCKS: - return "max fee per gas less than block base fee"; - case BLOB_FEE_CAP_LESS_THAN_BLOCKS: - return "max blob fee per gas less than block base fee"; - case GAS_LIMIT_REACHED: - return "gas limit reached"; + return "TransactionException.NONCE_MISMATCH_TOO_LOW"; + case PRIORITY_GREATER_THAN_MAX_FEE_PER_GAS: + return "TransactionException.PRIORITY_GREATER_THAN_MAX_FEE_PER_GAS"; + case INSUFFICIENT_MAX_FEE_PER_GAS: + return "TransactionException.INSUFFICIENT_MAX_FEE_PER_GAS"; + case INSUFFICIENT_MAX_FEE_PER_BLOB_GAS: + return "TransactionException.INSUFFICIENT_MAX_FEE_PER_BLOB_GAS"; + case GAS_ALLOWANCE_EXCEEDED: + return "TransactionException.GAS_ALLOWANCE_EXCEEDED"; case SENDER_NOT_EOA: - return "sender not an eoa:"; - case INIT_CODE_SIZE_LIMIT_EXCEEDED: - return "max initcode size exceeded"; + return "TransactionException.SENDER_NOT_EOA"; + case INITCODE_SIZE_EXCEEDED: + return "TransactionException.INITCODE_SIZE_EXCEEDED"; case CREATE_BLOB_TX: - return "blob transaction must not be a create transaction"; + return "TransactionException.TYPE_3_TX_CONTRACT_CREATION"; case EMPTY_BLOB_HASHES_LIST: - return "empty blob hashes list"; + return "TransactionException.TYPE_3_TX_ZERO_BLOBS"; case INVALID_BLOB_HASH_VERSION: - return "invalid blob hash version"; + return "TransactionException.TYPE_3_TX_INVALID_BLOB_VERSIONED_HASH"; case BLOB_GAS_LIMIT_EXCEEDED: - return "blob gas limit exceeded"; + return "TransactionException.TYPE_3_TX_BLOB_COUNT_EXCEEDED"; case CREATE_SET_CODE_TX: - return "set code transaction must not be a create transaction"; + return "TransactionException.TYPE_4_TX_CONTRACT_CREATION"; case EMPTY_AUTHORIZATION_LIST: - return "empty authorization list"; - case MAX_GAS_LIMIT_EXCEEDED: - return "max gas limit exceeded"; + return "TransactionException.TYPE_4_EMPTY_AUTHORIZATION_LIST"; + case GAS_LIMIT_EXCEEDS_MAXIMUM: + return "TransactionException.GAS_LIMIT_EXCEEDS_MAXIMUM"; case INVALID_CHAIN_ID: - return "invalid transaction chain id"; + return "TransactionException.INVALID_CHAINID"; case INVALID_ENCODING: + // The execution specs name every way an encoding can be malformed separately + // (RLP_*), so there is no single constant standing for this one. return "invalid transaction encoding"; case INVALID_SIGNATURE: - return "invalid transaction signature"; + return "TransactionException.INVALID_SIGNATURE_VRS"; case UNKNOWN_ERROR: - return "Unknown error"; + return "unknown error"; case INCORRECT_BLOCK_FORMAT: return "BlockException.INCORRECT_BLOCK_FORMAT"; case INVALID_GASLIMIT: @@ -123,8 +131,8 @@ inline const std::error_category& evmone_category() noexcept return "BlockException.RLP_BLOCK_LIMIT_EXCEEDED"; case INVALID_BLOCK_TIMESTAMP_OLDER_THAN_PARENT: return "BlockException.INVALID_BLOCK_TIMESTAMP_OLDER_THAN_PARENT"; - case INVALID_BLOCK_PARENT: - return "BlockException.INVALID_BLOCK_PARENT"; + case UNKNOWN_PARENT: + return "BlockException.UNKNOWN_PARENT"; case INVALID_BLOCK_NUMBER: return "BlockException.INVALID_BLOCK_NUMBER"; case INVALID_DEPOSIT_EVENT_LAYOUT: diff --git a/test/state/state.cpp b/test/state/state.cpp index 44f04ca1e1..97befc4445 100644 --- a/test/state/state.cpp +++ b/test/state/state.cpp @@ -445,7 +445,7 @@ std::variant validate_transaction( { case Transaction::Type::blob: if (rev < EVMC_CANCUN) - return make_error_code(TX_TYPE_NOT_SUPPORTED); + return make_error_code(TYPE_NOT_SUPPORTED); if (!tx.to.has_value()) return make_error_code(CREATE_BLOB_TX); if (tx.blob_hashes.empty()) @@ -455,7 +455,7 @@ std::variant validate_transaction( assert(block.blob_base_fee.has_value()); if (tx.max_blob_gas_price < *block.blob_base_fee) - return make_error_code(BLOB_FEE_CAP_LESS_THAN_BLOCKS); + return make_error_code(INSUFFICIENT_MAX_FEE_PER_BLOB_GAS); if (std::ranges::any_of(tx.blob_hashes, [](const auto& h) { return h.bytes[0] != 0x01; })) return make_error_code(INVALID_BLOB_HASH_VERSION); @@ -465,7 +465,7 @@ std::variant validate_transaction( case Transaction::Type::set_code: if (rev < EVMC_PRAGUE) - return make_error_code(TX_TYPE_NOT_SUPPORTED); + return make_error_code(TYPE_NOT_SUPPORTED); if (!tx.to.has_value()) return make_error_code(CREATE_SET_CODE_TX); if (tx.authorization_list.empty()) @@ -481,15 +481,15 @@ std::variant validate_transaction( case Transaction::Type::blob: case Transaction::Type::eip1559: if (rev < EVMC_LONDON) - return make_error_code(TX_TYPE_NOT_SUPPORTED); + return make_error_code(TYPE_NOT_SUPPORTED); if (tx.max_priority_gas_price > tx.max_gas_price) - return make_error_code(TIP_GT_FEE_CAP); // Priority gas price is too high. + return make_error_code(PRIORITY_GREATER_THAN_MAX_FEE_PER_GAS); [[fallthrough]]; case Transaction::Type::access_list: if (rev < EVMC_BERLIN) - return make_error_code(TX_TYPE_NOT_SUPPORTED); + return make_error_code(TYPE_NOT_SUPPORTED); [[fallthrough]]; case Transaction::Type::legacy:; @@ -498,13 +498,13 @@ std::variant validate_transaction( assert(tx.max_priority_gas_price <= tx.max_gas_price); if (rev >= EVMC_OSAKA && tx.gas_limit > MAX_TX_GAS_LIMIT) - return make_error_code(MAX_GAS_LIMIT_EXCEEDED); + return make_error_code(GAS_LIMIT_EXCEEDS_MAXIMUM); if (tx.gas_limit > block_gas_left) - return make_error_code(GAS_LIMIT_REACHED); + return make_error_code(GAS_ALLOWANCE_EXCEEDED); if (tx.max_gas_price < block.base_fee) - return make_error_code(FEE_CAP_LESS_THAN_BLOCKS); + return make_error_code(INSUFFICIENT_MAX_FEE_PER_GAS); // We need some information about the sender so lookup the account in the state. // TODO: During transaction execution this account will be also needed, so we may pass it along. @@ -516,7 +516,7 @@ std::variant validate_transaction( return make_error_code(SENDER_NOT_EOA); // Origin must not be a contract (EIP-3607). if (sender_acc.nonce == MAX_NONCE) // Nonce value limit (EIP-2681). - return make_error_code(NONCE_HAS_MAX_VALUE); + return make_error_code(NONCE_IS_MAX); if (sender_acc.nonce < tx.nonce) return make_error_code(NONCE_TOO_HIGH); @@ -526,7 +526,7 @@ std::variant validate_transaction( // initcode size is limited by EIP-3860. if (rev >= EVMC_SHANGHAI && !tx.to.has_value() && tx.data.size() > MAX_INITCODE_SIZE) - return make_error_code(INIT_CODE_SIZE_LIMIT_EXCEEDED); + return make_error_code(INITCODE_SIZE_EXCEEDED); // Compute and check if sender has enough balance for the theoretical maximum transaction cost. // Note this is different from tx_max_cost computed with effective gas price later. @@ -541,7 +541,7 @@ std::variant validate_transaction( max_total_fee += total_blob_gas * tx.max_blob_gas_price; } if (sender_acc.balance < max_total_fee) - return make_error_code(INSUFFICIENT_FUNDS); + return make_error_code(INSUFFICIENT_ACCOUNT_FUNDS); const auto [intrinsic_cost, min_cost] = compute_tx_intrinsic_cost(rev, tx); if (tx.gas_limit < std::max(intrinsic_cost, min_cost)) diff --git a/test/unittests/state_transition_tx_test.cpp b/test/unittests/state_transition_tx_test.cpp index b32eb1b9e5..b0d9d6afe2 100644 --- a/test/unittests/state_transition_tx_test.cpp +++ b/test/unittests/state_transition_tx_test.cpp @@ -46,7 +46,7 @@ TEST_F(state_transition, invalid_tx_non_existing_sender) tx.nonce = 0; pre.erase(Sender); - expect.tx_error = INSUFFICIENT_FUNDS; + expect.tx_error = INSUFFICIENT_ACCOUNT_FUNDS; expect.post[Sender].exists = false; } diff --git a/test/unittests/state_tx_test.cpp b/test/unittests/state_tx_test.cpp index 64d8f5839a..1db2b90934 100644 --- a/test/unittests/state_tx_test.cpp +++ b/test/unittests/state_tx_test.cpp @@ -32,13 +32,13 @@ TEST(state_tx, validate_nonce) EXPECT_EQ(std::get( validate_transaction(state, block, tx, EVMC_BERLIN, block.gas_limit, 0)) .message(), - "nonce too low"); + "TransactionException.NONCE_MISMATCH_TOO_LOW"); tx.nonce = 2; EXPECT_EQ(std::get( validate_transaction(state, block, tx, EVMC_BERLIN, block.gas_limit, 0)) .message(), - "nonce too high"); + "TransactionException.NONCE_MISMATCH_TOO_HIGH"); } TEST(state_tx, validate_sender) @@ -61,14 +61,14 @@ TEST(state_tx, validate_sender) EXPECT_EQ(std::get( validate_transaction(state, block, tx, EVMC_LONDON, block.gas_limit, 0)) .message(), - "max fee per gas less than block base fee"); + "TransactionException.INSUFFICIENT_MAX_FEE_PER_GAS"); tx.max_gas_price = block.base_fee; EXPECT_EQ(std::get( validate_transaction(state, block, tx, EVMC_LONDON, block.gas_limit, 0)) .message(), - "insufficient funds for gas * price + value"); + "TransactionException.INSUFFICIENT_ACCOUNT_FUNDS"); } TEST(state_tx, validate_blob_tx) @@ -93,7 +93,7 @@ TEST(state_tx, validate_blob_tx) static_cast(max_blob_gas_per_block(get_blob_params(EVMC_CANCUN))); EXPECT_EQ(std::get(validate_transaction( state, block, tx, EVMC_SHANGHAI, block.gas_limit, blob_gas_limit)), - make_error_code(ErrorCode::TX_TYPE_NOT_SUPPORTED)); + make_error_code(ErrorCode::TYPE_NOT_SUPPORTED)); EXPECT_EQ(std::get(validate_transaction(state, block, tx, EVMC_CANCUN, block.gas_limit, blob_gas_limit)) @@ -117,8 +117,8 @@ TEST(state_tx, validate_blob_tx) validate_transaction(state, block, tx, EVMC_CANCUN, block.gas_limit, g)); }; - EXPECT_EQ( - expect_error(blob_gas_limit), make_error_code(ErrorCode::BLOB_FEE_CAP_LESS_THAN_BLOCKS)); + EXPECT_EQ(expect_error(blob_gas_limit), + make_error_code(ErrorCode::INSUFFICIENT_MAX_FEE_PER_BLOB_GAS)); tx.max_blob_gas_price = 1; tx.blob_hashes.push_back( @@ -252,5 +252,5 @@ TEST(state_tx, max_gas_limit_exceeded) EXPECT_EQ(std::get( validate_transaction(state, block, tx, EVMC_OSAKA, block.gas_limit, 0)), - make_error_code(ErrorCode::MAX_GAS_LIMIT_EXCEEDED)); + make_error_code(ErrorCode::GAS_LIMIT_EXCEEDS_MAXIMUM)); } diff --git a/test/utils/statetest.hpp b/test/utils/statetest.hpp index 3f554daf5d..72639e73fc 100644 --- a/test/utils/statetest.hpp +++ b/test/utils/statetest.hpp @@ -145,9 +145,6 @@ json::json to_state_test(std::string_view test_name, const state::BlockInfo& blo state::Transaction& tx, const TestState& pre, evmc_revision rev, const std::variant& res, const TestState& post); -/// Returns the standardized error message for the transaction validation error. -[[nodiscard]] std::string get_invalid_tx_message(state::ErrorCode errc) noexcept; - std::vector load_state_tests(std::istream& input); diff --git a/test/utils/statetest_export.cpp b/test/utils/statetest_export.cpp index 11c3cf1742..2ab10415c3 100644 --- a/test/utils/statetest_export.cpp +++ b/test/utils/statetest_export.cpp @@ -24,60 +24,6 @@ std::string_view to_test_fork_name(evmc_revision rev) noexcept } } // namespace -[[nodiscard]] std::string get_invalid_tx_message(state::ErrorCode errc) noexcept -{ - using namespace state; - switch (errc) - { - case SUCCESS: - return ""; - case INTRINSIC_GAS_TOO_LOW: - return "TR_IntrinsicGas"; - case TX_TYPE_NOT_SUPPORTED: - return "TR_TypeNotSupported"; - case INSUFFICIENT_FUNDS: - return "TR_NoFunds"; - case NONCE_HAS_MAX_VALUE: - return "TR_NonceHasMaxValue:"; - case NONCE_TOO_HIGH: - return "TR_NonceTooHigh"; - case NONCE_TOO_LOW: - return "TR_NonceTooLow"; - case TIP_GT_FEE_CAP: - return "TR_TipGtFeeCap"; - case FEE_CAP_LESS_THAN_BLOCKS: - return "TR_FeeCapLessThanBlocks"; - case GAS_LIMIT_REACHED: - return "TR_GasLimitReached"; - case SENDER_NOT_EOA: - return "SenderNotEOA"; - case INIT_CODE_SIZE_LIMIT_EXCEEDED: - return "TR_InitCodeLimitExceeded"; - case CREATE_BLOB_TX: - return "TR_BLOBCREATE"; - case EMPTY_BLOB_HASHES_LIST: - return "TR_EMPTYBLOB"; - case INVALID_BLOB_HASH_VERSION: - return "TR_BLOBVERSION_INVALID"; - case BLOB_GAS_LIMIT_EXCEEDED: - return "TR_BLOBLIST_OVERSIZE"; - case INVALID_CHAIN_ID: - return "TransactionException.INVALID_CHAINID"; - case INVALID_ENCODING: - // EEST names every way an encoding can be malformed separately (RLP_*), so there is no - // single constant to export here; the plain message follows UNKNOWN_ERROR below. - return "Invalid transaction encoding"; - case INVALID_SIGNATURE: - return "TransactionException.INVALID_SIGNATURE_VRS"; - case UNKNOWN_ERROR: - return "Unknown error"; - default: - assert(false); - return "Wrong error code"; - } -} - - json::json to_json(const TestState& state) { json::json j = json::json::object(); @@ -195,8 +141,8 @@ json::json to_state_test(std::string_view test_name, const state::BlockInfo& blo if (holds_alternative(res)) { - jpost["expectException"] = get_invalid_tx_message( - static_cast(std::get(res).value())); + // The error message is the execution-spec-tests exception name. + jpost["expectException"] = std::get(res).message(); jpost["logs"] = hex0x(logs_hash(std::vector())); } else