Skip to content

Commit b76a4f8

Browse files
authored
test: Match blockchain test exceptions (#1556)
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 b76a4f8

4 files changed

Lines changed: 108 additions & 37 deletions

File tree

test/blockchaintest/blockchaintest_runner.cpp

Lines changed: 51 additions & 35 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,104 @@ 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

133-
// Fail if parent header was not found.
137+
// Fail if parent header was not found: the block references a parent that is neither the
138+
// genesis nor any previously-accepted block (an unknown or rejected parent).
134139
if (parent_header == nullptr)
135-
return false;
140+
return make_error_code(INVALID_BLOCK_PARENT);
136141

137142
if (test_block.block_info.number != parent_header->block_number + 1)
138-
return false;
143+
return make_error_code(INVALID_BLOCK_NUMBER);
139144

140145
if (test_block.block_info.gas_used > test_block.block_info.gas_limit)
141-
return false;
146+
return make_error_code(INCORRECT_BLOCK_FORMAT);
142147

143148
// Some tests have gas limit at INT64_MAX, so we cast to uint64_t to avoid overflow.
144149
const auto parent_header_gas_limit_u64 = static_cast<uint64_t>(parent_header->gas_limit);
145150
const auto test_block_gas_limit_u64 = static_cast<uint64_t>(test_block.block_info.gas_limit);
146151
if (test_block_gas_limit_u64 >=
147152
parent_header_gas_limit_u64 + parent_header_gas_limit_u64 / 1024)
148-
return false;
153+
return make_error_code(INVALID_GASLIMIT);
149154
if (test_block_gas_limit_u64 <=
150155
parent_header_gas_limit_u64 - parent_header_gas_limit_u64 / 1024)
151-
return false;
156+
return make_error_code(INVALID_GASLIMIT);
152157

153158
// Block gas limit minimum from Yellow Paper.
154159
if (test_block.block_info.gas_limit < 5000)
155-
return false;
160+
return make_error_code(INVALID_GASLIMIT);
156161

157162
// FIXME: Some tests have timestamp not fitting into int64_t, type has to be uint64_t.
158163
if (static_cast<uint64_t>(test_block.block_info.timestamp) <=
159164
static_cast<uint64_t>(parent_header->timestamp))
160-
return false;
165+
return make_error_code(INVALID_BLOCK_TIMESTAMP_OLDER_THAN_PARENT);
161166

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;
167+
if (test_block.block_info.difficulty !=
168+
calculate_difficulty(parent_header->difficulty, parent_has_ommers, parent_header->timestamp,
169+
test_block.block_info.timestamp, test_block.block_info.number, rev))
170+
return make_error_code(INCORRECT_BLOCK_FORMAT);
167171

168172
if (rev >= EVMC_PARIS && !test_block.block_info.ommers.empty())
169-
return false;
170-
173+
return make_error_code(INCORRECT_BLOCK_FORMAT);
171174

172175
for (const auto& ommer : test_block.block_info.ommers)
173176
{
174177
// Check that ommer block number difference with current block is within allowed range.
175178
// https://github.com/ethereum/execution-specs/blob/ee73be5c4d83a2e3c358bd14990878002e52ba9e/src/ethereum/gray_glacier/fork.py#L623
176179
if (ommer.delta < 1 || ommer.delta > 6)
177-
return false;
180+
return make_error_code(INCORRECT_BLOCK_FORMAT);
178181
}
179182

180183
if (test_block.block_info.extra_data.size() > 32)
181-
return false;
184+
return make_error_code(INCORRECT_BLOCK_FORMAT);
182185

183186
if (rev >= EVMC_LONDON)
184187
{
185-
const auto calculated_base_fee = state::calc_base_fee(
188+
const auto calculated_base_fee = calc_base_fee(
186189
parent_header->gas_limit, parent_header->gas_used, parent_header->base_fee_per_gas);
187190
if (test_block.block_info.base_fee != calculated_base_fee)
188-
return false;
191+
return make_error_code(INVALID_BASEFEE_PER_GAS);
189192
}
190193

191194
if (rev >= EVMC_CANCUN)
192195
{
193196
// `excess_blob_gas` and `blob_gas_used` mandatory after Cancun and invalid before.
194197
if (!test_block.block_info.excess_blob_gas.has_value() ||
195198
!test_block.block_info.blob_gas_used.has_value())
196-
return false;
199+
return make_error_code(INCORRECT_BLOCK_FORMAT);
197200

198201
// Check that the excess blob gas was updated correctly.
199202
// According to EIP-7918 current blocks params (`rev`) should be used for parent base fee
200203
// calculation.
201204
const auto parent_blob_base_fee =
202-
state::compute_blob_gas_price(blob_params, parent_header->excess_blob_gas.value_or(0));
205+
compute_blob_gas_price(blob_params, parent_header->excess_blob_gas.value_or(0));
203206
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),
207+
calc_excess_blob_gas(rev, blob_params, parent_header->blob_gas_used.value_or(0),
205208
parent_header->excess_blob_gas.value_or(0), parent_header->base_fee_per_gas,
206209
parent_blob_base_fee))
207-
return false;
210+
return make_error_code(INCORRECT_EXCESS_BLOB_GAS);
208211
}
209212
else
210213
{
211214
if (test_block.block_info.excess_blob_gas.has_value() ||
212215
test_block.block_info.blob_gas_used.has_value())
213-
return false;
216+
return make_error_code(INCORRECT_BLOCK_FORMAT);
214217
}
215218

216219
// Block is invalid if some of the withdrawal fields failed to be parsed.
217220
if (!test_block.withdrawals_parse_success)
218-
return false;
221+
return make_error_code(INCORRECT_BLOCK_FORMAT);
219222

220223
if (rev >= EVMC_OSAKA && test_block.rlp_size > MAX_RLP_BLOCK_SIZE)
221-
return false;
224+
return make_error_code(RLP_BLOCK_LIMIT_EXCEEDED);
222225

223-
return true;
226+
return {};
224227
}
225228

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

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

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

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

test/state/errors.hpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ 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,
43+
INVALID_BLOCK_PARENT,
44+
INVALID_BLOCK_NUMBER,
3545
};
3646

3747
/// Obtains a reference to the static error category object for evmone errors.
@@ -87,6 +97,22 @@ inline const std::error_category& evmone_category() noexcept
8797
return "max gas limit exceeded";
8898
case UNKNOWN_ERROR:
8999
return "Unknown error";
100+
case INCORRECT_BLOCK_FORMAT:
101+
return "BlockException.INCORRECT_BLOCK_FORMAT";
102+
case INVALID_GASLIMIT:
103+
return "BlockException.INVALID_GASLIMIT";
104+
case INVALID_BASEFEE_PER_GAS:
105+
return "BlockException.INVALID_BASEFEE_PER_GAS";
106+
case INCORRECT_EXCESS_BLOB_GAS:
107+
return "BlockException.INCORRECT_EXCESS_BLOB_GAS";
108+
case RLP_BLOCK_LIMIT_EXCEEDED:
109+
return "BlockException.RLP_BLOCK_LIMIT_EXCEEDED";
110+
case INVALID_BLOCK_TIMESTAMP_OLDER_THAN_PARENT:
111+
return "BlockException.INVALID_BLOCK_TIMESTAMP_OLDER_THAN_PARENT";
112+
case INVALID_BLOCK_PARENT:
113+
return "BlockException.INVALID_BLOCK_PARENT";
114+
case INVALID_BLOCK_NUMBER:
115+
return "BlockException.INVALID_BLOCK_NUMBER";
90116
default:
91117
assert(false);
92118
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)