Merge bitcoin/bitcoin#28339: validation: improve performance of CheckBlockIndex

5bc2077e8f592442b089affdf0b5795fbc053bb8 validation: allow to specify frequency for -checkblockindex (Martin Zumsande)
d5a631b9597e5029a5048d9b8ad84ea4536bbac0 validation: improve performance of CheckBlockIndex (Martin Zumsande)
32c80413fdb063199f3bee719c4651bd63f05fce bench: add benchmark for checkblockindex (Martin Zumsande)

Pull request description:

  `CheckBlockIndex() ` are consistency checks that are currently enabled by default on regtest.

  The function is rather slow, which is annoying if you
  * attempt to run it on other networks, especially if not fully synced
  * want to generate a long chain on regtest and see block generation slow down because you forgot to disable `-checkblockindex` or don't know it existed.

  One reason why it's slow is that in order to be able to traverse the block tree depth-first from genesis, it inserts pointers to all block indices into a `std::multimap` - for which inserts and lookups become slow once there are hundred thousands of entries.
  However, typically the block index is mostly chain-like with just a few forks so a multimap isn't really needed for the most part. This PR suggests to store the block indices of the chain ending in the best header in a vector instead, and store only the rest of the indices in a multimap. This does not change the actual consistency checks that are being performed for each index, just the way the block index tree is stored and traversed.

  This adds a bit of complication to make sure each block is visited (note that there are asserts that check it), making sure that the two containers are traversed correctly, but it speeds up the function considerably:

  On master, a single invocation of `CheckBlockIndex` takes ~1.4s on mainnet for me (4.9s on testnet which has >2.4 million blocks).
  With this branch, the runtime goes down to ~0.27s (0.85s on testnet).This is a speedup by a factor ~5.

ACKs for top commit:
  achow101:
    ACK 5bc2077e8f592442b089affdf0b5795fbc053bb8
  furszy:
    ACK 5bc2077e8f592442b089affdf0b5795fbc053bb8
  ryanofsky:
    Code review ACK 5bc2077e8f592442b089affdf0b5795fbc053bb8. Just added suggested assert and simplification since last review

Tree-SHA512: 6b9c3e3e5069d6152b45a09040f962380d114851ff0f9ff1771cf8cad7bb4fa0ba25cd787ceaa3dfa5241fb249748e2ee6987af0ccb24b786a5301b2836f8487
This commit is contained in:
Ava Chow 2024-06-11 16:41:44 -04:00
commit 891e4bf374
No known key found for this signature in database
GPG Key ID: 17565732E08E5E41
8 changed files with 73 additions and 19 deletions

View File

@ -23,6 +23,7 @@ bench_bench_bitcoin_SOURCES = \
bench/ccoins_caching.cpp \
bench/chacha20.cpp \
bench/checkblock.cpp \
bench/checkblockindex.cpp \
bench/checkqueue.cpp \
bench/crypto_hash.cpp \
bench/data.cpp \

View File

@ -0,0 +1,20 @@
// Copyright (c) 2023-present 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 <bench/bench.h>
#include <test/util/setup_common.h>
#include <validation.h>
static void CheckBlockIndex(benchmark::Bench& bench)
{
auto testing_setup{MakeNoLogFileContext<TestChain100Setup>()};
// Mine some more blocks
testing_setup->mineBlocks(1000);
bench.run([&] {
testing_setup->m_node.chainman->CheckBlockIndex();
});
}
BENCHMARK(CheckBlockIndex, benchmark::PriorityLevel::HIGH);

View File

