mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-02-13 11:25:02 -05:00
![Martin Zumsande](/assets/img/avatar_default.png)
This is achieved by letting the index sync thread wait until reindex-chainstate is finished. This also disables the pruning check when reindexing the chainstate (which is incompatible with prune mode) because there would be no chain at this point in init.
455 lines
17 KiB
C++
455 lines
17 KiB
C++
// Copyright (c) 2017-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 <common/args.h>
|
|
#include <index/base.h>
|
|
#include <interfaces/chain.h>
|
|
#include <kernel/chain.h>
|
|
#include <logging.h>
|
|
#include <node/blockstorage.h>
|
|
#include <node/context.h>
|
|
#include <node/database_args.h>
|
|
#include <node/interface_ui.h>
|
|
#include <shutdown.h>
|
|
#include <tinyformat.h>
|
|
#include <util/syscall_sandbox.h>
|
|
#include <util/thread.h>
|
|
#include <util/translation.h>
|
|
#include <validation.h> // For g_chainman
|
|
#include <warnings.h>
|
|
|
|
#include <string>
|
|
#include <utility>
|
|
|
|
using node::g_indexes_ready_to_sync;
|
|
|
|
constexpr uint8_t DB_BEST_BLOCK{'B'};
|
|
|
|
constexpr auto SYNC_LOG_INTERVAL{30s};
|
|
constexpr auto SYNC_LOCATOR_WRITE_INTERVAL{30s};
|
|
|
|
template <typename... Args>
|
|
static void FatalError(const char* fmt, const Args&... args)
|
|
{
|
|
std::string strMessage = tfm::format(fmt, args...);
|
|
SetMiscWarning(Untranslated(strMessage));
|
|
LogPrintf("*** %s\n", strMessage);
|
|
InitError(_("A fatal internal error occurred, see debug.log for details"));
|
|
StartShutdown();
|
|
}
|
|
|
|
CBlockLocator GetLocator(interfaces::Chain& chain, const uint256& block_hash)
|
|
{
|
|
CBlockLocator locator;
|
|
bool found = chain.findBlock(block_hash, interfaces::FoundBlock().locator(locator));
|
|
assert(found);
|
|
assert(!locator.IsNull());
|
|
return locator;
|
|
}
|
|
|
|
BaseIndex::DB::DB(const fs::path& path, size_t n_cache_size, bool f_memory, bool f_wipe, bool f_obfuscate) :
|
|
CDBWrapper{DBParams{
|
|
.path = path,
|
|
.cache_bytes = n_cache_size,
|
|
.memory_only = f_memory,
|
|
.wipe_data = f_wipe,
|
|
.obfuscate = f_obfuscate,
|
|
.options = [] { DBOptions options; node::ReadDatabaseArgs(gArgs, options); return options; }()}}
|
|
{}
|
|
|
|
bool BaseIndex::DB::ReadBestBlock(CBlockLocator& locator) const
|
|
{
|
|
bool success = Read(DB_BEST_BLOCK, locator);
|
|
if (!success) {
|
|
locator.SetNull();
|
|
}
|
|
return success;
|
|
}
|
|
|
|
void BaseIndex::DB::WriteBestBlock(CDBBatch& batch, const CBlockLocator& locator)
|
|
{
|
|
batch.Write(DB_BEST_BLOCK, locator);
|
|
}
|
|
|
|
BaseIndex::BaseIndex(std::unique_ptr<interfaces::Chain> chain, std::string name)
|
|
: m_chain{std::move(chain)}, m_name{std::move(name)} {}
|
|
|
|
BaseIndex::~BaseIndex()
|
|
{
|
|
Interrupt();
|
|
Stop();
|
|
}
|
|
|
|
bool BaseIndex::Init()
|
|
{
|
|
CBlockLocator locator;
|
|
if (!GetDB().ReadBestBlock(locator)) {
|
|
locator.SetNull();
|
|
}
|
|
|
|
LOCK(cs_main);
|
|
CChain& active_chain = m_chainstate->m_chain;
|
|
if (locator.IsNull()) {
|
|
SetBestBlockIndex(nullptr);
|
|
} else {
|
|
// Setting the best block to the locator's top block. If it is not part of the
|
|
// best chain, we will rewind to the fork point during index sync
|
|
const CBlockIndex* locator_index{m_chainstate->m_blockman.LookupBlockIndex(locator.vHave.at(0))};
|
|
if (!locator_index) {
|
|
return InitError(strprintf(Untranslated("%s: best block of the index not found. Please rebuild the index."), GetName()));
|
|
}
|
|
SetBestBlockIndex(locator_index);
|
|
}
|
|
|
|
// Note: this will latch to true immediately if the user starts up with an empty
|
|
// datadir and an index enabled. If this is the case, indexation will happen solely
|
|
// via `BlockConnected` signals until, possibly, the next restart.
|
|
m_synced = m_best_block_index.load() == active_chain.Tip();
|
|
|
|
// Skip pruning check if indexes are not ready to sync (because reindex-chainstate has wiped the chain).
|
|
if (!m_synced && g_indexes_ready_to_sync) {
|
|
bool prune_violation = false;
|
|
if (!m_best_block_index) {
|
|
// index is not built yet
|
|
// make sure we have all block data back to the genesis
|
|
prune_violation = m_chainstate->m_blockman.GetFirstStoredBlock(*active_chain.Tip()) != active_chain.Genesis();
|
|
}
|
|
// in case the index has a best block set and is not fully synced
|
|
// check if we have the required blocks to continue building the index
|
|
else {
|
|
const CBlockIndex* block_to_test = m_best_block_index.load();
|
|
if (!active_chain.Contains(block_to_test)) {
|
|
// if the bestblock is not part of the mainchain, find the fork
|
|
// and make sure we have all data down to the fork
|
|
block_to_test = active_chain.FindFork(block_to_test);
|
|
}
|
|
const CBlockIndex* block = active_chain.Tip();
|
|
prune_violation = true;
|
|
// check backwards from the tip if we have all block data until we reach the indexes bestblock
|
|
while (block_to_test && block && (block->nStatus & BLOCK_HAVE_DATA)) {
|
|
if (block_to_test == block) {
|
|
prune_violation = false;
|
|
break;
|
|
}
|
|
// block->pprev must exist at this point, since block_to_test is part of the chain
|
|
// and thus must be encountered when going backwards from the tip
|
|
assert(block->pprev);
|
|
block = block->pprev;
|
|
}
|
|
}
|
|
if (prune_violation) {
|
|
return InitError(strprintf(Untranslated("%s best block of the index goes beyond pruned data. Please disable the index or reindex (which will download the whole blockchain again)"), GetName()));
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static const CBlockIndex* NextSyncBlock(const CBlockIndex* pindex_prev, CChain& chain) EXCLUSIVE_LOCKS_REQUIRED(cs_main)
|
|
{
|
|
AssertLockHeld(cs_main);
|
|
|
|
if (!pindex_prev) {
|
|
return chain.Genesis();
|
|
}
|
|
|
|
const CBlockIndex* pindex = chain.Next(pindex_prev);
|
|
if (pindex) {
|
|
return pindex;
|
|
}
|
|
|
|
return chain.Next(chain.FindFork(pindex_prev));
|
|
}
|
|
|
|
void BaseIndex::ThreadSync()
|
|
{
|
|
SetSyscallSandboxPolicy(SyscallSandboxPolicy::TX_INDEX);
|
|
// Wait for a possible reindex-chainstate to finish until continuing
|
|
// with the index sync
|
|
while (!g_indexes_ready_to_sync) {
|
|
if (!m_interrupt.sleep_for(std::chrono::milliseconds(500))) return;
|
|
}
|
|
|
|
const CBlockIndex* pindex = m_best_block_index.load();
|
|
if (!m_synced) {
|
|
std::chrono::steady_clock::time_point last_log_time{0s};
|
|
std::chrono::steady_clock::time_point last_locator_write_time{0s};
|
|
while (true) {
|
|
if (m_interrupt) {
|
|
SetBestBlockIndex(pindex);
|
|
// No need to handle errors in Commit. If it fails, the error will be already be
|
|
// logged. The best way to recover is to continue, as index cannot be corrupted by
|
|
// a missed commit to disk for an advanced index state.
|
|
Commit();
|
|
return;
|
|
}
|
|
|
|
{
|
|
LOCK(cs_main);
|
|
const CBlockIndex* pindex_next = NextSyncBlock(pindex, m_chainstate->m_chain);
|
|
if (!pindex_next) {
|
|
SetBestBlockIndex(pindex);
|
|
m_synced = true;
|
|
// No need to handle errors in Commit. See rationale above.
|
|
Commit();
|
|
break;
|
|
}
|
|
if (pindex_next->pprev != pindex && !Rewind(pindex, pindex_next->pprev)) {
|
|
FatalError("%s: Failed to rewind index %s to a previous chain tip",
|
|
__func__, GetName());
|
|
return;
|
|
}
|
|
pindex = pindex_next;
|
|
}
|
|
|
|
auto current_time{std::chrono::steady_clock::now()};
|
|
if (last_log_time + SYNC_LOG_INTERVAL < current_time) {
|
|
LogPrintf("Syncing %s with block chain from height %d\n",
|
|
GetName(), pindex->nHeight);
|
|
last_log_time = current_time;
|
|
}
|
|
|
|
if (last_locator_write_time + SYNC_LOCATOR_WRITE_INTERVAL < current_time) {
|
|
SetBestBlockIndex(pindex->pprev);
|
|
last_locator_write_time = current_time;
|
|
// No need to handle errors in Commit. See rationale above.
|
|
Commit();
|
|
}
|
|
|
|
CBlock block;
|
|
interfaces::BlockInfo block_info = kernel::MakeBlockInfo(pindex);
|
|
if (!m_chainstate->m_blockman.ReadBlockFromDisk(block, *pindex)) {
|
|
FatalError("%s: Failed to read block %s from disk",
|
|
__func__, pindex->GetBlockHash().ToString());
|
|
return;
|
|
} else {
|
|
block_info.data = █
|
|
}
|
|
if (!CustomAppend(block_info)) {
|
|
FatalError("%s: Failed to write block %s to index database",
|
|
__func__, pindex->GetBlockHash().ToString());
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pindex) {
|
|
LogPrintf("%s is enabled at height %d\n", GetName(), pindex->nHeight);
|
|
} else {
|
|
LogPrintf("%s is enabled\n", GetName());
|
|
}
|
|
}
|
|
|
|
bool BaseIndex::Commit()
|
|
{
|
|
// Don't commit anything if we haven't indexed any block yet
|
|
// (this could happen if init is interrupted).
|
|
bool ok = m_best_block_index != nullptr;
|
|
if (ok) {
|
|
CDBBatch batch(GetDB());
|
|
ok = CustomCommit(batch);
|
|
if (ok) {
|
|
GetDB().WriteBestBlock(batch, GetLocator(*m_chain, m_best_block_index.load()->GetBlockHash()));
|
|
ok = GetDB().WriteBatch(batch);
|
|
}
|
|
}
|
|
if (!ok) {
|
|
return error("%s: Failed to commit latest %s state", __func__, GetName());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool BaseIndex::Rewind(const CBlockIndex* current_tip, const CBlockIndex* new_tip)
|
|
{
|
|
assert(current_tip == m_best_block_index);
|
|
assert(current_tip->GetAncestor(new_tip->nHeight) == new_tip);
|
|
|
|
if (!CustomRewind({current_tip->GetBlockHash(), current_tip->nHeight}, {new_tip->GetBlockHash(), new_tip->nHeight})) {
|
|
return false;
|
|
}
|
|
|
|
// In the case of a reorg, ensure persisted block locator is not stale.
|
|
// Pruning has a minimum of 288 blocks-to-keep and getting the index
|
|
// out of sync may be possible but a users fault.
|
|
// In case we reorg beyond the pruned depth, ReadBlockFromDisk would
|
|
// throw and lead to a graceful shutdown
|
|
SetBestBlockIndex(new_tip);
|
|
if (!Commit()) {
|
|
// If commit fails, revert the best block index to avoid corruption.
|
|
SetBestBlockIndex(current_tip);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void BaseIndex::BlockConnected(const std::shared_ptr<const CBlock>& block, const CBlockIndex* pindex)
|
|
{
|
|
if (!m_synced) {
|
|
return;
|
|
}
|
|
|
|
const CBlockIndex* best_block_index = m_best_block_index.load();
|
|
if (!best_block_index) {
|
|
if (pindex->nHeight != 0) {
|
|
FatalError("%s: First block connected is not the genesis block (height=%d)",
|
|
__func__, pindex->nHeight);
|
|
return;
|
|
}
|
|
} else {
|
|
// Ensure block connects to an ancestor of the current best block. This should be the case
|
|
// most of the time, but may not be immediately after the sync thread catches up and sets
|
|
// m_synced. Consider the case where there is a reorg and the blocks on the stale branch are
|
|
// in the ValidationInterface queue backlog even after the sync thread has caught up to the
|
|
// new chain tip. In this unlikely event, log a warning and let the queue clear.
|
|
if (best_block_index->GetAncestor(pindex->nHeight - 1) != pindex->pprev) {
|
|
LogPrintf("%s: WARNING: Block %s does not connect to an ancestor of " /* Continued */
|
|
"known best chain (tip=%s); not updating index\n",
|
|
__func__, pindex->GetBlockHash().ToString(),
|
|
best_block_index->GetBlockHash().ToString());
|
|
return;
|
|
}
|
|
if (best_block_index != pindex->pprev && !Rewind(best_block_index, pindex->pprev)) {
|
|
FatalError("%s: Failed to rewind index %s to a previous chain tip",
|
|
__func__, GetName());
|
|
return;
|
|
}
|
|
}
|
|
interfaces::BlockInfo block_info = kernel::MakeBlockInfo(pindex, block.get());
|
|
if (CustomAppend(block_info)) {
|
|
// Setting the best block index is intentionally the last step of this
|
|
// function, so BlockUntilSyncedToCurrentChain callers waiting for the
|
|
// best block index to be updated can rely on the block being fully
|
|
// processed, and the index object being safe to delete.
|
|
SetBestBlockIndex(pindex);
|
|
} else {
|
|
FatalError("%s: Failed to write block %s to index",
|
|
__func__, pindex->GetBlockHash().ToString());
|
|
return;
|
|
}
|
|
}
|
|
|
|
void BaseIndex::ChainStateFlushed(const CBlockLocator& locator)
|
|
{
|
|
if (!m_synced) {
|
|
return;
|
|
}
|
|
|
|
const uint256& locator_tip_hash = locator.vHave.front();
|
|
const CBlockIndex* locator_tip_index;
|
|
{
|
|
LOCK(cs_main);
|
|
locator_tip_index = m_chainstate->m_blockman.LookupBlockIndex(locator_tip_hash);
|
|
}
|
|
|
|
if (!locator_tip_index) {
|
|
FatalError("%s: First block (hash=%s) in locator was not found",
|
|
__func__, locator_tip_hash.ToString());
|
|
return;
|
|
}
|
|
|
|
// This checks that ChainStateFlushed callbacks are received after BlockConnected. The check may fail
|
|
// immediately after the sync thread catches up and sets m_synced. Consider the case where
|
|
// there is a reorg and the blocks on the stale branch are in the ValidationInterface queue
|
|
// backlog even after the sync thread has caught up to the new chain tip. In this unlikely
|
|
// event, log a warning and let the queue clear.
|
|
const CBlockIndex* best_block_index = m_best_block_index.load();
|
|
if (best_block_index->GetAncestor(locator_tip_index->nHeight) != locator_tip_index) {
|
|
LogPrintf("%s: WARNING: Locator contains block (hash=%s) not on known best " /* Continued */
|
|
"chain (tip=%s); not writing index locator\n",
|
|
__func__, locator_tip_hash.ToString(),
|
|
best_block_index->GetBlockHash().ToString());
|
|
return;
|
|
}
|
|
|
|
// No need to handle errors in Commit. If it fails, the error will be already be logged. The
|
|
// best way to recover is to continue, as index cannot be corrupted by a missed commit to disk
|
|
// for an advanced index state.
|
|
Commit();
|
|
}
|
|
|
|
bool BaseIndex::BlockUntilSyncedToCurrentChain() const
|
|
{
|
|
AssertLockNotHeld(cs_main);
|
|
|
|
if (!m_synced) {
|
|
return false;
|
|
}
|
|
|
|
{
|
|
// Skip the queue-draining stuff if we know we're caught up with
|
|
// m_chain.Tip().
|
|
LOCK(cs_main);
|
|
const CBlockIndex* chain_tip = m_chainstate->m_chain.Tip();
|
|
const CBlockIndex* best_block_index = m_best_block_index.load();
|
|
if (best_block_index->GetAncestor(chain_tip->nHeight) == chain_tip) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
LogPrintf("%s: %s is catching up on block notifications\n", __func__, GetName());
|
|
SyncWithValidationInterfaceQueue();
|
|
return true;
|
|
}
|
|
|
|
void BaseIndex::Interrupt()
|
|
{
|
|
m_interrupt();
|
|
}
|
|
|
|
bool BaseIndex::Start()
|
|
{
|
|
// m_chainstate member gives indexing code access to node internals. It is
|
|
// removed in followup https://github.com/bitcoin/bitcoin/pull/24230
|
|
m_chainstate = &m_chain->context()->chainman->ActiveChainstate();
|
|
// Need to register this ValidationInterface before running Init(), so that
|
|
// callbacks are not missed if Init sets m_synced to true.
|
|
RegisterValidationInterface(this);
|
|
if (!Init()) return false;
|
|
|
|
const CBlockIndex* index = m_best_block_index.load();
|
|
if (!CustomInit(index ? std::make_optional(interfaces::BlockKey{index->GetBlockHash(), index->nHeight}) : std::nullopt)) {
|
|
return false;
|
|
}
|
|
|
|
m_thread_sync = std::thread(&util::TraceThread, GetName(), [this] { ThreadSync(); });
|
|
return true;
|
|
}
|
|
|
|
void BaseIndex::Stop()
|
|
{
|
|
UnregisterValidationInterface(this);
|
|
|
|
if (m_thread_sync.joinable()) {
|
|
m_thread_sync.join();
|
|
}
|
|
}
|
|
|
|
IndexSummary BaseIndex::GetSummary() const
|
|
{
|
|
IndexSummary summary{};
|
|
summary.name = GetName();
|
|
summary.synced = m_synced;
|
|
summary.best_block_height = m_best_block_index ? m_best_block_index.load()->nHeight : 0;
|
|
return summary;
|
|
}
|
|
|
|
void BaseIndex::SetBestBlockIndex(const CBlockIndex* block)
|
|
{
|
|
assert(!m_chainstate->m_blockman.IsPruneMode() || AllowPrune());
|
|
|
|
if (AllowPrune() && block) {
|
|
node::PruneLockInfo prune_lock;
|
|
prune_lock.height_first = block->nHeight;
|
|
WITH_LOCK(::cs_main, m_chainstate->m_blockman.UpdatePruneLock(GetName(), prune_lock));
|
|
}
|
|
|
|
// Intentionally set m_best_block_index as the last step in this function,
|
|
// after updating prune locks above, and after making any other references
|
|
// to *this, so the BlockUntilSyncedToCurrentChain function (which checks
|
|
// m_best_block_index as an optimization) can be used to wait for the last
|
|
// BlockConnected notification and safely assume that prune locks are
|
|
// updated and that the index object is safe to delete.
|
|
m_best_block_index = block;
|
|
}
|