From ef31df331a6f2c5161a9d60eee84b8fcbc58a737 Mon Sep 17 00:00:00 2001 From: pasta Date: Wed, 7 Oct 2026 03:23:29 -0500 Subject: [PATCH 1/2] fix(evo): keep credit-pool history through pruning Rebuilding a credit pool after a restart reads blocks back to one withdrawal window before the latest disk snapshot, which can be far older than the 288 blocks pruning keeps. Once those bodies were pruned, the failed read was reported as a consensus failure and a canonical block was permanently marked invalid. Protect the history that reconstruction needs with a prune lock. The lock is recomputed before every pruning decision from snapshots that are already committed to disk, for the oldest parent any chainstate may still need after a reorg or a restart. The EvoDB is synced before pruned files are unlinked. A block body that is unavailable locally now stops the node with a -reindex hint instead of invalidating the block. Pruning tests that prune early regtest blocks postpone V20, as the other pruning tests already do. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/evo/creditpool.cpp | 27 +++++- src/evo/creditpool.h | 11 +++ src/evo/evodb.cpp | 7 ++ src/evo/evodb.h | 3 + src/evo/specialtxman.cpp | 6 ++ src/test/evo_assetlocks_tests.cpp | 85 ++++++++++++++++++- src/validation.cpp | 34 ++++++++ test/functional/feature_creditpool_pruning.py | 75 ++++++++++++++++ .../feature_remove_pruned_files_on_startup.py | 7 +- test/functional/interface_usdt_utxocache.py | 9 +- test/functional/rpc_getblockfrompeer.py | 12 ++- test/functional/rpc_rawtransaction.py | 6 ++ test/functional/test_runner.py | 1 + 13 files changed, 274 insertions(+), 9 deletions(-) create mode 100755 test/functional/feature_creditpool_pruning.py diff --git a/src/evo/creditpool.cpp b/src/evo/creditpool.cpp index cafe65b023f4..a7400e044b0d 100644 --- a/src/evo/creditpool.cpp +++ b/src/evo/creditpool.cpp @@ -80,7 +80,8 @@ static std::optional GetCreditDataFromBlock(const gsl::n CBlock block; if (!ReadBlockFromDisk(block, block_index, consensusParams)) { - throw std::runtime_error("failed-getcbforblock-read"); + throw CreditPoolBlockReadError(strprintf("failed-getcbforblock-read: block %s at height %d is unavailable", + block_index->GetBlockHash().ToString(), block_index->nHeight)); } if (block.vtx.empty() || block.vtx[0]->vExtraPayload.empty() || !block.vtx[0]->IsSpecialTxVersion()) { @@ -143,6 +144,26 @@ std::optional CCreditPoolManager::GetFromCache(const CBlockIndex& b return std::nullopt; } +int CCreditPoolManager::GetPruneLockHeight(const CBlockIndex& oldest_parent) +{ + const auto& consensus = m_chainman.GetConsensus(); + const int window = m_chainman.GetParams().CreditPoolPeriodBlocks(); + // Pending snapshots cannot protect history: pruning unlinks files before + // the current coins and EvoDB overlays are committed. + int first_read = consensus.V20Height - window; + for (int height = oldest_parent.nHeight - oldest_parent.nHeight % DISK_SNAPSHOT_PERIOD; + height >= consensus.V20Height; height -= DISK_SNAPSHOT_PERIOD) { + CCreditPool pool; + if (evoDb.GetRawDB().Read(std::make_pair(DB_CREDITPOOL_SNAPSHOT, oldest_parent.GetAncestor(height)->GetBlockHash()), + pool)) { + first_read = height + 1 - window; + break; + } + } + // GetCreditDataFromBlock() never reads bodies from before DIP0003. + return std::max(consensus.DIP0003Height, first_read); +} + void CCreditPoolManager::MaybeWriteSnapshot(const uint256& block_hash, int height, const CCreditPool& pool) { // The disk snapshot is an optimization; skip it outside a block-scoped EvoDB transaction @@ -413,6 +434,10 @@ std::optional GetCreditPoolDiffForBlock(CCreditPoolManager& cpo } } return creditPoolDiff; + } catch (const CreditPoolBlockReadError& e) { + // Missing local history is not a statement about the block. + AbortNode(state, strprintf("%s; restart with -reindex to restore it", e.what())); + return std::nullopt; } catch (const EvoDbInconsistencyError& e) { // Local EvoDB corruption (the node is already aborting): fail with // M_ERROR so the block is not marked invalid. diff --git a/src/evo/creditpool.h b/src/evo/creditpool.h index e7676beace54..39ed7bb226f1 100644 --- a/src/evo/creditpool.h +++ b/src/evo/creditpool.h @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -36,6 +37,13 @@ namespace llmq { class CQuorumManager; } // namespace llmq +/** Unavailable local block history must not invalidate a candidate block. */ +class CreditPoolBlockReadError : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + struct CCreditPool { CAmount locked{0}; @@ -175,6 +183,9 @@ class CCreditPoolManager */ CCreditPool GetCreditPool(const CBlockIndex* block) EXCLUSIVE_LOCKS_REQUIRED(!cache_mutex); + /** First required body when reconstructing from a persisted snapshot at/before this parent. */ + int GetPruneLockHeight(const CBlockIndex& oldest_parent); + private: std::optional GetFromCache(const CBlockIndex& block_index) EXCLUSIVE_LOCKS_REQUIRED(!cache_mutex); void AddToCache(const uint256& block_hash, int height, const CCreditPool& pool) EXCLUSIVE_LOCKS_REQUIRED(!cache_mutex); diff --git a/src/evo/evodb.cpp b/src/evo/evodb.cpp index 99dbff46c5e6..40c1ab8fcd31 100644 --- a/src/evo/evodb.cpp +++ b/src/evo/evodb.cpp @@ -107,6 +107,13 @@ bool CEvoDB::CommitRootTransaction(EvoDbIdentity identity, bool sync) return ret; } +bool CEvoDB::Sync() +{ + LOCK(cs); + CDBBatch batch{*db}; + return db->WriteBatch(batch, /*fSync=*/true); +} + bool CEvoDB::ReadBestBlock(EvoDbIdentity identity, uint256& hash) { LOCK(cs); diff --git a/src/evo/evodb.h b/src/evo/evodb.h index f9485eb09b89..9107b36e178f 100644 --- a/src/evo/evodb.h +++ b/src/evo/evodb.h @@ -233,6 +233,9 @@ class CEvoDB return result; } + /** Sync committed records without committing any current/root overlay. */ + bool Sync() EXCLUSIVE_LOCKS_REQUIRED(!cs); + bool CommitRootTransaction(EvoDbIdentity identity = EvoDbIdentity::NORMAL, bool sync = false) EXCLUSIVE_LOCKS_REQUIRED(::cs_main, !cs); diff --git a/src/evo/specialtxman.cpp b/src/evo/specialtxman.cpp index aef331c99d8d..9332c8d76952 100644 --- a/src/evo/specialtxman.cpp +++ b/src/evo/specialtxman.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include CSpecialTxProcessor::CSpecialTxProcessor(CEvoDB& evodb, CDeterministicMNManager& dmnman, @@ -1014,6 +1015,11 @@ bool CSpecialTxProcessor::ProcessSpecialTxsInBlock(Chainstate& chainstate, const bls::bls_legacy_scheme.store(false); LogPrintf("CSpecialTxProcessor::%s -- bls_legacy_scheme=%d\n", __func__, bls::bls_legacy_scheme.load()); } + } catch (const CreditPoolBlockReadError& e) { + // Missing local history is not a statement about the block. + const std::string msg{strprintf("%s; restart with -reindex to restore it", e.what())}; + AbortNode(msg); + return state.Error(msg); } catch (const EvoDbInconsistencyError& e) { // Local EvoDB corruption detected below (the node is already // aborting): fail with M_ERROR so the block is not marked invalid. diff --git a/src/test/evo_assetlocks_tests.cpp b/src/test/evo_assetlocks_tests.cpp index 8db84d70d92a..d3fc3ad2f20e 100644 --- a/src/test/evo_assetlocks_tests.cpp +++ b/src/test/evo_assetlocks_tests.cpp @@ -10,14 +10,17 @@ #include #include #include -#include +#include #include #include +#include #include #include +#include #include