/ 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  )
 43  
 44  
 45  class WalletBackupTest(BitcoinTestFramework):
 46      def set_test_params(self):
 47          self.num_nodes = 4
 48          self.setup_clean_chain = True
 49          # whitelist peers to speed up tx relay / mempool sync
 50          self.noban_tx_relay = True
 51          # nodes 1, 2, 3 are spenders, let's give them a keypool=100
 52          self.extra_args = [
 53              ["-keypool=100"],
 54              ["-keypool=100"],
 55              ["-keypool=100"],
 56              [],
 57          ]
 58          self.rpc_timeout = 120
 59  
 60      def skip_test_if_missing_module(self):
 61          self.skip_if_no_wallet()
 62  
 63      def setup_network(self):
 64          self.setup_nodes()
 65          self.connect_nodes(0, 3)
 66          self.connect_nodes(1, 3)
 67          self.connect_nodes(2, 3)
 68          self.connect_nodes(2, 0)
 69          self.sync_all()
 70  
 71      def one_send(self, from_node, to_address):
 72          if (randint(1,2) == 1):
 73              amount = Decimal(randint(1,10)) / Decimal(10)
 74              self.nodes[from_node].sendtoaddress(to_address, amount)
 75  
 76      def do_one_round(self):
 77          a0 = self.nodes[0].getnewaddress()
 78          a1 = self.nodes[1].getnewaddress()
 79          a2 = self.nodes[2].getnewaddress()
 80  
 81          self.one_send(0, a1)
 82          self.one_send(0, a2)
 83          self.one_send(1, a0)
 84          self.one_send(1, a2)
 85          self.one_send(2, a0)
 86          self.one_send(2, a1)
 87  
 88          # Have the miner (node3) mine a block.
 89          # Must sync mempools before mining.
 90          self.sync_mempools()
 91          self.generate(self.nodes[3], 1)
 92  
 93      # As above, this mirrors the original bash test.
 94      def start_three(self, args=()):
 95          self.start_node(0, self.extra_args[0] + list(args))
 96          self.start_node(1, self.extra_args[1] + list(args))
 97          self.start_node(2, self.extra_args[2] + list(args))
 98          self.connect_nodes(0, 3)
 99          self.connect_nodes(1, 3)
