Skip to content

Commit

Permalink
fixup! Add pruneblockchain RPC to enable manual block file pruning.
Browse files Browse the repository at this point in the history
Extend pruneblockchain RPC to accept block timestamps as well as block indices.
  • Loading branch information
ryanofsky committed Jan 10, 2017
1 parent 1fc4ec7 commit afffeea
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 42 deletions.
97 changes: 56 additions & 41 deletions qa/rpc-tests/pruning.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class PruneTest(BitcoinTestFramework):
def __init__(self):
super().__init__()
self.setup_clean_chain = True
self.num_nodes = 5
self.num_nodes = 6

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

# Create node 3 to test manual pruning (it will be re-started with manual pruning later)
# Create nodes 3 and 4 to test manual pruning (they will be re-started with manual pruning later)
self.nodes.append(start_node(3, self.options.tmpdir, ["-debug=0","-maxreceivebuffer=20000","-blockmaxsize=999000"], timewait=900))
self.manualdir = self.options.tmpdir+"/node3/regtest/blocks/"
self.nodes.append(start_node(4, self.options.tmpdir, ["-debug=0","-maxreceivebuffer=20000","-blockmaxsize=999000"], timewait=900))

# Create node 4 to test wallet in prune mode, but do not connect
self.nodes.append(start_node(4, self.options.tmpdir, ["-debug=0", "-prune=550"]))
# Create nodes 5 to test wallet in prune mode, but do not connect
self.nodes.append(start_node(5, self.options.tmpdir, ["-debug=0", "-prune=550"]))

# Determine default relay fee
self.relayfee = self.nodes[0].getnetworkinfo()["relayfee"]
Expand All @@ -57,7 +57,8 @@ def setup_network(self):
connect_nodes(self.nodes[1], 2)
connect_nodes(self.nodes[2], 0)
connect_nodes(self.nodes[0], 3)
sync_blocks(self.nodes[0:4])
connect_nodes(self.nodes[0], 4)
sync_blocks(self.nodes[0:5])

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

sync_blocks(self.nodes[0:4])
sync_blocks(self.nodes[0:5])

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

def manual_test(self):
# at this point, node3 has 995 blocks and has not yet run in prune mode
self.nodes[3] = start_node(3, self.options.tmpdir, ["-debug=0"], timewait=900)
assert_raises_message(JSONRPCException, "not in prune mode", self.nodes[3].pruneblockchain, 500)
stop_node(self.nodes[3],3)
def manual_test(self, node_number, use_timestamp):
# at this point, node has 995 blocks and has not yet run in prune mode
node = self.nodes[node_number] = start_node(node_number, self.options.tmpdir, ["-debug=0"], timewait=900)
assert_equal(node.getblockcount(), 995)
assert_raises_message(JSONRPCException, "not in prune mode", node.pruneblockchain, 500)
stop_node(node, node_number)

# now re-start in manual pruning mode
self.nodes[3] = start_node(3, self.options.tmpdir, ["-debug=0","-prune=1"], timewait=900)
assert_equal(self.nodes[3].getblockcount(), 995)
node = self.nodes[node_number] = start_node(node_number, self.options.tmpdir, ["-debug=0","-prune=1"], timewait=900)
assert_equal(node.getblockcount(), 995)

def height(index):
if use_timestamp:
return node.getblockheader(node.getblockhash(index))["time"]
else:
return index

def has_block(index):
return os.path.isfile(self.options.tmpdir + "/node{}/regtest/blocks/blk{:05}.dat".format(node_number, index))

# should not prune because chain tip of node 3 (995) < PruneAfterHeight (1000)
assert_raises_message(JSONRPCException, "Blockchain is too short for pruning", self.nodes[3].pruneblockchain, 500)
assert_raises_message(JSONRPCException, "Blockchain is too short for pruning", node.pruneblockchain, height(500))

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

# negative and zero inputs should raise an exception
try:
self.nodes[3].pruneblockchain(-10)
node.pruneblockchain(-10)
raise AssertionError("pruneblockchain(-10) should have failed.")
except:
pass

