Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1.0.1] Add unit tests to verify replay of blocks with bad QC data #759

Merged
merged 14 commits into from
Sep 12, 2024
Merged
252 changes: 252 additions & 0 deletions unittests/replay_block_invariants_tests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
#include <eosio/testing/tester.hpp>
#include <boost/test/unit_test.hpp>

// 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,
heifner marked this conversation as resolved.
Show resolved Hide resolved
// 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 test_fixture {
eosio::testing::tester chain;

// 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);
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();
}

// Removes state directory
void remove_existing_states(std::filesystem::path& state_path) {
std::filesystem::remove_all(state_path);
std::filesystem::create_directories(state_path);
}
greg7mdp marked this conversation as resolved.
Show resolved Hide resolved

// 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();
greg7mdp marked this conversation as resolved.
Show resolved Hide resolved
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);
greg7mdp marked this conversation as resolved.
Show resolved Hide resolved

// 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();
while (!blog.read_block_by_num(block_num)->contains_extension(qc_ext_id)) {
--block_num;
BOOST_REQUIRE(block_num != 0);
}

// clone the QC block
auto qc_block = std::make_shared<signed_block>(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<quorum_certificate_extension>(qc_ext_itr->second);
auto& qc = qc_ext.qc;

// remove QC block extension from QC block
auto& exts = qc_block->block_extensions;
std::pair<uint16_t,vector<char>> 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);
greg7mdp marked this conversation as resolved.
Show resolved Hide resolved

// 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));

// add the corrupted block back to block log
block_log new_blog(blocks_dir, config.blog);
greg7mdp marked this conversation as resolved.
Show resolved Hide resolved
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;
auto fin_ext_id = finality_extension::extension_id();

block_log blog(blocks_dir, chain.get_config().blog);
greg7mdp marked this conversation as resolved.
Show resolved Hide resolved

// retrieve the last block in block log
uint32_t last_block_num = blog.head()->block_num();
auto last_block = std::make_shared<signed_block>(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<block_header_extension> 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<uint16_t,vector<char>> 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;
});
BOOST_REQUIRE(itr != exts.end());
exts.erase(itr);
greg7mdp marked this conversation as resolved.
Show resolved Hide resolved

// intentionally corrupt finality extension by changing its block_num
auto& f_ext = std::get<finality_extension>(*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, fin_ext_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
BOOST_FIXTURE_TEST_CASE(invalid_qc, test_fixture) try {
controller::config config = chain.get_config();
auto blocks_dir = chain.get_config().blocks_dir;
greg7mdp marked this conversation as resolved.
Show resolved Hide resolved

// set claimed block number backward
corrupt_finality_extension_in_block_log(0);

// 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);

try {
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);
} 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, test_fixture) try {
controller::config config = chain.get_config();
auto blocks_dir = chain.get_config().blocks_dir;
greg7mdp marked this conversation as resolved.
Show resolved Hide resolved

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();

// 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
// 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);
} 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, test_fixture) try {
controller::config config = chain.get_config();
auto blocks_dir = chain.get_config().blocks_dir;

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);

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, test_fixture) try {
controller::config config = chain.get_config();
auto blocks_dir = chain.get_config().blocks_dir;

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);

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
// 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);
} catch (...) {
BOOST_FAIL("replay failed with non block_validate_exception exception");
}
} FC_LOG_AND_RETHROW()

BOOST_AUTO_TEST_SUITE_END()