Skip to content

Commit afffeea

Browse files
committed
fixup! Add pruneblockchain RPC to enable manual block file pruning.
Extend pruneblockchain RPC to accept block timestamps as well as block indices.
1 parent 1fc4ec7 commit afffeea

File tree

2 files changed

+67
-42
lines changed

2 files changed

+67
-42
lines changed

qa/rpc-tests/pruning.py

Lines changed: 56 additions & 41 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 = 5
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,12 +43,12 @@ 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)
46+
# Create nodes 3 and 4 to test manual pruning (they will be re-started with manual pruning later)
4747
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/"
48+
self.nodes.append(start_node(4, self.options.tmpdir, ["-debug=0","-maxreceivebuffer=20000","-blockmaxsize=999000"], timewait=900))
4949

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"]))
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"]))
5252

5353
# Determine default relay fee
5454
self.relayfee = self.nodes[0].getnetworkinfo()["relayfee"]
@@ -57,7 +57,8 @@ def setup_network(self):
5757
connect_nodes(self.nodes[1], 2)
5858
connect_nodes(self.nodes[2], 0)
5959
connect_nodes(self.nodes[0], 3)
60-
sync_blocks(self.nodes[0:4])
60+
connect_nodes(self.nodes[0], 4)
61+
sync_blocks(self.nodes[0:5])
6162

6263
def create_big_chain(self):
6364
# Start by creating some coinbases we can spend later
@@ -68,7 +69,7 @@ def create_big_chain(self):
6869
for i in range(645):
6970
mine_large_block(self.nodes[0], self.utxo_cache_0)
7071

71-
sync_blocks(self.nodes[0:4])
72+
sync_blocks(self.nodes[0:5])
7273

7374
def test_height_min(self):
7475
if not os.path.isfile(self.prunedir+"blk00000.dat"):
@@ -223,68 +224,78 @@ def reorg_back(self):
223224
# Verify we can now have the data for a block previously pruned
224225
assert(self.nodes[2].getblock(self.forkhash)["height"] == self.forkheight)
225226

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)
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)
231233

232234
# 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+
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))
235246

236247
# 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)
248+
assert_raises_message(JSONRPCException, "Blockchain is too short for pruning", node.pruneblockchain, height(500))
238249

239250
# mine 6 blocks so we are at height 1001 (i.e., above PruneAfterHeight)
240-
self.nodes[3].generate(6)
251+
node.generate(6)
241252

242253
# negative and zero inputs should raise an exception
243254
try:
244-
self.nodes[3].pruneblockchain(-10)
255+
node.pruneblockchain(-10)
245256
raise AssertionError("pruneblockchain(-10) should have failed.")
246257
except:
247258
pass
248259

249260
try:
250-
self.nodes[3].pruneblockchain(0)
261+
node.pruneblockchain(0)
251262
raise AssertionError("pruneblockchain(0) should have failed.")
252263
except:
253264
pass
254265

255266
# 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"):
267+
node.pruneblockchain(height(100))
268+
if not has_block(0):
258269
raise AssertionError("blk00000.dat is missing when should still be there")
259270

260271
# height=500 should prune first file
261-
self.nodes[3].pruneblockchain(500)
262-
if os.path.isfile(self.manualdir+"blk00000.dat"):
272+
node.pruneblockchain(height(500))
273+
if has_block(0):
263274
raise AssertionError("blk00000.dat is still there, should be pruned by now")
264-
if not os.path.isfile(self.manualdir+"blk00001.dat"):
275+
if not has_block(1):
265276
raise AssertionError("blk00001.dat is missing when should still be there")
266277

267278
# height=650 should prune second file
268-
self.nodes[3].pruneblockchain(650)
269-
if os.path.isfile(self.manualdir+"blk00001.dat"):
279+
node.pruneblockchain(height(650))
280+
if has_block(1):
270281
raise AssertionError("blk00001.dat is still there, should be pruned by now")
271282

272283
# 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"):
284+
node.pruneblockchain(height(1000))
285+
if not has_block(2):
275286
raise AssertionError("blk00002.dat is still there, should be pruned by now")
276287

277288
# 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"):
289+
node.generate(288)
290+
node.pruneblockchain(height(1000))
291+
if has_block(2):
281292
raise AssertionError("blk00002.dat is still there, should be pruned by now")
282-
if os.path.isfile(self.manualdir+"blk00003.dat"):
293+
if has_block(3):
283294
raise AssertionError("blk00003.dat is still there, should be pruned by now")
284295

285296
# 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)
297+
stop_node(node, node_number)
298+
self.nodes[node_number] = start_node(node_number, self.options.tmpdir, ["-debug=0","-prune=550"], timewait=900)
288299

289300
print("Success")
290301

@@ -300,16 +311,16 @@ def wallet_test(self):
300311

301312
# check that wallet loads loads successfully when restarting a pruned node after IBD.
302313
# 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]]
314+
print ("Syncing node 5 to test wallet")
315+
connect_nodes(self.nodes[0], 5)
316+
nds = [self.nodes[0], self.nodes[5]]
306317
sync_blocks(nds)
307318
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"])
319+
stop_node(self.nodes[5],5) #stop and start to trigger rescan
320+
start_node(5, self.options.tmpdir, ["-debug=1","-prune=550"])
310321
print ("Success")
311322
except Exception as detail:
312-
raise AssertionError("Wallet test: unable to re-start node4")
323+
raise AssertionError("Wallet test: unable to re-start node5")
313324

314325
def run_test(self):
315326
print("Warning! This test requires 4GB of disk space and takes over 30 mins (up to 2 hours)")
@@ -326,6 +337,7 @@ def run_test(self):
326337

327338
# stop manual-pruning node with 995 blocks
328339
stop_node(self.nodes[3],3)
340+
stop_node(self.nodes[4],4)
329341

330342
print("Check that we haven't started pruning yet because we're below PruneAfterHeight")
331343
self.test_height_min()
@@ -409,8 +421,11 @@ def run_test(self):
409421
#
410422
# N1 doesn't change because 1033 on main chain (*) is invalid
411423

412-
print("Test manual pruning")
413-
self.manual_test()
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)
414429

415430
print("Test wallet re-scan")
416431
self.wallet_test()

src/rpc/blockchain.cpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -820,7 +820,7 @@ UniValue pruneblockchain(const JSONRPCRequest& request)
820820
throw runtime_error(
821821
"pruneblockchain\n"
822822
"\nArguments:\n"
823-
"1. \"height\" (int, required) The block height to prune up to.\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");
824824

825825
if (!fPruneMode)
826826
throw JSONRPCError(RPC_METHOD_NOT_FOUND, "Cannot prune blocks because node is not in prune mode.");
@@ -831,6 +831,16 @@ UniValue pruneblockchain(const JSONRPCRequest& request)
831831
if (heightParam < 0)
832832
throw JSONRPCError(RPC_INVALID_PARAMETER, "Negative block height.");
833833

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+
834844
unsigned int height = (unsigned int) heightParam;
835845
unsigned int chainHeight = (unsigned int) chainActive.Height();
836846
if (chainHeight < Params().PruneAfterHeight())

0 commit comments

Comments
 (0)