#!/usr/bin/env python3 # Copyright (c) 2022 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """Test wallet import on pruned node.""" from test_framework.util import assert_equal, assert_raises_rpc_error from test_framework.blocktools import ( COINBASE_MATURITY, create_block ) from test_framework.blocktools import create_coinbase from test_framework.test_framework import BitcoinTestFramework from test_framework.script import ( CScript, OP_RETURN, OP_TRUE, ) class WalletPruningTest(BitcoinTestFramework): def add_options(self, parser): self.add_wallet_options(parser, descriptors=False) def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 self.wallet_names = [] self.extra_args = [ [], # node dedicated to mining ['-prune=550'], # node dedicated to testing pruning ] def skip_test_if_missing_module(self): self.skip_if_no_wallet() self.skip_if_no_bdb() def mine_large_blocks(self, node, n): # Get the block parameters for the first block best_block = node.getblockheader(node.getbestblockhash()) height = int(best_block["height"]) + 1 self.nTime = max(self.nTime, int(best_block["time"])) + 1 previousblockhash = int(best_block["hash"], 16) big_script = CScript([OP_RETURN] + [OP_TRUE] * 950000) # Set mocktime to accept all future blocks for i in self.nodes: if i.running: i.setmocktime(self.nTime + 600 * n) for _ in range(n): block = create_block(hashprev=previousblockhash, ntime=self.nTime, coinbase=create_coinbase(height, script_pubkey=big_script)) block.solve() # Submit to the node node.submitblock(block.serialize().hex()) previousblockhash = block.sha256 height += 1 # Simulate 10 minutes of work time per block # Important for matching a timestamp with a block +- some window self.nTime += 600 self.sync_all() def test_wallet_import_pruned(self, wallet_name): self.log.info("Make sure we can import wallet when pruned and required blocks are still available") wallet_file = wallet_name + ".dat" wallet_birthheight = self.get_birthheight(wallet_file) # Verify that the block at wallet's birthheight is available at the pruned node self.nodes[1].getblock(self.nodes[1].getblockhash(wallet_birthheight)) # Import wallet into pruned node self.nodes[1].createwallet(wallet_name="wallet_pruned", descriptors=False, load_on_startup=True) self.nodes[1].importwallet(self.nodes[0].datadir_path / wallet_file) # Make sure that prune node's wallet correctly accounts for balances assert_equal(self.nodes[1].getbalance(), self.nodes[0].getbalance()) self.log.info("- Done") def test_wallet_import_pruned_with_missing_blocks(self, wallet_name): self.log.info("Make sure we cannot import wallet when pruned and required blocks are not available") wallet_file = wallet_name + ".dat" wallet_birthheight = self.get_birthheight(wallet_file) # Verify that the block at wallet's birthheight is not available at the pruned node assert_raises_rpc_error(-1, "Block not available (pruned data)", self.nodes[1].getblock, self.nodes[1].getblockhash(wallet_birthheight)) # Make sure wallet cannot be imported because of missing blocks # This will try to rescan blocks `TIMESTAMP_WINDOW` (2h) before the wallet birthheight. # There are 6 blocks an hour, so 11 blocks (excluding birthheight). 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) self.log.info("- Done") def get_birthheight(self, wallet_file): """Gets birthheight of a wallet on node0""" with open(self.nodes[0].datadir_path / wallet_file, 'r', encoding="utf8") as f: for line in f: if line.startswith('# * Best block at time of backup'): wallet_birthheight = int(line.split(' ')[9]) return wallet_birthheight def has_block(self, block_index): """Checks if the pruned node has the specific blk0000*.dat file""" return (self.nodes[1].blocks_path / f"blk{block_index:05}.dat").is_file() def create_wallet(self, wallet_name, *, unload=False): """Creates and dumps a wallet on the non-pruned node0 to be later import by the pruned node""" self.nodes[0].createwallet(wallet_name=wallet_name, descriptors=False, load_on_startup=True) self.nodes[0].dumpwallet(self.nodes[0].datadir_path / f"{wallet_name}.dat") if (unload): self.nodes[0].unloadwallet(wallet_name) def run_test(self): self.nTime = 0 self.log.info("Warning! This test requires ~1.3GB of disk space") self.log.info("Generating a long chain of blocks...") # A blk*.dat file is 128MB # Generate 250 light blocks self.generate(self.nodes[0], 250) # Generate 50MB worth of large blocks in the blk00000.dat file self.mine_large_blocks(self.nodes[0], 50) # Create a wallet which birth's block is in the blk00000.dat file wallet_birthheight_1 = "wallet_birthheight_1" assert_equal(self.has_block(1), False) self.create_wallet(wallet_birthheight_1, unload=True) # Generate enough large blocks to reach pruning disk limit # Not pruning yet because we are still below PruneAfterHeight self.mine_large_blocks(self.nodes[0], 600) self.log.info("- Long chain created") # Create a wallet with birth height > wallet_birthheight_1 wallet_birthheight_2 = "wallet_birthheight_2" self.create_wallet(wallet_birthheight_2) # Fund wallet to later verify that importwallet correctly accounts for balances self.generatetoaddress(self.nodes[0], COINBASE_MATURITY + 1, self.nodes[0].getnewaddress(), sync_fun=self.no_op) # We've reached pruning storage & height limit but # pruning doesn't run until another chunk (blk*.dat file) is allocated. # That's why we are generating another 5 large blocks self.mine_large_blocks(self.nodes[0], 5) # blk00000.dat file is now pruned from node1 assert_equal(self.has_block(0), False) self.test_wallet_import_pruned(wallet_birthheight_2) self.test_wallet_import_pruned_with_missing_blocks(wallet_birthheight_1) if __name__ == '__main__': WalletPruningTest().main()