From 927acde464dd973284614e26d7db9a865ed3baba Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Thu, 15 Oct 2026 17:22:22 +0700 Subject: [PATCH 1/3] refactor: make the pruning retention a chain parameter MIN_BLOCKS_TO_KEEP in validation.h is one constant for two things: how many blocks pruning keeps below the tip, and how far from the tip a node accepts unrequested blocks and announces blocks it has on disk. The pruning retention has to grow on the Dash networks that read older blocks during validation, while regtest keeps Bitcoin Core's 288 for the pruning tests, so it becomes CChainParams::MinBlocksToKeep(). The remaining constant is renamed to BLOCKS_NEAR_TIP so a Bitcoin Core backport that references MIN_BLOCKS_TO_KEEP fails to compile instead of silently binding to whichever meaning it finds. No behaviour change: every network sets 288. --- src/init.cpp | 4 ++-- src/kernel/chainparams.cpp | 4 ++++ src/kernel/chainparams.h | 3 +++ src/net_processing.cpp | 4 ++-- src/node/blockstorage.cpp | 9 +++++---- src/node/blockstorage.h | 2 +- src/rpc/blockchain.cpp | 4 ++-- src/validation.cpp | 2 +- src/validation.h | 5 +++-- 9 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/init.cpp b/src/init.cpp index a495ac1ec6a3..224d3267b22e 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -2106,9 +2106,9 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) } if (status == node::ChainstateLoadStatus::SUCCESS) { uiInterface.InitMessage(_("Verifying blocks…").translated); - if (chainman.m_blockman.m_have_pruned && options.check_blocks > MIN_BLOCKS_TO_KEEP) { + if (chainman.m_blockman.m_have_pruned && options.check_blocks > chainman.GetParams().MinBlocksToKeep()) { LogWarning("pruned datadir may not have more than %d blocks; only checking available blocks\n", - MIN_BLOCKS_TO_KEEP); + chainman.GetParams().MinBlocksToKeep()); } std::tie(status, error) = catch_exceptions([&]{ return VerifyLoadedChainstate(chainman, options, *Assert(node.evodb), [](bool bls_state) { LogPrintf("AppInitMain: bls_legacy_scheme=%d\n", bls_state); diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp index a9310b5245a6..10cca25d919f 100644 --- a/src/kernel/chainparams.cpp +++ b/src/kernel/chainparams.cpp @@ -255,6 +255,7 @@ class CMainParams : public CChainParams { nDefaultPlatformP2PPort = 26656; nDefaultPlatformHTTPPort = 443; nPruneAfterHeight = 100000; + m_min_blocks_to_keep = 288; m_assumed_blockchain_size = 57; m_assumed_chain_state_size = 1; @@ -459,6 +460,7 @@ class CTestNetParams : public CChainParams { nDefaultPlatformP2PPort = 22000; nDefaultPlatformHTTPPort = 22001; nPruneAfterHeight = 1000; + m_min_blocks_to_keep = 288; m_assumed_blockchain_size = 10; m_assumed_chain_state_size = 1; @@ -641,6 +643,7 @@ class CDevNetParams : public CChainParams { nDefaultPlatformP2PPort = 22100; nDefaultPlatformHTTPPort = 22101; nPruneAfterHeight = 1000; + m_min_blocks_to_keep = 288; m_assumed_blockchain_size = 0; m_assumed_chain_state_size = 0; @@ -936,6 +939,7 @@ class CRegTestParams : public CChainParams { nDefaultPlatformP2PPort = 22200; nDefaultPlatformHTTPPort = 22201; nPruneAfterHeight = opts.fastprune ? 100 : 1000; + m_min_blocks_to_keep = 288; m_assumed_blockchain_size = 0; m_assumed_chain_state_size = 0; diff --git a/src/kernel/chainparams.h b/src/kernel/chainparams.h index 073442ddd9e9..9728af03567f 100644 --- a/src/kernel/chainparams.h +++ b/src/kernel/chainparams.h @@ -118,6 +118,8 @@ class CChainParams /** If this chain is exclusively used for testing */ bool IsTestChain() const { return m_is_test_chain; } uint64_t PruneAfterHeight() const { return nPruneAfterHeight; } + /** Block files containing a block-height within MinBlocksToKeep() of ActiveChain().Tip() will not be pruned. */ + int MinBlocksToKeep() const { return m_min_blocks_to_keep; } /** Minimum free space (in GB) needed for data directory */ uint64_t AssumedBlockchainSize() const { return m_assumed_blockchain_size; } /** Minimum free space (in GB) needed for data directory when pruned; Does not include prune target*/ @@ -252,6 +254,7 @@ class CChainParams CMessageHeader::MessageStartChars pchMessageStart; uint16_t nDefaultPort; uint64_t nPruneAfterHeight; + int m_min_blocks_to_keep{288}; uint64_t m_assumed_blockchain_size; uint64_t m_assumed_chain_state_size; std::vector vSeeds; diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 1ff06b047097..9ca230901f5f 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -161,7 +161,7 @@ static constexpr auto BLOCK_STALLING_TIMEOUT_MAX{64s}; static const int MAX_CMPCTBLOCK_DEPTH = 5; /** Maximum depth of blocks we're willing to respond to GETBLOCKTXN requests for. */ static const int MAX_BLOCKTXN_DEPTH = 10; -static_assert(MAX_BLOCKTXN_DEPTH <= MIN_BLOCKS_TO_KEEP, "MAX_BLOCKTXN_DEPTH too high"); +static_assert(MAX_BLOCKTXN_DEPTH <= BLOCKS_NEAR_TIP, "MAX_BLOCKTXN_DEPTH too high"); /** Size of the "block download window": how far ahead of our current height do we fetch? * Larger windows tolerate larger download speed differences between peer, but increase the potential * degree of disordering of blocks on disk (which make reindexing and pruning harder). We'll probably @@ -4716,7 +4716,7 @@ void PeerManagerImpl::ProcessMessage( } // If pruning, don't inv blocks unless we have on disk and are likely to still have // for some reasonable time window (1 hour) that block relay might require. - const int nPrunedBlocksLikelyToHave = MIN_BLOCKS_TO_KEEP - 3600 / m_chainparams.GetConsensus().nPowTargetSpacing; + const int nPrunedBlocksLikelyToHave = BLOCKS_NEAR_TIP - 3600 / m_chainparams.GetConsensus().nPowTargetSpacing; if (m_chainman.m_blockman.IsPruneMode() && (!(pindex->nStatus & BLOCK_HAVE_DATA) || pindex->nHeight <= m_chainman.ActiveChain().Tip()->nHeight - nPrunedBlocksLikelyToHave)) { LogPrint(BCLog::NET, " getblocks stopping, pruned or too old block at %d %s\n", pindex->nHeight, pindex->GetBlockHash().ToString()); break; diff --git a/src/node/blockstorage.cpp b/src/node/blockstorage.cpp index 71553ad41ba8..822e983573cb 100644 --- a/src/node/blockstorage.cpp +++ b/src/node/blockstorage.cpp @@ -172,8 +172,9 @@ void BlockManager::FindFilesToPruneManual(std::set& setFilesToPrune, int nM 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); + // last block to prune is the lesser of (user-specified height, MinBlocksToKeep() from the tip) + unsigned int nLastBlockWeCanPrune = std::min((unsigned)nManualPruneHeight, + static_cast(chain_tip_height - GetParams().MinBlocksToKeep())); 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) { @@ -196,7 +197,7 @@ void BlockManager::FindFilesToPrune(std::set& setFilesToPrune, uint64_t nPr return; } - unsigned int nLastBlockWeCanPrune{(unsigned)std::min(prune_height, chain_tip_height - static_cast(MIN_BLOCKS_TO_KEEP))}; + unsigned int nLastBlockWeCanPrune{(unsigned)std::min(prune_height, chain_tip_height - GetParams().MinBlocksToKeep())}; 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 @@ -226,7 +227,7 @@ void BlockManager::FindFilesToPrune(std::set& setFilesToPrune, uint64_t nPr break; } - // don't prune files that could have a block within MIN_BLOCKS_TO_KEEP of the main chain's tip but keep scanning + // don't prune files that could have a block within MinBlocksToKeep() of the main chain's tip but keep scanning if (m_blockfile_info[fileNumber].nHeightLast > nLastBlockWeCanPrune) { continue; } diff --git a/src/node/blockstorage.h b/src/node/blockstorage.h index 907baa2bd1b5..66eb4bfc35dd 100644 --- a/src/node/blockstorage.h +++ b/src/node/blockstorage.h @@ -118,7 +118,7 @@ class BlockManager * Pruning functions are called from FlushStateToDisk when the m_check_for_pruning flag has been set. * Block and undo files are deleted in lock-step (when blk00003.dat is deleted, so is rev00003.dat.) * Pruning cannot take place until the longest chain is at least a certain length (CChainParams::nPruneAfterHeight). - * Pruning will never delete a block within a defined distance (currently 288) from the active chain's tip. + * Pruning will never delete a block within CChainParams::MinBlocksToKeep() of the active chain's tip. * The block index is updated by unsetting HAVE_DATA and HAVE_UNDO for any blocks that were stored in the deleted files. * A db flag records the fact that at least some block files have been pruned. * diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index cbad463336b6..e3f7a02a6bc7 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -1122,9 +1122,9 @@ static RPCHelpMan pruneblockchain() throw JSONRPCError(RPC_MISC_ERROR, "Blockchain is too short for pruning."); } else if (height > chainHeight) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Blockchain is shorter than the attempted prune height."); - } else if (height > chainHeight - MIN_BLOCKS_TO_KEEP) { + } else if (height > chainHeight - chainman.GetParams().MinBlocksToKeep()) { LogPrint(BCLog::RPC, "Attempt to prune blocks close to the tip. Retaining the minimum number of blocks.\n"); - height = chainHeight - MIN_BLOCKS_TO_KEEP; + height = chainHeight - chainman.GetParams().MinBlocksToKeep(); } PruneBlockFilesManual(active_chainstate, height); diff --git a/src/validation.cpp b/src/validation.cpp index 796553c1b79f..03daf29ef5ce 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -5008,7 +5008,7 @@ bool ChainstateManager::AcceptBlock(const std::shared_ptr& pblock, // blocks which are too close in height to the tip. Apply this test // regardless of whether pruning is enabled; it should generally be safe to // not process unrequested blocks. - bool fTooFarAhead{pindex->nHeight > ActiveHeight() + int(MIN_BLOCKS_TO_KEEP)}; + bool fTooFarAhead{pindex->nHeight > ActiveHeight() + int(BLOCKS_NEAR_TIP)}; // TODO: Decouple this function from the block download logic by removing fRequested // This requires some new chain data structure to efficiently look up if a diff --git a/src/validation.h b/src/validation.h index f5aea3c036ed..566837c4b1fc 100644 --- a/src/validation.h +++ b/src/validation.h @@ -85,8 +85,9 @@ static const unsigned int MAX_HEADERS_COMPRESSED_RESULT = 8000; /** Default for -stopatheight */ static const int DEFAULT_STOPATHEIGHT = 0; -/** Block files containing a block-height within MIN_BLOCKS_TO_KEEP of ActiveChain().Tip() will not be pruned. */ -static const unsigned int MIN_BLOCKS_TO_KEEP = 288; +/** Blocks within this distance of ActiveChain().Tip() are accepted unrequested and announced by pruned nodes. + * Bitcoin Core's MIN_BLOCKS_TO_KEEP; pruning retention is CChainParams::MinBlocksToKeep(). */ +static const unsigned int BLOCKS_NEAR_TIP = 288; static const signed int DEFAULT_CHECKBLOCKS = 6; static constexpr int DEFAULT_CHECKLEVEL{3}; From e2ec035898f3fc5ae8f4e34f9865f443ea04b6a2 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Thu, 15 Oct 2026 17:22:41 +0700 Subject: [PATCH 2/3] feat: keep four days of blocks on pruned nodes 288 blocks below the tip is not enough on Dash. Validation reads older blocks from disk: after a restart a credit pool is rebuilt from its newest daily snapshot, reading each block since it and the block one withdrawal window earlier, which reaches 1152 blocks below the tip; rotated LLMQ modifiers read the work block of up to three earlier DKG cycles. A pruned node that has deleted one of those blocks rejects a canonical block, or computes different quorum members than the rest of the network. 288 is also only twelve hours on Dash where it is two days on Bitcoin. Mainnet, testnet and devnets now keep 2304 blocks (four days), which covers those reads with a reorg margin. Regtest keeps 288 so the upstream pruning tests and their disk budget are unchanged. Testnet and devnet start pruning after height 3000 instead of 1000 because FindFilesToPrune subtracts the retention from the tip height as unsigned. The minimum -prune target stays at 945 MiB: the target is a ceiling pruning tries to respect, not a condition it fails on, and typical Dash blocks are far below the 2 MiB the derivation assumes. --- doc/release-notes-7861.md | 12 ++++++++++++ src/kernel/chainparams.cpp | 10 +++++----- src/kernel/chainparams.h | 4 ++++ 3 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 doc/release-notes-7861.md diff --git a/doc/release-notes-7861.md b/doc/release-notes-7861.md new file mode 100644 index 000000000000..cfa126788d91 --- /dev/null +++ b/doc/release-notes-7861.md @@ -0,0 +1,12 @@ +Pruning +------- + +- Pruned nodes on mainnet, testnet and devnets now keep at least four days of + blocks (2304) below the tip instead of 288. Validation reads older blocks + from disk: the credit pool is rebuilt from its newest daily snapshot after a + restart, and rotated quorum membership reads the work blocks of earlier DKG + cycles. `pruneblockchain` clamps its height accordingly, and on testnet and + devnets pruning starts once the chain is longer than 3000 blocks + (previously 1000). Blocks that an earlier version already pruned are not + recovered; a node that fails to validate a block after upgrading needs + `-reindex`. diff --git a/src/kernel/chainparams.cpp b/src/kernel/chainparams.cpp index 10cca25d919f..225268291879 100644 --- a/src/kernel/chainparams.cpp +++ b/src/kernel/chainparams.cpp @@ -255,7 +255,7 @@ class CMainParams : public CChainParams { nDefaultPlatformP2PPort = 26656; nDefaultPlatformHTTPPort = 443; nPruneAfterHeight = 100000; - m_min_blocks_to_keep = 288; + m_min_blocks_to_keep = 2304; // four days m_assumed_blockchain_size = 57; m_assumed_chain_state_size = 1; @@ -459,8 +459,8 @@ class CTestNetParams : public CChainParams { nDefaultPort = 19999; nDefaultPlatformP2PPort = 22000; nDefaultPlatformHTTPPort = 22001; - nPruneAfterHeight = 1000; - m_min_blocks_to_keep = 288; + nPruneAfterHeight = 3000; + m_min_blocks_to_keep = 2304; // four days m_assumed_blockchain_size = 10; m_assumed_chain_state_size = 1; @@ -642,8 +642,8 @@ class CDevNetParams : public CChainParams { nDefaultPort = 19799; nDefaultPlatformP2PPort = 22100; nDefaultPlatformHTTPPort = 22101; - nPruneAfterHeight = 1000; - m_min_blocks_to_keep = 288; + nPruneAfterHeight = 3000; + m_min_blocks_to_keep = 2304; // four days m_assumed_blockchain_size = 0; m_assumed_chain_state_size = 0; diff --git a/src/kernel/chainparams.h b/src/kernel/chainparams.h index 9728af03567f..777dc15d9627 100644 --- a/src/kernel/chainparams.h +++ b/src/kernel/chainparams.h @@ -254,6 +254,10 @@ class CChainParams CMessageHeader::MessageStartChars pchMessageStart; uint16_t nDefaultPort; uint64_t nPruneAfterHeight; + /// Four days of blocks (2304) on every network but regtest. Validation reads older blocks from disk: + /// a credit pool is rebuilt from its newest daily snapshot, reading the block one withdrawal window + /// before each block since it, and rotated LLMQ modifiers read the work block of earlier cycles. + /// Regtest keeps Bitcoin Core's 288 for the upstream pruning tests. int m_min_blocks_to_keep{288}; uint64_t m_assumed_blockchain_size; uint64_t m_assumed_chain_state_size; From e828c5c3a3de984b91814cc0fa92a657966b4ff0 Mon Sep 17 00:00:00 2001 From: Konstantin Akimov Date: Thu, 15 Oct 2026 17:23:07 +0700 Subject: [PATCH 3/3] test: check pruned nodes keep the credit-pool rebuild history MinBlocksToKeep() has to cover the deepest block a cold credit-pool rebuild reads: one snapshot period plus one withdrawal window. Nothing tied the two together, so a later change to either parameter could silently outgrow the retention. Check it for mainnet, testnet and devnet from the chain parameters. --- src/evo/creditpool.h | 3 +-- src/test/evo_assetlocks_tests.cpp | 12 ++++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/evo/creditpool.h b/src/evo/creditpool.h index e7676beace54..060a570e17ca 100644 --- a/src/evo/creditpool.h +++ b/src/evo/creditpool.h @@ -123,9 +123,8 @@ class CCreditPoolManager CEvoDB& evoDb; const ChainstateManager& m_chainman; - static constexpr int DISK_SNAPSHOT_PERIOD = 576; // once per day - public: + static constexpr int DISK_SNAPSHOT_PERIOD = 576; // once per day static constexpr CAmount LimitAmountLow = 100 * COIN; static constexpr CAmount LimitAmountHigh = 1000 * COIN; static constexpr CAmount LimitAmountV22 = 2000 * COIN; diff --git a/src/test/evo_assetlocks_tests.cpp b/src/test/evo_assetlocks_tests.cpp index 8db84d70d92a..aa23c80aa846 100644 --- a/src/test/evo_assetlocks_tests.cpp +++ b/src/test/evo_assetlocks_tests.cpp @@ -4,6 +4,7 @@ #include +#include #include #include #include @@ -858,6 +859,17 @@ BOOST_FIXTURE_TEST_CASE(credit_pool_snapshot_persisted_after_transactionless_con BOOST_CHECK_EQUAL(snapshot.currentLimit, pool.currentLimit); } +BOOST_FIXTURE_TEST_CASE(min_blocks_to_keep_covers_credit_pool_rebuild, BasicTestingSetup) +{ + // A pool is rebuilt from a snapshot up to one period old, reading the block one window before + // each block since it. Regtest deliberately keeps Bitcoin Core's 288 and is not checked. + for (const auto& chain : {CBaseChainParams::MAIN, CBaseChainParams::TESTNET, CBaseChainParams::DEVNET}) { + const auto params = CreateChainParams(*m_node.args, chain); + BOOST_CHECK_GE(params->MinBlocksToKeep(), + CCreditPoolManager::DISK_SNAPSHOT_PERIOD + params->CreditPoolPeriodBlocks()); + } +} + BOOST_AUTO_TEST_CASE(credit_pool_unlock_limit_v24) { // From v24 the limit is relative and net: 20% of the balance at the window start, never less