Skip to content

Commit e2e624d

Browse files
committed
Merge #7871: Manual block file pruning.
afffeea fixup! Add pruneblockchain RPC to enable manual block file pruning. (Russell Yanofsky) 1fc4ec7 Add pruneblockchain RPC to enable manual block file pruning. (mrbandrews)
2 parents ca615e6 + afffeea commit e2e624d

File tree

8 files changed

+232
-24
lines changed

8 files changed

+232
-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: 125 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 = 6
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,22 @@ 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 nodes 3 and 4 to test manual pruning (they 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.nodes.append(start_node(4, self.options.tmpdir, ["-debug=0","-maxreceivebuffer=20000","-blockmaxsize=999000"], timewait=900))
49+
50+
# Create nodes 5 to test wallet in prune mode, but do not connect
51+
self.nodes.append(start_node(5, 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+
connect_nodes(self.nodes[0], 4)
61+
sync_blocks(self.nodes[0:5])
5062

5163
def create_big_chain(self):
5264
# Start by creating some coinbases we can spend later
@@ -57,7 +69,7 @@ def create_big_chain(self):
5769
for i in range(645):
5870
mine_large_block(self.nodes[0], self.utxo_cache_0)
5971

60-
sync_blocks(self.nodes[0:3])
72+
sync_blocks(self.nodes[0:5])
6173

6274
def test_height_min(self):
6375
if not os.path.isfile(self.prunedir+"blk00000.dat"):
@@ -212,6 +224,103 @@ def reorg_back(self):
212224
# Verify we can now have the data for a block previously pruned
213225
assert(self.nodes[2].getblock(self.forkhash)["height"] == self.forkheight)
214226

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

216325
def run_test(self):
217326
print("Warning! This test requires 4GB of disk space and takes over 30 mins (up to 2 hours)")
@@ -226,6 +335,10 @@ def run_test(self):
226335
# Start by mining a simple chain that all nodes have
227336
# N0=N1=N2 **...*(995)
228337

338+
# stop manual-pruning node with 995 blocks
339+
stop_node(self.nodes[3],3)
340+
stop_node(self.nodes[4],4)
341+
229342
print("Check that we haven't started pruning yet because we're below PruneAfterHeight")
230343
self.test_height_min()
231344
# Extend this chain past the PruneAfterHeight
@@ -308,6 +421,15 @@ def run_test(self):
308421
#
309422
# N1 doesn't change because 1033 on main chain (*) is invalid
310423

424+
print("Test manual pruning with block indices")
425+
self.manual_test(3, use_timestamp=False)
426+
427+
print("Test manual pruning with timestamps")
428+
self.manual_test(4, use_timestamp=True)
429+
430+
print("Test wallet re-scan")
431+
self.wallet_test()
432+
311433
print("Done")
312434

313435
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: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,46 @@ 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\" (numeric, required) The block height to prune up to. May be set to a discrete height, or to a unix timestamp to prune based on block time.\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+
// Height value more than a billion is too high to be a block height, and
835+
// too low to be a block time (corresponds to timestamp from Sep 2001).
836+
if (heightParam > 1000000000) {
837+
CBlockIndex* pindex = chainActive.FindLatestBefore(heightParam);
838+
if (!pindex) {
839+
throw JSONRPCError(RPC_INTERNAL_ERROR, "Could not find block before specified timestamp.");
840+
}
841+
heightParam = pindex->nHeight;
842+
}
843+
844+
unsigned int height = (unsigned int) heightParam;
845+
unsigned int chainHeight = (unsigned int) chainActive.Height();
846+
if (chainHeight < Params().PruneAfterHeight())
847+
throw JSONRPCError(RPC_INTERNAL_ERROR, "Blockchain is too short for pruning.");
848+
else if (height > chainHeight)
849+
throw JSONRPCError(RPC_INVALID_PARAMETER, "Blockchain is shorter than the attempted prune height.");
850+
else if (height > chainHeight - MIN_BLOCKS_TO_KEEP)
851+
LogPrint("rpc", "Attempt to prune blocks close to the tip. Retaining the minimum number of blocks.");
852+
853+
PruneBlockFilesManual(height);
854+
return NullUniValue;
855+
}
856+
817857
UniValue gettxoutsetinfo(const JSONRPCRequest& request)
818858
{
819859
if (request.fHelp || request.params.size() != 0)
@@ -1384,6 +1424,7 @@ static const CRPCCommand commands[] =
13841424
{ "blockchain", "getrawmempool", &getrawmempool, true, {"verbose"} },
13851425
{ "blockchain", "gettxout", &gettxout, true, {"txid","n","include_mempool"} },
13861426
{ "blockchain", "gettxoutsetinfo", &gettxoutsetinfo, true, {} },
1427+
{ "blockchain", "pruneblockchain", &pruneblockchain, true, {"height"} },
13871428
{ "blockchain", "verifychain", &verifychain, true, {"checklevel","nblocks"} },
13881429

13891430
{ "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)