100          self.connect_nodes(2, 3)
101          self.connect_nodes(2, 0)
102  
103      def stop_three(self):
104          self.stop_node(0)
105          self.stop_node(1)
106          self.stop_node(2)
107  
108      def erase_three(self):
109          for node_num in range(3):
110              (self.nodes[node_num].wallets_path / self.default_wallet_name / self.wallet_data_filename).unlink()
111  
112      def restore_invalid_wallet(self):
113          node = self.nodes[3]
114          invalid_wallet_file = self.nodes[0].datadir_path / 'invalid_wallet_file.bak'
115          open(invalid_wallet_file, "a").write("invalid_wallet_content")
116          wallet_name = "res0"
117          not_created_wallet_file = node.wallets_path / wallet_name
118          error_message = "Wallet file verification failed. Failed to load database path '{}'. Data is not in recognized format.".format(not_created_wallet_file)
119          assert_raises_rpc_error(-18, error_message, node.restorewallet, wallet_name, invalid_wallet_file)
120          assert not not_created_wallet_file.exists()
121  
122      def restore_nonexistent_wallet(self):
123          node = self.nodes[3]
124          nonexistent_wallet_file = self.nodes[0].datadir_path / 'nonexistent_wallet.bak'
125          wallet_name = "res0"
126          assert_raises_rpc_error(-8, "Backup file does not exist", node.restorewallet, wallet_name, nonexistent_wallet_file)
127          not_created_wallet_file = node.wallets_path / wallet_name
128          assert not not_created_wallet_file.exists()
129  
130      def restore_wallet_existent_name(self):
131          node = self.nodes[3]
132          backup_file = self.nodes[0].datadir_path / 'wallet.bak'
133          wallet_name = "res0"
134          wallet_file = node.wallets_path / wallet_name
135          error_message = "Failed to create database path '{}'. Database already exists.".format(wallet_file)
136          assert_raises_rpc_error(-36, error_message, node.restorewallet, wallet_name, backup_file)
137          assert wallet_file.exists()
138  
139      def test_pruned_wallet_backup(self):
140          self.log.info("Test loading backup on a pruned node when the backup was created close to the prune height of the restoring node")
141          node = self.nodes[3]
142          self.restart_node(3, ["-prune=1", "-fastprune=1"])
143          # Ensure the chain tip is at height 214, because this test assume it is.
144          assert_equal(node.getchaintips()[0]["height"], 214)
145          # We need a few more blocks so we can actually get above an realistic
146          # minimal prune height
147          self.generate(node, 50, sync_fun=self.no_op)
148          # Backup created at block height 264
149          node.backupwallet(node.datadir_path / 'wallet_pruned.bak')
150          # Generate more blocks so we can actually prune the older blocks
151          self.generate(node, 300, sync_fun=self.no_op)
152          # This gives us an actual prune height roughly in the range of 220 - 240
153          node.pruneblockchain(250)
154          # The backup should be updated with the latest height (locator) for
155          # the backup to load successfully this close to the prune height
156          node.restorewallet('pruned', node.datadir_path / 'wallet_pruned.bak')
157  
158      def run_test(self):
159          self.log.info("Generating initial blockchain")
160          self.generate(self.nodes[0], 1)
161          self.generate(self.nodes[1], 1)
162          self.generate(self.nodes[2], 1)
163          self.generate(self.nodes[3], COINBASE_MATURITY)
164  
165          assert_equal(self.nodes[0].getbalance(), 50)
166          assert_equal(self.nodes[1].getbalance(), 50)
167          assert_equal(self.nodes[2].getbalance(), 50)
168          assert_equal(self.nodes[3].getbalance(), 0)
169  
170          self.log.info("Creating transactions")
171          # Five rounds of sending each other transactions.
172          for _ in range(5):
173              self.do_one_round()
174  
175          self.log.info("Backing up")
176  
177          for node_num in range(3):
178              self.nodes[node_num].backupwallet(self.nodes[node_num].datadir_path / 'wallet.bak')
179  
180          self.log.info("More transactions")
181          for _ in range(5):
182              self.do_one_round()
183  
184          # Generate 101 more blocks, so any fees paid mature
185          self.generate(self.nodes[3], COINBASE_MATURITY + 1)
186  
187          balance0 = self.nodes[0].getbalance()
188          balance1 = self.nodes[1].getbalance()
189          balance2 = self.nodes[2].getbalance()
190          balance3 = self.nodes[3].getbalance()
191          total = balance0 + balance1 + balance2 + balance3
192  
193          # At this point, there are 214 blocks (103 for setup, then 10 rounds, then 101.)
194          # 114 are mature, so the sum of all wallets should be 114 * 50 = 5700.
195          assert_equal(total, 5700)
196  
197          ##
198          # Test restoring spender wallets from backups
199          ##
200          self.log.info("Restoring wallets on node 3 using backup files")
201  
202          self.restore_invalid_wallet()
203          self.restore_nonexistent_wallet()
204  
205          backup_files = []
206          for node_num in range(3):
207              backup_files.append(self.nodes[node_num].datadir_path / 'wallet.bak')
208  
209          for idx, backup_file in enumerate(backup_files):
210              self.nodes[3].restorewallet(f'res{idx}', backup_file)
211              assert (self.nodes[3].wallets_path / f'res{idx}').exists()
212  
213          res0_rpc = self.nodes[3].get_wallet_rpc("res0")
214          res1_rpc = self.nodes[3].get_wallet_rpc("res1")
215          res2_rpc = self.nodes[3].get_wallet_rpc("res2")
216  
217          assert_equal(res0_rpc.getbalance(), balance0)
218          assert_equal(res1_rpc.getbalance(), balance1)
219          assert_equal(res2_rpc.getbalance(), balance2)
220  
221          self.restore_wallet_existent_name()
222  
223          # Backup to source wallet file must fail
224          sourcePaths = [
225              os.path.join(self.nodes[0].wallets_path, self.default_wallet_name, self.wallet_data_filename),
226              os.path.join(self.nodes[0].wallets_path, '.', self.default_wallet_name, self.wallet_data_filename),
227              os.path.join(self.nodes[0].wallets_path, self.default_wallet_name),
228              os.path.join(self.nodes[0].wallets_path)]
229  
230          for sourcePath in sourcePaths:
231              assert_raises_rpc_error(-4, "backup failed", self.nodes[0].backupwallet, sourcePath)
232  
233          self.test_pruned_wallet_backup()
234  
235  
236  if __name__ == '__main__':
237      WalletBackupTest(__file__).main()