From b73d3bbd23220857bf17cbb6401275bf58013b72 Mon Sep 17 00:00:00 2001 From: Suhas Daftuar Date: Thu, 4 May 2023 14:08:37 -0400 Subject: [PATCH 01/26] net_processing: Request assumeutxo background chain blocks Add new PeerManagerImpl::TryDownloadingHistoricalBlocks method and use it to request background chain blocks in addition to blocks normally requested by FindNextBlocksToDownload. Co-authored-by: Ryan Ofsky Co-authored-by: James O'Beirne --- src/net_processing.cpp | 91 +++++++++++++++++++++++++++++++++++++++--- src/validation.cpp | 6 +++ src/validation.h | 16 ++++++-- 3 files changed, 105 insertions(+), 8 deletions(-) diff --git a/src/net_processing.cpp b/src/net_processing.cpp index b046b3ac168c7..46759423663d5 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -892,6 +892,38 @@ class PeerManagerImpl final : public PeerManager */ void FindNextBlocksToDownload(const Peer& peer, unsigned int count, std::vector& vBlocks, NodeId& nodeStaller) EXCLUSIVE_LOCKS_REQUIRED(cs_main); + /** Request blocks for the background chainstate, if one is in use. */ + void TryDownloadingHistoricalBlocks(const Peer& peer, unsigned int count, std::vector& vBlocks, const CBlockIndex* from_tip, const CBlockIndex* target_block) EXCLUSIVE_LOCKS_REQUIRED(cs_main); + + /** + * \brief Find next blocks to download from a peer after a starting block. + * + * \param vBlocks Vector of blocks to download which will be appended to. + * \param peer Peer which blocks will be downloaded from. + * \param state Pointer to the state of the peer. + * \param pindexWalk Pointer to the starting block to add to vBlocks. + * \param count Maximum number of blocks to allow in vBlocks. No more + * blocks will be added if it reaches this size. + * \param nWindowEnd Maximum height of blocks to allow in vBlocks. No + * blocks will be added above this height. + * \param activeChain Optional pointer to a chain to compare against. If + * provided, any next blocks which are already contained + * in this chain will not be appended to vBlocks, but + * instead will be used to update the + * state->pindexLastCommonBlock pointer. + * \param nodeStaller Optional pointer to a NodeId variable that will receive + * the ID of another peer that might be causing this peer + * to stall. This is set to the ID of the peer which + * first requested the first in-flight block in the + * download window. It is only set if vBlocks is empty at + * the end of this function call and if increasing + * nWindowEnd by 1 would cause it to be non-empty (which + * indicates the download might be stalled because every + * block in the window is in flight and no other peer is + * trying to download the next block). + */ + void FindNextBlocks(std::vector& vBlocks, const Peer& peer, CNodeState *state, const CBlockIndex *pindexWalk, unsigned int count, int nWindowEnd, const CChain* activeChain=nullptr, NodeId* nodeStaller=nullptr) EXCLUSIVE_LOCKS_REQUIRED(cs_main); + /* Multimap used to preserve insertion order */ typedef std::multimap::iterator>> BlockDownloadMap; BlockDownloadMap mapBlocksInFlight GUARDED_BY(cs_main); @@ -1312,6 +1344,7 @@ void PeerManagerImpl::UpdateBlockAvailability(NodeId nodeid, const uint256 &hash } } +// Logic for calculating which blocks to download from a given peer, given our current tip. void PeerManagerImpl::FindNextBlocksToDownload(const Peer& peer, unsigned int count, std::vector& vBlocks, NodeId& nodeStaller) { if (count == 0) @@ -1341,12 +1374,47 @@ void PeerManagerImpl::FindNextBlocksToDownload(const Peer& peer, unsigned int co if (state->pindexLastCommonBlock == state->pindexBestKnownBlock) return; - std::vector vToFetch; const CBlockIndex *pindexWalk = state->pindexLastCommonBlock; // Never fetch further than the best block we know the peer has, or more than BLOCK_DOWNLOAD_WINDOW + 1 beyond the last // linked block we have in common with this peer. The +1 is so we can detect stalling, namely if we would be able to // download that next block if the window were 1 larger. int nWindowEnd = state->pindexLastCommonBlock->nHeight + BLOCK_DOWNLOAD_WINDOW; + + FindNextBlocks(vBlocks, peer, state, pindexWalk, count, nWindowEnd, &m_chainman.ActiveChain(), &nodeStaller); +} + +void PeerManagerImpl::TryDownloadingHistoricalBlocks(const Peer& peer, unsigned int count, std::vector& vBlocks, const CBlockIndex *from_tip, const CBlockIndex* target_block) +{ + Assert(from_tip); + Assert(target_block); + + if (vBlocks.size() >= count) { + return; + } + + vBlocks.reserve(count); + CNodeState *state = Assert(State(peer.m_id)); + + if (state->pindexBestKnownBlock == nullptr || state->pindexBestKnownBlock->GetAncestor(target_block->nHeight) != target_block) { + // This peer can't provide us the complete series of blocks leading up to the + // assumeutxo snapshot base. + // + // Presumably this peer's chain has less work than our ActiveChain()'s tip, or else we + // will eventually crash when we try to reorg to it. Let other logic + // deal with whether we disconnect this peer. + // + // TODO at some point in the future, we might choose to request what blocks + // this peer does have from the historical chain, despite it not having a + // complete history beneath the snapshot base. + return; + } + + FindNextBlocks(vBlocks, peer, state, from_tip, count, std::min(from_tip->nHeight + BLOCK_DOWNLOAD_WINDOW, target_block->nHeight)); +} + +void PeerManagerImpl::FindNextBlocks(std::vector& vBlocks, const Peer& peer, CNodeState *state, const CBlockIndex *pindexWalk, unsigned int count, int nWindowEnd, const CChain* activeChain, NodeId* nodeStaller) +{ + std::vector vToFetch; int nMaxHeight = std::min(state->pindexBestKnownBlock->nHeight, nWindowEnd + 1); NodeId waitingfor = -1; while (pindexWalk->nHeight < nMaxHeight) { @@ -1374,8 +1442,8 @@ void PeerManagerImpl::FindNextBlocksToDownload(const Peer& peer, unsigned int co // We wouldn't download this block or its descendants from this peer. return; } - if (pindex->nStatus & BLOCK_HAVE_DATA || m_chainman.ActiveChain().Contains(pindex)) { - if (pindex->HaveTxsDownloaded()) + if (pindex->nStatus & BLOCK_HAVE_DATA || (activeChain && activeChain->Contains(pindex))) { + if (activeChain && pindex->HaveTxsDownloaded()) state->pindexLastCommonBlock = pindex; } else if (!IsBlockRequested(pindex->GetBlockHash())) { // The block is not already downloaded, and not yet in flight. @@ -1383,7 +1451,7 @@ void PeerManagerImpl::FindNextBlocksToDownload(const Peer& peer, unsigned int co // We reached the end of the window. if (vBlocks.size() == 0 && waitingfor != peer.m_id) { // We aren't able to fetch anything, but we would be if the download window was one larger. - nodeStaller = waitingfor; + if (nodeStaller) *nodeStaller = waitingfor; } return; } @@ -5847,7 +5915,20 @@ bool PeerManagerImpl::SendMessages(CNode* pto) if (CanServeBlocks(*peer) && ((sync_blocks_and_headers_from_peer && !IsLimitedPeer(*peer)) || !m_chainman.IsInitialBlockDownload()) && state.vBlocksInFlight.size() < MAX_BLOCKS_IN_TRANSIT_PER_PEER) { std::vector vToDownload; NodeId staller = -1; - FindNextBlocksToDownload(*peer, MAX_BLOCKS_IN_TRANSIT_PER_PEER - state.vBlocksInFlight.size(), vToDownload, staller); + auto get_inflight_budget = [&state]() { + return std::max(0, MAX_BLOCKS_IN_TRANSIT_PER_PEER - static_cast(state.vBlocksInFlight.size())); + }; + + // If a snapshot chainstate is in use, we want to find its next blocks + // before the background chainstate to prioritize getting to network tip. + FindNextBlocksToDownload(*peer, get_inflight_budget(), vToDownload, staller); + if (m_chainman.BackgroundSyncInProgress() && !IsLimitedPeer(*peer)) { + TryDownloadingHistoricalBlocks( + *peer, + get_inflight_budget(), + vToDownload, m_chainman.GetBackgroundSyncTip(), + Assert(m_chainman.GetSnapshotBaseBlock())); + } for (const CBlockIndex *pindex : vToDownload) { uint32_t nFetchFlags = GetFetchFlags(*peer); vGetData.push_back(CInv(MSG_BLOCK | nFetchFlags, pindex->GetBlockHash())); diff --git a/src/validation.cpp b/src/validation.cpp index 357b4d422d23a..a12f121dc329a 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -4148,6 +4148,12 @@ bool ChainstateManager::ProcessNewBlock(const std::shared_ptr& blo return error("%s: ActivateBestChain failed (%s)", __func__, state.ToString()); } + Chainstate* bg_chain{WITH_LOCK(cs_main, return BackgroundSyncInProgress() ? m_ibd_chainstate.get() : nullptr)}; + BlockValidationState bg_state; + if (bg_chain && !bg_chain->ActivateBestChain(bg_state, block)) { + return error("%s: [background] ActivateBestChain failed (%s)", __func__, bg_state.ToString()); + } + return true; } diff --git a/src/validation.h b/src/validation.h index 3f0a2312b52ee..319e40447b48d 100644 --- a/src/validation.h +++ b/src/validation.h @@ -881,9 +881,6 @@ class ChainstateManager /** Most recent headers presync progress update, for rate-limiting. */ std::chrono::time_point m_last_presync_update GUARDED_BY(::cs_main) {}; - //! Returns nullptr if no snapshot has been loaded. - const CBlockIndex* GetSnapshotBaseBlock() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main); - //! Return the height of the base block of the snapshot in use, if one exists, else //! nullopt. std::optional GetSnapshotBaseHeight() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main); @@ -1034,12 +1031,25 @@ class ChainstateManager //! Otherwise, revert to using the ibd chainstate and shutdown. SnapshotCompletionResult MaybeCompleteSnapshotValidation() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + //! Returns nullptr if no snapshot has been loaded. + const CBlockIndex* GetSnapshotBaseBlock() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + //! The most-work chain. Chainstate& ActiveChainstate() const; CChain& ActiveChain() const EXCLUSIVE_LOCKS_REQUIRED(GetMutex()) { return ActiveChainstate().m_chain; } int ActiveHeight() const EXCLUSIVE_LOCKS_REQUIRED(GetMutex()) { return ActiveChain().Height(); } CBlockIndex* ActiveTip() const EXCLUSIVE_LOCKS_REQUIRED(GetMutex()) { return ActiveChain().Tip(); } + //! The state of a background sync (for net processing) + bool BackgroundSyncInProgress() const EXCLUSIVE_LOCKS_REQUIRED(GetMutex()) { + return IsUsable(m_snapshot_chainstate.get()) && IsUsable(m_ibd_chainstate.get()); + } + + //! The tip of the background sync chain + const CBlockIndex* GetBackgroundSyncTip() const EXCLUSIVE_LOCKS_REQUIRED(GetMutex()) { + return BackgroundSyncInProgress() ? m_ibd_chainstate->m_chain.Tip() : nullptr; + } + node::BlockMap& BlockIndex() EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { AssertLockHeld(::cs_main); From c93ef43e4fd4fbc1263cdc9e98ae5856830fe89e Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Wed, 24 May 2023 21:10:12 -0400 Subject: [PATCH 02/26] bugfix: correct is_snapshot_cs in VerifyDB --- src/validation.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/validation.cpp b/src/validation.cpp index a12f121dc329a..a57a66478690d 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -4281,7 +4281,7 @@ VerifyDBResult CVerifyDB::VerifyDB( bool skipped_l3_checks{false}; LogPrintf("Verification progress: 0%%\n"); - const bool is_snapshot_cs{!chainstate.m_from_snapshot_blockhash}; + const bool is_snapshot_cs{chainstate.m_from_snapshot_blockhash}; for (pindex = chainstate.m_chain.Tip(); pindex && pindex->pprev; pindex = pindex->pprev) { const int percentageDone = std::max(1, std::min(99, (int)(((double)(chainstate.m_chain.Height() - pindex->nHeight)) / (double)nCheckDepth * (nCheckLevel >= 4 ? 50 : 100)))); From c711ca186f8d8a28810be0beedcb615ddcf93163 Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Wed, 3 May 2023 15:39:51 -0400 Subject: [PATCH 03/26] assumeutxo: remove snapshot during -reindex{-chainstate} Removing a snapshot chainstate from disk (and memory) is consistent with existing reindex operations. --- src/node/chainstate.cpp | 9 ++++++++- src/validation.cpp | 32 +++++++++++++++++++++++++++----- src/validation.h | 7 ++++--- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/node/chainstate.cpp b/src/node/chainstate.cpp index ae1457a87ea27..16ca1d9156ba8 100644 --- a/src/node/chainstate.cpp +++ b/src/node/chainstate.cpp @@ -185,7 +185,14 @@ ChainstateLoadResult LoadChainstate(ChainstateManager& chainman, const CacheSize chainman.InitializeChainstate(options.mempool); // Load a chain created from a UTXO snapshot, if any exist. - chainman.DetectSnapshotChainstate(options.mempool); + bool has_snapshot = chainman.DetectSnapshotChainstate(options.mempool); + + if (has_snapshot && (options.reindex || options.reindex_chainstate)) { + LogPrintf("[snapshot] deleting snapshot chainstate due to reindexing\n"); + if (!chainman.DeleteSnapshotChainstate()) { + return {ChainstateLoadStatus::FAILURE_FATAL, Untranslated("Couldn't remove snapshot chainstate.")}; + } + } auto [init_status, init_error] = CompleteChainstateInitialization(chainman, cache_sizes, options); if (init_status != ChainstateLoadStatus::SUCCESS) { diff --git a/src/validation.cpp b/src/validation.cpp index a57a66478690d..240543e6ebf59 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -5111,7 +5111,7 @@ const AssumeutxoData* ExpectedAssumeutxo( return nullptr; } -static bool DeleteCoinsDBFromDisk(const fs::path db_path, bool is_snapshot) +[[nodiscard]] static bool DeleteCoinsDBFromDisk(const fs::path db_path, bool is_snapshot) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { AssertLockHeld(::cs_main); @@ -5750,15 +5750,20 @@ bool IsBIP30Unspendable(const CBlockIndex& block_index) (block_index.nHeight==91812 && block_index.GetBlockHash() == uint256S("0x00000000000af0aed4792b1acee3d966af36cf5def14935db8de83d6f9306f2f")); } -util::Result Chainstate::InvalidateCoinsDBOnDisk() +static fs::path GetSnapshotCoinsDBPath(Chainstate& cs) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { AssertLockHeld(::cs_main); // Should never be called on a non-snapshot chainstate. - assert(m_from_snapshot_blockhash); - auto storage_path_maybe = this->CoinsDB().StoragePath(); + assert(cs.m_from_snapshot_blockhash); + auto storage_path_maybe = cs.CoinsDB().StoragePath(); // Should never be called with a non-existent storage path. assert(storage_path_maybe); - fs::path snapshot_datadir = *storage_path_maybe; + return *storage_path_maybe; +} + +util::Result Chainstate::InvalidateCoinsDBOnDisk() +{ + fs::path snapshot_datadir = GetSnapshotCoinsDBPath(*this); // Coins views no longer usable. m_coins_views.reset(); @@ -5789,6 +5794,23 @@ util::Result Chainstate::InvalidateCoinsDBOnDisk() return {}; } +bool ChainstateManager::DeleteSnapshotChainstate() +{ + AssertLockHeld(::cs_main); + Assert(m_snapshot_chainstate); + Assert(m_ibd_chainstate); + + fs::path snapshot_datadir = GetSnapshotCoinsDBPath(*m_snapshot_chainstate); + if (!DeleteCoinsDBFromDisk(snapshot_datadir, /*is_snapshot=*/ true)) { + LogPrintf("Deletion of %s failed. Please remove it manually to continue reindexing.\n", + fs::PathToString(snapshot_datadir)); + return false; + } + m_active_chainstate = m_ibd_chainstate.get(); + m_snapshot_chainstate.reset(); + return true; +} + const CBlockIndex* ChainstateManager::GetSnapshotBaseBlock() const { return m_active_chainstate ? m_active_chainstate->SnapshotBase() : nullptr; diff --git a/src/validation.h b/src/validation.h index 319e40447b48d..4e9e91c299ad9 100644 --- a/src/validation.h +++ b/src/validation.h @@ -848,9 +848,6 @@ class ChainstateManager //! Points to either the ibd or snapshot chainstate; indicates our //! most-work chain. //! - //! Once this pointer is set to a corresponding chainstate, it will not - //! be reset until init.cpp:Shutdown(). - //! //! This is especially important when, e.g., calling ActivateBestChain() //! on all chainstates because we are not able to hold ::cs_main going into //! that call. @@ -1203,6 +1200,10 @@ class ChainstateManager void ResetChainstates() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + //! Remove the snapshot-based chainstate and all on-disk artifacts. + //! Used when reindex{-chainstate} is called during snapshot use. + [[nodiscard]] bool DeleteSnapshotChainstate() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + //! Switch the active chainstate to one based on a UTXO snapshot that was loaded //! previously. Chainstate& ActivateExistingSnapshot(CTxMemPool* mempool, uint256 base_blockhash) From 434495a8c1496ca23fe35b84499f3daf668d76b8 Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Thu, 25 May 2023 13:05:27 -0400 Subject: [PATCH 04/26] chainparams: add blockhash to AssumeutxoData This allows us to reference assumeutxo configuration by blockhash as well as height; this is helpful in future changes when we want to reference assumeutxo configurations before the block index is loaded. --- src/kernel/chainparams.cpp | 18 ++++++------- src/kernel/chainparams.h | 26 +++++++++++++------ .../validation_chainstatemanager_tests.cpp | 4 +-- src/test/validation_tests.cpp | 10 +++---- src/util/vector.h | 13 ++++++++++ src/validation.cpp | 18 +++---------- src/validation.h | 9 ------- 7 files changed, 49 insertions(+), 49 deletions(-) diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp index 7e69c097a6af5..ca418fc6abc07 100644 --- a/src/kernel/chainparams.cpp +++ b/src/kernel/chainparams.cpp @@ -172,8 +172,8 @@ class CMainParams : public CChainParams { } }; - m_assumeutxo_data = MapAssumeutxo{ - // TODO to be specified in a future patch. + m_assumeutxo_data = { + // TODO to be specified in a future patch. }; chainTxData = ChainTxData{ @@ -266,7 +266,7 @@ class CTestNetParams : public CChainParams { } }; - m_assumeutxo_data = MapAssumeutxo{ + m_assumeutxo_data = { // TODO to be specified in a future patch. }; @@ -477,14 +477,12 @@ class CRegTestParams : public CChainParams } }; - m_assumeutxo_data = MapAssumeutxo{ - { - 110, - {AssumeutxoHash{uint256S("0x1ebbf5850204c0bdb15bf030f47c7fe91d45c44c712697e4509ba67adb01c618")}, 110}, - }, + m_assumeutxo_data = { { - 200, - {AssumeutxoHash{uint256S("0x51c8d11d8b5c1de51543c579736e786aa2736206d1e11e627568029ce092cf62")}, 200}, + .height = 110, + .hash_serialized = AssumeutxoHash{uint256S("0x1ebbf5850204c0bdb15bf030f47c7fe91d45c44c712697e4509ba67adb01c618")}, + .nChainTx = 110, + .blockhash = uint256S("0x696e92821f65549c7ee134edceeeeaaa4105647a3c4fd9f298c0aec0ab50425c") }, }; diff --git a/src/kernel/chainparams.h b/src/kernel/chainparams.h index ec1697493c96e..7a5539bc71c54 100644 --- a/src/kernel/chainparams.h +++ b/src/kernel/chainparams.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -44,17 +45,21 @@ struct AssumeutxoHash : public BaseHash { * as valid. */ struct AssumeutxoData { + int height; + //! The expected hash of the deserialized UTXO set. - const AssumeutxoHash hash_serialized; + AssumeutxoHash hash_serialized; //! Used to populate the nChainTx value, which is used during BlockManager::LoadBlockIndex(). //! //! We need to hardcode the value here because this is computed cumulatively using block data, //! which we do not necessarily have at the time of snapshot load. - const unsigned int nChainTx; -}; + unsigned int nChainTx; -using MapAssumeutxo = std::map; + //! The hash of the base block for this snapshot. Used to refer to assumeutxo data + //! prior to having a loaded blockindex. + uint256 blockhash; +}; /** * Holds various statistics on transactions within a chain. Used to estimate @@ -114,9 +119,14 @@ class CChainParams const std::vector& FixedSeeds() const { return vFixedSeeds; } const CCheckpointData& Checkpoints() const { return checkpointData; } - //! Get allowed assumeutxo configuration. - //! @see ChainstateManager - const MapAssumeutxo& Assumeutxo() const { return m_assumeutxo_data; } + std::optional AssumeutxoForHeight(int height) const + { + return FindFirst(m_assumeutxo_data, [&](const auto& d) { return d.height == height; }); + } + std::optional AssumeutxoForBlockhash(const uint256& blockhash) const + { + return FindFirst(m_assumeutxo_data, [&](const auto& d) { return d.blockhash == blockhash; }); + } const ChainTxData& TxData() const { return chainTxData; } @@ -169,7 +179,7 @@ class CChainParams bool fDefaultConsistencyChecks; bool m_is_mockable_chain; CCheckpointData checkpointData; - MapAssumeutxo m_assumeutxo_data; + std::vector m_assumeutxo_data; ChainTxData chainTxData; }; diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp index 7b7be4be9eeb0..74e577c17cbc2 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -289,10 +289,10 @@ struct SnapshotTestSetup : TestChain100Setup { BOOST_CHECK(!chainman.ActiveChain().Genesis()->IsAssumedValid()); } - const AssumeutxoData& au_data = *ExpectedAssumeutxo(snapshot_height, ::Params()); + const auto& au_data = ::Params().AssumeutxoForHeight(snapshot_height); const CBlockIndex* tip = WITH_LOCK(chainman.GetMutex(), return chainman.ActiveTip()); - BOOST_CHECK_EQUAL(tip->nChainTx, au_data.nChainTx); + BOOST_CHECK_EQUAL(tip->nChainTx, au_data->nChainTx); // To be checked against later when we try loading a subsequent snapshot. uint256 loaded_snapshot_blockhash{*chainman.SnapshotBlockhash()}; diff --git a/src/test/validation_tests.cpp b/src/test/validation_tests.cpp index d00f2ff4d1cac..d34d98c219a83 100644 --- a/src/test/validation_tests.cpp +++ b/src/test/validation_tests.cpp @@ -132,17 +132,17 @@ BOOST_AUTO_TEST_CASE(test_assumeutxo) std::vector bad_heights{0, 100, 111, 115, 209, 211}; for (auto empty : bad_heights) { - const auto out = ExpectedAssumeutxo(empty, *params); + const auto out = params->AssumeutxoForHeight(empty); BOOST_CHECK(!out); } - const auto out110 = *ExpectedAssumeutxo(110, *params); + const auto out110 = *params->AssumeutxoForHeight(110); BOOST_CHECK_EQUAL(out110.hash_serialized.ToString(), "1ebbf5850204c0bdb15bf030f47c7fe91d45c44c712697e4509ba67adb01c618"); BOOST_CHECK_EQUAL(out110.nChainTx, 110U); - const auto out210 = *ExpectedAssumeutxo(200, *params); - BOOST_CHECK_EQUAL(out210.hash_serialized.ToString(), "51c8d11d8b5c1de51543c579736e786aa2736206d1e11e627568029ce092cf62"); - BOOST_CHECK_EQUAL(out210.nChainTx, 200U); + const auto out110_2 = *params->AssumeutxoForBlockhash(uint256S("0x696e92821f65549c7ee134edceeeeaaa4105647a3c4fd9f298c0aec0ab50425c")); + BOOST_CHECK_EQUAL(out110_2.hash_serialized.ToString(), "1ebbf5850204c0bdb15bf030f47c7fe91d45c44c712697e4509ba67adb01c618"); + BOOST_CHECK_EQUAL(out110_2.nChainTx, 110U); } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/util/vector.h b/src/util/vector.h index 40ff73c293c17..1513562f1bee7 100644 --- a/src/util/vector.h +++ b/src/util/vector.h @@ -5,7 +5,9 @@ #ifndef BITCOIN_UTIL_VECTOR_H #define BITCOIN_UTIL_VECTOR_H +#include #include +#include #include #include #include @@ -67,4 +69,15 @@ inline void ClearShrink(V& v) noexcept V{}.swap(v); } +template +inline std::optional FindFirst(const std::vector& vec, const L fnc) +{ + for (const auto& el : vec) { + if (fnc(el)) { + return el; + } + } + return std::nullopt; +} + #endif // BITCOIN_UTIL_VECTOR_H diff --git a/src/validation.cpp b/src/validation.cpp index 240543e6ebf59..8afd37726547e 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -5099,18 +5099,6 @@ Chainstate& ChainstateManager::InitializeChainstate(CTxMemPool* mempool) return *m_active_chainstate; } -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; -} - [[nodiscard]] static bool DeleteCoinsDBFromDisk(const fs::path db_path, bool is_snapshot) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { @@ -5295,7 +5283,7 @@ bool ChainstateManager::PopulateAndValidateSnapshot( CBlockIndex* snapshot_start_block = WITH_LOCK(::cs_main, return m_blockman.LookupBlockIndex(base_blockhash)); if (!snapshot_start_block) { - // Needed for ComputeUTXOStats and ExpectedAssumeutxo to determine the + // Needed for ComputeUTXOStats to determine the // height and to avoid a crash when base_blockhash.IsNull() LogPrintf("[snapshot] Did not find snapshot start blockheader %s\n", base_blockhash.ToString()); @@ -5303,7 +5291,7 @@ bool ChainstateManager::PopulateAndValidateSnapshot( } int base_height = snapshot_start_block->nHeight; - auto maybe_au_data = ExpectedAssumeutxo(base_height, GetParams()); + const auto& maybe_au_data = GetParams().AssumeutxoForHeight(base_height); if (!maybe_au_data) { LogPrintf("[snapshot] assumeutxo height in snapshot metadata not recognized " @@ -5572,7 +5560,7 @@ SnapshotCompletionResult ChainstateManager::MaybeCompleteSnapshotValidation() CCoinsViewDB& ibd_coins_db = m_ibd_chainstate->CoinsDB(); m_ibd_chainstate->ForceFlushStateToDisk(); - auto maybe_au_data = ExpectedAssumeutxo(curr_height, m_options.chainparams); + const auto& maybe_au_data = m_options.chainparams.AssumeutxoForHeight(curr_height); if (!maybe_au_data) { LogPrintf("[snapshot] assumeutxo data not found for height " "(%d) - refusing to validate snapshot\n", curr_height); diff --git a/src/validation.h b/src/validation.h index 4e9e91c299ad9..c2434264d6731 100644 --- a/src/validation.h +++ b/src/validation.h @@ -1242,15 +1242,6 @@ bool DeploymentEnabled(const ChainstateManager& chainman, DEP dep) return DeploymentEnabled(chainman.GetConsensus(), dep); } -/** - * Return the expected assumeutxo value for a given height, if one exists. - * - * @param[in] height Get the assumeutxo value for this height. - * - * @returns empty if no assumeutxo configuration exists for the given height. - */ -const AssumeutxoData* ExpectedAssumeutxo(const int height, const CChainParams& params); - /** Identifies blocks that overwrote an existing coinbase output in the UTXO set (see BIP30) */ bool IsBIP30Repeat(const CBlockIndex& block_index); From 9f2318c76cc6986d48e13831cf5bd8dab194fdf4 Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Mon, 1 May 2023 17:39:28 -0400 Subject: [PATCH 05/26] validation: MaybeRebalanceCaches when chain leaves IBD Check to see if we need to rebalance caches across chainstates when a chain leaves IBD. --- src/validation.cpp | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/validation.cpp b/src/validation.cpp index 8afd37726547e..e2091a2c9a85e 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -3192,6 +3192,7 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr< CBlockIndex *pindexMostWork = nullptr; CBlockIndex *pindexNewTip = nullptr; + bool exited_ibd{false}; do { // Block until the validation queue drains. This should largely // never happen in normal operation, however may happen during @@ -3205,6 +3206,7 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr< LOCK(cs_main); // Lock transaction pool for at least as long as it takes for connectTrace to be consumed LOCK(MempoolMutex()); + const bool was_in_ibd = m_chainman.IsInitialBlockDownload(); CBlockIndex* starting_tip = m_chain.Tip(); bool blocks_connected = false; do { @@ -3252,16 +3254,21 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr< if (!blocks_connected) return true; const CBlockIndex* pindexFork = m_chain.FindFork(starting_tip); - bool fInitialDownload = m_chainman.IsInitialBlockDownload(); + bool still_in_ibd = m_chainman.IsInitialBlockDownload(); + + if (was_in_ibd && !still_in_ibd) { + // Active chainstate has exited IBD. + exited_ibd = true; + } // Notify external listeners about the new tip. // Enqueue while holding cs_main to ensure that UpdatedBlockTip is called in the order in which blocks are connected if (pindexFork != pindexNewTip) { // Notify ValidationInterface subscribers - GetMainSignals().UpdatedBlockTip(pindexNewTip, pindexFork, fInitialDownload); + GetMainSignals().UpdatedBlockTip(pindexNewTip, pindexFork, still_in_ibd); // Always notify the UI if a new block tip was connected - if (kernel::IsInterrupted(m_chainman.GetNotifications().blockTip(GetSynchronizationState(fInitialDownload), *pindexNewTip))) { + if (kernel::IsInterrupted(m_chainman.GetNotifications().blockTip(GetSynchronizationState(still_in_ibd), *pindexNewTip))) { // Just breaking and returning success for now. This could // be changed to bubble up the kernel::Interrupted value to // the caller so the caller could distinguish between @@ -3272,6 +3279,13 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr< } // When we reach this point, we switched to a new tip (stored in pindexNewTip). + if (exited_ibd) { + // If a background chainstate is in use, we may need to rebalance our + // allocation of caches once a chainstate exits initial block download. + LOCK(::cs_main); + m_chainman.MaybeRebalanceCaches(); + } + if (WITH_LOCK(::cs_main, return m_disabled)) { // Background chainstate has reached the snapshot base block, so exit. break; From c6af23c5179cc383f8e6c275373af8d11e6a989f Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Thu, 10 Nov 2022 12:03:39 -0500 Subject: [PATCH 06/26] validation: add ChainstateRole --- src/kernel/chain.cpp | 11 +++++++++++ src/kernel/chain.h | 20 ++++++++++++++++++++ src/validation.cpp | 10 ++++++++++ src/validation.h | 7 +++++++ 4 files changed, 48 insertions(+) diff --git a/src/kernel/chain.cpp b/src/kernel/chain.cpp index 1c877866d0a28..318c956b386dc 100644 --- a/src/kernel/chain.cpp +++ b/src/kernel/chain.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -25,3 +26,13 @@ interfaces::BlockInfo MakeBlockInfo(const CBlockIndex* index, const CBlock* data return info; } } // namespace kernel + +std::ostream& operator<<(std::ostream& os, const ChainstateRole& role) { + switch(role) { + case ChainstateRole::NORMAL: os << "normal"; break; + case ChainstateRole::ASSUMEDVALID: os << "assumedvalid"; break; + case ChainstateRole::BACKGROUND: os << "background"; break; + default: os.setstate(std::ios_base::failbit); + } + return os; +} diff --git a/src/kernel/chain.h b/src/kernel/chain.h index f0750f82663f7..feba24a557e69 100644 --- a/src/kernel/chain.h +++ b/src/kernel/chain.h @@ -5,6 +5,8 @@ #ifndef BITCOIN_KERNEL_CHAIN_H #define BITCOIN_KERNEL_CHAIN_H +#include + class CBlock; class CBlockIndex; namespace interfaces { @@ -14,6 +16,24 @@ struct BlockInfo; namespace kernel { //! Return data from block index. interfaces::BlockInfo MakeBlockInfo(const CBlockIndex* block_index, const CBlock* data = nullptr); + } // namespace kernel +//! This enum describes the various roles a specific Chainstate instance can take. +//! Other parts of the system sometimes need to vary in behavior depending on the +//! existence of a background validation chainstate, e.g. when building indexes. +enum class ChainstateRole { + // Single chainstate in use, "normal" IBD mode. + NORMAL, + + // Doing IBD-style validation in the background. Implies use of an assumed-valid + // chainstate. + BACKGROUND, + + // Active assumed-valid chainstate. Implies use of a background IBD chainstate. + ASSUMEDVALID, +}; + +std::ostream& operator<<(std::ostream& os, const ChainstateRole& role); + #endif // BITCOIN_KERNEL_CHAIN_H diff --git a/src/validation.cpp b/src/validation.cpp index e2091a2c9a85e..9a543d3a389a3 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -5813,6 +5813,16 @@ bool ChainstateManager::DeleteSnapshotChainstate() return true; } +ChainstateRole Chainstate::GetRole() const +{ + if (m_chainman.GetAll().size() <= 1) { + return ChainstateRole::NORMAL; + } + return (this != &m_chainman.ActiveChainstate()) ? + ChainstateRole::BACKGROUND : + ChainstateRole::ASSUMEDVALID; +} + const CBlockIndex* ChainstateManager::GetSnapshotBaseBlock() const { return m_active_chainstate ? m_active_chainstate->SnapshotBase() : nullptr; diff --git a/src/validation.h b/src/validation.h index c2434264d6731..38f57ed1b597a 100644 --- a/src/validation.h +++ b/src/validation.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -511,6 +512,12 @@ class Chainstate ChainstateManager& chainman, std::optional from_snapshot_blockhash = std::nullopt); + //! Return the current role of the chainstate. See `ChainstateManager` + //! documentation for a description of the different types of chainstates. + //! + //! @sa ChainstateRole + ChainstateRole GetRole() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + /** * Initialize the CoinsViews UTXO set database management data structures. The in-memory * cache is initialized separately. From 1e59acdf17309f567c370885f0cf02605e2baa58 Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Thu, 24 Aug 2023 16:51:16 -0400 Subject: [PATCH 07/26] validation: only call UpdatedBlockTip for active chainstate This notification isn't needed for background chainstates. `kernel::Notifications::blockTip` are also skipped. --- src/validation.cpp | 2 +- src/validationinterface.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/validation.cpp b/src/validation.cpp index 9a543d3a389a3..4873eb964ca3e 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -3263,7 +3263,7 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr< // Notify external listeners about the new tip. // Enqueue while holding cs_main to ensure that UpdatedBlockTip is called in the order in which blocks are connected - if (pindexFork != pindexNewTip) { + if (this == &m_chainman.ActiveChainstate() && pindexFork != pindexNewTip) { // Notify ValidationInterface subscribers GetMainSignals().UpdatedBlockTip(pindexNewTip, pindexFork, still_in_ibd); diff --git a/src/validationinterface.h b/src/validationinterface.h index 8c20cc8ffbf73..5bdd7e0123892 100644 --- a/src/validationinterface.h +++ b/src/validationinterface.h @@ -87,7 +87,7 @@ class CValidationInterface { * but may not be called on every intermediate tip. If the latter behavior is desired, * subscribe to BlockConnected() instead. * - * Called on a background thread. + * Called on a background thread. Only called for the active chainstate. */ virtual void UpdatedBlockTip(const CBlockIndex *pindexNew, const CBlockIndex *pindexFork, bool fInitialDownload) {} /** From 4d8f4dcb450d31e4847804e62bf91545b949fa14 Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Mon, 23 Sep 2019 13:54:21 -0400 Subject: [PATCH 08/26] validation: pass ChainstateRole for validationinterface calls This allows consumers to decide how to handle events from background or assumedvalid chainstates. --- src/bench/wallet_create_tx.cpp | 2 +- src/index/base.cpp | 4 ++-- src/index/base.h | 4 ++-- src/interfaces/chain.h | 5 +++-- src/net_processing.cpp | 8 ++++++-- src/node/interfaces.cpp | 8 +++++--- src/test/coinstatsindex_tests.cpp | 2 +- src/test/util/validation.cpp | 8 ++++++-- src/test/util/validation.h | 6 +++++- src/test/validation_block_tests.cpp | 2 +- src/test/validationinterface_tests.cpp | 1 + src/validation.cpp | 5 +++-- src/validationinterface.cpp | 13 +++++++------ src/validationinterface.h | 17 ++++++++++------- src/wallet/test/fuzz/notifications.cpp | 5 +++-- src/wallet/wallet.cpp | 9 +++++---- src/wallet/wallet.h | 4 ++-- src/zmq/zmqnotificationinterface.cpp | 3 ++- src/zmq/zmqnotificationinterface.h | 2 +- 19 files changed, 66 insertions(+), 42 deletions(-) diff --git a/src/bench/wallet_create_tx.cpp b/src/bench/wallet_create_tx.cpp index 5e5bc76fd21c5..160534b63ca41 100644 --- a/src/bench/wallet_create_tx.cpp +++ b/src/bench/wallet_create_tx.cpp @@ -70,7 +70,7 @@ void generateFakeBlock(const CChainParams& params, // notify wallet const auto& pindex = WITH_LOCK(::cs_main, return context.chainman->ActiveChain().Tip()); - wallet.blockConnected(kernel::MakeBlockInfo(pindex, &block)); + wallet.blockConnected(ChainstateRole::NORMAL, kernel::MakeBlockInfo(pindex, &block)); } struct PreSelectInputs { diff --git a/src/index/base.cpp b/src/index/base.cpp index f18205a76ffd0..98a8bad102db3 100644 --- a/src/index/base.cpp +++ b/src/index/base.cpp @@ -250,7 +250,7 @@ bool BaseIndex::Rewind(const CBlockIndex* current_tip, const CBlockIndex* new_ti return true; } -void BaseIndex::BlockConnected(const std::shared_ptr& block, const CBlockIndex* pindex) +void BaseIndex::BlockConnected(ChainstateRole role, const std::shared_ptr& block, const CBlockIndex* pindex) { if (!m_synced) { return; @@ -296,7 +296,7 @@ void BaseIndex::BlockConnected(const std::shared_ptr& block, const } } -void BaseIndex::ChainStateFlushed(const CBlockLocator& locator) +void BaseIndex::ChainStateFlushed(ChainstateRole role, const CBlockLocator& locator) { if (!m_synced) { return; diff --git a/src/index/base.h b/src/index/base.h index 9b2a41dc92b13..b93103eb36606 100644 --- a/src/index/base.h +++ b/src/index/base.h @@ -102,9 +102,9 @@ class BaseIndex : public CValidationInterface Chainstate* m_chainstate{nullptr}; const std::string m_name; - void BlockConnected(const std::shared_ptr& block, const CBlockIndex* pindex) override; + void BlockConnected(ChainstateRole role, const std::shared_ptr& block, const CBlockIndex* pindex) override; - void ChainStateFlushed(const CBlockLocator& locator) override; + void ChainStateFlushed(ChainstateRole role, const CBlockLocator& locator) override; /// Initialize internal state from the database and block index. [[nodiscard]] virtual bool CustomInit(const std::optional& block) { return true; } diff --git a/src/interfaces/chain.h b/src/interfaces/chain.h index b5243725ad0e7..dea868f844da4 100644 --- a/src/interfaces/chain.h +++ b/src/interfaces/chain.h @@ -27,6 +27,7 @@ class Coin; class uint256; enum class MemPoolRemovalReason; enum class RBFTransactionState; +enum class ChainstateRole; struct bilingual_str; struct CBlockLocator; struct FeeCalculation; @@ -310,10 +311,10 @@ class Chain virtual ~Notifications() {} virtual void transactionAddedToMempool(const CTransactionRef& tx) {} virtual void transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRemovalReason reason) {} - virtual void blockConnected(const BlockInfo& block) {} + virtual void blockConnected(ChainstateRole role, const BlockInfo& block) {} virtual void blockDisconnected(const BlockInfo& block) {} virtual void updatedBlockTip() {} - virtual void chainStateFlushed(const CBlockLocator& locator) {} + virtual void chainStateFlushed(ChainstateRole role, const CBlockLocator& locator) {} }; //! Register handler for notifications. diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 46759423663d5..12dca182c3800 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -483,7 +484,7 @@ class PeerManagerImpl final : public PeerManager CTxMemPool& pool, Options opts); /** Overridden from CValidationInterface. */ - void BlockConnected(const std::shared_ptr& pblock, const CBlockIndex* pindexConnected) override + void BlockConnected(ChainstateRole role, const std::shared_ptr& pblock, const CBlockIndex* pindexConnected) override EXCLUSIVE_LOCKS_REQUIRED(!m_recent_confirmed_transactions_mutex); void BlockDisconnected(const std::shared_ptr &block, const CBlockIndex* pindex) override EXCLUSIVE_LOCKS_REQUIRED(!m_recent_confirmed_transactions_mutex); @@ -1911,7 +1912,10 @@ void PeerManagerImpl::StartScheduledTasks(CScheduler& scheduler) * announcements for them. Also save the time of the last tip update and * possibly reduce dynamic block stalling timeout. */ -void PeerManagerImpl::BlockConnected(const std::shared_ptr& pblock, const CBlockIndex* pindex) +void PeerManagerImpl::BlockConnected( + ChainstateRole role, + const std::shared_ptr& pblock, + const CBlockIndex* pindex) { m_orphanage.EraseForBlock(*pblock); m_last_tip_update = GetTime(); diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index e0c40036d90d2..4baa0da67cd52 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -434,9 +434,9 @@ class NotificationsProxy : public CValidationInterface { m_notifications->transactionRemovedFromMempool(tx, reason); } - void BlockConnected(const std::shared_ptr& block, const CBlockIndex* index) override + void BlockConnected(ChainstateRole role, const std::shared_ptr& block, const CBlockIndex* index) override { - m_notifications->blockConnected(kernel::MakeBlockInfo(index, block.get())); + m_notifications->blockConnected(role, kernel::MakeBlockInfo(index, block.get())); } void BlockDisconnected(const std::shared_ptr& block, const CBlockIndex* index) override { @@ -446,7 +446,9 @@ class NotificationsProxy : public CValidationInterface { m_notifications->updatedBlockTip(); } - void ChainStateFlushed(const CBlockLocator& locator) override { m_notifications->chainStateFlushed(locator); } + void ChainStateFlushed(ChainstateRole role, const CBlockLocator& locator) override { + m_notifications->chainStateFlushed(role, locator); + } std::shared_ptr m_notifications; }; diff --git a/src/test/coinstatsindex_tests.cpp b/src/test/coinstatsindex_tests.cpp index 787a196a0ca42..50f3f7d833682 100644 --- a/src/test/coinstatsindex_tests.cpp +++ b/src/test/coinstatsindex_tests.cpp @@ -105,7 +105,7 @@ BOOST_FIXTURE_TEST_CASE(coinstatsindex_unclean_shutdown, TestChain100Setup) // Send block connected notification, then stop the index without // sending a chainstate flushed notification. Prior to #24138, this // would cause the index to be corrupted and fail to reload. - ValidationInterfaceTest::BlockConnected(index, new_block, new_block_index); + ValidationInterfaceTest::BlockConnected(ChainstateRole::NORMAL, index, new_block, new_block_index); index.Stop(); } diff --git a/src/test/util/validation.cpp b/src/test/util/validation.cpp index 2d5562ae66c11..bcd6a7a7dc330 100644 --- a/src/test/util/validation.cpp +++ b/src/test/util/validation.cpp @@ -22,7 +22,11 @@ void TestChainstateManager::JumpOutOfIbd() Assert(!IsInitialBlockDownload()); } -void ValidationInterfaceTest::BlockConnected(CValidationInterface& obj, const std::shared_ptr& block, const CBlockIndex* pindex) +void ValidationInterfaceTest::BlockConnected( + ChainstateRole role, + CValidationInterface& obj, + const std::shared_ptr& block, + const CBlockIndex* pindex) { - obj.BlockConnected(block, pindex); + obj.BlockConnected(role, block, pindex); } diff --git a/src/test/util/validation.h b/src/test/util/validation.h index 64654f3fb6137..45ef773409a14 100644 --- a/src/test/util/validation.h +++ b/src/test/util/validation.h @@ -19,7 +19,11 @@ struct TestChainstateManager : public ChainstateManager { class ValidationInterfaceTest { public: - static void BlockConnected(CValidationInterface& obj, const std::shared_ptr& block, const CBlockIndex* pindex); + static void BlockConnected( + ChainstateRole role, + CValidationInterface& obj, + const std::shared_ptr& block, + const CBlockIndex* pindex); }; #endif // BITCOIN_TEST_UTIL_VALIDATION_H diff --git a/src/test/validation_block_tests.cpp b/src/test/validation_block_tests.cpp index d1463634cc209..411371f7c16b0 100644 --- a/src/test/validation_block_tests.cpp +++ b/src/test/validation_block_tests.cpp @@ -43,7 +43,7 @@ struct TestSubscriber final : public CValidationInterface { BOOST_CHECK_EQUAL(m_expected_tip, pindexNew->GetBlockHash()); } - void BlockConnected(const std::shared_ptr& block, const CBlockIndex* pindex) override + void BlockConnected(ChainstateRole role, const std::shared_ptr& block, const CBlockIndex* pindex) override { BOOST_CHECK_EQUAL(m_expected_tip, block->hashPrevBlock); BOOST_CHECK_EQUAL(m_expected_tip, pindex->pprev->GetBlockHash()); diff --git a/src/test/validationinterface_tests.cpp b/src/test/validationinterface_tests.cpp index fcd0b25b3887c..5979441057cc8 100644 --- a/src/test/validationinterface_tests.cpp +++ b/src/test/validationinterface_tests.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include diff --git a/src/validation.cpp b/src/validation.cpp index 4873eb964ca3e..8c657839e8d3a 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -5,6 +5,7 @@ #include +#include #include #include @@ -2645,7 +2646,7 @@ bool Chainstate::FlushStateToDisk( } if (full_flush_completed) { // Update best block in wallet (so we can detect restored wallets). - GetMainSignals().ChainStateFlushed(m_chain.GetLocator()); + GetMainSignals().ChainStateFlushed(this->GetRole(), m_chain.GetLocator()); } } catch (const std::runtime_error& e) { return FatalError(m_chainman.GetNotifications(), state, std::string("System error while flushing: ") + e.what()); @@ -3239,7 +3240,7 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr< for (const PerBlockConnectTrace& trace : connectTrace.GetBlocksConnected()) { assert(trace.pblock && trace.pindex); - GetMainSignals().BlockConnected(trace.pblock, trace.pindex); + GetMainSignals().BlockConnected(this->GetRole(), trace.pblock, trace.pindex); } // This will have been toggled in diff --git a/src/validationinterface.cpp b/src/validationinterface.cpp index d344c8bfbda04..9241395ad54ed 100644 --- a/src/validationinterface.cpp +++ b/src/validationinterface.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -223,9 +224,9 @@ void CMainSignals::TransactionRemovedFromMempool(const CTransactionRef& tx, MemP RemovalReasonToString(reason)); } -void CMainSignals::BlockConnected(const std::shared_ptr &pblock, const CBlockIndex *pindex) { - auto event = [pblock, pindex, this] { - m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.BlockConnected(pblock, pindex); }); +void CMainSignals::BlockConnected(ChainstateRole role, const std::shared_ptr &pblock, const CBlockIndex *pindex) { + auto event = [role, pblock, pindex, this] { + m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.BlockConnected(role, pblock, pindex); }); }; ENQUEUE_AND_LOG_EVENT(event, "%s: block hash=%s block height=%d", __func__, pblock->GetHash().ToString(), @@ -242,9 +243,9 @@ void CMainSignals::BlockDisconnected(const std::shared_ptr& pblock pindex->nHeight); } -void CMainSignals::ChainStateFlushed(const CBlockLocator &locator) { - auto event = [locator, this] { - m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.ChainStateFlushed(locator); }); +void CMainSignals::ChainStateFlushed(ChainstateRole role, const CBlockLocator &locator) { + auto event = [role, locator, this] { + m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.ChainStateFlushed(role, locator); }); }; ENQUEUE_AND_LOG_EVENT(event, "%s: block hash=%s", __func__, locator.IsNull() ? "null" : locator.vHave.front().ToString()); diff --git a/src/validationinterface.h b/src/validationinterface.h index 5bdd7e0123892..eb15aa4d5f707 100644 --- a/src/validationinterface.h +++ b/src/validationinterface.h @@ -7,6 +7,7 @@ #define BITCOIN_VALIDATIONINTERFACE_H #include +#include #include // CTransaction(Ref) #include @@ -136,11 +137,12 @@ class CValidationInterface { * * Called on a background thread. */ - virtual void BlockConnected(const std::shared_ptr &block, const CBlockIndex *pindex) {} + virtual void BlockConnected(ChainstateRole role, const std::shared_ptr &block, const CBlockIndex *pindex) {} /** * Notifies listeners of a block being disconnected * - * Called on a background thread. + * Called on a background thread. Only called for the active chainstate, since + * background chainstates should never disconnect blocks. */ virtual void BlockDisconnected(const std::shared_ptr &block, const CBlockIndex* pindex) {} /** @@ -159,17 +161,18 @@ class CValidationInterface { * * Called on a background thread. */ - virtual void ChainStateFlushed(const CBlockLocator &locator) {} + virtual void ChainStateFlushed(ChainstateRole role, const CBlockLocator &locator) {} /** * Notifies listeners of a block validation result. * If the provided BlockValidationState IsValid, the provided block * is guaranteed to be the current best block at the time the - * callback was generated (not necessarily now) + * callback was generated (not necessarily now). */ virtual void BlockChecked(const CBlock&, const BlockValidationState&) {} /** * Notifies listeners that a block which builds directly on our current tip - * has been received and connected to the headers tree, though not validated yet */ + * has been received and connected to the headers tree, though not validated yet. + */ virtual void NewPoWValidBlock(const CBlockIndex *pindex, const std::shared_ptr& block) {}; friend class CMainSignals; friend class ValidationInterfaceTest; @@ -199,9 +202,9 @@ class CMainSignals { void UpdatedBlockTip(const CBlockIndex *, const CBlockIndex *, bool fInitialDownload); void TransactionAddedToMempool(const CTransactionRef&, uint64_t mempool_sequence); void TransactionRemovedFromMempool(const CTransactionRef&, MemPoolRemovalReason, uint64_t mempool_sequence); - void BlockConnected(const std::shared_ptr &, const CBlockIndex *pindex); + void BlockConnected(ChainstateRole, const std::shared_ptr &, const CBlockIndex *pindex); void BlockDisconnected(const std::shared_ptr &, const CBlockIndex* pindex); - void ChainStateFlushed(const CBlockLocator &); + void ChainStateFlushed(ChainstateRole, const CBlockLocator &); void BlockChecked(const CBlock&, const BlockValidationState&); void NewPoWValidBlock(const CBlockIndex *, const std::shared_ptr&); }; diff --git a/src/wallet/test/fuzz/notifications.cpp b/src/wallet/test/fuzz/notifications.cpp index 42accafe5b0ae..abd788f96fbe9 100644 --- a/src/wallet/test/fuzz/notifications.cpp +++ b/src/wallet/test/fuzz/notifications.cpp @@ -2,6 +2,7 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include #include #include #include @@ -145,8 +146,8 @@ FUZZ_TARGET(wallet_notifications, .init = initialize_setup) // time to the maximum value. This ensures that the wallet's birth time is always // earlier than this maximum time. info.chain_time_max = std::numeric_limits::max(); - a.wallet->blockConnected(info); - b.wallet->blockConnected(info); + a.wallet->blockConnected(ChainstateRole::NORMAL, info); + b.wallet->blockConnected(ChainstateRole::NORMAL, info); // Store the coins for the next block Coins coins_new; for (const auto& tx : block.vtx) { diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 245990841950c..c840c2ee1f3d8 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -626,7 +627,7 @@ bool CWallet::ChangeWalletPassphrase(const SecureString& strOldWalletPassphrase, return false; } -void CWallet::chainStateFlushed(const CBlockLocator& loc) +void CWallet::chainStateFlushed(ChainstateRole role, const CBlockLocator& loc) { // Don't update the best block until the chain is attached so that in case of a shutdown, // the rescan will be restarted at next startup. @@ -1462,7 +1463,7 @@ void CWallet::transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRe } } -void CWallet::blockConnected(const interfaces::BlockInfo& block) +void CWallet::blockConnected(ChainstateRole role, const interfaces::BlockInfo& block) { assert(block.data); LOCK(cs_wallet); @@ -2941,7 +2942,7 @@ std::shared_ptr CWallet::Create(WalletContext& context, const std::stri } if (chain) { - walletInstance->chainStateFlushed(chain->getTipLocator()); + walletInstance->chainStateFlushed(ChainstateRole::NORMAL, chain->getTipLocator()); } } else if (wallet_creation_flags & WALLET_FLAG_DISABLE_PRIVATE_KEYS) { // Make it impossible to disable private keys after creation @@ -3227,7 +3228,7 @@ bool CWallet::AttachChain(const std::shared_ptr& walletInstance, interf } } walletInstance->m_attaching_chain = false; - walletInstance->chainStateFlushed(chain.getTipLocator()); + walletInstance->chainStateFlushed(ChainstateRole::NORMAL, chain.getTipLocator()); walletInstance->GetDatabase().IncrementUpdateCounter(); } walletInstance->m_attaching_chain = false; diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 5adb8b6e2703a..9333493a6eebc 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -599,7 +599,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati CWalletTx* AddToWallet(CTransactionRef tx, const TxState& state, const UpdateWalletTxFn& update_wtx=nullptr, bool fFlushOnClose=true, bool rescanning_old_block = false); bool LoadToWallet(const uint256& hash, const UpdateWalletTxFn& fill_wtx) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); void transactionAddedToMempool(const CTransactionRef& tx) override; - void blockConnected(const interfaces::BlockInfo& block) override; + void blockConnected(ChainstateRole role, const interfaces::BlockInfo& block) override; void blockDisconnected(const interfaces::BlockInfo& block) override; void updatedBlockTip() override; int64_t RescanFromTime(int64_t startTime, const WalletRescanReserver& reserver, bool update); @@ -777,7 +777,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati /** should probably be renamed to IsRelevantToMe */ bool IsFromMe(const CTransaction& tx) const; CAmount GetDebit(const CTransaction& tx, const isminefilter& filter) const; - void chainStateFlushed(const CBlockLocator& loc) override; + void chainStateFlushed(ChainstateRole role, const CBlockLocator& loc) override; DBErrors LoadWallet(); DBErrors ZapSelectTx(std::vector& vHashIn, std::vector& vHashOut) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); diff --git a/src/zmq/zmqnotificationinterface.cpp b/src/zmq/zmqnotificationinterface.cpp index 6755368249f53..97355b45a7a14 100644 --- a/src/zmq/zmqnotificationinterface.cpp +++ b/src/zmq/zmqnotificationinterface.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -170,7 +171,7 @@ void CZMQNotificationInterface::TransactionRemovedFromMempool(const CTransaction }); } -void CZMQNotificationInterface::BlockConnected(const std::shared_ptr& pblock, const CBlockIndex* pindexConnected) +void CZMQNotificationInterface::BlockConnected(ChainstateRole role, const std::shared_ptr& pblock, const CBlockIndex* pindexConnected) { for (const CTransactionRef& ptx : pblock->vtx) { const CTransaction& tx = *ptx; diff --git a/src/zmq/zmqnotificationinterface.h b/src/zmq/zmqnotificationinterface.h index ce67633b30f8b..4246c53bd3718 100644 --- a/src/zmq/zmqnotificationinterface.h +++ b/src/zmq/zmqnotificationinterface.h @@ -33,7 +33,7 @@ class CZMQNotificationInterface final : public CValidationInterface // CValidationInterface void TransactionAddedToMempool(const CTransactionRef& tx, uint64_t mempool_sequence) override; void TransactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRemovalReason reason, uint64_t mempool_sequence) override; - void BlockConnected(const std::shared_ptr& pblock, const CBlockIndex* pindexConnected) override; + void BlockConnected(ChainstateRole role, const std::shared_ptr& pblock, const CBlockIndex* pindexConnected) override; void BlockDisconnected(const std::shared_ptr& pblock, const CBlockIndex* pindexDisconnected) override; void UpdatedBlockTip(const CBlockIndex *pindexNew, const CBlockIndex *pindexFork, bool fInitialDownload) override; From f073917a9e7ba423643dcae0339776470b628f65 Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Thu, 10 Nov 2022 16:09:25 -0500 Subject: [PATCH 09/26] validationinterface: only send zmq notifications for active --- doc/zmq.md | 4 ++-- src/zmq/zmqnotificationinterface.cpp | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/zmq.md b/doc/zmq.md index 4055505d7480b..07c340fb99e8f 100644 --- a/doc/zmq.md +++ b/doc/zmq.md @@ -113,11 +113,11 @@ Where the 8-byte uints correspond to the mempool sequence number. | hashtx | <32-byte transaction hash in Little Endian> | -`rawblock`: Notifies when the chain tip is updated. Messages are ZMQ multipart messages with three parts. The first part is the topic (`rawblock`), the second part is the serialized block, and the last part is a sequence number (representing the message count to detect lost messages). +`rawblock`: Notifies when the chain tip is updated. When assumeutxo is in use, this notification will not be issued for historical blocks connected to the background validation chainstate. Messages are ZMQ multipart messages with three parts. The first part is the topic (`rawblock`), the second part is the serialized block, and the last part is a sequence number (representing the message count to detect lost messages). | rawblock | | -`hashblock`: Notifies when the chain tip is updated. Messages are ZMQ multipart messages with three parts. The first part is the topic (`hashblock`), the second part is the 32-byte block hash, and the last part is a sequence number (representing the message count to detect lost messages). +`hashblock`: Notifies when the chain tip is updated. When assumeutxo is in use, this notification will not be issued for historical blocks connected to the background validation chainstate. Messages are ZMQ multipart messages with three parts. The first part is the topic (`hashblock`), the second part is the 32-byte block hash, and the last part is a sequence number (representing the message count to detect lost messages). | hashblock | <32-byte block hash in Little Endian> | diff --git a/src/zmq/zmqnotificationinterface.cpp b/src/zmq/zmqnotificationinterface.cpp index 97355b45a7a14..03aae865776e8 100644 --- a/src/zmq/zmqnotificationinterface.cpp +++ b/src/zmq/zmqnotificationinterface.cpp @@ -173,6 +173,9 @@ void CZMQNotificationInterface::TransactionRemovedFromMempool(const CTransaction void CZMQNotificationInterface::BlockConnected(ChainstateRole role, const std::shared_ptr& pblock, const CBlockIndex* pindexConnected) { + if (role == ChainstateRole::BACKGROUND) { + return; + } for (const CTransactionRef& ptx : pblock->vtx) { const CTransaction& tx = *ptx; TryForEachAndRemoveFailed(notifiers, [&tx](CZMQAbstractNotifier* notifier) { From fbe0a7d7ca680358237b6c2369b3fd2b43221113 Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Thu, 10 Nov 2022 16:09:41 -0500 Subject: [PATCH 10/26] wallet: validationinterface: only handle active chain notifications --- src/wallet/wallet.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index c840c2ee1f3d8..7b85cc36c449a 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -631,7 +631,7 @@ void CWallet::chainStateFlushed(ChainstateRole role, const CBlockLocator& loc) { // Don't update the best block until the chain is attached so that in case of a shutdown, // the rescan will be restarted at next startup. - if (m_attaching_chain) { + if (m_attaching_chain || role == ChainstateRole::BACKGROUND) { return; } WalletBatch batch(GetDatabase()); @@ -1465,6 +1465,9 @@ void CWallet::transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRe void CWallet::blockConnected(ChainstateRole role, const interfaces::BlockInfo& block) { + if (role == ChainstateRole::BACKGROUND) { + return; + } assert(block.data); LOCK(cs_wallet); From 1fffdd76a1bca908f55d73b64983655b14cf7432 Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Thu, 10 Nov 2022 16:10:35 -0500 Subject: [PATCH 11/26] net_processing: validationinterface: ignore some events for bg chain --- src/net_processing.cpp | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 12dca182c3800..03dee1351207c 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -1917,9 +1917,25 @@ void PeerManagerImpl::BlockConnected( const std::shared_ptr& pblock, const CBlockIndex* pindex) { - m_orphanage.EraseForBlock(*pblock); + // Update this for all chainstate roles so that we don't mistakenly see peers + // helping us do background IBD as having a stale tip. m_last_tip_update = GetTime(); + // In case the dynamic timeout was doubled once or more, reduce it slowly back to its default value + auto stalling_timeout = m_block_stalling_timeout.load(); + Assume(stalling_timeout >= BLOCK_STALLING_TIMEOUT_DEFAULT); + if (stalling_timeout != BLOCK_STALLING_TIMEOUT_DEFAULT) { + const auto new_timeout = std::max(std::chrono::duration_cast(stalling_timeout * 0.85), BLOCK_STALLING_TIMEOUT_DEFAULT); + if (m_block_stalling_timeout.compare_exchange_strong(stalling_timeout, new_timeout)) { + LogPrint(BCLog::NET, "Decreased stalling timeout to %d seconds\n", count_seconds(new_timeout)); + } + } + + if (role == ChainstateRole::BACKGROUND) { + return; + } + m_orphanage.EraseForBlock(*pblock); + { LOCK(m_recent_confirmed_transactions_mutex); for (const auto& ptx : pblock->vtx) { @@ -1936,16 +1952,6 @@ void PeerManagerImpl::BlockConnected( m_txrequest.ForgetTxHash(ptx->GetWitnessHash()); } } - - // In case the dynamic timeout was doubled once or more, reduce it slowly back to its default value - auto stalling_timeout = m_block_stalling_timeout.load(); - Assume(stalling_timeout >= BLOCK_STALLING_TIMEOUT_DEFAULT); - if (stalling_timeout != BLOCK_STALLING_TIMEOUT_DEFAULT) { - const auto new_timeout = std::max(std::chrono::duration_cast(stalling_timeout * 0.85), BLOCK_STALLING_TIMEOUT_DEFAULT); - if (m_block_stalling_timeout.compare_exchange_strong(stalling_timeout, new_timeout)) { - LogPrint(BCLog::NET, "Decreased stalling timeout to %d seconds\n", count_seconds(new_timeout)); - } - } } void PeerManagerImpl::BlockDisconnected(const std::shared_ptr &block, const CBlockIndex* pindex) From 373cf91531b84bfdd06fdf8abf4dca228029ce6b Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Mon, 23 Sep 2019 14:44:54 -0400 Subject: [PATCH 12/26] validation: indexing changes for assumeutxo When using an assumedvalid chainstate, only process validationinterface callbacks from the background chainstate within indexes. This ensures that all indexes are built in-order. Later, we can possibly designate indexes which can be built out of order and continue their operation during snapshot use. Once the background sync has completed, restart the indexes so that they continue to index the now-validated snapshot chainstate. --- src/index/base.cpp | 32 +++++++++++++++++++++++++++++--- src/index/base.h | 12 +++++++++--- src/init.cpp | 30 +++++++++++++++++++++++++----- src/validation.cpp | 18 ++++++++++++++++++ src/validation.h | 14 ++++++++++++++ 5 files changed, 95 insertions(+), 11 deletions(-) diff --git a/src/index/base.cpp b/src/index/base.cpp index 98a8bad102db3..8474d01c41f8f 100644 --- a/src/index/base.cpp +++ b/src/index/base.cpp @@ -79,9 +79,15 @@ BaseIndex::~BaseIndex() bool BaseIndex::Init() { + AssertLockNotHeld(cs_main); + + // May need reset if index is being restarted. + m_interrupt.reset(); + // m_chainstate member gives indexing code access to node internals. It is // removed in followup https://github.com/bitcoin/bitcoin/pull/24230 - m_chainstate = &m_chain->context()->chainman->ActiveChainstate(); + m_chainstate = WITH_LOCK(::cs_main, + return &m_chain->context()->chainman->GetChainstateForIndexing()); // Register to validation interface before setting the 'm_synced' flag, so that // callbacks are not missed once m_synced is true. RegisterValidationInterface(this); @@ -92,7 +98,8 @@ bool BaseIndex::Init() } LOCK(cs_main); - CChain& active_chain = m_chainstate->m_chain; + CChain& index_chain = m_chainstate->m_chain; + if (locator.IsNull()) { SetBestBlockIndex(nullptr); } else { @@ -114,7 +121,7 @@ bool BaseIndex::Init() // Note: this will latch to true immediately if the user starts up with an empty // datadir and an index enabled. If this is the case, indexation will happen solely // via `BlockConnected` signals until, possibly, the next restart. - m_synced = start_block == active_chain.Tip(); + m_synced = start_block == index_chain.Tip(); m_init = true; return true; } @@ -143,6 +150,8 @@ void BaseIndex::ThreadSync() std::chrono::steady_clock::time_point last_locator_write_time{0s}; while (true) { if (m_interrupt) { + LogPrintf("%s: m_interrupt set; exiting ThreadSync\n", GetName()); + SetBestBlockIndex(pindex); // No need to handle errors in Commit. If it fails, the error will be already be // logged. The best way to recover is to continue, as index cannot be corrupted by @@ -252,6 +261,17 @@ bool BaseIndex::Rewind(const CBlockIndex* current_tip, const CBlockIndex* new_ti void BaseIndex::BlockConnected(ChainstateRole role, const std::shared_ptr& block, const CBlockIndex* pindex) { + // Ignore events from the assumed-valid chain; we will process its blocks + // (sequentially) after it is fully verified by the background chainstate. This + // is to avoid any out-of-order indexing. + // + // TODO at some point we could parameterize whether a particular index can be + // built out of order, but for now just do the conservative simple thing. + if (role == ChainstateRole::ASSUMEDVALID) { + return; + } + + // Ignore BlockConnected signals until we have fully indexed the chain. if (!m_synced) { return; } @@ -298,6 +318,12 @@ void BaseIndex::BlockConnected(ChainstateRole role, const std::shared_ptr(node.kernel->interrupt, chainman_opts, blockman_opts); ChainstateManager& chainman = *node.chainman; + // This is defined and set here instead of inline in validation.h to avoid a hard + // dependency between validation and index/base, since the latter is not in + // libbitcoinkernel. + chainman.restart_indexes = [&node]() { + LogPrintf("[snapshot] restarting indexes\n"); + + // Drain the validation interface queue to ensure that the old indexes + // don't have any pending work. + SyncWithValidationInterfaceQueue(); + + for (auto* index : node.indexes) { + index->Interrupt(); + index->Stop(); + if (!(index->Init() && index->StartBackgroundSync())) { + LogPrintf("[snapshot] WARNING failed to restart index %s on snapshot chain\n", index->GetName()); + } + } + }; + node::ChainstateLoadOptions options; options.mempool = Assert(node.mempool.get()); options.reindex = node::fReindex; @@ -1906,18 +1925,19 @@ bool StartIndexBackgroundSync(NodeContext& node) // indexes_start_block='nullptr' means "start from height 0". std::optional indexes_start_block; std::string older_index_name; - ChainstateManager& chainman = *Assert(node.chainman); + const Chainstate& chainstate = WITH_LOCK(::cs_main, return chainman.GetChainstateForIndexing()); + const CChain& index_chain = chainstate.m_chain; + for (auto index : node.indexes) { const IndexSummary& summary = index->GetSummary(); if (summary.synced) continue; // Get the last common block between the index best block and the active chain LOCK(::cs_main); - const CChain& active_chain = chainman.ActiveChain(); const CBlockIndex* pindex = chainman.m_blockman.LookupBlockIndex(summary.best_block_hash); - if (!active_chain.Contains(pindex)) { - pindex = active_chain.FindFork(pindex); + if (!index_chain.Contains(pindex)) { + pindex = index_chain.FindFork(pindex); } if (!indexes_start_block || !pindex || pindex->nHeight < indexes_start_block.value()->nHeight) { @@ -1932,7 +1952,7 @@ bool StartIndexBackgroundSync(NodeContext& node) LOCK(::cs_main); const CBlockIndex* start_block = *indexes_start_block; if (!start_block) start_block = chainman.ActiveChain().Genesis(); - if (!chainman.m_blockman.CheckBlockDataAvailability(*chainman.ActiveChain().Tip(), *Assert(start_block))) { + if (!chainman.m_blockman.CheckBlockDataAvailability(*index_chain.Tip(), *Assert(start_block))) { return InitError(strprintf(Untranslated("%s best block of the index goes beyond pruned data. Please disable the index or reindex (which will download the whole blockchain again)"), older_index_name)); } } diff --git a/src/validation.cpp b/src/validation.cpp index 8c657839e8d3a..00da4a1c9ebf7 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -3289,6 +3289,16 @@ bool Chainstate::ActivateBestChain(BlockValidationState& state, std::shared_ptr< if (WITH_LOCK(::cs_main, return m_disabled)) { // Background chainstate has reached the snapshot base block, so exit. + + // Restart indexes to resume indexing for all blocks unique to the snapshot + // chain. This resumes indexing "in order" from where the indexing on the + // background validation chain left off. + // + // This cannot be done while holding cs_main (within + // MaybeCompleteSnapshotValidation) or a cs_main deadlock will occur. + if (m_chainman.restart_indexes) { + m_chainman.restart_indexes(); + } break; } @@ -5921,3 +5931,11 @@ bool ChainstateManager::ValidatedSnapshotCleanup() } return true; } + +Chainstate& ChainstateManager::GetChainstateForIndexing() +{ + // We can't always return `m_ibd_chainstate` because after background validation + // has completed, `m_snapshot_chainstate == m_active_chainstate`, but it can be + // indexed. + return (this->GetAll().size() > 1) ? *m_ibd_chainstate : *m_active_chainstate; +} diff --git a/src/validation.h b/src/validation.h index 38f57ed1b597a..b46b55b65014c 100644 --- a/src/validation.h +++ b/src/validation.h @@ -905,6 +905,10 @@ class ChainstateManager explicit ChainstateManager(const util::SignalInterrupt& interrupt, Options options, node::BlockManager::Options blockman_options); + //! Function to restart active indexes; set dynamically to avoid a circular + //! dependency on `base/index.cpp`. + std::function restart_indexes = std::function(); + const CChainParams& GetParams() const { return m_options.chainparams; } const Consensus::Params& GetConsensus() const { return m_options.chainparams.GetConsensus(); } bool ShouldCheckBlockIndex() const { return *Assert(m_options.check_block_index); } @@ -1227,6 +1231,16 @@ class ChainstateManager //! @sa node/chainstate:LoadChainstate() bool ValidatedSnapshotCleanup() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + //! @returns the chainstate that indexes should consult when ensuring that an + //! index is synced with a chain where we can expect block index entries to have + //! BLOCK_HAVE_DATA beneath the tip. + //! + //! In other words, give us the chainstate for which we can reasonably expect + //! that all blocks beneath the tip have been indexed. In practice this means + //! when using an assumed-valid chainstate based upon a snapshot, return only the + //! fully validated chain. + Chainstate& GetChainstateForIndexing() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + ~ChainstateManager(); }; From 1019c399825b0d512c1fd751c376d46fed4992b9 Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Mon, 16 Sep 2019 16:34:45 -0400 Subject: [PATCH 13/26] validation: pruning for multiple chainstates Introduces ChainstateManager::GetPruneRange(). The prune budget is split evenly between the number of chainstates, however the prune budget may be exceeded if the resulting shares are beneath `MIN_DISK_SPACE_FOR_BLOCK_FILES`. --- doc/release-notes-27596.md | 7 +++++ src/node/blockstorage.cpp | 62 ++++++++++++++++++++++++-------------- src/node/blockstorage.h | 14 +++++++-- src/validation.cpp | 35 +++++++++++++++++++-- src/validation.h | 14 ++++++--- 5 files changed, 102 insertions(+), 30 deletions(-) create mode 100644 doc/release-notes-27596.md diff --git a/doc/release-notes-27596.md b/doc/release-notes-27596.md new file mode 100644 index 0000000000000..4f96adb0f354f --- /dev/null +++ b/doc/release-notes-27596.md @@ -0,0 +1,7 @@ +Pruning +------- + +When using assumeutxo with `-prune`, the prune budget may be exceeded if it is set +lower than 1100MB (i.e. `MIN_DISK_SPACE_FOR_BLOCK_FILES * 2`). Prune budget is normally +split evenly across each chainstate, unless the resulting prune budget per chainstate +is beneath `MIN_DISK_SPACE_FOR_BLOCK_FILES` in which case that value will be used. diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index 5c3b7f958e0b3..9ae4ad67b4a51 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -257,40 +257,56 @@ void BlockManager::PruneOneBlockFile(const int fileNumber) m_dirty_fileinfo.insert(fileNumber); } -void BlockManager::FindFilesToPruneManual(std::set& setFilesToPrune, int nManualPruneHeight, int chain_tip_height) +void BlockManager::FindFilesToPruneManual( + std::set& setFilesToPrune, + int nManualPruneHeight, + const Chainstate& chain, + ChainstateManager& chainman) { assert(IsPruneMode() && nManualPruneHeight > 0); LOCK2(cs_main, cs_LastBlockFile); - if (chain_tip_height < 0) { + if (chain.m_chain.Height() < 0) { return; } - // last block to prune is the lesser of (user-specified height, MIN_BLOCKS_TO_KEEP from the tip) - unsigned int nLastBlockWeCanPrune = std::min((unsigned)nManualPruneHeight, chain_tip_height - MIN_BLOCKS_TO_KEEP); + const auto [min_block_to_prune, last_block_can_prune] = chainman.GetPruneRange(chain, nManualPruneHeight); + int count = 0; for (int fileNumber = 0; fileNumber < m_last_blockfile; fileNumber++) { - if (m_blockfile_info[fileNumber].nSize == 0 || m_blockfile_info[fileNumber].nHeightLast > nLastBlockWeCanPrune) { + const auto& fileinfo = m_blockfile_info[fileNumber]; + if (fileinfo.nSize == 0 || fileinfo.nHeightLast > (unsigned)last_block_can_prune || fileinfo.nHeightFirst < (unsigned)min_block_to_prune) { continue; } + PruneOneBlockFile(fileNumber); setFilesToPrune.insert(fileNumber); count++; } - LogPrintf("Prune (Manual): prune_height=%d removed %d blk/rev pairs\n", nLastBlockWeCanPrune, count); + LogPrintf("[%s] Prune (Manual): prune_height=%d removed %d blk/rev pairs\n", + chain.GetRole(), last_block_can_prune, count); } -void BlockManager::FindFilesToPrune(std::set& setFilesToPrune, uint64_t nPruneAfterHeight, int chain_tip_height, int prune_height, bool is_ibd) +void BlockManager::FindFilesToPrune( + std::set& setFilesToPrune, + int last_prune, + const Chainstate& chain, + ChainstateManager& chainman) { LOCK2(cs_main, cs_LastBlockFile); - if (chain_tip_height < 0 || GetPruneTarget() == 0) { + // Distribute our -prune budget over all chainstates. + const auto target = std::max( + MIN_DISK_SPACE_FOR_BLOCK_FILES, GetPruneTarget() / chainman.GetAll().size()); + + if (chain.m_chain.Height() < 0 || target == 0) { return; } - if ((uint64_t)chain_tip_height <= nPruneAfterHeight) { + if (static_cast(chain.m_chain.Height()) <= chainman.GetParams().PruneAfterHeight()) { return; } - unsigned int nLastBlockWeCanPrune{(unsigned)std::min(prune_height, chain_tip_height - static_cast(MIN_BLOCKS_TO_KEEP))}; + const auto [min_block_to_prune, last_block_can_prune] = chainman.GetPruneRange(chain, last_prune); + uint64_t nCurrentUsage = CalculateCurrentUsage(); // We don't check to prune until after we've allocated new space for files // So we should leave a buffer under our target to account for another allocation @@ -299,29 +315,31 @@ void BlockManager::FindFilesToPrune(std::set& setFilesToPrune, uint64_t nPr uint64_t nBytesToPrune; int count = 0; - if (nCurrentUsage + nBuffer >= GetPruneTarget()) { + if (nCurrentUsage + nBuffer >= target) { // On a prune event, the chainstate DB is flushed. // To avoid excessive prune events negating the benefit of high dbcache // values, we should not prune too rapidly. // So when pruning in IBD, increase the buffer a bit to avoid a re-prune too soon. - if (is_ibd) { + if (chainman.IsInitialBlockDownload()) { // Since this is only relevant during IBD, we use a fixed 10% - nBuffer += GetPruneTarget() / 10; + nBuffer += target / 10; } for (int fileNumber = 0; fileNumber < m_last_blockfile; fileNumber++) { - nBytesToPrune = m_blockfile_info[fileNumber].nSize + m_blockfile_info[fileNumber].nUndoSize; + const auto& fileinfo = m_blockfile_info[fileNumber]; + nBytesToPrune = fileinfo.nSize + fileinfo.nUndoSize; - if (m_blockfile_info[fileNumber].nSize == 0) { + if (fileinfo.nSize == 0) { continue; } - if (nCurrentUsage + nBuffer < GetPruneTarget()) { // are we below our target? + if (nCurrentUsage + nBuffer < target) { // are we below our target? break; } - // don't prune files that could have a block within MIN_BLOCKS_TO_KEEP of the main chain's tip but keep scanning - if (m_blockfile_info[fileNumber].nHeightLast > nLastBlockWeCanPrune) { + // don't prune files that could have a block that's not within the allowable + // prune range for the chain being pruned. + if (fileinfo.nHeightLast > (unsigned)last_block_can_prune || fileinfo.nHeightFirst < (unsigned)min_block_to_prune) { continue; } @@ -333,10 +351,10 @@ void BlockManager::FindFilesToPrune(std::set& setFilesToPrune, uint64_t nPr } } - LogPrint(BCLog::PRUNE, "target=%dMiB actual=%dMiB diff=%dMiB max_prune_height=%d removed %d blk/rev pairs\n", - GetPruneTarget() / 1024 / 1024, nCurrentUsage / 1024 / 1024, - (int64_t(GetPruneTarget()) - int64_t(nCurrentUsage)) / 1024 / 1024, - nLastBlockWeCanPrune, count); + LogPrint(BCLog::PRUNE, "[%s] target=%dMiB actual=%dMiB diff=%dMiB min_height=%d max_prune_height=%d removed %d blk/rev pairs\n", + chain.GetRole(), target / 1024 / 1024, nCurrentUsage / 1024 / 1024, + (int64_t(target) - int64_t(nCurrentUsage)) / 1024 / 1024, + min_block_to_prune, last_block_can_prune, count); } void BlockManager::UpdatePruneLock(const std::string& name, const PruneLockInfo& lock_info) { diff --git a/src/node/blockstorage.h b/src/node/blockstorage.h index 9a1d44cc7508d..64488584062f6 100644 --- a/src/node/blockstorage.h +++ b/src/node/blockstorage.h @@ -36,6 +36,7 @@ class CBlockUndo; class CChainParams; class Chainstate; class ChainstateManager; +enum class ChainstateRole; struct CCheckpointData; struct FlatFilePos; namespace Consensus { @@ -138,7 +139,11 @@ class BlockManager bool UndoWriteToDisk(const CBlockUndo& blockundo, FlatFilePos& pos, const uint256& hashBlock) const; /* Calculate the block/rev files to delete based on height specified by user with RPC command pruneblockchain */ - void FindFilesToPruneManual(std::set& setFilesToPrune, int nManualPruneHeight, int chain_tip_height); + void FindFilesToPruneManual( + std::set& setFilesToPrune, + int nManualPruneHeight, + const Chainstate& chain, + ChainstateManager& chainman); /** * Prune block and undo files (blk???.dat and rev???.dat) so that the disk space used is less than a user-defined target. @@ -154,8 +159,13 @@ class BlockManager * A db flag records the fact that at least some block files have been pruned. * * @param[out] setFilesToPrune The set of file indices that can be unlinked will be returned + * @param last_prune The last height we're able to prune, according to the prune locks */ - void FindFilesToPrune(std::set& setFilesToPrune, uint64_t nPruneAfterHeight, int chain_tip_height, int prune_height, bool is_ibd); + void FindFilesToPrune( + std::set& setFilesToPrune, + int last_prune, + const Chainstate& chain, + ChainstateManager& chainman); RecursiveMutex cs_LastBlockFile; std::vector m_blockfile_info; diff --git a/src/validation.cpp b/src/validation.cpp index 00da4a1c9ebf7..ad135994ec7a9 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -69,6 +69,7 @@ #include #include #include +#include using kernel::CCoinsStats; using kernel::CoinStatsHashType; @@ -2552,11 +2553,14 @@ bool Chainstate::FlushStateToDisk( if (nManualPruneHeight > 0) { LOG_TIME_MILLIS_WITH_CATEGORY("find files to prune (manual)", BCLog::BENCH); - m_blockman.FindFilesToPruneManual(setFilesToPrune, std::min(last_prune, nManualPruneHeight), m_chain.Height()); + m_blockman.FindFilesToPruneManual( + setFilesToPrune, + std::min(last_prune, nManualPruneHeight), + *this, m_chainman); } else { LOG_TIME_MILLIS_WITH_CATEGORY("find files to prune", BCLog::BENCH); - m_blockman.FindFilesToPrune(setFilesToPrune, m_chainman.GetParams().PruneAfterHeight(), m_chain.Height(), last_prune, m_chainman.IsInitialBlockDownload()); + m_blockman.FindFilesToPrune(setFilesToPrune, last_prune, *this, m_chainman); m_blockman.m_check_for_pruning = false; } if (!setFilesToPrune.empty()) { @@ -5939,3 +5943,30 @@ Chainstate& ChainstateManager::GetChainstateForIndexing() // indexed. return (this->GetAll().size() > 1) ? *m_ibd_chainstate : *m_active_chainstate; } + +std::pair ChainstateManager::GetPruneRange(const Chainstate& chainstate, int last_height_can_prune) +{ + if (chainstate.m_chain.Height() <= 0) { + return {0, 0}; + } + int prune_start{0}; + + if (this->GetAll().size() > 1 && m_snapshot_chainstate.get() == &chainstate) { + // Leave the blocks in the background IBD chain alone if we're pruning + // the snapshot chain. + prune_start = *Assert(GetSnapshotBaseHeight()) + 1; + } + + int max_prune = std::max( + 0, chainstate.m_chain.Height() - static_cast(MIN_BLOCKS_TO_KEEP)); + + // last block to prune is the lesser of (caller-specified height, MIN_BLOCKS_TO_KEEP from the tip) + // + // While you might be tempted to prune the background chainstate more + // aggressively (i.e. fewer MIN_BLOCKS_TO_KEEP), this won't work with index + // building - specifically blockfilterindex requires undo data, and if + // we don't maintain this trailing window, we hit indexing failures. + int prune_end = std::min(last_height_can_prune, max_prune); + + return {prune_start, prune_end}; +} diff --git a/src/validation.h b/src/validation.h index b46b55b65014c..2aa4221102d22 100644 --- a/src/validation.h +++ b/src/validation.h @@ -885,10 +885,6 @@ class ChainstateManager /** Most recent headers presync progress update, for rate-limiting. */ std::chrono::time_point m_last_presync_update GUARDED_BY(::cs_main) {}; - //! Return the height of the base block of the snapshot in use, if one exists, else - //! nullopt. - std::optional GetSnapshotBaseHeight() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main); - std::array m_warningcache GUARDED_BY(::cs_main); //! Return true if a chainstate is considered usable. @@ -1241,6 +1237,16 @@ class ChainstateManager //! fully validated chain. Chainstate& GetChainstateForIndexing() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + //! Return the [start, end] (inclusive) of block heights we can prune. + //! + //! start > end is possible, meaning no blocks can be pruned. + std::pair GetPruneRange( + 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 GetSnapshotBaseHeight() const EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + ~ChainstateManager(); }; From 49ef778158c43859946a592e11ec34fe1b93a5b6 Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Fri, 25 Aug 2023 14:05:07 -0400 Subject: [PATCH 14/26] test: adjust chainstate tests to use recognized snapshot base In future commits, loading the block index while making use of a snapshot is contingent on the snapshot being recognized by chainparams. Ensure all existing unittests that use snapshots use a recognized snapshot (at height 110). Co-authored-by: Ryan Ofsky --- .../validation_chainstatemanager_tests.cpp | 103 +++++++++++++----- src/validation.cpp | 3 +- 2 files changed, 77 insertions(+), 29 deletions(-) diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp index 74e577c17cbc2..f219d6bc4b793 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -30,12 +30,12 @@ using node::BlockManager; using node::KernelNotifications; using node::SnapshotMetadata; -BOOST_FIXTURE_TEST_SUITE(validation_chainstatemanager_tests, ChainTestingSetup) +BOOST_FIXTURE_TEST_SUITE(validation_chainstatemanager_tests, TestingSetup) //! Basic tests for ChainstateManager. //! //! First create a legacy (IBD) chainstate, then create a snapshot chainstate. -BOOST_AUTO_TEST_CASE(chainstatemanager) +BOOST_FIXTURE_TEST_CASE(chainstatemanager, TestChain100Setup) { ChainstateManager& manager = *m_node.chainman; CTxMemPool& mempool = *m_node.mempool; @@ -46,14 +46,8 @@ BOOST_AUTO_TEST_CASE(chainstatemanager) // Create a legacy (IBD) chainstate. // - Chainstate& c1 = WITH_LOCK(::cs_main, return manager.InitializeChainstate(&mempool)); + Chainstate& c1 = manager.ActiveChainstate(); chainstates.push_back(&c1); - c1.InitCoinsDB( - /*cache_size_bytes=*/1 << 23, /*in_memory=*/true, /*should_wipe=*/false); - WITH_LOCK(::cs_main, c1.InitCoinsCache(1 << 23)); - c1.LoadGenesisBlock(); - BlockValidationState val_state; - BOOST_CHECK(c1.ActivateBestChain(val_state, nullptr)); BOOST_CHECK(!manager.IsSnapshotActive()); BOOST_CHECK(WITH_LOCK(::cs_main, return !manager.IsSnapshotValidated())); @@ -63,8 +57,9 @@ BOOST_AUTO_TEST_CASE(chainstatemanager) auto& active_chain = WITH_LOCK(manager.GetMutex(), return manager.ActiveChain()); BOOST_CHECK_EQUAL(&active_chain, &c1.m_chain); - BOOST_CHECK_EQUAL(WITH_LOCK(manager.GetMutex(), return manager.ActiveHeight()), 0); - + // Get to a valid assumeutxo tip (per chainparams); + mineBlocks(10); + BOOST_CHECK_EQUAL(WITH_LOCK(manager.GetMutex(), return manager.ActiveHeight()), 110); auto active_tip = WITH_LOCK(manager.GetMutex(), return manager.ActiveTip()); auto exp_tip = c1.m_chain.Tip(); BOOST_CHECK_EQUAL(active_tip, exp_tip); @@ -77,16 +72,19 @@ BOOST_AUTO_TEST_CASE(chainstatemanager) Chainstate& c2 = WITH_LOCK(::cs_main, return manager.ActivateExistingSnapshot( &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); - WITH_LOCK(::cs_main, c2.InitCoinsCache(1 << 23)); - c2.m_chain.SetTip(*active_tip); + { + LOCK(::cs_main); + c2.InitCoinsCache(1 << 23); + c2.CoinsTip().SetBestBlock(active_tip->GetBlockHash()); + c2.setBlockIndexCandidates.insert(manager.m_blockman.LookupBlockIndex(active_tip->GetBlockHash())); + c2.LoadChainTip(); + } BlockValidationState _; BOOST_CHECK(c2.ActivateBestChain(_, nullptr)); + BOOST_CHECK_EQUAL(manager.SnapshotBlockhash().value(), snapshot_blockhash); BOOST_CHECK(manager.IsSnapshotActive()); BOOST_CHECK(WITH_LOCK(::cs_main, return !manager.IsSnapshotValidated())); BOOST_CHECK_EQUAL(&c2, &manager.ActiveChainstate()); @@ -97,13 +95,15 @@ BOOST_AUTO_TEST_CASE(chainstatemanager) auto& active_chain2 = WITH_LOCK(manager.GetMutex(), return manager.ActiveChain()); BOOST_CHECK_EQUAL(&active_chain2, &c2.m_chain); - BOOST_CHECK_EQUAL(WITH_LOCK(manager.GetMutex(), return manager.ActiveHeight()), 0); + BOOST_CHECK_EQUAL(WITH_LOCK(manager.GetMutex(), return manager.ActiveHeight()), 110); + mineBlocks(1); + BOOST_CHECK_EQUAL(WITH_LOCK(manager.GetMutex(), return manager.ActiveHeight()), 111); + BOOST_CHECK_EQUAL(WITH_LOCK(manager.GetMutex(), return c1.m_chain.Height()), 110); auto active_tip2 = WITH_LOCK(manager.GetMutex(), return manager.ActiveTip()); - auto exp_tip2 = c2.m_chain.Tip(); - BOOST_CHECK_EQUAL(active_tip2, exp_tip2); - - BOOST_CHECK_EQUAL(exp_tip, exp_tip2); + BOOST_CHECK_EQUAL(active_tip, active_tip2->pprev); + BOOST_CHECK_EQUAL(active_tip, c1.m_chain.Tip()); + BOOST_CHECK_EQUAL(active_tip2, c2.m_chain.Tip()); // Let scheduler events finish running to avoid accessing memory that is going to be unloaded SyncWithValidationInterfaceQueue(); @@ -125,9 +125,6 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_rebalance_caches, TestChain100Setup) // Chainstate& c1 = manager.ActiveChainstate(); chainstates.push_back(&c1); - c1.InitCoinsDB( - /*cache_size_bytes=*/1 << 23, /*in_memory=*/true, /*should_wipe=*/false); - { LOCK(::cs_main); c1.InitCoinsCache(1 << 23); @@ -431,13 +428,20 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_loadblockindex, TestChain100Setup) int num_indexes{0}; int num_assumed_valid{0}; + // Blocks in range [assumed_valid_start_idx, last_assumed_valid_idx) will be + // marked as assumed-valid and not having data. const int expected_assumed_valid{20}; - const int last_assumed_valid_idx{40}; + const int last_assumed_valid_idx{111}; const int assumed_valid_start_idx = last_assumed_valid_idx - expected_assumed_valid; + // Mine to height 120, past the hardcoded regtest assumeutxo snapshot at + // height 110 + mineBlocks(20); + CBlockIndex* validated_tip{nullptr}; CBlockIndex* assumed_base{nullptr}; CBlockIndex* assumed_tip{WITH_LOCK(chainman.GetMutex(), return chainman.ActiveChain().Tip())}; + BOOST_CHECK_EQUAL(assumed_tip->nHeight, 120); auto reload_all_block_indexes = [&]() { // For completeness, we also reset the block sequence counters to @@ -463,7 +467,7 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_loadblockindex, TestChain100Setup) LOCK(::cs_main); auto index = cs1.m_chain[i]; - // Blocks with heights in range [20, 40) are marked ASSUMED_VALID + // Blocks with heights in range [91, 110] are marked ASSUMED_VALID if (i < last_assumed_valid_idx && i >= assumed_valid_start_idx) { index->nStatus = BlockStatus::BLOCK_VALID_TREE | BlockStatus::BLOCK_ASSUMED_VALID; } @@ -497,10 +501,36 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_loadblockindex, TestChain100Setup) // Set tip of the assume-valid-based chain to the assume-valid block cs2.m_chain.SetTip(*assumed_base); + // Sanity check test variables. + BOOST_CHECK_EQUAL(num_indexes, 121); // 121 total blocks, including genesis + BOOST_CHECK_EQUAL(assumed_tip->nHeight, 120); // original chain has height 120 + BOOST_CHECK_EQUAL(validated_tip->nHeight, 90); // current cs1 chain has height 90 + BOOST_CHECK_EQUAL(assumed_base->nHeight, 110); // current cs2 chain has height 110 + + // Regenerate cs1.setBlockIndexCandidates and cs2.setBlockIndexCandidate and + // check contents below. reload_all_block_indexes(); - // The fully validated chain should have the current validated tip - // and the assumed valid base as candidates. + // The fully validated chain should only have the current validated tip and + // the assumed valid base as candidates, blocks 90 and 110. Specifically: + // + // - It does not have blocks 0-89 because they contain less work than the + // chain tip. + // + // - It has block 90 because it has data and equal work to the chain tip, + // (since it is the chain tip). + // + // - It does not have blocks 91-109 because they do not contain data. + // + // - It has block 110 even though it does not have data, because + // LoadBlockIndex has a special case to always add the snapshot block as a + // candidate. The special case is only actually intended to apply to the + // snapshot chainstate cs2, not the background chainstate cs1, but it is + // written broadly and applies to both. + // + // - It does not have any blocks after height 110 because cs1 is a background + // chainstate, and only blocks where are ancestors of the snapshot block + // are added as candidates for the background chainstate. BOOST_CHECK_EQUAL(cs1.setBlockIndexCandidates.size(), 2); BOOST_CHECK_EQUAL(cs1.setBlockIndexCandidates.count(validated_tip), 1); BOOST_CHECK_EQUAL(cs1.setBlockIndexCandidates.count(assumed_base), 1); @@ -508,8 +538,25 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_loadblockindex, TestChain100Setup) // The assumed-valid tolerant chain has the assumed valid base as a // candidate, but otherwise has none of the assumed-valid (which do not // HAVE_DATA) blocks as candidates. + // + // Specifically: + // - All blocks below height 110 are not candidates, because cs2 chain tip + // has height 110 and they have less work than it does. + // + // - Block 110 is a candidate even though it does not have data, because it + // is the snapshot block, which is assumed valid. + // + // - Blocks 111-120 are added because they have data. + + // Check that block 90 is absent BOOST_CHECK_EQUAL(cs2.setBlockIndexCandidates.count(validated_tip), 0); + // Check that block 109 is absent + BOOST_CHECK_EQUAL(cs2.setBlockIndexCandidates.count(assumed_base->pprev), 0); + // Check that block 110 is present + BOOST_CHECK_EQUAL(cs2.setBlockIndexCandidates.count(assumed_base), 1); + // Check that block 120 is present BOOST_CHECK_EQUAL(cs2.setBlockIndexCandidates.count(assumed_tip), 1); + // Check that 11 blocks total are present. BOOST_CHECK_EQUAL(cs2.setBlockIndexCandidates.size(), num_indexes - last_assumed_valid_idx + 1); } diff --git a/src/validation.cpp b/src/validation.cpp index ad135994ec7a9..b5cff0a4fdbb2 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -3539,7 +3539,8 @@ void Chainstate::ResetBlockFailureFlags(CBlockIndex *pindex) { void Chainstate::TryAddBlockIndexCandidate(CBlockIndex* pindex) { AssertLockHeld(cs_main); - // The block only is a candidate for the most-work-chain if it has more work than our current tip. + // The block only is a candidate for the most-work-chain if it has the same + // or more work than our current tip. if (m_chain.Tip() != nullptr && setBlockIndexCandidates.value_comp()(pindex, m_chain.Tip())) { return; } From 4c3b8ca35c2e4a441264749bb312df2bd054b5b8 Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Fri, 5 May 2023 15:54:13 -0400 Subject: [PATCH 15/26] validation: populate nChainTx value for assumedvalid chainstates Use the expected AssumeutxoData in order to bootstrap nChainTx values for assumedvalid blockindex entries in the snapshot chainstate. This is necessary because nChainTx is normally built up from nTx values, which are populated using blockdata which the snapshot chainstate does not yet have. --- src/node/blockstorage.cpp | 25 +++++++++++++++++++++---- src/node/blockstorage.h | 5 +++-- src/validation.cpp | 6 +++++- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index 9ae4ad67b4a51..7ed2346ae4f8a 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -378,13 +378,26 @@ CBlockIndex* BlockManager::InsertBlockIndex(const uint256& hash) return pindex; } -bool BlockManager::LoadBlockIndex() +bool BlockManager::LoadBlockIndex(const std::optional& snapshot_blockhash) { if (!m_block_tree_db->LoadBlockIndexGuts( GetConsensus(), [this](const uint256& hash) EXCLUSIVE_LOCKS_REQUIRED(cs_main) { return this->InsertBlockIndex(hash); }, m_interrupt)) { return false; } + int snapshot_height = -1; + if (snapshot_blockhash) { + const AssumeutxoData au_data = *Assert(GetParams().AssumeutxoForBlockhash(*snapshot_blockhash)); + snapshot_height = au_data.height; + CBlockIndex* base{LookupBlockIndex(*snapshot_blockhash)}; + + // Since nChainTx (responsible for estiamted progress) isn't persisted + // to disk, we must bootstrap the value for assumedvalid chainstates + // from the hardcoded assumeutxo chainparams. + base->nChainTx = au_data.nChainTx; + LogPrintf("[snapshot] set nChainTx=%d for %s\n", au_data.nChainTx, snapshot_blockhash->ToString()); + } + // Calculate nChainWork std::vector vSortedByHeight{GetAllBlockIndices()}; std::sort(vSortedByHeight.begin(), vSortedByHeight.end(), @@ -401,7 +414,11 @@ bool BlockManager::LoadBlockIndex() // Pruned nodes may have deleted the block. if (pindex->nTx > 0) { if (pindex->pprev) { - if (pindex->pprev->nChainTx > 0) { + if (snapshot_blockhash && pindex->nHeight == snapshot_height && + pindex->GetBlockHash() == *snapshot_blockhash) { + // Should have been set above; don't disturb it with code below. + Assert(pindex->nChainTx > 0); + } else if (pindex->pprev->nChainTx > 0) { pindex->nChainTx = pindex->pprev->nChainTx + pindex->nTx; } else { pindex->nChainTx = 0; @@ -444,9 +461,9 @@ bool BlockManager::WriteBlockIndexDB() return true; } -bool BlockManager::LoadBlockIndexDB() +bool BlockManager::LoadBlockIndexDB(const std::optional& snapshot_blockhash) { - if (!LoadBlockIndex()) { + if (!LoadBlockIndex(snapshot_blockhash)) { return false; } diff --git a/src/node/blockstorage.h b/src/node/blockstorage.h index 64488584062f6..fcd9fb9f67aed 100644 --- a/src/node/blockstorage.h +++ b/src/node/blockstorage.h @@ -118,7 +118,7 @@ class BlockManager * per index entry (nStatus, nChainWork, nTimeMax, etc.) as well as peripheral * collections like m_dirty_blockindex. */ - bool LoadBlockIndex() + bool LoadBlockIndex(const std::optional& snapshot_blockhash) EXCLUSIVE_LOCKS_REQUIRED(cs_main); /** Return false if block file or undo file flushing fails. */ @@ -231,7 +231,8 @@ class BlockManager std::unique_ptr m_block_tree_db GUARDED_BY(::cs_main); bool WriteBlockIndexDB() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); - bool LoadBlockIndexDB() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + bool LoadBlockIndexDB(const std::optional& snapshot_blockhash) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main); /** * Remove any pruned block & undo files that are still on disk. diff --git a/src/validation.cpp b/src/validation.cpp index b5cff0a4fdbb2..9c783ece65176 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -4542,7 +4542,7 @@ bool ChainstateManager::LoadBlockIndex() // Load block index from databases bool needs_init = fReindex; if (!fReindex) { - bool ret{m_blockman.LoadBlockIndexDB()}; + bool ret{m_blockman.LoadBlockIndexDB(SnapshotBlockhash())}; if (!ret) return false; m_blockman.ScanAndUnlinkAlreadyPrunedFiles(); @@ -4838,6 +4838,10 @@ void ChainstateManager::CheckBlockIndex() CBlockIndex* pindexFirstAssumeValid = nullptr; // Oldest ancestor of pindex which has BLOCK_ASSUMED_VALID while (pindex != nullptr) { nNodes++; + if (pindex->pprev && pindex->nTx > 0) { + // nChainTx should increase monotonically + assert(pindex->pprev->nChainTx <= pindex->nChainTx); + } if (pindexFirstAssumeValid == nullptr && pindex->nStatus & BLOCK_ASSUMED_VALID) pindexFirstAssumeValid = pindex; if (pindexFirstInvalid == nullptr && pindex->nStatus & BLOCK_FAILED_VALID) pindexFirstInvalid = pindex; if (pindexFirstMissing == nullptr && !(pindex->nStatus & BLOCK_HAVE_DATA)) { From 7fcd21544a333ffdf1910b65c573579860be6a36 Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Wed, 3 May 2023 14:55:03 -0400 Subject: [PATCH 16/26] blockstorage: segment normal/assumedvalid blockfiles When using an assumedvalid (snapshot) chainstate along with a background chainstate, we are syncing two very different regions of the chain simultaneously. If we use the same blockfile space for both of these syncs, wildly different height blocks will be stored alongside one another, making pruning ineffective. This change implements a separate blockfile cursor for the assumedvalid chainstate when one is in use. --- src/node/blockstorage.cpp | 139 +++++++++++++++++++++++++++++--------- src/node/blockstorage.h | 84 +++++++++++++++++++---- src/validation.cpp | 3 +- 3 files changed, 179 insertions(+), 47 deletions(-) diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index 7ed2346ae4f8a..5e61ed3100d4c 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -273,7 +274,7 @@ void BlockManager::FindFilesToPruneManual( const auto [min_block_to_prune, last_block_can_prune] = chainman.GetPruneRange(chain, nManualPruneHeight); int count = 0; - for (int fileNumber = 0; fileNumber < m_last_blockfile; fileNumber++) { + for (int fileNumber = 0; fileNumber < this->MaxBlockfileNum(); fileNumber++) { const auto& fileinfo = m_blockfile_info[fileNumber]; if (fileinfo.nSize == 0 || fileinfo.nHeightLast > (unsigned)last_block_can_prune || fileinfo.nHeightFirst < (unsigned)min_block_to_prune) { continue; @@ -325,7 +326,7 @@ void BlockManager::FindFilesToPrune( nBuffer += target / 10; } - for (int fileNumber = 0; fileNumber < m_last_blockfile; fileNumber++) { + for (int fileNumber = 0; fileNumber < this->MaxBlockfileNum(); fileNumber++) { const auto& fileinfo = m_blockfile_info[fileNumber]; nBytesToPrune = fileinfo.nSize + fileinfo.nUndoSize; @@ -385,19 +386,25 @@ bool BlockManager::LoadBlockIndex(const std::optional& snapshot_blockha return false; } - int snapshot_height = -1; if (snapshot_blockhash) { const AssumeutxoData au_data = *Assert(GetParams().AssumeutxoForBlockhash(*snapshot_blockhash)); - snapshot_height = au_data.height; + m_snapshot_height = au_data.height; CBlockIndex* base{LookupBlockIndex(*snapshot_blockhash)}; - // Since nChainTx (responsible for estiamted progress) isn't persisted + // Since nChainTx (responsible for estimated progress) isn't persisted // to disk, we must bootstrap the value for assumedvalid chainstates // from the hardcoded assumeutxo chainparams. base->nChainTx = au_data.nChainTx; LogPrintf("[snapshot] set nChainTx=%d for %s\n", au_data.nChainTx, snapshot_blockhash->ToString()); + } else { + // If this isn't called with a snapshot blockhash, make sure the cached snapshot height + // is null. This is relevant during snapshot completion, when the blockman may be loaded + // with a height that then needs to be cleared after the snapshot is fully validated. + m_snapshot_height.reset(); } + Assert(m_snapshot_height.has_value() == snapshot_blockhash.has_value()); + // Calculate nChainWork std::vector vSortedByHeight{GetAllBlockIndices()}; std::sort(vSortedByHeight.begin(), vSortedByHeight.end(), @@ -414,7 +421,7 @@ bool BlockManager::LoadBlockIndex(const std::optional& snapshot_blockha // Pruned nodes may have deleted the block. if (pindex->nTx > 0) { if (pindex->pprev) { - if (snapshot_blockhash && pindex->nHeight == snapshot_height && + if (m_snapshot_height && pindex->nHeight == *m_snapshot_height && pindex->GetBlockHash() == *snapshot_blockhash) { // Should have been set above; don't disturb it with code below. Assert(pindex->nChainTx > 0); @@ -455,7 +462,8 @@ bool BlockManager::WriteBlockIndexDB() vBlocks.push_back(*it); m_dirty_blockindex.erase(it++); } - if (!m_block_tree_db->WriteBatchSync(vFiles, m_last_blockfile, vBlocks)) { + int max_blockfile = WITH_LOCK(cs_LastBlockFile, return this->MaxBlockfileNum()); + if (!m_block_tree_db->WriteBatchSync(vFiles, max_blockfile, vBlocks)) { return false; } return true; @@ -466,16 +474,17 @@ bool BlockManager::LoadBlockIndexDB(const std::optional& snapshot_block if (!LoadBlockIndex(snapshot_blockhash)) { return false; } + int max_blockfile_num{0}; // Load block file info - m_block_tree_db->ReadLastBlockFile(m_last_blockfile); - m_blockfile_info.resize(m_last_blockfile + 1); - LogPrintf("%s: last block file = %i\n", __func__, m_last_blockfile); - for (int nFile = 0; nFile <= m_last_blockfile; nFile++) { + m_block_tree_db->ReadLastBlockFile(max_blockfile_num); + m_blockfile_info.resize(max_blockfile_num + 1); + LogPrintf("%s: last block file = %i\n", __func__, max_blockfile_num); + for (int nFile = 0; nFile <= max_blockfile_num; nFile++) { m_block_tree_db->ReadBlockFileInfo(nFile, m_blockfile_info[nFile]); } - LogPrintf("%s: last block file info: %s\n", __func__, m_blockfile_info[m_last_blockfile].ToString()); - for (int nFile = m_last_blockfile + 1; true; nFile++) { + LogPrintf("%s: last block file info: %s\n", __func__, m_blockfile_info[max_blockfile_num].ToString()); + for (int nFile = max_blockfile_num + 1; true; nFile++) { CBlockFileInfo info; if (m_block_tree_db->ReadBlockFileInfo(nFile, info)) { m_blockfile_info.push_back(info); @@ -499,6 +508,15 @@ bool BlockManager::LoadBlockIndexDB(const std::optional& snapshot_block } } + { + // Initialize the blockfile cursors. + LOCK(cs_LastBlockFile); + for (size_t i = 0; i < m_blockfile_info.size(); ++i) { + const auto last_height_in_file = m_blockfile_info[i].nHeightLast; + m_blockfile_cursors[BlockfileTypeForHeight(last_height_in_file)] = {static_cast(i), 0}; + } + } + // Check whether we have ever pruned block & undo files m_block_tree_db->ReadFlag("prunedblockfiles", m_have_pruned); if (m_have_pruned) { @@ -516,12 +534,13 @@ bool BlockManager::LoadBlockIndexDB(const std::optional& snapshot_block void BlockManager::ScanAndUnlinkAlreadyPrunedFiles() { AssertLockHeld(::cs_main); + int max_blockfile = WITH_LOCK(cs_LastBlockFile, return this->MaxBlockfileNum()); if (!m_have_pruned) { return; } std::set block_files_to_prune; - for (int file_number = 0; file_number < m_last_blockfile; file_number++) { + for (int file_number = 0; file_number < max_blockfile; file_number++) { if (m_blockfile_info[file_number].nSize == 0) { block_files_to_prune.insert(file_number); } @@ -696,7 +715,7 @@ bool BlockManager::FlushUndoFile(int block_file, bool finalize) return true; } -bool BlockManager::FlushBlockFile(bool fFinalize, bool finalize_undo) +bool BlockManager::FlushBlockFile(int blockfile_num, bool fFinalize, bool finalize_undo) { bool success = true; LOCK(cs_LastBlockFile); @@ -708,9 +727,9 @@ bool BlockManager::FlushBlockFile(bool fFinalize, bool finalize_undo) // have populated `m_blockfile_info` via LoadBlockIndexDB(). return true; } - assert(static_cast(m_blockfile_info.size()) > m_last_blockfile); + assert(static_cast(m_blockfile_info.size()) > blockfile_num); - FlatFilePos block_pos_old(m_last_blockfile, m_blockfile_info[m_last_blockfile].nSize); + FlatFilePos block_pos_old(blockfile_num, m_blockfile_info[blockfile_num].nSize); if (!BlockFileSeq().Flush(block_pos_old, fFinalize)) { m_opts.notifications.flushError("Flushing block file to disk failed. This is likely the result of an I/O error."); success = false; @@ -718,13 +737,33 @@ bool BlockManager::FlushBlockFile(bool fFinalize, bool finalize_undo) // we do not always flush the undo file, as the chain tip may be lagging behind the incoming blocks, // e.g. during IBD or a sync after a node going offline if (!fFinalize || finalize_undo) { - if (!FlushUndoFile(m_last_blockfile, finalize_undo)) { + if (!FlushUndoFile(blockfile_num, finalize_undo)) { success = false; } } return success; } +BlockfileType BlockManager::BlockfileTypeForHeight(int height) +{ + if (!m_snapshot_height) { + return BlockfileType::NORMAL; + } + return (height >= *m_snapshot_height) ? BlockfileType::ASSUMED : BlockfileType::NORMAL; +} + +bool BlockManager::FlushChainstateBlockFile(int tip_height) +{ + LOCK(cs_LastBlockFile); + auto& cursor = m_blockfile_cursors[BlockfileTypeForHeight(tip_height)]; + if (cursor) { + // The cursor may not exist after a snapshot has been loaded but before any + // blocks have been downloaded. + return FlushBlockFile(cursor->file_num, /*fFinalize=*/false, /*finalize_undo=*/false); + } + return false; +} + uint64_t BlockManager::CalculateCurrentUsage() { LOCK(cs_LastBlockFile); @@ -779,8 +818,19 @@ bool BlockManager::FindBlockPos(FlatFilePos& pos, unsigned int nAddSize, unsigne { LOCK(cs_LastBlockFile); - unsigned int nFile = fKnown ? pos.nFile : m_last_blockfile; - if (m_blockfile_info.size() <= nFile) { + const BlockfileType chain_type = BlockfileTypeForHeight(nHeight); + + if (!m_blockfile_cursors[chain_type]) { + // If a snapshot is loaded during runtime, we may not have initialized this cursor yet. + assert(chain_type == BlockfileType::ASSUMED); + const auto new_cursor = BlockfileCursor{this->MaxBlockfileNum() + 1}; + m_blockfile_cursors[chain_type] = new_cursor; + LogPrint(BCLog::BLOCKSTORAGE, "[%s] initializing blockfile cursor to %s\n", chain_type, new_cursor); + } + const int last_blockfile = m_blockfile_cursors[chain_type]->file_num; + + int nFile = fKnown ? pos.nFile : last_blockfile; + if (static_cast(m_blockfile_info.size()) <= nFile) { m_blockfile_info.resize(nFile + 1); } @@ -797,13 +847,20 @@ bool BlockManager::FindBlockPos(FlatFilePos& pos, unsigned int nAddSize, unsigne } } assert(nAddSize < max_blockfile_size); + while (m_blockfile_info[nFile].nSize + nAddSize >= max_blockfile_size) { // when the undo file is keeping up with the block file, we want to flush it explicitly // when it is lagging behind (more blocks arrive than are being connected), we let the // undo block write case handle it - finalize_undo = (m_blockfile_info[nFile].nHeightLast == m_undo_height_in_last_blockfile); - nFile++; - if (m_blockfile_info.size() <= nFile) { + finalize_undo = (static_cast(m_blockfile_info[nFile].nHeightLast) == + Assert(m_blockfile_cursors[chain_type])->undo_height); + + // Try the next unclaimed blockfile number + nFile = this->MaxBlockfileNum() + 1; + // Set to increment MaxBlockfileNum() for next iteration + m_blockfile_cursors[chain_type] = BlockfileCursor{nFile}; + + if (static_cast(m_blockfile_info.size()) <= nFile) { m_blockfile_info.resize(nFile + 1); } } @@ -811,9 +868,10 @@ bool BlockManager::FindBlockPos(FlatFilePos& pos, unsigned int nAddSize, unsigne pos.nPos = m_blockfile_info[nFile].nSize; } - if ((int)nFile != m_last_blockfile) { + if (nFile != last_blockfile) { if (!fKnown) { - LogPrint(BCLog::BLOCKSTORAGE, "Leaving block file %i: %s\n", m_last_blockfile, m_blockfile_info[m_last_blockfile].ToString()); + LogPrint(BCLog::BLOCKSTORAGE, "Leaving block file %i: %s (onto %i) (height %i)\n", + last_blockfile, m_blockfile_info[last_blockfile].ToString(), nFile, nHeight); } // Do not propagate the return code. The flush concerns a previous block @@ -823,13 +881,13 @@ bool BlockManager::FindBlockPos(FlatFilePos& pos, unsigned int nAddSize, unsigne // data may be inconsistent after a crash if the flush is called during // a reindex. A flush error might also leave some of the data files // untrimmed. - if (!FlushBlockFile(!fKnown, finalize_undo)) { + if (!FlushBlockFile(last_blockfile, !fKnown, finalize_undo)) { LogPrintLevel(BCLog::BLOCKSTORAGE, BCLog::Level::Warning, "Failed to flush previous block file %05i (finalize=%i, finalize_undo=%i) before opening new block file %05i\n", - m_last_blockfile, !fKnown, finalize_undo, nFile); + last_blockfile, !fKnown, finalize_undo, nFile); } - m_last_blockfile = nFile; - m_undo_height_in_last_blockfile = 0; // No undo data yet in the new file, so reset our undo-height tracking. + // No undo data yet in the new file, so reset our undo-height tracking. + m_blockfile_cursors[chain_type] = BlockfileCursor{nFile}; } m_blockfile_info[nFile].AddBlock(nHeight, nTime); @@ -903,6 +961,9 @@ bool BlockManager::WriteBlockToDisk(const CBlock& block, FlatFilePos& pos) const bool BlockManager::WriteUndoDataForBlock(const CBlockUndo& blockundo, BlockValidationState& state, CBlockIndex& block) { AssertLockHeld(::cs_main); + const BlockfileType type = BlockfileTypeForHeight(block.nHeight); + auto& cursor = *Assert(WITH_LOCK(cs_LastBlockFile, return m_blockfile_cursors[type])); + // Write undo information to disk if (block.GetUndoPos().IsNull()) { FlatFilePos _pos; @@ -917,7 +978,7 @@ bool BlockManager::WriteUndoDataForBlock(const CBlockUndo& blockundo, BlockValid // in the block file info as below; note that this does not catch the case where the undo writes are keeping up // with the block writes (usually when a synced up node is getting newly mined blocks) -- this case is caught in // the FindBlockPos function - if (_pos.nFile < m_last_blockfile && static_cast(block.nHeight) == m_blockfile_info[_pos.nFile].nHeightLast) { + if (_pos.nFile < cursor.file_num && static_cast(block.nHeight) == m_blockfile_info[_pos.nFile].nHeightLast) { // Do not propagate the return code, a failed flush here should not // be an indication for a failed write. If it were propagated here, // the caller would assume the undo data not to be written, when in @@ -926,8 +987,8 @@ bool BlockManager::WriteUndoDataForBlock(const CBlockUndo& blockundo, BlockValid if (!FlushUndoFile(_pos.nFile, true)) { LogPrintLevel(BCLog::BLOCKSTORAGE, BCLog::Level::Warning, "Failed to flush undo file %05i\n", _pos.nFile); } - } else if (_pos.nFile == m_last_blockfile && static_cast(block.nHeight) > m_undo_height_in_last_blockfile) { - m_undo_height_in_last_blockfile = block.nHeight; + } else if (_pos.nFile == cursor.file_num && block.nHeight > cursor.undo_height) { + cursor.undo_height = block.nHeight; } // update nUndoPos in block index block.nUndoPos = _pos.nPos; @@ -1126,4 +1187,18 @@ void ImportBlocks(ChainstateManager& chainman, std::vector vImportFile } } // End scope of ImportingNow } + +std::ostream& operator<<(std::ostream& os, const BlockfileType& type) { + switch(type) { + case BlockfileType::NORMAL: os << "normal"; break; + case BlockfileType::ASSUMED: os << "assumed"; break; + default: os.setstate(std::ios_base::failbit); + } + return os; +} + +std::ostream& operator<<(std::ostream& os, const BlockfileCursor& cursor) { + os << strprintf("BlockfileCursor(file_num=%d, undo_height=%d)", cursor.file_num, cursor.undo_height); + return os; +} } // namespace node diff --git a/src/node/blockstorage.h b/src/node/blockstorage.h index fcd9fb9f67aed..ac97728c0567e 100644 --- a/src/node/blockstorage.h +++ b/src/node/blockstorage.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -36,7 +37,6 @@ class CBlockUndo; class CChainParams; class Chainstate; class ChainstateManager; -enum class ChainstateRole; struct CCheckpointData; struct FlatFilePos; namespace Consensus { @@ -98,6 +98,35 @@ struct PruneLockInfo { int height_first{std::numeric_limits::max()}; //! Height of earliest block that should be kept and not pruned }; +enum BlockfileType { + // Values used as array indexes - do not change carelessly. + NORMAL = 0, + ASSUMED = 1, + NUM_TYPES = 2, +}; + +std::ostream& operator<<(std::ostream& os, const BlockfileType& type); + +struct BlockfileCursor { + // The latest blockfile number. + int file_num{0}; + + // Track the height of the highest block in file_num whose undo + // data has been written. Block data is written to block files in download + // order, but is written to undo files in validation order, which is + // usually in order by height. To avoid wasting disk space, undo files will + // be trimmed whenever the corresponding block file is finalized and + // the height of the highest block written to the block file equals the + // height of the highest block written to the undo file. This is a + // heuristic and can sometimes preemptively trim undo files that will write + // more data later, and sometimes fail to trim undo files that can't have + // more data written later. + int undo_height{0}; +}; + +std::ostream& operator<<(std::ostream& os, const BlockfileCursor& cursor); + + /** * Maintains a tree of blocks (stored in `m_block_index`) which is consulted * to determine where the most-work tip is. @@ -122,12 +151,13 @@ class BlockManager EXCLUSIVE_LOCKS_REQUIRED(cs_main); /** Return false if block file or undo file flushing fails. */ - [[nodiscard]] bool FlushBlockFile(bool fFinalize = false, bool finalize_undo = false); + [[nodiscard]] bool FlushBlockFile(int blockfile_num, bool fFinalize, bool finalize_undo); /** Return false if undo file flushing fails. */ [[nodiscard]] bool FlushUndoFile(int block_file, bool finalize = false); [[nodiscard]] bool FindBlockPos(FlatFilePos& pos, unsigned int nAddSize, unsigned int nHeight, uint64_t nTime, bool fKnown); + [[nodiscard]] bool FlushChainstateBlockFile(int tip_height); bool FindUndoPos(BlockValidationState& state, int nFile, FlatFilePos& pos, unsigned int nAddSize); FlatFileSeq BlockFileSeq() const; @@ -169,19 +199,29 @@ class BlockManager RecursiveMutex cs_LastBlockFile; std::vector m_blockfile_info; - int m_last_blockfile = 0; - // Track the height of the highest block in m_last_blockfile whose undo - // data has been written. Block data is written to block files in download - // order, but is written to undo files in validation order, which is - // usually in order by height. To avoid wasting disk space, undo files will - // be trimmed whenever the corresponding block file is finalized and - // the height of the highest block written to the block file equals the - // height of the highest block written to the undo file. This is a - // heuristic and can sometimes preemptively trim undo files that will write - // more data later, and sometimes fail to trim undo files that can't have - // more data written later. - unsigned int m_undo_height_in_last_blockfile = 0; + //! Since assumedvalid chainstates may be syncing a range of the chain that is very + //! far away from the normal/background validation process, we should segment blockfiles + //! for assumed chainstates. Otherwise, we might have wildly different height ranges + //! mixed into the same block files, which would impair our ability to prune + //! effectively. + //! + //! This data structure maintains separate blockfile number cursors for each + //! BlockfileType. The ASSUMED state is initialized, when necessary, in FindBlockPos(). + //! + //! The first element is the NORMAL cursor, second is ASSUMED. + std::array, BlockfileType::NUM_TYPES> + m_blockfile_cursors GUARDED_BY(cs_LastBlockFile) = { + BlockfileCursor{}, + std::nullopt, + }; + int MaxBlockfileNum() const EXCLUSIVE_LOCKS_REQUIRED(cs_LastBlockFile) + { + static const BlockfileCursor empty_cursor; + const auto& normal = m_blockfile_cursors[BlockfileType::NORMAL].value_or(empty_cursor); + const auto& assumed = m_blockfile_cursors[BlockfileType::ASSUMED].value_or(empty_cursor); + return std::max(normal.file_num, assumed.file_num); + } /** Global flag to indicate we should check to see if there are * block/undo files that should be deleted. Set on startup @@ -205,6 +245,8 @@ class BlockManager */ std::unordered_map m_prune_locks GUARDED_BY(::cs_main); + BlockfileType BlockfileTypeForHeight(int height); + const kernel::BlockManagerOpts m_opts; public: @@ -220,6 +262,20 @@ class BlockManager BlockMap m_block_index GUARDED_BY(cs_main); + /** + * The height of the base block of an assumeutxo snapshot, if one is in use. + * + * This controls how blockfiles are segmented by chainstate type to avoid + * comingling different height regions of the chain when an assumedvalid chainstate + * is in use. If heights are drastically different in the same blockfile, pruning + * suffers. + * + * This is set during ActivateSnapshot() or upon LoadBlockIndex() if a snapshot + * had been previously loaded. After the snapshot is validated, this is unset to + * restore normal LoadBlockIndex behavior. + */ + std::optional m_snapshot_height; + std::vector GetAllBlockIndices() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); /** diff --git a/src/validation.cpp b/src/validation.cpp index 9c783ece65176..01081011b085a 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -2601,7 +2601,7 @@ bool Chainstate::FlushStateToDisk( // First make sure all block and undo data is flushed to disk. // TODO: Handle return error, or add detailed comment why it is // safe to not return an error upon failure. - if (!m_blockman.FlushBlockFile()) { + if (!m_blockman.FlushChainstateBlockFile(m_chain.Height())) { LogPrintLevel(BCLog::VALIDATION, BCLog::Level::Warning, "%s: Failed to flush block file.\n", __func__); } } @@ -5269,6 +5269,7 @@ bool ChainstateManager::ActivateSnapshot( assert(chaintip_loaded); m_active_chainstate = m_snapshot_chainstate.get(); + m_blockman.m_snapshot_height = this->GetSnapshotBaseHeight(); LogPrintf("[snapshot] successfully activated snapshot %s\n", base_blockhash.ToString()); LogPrintf("[snapshot] (%.2f MB)\n", From 9511fb3616b7bbe1d0d2f54a45ea0a650ba0367b Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Fri, 5 May 2023 18:27:56 -0400 Subject: [PATCH 17/26] validation: assumeutxo: swap m_mempool on snapshot activation Otherwise we will not receive transactions during background sync until restart. --- .../validation_chainstatemanager_tests.cpp | 11 +++-------- src/validation.cpp | 18 +++++++++++++++--- src/validation.h | 3 +-- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp index f219d6bc4b793..227d7d4633e18 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -38,8 +38,6 @@ BOOST_FIXTURE_TEST_SUITE(validation_chainstatemanager_tests, TestingSetup) BOOST_FIXTURE_TEST_CASE(chainstatemanager, TestChain100Setup) { ChainstateManager& manager = *m_node.chainman; - CTxMemPool& mempool = *m_node.mempool; - std::vector chainstates; BOOST_CHECK(!manager.SnapshotBlockhash().has_value()); @@ -69,8 +67,7 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager, TestChain100Setup) // Create a snapshot-based chainstate. // const uint256 snapshot_blockhash = active_tip->GetBlockHash(); - Chainstate& c2 = WITH_LOCK(::cs_main, return manager.ActivateExistingSnapshot( - &mempool, snapshot_blockhash)); + Chainstate& c2 = WITH_LOCK(::cs_main, return manager.ActivateExistingSnapshot(snapshot_blockhash)); chainstates.push_back(&c2); c2.InitCoinsDB( /*cache_size_bytes=*/1 << 23, /*in_memory=*/true, /*should_wipe=*/false); @@ -113,7 +110,6 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager, TestChain100Setup) BOOST_FIXTURE_TEST_CASE(chainstatemanager_rebalance_caches, TestChain100Setup) { ChainstateManager& manager = *m_node.chainman; - CTxMemPool& mempool = *m_node.mempool; size_t max_cache = 10000; manager.m_total_coinsdb_cache = max_cache; @@ -137,7 +133,7 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_rebalance_caches, TestChain100Setup) // Create a snapshot-based chainstate. // CBlockIndex* snapshot_base{WITH_LOCK(manager.GetMutex(), return manager.ActiveChain()[manager.ActiveChain().Height() / 2])}; - Chainstate& c2 = WITH_LOCK(cs_main, return manager.ActivateExistingSnapshot(&mempool, *snapshot_base->phashBlock)); + Chainstate& c2 = WITH_LOCK(cs_main, return manager.ActivateExistingSnapshot(*snapshot_base->phashBlock)); chainstates.push_back(&c2); c2.InitCoinsDB( /*cache_size_bytes=*/1 << 23, /*in_memory=*/true, /*should_wipe=*/false); @@ -423,7 +419,6 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_activate_snapshot, SnapshotTestSetup) BOOST_FIXTURE_TEST_CASE(chainstatemanager_loadblockindex, TestChain100Setup) { ChainstateManager& chainman = *Assert(m_node.chainman); - CTxMemPool& mempool = *m_node.mempool; Chainstate& cs1 = chainman.ActiveChainstate(); int num_indexes{0}; @@ -493,7 +488,7 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_loadblockindex, TestChain100Setup) // Note: cs2's tip is not set when ActivateExistingSnapshot is called. Chainstate& cs2 = WITH_LOCK(::cs_main, - return chainman.ActivateExistingSnapshot(&mempool, *assumed_base->phashBlock)); + return chainman.ActivateExistingSnapshot(*assumed_base->phashBlock)); // Set tip of the fully validated chain to be the validated tip cs1.m_chain.SetTip(*validated_tip); diff --git a/src/validation.cpp b/src/validation.cpp index 01081011b085a..d72b017cc4a37 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -5268,6 +5268,12 @@ bool ChainstateManager::ActivateSnapshot( const bool chaintip_loaded = m_snapshot_chainstate->LoadChainTip(); assert(chaintip_loaded); + // Transfer possession of the mempool to the snapshot chianstate. + // Mempool is empty at this point because we're still in IBD. + Assert(m_active_chainstate->m_mempool->size() == 0); + Assert(!m_snapshot_chainstate->m_mempool); + m_snapshot_chainstate->m_mempool = m_active_chainstate->m_mempool; + m_active_chainstate->m_mempool = nullptr; m_active_chainstate = m_snapshot_chainstate.get(); m_blockman.m_snapshot_height = this->GetSnapshotBaseHeight(); @@ -5747,16 +5753,22 @@ bool ChainstateManager::DetectSnapshotChainstate(CTxMemPool* mempool) LogPrintf("[snapshot] detected active snapshot chainstate (%s) - loading\n", fs::PathToString(*path)); - this->ActivateExistingSnapshot(mempool, *base_blockhash); + this->ActivateExistingSnapshot(*base_blockhash); return true; } -Chainstate& ChainstateManager::ActivateExistingSnapshot(CTxMemPool* mempool, uint256 base_blockhash) +Chainstate& ChainstateManager::ActivateExistingSnapshot(uint256 base_blockhash) { assert(!m_snapshot_chainstate); m_snapshot_chainstate = - std::make_unique(mempool, m_blockman, *this, base_blockhash); + std::make_unique(nullptr, m_blockman, *this, base_blockhash); LogPrintf("[snapshot] switching active chainstate to %s\n", m_snapshot_chainstate->ToString()); + + // Mempool is empty at this point because we're still in IBD. + Assert(m_active_chainstate->m_mempool->size() == 0); + Assert(!m_snapshot_chainstate->m_mempool); + m_snapshot_chainstate->m_mempool = m_active_chainstate->m_mempool; + m_active_chainstate->m_mempool = nullptr; m_active_chainstate = m_snapshot_chainstate.get(); return *m_snapshot_chainstate; } diff --git a/src/validation.h b/src/validation.h index 2aa4221102d22..94a00e44a4eed 100644 --- a/src/validation.h +++ b/src/validation.h @@ -1213,8 +1213,7 @@ class ChainstateManager //! Switch the active chainstate to one based on a UTXO snapshot that was loaded //! previously. - Chainstate& ActivateExistingSnapshot(CTxMemPool* mempool, uint256 base_blockhash) - EXCLUSIVE_LOCKS_REQUIRED(::cs_main); + Chainstate& ActivateExistingSnapshot(uint256 base_blockhash) 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 From 62ac519e718eb7a31dca1102a96ba219fbc7f95d Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Sun, 17 Sep 2023 13:56:12 -0400 Subject: [PATCH 18/26] validation: do not activate snapshot if behind active chain Most easily reviewed with git show --color-moved=dimmed-zebra --color-moved-ws=ignore-all-space Co-authored-by: Ryan Ofsky --- src/test/util/chainstate.h | 18 ++++++++- src/validation.cpp | 81 ++++++++++++++++++++++---------------- 2 files changed, 65 insertions(+), 34 deletions(-) diff --git a/src/test/util/chainstate.h b/src/test/util/chainstate.h index 7f559168703fe..e2a88eacddabf 100644 --- a/src/test/util/chainstate.h +++ b/src/test/util/chainstate.h @@ -109,7 +109,23 @@ CreateAndActivateUTXOSnapshot( 0 == WITH_LOCK(node.chainman->GetMutex(), return node.chainman->ActiveHeight())); } - return node.chainman->ActivateSnapshot(auto_infile, metadata, in_memory_chainstate); + auto& new_active = node.chainman->ActiveChainstate(); + auto* tip = new_active.m_chain.Tip(); + + // Disconnect a block so that the snapshot chainstate will be ahead, otherwise + // it will refuse to activate. + // + // TODO this is a unittest-specific hack, and we should probably rethink how to + // better generate/activate snapshots in unittests. + if (tip->pprev) { + new_active.m_chain.SetTip(*(tip->pprev)); + } + + bool res = node.chainman->ActivateSnapshot(auto_infile, metadata, in_memory_chainstate); + + // Restore the old tip. + new_active.m_chain.SetTip(*tip); + return res; } diff --git a/src/validation.cpp b/src/validation.cpp index d72b017cc4a37..82aafd97f87bd 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -5230,19 +5230,8 @@ bool ChainstateManager::ActivateSnapshot( static_cast(current_coinstip_cache_size * SNAPSHOT_CACHE_PERC)); } - bool snapshot_ok = this->PopulateAndValidateSnapshot( - *snapshot_chainstate, coins_file, metadata); - - // If not in-memory, persist the base blockhash for use during subsequent - // initialization. - if (!in_memory) { - LOCK(::cs_main); - if (!node::WriteSnapshotBaseBlockhash(*snapshot_chainstate)) { - snapshot_ok = false; - } - } - if (!snapshot_ok) { - LOCK(::cs_main); + auto cleanup_bad_snapshot = [&](const char* reason) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { + LogPrintf("[snapshot] activation failed - %s\n", reason); this->MaybeRebalanceCaches(); // PopulateAndValidateSnapshot can return (in error) before the leveldb datadir @@ -5259,30 +5248,48 @@ bool ChainstateManager::ActivateSnapshot( } } return false; - } + }; - { + if (!this->PopulateAndValidateSnapshot(*snapshot_chainstate, coins_file, metadata)) { LOCK(::cs_main); - assert(!m_snapshot_chainstate); - m_snapshot_chainstate.swap(snapshot_chainstate); - const bool chaintip_loaded = m_snapshot_chainstate->LoadChainTip(); - assert(chaintip_loaded); - - // Transfer possession of the mempool to the snapshot chianstate. - // Mempool is empty at this point because we're still in IBD. - Assert(m_active_chainstate->m_mempool->size() == 0); - Assert(!m_snapshot_chainstate->m_mempool); - m_snapshot_chainstate->m_mempool = m_active_chainstate->m_mempool; - m_active_chainstate->m_mempool = nullptr; - m_active_chainstate = m_snapshot_chainstate.get(); - m_blockman.m_snapshot_height = this->GetSnapshotBaseHeight(); - - LogPrintf("[snapshot] successfully activated snapshot %s\n", base_blockhash.ToString()); - LogPrintf("[snapshot] (%.2f MB)\n", - m_snapshot_chainstate->CoinsTip().DynamicMemoryUsage() / (1000 * 1000)); + return cleanup_bad_snapshot("population failed"); + } - this->MaybeRebalanceCaches(); + LOCK(::cs_main); // cs_main required for rest of snapshot activation. + + // Do a final check to ensure that the snapshot chainstate is actually a more + // work chain than the active chainstate; a user could have loaded a snapshot + // very late in the IBD process, and we wouldn't want to load a useless chainstate. + if (!CBlockIndexWorkComparator()(ActiveTip(), snapshot_chainstate->m_chain.Tip())) { + return cleanup_bad_snapshot("work does not exceed active chainstate"); + } + // If not in-memory, persist the base blockhash for use during subsequent + // initialization. + if (!in_memory) { + if (!node::WriteSnapshotBaseBlockhash(*snapshot_chainstate)) { + return cleanup_bad_snapshot("could not write base blockhash"); + } } + + assert(!m_snapshot_chainstate); + m_snapshot_chainstate.swap(snapshot_chainstate); + const bool chaintip_loaded = m_snapshot_chainstate->LoadChainTip(); + assert(chaintip_loaded); + + // Transfer possession of the mempool to the snapshot chainstate. + // Mempool is empty at this point because we're still in IBD. + Assert(m_active_chainstate->m_mempool->size() == 0); + Assert(!m_snapshot_chainstate->m_mempool); + m_snapshot_chainstate->m_mempool = m_active_chainstate->m_mempool; + m_active_chainstate->m_mempool = nullptr; + m_active_chainstate = m_snapshot_chainstate.get(); + m_blockman.m_snapshot_height = this->GetSnapshotBaseHeight(); + + 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; } @@ -5342,6 +5349,14 @@ bool ChainstateManager::PopulateAndValidateSnapshot( const AssumeutxoData& au_data = *maybe_au_data; + // This work comparison is a duplicate check with the one performed later in + // ActivateSnapshot(), but is done so that we avoid doing the long work of staging + // a snapshot that isn't actually usable. + if (WITH_LOCK(::cs_main, return !CBlockIndexWorkComparator()(ActiveTip(), snapshot_start_block))) { + LogPrintf("[snapshot] activation failed - height does not exceed active chainstate\n"); + return false; + } + COutPoint outpoint; Coin coin; const uint64_t coins_count = metadata.m_coins_count; From ce585a9a158476b0ad3296477b922e79f308e795 Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Fri, 29 Mar 2019 15:31:54 -0400 Subject: [PATCH 19/26] rpc: add loadtxoutset Co-authored-by: Sebastian Falbesoner --- doc/design/assumeutxo.md | 2 +- doc/release-notes-27596.md | 19 +++++++ src/rpc/blockchain.cpp | 103 ++++++++++++++++++++++++++++++++++++- src/test/fuzz/rpc.cpp | 1 + 4 files changed, 123 insertions(+), 2 deletions(-) diff --git a/doc/design/assumeutxo.md b/doc/design/assumeutxo.md index 1492877e62224..8068a93f27a7d 100644 --- a/doc/design/assumeutxo.md +++ b/doc/design/assumeutxo.md @@ -3,7 +3,7 @@ Assumeutxo is a feature that allows fast bootstrapping of a validating bitcoind instance with a very similar security model to assumevalid. -The RPC commands `dumptxoutset` and `loadtxoutset` (yet to be merged) are used to +The RPC commands `dumptxoutset` and `loadtxoutset` are used to respectively generate and load UTXO snapshots. The utility script `./contrib/devtools/utxo_snapshot.sh` may be of use. diff --git a/doc/release-notes-27596.md b/doc/release-notes-27596.md index 4f96adb0f354f..da96b36189c47 100644 --- a/doc/release-notes-27596.md +++ b/doc/release-notes-27596.md @@ -5,3 +5,22 @@ When using assumeutxo with `-prune`, the prune budget may be exceeded if it is s lower than 1100MB (i.e. `MIN_DISK_SPACE_FOR_BLOCK_FILES * 2`). Prune budget is normally split evenly across each chainstate, unless the resulting prune budget per chainstate is beneath `MIN_DISK_SPACE_FOR_BLOCK_FILES` in which case that value will be used. + +RPC +--- + +`loadtxoutset` has been added, which allows loading a UTXO snapshot of the format +generated by `dumptxoutset`. Once this snapshot is loaded, its contents will be +deserialized into a second chainstate data structure, which is then used to sync to +the network's tip under a security model very much like `assumevalid`. + +Meanwhile, the original chainstate will complete the initial block download process in +the background, eventually validating up to the block that the snapshot is based upon. + +The result is a usable bitcoind instance that is current with the network tip in a +matter of minutes rather than hours. UTXO snapshot are typically obtained via +third-party sources (HTTP, torrent, etc.) which is reasonable since their contents +are always checked by hash. + +You can find more information on this process in the `assumeutxo` design +document (). diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index f4d88e4209f6e..c7ffa0a0b2e98 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -2699,6 +2700,105 @@ UniValue CreateUTXOSnapshot( return result; } +static RPCHelpMan loadtxoutset() +{ + return RPCHelpMan{ + "loadtxoutset", + "Load the serialized UTXO set from disk.\n" + "Once this snapshot is loaded, its contents will be " + "deserialized into a second chainstate data structure, which is then used to sync to " + "the network's tip under a security model very much like `assumevalid`. " + "Meanwhile, the original chainstate will complete the initial block download process in " + "the background, eventually validating up to the block that the snapshot is based upon.\n\n" + + "The result is a usable bitcoind instance that is current with the network tip in a " + "matter of minutes rather than hours. UTXO snapshot are typically obtained from " + "third-party sources (HTTP, torrent, etc.) which is reasonable since their " + "contents are always checked by hash.\n\n" + + "You can find more information on this process in the `assumeutxo` design " + "document ().", + { + {"path", + RPCArg::Type::STR, + RPCArg::Optional::NO, + "path to the snapshot file. If relative, will be prefixed by datadir."}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::NUM, "coins_loaded", "the number of coins loaded from the snapshot"}, + {RPCResult::Type::STR_HEX, "tip_hash", "the hash of the base of the snapshot"}, + {RPCResult::Type::NUM, "base_height", "the height of the base of the snapshot"}, + {RPCResult::Type::STR, "path", "the absolute path that the snapshot was loaded from"}, + } + }, + RPCExamples{ + HelpExampleCli("loadtxoutset", "utxo.dat") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + NodeContext& node = EnsureAnyNodeContext(request.context); + fs::path path{AbsPathForConfigVal(EnsureArgsman(node), fs::u8path(request.params[0].get_str()))}; + + FILE* file{fsbridge::fopen(path, "rb")}; + AutoFile afile{file}; + if (afile.IsNull()) { + throw JSONRPCError( + RPC_INVALID_PARAMETER, + "Couldn't open file " + path.u8string() + " for reading."); + } + + SnapshotMetadata metadata; + afile >> metadata; + + uint256 base_blockhash = metadata.m_base_blockhash; + int max_secs_to_wait_for_headers = 60 * 10; + CBlockIndex* snapshot_start_block = nullptr; + + LogPrintf("[snapshot] waiting to see blockheader %s in headers chain before snapshot activation\n", + base_blockhash.ToString()); + + ChainstateManager& chainman = *node.chainman; + + while (max_secs_to_wait_for_headers > 0) { + snapshot_start_block = WITH_LOCK(::cs_main, + return chainman.m_blockman.LookupBlockIndex(base_blockhash)); + max_secs_to_wait_for_headers -= 1; + + if (!IsRPCRunning()) { + throw JSONRPCError(RPC_CLIENT_NOT_CONNECTED, "Shutting down"); + } + + if (!snapshot_start_block) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + } else { + break; + } + } + + if (!snapshot_start_block) { + LogPrintf("[snapshot] timed out waiting for snapshot start blockheader %s\n", + base_blockhash.ToString()); + throw JSONRPCError( + RPC_INTERNAL_ERROR, + "Timed out waiting for base block header to appear in headers chain"); + } + if (!chainman.ActivateSnapshot(afile, metadata, false)) { + throw JSONRPCError(RPC_INTERNAL_ERROR, "Unable to load UTXO snapshot " + fs::PathToString(path)); + } + CBlockIndex* new_tip{WITH_LOCK(::cs_main, return chainman.ActiveTip())}; + + UniValue result(UniValue::VOBJ); + result.pushKV("coins_loaded", metadata.m_coins_count); + result.pushKV("tip_hash", new_tip->GetBlockHash().ToString()); + result.pushKV("base_height", new_tip->nHeight); + result.pushKV("path", fs::PathToString(path)); + return result; +}, + }; +} + void RegisterBlockchainRPCCommands(CRPCTable& t) { static const CRPCCommand commands[]{ @@ -2722,13 +2822,14 @@ void RegisterBlockchainRPCCommands(CRPCTable& t) {"blockchain", &scantxoutset}, {"blockchain", &scanblocks}, {"blockchain", &getblockfilter}, + {"blockchain", &dumptxoutset}, + {"blockchain", &loadtxoutset}, {"hidden", &invalidateblock}, {"hidden", &reconsiderblock}, {"hidden", &waitfornewblock}, {"hidden", &waitforblock}, {"hidden", &waitforblockheight}, {"hidden", &syncwithvalidationinterfacequeue}, - {"hidden", &dumptxoutset}, }; for (const auto& c : commands) { t.appendCommand(c.name, &c); diff --git a/src/test/fuzz/rpc.cpp b/src/test/fuzz/rpc.cpp index 7e9a18e1d0bbe..2ef3fe1b4779a 100644 --- a/src/test/fuzz/rpc.cpp +++ b/src/test/fuzz/rpc.cpp @@ -80,6 +80,7 @@ const std::vector RPC_COMMANDS_NOT_SAFE_FOR_FUZZING{ "gettxoutproof", // avoid prohibitively slow execution "importmempool", // avoid reading from disk "importwallet", // avoid reading from disk + "loadtxoutset", // avoid reading from disk "loadwallet", // avoid reading from disk "savemempool", // disabled as a precautionary measure: may take a file path argument in the future "setban", // avoid DNS lookups From bb0585779472962f40d9cdd9c6532132850d371c Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Fri, 8 Sep 2023 06:29:32 -0400 Subject: [PATCH 20/26] refuse to activate a UTXO snapshot if mempool not empty This ensures that we avoid any unexpected conditions inherent in transferring non-empty mempools across chainstates. Note that this should never happen in practice given that snapshot activation will not occur outside of IBD, based upon the height checks in `loadtxoutset`. --- src/validation.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/validation.cpp b/src/validation.cpp index 82aafd97f87bd..30b3dde74f010 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -5185,6 +5185,14 @@ bool ChainstateManager::ActivateSnapshot( return false; } + { + LOCK(::cs_main); + if (Assert(m_active_chainstate->GetMempool())->size() > 0) { + LogPrintf("[snapshot] can't activate a snapshot when mempool not empty\n"); + return false; + } + } + int64_t current_coinsdb_cache_size{0}; int64_t current_coinstip_cache_size{0}; From 0f64bac6030334d798ae205cd7af4bf248feddd9 Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Fri, 29 Mar 2019 17:55:08 -0400 Subject: [PATCH 21/26] rpc: add getchainstates Co-authored-by: Ryan Ofsky --- doc/release-notes-27596.md | 2 ++ src/rpc/blockchain.cpp | 74 ++++++++++++++++++++++++++++++++++++++ src/test/fuzz/rpc.cpp | 1 + 3 files changed, 77 insertions(+) diff --git a/doc/release-notes-27596.md b/doc/release-notes-27596.md index da96b36189c47..799b82643fec6 100644 --- a/doc/release-notes-27596.md +++ b/doc/release-notes-27596.md @@ -24,3 +24,5 @@ are always checked by hash. You can find more information on this process in the `assumeutxo` design document (). + +`getchainstates` has been added to aid in monitoring the assumeutxo sync process. diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index c7ffa0a0b2e98..0f4941b40ccfd 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -2799,6 +2799,79 @@ static RPCHelpMan loadtxoutset() }; } +const std::vector RPCHelpForChainstate{ + {RPCResult::Type::NUM, "blocks", "number of blocks in this chainstate"}, + {RPCResult::Type::STR_HEX, "bestblockhash", "blockhash of the tip"}, + {RPCResult::Type::NUM, "difficulty", "difficulty of the tip"}, + {RPCResult::Type::NUM, "verificationprogress", "progress towards the network tip"}, + {RPCResult::Type::STR_HEX, "snapshot_blockhash", /*optional=*/true, "the base block of the snapshot this chainstate is based on, if any"}, + {RPCResult::Type::NUM, "coins_db_cache_bytes", "size of the coinsdb cache"}, + {RPCResult::Type::NUM, "coins_tip_cache_bytes", "size of the coinstip cache"}, +}; + +static RPCHelpMan getchainstates() +{ +return RPCHelpMan{ + "getchainstates", + "\nReturn information about chainstates.\n", + {}, + RPCResult{ + RPCResult::Type::OBJ, "", "", { + {RPCResult::Type::NUM, "headers", "the number of headers seen so far"}, + {RPCResult::Type::OBJ, "normal", /*optional=*/true, "fully validated chainstate containing blocks this node has validated starting from the genesis block", RPCHelpForChainstate}, + {RPCResult::Type::OBJ, "snapshot", /*optional=*/true, "only present if an assumeutxo snapshot is loaded. Partially validated chainstate containing blocks this node has validated starting from the snapshot. After the snapshot is validated (when the 'normal' chainstate advances far enough to validate it), this chainstate will replace and become the 'normal' chainstate.", RPCHelpForChainstate}, + } + }, + RPCExamples{ + HelpExampleCli("getchainstates", "") + + HelpExampleRpc("getchainstates", "") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + LOCK(cs_main); + UniValue obj(UniValue::VOBJ); + + NodeContext& node = EnsureAnyNodeContext(request.context); + ChainstateManager& chainman = *node.chainman; + + auto make_chain_data = [&](const Chainstate& cs) EXCLUSIVE_LOCKS_REQUIRED(::cs_main) { + AssertLockHeld(::cs_main); + UniValue data(UniValue::VOBJ); + if (!cs.m_chain.Tip()) { + return data; + } + const CChain& chain = cs.m_chain; + const CBlockIndex* tip = chain.Tip(); + + data.pushKV("blocks", (int)chain.Height()); + data.pushKV("bestblockhash", tip->GetBlockHash().GetHex()); + data.pushKV("difficulty", (double)GetDifficulty(tip)); + data.pushKV("verificationprogress", GuessVerificationProgress(Params().TxData(), tip)); + data.pushKV("coins_db_cache_bytes", cs.m_coinsdb_cache_size_bytes); + data.pushKV("coins_tip_cache_bytes", cs.m_coinstip_cache_size_bytes); + if (cs.m_from_snapshot_blockhash) { + data.pushKV("snapshot_blockhash", cs.m_from_snapshot_blockhash->ToString()); + } + return data; + }; + + if (chainman.GetAll().size() > 1) { + for (Chainstate* chainstate : chainman.GetAll()) { + obj.pushKV( + chainstate->m_from_snapshot_blockhash ? "snapshot" : "normal", + make_chain_data(*chainstate)); + } + } else { + obj.pushKV("normal", make_chain_data(chainman.ActiveChainstate())); + } + obj.pushKV("headers", chainman.m_best_header ? chainman.m_best_header->nHeight : -1); + + return obj; +} + }; +} + + void RegisterBlockchainRPCCommands(CRPCTable& t) { static const CRPCCommand commands[]{ @@ -2824,6 +2897,7 @@ void RegisterBlockchainRPCCommands(CRPCTable& t) {"blockchain", &getblockfilter}, {"blockchain", &dumptxoutset}, {"blockchain", &loadtxoutset}, + {"blockchain", &getchainstates}, {"hidden", &invalidateblock}, {"hidden", &reconsiderblock}, {"hidden", &waitfornewblock}, diff --git a/src/test/fuzz/rpc.cpp b/src/test/fuzz/rpc.cpp index 2ef3fe1b4779a..27bb60d6b6125 100644 --- a/src/test/fuzz/rpc.cpp +++ b/src/test/fuzz/rpc.cpp @@ -123,6 +123,7 @@ const std::vector RPC_COMMANDS_SAFE_FOR_FUZZING{ "getblockstats", "getblocktemplate", "getchaintips", + "getchainstates", "getchaintxstats", "getconnectioncount", "getdeploymentinfo", From 42cae39356fd20d521aaf99aff1ed85856f3c9f3 Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Thu, 17 Jun 2021 16:09:38 -0400 Subject: [PATCH 22/26] test: add feature_assumeutxo functional test Most ideas for test improvements (TODOs) provided by Russ Yanofsky. --- src/kernel/chainparams.cpp | 7 + test/functional/feature_assumeutxo.py | 246 ++++++++++++++++++ .../test_framework/test_framework.py | 4 + test/functional/test_runner.py | 1 + 4 files changed, 258 insertions(+) create mode 100755 test/functional/feature_assumeutxo.py diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp index ca418fc6abc07..3ae24d0eb50bd 100644 --- a/src/kernel/chainparams.cpp +++ b/src/kernel/chainparams.cpp @@ -484,6 +484,13 @@ class CRegTestParams : public CChainParams .nChainTx = 110, .blockhash = uint256S("0x696e92821f65549c7ee134edceeeeaaa4105647a3c4fd9f298c0aec0ab50425c") }, + { + // For use by test/functional/feature_assumeutxo.py + .height = 299, + .hash_serialized = AssumeutxoHash{uint256S("0xef45ccdca5898b6c2145e4581d2b88c56564dd389e4bd75a1aaf6961d3edd3c0")}, + .nChainTx = 300, + .blockhash = uint256S("0x7e0517ef3ea6ecbed9117858e42eedc8eb39e8698a38dcbd1b3962a283233f4c") + }, }; chainTxData = ChainTxData{ diff --git a/test/functional/feature_assumeutxo.py b/test/functional/feature_assumeutxo.py new file mode 100755 index 0000000000000..be1aa1899380a --- /dev/null +++ b/test/functional/feature_assumeutxo.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Test for assumeutxo, a means of quickly bootstrapping a node using +a serialized version of the UTXO set at a certain height, which corresponds +to a hash that has been compiled into bitcoind. + +The assumeutxo value generated and used here is committed to in +`CRegTestParams::m_assumeutxo_data` in `src/chainparams.cpp`. + +## Possible test improvements + +- TODO: test submitting a transaction and verifying it appears in mempool +- TODO: test what happens with -reindex and -reindex-chainstate before the + snapshot is validated, and make sure it's deleted successfully. + +Interesting test cases could be loading an assumeutxo snapshot file with: + +- TODO: An invalid hash +- TODO: Valid hash but invalid snapshot file (bad coin height or truncated file or + bad other serialization) +- TODO: Valid snapshot file, but referencing an unknown block +- TODO: Valid snapshot file, but referencing a snapshot block that turns out to be + invalid, or has an invalid parent +- TODO: Valid snapshot file and snapshot block, but the block is not on the + most-work chain + +Interesting starting states could be loading a snapshot when the current chain tip is: + +- TODO: An ancestor of snapshot block +- TODO: Not an ancestor of the snapshot block but has less work +- TODO: The snapshot block +- TODO: A descendant of the snapshot block +- TODO: Not an ancestor or a descendant of the snapshot block and has more work + +""" +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import assert_equal, wait_until_helper + +START_HEIGHT = 199 +SNAPSHOT_BASE_HEIGHT = 299 +FINAL_HEIGHT = 399 +COMPLETE_IDX = {'synced': True, 'best_block_height': FINAL_HEIGHT} + + +class AssumeutxoTest(BitcoinTestFramework): + + def set_test_params(self): + """Use the pregenerated, deterministic chain up to height 199.""" + self.num_nodes = 3 + self.rpc_timeout = 120 + self.extra_args = [ + [], + ["-fastprune", "-prune=1", "-blockfilterindex=1", "-coinstatsindex=1"], + ["-txindex=1", "-blockfilterindex=1", "-coinstatsindex=1"], + ] + + def setup_network(self): + """Start with the nodes disconnected so that one can generate a snapshot + including blocks the other hasn't yet seen.""" + self.add_nodes(3) + self.start_nodes(extra_args=self.extra_args) + + def run_test(self): + """ + Bring up two (disconnected) nodes, mine some new blocks on the first, + and generate a UTXO snapshot. + + Load the snapshot into the second, ensure it syncs to tip and completes + background validation when connected to the first. + """ + n0 = self.nodes[0] + n1 = self.nodes[1] + n2 = self.nodes[2] + + # Mock time for a deterministic chain + for n in self.nodes: + n.setmocktime(n.getblockheader(n.getbestblockhash())['time']) + + self.sync_blocks() + + def no_sync(): + pass + + # Generate a series of blocks that `n0` will have in the snapshot, + # but that n1 doesn't yet see. In order for the snapshot to activate, + # though, we have to ferry over the new headers to n1 so that it + # isn't waiting forever to see the header of the snapshot's base block + # while disconnected from n0. + for i in range(100): + self.generate(n0, nblocks=1, sync_fun=no_sync) + newblock = n0.getblock(n0.getbestblockhash(), 0) + + # make n1 aware of the new header, but don't give it the block. + n1.submitheader(newblock) + n2.submitheader(newblock) + + # Ensure everyone is seeing the same headers. + for n in self.nodes: + assert_equal(n.getblockchaininfo()["headers"], SNAPSHOT_BASE_HEIGHT) + + self.log.info("-- Testing assumeutxo + some indexes + pruning") + + assert_equal(n0.getblockcount(), SNAPSHOT_BASE_HEIGHT) + assert_equal(n1.getblockcount(), START_HEIGHT) + + self.log.info(f"Creating a UTXO snapshot at height {SNAPSHOT_BASE_HEIGHT}") + dump_output = n0.dumptxoutset('utxos.dat') + + assert_equal( + dump_output['txoutset_hash'], + 'ef45ccdca5898b6c2145e4581d2b88c56564dd389e4bd75a1aaf6961d3edd3c0') + assert_equal(dump_output['nchaintx'], 300) + assert_equal(n0.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT) + + # Mine more blocks on top of the snapshot that n1 hasn't yet seen. This + # will allow us to test n1's sync-to-tip on top of a snapshot. + self.generate(n0, nblocks=100, sync_fun=no_sync) + + assert_equal(n0.getblockcount(), FINAL_HEIGHT) + assert_equal(n1.getblockcount(), START_HEIGHT) + + assert_equal(n0.getblockchaininfo()["blocks"], FINAL_HEIGHT) + + self.log.info(f"Loading snapshot into second node from {dump_output['path']}") + loaded = n1.loadtxoutset(dump_output['path']) + assert_equal(loaded['coins_loaded'], SNAPSHOT_BASE_HEIGHT) + assert_equal(loaded['base_height'], SNAPSHOT_BASE_HEIGHT) + + monitor = n1.getchainstates() + assert_equal(monitor['normal']['blocks'], START_HEIGHT) + assert_equal(monitor['snapshot']['blocks'], SNAPSHOT_BASE_HEIGHT) + assert_equal(monitor['snapshot']['snapshot_blockhash'], dump_output['base_hash']) + + assert_equal(n1.getblockchaininfo()["blocks"], SNAPSHOT_BASE_HEIGHT) + + PAUSE_HEIGHT = FINAL_HEIGHT - 40 + + self.log.info("Restarting node to stop at height %d", PAUSE_HEIGHT) + self.restart_node(1, extra_args=[ + f"-stopatheight={PAUSE_HEIGHT}", *self.extra_args[1]]) + + # Finally connect the nodes and let them sync. + self.connect_nodes(0, 1) + + n1.wait_until_stopped(timeout=5) + + self.log.info("Checking that blocks are segmented on disk") + assert self.has_blockfile(n1, "00000"), "normal blockfile missing" + assert self.has_blockfile(n1, "00001"), "assumed blockfile missing" + assert not self.has_blockfile(n1, "00002"), "too many blockfiles" + + self.log.info("Restarted node before snapshot validation completed, reloading...") + self.restart_node(1, extra_args=self.extra_args[1]) + self.connect_nodes(0, 1) + + self.log.info(f"Ensuring snapshot chain syncs to tip. ({FINAL_HEIGHT})") + wait_until_helper(lambda: n1.getchainstates()['snapshot']['blocks'] == FINAL_HEIGHT) + self.sync_blocks(nodes=(n0, n1)) + + self.log.info("Ensuring background validation completes") + # N.B.: the `snapshot` key disappears once the background validation is complete. + wait_until_helper(lambda: not n1.getchainstates().get('snapshot')) + + # Ensure indexes have synced. + completed_idx_state = { + 'basic block filter index': COMPLETE_IDX, + 'coinstatsindex': COMPLETE_IDX, + } + self.wait_until(lambda: n1.getindexinfo() == completed_idx_state) + + + for i in (0, 1): + n = self.nodes[i] + self.log.info(f"Restarting node {i} to ensure (Check|Load)BlockIndex passes") + self.restart_node(i, extra_args=self.extra_args[i]) + + assert_equal(n.getblockchaininfo()["blocks"], FINAL_HEIGHT) + + assert_equal(n.getchainstates()['normal']['blocks'], FINAL_HEIGHT) + assert_equal(n.getchainstates().get('snapshot'), None) + + if i != 0: + # Ensure indexes have synced for the assumeutxo node + self.wait_until(lambda: n.getindexinfo() == completed_idx_state) + + + # Node 2: all indexes + reindex + # ----------------------------- + + self.log.info("-- Testing all indexes + reindex") + assert_equal(n2.getblockcount(), START_HEIGHT) + + self.log.info(f"Loading snapshot into third node from {dump_output['path']}") + loaded = n2.loadtxoutset(dump_output['path']) + assert_equal(loaded['coins_loaded'], SNAPSHOT_BASE_HEIGHT) + assert_equal(loaded['base_height'], SNAPSHOT_BASE_HEIGHT) + + monitor = n2.getchainstates() + assert_equal(monitor['normal']['blocks'], START_HEIGHT) + assert_equal(monitor['snapshot']['blocks'], SNAPSHOT_BASE_HEIGHT) + assert_equal(monitor['snapshot']['snapshot_blockhash'], dump_output['base_hash']) + + self.connect_nodes(0, 2) + wait_until_helper(lambda: n2.getchainstates()['snapshot']['blocks'] == FINAL_HEIGHT) + self.sync_blocks() + + self.log.info("Ensuring background validation completes") + wait_until_helper(lambda: not n2.getchainstates().get('snapshot')) + + completed_idx_state = { + 'basic block filter index': COMPLETE_IDX, + 'coinstatsindex': COMPLETE_IDX, + 'txindex': COMPLETE_IDX, + } + self.wait_until(lambda: n2.getindexinfo() == completed_idx_state) + + for i in (0, 2): + n = self.nodes[i] + self.log.info(f"Restarting node {i} to ensure (Check|Load)BlockIndex passes") + self.restart_node(i, extra_args=self.extra_args[i]) + + assert_equal(n.getblockchaininfo()["blocks"], FINAL_HEIGHT) + + assert_equal(n.getchainstates()['normal']['blocks'], FINAL_HEIGHT) + assert_equal(n.getchainstates().get('snapshot'), None) + + if i != 0: + # Ensure indexes have synced for the assumeutxo node + self.wait_until(lambda: n.getindexinfo() == completed_idx_state) + + self.log.info("Test -reindex-chainstate of an assumeutxo-synced node") + self.restart_node(2, extra_args=[ + '-reindex-chainstate=1', *self.extra_args[2]]) + assert_equal(n2.getblockchaininfo()["blocks"], FINAL_HEIGHT) + wait_until_helper(lambda: n2.getblockcount() == FINAL_HEIGHT) + + self.log.info("Test -reindex of an assumeutxo-synced node") + self.restart_node(2, extra_args=['-reindex=1', *self.extra_args[2]]) + self.connect_nodes(0, 2) + wait_until_helper(lambda: n2.getblockcount() == FINAL_HEIGHT) + + +if __name__ == '__main__': + AssumeutxoTest().main() diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py index 73e7516ea7e48..73635b4397e1e 100755 --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -979,3 +979,7 @@ def is_sqlite_compiled(self): def is_bdb_compiled(self): """Checks whether the wallet module was compiled with BDB support.""" return self.config["components"].getboolean("USE_BDB") + + def has_blockfile(self, node, filenum: str): + blocksdir = os.path.join(node.datadir, self.chain, 'blocks', '') + return os.path.isfile(os.path.join(blocksdir, f"blk{filenum}.dat")) diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index 32aee3aa8000e..9a0b5c6f0a678 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -324,6 +324,7 @@ 'wallet_coinbase_category.py --descriptors', 'feature_filelock.py', 'feature_loadblock.py', + 'feature_assumeutxo.py', 'p2p_dos_header_tree.py', 'p2p_add_connections.py', 'feature_bind_port_discover.py', From 7ee46a755f1d57ce9d51975d3b54dc9ac3d08d52 Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Wed, 16 Jun 2021 12:09:29 -0400 Subject: [PATCH 23/26] contrib: add script to demo/test assumeutxo Add the script to the shellcheck exception list since the quoted variables rule needs to be violated in order to get bitcoind to pick up on $CHAIN_HACK_FLAGS. --- contrib/devtools/test_utxo_snapshots.sh | 200 ++++++++++++++++++++++++ test/lint/lint-shell.py | 8 +- 2 files changed, 206 insertions(+), 2 deletions(-) create mode 100755 contrib/devtools/test_utxo_snapshots.sh diff --git a/contrib/devtools/test_utxo_snapshots.sh b/contrib/devtools/test_utxo_snapshots.sh new file mode 100755 index 0000000000000..d4c49bf098f28 --- /dev/null +++ b/contrib/devtools/test_utxo_snapshots.sh @@ -0,0 +1,200 @@ +#!/usr/bin/env bash +# Demonstrate the creation and usage of UTXO snapshots. +# +# A server node starts up, IBDs up to a certain height, then generates a UTXO +# snapshot at that point. +# +# The server then downloads more blocks (to create a diff from the snapshot). +# +# We bring a client up, load the UTXO snapshot, and we show the client sync to +# the "network tip" and then start a background validation of the snapshot it +# loaded. We see the background validation chainstate removed after validation +# completes. +# + +export LC_ALL=C +set -e + +BASE_HEIGHT=${1:-30000} +INCREMENTAL_HEIGHT=20000 +FINAL_HEIGHT=$(($BASE_HEIGHT + $INCREMENTAL_HEIGHT)) + +SERVER_DATADIR="$(pwd)/utxodemo-data-server-$BASE_HEIGHT" +CLIENT_DATADIR="$(pwd)/utxodemo-data-client-$BASE_HEIGHT" +UTXO_DAT_FILE="$(pwd)/utxo.$BASE_HEIGHT.dat" + +# Chosen to try to not interfere with any running bitcoind processes. +SERVER_PORT=8633 +SERVER_RPC_PORT=8632 + +CLIENT_PORT=8733 +CLIENT_RPC_PORT=8732 + +SERVER_PORTS="-port=${SERVER_PORT} -rpcport=${SERVER_RPC_PORT}" +CLIENT_PORTS="-port=${CLIENT_PORT} -rpcport=${CLIENT_RPC_PORT}" + +# Ensure the client exercises all indexes to test that snapshot use works +# properly with indexes. +ALL_INDEXES="-txindex -coinstatsindex -blockfilterindex=1" + +if ! command -v jq >/dev/null ; then + echo "This script requires jq to parse JSON RPC output. Please install it." + echo "(e.g. sudo apt install jq)" + exit 1 +fi + +DUMP_OUTPUT="dumptxoutset-output-$BASE_HEIGHT.json" + +finish() { + echo + echo "Killing server and client PIDs ($SERVER_PID, $CLIENT_PID) and cleaning up datadirs" + echo + rm -f "$UTXO_DAT_FILE" "$DUMP_OUTPUT" + rm -rf "$SERVER_DATADIR" "$CLIENT_DATADIR" + kill -9 "$SERVER_PID" "$CLIENT_PID" +} + +trap finish EXIT + +# Need to specify these to trick client into accepting server as a peer +# it can IBD from, otherwise the default values prevent IBD from the server node. +EARLY_IBD_FLAGS="-maxtipage=9223372036854775207 -minimumchainwork=0x00" + +server_rpc() { + ./src/bitcoin-cli -rpcport=$SERVER_RPC_PORT -datadir="$SERVER_DATADIR" "$@" +} +client_rpc() { + ./src/bitcoin-cli -rpcport=$CLIENT_RPC_PORT -datadir="$CLIENT_DATADIR" "$@" +} +server_sleep_til_boot() { + while ! server_rpc ping >/dev/null 2>&1; do sleep 0.1; done +} +client_sleep_til_boot() { + while ! client_rpc ping >/dev/null 2>&1; do sleep 0.1; done +} + +mkdir -p "$SERVER_DATADIR" "$CLIENT_DATADIR" + +echo "Hi, welcome to the assumeutxo demo/test" +echo +echo "We're going to" +echo +echo " - start up a 'server' node, sync it via mainnet IBD to height ${BASE_HEIGHT}" +echo " - create a UTXO snapshot at that height" +echo " - IBD ${INCREMENTAL_HEIGHT} more blocks on top of that" +echo +echo "then we'll demonstrate assumeutxo by " +echo +echo " - starting another node (the 'client') and loading the snapshot in" +echo " * first you'll have to modify the code slightly (chainparams) and recompile" +echo " * don't worry, we'll make it easy" +echo " - observing the client sync ${INCREMENTAL_HEIGHT} blocks on top of the snapshot from the server" +echo " - observing the client validate the snapshot chain via background IBD" +echo +read -p "Press [enter] to continue" _ + +echo +echo "-- Starting the demo. You might want to run the two following commands in" +echo " separate terminal windows:" +echo +echo " watch -n0.1 tail -n 30 $SERVER_DATADIR/debug.log" +echo " watch -n0.1 tail -n 30 $CLIENT_DATADIR/debug.log" +echo +read -p "Press [enter] to continue" _ + +echo +echo "-- IBDing the blocks (height=$BASE_HEIGHT) required to the server node..." +./src/bitcoind -logthreadnames=1 $SERVER_PORTS \ + -datadir="$SERVER_DATADIR" $EARLY_IBD_FLAGS -stopatheight="$BASE_HEIGHT" >/dev/null + +echo +echo "-- Creating snapshot at ~ height $BASE_HEIGHT ($UTXO_DAT_FILE)..." +sleep 2 +./src/bitcoind -logthreadnames=1 $SERVER_PORTS \ + -datadir="$SERVER_DATADIR" $EARLY_IBD_FLAGS -connect=0 -listen=0 >/dev/null & +SERVER_PID="$!" + +server_sleep_til_boot +server_rpc dumptxoutset "$UTXO_DAT_FILE" > "$DUMP_OUTPUT" +cat "$DUMP_OUTPUT" +kill -9 "$SERVER_PID" + +RPC_BASE_HEIGHT=$(jq -r .base_height < "$DUMP_OUTPUT") +RPC_AU=$(jq -r .txoutset_hash < "$DUMP_OUTPUT") +RPC_NCHAINTX=$(jq -r .nchaintx < "$DUMP_OUTPUT") +RPC_BLOCKHASH=$(jq -r .base_hash < "$DUMP_OUTPUT") + +# Wait for server to shutdown... +while server_rpc ping >/dev/null 2>&1; do sleep 0.1; done + +echo +echo "-- Now: add the following to CMainParams::m_assumeutxo_data" +echo " in src/kernel/chainparams.cpp, and recompile:" +echo +echo " {${RPC_BASE_HEIGHT}, AssumeutxoHash{uint256S(\"0x${RPC_AU}\")}, ${RPC_NCHAINTX}, uint256S(\"0x${RPC_BLOCKHASH}\")}," +echo +echo +echo "-- IBDing more blocks to the server node (height=$FINAL_HEIGHT) so there is a diff between snapshot and tip..." +./src/bitcoind $SERVER_PORTS -logthreadnames=1 -datadir="$SERVER_DATADIR" \ + $EARLY_IBD_FLAGS -stopatheight="$FINAL_HEIGHT" >/dev/null + +echo +echo "-- Starting the server node to provide blocks to the client node..." +./src/bitcoind $SERVER_PORTS -logthreadnames=1 -debug=net -datadir="$SERVER_DATADIR" \ + $EARLY_IBD_FLAGS -connect=0 -listen=1 >/dev/null & +SERVER_PID="$!" +server_sleep_til_boot + +echo +echo "-- Okay, what you're about to see is the client starting up and activating the snapshot." +echo " I'm going to display the top 14 log lines from the client on top of an RPC called" +echo " getchainstates, which is like getblockchaininfo but for both the snapshot and " +echo " background validation chainstates." +echo +echo " You're going to first see the snapshot chainstate sync to the server's tip, then" +echo " the background IBD chain kicks in to validate up to the base of the snapshot." +echo +echo " Once validation of the snapshot is done, you should see log lines indicating" +echo " that we've deleted the background validation chainstate." +echo +echo " Once everything completes, exit the watch command with CTRL+C." +echo +read -p "When you're ready for all this, hit [enter]" _ + +echo +echo "-- Starting the client node to get headers from the server, then load the snapshot..." +./src/bitcoind $CLIENT_PORTS $ALL_INDEXES -logthreadnames=1 -datadir="$CLIENT_DATADIR" \ + -connect=0 -addnode=127.0.0.1:$SERVER_PORT -debug=net $EARLY_IBD_FLAGS >/dev/null & +CLIENT_PID="$!" +client_sleep_til_boot + +echo +echo "-- Initial state of the client:" +client_rpc getchainstates + +echo +echo "-- Loading UTXO snapshot into client..." +client_rpc loadtxoutset "$UTXO_DAT_FILE" + +watch -n 0.3 "( tail -n 14 $CLIENT_DATADIR/debug.log ; echo ; ./src/bitcoin-cli -rpcport=$CLIENT_RPC_PORT -datadir=$CLIENT_DATADIR getchainstates) | cat" + +echo +echo "-- Okay, now I'm going to restart the client to make sure that the snapshot chain reloads " +echo " as the main chain properly..." +echo +echo " Press CTRL+C after you're satisfied to exit the demo" +echo +read -p "Press [enter] to continue" + +while kill -0 "$CLIENT_PID"; do + sleep 1 +done +./src/bitcoind $CLIENT_PORTS $ALL_INDEXES -logthreadnames=1 -datadir="$CLIENT_DATADIR" -connect=0 \ + -addnode=127.0.0.1:$SERVER_PORT "$EARLY_IBD_FLAGS" >/dev/null & +CLIENT_PID="$!" +client_sleep_til_boot + +watch -n 0.3 "( tail -n 14 $CLIENT_DATADIR/debug.log ; echo ; ./src/bitcoin-cli -rpcport=$CLIENT_RPC_PORT -datadir=$CLIENT_DATADIR getchainstates) | cat" + +echo +echo "-- Done!" diff --git a/test/lint/lint-shell.py b/test/lint/lint-shell.py index 1646bf0d3ed97..db84ca3d394e2 100755 --- a/test/lint/lint-shell.py +++ b/test/lint/lint-shell.py @@ -67,9 +67,13 @@ def main(): '*.sh', ] files = get_files(files_cmd) - # remove everything that doesn't match this regex reg = re.compile(r'src/[leveldb,secp256k1,minisketch]') - files[:] = [file for file in files if not reg.match(file)] + + def should_exclude(fname: str) -> bool: + return bool(reg.match(fname)) or 'test_utxo_snapshots.sh' in fname + + # remove everything that doesn't match this regex + files[:] = [file for file in files if not should_exclude(file)] # build the `shellcheck` command shellcheck_cmd = [ From 99839bbfa7110c7abf22e587ae2f72c9c57d3c85 Mon Sep 17 00:00:00 2001 From: James O'Beirne Date: Mon, 11 Sep 2023 13:41:28 -0400 Subject: [PATCH 24/26] doc: add note about confusing HaveTxsDownloaded name --- src/chain.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/chain.h b/src/chain.h index 7806720ce9e8b..78b06719f43a4 100644 --- a/src/chain.h +++ b/src/chain.h @@ -276,6 +276,12 @@ class CBlockIndex * * Does not imply the transactions are consensus-valid (ConnectTip might fail) * Does not imply the transactions are still stored on disk. (IsBlockPruned might return true) + * + * Note that this will be true for the snapshot base block, if one is loaded (and + * all subsequent assumed-valid blocks) since its nChainTx value will have been set + * manually based on the related AssumeutxoData entry. + * + * TODO: potentially change the name of this based on the fact above. */ bool HaveTxsDownloaded() const { return nChainTx != 0; } From b8cafe38713cbf10d15459042f7f911bcc1b1e4e Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Thu, 21 Sep 2023 10:52:00 +0000 Subject: [PATCH 25/26] chainparams: add testnet assumeutxo param at height 2_500_000 --- src/kernel/chainparams.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp index 3ae24d0eb50bd..e114a6336352c 100644 --- a/src/kernel/chainparams.cpp +++ b/src/kernel/chainparams.cpp @@ -267,7 +267,12 @@ class CTestNetParams : public CChainParams { }; m_assumeutxo_data = { - // TODO to be specified in a future patch. + { + .height = 2'500'000, + .hash_serialized = AssumeutxoHash{uint256S("0x2a8fdefef3bf75fa00540ccaaaba4b5281bea94229327bdb0f7416ef1e7a645c")}, + .nChainTx = 66484552, + .blockhash = uint256S("0x0000000000000093bcb68c03a9a168ae252572d348a2eaeba2cdf9231d73206f") + } }; chainTxData = ChainTxData{ From edbed31066e3674ba52b8c093ab235625527f383 Mon Sep 17 00:00:00 2001 From: Sjors Provoost Date: Thu, 21 Sep 2023 11:00:23 +0000 Subject: [PATCH 26/26] chainparams: add signet assumeutxo param at height 160_000 --- src/kernel/chainparams.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp index e114a6336352c..5e893a3f58c4d 100644 --- a/src/kernel/chainparams.cpp +++ b/src/kernel/chainparams.cpp @@ -375,6 +375,15 @@ class SigNetParams : public CChainParams { vFixedSeeds.clear(); + m_assumeutxo_data = { + { + .height = 160'000, + .hash_serialized = AssumeutxoHash{uint256S("0x5225141cb62dee63ab3be95f9b03d60801f264010b1816d4bd00618b2736e7be")}, + .nChainTx = 2289496, + .blockhash = uint256S("0x0000003ca3c99aff040f2563c2ad8f8ec88bd0fd6b8f0895cfaf1ef90353a62c") + } + }; + base58Prefixes[PUBKEY_ADDRESS] = std::vector(1,111); base58Prefixes[SCRIPT_ADDRESS] = std::vector(1,196); base58Prefixes[SECRET_KEY] = std::vector(1,239);