Skip to content

Commit 1fc4ec7

Browse files
committed
Add pruneblockchain RPC to enable manual block file pruning.
1 parent 5754e03 commit 1fc4ec7

File tree

8 files changed

+207
-24
lines changed

8 files changed

+207
-24
lines changed

doc/man/bitcoin-qt.1

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,13 @@ Specify pid file (default: bitcoind.pid)
7575
.HP
7676
\fB\-prune=\fR<n>
7777
.IP
78-
Reduce storage requirements by pruning (deleting) old blocks. This mode
79-
is incompatible with \fB\-txindex\fR and \fB\-rescan\fR. Warning: Reverting
80-
this setting requires re\-downloading the entire blockchain.
81-
(default: 0 = disable pruning blocks, >550 = target size in MiB
82-
to use for block files)
78+
Reduce storage requirements by enabling pruning (deleting) of old blocks.
79+
This allows the pruneblockchain RPC to be called to delete specific blocks,
80+
and enables automatic pruning of old blocks if a target size in MiB is
81+
provided. This mode is incompatible with \fB\-txindex\fR and \fB\-rescan\fR.
82+
Warning: Reverting this setting requires re\-downloading the entire blockchain.
83+
(default: 0 = disable pruning blocks, 1 = allow manual pruning via RPC, >550 =
84+
automatically prune block files to stay under the specified target size in MiB)
8385
.HP
8486
\fB\-reindex\-chainstate\fR
8587
.IP

doc/man/bitcoind.1

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,13 @@ Specify pid file (default: bitcoind.pid)
8080
.HP
8181
\fB\-prune=\fR<n>
8282
.IP
83-
Reduce storage requirements by pruning (deleting) old blocks. This mode
84-
is incompatible with \fB\-txindex\fR and \fB\-rescan\fR. Warning: Reverting
85-
this setting requires re\-downloading the entire blockchain.
86-
(default: 0 = disable pruning blocks, >550 = target size in MiB
87-
to use for block files)
83+
Reduce storage requirements by enabling pruning (deleting) of old blocks.
84+
This allows the pruneblockchain RPC to be called to delete specific blocks,
85+
and enables automatic pruning of old blocks if a target size in MiB is
86+
provided. This mode is incompatible with \fB\-txindex\fR and \fB\-rescan\fR.
87+
Warning: Reverting this setting requires re\-downloading the entire blockchain.
88+
(default: 0 = disable pruning blocks, 1 = allow manual pruning via RPC, >550 =
89+
automatically prune block files to stay under the specified target size in MiB)
8890
.HP
8991
\fB\-reindex\-chainstate\fR
9092
.IP

qa/rpc-tests/pruning.py

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class PruneTest(BitcoinTestFramework):
2525
def __init__(self):
2626
super().__init__()
2727
self.setup_clean_chain = True
28-
self.num_nodes = 3
28+
self.num_nodes = 5
2929

3030
# Cache for utxos, as the listunspent may take a long time later in the test
3131
self.utxo_cache_0 = []
@@ -43,10 +43,21 @@ def setup_network(self):
4343
self.nodes.append(start_node(2, self.options.tmpdir, ["-debug","-maxreceivebuffer=20000","-prune=550"], timewait=900))
4444
self.prunedir = self.options.tmpdir+"/node2/regtest/blocks/"
4545

46+
# Create node 3 to test manual pruning (it will be re-started with manual pruning later)
47+
self.nodes.append(start_node(3, self.options.tmpdir, ["-debug=0","-maxreceivebuffer=20000","-blockmaxsize=999000"], timewait=900))
48+
self.manualdir = self.options.tmpdir+"/node3/regtest/blocks/"
49+
50+
# Create node 4 to test wallet in prune mode, but do not connect
51+
self.nodes.append(start_node(4, self.options.tmpdir, ["-debug=0", "-prune=550"]))
52+
53+
# Determine default relay fee
54+
self.relayfee = self.nodes[0].getnetworkinfo()["relayfee"]
55+
4656
connect_nodes(self.nodes[0], 1)
4757
connect_nodes(self.nodes[1], 2)
4858
connect_nodes(self.nodes[2], 0)
49-
sync_blocks(self.nodes[0:3])
59+
connect_nodes(self.nodes[0], 3)
60+
sync_blocks(self.nodes[0:4])
5061

