Skip to content

Commit 90571ec

Browse files
committed
test: Match blockchain test block exceptions by reason
The blockchain test runner treated block validity as a bool: for a block expected to be invalid it accepted ANY rejection and never checked that the reason matched the fixture's `expectException`. So a block rejected for the wrong reason still passed. Make `validate_block` return a `std::error_code` instead of `bool`, with new block-level entries appended (at the back, keeping existing values stable) to `state::ErrorCode`. Each entry's message is the matching execution-spec-tests `BlockException` constant (INVALID_GASLIMIT, INVALID_BASEFEE_PER_GAS, INCORRECT_EXCESS_BLOB_GAS, RLP_BLOCK_LIMIT_EXCEEDED, INVALID_BLOCK_TIMESTAMP_OLDER_THAN_PARENT, INCORRECT_BLOCK_FORMAT for the rest). The loader captures the `expectException` string; the runner substring-matches the actual error's constant against it, which also handles the fixtures' `|`-separated alternatives. Legacy ethereum/tests fixtures use older, sometimes finer-grained exception names (e.g. uncles after Paris, a number gap, a wrong base fee). The loader maps those (many-to-one) onto the `BlockException` constant evmone reports, so they match by reason as well.
1 parent 1c0a9b9 commit 90571ec

4 files changed

Lines changed: 100 additions & 36 deletions

File tree

test/blockchaintest/blockchaintest_runner.cpp

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
#include "blockchaintest_runner.hpp"
66
#include <gtest/gtest.h>
7+
#include <test/state/errors.hpp>
78
#include <test/state/ethash_difficulty.hpp>
89
#include <test/state/requests.hpp>
910
#include <test/utils/mpt_hash.hpp>
@@ -125,102 +126,103 @@ TransitionResult apply_block(const TestState& state, evmc::VM& vm, const state::
125126
bloom, blob_gas_left, std::move(block_state)};
126127
}
127128

