diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp index 4c1f73389c2201..630884832671b2 100644 --- a/src/wallet/scriptpubkeyman.cpp +++ b/src/wallet/scriptpubkeyman.cpp @@ -1955,6 +1955,7 @@ std::optional LegacyDataSPKM::MigrateToDescriptor() std::string desc_str; bool watchonly = !desc->ToPrivateString(*this, desc_str); if (watchonly && !m_storage.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + WalletLogPrintf("%s\n", desc->ToString()); out.watch_descs.emplace_back(desc->ToString(), creation_time); // Get the scriptPubKeys without writing this to the wallet diff --git a/test/functional/wallet_migration.py b/test/functional/wallet_migration.py index 62b4756a8ffcbb..4a034ff18dd99d 100755 --- a/test/functional/wallet_migration.py +++ b/test/functional/wallet_migration.py @@ -1060,6 +1060,60 @@ def test_manual_keys_import(self): assert_equal(expected_descs, migrated_desc) wo_wallet.unloadwallet() + def test_miniscript(self): + # It turns out that due to how signing logic works, legacy wallets that have valid miniscript witnessScripts + # and the private keys for them can still sign and spend them, even though output scripts involving them + # as a witnessScript would not be detected as ISMINE_SPENDABLE. + self.log.info("Test migration of a legacy wallet containing miniscript") + def_wallet = self.master_node.get_wallet_rpc(self.default_wallet_name) + wallet = self.create_legacy_wallet("miniscript") + + privkey, _ = generate_keypair(compressed=True, wif=True) + + # Make a descriptor where we only have some of the keys. This will be migrated to the watchonly wallet. + some_keys_priv_desc = descsum_create(f"wsh(or_b(pk({privkey}),s:pk(029ffbe722b147f3035c87cb1c60b9a5947dd49c774cc31e94773478711a929ac0)))") + some_keys_addr = self.master_node.deriveaddresses(some_keys_priv_desc)[0] + + # Make a descriptor where we have all of the keys. This will stay in the migrated wallet + all_keys_priv_desc = descsum_create(f"wsh(and_v(v:pk({privkey}),1))") + all_keys_addr = self.master_node.deriveaddresses(all_keys_priv_desc)[0] + + imp = wallet.importmulti([ + { + "desc": some_keys_priv_desc, + "timestamp": "now", + }, + { + "desc": all_keys_priv_desc, + "timestamp": "now", + } + ]) + assert_equal(imp[0]["success"], True) + assert_equal(imp[1]["success"], True) + + def_wallet.sendtoaddress(some_keys_addr, 1) + def_wallet.sendtoaddress(all_keys_addr, 1) + self.generate(self.master_node, 6) + # Double check that the miniscript can be spent by the legacy wallet + send_res = wallet.send(outputs=[{some_keys_addr: 1},{all_keys_addr: 0.75}], include_watching=True, change_address=def_wallet.getnewaddress()) + assert_equal(send_res["complete"], True) + self.generate(self.old_node, 6) + assert_equal(wallet.getbalances()["watchonly"]["trusted"], 1.75) + + res, wallet = self.migrate_and_get_rpc("miniscript") + + # The miniscript with all keys should be in the migrated wallet + assert_equal(wallet.getbalances()["mine"], {"trusted": 0.75, "untrusted_pending": 0, "immature": 0}) + assert_equal(wallet.getaddressinfo(all_keys_addr)["ismine"], True) + assert_equal(wallet.getaddressinfo(some_keys_addr)["ismine"], False) + + # The miniscript with some keys should be in the watchonly wallet + assert "miniscript_watchonly" in self.master_node.listwallets() + watchonly = self.master_node.get_wallet_rpc("miniscript_watchonly") + assert_equal(watchonly.getbalances()["mine"], {"trusted": 1, "untrusted_pending": 0, "immature": 0}) + assert_equal(watchonly.getaddressinfo(some_keys_addr)["ismine"], True) + assert_equal(watchonly.getaddressinfo(all_keys_addr)["ismine"], False) + def run_test(self): self.master_node = self.nodes[0] self.old_node = self.nodes[1] @@ -1087,6 +1141,7 @@ def run_test(self): self.test_blank() self.test_migrate_simple_watch_only() self.test_manual_keys_import() + self.test_miniscript() if __name__ == '__main__':