wallet_backup.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2014-present 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  """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  
  37  from test_framework.blocktools import COINBASE_MATURITY
  38  from test_framework.test_framework import BitcoinTestFramework
  39  from test_framework.util import (
  40      assert_equal,
  41      assert_raises_rpc_error,
  42      sha256sum_file,
  43  )
  44  
  45  
  46  class WalletBackupTest(BitcoinTestFramework):
  47      def set_test_params(self):
  48          self.num_nodes = 4
  49          self.setup_clean_chain = True
  50          # whitelist peers to speed up tx relay / mempool sync
  51          self.noban_tx_relay = True
  52          # nodes 1, 2, 3 are spenders, let's give them a keypool=100
  53          self.extra_args = [
  54              ["-keypool=100"],
  55              ["-keypool=100"],
  56              ["-keypool=100"],
  57              [],
  58          ]
  59          self.rpc_timeout = 120
  60  
  61      def skip_test_if_missing_module(self):
  62          self.skip_if_no_wallet()
  63  
  64      def setup_network(self):
  65          self.setup_nodes()
  66          self.connect_nodes(0, 3)
  67          self.connect_nodes(1, 3)
  68          self.connect_nodes(2, 3)
  69          self.connect_nodes(2, 0)
  70          self.sync_all()
  71  
  72      def one_send(self, from_node, to_address):
  73          if (randint(1,2) == 1):
  74              amount = Decimal(randint(1,10)) / Decimal(10)
  75              self.nodes[from_node].sendtoaddress(to_address, amount)
  76  
  77      def do_one_round(self):
  78          a0 = self.nodes[0].getnewaddress()
  79          a1 = self.nodes[1].getnewaddress()
  80          a2 = self.nodes[2].getnewaddress()
  81  
  82          self.one_send(0, a1)
  83          self.one_send(0, a2)
  84          self.one_send(1, a0)
  85          self.one_send(1, a2)
  86          self.one_send(2, a0)
  87          self.one_send(2, a1)
  88  
  89          # Have the miner (node3) mine a block.
  90          # Must sync mempools before mining.
  91          self.sync_mempools()
  92          self.generate(self.nodes[3], 1)
  93  
  94      def restore_invalid_wallet(self):
  95          node = self.nodes[3]
  96          invalid_wallet_file = self.nodes[0].datadir_path / 'invalid_wallet_file.bak'
  97          open(invalid_wallet_file, "a").write("invalid_wallet_content")
  98          wallet_name = "res0"
  99          not_created_wallet_file = node.wallets_path / wallet_name
 100          error_message = "Wallet file verification failed. Failed to load database path '{}'. Data is not in recognized format.".format(not_created_wallet_file)
 101          assert_raises_rpc_error(-18, error_message, node.restorewallet, wallet_name, invalid_wallet_file)
 102          assert not not_created_wallet_file.exists()
 103  
 104      def restore_nonexistent_wallet(self):
 105          node = self.nodes[3]
 106          nonexistent_wallet_file = self.nodes[0].datadir_path / 'nonexistent_wallet.bak'
 107          wallet_name = "res0"
 108          assert_raises_rpc_error(-8, "Backup file does not exist", node.restorewallet, wallet_name, nonexistent_wallet_file)
 109          not_created_wallet_file = node.wallets_path / wallet_name
 110          assert not not_created_wallet_file.exists()
 111  
 112      def restore_wallet_existent_name(self):
 113          node = self.nodes[3]
 114          backup_file = self.nodes[0].datadir_path / 'wallet.bak'
 115          wallet_name = "res0"
 116          wallet_file = node.wallets_path / wallet_name
 117          error_message = "Failed to restore wallet. Database file exists in '{}'.".format(wallet_file / "wallet.dat")
 118          assert_raises_rpc_error(-36, error_message, node.restorewallet, wallet_name, backup_file)
 119          assert wallet_file.exists()
 120  
 121      def test_restore_existent_dir(self):
 122          self.log.info("Test restore on an existent empty directory")
 123          node = self.nodes[3]
 124          backup_file = self.nodes[0].datadir_path / 'wallet.bak'
 125          wallet_name = "restored_wallet"
 126          wallet_dir = node.wallets_path / wallet_name
 127          os.mkdir(wallet_dir)
 128          res = node.restorewallet(wallet_name, backup_file)
 129          assert_equal(res['name'], wallet_name)
 130          node.unloadwallet(wallet_name)
 131  
 132          self.log.info("Test restore succeeds when the target directory contains non-wallet files")
 133          wallet_file = node.wallets_path / wallet_name / "wallet.dat"
 134          os.remove(wallet_file)
 135          extra_file = node.wallets_path / wallet_name / "not_a_wallet.txt"
 136          extra_file.touch()
 137          res = node.restorewallet(wallet_name, backup_file)
 138          assert_equal(res['name'], wallet_name)
 139          assert extra_file.exists() # extra file was not removed by mistake
 140          node.unloadwallet(wallet_name)
 141  
 142          self.log.info("Test restore failure due to existing db file in the destination directory")
 143          original_shasum = sha256sum_file(wallet_file)
 144          error_message = "Failed to restore wallet. Database file exists in '{}'.".format(wallet_dir / "wallet.dat")
 145          assert_raises_rpc_error(-36, error_message, node.restorewallet, wallet_name, backup_file)
 146          # Ensure the wallet file remains untouched
 147          assert wallet_dir.exists()
 148          assert_equal(original_shasum, sha256sum_file(wallet_file))
 149  
 150          self.log.info("Test restore succeeds when the .dat file in the destination has a different name")
 151          second_wallet = wallet_dir / "hidden_storage.dat"
 152          os.rename(wallet_dir / "wallet.dat", second_wallet)
 153          original_shasum = sha256sum_file(second_wallet)
 154          res = node.restorewallet(wallet_name, backup_file)
 155          assert_equal(res['name'], wallet_name)
 156          assert (wallet_dir / "hidden_storage.dat").exists()
 157          assert_equal(original_shasum, sha256sum_file(second_wallet))
 158          node.unloadwallet(wallet_name)
 159  
 160          # Clean for follow-up tests
 161          os.remove(wallet_file)
 162  
 163      def test_restore_into_unnamed_wallet(self):
 164          self.log.info("Test restore into a default unnamed wallet")
 165          # This is also useful to test the migration recovery after failure logic
 166          node = self.nodes[3]
 167          backup_file = self.nodes[0].datadir_path / 'wallet.bak'
 168          assert_raises_rpc_error(-8, "Wallet name cannot be empty", node.restorewallet, "", backup_file)
 169          assert not (node.wallets_path / "wallet.dat").exists()
 170  
 171      def test_pruned_wallet_backup(self):
 172          self.log.info("Test loading backup on a pruned node when the backup was created close to the prune height of the restoring node")
 173          node = self.nodes[3]
 174          self.restart_node(3, ["-prune=1", "-fastprune=1"])
 175          # Ensure the chain tip is at height 214, because this test assumes it is.
 176          assert_equal(node.getchaintips()[0]["height"], 214)
 177          # We need a few more blocks so we can actually get above an realistic
 178          # minimal prune height
 179          self.generate(node, 50, sync_fun=self.no_op)
 180          # Backup created at block height 264
 181          node.backupwallet(node.datadir_path / 'wallet_pruned.bak')
 182          # Generate more blocks so we can actually prune the older blocks
 183          self.generate(node, 300, sync_fun=self.no_op)
 184          # This gives us an actual prune height roughly in the range of 220 - 240
 185          node.pruneblockchain(250)
 186          # The backup should be updated with the latest height (locator) for
 187          # the backup to load successfully this close to the prune height
 188          node.restorewallet('pruned', node.datadir_path / 'wallet_pruned.bak')
 189  
 190          self.log.info("Test restore on a pruned node when the backup was beyond the pruning point")
 191          backup_file = self.nodes[0].datadir_path / 'wallet.bak'
 192          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)"
 193          assert_raises_rpc_error(-4, error_message, node.restorewallet, "restore_pruned", backup_file)
 194          assert node.wallets_path.exists() # ensure the wallets dir exists
 195  
 196      def run_test(self):
 197          self.log.info("Generating initial blockchain")
 198          self.generate(self.nodes[0], 1)
 199          self.generate(self.nodes[1], 1)
 200          self.generate(self.nodes[2], 1)
 201          self.generate(self.nodes[3], COINBASE_MATURITY)
 202  
 203          assert_equal(self.nodes[0].getbalance(), 50)
 204          assert_equal(self.nodes[1].getbalance(), 50)
 205          assert_equal(self.nodes[2].getbalance(), 50)
 206          assert_equal(self.nodes[3].getbalance(), 0)
 207  
 208          self.log.info("Creating transactions")
 209          # Five rounds of sending each other transactions.
 210          for _ in range(5):
 211              self.do_one_round()
 212  
 213          self.log.info("Backing up")
 214  
 215          for node_num in range(3):
 216              self.nodes[node_num].backupwallet(self.nodes[node_num].datadir_path / 'wallet.bak')
 217  
 218          self.log.info("More transactions")
 219          for _ in range(5):
 220              self.do_one_round()
 221  
 222          # Generate 101 more blocks, so any fees paid mature
 223          self.generate(self.nodes[3], COINBASE_MATURITY + 1)
 224  
 225          balance0 = self.nodes[0].getbalance()
 226          balance1 = self.nodes[1].getbalance()
 227          balance2 = self.nodes[2].getbalance()
 228          balance3 = self.nodes[3].getbalance()
 229          total = balance0 + balance1 + balance2 + balance3
 230  
 231          # At this point, there are 214 blocks (103 for setup, then 10 rounds, then 101.)
 232          # 114 are mature, so the sum of all wallets should be 114 * 50 = 5700.
 233          assert_equal(total, 5700)
 234  
 235          ##
 236          # Test restoring spender wallets from backups
 237          ##
 238          self.log.info("Restoring wallets on node 3 using backup files")
 239  
 240          self.restore_invalid_wallet()
 241          self.restore_nonexistent_wallet()
 242  
 243          backup_files = []
 244          for node_num in range(3):
 245              backup_files.append(self.nodes[node_num].datadir_path / 'wallet.bak')
 246  
 247          for idx, backup_file in enumerate(backup_files):
 248              self.nodes[3].restorewallet(f'res{idx}', backup_file)
 249              assert (self.nodes[3].wallets_path / f'res{idx}').exists()
 250  
 251          res0_rpc = self.nodes[3].get_wallet_rpc("res0")
 252          res1_rpc = self.nodes[3].get_wallet_rpc("res1")
 253          res2_rpc = self.nodes[3].get_wallet_rpc("res2")
 254  
 255          assert_equal(res0_rpc.getbalance(), balance0)
 256          assert_equal(res1_rpc.getbalance(), balance1)
 257          assert_equal(res2_rpc.getbalance(), balance2)
 258  
 259          self.restore_wallet_existent_name()
 260          self.test_restore_existent_dir()
 261          self.test_restore_into_unnamed_wallet()
 262  
 263          # Backup to source wallet file must fail
 264          sourcePaths = [
 265              os.path.join(self.nodes[0].wallets_path, self.default_wallet_name, self.wallet_data_filename),
 266              os.path.join(self.nodes[0].wallets_path, '.', self.default_wallet_name, self.wallet_data_filename),
 267              os.path.join(self.nodes[0].wallets_path, self.default_wallet_name),
 268              os.path.join(self.nodes[0].wallets_path)]
 269  
 270          for sourcePath in sourcePaths:
 271              assert_raises_rpc_error(-4, "backup failed", self.nodes[0].backupwallet, sourcePath)
 272  
 273          self.test_pruned_wallet_backup()
 274  
 275  
 276  if __name__ == '__main__':
 277      WalletBackupTest(__file__).main()
 278