@ -598,7 +598,7 @@ void SetupServerArgs(ArgsManager& argsman)
argsman.AddArg("-checkblocks=<n>", strprintf("How many blocks to check at startup (default: %u, 0 = all)", DEFAULT_CHECKBLOCKS), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-checklevel=<n>", strprintf("How thorough the block verification of -checkblocks is: %s (0-4, default: %u)", Join(CHECKLEVEL_DOC, ", "), DEFAULT_CHECKLEVEL), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-checkblockindex", strprintf("Do a consistency check for the block tree, chainstate, and other validation data structures occasionally. (default: %u, regtest: %u)", defaultChainParams->DefaultConsistencyChecks(), regtestChainParams->DefaultConsistencyChecks()), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-checkblockindex", strprintf("Do a consistency check for the block tree, chainstate, and other validation data structures every <n> operations. Use 0 to disable. (default: %u, regtest: %u)", defaultChainParams->DefaultConsistencyChecks(), regtestChainParams->DefaultConsistencyChecks()), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-checkaddrman=<n>", strprintf("Run addrman consistency checks every <n> operations. Use 0 to disable. (default: %u)", DEFAULT_ADDRMAN_CONSISTENCY_CHECKS), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-checkmempool=<n>", strprintf("Run mempool consistency checks every <n> transactions. Use 0 to disable. (default: %u, regtest: %u)", defaultChainParams->DefaultConsistencyChecks(), regtestChainParams->DefaultConsistencyChecks()), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-checkpoints", strprintf("Enable rejection of any forks from the known historical chain until block %s (default: %u)", defaultChainParams->Checkpoints().GetHeight(), DEFAULT_CHECKPOINTS_ENABLED), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);

View File