5162
def create_big_chain(self):
5263
# Start by creating some coinbases we can spend later
@@ -57,7 +68,7 @@ def create_big_chain(self):
5768
for i in range(645):
5869
mine_large_block(self.nodes[0], self.utxo_cache_0)
5970

60-
sync_blocks(self.nodes[0:3])
71+
sync_blocks(self.nodes[0:4])
6172

6273
def test_height_min(self):
6374
if not os.path.isfile(self.prunedir+"blk00000.dat"):
@@ -212,6 +223,93 @@ def reorg_back(self):
212223
# Verify we can now have the data for a block previously pruned
213224
assert(self.nodes[2].getblock(self.forkhash)["height"] == self.forkheight)
214225

226+
def manual_test(self):
227+
# at this point, node3 has 995 blocks and has not yet run in prune mode
228+
self.nodes[3] = start_node(3, self.options.tmpdir, ["-debug=0"], timewait=900)
229+
assert_raises_message(JSONRPCException, "not in prune mode", self.nodes[3].pruneblockchain, 500)
230+
stop_node(self.nodes[3],3)
231+
232+
# now re-start in manual pruning mode
233+
self.nodes[3] = start_node(3, self.options.tmpdir, ["-debug=0","-prune=1"], timewait=900)
234+
assert_equal(self.nodes[3].getblockcount(), 995)
235+
236+
# should not prune because chain tip of node 3 (995) < PruneAfterHeight (1000)
237+
assert_raises_message(JSONRPCException, "Blockchain is too short for pruning", self.nodes[3].pruneblockchain, 500)
238+
239+
# mine 6 blocks so we are at height 1001 (i.e., above PruneAfterHeight)
240+
self.nodes[3].generate(6)
241+
242+
# negative and zero inputs should raise an exception
243+
try:
244+
self.nodes[3].pruneblockchain(-10)
245+
raise AssertionError("pruneblockchain(-10) should have failed.")
246+
except:
247+
pass
248+
249+
try:
250+
self.nodes[3].pruneblockchain(0)
251+
raise AssertionError("pruneblockchain(0) should have failed.")
252+
except:
253+
pass
254+
255+
# height=100 too low to prune first block file so this is a no-op
256+
self.nodes[3].pruneblockchain(100)
257+
if not os.path.isfile(self.manualdir+"blk00000.dat"):
258+
raise AssertionError("blk00000.dat is missing when should still be there")
259+
260+
# height=500 should prune first file
261+
self.nodes[3].pruneblockchain(500)
262+
if os.path.isfile(self.manualdir+"blk00000.dat"):
263+
raise AssertionError("blk00000.dat is still there, should be pruned by now")
264+
if not os.path.isfile(self.manualdir+"blk00001.dat"):
265+
raise AssertionError("blk00001.dat is missing when should still be there")
266+
267+
# height=650 should prune second file
268+
self.nodes[3].pruneblockchain(650)
269+
if os.path.isfile(self.manualdir+"blk00001.dat"):
270+
raise AssertionError("blk00001.dat is still there, should be pruned by now")
271+
272+
# height=1000 should not prune anything more, because tip-288 is in blk00002.dat.
273+
self.nodes[3].pruneblockchain(1000)
274+
if not os.path.isfile(self.manualdir+"blk00002.dat"):
275+
raise AssertionError("blk00002.dat is still there, should be pruned by now")
276+
277+
# advance the tip so blk00002.dat and blk00003.dat can be pruned (the last 288 blocks should now be in blk00004.dat)
278+
self.nodes[3].generate(288)
279+
self.nodes[3].pruneblockchain(1000)
280+
if os.path.isfile(self.manualdir+"blk00002.dat"):
281+
raise AssertionError("blk00002.dat is still there, should be pruned by now")
282+
if os.path.isfile(self.manualdir+"blk00003.dat"):
283+
raise AssertionError("blk00003.dat is still there, should be pruned by now")
284+
285+
# stop node, start back up with auto-prune at 550MB, make sure still runs
286+
stop_node(self.nodes[3],3)
287+
self.nodes[3] = start_node(3, self.options.tmpdir, ["-debug=0","-prune=550"], timewait=900)
288+
289+
print("Success")
290+
291+
def wallet_test(self):
292+
# check that the pruning node's wallet is still in good shape
293+
print("Stop and start pruning node to trigger wallet rescan")
294+
try:
295+
stop_node(self.nodes[2], 2)
296+
start_node(2, self.options.tmpdir, ["-debug=1","-prune=550"])
297+
print("Success")
298+
except Exception as detail:
299+
raise AssertionError("Wallet test: unable to re-start the pruning node")
300+
301+
# check that wallet loads loads successfully when restarting a pruned node after IBD.
302+
# this was reported to fail in #7494.
303+
print ("Syncing node 4 to test wallet")
304+
connect_nodes(self.nodes[0], 4)
305+
nds = [self.nodes[0], self.nodes[4]]
306+
sync_blocks(nds)
307+
try:
308+
stop_node(self.nodes[4],4) #stop and start to trigger rescan
309+
start_node(4, self.options.tmpdir, ["-debug=1","-prune=550"])
310+
print ("Success")
311+
except Exception as detail:
312+
raise AssertionError("Wallet test: unable to re-start node4")
215313

