Page MenuHomePhabricator

D11239.id32916.diff
No OneTemporary

D11239.id32916.diff

diff --git a/src/blockindex.h b/src/blockindex.h
--- a/src/blockindex.h
+++ b/src/blockindex.h
@@ -51,6 +51,10 @@
//! Number of transactions in this block.
//! Note: in a potential headers-first mode, this number cannot be relied
//! upon
+ //! Note: this value is faked during UTXO snapshot load to ensure that
+ //! LoadBlockIndex() will load index entries for blocks that we lack data
+ //! for.
+ //! @sa ActivateSnapshot
unsigned int nTx{0};
//! Size of this block.
@@ -58,14 +62,19 @@
//! upon
unsigned int nSize{0};
-private:
//! (memory only) Number of transactions in the chain up to and including
//! this block.
//! This value will be non-zero only if and only if transactions for this
//! block and all its parents are available. Change to 64-bit type when
//! necessary; won't happen before 2030
+ //!
+ //! Note: this value is faked during use of a UTXO snapshot because we don't
+ //! have the underlying block data available during snapshot load.
+ //! @sa AssumeutxoData
+ //! @sa ActivateSnapshot
unsigned int nChainTx{0};
+private:
//! (memory only) Size of all blocks in the chain up to and including this
//! block. This value will be non-zero only if and only if transactions for
//! this block and all its parents are available.
diff --git a/src/coins.h b/src/coins.h
--- a/src/coins.h
+++ b/src/coins.h
@@ -17,6 +17,8 @@
#include <functional>
#include <unordered_map>
+class ChainstateManager;
+
/**
* A UTXO entry.
*
@@ -146,6 +148,8 @@
CCoinsCacheEntry() : flags(0) {}
explicit CCoinsCacheEntry(Coin coinIn)
: coin(std::move(coinIn)), flags(0) {}
+ CCoinsCacheEntry(Coin &&coin_, uint8_t flag)
+ : coin(std::move(coin_)), flags(flag) {}
};
typedef std::unordered_map<COutPoint, CCoinsCacheEntry, SaltedOutpointHasher>
@@ -285,6 +289,15 @@
*/
void AddCoin(const COutPoint &outpoint, Coin coin, bool possible_overwrite);
+ /**
+ * Emplace a coin into cacheCoins without performing any checks, marking
+ * the emplaced coin as dirty.
+ *
+ * NOT FOR GENERAL USE. Used only when loading coins from a UTXO snapshot.
+ * @sa ChainstateManager::PopulateAndValidateSnapshot()
+ */
+ void EmplaceCoinInternalDANGER(COutPoint &&outpoint, Coin &&coin);
+
/**
* Spend a coin. Pass moveto in order to get the deleted data.
* If no unspent output exists for the passed outpoint, this call has no
diff --git a/src/coins.cpp b/src/coins.cpp
--- a/src/coins.cpp
+++ b/src/coins.cpp
@@ -141,6 +141,14 @@
cachedCoinsUsage += it->second.coin.DynamicMemoryUsage();
}
+void CCoinsViewCache::EmplaceCoinInternalDANGER(COutPoint &&outpoint,
+ Coin &&coin) {
+ cachedCoinsUsage += coin.DynamicMemoryUsage();
+ cacheCoins.emplace(
+ std::piecewise_construct, std::forward_as_tuple(std::move(outpoint)),
+ std::forward_as_tuple(std::move(coin), CCoinsCacheEntry::DIRTY));
+}
+
void AddCoins(CCoinsViewCache &cache, const CTransaction &tx, int nHeight,
bool check_for_overwrite) {
bool fCoinbase = tx.IsCoinBase();
diff --git a/src/node/utxo_snapshot.h b/src/node/utxo_snapshot.h
--- a/src/node/utxo_snapshot.h
+++ b/src/node/utxo_snapshot.h
@@ -21,18 +21,13 @@
//! during snapshot load to estimate progress of UTXO set reconstruction.
uint64_t m_coins_count = 0;
- //! Necessary to "fake" the base nChainTx so that we can estimate progress
- //! during initial block download for the assumeutxo chainstate.
- uint64_t m_nchaintx = 0;
-
SnapshotMetadata() {}
SnapshotMetadata(const BlockHash &base_blockhash, uint64_t coins_count,
uint64_t nchaintx)
- : m_base_blockhash(base_blockhash), m_coins_count(coins_count),
- m_nchaintx(nchaintx) {}
+ : m_base_blockhash(base_blockhash), m_coins_count(coins_count) {}
SERIALIZE_METHODS(SnapshotMetadata, obj) {
- READWRITE(obj.m_base_blockhash, obj.m_coins_count, obj.m_nchaintx);
+ READWRITE(obj.m_base_blockhash, obj.m_coins_count);
}
};
diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp
--- a/src/test/validation_chainstatemanager_tests.cpp
+++ b/src/test/validation_chainstatemanager_tests.cpp
@@ -27,6 +27,8 @@
std::vector<CChainState *> chainstates;
const CChainParams &chainparams = Params();
+ BOOST_CHECK(!manager.SnapshotBlockhash().has_value());
+
// Create a legacy (IBD) chainstate.
//
CChainState &c1 =
@@ -56,12 +58,18 @@
auto &validated_cs = manager.ValidatedChainstate();
BOOST_CHECK_EQUAL(&validated_cs, &c1);
+ BOOST_CHECK(!manager.SnapshotBlockhash().has_value());
+
// Create a snapshot-based chainstate.
//
- CChainState &c2 =
- *WITH_LOCK(::cs_main, return &manager.InitializeChainstate(
- mempool, BlockHash{GetRandHash()}));
+ const BlockHash snapshot_blockhash{GetRandHash()};
+ CChainState &c2 = *WITH_LOCK(
+ ::cs_main,
+ return &manager.InitializeChainstate(mempool, snapshot_blockhash));
chainstates.push_back(&c2);
+
+ BOOST_CHECK_EQUAL(manager.SnapshotBlockhash().value(), snapshot_blockhash);
+
c2.InitCoinsDB(
/* cache_size_bytes */ 1 << 23, /* in_memory */ true,
/* should_wipe */ false);
diff --git a/src/validation.h b/src/validation.h
--- a/src/validation.h
+++ b/src/validation.h
@@ -12,6 +12,7 @@
#endif
#include <amount.h>
+#include <attributes.h>
#include <blockfileinfo.h>
#include <blockindexworkcomparator.h>
#include <coins.h>
@@ -19,6 +20,7 @@
#include <disconnectresult.h>
#include <flatfile.h>
#include <fs.h>
+#include <node/utxo_snapshot.h>
#include <protocol.h> // For CMessageHeader::MessageMagic
#include <script/script_error.h>
#include <script/script_metrics.h>
@@ -1133,6 +1135,12 @@
//! by the background validation chainstate.
bool m_snapshot_validated{false};
+ //! Internal helper for ActivateSnapshot().
+ [[nodiscard]] bool
+ PopulateAndValidateSnapshot(CChainState &snapshot_chainstate,
+ CAutoFile &coins_file,
+ const SnapshotMetadata &metadata);
+
// For access to m_active_chainstate.
friend CChainState &ChainstateActive();
friend CChain &ChainActive();
@@ -1165,6 +1173,23 @@
//! Get all chainstates currently being used.
std::vector<CChainState *> GetAll();
+ //! Construct and activate a Chainstate on the basis of UTXO snapshot data.
+ //!
+ //! Steps:
+ //!
+ //! - Initialize an unused CChainState.
+ //! - Load its `CoinsViews` contents from `coins_file`.
+ //! - Verify that the hash of the resulting coinsdb matches the expected
+ //! hash per assumeutxo chain parameters.
+ //! - Wait for our headers chain to include the base block of the snapshot.
+ //! - "Fast forward" the tip of the new chainstate to the base of the
+ //! snapshot, faking nTx* block index data along the way.
+ //! - Move the new chainstate to `m_snapshot_chainstate` and make it our
+ //! ChainstateActive().
+ [[nodiscard]] bool ActivateSnapshot(CAutoFile &coins_file,
+ const SnapshotMetadata &metadata,
+ bool in_memory);
+
//! The most-work chain.
CChainState &ActiveChainstate() const;
CChain &ActiveChain() const { return ActiveChainstate().m_chain; }
diff --git a/src/validation.cpp b/src/validation.cpp
--- a/src/validation.cpp
+++ b/src/validation.cpp
@@ -6063,7 +6063,8 @@
std::optional<BlockHash> ChainstateManager::SnapshotBlockhash() const {
// for m_active_chainstate access
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;
}
@@ -6118,6 +6119,300 @@
return nullptr;
}
+bool ChainstateManager::ActivateSnapshot(CAutoFile &coins_file,
+ const SnapshotMetadata &metadata,
+ bool in_memory) {
+ BlockHash 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 std::make_unique<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());
+
+ BlockHash 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) {
+ LogPrintfToBeContinued(
+ "[snapshot] flushing coins cache (%.2f MB)... ",
+ 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(BlockHash{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.
+ // TODO: if #17487 is merged, add erase=false here for better performance.
+ coins_cache.Flush();
+
+ 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.
+
+ auto max_secs_to_wait_for_headers = 600s;
+ CBlockIndex *snapshot_start_block = nullptr;
+
+ while (max_secs_to_wait_for_headers.count() > 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(1s);
+ } 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)
+ //
+ 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;
+ }
+
+ assert(index);
+ index->nChainTx = au_data.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);
assert(m_active_chainstate);
diff --git a/test/functional/rpc_dumptxoutset.py b/test/functional/rpc_dumptxoutset.py
--- a/test/functional/rpc_dumptxoutset.py
+++ b/test/functional/rpc_dumptxoutset.py
@@ -42,7 +42,7 @@
# UTXO snapshot hash should be deterministic based on mocked time.
assert_equal(
digest,
- '05957e146e38153d84e9294999cc24f0dcdb9902c4834b32c79ae8e8985babea')
+ 'a92dc32a15975b3c84bb1e6ac5218ff94194b4ea7d1b9372fb80184a7533a89f')
# Specifying a path to an existing file will fail.
assert_raises_rpc_error(

File Metadata

Mime Type
text/plain
Expires
Sat, Apr 26, 11:58 (2 h, 41 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5573491
Default Alt Text
D11239.id32916.diff (20 KB)

Event Timeline