Skip to content
5 changes: 5 additions & 0 deletions src/interfaces/chain.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ struct CBlockLocator;
struct FeeCalculation;
namespace node {
struct NodeContext;
struct PruneLockInfo;
} // namespace node

namespace interfaces {
Expand Down Expand Up @@ -136,6 +137,10 @@ class Chain
//! pruned), and contains transactions.
virtual bool haveBlockOnDisk(int height) = 0;

virtual bool pruneLockExists(const std::string& name) const = 0;
virtual bool updatePruneLock(const std::string& name, const node::PruneLockInfo& lock_info, bool sync=false) = 0;
virtual bool deletePruneLock(const std::string& name) = 0;

//! Get locator for the current chain tip.
virtual CBlockLocator getTipLocator() = 0;

Expand Down
101 changes: 97 additions & 4 deletions src/node/blockstorage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <undo.h>
#include <util/batchpriority.h>
#include <util/fs.h>
#include <util/overflow.h>
#include <util/signalinterrupt.h>
#include <util/strencodings.h>
#include <util/translation.h>
Expand All @@ -36,6 +37,7 @@ static constexpr uint8_t DB_BLOCK_INDEX{'b'};
static constexpr uint8_t DB_FLAG{'F'};
static constexpr uint8_t DB_REINDEX_FLAG{'R'};
static constexpr uint8_t DB_LAST_BLOCK{'l'};
static constexpr uint8_t DB_PRUNE_LOCK{'L'};
// Keys used in previous version that might still be found in the DB:
// BlockTreeDB::DB_TXINDEX_BLOCK{'T'};
// BlockTreeDB::DB_TXINDEX{'t'}
Expand Down Expand Up @@ -65,7 +67,7 @@ bool BlockTreeDB::ReadLastBlockFile(int& nFile)
return Read(DB_LAST_BLOCK, nFile);
}