216314
def run_test(self):
217315
print("Warning! This test requires 4GB of disk space and takes over 30 mins (up to 2 hours)")
@@ -226,6 +324,9 @@ def run_test(self):
226324
# Start by mining a simple chain that all nodes have
227325
# N0=N1=N2 **...*(995)
228326

327+
# stop manual-pruning node with 995 blocks
328+
stop_node(self.nodes[3],3)
329+
229330
print("Check that we haven't started pruning yet because we're below PruneAfterHeight")
230331
self.test_height_min()
231332
# Extend this chain past the PruneAfterHeight
@@ -308,6 +409,12 @@ def run_test(self):
308409
#
309410
# N1 doesn't change because 1033 on main chain (*) is invalid
310411

412+
print("Test manual pruning")
413+
self.manual_test()
414+
415+
print("Test wallet re-scan")
416+
self.wallet_test()
417+
311418
print("Done")
312419

313420
if __name__ == '__main__':

src/init.cpp

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -351,9 +351,9 @@ std::string HelpMessage(HelpMessageMode mode)
351351
#ifndef WIN32
352352
strUsage += HelpMessageOpt("-pid=<file>", strprintf(_("Specify pid file (default: %s)"), BITCOIN_PID_FILENAME));
353353
#endif
354-
strUsage += HelpMessageOpt("-prune=<n>", strprintf(_("Reduce storage requirements by pruning (deleting) old blocks. This mode is incompatible with -txindex and -rescan. "
354+
strUsage += HelpMessageOpt("-prune=<n>", strprintf(_("Reduce storage requirements by enabling pruning (deleting) of old blocks. This allows the pruneblockchain RPC to be called to delete specific blocks, and enables automatic pruning of old blocks if a target size in MiB is provided. This mode is incompatible with -txindex and -rescan. "
355355
"Warning: Reverting this setting requires re-downloading the entire blockchain. "
356-
"(default: 0 = disable pruning blocks, >%u = target size in MiB to use for block files)"), MIN_DISK_SPACE_FOR_BLOCK_FILES / 1024 / 1024));
356+
"(default: 0 = disable pruning blocks, 1 = allow manual pruning via RPC, >%u = automatically prune block files to stay under the specified target size in MiB)"), MIN_DISK_SPACE_FOR_BLOCK_FILES / 1024 / 1024));
357357
strUsage += HelpMessageOpt("-reindex-chainstate", _("Rebuild chain state from the currently indexed blocks"));
358358
strUsage += HelpMessageOpt("-reindex", _("Rebuild chain state and block index from the blk*.dat files on disk"));
359359
#ifndef WIN32
@@ -936,12 +936,16 @@ bool AppInitParameterInteraction()
936936
nScriptCheckThreads = MAX_SCRIPTCHECK_THREADS;
937937

