Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F13711430
D11239.id32916.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
20 KB
Subscribers
None
D11239.id32916.diff
View Options
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
Details
Attached
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)
Attached To
D11239: simplify ChainstateManager::SnapshotBlockhash() return semantics
Event Timeline
Log In to Comment