wallet_pruning.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2022 The Limenka 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 LimenkaTestFramework
  15  
  16  from test_framework.script import (
  17      CScript,
  18      OP_RETURN,
  19      OP_TRUE,
  20  )
  21  
  22  class WalletPruningTest(LimenkaTestFramework):
  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(__file__).main()
 159