try:
self.nodes[3].pruneblockchain(0)
node.pruneblockchain(0)
raise AssertionError("pruneblockchain(0) should have failed.")
except:
pass

# height=100 too low to prune first block file so this is a no-op
self.nodes[3].pruneblockchain(100)
if not os.path.isfile(self.manualdir+"blk00000.dat"):
node.pruneblockchain(height(100))
if not has_block(0):
raise AssertionError("blk00000.dat is missing when should still be there")

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

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

# height=1000 should not prune anything more, because tip-288 is in blk00002.dat.
self.nodes[3].pruneblockchain(1000)
if not os.path.isfile(self.manualdir+"blk00002.dat"):
node.pruneblockchain(height(1000))
if not has_block(2):
raise AssertionError("blk00002.dat is still there, should be pruned by now")

# advance the tip so blk00002.dat and blk00003.dat can be pruned (the last 288 blocks should now be in blk00004.dat)
self.nodes[3].generate(288)
self.nodes[3].pruneblockchain(1000)
if os.path.isfile(self.manualdir+"blk00002.dat"):
node.generate(288)
node.pruneblockchain(height(1000))
if has_block(2):
raise AssertionError("blk00002.dat is still there, should be pruned by now")
if os.path.isfile(self.manualdir+"blk00003.dat"):
if has_block(3):
raise AssertionError("blk00003.dat is still there, should be pruned by now")

# stop node, start back up with auto-prune at 550MB, make sure still runs
stop_node(self.nodes[3],3)
self.nodes[3] = start_node(3, self.options.tmpdir, ["-debug=0","-prune=550"], timewait=900)
stop_node(node, node_number)
self.nodes[node_number] = start_node(node_number, self.options.tmpdir, ["-debug=0","-prune=550"], timewait=900)

print("Success")

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

# check that wallet loads loads successfully when restarting a pruned node after IBD.
# this was reported to fail in #7494.
print ("Syncing node 4 to test wallet")
connect_nodes(self.nodes[0], 4)
nds = [self.nodes[0], self.nodes[4]]
print ("Syncing node 5 to test wallet")
connect_nodes(self.nodes[0], 5)
nds = [self.nodes[0], self.nodes[5]]
sync_blocks(nds)
try:
stop_node(self.nodes[4],4) #stop and start to trigger rescan
start_node(4, self.options.tmpdir, ["-debug=1","-prune=550"])
stop_node(self.nodes[5],5) #stop and start to trigger rescan
start_node(5, self.options.tmpdir, ["-debug=1","-prune=550"])
print ("Success")
except Exception as detail:
raise AssertionError("Wallet test: unable to re-start node4")
raise AssertionError("Wallet test: unable to re-start node5")

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

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

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

print("Test manual pruning")
self.manual_test()
print("Test manual pruning with block indices")
self.manual_test(3, use_timestamp=False)

print("Test manual pruning with timestamps")
self.manual_test(4, use_timestamp=True)

print("Test wallet re-scan")
self.wallet_test()
Expand Down
12 changes: 11 additions & 1 deletion src/rpc/blockchain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -820,7 +820,7 @@ UniValue pruneblockchain(const JSONRPCRequest& request)
throw runtime_error(
"pruneblockchain\n"
"\nArguments:\n"
"1. \"height\" (int, required) The block height to prune up to.\n");
"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");

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

// Height value more than a billion is too high to be a block height, and
// too low to be a block time (corresponds to timestamp from Sep 2001).
if (heightParam > 1000000000) {
CBlockIndex* pindex = chainActive.FindLatestBefore(heightParam);
if (!pindex) {
throw JSONRPCError(RPC_INTERNAL_ERROR, "Could not find block before specified timestamp.");
}
heightParam = pindex->nHeight;
}

unsigned int height = (unsigned int) heightParam;
unsigned int chainHeight = (unsigned int) chainActive.Height();
if (chainHeight < Params().PruneAfterHeight())
Expand Down

0 comments on commit afffeea

Please sign in to comment.