|
| 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 | +import os |
| 8 | + |
| 9 | +from test_framework.util import assert_equal, assert_raises_rpc_error |
| 10 | +from test_framework.blocktools import ( |
| 11 | + COINBASE_MATURITY, |
| 12 | + create_block |
| 13 | +) |
| 14 | +from test_framework.blocktools import create_coinbase |
| 15 | +from test_framework.test_framework import BitcoinTestFramework |
| 16 | + |
| 17 | +from test_framework.script import ( |
| 18 | + CScript, |
| 19 | + OP_RETURN, |
| 20 | + OP_TRUE, |
| 21 | +) |
| 22 | + |
| 23 | +class WalletPruningTest(BitcoinTestFramework): |
| 24 | + def add_options(self, parser): |
| 25 | + self.add_wallet_options(parser, descriptors=False) |
| 26 | + |
| 27 | + def set_test_params(self): |
| 28 | + self.setup_clean_chain = True |
| 29 | + self.num_nodes = 2 |
| 30 | + self.wallet_names = [] |
| 31 | + self.extra_args = [ |
| 32 | + [], # node dedicated to mining |
| 33 | + ['-prune=550'], # node dedicated to testing pruning |
| 34 | + ] |
| 35 | + |
| 36 | + def skip_test_if_missing_module(self): |
| 37 | + self.skip_if_no_wallet() |
| 38 | + self.skip_if_no_bdb() |
| 39 | + |
| 40 | + def mine_large_blocks(self, node, n): |
| 41 | + # Get the block parameters for the first block |
| 42 | + best_block = node.getblock(node.getbestblockhash()) |
| 43 | + height = int(best_block["height"]) + 1 |
| 44 | + self.nTime = max(self.nTime, int(best_block["time"])) + 1 |
| 45 | + previousblockhash = int(best_block["hash"], 16) |
| 46 | + big_script = CScript([OP_RETURN] + [OP_TRUE] * 950000) |
| 47 | + for _ in range(n): |
| 48 | + block = create_block(hashprev=previousblockhash, ntime=self.nTime, coinbase=create_coinbase(height, script_pubkey=big_script)) |
| 49 | + block.solve() |
| 50 | + |
| 51 | + # Submit to the node |
| 52 | + node.submitblock(block.serialize().hex()) |
| 53 | + |
| 54 | + previousblockhash = block.sha256 |
| 55 | + height += 1 |
| 56 | + |
| 57 | + # Simulate 10 minutes of work time per block |
| 58 | + # Important for matching a timestamp with a block +- some window |
| 59 | + self.nTime += 600 |
| 60 | + for n in self.nodes: |
| 61 | + if n.running: |
| 62 | + n.setmocktime(self.nTime) # Update node's time to accept future blocks |
| 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(os.path.join(self.nodes[0].datadir, 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, os.path.join(self.nodes[0].datadir, 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(os.path.join(self.nodes[0].datadir, 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 os.path.isfile(os.path.join(self.nodes[1].datadir, self.chain, "blocks", f"blk{block_index:05}.dat")) |
| 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(os.path.join(self.nodes[0].datadir, 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, sync_fun=self.no_op) |
| 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() |
0 commit comments