@ -33,7 +33,7 @@ namespace kernel {
struct ChainstateManagerOpts {
const CChainParams& chainparams;
fs::path datadir;
std::optional<bool> check_block_index{};
std::optional<int32_t> check_block_index{};
bool checkpoints_enabled{DEFAULT_CHECKPOINTS_ENABLED};
//! If set, it will override the minimum work we will assume exists on some valid chain.
std::optional<arith_uint256> minimum_chain_work{};

View File

@ -24,7 +24,10 @@
namespace node {
util::Result<void> ApplyArgsManOptions(const ArgsManager& args, ChainstateManager::Options& opts)
{
if (auto value{args.GetBoolArg("-checkblockindex")}) opts.check_block_index = *value;
if (auto value{args.GetIntArg("-checkblockindex")}) {
// Interpret bare -checkblockindex argument as 1 instead of 0.
opts.check_block_index = args.GetArg("-checkblockindex")->empty() ? 1 : *value;
}
if (auto value{args.GetBoolArg("-checkpoints")}) opts.checkpoints_enabled = *value;

View File

@ -238,7 +238,7 @@ ChainTestingSetup::ChainTestingSetup(const ChainType chainType, const std::vecto
const ChainstateManager::Options chainman_opts{
.chainparams = chainparams,
.datadir = m_args.GetDataDirNet(),
.check_block_index = true,
.check_block_index = 1,
.notifications = *m_node.notifications,
.signals = m_node.validation_signals.get(),
.worker_threads_num = 2,

View File

@ -5092,6 +5092,14 @@ void ChainstateManager::LoadExternalBlockFile(
LogPrintf("Loaded %i blocks from external file in %dms\n", nLoaded, Ticks<std::chrono::milliseconds>(SteadyClock::now() - start));
}
bool ChainstateManager::ShouldCheckBlockIndex() const
{
// Assert to verify Flatten() has been called.
if (!*Assert(m_options.check_block_index)) return false;
if (GetRand(*m_options.check_block_index) >= 1) return false;
return true;
}
void ChainstateManager::CheckBlockIndex()
{
if (!ShouldCheckBlockIndex()) {
@ -5108,19 +5116,28 @@ void ChainstateManager::CheckBlockIndex()
return;
}
// Build forward-pointing map of the entire block tree.
// Build forward-pointing data structure for the entire block tree.
// For performance reasons, indexes of the best header chain are stored in a vector (within CChain).
// All remaining blocks are stored in a multimap.
// The best header chain can differ from the active chain: E.g. its entries may belong to blocks that
// are not yet validated.
CChain best_hdr_chain;
assert(m_best_header);
best_hdr_chain.SetTip(*m_best_header);
std::multimap<CBlockIndex*,CBlockIndex*> forward;
for (auto& [_, block_index] : m_blockman.m_block_index) {
forward.emplace(block_index.pprev, &block_index);
// Only save indexes in forward that are not part of the best header chain.
if (!best_hdr_chain.Contains(&block_index)) {
// Only genesis, which must be part of the best header chain, can have a nullptr parent.
assert(block_index.pprev);
forward.emplace(block_index.pprev, &block_index);
}
}
assert(forward.size() + best_hdr_chain.Height() + 1 == m_blockman.m_block_index.size());
assert(forward.size() == m_blockman.m_block_index.size());
std::pair<std::multimap<CBlockIndex*,CBlockIndex*>::iterator,std::multimap<CBlockIndex*,CBlockIndex*>::iterator> rangeGenesis = forward.equal_range(nullptr);
CBlockIndex *pindex = rangeGenesis.first->second;
rangeGenesis.first++;
assert(rangeGenesis.first == rangeGenesis.second); // There is only one index entry with parent nullptr.
CBlockIndex* pindex = best_hdr_chain[0];
assert(pindex);
// Iterate over the entire block tree, using depth-first search.
// Along the way, remember whether there are blocks on the path from genesis
// block being explored which are the first to have certain properties.
@ -5332,14 +5349,21 @@ void ChainstateManager::CheckBlockIndex()
// assert(pindex->GetBlockHash() == pindex->GetBlockHeader().GetHash()); // Perhaps too slow
// End: actual consistency checks.
// Try descending into the first subnode.
// Try descending into the first subnode. Always process forks first and the best header chain after.
snap_update_firsts();
std::pair<std::multimap<CBlockIndex*,CBlockIndex*>::iterator,std::multimap<CBlockIndex*,CBlockIndex*>::iterator> range = forward.equal_range(pindex);
if (range.first != range.second) {
// A subnode was found.
// A subnode not part of the best header chain was found.
pindex = range.first->second;
nHeight++;
continue;
} else if (best_hdr_chain.Contains(pindex)) {
// Descend further into best header chain.
nHeight++;
pindex = best_hdr_chain[nHeight];
if (!pindex) break; // we are finished, since the best header chain is always processed last
continue;
}
// This is a leaf node.
// Move upwards until we reach a node of which we have not yet visited the last child.
@ -5365,9 +5389,15 @@ void ChainstateManager::CheckBlockIndex()
// Proceed to the next one.
rangePar.first++;
if (rangePar.first != rangePar.second) {
// Move to the sibling.
// Move to a sibling not part of the best header chain.
pindex = rangePar.first->second;
break;
} else if (pindexPar == best_hdr_chain[nHeight - 1]) {
// Move to pindex's sibling on the best-chain, if it has one.
pindex = best_hdr_chain[nHeight];
// There will not be a next block if (and only if) parent block is the best header.
assert((pindex == nullptr) == (pindexPar == best_hdr_chain.Tip()));
break;
} else {
// Move up further.
pindex = pindexPar;
@ -5377,8 +5407,8 @@ void ChainstateManager::CheckBlockIndex()
}
}
// Check that we actually traversed the entire map.
assert(nNodes == forward.size());
// Check that we actually traversed the entire block index.
assert(nNodes == forward.size() + best_hdr_chain.Height() + 1);
}
std::string Chainstate::ToString()

View File

@ -938,7 +938,7 @@ public:
const CChainParams& GetParams() const { return m_options.chainparams; }
const Consensus::Params& GetConsensus() const { return m_options.chainparams.GetConsensus(); }
bool ShouldCheckBlockIndex() const { return *Assert(m_options.check_block_index); }
bool ShouldCheckBlockIndex() const;
const arith_uint256& MinimumChainWork() const { return *Assert(m_options.minimum_chain_work); }
const uint256& AssumedValidBlock() const { return *Assert(m_options.assumed_valid_block); }
kernel::Notifications& GetNotifications() const { return m_options.notifications; };