From d2f1aff6402b702c32972ea77c008b05b4dcb27b Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Wed, 11 Sep 2024 22:28:20 -0400 Subject: [PATCH 01/13] Add unit test to verify replay of blocks with bad QC data --- unittests/replay_block_invariants_tests.cpp | 251 ++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 unittests/replay_block_invariants_tests.cpp diff --git a/unittests/replay_block_invariants_tests.cpp b/unittests/replay_block_invariants_tests.cpp new file mode 100644 index 0000000000..7eef2f2e71 --- /dev/null +++ b/unittests/replay_block_invariants_tests.cpp @@ -0,0 +1,251 @@ +#include +#include + +BOOST_AUTO_TEST_SUITE(replay_block_invariants_tests) + +using namespace eosio::testing; +using namespace eosio::chain; + +struct blog_replay_fixture { + eosio::testing::tester chain; + + // Create blocks log + blog_replay_fixture() { + // Create a few accounts and produce a few blocks to fill in blocks log + chain.create_account("replay1"_n); + chain.produce_blocks(1); + chain.create_account("replay2"_n); + chain.produce_blocks(1); + chain.create_account("replay3"_n); + + chain.produce_blocks(10); + + // Make sure the accounts were created + BOOST_REQUIRE_NO_THROW(chain.get_account("replay1"_n)); + BOOST_REQUIRE_NO_THROW(chain.get_account("replay2"_n)); + BOOST_REQUIRE_NO_THROW(chain.get_account("replay3"_n)); + + // Stop the node and save blocks_log + chain.close(); + } + + void remove_existing_states(std::filesystem::path& state_path) { + std::filesystem::remove_all(state_path); + std::filesystem::create_directories(state_path); + } + + void corrupt_qc_extension_in_block_log() { + controller::config config = chain.get_config(); + auto blocks_dir = chain.get_config().blocks_dir; + + block_log blog(blocks_dir, chain.get_config().blog); + + // find the first block which has QC extension starting from the end of + // block log and going backward + uint32_t block_num = blog.head()->block_num(); + auto qc_ext_id = quorum_certificate_extension::extension_id(); + while (!blog.read_block_by_num(block_num)->contains_extension(qc_ext_id)) { + --block_num; + BOOST_REQUIRE(block_num != 0); + } + + // clone the QC block + signed_block_ptr qc_block = std::make_shared(blog.read_block_by_num(block_num)->clone()); + BOOST_TEST(qc_block); + + // remove qc block until the end from block log + BOOST_REQUIRE_NO_THROW(block_log::trim_blocklog_end(blocks_dir, block_num-1)); + BOOST_REQUIRE_NO_THROW(block_log::smoke_test(blocks_dir, 1)); + + // retrieve QC block extension from QC block + auto block_exts = qc_block->validate_and_extract_extensions(); + auto qc_ext_itr = block_exts.find(qc_ext_id); + auto& qc_ext = std::get(qc_ext_itr->second); + auto& qc = qc_ext.qc; + + // remove QC block extension from QC block + auto& exts = qc_block->block_extensions; + std::pair> target{qc_ext_id, {}}; + auto itr = std::lower_bound(exts.begin(), exts.end(), target, [](const auto& ext1, const auto& ext2){ + return ext1.first < ext2.first; + }); + BOOST_REQUIRE(itr != exts.end()); + qc_block->block_extensions.erase(itr); + + // intentionally corrupt QC's strength. + if (qc.active_policy_sig.strong_votes) { + (*qc.active_policy_sig.strong_votes)[0] ^= 1; // flip one bit + } else if (qc.active_policy_sig.weak_votes) { + (*qc.active_policy_sig.weak_votes)[0] ^= 1; // flip one bit + } + + // add the corrupted QC block extension back to the block + emplace_extension(exts, qc_ext_id, fc::raw::pack(qc_ext)); + + // add the corrupted block back to block log + block_log new_blog(blocks_dir, config.blog); + new_blog.append(qc_block, qc_block->calculate_id()); + } +}; + +// Test replay with invalid QC claim -- claimed block number goes backward +BOOST_FIXTURE_TEST_CASE(invalid_qc, blog_replay_fixture) try { + controller::config config = chain.get_config(); + auto blocks_dir = chain.get_config().blocks_dir; + + block_log blog(blocks_dir, chain.get_config().blog); + + // retrieve the last block in block log + uint32_t last_block_num = blog.head()->block_num(); + signed_block_ptr last_block = std::make_shared(blog.read_block_by_num(last_block_num)->clone()); + BOOST_TEST(last_block); + + // remove last block from block log + BOOST_REQUIRE_NO_THROW(block_log::trim_blocklog_end(blocks_dir, last_block_num-1)); + BOOST_REQUIRE_NO_THROW(block_log::smoke_test(blocks_dir, 1)); + + // retrieve finality extension + std::optional head_fin_ext = last_block->extract_header_extension(finality_extension::extension_id()); + BOOST_TEST(!!head_fin_ext); + + // remove finality extension from extensions + auto& exts = last_block->header_extensions; + std::pair> target{finality_extension::extension_id(), {}}; + auto itr = std::lower_bound(exts.begin(), exts.end(), target, [](const auto& ext1, const auto& ext2){ + return ext1.first < ext2.first; + }); + BOOST_REQUIRE(itr != exts.end()); + exts.erase(itr); + + // intentionally corrupt finality extension by making its claimed QC + // block number to be 0 so it is smaller than previous block's claimed QC block number + auto& f_ext = std::get(*head_fin_ext); + f_ext.qc_claim.block_num = 0; + + // add the corrupted finality extension back to last block + emplace_extension(exts, finality_extension::extension_id(), fc::raw::pack(f_ext)); + + // add the corrupted block to block log + block_log new_blog(blocks_dir, config.blog); + new_blog.append(last_block, last_block->calculate_id()); + + // remove the state files to make sure we are starting from block log + remove_existing_states(config.state_dir); + + auto genesis = block_log::extract_genesis_state(blocks_dir); + BOOST_REQUIRE(genesis); + + try { + eosio::testing::tester replay_chain(config, *genesis); // // start replay + BOOST_FAIL("replay should have failed with invalid_qc_claim exception"); + } catch (invalid_qc_claim &) { + } catch (...) { + BOOST_FAIL("replay failed with non invalid_qc_claim exception"); + } +} FC_LOG_AND_RETHROW() + +// Test replay with irrelevant QC -- claims a block number other than the one +// claimed in the block header +BOOST_FIXTURE_TEST_CASE(irrelevant_qc, blog_replay_fixture) try { + controller::config config = chain.get_config(); + auto blocks_dir = chain.get_config().blocks_dir; + + block_log blog(blocks_dir, chain.get_config().blog); + + // retrieve the last block in block log + uint32_t last_block_num = blog.head()->block_num(); + signed_block_ptr last_block = std::make_shared(blog.read_block_by_num(last_block_num)->clone()); + BOOST_TEST(last_block); + + // remove last block from block log + BOOST_REQUIRE_NO_THROW(block_log::trim_blocklog_end(blocks_dir, last_block_num-1)); + BOOST_REQUIRE_NO_THROW(block_log::smoke_test(blocks_dir, 1)); + + // retrieve finality extension + std::optional head_fin_ext = last_block->extract_header_extension(finality_extension::extension_id()); + BOOST_TEST(!!head_fin_ext); + + // remove finality extension from extensions + auto& exts = last_block->header_extensions; + std::pair> target{finality_extension::extension_id(), {}}; + auto itr = std::lower_bound(exts.begin(), exts.end(), target, [](const auto& ext1, const auto& ext2){ + return ext1.first < ext2.first; + }); + BOOST_REQUIRE(itr != exts.end()); + exts.erase(itr); + + // intentionally corrupt finality extension by making its claimed QC + // block number to be a non-existent block number + auto& f_ext = std::get(*head_fin_ext); + f_ext.qc_claim.block_num = last_block_num + 1; // `last_block_num + 1` does not exist + + // add the corrupted finality extension back to last block + emplace_extension(exts, finality_extension::extension_id(), fc::raw::pack(f_ext)); + + // add the corrupted block to block log + block_log new_blog(blocks_dir, config.blog); + new_blog.append(last_block, last_block->calculate_id()); + + // remove the state files to make sure we are starting from block log + remove_existing_states(config.state_dir); + + auto genesis = block_log::extract_genesis_state(blocks_dir); + BOOST_REQUIRE(genesis); + + try { + eosio::testing::tester replay_chain(config, *genesis); // start replay + BOOST_FAIL("replay should have failed with block_validate_exception exception"); + } catch (block_validate_exception &) { + } catch (...) { + BOOST_FAIL("replay failed with non block_validate_exception exception"); + } +} FC_LOG_AND_RETHROW() + +// Test replay with bad QC (signature validation), but run without --force-all-checks. +// Replay should pass as QC is not validated. +BOOST_FIXTURE_TEST_CASE(bad_qc_no_force_all_checks, blog_replay_fixture) try { + controller::config config = chain.get_config(); + auto blocks_dir = chain.get_config().blocks_dir; + + corrupt_qc_extension_in_block_log(); + + // remove the state files to make sure we are starting from block log + remove_existing_states(config.state_dir); + + auto genesis = block_log::extract_genesis_state(blocks_dir); + BOOST_REQUIRE(genesis); + + try { + eosio::testing::tester replay_chain(config, *genesis); // start replay + } catch (...) { + BOOST_FAIL("replay should not fail without --force-all-checks"); + } +} FC_LOG_AND_RETHROW() + +// Test replay with bad QC (signature validation), but run with --force-all-checks. +// Replay should fail as QC is validated. +BOOST_FIXTURE_TEST_CASE(bad_qc_force_all_checks, blog_replay_fixture) try { + controller::config config = chain.get_config(); + auto blocks_dir = chain.get_config().blocks_dir; + + corrupt_qc_extension_in_block_log(); + + // remove the state files to make sure we are starting from block log + remove_existing_states(config.state_dir); + + auto genesis = block_log::extract_genesis_state(blocks_dir); + BOOST_REQUIRE(genesis); + + config.force_all_checks = true; + + try { + eosio::testing::tester replay_chain(config, *genesis); // start replay + BOOST_FAIL("replay should have failed with --force-all-checks"); + } catch (block_validate_exception& e) { + BOOST_REQUIRE(e.to_detail_string().find("strong quorum is not met") != std::string::npos); + } catch (...) { + BOOST_FAIL("replay failed with non block_validate_exception exception"); + } +} FC_LOG_AND_RETHROW() + +BOOST_AUTO_TEST_SUITE_END() From c013ee02f561aea2cc8ad25a0c9d24198dc1e08c Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Thu, 12 Sep 2024 08:13:08 -0400 Subject: [PATCH 02/13] Refactor to have a common corrupt_finality_extension_in_block_log() --- unittests/replay_block_invariants_tests.cpp | 122 +++++++++----------- 1 file changed, 52 insertions(+), 70 deletions(-) diff --git a/unittests/replay_block_invariants_tests.cpp b/unittests/replay_block_invariants_tests.cpp index 7eef2f2e71..17a5713579 100644 --- a/unittests/replay_block_invariants_tests.cpp +++ b/unittests/replay_block_invariants_tests.cpp @@ -86,6 +86,46 @@ struct blog_replay_fixture { block_log new_blog(blocks_dir, config.blog); new_blog.append(qc_block, qc_block->calculate_id()); } + + void corrupt_finality_extension_in_block_log(uint32_t new_qc_claim_block_num) { + controller::config config = chain.get_config(); + auto blocks_dir = chain.get_config().blocks_dir; + + block_log blog(blocks_dir, chain.get_config().blog); + + // retrieve the last block in block log + uint32_t last_block_num = blog.head()->block_num(); + signed_block_ptr last_block = std::make_shared(blog.read_block_by_num(last_block_num)->clone()); + BOOST_TEST(last_block); + + // remove last block from block log + BOOST_REQUIRE_NO_THROW(block_log::trim_blocklog_end(blocks_dir, last_block_num-1)); + BOOST_REQUIRE_NO_THROW(block_log::smoke_test(blocks_dir, 1)); + + // retrieve finality extension + std::optional head_fin_ext = last_block->extract_header_extension(finality_extension::extension_id()); + BOOST_TEST(!!head_fin_ext); + + // remove finality extension from extensions + auto& exts = last_block->header_extensions; + std::pair> target{finality_extension::extension_id(), {}}; + auto itr = std::lower_bound(exts.begin(), exts.end(), target, [](const auto& ext1, const auto& ext2){ + return ext1.first < ext2.first; + }); + BOOST_REQUIRE(itr != exts.end()); + exts.erase(itr); + + // intentionally corrupt finality extension by changing its block_num + auto& f_ext = std::get(*head_fin_ext); + f_ext.qc_claim.block_num = new_qc_claim_block_num; + + // add the corrupted finality extension back to last block + emplace_extension(exts, finality_extension::extension_id(), fc::raw::pack(f_ext)); + + // add the corrupted block to block log + block_log new_blog(blocks_dir, config.blog); + new_blog.append(last_block, last_block->calculate_id()); + } }; // Test replay with invalid QC claim -- claimed block number goes backward @@ -93,48 +133,17 @@ BOOST_FIXTURE_TEST_CASE(invalid_qc, blog_replay_fixture) try { controller::config config = chain.get_config(); auto blocks_dir = chain.get_config().blocks_dir; - block_log blog(blocks_dir, chain.get_config().blog); + // set claimed block number backward + corrupt_finality_extension_in_block_log(0); - // retrieve the last block in block log - uint32_t last_block_num = blog.head()->block_num(); - signed_block_ptr last_block = std::make_shared(blog.read_block_by_num(last_block_num)->clone()); - BOOST_TEST(last_block); - - // remove last block from block log - BOOST_REQUIRE_NO_THROW(block_log::trim_blocklog_end(blocks_dir, last_block_num-1)); - BOOST_REQUIRE_NO_THROW(block_log::smoke_test(blocks_dir, 1)); - - // retrieve finality extension - std::optional head_fin_ext = last_block->extract_header_extension(finality_extension::extension_id()); - BOOST_TEST(!!head_fin_ext); - - // remove finality extension from extensions - auto& exts = last_block->header_extensions; - std::pair> target{finality_extension::extension_id(), {}}; - auto itr = std::lower_bound(exts.begin(), exts.end(), target, [](const auto& ext1, const auto& ext2){ - return ext1.first < ext2.first; - }); - BOOST_REQUIRE(itr != exts.end()); - exts.erase(itr); - - // intentionally corrupt finality extension by making its claimed QC - // block number to be 0 so it is smaller than previous block's claimed QC block number - auto& f_ext = std::get(*head_fin_ext); - f_ext.qc_claim.block_num = 0; - - // add the corrupted finality extension back to last block - emplace_extension(exts, finality_extension::extension_id(), fc::raw::pack(f_ext)); - - // add the corrupted block to block log - block_log new_blog(blocks_dir, config.blog); - new_blog.append(last_block, last_block->calculate_id()); + // retrieve genesis + block_log blog(blocks_dir, chain.get_config().blog); + auto genesis = block_log::extract_genesis_state(blocks_dir); + BOOST_REQUIRE(genesis); // remove the state files to make sure we are starting from block log remove_existing_states(config.state_dir); - auto genesis = block_log::extract_genesis_state(blocks_dir); - BOOST_REQUIRE(genesis); - try { eosio::testing::tester replay_chain(config, *genesis); // // start replay BOOST_FAIL("replay should have failed with invalid_qc_claim exception"); @@ -154,44 +163,17 @@ BOOST_FIXTURE_TEST_CASE(irrelevant_qc, blog_replay_fixture) try { // retrieve the last block in block log uint32_t last_block_num = blog.head()->block_num(); - signed_block_ptr last_block = std::make_shared(blog.read_block_by_num(last_block_num)->clone()); - BOOST_TEST(last_block); - - // remove last block from block log - BOOST_REQUIRE_NO_THROW(block_log::trim_blocklog_end(blocks_dir, last_block_num-1)); - BOOST_REQUIRE_NO_THROW(block_log::smoke_test(blocks_dir, 1)); - - // retrieve finality extension - std::optional head_fin_ext = last_block->extract_header_extension(finality_extension::extension_id()); - BOOST_TEST(!!head_fin_ext); - - // remove finality extension from extensions - auto& exts = last_block->header_extensions; - std::pair> target{finality_extension::extension_id(), {}}; - auto itr = std::lower_bound(exts.begin(), exts.end(), target, [](const auto& ext1, const auto& ext2){ - return ext1.first < ext2.first; - }); - BOOST_REQUIRE(itr != exts.end()); - exts.erase(itr); - - // intentionally corrupt finality extension by making its claimed QC - // block number to be a non-existent block number - auto& f_ext = std::get(*head_fin_ext); - f_ext.qc_claim.block_num = last_block_num + 1; // `last_block_num + 1` does not exist - - // add the corrupted finality extension back to last block - emplace_extension(exts, finality_extension::extension_id(), fc::raw::pack(f_ext)); - - // add the corrupted block to block log - block_log new_blog(blocks_dir, config.blog); - new_blog.append(last_block, last_block->calculate_id()); - // remove the state files to make sure we are starting from block log - remove_existing_states(config.state_dir); + // set claimed block number to a non-existent number + corrupt_finality_extension_in_block_log(last_block_num + 1); + // retrieve genesis auto genesis = block_log::extract_genesis_state(blocks_dir); BOOST_REQUIRE(genesis); + // remove the state files to make sure we are starting from block log + remove_existing_states(config.state_dir); + try { eosio::testing::tester replay_chain(config, *genesis); // start replay BOOST_FAIL("replay should have failed with block_validate_exception exception"); From cc468370af6e4e3b51d796eb4e23b9ef2118ee97 Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Thu, 12 Sep 2024 08:21:44 -0400 Subject: [PATCH 03/13] Verify exception thrown is exactly what to expected --- unittests/replay_block_invariants_tests.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/unittests/replay_block_invariants_tests.cpp b/unittests/replay_block_invariants_tests.cpp index 17a5713579..9b4a85ed5b 100644 --- a/unittests/replay_block_invariants_tests.cpp +++ b/unittests/replay_block_invariants_tests.cpp @@ -147,7 +147,8 @@ BOOST_FIXTURE_TEST_CASE(invalid_qc, blog_replay_fixture) try { try { eosio::testing::tester replay_chain(config, *genesis); // // start replay BOOST_FAIL("replay should have failed with invalid_qc_claim exception"); - } catch (invalid_qc_claim &) { + } catch (invalid_qc_claim& e) { + BOOST_REQUIRE(e.to_detail_string().find("less than the previous block") != std::string::npos); } catch (...) { BOOST_FAIL("replay failed with non invalid_qc_claim exception"); } @@ -177,7 +178,8 @@ BOOST_FIXTURE_TEST_CASE(irrelevant_qc, blog_replay_fixture) try { try { eosio::testing::tester replay_chain(config, *genesis); // start replay BOOST_FAIL("replay should have failed with block_validate_exception exception"); - } catch (block_validate_exception &) { + } catch (block_validate_exception& e) { + BOOST_REQUIRE(e.to_detail_string().find("Mismatch between qc.block_num") != std::string::npos); } catch (...) { BOOST_FAIL("replay failed with non block_validate_exception exception"); } From 50eaededc8f6c71cde35b1db997f6f04ca6f53da Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Thu, 12 Sep 2024 08:41:05 -0400 Subject: [PATCH 04/13] Fix use of signed_block_ptr due to newly changed definition of signed_block_ptr --- unittests/replay_block_invariants_tests.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/unittests/replay_block_invariants_tests.cpp b/unittests/replay_block_invariants_tests.cpp index 9b4a85ed5b..7135662912 100644 --- a/unittests/replay_block_invariants_tests.cpp +++ b/unittests/replay_block_invariants_tests.cpp @@ -50,7 +50,7 @@ struct blog_replay_fixture { } // clone the QC block - signed_block_ptr qc_block = std::make_shared(blog.read_block_by_num(block_num)->clone()); + auto qc_block = std::make_shared(blog.read_block_by_num(block_num)->clone()); BOOST_TEST(qc_block); // remove qc block until the end from block log @@ -95,7 +95,7 @@ struct blog_replay_fixture { // retrieve the last block in block log uint32_t last_block_num = blog.head()->block_num(); - signed_block_ptr last_block = std::make_shared(blog.read_block_by_num(last_block_num)->clone()); + auto last_block = std::make_shared(blog.read_block_by_num(last_block_num)->clone()); BOOST_TEST(last_block); // remove last block from block log From 973ab6e1f8871054c48e2e97fec2228a4adf6892 Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Thu, 12 Sep 2024 08:56:01 -0400 Subject: [PATCH 05/13] Test corrupted signature verification --- unittests/replay_block_invariants_tests.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/unittests/replay_block_invariants_tests.cpp b/unittests/replay_block_invariants_tests.cpp index 7135662912..2835c990b9 100644 --- a/unittests/replay_block_invariants_tests.cpp +++ b/unittests/replay_block_invariants_tests.cpp @@ -5,6 +5,7 @@ BOOST_AUTO_TEST_SUITE(replay_block_invariants_tests) using namespace eosio::testing; using namespace eosio::chain; +using namespace fc::crypto; struct blog_replay_fixture { eosio::testing::tester chain; @@ -34,7 +35,7 @@ struct blog_replay_fixture { std::filesystem::create_directories(state_path); } - void corrupt_qc_extension_in_block_log() { + void corrupt_qc_signature_in_block_log() { controller::config config = chain.get_config(); auto blocks_dir = chain.get_config().blocks_dir; @@ -72,12 +73,11 @@ struct blog_replay_fixture { BOOST_REQUIRE(itr != exts.end()); qc_block->block_extensions.erase(itr); - // intentionally corrupt QC's strength. - if (qc.active_policy_sig.strong_votes) { - (*qc.active_policy_sig.strong_votes)[0] ^= 1; // flip one bit - } else if (qc.active_policy_sig.weak_votes) { - (*qc.active_policy_sig.weak_votes)[0] ^= 1; // flip one bit - } + // intentionally corrupt QC's signature. + auto g2 = qc.active_policy_sig.sig.jacobian_montgomery_le(); + g2 = bls12_381::aggregate_signatures(std::array{g2, g2}); + auto affine = g2.toAffineBytesLE(bls12_381::from_mont::yes); + qc.active_policy_sig.sig = blslib::bls_aggregate_signature(blslib::bls_signature(affine)); // add the corrupted QC block extension back to the block emplace_extension(exts, qc_ext_id, fc::raw::pack(qc_ext)); @@ -191,7 +191,7 @@ BOOST_FIXTURE_TEST_CASE(bad_qc_no_force_all_checks, blog_replay_fixture) try { controller::config config = chain.get_config(); auto blocks_dir = chain.get_config().blocks_dir; - corrupt_qc_extension_in_block_log(); + corrupt_qc_signature_in_block_log(); // remove the state files to make sure we are starting from block log remove_existing_states(config.state_dir); @@ -212,7 +212,7 @@ BOOST_FIXTURE_TEST_CASE(bad_qc_force_all_checks, blog_replay_fixture) try { controller::config config = chain.get_config(); auto blocks_dir = chain.get_config().blocks_dir; - corrupt_qc_extension_in_block_log(); + corrupt_qc_signature_in_block_log(); // remove the state files to make sure we are starting from block log remove_existing_states(config.state_dir); @@ -226,7 +226,7 @@ BOOST_FIXTURE_TEST_CASE(bad_qc_force_all_checks, blog_replay_fixture) try { eosio::testing::tester replay_chain(config, *genesis); // start replay BOOST_FAIL("replay should have failed with --force-all-checks"); } catch (block_validate_exception& e) { - BOOST_REQUIRE(e.to_detail_string().find("strong quorum is not met") != std::string::npos); + BOOST_REQUIRE(e.to_detail_string().find("qc signature validation failed") != std::string::npos); } catch (...) { BOOST_FAIL("replay failed with non block_validate_exception exception"); } From 081f9c5ac107ff0e67938da20032d68f0fea19e6 Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Thu, 12 Sep 2024 09:57:43 -0400 Subject: [PATCH 06/13] Add more comments --- unittests/replay_block_invariants_tests.cpp | 34 +++++++++++++++------ 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/unittests/replay_block_invariants_tests.cpp b/unittests/replay_block_invariants_tests.cpp index 2835c990b9..aa635dc5ae 100644 --- a/unittests/replay_block_invariants_tests.cpp +++ b/unittests/replay_block_invariants_tests.cpp @@ -1,17 +1,26 @@ #include #include +// Test scenarios +// * replay through through a block containing an invalid QC claim block num (backward). +// * replay through through a block containing a QC claim block num referring to an +// unknown block number. +// * replay through through a block containing a invalid QC signature, +// without --force-all-checks +// * replay through through a block containing a invalid QC signature, +// with --force-all-checks + BOOST_AUTO_TEST_SUITE(replay_block_invariants_tests) using namespace eosio::testing; using namespace eosio::chain; using namespace fc::crypto; -struct blog_replay_fixture { +struct test_fixture { eosio::testing::tester chain; - // Create blocks log - blog_replay_fixture() { + // Creates blocks log + test_fixture() { // Create a few accounts and produce a few blocks to fill in blocks log chain.create_account("replay1"_n); chain.produce_blocks(1); @@ -30,11 +39,13 @@ struct blog_replay_fixture { chain.close(); } + // Removes state directory void remove_existing_states(std::filesystem::path& state_path) { std::filesystem::remove_all(state_path); std::filesystem::create_directories(state_path); } + // Corrupts the signature of last block which attaches a QC in the blocks log void corrupt_qc_signature_in_block_log() { controller::config config = chain.get_config(); auto blocks_dir = chain.get_config().blocks_dir; @@ -87,6 +98,8 @@ struct blog_replay_fixture { new_blog.append(qc_block, qc_block->calculate_id()); } + // Corrupts finality_extension in the last block of the blocks log + // by setting the claimed block number to a different one. void corrupt_finality_extension_in_block_log(uint32_t new_qc_claim_block_num) { controller::config config = chain.get_config(); auto blocks_dir = chain.get_config().blocks_dir; @@ -129,7 +142,7 @@ struct blog_replay_fixture { }; // Test replay with invalid QC claim -- claimed block number goes backward -BOOST_FIXTURE_TEST_CASE(invalid_qc, blog_replay_fixture) try { +BOOST_FIXTURE_TEST_CASE(invalid_qc, test_fixture) try { controller::config config = chain.get_config(); auto blocks_dir = chain.get_config().blocks_dir; @@ -145,7 +158,8 @@ BOOST_FIXTURE_TEST_CASE(invalid_qc, blog_replay_fixture) try { remove_existing_states(config.state_dir); try { - eosio::testing::tester replay_chain(config, *genesis); // // start replay + eosio::testing::tester replay_chain(config, *genesis); // start replay + // An exception should have thrown BOOST_FAIL("replay should have failed with invalid_qc_claim exception"); } catch (invalid_qc_claim& e) { BOOST_REQUIRE(e.to_detail_string().find("less than the previous block") != std::string::npos); @@ -156,14 +170,14 @@ BOOST_FIXTURE_TEST_CASE(invalid_qc, blog_replay_fixture) try { // Test replay with irrelevant QC -- claims a block number other than the one // claimed in the block header -BOOST_FIXTURE_TEST_CASE(irrelevant_qc, blog_replay_fixture) try { +BOOST_FIXTURE_TEST_CASE(irrelevant_qc, test_fixture) try { controller::config config = chain.get_config(); auto blocks_dir = chain.get_config().blocks_dir; block_log blog(blocks_dir, chain.get_config().blog); // retrieve the last block in block log - uint32_t last_block_num = blog.head()->block_num(); + uint32_t last_block_num = blog.head()->block_num(); // set claimed block number to a non-existent number corrupt_finality_extension_in_block_log(last_block_num + 1); @@ -177,6 +191,7 @@ BOOST_FIXTURE_TEST_CASE(irrelevant_qc, blog_replay_fixture) try { try { eosio::testing::tester replay_chain(config, *genesis); // start replay + // An exception should have thrown BOOST_FAIL("replay should have failed with block_validate_exception exception"); } catch (block_validate_exception& e) { BOOST_REQUIRE(e.to_detail_string().find("Mismatch between qc.block_num") != std::string::npos); @@ -187,7 +202,7 @@ BOOST_FIXTURE_TEST_CASE(irrelevant_qc, blog_replay_fixture) try { // Test replay with bad QC (signature validation), but run without --force-all-checks. // Replay should pass as QC is not validated. -BOOST_FIXTURE_TEST_CASE(bad_qc_no_force_all_checks, blog_replay_fixture) try { +BOOST_FIXTURE_TEST_CASE(bad_qc_no_force_all_checks, test_fixture) try { controller::config config = chain.get_config(); auto blocks_dir = chain.get_config().blocks_dir; @@ -208,7 +223,7 @@ BOOST_FIXTURE_TEST_CASE(bad_qc_no_force_all_checks, blog_replay_fixture) try { // Test replay with bad QC (signature validation), but run with --force-all-checks. // Replay should fail as QC is validated. -BOOST_FIXTURE_TEST_CASE(bad_qc_force_all_checks, blog_replay_fixture) try { +BOOST_FIXTURE_TEST_CASE(bad_qc_force_all_checks, test_fixture) try { controller::config config = chain.get_config(); auto blocks_dir = chain.get_config().blocks_dir; @@ -224,6 +239,7 @@ BOOST_FIXTURE_TEST_CASE(bad_qc_force_all_checks, blog_replay_fixture) try { try { eosio::testing::tester replay_chain(config, *genesis); // start replay + // An exception should have thrown BOOST_FAIL("replay should have failed with --force-all-checks"); } catch (block_validate_exception& e) { BOOST_REQUIRE(e.to_detail_string().find("qc signature validation failed") != std::string::npos); From 3d31a30c524d860a92520d6ceae0eb6f865e00da Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Thu, 12 Sep 2024 10:03:55 -0400 Subject: [PATCH 07/13] Use a common fin_ext_id --- unittests/replay_block_invariants_tests.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/unittests/replay_block_invariants_tests.cpp b/unittests/replay_block_invariants_tests.cpp index aa635dc5ae..db3bdcaf69 100644 --- a/unittests/replay_block_invariants_tests.cpp +++ b/unittests/replay_block_invariants_tests.cpp @@ -47,15 +47,15 @@ struct test_fixture { // Corrupts the signature of last block which attaches a QC in the blocks log void corrupt_qc_signature_in_block_log() { - controller::config config = chain.get_config(); - auto blocks_dir = chain.get_config().blocks_dir; + controller::config config = chain.get_config(); + auto blocks_dir = chain.get_config().blocks_dir; + auto qc_ext_id = quorum_certificate_extension::extension_id(); block_log blog(blocks_dir, chain.get_config().blog); // find the first block which has QC extension starting from the end of // block log and going backward uint32_t block_num = blog.head()->block_num(); - auto qc_ext_id = quorum_certificate_extension::extension_id(); while (!blog.read_block_by_num(block_num)->contains_extension(qc_ext_id)) { --block_num; BOOST_REQUIRE(block_num != 0); @@ -101,8 +101,9 @@ struct test_fixture { // Corrupts finality_extension in the last block of the blocks log // by setting the claimed block number to a different one. void corrupt_finality_extension_in_block_log(uint32_t new_qc_claim_block_num) { - controller::config config = chain.get_config(); - auto blocks_dir = chain.get_config().blocks_dir; + controller::config config = chain.get_config(); + auto blocks_dir = chain.get_config().blocks_dir; + auto fin_ext_id = finality_extension::extension_id(); block_log blog(blocks_dir, chain.get_config().blog); @@ -116,12 +117,12 @@ struct test_fixture { BOOST_REQUIRE_NO_THROW(block_log::smoke_test(blocks_dir, 1)); // retrieve finality extension - std::optional head_fin_ext = last_block->extract_header_extension(finality_extension::extension_id()); + std::optional head_fin_ext = last_block->extract_header_extension(fin_ext_id); BOOST_TEST(!!head_fin_ext); // remove finality extension from extensions auto& exts = last_block->header_extensions; - std::pair> target{finality_extension::extension_id(), {}}; + std::pair> target{fin_ext_id, {}}; auto itr = std::lower_bound(exts.begin(), exts.end(), target, [](const auto& ext1, const auto& ext2){ return ext1.first < ext2.first; }); @@ -133,7 +134,7 @@ struct test_fixture { f_ext.qc_claim.block_num = new_qc_claim_block_num; // add the corrupted finality extension back to last block - emplace_extension(exts, finality_extension::extension_id(), fc::raw::pack(f_ext)); + emplace_extension(exts, fin_ext_id, fc::raw::pack(f_ext)); // add the corrupted block to block log block_log new_blog(blocks_dir, config.blog); From e94ee707a434fa667c04b7b5b3792f9a92c1fb4f Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Thu, 12 Sep 2024 10:49:21 -0400 Subject: [PATCH 08/13] remove through from through through in comments --- unittests/replay_block_invariants_tests.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/unittests/replay_block_invariants_tests.cpp b/unittests/replay_block_invariants_tests.cpp index db3bdcaf69..9b0a64fdf8 100644 --- a/unittests/replay_block_invariants_tests.cpp +++ b/unittests/replay_block_invariants_tests.cpp @@ -2,12 +2,12 @@ #include // Test scenarios -// * replay through through a block containing an invalid QC claim block num (backward). -// * replay through through a block containing a QC claim block num referring to an +// * replay through a block containing an invalid QC claim block num (backward). +// * replay through a block containing a QC claim block num referring to an // unknown block number. -// * replay through through a block containing a invalid QC signature, +// * replay through a block containing a invalid QC signature, // without --force-all-checks -// * replay through through a block containing a invalid QC signature, +// * replay through a block containing a invalid QC signature, // with --force-all-checks BOOST_AUTO_TEST_SUITE(replay_block_invariants_tests) From d9f6faf6cf537398f4038bed9325ea2a871e6baf Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Thu, 12 Sep 2024 11:22:44 -0400 Subject: [PATCH 09/13] Use config instead of chain.get_config() --- unittests/replay_block_invariants_tests.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/unittests/replay_block_invariants_tests.cpp b/unittests/replay_block_invariants_tests.cpp index 9b0a64fdf8..74e19a50db 100644 --- a/unittests/replay_block_invariants_tests.cpp +++ b/unittests/replay_block_invariants_tests.cpp @@ -48,10 +48,10 @@ struct test_fixture { // Corrupts the signature of last block which attaches a QC in the blocks log void corrupt_qc_signature_in_block_log() { controller::config config = chain.get_config(); - auto blocks_dir = chain.get_config().blocks_dir; + auto blocks_dir = config.blocks_dir; auto qc_ext_id = quorum_certificate_extension::extension_id(); - block_log blog(blocks_dir, chain.get_config().blog); + block_log blog(blocks_dir, config.blog); // find the first block which has QC extension starting from the end of // block log and going backward @@ -102,10 +102,10 @@ struct test_fixture { // by setting the claimed block number to a different one. void corrupt_finality_extension_in_block_log(uint32_t new_qc_claim_block_num) { controller::config config = chain.get_config(); - auto blocks_dir = chain.get_config().blocks_dir; + auto blocks_dir = config.blocks_dir; auto fin_ext_id = finality_extension::extension_id(); - block_log blog(blocks_dir, chain.get_config().blog); + block_log blog(blocks_dir, config.blog); // retrieve the last block in block log uint32_t last_block_num = blog.head()->block_num(); @@ -145,13 +145,13 @@ struct test_fixture { // Test replay with invalid QC claim -- claimed block number goes backward BOOST_FIXTURE_TEST_CASE(invalid_qc, test_fixture) try { controller::config config = chain.get_config(); - auto blocks_dir = chain.get_config().blocks_dir; + auto blocks_dir = config.blocks_dir; // set claimed block number backward corrupt_finality_extension_in_block_log(0); // retrieve genesis - block_log blog(blocks_dir, chain.get_config().blog); + block_log blog(blocks_dir, config.blog); auto genesis = block_log::extract_genesis_state(blocks_dir); BOOST_REQUIRE(genesis); @@ -173,9 +173,9 @@ BOOST_FIXTURE_TEST_CASE(invalid_qc, test_fixture) try { // claimed in the block header BOOST_FIXTURE_TEST_CASE(irrelevant_qc, test_fixture) try { controller::config config = chain.get_config(); - auto blocks_dir = chain.get_config().blocks_dir; + auto blocks_dir = config.blocks_dir; - block_log blog(blocks_dir, chain.get_config().blog); + block_log blog(blocks_dir, config.blog); // retrieve the last block in block log uint32_t last_block_num = blog.head()->block_num(); @@ -205,7 +205,7 @@ BOOST_FIXTURE_TEST_CASE(irrelevant_qc, test_fixture) try { // Replay should pass as QC is not validated. BOOST_FIXTURE_TEST_CASE(bad_qc_no_force_all_checks, test_fixture) try { controller::config config = chain.get_config(); - auto blocks_dir = chain.get_config().blocks_dir; + auto blocks_dir = config.blocks_dir; corrupt_qc_signature_in_block_log(); @@ -226,7 +226,7 @@ BOOST_FIXTURE_TEST_CASE(bad_qc_no_force_all_checks, test_fixture) try { // Replay should fail as QC is validated. BOOST_FIXTURE_TEST_CASE(bad_qc_force_all_checks, test_fixture) try { controller::config config = chain.get_config(); - auto blocks_dir = chain.get_config().blocks_dir; + auto blocks_dir = config.blocks_dir; corrupt_qc_signature_in_block_log(); From 3b18b8a4638b5fdcfa712eaf1fd5ec5104dfbe64 Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Thu, 12 Sep 2024 11:35:40 -0400 Subject: [PATCH 10/13] Use std::erase_if for deletion --- unittests/replay_block_invariants_tests.cpp | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/unittests/replay_block_invariants_tests.cpp b/unittests/replay_block_invariants_tests.cpp index 74e19a50db..a62c0a6bdd 100644 --- a/unittests/replay_block_invariants_tests.cpp +++ b/unittests/replay_block_invariants_tests.cpp @@ -77,12 +77,9 @@ struct test_fixture { // remove QC block extension from QC block auto& exts = qc_block->block_extensions; - std::pair> target{qc_ext_id, {}}; - auto itr = std::lower_bound(exts.begin(), exts.end(), target, [](const auto& ext1, const auto& ext2){ - return ext1.first < ext2.first; + std::erase_if(exts, [&](const auto& ext) { + return ext.first == qc_ext_id; }); - BOOST_REQUIRE(itr != exts.end()); - qc_block->block_extensions.erase(itr); // intentionally corrupt QC's signature. auto g2 = qc.active_policy_sig.sig.jacobian_montgomery_le(); @@ -122,12 +119,9 @@ struct test_fixture { // remove finality extension from extensions auto& exts = last_block->header_extensions; - std::pair> target{fin_ext_id, {}}; - auto itr = std::lower_bound(exts.begin(), exts.end(), target, [](const auto& ext1, const auto& ext2){ - return ext1.first < ext2.first; + std::erase_if(exts, [&](const auto& ext) { + return ext.first == fin_ext_id; }); - BOOST_REQUIRE(itr != exts.end()); - exts.erase(itr); // intentionally corrupt finality extension by changing its block_num auto& f_ext = std::get(*head_fin_ext); From c84093811c60b63e81b9912aac087bd466bf1fff Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Thu, 12 Sep 2024 11:40:42 -0400 Subject: [PATCH 11/13] Add a comment --- unittests/replay_block_invariants_tests.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/unittests/replay_block_invariants_tests.cpp b/unittests/replay_block_invariants_tests.cpp index a62c0a6bdd..7d4fe0eafd 100644 --- a/unittests/replay_block_invariants_tests.cpp +++ b/unittests/replay_block_invariants_tests.cpp @@ -130,7 +130,8 @@ struct test_fixture { // add the corrupted finality extension back to last block emplace_extension(exts, fin_ext_id, fc::raw::pack(f_ext)); - // add the corrupted block to block log + // add the corrupted block to block log. Need to instantiate a + // new_blog as a block has been removed from blocks log. block_log new_blog(blocks_dir, config.blog); new_blog.append(last_block, last_block->calculate_id()); } From 601c7ccad639a441bc2fd0a1c0552a9dfd2195e6 Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Thu, 12 Sep 2024 11:51:14 -0400 Subject: [PATCH 12/13] Use const controller::config& --- unittests/replay_block_invariants_tests.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/unittests/replay_block_invariants_tests.cpp b/unittests/replay_block_invariants_tests.cpp index 7d4fe0eafd..5807c3717e 100644 --- a/unittests/replay_block_invariants_tests.cpp +++ b/unittests/replay_block_invariants_tests.cpp @@ -47,9 +47,9 @@ struct test_fixture { // Corrupts the signature of last block which attaches a QC in the blocks log void corrupt_qc_signature_in_block_log() { - controller::config config = chain.get_config(); - auto blocks_dir = config.blocks_dir; - auto qc_ext_id = quorum_certificate_extension::extension_id(); + const controller::config& config = chain.get_config(); + auto blocks_dir = config.blocks_dir; + auto qc_ext_id = quorum_certificate_extension::extension_id(); block_log blog(blocks_dir, config.blog); @@ -98,9 +98,9 @@ struct test_fixture { // Corrupts finality_extension in the last block of the blocks log // by setting the claimed block number to a different one. void corrupt_finality_extension_in_block_log(uint32_t new_qc_claim_block_num) { - controller::config config = chain.get_config(); - auto blocks_dir = config.blocks_dir; - auto fin_ext_id = finality_extension::extension_id(); + const controller::config& config = chain.get_config(); + auto blocks_dir = config.blocks_dir; + auto fin_ext_id = finality_extension::extension_id(); block_log blog(blocks_dir, config.blog); From 35653746b300c35426fe8eaba12a1b8e14bf5ae0 Mon Sep 17 00:00:00 2001 From: Lin Huang Date: Thu, 12 Sep 2024 12:17:04 -0400 Subject: [PATCH 13/13] More const controller::config& --- unittests/replay_block_invariants_tests.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/unittests/replay_block_invariants_tests.cpp b/unittests/replay_block_invariants_tests.cpp index 5807c3717e..9fd5e52501 100644 --- a/unittests/replay_block_invariants_tests.cpp +++ b/unittests/replay_block_invariants_tests.cpp @@ -40,7 +40,7 @@ struct test_fixture { } // Removes state directory - void remove_existing_states(std::filesystem::path& state_path) { + void remove_existing_states(const std::filesystem::path& state_path) { std::filesystem::remove_all(state_path); std::filesystem::create_directories(state_path); } @@ -139,7 +139,7 @@ struct test_fixture { // Test replay with invalid QC claim -- claimed block number goes backward BOOST_FIXTURE_TEST_CASE(invalid_qc, test_fixture) try { - controller::config config = chain.get_config(); + const controller::config& config = chain.get_config(); auto blocks_dir = config.blocks_dir; // set claimed block number backward @@ -167,7 +167,7 @@ BOOST_FIXTURE_TEST_CASE(invalid_qc, test_fixture) try { // Test replay with irrelevant QC -- claims a block number other than the one // claimed in the block header BOOST_FIXTURE_TEST_CASE(irrelevant_qc, test_fixture) try { - controller::config config = chain.get_config(); + const controller::config& config = chain.get_config(); auto blocks_dir = config.blocks_dir; block_log blog(blocks_dir, config.blog); @@ -199,7 +199,7 @@ BOOST_FIXTURE_TEST_CASE(irrelevant_qc, test_fixture) try { // Test replay with bad QC (signature validation), but run without --force-all-checks. // Replay should pass as QC is not validated. BOOST_FIXTURE_TEST_CASE(bad_qc_no_force_all_checks, test_fixture) try { - controller::config config = chain.get_config(); + const controller::config& config = chain.get_config(); auto blocks_dir = config.blocks_dir; corrupt_qc_signature_in_block_log();