Skip to content

Commit

Permalink
merge bitcoin#23174: have LoadBlockIndex account for snapshot use
Browse files Browse the repository at this point in the history
  • Loading branch information
kwvg committed Jul 19, 2024
1 parent a08f2f4 commit b402fd5
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 17 deletions.
80 changes: 80 additions & 0 deletions src/test/validation_chainstatemanager_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,9 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_activate_snapshot, TestChain100Setup)
*chainman.ActiveChainstate().m_from_snapshot_blockhash,
*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 = chainman.ActiveTip();

Expand Down Expand Up @@ -326,4 +329,81 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_activate_snapshot, TestChain100Setup)
loaded_snapshot_blockhash);
}

//! Test LoadBlockIndex behavior when multiple chainstates are in use.
//!
//! - First, verfiy 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;
CChainState& 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{chainman.ActiveChain().Tip()};

auto reload_all_block_indexes = [&]() {
for (CChainState* 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) {
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);

CChainState& cs2 = WITH_LOCK(::cs_main,
return chainman.InitializeChainstate(&mempool, *m_node.evodb, m_node.chain_helper, llmq::chainLocksHandler, llmq::quorumInstantSendManager, 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);
}

BOOST_AUTO_TEST_SUITE_END()
81 changes: 70 additions & 11 deletions src/validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@

#include <statsd_client.h>

#include <algorithm>
#include <deque>
#include <numeric>
#include <optional>
Expand Down Expand Up @@ -4245,7 +4246,7 @@ CBlockIndex * BlockManager::InsertBlockIndex(const uint256& hash)

bool BlockManager::LoadBlockIndex(
const Consensus::Params& consensus_params,
std::set<CBlockIndex*, CBlockIndexWorkComparator>& block_index_candidates)
ChainstateManager& chainman)
{
if (!m_block_tree_db->LoadBlockIndexGuts(consensus_params, [this](const uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(cs_main) { return this->InsertBlockIndex(hash); })) {
return false;
Expand All @@ -4265,17 +4266,41 @@ bool BlockManager::LoadBlockIndex(
}
}
sort(vSortedByHeight.begin(), vSortedByHeight.end());

// Find start of assumed-valid region.
int first_assumed_valid_height = std::numeric_limits<int>::max();

for (const auto& [height, block] : vSortedByHeight) {
if (block->IsAssumedValid()) {
auto chainstates = chainman.GetAll();

// If we encounter an assumed-valid block index entry, ensure that we have
// one chainstate that tolerates assumed-valid entries and another that does
// not (i.e. the background validation chainstate), since assumed-valid
// entries should always be pending validation by a fully-validated chainstate.
auto any_chain = [&](auto fnc) { return std::any_of(chainstates.cbegin(), chainstates.cend(), fnc); };
assert(any_chain([](auto chainstate) { return chainstate->reliesOnAssumedValid(); }));
assert(any_chain([](auto chainstate) { return !chainstate->reliesOnAssumedValid(); }));

first_assumed_valid_height = height;
break;
}
}

for (const std::pair<int, CBlockIndex*>& item : vSortedByHeight)
{
if (ShutdownRequested()) return false;
CBlockIndex* pindex = item.second;
pindex->nChainWork = (pindex->pprev ? pindex->pprev->nChainWork : 0) + GetBlockProof(*pindex);
pindex->nTimeMax = (pindex->pprev ? std::max(pindex->pprev->nTimeMax, pindex->nTime) : pindex->nTime);
// We can link the chain of blocks for which we've received transactions at some point.

// We can link the chain of blocks for which we've received transactions at some point, or
// blocks that are assumed-valid on the basis of snapshot load (see
// PopulateAndValidateSnapshot()).
// Pruned nodes may have deleted the block.
if (pindex->nTx > 0) {
if (pindex->pprev) {
if (pindex->pprev->HaveTxsDownloaded()) {
if (pindex->pprev->nChainTx > 0) {
pindex->nChainTx = pindex->pprev->nChainTx + pindex->nTx;
} else {
pindex->nChainTx = 0;
Expand All @@ -4292,7 +4317,36 @@ bool BlockManager::LoadBlockIndex(
if (pindex->IsAssumedValid() ||
(pindex->IsValid(BLOCK_VALID_TRANSACTIONS) &&
(pindex->HaveTxsDownloaded() || pindex->pprev == nullptr))) {
block_index_candidates.insert(pindex);

// Fill each chainstate's block candidate set. Only add assumed-valid
// blocks to the tip candidate set if the chainstate is allowed to rely on
// assumed-valid blocks.
//
// If all setBlockIndexCandidates contained the assumed-valid blocks, the
// background chainstate's ActivateBestChain() call would add assumed-valid
// blocks to the chain (based on how FindMostWorkChain() works). Obviously
// we don't want this since the purpose of the background validation chain
// is to validate assued-valid blocks.
//
// Note: This is considering all blocks whose height is greater or equal to
// the first assumed-valid block to be assumed-valid blocks, and excluding
// them from the background chainstate's setBlockIndexCandidates set. This
// does mean that some blocks which are not technically assumed-valid
// (later blocks on a fork beginning before the first assumed-valid block)
// might not get added to the the background chainstate, but this is ok,
// because they will still be attached to the active chainstate if they
// actually contain more work.
//
// Instad of this height-based approach, an earlier attempt was made at
// detecting "holistically" whether the block index under consideration
// relied on an assumed-valid ancestor, but this proved to be too slow to
// be practical.
for (CChainState* chainstate : chainman.GetAll()) {
if (chainstate->reliesOnAssumedValid() ||
pindex->nHeight < first_assumed_valid_height) {
chainstate->setBlockIndexCandidates.insert(pindex);
}
}
}
if (pindex->nStatus & BLOCK_FAILED_MASK && (!pindexBestInvalid || pindex->nChainWork > pindexBestInvalid->nChainWork))
pindexBestInvalid = pindex;
Expand All @@ -4317,11 +4371,9 @@ void BlockManager::Unload() {
m_prev_block_index.clear();
}

bool BlockManager::LoadBlockIndexDB(std::set<CBlockIndex*, CBlockIndexWorkComparator>& setBlockIndexCandidates)
bool BlockManager::LoadBlockIndexDB(ChainstateManager& chainman)
{
if (!LoadBlockIndex(
::Params().GetConsensus(),
setBlockIndexCandidates)) {
if (!LoadBlockIndex(::Params().GetConsensus(), chainman)) {
return false;
}

Expand Down Expand Up @@ -4758,7 +4810,7 @@ bool ChainstateManager::LoadBlockIndex()
// Load block index from databases
bool needs_init = fReindex;
if (!fReindex) {
bool ret = m_blockman.LoadBlockIndexDB(ActiveChainstate().setBlockIndexCandidates);
bool ret = m_blockman.LoadBlockIndexDB(*this);
if (!ret) return false;
needs_init = m_blockman.m_block_index.empty();
}
Expand Down Expand Up @@ -5694,7 +5746,14 @@ bool ChainstateManager::PopulateAndValidateSnapshot(

// Fake various pieces of CBlockIndex state:
CBlockIndex* index = nullptr;
for (int i = 0; i <= snapshot_chainstate.m_chain.Height(); ++i) {

// Don't make any modifications to the genesis block.
// This is especially important because we don't want to erroneously
// apply BLOCK_ASSUMED_VALID to genesis, which would happen if we didn't skip
// it here (since it apparently isn't BLOCK_VALID_SCRIPTS).
constexpr int AFTER_GENESIS_START{1};

for (int i = AFTER_GENESIS_START; i <= snapshot_chainstate.m_chain.Height(); ++i) {
index = snapshot_chainstate.m_chain[i];

// Fake nTx so that LoadBlockIndex() loads assumed-valid CBlockIndex
Expand All @@ -5703,7 +5762,7 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
index->nTx = 1;
}
// Fake nChainTx so that GuessVerificationProgress reports accurately
index->nChainTx = index->pprev ? index->pprev->nChainTx + index->nTx : 1;
index->nChainTx = index->pprev->nChainTx + index->nTx;

// Mark unvalidated block index entries beneath the snapshot base block as assumed-valid.
if (!index->IsValid(BLOCK_VALID_SCRIPTS)) {
Expand Down
13 changes: 7 additions & 6 deletions src/validation.h
Original file line number Diff line number Diff line change
Expand Up @@ -471,20 +471,17 @@ class BlockManager

std::unique_ptr<CBlockTreeDB> m_block_tree_db GUARDED_BY(::cs_main);

bool LoadBlockIndexDB(std::set<CBlockIndex*, CBlockIndexWorkComparator>& setBlockIndexCandidates) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
bool LoadBlockIndexDB(ChainstateManager& chainman) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);

/**
* Load the blocktree off disk and into memory. Populate certain metadata
* per index entry (nStatus, nChainWork, nTimeMax, etc.) as well as peripheral
* collections like setDirtyBlockIndex.
*
* @param[out] block_index_candidates Fill this set with any valid blocks for
* which we've downloaded all transactions.
*/
bool LoadBlockIndex(
const Consensus::Params& consensus_params,
std::set<CBlockIndex*, CBlockIndexWorkComparator>& block_index_candidates)
EXCLUSIVE_LOCKS_REQUIRED(cs_main);
ChainstateManager& chainman) EXCLUSIVE_LOCKS_REQUIRED(cs_main);

/** Clear all data members. */
void Unload() EXCLUSIVE_LOCKS_REQUIRED(cs_main);

Expand Down Expand Up @@ -678,6 +675,10 @@ class CChainState
*/
const std::optional<uint256> m_from_snapshot_blockhash;

//! Return true if this chainstate relies on blocks that are assumed-valid. In
//! practice this means it was created based on a UTXO snapshot.
bool reliesOnAssumedValid() { return m_from_snapshot_blockhash.has_value(); }

/**
* The set of all CBlockIndex entries with either BLOCK_VALID_TRANSACTIONS (for
* itself and all ancestors) *or* BLOCK_ASSUMED_VALID (if using background
Expand Down

0 comments on commit b402fd5

Please sign in to comment.