Skip to content

Commit baf6e26

Browse files
committed
merge bitcoin#21726: Improve Indices on pruned nodes via prune blockers
1 parent c65ec19 commit baf6e26

15 files changed

+269
-118
lines changed

src/index/base.cpp

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ bool BaseIndex::Init()
6262
LOCK(cs_main);
6363
CChain& active_chain = m_chainstate->m_chain;
6464
if (locator.IsNull()) {
65-
m_best_block_index = nullptr;
65+
SetBestBlockIndex(nullptr);
6666
} else {
67-
m_best_block_index = m_chainstate->FindForkInGlobalIndex(locator);
67+
SetBestBlockIndex(m_chainstate->FindForkInGlobalIndex(locator));
6868
}
6969

7070
// Note: this will latch to true immediately if the user starts up with an empty
@@ -76,11 +76,7 @@ bool BaseIndex::Init()
7676
if (!m_best_block_index) {
7777
// index is not built yet
7878
// make sure we have all block data back to the genesis
79-
const CBlockIndex* block = active_chain.Tip();
80-
while (block->pprev && (block->pprev->nStatus & BLOCK_HAVE_DATA)) {
81-
block = block->pprev;
82-
}
83-
prune_violation = block != active_chain.Genesis();
79+
prune_violation = GetFirstStoredBlock(active_chain.Tip()) != active_chain.Genesis();
8480
}
8581
// in case the index has a best block set and is not fully synced
8682
// check if we have the required blocks to continue building the index
@@ -138,7 +134,7 @@ void BaseIndex::ThreadSync()
138134
std::chrono::steady_clock::time_point last_locator_write_time{0s};
139135
while (true) {
140136
if (m_interrupt) {
141-
m_best_block_index = pindex;
137+
SetBestBlockIndex(pindex);
142138
// No need to handle errors in Commit. If it fails, the error will be already be
143139
// logged. The best way to recover is to continue, as index cannot be corrupted by
144140
// a missed commit to disk for an advanced index state.
@@ -150,7 +146,7 @@ void BaseIndex::ThreadSync()
150146
LOCK(cs_main);
151147
const CBlockIndex* pindex_next = NextSyncBlock(pindex, m_chainstate->m_chain);
152148
if (!pindex_next) {
153-
m_best_block_index = pindex;
149+
SetBestBlockIndex(pindex);
154150
m_synced = true;
155151
// No need to handle errors in Commit. See rationale above.
156152
Commit();
@@ -172,7 +168,7 @@ void BaseIndex::ThreadSync()
172168
}
173169

174170
if (last_locator_write_time + SYNC_LOCATOR_WRITE_INTERVAL < current_time) {
175-
m_best_block_index = pindex;
171+
SetBestBlockIndex(pindex);
176172
last_locator_write_time = current_time;
177173
// No need to handle errors in Commit. See rationale above.
178174
Commit();
@@ -230,10 +226,10 @@ bool BaseIndex::Rewind(const CBlockIndex* current_tip, const CBlockIndex* new_ti
230226
// out of sync may be possible but a users fault.
231227
// In case we reorg beyond the pruned depth, ReadBlockFromDisk would
232228
// throw and lead to a graceful shutdown
233-
m_best_block_index = new_tip;
229+
SetBestBlockIndex(new_tip);
234230
if (!Commit()) {
235231
// If commit fails, revert the best block index to avoid corruption.
236-
m_best_block_index = current_tip;
232+
SetBestBlockIndex(current_tip);
237233
return false;
238234
}
239235

@@ -274,7 +270,7 @@ void BaseIndex::BlockConnected(const std::shared_ptr<const CBlock>& block, const
274270
}
275271

276272
if (WriteBlock(*block, pindex)) {
277-
m_best_block_index = pindex;
273+
SetBestBlockIndex(pindex);
278274
} else {
279275
FatalError("%s: Failed to write block %s to index",
280276
__func__, pindex->GetBlockHash().ToString());
@@ -381,3 +377,14 @@ IndexSummary BaseIndex::GetSummary() const
381377
summary.best_block_height = m_best_block_index ? m_best_block_index.load()->nHeight : 0;
382378
return summary;
383379
}
380+
381+
void BaseIndex::SetBestBlockIndex(const CBlockIndex* block) {
382+
assert(!fPruneMode || AllowPrune());
383+
384+
m_best_block_index = block;
385+
if (AllowPrune() && block) {
386+
PruneLockInfo prune_lock;
387+
prune_lock.height_first = block->nHeight;
388+
WITH_LOCK(::cs_main, m_chainstate->m_blockman.UpdatePruneLock(GetName(), prune_lock));
389+
}
390+
}

src/index/base.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ class BaseIndex : public CValidationInterface
8181
/// to a chain reorganization), the index must halt until Commit succeeds or else it could end up
8282
/// getting corrupted.
8383
bool Commit();
84+
85+
virtual bool AllowPrune() const = 0;
86+
8487
protected:
8588
CChainState* m_chainstate{nullptr};
8689

@@ -109,6 +112,9 @@ class BaseIndex : public CValidationInterface
109112
/// Get the name of the index for display in logs.
110113
virtual const char* GetName() const = 0;
111114

115+
/// Update the internal best block index as well as the prune lock.
116+
void SetBestBlockIndex(const CBlockIndex* block);
117+
112118
public:
113119
/// Destructor interrupts sync thread if running and blocks until it exits.
114120
virtual ~BaseIndex();

src/index/blockfilterindex.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ class BlockFilterIndex final : public BaseIndex
3939
/** cache of block hash to filter header, to avoid disk access when responding to getcfcheckpt. */
4040
std::unordered_map<uint256, uint256, FilterHeaderHasher> m_headers_cache GUARDED_BY(m_cs_headers_cache);
4141

42+
bool AllowPrune() const override { return true; }
43+
4244
protected:
4345
bool Init() override;
4446

src/index/coinstatsindex.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ class CoinStatsIndex final : public BaseIndex
3636

3737
bool ReverseBlock(const CBlock& block, const CBlockIndex* pindex);
3838

39+
bool AllowPrune() const override { return true; }
40+
3941
protected:
4042
bool Init() override;
4143

src/index/txindex.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ class TxIndex final : public BaseIndex
2020
private:
2121
const std::unique_ptr<DB> m_db;
2222

23+
bool AllowPrune() const override { return false; }
24+
2325
protected:
2426
bool WriteBlock(const CBlock& block, const CBlockIndex* pindex) override;
2527

src/init.cpp

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,7 @@ void SetupServerArgs(ArgsManager& argsman)
538538
-GetNumCores(), MAX_SCRIPTCHECK_THREADS, DEFAULT_SCRIPTCHECK_THREADS), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
539539
argsman.AddArg("-persistmempool", strprintf("Whether to save the mempool on shutdown and load on restart (default: %u)", DEFAULT_PERSIST_MEMPOOL), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
540540
argsman.AddArg("-pid=<file>", strprintf("Specify pid file. Relative paths will be prefixed by a net-specific datadir location. (default: %s)", BITCOIN_PID_FILENAME), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
541-
argsman.AddArg("-prune=<n>", strprintf("Reduce storage requirements by enabling pruning (deleting) of old blocks. This allows the pruneblockchain RPC to be called to delete specific blocks, and enables automatic pruning of old blocks if a target size in MiB is provided. This mode is incompatible with -txindex, -coinstatsindex, -rescan and -disablegovernance=false. "
541+
argsman.AddArg("-prune=<n>", strprintf("Reduce storage requirements by enabling pruning (deleting) of old blocks. This allows the pruneblockchain RPC to be called to delete specific blocks, and enables automatic pruning of old blocks if a target size in MiB is provided. This mode is incompatible with -txindex, -rescan and -disablegovernance=false. "
542542
"Warning: Reverting this setting requires re-downloading the entire blockchain. "
543543
"(default: 0 = disable pruning blocks, 1 = allow manual pruning via RPC, >%u = automatically prune block files to stay under the specified target size in MiB)", MIN_DISK_SPACE_FOR_BLOCK_FILES / 1024 / 1024), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
544544
argsman.AddArg("-settings=<file>", strprintf("Specify path to dynamic settings data file. Can be disabled with -nosettings. File is written at runtime and not meant to be edited by users (use %s instead for custom settings). Relative paths will be prefixed by datadir location. (default: %s)", BITCOIN_CONF_FILENAME, BITCOIN_SETTINGS_FILENAME), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
@@ -1159,8 +1159,6 @@ bool AppInitParameterInteraction(const ArgsManager& args)
11591159
if (args.GetArg("-prune", 0)) {
11601160
if (args.GetBoolArg("-txindex", DEFAULT_TXINDEX))
11611161
return InitError(_("Prune mode is incompatible with -txindex."));
1162-
if (args.GetBoolArg("-coinstatsindex", DEFAULT_COINSTATSINDEX))
1163-
return InitError(_("Prune mode is incompatible with -coinstatsindex."));
11641162
if (args.GetBoolArg("-reindex-chainstate", false)) {
11651163
return InitError(_("Prune mode is incompatible with -reindex-chainstate. Use full -reindex instead."));
11661164
}

src/node/blockstorage.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include <walletinitinterface.h>
2525

2626
#include <map>
27+
#include <unordered_map>
2728

2829
std::atomic_bool fImporting(false);
2930
std::atomic_bool fReindex(false);
@@ -249,6 +250,11 @@ void BlockManager::FindFilesToPrune(std::set<int>& setFilesToPrune, uint64_t nPr
249250
nLastBlockWeCanPrune, count);
250251
}
251252

253+
void BlockManager::UpdatePruneLock(const std::string& name, const PruneLockInfo& lock_info) {
254+
AssertLockHeld(::cs_main);
255+
m_prune_locks[name] = lock_info;
256+
}
257+
252258
CBlockIndex* BlockManager::InsertBlockIndex(const uint256& hash)
253259
{
254260
AssertLockHeld(cs_main);
@@ -421,6 +427,16 @@ bool BlockManager::IsBlockPruned(const CBlockIndex* pblockindex)
421427
return (m_have_pruned && !(pblockindex->nStatus & BLOCK_HAVE_DATA) && pblockindex->nTx > 0);
422428
}
423429

430+
const CBlockIndex* GetFirstStoredBlock(const CBlockIndex* start_block) {
431+
AssertLockHeld(::cs_main);
432+
assert(start_block);
433+
const CBlockIndex* last_block = start_block;
434+
while (last_block->pprev && (last_block->pprev->nStatus & BLOCK_HAVE_DATA)) {
435+
last_block = last_block->pprev;
436+
}
437+
return last_block;
438+
}
439+
424440
// If we're using -prune with -reindex, then delete block files that will be ignored by the
425441
// reindex. Since reindexing works by starting at block file 0 and looping until a blockfile
426442
// is missing, do the same here to delete any later block files after a gap. Also delete all

src/node/blockstorage.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <txdb.h>
1313

1414
#include <cstdint>
15+
#include <unordered_map>
1516
#include <vector>
1617

1718
extern RecursiveMutex cs_main;
@@ -77,6 +78,10 @@ struct CBlockIndexHeightOnlyComparator {
7778
bool operator()(const CBlockIndex* pa, const CBlockIndex* pb) const;
7879
};
7980

81+
struct PruneLockInfo {
82+
int height_first{std::numeric_limits<int>::max()}; //! Height of earliest block that should be kept and not pruned
83+
};
84+
8085
/**
8186
* Maintains a tree of blocks (stored in `m_block_index`) which is consulted
8287
* to determine where the most-work tip is.
@@ -137,6 +142,14 @@ class BlockManager
137142
/** Dirty block file entries. */
138143
std::set<int> m_dirty_fileinfo;
139144

145+
/**
146+
* Map from external index name to oldest block that must not be pruned.
147+
*
148+
* @note Internally, only blocks at height (height_first - PRUNE_LOCK_BUFFER - 1) and
149+
* below will be pruned, but callers should avoid assuming any particular buffer size.
150+
*/
151+
std::unordered_map<std::string, PruneLockInfo> m_prune_locks GUARDED_BY(::cs_main);
152+
140153
public:
141154
BlockMap m_block_index GUARDED_BY(cs_main);
142155
PrevBlockMap m_prev_block_index GUARDED_BY(cs_main);
@@ -185,8 +198,14 @@ class BlockManager
185198

186199
//! Check whether the block associated with this index entry is pruned or not.
187200
bool IsBlockPruned(const CBlockIndex* pblockindex) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
201+
202+
//! Create or update a prune lock identified by its name
203+
void UpdatePruneLock(const std::string& name, const PruneLockInfo& lock_info) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
188204
};
189205

206+
//! Find the first block that is not pruned
207+
const CBlockIndex* GetFirstStoredBlock(const CBlockIndex* start_block) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
208+
190209
void CleanupBlockRevFiles();
191210

192211
/** Open a block file (blk?????.dat) */

src/rpc/blockchain.cpp

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1361,10 +1361,9 @@ static RPCHelpMan pruneblockchain()
13611361

13621362
PruneBlockFilesManual(active_chainstate, height);
13631363
const CBlockIndex* block = CHECK_NONFATAL(active_chain.Tip());
1364-
while (block->pprev && (block->pprev->nStatus & BLOCK_HAVE_DATA)) {
1365-
block = block->pprev;
1366-
}
1367-
return uint64_t(block->nHeight);
1364+
const CBlockIndex* last_block = GetFirstStoredBlock(block);
1365+
1366+
return static_cast<uint64_t>(last_block->nHeight);
13681367
},
13691368
};
13701369
}
@@ -1806,11 +1805,7 @@ RPCHelpMan getblockchaininfo()
18061805
obj.pushKV("pruned", fPruneMode);
18071806
if (fPruneMode) {
18081807
const CBlockIndex* block = CHECK_NONFATAL(tip);
1809-
while (block->pprev && (block->pprev->nStatus & BLOCK_HAVE_DATA)) {
1810-
block = block->pprev;
1811-
}
1812-
1813-
obj.pushKV("pruneheight", block->nHeight);
1808+
obj.pushKV("pruneheight", GetFirstStoredBlock(block)->nHeight);
18141809

18151810
// if 0, execution bypasses the whole if block.
18161811
bool automatic_pruning{args.GetArg("-prune", 0) != 1};

src/validation.cpp

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
#include <deploymentstatus.h>
2020
#include <flatfile.h>
2121
#include <hash.h>
22-
#include <index/blockfilterindex.h>
2322
#include <logging.h>
2423
#include <logging/timer.h>
2524
#include <node/blockstorage.h>
@@ -93,6 +92,12 @@ const std::vector<std::string> CHECKLEVEL_DOC {
9392
"level 4 tries to reconnect the blocks",
9493
"each level includes the checks of the previous levels",
9594
};
95+
/** The number of blocks to keep below the deepest prune lock.
96+
* There is nothing special about this number. It is higher than what we
97+
* expect to see in regular mainnet reorgs, but not so high that it would
98+
* noticeably interfere with the pruning mechanism.
99+
* */
100+
static constexpr int PRUNE_LOCK_BUFFER{10};
96101

97102
/**
98103
* Mutex to guard access to validation specific variables, such as reading
@@ -2368,12 +2373,24 @@ bool CChainState::FlushStateToDisk(
23682373
CoinsCacheSizeState cache_state = GetCoinsCacheSizeState();
23692374
LOCK(m_blockman.cs_LastBlockFile);
23702375
if (fPruneMode && (m_blockman.m_check_for_pruning || nManualPruneHeight > 0) && !fReindex) {
2371-
// make sure we don't prune above the blockfilterindexes bestblocks
2376+
// make sure we don't prune above any of the prune locks bestblocks
23722377
// pruning is height-based
2373-
int last_prune = m_chain.Height(); // last height we can prune
2374-
ForEachBlockFilterIndex([&](BlockFilterIndex& index) {
2375-
last_prune = std::max(1, std::min(last_prune, index.GetSummary().best_block_height));
2376-
});
2378+
int last_prune{m_chain.Height()}; // last height we can prune
2379+
std::optional<std::string> limiting_lock; // prune lock that actually was the limiting factor, only used for logging
2380+
2381+
for (const auto& prune_lock : m_blockman.m_prune_locks) {
2382+
if (prune_lock.second.height_first == std::numeric_limits<int>::max()) continue;
2383+
// Remove the buffer and one additional block here to get actual height that is outside of the buffer
2384+
const int lock_height{prune_lock.second.height_first - PRUNE_LOCK_BUFFER - 1};
2385+
last_prune = std::max(1, std::min(last_prune, lock_height));
2386+
if (last_prune == lock_height) {
2387+
limiting_lock = prune_lock.first;
2388+
}
2389+
}
2390+
2391+
if (limiting_lock) {
2392+
LogPrint(BCLog::PRUNE, "%s limited pruning to height %d\n", limiting_lock.value(), last_prune);
2393+
}
23772394

23782395
if (nManualPruneHeight > 0) {
23792396
LOG_TIME_MILLIS_WITH_CATEGORY("find files to prune (manual)", BCLog::BENCHMARK);
@@ -2628,6 +2645,18 @@ bool CChainState::DisconnectTip(BlockValidationState& state, DisconnectedBlockTr
26282645
dbTx->Commit();
26292646
}
26302647
LogPrint(BCLog::BENCHMARK, "- Disconnect block: %.2fms\n", (GetTimeMicros() - nStart) * MILLI);
2648+
2649+
{
2650+
// Prune locks that began at or after the tip should be moved backward so they get a chance to reorg
2651+
const int max_height_first{pindexDelete->nHeight - 1};
2652+
for (auto& prune_lock : m_blockman.m_prune_locks) {
2653+
if (prune_lock.second.height_first <= max_height_first) continue;
2654+
2655+
prune_lock.second.height_first = max_height_first;
2656+
LogPrint(BCLog::PRUNE, "%s prune lock moved back to %d\n", prune_lock.first, max_height_first);
2657+
}
2658+
}
2659+
26312660
// Write the chain state to disk, if necessary.
26322661
if (!FlushStateToDisk(state, FlushStateMode::IF_NEEDED)) {
26332662
return false;

0 commit comments

Comments
 (0)