wallet_backup.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2014-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  """Test the wallet backup features.
   6  
   7  Test case is:
   8  4 nodes. 1 2 and 3 send transactions between each other,
   9  fourth node is a miner.
  10  1 2 3 each mine a block to start, then
  11  Miner creates 100 blocks so 1 2 3 each have 50 mature
  12  coins to spend.
  13  Then 5 iterations of 1/2/3 sending coins amongst
  14  themselves to get transactions in the wallets,
  15  and the miner mining one block.
  16  
  17  Wallets are backed up using dumpwallet/backupwallet.
  18  Then 5 more iterations of transactions and mining a block.
  19  
  20  Miner then generates 101 more blocks, so any
  21  transaction fees paid mature.
  22  
  23  Sanity check:
  24    Sum(1,2,3,4 balances) == 114*50
  25  
  26  1/2/3 are shutdown, and their wallets erased.
  27  Then restore using wallet.dat backup. And
  28  confirm 1/2/3/4 balances are same as before.
  29  
  30  Shutdown again, restore using importwallet,
  31  and confirm again balances are correct.
  32  """
  33  from decimal import Decimal
  34  import os
  35  from random import randint
  36  import shutil
  37  
  38  from test_framework.blocktools import COINBASE_MATURITY
  39  from test_framework.test_framework import LimenkaTestFramework
  40  from test_framework.util import (
  41      assert_equal,
  42      assert_raises_rpc_error,
  43      sha256sum_file,
  44  )
  45  
  46  
  47  class WalletBackupTest(LimenkaTestFramework):
  48      def add_options(self, parser):
  49          self.add_wallet_options(parser)
  50  
  51      def set_test_params(self):
  52          self.num_nodes = 4
  53          self.setup_clean_chain = True
  54          # whitelist peers to speed up tx relay / mempool sync
  55          self.noban_tx_relay = True
  56          # nodes 1, 2, 3 are spenders, let's give them a keypool=100
  57          self.extra_args = [
  58              ["-keypool=100"],
  59              ["-keypool=100"],
  60              ["-keypool=100"],
  61              [],
  62          ]
  63          self.rpc_timeout = 120
  64  
  65      def skip_test_if_missing_module(self):
  66          self.skip_if_no_wallet()
  67  
  68      def setup_network(self):
  69          self.setup_nodes()
  70          self.connect_nodes(0, 3)
  71          self.connect_nodes(1, 3)
  72          self.connect_nodes(2, 3)
  73          self.connect_nodes(2, 0)
  74          self.sync_all()
  75  
  76      def one_send(self, from_node, to_address):
  77          if (randint(1,2) == 1):
  78              amount = Decimal(randint(1,10)) / Decimal(10)
  79              self.nodes[from_node].sendtoaddress(to_address, amount)
  80  
  81      def do_one_round(self):
  82          a0 = self.nodes[0].getnewaddress()
  83          a1 = self.nodes[1].getnewaddress()
  84          a2 = self.nodes[2].getnewaddress()
  85  
  86          self.one_send(0, a1)
  87          self.one_send(0, a2)
  88          self.one_send(1, a0)
  89          self.one_send(1, a2)
  90          self.one_send(2, a0)
  91          self.one_send(2, a1)
  92  
  93          # Have the miner (node3) mine a block.
  94          # Must sync mempools before mining.
  95          self.sync_mempools()
  96          self.generate(self.nodes[3], 1)
  97  
  98      # As above, this mirrors the original bash test.
  99      def start_three(self, args=()):
 100          self.start_node(0, self.extra_args[0] + list(args))
 101          self.start_node(1, self.extra_args[1] + list(args))
 102          self.start_node(2, self.extra_args[2] + list(args))
 103          self.connect_nodes(0, 3)
 104          self.connect_nodes(1, 3)
 105          self.connect_nodes(2, 3)
 106          self.connect_nodes(2, 0)
 107  
 108      def stop_three(self):
 109          self.stop_node(0)
 110          self.stop_node(1)
 111          self.stop_node(2)
 112  
 113      def erase_three(self):
 114          for node_num in range(3):
 115              (self.nodes[node_num].wallets_path / self.default_wallet_name / self.wallet_data_filename).unlink()
 116  
 117      def restore_invalid_wallet(self):
 118          node = self.nodes[3]
 119          invalid_wallet_file = self.nodes[0].datadir_path / 'invalid_wallet_file.bak'
 120          open(invalid_wallet_file, 'a', encoding="utf8").write('invald wallet')
 121          wallet_name = "res0"
 122          not_created_wallet_file = node.wallets_path / wallet_name
 123          error_message = "Wallet file verification failed. Failed to load database path '{}'. Data is not in recognized format.".format(not_created_wallet_file)
 124          assert_raises_rpc_error(-18, error_message, node.restorewallet, wallet_name, invalid_wallet_file)
 125          assert not not_created_wallet_file.exists()
 126  
 127      def restore_nonexistent_wallet(self):
 128          node = self.nodes[3]
 129          nonexistent_wallet_file = self.nodes[0].datadir_path / 'nonexistent_wallet.bak'
 130          wallet_name = "res0"
 131          assert_raises_rpc_error(-8, "Backup file does not exist", node.restorewallet, wallet_name, nonexistent_wallet_file)
 132          not_created_wallet_file = node.wallets_path / wallet_name
 133          assert not not_created_wallet_file.exists()
 134  
 135      def restore_wallet_existent_name(self):
 136          node = self.nodes[3]
 137          backup_file = self.nodes[0].datadir_path / 'wallet.bak'
 138          wallet_name = "res0"
 139          wallet_file = node.wallets_path / wallet_name
 140          error_message = "Failed to restore wallet. Database file exists in '{}'.".format(wallet_file / "wallet.dat")
 141          assert_raises_rpc_error(-36, error_message, node.restorewallet, wallet_name, backup_file)
 142          assert wallet_file.exists()
 143  
 144      def test_restore_existent_dir(self):
 145          self.log.info("Test restore on an existent empty directory")
 146          node = self.nodes[3]
 147          backup_file = self.nodes[0].datadir_path / 'wallet.bak'
 148          wallet_name = "restored_wallet"
 149          wallet_dir = node.wallets_path / wallet_name
 150          os.mkdir(wallet_dir)
 151          res = node.restorewallet(wallet_name, backup_file)
 152          assert_equal(res['name'], wallet_name)
 153          node.unloadwallet(wallet_name)
 154  
 155          self.log.info("Test restore succeeds when the target directory contains non-wallet files")
 156          wallet_file = node.wallets_path / wallet_name / "wallet.dat"
 157          os.remove(wallet_file)
 158          extra_file = node.wallets_path / wallet_name / "not_a_wallet.txt"
 159          extra_file.touch()
 160          res = node.restorewallet(wallet_name, backup_file)
 161          assert_equal(res['name'], wallet_name)
 162          assert extra_file.exists() # extra file was not removed by mistake
 163          node.unloadwallet(wallet_name)
 164  
 165          self.log.info("Test restore failure due to existing db file in the destination directory")
 166          original_shasum = sha256sum_file(wallet_file)
 167          error_message = "Failed to restore wallet. Database file exists in '{}'.".format(wallet_dir / "wallet.dat")
 168          assert_raises_rpc_error(-36, error_message, node.restorewallet, wallet_name, backup_file)
 169          # Ensure the wallet file remains untouched
 170          assert wallet_dir.exists()
 171          assert_equal(original_shasum, sha256sum_file(wallet_file))
 172  
 173          self.log.info("Test restore succeeds when the .dat file in the destination has a different name")
 174          second_wallet = wallet_dir / "hidden_storage.dat"
 175          os.rename(wallet_dir / "wallet.dat", second_wallet)
 176          original_shasum = sha256sum_file(second_wallet)
 177          res = node.restorewallet(wallet_name, backup_file)
 178          assert_equal(res['name'], wallet_name)
 179          assert (wallet_dir / "hidden_storage.dat").exists()
 180          assert_equal(original_shasum, sha256sum_file(second_wallet))
 181          node.unloadwallet(wallet_name)
 182  
 183          # Clean for follow-up tests
 184          os.remove(wallet_file)
 185  
 186      def test_restore_into_unnamed_wallet(self):
 187          self.log.info("Test restore into a default unnamed wallet")
 188          # This is also useful to test the migration recovery after failure logic
 189          node = self.nodes[3]
 190          if not self.options.descriptors:
 191              node.unloadwallet("")
 192              os.rename(node.wallets_path / "wallet.dat", node.wallets_path / "default.wallet.dat")
 193          backup_file = self.nodes[0].datadir_path / 'wallet.bak'
 194          wallet_name = ""
 195          res = node.restorewallet(wallet_name, backup_file)
 196          assert_equal(res['name'], "")
 197          assert (node.wallets_path / "wallet.dat").exists()
 198          # Clean for follow-up tests
 199          node.unloadwallet("")
 200          os.remove(node.wallets_path / "wallet.dat")
 201          if not self.options.descriptors:
 202              os.rename(node.wallets_path / "default.wallet.dat", node.wallets_path / "wallet.dat")
 203              node.loadwallet("")
 204  
 205      def test_pruned_wallet_backup(self):
 206          self.log.info("Test loading backup on a pruned node when the backup was created close to the prune height of the restoring node")
 207          node = self.nodes[3]
 208          self.restart_node(3, ["-prune=1", "-fastprune=1"])
 209          # Ensure the chain tip is at height 214, because this test assume it is.
 210          assert_equal(node.getchaintips()[0]["height"], 214)
 211          # We need a few more blocks so we can actually get above an realistic
 212          # minimal prune height
 213          self.generate(node, 50, sync_fun=self.no_op)
 214          # Backup created at block height 264
 215          node.backupwallet(node.datadir_path / 'wallet_pruned.bak')
 216          # Generate more blocks so we can actually prune the older blocks
 217          self.generate(node, 300, sync_fun=self.no_op)
 218          # This gives us an actual prune height roughly in the range of 220 - 240
 219          node.pruneblockchain(250)
 220          # The backup should be updated with the latest height (locator) for
 221          # the backup to load successfully this close to the prune height
 222          node.restorewallet('pruned', node.datadir_path / 'wallet_pruned.bak')
 223  
 224          self.log.info("Test restore on a pruned node when the backup was beyond the pruning point")
 225          if not self.options.descriptors:
 226              node.unloadwallet("")
 227              os.rename(node.wallets_path / "wallet.dat", node.wallets_path / "default.wallet.dat")
 228          backup_file = self.nodes[0].datadir_path / 'wallet.bak'
 229          wallet_name = ""
 230          error_message = "Wallet loading failed. Prune: last wallet synchronisation goes beyond pruned data. You need to -reindex (download the whole blockchain again in case of a pruned node)"
 231          assert_raises_rpc_error(-4, error_message, node.restorewallet, wallet_name, backup_file)
 232          assert node.wallets_path.exists() # ensure the wallets dir exists
 233          if not self.options.descriptors:
 234              os.rename(node.wallets_path / "default.wallet.dat", node.wallets_path / "wallet.dat")
 235              node.loadwallet("")
 236  
 237      def run_test(self):
 238          self.log.info("Generating initial blockchain")
 239          self.generate(self.nodes[0], 1)
 240          self.generate(self.nodes[1], 1)
 241          self.generate(self.nodes[2], 1)
 242          self.generate(self.nodes[3], COINBASE_MATURITY)
 243  
 244          assert_equal(self.nodes[0].getbalance(), 50)
 245          assert_equal(self.nodes[1].getbalance(), 50)
 246          assert_equal(self.nodes[2].getbalance(), 50)
 247          assert_equal(self.nodes[3].getbalance(), 0)
 248  
 249          self.log.info("Creating transactions")
 250          # Five rounds of sending each other transactions.
 251          for _ in range(5):
 252              self.do_one_round()
 253  
 254          self.log.info("Backing up")
 255  
 256          for node_num in range(3):
 257              self.nodes[node_num].backupwallet(self.nodes[node_num].datadir_path / 'wallet.bak')
 258  
 259          if not self.options.descriptors:
 260              for node_num in range(3):
 261                  self.nodes[node_num].dumpwallet(self.nodes[node_num].datadir_path / 'wallet.dump')
 262  
 263          self.log.info("More transactions")
 264          for _ in range(5):
 265              self.do_one_round()
 266  
 267          # Generate 101 more blocks, so any fees paid mature
 268          self.generate(self.nodes[3], COINBASE_MATURITY + 1)
 269  
 270          balance0 = self.nodes[0].getbalance()
 271          balance1 = self.nodes[1].getbalance()
 272          balance2 = self.nodes[2].getbalance()
 273          balance3 = self.nodes[3].getbalance()
 274          total = balance0 + balance1 + balance2 + balance3
 275  
 276          # At this point, there are 214 blocks (103 for setup, then 10 rounds, then 101.)
 277          # 114 are mature, so the sum of all wallets should be 114 * 50 = 5700.
 278          assert_equal(total, 5700)
 279  
 280          ##
 281          # Test restoring spender wallets from backups
 282          ##
 283          self.log.info("Restoring wallets on node 3 using backup files")
 284  
 285          self.restore_invalid_wallet()
 286          self.restore_nonexistent_wallet()
 287  
 288          backup_files = []
 289          for node_num in range(3):
 290              backup_files.append(self.nodes[node_num].datadir_path / 'wallet.bak')
 291  
 292          for idx, backup_file in enumerate(backup_files):
 293              self.nodes[3].restorewallet(f'res{idx}', backup_file)
 294              assert (self.nodes[3].wallets_path / f'res{idx}').exists()
 295  
 296          res0_rpc = self.nodes[3].get_wallet_rpc("res0")
 297          res1_rpc = self.nodes[3].get_wallet_rpc("res1")
 298          res2_rpc = self.nodes[3].get_wallet_rpc("res2")
 299  
 300          assert_equal(res0_rpc.getbalance(), balance0)
 301          assert_equal(res1_rpc.getbalance(), balance1)
 302          assert_equal(res2_rpc.getbalance(), balance2)
 303  
 304          self.restore_wallet_existent_name()
 305          self.test_restore_existent_dir()
 306          self.test_restore_into_unnamed_wallet()
 307  
 308          if not self.options.descriptors:
 309              self.log.info("Restoring using dumped wallet")
 310              self.stop_three()
 311              self.erase_three()
 312  
 313              #start node2 with no chain
 314              shutil.rmtree(self.nodes[2].blocks_path)
 315              shutil.rmtree(self.nodes[2].chain_path / 'chainstate')
 316  
 317              self.start_three(["-nowallet"])
 318              # Create new wallets for the three nodes.
 319              # We will use this empty wallets to test the 'importwallet()' RPC command below.
 320              for node_num in range(3):
 321                  self.nodes[node_num].createwallet(wallet_name=self.default_wallet_name, descriptors=self.options.descriptors, load_on_startup=True)
 322                  assert_equal(self.nodes[node_num].getbalance(), 0)
 323                  self.nodes[node_num].importwallet(self.nodes[node_num].datadir_path / 'wallet.dump')
 324  
 325              self.sync_blocks()
 326  
 327              assert_equal(self.nodes[0].getbalance(), balance0)
 328              assert_equal(self.nodes[1].getbalance(), balance1)
 329              assert_equal(self.nodes[2].getbalance(), balance2)
 330  
 331          # Backup to source wallet file must fail
 332          sourcePaths = [
 333              os.path.join(self.nodes[0].wallets_path, self.default_wallet_name, self.wallet_data_filename),
 334              os.path.join(self.nodes[0].wallets_path, '.', self.default_wallet_name, self.wallet_data_filename),
 335              os.path.join(self.nodes[0].wallets_path, self.default_wallet_name),
 336              os.path.join(self.nodes[0].wallets_path)]
 337  
 338          for sourcePath in sourcePaths:
 339              assert_raises_rpc_error(-4, "backup failed", self.nodes[0].backupwallet, sourcePath)
 340  
 341          self.test_pruned_wallet_backup()
 342  
 343  
 344  if __name__ == '__main__':
 345      WalletBackupTest(__file__).main()
 346