bool BlockTreeDB::WriteBatchSync(const std::vector<std::pair<int, const CBlockFileInfo*>>& fileInfo, int nLastFile, const std::vector<const CBlockIndex*>& blockinfo)
bool BlockTreeDB::WriteBatchSync(const std::vector<std::pair<int, const CBlockFileInfo*>>& fileInfo, int nLastFile, const std::vector<const CBlockIndex*>& blockinfo, const std::unordered_map<std::string, node::PruneLockInfo>& prune_locks)
{
CDBBatch batch(*this);
for (const auto& [file, info] : fileInfo) {
Expand All @@ -75,9 +77,40 @@ bool BlockTreeDB::WriteBatchSync(const std::vector<std::pair<int, const CBlockFi
for (const CBlockIndex* bi : blockinfo) {
batch.Write(std::make_pair(DB_BLOCK_INDEX, bi->GetBlockHash()), CDiskBlockIndex{bi});
}
for (const auto& prune_lock : prune_locks) {
if (prune_lock.second.temporary) continue;
batch.Write(std::make_pair(DB_PRUNE_LOCK, prune_lock.first), prune_lock.second);
}
return WriteBatch(batch, true);
}

bool BlockTreeDB::WritePruneLock(const std::string& name, const node::PruneLockInfo& lock_info) {
if (lock_info.temporary) return true;
return Write(std::make_pair(DB_PRUNE_LOCK, name), lock_info);
}

bool BlockTreeDB::DeletePruneLock(const std::string& name) {
return Erase(std::make_pair(DB_PRUNE_LOCK, name));
}

bool BlockTreeDB::LoadPruneLocks(std::unordered_map<std::string, node::PruneLockInfo>& prune_locks, const util::SignalInterrupt& interrupt) {
std::unique_ptr<CDBIterator> pcursor(NewIterator());
for (pcursor->Seek(DB_PRUNE_LOCK); pcursor->Valid(); pcursor->Next()) {
if (interrupt) return false;

std::pair<uint8_t, std::string> key;
if ((!pcursor->GetKey(key)) || key.first != DB_PRUNE_LOCK) break;

node::PruneLockInfo& lock_info = prune_locks[key.second];
if (!pcursor->GetValue(lock_info)) {
return error("%s: failed to %s prune lock '%s'", __func__, "read", key.second);
}
lock_info.temporary = false;
}

return true;
}

bool BlockTreeDB::WriteFlag(const std::string& name, bool fValue)
{
return Write(std::make_pair(DB_FLAG, name), fValue ? uint8_t{'1'} : uint8_t{'0'});
Expand Down Expand Up @@ -165,6 +198,13 @@ bool CBlockIndexHeightOnlyComparator::operator()(const CBlockIndex* pa, const CB
return pa->nHeight < pb->nHeight;
}

/** The number of blocks to keep below the deepest prune lock.
* There is nothing special about this number. It is higher than what we
* expect to see in regular mainnet reorgs, but not so high that it would
* noticeably interfere with the pruning mechanism.
* */
static constexpr int PRUNE_LOCK_BUFFER{10};

std::vector<CBlockIndex*> BlockManager::GetAllBlockIndices()
{
AssertLockHeld(cs_main);
Expand Down Expand Up @@ -258,6 +298,24 @@ void BlockManager::PruneOneBlockFile(const int fileNumber)
m_dirty_fileinfo.insert(fileNumber);
}

bool BlockManager::DoPruneLocksForbidPruning(const CBlockFileInfo& block_file_info)
{
AssertLockHeld(cs_main);
for (const auto& prune_lock : m_prune_locks) {
if (prune_lock.second.height_first == std::numeric_limits<uint64_t>::max()) continue;
// Remove the buffer and one additional block here to get actual height that is outside of the buffer
const uint64_t lock_height{(prune_lock.second.height_first <= PRUNE_LOCK_BUFFER + 1) ? 1 : (prune_lock.second.height_first - PRUNE_LOCK_BUFFER - 1)};
const uint64_t lock_height_last{SaturatingAdd(prune_lock.second.height_last, (uint64_t)PRUNE_LOCK_BUFFER)};
if (block_file_info.nHeightFirst > lock_height_last) continue;
if (block_file_info.nHeightLast <= lock_height) continue;
// TODO: Check each block within the file against the prune_lock range

LogPrint(BCLog::PRUNE, "%s limited pruning to height %d\n", prune_lock.first, lock_height);
return true;
}
return false;
}

void BlockManager::FindFilesToPruneManual(
std::set<int>& setFilesToPrune,
int nManualPruneHeight,
Expand All @@ -280,6 +338,8 @@ void BlockManager::FindFilesToPruneManual(
continue;
}

if (DoPruneLocksForbidPruning(m_blockfile_info[fileNumber])) continue;

PruneOneBlockFile(fileNumber);
setFilesToPrune.insert(fileNumber);
count++;
Expand Down Expand Up @@ -344,6 +404,8 @@ void BlockManager::FindFilesToPrune(
continue;
}

if (DoPruneLocksForbidPruning(m_blockfile_info[fileNumber])) continue;

PruneOneBlockFile(fileNumber);
// Queue up the files for removal
setFilesToPrune.insert(fileNumber);
Expand All @@ -358,9 +420,38 @@ void BlockManager::FindFilesToPrune(
min_block_to_prune, last_block_can_prune, count);
}

void BlockManager::UpdatePruneLock(const std::string& name, const PruneLockInfo& lock_info) {
bool BlockManager::PruneLockExists(const std::string& name) const {
return m_prune_locks.count(name);
}

bool BlockManager::UpdatePruneLock(const std::string& name, const PruneLockInfo& lock_info, const bool sync) {
AssertLockHeld(::cs_main);
if (sync) {
if (!m_block_tree_db->WritePruneLock(name, lock_info)) {
return error("%s: failed to %s prune lock '%s'", __func__, "write", name);
}
}
PruneLockInfo& stored_lock_info = m_prune_locks[name];
if (lock_info.temporary && !stored_lock_info.temporary) {
// Erase non-temporary lock from disk
if (!m_block_tree_db->DeletePruneLock(name)) {
return error("%s: failed to %s prune lock '%s'", __func__, "erase", name);
}
}
stored_lock_info = lock_info;
return true;
}

bool BlockManager::DeletePruneLock(const std::string& name)
{
AssertLockHeld(::cs_main);
m_prune_locks[name] = lock_info;
m_prune_locks.erase(name);

// Since there is no reasonable expectation for any follow-up to this prune lock, actually ensure it gets committed to disk immediately
if (!m_block_tree_db->DeletePruneLock(name)) {
return error("%s: failed to %s prune lock '%s'", __func__, "erase", name);
}
return true;
}

CBlockIndex* BlockManager::InsertBlockIndex(const uint256& hash)
Expand All @@ -386,6 +477,8 @@ bool BlockManager::LoadBlockIndex(const std::optional<uint256>& snapshot_blockha
return false;
}

if (!m_block_tree_db->LoadPruneLocks(m_prune_locks, m_interrupt)) return false;

if (snapshot_blockhash) {
const AssumeutxoData au_data = *Assert(GetParams().AssumeutxoForBlockhash(*snapshot_blockhash));
m_snapshot_height = au_data.height;
Expand Down Expand Up @@ -468,7 +561,7 @@ bool BlockManager::WriteBlockIndexDB()
m_dirty_blockindex.erase(it++);
}
int max_blockfile = WITH_LOCK(cs_LastBlockFile, return this->MaxBlockfileNum());
if (!m_block_tree_db->WriteBatchSync(vFiles, max_blockfile, vBlocks)) {
if (!m_block_tree_db->WriteBatchSync(vFiles, max_blockfile, vBlocks, m_prune_locks)) {
return false;
}
return true;
Expand Down
33 changes: 28 additions & 5 deletions src/node/blockstorage.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ struct FlatFilePos;
namespace Consensus {
struct Params;
}
namespace node {
struct PruneLockInfo;
};
namespace util {
class SignalInterrupt;
} // namespace util
Expand All @@ -51,15 +54,18 @@ class BlockTreeDB : public CDBWrapper
{
public:
using CDBWrapper::CDBWrapper;
bool WriteBatchSync(const std::vector<std::pair<int, const CBlockFileInfo*>>& fileInfo, int nLastFile, const std::vector<const CBlockIndex*>& blockinfo);
bool WriteBatchSync(const std::vector<std::pair<int, const CBlockFileInfo*>>& fileInfo, int nLastFile, const std::vector<const CBlockIndex*>& blockinfo, const std::unordered_map<std::string, node::PruneLockInfo>& prune_locks);
bool ReadBlockFileInfo(int nFile, CBlockFileInfo& info);
bool ReadLastBlockFile(int& nFile);
bool WriteReindexing(bool fReindexing);
void ReadReindexing(bool& fReindexing);
bool WritePruneLock(const std::string& name, const node::PruneLockInfo&);
bool DeletePruneLock(const std::string& name);
bool WriteFlag(const std::string& name, bool fValue);
bool ReadFlag(const std::string& name, bool& fValue);
bool LoadBlockIndexGuts(const Consensus::Params& consensusParams, std::function<CBlockIndex*(const uint256&)> insertBlockIndex, const util::SignalInterrupt& interrupt)
EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
bool LoadPruneLocks(std::unordered_map<std::string, node::PruneLockInfo>& prune_locks, const util::SignalInterrupt& interrupt);
};
} // namespace kernel

Expand Down Expand Up @@ -94,7 +100,17 @@ struct CBlockIndexHeightOnlyComparator {
};

struct PruneLockInfo {
int height_first{std::numeric_limits<int>::max()}; //! Height of earliest block that should be kept and not pruned
std::string desc; //! Arbitrary human-readable description of the lock purpose
uint64_t height_first{std::numeric_limits<uint64_t>::max()}; //! Height of earliest block that should be kept and not pruned
uint64_t height_last{std::numeric_limits<uint64_t>::max()}; //! Height of latest block that should be kept and not pruned
bool temporary{true};

SERIALIZE_METHODS(PruneLockInfo, obj)
{
READWRITE(obj.desc);
READWRITE(VARINT(obj.height_first));
READWRITE(VARINT(obj.height_last));
}
};

enum BlockfileType {
Expand Down Expand Up @@ -167,6 +183,8 @@ class BlockManager
bool WriteBlockToDisk(const CBlock& block, FlatFilePos& pos) const;
bool UndoWriteToDisk(const CBlockUndo& blockundo, FlatFilePos& pos, const uint256& hashBlock) const;

bool DoPruneLocksForbidPruning(const CBlockFileInfo& block_file_info) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);

/* Calculate the block/rev files to delete based on height specified by user with RPC command pruneblockchain */
void FindFilesToPruneManual(
std::set<int>& setFilesToPrune,
Expand Down Expand Up @@ -236,14 +254,17 @@ class BlockManager
/** Dirty block file entries. */
std::set<int> m_dirty_fileinfo;

public:
/**
* Map from external index name 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.
* @note Internally, only blocks before height (height_first - PRUNE_LOCK_BUFFER - 1) and
* after height (height_last + PRUNE_LOCK_BUFFER) will be pruned, but callers should
* avoid assuming any particular buffer size.
*/
std::unordered_map<std::string, PruneLockInfo> m_prune_locks GUARDED_BY(::cs_main);

private:
BlockfileType BlockfileTypeForHeight(int height);

const kernel::BlockManagerOpts m_opts;
Expand Down Expand Up @@ -346,8 +367,10 @@ class BlockManager
//! Check whether the block associated with this index entry is pruned or not.
bool IsBlockPruned(const CBlockIndex* pblockindex) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);

bool PruneLockExists(const std::string& name) const SHARED_LOCKS_REQUIRED(::cs_main);
//! Create or update a prune lock identified by its name
void UpdatePruneLock(const std::string& name, const PruneLockInfo& lock_info) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
bool UpdatePruneLock(const std::string& name, const PruneLockInfo& lock_info, bool sync=false) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);
bool DeletePruneLock(const std::string& name) EXCLUSIVE_LOCKS_REQUIRED(::cs_main);

/** Open a block file (blk?????.dat) */
CAutoFile OpenBlockFile(const FlatFilePos& pos, bool fReadOnly = false) const;
Expand Down
18 changes: 18 additions & 0 deletions src/node/interfaces.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,24 @@ class ChainImpl : public Chain
const CBlockIndex* block{chainman().ActiveChain()[height]};
return block && ((block->nStatus & BLOCK_HAVE_DATA) != 0) && block->nTx > 0;
}
bool pruneLockExists(const std::string& name) const override
{
LOCK(cs_main);
auto& blockman = m_node.chainman->m_blockman;
return blockman.PruneLockExists(name);
}
bool updatePruneLock(const std::string& name, const node::PruneLockInfo& lock_info, bool sync) override
{
LOCK(cs_main);
auto& blockman = m_node.chainman->m_blockman;
return blockman.UpdatePruneLock(name, lock_info, sync);
}
bool deletePruneLock(const std::string& name) override
{
LOCK(cs_main);
auto& blockman = m_node.chainman->m_blockman;
return blockman.DeletePruneLock(name);
}
CBlockLocator getTipLocator() override
{
LOCK(::cs_main);
Expand Down
Loading