mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-03-05 14:06:27 -05:00
validation: add ChainMan logic for completing UTXO snapshot validation
Trigger completion when a background validation chainstate reaches the same height as a UTXO snapshot, and handle cleaning up the chainstate on subsequent startup.
This commit is contained in:
parent
f2a4f3376f
commit
d96c59cc5c
3 changed files with 421 additions and 3 deletions
|
@ -85,6 +85,9 @@ static ChainstateLoadResult CompleteChainstateInitialization(
|
||||||
return options.reindex || options.reindex_chainstate || chainstate->CoinsTip().GetBestBlock().IsNull();
|
return options.reindex || options.reindex_chainstate || chainstate->CoinsTip().GetBestBlock().IsNull();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
assert(chainman.m_total_coinstip_cache > 0);
|
||||||
|
assert(chainman.m_total_coinsdb_cache > 0);
|
||||||
|
|
||||||
// Conservative value which is arbitrarily chosen, as it will ultimately be changed
|
// Conservative value which is arbitrarily chosen, as it will ultimately be changed
|
||||||
// by a call to `chainman.MaybeRebalanceCaches()`. We just need to make sure
|
// by a call to `chainman.MaybeRebalanceCaches()`. We just need to make sure
|
||||||
// that the sum of the two caches (40%) does not exceed the allowable amount
|
// that the sum of the two caches (40%) does not exceed the allowable amount
|
||||||
|
@ -183,6 +186,47 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSize
|
||||||
return {init_status, init_error};
|
return {init_status, init_error};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a snapshot chainstate was fully validated by a background chainstate during
|
||||||
|
// the last run, detect it here and clean up the now-unneeded background
|
||||||
|
// chainstate.
|
||||||
|
//
|
||||||
|
// Why is this cleanup done here (on subsequent restart) and not just when the
|
||||||
|
// snapshot is actually validated? Because this entails unusual
|
||||||
|
// filesystem operations to move leveldb data directories around, and that seems
|
||||||
|
// too risky to do in the middle of normal runtime.
|
||||||
|
auto snapshot_completion = chainman.MaybeCompleteSnapshotValidation();
|
||||||
|
|
||||||
|
if (snapshot_completion == SnapshotCompletionResult::SKIPPED) {
|
||||||
|
// do nothing; expected case
|
||||||
|
} else if (snapshot_completion == SnapshotCompletionResult::SUCCESS) {
|
||||||
|
LogPrintf("[snapshot] cleaning up unneeded background chainstate, then reinitializing\n");
|
||||||
|
if (!chainman.ValidatedSnapshotCleanup()) {
|
||||||
|
AbortNode("Background chainstate cleanup failed unexpectedly.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Because ValidatedSnapshotCleanup() has torn down chainstates with
|
||||||
|
// ChainstateManager::ResetChainstates(), reinitialize them here without
|
||||||
|
// duplicating the blockindex work above.
|
||||||
|
assert(chainman.GetAll().empty());
|
||||||
|
assert(!chainman.IsSnapshotActive());
|
||||||
|
assert(!chainman.IsSnapshotValidated());
|
||||||
|
|
||||||
|
chainman.InitializeChainstate(options.mempool);
|
||||||
|
|
||||||
|
// A reload of the block index is required to recompute setBlockIndexCandidates
|
||||||
|
// for the fully validated chainstate.
|
||||||
|
chainman.ActiveChainstate().UnloadBlockIndex();
|
||||||
|
|
||||||
|
auto [init_status, init_error] = CompleteChainstateInitialization(chainman, cache_sizes, options);
|
||||||
|
if (init_status != ChainstateLoadStatus::SUCCESS) {
|
||||||
|
return {init_status, init_error};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {ChainstateLoadStatus::FAILURE, _(
|
||||||
|
"UTXO snapshot failed to validate. "
|
||||||
|
"Restart to resume normal initial block download, or try loading a different snapshot.")};
|
||||||
|
}
|
||||||
|
|
||||||
return {ChainstateLoadStatus::SUCCESS, {}};
|
return {ChainstateLoadStatus::SUCCESS, {}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2845,6 +2845,14 @@ bool Chainstate::ConnectTip(BlockValidationState& state, CBlockIndex* pindexNew,
|
||||||
Ticks<SecondsDouble>(time_total),
|
Ticks<SecondsDouble>(time_total),
|
||||||
Ticks<MillisecondsDouble>(time_total) / num_blocks_total);
|
Ticks<MillisecondsDouble>(time_total) / num_blocks_total);
|
||||||
|
|
||||||
|
// If we are the background validation chainstate, check to see if we are done
|
||||||
|
// validating the snapshot (i.e. our tip has reached the snapshot's base block).
|
||||||
|
if (this != &m_chainman.ActiveChainstate()) {
|
||||||
|
// This call may set `m_disabled`, which is referenced immediately afterwards in
|
||||||
|
// ActivateBestChain, so that we stop connecting blocks past the snapshot base.
|
||||||
|
m_chainman.MaybeCompleteSnapshotValidation();
|
||||||
|
}
|
||||||
|
|
||||||
connectTrace.BlockConnected(pindexNew, std::move(pthisBlock));
|
connectTrace.BlockConnected(pindexNew, std::move(pthisBlock));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -3067,6 +3075,14 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr<
|
||||||
// we use m_chainstate_mutex to enforce mutual exclusion so that only one caller may execute this function at a time
|
// we use m_chainstate_mutex to enforce mutual exclusion so that only one caller may execute this function at a time
|
||||||
LOCK(m_chainstate_mutex);
|
LOCK(m_chainstate_mutex);
|
||||||
|
|
||||||
|
// Belt-and-suspenders check that we aren't attempting to advance the background
|
||||||
|
// chainstate past the snapshot base block.
|
||||||
|
if (WITH_LOCK(::cs_main, return m_disabled)) {
|
||||||
|
LogPrintf("m_disabled is set - this chainstate should not be in operation. " /* Continued */
|
||||||
|
"Please report this as a bug. %s\n", PACKAGE_BUGREPORT);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
CBlockIndex *pindexMostWork = nullptr;
|
CBlockIndex *pindexMostWork = nullptr;
|
||||||
CBlockIndex *pindexNewTip = nullptr;
|
CBlockIndex *pindexNewTip = nullptr;
|
||||||
int nStopAtHeight = gArgs.GetIntArg("-stopatheight", DEFAULT_STOPATHEIGHT);
|
int nStopAtHeight = gArgs.GetIntArg("-stopatheight", DEFAULT_STOPATHEIGHT);
|
||||||
|
@ -3117,6 +3133,15 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr<
|
||||||
assert(trace.pblock && trace.pindex);
|
assert(trace.pblock && trace.pindex);
|
||||||
GetMainSignals().BlockConnected(trace.pblock, trace.pindex);
|
GetMainSignals().BlockConnected(trace.pblock, trace.pindex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This will have been toggled in
|
||||||
|
// ActivateBestChainStep -> ConnectTip -> MaybeCompleteSnapshotValidation,
|
||||||
|
// if at all, so we should catch it here.
|
||||||
|
//
|
||||||
|
// Break this do-while to ensure we don't advance past the base snapshot.
|
||||||
|
if (m_disabled) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
} while (!m_chain.Tip() || (starting_tip && CBlockIndexWorkComparator()(m_chain.Tip(), starting_tip)));
|
} while (!m_chain.Tip() || (starting_tip && CBlockIndexWorkComparator()(m_chain.Tip(), starting_tip)));
|
||||||
if (!blocks_connected) return true;
|
if (!blocks_connected) return true;
|
||||||
|
|
||||||
|
@ -3137,6 +3162,11 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr<
|
||||||
|
|
||||||
if (nStopAtHeight && pindexNewTip && pindexNewTip->nHeight >= nStopAtHeight) StartShutdown();
|
if (nStopAtHeight && pindexNewTip && pindexNewTip->nHeight >= nStopAtHeight) StartShutdown();
|
||||||
|
|
||||||
|
if (WITH_LOCK(::cs_main, return m_disabled)) {
|
||||||
|
// Background chainstate has reached the snapshot base block, so exit.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// We check shutdown only after giving ActivateBestChainStep a chance to run once so that we
|
// We check shutdown only after giving ActivateBestChainStep a chance to run once so that we
|
||||||
// never shutdown before connecting the genesis block during LoadChainTip(). Previously this
|
// never shutdown before connecting the genesis block during LoadChainTip(). Previously this
|
||||||
// caused an assert() failure during shutdown in such cases as the UTXO DB flushing checks
|
// caused an assert() failure during shutdown in such cases as the UTXO DB flushing checks
|
||||||
|
@ -5046,6 +5076,19 @@ static void FlushSnapshotToDisk(CCoinsViewCache& coins_cache, bool snapshot_load
|
||||||
coins_cache.Flush();
|
coins_cache.Flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct StopHashingException : public std::exception
|
||||||
|
{
|
||||||
|
const char* what() const throw() override
|
||||||
|
{
|
||||||
|
return "ComputeUTXOStats interrupted by shutdown.";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static void SnapshotUTXOHashBreakpoint()
|
||||||
|
{
|
||||||
|
if (ShutdownRequested()) throw StopHashingException();
|
||||||
|
}
|
||||||
|
|
||||||
bool ChainstateManager::PopulateAndValidateSnapshot(
|
bool ChainstateManager::PopulateAndValidateSnapshot(
|
||||||
Chainstate& snapshot_chainstate,
|
Chainstate& snapshot_chainstate,
|
||||||
AutoFile& coins_file,
|
AutoFile& coins_file,
|
||||||
|
@ -5169,13 +5212,18 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
|
||||||
|
|
||||||
assert(coins_cache.GetBestBlock() == base_blockhash);
|
assert(coins_cache.GetBestBlock() == base_blockhash);
|
||||||
|
|
||||||
auto breakpoint_fnc = [] { /* TODO insert breakpoint here? */ };
|
|
||||||
|
|
||||||
// As above, okay to immediately release cs_main here since no other context knows
|
// As above, okay to immediately release cs_main here since no other context knows
|
||||||
// about the snapshot_chainstate.
|
// about the snapshot_chainstate.
|
||||||
CCoinsViewDB* snapshot_coinsdb = WITH_LOCK(::cs_main, return &snapshot_chainstate.CoinsDB());
|
CCoinsViewDB* snapshot_coinsdb = WITH_LOCK(::cs_main, return &snapshot_chainstate.CoinsDB());
|
||||||
|
|
||||||
const std::optional<CCoinsStats> maybe_stats = ComputeUTXOStats(CoinStatsHashType::HASH_SERIALIZED, snapshot_coinsdb, m_blockman, breakpoint_fnc);
|
std::optional<CCoinsStats> maybe_stats;
|
||||||
|
|
||||||
|
try {
|
||||||
|
maybe_stats = ComputeUTXOStats(
|
||||||
|
CoinStatsHashType::HASH_SERIALIZED, snapshot_coinsdb, m_blockman, SnapshotUTXOHashBreakpoint);
|
||||||
|
} catch (StopHashingException const&) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!maybe_stats.has_value()) {
|
if (!maybe_stats.has_value()) {
|
||||||
LogPrintf("[snapshot] failed to generate coins stats\n");
|
LogPrintf("[snapshot] failed to generate coins stats\n");
|
||||||
return false;
|
return false;
|
||||||
|
@ -5243,6 +5291,149 @@ bool ChainstateManager::PopulateAndValidateSnapshot(
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Currently, this function holds cs_main for its duration, which could be for
|
||||||
|
// multiple minutes due to the ComputeUTXOStats call. This hold is necessary
|
||||||
|
// because we need to avoid advancing the background validation chainstate
|
||||||
|
// farther than the snapshot base block - and this function is also invoked
|
||||||
|
// from within ConnectTip, i.e. from within ActivateBestChain, so cs_main is
|
||||||
|
// held anyway.
|
||||||
|
//
|
||||||
|
// Eventually (TODO), we could somehow separate this function's runtime from
|
||||||
|
// maintenance of the active chain, but that will either require
|
||||||
|
//
|
||||||
|
// (i) setting `m_disabled` immediately and ensuring all chainstate accesses go
|
||||||
|
// through IsUsable() checks, or
|
||||||
|
//
|
||||||
|
// (ii) giving each chainstate its own lock instead of using cs_main for everything.
|
||||||
|
SnapshotCompletionResult ChainstateManager::MaybeCompleteSnapshotValidation(
|
||||||
|
std::function<void(bilingual_str)> shutdown_fnc)
|
||||||
|
{
|
||||||
|
AssertLockHeld(cs_main);
|
||||||
|
if (m_ibd_chainstate.get() == &this->ActiveChainstate() ||
|
||||||
|
!this->IsUsable(m_snapshot_chainstate.get()) ||
|
||||||
|
!this->IsUsable(m_ibd_chainstate.get()) ||
|
||||||
|
!m_ibd_chainstate->m_chain.Tip()) {
|
||||||
|
// Nothing to do - this function only applies to the background
|
||||||
|
// validation chainstate.
|
||||||
|
return SnapshotCompletionResult::SKIPPED;
|
||||||
|
}
|
||||||
|
const int snapshot_tip_height = this->ActiveHeight();
|
||||||
|
const int snapshot_base_height = *Assert(this->GetSnapshotBaseHeight());
|
||||||
|
const CBlockIndex& index_new = *Assert(m_ibd_chainstate->m_chain.Tip());
|
||||||
|
|
||||||
|
if (index_new.nHeight < snapshot_base_height) {
|
||||||
|
// Background IBD not complete yet.
|
||||||
|
return SnapshotCompletionResult::SKIPPED;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(SnapshotBlockhash());
|
||||||
|
uint256 snapshot_blockhash = *Assert(SnapshotBlockhash());
|
||||||
|
|
||||||
|
auto handle_invalid_snapshot = [&]() EXCLUSIVE_LOCKS_REQUIRED(::cs_main) {
|
||||||
|
bilingual_str user_error = strprintf(_(
|
||||||
|
"%s failed to validate the -assumeutxo snapshot state. "
|
||||||
|
"This indicates a hardware problem, or a bug in the software, or a "
|
||||||
|
"bad software modification that allowed an invalid snapshot to be "
|
||||||
|
"loaded. As a result of this, the node will shut down and stop using any "
|
||||||
|
"state that was built on the snapshot, resetting the chain height "
|
||||||
|
"from %d to %d. On the next "
|
||||||
|
"restart, the node will resume syncing from %d "
|
||||||
|
"without using any snapshot data. "
|
||||||
|
"Please report this incident to %s, including how you obtained the snapshot. "
|
||||||
|
"The invalid snapshot chainstate has been left on disk in case it is "
|
||||||
|
"helpful in diagnosing the issue that caused this error."),
|
||||||
|
PACKAGE_NAME, snapshot_tip_height, snapshot_base_height, snapshot_base_height, PACKAGE_BUGREPORT
|
||||||
|
);
|
||||||
|
|
||||||
|
LogPrintf("[snapshot] !!! %s\n", user_error.original);
|
||||||
|
LogPrintf("[snapshot] deleting snapshot, reverting to validated chain, and stopping node\n");
|
||||||
|
|
||||||
|
m_active_chainstate = m_ibd_chainstate.get();
|
||||||
|
m_snapshot_chainstate->m_disabled = true;
|
||||||
|
assert(!this->IsUsable(m_snapshot_chainstate.get()));
|
||||||
|
assert(this->IsUsable(m_ibd_chainstate.get()));
|
||||||
|
|
||||||
|
m_snapshot_chainstate->InvalidateCoinsDBOnDisk();
|
||||||
|
|
||||||
|
shutdown_fnc(user_error);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (index_new.GetBlockHash() != snapshot_blockhash) {
|
||||||
|
LogPrintf("[snapshot] supposed base block %s does not match the " /* Continued */
|
||||||
|
"snapshot base block %s (height %d). Snapshot is not valid.",
|
||||||
|
index_new.ToString(), snapshot_blockhash.ToString(), snapshot_base_height);
|
||||||
|
handle_invalid_snapshot();
|
||||||
|
return SnapshotCompletionResult::BASE_BLOCKHASH_MISMATCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(index_new.nHeight == snapshot_base_height);
|
||||||
|
|
||||||
|
int curr_height = m_ibd_chainstate->m_chain.Height();
|
||||||
|
|
||||||
|
assert(snapshot_base_height == curr_height);
|
||||||
|
assert(snapshot_base_height == index_new.nHeight);
|
||||||
|
assert(this->IsUsable(m_snapshot_chainstate.get()));
|
||||||
|
assert(this->GetAll().size() == 2);
|
||||||
|
|
||||||
|
CCoinsViewDB& ibd_coins_db = m_ibd_chainstate->CoinsDB();
|
||||||
|
m_ibd_chainstate->ForceFlushStateToDisk();
|
||||||
|
|
||||||
|
auto maybe_au_data = ExpectedAssumeutxo(curr_height, ::Params());
|
||||||
|
if (!maybe_au_data) {
|
||||||
|
LogPrintf("[snapshot] assumeutxo data not found for height " /* Continued */
|
||||||
|
"(%d) - refusing to validate snapshot\n", curr_height);
|
||||||
|
handle_invalid_snapshot();
|
||||||
|
return SnapshotCompletionResult::MISSING_CHAINPARAMS;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AssumeutxoData& au_data = *maybe_au_data;
|
||||||
|
std::optional<CCoinsStats> maybe_ibd_stats;
|
||||||
|
LogPrintf("[snapshot] computing UTXO stats for background chainstate to validate " /* Continued */
|
||||||
|
"snapshot - this could take a few minutes\n");
|
||||||
|
try {
|
||||||
|
maybe_ibd_stats = ComputeUTXOStats(
|
||||||
|
CoinStatsHashType::HASH_SERIALIZED,
|
||||||
|
&ibd_coins_db,
|
||||||
|
m_blockman,
|
||||||
|
SnapshotUTXOHashBreakpoint);
|
||||||
|
} catch (StopHashingException const&) {
|
||||||
|
return SnapshotCompletionResult::STATS_FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX note that this function is slow and will hold cs_main for potentially minutes.
|
||||||
|
if (!maybe_ibd_stats) {
|
||||||
|
LogPrintf("[snapshot] failed to generate stats for validation coins db\n");
|
||||||
|
// While this isn't a problem with the snapshot per se, this condition
|
||||||
|
// prevents us from validating the snapshot, so we should shut down and let the
|
||||||
|
// user handle the issue manually.
|
||||||
|
handle_invalid_snapshot();
|
||||||
|
return SnapshotCompletionResult::STATS_FAILED;
|
||||||
|
}
|
||||||
|
const auto& ibd_stats = *maybe_ibd_stats;
|
||||||
|
|
||||||
|
// Compare the background validation chainstate's UTXO set hash against the hard-coded
|
||||||
|
// assumeutxo hash we expect.
|
||||||
|
//
|
||||||
|
// TODO: For belt-and-suspenders, we could cache the UTXO set
|
||||||
|
// hash for the snapshot when it's loaded in its chainstate's leveldb. We could then
|
||||||
|
// reference that here for an additional check.
|
||||||
|
if (AssumeutxoHash{ibd_stats.hashSerialized} != au_data.hash_serialized) {
|
||||||
|
LogPrintf("[snapshot] hash mismatch: actual=%s, expected=%s\n",
|
||||||
|
ibd_stats.hashSerialized.ToString(),
|
||||||
|
au_data.hash_serialized.ToString());
|
||||||
|
handle_invalid_snapshot();
|
||||||
|
return SnapshotCompletionResult::HASH_MISMATCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogPrintf("[snapshot] snapshot beginning at %s has been fully validated\n",
|
||||||
|
snapshot_blockhash.ToString());
|
||||||
|
|
||||||
|
m_ibd_chainstate->m_disabled = true;
|
||||||
|
this->MaybeRebalanceCaches();
|
||||||
|
|
||||||
|
return SnapshotCompletionResult::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
Chainstate& ChainstateManager::ActiveChainstate() const
|
Chainstate& ChainstateManager::ActiveChainstate() const
|
||||||
{
|
{
|
||||||
LOCK(::cs_main);
|
LOCK(::cs_main);
|
||||||
|
@ -5367,6 +5558,44 @@ bool IsBIP30Unspendable(const CBlockIndex& block_index)
|
||||||
(block_index.nHeight==91812 && block_index.GetBlockHash() == uint256S("0x00000000000af0aed4792b1acee3d966af36cf5def14935db8de83d6f9306f2f"));
|
(block_index.nHeight==91812 && block_index.GetBlockHash() == uint256S("0x00000000000af0aed4792b1acee3d966af36cf5def14935db8de83d6f9306f2f"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Chainstate::InvalidateCoinsDBOnDisk()
|
||||||
|
{
|
||||||
|
AssertLockHeld(::cs_main);
|
||||||
|
// Should never be called on a non-snapshot chainstate.
|
||||||
|
assert(m_from_snapshot_blockhash);
|
||||||
|
auto storage_path_maybe = this->CoinsDB().StoragePath();
|
||||||
|
// Should never be called with a non-existent storage path.
|
||||||
|
assert(storage_path_maybe);
|
||||||
|
fs::path snapshot_datadir = *storage_path_maybe;
|
||||||
|
|
||||||
|
// Coins views no longer usable.
|
||||||
|
m_coins_views.reset();
|
||||||
|
|
||||||
|
auto invalid_path = snapshot_datadir + "_INVALID";
|
||||||
|
std::string dbpath = fs::PathToString(snapshot_datadir);
|
||||||
|
std::string target = fs::PathToString(invalid_path);
|
||||||
|
LogPrintf("[snapshot] renaming snapshot datadir %s to %s\n", dbpath, target);
|
||||||
|
|
||||||
|
// The invalid snapshot datadir is simply moved and not deleted because we may
|
||||||
|
// want to do forensics later during issue investigation. The user is instructed
|
||||||
|
// accordingly in MaybeCompleteSnapshotValidation().
|
||||||
|
try {
|
||||||
|
fs::rename(snapshot_datadir, invalid_path);
|
||||||
|
} catch (const fs::filesystem_error& e) {
|
||||||
|
auto src_str = fs::PathToString(snapshot_datadir);
|
||||||
|
auto dest_str = fs::PathToString(invalid_path);
|
||||||
|
|
||||||
|
LogPrintf("%s: error renaming file '%s' -> '%s': %s\n",
|
||||||
|
__func__, src_str, dest_str, e.what());
|
||||||
|
AbortNode(strprintf(
|
||||||
|
"Rename of '%s' -> '%s' failed. "
|
||||||
|
"You should resolve this by manually moving or deleting the invalid "
|
||||||
|
"snapshot directory %s, otherwise you will encounter the same error again "
|
||||||
|
"on the next startup.",
|
||||||
|
src_str, dest_str, src_str));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const CBlockIndex* ChainstateManager::GetSnapshotBaseBlock() const
|
const CBlockIndex* ChainstateManager::GetSnapshotBaseBlock() const
|
||||||
{
|
{
|
||||||
const auto blockhash_op = this->SnapshotBlockhash();
|
const auto blockhash_op = this->SnapshotBlockhash();
|
||||||
|
@ -5379,3 +5608,90 @@ std::optional<int> ChainstateManager::GetSnapshotBaseHeight() const
|
||||||
const CBlockIndex* base = this->GetSnapshotBaseBlock();
|
const CBlockIndex* base = this->GetSnapshotBaseBlock();
|
||||||
return base ? std::make_optional(base->nHeight) : std::nullopt;
|
return base ? std::make_optional(base->nHeight) : std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ChainstateManager::ValidatedSnapshotCleanup()
|
||||||
|
{
|
||||||
|
AssertLockHeld(::cs_main);
|
||||||
|
auto get_storage_path = [](auto& chainstate) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) -> std::optional<fs::path> {
|
||||||
|
if (!(chainstate && chainstate->HasCoinsViews())) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return chainstate->CoinsDB().StoragePath();
|
||||||
|
};
|
||||||
|
std::optional<fs::path> ibd_chainstate_path_maybe = get_storage_path(m_ibd_chainstate);
|
||||||
|
std::optional<fs::path> snapshot_chainstate_path_maybe = get_storage_path(m_snapshot_chainstate);
|
||||||
|
|
||||||
|
if (!this->IsSnapshotValidated()) {
|
||||||
|
// No need to clean up.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// If either path doesn't exist, that means at least one of the chainstates
|
||||||
|
// is in-memory, in which case we can't do on-disk cleanup. You'd better be
|
||||||
|
// in a unittest!
|
||||||
|
if (!ibd_chainstate_path_maybe || !snapshot_chainstate_path_maybe) {
|
||||||
|
LogPrintf("[snapshot] snapshot chainstate cleanup cannot happen with " /* Continued */
|
||||||
|
"in-memory chainstates. You are testing, right?\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& snapshot_chainstate_path = *snapshot_chainstate_path_maybe;
|
||||||
|
const auto& ibd_chainstate_path = *ibd_chainstate_path_maybe;
|
||||||
|
|
||||||
|
// Since we're going to be moving around the underlying leveldb filesystem content
|
||||||
|
// for each chainstate, make sure that the chainstates (and their constituent
|
||||||
|
// CoinsViews members) have been destructed first.
|
||||||
|
//
|
||||||
|
// The caller of this method will be responsible for reinitializing chainstates
|
||||||
|
// if they want to continue operation.
|
||||||
|
this->ResetChainstates();
|
||||||
|
|
||||||
|
// No chainstates should be considered usable.
|
||||||
|
assert(this->GetAll().size() == 0);
|
||||||
|
|
||||||
|
LogPrintf("[snapshot] deleting background chainstate directory (now unnecessary) (%s)\n",
|
||||||
|
fs::PathToString(ibd_chainstate_path));
|
||||||
|
|
||||||
|
fs::path tmp_old{ibd_chainstate_path + "_todelete"};
|
||||||
|
|
||||||
|
auto rename_failed_abort = [](
|
||||||
|
fs::path p_old,
|
||||||
|
fs::path p_new,
|
||||||
|
const fs::filesystem_error& err) {
|
||||||
|
LogPrintf("%s: error renaming file (%s): %s\n",
|
||||||
|
__func__, fs::PathToString(p_old), err.what());
|
||||||
|
AbortNode(strprintf(
|
||||||
|
"Rename of '%s' -> '%s' failed. "
|
||||||
|
"Cannot clean up the background chainstate leveldb directory.",
|
||||||
|
fs::PathToString(p_old), fs::PathToString(p_new)));
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs::rename(ibd_chainstate_path, tmp_old);
|
||||||
|
} catch (const fs::filesystem_error& e) {
|
||||||
|
rename_failed_abort(ibd_chainstate_path, tmp_old, e);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogPrintf("[snapshot] moving snapshot chainstate (%s) to " /* Continued */
|
||||||
|
"default chainstate directory (%s)\n",
|
||||||
|
fs::PathToString(snapshot_chainstate_path), fs::PathToString(ibd_chainstate_path));
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs::rename(snapshot_chainstate_path, ibd_chainstate_path);
|
||||||
|
} catch (const fs::filesystem_error& e) {
|
||||||
|
rename_failed_abort(snapshot_chainstate_path, ibd_chainstate_path, e);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DeleteCoinsDBFromDisk(tmp_old, /*is_snapshot=*/false)) {
|
||||||
|
// No need to AbortNode because once the unneeded bg chainstate data is
|
||||||
|
// moved, it will not interfere with subsequent initialization.
|
||||||
|
LogPrintf("Deletion of %s failed. Please remove it manually, as the " /* Continued */
|
||||||
|
"directory is now unnecessary.\n",
|
||||||
|
fs::PathToString(tmp_old));
|
||||||
|
} else {
|
||||||
|
LogPrintf("[snapshot] deleted background chainstate directory (%s)\n",
|
||||||
|
fs::PathToString(ibd_chainstate_path));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
#include <policy/packages.h>
|
#include <policy/packages.h>
|
||||||
#include <policy/policy.h>
|
#include <policy/policy.h>
|
||||||
#include <script/script_error.h>
|
#include <script/script_error.h>
|
||||||
|
#include <shutdown.h>
|
||||||
#include <sync.h>
|
#include <sync.h>
|
||||||
#include <txdb.h>
|
#include <txdb.h>
|
||||||
#include <txmempool.h> // For CTxMemPool::cs
|
#include <txmempool.h> // For CTxMemPool::cs
|
||||||
|
@ -663,6 +664,12 @@ public:
|
||||||
* May not be called with cs_main held. May not be called in a
|
* May not be called with cs_main held. May not be called in a
|
||||||
* validationinterface callback.
|
* validationinterface callback.
|
||||||
*
|
*
|
||||||
|
* Note that if this is called while a snapshot chainstate is active, and if
|
||||||
|
* it is called on a background chainstate whose tip has reached the base block
|
||||||
|
* of the snapshot, its execution will take *MINUTES* while it hashes the
|
||||||
|
* background UTXO set to verify the assumeutxo value the snapshot was activated
|
||||||
|
* with. `cs_main` will be held during this time.
|
||||||
|
*
|
||||||
* @returns true unless a system error occurred
|
* @returns true unless a system error occurred
|
||||||
*/
|
*/
|
||||||
bool ActivateBestChain(
|
bool ActivateBestChain(
|
||||||
|
@ -784,9 +791,37 @@ private:
|
||||||
std::chrono::microseconds m_last_write{0};
|
std::chrono::microseconds m_last_write{0};
|
||||||
std::chrono::microseconds m_last_flush{0};
|
std::chrono::microseconds m_last_flush{0};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In case of an invalid snapshot, rename the coins leveldb directory so
|
||||||
|
* that it can be examined for issue diagnosis.
|
||||||
|
*/
|
||||||
|
void InvalidateCoinsDBOnDisk() EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
|
||||||
|
|
||||||
friend ChainstateManager;
|
friend ChainstateManager;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
enum class SnapshotCompletionResult {
|
||||||
|
SUCCESS,
|
||||||
|
SKIPPED,
|
||||||
|
|
||||||
|
// Expected assumeutxo configuration data is not found for the height of the
|
||||||
|
// base block.
|
||||||
|
MISSING_CHAINPARAMS,
|
||||||
|
|
||||||
|
// Failed to generate UTXO statistics (to check UTXO set hash) for the background
|
||||||
|
// chainstate.
|
||||||
|
STATS_FAILED,
|
||||||
|
|
||||||
|
// The UTXO set hash of the background validation chainstate does not match
|
||||||
|
// the one expected by assumeutxo chainparams.
|
||||||
|
HASH_MISMATCH,
|
||||||
|
|
||||||
|
// The blockhash of the current tip of the background validation chainstate does
|
||||||
|
// not match the one expected by the snapshot chainstate.
|
||||||
|
BASE_BLOCKHASH_MISMATCH,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides an interface for creating and interacting with one or two
|
* Provides an interface for creating and interacting with one or two
|
||||||
* chainstates: an IBD chainstate generated by downloading blocks, and
|
* chainstates: an IBD chainstate generated by downloading blocks, and
|
||||||
|
@ -984,6 +1019,18 @@ public:
|
||||||
[[nodiscard]] bool ActivateSnapshot(
|
[[nodiscard]] bool ActivateSnapshot(
|
||||||
AutoFile& coins_file, const node::SnapshotMetadata& metadata, bool in_memory);
|
AutoFile& coins_file, const node::SnapshotMetadata& metadata, bool in_memory);
|
||||||
|
|
||||||
|
//! Once the background validation chainstate has reached the height which
|
||||||
|
//! is the base of the UTXO snapshot in use, compare its coins to ensure
|
||||||
|
//! they match those expected by the snapshot.
|
||||||
|
//!
|
||||||
|
//! If the coins match (expected), then mark the validation chainstate for
|
||||||
|
//! deletion and continue using the snapshot chainstate as active.
|
||||||
|
//! Otherwise, revert to using the ibd chainstate and shutdown.
|
||||||
|
SnapshotCompletionResult MaybeCompleteSnapshotValidation(
|
||||||
|
std::function<void(bilingual_str)> shutdown_fnc =
|
||||||
|
[](bilingual_str msg) { AbortNode(msg.original, msg); })
|
||||||
|
EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
|
||||||
|
|
||||||
//! The most-work chain.
|
//! The most-work chain.
|
||||||
Chainstate& ActiveChainstate() const;
|
Chainstate& ActiveChainstate() const;
|
||||||
CChain& ActiveChain() const EXCLUSIVE_LOCKS_REQUIRED(GetMutex()) { return ActiveChainstate().m_chain; }
|
CChain& ActiveChain() const EXCLUSIVE_LOCKS_REQUIRED(GetMutex()) { return ActiveChainstate().m_chain; }
|
||||||
|
@ -1091,6 +1138,17 @@ public:
|
||||||
Chainstate& ActivateExistingSnapshot(CTxMemPool* mempool, uint256 base_blockhash)
|
Chainstate& ActivateExistingSnapshot(CTxMemPool* mempool, uint256 base_blockhash)
|
||||||
EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
|
EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
|
||||||
|
|
||||||
|
//! If we have validated a snapshot chain during this runtime, copy its
|
||||||
|
//! chainstate directory over to the main `chainstate` location, completing
|
||||||
|
//! validation of the snapshot.
|
||||||
|
//!
|
||||||
|
//! If the cleanup succeeds, the caller will need to ensure chainstates are
|
||||||
|
//! reinitialized, since ResetChainstates() will be called before leveldb
|
||||||
|
//! directories are moved or deleted.
|
||||||
|
//!
|
||||||
|
//! @sa node/chainstate:LoadChainstate()
|
||||||
|
bool ValidatedSnapshotCleanup() EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
|
||||||
|
|
||||||
~ChainstateManager();
|
~ChainstateManager();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue