/ test / functional / wallet_backup.py
wallet_backup.py
  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()