diff --git a/src/node/blockstorage.h b/src/node/blockstorage.h --- a/src/node/blockstorage.h +++ b/src/node/blockstorage.h @@ -13,6 +13,7 @@ #include <chain.h> #include <chainparams.h> #include <kernel/blockmanager_opts.h> +#include <kernel/chain.h> #include <kernel/cs_main.h> #include <protocol.h> #include <sync.h> @@ -64,6 +65,34 @@ int height_first{std::numeric_limits<int>::max()}; }; +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. @@ -89,8 +118,8 @@ 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); @@ -98,6 +127,7 @@ [[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); @@ -150,19 +180,31 @@ RecursiveMutex cs_LastBlockFile; std::vector<CBlockFileInfo> 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<std::optional<BlockfileCursor>, 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 @@ -189,6 +231,8 @@ std::unordered_map<std::string, PruneLockInfo> m_prune_locks GUARDED_BY(::cs_main); + BlockfileType BlockfileTypeForHeight(int height); + const kernel::BlockManagerOpts m_opts; public: @@ -201,6 +245,20 @@ 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<int> m_snapshot_height; + std::vector<CBlockIndex *> GetAllBlockIndices() EXCLUSIVE_LOCKS_REQUIRED(::cs_main); diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -13,6 +13,7 @@ #include <consensus/validation.h> #include <flatfile.h> #include <hash.h> +#include <kernel/chain.h> #include <kernel/chainparams.h> #include <logging.h> #include <pow/pow.h> @@ -142,7 +143,8 @@ 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 || @@ -196,7 +198,8 @@ 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; @@ -263,11 +266,10 @@ 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 estimated progress) isn't persisted @@ -276,8 +278,16 @@ 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<CBlockIndex *> vSortedByHeight{GetAllBlockIndices()}; std::sort(vSortedByHeight.begin(), vSortedByHeight.end(), @@ -298,7 +308,7 @@ // basis of snapshot load (see PopulateAndValidateSnapshot()). // Pruned nodes may have deleted the block. if (pindex->nTx > 0) { - if (snapshot_blockhash && pindex->nHeight == snapshot_height && + if (m_snapshot_height && pindex->nHeight == *m_snapshot_height && pindex->GetBlockHash() == *snapshot_blockhash) { Assert(pindex->pprev); // Should have been set above; don't disturb it with code below. @@ -340,7 +350,9 @@ m_dirty_blockindex.clear(); - 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; @@ -351,17 +363,18 @@ 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++) { + 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); @@ -387,6 +400,16 @@ } } + { + // 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<int>(i), 0}; + } + } + // Check whether we have ever pruned block & undo files m_block_tree_db->ReadFlag("prunedblockfiles", m_have_pruned); if (m_have_pruned) { @@ -404,12 +427,14 @@ void BlockManager::ScanAndUnlinkAlreadyPrunedFiles() { AssertLockHeld(::cs_main); + int max_blockfile = + WITH_LOCK(cs_LastBlockFile, return this->MaxBlockfileNum()); if (!m_have_pruned) { return; } std::set<int> 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); } @@ -592,7 +617,8 @@ 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); @@ -604,10 +630,10 @@ // have populated `m_blockfile_info` via LoadBlockIndexDB(). return true; } - assert(static_cast<int>(m_blockfile_info.size()) > m_last_blockfile); + assert(static_cast<int>(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)) { AbortNode("Flushing block file to disk failed. This is likely the " "result of an I/O error."); @@ -617,13 +643,35 @@ // 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 the cursor does not exist, it means an assumeutxo snapshot is loaded, + // but no blocks past the snapshot height have been written yet, so there + // is no data associated with the chainstate, and it is safe not to flush. + if (cursor) { + return FlushBlockFile(cursor->file_num, /*fFinalize=*/false, + /*finalize_undo=*/false); + } + // No need to log warnings in this case. + return true; +} + uint64_t BlockManager::CalculateCurrentUsage() { LOCK(cs_LastBlockFile); @@ -680,8 +728,22 @@ bool fKnown) { 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::BLOCKSTORE, + "[%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<int>(m_blockfile_info.size()) <= nFile) { m_blockfile_info.resize(nFile + 1); } @@ -702,15 +764,22 @@ // or raise MAX_BLOCKFILE_SIZE when we reach block sizes larger than // 128 MiB 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<int>(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<int>(m_blockfile_info.size()) <= nFile) { m_blockfile_info.resize(nFile + 1); } } @@ -718,11 +787,13 @@ pos.nPos = m_blockfile_info[nFile].nSize; } - if ((int)nFile != m_last_blockfile) { + if (nFile != last_blockfile) { if (!fKnown) { - LogPrint(BCLog::BLOCKSTORE, "Leaving block file %i: %s\n", - m_last_blockfile, - m_blockfile_info[m_last_blockfile].ToString()); + LogPrint(BCLog::BLOCKSTORE, + "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 @@ -732,16 +803,15 @@ // 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::BLOCKSTORE, 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; // No undo data yet in the new file, so reset our undo-height tracking. - m_undo_height_in_last_blockfile = 0; + m_blockfile_cursors[chain_type] = BlockfileCursor{nFile}; } m_blockfile_info[nFile].AddBlock(nHeight, nTime); @@ -822,6 +892,10 @@ 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; @@ -840,7 +914,7 @@ // 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 && + if (_pos.nFile < cursor.file_num && static_cast<uint32_t>(block.nHeight) == m_blockfile_info[_pos.nFile].nHeightLast) { // Do not propagate the return code, a failed flush here should not @@ -852,10 +926,9 @@ LogPrintLevel(BCLog::BLOCKSTORE, BCLog::Level::Warning, "Failed to flush undo file %05i\n", _pos.nFile); } - } else if (_pos.nFile == m_last_blockfile && - static_cast<uint32_t>(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; @@ -1110,4 +1183,24 @@ } } // 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/validation.cpp b/src/validation.cpp --- a/src/validation.cpp +++ b/src/validation.cpp @@ -2651,7 +2651,8 @@ // 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__); @@ -6556,6 +6557,7 @@ 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());