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