refactor: Pass chainstate parameters to MaybeCompleteSnapshotValidation

Remove hardcoded references to m_ibd_chainstate and m_snapshot_chainstate so
MaybeCompleteSnapshotValidation function can be simpler and focus on validating
the snapshot without dealing with internal ChainstateManager states.

This is a step towards being able to validate the snapshot outside of
ActivateBestChain loop so cs_main is not locked for minutes when the snapshot
block is connected.
This commit is contained in:
Ryan Ofsky committed 2025-12-12 06:49:59 -04:00
1 parent 1598a15aed
commit 840bd2ef23
8 files changed
+93 -110

No files matched your search

+1 -1
View File
@@ -63,7 +63,7 @@ chainstate and a sync to tip begins. A new chainstate directory is created in th
datadir for the snapshot chainstate called `chainstate_snapshot`. datadir for the snapshot chainstate called `chainstate_snapshot`.
When this directory is present in the datadir, the snapshot chainstate will be detected When this directory is present in the datadir, the snapshot chainstate will be detected
and loaded as active on node startup (via `DetectSnapshotChainstate()`). and loaded as active on node startup (via `LoadAssumeutxoChainstate()`).
A special file is created within that directory, `base_blockhash`, which contains the A special file is created within that directory, `base_blockhash`, which contains the
serialized `uint256` of the base block of the snapshot. This is used to reinitialize serialized `uint256` of the base block of the snapshot. This is used to reinitialize
+6 -3
View File
@@ -169,15 +169,16 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSize
Chainstate& validated_cs{chainman.InitializeChainstate(options.mempool)}; Chainstate& validated_cs{chainman.InitializeChainstate(options.mempool)};
// Load a chain created from a UTXO snapshot, if any exist. // Load a chain created from a UTXO snapshot, if any exist.
bool has_snapshot = chainman.DetectSnapshotChainstate(); Chainstate* assumeutxo_cs{chainman.LoadAssumeutxoChainstate()};
if (has_snapshot && options.wipe_chainstate_db) { if (assumeutxo_cs && options.wipe_chainstate_db) {
// Reset chainstate target to network tip instead of snapshot block. // Reset chainstate target to network tip instead of snapshot block.
validated_cs.SetTargetBlock(nullptr); validated_cs.SetTargetBlock(nullptr);
LogInfo("[snapshot] deleting snapshot chainstate due to reindexing"); LogInfo("[snapshot] deleting snapshot chainstate due to reindexing");
if (!chainman.DeleteSnapshotChainstate()) { if (!chainman.DeleteSnapshotChainstate()) {
return {ChainstateLoadStatus::FAILURE_FATAL, Untranslated("Couldn't remove snapshot chainstate.")}; return {ChainstateLoadStatus::FAILURE_FATAL, Untranslated("Couldn't remove snapshot chainstate.")};
} }
assumeutxo_cs = nullptr;
} }
auto [init_status, init_error] = CompleteChainstateInitialization(chainman, options); auto [init_status, init_error] = CompleteChainstateInitialization(chainman, options);
@@ -193,7 +194,9 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSize
// snapshot is actually validated? Because this entails unusual // snapshot is actually validated? Because this entails unusual
// filesystem operations to move leveldb data directories around, and that seems // filesystem operations to move leveldb data directories around, and that seems
// too risky to do in the middle of normal runtime. // too risky to do in the middle of normal runtime.
auto snapshot_completion = chainman.MaybeCompleteSnapshotValidation(); auto snapshot_completion{assumeutxo_cs
? chainman.MaybeValidateSnapshot(validated_cs, *assumeutxo_cs)
: SnapshotCompletionResult::SKIPPED};
if (snapshot_completion == SnapshotCompletionResult::SKIPPED) { if (snapshot_completion == SnapshotCompletionResult::SKIPPED) {
// do nothing; expected case // do nothing; expected case
+1 -1
View File
@@ -81,7 +81,7 @@ std::optional<uint256> ReadSnapshotBaseBlockhash(fs::path chaindir)
return base_blockhash; return base_blockhash;
} }
std::optional<fs::path> FindSnapshotChainstateDir(const fs::path& data_dir) std::optional<fs::path> FindAssumeutxoChainstateDir(const fs::path& data_dir)
{ {
fs::path possible_dir = fs::path possible_dir =
data_dir / fs::u8path(strprintf("chainstate%s", SNAPSHOT_CHAINSTATE_SUFFIX)); data_dir / fs::u8path(strprintf("chainstate%s", SNAPSHOT_CHAINSTATE_SUFFIX));
+1 -1
View File
@@ -125,7 +125,7 @@ constexpr std::string_view SNAPSHOT_CHAINSTATE_SUFFIX = "_snapshot";
//! Return a path to the snapshot-based chainstate dir, if one exists. //! Return a path to the snapshot-based chainstate dir, if one exists.
std::optional<fs::path> FindSnapshotChainstateDir(const fs::path& data_dir); std::optional<fs::path> FindAssumeutxoChainstateDir(const fs::path& data_dir);
} // namespace node } // namespace node
+1 -1
View File
@@ -191,7 +191,7 @@ void utxo_snapshot_fuzz(FuzzBufferType buffer)
const auto* index{chainman.m_blockman.LookupBlockIndex(block->GetHash())}; const auto* index{chainman.m_blockman.LookupBlockIndex(block->GetHash())};
Assert(index); Assert(index);
Assert(index->nTx == 0); Assert(index->nTx == 0);
if (index->nHeight == chainman.GetSnapshotBaseHeight()) { if (index->nHeight == chainman.ActiveChainstate().SnapshotBase()->nHeight) {
auto params{chainman.GetParams().AssumeutxoForHeight(index->nHeight)}; auto params{chainman.GetParams().AssumeutxoForHeight(index->nHeight)};
Assert(params.has_value()); Assert(params.has_value());
Assert(params.value().m_chain_tx_count == index->m_chain_tx_count); Assert(params.value().m_chain_tx_count == index->m_chain_tx_count);
@@ -187,7 +187,7 @@ struct SnapshotTestSetup : TestChain100Setup {
{ {
LOCK(::cs_main); LOCK(::cs_main);
BOOST_CHECK(!chainman.IsSnapshotValidated()); BOOST_CHECK(!chainman.IsSnapshotValidated());
BOOST_CHECK(!node::FindSnapshotChainstateDir(chainman.m_options.datadir)); BOOST_CHECK(!node::FindAssumeutxoChainstateDir(chainman.m_options.datadir));
} }
size_t initial_size; size_t initial_size;
@@ -240,7 +240,7 @@ struct SnapshotTestSetup : TestChain100Setup {
auto_infile >> coin; auto_infile >> coin;
})); }));
BOOST_CHECK(!node::FindSnapshotChainstateDir(chainman.m_options.datadir)); BOOST_CHECK(!node::FindAssumeutxoChainstateDir(chainman.m_options.datadir));
BOOST_REQUIRE(!CreateAndActivateUTXOSnapshot( BOOST_REQUIRE(!CreateAndActivateUTXOSnapshot(
this, [](AutoFile& auto_infile, SnapshotMetadata& metadata) { this, [](AutoFile& auto_infile, SnapshotMetadata& metadata) {
@@ -264,7 +264,7 @@ struct SnapshotTestSetup : TestChain100Setup {
})); }));
BOOST_REQUIRE(CreateAndActivateUTXOSnapshot(this)); BOOST_REQUIRE(CreateAndActivateUTXOSnapshot(this));
BOOST_CHECK(fs::exists(*node::FindSnapshotChainstateDir(chainman.m_options.datadir))); BOOST_CHECK(fs::exists(*node::FindAssumeutxoChainstateDir(chainman.m_options.datadir)));
// Ensure our active chain is the snapshot chainstate. // Ensure our active chain is the snapshot chainstate.
BOOST_CHECK(!chainman.ActiveChainstate().m_from_snapshot_blockhash->IsNull()); BOOST_CHECK(!chainman.ActiveChainstate().m_from_snapshot_blockhash->IsNull());
@@ -277,7 +277,7 @@ struct SnapshotTestSetup : TestChain100Setup {
{ {
LOCK(::cs_main); LOCK(::cs_main);
fs::path found = *node::FindSnapshotChainstateDir(chainman.m_options.datadir); fs::path found = *node::FindAssumeutxoChainstateDir(chainman.m_options.datadir);
// Note: WriteSnapshotBaseBlockhash() is implicitly tested above. // Note: WriteSnapshotBaseBlockhash() is implicitly tested above.
BOOST_CHECK_EQUAL( BOOST_CHECK_EQUAL(
@@ -565,7 +565,7 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_init, SnapshotTestSetup)
this->SetupSnapshot(); this->SetupSnapshot();
fs::path snapshot_chainstate_dir = *node::FindSnapshotChainstateDir(chainman.m_options.datadir); fs::path snapshot_chainstate_dir = *node::FindAssumeutxoChainstateDir(chainman.m_options.datadir);
BOOST_CHECK(fs::exists(snapshot_chainstate_dir)); BOOST_CHECK(fs::exists(snapshot_chainstate_dir));
BOOST_CHECK_EQUAL(snapshot_chainstate_dir, gArgs.GetDataDirNet() / "chainstate_snapshot"); BOOST_CHECK_EQUAL(snapshot_chainstate_dir, gArgs.GetDataDirNet() / "chainstate_snapshot");
@@ -639,13 +639,14 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_completion, SnapshotTestSetup
ChainstateManager& chainman = *Assert(m_node.chainman); ChainstateManager& chainman = *Assert(m_node.chainman);
Chainstate& active_cs = chainman.ActiveChainstate(); Chainstate& active_cs = chainman.ActiveChainstate();
Chainstate& validated_cs{*Assert(WITH_LOCK(cs_main, return chainman.HistoricalChainstate()))};
auto tip_cache_before_complete = active_cs.m_coinstip_cache_size_bytes; auto tip_cache_before_complete = active_cs.m_coinstip_cache_size_bytes;
auto db_cache_before_complete = active_cs.m_coinsdb_cache_size_bytes; auto db_cache_before_complete = active_cs.m_coinsdb_cache_size_bytes;
SnapshotCompletionResult res; SnapshotCompletionResult res;
m_node.notifications->m_shutdown_on_fatal_error = false; m_node.notifications->m_shutdown_on_fatal_error = false;
fs::path snapshot_chainstate_dir = *node::FindSnapshotChainstateDir(chainman.m_options.datadir); fs::path snapshot_chainstate_dir = *node::FindAssumeutxoChainstateDir(chainman.m_options.datadir);
BOOST_CHECK(fs::exists(snapshot_chainstate_dir)); BOOST_CHECK(fs::exists(snapshot_chainstate_dir));
BOOST_CHECK_EQUAL(snapshot_chainstate_dir, gArgs.GetDataDirNet() / "chainstate_snapshot"); BOOST_CHECK_EQUAL(snapshot_chainstate_dir, gArgs.GetDataDirNet() / "chainstate_snapshot");
@@ -653,7 +654,7 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_completion, SnapshotTestSetup
const uint256 snapshot_tip_hash = WITH_LOCK(chainman.GetMutex(), const uint256 snapshot_tip_hash = WITH_LOCK(chainman.GetMutex(),
return chainman.ActiveTip()->GetBlockHash()); return chainman.ActiveTip()->GetBlockHash());
res = WITH_LOCK(::cs_main, return chainman.MaybeCompleteSnapshotValidation()); res = WITH_LOCK(::cs_main, return chainman.MaybeValidateSnapshot(validated_cs, active_cs));
BOOST_CHECK_EQUAL(res, SnapshotCompletionResult::SUCCESS); BOOST_CHECK_EQUAL(res, SnapshotCompletionResult::SUCCESS);
WITH_LOCK(::cs_main, BOOST_CHECK(chainman.IsSnapshotValidated())); WITH_LOCK(::cs_main, BOOST_CHECK(chainman.IsSnapshotValidated()));
@@ -669,7 +670,7 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_completion, SnapshotTestSetup
BOOST_CHECK_EQUAL(all_chainstates[0], &active_cs); BOOST_CHECK_EQUAL(all_chainstates[0], &active_cs);
// Trying completion again should return false. // Trying completion again should return false.
res = WITH_LOCK(::cs_main, return chainman.MaybeCompleteSnapshotValidation()); res = WITH_LOCK(::cs_main, return chainman.MaybeValidateSnapshot(validated_cs, active_cs));
BOOST_CHECK_EQUAL(res, SnapshotCompletionResult::SKIPPED); BOOST_CHECK_EQUAL(res, SnapshotCompletionResult::SKIPPED);
// The invalid snapshot path should not have been used. // The invalid snapshot path should not have been used.
@@ -720,6 +721,7 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_completion_hash_mismatch, Sna
{ {
auto chainstates = this->SetupSnapshot(); auto chainstates = this->SetupSnapshot();
Chainstate& validation_chainstate = *std::get<0>(chainstates); Chainstate& validation_chainstate = *std::get<0>(chainstates);
Chainstate& unvalidated_cs = *std::get<1>(chainstates);
ChainstateManager& chainman = *Assert(m_node.chainman); ChainstateManager& chainman = *Assert(m_node.chainman);
SnapshotCompletionResult res; SnapshotCompletionResult res;
m_node.notifications->m_shutdown_on_fatal_error = false; m_node.notifications->m_shutdown_on_fatal_error = false;
@@ -740,7 +742,7 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_snapshot_completion_hash_mismatch, Sna
{ {
ASSERT_DEBUG_LOG("failed to validate the -assumeutxo snapshot state"); ASSERT_DEBUG_LOG("failed to validate the -assumeutxo snapshot state");
res = WITH_LOCK(::cs_main, return chainman.MaybeCompleteSnapshotValidation()); res = WITH_LOCK(::cs_main, return chainman.MaybeValidateSnapshot(validation_chainstate, unvalidated_cs));
BOOST_CHECK_EQUAL(res, SnapshotCompletionResult::HASH_MISMATCH); BOOST_CHECK_EQUAL(res, SnapshotCompletionResult::HASH_MISMATCH);
} }
+60 -80
View File
@@ -3165,11 +3165,17 @@ bool Chainstate::ConnectTip(
Ticks<SecondsDouble>(m_chainman.time_total), Ticks<SecondsDouble>(m_chainman.time_total),
Ticks<MillisecondsDouble>(m_chainman.time_total) / m_chainman.num_blocks_total); Ticks<MillisecondsDouble>(m_chainman.time_total) / m_chainman.num_blocks_total);
// If we are the background validation chainstate, check to see if we are done // See if this chainstate has reached a target block and can be used to
// validating the snapshot (i.e. our tip has reached the snapshot's base block). // validate an assumeutxo snapshot. If it can, hashing the UTXO database
if (this != &m_chainman.ActiveChainstate()) { // will be slow, and cs_main could remain locked here for several minutes.
m_chainman.MaybeCompleteSnapshotValidation(); // If the snapshot is validated, the UTXO hash will be saved to
} // this->m_target_utxohash, causing HistoricalChainstate() to return null
// and this chainstate to no longer be used. ActivateBestChain() will also
// stop connecting blocks to this chainstate because this->ReachedTarget()
// will be true and this->setBlockIndexCandidates will not have additional
// blocks.
Chainstate& current_cs{*Assert(m_chainman.m_active_chainstate)};
m_chainman.MaybeValidateSnapshot(*this, current_cs);
connectTrace.BlockConnected(pindexNew, std::move(block_to_connect)); connectTrace.BlockConnected(pindexNew, std::move(block_to_connect));
return true; return true;
@@ -3527,7 +3533,7 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr<
// the target block. // the target block.
// //
// This cannot be done while holding cs_main (within // This cannot be done while holding cs_main (within
// MaybeCompleteSnapshotValidation) or a cs_main deadlock will occur. // MaybeValidateSnapshot) or a cs_main deadlock will occur.
if (m_chainman.snapshot_download_completed) { if (m_chainman.snapshot_download_completed) {
m_chainman.snapshot_download_completed(); m_chainman.snapshot_download_completed();
} }
@@ -5761,7 +5767,7 @@ util::Result<CBlockIndex*> ChainstateManager::ActivateSnapshot(
// PopulateAndValidateSnapshot can return (in error) before the leveldb datadir // PopulateAndValidateSnapshot can return (in error) before the leveldb datadir
// has been created, so only attempt removal if we got that far. // has been created, so only attempt removal if we got that far.
if (auto snapshot_datadir = node::FindSnapshotChainstateDir(m_options.datadir)) { if (auto snapshot_datadir = node::FindAssumeutxoChainstateDir(m_options.datadir)) {
// We have to destruct leveldb::DB in order to release the db lock, otherwise // We have to destruct leveldb::DB in order to release the db lock, otherwise
// DestroyDB() (in DeleteCoinsDBFromDisk()) will fail. See `leveldb::~DBImpl()`. // DestroyDB() (in DeleteCoinsDBFromDisk()) will fail. See `leveldb::~DBImpl()`.
// Destructing the chainstate (and so resetting the coinsviews object) does this. // Destructing the chainstate (and so resetting the coinsviews object) does this.
@@ -6037,45 +6043,35 @@ util::Result<void> ChainstateManager::PopulateAndValidateSnapshot(
} }
// Currently, this function holds cs_main for its duration, which could be for // Currently, this function holds cs_main for its duration, which could be for
// multiple minutes due to the ComputeUTXOStats call. This hold is necessary // multiple minutes due to the ComputeUTXOStats call. Holding cs_main used to be
// because we need to avoid advancing the background validation chainstate // necessary (before d43a1f1a2fa3) to avoid advancing validated_cs farther than
// farther than the snapshot base block - and this function is also invoked // its target block. Now it should be possible to avoid this, but simply
// from within ConnectTip, i.e. from within ActivateBestChain, so cs_main is // releasing cs_main here would not be possible because this function is invoked
// held anyway. // by ConnectTip within ActivateBestChain.
// //
// Eventually (TODO), we could somehow separate this function's runtime from // Eventually (TODO) it would be better to call this function outside of
// maintenance of the active chain, but that will either require // ActivateBestChain, on a separate thread that should not require cs_main to
// // hash, because the UTXO set is only hashed after the historical chainstate
// (i) setting `m_disabled` immediately and ensuring all chainstate accesses go // reaches its target block and is no longer changing.
// through IsUsable() checks, or SnapshotCompletionResult ChainstateManager::MaybeValidateSnapshot(Chainstate& validated_cs, Chainstate& unvalidated_cs)
//
// (ii) giving each chainstate its own lock instead of using cs_main for everything.
SnapshotCompletionResult ChainstateManager::MaybeCompleteSnapshotValidation()
{ {
AssertLockHeld(cs_main); AssertLockHeld(cs_main);
// If a snapshot does not need to be validated... // If the snapshot does not need to be validated...
if (m_ibd_chainstate.get() == &this->ActiveChainstate() || if (unvalidated_cs.m_assumeutxo != Assumeutxo::UNVALIDATED ||
// Or if either chainstate is unusable... // Or if either chainstate is unusable...
!m_snapshot_chainstate || !unvalidated_cs.m_from_snapshot_blockhash ||
m_snapshot_chainstate->m_assumeutxo != Assumeutxo::UNVALIDATED || validated_cs.m_assumeutxo != Assumeutxo::VALIDATED ||
!m_snapshot_chainstate->m_from_snapshot_blockhash || !validated_cs.m_chain.Tip() ||
!m_ibd_chainstate ||
m_ibd_chainstate->m_assumeutxo != Assumeutxo::VALIDATED ||
!m_ibd_chainstate->m_chain.Tip() ||
// Or the validated chainstate is not targeting the snapshot block... // Or the validated chainstate is not targeting the snapshot block...
!m_ibd_chainstate->m_target_blockhash || !validated_cs.m_target_blockhash ||
*m_ibd_chainstate->m_target_blockhash != *m_snapshot_chainstate->m_from_snapshot_blockhash || *validated_cs.m_target_blockhash != *unvalidated_cs.m_from_snapshot_blockhash ||
// Or the validated chainstate has not reached the snapshot block yet... // Or the validated chainstate has not reached the snapshot block yet...
!m_ibd_chainstate->ReachedTarget()) { !validated_cs.ReachedTarget()) {
// Then a snapshot cannot be validated and there is nothing to do. // Then the snapshot cannot be validated and there is nothing to do.
return SnapshotCompletionResult::SKIPPED; return SnapshotCompletionResult::SKIPPED;
} }
const int snapshot_tip_height = this->ActiveHeight(); assert(validated_cs.TargetBlock() == validated_cs.m_chain.Tip());
const int snapshot_base_height = *Assert(this->GetSnapshotBaseHeight());
const CBlockIndex& index_new = *Assert(m_ibd_chainstate->m_chain.Tip());
assert(SnapshotBlockhash());
uint256 snapshot_blockhash = *Assert(SnapshotBlockhash());
auto handle_invalid_snapshot = [&]() EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { auto handle_invalid_snapshot = [&]() EXCLUSIVE_LOCKS_REQUIRED(::cs_main) {
bilingual_str user_error = strprintf(_( bilingual_str user_error = strprintf(_(
@@ -6090,19 +6086,20 @@ SnapshotCompletionResult ChainstateManager::MaybeCompleteSnapshotValidation()
"Please report this incident to %s, including how you obtained the snapshot. " "Please report this incident to %s, including how you obtained the snapshot. "
"The invalid snapshot chainstate will be left on disk in case it is " "The invalid snapshot chainstate will be left on disk in case it is "
"helpful in diagnosing the issue that caused this error."), "helpful in diagnosing the issue that caused this error."),
CLIENT_NAME, snapshot_tip_height, snapshot_base_height, snapshot_base_height, CLIENT_BUGREPORT CLIENT_NAME, unvalidated_cs.m_chain.Height(),
); validated_cs.m_chain.Height(),
validated_cs.m_chain.Height(), CLIENT_BUGREPORT);
LogError("[snapshot] !!! %s\n", user_error.original); LogError("[snapshot] !!! %s\n", user_error.original);
LogError("[snapshot] deleting snapshot, reverting to validated chain, and stopping node\n"); LogError("[snapshot] deleting snapshot, reverting to validated chain, and stopping node\n");
// Reset chainstate target to network tip instead of snapshot block. // Reset chainstate target to network tip instead of snapshot block.
m_ibd_chainstate->SetTargetBlock(nullptr); validated_cs.SetTargetBlock(nullptr);
m_active_chainstate = m_ibd_chainstate.get(); m_active_chainstate = &validated_cs;
m_snapshot_chainstate->m_assumeutxo = Assumeutxo::INVALID; unvalidated_cs.m_assumeutxo = Assumeutxo::INVALID;
auto rename_result = m_snapshot_chainstate->InvalidateCoinsDBOnDisk(); auto rename_result = unvalidated_cs.InvalidateCoinsDBOnDisk();
if (!rename_result) { if (!rename_result) {
user_error += Untranslated("\n") + util::ErrorString(rename_result); user_error += Untranslated("\n") + util::ErrorString(rename_result);
} }
@@ -6110,34 +6107,25 @@ SnapshotCompletionResult ChainstateManager::MaybeCompleteSnapshotValidation()
GetNotifications().fatalError(user_error); GetNotifications().fatalError(user_error);
}; };
assert(index_new.GetBlockHash() == snapshot_blockhash); CCoinsViewDB& validated_coins_db = validated_cs.CoinsDB();
assert(index_new.nHeight == snapshot_base_height); validated_cs.ForceFlushStateToDisk();
int curr_height = m_ibd_chainstate->m_chain.Height(); const auto& maybe_au_data = m_options.chainparams.AssumeutxoForHeight(validated_cs.m_chain.Height());
assert(snapshot_base_height == curr_height);
assert(snapshot_base_height == index_new.nHeight);
assert(this->GetAll().size() == 2);
CCoinsViewDB& ibd_coins_db = m_ibd_chainstate->CoinsDB();
m_ibd_chainstate->ForceFlushStateToDisk();
const auto& maybe_au_data = m_options.chainparams.AssumeutxoForHeight(curr_height);
if (!maybe_au_data) { if (!maybe_au_data) {
LogWarning("[snapshot] assumeutxo data not found for height " LogWarning("[snapshot] assumeutxo data not found for height "
"(%d) - refusing to validate snapshot", curr_height); "(%d) - refusing to validate snapshot", validated_cs.m_chain.Height());
handle_invalid_snapshot(); handle_invalid_snapshot();
return SnapshotCompletionResult::MISSING_CHAINPARAMS; return SnapshotCompletionResult::MISSING_CHAINPARAMS;
} }
const AssumeutxoData& au_data = *maybe_au_data; const AssumeutxoData& au_data = *maybe_au_data;
std::optional<CCoinsStats> maybe_ibd_stats; std::optional<CCoinsStats> validated_cs_stats;
LogInfo("[snapshot] computing UTXO stats for background chainstate to validate " LogInfo("[snapshot] computing UTXO stats for background chainstate to validate "
"snapshot - this could take a few minutes"); "snapshot - this could take a few minutes");
try { try {
maybe_ibd_stats = ComputeUTXOStats( validated_cs_stats = ComputeUTXOStats(
CoinStatsHashType::HASH_SERIALIZED, CoinStatsHashType::HASH_SERIALIZED,
&ibd_coins_db, &validated_coins_db,
m_blockman, m_blockman,
[&interrupt = m_interrupt] { SnapshotUTXOHashBreakpoint(interrupt); }); [&interrupt = m_interrupt] { SnapshotUTXOHashBreakpoint(interrupt); });
} catch (StopHashingException const&) { } catch (StopHashingException const&) {
@@ -6145,7 +6133,7 @@ SnapshotCompletionResult ChainstateManager::MaybeCompleteSnapshotValidation()
} }
// XXX note that this function is slow and will hold cs_main for potentially minutes. // XXX note that this function is slow and will hold cs_main for potentially minutes.
if (!maybe_ibd_stats) { if (!validated_cs_stats) {
LogWarning("[snapshot] failed to generate stats for validation coins db"); LogWarning("[snapshot] failed to generate stats for validation coins db");
// While this isn't a problem with the snapshot per se, this condition // 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 // prevents us from validating the snapshot, so we should shut down and let the
@@ -6153,7 +6141,6 @@ SnapshotCompletionResult ChainstateManager::MaybeCompleteSnapshotValidation()
handle_invalid_snapshot(); handle_invalid_snapshot();
return SnapshotCompletionResult::STATS_FAILED; return SnapshotCompletionResult::STATS_FAILED;
} }
const auto& ibd_stats = *maybe_ibd_stats;
// Compare the background validation chainstate's UTXO set hash against the hard-coded // Compare the background validation chainstate's UTXO set hash against the hard-coded
// assumeutxo hash we expect. // assumeutxo hash we expect.
@@ -6161,19 +6148,19 @@ SnapshotCompletionResult ChainstateManager::MaybeCompleteSnapshotValidation()
// TODO: For belt-and-suspenders, we could cache the UTXO set // 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 // hash for the snapshot when it's loaded in its chainstate's leveldb. We could then
// reference that here for an additional check. // reference that here for an additional check.
if (AssumeutxoHash{ibd_stats.hashSerialized} != au_data.hash_serialized) { if (AssumeutxoHash{validated_cs_stats->hashSerialized} != au_data.hash_serialized) {
LogWarning("[snapshot] hash mismatch: actual=%s, expected=%s", LogWarning("[snapshot] hash mismatch: actual=%s, expected=%s",
ibd_stats.hashSerialized.ToString(), validated_cs_stats->hashSerialized.ToString(),
au_data.hash_serialized.ToString()); au_data.hash_serialized.ToString());
handle_invalid_snapshot(); handle_invalid_snapshot();
return SnapshotCompletionResult::HASH_MISMATCH; return SnapshotCompletionResult::HASH_MISMATCH;
} }
LogInfo("[snapshot] snapshot beginning at %s has been fully validated", LogInfo("[snapshot] snapshot beginning at %s has been fully validated",
snapshot_blockhash.ToString()); unvalidated_cs.m_from_snapshot_blockhash->ToString());
m_snapshot_chainstate->m_assumeutxo = Assumeutxo::VALIDATED; unvalidated_cs.m_assumeutxo = Assumeutxo::VALIDATED;
m_ibd_chainstate->m_target_utxohash = AssumeutxoHash{ibd_stats.hashSerialized}; validated_cs.m_target_utxohash = AssumeutxoHash{validated_cs_stats->hashSerialized};
this->MaybeRebalanceCaches(); this->MaybeRebalanceCaches();
return SnapshotCompletionResult::SUCCESS; return SnapshotCompletionResult::SUCCESS;
@@ -6260,24 +6247,23 @@ ChainstateManager::~ChainstateManager()
m_versionbitscache.Clear(); m_versionbitscache.Clear();
} }
bool ChainstateManager::DetectSnapshotChainstate() Chainstate* ChainstateManager::LoadAssumeutxoChainstate()
{ {
assert(!m_snapshot_chainstate); assert(!m_snapshot_chainstate);
std::optional<fs::path> path = node::FindSnapshotChainstateDir(m_options.datadir); std::optional<fs::path> path = node::FindAssumeutxoChainstateDir(m_options.datadir);
if (!path) { if (!path) {
return false; return nullptr;
} }
std::optional<uint256> base_blockhash = node::ReadSnapshotBaseBlockhash(*path); std::optional<uint256> base_blockhash = node::ReadSnapshotBaseBlockhash(*path);
if (!base_blockhash) { if (!base_blockhash) {
return false; return nullptr;
} }
LogInfo("[snapshot] detected active snapshot chainstate (%s) - loading", LogInfo("[snapshot] detected active snapshot chainstate (%s) - loading",
fs::PathToString(*path)); fs::PathToString(*path));
auto snapshot_chainstate{std::make_unique<Chainstate>(nullptr, m_blockman, *this, base_blockhash)}; auto snapshot_chainstate{std::make_unique<Chainstate>(nullptr, m_blockman, *this, base_blockhash)};
LogInfo("[snapshot] switching active chainstate to %s", snapshot_chainstate->ToString()); LogInfo("[snapshot] switching active chainstate to %s", snapshot_chainstate->ToString());
this->AddChainstate(std::move(snapshot_chainstate)); return &this->AddChainstate(std::move(snapshot_chainstate));
return true;
} }
Chainstate& ChainstateManager::AddChainstate(std::unique_ptr<Chainstate> chainstate) Chainstate& ChainstateManager::AddChainstate(std::unique_ptr<Chainstate> chainstate)
@@ -6337,7 +6323,7 @@ util::Result<void> Chainstate::InvalidateCoinsDBOnDisk()
// The invalid snapshot datadir is simply moved and not deleted because we may // 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 // want to do forensics later during issue investigation. The user is instructed
// accordingly in MaybeCompleteSnapshotValidation(). // accordingly in MaybeValidateSnapshot().
try { try {
fs::rename(snapshot_datadir, invalid_path); fs::rename(snapshot_datadir, invalid_path);
} catch (const fs::filesystem_error& e) { } catch (const fs::filesystem_error& e) {
@@ -6362,7 +6348,7 @@ bool ChainstateManager::DeleteSnapshotChainstate()
Assert(m_snapshot_chainstate); Assert(m_snapshot_chainstate);
Assert(m_ibd_chainstate); Assert(m_ibd_chainstate);
fs::path snapshot_datadir = Assert(node::FindSnapshotChainstateDir(m_options.datadir)).value(); fs::path snapshot_datadir = Assert(node::FindAssumeutxoChainstateDir(m_options.datadir)).value();
if (!DeleteCoinsDBFromDisk(snapshot_datadir, /*is_snapshot=*/ true)) { if (!DeleteCoinsDBFromDisk(snapshot_datadir, /*is_snapshot=*/ true)) {
LogError("Deletion of %s failed. Please remove it manually to continue reindexing.", LogError("Deletion of %s failed. Please remove it manually to continue reindexing.",
fs::PathToString(snapshot_datadir)); fs::PathToString(snapshot_datadir));
@@ -6384,12 +6370,6 @@ const CBlockIndex* ChainstateManager::GetSnapshotBaseBlock() const
return m_active_chainstate ? m_active_chainstate->SnapshotBase() : nullptr; return m_active_chainstate ? m_active_chainstate->SnapshotBase() : nullptr;
} }
std::optional<int> ChainstateManager::GetSnapshotBaseHeight() const
{
const CBlockIndex* base = this->GetSnapshotBaseBlock();
return base ? std::make_optional(base->nHeight) : std::nullopt;
}
void ChainstateManager::RecalculateBestHeader() void ChainstateManager::RecalculateBestHeader()
{ {
AssertLockHeld(cs_main); AssertLockHeld(cs_main);
+12 -14
View File
@@ -1131,14 +1131,15 @@ public:
[[nodiscard]] util::Result<CBlockIndex*> ActivateSnapshot( [[nodiscard]] util::Result<CBlockIndex*> 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 //! Try to validate an assumeutxo snapshot by using a validated historical
//! is the base of the UTXO snapshot in use, compare its coins to ensure //! chainstate targeted at the snapshot block. When the target block is
//! they match those expected by the snapshot. //! reached, the UTXO hash is computed and saved to
//! //! `validated_cs.m_target_utxohash`, and `unvalidated_cs.m_assumeutxo` will
//! If the coins match (expected), then mark the validation chainstate for //! be updated from UNVALIDATED to either VALIDATED or INVALID depending on
//! deletion and continue using the snapshot chainstate as active. //! whether the hash matches. The INVALID case should not happen in practice
//! Otherwise, revert to using the ibd chainstate and shutdown. //! because the software should refuse to load unrecognized snapshots, but
SnapshotCompletionResult MaybeCompleteSnapshotValidation() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); //! if it does happen, it is a fatal error.
SnapshotCompletionResult MaybeValidateSnapshot(Chainstate& validated_cs, Chainstate& unvalidated_cs) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
//! Returns nullptr if no snapshot has been loaded. //! Returns nullptr if no snapshot has been loaded.
const CBlockIndex* GetSnapshotBaseBlock() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main); const CBlockIndex* GetSnapshotBaseBlock() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
@@ -1312,8 +1313,9 @@ public:
void ReportHeadersPresync(const arith_uint256& work, int64_t height, int64_t timestamp); void ReportHeadersPresync(const arith_uint256& work, int64_t height, int64_t timestamp);
//! When starting up, search the datadir for a chainstate based on a UTXO //! When starting up, search the datadir for a chainstate based on a UTXO
//! snapshot that is in the process of being validated. //! snapshot that is in the process of being validated and load it if found.
bool DetectSnapshotChainstate() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); //! Return pointer to the Chainstate if it is loaded.
Chainstate* LoadAssumeutxoChainstate() EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
//! Add new chainstate. //! Add new chainstate.
Chainstate& AddChainstate(std::unique_ptr<Chainstate> chainstate) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); Chainstate& AddChainstate(std::unique_ptr<Chainstate> chainstate) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
@@ -1351,10 +1353,6 @@ public:
std::pair<int, int> GetPruneRange( std::pair<int, int> GetPruneRange(
const Chainstate& chainstate, int last_height_can_prune) EXCLUSIVE_LOCKS_REQUIRED(::cs_main); const Chainstate& chainstate, int last_height_can_prune) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
//! Return the height of the base block of the snapshot in use, if one exists, else
//! nullopt.
std::optional<int> GetSnapshotBaseHeight() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
//! Get range of historical blocks to download. //! Get range of historical blocks to download.
std::optional<std::pair<const CBlockIndex*, const CBlockIndex*>> GetHistoricalBlockRange() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main); std::optional<std::pair<const CBlockIndex*, const CBlockIndex*>> GetHistoricalBlockRange() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main);