aboutsummaryrefslogtreecommitdiff
path: root/src/validation.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/validation.cpp')
-rw-r--r--src/validation.cpp293
1 files changed, 292 insertions, 1 deletions
diff --git a/src/validation.cpp b/src/validation.cpp
index 778d75ce18..31609ea3e5 100644
--- a/src/validation.cpp
+++ b/src/validation.cpp
@@ -20,6 +20,7 @@
#include <index/txindex.h>
#include <logging.h>
#include <logging/timer.h>
+#include <node/coinstats.h>
#include <node/ui_interface.h>
#include <optional.h>
#include <policy/policy.h>
@@ -5147,7 +5148,8 @@ double GuessVerificationProgress(const ChainTxData& data, const CBlockIndex *pin
Optional<uint256> ChainstateManager::SnapshotBlockhash() const {
LOCK(::cs_main);
- if (m_active_chainstate != nullptr) {
+ if (m_active_chainstate != nullptr &&
+ !m_active_chainstate->m_from_snapshot_blockhash.IsNull()) {
// If a snapshot chainstate exists, it will always be our active.
return m_active_chainstate->m_from_snapshot_blockhash;
}
@@ -5192,6 +5194,295 @@ CChainState& ChainstateManager::InitializeChainstate(CTxMemPool& mempool, const
return *to_modify;
}
+const AssumeutxoData* ExpectedAssumeutxo(
+ const int height, const CChainParams& chainparams)
+{
+ const MapAssumeutxo& valid_assumeutxos_map = chainparams.Assumeutxo();
+ const auto assumeutxo_found = valid_assumeutxos_map.find(height);
+
+ if (assumeutxo_found != valid_assumeutxos_map.end()) {
+ return &assumeutxo_found->second;
+ }
+ return nullptr;
+}
+
+bool ChainstateManager::ActivateSnapshot(
+ CAutoFile& coins_file,
+ const SnapshotMetadata& metadata,
+ bool in_memory)
+{
+ uint256 base_blockhash = metadata.m_base_blockhash;
+
+ if (this->SnapshotBlockhash()) {
+ LogPrintf("[snapshot] can't activate a snapshot-based chainstate more than once\n");
+ return false;
+ }
+
+ int64_t current_coinsdb_cache_size{0};
+ int64_t current_coinstip_cache_size{0};
+
+ // Cache percentages to allocate to each chainstate.
+ //
+ // These particular percentages don't matter so much since they will only be
+ // relevant during snapshot activation; caches are rebalanced at the conclusion of
+ // this function. We want to give (essentially) all available cache capacity to the
+ // snapshot to aid the bulk load later in this function.
+ static constexpr double IBD_CACHE_PERC = 0.01;
+ static constexpr double SNAPSHOT_CACHE_PERC = 0.99;
+
+ {
+ LOCK(::cs_main);
+ // Resize the coins caches to ensure we're not exceeding memory limits.
+ //
+ // Allocate the majority of the cache to the incoming snapshot chainstate, since
+ // (optimistically) getting to its tip will be the top priority. We'll need to call
+ // `MaybeRebalanceCaches()` once we're done with this function to ensure
+ // the right allocation (including the possibility that no snapshot was activated
+ // and that we should restore the active chainstate caches to their original size).
+ //
+ current_coinsdb_cache_size = this->ActiveChainstate().m_coinsdb_cache_size_bytes;
+ current_coinstip_cache_size = this->ActiveChainstate().m_coinstip_cache_size_bytes;
+
+ // Temporarily resize the active coins cache to make room for the newly-created
+ // snapshot chain.
+ this->ActiveChainstate().ResizeCoinsCaches(
+ static_cast<size_t>(current_coinstip_cache_size * IBD_CACHE_PERC),
+ static_cast<size_t>(current_coinsdb_cache_size * IBD_CACHE_PERC));
+ }
+
+ auto snapshot_chainstate = WITH_LOCK(::cs_main, return MakeUnique<CChainState>(
+ this->ActiveChainstate().m_mempool, m_blockman, base_blockhash));
+
+ {
+ LOCK(::cs_main);
+ snapshot_chainstate->InitCoinsDB(
+ static_cast<size_t>(current_coinsdb_cache_size * SNAPSHOT_CACHE_PERC),
+ in_memory, false, "chainstate");
+ snapshot_chainstate->InitCoinsCache(
+ static_cast<size_t>(current_coinstip_cache_size * SNAPSHOT_CACHE_PERC));
+ }
+
+ const bool snapshot_ok = this->PopulateAndValidateSnapshot(
+ *snapshot_chainstate, coins_file, metadata);
+
+ if (!snapshot_ok) {
+ WITH_LOCK(::cs_main, this->MaybeRebalanceCaches());
+ return false;
+ }
+
+ {
+ LOCK(::cs_main);
+ assert(!m_snapshot_chainstate);
+ m_snapshot_chainstate.swap(snapshot_chainstate);
+ const bool chaintip_loaded = m_snapshot_chainstate->LoadChainTip(::Params());
+ assert(chaintip_loaded);
+
+ m_active_chainstate = m_snapshot_chainstate.get();
+
+ LogPrintf("[snapshot] successfully activated snapshot %s\n", base_blockhash.ToString());
+ LogPrintf("[snapshot] (%.2f MB)\n",
+ m_snapshot_chainstate->CoinsTip().DynamicMemoryUsage() / (1000 * 1000));
+
+ this->MaybeRebalanceCaches();
+ }
+ return true;
+}
+
+bool ChainstateManager::PopulateAndValidateSnapshot(
+ CChainState& snapshot_chainstate,
+ CAutoFile& coins_file,
+ const SnapshotMetadata& metadata)
+{
+ // It's okay to release cs_main before we're done using `coins_cache` because we know
+ // that nothing else will be referencing the newly created snapshot_chainstate yet.
+ CCoinsViewCache& coins_cache = *WITH_LOCK(::cs_main, return &snapshot_chainstate.CoinsTip());
+
+ uint256 base_blockhash = metadata.m_base_blockhash;
+
+ COutPoint outpoint;
+ Coin coin;
+ const uint64_t coins_count = metadata.m_coins_count;
+ uint64_t coins_left = metadata.m_coins_count;
+
+ LogPrintf("[snapshot] loading coins from snapshot %s\n", base_blockhash.ToString());
+ int64_t flush_now{0};
+ int64_t coins_processed{0};
+
+ while (coins_left > 0) {
+ try {
+ coins_file >> outpoint;
+ } catch (const std::ios_base::failure&) {
+ LogPrintf("[snapshot] bad snapshot - no coins left after deserializing %d coins\n",
+ coins_count - coins_left);
+ return false;
+ }
+ coins_file >> coin;
+ coins_cache.EmplaceCoinInternalDANGER(std::move(outpoint), std::move(coin));
+
+ --coins_left;
+ ++coins_processed;
+
+ if (coins_processed % 1000000 == 0) {
+ LogPrintf("[snapshot] %d coins loaded (%.2f%%, %.2f MB)\n",
+ coins_processed,
+ static_cast<float>(coins_processed) * 100 / static_cast<float>(coins_count),
+ coins_cache.DynamicMemoryUsage() / (1000 * 1000));
+ }
+
+ // Batch write and flush (if we need to) every so often.
+ //
+ // If our average Coin size is roughly 41 bytes, checking every 120,000 coins
+ // means <5MB of memory imprecision.
+ if (coins_processed % 120000 == 0) {
+ if (ShutdownRequested()) {
+ return false;
+ }
+
+ const auto snapshot_cache_state = WITH_LOCK(::cs_main,
+ return snapshot_chainstate.GetCoinsCacheSizeState(&snapshot_chainstate.m_mempool));
+
+ if (snapshot_cache_state >=
+ CoinsCacheSizeState::CRITICAL) {
+ LogPrintf("[snapshot] flushing coins cache (%.2f MB)... ", /* Continued */
+ coins_cache.DynamicMemoryUsage() / (1000 * 1000));
+ flush_now = GetTimeMillis();
+
+ // This is a hack - we don't know what the actual best block is, but that
+ // doesn't matter for the purposes of flushing the cache here. We'll set this
+ // to its correct value (`base_blockhash`) below after the coins are loaded.
+ coins_cache.SetBestBlock(GetRandHash());
+
+ coins_cache.Flush();
+ LogPrintf("done (%.2fms)\n", GetTimeMillis() - flush_now);
+ }
+ }
+ }
+
+ // Important that we set this. This and the coins_cache accesses above are
+ // sort of a layer violation, but either we reach into the innards of
+ // CCoinsViewCache here or we have to invert some of the CChainState to
+ // embed them in a snapshot-activation-specific CCoinsViewCache bulk load
+ // method.
+ coins_cache.SetBestBlock(base_blockhash);
+
+ bool out_of_coins{false};
+ try {
+ coins_file >> outpoint;
+ } catch (const std::ios_base::failure&) {
+ // We expect an exception since we should be out of coins.
+ out_of_coins = true;
+ }
+ if (!out_of_coins) {
+ LogPrintf("[snapshot] bad snapshot - coins left over after deserializing %d coins\n",
+ coins_count);
+ return false;
+ }
+
+ LogPrintf("[snapshot] loaded %d (%.2f MB) coins from snapshot %s\n",
+ coins_count,
+ coins_cache.DynamicMemoryUsage() / (1000 * 1000),
+ base_blockhash.ToString());
+
+ LogPrintf("[snapshot] flushing snapshot chainstate to disk\n");
+ // No need to acquire cs_main since this chainstate isn't being used yet.
+ coins_cache.Flush(); // TODO: if #17487 is merged, add erase=false here for better performance.
+
+ assert(coins_cache.GetBestBlock() == base_blockhash);
+
+ CCoinsStats stats;
+ auto breakpoint_fnc = [] { /* TODO insert breakpoint here? */ };
+
+ // As above, okay to immediately release cs_main here since no other context knows
+ // about the snapshot_chainstate.
+ CCoinsViewDB* snapshot_coinsdb = WITH_LOCK(::cs_main, return &snapshot_chainstate.CoinsDB());
+
+ if (!GetUTXOStats(snapshot_coinsdb, stats, CoinStatsHashType::HASH_SERIALIZED, breakpoint_fnc)) {
+ LogPrintf("[snapshot] failed to generate coins stats\n");
+ return false;
+ }
+
+ // Ensure that the base blockhash appears in the known chain of valid headers. We're willing to
+ // wait a bit here because the snapshot may have been loaded on startup, before we've
+ // received headers from the network.
+
+ int max_secs_to_wait_for_headers = 60 * 10;
+ CBlockIndex* snapshot_start_block = nullptr;
+
+ while (max_secs_to_wait_for_headers > 0) {
+ snapshot_start_block = WITH_LOCK(::cs_main,
+ return m_blockman.LookupBlockIndex(base_blockhash));
+ --max_secs_to_wait_for_headers;
+
+ if (!snapshot_start_block) {
+ std::this_thread::sleep_for(std::chrono::seconds(1));
+ } else {
+ break;
+ }
+ }
+
+ if (snapshot_start_block == nullptr) {
+ LogPrintf("[snapshot] timed out waiting for snapshot start blockheader %s\n",
+ base_blockhash.ToString());
+ return false;
+ }
+
+ // Assert that the deserialized chainstate contents match the expected assumeutxo value.
+
+ int base_height = snapshot_start_block->nHeight;
+ auto maybe_au_data = ExpectedAssumeutxo(base_height, ::Params());
+
+ if (!maybe_au_data) {
+ LogPrintf("[snapshot] assumeutxo height in snapshot metadata not recognized " /* Continued */
+ "(%d) - refusing to load snapshot\n", base_height);
+ return false;
+ }
+
+ const AssumeutxoData& au_data = *maybe_au_data;
+
+ if (stats.hashSerialized != au_data.hash_serialized) {
+ LogPrintf("[snapshot] bad snapshot content hash: expected %s, got %s\n",
+ au_data.hash_serialized.ToString(), stats.hashSerialized.ToString());
+ return false;
+ }
+
+ snapshot_chainstate.m_chain.SetTip(snapshot_start_block);
+
+ // The remainder of this function requires modifying data protected by cs_main.
+ LOCK(::cs_main);
+
+ // Fake various pieces of CBlockIndex state:
+ //
+ // - nChainTx: so that we accurately report IBD-to-tip progress
+ // - nTx: so that LoadBlockIndex() loads assumed-valid CBlockIndex entries
+ // (among other things)
+ // - nStatus & BLOCK_OPT_WITNESS: so that RewindBlockIndex() doesn't zealously
+ // unwind the assumed-valid chain.
+ //
+ CBlockIndex* index = nullptr;
+ for (int i = 0; i <= snapshot_chainstate.m_chain.Height(); ++i) {
+ index = snapshot_chainstate.m_chain[i];
+
+ if (!index->nTx) {
+ index->nTx = 1;
+ }
+ index->nChainTx = index->pprev ? index->pprev->nChainTx + index->nTx : 1;
+
+ // We need to fake this flag so that CChainState::RewindBlockIndex()
+ // won't try to rewind the entire assumed-valid chain on startup.
+ if (index->pprev && ::IsWitnessEnabled(index->pprev, ::Params().GetConsensus())) {
+ index->nStatus |= BLOCK_OPT_WITNESS;
+ }
+ }
+
+ assert(index);
+ index->nChainTx = metadata.m_nchaintx;
+ snapshot_chainstate.setBlockIndexCandidates.insert(snapshot_start_block);
+
+ LogPrintf("[snapshot] validated snapshot (%.2f MB)\n",
+ coins_cache.DynamicMemoryUsage() / (1000 * 1000));
+ return true;
+}
+
CChainState& ChainstateManager::ActiveChainstate() const
{
LOCK(::cs_main);