wallet_migration.py raw
1 #!/usr/bin/env python3
2 # Copyright (c) 2020-present 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 Migrating a wallet from legacy to descriptor."""
6
7 import os
8 from pathlib import Path
9 import random
10 import shutil
11 import struct
12 import time
13
14 from test_framework.address import (
15 key_to_p2pkh,
16 key_to_p2wpkh,
17 script_to_p2sh,
18 script_to_p2wsh,
19 )
20 from test_framework.bdb import BTREE_MAGIC
21 from test_framework.descriptors import descsum_create
22 from test_framework.key import ECPubKey
23 from test_framework.test_framework import LimenkaTestFramework
24 from test_framework.messages import COIN, CTransaction, CTxOut
25 from test_framework.script import hash160
26 from test_framework.script_util import key_to_p2pkh_script, key_to_p2pk_script, script_to_p2sh_script, script_to_p2wsh_script
27 from test_framework.util import (
28 assert_equal,
29 assert_greater_than,
30 assert_raises_rpc_error,
31 find_vout_for_address,
32 sha256sum_file,
33 )
34 from test_framework.wallet_util import (
35 get_generate_key,
36 generate_keypair,
37 )
38
39
40 class WalletMigrationTest(LimenkaTestFramework):
41 def add_options(self, parser):
42 self.add_wallet_options(parser)
43
44 def set_test_params(self):
45 self.setup_clean_chain = True
46 self.num_nodes = 2
47 self.supports_cli = False
48 self.extra_args = [[], ["-deprecatedrpc=create_bdb"]]
49
50 def skip_test_if_missing_module(self):
51 self.skip_if_no_wallet()
52 self.skip_if_no_previous_releases()
53
54 def setup_nodes(self):
55 self.add_nodes(self.num_nodes, versions=[
56 None,
57 280000,
58 ])
59 self.start_nodes()
60 self.init_wallet(node=0)
61
62 def assert_is_sqlite(self, wallet_name):
63 wallet_file_path = self.master_node.wallets_path / wallet_name / self.wallet_data_filename
64 with open(wallet_file_path, 'rb') as f:
65 file_magic = f.read(16)
66 assert_equal(file_magic, b'SQLite format 3\x00')
67 assert_equal(self.master_node.get_wallet_rpc(wallet_name).getwalletinfo()["format"], "sqlite")
68
69 def assert_is_bdb(self, wallet_name):
70 with open(self.master_node.wallets_path / wallet_name / self.wallet_data_filename, "rb") as f:
71 data = f.read(16)
72 _, _, magic = struct.unpack("QII", data)
73 assert_equal(magic, BTREE_MAGIC)
74
75 def create_legacy_wallet(self, wallet_name, **kwargs):
76 self.old_node.createwallet(wallet_name=wallet_name, descriptors=False, **kwargs)
77 wallet = self.old_node.get_wallet_rpc(wallet_name)
78 info = wallet.getwalletinfo()
79 assert_equal(info["descriptors"], False)
80 assert_equal(info["format"], "bdb")
81 return wallet
82
83 def assert_addr_info_equal(self, addr_info, addr_info_old):
84 assert_equal(addr_info["address"], addr_info_old["address"])
85 assert_equal(addr_info["scriptPubKey"], addr_info_old["scriptPubKey"])
86 assert_equal(addr_info["ismine"], addr_info_old["ismine"])
87 assert_equal(addr_info["hdkeypath"], addr_info_old["hdkeypath"].replace("'","h"))
88 assert_equal(addr_info["solvable"], addr_info_old["solvable"])
89 assert_equal(addr_info["ischange"], addr_info_old["ischange"])
90 assert_equal(addr_info["hdmasterfingerprint"], addr_info_old["hdmasterfingerprint"])
91
92 def assert_list_txs_equal(self, received_list_txs, expected_list_txs):
93 for d in received_list_txs:
94 if 'in_mempool' in d:
95 del d['in_mempool']
96 if "parent_descs" in d:
97 del d["parent_descs"]
98 for d in expected_list_txs:
99 if "parent_descs" in d:
100 del d["parent_descs"]
101 assert_equal(received_list_txs, expected_list_txs)
102
103 def check_address(self, wallet, addr, is_mine, is_change, label):
104 addr_info = wallet.getaddressinfo(addr)
105 assert_equal(addr_info['ismine'], is_mine)
106 assert_equal(addr_info['ischange'], is_change)
107 if label is not None:
108 assert_equal(addr_info['labels'], [label]),
109 else:
110 assert_equal(addr_info['labels'], []),
111
112 def migrate_and_get_rpc(self, wallet_name, **kwargs):
113 # Since we may rescan on loading of a wallet, make sure that the best block
114 # is written before beginning migration
115 # Reload to force write that record
116 self.old_node.unloadwallet(wallet_name)
117 self.old_node.loadwallet(wallet_name)
118 assert_equal(self.old_node.get_wallet_rpc(wallet_name).getwalletinfo()["descriptors"], False)
119 # Now unload so we can copy it to the master node for the migration test
120 self.old_node.unloadwallet(wallet_name)
121 if wallet_name == "":
122 shutil.copyfile(self.old_node.wallets_path / "wallet.dat", self.master_node.wallets_path / "wallet.dat")
123 else:
124 src = os.path.abspath(self.old_node.wallets_path / wallet_name)
125 dst = os.path.abspath(self.master_node.wallets_path / wallet_name)
126 if src != dst :
127 shutil.copytree(self.old_node.wallets_path / wallet_name, self.master_node.wallets_path / wallet_name, dirs_exist_ok=True)
128 # Migrate, checking that rescan does not occur
129 with self.master_node.assert_debug_log(expected_msgs=[], unexpected_msgs=["Rescanning"]):
130 migrate_info = self.master_node.migratewallet(wallet_name=wallet_name, **kwargs)
131 # Update wallet name in case the initial wallet was completely migrated to a watch-only wallet
132 # (in which case the wallet name would be suffixed by the 'watchonly' term)
133 wallet_name = migrate_info['wallet_name']
134 wallet = self.master_node.get_wallet_rpc(wallet_name)
135 assert_equal(wallet.getwalletinfo()["descriptors"], True)
136 self.assert_is_sqlite(wallet_name)
137 # Always verify the backup path exist after migration
138 assert os.path.exists(migrate_info['backup_path'])
139 return migrate_info, wallet
140
141 def test_basic(self):
142 default = self.master_node.get_wallet_rpc(self.default_wallet_name)
143
144 self.log.info("Test migration of a basic keys only wallet without balance")
145 basic0 = self.create_legacy_wallet("basic0")
146
147 addr = basic0.getnewaddress()
148 change = basic0.getrawchangeaddress()
149
150 old_addr_info = basic0.getaddressinfo(addr)
151 old_change_addr_info = basic0.getaddressinfo(change)
152 assert_equal(old_addr_info["ismine"], True)
153 assert_equal(old_addr_info["hdkeypath"], "m/0'/0'/0'")
154 assert_equal(old_change_addr_info["ismine"], True)
155 assert_equal(old_change_addr_info["hdkeypath"], "m/0'/1'/0'")
156
157 # Note: migration could take a while.
158 _, basic0 = self.migrate_and_get_rpc("basic0")
159
160 # The wallet should create the following descriptors:
161 # * BIP32 descriptors in the form of "0h/0h/*" and "0h/1h/*" (2 descriptors)
162 # * BIP44 descriptors in the form of "44h/1h/0h/0/*" and "44h/1h/0h/1/*" (2 descriptors)
163 # * BIP49 descriptors, P2SH(P2WPKH), in the form of "86h/1h/0h/0/*" and "86h/1h/0h/1/*" (2 descriptors)
164 # * BIP84 descriptors, P2WPKH, in the form of "84h/1h/0h/1/*" and "84h/1h/0h/1/*" (2 descriptors)
165 # * BIP86 descriptors, P2TR, in the form of "86h/1h/0h/0/*" and "86h/1h/0h/1/*" (2 descriptors)
166 # * A combo(PK) descriptor for the wallet master key.
167 # So, should have a total of 11 descriptors on it.
168 assert_equal(len(basic0.listdescriptors()["descriptors"]), 11)
169
170 # Compare addresses info
171 addr_info = basic0.getaddressinfo(addr)
172 change_addr_info = basic0.getaddressinfo(change)
173 self.assert_addr_info_equal(addr_info, old_addr_info)
174 self.assert_addr_info_equal(change_addr_info, old_change_addr_info)
175
176 addr_info = basic0.getaddressinfo(basic0.getnewaddress("", "bech32"))
177 assert_equal(addr_info["hdkeypath"], "m/84h/1h/0h/0/0")
178
179 self.log.info("Test migration of a basic keys only wallet with a balance")
180 basic1 = self.create_legacy_wallet("basic1")
181
182 for _ in range(0, 10):
183 default.sendtoaddress(basic1.getnewaddress(), 1)
184
185 self.generate(self.master_node, 1)
186
187 for _ in range(0, 5):
188 basic1.sendtoaddress(default.getnewaddress(), 0.5)
189
190 self.generate(self.master_node, 1)
191 bal = basic1.getbalance()
192 txs = basic1.listtransactions()
193 addr_gps = basic1.listaddressgroupings()
194
195 basic1_migrate, basic1 = self.migrate_and_get_rpc("basic1")
196 assert_equal(basic1.getbalance(), bal)
197 self.assert_list_txs_equal(basic1.listtransactions(), txs)
198
199 self.log.info("Test backup file can be successfully restored")
200 self.old_node.restorewallet("basic1_restored", basic1_migrate['backup_path'])
201 basic1_restored = self.old_node.get_wallet_rpc("basic1_restored")
202 basic1_restored_wi = basic1_restored.getwalletinfo()
203 assert_equal(basic1_restored_wi['balance'], bal)
204 assert_equal(basic1_restored.listaddressgroupings(), addr_gps)
205 self.assert_list_txs_equal(basic1_restored.listtransactions(), txs)
206
207 # restart master node and verify that everything is still there
208 self.restart_node(0)
209 self.connect_nodes(0, 1)
210 default = self.master_node.get_wallet_rpc(self.default_wallet_name)
211 self.master_node.loadwallet("basic1")
212 basic1 = self.master_node.get_wallet_rpc("basic1")
213 assert_equal(basic1.getbalance(), bal)
214 self.assert_list_txs_equal(basic1.listtransactions(), txs)
215
216 self.log.info("Test migration of a wallet with balance received on the seed")
217 basic2 = self.create_legacy_wallet("basic2")
218 basic2_seed = get_generate_key()
219 basic2.sethdseed(True, basic2_seed.privkey)
220 assert_equal(basic2.getbalance(), 0)
221
222 # Receive coins on different output types for the same seed
223 basic2_balance = 0
224 for addr in [basic2_seed.p2pkh_addr, basic2_seed.p2wpkh_addr, basic2_seed.p2sh_p2wpkh_addr]:
225 send_value = random.randint(1, 4)
226 default.sendtoaddress(addr, send_value)
227 basic2_balance += send_value
228 self.generate(self.master_node, 1)
229 assert_equal(basic2.getbalance(), basic2_balance)
230 basic2_txs = basic2.listtransactions()
231
232 # Now migrate and test that we still have the same balance/transactions
233 _, basic2 = self.migrate_and_get_rpc("basic2")
234 assert_equal(basic2.getbalance(), basic2_balance)
235 self.assert_list_txs_equal(basic2.listtransactions(), basic2_txs)
236
237 # Now test migration on a descriptor wallet
238 self.log.info("Test \"nothing to migrate\" when the user tries to migrate a loaded wallet with no legacy data")
239 assert_raises_rpc_error(-4, "Error: This wallet is already a descriptor wallet", basic2.migratewallet)
240
241 self.log.info("Test \"nothing to migrate\" when the user tries to migrate an unloaded wallet with no legacy data")
242 basic2.unloadwallet()
243 assert_raises_rpc_error(-4, "Error: This wallet is already a descriptor wallet", self.master_node.migratewallet, "basic2")
244
245 def test_multisig(self):
246 default = self.master_node.get_wallet_rpc(self.default_wallet_name)
247
248 # Contrived case where all the multisig keys are in a single wallet
249 self.log.info("Test migration of a wallet with all keys for a multisig")
250 multisig0 = self.create_legacy_wallet("multisig0")
251 addr1 = multisig0.getnewaddress()
252 addr2 = multisig0.getnewaddress()
253 addr3 = multisig0.getnewaddress()
254
255 ms_info = multisig0.addmultisigaddress(2, [addr1, addr2, addr3])
256
257 _, multisig0 = self.migrate_and_get_rpc("multisig0")
258 ms_addr_info = multisig0.getaddressinfo(ms_info["address"])
259 assert_equal(ms_addr_info["ismine"], True)
260 assert_equal(ms_addr_info["desc"], ms_info["descriptor"])
261 assert_equal("multisig0_watchonly" in self.master_node.listwallets(), False)
262 assert_equal("multisig0_solvables" in self.master_node.listwallets(), False)
263
264 pub1 = multisig0.getaddressinfo(addr1)["pubkey"]
265 pub2 = multisig0.getaddressinfo(addr2)["pubkey"]
266
267 # Some keys in multisig do not belong to this wallet
268 self.log.info("Test migration of a wallet that has some keys in a multisig")
269 multisig1 = self.create_legacy_wallet("multisig1")
270 ms_info = multisig1.addmultisigaddress(2, [multisig1.getnewaddress(), pub1, pub2])
271 ms_info2 = multisig1.addmultisigaddress(2, [multisig1.getnewaddress(), pub1, pub2])
272
273 addr1 = ms_info["address"]
274 addr2 = ms_info2["address"]
275 txid = default.sendtoaddress(addr1, 10)
276 multisig1.importaddress(addr1)
277 assert_equal(multisig1.getaddressinfo(addr1)["ismine"], False)
278 assert_equal(multisig1.getaddressinfo(addr1)["iswatchonly"], True)
279 assert_equal(multisig1.getaddressinfo(addr1)["solvable"], True)
280 self.generate(self.master_node, 1)
281 multisig1.gettransaction(txid)
282 assert_equal(multisig1.getbalances()["watchonly"]["trusted"], 10)
283 assert_equal(multisig1.getaddressinfo(addr2)["ismine"], False)
284 assert_equal(multisig1.getaddressinfo(addr2)["iswatchonly"], False)
285 assert_equal(multisig1.getaddressinfo(addr2)["solvable"], True)
286
287 # Migrating multisig1 should see the multisig is no longer part of multisig1
288 # A new wallet multisig1_watchonly is created which has the multisig address
289 # Transaction to multisig is in multisig1_watchonly and not multisig1
290 _, multisig1 = self.migrate_and_get_rpc("multisig1")
291 assert_equal(multisig1.getaddressinfo(addr1)["ismine"], False)
292 assert_equal(multisig1.getaddressinfo(addr1)["iswatchonly"], False)
293 assert_equal(multisig1.getaddressinfo(addr1)["solvable"], False)
294 assert_raises_rpc_error(-5, "Invalid or non-wallet transaction id", multisig1.gettransaction, txid)
295 assert_equal(multisig1.getbalance(), 0)
296 assert_equal(multisig1.listtransactions(), [])
297
298 assert_equal("multisig1_watchonly" in self.master_node.listwallets(), True)
299 ms1_watchonly = self.master_node.get_wallet_rpc("multisig1_watchonly")
300 ms1_wallet_info = ms1_watchonly.getwalletinfo()
301 assert_equal(ms1_wallet_info['descriptors'], True)
302 assert_equal(ms1_wallet_info['private_keys_enabled'], False)
303 self.assert_is_sqlite("multisig1_watchonly")
304 assert_equal(ms1_watchonly.getaddressinfo(addr1)["ismine"], True)
305 assert_equal(ms1_watchonly.getaddressinfo(addr1)["solvable"], True)
306 # Because addr2 was not being watched, it isn't in multisig1_watchonly but rather multisig1_solvables
307 assert_equal(ms1_watchonly.getaddressinfo(addr2)["ismine"], False)
308 assert_equal(ms1_watchonly.getaddressinfo(addr2)["solvable"], False)
309 ms1_watchonly.gettransaction(txid)
310 assert_equal(ms1_watchonly.getbalance(), 10)
311
312 # Migrating multisig1 should see the second multisig is no longer part of multisig1
313 # A new wallet multisig1_solvables is created which has the second address
314 # This should have no transactions
315 assert_equal("multisig1_solvables" in self.master_node.listwallets(), True)
316 ms1_solvable = self.master_node.get_wallet_rpc("multisig1_solvables")
317 ms1_wallet_info = ms1_solvable.getwalletinfo()
318 assert_equal(ms1_wallet_info['descriptors'], True)
319 assert_equal(ms1_wallet_info['private_keys_enabled'], False)
320 self.assert_is_sqlite("multisig1_solvables")
321 assert_equal(ms1_solvable.getaddressinfo(addr1)["ismine"], False)
322 assert_equal(ms1_solvable.getaddressinfo(addr1)["solvable"], False)
323 assert_equal(ms1_solvable.getaddressinfo(addr2)["ismine"], True)
324 assert_equal(ms1_solvable.getaddressinfo(addr2)["solvable"], True)
325 assert_equal(ms1_solvable.getbalance(), 0)
326 assert_equal(ms1_solvable.listtransactions(), [])
327
328
329 def test_other_watchonly(self):
330 default = self.master_node.get_wallet_rpc(self.default_wallet_name)
331
332 # Wallet with an imported address. Should be the same thing as the multisig test
333 self.log.info("Test migration of a wallet with watchonly imports")
334 imports0 = self.create_legacy_wallet("imports0")
335
336 # External address label
337 imports0.setlabel(default.getnewaddress(), "external")
338
339 # Normal non-watchonly tx
340 received_addr = imports0.getnewaddress()
341 imports0.setlabel(received_addr, "Receiving")
342 received_txid = default.sendtoaddress(received_addr, 10)
343
344 # Watchonly tx
345 import_addr = default.getnewaddress()
346 imports0.importaddress(import_addr)
347 imports0.setlabel(import_addr, "imported")
348 received_watchonly_txid = default.sendtoaddress(import_addr, 10)
349
350 # Received watchonly tx that is then spent
351 import_sent_addr = default.getnewaddress()
352 imports0.importaddress(import_sent_addr)
353 received_sent_watchonly_utxo = self.create_outpoints(node=default, outputs=[{import_sent_addr: 10}])[0]
354
355 send = default.sendall(recipients=[default.getnewaddress()], inputs=[received_sent_watchonly_utxo])
356 sent_watchonly_txid = send["txid"]
357
358 # Tx that has both a watchonly and spendable output
359 watchonly_spendable_txid = default.send(outputs=[{received_addr: 1}, {import_addr:1}])["txid"]
360
361 self.generate(self.master_node, 2)
362 received_watchonly_tx_info = imports0.gettransaction(received_watchonly_txid, True)
363 received_sent_watchonly_tx_info = imports0.gettransaction(received_sent_watchonly_utxo["txid"], True)
364
365 balances = imports0.getbalances()
366 spendable_bal = balances["mine"]["trusted"]
367 watchonly_bal = balances["watchonly"]["trusted"]
368 assert_equal(len(imports0.listtransactions(include_watchonly=True)), 6)
369
370 # Mock time forward a bit so we can check that tx metadata is preserved
371 self.master_node.setmocktime(int(time.time()) + 100)
372
373 # Migrate
374 _, imports0 = self.migrate_and_get_rpc("imports0")
375 assert_raises_rpc_error(-5, "Invalid or non-wallet transaction id", imports0.gettransaction, received_watchonly_txid)
376 assert_raises_rpc_error(-5, "Invalid or non-wallet transaction id", imports0.gettransaction, received_sent_watchonly_utxo['txid'])
377 assert_raises_rpc_error(-5, "Invalid or non-wallet transaction id", imports0.gettransaction, sent_watchonly_txid)
378 assert_equal(len(imports0.listtransactions(include_watchonly=True)), 2)
379 imports0.gettransaction(received_txid)
380 imports0.gettransaction(watchonly_spendable_txid)
381 assert_equal(imports0.getbalance(), spendable_bal)
382
383 assert_equal("imports0_watchonly" in self.master_node.listwallets(), True)
384 watchonly = self.master_node.get_wallet_rpc("imports0_watchonly")
385 watchonly_info = watchonly.getwalletinfo()
386 assert_equal(watchonly_info["descriptors"], True)
387 self.assert_is_sqlite("imports0_watchonly")
388 assert_equal(watchonly_info["private_keys_enabled"], False)
389 received_migrated_watchonly_tx_info = watchonly.gettransaction(received_watchonly_txid)
390 assert_equal(received_watchonly_tx_info["time"], received_migrated_watchonly_tx_info["time"])
391 assert_equal(received_watchonly_tx_info["timereceived"], received_migrated_watchonly_tx_info["timereceived"])
392 received_sent_migrated_watchonly_tx_info = watchonly.gettransaction(received_sent_watchonly_utxo["txid"])
393 assert_equal(received_sent_watchonly_tx_info["time"], received_sent_migrated_watchonly_tx_info["time"])
394 assert_equal(received_sent_watchonly_tx_info["timereceived"], received_sent_migrated_watchonly_tx_info["timereceived"])
395 watchonly.gettransaction(sent_watchonly_txid)
396 watchonly.gettransaction(watchonly_spendable_txid)
397 assert_equal(watchonly.getbalance(), watchonly_bal)
398 assert_raises_rpc_error(-5, "Invalid or non-wallet transaction id", watchonly.gettransaction, received_txid)
399 assert_equal(len(watchonly.listtransactions(include_watchonly=True)), 4)
400
401 # Check that labels were migrated and persisted to watchonly wallet
402 self.master_node.unloadwallet("imports0_watchonly")
403 self.master_node.loadwallet("imports0_watchonly")
404 labels = watchonly.listlabels()
405 assert "external" in labels
406 assert "imported" in labels
407
408 def test_no_privkeys(self):
409 default = self.master_node.get_wallet_rpc(self.default_wallet_name)
410
411 # Migrating an actual watchonly wallet should not create a new watchonly wallet
412 self.log.info("Test migration of a pure watchonly wallet")
413 watchonly0 = self.create_legacy_wallet("watchonly0", disable_private_keys=True)
414
415 addr = default.getnewaddress()
416 desc = default.getaddressinfo(addr)["desc"]
417 res = watchonly0.importmulti([
418 {
419 "desc": desc,
420 "watchonly": True,
421 "timestamp": "now",
422 }])
423 assert_equal(res[0]['success'], True)
424 default.sendtoaddress(addr, 10)
425 self.generate(self.master_node, 1)
426
427 _, watchonly0 = self.migrate_and_get_rpc("watchonly0")
428 assert_equal("watchonly0_watchonly" in self.master_node.listwallets(), False)
429 info = watchonly0.getwalletinfo()
430 assert_equal(info["descriptors"], True)
431 assert_equal(info["private_keys_enabled"], False)
432 self.assert_is_sqlite("watchonly0")
433
434 # Migrating a wallet with pubkeys added to the keypool
435 self.log.info("Test migration of a pure watchonly wallet with pubkeys in keypool")
436 watchonly1 = self.create_legacy_wallet("watchonly1", disable_private_keys=True)
437
438 addr1 = default.getnewaddress(address_type="bech32")
439 addr2 = default.getnewaddress(address_type="bech32")
440 desc1 = default.getaddressinfo(addr1)["desc"]
441 desc2 = default.getaddressinfo(addr2)["desc"]
442 res = watchonly1.importmulti([
443 {
444 "desc": desc1,
445 "keypool": True,
446 "timestamp": "now",
447 },
448 {
449 "desc": desc2,
450 "keypool": True,
451 "timestamp": "now",
452 }
453 ])
454 assert_equal(res[0]["success"], True)
455 assert_equal(res[1]["success"], True)
456 # Before migrating, we can fetch addr1 from the keypool
457 assert_equal(watchonly1.getnewaddress(address_type="bech32"), addr1)
458
459 _, watchonly1 = self.migrate_and_get_rpc("watchonly1")
460 info = watchonly1.getwalletinfo()
461 assert_equal(info["descriptors"], True)
462 assert_equal(info["private_keys_enabled"], False)
463 self.assert_is_sqlite("watchonly1")
464 # After migrating, the "keypool" is empty
465 assert_raises_rpc_error(-4, "Error: This wallet has no available keys", watchonly1.getnewaddress)
466
467 def test_pk_coinbases(self):
468 self.log.info("Test migration of a wallet using old pk() coinbases")
469 wallet = self.create_legacy_wallet("pkcb")
470
471 addr = wallet.getnewaddress()
472 addr_info = wallet.getaddressinfo(addr)
473 desc = descsum_create("pk(" + addr_info["pubkey"] + ")")
474
475 self.generatetodescriptor(self.master_node, 1, desc)
476
477 bals = wallet.getbalances()
478
479 _, wallet = self.migrate_and_get_rpc("pkcb")
480
481 assert_equal(bals, wallet.getbalances())
482
483 def test_encrypted(self):
484 self.log.info("Test migration of an encrypted wallet")
485 wallet = self.create_legacy_wallet("encrypted")
486 default = self.master_node.get_wallet_rpc(self.default_wallet_name)
487
488 wallet.encryptwallet("pass")
489 addr = wallet.getnewaddress()
490 txid = default.sendtoaddress(addr, 1)
491 self.generate(self.master_node, 1)
492 bals = wallet.getbalances()
493
494 # Use self.migrate_and_get_rpc to test this error to get everything copied over to the master node
495 assert_raises_rpc_error(-4, "Error: Wallet decryption failed, the wallet passphrase was not provided or was incorrect", self.migrate_and_get_rpc, "encrypted")
496 # Use the RPC directly on the master node for the rest of these checks
497 assert_raises_rpc_error(-4, "Error: Wallet decryption failed, the wallet passphrase was not provided or was incorrect", self.master_node.migratewallet, "encrypted", "badpass")
498 assert_raises_rpc_error(-4, "The passphrase contains a null character", self.master_node.migratewallet, "encrypted", "pass\0with\0null")
499
500 # Verify we can properly migrate the encrypted wallet
501 self.master_node.migratewallet("encrypted", passphrase="pass")
502 wallet = self.master_node.get_wallet_rpc("encrypted")
503
504 info = wallet.getwalletinfo()
505 assert_equal(info["descriptors"], True)
506 assert_equal(info["format"], "sqlite")
507 assert_equal(info["unlocked_until"], 0)
508 wallet.gettransaction(txid)
509
510 assert_equal(bals, wallet.getbalances())
511
512 def test_nonexistent(self):
513 self.log.info("Check migratewallet errors for nonexistent wallets")
514 default = self.master_node.get_wallet_rpc(self.default_wallet_name)
515 assert_raises_rpc_error(-8, "The RPC endpoint wallet and the wallet name parameter specify different wallets", default.migratewallet, "someotherwallet")
516 assert_raises_rpc_error(-8, "Either the RPC endpoint wallet or the wallet name parameter must be provided", self.master_node.migratewallet)
517 assert_raises_rpc_error(-4, "Error: Wallet does not exist", self.master_node.migratewallet, "notawallet")
518
519 def test_unloaded_by_path(self):
520 self.log.info("Test migration of a wallet that isn't loaded, specified by path")
521 wallet = self.create_legacy_wallet("notloaded2")
522 default = self.master_node.get_wallet_rpc(self.default_wallet_name)
523
524 addr = wallet.getnewaddress()
525 txid = default.sendtoaddress(addr, 1)
526 self.generate(self.master_node, 1)
527 bals = wallet.getbalances()
528
529 wallet.unloadwallet()
530
531 wallet_file_path = self.old_node.wallets_path / "notloaded2"
532 self.master_node.migratewallet(wallet_file_path)
533
534 # Because we gave the name by full path, the loaded wallet's name is that path too.
535 wallet = self.master_node.get_wallet_rpc(str(wallet_file_path))
536
537 info = wallet.getwalletinfo()
538 assert_equal(info["descriptors"], True)
539 assert_equal(info["format"], "sqlite")
540 wallet.gettransaction(txid)
541
542 assert_equal(bals, wallet.getbalances())
543
544 def test_wallet_with_relative_path(self):
545 self.log.info("Test migration of a wallet that isn't loaded, specified by a relative path")
546
547 # Get the nearest common path of both nodes' wallet paths.
548 common_parent = os.path.commonpath([self.master_node.wallets_path, self.old_node.wallets_path])
549
550 # This test assumes that the relative path from each wallet directory to the common path is identical.
551 assert_equal(os.path.relpath(common_parent, start=self.master_node.wallets_path), os.path.relpath(common_parent, start=self.old_node.wallets_path))
552
553 wallet_name = "relative"
554 absolute_path = os.path.abspath(os.path.join(common_parent, wallet_name))
555 relative_name = os.path.relpath(absolute_path, start=self.master_node.wallets_path)
556
557 wallet = self.create_legacy_wallet(relative_name)
558 # listwalletdirs only returns wallets in the wallet directory
559 assert {"name": relative_name} not in wallet.listwalletdir()["wallets"]
560 assert relative_name in wallet.listwallets()
561
562 default = self.master_node.get_wallet_rpc(self.default_wallet_name)
563 addr = wallet.getnewaddress()
564 txid = default.sendtoaddress(addr, 1)
565 self.generate(self.master_node, 1)
566 bals = wallet.getbalances()
567
568 # migratewallet uses current time in naming the backup file, set a mock time
569 # to check that this works correctly.
570 curr_time = int(time.time())
571 self.master_node.setmocktime(curr_time)
572 migrate_res, wallet = self.migrate_and_get_rpc(relative_name)
573 self.master_node.setmocktime(0)
574
575 # Check that the wallet was migrated, knows the right txid, and has the right balance.
576 assert wallet.gettransaction(txid)
577 assert_equal(bals, wallet.getbalances())
578
579 # The migrated wallet should not be in the wallet dir, but should be in the list of wallets.
580 info = wallet.getwalletinfo()
581
582 walletdirlist = wallet.listwalletdir()
583 assert {"name": info["walletname"]} not in walletdirlist["wallets"]
584
585 walletlist = wallet.listwallets()
586 assert info["walletname"] in walletlist
587
588 # Check that old node can restore from the backup.
589 self.old_node.restorewallet("relative_restored", migrate_res['backup_path'])
590 wallet = self.old_node.get_wallet_rpc("relative_restored")
591 assert wallet.gettransaction(txid)
592 assert_equal(bals, wallet.getbalances())
593
594 info = wallet.getwalletinfo()
595 assert_equal(info["descriptors"], False)
596 assert_equal(info["format"], "bdb")
597
598 def test_wallet_with_path_ending_in_slash(self):
599 self.log.info("Test migrating a wallet with a name/path ending in '/'")
600
601 # The last directory in the wallet's path
602 final_dir = "mywallet"
603 wallet_name = f"path/to/{final_dir}/"
604 wallet = self.create_legacy_wallet(wallet_name)
605 default = self.master_node.get_wallet_rpc(self.default_wallet_name)
606
607 addr = wallet.getnewaddress()
608 txid = default.sendtoaddress(addr, 1)
609 self.generate(self.master_node, 1)
610 bals = wallet.getbalances()
611
612 _, wallet = self.migrate_and_get_rpc(wallet_name)
613
614 assert wallet.gettransaction(txid)
615
616 assert_equal(bals, wallet.getbalances())
617
618 def test_wallet_with_path_ending_in_relative_specifier(self):
619 self.log.info("Test migrating a wallet with a name/path ending in a relative specifier, '..'")
620 wallet_ending_in_relative = "path/that/ends/in/.."
621 # the wallet data is actually inside of path/that/ends/
622 wallet = self.create_legacy_wallet(wallet_ending_in_relative)
623 default = self.master_node.get_wallet_rpc(self.default_wallet_name)
624
625 addr = wallet.getnewaddress()
626 txid = default.sendtoaddress(addr, 1)
627 self.generate(self.master_node, 1)
628 bals = wallet.getbalances()
629
630 _, wallet = self.migrate_and_get_rpc(wallet_ending_in_relative)
631
632 assert wallet.gettransaction(txid)
633
634 assert_equal(bals, wallet.getbalances())
635
636 def clear_default_wallet(self, backup_file):
637 # Test cleanup: Clear unnamed default wallet for subsequent tests
638 (self.old_node.wallets_path / "wallet.dat").unlink()
639 (self.master_node.wallets_path / "wallet.dat").unlink(missing_ok=True)
640 (self.master_node.wallets_path / "wallet.dat-journal").unlink(missing_ok=True)
641 shutil.rmtree(self.master_node.wallets_path / "default_wallet_watchonly", ignore_errors=True)
642 shutil.rmtree(self.master_node.wallets_path / "default_wallet_solvables", ignore_errors=True)
643 backup_file.unlink()
644
645 def clear_wallet(self, path, is_sqlite, rmdir_fail_ok=False):
646 Path(path / "wallet.dat").unlink()
647 if is_sqlite:
648 Path(path / "wallet.dat-journal").unlink(missing_ok=True)
649 else:
650 if (path / "database").exists():
651 shutil.rmtree(path / "database")
652 Path(path / "db.log").unlink(missing_ok=True)
653 Path(path / ".walletlock").unlink(missing_ok=True)
654 try:
655 path.rmdir()
656 except Exception:
657 if not rmdir_fail_ok:
658 raise
659
660 def test_default_wallet(self):
661 self.log.info("Test migration of the wallet named as the empty string")
662 wallet = self.create_legacy_wallet("")
663
664 # Set time to verify backup existence later
665 curr_time = int(time.time())
666 self.master_node.setmocktime(curr_time)
667
668 res, wallet = self.migrate_and_get_rpc("")
669 self.master_node.setmocktime(0)
670 info = wallet.getwalletinfo()
671 assert_equal(info["descriptors"], True)
672 assert_equal(info["format"], "sqlite")
673
674 walletdir_list = wallet.listwalletdir()
675 assert {"name": info["walletname"]} in walletdir_list["wallets"]
676
677 # Check backup existence and its non-empty wallet filename
678 backup_filename = f"default_wallet_{curr_time}.legacy.bak"
679 backup_path = self.master_node.wallets_path / backup_filename
680 assert backup_path.exists()
681 assert_equal(str(backup_path), res['backup_path'])
682 assert {"name": backup_filename} not in walletdir_list["wallets"]
683
684 self.master_node.setmocktime(0)
685
686 wallet.unloadwallet()
687 self.clear_default_wallet(backup_file=Path(res["backup_path"]))
688
689 def test_default_wallet_watch_only(self):
690 self.log.info("Test unnamed (default) watch-only wallet migration")
691 master_wallet = self.master_node.get_wallet_rpc(self.default_wallet_name)
692 wallet = self.create_legacy_wallet("", blank=True)
693 wallet.importaddress(master_wallet.getnewaddress(address_type="legacy"))
694
695 res, wallet = self.migrate_and_get_rpc("")
696
697 info = wallet.getwalletinfo()
698 assert_equal(info["descriptors"], True)
699 assert_equal(info["format"], "sqlite")
700 assert_equal(info["private_keys_enabled"], False)
701 assert_equal(info["walletname"], "default_wallet_watchonly")
702
703 # Check the default wallet is not available anymore
704 assert not (self.master_node.wallets_path / "wallet.dat").exists()
705
706 wallet.unloadwallet()
707 self.clear_default_wallet(backup_file=Path(res["backup_path"]))
708
709 def test_migration_failure(self, wallet_name, fail=True):
710 is_default = wallet_name == ""
711 wallet_pretty_name = "unnamed (default)" if is_default else f'"{wallet_name}"'
712 self.log.info(f"Test {'failure' if fail else 'success'} during migration of wallet named: {wallet_pretty_name}")
713 # Preface, set up legacy wallet and unload it
714 master_wallet = self.master_node.get_wallet_rpc(self.default_wallet_name)
715 wallet = self.create_legacy_wallet(wallet_name)
716 wallet.importaddress(master_wallet.getnewaddress(address_type="legacy"))
717 wallet.unloadwallet()
718
719 migrating_file_to_dir = wallet_name.endswith('.dat')
720 if os.path.isabs(wallet_name):
721 assert not migrating_file_to_dir
722 old_path = master_path = Path(wallet_name)
723 else:
724 old_path = self.old_node.wallets_path / wallet_name
725 master_path = self.master_node.wallets_path / wallet_name
726 if migrating_file_to_dir:
727 assert not master_path.exists()
728 shutil.copyfile(old_path / "wallet.dat", master_path)
729 else:
730 os.makedirs(master_path, exist_ok=True)
731 shutil.copyfile(old_path / "wallet.dat", master_path / "wallet.dat")
732
733 # This will be the watch-only directory the migration tries to create,
734 # we make migration fail by placing a wallet.dat file there.
735 wo_prefix = wallet_name or "default_wallet"
736 # wo_prefix might have path characters in it, this corresponds with
737 # DoMigration().
738 wo_dirname = f"{wo_prefix}_watchonly"
739 watch_only_dir = self.master_node.wallets_path / wo_dirname
740 if fail:
741 os.mkdir(watch_only_dir)
742 shutil.copyfile(old_path / "wallet.dat", watch_only_dir / "wallet.dat")
743
744 # Make a file in the wallets dir that must still exist after migration
745 survive_path = self.master_node.wallets_path / "survive"
746 open(survive_path, "wb").close()
747 assert survive_path.exists()
748 if migrating_file_to_dir:
749 survive2_path = master_path.parent / "survive"
750 else:
751 survive2_path = master_path / "survive"
752 open(survive2_path, "wb").close()
753 assert survive2_path.exists()
754
755 mocked_time = int(time.time())
756 self.master_node.setmocktime(mocked_time)
757 if fail:
758 assert_raises_rpc_error(-4, "Failed to create database", self.master_node.migratewallet, wallet_name)
759 else:
760 self.master_node.migratewallet(wallet_name)
761 self.master_node.setmocktime(0)
762
763 # Verify the /wallets/ path exists.
764 assert self.master_node.wallets_path.exists()
765
766 # Verify survive is still there
767 assert survive_path.exists()
768 assert survive2_path.exists()
769 # Verify both wallet paths exist.
770 assert Path(old_path / "wallet.dat").exists()
771 assert Path(master_path / "wallet.dat").exists()
772
773 backup_prefix = "default_wallet" if is_default else os.path.basename(os.path.abspath(master_path))
774 backup_path = master_path / f"{backup_prefix}_{mocked_time}.legacy.bak"
775 assert backup_path.exists()
776
777 if fail:
778 self.assert_is_bdb(wallet_name)
779 else:
780 wallet = self.master_node.get_wallet_rpc(wallet_name)
781 info = wallet.getwalletinfo()
782 assert_equal(info["descriptors"], True)
783 self.assert_is_sqlite(wallet_name)
784 self.assert_is_sqlite(wo_dirname)
785
786 self.master_node.unloadwallet(wallet_name)
787 self.master_node.unloadwallet(wo_dirname)
788
789 # Cleanup
790 if is_default:
791 self.clear_default_wallet(backup_path)
792 else:
793 backup_path.unlink()
794 self.clear_wallet(watch_only_dir, is_sqlite=not fail)
795 self.clear_wallet(master_path, is_sqlite=not fail, rmdir_fail_ok=True)
796 if old_path != master_path:
797 self.clear_wallet(old_path, is_sqlite=False, rmdir_fail_ok=True)
798
799 def test_direct_file(self):
800 self.log.info("Test migration of a wallet that is not in a wallet directory")
801 wallet = self.create_legacy_wallet("plainfile")
802 wallet.unloadwallet()
803
804 shutil.copyfile(
805 self.old_node.wallets_path / "plainfile" / "wallet.dat" ,
806 self.master_node.wallets_path / "plainfile"
807 )
808 assert (self.master_node.wallets_path / "plainfile").is_file()
809
810 mocked_time = int(time.time())
811 self.master_node.setmocktime(mocked_time)
812 migrate_res = self.master_node.migratewallet("plainfile")
813 assert_equal(f"plainfile_{mocked_time}.legacy.bak", os.path.basename(migrate_res["backup_path"]))
814 wallet = self.master_node.get_wallet_rpc("plainfile")
815 info = wallet.getwalletinfo()
816 assert_equal(info["descriptors"], True)
817 assert_equal(info["format"], "sqlite")
818
819 assert (self.master_node.wallets_path / "plainfile").is_dir()
820 assert (self.master_node.wallets_path / "plainfile" / "wallet.dat").is_file()
821
822 def test_addressbook(self):
823 df_wallet = self.master_node.get_wallet_rpc(self.default_wallet_name)
824
825 self.log.info("Test migration of address book data")
826 wallet = self.create_legacy_wallet("legacy_addrbook")
827 df_wallet.sendtoaddress(wallet.getnewaddress(), 3)
828
829 # Import watch-only script to create a watch-only wallet after migration
830 watch_addr = df_wallet.getnewaddress()
831 wallet.importaddress(watch_addr)
832 df_wallet.sendtoaddress(watch_addr, 2)
833
834 # Import solvable script
835 multi_addr1 = wallet.getnewaddress()
836 multi_addr2 = wallet.getnewaddress()
837 multi_addr3 = df_wallet.getnewaddress()
838 wallet.importpubkey(df_wallet.getaddressinfo(multi_addr3)["pubkey"])
839 ms_addr_info = wallet.addmultisigaddress(2, [multi_addr1, multi_addr2, multi_addr3])
840
841 self.generate(self.master_node, 1)
842
843 # Test vectors
844 addr_external = {
845 "addr": df_wallet.getnewaddress(),
846 "is_mine": False,
847 "is_change": False,
848 "label": ""
849 }
850 addr_external_with_label = {
851 "addr": df_wallet.getnewaddress(),
852 "is_mine": False,
853 "is_change": False,
854 "label": "external"
855 }
856 addr_internal = {
857 "addr": wallet.getnewaddress(),
858 "is_mine": True,
859 "is_change": False,
860 "label": ""
861 }
862 addr_internal_with_label = {
863 "addr": wallet.getnewaddress(),
864 "is_mine": True,
865 "is_change": False,
866 "label": "internal"
867 }
868 change_address = {
869 "addr": wallet.getrawchangeaddress(),
870 "is_mine": True,
871 "is_change": True,
872 "label": None
873 }
874 watch_only_addr = {
875 "addr": watch_addr,
876 "is_mine": False,
877 "is_change": False,
878 "label": "imported"
879 }
880 ms_addr = {
881 "addr": ms_addr_info['address'],
882 "is_mine": False,
883 "is_change": False,
884 "label": "multisig"
885 }
886
887 # To store the change address in the addressbook need to send coins to it
888 wallet.send(outputs=[{wallet.getnewaddress(): 2}], options={"change_address": change_address['addr']})
889 self.generate(self.master_node, 1)
890
891 # Util wrapper func for 'addr_info'
892 def check(info, node):
893 self.check_address(node, info['addr'], info['is_mine'], info['is_change'], info["label"])
894
895 # Pre-migration: set label and perform initial checks
896 for addr_info in [addr_external, addr_external_with_label, addr_internal, addr_internal_with_label, change_address, watch_only_addr, ms_addr]:
897 if not addr_info['is_change']:
898 wallet.setlabel(addr_info['addr'], addr_info["label"])
899 check(addr_info, wallet)
900
901 # Migrate wallet
902 info_migration, wallet = self.migrate_and_get_rpc("legacy_addrbook")
903 wallet_wo = self.master_node.get_wallet_rpc(info_migration["watchonly_name"])
904 wallet_solvables = self.master_node.get_wallet_rpc(info_migration["solvables_name"])
905
906 #########################
907 # Post migration checks #
908 #########################
909
910 # First check the main wallet
911 for addr_info in [addr_external, addr_external_with_label, addr_internal, addr_internal_with_label, change_address, ms_addr]:
912 check(addr_info, wallet)
913
914 # Watch-only wallet will contain the watch-only entry (with 'is_mine=True') and all external addresses ('send')
915 self.check_address(wallet_wo, watch_only_addr['addr'], is_mine=True, is_change=watch_only_addr['is_change'], label=watch_only_addr["label"])
916 for addr_info in [addr_external, addr_external_with_label, ms_addr]:
917 check(addr_info, wallet_wo)
918
919 # Solvables wallet will contain the multisig entry (with 'is_mine=True') and all external addresses ('send')
920 self.check_address(wallet_solvables, ms_addr['addr'], is_mine=True, is_change=ms_addr['is_change'], label=ms_addr["label"])
921 for addr_info in [addr_external, addr_external_with_label]:
922 check(addr_info, wallet_solvables)
923
924 ########################################################################################
925 # Now restart migrated wallets and verify that the addressbook entries are still there #
926 ########################################################################################
927
928 # First the main wallet
929 self.master_node.unloadwallet("legacy_addrbook")
930 self.master_node.loadwallet("legacy_addrbook")
931 for addr_info in [addr_external, addr_external_with_label, addr_internal, addr_internal_with_label, change_address, ms_addr]:
932 check(addr_info, wallet)
933
934 # Watch-only wallet
935 self.master_node.unloadwallet(info_migration["watchonly_name"])
936 self.master_node.loadwallet(info_migration["watchonly_name"])
937 self.check_address(wallet_wo, watch_only_addr['addr'], is_mine=True, is_change=watch_only_addr['is_change'], label=watch_only_addr["label"])
938 for addr_info in [addr_external, addr_external_with_label, ms_addr]:
939 check(addr_info, wallet_wo)
940
941 # Solvables wallet
942 self.master_node.unloadwallet(info_migration["solvables_name"])
943 self.master_node.loadwallet(info_migration["solvables_name"])
944 self.check_address(wallet_solvables, ms_addr['addr'], is_mine=True, is_change=ms_addr['is_change'], label=ms_addr["label"])
945 for addr_info in [addr_external, addr_external_with_label]:
946 check(addr_info, wallet_solvables)
947
948 def test_migrate_raw_p2sh(self):
949 self.log.info("Test migration of watch-only raw p2sh script")
950 df_wallet = self.master_node.get_wallet_rpc(self.default_wallet_name)
951 wallet = self.create_legacy_wallet("raw_p2sh")
952
953 def send_to_script(script, amount):
954 tx = CTransaction()
955 tx.vout.append(CTxOut(nValue=amount*COIN, scriptPubKey=script))
956
957 hex_tx = df_wallet.fundrawtransaction(tx.serialize().hex())['hex']
958 signed_tx = df_wallet.signrawtransactionwithwallet(hex_tx)
959 df_wallet.sendrawtransaction(signed_tx['hex'])
960 self.generate(self.master_node, 1)
961
962 # Craft sh(pkh(key)) script and send coins to it
963 pubkey = df_wallet.getaddressinfo(df_wallet.getnewaddress())["pubkey"]
964 script_pkh = key_to_p2pkh_script(pubkey)
965 script_sh_pkh = script_to_p2sh_script(script_pkh)
966 send_to_script(script=script_sh_pkh, amount=2)
967
968 # Import script and check balance
969 wallet.rpc.importaddress(address=script_pkh.hex(), label="raw_spk", rescan=True, p2sh=True)
970 assert_equal(wallet.getbalances()['watchonly']['trusted'], 2)
971
972 # Craft wsh(pkh(key)) and send coins to it
973 pubkey = df_wallet.getaddressinfo(df_wallet.getnewaddress())["pubkey"]
974 script_wsh_pkh = script_to_p2wsh_script(key_to_p2pkh_script(pubkey))
975 send_to_script(script=script_wsh_pkh, amount=3)
976
977 # Import script and check balance
978 wallet.rpc.importaddress(address=script_wsh_pkh.hex(), label="raw_spk2", rescan=True, p2sh=False)
979 assert_equal(wallet.getbalances()['watchonly']['trusted'], 5)
980
981 # Import sh(pkh()) script, by using importaddress(), with the p2sh flag enabled.
982 # This will wrap the script under another sh level, which is invalid!, and store it inside the wallet.
983 # The migration process must skip the invalid scripts and the addressbook records linked to them.
984 # They are not being watched by the current wallet, nor should be watched by the migrated one.
985 label_sh_pkh = "raw_sh_pkh"
986 script_pkh = key_to_p2pkh_script(df_wallet.getaddressinfo(df_wallet.getnewaddress())["pubkey"])
987 script_sh_pkh = script_to_p2sh_script(script_pkh)
988 addy_script_sh_pkh = script_to_p2sh(script_pkh) # valid script address
989 addy_script_double_sh_pkh = script_to_p2sh(script_sh_pkh) # invalid script address
990
991 # Note: 'importaddress()' will add two scripts, a valid one sh(pkh()) and an invalid one 'sh(sh(pkh()))'.
992 # Both of them will be stored with the same addressbook label. And only the latter one should
993 # be discarded during migration. The first one must be migrated.
994 wallet.rpc.importaddress(address=script_sh_pkh.hex(), label=label_sh_pkh, rescan=False, p2sh=True)
995
996 # Migrate wallet and re-check balance
997 info_migration, wallet = self.migrate_and_get_rpc("raw_p2sh")
998 wallet_wo = self.master_node.get_wallet_rpc(info_migration["watchonly_name"])
999
1000 # Watch-only balance is under "mine".
1001 assert_equal(wallet_wo.getbalances()['mine']['trusted'], 5)
1002 # The watch-only scripts are no longer part of the main wallet
1003 assert_equal(wallet.getbalances()['mine']['trusted'], 0)
1004
1005 # The invalid sh(sh(pk())) script label must not be part of the main wallet anymore
1006 assert label_sh_pkh not in wallet.listlabels()
1007 # But, the standard sh(pkh()) script should be part of the watch-only wallet.
1008 addrs_by_label = wallet_wo.getaddressesbylabel(label_sh_pkh)
1009 assert addy_script_sh_pkh in addrs_by_label
1010 assert addy_script_double_sh_pkh not in addrs_by_label
1011
1012 # Also, the watch-only wallet should have the descriptor for the standard sh(pkh())
1013 desc = descsum_create(f"addr({addy_script_sh_pkh})")
1014 assert next(it['desc'] for it in wallet_wo.listdescriptors()['descriptors'] if it['desc'] == desc)
1015 # And doesn't have a descriptor for the invalid one
1016 desc_invalid = descsum_create(f"addr({addy_script_double_sh_pkh})")
1017 assert_equal(next((it['desc'] for it in wallet_wo.listdescriptors()['descriptors'] if it['desc'] == desc_invalid), None), None)
1018
1019 # Just in case, also verify wallet restart
1020 self.master_node.unloadwallet(info_migration["watchonly_name"])
1021 self.master_node.loadwallet(info_migration["watchonly_name"])
1022 assert_equal(wallet_wo.getbalances()['mine']['trusted'], 5)
1023
1024 def test_conflict_txs(self):
1025 self.log.info("Test migration when wallet contains conflicting transactions")
1026 def_wallet = self.master_node.get_wallet_rpc(self.default_wallet_name)
1027
1028 wallet = self.create_legacy_wallet("conflicts")
1029 def_wallet.sendtoaddress(wallet.getnewaddress(), 10)
1030 self.generate(self.master_node, 1)
1031
1032 # parent tx
1033 parent_txid = wallet.sendtoaddress(wallet.getnewaddress(), 9)
1034 parent_txid_bytes = bytes.fromhex(parent_txid)[::-1]
1035 conflict_utxo = wallet.gettransaction(txid=parent_txid, verbose=True)["decoded"]["vin"][0]
1036
1037 # The specific assertion in MarkConflicted being tested requires that the parent tx is already loaded
1038 # by the time the child tx is loaded. Since transactions end up being loaded in txid order due to how
1039 # sqlite stores things, we can just grind the child tx until it has a txid that is greater than the parent's.
1040 locktime = 500000000 # Use locktime as nonce, starting at unix timestamp minimum
1041 addr = wallet.getnewaddress()
1042 while True:
1043 child_send_res = wallet.send(outputs=[{addr: 8}], options={"add_to_wallet": False, "locktime": locktime})
1044 child_txid = child_send_res["txid"]
1045 child_txid_bytes = bytes.fromhex(child_txid)[::-1]
1046 if (child_txid_bytes > parent_txid_bytes):
1047 wallet.sendrawtransaction(child_send_res["hex"])
1048 break
1049 locktime += 1
1050
1051 # conflict with parent
1052 conflict_unsigned = self.master_node.createrawtransaction(inputs=[conflict_utxo], outputs=[{wallet.getnewaddress(): 9.9999}])
1053 conflict_signed = wallet.signrawtransactionwithwallet(conflict_unsigned)["hex"]
1054 conflict_txid = self.master_node.sendrawtransaction(conflict_signed)
1055 self.generate(self.master_node, 1)
1056 assert_equal(wallet.gettransaction(txid=parent_txid)["confirmations"], -1)
1057 assert_equal(wallet.gettransaction(txid=child_txid)["confirmations"], -1)
1058 assert_equal(wallet.gettransaction(txid=conflict_txid)["confirmations"], 1)
1059
1060 _, wallet = self.migrate_and_get_rpc("conflicts")
1061 assert_equal(wallet.gettransaction(txid=parent_txid)["confirmations"], -1)
1062 assert_equal(wallet.gettransaction(txid=child_txid)["confirmations"], -1)
1063 assert_equal(wallet.gettransaction(txid=conflict_txid)["confirmations"], 1)
1064
1065 wallet.unloadwallet()
1066
1067 def test_hybrid_pubkey(self):
1068 self.log.info("Test migration when wallet contains a hybrid pubkey")
1069
1070 wallet = self.create_legacy_wallet("hybrid_keys")
1071
1072 # Get the hybrid pubkey for one of the keys in the wallet
1073 normal_pubkey = wallet.getaddressinfo(wallet.getnewaddress())["pubkey"]
1074 first_byte = bytes.fromhex(normal_pubkey)[0] + 4 # Get the hybrid pubkey first byte
1075 parsed_pubkey = ECPubKey()
1076 parsed_pubkey.set(bytes.fromhex(normal_pubkey))
1077 parsed_pubkey.compressed = False
1078 hybrid_pubkey_bytes = bytearray(parsed_pubkey.get_bytes())
1079 hybrid_pubkey_bytes[0] = first_byte # Make it hybrid
1080 hybrid_pubkey = hybrid_pubkey_bytes.hex()
1081
1082 # Import the hybrid pubkey
1083 wallet.importpubkey(hybrid_pubkey)
1084 p2pkh_addr = key_to_p2pkh(hybrid_pubkey)
1085 p2pkh_addr_info = wallet.getaddressinfo(p2pkh_addr)
1086 assert_equal(p2pkh_addr_info["iswatchonly"], True)
1087 assert_equal(p2pkh_addr_info["ismine"], False) # Things involving hybrid pubkeys are not spendable
1088
1089 # Also import the p2wpkh for the pubkey to make sure we don't migrate it
1090 p2wpkh_addr = key_to_p2wpkh(hybrid_pubkey)
1091 wallet.importaddress(p2wpkh_addr)
1092
1093 migrate_info, wallet = self.migrate_and_get_rpc("hybrid_keys")
1094
1095 # Both addresses should only appear in the watchonly wallet
1096 p2pkh_addr_info = wallet.getaddressinfo(p2pkh_addr)
1097 assert_equal(p2pkh_addr_info["iswatchonly"], False)
1098 assert_equal(p2pkh_addr_info["ismine"], False)
1099 p2wpkh_addr_info = wallet.getaddressinfo(p2wpkh_addr)
1100 assert_equal(p2wpkh_addr_info["iswatchonly"], False)
1101 assert_equal(p2wpkh_addr_info["ismine"], False)
1102
1103 watchonly_wallet = self.master_node.get_wallet_rpc(migrate_info["watchonly_name"])
1104 watchonly_p2pkh_addr_info = watchonly_wallet.getaddressinfo(p2pkh_addr)
1105 assert_equal(watchonly_p2pkh_addr_info["iswatchonly"], False)
1106 assert_equal(watchonly_p2pkh_addr_info["ismine"], True)
1107 watchonly_p2wpkh_addr_info = watchonly_wallet.getaddressinfo(p2wpkh_addr)
1108 assert_equal(watchonly_p2wpkh_addr_info["iswatchonly"], False)
1109 assert_equal(watchonly_p2wpkh_addr_info["ismine"], True)
1110
1111 # There should only be raw or addr descriptors
1112 for desc in watchonly_wallet.listdescriptors()["descriptors"]:
1113 if desc["desc"].startswith("raw(") or desc["desc"].startswith("addr("):
1114 continue
1115 assert False, "Hybrid pubkey watchonly wallet has more than just raw() and addr()"
1116
1117 wallet.unloadwallet()
1118
1119 def test_failed_migration_cleanup(self):
1120 self.log.info("Test that a failed migration is cleaned up")
1121 wallet = self.create_legacy_wallet("failed")
1122
1123 # Make a copy of the wallet with the solvables wallet name so that we are unable
1124 # to create the solvables wallet when migrating, thus failing to migrate
1125 wallet.unloadwallet()
1126 solvables_path = self.master_node.wallets_path / "failed_solvables"
1127 shutil.copytree(self.old_node.wallets_path / "failed", solvables_path)
1128 original_shasum = sha256sum_file(solvables_path / "wallet.dat")
1129
1130 self.old_node.loadwallet("failed")
1131
1132 # Add a multisig so that a solvables wallet is created
1133 wallet.addmultisigaddress(2, [wallet.getnewaddress(), get_generate_key().pubkey])
1134 wallet.importaddress(get_generate_key().p2pkh_addr)
1135
1136 self.old_node.unloadwallet("failed")
1137 shutil.copytree(self.old_node.wallets_path / "failed", self.master_node.wallets_path / "failed")
1138 assert_raises_rpc_error(-4, "Failed to create database", self.master_node.migratewallet, "failed")
1139
1140 assert all(wallet not in self.master_node.listwallets() for wallet in ["failed", "failed_watchonly", "failed_solvables"])
1141
1142 assert not (self.master_node.wallets_path / "failed_watchonly").exists()
1143 # Since the file in failed_solvables is one that we put there, migration shouldn't touch it
1144 assert solvables_path.exists()
1145 new_shasum = sha256sum_file(solvables_path / "wallet.dat")
1146 assert_equal(original_shasum, new_shasum)
1147
1148 # Check the wallet we tried to migrate is still BDB
1149 self.assert_is_bdb("failed")
1150
1151 ####################################################
1152 # Perform the same test with a loaded legacy wallet.
1153 # The wallet should remain loaded after the failure.
1154 #
1155 # This applies only when BDB is enabled, as the user
1156 # cannot interact with the legacy wallet database
1157 # without BDB support.
1158 if self.is_bdb_compiled() is not None:
1159 # Advance time to generate a different backup name
1160 self.master_node.setmocktime(self.master_node.getblockheader(self.master_node.getbestblockhash())['time'] + 100)
1161 assert "failed" not in self.master_node.listwallets()
1162 self.master_node.loadwallet("failed")
1163 assert_raises_rpc_error(-4, "Failed to create database", self.master_node.migratewallet, "failed")
1164 wallets = self.master_node.listwallets()
1165 assert "failed" in wallets and all(wallet not in wallets for wallet in ["failed_watchonly", "failed_solvables"])
1166
1167 def test_failed_migration_cleanup_relative_path(self):
1168 self.log.info("Test that a failed migration with a relative path is cleaned up")
1169
1170 # Get the nearest common path of both nodes' wallet paths.
1171 common_parent = os.path.commonpath([self.master_node.wallets_path, self.old_node.wallets_path])
1172
1173 # This test assumes that the relative path from each wallet directory to the common path is identical.
1174 assert_equal(os.path.relpath(common_parent, start=self.master_node.wallets_path), os.path.relpath(common_parent, start=self.old_node.wallets_path))
1175
1176 wallet_name = "relativefailure"
1177 absolute_path = os.path.abspath(os.path.join(common_parent, wallet_name))
1178 relative_name = os.path.relpath(absolute_path, start=self.master_node.wallets_path)
1179
1180 wallet = self.create_legacy_wallet(relative_name)
1181
1182 # Make a copy of the wallet with the solvables wallet name so that we are unable
1183 # to create the solvables wallet when migrating, thus failing to migrate
1184 wallet.unloadwallet()
1185 solvables_path = os.path.join(common_parent, f"{wallet_name}_solvables")
1186
1187 shutil.copytree(self.old_node.wallets_path / relative_name, solvables_path)
1188 original_shasum = sha256sum_file(os.path.join(solvables_path, "wallet.dat"))
1189
1190 self.old_node.loadwallet(relative_name)
1191
1192 # Add a multisig so that a solvables wallet is created
1193 wallet.addmultisigaddress(2, [wallet.getnewaddress(), get_generate_key().pubkey])
1194 wallet.importaddress(get_generate_key().p2pkh_addr)
1195
1196 self.old_node.unloadwallet(relative_name)
1197 assert_raises_rpc_error(-4, "Failed to create database", self.master_node.migratewallet, relative_name)
1198
1199 assert all(wallet not in self.master_node.listwallets() for wallet in [f"{wallet_name}", f"{wallet_name}_watchonly", f"{wallet_name}_solvables"])
1200
1201 assert not (self.master_node.wallets_path / f"{wallet_name}_watchonly").exists()
1202 # Since the file in failed_solvables is one that we put there, migration shouldn't touch it
1203 assert os.path.exists(solvables_path)
1204 new_shasum = sha256sum_file(os.path.join(solvables_path , "wallet.dat"))
1205 assert_equal(original_shasum, new_shasum)
1206
1207 # Check the wallet we tried to migrate is still BDB
1208 datfile = os.path.join(absolute_path, "wallet.dat")
1209 with open(datfile, "rb") as f:
1210 data = f.read(16)
1211 _, _, magic = struct.unpack("QII", data)
1212 assert_equal(magic, BTREE_MAGIC)
1213
1214 def test_blank(self):
1215 self.log.info("Test that a blank wallet is migrated")
1216 wallet = self.create_legacy_wallet("blank", blank=True)
1217 assert_equal(wallet.getwalletinfo()["blank"], True)
1218 _, wallet = self.migrate_and_get_rpc("blank")
1219 assert_equal(wallet.getwalletinfo()["blank"], True)
1220
1221 def test_avoidreuse(self):
1222 self.log.info("Test that avoidreuse persists after migration")
1223 def_wallet = self.master_node.get_wallet_rpc(self.default_wallet_name)
1224
1225 wallet = self.create_legacy_wallet("avoidreuse")
1226 wallet.setwalletflag("avoid_reuse", True)
1227
1228 # Import a pubkey to the test wallet and send some funds to it
1229 reused_imported_addr = def_wallet.getnewaddress()
1230 wallet.importpubkey(def_wallet.getaddressinfo(reused_imported_addr)["pubkey"])
1231 imported_utxos = self.create_outpoints(def_wallet, outputs=[{reused_imported_addr: 2}])
1232 def_wallet.lockunspent(False, imported_utxos)
1233
1234 # Send funds to the test wallet
1235 reused_addr = wallet.getnewaddress()
1236 def_wallet.sendtoaddress(reused_addr, 2)
1237
1238 self.generate(self.master_node, 1)
1239
1240 # Send funds from the test wallet with both its own and the imported
1241 wallet.sendall([def_wallet.getnewaddress()])
1242 def_wallet.sendall(recipients=[def_wallet.getnewaddress()], inputs=imported_utxos)
1243 self.generate(self.master_node, 1)
1244 balances = wallet.getbalances()
1245 assert_equal(balances["mine"]["trusted"], 0)
1246 assert_equal(balances["watchonly"]["trusted"], 0)
1247
1248 # Reuse the addresses
1249 def_wallet.sendtoaddress(reused_addr, 1)
1250 def_wallet.sendtoaddress(reused_imported_addr, 1)
1251 self.generate(self.master_node, 1)
1252 balances = wallet.getbalances()
1253 assert_equal(balances["mine"]["used"], 1)
1254 # Reused watchonly will not show up in balances
1255 assert_equal(balances["watchonly"]["trusted"], 0)
1256 assert_equal(balances["watchonly"]["untrusted_pending"], 0)
1257 assert_equal(balances["watchonly"]["immature"], 0)
1258
1259 utxos = wallet.listunspent()
1260 assert_equal(len(utxos), 2)
1261 for utxo in utxos:
1262 assert_equal(utxo["reused"], True)
1263
1264 # Migrate
1265 _, wallet = self.migrate_and_get_rpc("avoidreuse")
1266 watchonly_wallet = self.master_node.get_wallet_rpc("avoidreuse_watchonly")
1267
1268 # One utxo in each wallet, marked used
1269 utxos = wallet.listunspent()
1270 assert_equal(len(utxos), 1)
1271 assert_equal(utxos[0]["reused"], True)
1272 watchonly_utxos = watchonly_wallet.listunspent()
1273 assert_equal(len(watchonly_utxos), 1)
1274 assert_equal(watchonly_utxos[0]["reused"], True)
1275
1276 def test_preserve_tx_extra_info(self):
1277 self.log.info("Test that tx extra data is preserved after migration")
1278 def_wallet = self.master_node.get_wallet_rpc(self.default_wallet_name)
1279
1280 # Create and fund wallet
1281 wallet = self.create_legacy_wallet("persist_comments")
1282 def_wallet.sendtoaddress(wallet.getnewaddress(), 2)
1283
1284 self.generate(self.master_node, 6)
1285
1286 # Create tx and bump it to store 'replaced_by_txid' and 'replaces_txid' data within the transactions.
1287 # Additionally, store an extra comment within the original tx.
1288 extra_comment = "don't discard me"
1289 original_tx_id = wallet.sendtoaddress(address=wallet.getnewaddress(), amount=1, comment=extra_comment)
1290 bumped_tx = wallet.bumpfee(txid=original_tx_id)
1291
1292 def check_comments():
1293 for record in wallet.listtransactions():
1294 if record["txid"] == original_tx_id:
1295 assert_equal(record["replaced_by_txid"], bumped_tx["txid"])
1296 assert_equal(record['comment'], extra_comment)
1297 elif record["txid"] == bumped_tx["txid"]:
1298 assert_equal(record["replaces_txid"], original_tx_id)
1299
1300 # Pre-migration verification
1301 check_comments()
1302 # Migrate
1303 _, wallet = self.migrate_and_get_rpc("persist_comments")
1304 # Post-migration verification
1305 check_comments()
1306
1307 wallet.unloadwallet()
1308
1309 def test_migrate_simple_watch_only(self):
1310 self.log.info("Test migrating a watch-only p2pk script")
1311 wallet = self.create_legacy_wallet("bare_p2pk", blank=True)
1312 _, pubkey = generate_keypair()
1313 p2pk_script = key_to_p2pk_script(pubkey)
1314 wallet.importaddress(address=p2pk_script.hex())
1315 # Migrate wallet in the latest node
1316 res, _ = self.migrate_and_get_rpc("bare_p2pk")
1317 wo_wallet = self.master_node.get_wallet_rpc(res['wallet_name'])
1318 assert_equal(wo_wallet.listdescriptors()['descriptors'][0]['desc'], descsum_create(f'pk({pubkey.hex()})'))
1319 assert_equal(wo_wallet.getwalletinfo()["private_keys_enabled"], False)
1320
1321 # Ensure that migrating a wallet with watch-only scripts does not create a spendable wallet.
1322 assert_equal('bare_p2pk_watchonly', res['wallet_name'])
1323 assert "bare_p2pk" not in self.master_node.listwallets()
1324 assert "bare_p2pk" not in [w["name"] for w in self.master_node.listwalletdir()["wallets"]]
1325
1326 wo_wallet.unloadwallet()
1327
1328 def test_manual_keys_import(self):
1329 self.log.info("Test migrating standalone private keys")
1330 wallet = self.create_legacy_wallet("import_privkeys", blank=True)
1331 privkey, pubkey = generate_keypair(wif=True)
1332 wallet.importprivkey(privkey=privkey, label="hi", rescan=False)
1333
1334 # Migrate and verify
1335 res, wallet = self.migrate_and_get_rpc("import_privkeys")
1336
1337 # There should be descriptors containing the imported key for: pk(), pkh(), sh(wpkh()), wpkh()
1338 key_origin = hash160(pubkey)[:4].hex()
1339 pubkey_hex = pubkey.hex()
1340 combo_desc = descsum_create(f"combo([{key_origin}]{pubkey_hex})")
1341
1342 # Verify all expected descriptors were migrated
1343 migrated_desc = [item['desc'] for item in wallet.listdescriptors()['descriptors'] if pubkey.hex() in item['desc']]
1344 assert_equal([combo_desc], migrated_desc)
1345 wallet.unloadwallet()
1346
1347 ######################################################
1348 self.log.info("Test migrating standalone public keys")
1349 wallet = self.create_legacy_wallet("import_pubkeys", blank=True)
1350 wallet.importpubkey(pubkey=pubkey_hex, rescan=False)
1351
1352 res, _ = self.migrate_and_get_rpc("import_pubkeys")
1353
1354 # Same as before, there should be descriptors in the watch-only wallet for the imported pubkey
1355 wo_wallet = self.nodes[0].get_wallet_rpc(res['wallet_name'])
1356 # Assert this is a watch-only wallet
1357 assert_equal(wo_wallet.getwalletinfo()["private_keys_enabled"], False)
1358 # As we imported the pubkey only, there will be no key origin in the following descriptors
1359 pk_desc = descsum_create(f'pk({pubkey_hex})')
1360 pkh_desc = descsum_create(f'pkh({pubkey_hex})')
1361 sh_wpkh_desc = descsum_create(f'sh(wpkh({pubkey_hex}))')
1362 wpkh_desc = descsum_create(f'wpkh({pubkey_hex})')
1363 expected_descs = [pk_desc, pkh_desc, sh_wpkh_desc, wpkh_desc]
1364
1365 # Verify all expected descriptors were migrated
1366 migrated_desc = [item['desc'] for item in wo_wallet.listdescriptors()['descriptors']]
1367 assert_equal(expected_descs, migrated_desc)
1368 # Ensure that migrating a wallet with watch-only scripts does not create a spendable wallet.
1369 assert_equal('import_pubkeys_watchonly', res['wallet_name'])
1370 assert "import_pubkeys" not in self.master_node.listwallets()
1371 assert "import_pubkeys" not in [w["name"] for w in self.master_node.listwalletdir()["wallets"]]
1372 wo_wallet.unloadwallet()
1373
1374 def test_p2wsh(self):
1375 self.log.info("Test that non-multisig P2WSH output scripts are migrated")
1376 def_wallet = self.master_node.get_wallet_rpc(self.default_wallet_name)
1377
1378 wallet = self.create_legacy_wallet("p2wsh")
1379
1380 # Craft wsh(pkh(key))
1381 pubkey = wallet.getaddressinfo(wallet.getnewaddress())["pubkey"]
1382 pkh_script = key_to_p2pkh_script(pubkey).hex()
1383 wsh_pkh_script = script_to_p2wsh_script(pkh_script).hex()
1384 wsh_pkh_addr = script_to_p2wsh(pkh_script)
1385
1386 # Legacy single key scripts (i.e. pkh(key) and pk(key)) are not inserted into mapScripts
1387 # automatically, they need to be imported directly if we want to receive to P2WSH (or P2SH)
1388 # wrappings of such scripts.
1389 wallet.importaddress(address=pkh_script, p2sh=False)
1390 wallet.importaddress(address=wsh_pkh_script, p2sh=False)
1391
1392 def_wallet.sendtoaddress(wsh_pkh_addr, 5)
1393 self.generate(self.nodes[0], 6)
1394 assert_equal(wallet.getbalances()['mine']['trusted'], 5)
1395
1396 _, wallet = self.migrate_and_get_rpc("p2wsh")
1397
1398 assert_equal(wallet.getbalances()['mine']['trusted'], 5)
1399 addr_info = wallet.getaddressinfo(wsh_pkh_addr)
1400 assert_equal(addr_info["ismine"], True)
1401 assert_equal(addr_info["iswatchonly"], False)
1402 assert_equal(addr_info["solvable"], True)
1403
1404 wallet.unloadwallet()
1405
1406 def test_disallowed_p2wsh(self):
1407 self.log.info("Test that P2WSH output scripts with invalid witnessScripts are not migrated and do not cause migration failure")
1408 def_wallet = self.master_node.get_wallet_rpc(self.default_wallet_name)
1409
1410 wallet = self.create_legacy_wallet("invalid_p2wsh")
1411
1412 invalid_addrs = []
1413
1414 # For a P2WSH output script stored in the legacy wallet's mapScripts, both the native P2WSH
1415 # and the P2SH-P2WSH are detected by IsMine. We need to verify that descriptors for both
1416 # output scripts are added to the resulting descriptor wallet.
1417 # However, this cannot be done using a multisig as wallet migration treats multisigs specially.
1418 # Instead, this is tested by importing a wsh(pkh()) script. But importing this directly will
1419 # insert the wsh() into setWatchOnly which means that the setWatchOnly migration ends up handling
1420 # this case, which we do not want.
1421 # In order to get the wsh(pkh()) into only mapScripts and not setWatchOnly, we need to utilize
1422 # importmulti and wrap the wsh(pkh()) inside of a sh(). This will insert the sh(wsh(pkh())) into
1423 # setWatchOnly but not the wsh(pkh()).
1424 # Furthermore, migration should not migrate the wsh(pkh()) if the key is uncompressed.
1425 comp_wif, comp_pubkey = generate_keypair(compressed=True, wif=True)
1426 comp_pkh_script = key_to_p2pkh_script(comp_pubkey).hex()
1427 comp_wsh_pkh_script = script_to_p2wsh_script(comp_pkh_script).hex()
1428 comp_sh_wsh_pkh_script = script_to_p2sh_script(comp_wsh_pkh_script).hex()
1429 comp_wsh_pkh_addr = script_to_p2wsh(comp_pkh_script)
1430
1431 uncomp_wif, uncomp_pubkey = generate_keypair(compressed=False, wif=True)
1432 uncomp_pkh_script = key_to_p2pkh_script(uncomp_pubkey).hex()
1433 uncomp_wsh_pkh_script = script_to_p2wsh_script(uncomp_pkh_script).hex()
1434 uncomp_sh_wsh_pkh_script = script_to_p2sh_script(uncomp_wsh_pkh_script).hex()
1435 uncomp_wsh_pkh_addr = script_to_p2wsh(uncomp_pkh_script)
1436 invalid_addrs.append(uncomp_wsh_pkh_addr)
1437
1438 import_res = wallet.importmulti([
1439 {
1440 "scriptPubKey": comp_sh_wsh_pkh_script,
1441 "timestamp": "now",
1442 "redeemscript": comp_wsh_pkh_script,
1443 "witnessscript": comp_pkh_script,
1444 "keys": [
1445 comp_wif,
1446 ],
1447 },
1448 {
1449 "scriptPubKey": uncomp_sh_wsh_pkh_script,
1450 "timestamp": "now",
1451 "redeemscript": uncomp_wsh_pkh_script,
1452 "witnessscript": uncomp_pkh_script,
1453 "keys": [
1454 uncomp_wif,
1455 ],
1456 },
1457 ])
1458 assert_equal(import_res[0]["success"], True)
1459 assert_equal(import_res[1]["success"], True)
1460
1461 # Create a wsh(sh(pkh())) - P2SH inside of P2WSH is invalid
1462 comp_sh_pkh_script = script_to_p2sh_script(comp_pkh_script).hex()
1463 wsh_sh_pkh_script = script_to_p2wsh_script(comp_sh_pkh_script).hex()
1464 wsh_sh_pkh_addr = script_to_p2wsh(comp_sh_pkh_script)
1465 invalid_addrs.append(wsh_sh_pkh_addr)
1466
1467 # Import wsh(sh(pkh()))
1468 wallet.importaddress(address=comp_sh_pkh_script, p2sh=False)
1469 wallet.importaddress(address=wsh_sh_pkh_script, p2sh=False)
1470
1471 # Create a wsh(wsh(pkh())) - P2WSH inside of P2WSH is invalid
1472 wsh_wsh_pkh_script = script_to_p2wsh_script(comp_wsh_pkh_script).hex()
1473 wsh_wsh_pkh_addr = script_to_p2wsh(comp_wsh_pkh_script)
1474 invalid_addrs.append(wsh_wsh_pkh_addr)
1475
1476 # Import wsh(wsh(pkh()))
1477 wallet.importaddress(address=wsh_wsh_pkh_script, p2sh=False)
1478
1479 # The wsh(pkh()) with a compressed key is always valid, so we should see that the wallet detects it as ismine, not
1480 # watchonly, and can provide us information about the witnessScript via "embedded"
1481 comp_wsh_pkh_addr_info = wallet.getaddressinfo(comp_wsh_pkh_addr)
1482 assert_equal(comp_wsh_pkh_addr_info["ismine"], True)
1483 assert_equal(comp_wsh_pkh_addr_info["iswatchonly"], False)
1484 assert "embedded" in comp_wsh_pkh_addr_info
1485
1486 # The invalid addresses are invalid, so the legacy wallet should not detect them as ismine,
1487 # nor consider them watchonly. However, because the legacy wallet has the witnessScripts/redeemScripts,
1488 # we should see information about those in "embedded"
1489 for addr in invalid_addrs:
1490 addr_info = wallet.getaddressinfo(addr)
1491 assert_equal(addr_info["ismine"], False)
1492 assert_equal(addr_info["iswatchonly"], False)
1493 assert "embedded" in addr_info
1494
1495 # Fund those output scripts, although the invalid addresses will not have any balance.
1496 # This behavior follows as the addresses are not ismine.
1497 def_wallet.send([{comp_wsh_pkh_addr: 1}] + [{k: i + 1} for i, k in enumerate(invalid_addrs)])
1498 self.generate(self.nodes[0], 6)
1499 bal = wallet.getbalances()
1500 assert_equal(bal["mine"]["trusted"], 1)
1501 assert_equal(bal["watchonly"]["trusted"], 0)
1502
1503 res, wallet = self.migrate_and_get_rpc("invalid_p2wsh")
1504 assert "watchonly_name" not in res
1505 assert "solvables_name" not in res
1506
1507 assert_equal(wallet.getbalances()["mine"]["trusted"], 1)
1508
1509 # After migration, the wsh(pkh()) with a compressed key is still valid and the descriptor wallet will have
1510 # information about the witnessScript
1511 comp_wsh_pkh_addr_info = wallet.getaddressinfo(comp_wsh_pkh_addr)
1512 assert_equal(comp_wsh_pkh_addr_info["ismine"], True)
1513 assert_equal(comp_wsh_pkh_addr_info["iswatchonly"], False)
1514 assert "embedded" in comp_wsh_pkh_addr_info
1515
1516 # After migration, the invalid addresses should still not be detected as ismine and not watchonly.
1517 # The descriptor wallet should not have migrated these at all, so there should additionally be no
1518 # information in "embedded" about the witnessScripts/redeemScripts.
1519 for addr in invalid_addrs:
1520 addr_info = wallet.getaddressinfo(addr)
1521 assert_equal(addr_info["ismine"], False)
1522 assert_equal(addr_info["iswatchonly"], False)
1523 assert "embedded" not in addr_info
1524
1525 wallet.unloadwallet()
1526
1527 def test_miniscript(self):
1528 # It turns out that due to how signing logic works, legacy wallets that have valid miniscript witnessScripts
1529 # and the private keys for them can still sign and spend them, even though output scripts involving them
1530 # as a witnessScript would not be detected as ISMINE_SPENDABLE.
1531 self.log.info("Test migration of a legacy wallet containing miniscript")
1532 def_wallet = self.master_node.get_wallet_rpc(self.default_wallet_name)
1533 wallet = self.create_legacy_wallet("miniscript")
1534
1535 privkey, _ = generate_keypair(compressed=True, wif=True)
1536
1537 # Make a descriptor where we only have some of the keys. This will be migrated to the watchonly wallet.
1538 some_keys_priv_desc = descsum_create(f"wsh(or_b(pk({privkey}),s:pk(029ffbe722b147f3035c87cb1c60b9a5947dd49c774cc31e94773478711a929ac0)))")
1539 some_keys_addr = self.master_node.deriveaddresses(some_keys_priv_desc)[0]
1540
1541 # Make a descriptor where we have all of the keys. This will stay in the migrated wallet
1542 all_keys_priv_desc = descsum_create(f"wsh(and_v(v:pk({privkey}),1))")
1543 all_keys_addr = self.master_node.deriveaddresses(all_keys_priv_desc)[0]
1544
1545 imp = wallet.importmulti([
1546 {
1547 "desc": some_keys_priv_desc,
1548 "timestamp": "now",
1549 },
1550 {
1551 "desc": all_keys_priv_desc,
1552 "timestamp": "now",
1553 }
1554 ])
1555 assert_equal(imp[0]["success"], True)
1556 assert_equal(imp[1]["success"], True)
1557
1558 def_wallet.sendtoaddress(some_keys_addr, 1)
1559 def_wallet.sendtoaddress(all_keys_addr, 1)
1560 self.generate(self.master_node, 6)
1561 # Check that the miniscript can be spent by the legacy wallet
1562 send_res = wallet.send(outputs=[{some_keys_addr: 1},{all_keys_addr: 0.75}], include_watching=True, change_address=def_wallet.getnewaddress())
1563 assert_equal(send_res["complete"], True)
1564 self.generate(self.old_node, 6)
1565 assert_equal(wallet.getbalances()["watchonly"]["trusted"], 1.75)
1566
1567 _, wallet = self.migrate_and_get_rpc("miniscript")
1568
1569 # The miniscript with all keys should be in the migrated wallet
1570 assert_equal(wallet.getbalances()["mine"], {"trusted": 0.75, "untrusted_pending": 0, "immature": 0})
1571 assert_equal(wallet.getaddressinfo(all_keys_addr)["ismine"], True)
1572 assert_equal(wallet.getaddressinfo(some_keys_addr)["ismine"], False)
1573
1574 # The miniscript with some keys should be in the watchonly wallet
1575 assert "miniscript_watchonly" in self.master_node.listwallets()
1576 watchonly = self.master_node.get_wallet_rpc("miniscript_watchonly")
1577 assert_equal(watchonly.getbalances()["mine"], {"trusted": 1, "untrusted_pending": 0, "immature": 0})
1578 assert_equal(watchonly.getaddressinfo(some_keys_addr)["ismine"], True)
1579 assert_equal(watchonly.getaddressinfo(all_keys_addr)["ismine"], False)
1580
1581 def test_taproot(self):
1582 # It turns out that due to how signing logic works, legacy wallets that have the private key for a Taproot
1583 # output key will be able to sign and spend those scripts, even though they would not be detected as ISMINE_SPENDABLE.
1584 self.log.info("Test migration of Taproot scripts")
1585 def_wallet = self.master_node.get_wallet_rpc(self.default_wallet_name)
1586 wallet = self.create_legacy_wallet("taproot")
1587
1588 privkey, _ = generate_keypair(compressed=True, wif=True)
1589
1590 rawtr_desc = descsum_create(f"rawtr({privkey})")
1591 rawtr_addr = self.master_node.deriveaddresses(rawtr_desc)[0]
1592 rawtr_spk = self.master_node.validateaddress(rawtr_addr)["scriptPubKey"]
1593 tr_desc = descsum_create(f"tr({privkey})")
1594 tr_addr = self.master_node.deriveaddresses(tr_desc)[0]
1595 tr_spk = self.master_node.validateaddress(tr_addr)["scriptPubKey"]
1596 tr_script_desc = descsum_create(f"tr(9ffbe722b147f3035c87cb1c60b9a5947dd49c774cc31e94773478711a929ac0,pk({privkey}))")
1597 tr_script_addr = self.master_node.deriveaddresses(tr_script_desc)[0]
1598 tr_script_spk = self.master_node.validateaddress(tr_script_addr)["scriptPubKey"]
1599
1600 wallet.importaddress(rawtr_spk)
1601 wallet.importaddress(tr_spk)
1602 wallet.importaddress(tr_script_spk)
1603 wallet.importprivkey(privkey)
1604
1605 txid = def_wallet.send([{rawtr_addr: 1},{tr_addr: 2}, {tr_script_addr: 3}])["txid"]
1606 rawtr_vout = find_vout_for_address(self.master_node, txid, rawtr_addr)
1607 tr_vout = find_vout_for_address(self.master_node, txid, tr_addr)
1608 tr_script_vout = find_vout_for_address(self.master_node, txid, tr_script_addr)
1609 self.generate(self.master_node, 6)
1610 assert_equal(wallet.getbalances()["watchonly"]["trusted"], 6)
1611
1612 # Check that the rawtr can be spent by the legacy wallet
1613 send_res = wallet.send(outputs=[{rawtr_addr: 0.5}], include_watching=True, change_address=def_wallet.getnewaddress(), inputs=[{"txid": txid, "vout": rawtr_vout}])
1614 assert_equal(send_res["complete"], True)
1615 self.generate(self.old_node, 6)
1616 assert_equal(wallet.getbalances()["watchonly"]["trusted"], 5.5)
1617 assert_equal(wallet.getbalances()["mine"]["trusted"], 0)
1618
1619 # Check that the tr() cannot be spent by the legacy wallet
1620 send_res = wallet.send(outputs=[{def_wallet.getnewaddress(): 4}], include_watching=True, inputs=[{"txid": txid, "vout": tr_vout}, {"txid": txid, "vout": tr_script_vout}])
1621 assert_equal(send_res["complete"], False)
1622
1623 res, wallet = self.migrate_and_get_rpc("taproot")
1624
1625 # The rawtr should be migrated
1626 assert_equal(wallet.getbalances()["mine"], {"trusted": 0.5, "untrusted_pending": 0, "immature": 0})
1627 assert_equal(wallet.getaddressinfo(rawtr_addr)["ismine"], True)
1628 assert_equal(wallet.getaddressinfo(tr_addr)["ismine"], False)
1629 assert_equal(wallet.getaddressinfo(tr_script_addr)["ismine"], False)
1630
1631 # The tr() with some keys should be in the watchonly wallet
1632 assert "taproot_watchonly" in self.master_node.listwallets()
1633 watchonly = self.master_node.get_wallet_rpc("taproot_watchonly")
1634 assert_equal(watchonly.getbalances()["mine"], {"trusted": 5, "untrusted_pending": 0, "immature": 0})
1635 assert_equal(watchonly.getaddressinfo(rawtr_addr)["ismine"], False)
1636 assert_equal(watchonly.getaddressinfo(tr_addr)["ismine"], True)
1637 assert_equal(watchonly.getaddressinfo(tr_script_addr)["ismine"], True)
1638
1639 def test_solvable_no_privs(self):
1640 self.log.info("Test migrating a multisig that we do not have any private keys for")
1641 wallet = self.create_legacy_wallet("multisig_noprivs")
1642
1643 _, pubkey = generate_keypair(compressed=True, wif=True)
1644
1645 add_ms_res = wallet.addmultisigaddress(nrequired=1, keys=[pubkey.hex()])
1646 addr = add_ms_res["address"]
1647
1648 # The multisig address should be ISMINE_NO but we should have the script info
1649 addr_info = wallet.getaddressinfo(addr)
1650 assert_equal(addr_info["ismine"], False)
1651 assert "hex" in addr_info
1652
1653 migrate_res, wallet = self.migrate_and_get_rpc("multisig_noprivs")
1654 assert_equal(migrate_res["solvables_name"], "multisig_noprivs_solvables")
1655 solvables = self.master_node.get_wallet_rpc(migrate_res["solvables_name"])
1656
1657 # The multisig should not be in the spendable wallet
1658 addr_info = wallet.getaddressinfo(addr)
1659 assert_equal(addr_info["ismine"], False)
1660 assert "hex" not in addr_info
1661
1662 # The multisig address should be in the solvables wallet
1663 addr_info = solvables.getaddressinfo(addr)
1664 assert_equal(addr_info["ismine"], True)
1665 assert_equal(addr_info["solvable"], True)
1666 assert "hex" in addr_info
1667
1668 def test_loading_failure_after_migration(self):
1669 self.log.info("Test that a failed loading of the wallet at the end of migration restores the backup")
1670 self.stop_node(self.old_node.index)
1671 self.old_node.chain = "signet"
1672 self.old_node.replace_in_config([("regtest=", "signet="), ("[regtest]", "[signet]")])
1673 # Disable network sync and prevent disk space warning on small (tmp)fs
1674 self.start_node(self.old_node.index, extra_args=self.old_node.extra_args + ["-maxconnections=0", "-prune=550"])
1675
1676 wallet_name = "failed_load_after_migrate"
1677 self.create_legacy_wallet(wallet_name)
1678 assert_raises_rpc_error(-4, "Wallet loading failed. Wallet files should not be reused across chains.", lambda: self.migrate_and_get_rpc(wallet_name))
1679
1680 # Check the wallet we tried to migrate is still BDB
1681 self.assert_is_bdb(wallet_name)
1682
1683 self.stop_node(self.old_node.index)
1684 self.old_node.chain = "regtest"
1685 self.old_node.replace_in_config([("signet=", "regtest="), ("[signet]", "[regtest]")])
1686 self.start_node(self.old_node.index)
1687 self.connect_nodes(1, 0)
1688
1689 def unsynced_wallet_on_pruned_node_fails(self):
1690 self.log.info("Test migration of an unsynced wallet on a pruned node fails gracefully")
1691 wallet = self.create_legacy_wallet("", load_on_startup=False)
1692 last_wallet_synced_block = wallet.getwalletinfo()['lastprocessedblock']['height']
1693 wallet.unloadwallet()
1694
1695 shutil.copyfile(self.old_node.wallets_path / "wallet.dat", self.master_node.wallets_path / "wallet.dat")
1696
1697 # Generate blocks just so the wallet best block is pruned
1698 self.restart_node(0, ["-fastprune", "-prune=1", "-nowallet"])
1699 self.connect_nodes(0, 1)
1700 self.generate(self.master_node, 450, sync_fun=self.no_op)
1701 self.master_node.pruneblockchain(250)
1702 # Ensure next block to sync is unavailable
1703 assert_raises_rpc_error(-1, "Block not available (pruned data)", self.master_node.getblock, self.master_node.getblockhash(last_wallet_synced_block + 1))
1704
1705 # Check migration failure
1706 mocked_time = int(time.time())
1707 self.master_node.setmocktime(mocked_time)
1708 assert_raises_rpc_error(-4, "last wallet synchronisation goes beyond pruned data. You need to -reindex (download the whole blockchain again in case of a pruned node)", self.master_node.migratewallet, wallet_name="")
1709 self.master_node.setmocktime(0)
1710
1711 # Verify the /wallets/ path exists, the wallet is still BDB and the backup file is there.
1712 assert self.master_node.wallets_path.exists()
1713
1714 self.assert_is_bdb("")
1715
1716 backup_path = self.master_node.wallets_path / f"default_wallet_{mocked_time}.legacy.bak"
1717 assert backup_path.exists()
1718
1719 assert not (self.master_node.wallets_path / "wallet.dat-journal").exists()
1720
1721 self.clear_default_wallet(backup_path)
1722
1723
1724 def run_test(self):
1725 self.master_node = self.nodes[0]
1726 self.old_node = self.nodes[1]
1727
1728 self.generate(self.master_node, 101)
1729
1730 # TODO: Test the actual records in the wallet for these tests too. The behavior may be correct, but the data written may not be what we actually want
1731 self.test_basic()
1732 self.test_multisig()
1733 self.test_other_watchonly()
1734 self.test_no_privkeys()
1735 self.test_pk_coinbases()
1736 self.test_encrypted()
1737 self.test_nonexistent()
1738 self.test_unloaded_by_path()
1739 self.test_wallet_with_relative_path()
1740 self.test_wallet_with_path_ending_in_slash()
1741 self.test_wallet_with_path_ending_in_relative_specifier()
1742
1743 migration_failure_cases = [
1744 "",
1745 ".",
1746 "./",
1747 self.wallet_data_filename,
1748 "test.dat",
1749 "..",
1750 "../",
1751 "../subdir",
1752 "../subdir/..",
1753 "subdir/two",
1754 "subdir/../two",
1755 "subdir/two/..",
1756 os.path.abspath(self.master_node.datadir_path / "absolute_path"),
1757 "normallynamedwallet"
1758 ]
1759 for wallet_name in migration_failure_cases:
1760 self.test_migration_failure(wallet_name=wallet_name)
1761 self.test_migration_failure(wallet_name=wallet_name, fail=False)
1762
1763 self.test_default_wallet()
1764 self.test_default_wallet_watch_only()
1765 self.test_direct_file()
1766 self.test_addressbook()
1767 self.test_migrate_raw_p2sh()
1768 self.test_conflict_txs()
1769 self.test_hybrid_pubkey()
1770 self.test_failed_migration_cleanup()
1771 self.test_failed_migration_cleanup_relative_path()
1772 self.test_avoidreuse()
1773 self.test_preserve_tx_extra_info()
1774 self.test_blank()
1775 self.test_migrate_simple_watch_only()
1776 self.test_manual_keys_import()
1777 self.test_p2wsh()
1778 self.test_disallowed_p2wsh()
1779 self.test_miniscript()
1780 self.test_taproot()
1781 self.test_solvable_no_privs()
1782 self.test_loading_failure_after_migration()
1783
1784 # Note: After this test the first 250 blocks of 'master_node' are pruned
1785 self.unsynced_wallet_on_pruned_node_fails()
1786
1787 if __name__ == '__main__':
1788 WalletMigrationTest(__file__).main()
1789