128-
bool validate_block(evmc_revision rev, state::BlobParams blob_params, const TestBlock& test_block,
129-
const BlockHeader* parent_header, bool parent_has_ommers) noexcept
129+
/// Validates block-level validity unrelated to individual transactions.
130+
///
131+
/// Returns an empty error_code if the block is valid, otherwise the specific validation error.
132+
std::error_code validate_block(evmc_revision rev, state::BlobParams blob_params,
133+
const TestBlock& test_block, const BlockHeader* parent_header, bool parent_has_ommers) noexcept
130134
{
131-
// NOTE: includes only block validity unrelated to individual txs. See `apply_block`.
135+
using namespace state;
132136

133137
// Fail if parent header was not found.
134138
if (parent_header == nullptr)
135-
return false;
139+
return make_error_code(INCORRECT_BLOCK_FORMAT);
136140

137141
if (test_block.block_info.number != parent_header->block_number + 1)
138-
return false;
142+
return make_error_code(INCORRECT_BLOCK_FORMAT);
139143

140144
if (test_block.block_info.gas_used > test_block.block_info.gas_limit)
141-
return false;
145+
return make_error_code(INCORRECT_BLOCK_FORMAT);
142146

143147
// Some tests have gas limit at INT64_MAX, so we cast to uint64_t to avoid overflow.
144148
const auto parent_header_gas_limit_u64 = static_cast<uint64_t>(parent_header->gas_limit);
145149
const auto test_block_gas_limit_u64 = static_cast<uint64_t>(test_block.block_info.gas_limit);
146150
if (test_block_gas_limit_u64 >=
147151
parent_header_gas_limit_u64 + parent_header_gas_limit_u64 / 1024)
148-
return false;
152+
return make_error_code(INVALID_GASLIMIT);
149153
if (test_block_gas_limit_u64 <=
150154
parent_header_gas_limit_u64 - parent_header_gas_limit_u64 / 1024)
151-
return false;
155+
return make_error_code(INVALID_GASLIMIT);
152156

153157
// Block gas limit minimum from Yellow Paper.
154158
if (test_block.block_info.gas_limit < 5000)
155-
return false;
159+
return make_error_code(INVALID_GASLIMIT);
156160

157161
// FIXME: Some tests have timestamp not fitting into int64_t, type has to be uint64_t.
158162
if (static_cast<uint64_t>(test_block.block_info.timestamp) <=
159163
static_cast<uint64_t>(parent_header->timestamp))
160-
return false;
164+
return make_error_code(INVALID_BLOCK_TIMESTAMP_OLDER_THAN_PARENT);
161165

162-
if (test_block.block_info.difficulty != state::calculate_difficulty(parent_header->difficulty,
163-
parent_has_ommers, parent_header->timestamp,
164-
test_block.block_info.timestamp,
165-
test_block.block_info.number, rev))
166-
return false;
166+
if (test_block.block_info.difficulty !=
167+
calculate_difficulty(parent_header->difficulty, parent_has_ommers, parent_header->timestamp,
168+
test_block.block_info.timestamp, test_block.block_info.number, rev))
169+
return make_error_code(INCORRECT_BLOCK_FORMAT);
167170

168171
if (rev >= EVMC_PARIS && !test_block.block_info.ommers.empty())
169-
return false;
170-
172+
return make_error_code(INCORRECT_BLOCK_FORMAT);
171173

172174
for (const auto& ommer : test_block.block_info.ommers)
173175
{
174176
// Check that ommer block number difference with current block is within allowed range.
175177
// https://github.com/ethereum/execution-specs/blob/ee73be5c4d83a2e3c358bd14990878002e52ba9e/src/ethereum/gray_glacier/fork.py#L623
176178
if (ommer.delta < 1 || ommer.delta > 6)
177-
return false;
179+
return make_error_code(INCORRECT_BLOCK_FORMAT);
178180
}
179181

180182
if (test_block.block_info.extra_data.size() > 32)
181-
return false;
183+
return make_error_code(INCORRECT_BLOCK_FORMAT);
182184

183185
if (rev >= EVMC_LONDON)
184186
{
185-
const auto calculated_base_fee = state::calc_base_fee(
187+
const auto calculated_base_fee = calc_base_fee(
186188
parent_header->gas_limit, parent_header->gas_used, parent_header->base_fee_per_gas);
187189
if (test_block.block_info.base_fee != calculated_base_fee)
188-
return false;
190+
return make_error_code(INVALID_BASEFEE_PER_GAS);
189191
}
190192

191193
if (rev >= EVMC_CANCUN)
192194
{
193195
// `excess_blob_gas` and `blob_gas_used` mandatory after Cancun and invalid before.
194196
if (!test_block.block_info.excess_blob_gas.has_value() ||
195197
!test_block.block_info.blob_gas_used.has_value())
196-
return false;
198+
return make_error_code(INCORRECT_BLOCK_FORMAT);
197199

198200
// Check that the excess blob gas was updated correctly.
199201
// According to EIP-7918 current blocks params (`rev`) should be used for parent base fee
200202
// calculation.
201203
const auto parent_blob_base_fee =
202-
state::compute_blob_gas_price(blob_params, parent_header->excess_blob_gas.value_or(0));
204+
compute_blob_gas_price(blob_params, parent_header->excess_blob_gas.value_or(0));
203205
if (*test_block.block_info.excess_blob_gas !=
204-
state::calc_excess_blob_gas(rev, blob_params, parent_header->blob_gas_used.value_or(0),
206+
calc_excess_blob_gas(rev, blob_params, parent_header->blob_gas_used.value_or(0),
205207
parent_header->excess_blob_gas.value_or(0), parent_header->base_fee_per_gas,
206208
parent_blob_base_fee))
207-
return false;
209+
return make_error_code(INCORRECT_EXCESS_BLOB_GAS);
208210
}
209211
else
210212
{
211213
if (test_block.block_info.excess_blob_gas.has_value() ||
212214
test_block.block_info.blob_gas_used.has_value())
213-
return false;
215+
return make_error_code(INCORRECT_BLOCK_FORMAT);
214216
}
215217

216218
// Block is invalid if some of the withdrawal fields failed to be parsed.
217219
if (!test_block.withdrawals_parse_success)
218-
return false;
220+
return make_error_code(INCORRECT_BLOCK_FORMAT);
219221

220222
if (rev >= EVMC_OSAKA && test_block.rlp_size > MAX_RLP_BLOCK_SIZE)
221-
return false;
223+
return make_error_code(RLP_BLOCK_LIMIT_EXCEEDED);
222224

223-
return true;
225+
return {};
224226
}
225227

226228
std::optional<int64_t> mining_reward(evmc_revision rev) noexcept
@@ -315,11 +317,13 @@ void run_blockchain_tests(std::span<const BlockchainTest> tests, evmc::VM& vm)
315317
SCOPED_TRACE(std::string{evmc::to_string(rev)} + '/' + std::to_string(case_index) +
316318
'/' + c.name + '/' + std::to_string(test_block.block_info.number));
317319

318-
if (test_block.valid)
320+
const auto block_error =
321+
validate_block(rev, blob_params, test_block, parent_header, parent_has_ommers);
322+
323+
if (test_block.expected_exception.empty())
319324
{
320-
ASSERT_TRUE(
321-
validate_block(rev, blob_params, test_block, parent_header, parent_has_ommers))
322-
<< "Expected block to be valid (validate_block)";
325+
ASSERT_FALSE(block_error)
326+
<< "Expected block to be valid (validate_block): " << block_error.message();
323327

324328
// Block being valid guarantees its parent was found.
325329
assert(parent_data_it != block_data.end());
@@ -377,8 +381,19 @@ void run_blockchain_tests(std::span<const BlockchainTest> tests, evmc::VM& vm)
377381
}
378382
else
379383
{
380-
if (!validate_block(rev, blob_params, test_block, parent_header, parent_has_ommers))
384+
if (block_error)
385+
{
386+
// Block correctly rejected at validation; verify the reason matches the
387+
// fixture's expected exception. The error message is the `BlockException`
388+
// constant; `expected_exception` may list `|`-separated alternatives, so a
389+
// substring search suffices as long as no constant name is a substring of
390+
// another (true for the constants evmone produces).
391+
EXPECT_NE(test_block.expected_exception.find(block_error.message()),
392+
std::string::npos)
393+
<< "Block invalidity reason mismatch: got " << block_error.message()
394+
<< ", expected " << test_block.expected_exception;
381395
continue;
396+
}
382397

383398
// Block being valid guarantees its parent was found.
384399
assert(parent_data_it != block_data.end());

test/state/errors.hpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ enum ErrorCode : int // NOLINT(*-use-enum-class)
3232
EMPTY_AUTHORIZATION_LIST,
3333
MAX_GAS_LIMIT_EXCEEDED,
3434
UNKNOWN_ERROR,
35+
36+
// Block-level validation.
37+
INCORRECT_BLOCK_FORMAT,
38+
INVALID_GASLIMIT,
39+
INVALID_BASEFEE_PER_GAS,
40+
INCORRECT_EXCESS_BLOB_GAS,
41+
RLP_BLOCK_LIMIT_EXCEEDED,
42+
INVALID_BLOCK_TIMESTAMP_OLDER_THAN_PARENT,
3543
};
3644

3745
/// Obtains a reference to the static error category object for evmone errors.
@@ -87,6 +95,18 @@ inline const std::error_category& evmone_category() noexcept
8795
return "max gas limit exceeded";
8896
case UNKNOWN_ERROR:
8997
return "Unknown error";
98+
case INCORRECT_BLOCK_FORMAT:
99+
return "BlockException.INCORRECT_BLOCK_FORMAT";
100+
case INVALID_GASLIMIT:
101+
return "BlockException.INVALID_GASLIMIT";
102+
case INVALID_BASEFEE_PER_GAS:
103+
return "BlockException.INVALID_BASEFEE_PER_GAS";
104+
case INCORRECT_EXCESS_BLOB_GAS:
105+
return "BlockException.INCORRECT_EXCESS_BLOB_GAS";
106+
case RLP_BLOCK_LIMIT_EXCEEDED:
107+
return "BlockException.RLP_BLOCK_LIMIT_EXCEEDED";
108+
case INVALID_BLOCK_TIMESTAMP_OLDER_THAN_PARENT:
109+
return "BlockException.INVALID_BLOCK_TIMESTAMP_OLDER_THAN_PARENT";
90110
default:
91111
assert(false);
92112
return "Wrong error code";

test/utils/blockchaintest.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ struct TestBlock
5151
std::vector<state::Transaction> transactions;
5252
size_t rlp_size = 0;
5353
bool withdrawals_parse_success = true;
54-
bool valid = true;
54+
std::string expected_exception; ///< Empty for valid blocks.
5555

5656
BlockHeader expected_block_header;
5757
};

test/utils/blockchaintest_loader.cpp

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "blockchaintest.hpp"
66
#include "statetest.hpp"
77
#include "utils.hpp"
8+
#include <test/state/errors.hpp>
89

910
namespace evmone::test
1011
{
@@ -136,6 +137,34 @@ static TestBlock load_test_block(
136137

137138
namespace
138139
{
140+
/// Maps a legacy "expectException" value to modern EEST-style exception name.
141+
std::string map_legacy_block_exception(std::string_view expected_exception)
142+
{
143+
using enum state::ErrorCode;
144+
using Entry = std::pair<std::string_view, state::ErrorCode>;
145+
146+
static constexpr Entry LEGACY_MAP[]{
147+
// ethereum/tests (EEST-format):
148+
{"BlockException.IMPORT_IMPOSSIBLE_UNCLES_OVER_PARIS", INCORRECT_BLOCK_FORMAT},
149+
{"BlockException.GAS_USED_OVERFLOW", INCORRECT_BLOCK_FORMAT},
150+
{"BlockException.RLP_STRUCTURES_ENCODING|BlockException.RLP_INVALID_FIELD_OVERFLOW_64",
151+
INCORRECT_BLOCK_FORMAT},
152+
// ethereum/legacytests (pre-EEST):
153+
{"PostParisUncleHashIsNotEmpty", INCORRECT_BLOCK_FORMAT},
154+
{"3675PreParis1559BlockRejected", INCORRECT_BLOCK_FORMAT},
155+
{"InvalidNumber", INCORRECT_BLOCK_FORMAT},
156+
{"InvalidTimestampOlderParent", INVALID_BLOCK_TIMESTAMP_OLDER_THAN_PARENT},
157+
{"TooMuchGasUsed", INCORRECT_BLOCK_FORMAT},
158+
{"UncleParentIsNotAncestor", INCORRECT_BLOCK_FORMAT},
159+
{"InvalidGasLimit2", INVALID_GASLIMIT},
160+
{"1559BlockImportImpossible_BaseFeeWrong", INVALID_BASEFEE_PER_GAS},
161+
};
162+
163+
const auto it = std::ranges::find(LEGACY_MAP, expected_exception, &Entry::first);
164+
return (it != std::end(LEGACY_MAP)) ? state::make_error_code(it->second).message() :
165+
std::string{expected_exception};
166+
}
167+
139168
BlockchainTest load_blockchain_test_case(const std::string& name, const json::json& j)
140169
{
141170
using namespace state;
@@ -165,7 +194,7 @@ BlockchainTest load_blockchain_test_case(const std::string& name, const json::js
165194
"tests with invalidly rlp-encoded blocks are not supported");
166195

167196
auto test_block = load_test_block(el.at("rlp_decoded"), bt.network, bt.blob_schedule);
168-
test_block.valid = false;
197+
test_block.expected_exception = map_legacy_block_exception(it->get<std::string>());
169198
test_block.rlp_size = from_json<bytes>(el.at("rlp")).size();
170199
bt.test_blocks.emplace_back(test_block);
171200
}

0 commit comments

Comments
 (0)