Skip to content
Closed
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
14 changes: 14 additions & 0 deletions doc/release-notes-7882.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Pruning
-------

- Pruned nodes now keep the blocks that block validation reads back from disk
for the credit pool and for quorum members. From V20 on that is at least the
last 2074 blocks on mainnet, testnet and devnet, instead of 288. Before, a
node pruned below that could reject a valid block and stop following the
chain. At current mainnet block sizes this is far below the minimum `-prune`
target, so it mostly affects manual pruning: `pruneblockchain` now returns a
lower height when the requested height is within that range. A pruned node
now refuses to start, and asks to restart with `-reindex`, if blocks that it
needs to validate the next blocks were already pruned, for example by an
earlier release. A node that has already rejected a valid block this way also
needs `-reindex`. (#7882)
1 change: 1 addition & 0 deletions src/Makefile.test.include
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ BITCOIN_TESTS =\
test/evo_mnauth_tests.cpp \
test/evo_mnhf_tests.cpp \
test/evo_netinfo_tests.cpp \
test/evo_prune_tests.cpp \
test/evo_sharedmn_tests.cpp \
test/evo_simplifiedmns_tests.cpp \
test/evo_snapshot_tests.cpp \
Expand Down
2 changes: 1 addition & 1 deletion src/evo/creditpool.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,9 @@ class CCreditPoolManager
CEvoDB& evoDb;
const ChainstateManager& m_chainman;

public:
static constexpr int DISK_SNAPSHOT_PERIOD = 576; // once per day

public:
static constexpr CAmount LimitAmountLow = 100 * COIN;
static constexpr CAmount LimitAmountHigh = 1000 * COIN;
static constexpr CAmount LimitAmountV22 = 2000 * COIN;
Expand Down
21 changes: 21 additions & 0 deletions src/evo/specialtxman.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <llmq/blockprocessor.h>
#include <llmq/commitment.h>
#include <llmq/quorumsman.h>
#include <llmq/snapshot.h>
#include <llmq/utils.h>
#include <messagesigner.h>

Expand Down Expand Up @@ -50,6 +51,26 @@ CSpecialTxProcessor::CSpecialTxProcessor(CEvoDB& evodb, CDeterministicMNManager&

CSpecialTxProcessor::~CSpecialTxProcessor() = default;

int GetEvoPruneLockHeight(const CChainParams& chainparams, int tip_height)
{
// On a cold cache the credit pool is rebuilt from the last disk snapshot, up to DISK_SNAPSHOT_PERIOD blocks back
// (the snapshot of a block is only written once the next block connects), reading every block on the way and the
// block CreditPoolPeriodBlocks before each of them
int depth{CCreditPoolManager::DISK_SNAPSHOT_PERIOD + chainparams.CreditPoolPeriodBlocks()};
for (const auto& llmq_params : chainparams.GetConsensus().llmqs) {
// Quorum members depend on the coinbase ChainLock in the work block of the quorum. Rotated quorum members
// also depend on the work blocks of the three previous cycles, and are stored as a snapshot that the next
// three cycles build on. Members and their snapshot are built and stored again whenever they are not cached,
// so keep the work blocks of a cycle and of the three cycles before it until the last commitment of the third
// cycle after it can be mined.
const int cycles{llmq_params.useRotation ? 6 : 0};

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 Use only three cycles for the rotated quorum depth

When upgrading a mainnet/testnet/devnet datadir whose old manual pruning stopped, for example, about 1,500 blocks behind the tip, this six-cycle bound makes FindMissingEvoBlockData() scan 1,786 blocks back and refuse startup with a full-reindex error even though every block whose data is required for upcoming validation is present. ComputeQuorumMembersByQuarterRotation() has exactly three PreviousQuorumQuarters, and GetQuorumQuarterMembersBySnapshot() reconstructs each directly from that cycle's persisted snapshot and work block; it does not recursively read the three cycles used to create that snapshot. The rotated bound is therefore 3 * dkgInterval + dkgMiningWindowEnd + WORK_DIFF_DEPTH (922 for LLMQ_60_75), below the 1,152-block credit-pool bound; using six both causes the false recovery failure and unnecessarily prevents pruning roughly 634 additional blocks.

AGENTS.md reference: AGENTS.md:L239-L242

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks. I checked this against the code and am keeping six cycles, with no code change.

You're right that building the members of cycle H reads only the work blocks of H, H−C, H−2C and H−3C, plus the stored snapshots of H−C, H−2C and H−3C, and doesn't recurse. But those snapshots aren't write-once:

  • GetAllQuorumMembers() builds a cycle again on every cache miss, with predicting=false (src/llmq/utils.cpp:707). That passes storeSnapshot=true to BuildNewQuorumQuarterMembers() (utils.cpp:513), which stores the cycle's snapshot again (utils.cpp:462). StoreSnapshotForBlock() is a plain write (src/llmq/snapshot.cpp:327).
  • The members cache is held only in memory. After a restart, the members of H−C, H−2C and H−3C are built again as soon as those quorums are used.
  • Building H−3C again reads the work blocks back to H−6C, through the previous-cycle modifiers (utils.cpp:204).

The commitments of H are checked against the snapshots of H−C, H−2C and H−3C. Each rebuild of those snapshots must read the same blocks they were first built from. That gives 6 × dkgInterval + dkgMiningWindowEnd + WORK_DIFF_DEPTH, as the comment on GetEvoPruneLockHeight() says.

Nothing else adds depth:

  • A missing previous snapshot is never rebuilt from older work blocks.
  • A cycle's snapshot is stored when its base block connects (src/llmq/blockprocessor.cpp:223), before anything reads it.
  • All quorum indexes of a cycle share its work block (cycleBase − WORK_DIFF_DEPTH) and its mining window (cycleBase + dkgMiningWindowStart to cycleBase + dkgMiningWindowEnd).

So refusing to start on a datadir that lacks those blocks is intended. On mainnet, testnet and devnet this keeps 634 more blocks than the credit pool alone needs. On regtest it keeps none.


🤖 Posted autonomously by Claude on behalf of pasta.

const int llmq_depth{cycles * llmq_params.dkgInterval + llmq_params.dkgMiningWindowEnd + llmq::WORK_DIFF_DEPTH};
depth = std::max(depth, llmq_depth);
}
// Nothing before the credit pool window of the first V20 block is read
return std::max(tip_height - depth, chainparams.GetConsensus().V20Height - chainparams.CreditPoolPeriodBlocks());
}

static bool AddNetInfoEntries(const std::shared_ptr<NetInfoInterface>& net_info, NetInfoPurpose purpose,
const NetInfoList& entries, BlockValidationState& state)
{
Expand Down
7 changes: 7 additions & 0 deletions src/evo/specialtxman.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class CBlock;
class CBlockIndex;
class CCbTx;
class CChain;
class CChainParams;
class CCoinsViewCache;
class CCreditPoolManager;
class CDeterministicMNList;
Expand Down Expand Up @@ -115,6 +116,12 @@ class CSpecialTxProcessor
CAmount blockSubsidy, BlockValidationState& state) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
};

/**
* Lowest height whose block data the credit pool and quorum member code may read back from disk while
* validating the blocks after tip_height. Pruning has to keep these blocks, see Chainstate::FlushStateToDisk().
*/
[[nodiscard]] int GetEvoPruneLockHeight(const CChainParams& chainparams, int tip_height);

/**
* This helper does some trivial validations that doesn't depends on collateral and
* masternode list. Use CheckPro*Tx for the full validation of transaction,
Expand Down
3 changes: 2 additions & 1 deletion src/init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,7 @@ void SetupServerArgs(ArgsManager& argsman)
#endif
argsman.AddArg("-assumevalid=<hex>", strprintf("If this block is in the chain assume that it and its ancestors are valid and potentially skip their script verification (0 to verify all, default: %s, testnet: %s, devnet: %s)", defaultChainParams->GetConsensus().defaultAssumeValid.GetHex(), testnetChainParams->GetConsensus().defaultAssumeValid.GetHex(), devnetChainParams->GetConsensus().defaultAssumeValid.GetHex()), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-blocksdir=<dir>", "Specify directory to hold blocks subdirectory for *.dat files (default: <datadir>)", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS);
argsman.AddArg("-evoprunelock", strprintf("Keep the blocks that evo validation reads back from disk when pruning, only disable it for testing (default: %u)", DEFAULT_EVO_PRUNE_LOCK), ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-fastprune", "Use smaller block files and lower minimum prune height for testing purposes", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
argsman.AddArg("-tinyblk", "Use smaller block files for testing purposes", ArgsManager::ALLOW_ANY | ArgsManager::DEBUG_ONLY, OptionsCategory::DEBUG_TEST);
#if HAVE_SYSTEM
Expand Down Expand Up @@ -2134,7 +2135,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
if (!options.reindex) {
bool fRet = uiInterface.ThreadSafeQuestion(
error + Untranslated(".\n\n") + _("Do you want to rebuild the block database now?"),
error.original + ".\nPlease restart with -reindex or -reindex-chainstate to recover.",
error.original + (args.GetIntArg("-prune", 0) ? ".\nPlease restart with -reindex to recover." : ".\nPlease restart with -reindex or -reindex-chainstate to recover."),
"", CClientUIInterface::MSG_ERROR | CClientUIInterface::BTN_ABORT);
if (fRet) {
fReindex = true;
Expand Down
4 changes: 4 additions & 0 deletions src/kernel/chainstatemanager_opts.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ struct SuperblockStatus;

static constexpr bool DEFAULT_CHECKPOINTS_ENABLED{true};
static constexpr auto DEFAULT_MAX_TIP_AGE{6h}; // ~144 blocks behind -> 2 x fork detection time, was 24h in bitcoin
static constexpr bool DEFAULT_EVO_PRUNE_LOCK{true};

namespace kernel {

Expand All @@ -38,6 +39,9 @@ struct ChainstateManagerOpts {
std::optional<uint256> assumed_valid_block{};
//! If the tip is older than this, the node is considered to be in initial block download.
std::chrono::seconds max_tip_age{DEFAULT_MAX_TIP_AGE};
//! Dash: keep the blocks evo validation reads back from disk when pruning, see GetEvoPruneLockHeight(). Only
//! disabled in tests, to get a datadir pruned without them.
bool evo_prune_lock{DEFAULT_EVO_PRUNE_LOCK};
//! Dash: governance's superblock status for a block height, consulted by
//! block validation and the miner. Unset means superblock validation is disabled.
std::function<SuperblockStatus(const CDeterministicMNList& mn_list, int nBlockHeight)> superblock_status{};
Expand Down
3 changes: 2 additions & 1 deletion src/node/blockstorage.h
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,8 @@ class BlockManager
std::set<int> m_dirty_fileinfo;

/**
* Map from external index name to oldest block that must not be pruned.
* Map from external index name (or "evo", see Chainstate::FlushStateToDisk()) to oldest block that must not be
* pruned.
*
* @note Internally, only blocks at height (height_first - PRUNE_LOCK_BUFFER - 1) and
* below will be pruned, but callers should avoid assuming any particular buffer size.
Expand Down
13 changes: 13 additions & 0 deletions src/node/chainstate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,19 @@ static ChainstateLoadResult CompleteChainstateInitialization(ChainstateManager&
}
}

// Blocks pruned without the "evo" prune lock, e.g. by an earlier release, may be missing blocks that validating the
// next blocks reads. A snapshot chainstate has no blocks below its base, -reindex can't help it.
if (chainman.m_blockman.m_have_pruned && !chainman.IsSnapshotActive() &&
!is_coinsview_empty(&chainman.ActiveChainstate())) {
const CBlockIndex* missing{FindMissingEvoBlockData(chainman.GetParams(), chainman.ActiveTip())};
if (missing) {
return {ChainstateLoadStatus::FAILURE,

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 Advertise only full reindex for missing block data

When a pruned pre-lock datadir reaches this branch, ChainstateLoadStatus::FAILURE is handled by AppInitMain with the generic stderr advice to restart with either -reindex or -reindex-chainstate. The latter cannot restore deleted block files and is explicitly rejected by the prune-mode parameter check in init.cpp with Prune mode is incompatible with -reindex-chainstate. Use full -reindex instead, so following the final recovery instruction merely produces another failed startup. Return a status/recovery message that preserves the GUI reindex flow but advertises only full -reindex for this case.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Valid, fixed in 8d701e4.

I reproduced it on 1bd42f8. When the startup check fails, dashd prints the error and then "Please restart with -reindex or -reindex-chainstate to recover.". Following the second option fails right away with "Prune mode is incompatible with -reindex-chainstate. Use full -reindex instead.". In the GUI, the question dialog shows the error and "Do you want to rebuild the block database now?". Choosing rebuild runs a full reindex, so that path was already correct; only the non-interactive hint (stderr and debug.log) was wrong.

Upstream has already fixed this in bitcoin#35849, which Dash hasn't backported yet. I took the same one-line change in AppInitMain: the hint is now "Please restart with -reindex to recover." whenever -prune is set, the same condition that rejects -reindex-chainstate. The status stays ChainstateLoadStatus::FAILURE, so the GUI rebuild prompt doesn't change. Unpruned nodes keep the existing hint.

Tests: feature_pruning_evo.py now expects the -reindex-only hint in the startup refusal case. evo_prune_tests, feature_pruning_evo.py, rpc_blockchain.py --v1transport (generic hint on an unpruned node) and feature_init.py pass. The release notes already say that such a node asks to restart with -reindex.


🤖 Posted autonomously by Claude on behalf of pasta.

strprintf(_("Block %d, which is needed to validate new blocks, has been pruned. You need to "
"rebuild the database using -reindex. This will redownload the entire blockchain"),
missing->nHeight)};
}
}

if (!special_tx.m_mnhfman->ForceSignalDBUpdate(chainman.ActiveTip())) {
return {ChainstateLoadStatus::FAILURE, _("Error upgrading evo database for EHF")};
}
Expand Down
2 changes: 2 additions & 0 deletions src/node/chainstatemanager_args.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ std::optional<bilingual_str> ApplyArgsManOptions(const ArgsManager& args, Chains

if (auto value{args.GetIntArg("-maxtipage")}) opts.max_tip_age = std::chrono::seconds{*value};

if (auto value{args.GetBoolArg("-evoprunelock")}) opts.evo_prune_lock = *value;

return std::nullopt;
}
} // namespace node
162 changes: 162 additions & 0 deletions src/test/evo_prune_tests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Copyright (c) 2026 The Dash Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <test/util/setup_common.h>

#include <chain.h>
#include <chainparams.h>
#include <chainparamsbase.h>
#include <evo/creditpool.h>
#include <evo/specialtxman.h>
#include <llmq/snapshot.h>
#include <util/system.h>
#include <validation.h>

#include <boost/test/unit_test.hpp>

#include <string>

BOOST_FIXTURE_TEST_SUITE(evo_prune_tests, BasicTestingSetup)

BOOST_AUTO_TEST_CASE(evo_prune_lock_depth)
{
// Mainnet, testnet and devnet: rebuilding LLMQ_60_75 members (dkgInterval 288, dkgMiningWindowEnd 50) dominates.
// Regtest: rebuilding the credit pool (snapshot period 576, CreditPoolPeriodBlocks 100) dominates.
const std::pair<std::string, int> expected_depths[]{
{CBaseChainParams::MAIN, 6 * 288 + 50 + 8},
{CBaseChainParams::TESTNET, 6 * 288 + 50 + 8},
{CBaseChainParams::DEVNET, 6 * 288 + 50 + 8},
{CBaseChainParams::REGTEST, 576 + 100},
};
for (const auto& [chain, depth] : expected_depths) {
const auto params{CreateChainParams(ArgsManager{}, chain)};
const int tip{params->GetConsensus().V20Height + 10'000};
BOOST_CHECK_EQUAL(GetEvoPruneLockHeight(*params, tip), tip - depth);
}
}

BOOST_AUTO_TEST_CASE(evo_prune_lock_keeps_rotated_quorum_work_blocks)
{
for (const auto& chain :
{CBaseChainParams::MAIN, CBaseChainParams::TESTNET, CBaseChainParams::DEVNET, CBaseChainParams::REGTEST}) {
const auto params{CreateChainParams(ArgsManager{}, chain)};
const auto& consensus{params->GetConsensus()};
for (const auto& llmq_params : consensus.llmqs) {
if (!llmq_params.useRotation) continue;
const int interval{llmq_params.dkgInterval};
const int cycle_base{(consensus.V20Height / interval + 10) * interval};
// The last block that can carry a commitment of the cycle connects on top of this tip
const int tip{cycle_base + llmq_params.dkgMiningWindowEnd - 1};
// Its members are built from the work blocks of the cycle and of the three previous cycles, and the
// members of the oldest of these cycles were built from the work blocks of the three cycles before it
const int oldest_work_block{cycle_base - 6 * interval - llmq::WORK_DIFF_DEPTH};
BOOST_CHECK_LE(GetEvoPruneLockHeight(*params, tip), oldest_work_block);
}
}
}

BOOST_AUTO_TEST_CASE(evo_prune_lock_keeps_credit_pool_blocks)
{
for (const auto& chain :
{CBaseChainParams::MAIN, CBaseChainParams::TESTNET, CBaseChainParams::DEVNET, CBaseChainParams::REGTEST}) {
const auto params{CreateChainParams(ArgsManager{}, chain)};
const int period{CCreditPoolManager::DISK_SNAPSHOT_PERIOD};
// A rebuild on a cold cache starts furthest back when the tip is at a disk snapshot height, as the snapshot of
// the tip is only written once the next block connects
const int tip{(params->GetConsensus().V20Height / period + 10) * period};
// The first block after the previous snapshot reads the block CreditPoolPeriodBlocks before it
const int oldest_read{tip - period + 1 - params->CreditPoolPeriodBlocks()};
BOOST_CHECK_LE(GetEvoPruneLockHeight(*params, tip), oldest_read);
}
}

struct RegTestV20At2000Setup : public BasicTestingSetup {
RegTestV20At2000Setup() :
BasicTestingSetup{CBaseChainParams::REGTEST, {"-testactivationheight=v20@2000", "-testactivationheight=mn_rr@2000"}}
{
}
};

BOOST_FIXTURE_TEST_CASE(evo_prune_lock_before_v20, RegTestV20At2000Setup)
{
const CChainParams& params{Params()};
BOOST_REQUIRE_EQUAL(params.GetConsensus().V20Height, 2000);
// Nothing before the credit pool window of the first V20 block is read
BOOST_CHECK_EQUAL(GetEvoPruneLockHeight(params, 1000), 2000 - 100);
BOOST_CHECK_EQUAL(GetEvoPruneLockHeight(params, 2000 + 576), 2000 - 100);
BOOST_CHECK_EQUAL(GetEvoPruneLockHeight(params, 2000 + 576 + 1), 2000 - 100 + 1);
BOOST_CHECK_EQUAL(GetEvoPruneLockHeight(params, 5000), 5000 - (576 + 100));
}

// Link blocks into a chain of block indexes from first_height on, all with data
static void MakeChain(std::vector<CBlockIndex>& blocks, int first_height) EXCLUSIVE_LOCKS_REQUIRED(::cs_main)
{
for (size_t i{0}; i < blocks.size(); ++i) {
blocks[i].nHeight = first_height + static_cast<int>(i);
blocks[i].pprev = i > 0 ? &blocks[i - 1] : nullptr;
blocks[i].nStatus = BLOCK_HAVE_DATA;
}
}

// The height of the block FindMissingEvoBlockData() reports for the last block when only the block at missing_height
// has no data, or -1 for none
static int FindMissingWith(const CChainParams& params, std::vector<CBlockIndex>& blocks, int missing_height)
EXCLUSIVE_LOCKS_REQUIRED(::cs_main)
{
CBlockIndex& missing{blocks.at(missing_height - blocks.front().nHeight)};
missing.nStatus &= ~BLOCK_HAVE_DATA;
const CBlockIndex* found{FindMissingEvoBlockData(params, &blocks.back())};
missing.nStatus |= BLOCK_HAVE_DATA;
return found ? found->nHeight : -1;
}

BOOST_AUTO_TEST_CASE(evo_prune_find_missing_block_data)
{
LOCK(cs_main);
const auto params{CreateChainParams(ArgsManager{}, CBaseChainParams::MAIN)};
const auto llmq_params{params->GetLLMQ(Consensus::LLMQType::LLMQ_60_75)};
BOOST_REQUIRE(llmq_params && llmq_params->useRotation);
const int interval{llmq_params->dkgInterval};
const int period{CCreditPoolManager::DISK_SNAPSHOT_PERIOD};
// A cycle of rotated quorums that starts at a credit pool snapshot height, with a commitment mined on top of tip
const int cycle_base{(params->GetConsensus().V20Height / period + 10) * period};
BOOST_REQUIRE_EQUAL(cycle_base % interval, 0);
const int tip{cycle_base + 40};
// Its members are built from the work block of the third cycle before it, which is older than any block that the
// credit pool reads for the next block (from the snapshot of cycle_base on)
const int work_block{cycle_base - 3 * interval - llmq::WORK_DIFF_DEPTH};
BOOST_REQUIRE_LT(work_block, cycle_base + 1 - params->CreditPoolPeriodBlocks());

const int height_first{GetEvoPruneLockHeight(*params, tip)};
BOOST_CHECK_EQUAL(height_first, tip - (6 * 288 + 50 + 8));
// The lock keeps more blocks below them for a reorg, which a rewound tip may have used up, so these aren't checked
const int lock_first{GetEvoPruneLockHeightForTip(*params, tip)};
BOOST_CHECK_EQUAL(lock_first, height_first - static_cast<int>(MIN_BLOCKS_TO_KEEP));
std::vector<CBlockIndex> blocks(tip - (lock_first - 10) + 1);
MakeChain(blocks, lock_first - 10);
BOOST_CHECK(FindMissingEvoBlockData(*params, &blocks.back()) == nullptr);
BOOST_CHECK_EQUAL(FindMissingWith(*params, blocks, work_block), work_block);
BOOST_CHECK_EQUAL(FindMissingWith(*params, blocks, tip), tip);
BOOST_CHECK_EQUAL(FindMissingWith(*params, blocks, height_first), height_first);
BOOST_CHECK_EQUAL(FindMissingWith(*params, blocks, height_first - 1), -1);
BOOST_CHECK_EQUAL(FindMissingWith(*params, blocks, lock_first), -1);
}

BOOST_AUTO_TEST_CASE(evo_prune_find_missing_block_data_before_v20)
{
LOCK(cs_main);
const auto params{CreateChainParams(ArgsManager{}, CBaseChainParams::REGTEST)};
// Nothing before the credit pool window of the V20 activation block is kept or checked
const int floor{params->GetConsensus().V20Height - params->CreditPoolPeriodBlocks()};
std::vector<CBlockIndex> blocks(floor + 11);
MakeChain(blocks, 0);
BOOST_CHECK_EQUAL(FindMissingWith(*params, blocks, floor), floor);
BOOST_CHECK_EQUAL(FindMissingWith(*params, blocks, floor - 1), -1);
// A chain that ends below it isn't checked at all
std::vector<CBlockIndex> short_blocks(floor);
MakeChain(short_blocks, 0);
BOOST_CHECK_EQUAL(FindMissingWith(*params, short_blocks, floor - 1), -1);
}

BOOST_AUTO_TEST_SUITE_END()
25 changes: 25 additions & 0 deletions src/validation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3191,6 +3191,11 @@ bool Chainstate::FlushStateToDisk(
CoinsCacheSizeState cache_state = GetCoinsCacheSizeState();
LOCK(m_blockman.cs_LastBlockFile);
if (m_blockman.IsPruneMode() && (m_blockman.m_check_for_pruning || nManualPruneHeight > 0) && !fReindex) {
if (m_chainman.m_options.evo_prune_lock) {
m_blockman.UpdatePruneLock("evo", {.height_first = GetEvoPruneLockHeightForTip(m_chainman.GetParams(),
m_chain.Height())});
}

// make sure we don't prune above any of the prune locks bestblocks
// pruning is height-based
int last_prune{m_chain.Height()}; // last height we can prune
Expand Down Expand Up @@ -5186,6 +5191,26 @@ void PruneBlockFilesManual(Chainstate& active_chainstate, int nManualPruneHeight
}
}

int GetEvoPruneLockHeightForTip(const CChainParams& chainparams, int tip_height)
{
// evo and LLMQ validation reads blocks from disk further back than MIN_BLOCKS_TO_KEEP, also for the blocks
// connected after a reorg of up to MIN_BLOCKS_TO_KEEP blocks
return GetEvoPruneLockHeight(chainparams, tip_height - static_cast<int>(MIN_BLOCKS_TO_KEEP));
}

const CBlockIndex* FindMissingEvoBlockData(const CChainParams& chainparams, const CBlockIndex* tip)
{
AssertLockHeld(::cs_main);
if (tip == nullptr) return nullptr;
// The blocks that validating the next blocks reads. The lock keeps more below them for a reorg, but a node that
// rewound its tip, e.g. with invalidateblock, may have used those up and can still connect the next blocks.
const int height_first{GetEvoPruneLockHeight(chainparams, tip->nHeight)};
for (const CBlockIndex* pindex{tip}; pindex != nullptr && pindex->nHeight >= height_first; pindex = pindex->pprev) {
if (!(pindex->nStatus & BLOCK_HAVE_DATA)) return pindex;
}
return nullptr;
}

void Chainstate::LoadMempool(const fs::path& load_path, FopenFn mockable_fopen_function)
{
if (!m_mempool) return;
Expand Down
Loading
Loading