938938
// block pruning; get the amount of disk space (in MiB) to allot for block & undo files
939-
int64_t nSignedPruneTarget = GetArg("-prune", 0) * 1024 * 1024;
940-
if (nSignedPruneTarget < 0) {
939+
int64_t nPruneArg = GetArg("-prune", 0);
940+
if (nPruneArg < 0) {
941941
return InitError(_("Prune cannot be configured with a negative value."));
942942
}
943-
nPruneTarget = (uint64_t) nSignedPruneTarget;
944-
if (nPruneTarget) {
943+
nPruneTarget = (uint64_t) nPruneArg * 1024 * 1024;
944+
if (nPruneArg == 1) { // manual pruning: -prune=1
945+
LogPrintf("Block pruning enabled. Use RPC call pruneblockchain(height) to manually prune block and undo files.\n");
946+
nPruneTarget = std::numeric_limits<uint64_t>::max();
947+
fPruneMode = true;
948+
} else if (nPruneTarget) {
945949
if (nPruneTarget < MIN_DISK_SPACE_FOR_BLOCK_FILES) {
946950
return InitError(strprintf(_("Prune configured below the minimum of %d MiB. Please use a higher number."), MIN_DISK_SPACE_FOR_BLOCK_FILES / 1024 / 1024));
947951
}

src/rpc/blockchain.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,36 @@ static bool GetUTXOStats(CCoinsView *view, CCoinsStats &stats)
814814
return true;
815815
}
816816

817+
UniValue pruneblockchain(const JSONRPCRequest& request)
818+
{
819+
if (request.fHelp || request.params.size() != 1)
820+
throw runtime_error(
821+
"pruneblockchain\n"
822+
"\nArguments:\n"
823+
"1. \"height\" (int, required) The block height to prune up to.\n");
824+
825+
if (!fPruneMode)
826+
throw JSONRPCError(RPC_METHOD_NOT_FOUND, "Cannot prune blocks because node is not in prune mode.");
827+
828+
LOCK(cs_main);
829+
830+
int heightParam = request.params[0].get_int();
831+
if (heightParam < 0)
832+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Negative block height.");
833+
834+
unsigned int height = (unsigned int) heightParam;
835+
unsigned int chainHeight = (unsigned int) chainActive.Height();
836+
if (chainHeight < Params().PruneAfterHeight())
837+
throw JSONRPCError(RPC_INTERNAL_ERROR, "Blockchain is too short for pruning.");
838+
else if (height > chainHeight)
839+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Blockchain is shorter than the attempted prune height.");
840+
else if (height > chainHeight - MIN_BLOCKS_TO_KEEP)
841+
LogPrint("rpc", "Attempt to prune blocks close to the tip. Retaining the minimum number of blocks.");
842+
843+
PruneBlockFilesManual(height);
844+
return NullUniValue;
845+
}
846+
817847
UniValue gettxoutsetinfo(const JSONRPCRequest& request)
818848
{
819849
if (request.fHelp || request.params.size() != 0)
@@ -1384,6 +1414,7 @@ static const CRPCCommand commands[] =
13841414
{ "blockchain", "getrawmempool", &getrawmempool, true, {"verbose"} },
13851415
{ "blockchain", "gettxout", &gettxout, true, {"txid","n","include_mempool"} },
13861416
{ "blockchain", "gettxoutsetinfo", &gettxoutsetinfo, true, {} },
1417+
{ "blockchain", "pruneblockchain", &pruneblockchain, true, {"height"} },
13871418
{ "blockchain", "verifychain", &verifychain, true, {"checklevel","nblocks"} },
13881419

13891420
{ "blockchain", "preciousblock", &preciousblock, true, {"blockhash"} },

src/rpc/client.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ static const CRPCConvertParam vRPCConvertParams[] =
103103
{ "importmulti", 1, "options" },
104104
{ "verifychain", 0, "checklevel" },
105105
{ "verifychain", 1, "nblocks" },
106+
{ "pruneblockchain", 0, "height" },
106107
{ "keypoolrefill", 0, "newsize" },
107108
{ "getrawmempool", 0, "verbose" },
108109
{ "estimatefee", 0, "nblocks" },

src/validation.cpp

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,8 @@ enum FlushStateMode {
185185
};
186186

187187
// See definition for documentation
188-
bool static FlushStateToDisk(CValidationState &state, FlushStateMode mode);
188+
bool static FlushStateToDisk(CValidationState &state, FlushStateMode mode, int nManualPruneHeight=0);
189+
void FindFilesToPruneManual(std::set<int>& setFilesToPrune, int nManualPruneHeight);
189190

190191
bool IsFinalTx(const CTransaction &tx, int nBlockHeight, int64_t nBlockTime)
191192
{
@@ -1934,7 +1935,7 @@ bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockIndex* pin
19341935
* if they're too large, if it's been a while since the last write,
19351936
* or always and in all cases if we're in prune mode and are deleting files.
19361937
*/
1937-
bool static FlushStateToDisk(CValidationState &state, FlushStateMode mode) {
1938+
bool static FlushStateToDisk(CValidationState &state, FlushStateMode mode, int nManualPruneHeight) {
19381939
int64_t nMempoolUsage = mempool.DynamicMemoryUsage();
19391940
const CChainParams& chainparams = Params();
19401941
LOCK2(cs_main, cs_LastBlockFile);
@@ -1944,9 +1945,13 @@ bool static FlushStateToDisk(CValidationState &state, FlushStateMode mode) {
19441945
std::set<int> setFilesToPrune;
19451946
bool fFlushForPrune = false;
19461947
try {
1947-
if (fPruneMode && fCheckForPruning && !fReindex) {
1948-
FindFilesToPrune(setFilesToPrune, chainparams.PruneAfterHeight());
1949-
fCheckForPruning = false;
1948+
if (fPruneMode && (fCheckForPruning || nManualPruneHeight > 0) && !fReindex) {
1949+
if (nManualPruneHeight > 0) {
1950+
FindFilesToPruneManual(setFilesToPrune, nManualPruneHeight);
1951+
} else {
1952+
FindFilesToPrune(setFilesToPrune, chainparams.PruneAfterHeight());
1953+
fCheckForPruning = false;
1954+
}
19501955
if (!setFilesToPrune.empty()) {
19511956
fFlushForPrune = true;
19521957
if (!fHavePruned) {
@@ -3247,6 +3252,35 @@ void UnlinkPrunedFiles(std::set<int>& setFilesToPrune)
32473252
}
32483253
}
32493254

3255+
/* Calculate the block/rev files to delete based on height specified by user with RPC command pruneblockchain */
3256+
void FindFilesToPruneManual(std::set<int>& setFilesToPrune, int nManualPruneHeight)
3257+
{
3258+
assert(fPruneMode && nManualPruneHeight > 0);
3259+
3260+
LOCK2(cs_main, cs_LastBlockFile);
3261+
if (chainActive.Tip() == NULL)
3262+
return;
3263+
3264+
// last block to prune is the lesser of (user-specified height, MIN_BLOCKS_TO_KEEP from the tip)
3265+
unsigned int nLastBlockWeCanPrune = min((unsigned)nManualPruneHeight, chainActive.Tip()->nHeight - MIN_BLOCKS_TO_KEEP);
3266+
int count=0;
3267+
for (int fileNumber = 0; fileNumber < nLastBlockFile; fileNumber++) {
3268+
if (vinfoBlockFile[fileNumber].nSize == 0 || vinfoBlockFile[fileNumber].nHeightLast > nLastBlockWeCanPrune)
3269+
continue;
3270+
PruneOneBlockFile(fileNumber);
3271+
setFilesToPrune.insert(fileNumber);
3272+
count++;
3273+
}
3274+
LogPrintf("Prune (Manual): prune_height=%d removed %d blk/rev pairs\n", nLastBlockWeCanPrune, count);
3275+
}
3276+
3277+
/* This function is called from the RPC code for pruneblockchain */
3278+
void PruneBlockFilesManual(int nManualPruneHeight)
3279+
{
3280+
CValidationState state;
3281+
FlushStateToDisk(state, FLUSH_STATE_NONE, nManualPruneHeight);
3282+
}
3283+
32503284
/* Calculate the block/rev files that should be deleted to remain under target*/
32513285
void FindFilesToPrune(std::set<int>& setFilesToPrune, uint64_t nPruneAfterHeight)
32523286
{

src/validation.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,8 @@ CBlockIndex * InsertBlockIndex(uint256 hash);
309309
void FlushStateToDisk();
310310
/** Prune block files and flush state to disk. */
311311
void PruneAndFlush();
312+
/** Prune block files up to a given height */
313+
void PruneBlockFilesManual(int nPruneUpToHeight);
312314

313315
/** (try to) add transaction to memory pool **/
314316
bool AcceptToMemoryPool(CTxMemPool& pool, CValidationState &state, const CTransactionRef &tx, bool fLimitFree,

0 commit comments

Comments
 (0)