Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions doc/release-notes-7861.md
Original file line number Diff line number Diff line change
@@ -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`.
3 changes: 1 addition & 2 deletions src/evo/creditpool.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Warn from the blocks actually available after upgrade

On an upgraded pruned mainnet, testnet, or devnet datadir, the persistent m_have_pruned flag can coexist with only the old 288-block window because this change does not restore deleted files. For explicit -checkblocks values between 289 and 2304, this comparison now suppresses the warning even though CVerifyDB stops at the missing data and startup treats SKIPPED_MISSING_BLOCKS as success, so operators are not told that their requested verification was shortened; compare against the first actually stored block or retain the conservative old threshold for previously pruned datadirs.

Useful? React with 👍 / 👎.

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);
Expand Down
8 changes: 6 additions & 2 deletions src/kernel/chainparams.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ class CMainParams : public CChainParams {
nDefaultPlatformP2PPort = 26656;
nDefaultPlatformHTTPPort = 443;
nPruneAfterHeight = 100000;
m_min_blocks_to_keep = 2304; // four days
m_assumed_blockchain_size = 57;
m_assumed_chain_state_size = 1;

Expand Down Expand Up @@ -458,7 +459,8 @@ class CTestNetParams : public CChainParams {
nDefaultPort = 19999;
nDefaultPlatformP2PPort = 22000;
nDefaultPlatformHTTPPort = 22001;
nPruneAfterHeight = 1000;
nPruneAfterHeight = 3000;
m_min_blocks_to_keep = 2304; // four days
m_assumed_blockchain_size = 10;
m_assumed_chain_state_size = 1;

Expand Down Expand Up @@ -640,7 +642,8 @@ class CDevNetParams : public CChainParams {
nDefaultPort = 19799;
nDefaultPlatformP2PPort = 22100;
nDefaultPlatformHTTPPort = 22101;
nPruneAfterHeight = 1000;
nPruneAfterHeight = 3000;
m_min_blocks_to_keep = 2304; // four days
m_assumed_blockchain_size = 0;
m_assumed_chain_state_size = 0;

Expand Down Expand Up @@ -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;

Expand Down
7 changes: 7 additions & 0 deletions src/kernel/chainparams.h
Original file line number Diff line number Diff line change
Expand Up @@ -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*/
Expand Down Expand Up @@ -252,6 +254,11 @@ 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;
std::vector<std::string> vSeeds;
Expand Down
4 changes: 2 additions & 2 deletions src/net_processing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
9 changes: 5 additions & 4 deletions src/node/blockstorage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,9 @@ void BlockManager::FindFilesToPruneManual(std::set<int>& 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<unsigned>(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) {
Expand All @@ -196,7 +197,7 @@ void BlockManager::FindFilesToPrune(std::set<int>& setFilesToPrune, uint64_t nPr
return;
}

unsigned int nLastBlockWeCanPrune{(unsigned)std::min(prune_height, chain_tip_height - static_cast<int>(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
Expand Down Expand Up @@ -226,7 +227,7 @@ void BlockManager::FindFilesToPrune(std::set<int>& 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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/node/blockstorage.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
4 changes: 2 additions & 2 deletions src/rpc/blockchain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
12 changes: 12 additions & 0 deletions src/test/evo_assetlocks_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include <test/util/setup_common.h>

#include <chainparams.h>
#include <consensus/amount.h>
#include <consensus/tx_check.h>
#include <consensus/validation.h>
Expand Down Expand Up @@ -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());
Comment on lines +868 to +869

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Verify retained blocks instead of parameter arithmetic

If FindFilesToPrune() later stops consulting MinBlocksToKeep() or has a boundary/file-selection regression, this case still passes because it only compares two configuration constants and never prunes or reads a historical block. Exercise the pruning path (or a pruned restart) and assert that the required block remains so the test observes the claimed behavior rather than a proxy.

AGENTS.md reference: AGENTS.md:L137-L137

Useful? React with 👍 / 👎.

Comment on lines +866 to +869

@thepastaclaw thepastaclaw Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⏭️ Deferred (maintainer decision; not blocking) at f1ea1a4a; see the replies below.

🟡 Suggestion: Extend pruning retention test to verify rotated LLMQ quorum depth

This invariant checks only the 1152-block credit-pool requirement, so reducing retention to 1152 would still pass despite removing protection needed by rotated quorum reconstruction. GetAllQuorumMembers() reconstructs three previous quarters, and GetQuorumQuarterMembersBySnapshot() obtains each quarter's modifier through GetHashModifier(), which reads the cycle's work block from disk. Materializing older retained quorum cycles through ScanQuorums() extends that history beyond the credit-pool bound; a missing work block makes GetNonNullCoinbaseChainlock() return no signature and changes the modifier. Extend the existing per-network parameter scenario to cover the quorum-history requirement and the intended reorg margin, deriving the bound from the relevant LLMQ parameters rather than checking only the credit-pool window.

source: gemini-3.8-flash-high (phase1-reviewer: general)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the same as point 3 in Pasta's review, and I'm leaving it out of this PR. The test guards the bound this PR is about (credit-pool rebuild). 2304 was picked with the rotated-quorum depth in mind (the 1786 estimate plus a 288 reorg margin), but the exact depth of quorum-member reconstruction is not a single parameter expression I can derive from the code with confidence, so I'd rather not encode an unverified formula as a test. If someone works out the exact bound, a follow-up can add it next to this check.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to defer the quorum-depth invariant to a follow-up that establishes the bound through the reconstruction and scanning paths, rather than encode an unverified formula in this PR's credit-pool test.

Noted as intentionally deferred; I will not press it further here.

}
}

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
Expand Down
2 changes: 1 addition & 1 deletion src/validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5008,7 +5008,7 @@ bool ChainstateManager::AcceptBlock(const std::shared_ptr<const CBlock>& 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
Expand Down
5 changes: 3 additions & 2 deletions src/validation.h
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down
Loading