diff --git a/doc/release-notes-7845.md b/doc/release-notes-7845.md new file mode 100644 index 000000000000..a29b299d3133 --- /dev/null +++ b/doc/release-notes-7845.md @@ -0,0 +1,15 @@ +Pruning +------- + +- Pruned nodes now keep the block history needed to rebuild the credit pool after a restart: + back to one withdrawal window (576 blocks on mainnet and testnet) before the newest + credit-pool snapshot on disk that is at or below the deepest block a reorg could return to. + This is typically around 1,400 blocks instead of 288, still far below the 550 MiB minimum + prune target. The kept range moves forward as the tip advances and new snapshots are written + to disk, so it can trail the tip by up to about a day of blocks. +- A node that upgrades from a version before v24.0.0-rc.1 may have no recent credit-pool + snapshots on disk. Pruning then pauses until the next snapshot is written, which takes one to + two days. +- If credit-pool history that the node needs has already been pruned, the node now stops with + an error asking for `-reindex`. Previously it marked a valid block as invalid and stopped + following the chain. 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