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