0
0
Fork 0
mirror of https://github.com/bitcoin/bitcoin.git synced 2025-02-20 12:12:41 -05:00
bitcoin-bitcoin-core/src/test/validation_chainstatemanager_tests.cpp
Andrew Chow d5e4f9a439
Merge bitcoin/bitcoin#25740: assumeutxo: background validation completion
2b373fe49d docs: update assumeutxo.md (James O'Beirne)
87a1108c81 test: add snapshot completion unittests (James O'Beirne)
d70919a88f refactor: make MempoolMutex() public (James O'Beirne)
7300ced9de log: add LoadBlockIndex() message for assumedvalid blocks (James O'Beirne)
d96c59cc5c validation: add ChainMan logic for completing UTXO snapshot validation (James O'Beirne)
f2a4f3376f move-only-ish: init: factor out chainstate initialization (James O'Beirne)
637a90b973 add Chainstate::HasCoinsViews() (James O'Beirne)
c29f26b47b validation: add CChainState::m_disabled and ChainMan::isUsable (James O'Beirne)
5ee22cdafd add ChainstateManager.GetSnapshot{BaseHeight,BaseBlock}() (James O'Beirne)

Pull request description:

  This is part of the [assumeutxo project](https://github.com/bitcoin/bitcoin/projects/11) (parent PR: https://github.com/bitcoin/bitcoin/pull/15606)

  Part two of replacing https://github.com/bitcoin/bitcoin/pull/24232.

  ---

  When a user activates a snapshot, the serialized UTXO set data is used to create an "assumed-valid" chainstate, which becomes active in an attempt to get the node to network tip as quickly as possible. Simultaneously in the background, the already-existing chainstate continues "conventional" IBD to both accumulate full block data and serve as a belt-and-suspenders to validate the assumed-valid chainstate.

  Once the background chainstate's tip reaches the base block of the snapshot used, we set `m_stop_use` on that chainstate and immediately take the hash of its UTXO set; we verify that this matches the assumeutxo value in the source code. Note that while we ultimately want to remove this background chainstate, we don't do so until the following initialization process, when we again check the UTXO set hash of the background chainstate, and if it continues to match, we remove the (now unnecessary) background chainstate, and move the (previously) assumed-valid chainstate into its place. We then reinitialize the chainstate in the normal way.

  As noted in previous comments, we could do the filesystem operations "inline" immediately when the background validation completes, but that's basically just an optimization that saves disk space until the next restart. It didn't strike me as worth the risk of moving chainstate data around on disk during runtime of the node, though maybe my concerns are overblown.

  The final result of this completion process is a fully-validated chain, where the only evidence that the user synced using assumeutxo is the existence of a `base_blockhash` file in the `chainstate` directory.

ACKs for top commit:
  achow101:
    ACK 2b373fe49d

Tree-SHA512: a204e1d6e6932dd83c799af3606b01a9faf893f04e9ee1a36d63f2f1ccfa9118bdc1c107d86976aa0312814267e6a42074bf3e2bf1dead4b2513efc6d955e13d
2023-03-07 18:54:59 -05:00

694 lines
27 KiB
C++

// Copyright (c) 2019-2022 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
//
#include <chainparams.h>
#include <consensus/validation.h>
#include <node/utxo_snapshot.h>
#include <random.h>
#include <rpc/blockchain.h>
#include <sync.h>
#include <test/util/chainstate.h>
#include <test/util/random.h>
#include <test/util/setup_common.h>
#include <timedata.h>
#include <uint256.h>
#include <validation.h>
#include <validationinterface.h>
#include <tinyformat.h>
#include <vector>
#include <boost/test/unit_test.hpp>
using node::SnapshotMetadata;
BOOST_FIXTURE_TEST_SUITE(validation_chainstatemanager_tests, ChainTestingSetup)
//! Basic tests for ChainstateManager.
//!
//! First create a legacy (IBD) chainstate, then create a snapshot chainstate.
BOOST_AUTO_TEST_CASE(chainstatemanager)
{
ChainstateManager& manager = *m_node.chainman;
CTxMemPool& mempool = *m_node.mempool;
std::vector<Chainstate*> chainstates;
BOOST_CHECK(!manager.SnapshotBlockhash().has_value());
// Create a legacy (IBD) chainstate.
//
Chainstate& c1 = WITH_LOCK(::cs_main, return manager.InitializeChainstate(&mempool));
chainstates.push_back(&c1);
c1.InitCoinsDB(
/*cache_size_bytes=*/1 << 23, /*in_memory=*/true, /*should_wipe=*/false);
WITH_LOCK(::cs_main, c1.InitCoinsCache(1 << 23));
BOOST_CHECK(!manager.IsSnapshotActive());
BOOST_CHECK(WITH_LOCK(::cs_main, return !manager.IsSnapshotValidated()));
auto all = manager.GetAll();
BOOST_CHECK_EQUAL_COLLECTIONS(all.begin(), all.end(), chainstates.begin(), chainstates.end());
auto& active_chain = WITH_LOCK(manager.GetMutex(), return manager.ActiveChain());
BOOST_CHECK_EQUAL(&active_chain, &c1.m_chain);
BOOST_CHECK_EQUAL(WITH_LOCK(manager.GetMutex(), return manager.ActiveHeight()), -1);
auto active_tip = WITH_LOCK(manager.GetMutex(), return manager.ActiveTip());
auto exp_tip = c1.m_chain.Tip();
BOOST_CHECK_EQUAL(active_tip, exp_tip);
BOOST_CHECK(!manager.SnapshotBlockhash().has_value());
// Create a snapshot-based chainstate.
//
const uint256 snapshot_blockhash = GetRandHash();
Chainstate& c2 = WITH_LOCK(::cs_main, return manager.ActivateExistingSnapshot(
&mempool, snapshot_blockhash));
chainstates.push_back(&c2);
BOOST_CHECK_EQUAL(manager.SnapshotBlockhash().value(), snapshot_blockhash);
c2.InitCoinsDB(
/*cache_size_bytes=*/1 << 23, /*in_memory=*/true, /*should_wipe=*/false);
WITH_LOCK(::cs_main, c2.InitCoinsCache(1 << 23));
// Unlike c1, which doesn't have any blocks. Gets us different tip, height.
c2.LoadGenesisBlock();
BlockValidationState _;
BOOST_CHECK(c2.ActivateBestChain(_, nullptr));
BOOST_CHECK(manager.IsSnapshotActive());
BOOST_CHECK(WITH_LOCK(::cs_main, return !manager.IsSnapshotValidated()));
BOOST_CHECK_EQUAL(&c2, &manager.ActiveChainstate());
BOOST_CHECK(&c1 != &manager.ActiveChainstate());
auto all2 = manager.GetAll();
BOOST_CHECK_EQUAL_COLLECTIONS(all2.begin(), all2.end(), chainstates.begin(), chainstates.end());
auto& active_chain2 = WITH_LOCK(manager.GetMutex(), return manager.ActiveChain());
BOOST_CHECK_EQUAL(&active_chain2, &c2.m_chain);
BOOST_CHECK_EQUAL(WITH_LOCK(manager.GetMutex(), return manager.ActiveHeight()), 0);
auto active_tip2 = WITH_LOCK(manager.GetMutex(), return manager.ActiveTip());
auto exp_tip2 = c2.m_chain.Tip();
BOOST_CHECK_EQUAL(active_tip2, exp_tip2);
// Ensure that these pointers actually correspond to different
// CCoinsViewCache instances.
BOOST_CHECK(exp_tip != exp_tip2);
// Let scheduler events finish running to avoid accessing memory that is going to be unloaded
SyncWithValidationInterfaceQueue();
}
//! Test rebalancing the caches associated with each chainstate.
BOOST_AUTO_TEST_CASE(chainstatemanager_rebalance_caches)
{
ChainstateManager& manager = *m_node.chainman;
CTxMemPool& mempool = *m_node.mempool;
size_t max_cache = 10000;
manager.m_total_coinsdb_cache = max_cache;
manager.m_total_coinstip_cache = max_cache;
std::vector<Chainstate*> chainstates;
// Create a legacy (IBD) chainstate.
//
Chainstate& c1 = WITH_LOCK(::cs_main, return manager.InitializeChainstate(&mempool));
chainstates.push_back(&c1);
c1.InitCoinsDB(
/*cache_size_bytes=*/1 << 23, /*in_memory=*/true, /*should_wipe=*/false);
{
LOCK(::cs_main);
c1.InitCoinsCache(1 << 23);
BOOST_REQUIRE(c1.LoadGenesisBlock());
c1.CoinsTip().SetBestBlock(InsecureRand256());
manager.MaybeRebalanceCaches();
}
BOOST_CHECK_EQUAL(c1.m_coinstip_cache_size_bytes, max_cache);
BOOST_CHECK_EQUAL(c1.m_coinsdb_cache_size_bytes, max_cache);
// Create a snapshot-based chainstate.
//
Chainstate& c2 = WITH_LOCK(cs_main, return manager.ActivateExistingSnapshot(&mempool, GetRandHash()));
chainstates.push_back(&c2);
c2.InitCoinsDB(
/*cache_size_bytes=*/1 << 23, /*in_memory=*/true, /*should_wipe=*/false);
{
LOCK(::cs_main);
c2.InitCoinsCache(1 << 23);
BOOST_REQUIRE(c2.LoadGenesisBlock());
c2.CoinsTip().SetBestBlock(InsecureRand256());
manager.MaybeRebalanceCaches();
}
// Since both chainstates are considered to be in initial block download,
// the snapshot chainstate should take priority.
BOOST_CHECK_CLOSE(c1.m_coinstip_cache_size_bytes, max_cache * 0.05, 1);
BOOST_CHECK_CLOSE(c1.m_coinsdb_cache_size_bytes, max_cache * 0.05, 1);
BOOST_CHECK_CLOSE(c2.m_coinstip_cache_size_bytes, max_cache * 0.95, 1);
BOOST_CHECK_CLOSE(c2.m_coinsdb_cache_size_bytes, max_cache * 0.95, 1);
}
struct SnapshotTestSetup : TestChain100Setup {
// Run with coinsdb on the filesystem to support, e.g., moving invalidated
// chainstate dirs to "*_invalid".
//
// Note that this means the tests run considerably slower than in-memory DB
// tests, but we can't otherwise test this functionality since it relies on
// destructive filesystem operations.
SnapshotTestSetup() : TestChain100Setup{
{},
{},
/*coins_db_in_memory=*/false,
/*block_tree_db_in_memory=*/false,
}
{
}
std::tuple<Chainstate*, Chainstate*> SetupSnapshot()
{
ChainstateManager& chainman = *Assert(m_node.chainman);
BOOST_CHECK(!chainman.IsSnapshotActive());
{
LOCK(::cs_main);
BOOST_CHECK(!chainman.IsSnapshotValidated());
BOOST_CHECK(!node::FindSnapshotChainstateDir());
}
size_t initial_size;
size_t initial_total_coins{100};
// Make some initial assertions about the contents of the chainstate.
{
LOCK(::cs_main);
CCoinsViewCache& ibd_coinscache = chainman.ActiveChainstate().CoinsTip();
initial_size = ibd_coinscache.GetCacheSize();
size_t total_coins{0};
for (CTransactionRef& txn : m_coinbase_txns) {
COutPoint op{txn->GetHash(), 0};
BOOST_CHECK(ibd_coinscache.HaveCoin(op));
total_coins++;
}
BOOST_CHECK_EQUAL(total_coins, initial_total_coins);
BOOST_CHECK_EQUAL(initial_size, initial_total_coins);
}
Chainstate& validation_chainstate = chainman.ActiveChainstate();
// Snapshot should refuse to load at this height.
BOOST_REQUIRE(!CreateAndActivateUTXOSnapshot(this));
BOOST_CHECK(!chainman.ActiveChainstate().m_from_snapshot_blockhash);
BOOST_CHECK(!chainman.SnapshotBlockhash());
// Mine 10 more blocks, putting at us height 110 where a valid assumeutxo value can
// be found.
constexpr int snapshot_height = 110;
mineBlocks(10);
initial_size += 10;
initial_total_coins += 10;
// Should not load malleated snapshots
BOOST_REQUIRE(!CreateAndActivateUTXOSnapshot(
this, [](AutoFile& auto_infile, SnapshotMetadata& metadata) {
// A UTXO is missing but count is correct
metadata.m_coins_count -= 1;
COutPoint outpoint;
Coin coin;
auto_infile >> outpoint;
auto_infile >> coin;
}));
BOOST_CHECK(!node::FindSnapshotChainstateDir());
BOOST_REQUIRE(!CreateAndActivateUTXOSnapshot(
this, [](AutoFile& auto_infile, SnapshotMetadata& metadata) {
// Coins count is larger than coins in file
metadata.m_coins_count += 1;
}));
BOOST_REQUIRE(!CreateAndActivateUTXOSnapshot(
this, [](AutoFile& auto_infile, SnapshotMetadata& metadata) {
// Coins count is smaller than coins in file
metadata.m_coins_count -= 1;
}));
BOOST_REQUIRE(!CreateAndActivateUTXOSnapshot(
this, [](AutoFile& auto_infile, SnapshotMetadata& metadata) {
// Wrong hash
metadata.m_base_blockhash = uint256::ZERO;
}));
BOOST_REQUIRE(!CreateAndActivateUTXOSnapshot(
this, [](AutoFile& auto_infile, SnapshotMetadata& metadata) {
// Wrong hash
metadata.m_base_blockhash = uint256::ONE;
}));
BOOST_REQUIRE(CreateAndActivateUTXOSnapshot(this));
BOOST_CHECK(fs::exists(*node::FindSnapshotChainstateDir()));
// Ensure our active chain is the snapshot chainstate.
BOOST_CHECK(!chainman.ActiveChainstate().m_from_snapshot_blockhash->IsNull());
BOOST_CHECK_EQUAL(
*chainman.ActiveChainstate().m_from_snapshot_blockhash,
*chainman.SnapshotBlockhash());
Chainstate& snapshot_chainstate = chainman.ActiveChainstate();
{
LOCK(::cs_main);
fs::path found = *node::FindSnapshotChainstateDir();
// Note: WriteSnapshotBaseBlockhash() is implicitly tested above.
BOOST_CHECK_EQUAL(
*node::ReadSnapshotBaseBlockhash(found),
*chainman.SnapshotBlockhash());
// Ensure that the genesis block was not marked assumed-valid.
BOOST_CHECK(!chainman.ActiveChain().Genesis()->IsAssumedValid());
}
const AssumeutxoData& au_data = *ExpectedAssumeutxo(snapshot_height, ::Params());
const CBlockIndex* tip = WITH_LOCK(chainman.GetMutex(), return chainman.ActiveTip());
BOOST_CHECK_EQUAL(tip->nChainTx, au_data.nChainTx);
// To be checked against later when we try loading a subsequent snapshot.
uint256 loaded_snapshot_blockhash{*chainman.SnapshotBlockhash()};
// Make some assertions about the both chainstates. These checks ensure the
// legacy chainstate hasn't changed and that the newly created chainstate
// reflects the expected content.
{
LOCK(::cs_main);
int chains_tested{0};
for (Chainstate* chainstate : chainman.GetAll()) {
BOOST_TEST_MESSAGE("Checking coins in " << chainstate->ToString());
CCoinsViewCache& coinscache = chainstate->CoinsTip();
// Both caches will be empty initially.
BOOST_CHECK_EQUAL((unsigned int)0, coinscache.GetCacheSize());
size_t total_coins{0};
for (CTransactionRef& txn : m_coinbase_txns) {
COutPoint op{txn->GetHash(), 0};
BOOST_CHECK(coinscache.HaveCoin(op));
total_coins++;
}
BOOST_CHECK_EQUAL(initial_size , coinscache.GetCacheSize());
BOOST_CHECK_EQUAL(total_coins, initial_total_coins);
chains_tested++;
}
BOOST_CHECK_EQUAL(chains_tested, 2);
}
// Mine some new blocks on top of the activated snapshot chainstate.
constexpr size_t new_coins{100};
mineBlocks(new_coins); // Defined in TestChain100Setup.
{
LOCK(::cs_main);
size_t coins_in_active{0};
size_t coins_in_background{0};
size_t coins_missing_from_background{0};
for (Chainstate* chainstate : chainman.GetAll()) {
BOOST_TEST_MESSAGE("Checking coins in " << chainstate->ToString());
CCoinsViewCache& coinscache = chainstate->CoinsTip();
bool is_background = chainstate != &chainman.ActiveChainstate();
for (CTransactionRef& txn : m_coinbase_txns) {
COutPoint op{txn->GetHash(), 0};
if (coinscache.HaveCoin(op)) {
(is_background ? coins_in_background : coins_in_active)++;
} else if (is_background) {
coins_missing_from_background++;
}
}
}
BOOST_CHECK_EQUAL(coins_in_active, initial_total_coins + new_coins);
BOOST_CHECK_EQUAL(coins_in_background, initial_total_coins);
BOOST_CHECK_EQUAL(coins_missing_from_background, new_coins);
}
// Snapshot should refuse to load after one has already loaded.
BOOST_REQUIRE(!CreateAndActivateUTXOSnapshot(this));
// Snapshot blockhash should be unchanged.
BOOST_CHECK_EQUAL(
*chainman.ActiveChainstate().m_from_snapshot_blockhash,
loaded_snapshot_blockhash);
return std::make_tuple(&validation_chainstate, &snapshot_chainstate);
}
// Simulate a restart of the node by flushing all state to disk, clearing the
// existing ChainstateManager, and unloading the block index.
//
// @returns a reference to the "restarted" ChainstateManager
ChainstateManager& SimulateNodeRestart()
{
ChainstateManager& chainman = *Assert(m_node.chainman);
BOOST_TEST_MESSAGE("Simulating node restart");
{
LOCK(::cs_main);
for (Chainstate* cs : chainman.GetAll()) {
cs->ForceFlushStateToDisk();
}
chainman.ResetChainstates();
BOOST_CHECK_EQUAL(chainman.GetAll().size(), 0);
const ChainstateManager::Options chainman_opts{
.chainparams = ::Params(),
.datadir = m_args.GetDataDirNet(),
.adjusted_time_callback = GetAdjustedTime,
};
// For robustness, ensure the old manager is destroyed before creating a
// new one.
m_node.chainman.reset();
m_node.chainman.reset(new ChainstateManager(chainman_opts));
}
return *Assert(m_node.chainman);
}
};
//! Test basic snapshot activation.
BOOST_FIXTURE_TEST_CASE(chainstatemanager_activate_snapshot, SnapshotTestSetup)
{
this->SetupSnapshot();
}
//! Test LoadBlockIndex behavior when multiple chainstates are in use.
//!
//! - First, verify that setBlockIndexCandidates is as expected when using a single,
//! fully-validating chainstate.
//!
//! - Then mark a region of the chain BLOCK_ASSUMED_VALID and introduce a second chainstate
//! that will tolerate assumed-valid blocks. Run LoadBlockIndex() and ensure that the first
//! chainstate only contains fully validated blocks and the other chainstate contains all blocks,
//! even those assumed-valid.
//!
BOOST_FIXTURE_TEST_CASE(chainstatemanager_loadblockindex, TestChain100Setup)
{
ChainstateManager& chainman = *Assert(m_node.chainman);
CTxMemPool& mempool = *m_node.mempool;
Chainstate& cs1 = chainman.ActiveChainstate();
int num_indexes{0};
int num_assumed_valid{0};
const int expected_assumed_valid{20};
const int last_assumed_valid_idx{40};
const int assumed_valid_start_idx = last_assumed_valid_idx - expected_assumed_valid;
CBlockIndex* validated_tip{nullptr};
CBlockIndex* assumed_tip{WITH_LOCK(chainman.GetMutex(), return chainman.ActiveChain().Tip())};
auto reload_all_block_indexes = [&]() {
for (Chainstate* cs : chainman.GetAll()) {
LOCK(::cs_main);
cs->UnloadBlockIndex();
BOOST_CHECK(cs->setBlockIndexCandidates.empty());
}
WITH_LOCK(::cs_main, chainman.LoadBlockIndex());
};
// Ensure that without any assumed-valid BlockIndex entries, all entries are considered
// tip candidates.
reload_all_block_indexes();
BOOST_CHECK_EQUAL(cs1.setBlockIndexCandidates.size(), cs1.m_chain.Height() + 1);
// Mark some region of the chain assumed-valid.
for (int i = 0; i <= cs1.m_chain.Height(); ++i) {
LOCK(::cs_main);
auto index = cs1.m_chain[i];
if (i < last_assumed_valid_idx && i >= assumed_valid_start_idx) {
index->nStatus = BlockStatus::BLOCK_VALID_TREE | BlockStatus::BLOCK_ASSUMED_VALID;
}
++num_indexes;
if (index->IsAssumedValid()) ++num_assumed_valid;
// Note the last fully-validated block as the expected validated tip.
if (i == (assumed_valid_start_idx - 1)) {
validated_tip = index;
BOOST_CHECK(!index->IsAssumedValid());
}
}
BOOST_CHECK_EQUAL(expected_assumed_valid, num_assumed_valid);
Chainstate& cs2 = WITH_LOCK(::cs_main,
return chainman.ActivateExistingSnapshot(&mempool, GetRandHash()));
reload_all_block_indexes();
// The fully validated chain only has candidates up to the start of the assumed-valid
// blocks.
BOOST_CHECK_EQUAL(cs1.setBlockIndexCandidates.count(validated_tip), 1);
BOOST_CHECK_EQUAL(cs1.setBlockIndexCandidates.count(assumed_tip), 0);
BOOST_CHECK_EQUAL(cs1.setBlockIndexCandidates.size(), assumed_valid_start_idx);
// The assumed-valid tolerant chain has all blocks as candidates.
BOOST_CHECK_EQUAL(cs2.setBlockIndexCandidates.count(validated_tip), 1);
BOOST_CHECK_EQUAL(cs2.setBlockIndexCandidates.count(assumed_tip), 1);
BOOST_CHECK_EQUAL(cs2.setBlockIndexCandidates.size(), num_indexes);
}
//! Ensure that snapshot chainstates initialize properly when found on disk.
BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_init, SnapshotTestSetup)
{
ChainstateManager& chainman = *Assert(m_node.chainman);
Chainstate& bg_chainstate = chainman.ActiveChainstate();
this->SetupSnapshot();
fs::path snapshot_chainstate_dir = *node::FindSnapshotChainstateDir();
BOOST_CHECK(fs::exists(snapshot_chainstate_dir));
BOOST_CHECK_EQUAL(snapshot_chainstate_dir, gArgs.GetDataDirNet() / "chainstate_snapshot");
BOOST_CHECK(chainman.IsSnapshotActive());
const uint256 snapshot_tip_hash = WITH_LOCK(chainman.GetMutex(),
return chainman.ActiveTip()->GetBlockHash());
auto all_chainstates = chainman.GetAll();
BOOST_CHECK_EQUAL(all_chainstates.size(), 2);
// "Rewind" the background chainstate so that its tip is not at the
// base block of the snapshot - this is so after simulating a node restart,
// it will initialize instead of attempting to complete validation.
//
// Note that this is not a realistic use of DisconnectTip().
DisconnectedBlockTransactions unused_pool;
BlockValidationState unused_state;
{
LOCK2(::cs_main, bg_chainstate.MempoolMutex());
BOOST_CHECK(bg_chainstate.DisconnectTip(unused_state, &unused_pool));
unused_pool.clear(); // to avoid queuedTx assertion errors on teardown
}
BOOST_CHECK_EQUAL(bg_chainstate.m_chain.Height(), 109);
// Test that simulating a shutdown (resetting ChainstateManager) and then performing
// chainstate reinitializing successfully cleans up the background-validation
// chainstate data, and we end up with a single chainstate that is at tip.
ChainstateManager& chainman_restarted = this->SimulateNodeRestart();
BOOST_TEST_MESSAGE("Performing Load/Verify/Activate of chainstate");
// This call reinitializes the chainstates.
this->LoadVerifyActivateChainstate();
{
LOCK(chainman_restarted.GetMutex());
BOOST_CHECK_EQUAL(chainman_restarted.GetAll().size(), 2);
BOOST_CHECK(chainman_restarted.IsSnapshotActive());
BOOST_CHECK(!chainman_restarted.IsSnapshotValidated());
BOOST_CHECK_EQUAL(chainman_restarted.ActiveTip()->GetBlockHash(), snapshot_tip_hash);
BOOST_CHECK_EQUAL(chainman_restarted.ActiveHeight(), 210);
}
BOOST_TEST_MESSAGE(
"Ensure we can mine blocks on top of the initialized snapshot chainstate");
mineBlocks(10);
{
LOCK(chainman_restarted.GetMutex());
BOOST_CHECK_EQUAL(chainman_restarted.ActiveHeight(), 220);
// Background chainstate should be unaware of new blocks on the snapshot
// chainstate.
for (Chainstate* cs : chainman_restarted.GetAll()) {
if (cs != &chainman_restarted.ActiveChainstate()) {
BOOST_CHECK_EQUAL(cs->m_chain.Height(), 109);
}
}
}
}
BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_completion, SnapshotTestSetup)
{
this->SetupSnapshot();
ChainstateManager& chainman = *Assert(m_node.chainman);
Chainstate& active_cs = chainman.ActiveChainstate();
auto tip_cache_before_complete = active_cs.m_coinstip_cache_size_bytes;
auto db_cache_before_complete = active_cs.m_coinsdb_cache_size_bytes;
SnapshotCompletionResult res;
auto mock_shutdown = [](bilingual_str msg) {};
fs::path snapshot_chainstate_dir = *node::FindSnapshotChainstateDir();
BOOST_CHECK(fs::exists(snapshot_chainstate_dir));
BOOST_CHECK_EQUAL(snapshot_chainstate_dir, gArgs.GetDataDirNet() / "chainstate_snapshot");
BOOST_CHECK(chainman.IsSnapshotActive());
const uint256 snapshot_tip_hash = WITH_LOCK(chainman.GetMutex(),
return chainman.ActiveTip()->GetBlockHash());
res = WITH_LOCK(::cs_main,
return chainman.MaybeCompleteSnapshotValidation(mock_shutdown));
BOOST_CHECK_EQUAL(res, SnapshotCompletionResult::SUCCESS);
WITH_LOCK(::cs_main, BOOST_CHECK(chainman.IsSnapshotValidated()));
BOOST_CHECK(chainman.IsSnapshotActive());
// Cache should have been rebalanced and reallocated to the "only" remaining
// chainstate.
BOOST_CHECK(active_cs.m_coinstip_cache_size_bytes > tip_cache_before_complete);
BOOST_CHECK(active_cs.m_coinsdb_cache_size_bytes > db_cache_before_complete);
auto all_chainstates = chainman.GetAll();
BOOST_CHECK_EQUAL(all_chainstates.size(), 1);
BOOST_CHECK_EQUAL(all_chainstates[0], &active_cs);
// Trying completion again should return false.
res = WITH_LOCK(::cs_main,
return chainman.MaybeCompleteSnapshotValidation(mock_shutdown));
BOOST_CHECK_EQUAL(res, SnapshotCompletionResult::SKIPPED);
// The invalid snapshot path should not have been used.
fs::path snapshot_invalid_dir = gArgs.GetDataDirNet() / "chainstate_snapshot_INVALID";
BOOST_CHECK(!fs::exists(snapshot_invalid_dir));
// chainstate_snapshot should still exist.
BOOST_CHECK(fs::exists(snapshot_chainstate_dir));
// Test that simulating a shutdown (reseting ChainstateManager) and then performing
// chainstate reinitializing successfully cleans up the background-validation
// chainstate data, and we end up with a single chainstate that is at tip.
ChainstateManager& chainman_restarted = this->SimulateNodeRestart();
BOOST_TEST_MESSAGE("Performing Load/Verify/Activate of chainstate");
// This call reinitializes the chainstates, and should clean up the now unnecessary
// background-validation leveldb contents.
this->LoadVerifyActivateChainstate();
BOOST_CHECK(!fs::exists(snapshot_invalid_dir));
// chainstate_snapshot should now *not* exist.
BOOST_CHECK(!fs::exists(snapshot_chainstate_dir));
const Chainstate& active_cs2 = chainman_restarted.ActiveChainstate();
{
LOCK(chainman_restarted.GetMutex());
BOOST_CHECK_EQUAL(chainman_restarted.GetAll().size(), 1);
BOOST_CHECK(!chainman_restarted.IsSnapshotActive());
BOOST_CHECK(!chainman_restarted.IsSnapshotValidated());
BOOST_CHECK(active_cs2.m_coinstip_cache_size_bytes > tip_cache_before_complete);
BOOST_CHECK(active_cs2.m_coinsdb_cache_size_bytes > db_cache_before_complete);
BOOST_CHECK_EQUAL(chainman_restarted.ActiveTip()->GetBlockHash(), snapshot_tip_hash);
BOOST_CHECK_EQUAL(chainman_restarted.ActiveHeight(), 210);
}
BOOST_TEST_MESSAGE(
"Ensure we can mine blocks on top of the \"new\" IBD chainstate");
mineBlocks(10);
{
LOCK(chainman_restarted.GetMutex());
BOOST_CHECK_EQUAL(chainman_restarted.ActiveHeight(), 220);
}
}
BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_completion_hash_mismatch, SnapshotTestSetup)
{
auto chainstates = this->SetupSnapshot();
Chainstate& validation_chainstate = *std::get<0>(chainstates);
ChainstateManager& chainman = *Assert(m_node.chainman);
SnapshotCompletionResult res;
auto mock_shutdown = [](bilingual_str msg) {};
// Test tampering with the IBD UTXO set with an extra coin to ensure it causes
// snapshot completion to fail.
CCoinsViewCache& ibd_coins = WITH_LOCK(::cs_main,
return validation_chainstate.CoinsTip());
Coin badcoin;
badcoin.out.nValue = InsecureRand32();
badcoin.nHeight = 1;
badcoin.out.scriptPubKey.assign(InsecureRandBits(6), 0);
uint256 txid = InsecureRand256();
ibd_coins.AddCoin(COutPoint(txid, 0), std::move(badcoin), false);
fs::path snapshot_chainstate_dir = gArgs.GetDataDirNet() / "chainstate_snapshot";
BOOST_CHECK(fs::exists(snapshot_chainstate_dir));
res = WITH_LOCK(::cs_main,
return chainman.MaybeCompleteSnapshotValidation(mock_shutdown));
BOOST_CHECK_EQUAL(res, SnapshotCompletionResult::HASH_MISMATCH);
auto all_chainstates = chainman.GetAll();
BOOST_CHECK_EQUAL(all_chainstates.size(), 1);
BOOST_CHECK_EQUAL(all_chainstates[0], &validation_chainstate);
BOOST_CHECK_EQUAL(&chainman.ActiveChainstate(), &validation_chainstate);
fs::path snapshot_invalid_dir = gArgs.GetDataDirNet() / "chainstate_snapshot_INVALID";
BOOST_CHECK(fs::exists(snapshot_invalid_dir));
// Test that simulating a shutdown (reseting ChainstateManager) and then performing
// chainstate reinitializing successfully loads only the fully-validated
// chainstate data, and we end up with a single chainstate that is at tip.
ChainstateManager& chainman_restarted = this->SimulateNodeRestart();
BOOST_TEST_MESSAGE("Performing Load/Verify/Activate of chainstate");
// This call reinitializes the chainstates, and should clean up the now unnecessary
// background-validation leveldb contents.
this->LoadVerifyActivateChainstate();
BOOST_CHECK(fs::exists(snapshot_invalid_dir));
BOOST_CHECK(!fs::exists(snapshot_chainstate_dir));
{
LOCK(::cs_main);
BOOST_CHECK_EQUAL(chainman_restarted.GetAll().size(), 1);
BOOST_CHECK(!chainman_restarted.IsSnapshotActive());
BOOST_CHECK(!chainman_restarted.IsSnapshotValidated());
BOOST_CHECK_EQUAL(chainman_restarted.ActiveHeight(), 210);
}
BOOST_TEST_MESSAGE(
"Ensure we can mine blocks on top of the \"new\" IBD chainstate");
mineBlocks(10);
{
LOCK(::cs_main);
BOOST_CHECK_EQUAL(chainman_restarted.ActiveHeight(), 220);
}
}
BOOST_AUTO_TEST_SUITE_END()