wallet_pruning.py
1 #!/usr/bin/env python3 2 # Copyright (c) 2022 The Bitcoin Core developers 3 # Distributed under the MIT software license, see the accompanying 4 # file COPYING or http://www.opensource.org/licenses/mit-license.php. 5 6 """Test wallet import on pruned node.""" 7 8 from test_framework.util import assert_equal, assert_raises_rpc_error 9 from test_framework.blocktools import ( 10 COINBASE_MATURITY, 11 create_block 12 ) 13 from test_framework.blocktools import create_coinbase 14 from test_framework.test_framework import BitcoinTestFramework 15 16 from test_framework.script import ( 17 CScript, 18 OP_RETURN, 19 OP_TRUE, 20 ) 21 22 class WalletPruningTest(BitcoinTestFramework): 23 def add_options(self, parser): 24 self.add_wallet_options(parser, descriptors=False) 25 26 def set_test_params(self): 27 self.setup_clean_chain = True 28 self.num_nodes = 2 29 self.wallet_names = [] 30 self.extra_args = [ 31 [], # node dedicated to mining 32 ['-prune=550'], # node dedicated to testing pruning 33 ] 34 35 def skip_test_if_missing_module(self): 36 self.skip_if_no_wallet() 37 self.skip_if_no_bdb() 38 39 def mine_large_blocks(self, node, n): 40 # Get the block parameters for the first block 41 best_block = node.getblockheader(node.getbestblockhash()) 42 height = int(best_block["height"]) + 1 43 self.nTime = max(self.nTime, int(best_block["time"])) + 1 44 previousblockhash = int(best_block["hash"], 16) 45 big_script = CScript([OP_RETURN] + [OP_TRUE] * 950000) 46 # Set mocktime to accept all future blocks 47 for i in self.nodes: 48 if i.running: 49 i.setmocktime(self.nTime + 600 * n) 50 for _ in range(n): 51 block = create_block(hashprev=previousblockhash, ntime=self.nTime, coinbase=create_coinbase(height, script_pubkey=big_script)) 52 block.solve() 53 54 # Submit to the node 55 node.submitblock(block.serialize().hex()) 56 57 previousblockhash = block.sha256 58 height += 1 59 60 # Simulate 10 minutes of work time per block 61 # Important for matching a timestamp with a block +- some window 62 self.nTime += 600 63 self.sync_all() 64 65 def test_wallet_import_pruned(self, wallet_name): 66 self.log.info("Make sure we can import wallet when pruned and required blocks are still available") 67 68 wallet_file = wallet_name + ".dat" 69 wallet_birthheight = self.get_birthheight(wallet_file) 70 71 # Verify that the block at wallet's birthheight is available at the pruned node 72 self.nodes[1].getblock(self.nodes[1].getblockhash(wallet_birthheight)) 73 74 # Import wallet into pruned node 75 self.nodes[1].createwallet(wallet_name="wallet_pruned", descriptors=False, load_on_startup=True) 76 self.nodes[1].importwallet(self.nodes[0].datadir_path / wallet_file) 77 78 # Make sure that prune node's wallet correctly accounts for balances 79 assert_equal(self.nodes[1].getbalance(), self.nodes[0].getbalance()) 80 81 self.log.info("- Done") 82 83 def test_wallet_import_pruned_with_missing_blocks(self, wallet_name): 84 self.log.info("Make sure we cannot import wallet when pruned and required blocks are not available") 85 86 wallet_file = wallet_name + ".dat" 87 wallet_birthheight = self.get_birthheight(wallet_file) 88 89 # Verify that the block at wallet's birthheight is not available at the pruned node 90 assert_raises_rpc_error(-1, "Block not available (pruned data)", self.nodes[1].getblock, self.nodes[1].getblockhash(wallet_birthheight)) 91 92 # Make sure wallet cannot be imported because of missing blocks 93 # This will try to rescan blocks `TIMESTAMP_WINDOW` (2h) before the wallet birthheight. 94 # There are 6 blocks an hour, so 11 blocks (excluding birthheight). 95 assert_raises_rpc_error(-4, f"Pruned blocks from height {wallet_birthheight - 11} required to import keys. Use RPC call getblockchaininfo to determine your pruned height.", self.nodes[1].importwallet, self.nodes[0].datadir_path / wallet_file) 96 self.log.info("- Done") 97 98 def get_birthheight(self, wallet_file): 99 """Gets birthheight of a wallet on node0""" 100 with open(self.nodes[0].datadir_path / wallet_file, 'r', encoding="utf8") as f: 101 for line in f: 102 if line.startswith('# * Best block at time of backup'): 103 wallet_birthheight = int(line.split(' ')[9]) 104 return wallet_birthheight 105 106 def has_block(self, block_index): 107 """Checks if the pruned node has the specific blk0000*.dat file""" 108 return (self.nodes[1].blocks_path / f"blk{block_index:05}.dat").is_file() 109 110 def create_wallet(self, wallet_name, *, unload=False): 111 """Creates and dumps a wallet on the non-pruned node0 to be later import by the pruned node""" 112 self.nodes[0].createwallet(wallet_name=wallet_name, descriptors=False, load_on_startup=True) 113 self.nodes[0].dumpwallet(self.nodes[0].datadir_path / f"{wallet_name}.dat") 114 if (unload): 115 self.nodes[0].unloadwallet(wallet_name) 116 117 def run_test(self): 118 self.nTime = 0 119 self.log.info("Warning! This test requires ~1.3GB of disk space") 120 121 self.log.info("Generating a long chain of blocks...") 122 123 # A blk*.dat file is 128MB 124 # Generate 250 light blocks 125 self.generate(self.nodes[0], 250) 126 # Generate 50MB worth of large blocks in the blk00000.dat file 127 self.mine_large_blocks(self.nodes[0], 50) 128 129 # Create a wallet which birth's block is in the blk00000.dat file 130 wallet_birthheight_1 = "wallet_birthheight_1" 131 assert_equal(self.has_block(1), False) 132 self.create_wallet(wallet_birthheight_1, unload=True) 133 134 # Generate enough large blocks to reach pruning disk limit 135 # Not pruning yet because we are still below PruneAfterHeight 136 self.mine_large_blocks(self.nodes[0], 600) 137 self.log.info("- Long chain created") 138 139 # Create a wallet with birth height > wallet_birthheight_1 140 wallet_birthheight_2 = "wallet_birthheight_2" 141 self.create_wallet(wallet_birthheight_2) 142 143 # Fund wallet to later verify that importwallet correctly accounts for balances 144 self.generatetoaddress(self.nodes[0], COINBASE_MATURITY + 1, self.nodes[0].getnewaddress(), sync_fun=self.no_op) 145 146 # We've reached pruning storage & height limit but 147 # pruning doesn't run until another chunk (blk*.dat file) is allocated. 148 # That's why we are generating another 5 large blocks 149 self.mine_large_blocks(self.nodes[0], 5) 150 151 # blk00000.dat file is now pruned from node1 152 assert_equal(self.has_block(0), False) 153 154 self.test_wallet_import_pruned(wallet_birthheight_2) 155 self.test_wallet_import_pruned_with_missing_blocks(wallet_birthheight_1) 156 157 if __name__ == '__main__': 158 WalletPruningTest().main()