From d957fc2d3deec0151c576a9a891d29ebf59ceac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Bylica?= Date: Tue, 1 Sep 2026 16:02:18 +0200 Subject: [PATCH] test: Run one blockchain test at a time A test which gave up took the rest of its file with it: the early returns left the loop over the whole span, so every test after it never ran. Taking one test lets the caller which has the file's tests do the loop. The index in the failure place goes with the loop, since the case name above it already says which case failed. --- test/blockchaintest/blockchaintest.cpp | 6 +- .../integration/blockchaintest/CMakeLists.txt | 12 + .../blockchaintest/stops_at_one_test.json | 128 +++++ test/unittests/blockchaintest_runner_test.cpp | 2 +- test/utils/blockchaintest.hpp | 4 +- test/utils/blockchaintest_runner.cpp | 515 +++++++++--------- 6 files changed, 399 insertions(+), 268 deletions(-) create mode 100644 test/integration/blockchaintest/stops_at_one_test.json diff --git a/test/blockchaintest/blockchaintest.cpp b/test/blockchaintest/blockchaintest.cpp index 0df9373de9..bf795491e9 100644 --- a/test/blockchaintest/blockchaintest.cpp +++ b/test/blockchaintest/blockchaintest.cpp @@ -32,8 +32,8 @@ bool collect_tests(std::vector& cases, const fs::path& root, cases.push_back( {file.path.string(), [path = file.path, &vm](evmone::test::TestReport& report) { std::ifstream f{path}; - evmone::test::run_blockchain_tests( - evmone::test::load_blockchain_tests(f), vm, report); + for (const auto& test : evmone::test::load_blockchain_tests(f)) + evmone::test::run_blockchain_test(test, vm, report); }}); } } @@ -67,7 +67,7 @@ bool collect_tests(std::vector& cases, const fs::path& root, { cases.push_back( {root.string() + "::" + test.name, [test, &vm](evmone::test::TestReport& report) { - evmone::test::run_blockchain_tests({&test, 1}, vm, report); + evmone::test::run_blockchain_test(test, vm, report); }}); } } diff --git a/test/integration/blockchaintest/CMakeLists.txt b/test/integration/blockchaintest/CMakeLists.txt index 10662377a8..031ba8e0ae 100644 --- a/test/integration/blockchaintest/CMakeLists.txt +++ b/test/integration/blockchaintest/CMakeLists.txt @@ -63,6 +63,18 @@ set_tests_properties( PASS_REGULAR_EXPRESSION "SKIPPED [^\n]*unsupported_rlp\\.json - tests with invalidly" ) +# A test which gives up gives up only itself. Collected from a directory the whole file is one +# test, so the test after the one which stopped has to report too. +add_test( + NAME ${PREFIX}/stops_at_one_test + COMMAND evmone-blockchaintest ${TESTS1} +) +set_tests_properties( + ${PREFIX}/stops_at_one_test PROPERTIES + PASS_REGULAR_EXPRESSION + "a_stops_early:.*block validity.*b_still_runs:.*post state root" +) + # --collect-only lists what would run instead of running it, and --ignore drops a path from that # list. Over a directory a test is listed as its file. add_test( diff --git a/test/integration/blockchaintest/stops_at_one_test.json b/test/integration/blockchaintest/stops_at_one_test.json new file mode 100644 index 0000000000..4a41b2c558 --- /dev/null +++ b/test/integration/blockchaintest/stops_at_one_test.json @@ -0,0 +1,128 @@ +{ + "a_stops_early": { + "network": "Amsterdam", + "genesisBlockHeader": { + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "uncleHash": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "coinbase": "0x0000000000000000000000000000000000000000", + "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "transactionsTrie": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "receiptTrie": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "difficulty": "0x00", + "number": "0x00", + "gasLimit": "0x01000000", + "gasUsed": "0x00", + "timestamp": "0x00", + "extraData": "0x00", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0000000000000000", + "baseFeePerGas": "0x10", + "withdrawalsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "blobGasUsed": "0x00", + "excessBlobGas": "0x00", + "parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "requestsHash": "0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "slotNumber": "0x00", + "hash": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "pre": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "code": "0x", + "nonce": "0x00", + "balance": "0x02540be400" + } + }, + "blocks": [ + { + "rlp": "0xf90285f90259a00000000000000000000000000000000000000000000000000000000000000001a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794000000000000000000000000000000000000c014a09906207bb91a552f2c9036ff75ffebd15d6262fe1f1dc40afa6e047158ee25e5a06e1d1e299aa4bca4b32c05a8bd8cf631d97c7d9b54b1401c373860db01a38efea0c220348cfca03fd761c800a2af15752cee5ba04f836e61f798c4c6222c0cab02b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800184010000008265960c80a000000000000000000000000000000000000000000000000000000000000000008800000000000000000ea056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b4218080a00000000000000000000000000000000000000000000000000000000000000000a0e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855e6a502e30180800e830186a094000000000000000000000000000000000000c0de8080c0800101c0c0", + "blockHeader": { + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "uncleHash": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "coinbase": "0x000000000000000000000000000000000000c014", + "stateRoot": "0x9906207bb91a552f2c9036ff75ffebd15d6262fe1f1dc40afa6e047158ee25e5", + "transactionsTrie": "0x6e1d1e299aa4bca4b32c05a8bd8cf631d97c7d9b54b1401c373860db01a38efe", + "receiptTrie": "0xc220348cfca03fd761c800a2af15752cee5ba04f836e61f798c4c6222c0cab02", + "bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "difficulty": "0x00", + "number": "0x01", + "gasLimit": "0x01000000", + "gasUsed": "0x6596", + "timestamp": "0x00", + "extraData": "0x", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0000000000000000", + "baseFeePerGas": "0x0e", + "withdrawalsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "blobGasUsed": "0x00", + "excessBlobGas": "0x00", + "parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "requestsHash": "0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "slotNumber": "0x00", + "hash": "0x0000000000000000000000000000000000000000000000000000000000000002" + }, + "transactions": [], + "withdrawals": [], + "uncleHeaders": [] + } + ], + "lastblockhash": "0x0000000000000000000000000000000000000000000000000000000000000002", + "postState": {}, + "config": { + "network": "Amsterdam", + "chainid": "0x01" + }, + "genesisRLP": "0x", + "sealEngine": "NoProof", + "_info": { + "comment": "Its block does not advance the timestamp, so it is invalid where the test says it is valid, and the test stops there." + } + }, + "b_still_runs": { + "_info": { + "comment": "Runs only if the test before it stopped by itself rather than taking the rest of the file with it. Its post state is wrong, so reaching it is visible." + }, + "network": "Amsterdam", + "genesisBlockHeader": { + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "uncleHash": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "coinbase": "0x0000000000000000000000000000000000000000", + "stateRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "transactionsTrie": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "receiptTrie": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "bloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "difficulty": "0x00", + "number": "0x00", + "gasLimit": "0x01000000", + "gasUsed": "0x00", + "timestamp": "0x00", + "extraData": "0x00", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0000000000000000", + "baseFeePerGas": "0x10", + "withdrawalsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "blobGasUsed": "0x00", + "excessBlobGas": "0x00", + "parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "requestsHash": "0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "slotNumber": "0x00", + "hash": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "pre": { + "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "code": "0x", + "nonce": "0x00", + "balance": "0x02540be400" + } + }, + "blocks": [], + "lastblockhash": "0x0000000000000000000000000000000000000000000000000000000000000001", + "postState": {}, + "config": { + "network": "Amsterdam", + "chainid": "0x01" + }, + "genesisRLP": "0x", + "sealEngine": "NoProof" + } +} diff --git a/test/unittests/blockchaintest_runner_test.cpp b/test/unittests/blockchaintest_runner_test.cpp index f16abd5ca9..87797bb3d6 100644 --- a/test/unittests/blockchaintest_runner_test.cpp +++ b/test/unittests/blockchaintest_runner_test.cpp @@ -56,7 +56,7 @@ std::vector run(const BlockchainTest& t) std::vector failures; TestReport report{[&](const Failure& failure) { failures.push_back(failure); }}; evmc::VM vm{evmc_create_evmone()}; - run_blockchain_tests({&t, 1}, vm, report); + run_blockchain_test(t, vm, report); return failures; } } // namespace diff --git a/test/utils/blockchaintest.hpp b/test/utils/blockchaintest.hpp index 20321fd6ad..bcb922ef01 100644 --- a/test/utils/blockchaintest.hpp +++ b/test/utils/blockchaintest.hpp @@ -80,6 +80,6 @@ std::vector load_blockchain_tests(std::istream& input); /// Builds the test named @p name in a fixture file from its JSON value @p j. BlockchainTest make_blockchain_test(const std::string& name, const json::json& j); -/// Execute the blockchain @p tests using the @p vm, recording what does not match into @p report. -void run_blockchain_tests(std::span tests, evmc::VM& vm, TestReport& report); +/// Execute the blockchain @p test using the @p vm, recording what does not match into @p report. +void run_blockchain_test(const BlockchainTest& test, evmc::VM& vm, TestReport& report); } // namespace evmone::test diff --git a/test/utils/blockchaintest_runner.cpp b/test/utils/blockchaintest_runner.cpp index 6cc6c65834..d97c7c36f6 100644 --- a/test/utils/blockchaintest_runner.cpp +++ b/test/utils/blockchaintest_runner.cpp @@ -212,300 +212,291 @@ std::string print_state(const TestState& s) } } // namespace -void run_blockchain_tests(std::span tests, evmc::VM& vm, TestReport& report) +void run_blockchain_test(const BlockchainTest& test, evmc::VM& vm, TestReport& report) { - for (size_t case_index = 0; case_index != tests.size(); ++case_index) + const auto rev_schedule = to_rev_schedule(test.network); + report.start_case(test.name); + // The network names the whole schedule, so a fork transition shows at the test level and a + // block needs only its index. The block number would not do: an invalid block does not + // advance it, so two of them can share one. + const auto in_test = report.at(test.network); + + // Validate the genesis block header. + report.check_eq("genesis block number", test.genesis_block_header.block_number, 0); + report.check_eq("genesis gas used", test.genesis_block_header.gas_used, 0); + report.check_eq("genesis transactions root", test.genesis_block_header.transactions_root, + state::EMPTY_MPT_HASH); + report.check_eq( + "genesis receipts root", test.genesis_block_header.receipts_root, state::EMPTY_MPT_HASH); + report.check_eq("genesis withdrawals root", test.genesis_block_header.withdrawal_root, + rev_schedule.get_revision(test.genesis_block_header.timestamp) >= EVMC_SHANGHAI ? + state::EMPTY_MPT_HASH : + bytes32{}); + report.check_eq("genesis logs bloom", bytes_view{test.genesis_block_header.logs_bloom}, + bytes_view{state::BloomFilter{}}); + + TestBlockHashes block_hashes{ + {test.genesis_block_header.block_number, test.genesis_block_header.hash}}; + + struct BlockData { - const auto& c = tests[case_index]; - const auto rev_schedule = to_rev_schedule(c.network); - report.start_case(c.name); - // The network names the whole schedule, so a fork transition shows at the case level and - // a block needs only its index. The block number would not do: an invalid block does not - // advance it, so two of them can share one. - const auto in_case = report.at(c.network, '/', case_index); - - // Validate the genesis block header. - report.check_eq("genesis block number", c.genesis_block_header.block_number, 0); - report.check_eq("genesis gas used", c.genesis_block_header.gas_used, 0); - report.check_eq("genesis transactions root", c.genesis_block_header.transactions_root, - state::EMPTY_MPT_HASH); - report.check_eq( - "genesis receipts root", c.genesis_block_header.receipts_root, state::EMPTY_MPT_HASH); - report.check_eq("genesis withdrawals root", c.genesis_block_header.withdrawal_root, - rev_schedule.get_revision(c.genesis_block_header.timestamp) >= EVMC_SHANGHAI ? - state::EMPTY_MPT_HASH : - bytes32{}); - report.check_eq("genesis logs bloom", bytes_view{c.genesis_block_header.logs_bloom}, - bytes_view{state::BloomFilter{}}); - - TestBlockHashes block_hashes{ - {c.genesis_block_header.block_number, c.genesis_block_header.hash}}; - - struct BlockData - { - const BlockHeader* header; - bool has_ommers = false; - TestState post_state; - intx::uint256 total_difficulty; - }; - std::unordered_map block_data{{{c.genesis_block_header.hash, - {&c.genesis_block_header, false, c.pre_state, c.genesis_block_header.difficulty}}}}; - const auto* canonical_state = &c.pre_state; - hash256 canonical_state_root; // Skip pre-state root hash computation (maybe not needed). - auto canonical_tip_hash = c.genesis_block_header.hash; - intx::uint256 max_total_difficulty = c.genesis_block_header.difficulty; - - for (size_t i = 0; i < c.test_blocks.size(); ++i) - { - const auto& test_block = c.test_blocks[i]; - const auto& bi = test_block.block_info; + const BlockHeader* header; + bool has_ommers = false; + TestState post_state; + intx::uint256 total_difficulty; + }; + std::unordered_map block_data{ + {{test.genesis_block_header.hash, {&test.genesis_block_header, false, test.pre_state, + test.genesis_block_header.difficulty}}}}; + const auto* canonical_state = &test.pre_state; + hash256 canonical_state_root; // Skip pre-state root hash computation (maybe not needed). + auto canonical_tip_hash = test.genesis_block_header.hash; + intx::uint256 max_total_difficulty = test.genesis_block_header.difficulty; + + for (size_t i = 0; i < test.test_blocks.size(); ++i) + { + const auto& test_block = test.test_blocks[i]; + const auto& bi = test_block.block_info; - const auto parent_data_it = block_data.find(test_block.block_info.parent_hash); - const auto* parent_header = - parent_data_it != block_data.end() ? parent_data_it->second.header : nullptr; - const auto parent_has_ommers = - parent_data_it != block_data.end() && parent_data_it->second.has_ommers; + const auto parent_data_it = block_data.find(test_block.block_info.parent_hash); + const auto* parent_header = + parent_data_it != block_data.end() ? parent_data_it->second.header : nullptr; + const auto parent_has_ommers = + parent_data_it != block_data.end() && parent_data_it->second.has_ommers; - const auto rev = rev_schedule.get_revision(bi.timestamp); - const auto blob_params = get_blob_params(c.network, c.blob_schedule, bi.timestamp); - const auto blob_gas_limit = - static_cast(state::max_blob_gas_per_block(blob_params)); + const auto rev = rev_schedule.get_revision(bi.timestamp); + const auto blob_params = get_blob_params(test.network, test.blob_schedule, bi.timestamp); + const auto blob_gas_limit = + static_cast(state::max_blob_gas_per_block(blob_params)); - const auto in_block = report.at(i); + const auto in_block = report.at(i); - // Invalid blocks are skipped: they may carry transactions that do not even decode. - if (test_block.expected_exception.empty()) - check_transactions_round_trip(test_block.rlp, report); + // Invalid blocks are skipped: they may carry transactions that do not even decode. + if (test_block.expected_exception.empty()) + check_transactions_round_trip(test_block.rlp, report); - const auto block_error = - validate_block(rev, blob_params, test_block, parent_header, parent_has_ommers); + const auto block_error = + validate_block(rev, blob_params, test_block, parent_header, parent_has_ommers); - if (test_block.expected_exception.empty()) + if (test_block.expected_exception.empty()) + { + if (block_error) { - if (block_error) - { - report.fail("block validity", - "expected the block to be valid: " + block_error.message()); - // TODO: This and the requests failure below abandon the whole file, not - // just this case. Give each case its own run so only that case stops. - return; - } + report.fail( + "block validity", "expected the block to be valid: " + block_error.message()); + return; + } - // Block being valid guarantees its parent was found. - assert(parent_data_it != block_data.end()); - const auto& pre_state = parent_data_it->second.post_state; + // Block being valid guarantees its parent was found. + assert(parent_data_it != block_data.end()); + const auto& pre_state = parent_data_it->second.post_state; - auto res = apply_block(pre_state, vm, bi, block_hashes, test_block.transactions, - rev, blob_gas_limit, {.block_reward = mining_reward(rev)}); + auto res = apply_block(pre_state, vm, bi, block_hashes, test_block.transactions, rev, + blob_gas_limit, {.block_reward = mining_reward(rev)}); - if (res.requests_error) - { - report.fail("requests", res.requests_error.message()); - return; - } + if (res.requests_error) + { + report.fail("requests", res.requests_error.message()); + return; + } - block_hashes[test_block.expected_block_header.block_number] = - test_block.expected_block_header.hash; - const auto [inserted_it, _] = block_data.insert({test_block.block_info.hash, - { - .header = &test_block.expected_block_header, - .has_ommers = !test_block.block_info.ommers.empty(), - .post_state = std::move(res.block_state), - .total_difficulty = parent_data_it->second.total_difficulty + - test_block.block_info.difficulty, - }}); + block_hashes[test_block.expected_block_header.block_number] = + test_block.expected_block_header.hash; + const auto [inserted_it, _] = block_data.insert({test_block.block_info.hash, + { + .header = &test_block.expected_block_header, + .has_ommers = !test_block.block_info.ommers.empty(), + .post_state = std::move(res.block_state), + .total_difficulty = + parent_data_it->second.total_difficulty + test_block.block_info.difficulty, + }}); - const auto state_root = state::mpt_hash(inserted_it->second.post_state); + const auto state_root = state::mpt_hash(inserted_it->second.post_state); - if (inserted_it->second.total_difficulty >= max_total_difficulty) - { - canonical_state = &inserted_it->second.post_state; - canonical_state_root = state_root; - canonical_tip_hash = test_block.expected_block_header.hash; - max_total_difficulty = inserted_it->second.total_difficulty; - } + if (inserted_it->second.total_difficulty >= max_total_difficulty) + { + canonical_state = &inserted_it->second.post_state; + canonical_state_root = state_root; + canonical_tip_hash = test_block.expected_block_header.hash; + max_total_difficulty = inserted_it->second.total_difficulty; + } - if (!res.rejected.empty()) - { - report.fail("transactions in a valid block", - "invalid transaction: " + res.rejected.front().error.message()); - } + if (!res.rejected.empty()) + { + report.fail("transactions in a valid block", + "invalid transaction: " + res.rejected.front().error.message()); + } - report.check_eq("blob gas used", blob_gas_limit - res.blob_gas_left, - static_cast(bi.blob_gas_used.value_or(0))); - report.check_eq( - "state root", state_root, test_block.expected_block_header.state_root); + report.check_eq("blob gas used", blob_gas_limit - res.blob_gas_left, + static_cast(bi.blob_gas_used.value_or(0))); + report.check_eq("state root", state_root, test_block.expected_block_header.state_root); - if (rev >= EVMC_SHANGHAI) - { - report.check_eq("withdrawals root", - state::mpt_hash(test_block.block_info.withdrawals), - test_block.expected_block_header.withdrawal_root); - } + if (rev >= EVMC_SHANGHAI) + { + report.check_eq("withdrawals root", + state::mpt_hash(test_block.block_info.withdrawals), + test_block.expected_block_header.withdrawal_root); + } - report.check_eq("transactions root", state::mpt_hash(test_block.transactions), - test_block.expected_block_header.transactions_root); - report.check_eq("receipts root", state::mpt_hash(res.receipts), - test_block.expected_block_header.receipts_root); - if (rev >= EVMC_PRAGUE) - { - report.check_eq("requests hash", calculate_requests_hash(res.requests), - test_block.expected_block_header.requests_hash); - } - report.check_eq( - "gas used", res.gas_used, test_block.expected_block_header.gas_used); - report.check_eq("logs bloom", bytes_view{res.bloom}, - bytes_view{test_block.expected_block_header.logs_bloom}); + report.check_eq("transactions root", state::mpt_hash(test_block.transactions), + test_block.expected_block_header.transactions_root); + report.check_eq("receipts root", state::mpt_hash(res.receipts), + test_block.expected_block_header.receipts_root); + if (rev >= EVMC_PRAGUE) + { + report.check_eq("requests hash", calculate_requests_hash(res.requests), + test_block.expected_block_header.requests_hash); } - else + report.check_eq("gas used", res.gas_used, test_block.expected_block_header.gas_used); + report.check_eq("logs bloom", bytes_view{res.bloom}, + bytes_view{test_block.expected_block_header.logs_bloom}); + } + else + { + if (block_error) { - if (block_error) - { - // Block correctly rejected at validation; verify the reason matches the - // fixture's expected exception. - report.check( - is_expected_block_exception(block_error, test_block.expected_exception), - "block rejection reason", block_error, test_block.expected_exception); - continue; - } + // Block correctly rejected at validation; verify the reason matches the + // fixture's expected exception. + report.check( + is_expected_block_exception(block_error, test_block.expected_exception), + "block rejection reason", block_error, test_block.expected_exception); + continue; + } - // Block being valid guarantees its parent was found. - assert(parent_data_it != block_data.end()); - const auto& pre_state = parent_data_it->second.post_state; - - // Legacy fixtures name the broken rule in vocabulary evmone does not speak - // (InvalidStateRoot, TooManyUncles); only the spec names can be compared. - const auto names_spec_exception = - test_block.expected_exception.find("Exception.") != std::string::npos; - - // TODO: The transaction senders come from the fixture instead of being recovered - // from the signatures, so evmone never sees the signature the test broke. Such a - // transaction executes as the sender the fixture names and the block is rejected - // by whatever rule that sender happens to break, or by its state root alone. - const auto sender_not_recovered = contains_any( - test_block.expected_exception, "TransactionException.INVALID_SIGNATURE_VRS"); - - const auto res = - apply_block(pre_state, vm, bi, block_hashes, test_block.transactions, rev, - blob_gas_limit, {.block_reward = mining_reward(rev)}); - if (!res.rejected.empty()) - { - // A transaction was rejected: the fixture must name that reason, not merely - // some rejection. - const auto& rejected = res.rejected.front(); - if (names_spec_exception && !sender_not_recovered) - { - report.check( - is_expected_tx_exception(rejected.error, test_block.expected_exception), - "transaction rejection reason", rejected.error, - test_block.expected_exception); - } - continue; - } - if (res.requests_error) + // Block being valid guarantees its parent was found. + assert(parent_data_it != block_data.end()); + const auto& pre_state = parent_data_it->second.post_state; + + // Legacy fixtures name the broken rule in vocabulary evmone does not speak + // (InvalidStateRoot, TooManyUncles); only the spec names can be compared. + const auto names_spec_exception = + test_block.expected_exception.find("Exception.") != std::string::npos; + + // TODO: The transaction senders come from the fixture instead of being recovered + // from the signatures, so evmone never sees the signature the test broke. Such a + // transaction executes as the sender the fixture names and the block is rejected + // by whatever rule that sender happens to break, or by its state root alone. + const auto sender_not_recovered = contains_any( + test_block.expected_exception, "TransactionException.INVALID_SIGNATURE_VRS"); + + const auto res = apply_block(pre_state, vm, bi, block_hashes, test_block.transactions, + rev, blob_gas_limit, {.block_reward = mining_reward(rev)}); + if (!res.rejected.empty()) + { + // A transaction was rejected: the fixture must name that reason, not merely + // some rejection. + const auto& rejected = res.rejected.front(); + if (names_spec_exception && !sender_not_recovered) { - if (!sender_not_recovered) - { - report.check(is_expected_block_exception( - res.requests_error, test_block.expected_exception), - "block rejection reason", res.requests_error, - test_block.expected_exception); - } - continue; + report.check( + is_expected_tx_exception(rejected.error, test_block.expected_exception), + "transaction rejection reason", rejected.error, + test_block.expected_exception); } - // The block executed, so it is invalid only if it computes something other than - // its header claims. Each difference below is the symptom of one BlockException: - // a block failing a check other than the one the fixture names breaks a different - // rule than the test is about. - // TODO: Of the ommers only the count and the distance to their nephew are - // validated, not the ommer headers themselves, so a fixture that breaks an - // ommer's gas limit, number or timestamp reaches execution and lands here. - const auto ommers_not_validated = !test_block.block_info.ommers.empty(); - - // Asserts the fixture names one of @p names, the exceptions the check that just - // fired is the symptom of. Silent where the reason cannot be compared. - const auto expect_fixture_names = [&](std::string_view names) { - if (!names_spec_exception || ommers_not_validated || sender_not_recovered) - return; - report.check(contains_any(test_block.expected_exception, names), - "block rejection reason", names, test_block.expected_exception); - }; - - if (blob_gas_limit - res.blob_gas_left != - static_cast(bi.blob_gas_used.value_or(0))) + continue; + } + if (res.requests_error) + { + if (!sender_not_recovered) { - expect_fixture_names( - "BlockException.INCORRECT_BLOB_GAS_USED|" - "BlockException.BLOB_GAS_USED_ABOVE_LIMIT"); - continue; + report.check(is_expected_block_exception( + res.requests_error, test_block.expected_exception), + "block rejection reason", res.requests_error, + test_block.expected_exception); } + continue; + } + // The block executed, so it is invalid only if it computes something other than + // its header claims. Each difference below is the symptom of one BlockException: + // a block failing a check other than the one the fixture names breaks a different + // rule than the test is about. + // TODO: Of the ommers only the count and the distance to their nephew are + // validated, not the ommer headers themselves, so a fixture that breaks an + // ommer's gas limit, number or timestamp reaches execution and lands here. + const auto ommers_not_validated = !test_block.block_info.ommers.empty(); + + // Asserts the fixture names one of @p names, the exceptions the check that just + // fired is the symptom of. Silent where the reason cannot be compared. + const auto expect_fixture_names = [&](std::string_view names) { + if (!names_spec_exception || ommers_not_validated || sender_not_recovered) + return; + report.check(contains_any(test_block.expected_exception, names), + "block rejection reason", names, test_block.expected_exception); + }; - if (state::mpt_hash(res.block_state) != test_block.expected_block_header.state_root) - { - expect_fixture_names("BlockException.INVALID_STATE_ROOT"); - continue; - } + if (blob_gas_limit - res.blob_gas_left != + static_cast(bi.blob_gas_used.value_or(0))) + { + expect_fixture_names( + "BlockException.INCORRECT_BLOB_GAS_USED|" + "BlockException.BLOB_GAS_USED_ABOVE_LIMIT"); + continue; + } - if (rev >= EVMC_SHANGHAI && state::mpt_hash(test_block.block_info.withdrawals) != - test_block.expected_block_header.withdrawal_root) - { - expect_fixture_names("BlockException.INVALID_WITHDRAWALS_ROOT"); - continue; - } - if (state::mpt_hash(test_block.transactions) != - test_block.expected_block_header.transactions_root) - { - expect_fixture_names("BlockException.INVALID_TRANSACTIONS_ROOT"); - continue; - } - if (state::mpt_hash(res.receipts) != test_block.expected_block_header.receipts_root) - { - expect_fixture_names("BlockException.INVALID_RECEIPTS_ROOT"); - continue; - } - if (rev >= EVMC_PRAGUE && calculate_requests_hash(res.requests) != - test_block.expected_block_header.requests_hash) - { - expect_fixture_names("BlockException.INVALID_REQUESTS"); - continue; - } - if (res.gas_used != test_block.expected_block_header.gas_used) - { - expect_fixture_names( - "BlockException.INVALID_GAS_USED|" - "BlockException.GAS_USED_OVERFLOW"); - continue; - } - if (bytes_view{res.bloom} != - bytes_view{test_block.expected_block_header.logs_bloom}) - { - expect_fixture_names("BlockException.INVALID_LOG_BLOOM"); - continue; - } + if (state::mpt_hash(res.block_state) != test_block.expected_block_header.state_root) + { + expect_fixture_names("BlockException.INVALID_STATE_ROOT"); + continue; + } - report.fail("block validity", "expected the block to be invalid"); + if (rev >= EVMC_SHANGHAI && state::mpt_hash(test_block.block_info.withdrawals) != + test_block.expected_block_header.withdrawal_root) + { + expect_fixture_names("BlockException.INVALID_WITHDRAWALS_ROOT"); + continue; + } + if (state::mpt_hash(test_block.transactions) != + test_block.expected_block_header.transactions_root) + { + expect_fixture_names("BlockException.INVALID_TRANSACTIONS_ROOT"); + continue; } + if (state::mpt_hash(res.receipts) != test_block.expected_block_header.receipts_root) + { + expect_fixture_names("BlockException.INVALID_RECEIPTS_ROOT"); + continue; + } + if (rev >= EVMC_PRAGUE && calculate_requests_hash(res.requests) != + test_block.expected_block_header.requests_hash) + { + expect_fixture_names("BlockException.INVALID_REQUESTS"); + continue; + } + if (res.gas_used != test_block.expected_block_header.gas_used) + { + expect_fixture_names( + "BlockException.INVALID_GAS_USED|" + "BlockException.GAS_USED_OVERFLOW"); + continue; + } + if (bytes_view{res.bloom} != bytes_view{test_block.expected_block_header.logs_bloom}) + { + expect_fixture_names("BlockException.INVALID_LOG_BLOOM"); + continue; + } + + report.fail("block validity", "expected the block to be invalid"); } - report.check_eq("canonical chain tip", canonical_tip_hash, c.expectation.last_block_hash); - - const auto expected_post_hash = - std::holds_alternative(c.expectation.post_state) ? - state::mpt_hash(std::get(c.expectation.post_state)) : - std::get(c.expectation.post_state); - - // Get the final state hash. In case none blocks have been applied, compute genesis one. - const auto canonical_post_hash = - canonical_state_root ? canonical_state_root : state::mpt_hash(c.pre_state); - // The state dumps are a callable so that formatting the whole state only happens once - // the roots are already known to differ. - report.check_eq("post state root", canonical_post_hash, expected_post_hash, [&] { - return "Result state:\n" + print_state(*canonical_state) + - (std::holds_alternative(c.expectation.post_state) ? - "\n\nExpected state:\n" + - print_state(std::get(c.expectation.post_state)) : - ""); - }); } + report.check_eq("canonical chain tip", canonical_tip_hash, test.expectation.last_block_hash); + + const auto expected_post_hash = + std::holds_alternative(test.expectation.post_state) ? + state::mpt_hash(std::get(test.expectation.post_state)) : + std::get(test.expectation.post_state); + + // Get the final state hash. In case none blocks have been applied, compute genesis one. + const auto canonical_post_hash = + canonical_state_root ? canonical_state_root : state::mpt_hash(test.pre_state); + // The state dumps are a callable so that formatting the whole state only happens once + // the roots are already known to differ. + report.check_eq("post state root", canonical_post_hash, expected_post_hash, [&] { + return "Result state:\n" + print_state(*canonical_state) + + (std::holds_alternative(test.expectation.post_state) ? + "\n\nExpected state:\n" + + print_state(std::get(test.expectation.post_state)) : + ""); + }); } } // namespace evmone::test