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 -> main] Add unit tests to verify replay of blocks with bad QC data #766

Merged
merged 16 commits into from
Sep 12, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 247 additions & 0 deletions unittests/replay_block_invariants_tests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
#include <eosio/testing/tester.hpp>
#include <boost/test/unit_test.hpp>

// Test scenarios
// * 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 a block containing a invalid QC signature,
// without --force-all-checks
// * replay 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 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(const 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() {
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);

// 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::erase_if(exts, [&](const auto& ext) {
return ext.first == qc_ext_id;
});

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

// 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::erase_if(exts, [&](const auto& ext) {
return ext.first == fin_ext_id;
});

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

// Test replay with invalid QC claim -- claimed block number goes backward
BOOST_FIXTURE_TEST_CASE(invalid_qc, test_fixture) try {
const controller::config& config = chain.get_config();
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, 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 {
const controller::config& config = chain.get_config();
auto blocks_dir = config.blocks_dir;

block_log blog(blocks_dir, 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 {
const controller::config& config = chain.get_config();
auto blocks_dir = 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 = 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()