This commit is contained in:
Ava Chow 2025-01-08 14:05:36 -05:00 committed by GitHub
commit c4b9e1bc76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 397 additions and 96 deletions

View file

@ -591,7 +591,7 @@ std::unique_ptr<SigningProvider> LegacyDataSPKM::GetSolvingProvider(const CScrip
return std::make_unique<LegacySigningProvider>(*this);
}
bool LegacyScriptPubKeyMan::CanProvide(const CScript& script, SignatureData& sigdata)
bool LegacyDataSPKM::CanProvide(const CScript& script, SignatureData& sigdata)
{
IsMineResult ismine = IsMineInner(*this, script, IsMineSigVersion::TOP, /* recurse_scripthash= */ false);
if (ismine == IsMineResult::SPENDABLE || ismine == IsMineResult::WATCH_ONLY) {
@ -1700,61 +1700,61 @@ std::set<CKeyID> LegacyScriptPubKeyMan::GetKeys() const
return set_address;
}
std::unordered_set<CScript, SaltedSipHasher> LegacyDataSPKM::GetScriptPubKeys() const
std::unordered_set<CScript, SaltedSipHasher> LegacyDataSPKM::GetCandidateScriptPubKeys() const
{
LOCK(cs_KeyStore);
std::unordered_set<CScript, SaltedSipHasher> candidate_spks;
// For every private key in the wallet, there should be a P2PK, P2PKH, P2WPKH, and P2SH-P2WPKH
const auto& add_pubkey = [&candidate_spks](const CPubKey& pub) -> void {
candidate_spks.insert(GetScriptForRawPubKey(pub));
candidate_spks.insert(GetScriptForDestination(PKHash(pub)));
CScript wpkh = GetScriptForDestination(WitnessV0KeyHash(pub));
candidate_spks.insert(wpkh);
candidate_spks.insert(GetScriptForDestination(ScriptHash(wpkh)));
};
for (const auto& [_, key] : mapKeys) {
add_pubkey(key.GetPubKey());
}
for (const auto& [_, ckeypair] : mapCryptedKeys) {
add_pubkey(ckeypair.first);
}
// mapScripts contains all redeemScripts and witnessScripts. Therefore each script in it has
// itself, P2SH, P2WSH, and P2SH-P2WSH as a candidate.
const auto& add_script = [&candidate_spks](const CScript& script) -> void {
candidate_spks.insert(script);
candidate_spks.insert(GetScriptForDestination(ScriptHash(script)));
CScript wsh = GetScriptForDestination(WitnessV0ScriptHash(script));
candidate_spks.insert(wsh);
candidate_spks.insert(GetScriptForDestination(ScriptHash(wsh)));
};
for (const auto& [_, script] : mapScripts) {
add_script(script);
}
// Although setWatchOnly should only contain output scripts, we will also include each script's
// P2SH, P2WSH, and P2SH-P2WSH as a precaution.
for (const auto& script : setWatchOnly) {
add_script(script);
}
return candidate_spks;
}
std::unordered_set<CScript, SaltedSipHasher> LegacyDataSPKM::GetScriptPubKeys() const
{
// Run IsMine() on each candidate output script. Any script that is not ISMINE_NO is an output
// script to return
std::unordered_set<CScript, SaltedSipHasher> spks;
// All keys have at least P2PK and P2PKH
for (const auto& key_pair : mapKeys) {
const CPubKey& pub = key_pair.second.GetPubKey();
spks.insert(GetScriptForRawPubKey(pub));
spks.insert(GetScriptForDestination(PKHash(pub)));
}
for (const auto& key_pair : mapCryptedKeys) {
const CPubKey& pub = key_pair.second.first;
spks.insert(GetScriptForRawPubKey(pub));
spks.insert(GetScriptForDestination(PKHash(pub)));
}
// For every script in mapScript, only the ISMINE_SPENDABLE ones are being tracked.
// The watchonly ones will be in setWatchOnly which we deal with later
// For all keys, if they have segwit scripts, those scripts will end up in mapScripts
for (const auto& script_pair : mapScripts) {
const CScript& script = script_pair.second;
if (IsMine(script) == ISMINE_SPENDABLE) {
// Add ScriptHash for scripts that are not already P2SH
if (!script.IsPayToScriptHash()) {
spks.insert(GetScriptForDestination(ScriptHash(script)));
}
// For segwit scripts, we only consider them spendable if we have the segwit spk
int wit_ver = -1;
std::vector<unsigned char> witprog;
if (script.IsWitnessProgram(wit_ver, witprog) && wit_ver == 0) {
spks.insert(script);
}
} else {
// Multisigs are special. They don't show up as ISMINE_SPENDABLE unless they are in a P2SH
// So check the P2SH of a multisig to see if we should insert it
std::vector<std::vector<unsigned char>> sols;
TxoutType type = Solver(script, sols);
if (type == TxoutType::MULTISIG) {
CScript ms_spk = GetScriptForDestination(ScriptHash(script));
if (IsMine(ms_spk) != ISMINE_NO) {
spks.insert(ms_spk);
}
}
for (const CScript& script : GetCandidateScriptPubKeys()) {
if (IsMine(script) != ISMINE_NO) {
spks.insert(script);
}
}
// All watchonly scripts are raw
for (const CScript& script : setWatchOnly) {
// As the legacy wallet allowed to import any script, we need to verify the validity here.
// LegacyScriptPubKeyMan::IsMine() return 'ISMINE_NO' for invalid or not watched scripts (IsMineResult::INVALID or IsMineResult::NO).
// e.g. a "sh(sh(pkh()))" which legacy wallets allowed to import!.
if (IsMine(script) != ISMINE_NO) spks.insert(script);
}
return spks;
}
@ -1925,6 +1925,19 @@ std::optional<MigrationData> LegacyDataSPKM::MigrateToDescriptor()
// InferDescriptor as that will get us all the solving info if it is there
std::unique_ptr<Descriptor> desc = InferDescriptor(spk, *GetSolvingProvider(spk));
// Past bugs in InferDescriptor has caused it to create descriptors which cannot be re-parsed
// Re-parse the descriptors to detect that, and skip any that do not parse.
{
std::string desc_str = desc->ToString();
FlatSigningProvider parsed_keys;
std::string parse_error;
std::vector<std::unique_ptr<Descriptor>> parsed_descs = Parse(desc_str, parsed_keys, parse_error, false);
if (parsed_descs.empty()) {
continue;
}
}
// Get the private keys for this descriptor
std::vector<CScript> scripts;
FlatSigningProvider keys;
@ -1974,57 +1987,51 @@ std::optional<MigrationData> LegacyDataSPKM::MigrateToDescriptor()
}
}
// Multisigs are special. They don't show up as ISMINE_SPENDABLE unless they are in a P2SH
// So we have to check if any of our scripts are a multisig and if so, add the P2SH
for (const auto& script_pair : mapScripts) {
const CScript script = script_pair.second;
// Make sure that we have accounted for all scriptPubKeys
assert(spks.size() == 0);
// Legacy wallets can also contains scripts whose P2SH, P2WSH, or P2SH-P2WSH it is not watching for
// but can provide script data to a PSBT spending them. These "solvable" output scripts will need to
// be put into the separate "solvables" wallet.
// These can be detected by going through the entire candidate output scripts, finding the ISMINE_NO scripts,
// and checking CanProvide() which will dummy sign.
for (const CScript& script : GetCandidateScriptPubKeys()) {
// Since we only care about P2SH, P2WSH, and P2SH-P2WSH, filter out any scripts that are not those
if (!script.IsPayToScriptHash() && !script.IsPayToWitnessScriptHash()) {
continue;
}
if (IsMine(script) != ISMINE_NO) {
continue;
}
SignatureData dummy_sigdata;
if (!CanProvide(script, dummy_sigdata)) {
continue;
}
// Get birthdate from script meta
uint64_t creation_time = 0;
const auto& it = m_script_metadata.find(CScriptID(script));
if (it != m_script_metadata.end()) {
creation_time = it->second.nCreateTime;
const auto& mit = m_script_metadata.find(CScriptID(script));
if (mit != m_script_metadata.end()) {
creation_time = mit->second.nCreateTime;
}
std::vector<std::vector<unsigned char>> sols;
TxoutType type = Solver(script, sols);
if (type == TxoutType::MULTISIG) {
CScript sh_spk = GetScriptForDestination(ScriptHash(script));
CTxDestination witdest = WitnessV0ScriptHash(script);
CScript witprog = GetScriptForDestination(witdest);
CScript sh_wsh_spk = GetScriptForDestination(ScriptHash(witprog));
// InferDescriptor as that will get us all the solving info if it is there
std::unique_ptr<Descriptor> desc = InferDescriptor(script, *GetSolvingProvider(script));
// We only want the multisigs that we have not already seen, i.e. they are not watchonly and not spendable
// For P2SH, a multisig is not ISMINE_NO when:
// * All keys are in the wallet
// * The multisig itself is watch only
// * The P2SH is watch only
// For P2SH-P2WSH, if the script is in the wallet, then it will have the same conditions as P2SH.
// For P2WSH, a multisig is not ISMINE_NO when, other than the P2SH conditions:
// * The P2WSH script is in the wallet and it is being watched
std::vector<std::vector<unsigned char>> keys(sols.begin() + 1, sols.begin() + sols.size() - 1);
if (HaveWatchOnly(sh_spk) || HaveWatchOnly(script) || HaveKeys(keys, *this) || (HaveCScript(CScriptID(witprog)) && HaveWatchOnly(witprog))) {
// The above emulates IsMine for these 3 scriptPubKeys, so double check that by running IsMine
assert(IsMine(sh_spk) != ISMINE_NO || IsMine(witprog) != ISMINE_NO || IsMine(sh_wsh_spk) != ISMINE_NO);
// Past bugs in InferDescriptor has caused it to create descriptors which cannot be re-parsed
// Re-parse the descriptors to detect that, and skip any that do not parse.
{
std::string desc_str = desc->ToString();
FlatSigningProvider parsed_keys;
std::string parse_error;
std::vector<std::unique_ptr<Descriptor>> parsed_descs = Parse(desc_str, parsed_keys, parse_error, false);
if (parsed_descs.empty()) {
continue;
}
assert(IsMine(sh_spk) == ISMINE_NO && IsMine(witprog) == ISMINE_NO && IsMine(sh_wsh_spk) == ISMINE_NO);
std::unique_ptr<Descriptor> sh_desc = InferDescriptor(sh_spk, *GetSolvingProvider(sh_spk));
out.solvable_descs.emplace_back(sh_desc->ToString(), creation_time);
const auto desc = InferDescriptor(witprog, *this);
if (desc->IsSolvable()) {
std::unique_ptr<Descriptor> wsh_desc = InferDescriptor(witprog, *GetSolvingProvider(witprog));
out.solvable_descs.emplace_back(wsh_desc->ToString(), creation_time);
std::unique_ptr<Descriptor> sh_wsh_desc = InferDescriptor(sh_wsh_spk, *GetSolvingProvider(sh_wsh_spk));
out.solvable_descs.emplace_back(sh_wsh_desc->ToString(), creation_time);
}
}
}
// Make sure that we have accounted for all scriptPubKeys
assert(spks.size() == 0);
out.solvable_descs.emplace_back(desc->ToString(), creation_time);
}
// Finalize transaction
if (!batch.TxnCommit()) {

View file

@ -302,6 +302,9 @@ protected:
virtual bool AddKeyPubKeyInner(const CKey& key, const CPubKey &pubkey);
bool AddCryptedKeyInner(const CPubKey &vchPubKey, const std::vector<unsigned char> &vchCryptedSecret);
// Helper function to retrieve a set of all output scripts that may be relevant to this LegacyDataSPKM
// Used only in migration.
std::unordered_set<CScript, SaltedSipHasher> GetCandidateScriptPubKeys() const;
public:
using ScriptPubKeyMan::ScriptPubKeyMan;
@ -318,6 +321,7 @@ public:
uint256 GetID() const override { return uint256::ONE; }
// TODO: Remove IsMine when deleting LegacyScriptPubKeyMan
isminetype IsMine(const CScript& script) const override;
bool CanProvide(const CScript& script, SignatureData& sigdata) override;
// FillableSigningProvider overrides
bool HaveKey(const CKeyID &address) const override;
@ -486,8 +490,6 @@ public:
bool CanGetAddresses(bool internal = false) const override;
bool CanProvide(const CScript& script, SignatureData& sigdata) override;
bool SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, bilingual_str>& input_errors) const override;
SigningResult SignMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) const override;
std::optional<common::PSBTError> FillPSBT(PartiallySignedTransaction& psbt, const PrecomputedTransactionData& txdata, int sighash_type = SIGHASH_DEFAULT, bool sign = true, bool bip32derivs = false, int* n_signed = nullptr, bool finalize = true) const override;

View file

@ -10,9 +10,10 @@ import struct
import time
from test_framework.address import (
script_to_p2sh,
key_to_p2pkh,
key_to_p2wpkh,
script_to_p2sh,
script_to_p2wsh,
)
from test_framework.bdb import BTREE_MAGIC
from test_framework.descriptors import descsum_create
@ -24,6 +25,7 @@ from test_framework.script_util import key_to_p2pkh_script, key_to_p2pk_script,
from test_framework.util import (
assert_equal,
assert_raises_rpc_error,
find_vout_for_address,
sha256sum_file,
)
from test_framework.wallet_util import (
@ -102,6 +104,7 @@ class WalletMigrationTest(BitcoinTestFramework):
# Reload to force write that record
self.old_node.unloadwallet(wallet_name)
self.old_node.loadwallet(wallet_name)
assert_equal(self.old_node.get_wallet_rpc(wallet_name).getwalletinfo()["descriptors"], False)
# Now unload so we can copy it to the master node for the migration test
self.old_node.unloadwallet(wallet_name)
if wallet_name == "":
@ -111,7 +114,9 @@ class WalletMigrationTest(BitcoinTestFramework):
# Migrate, checking that rescan does not occur
with self.master_node.assert_debug_log(expected_msgs=[], unexpected_msgs=["Rescanning"]):
migrate_info = self.master_node.migratewallet(wallet_name=wallet_name, **kwargs)
return migrate_info, self.master_node.get_wallet_rpc(wallet_name)
rpc = self.master_node.get_wallet_rpc(wallet_name)
assert_equal(rpc.getwalletinfo()["descriptors"], True)
return migrate_info, rpc
def test_basic(self):
default = self.master_node.get_wallet_rpc(self.default_wallet_name)
@ -1060,6 +1065,289 @@ class WalletMigrationTest(BitcoinTestFramework):
assert_equal(expected_descs, migrated_desc)
wo_wallet.unloadwallet()
def test_p2wsh(self):
self.log.info("Test that non-multisig P2WSH output scripts are migrated")
def_wallet = self.master_node.get_wallet_rpc(self.default_wallet_name)
wallet = self.create_legacy_wallet("p2wsh")
# Craft wsh(pkh(key))
pubkey = wallet.getaddressinfo(wallet.getnewaddress())["pubkey"]
pkh_script = key_to_p2pkh_script(pubkey).hex()
wsh_pkh_script = script_to_p2wsh_script(pkh_script).hex()
wsh_pkh_addr = script_to_p2wsh(pkh_script)
wallet.importaddress(address=pkh_script, p2sh=False)
wallet.importaddress(address=wsh_pkh_script, p2sh=False)
def_wallet.sendtoaddress(wsh_pkh_addr, 5)
self.generate(self.nodes[0], 6)
assert_equal(wallet.getbalances()['mine']['trusted'], 5)
_, wallet = self.migrate_and_get_rpc("p2wsh")
assert_equal(wallet.getbalances()['mine']['trusted'], 5)
addr_info = wallet.getaddressinfo(wsh_pkh_addr)
assert_equal(addr_info["ismine"], True)
assert_equal(addr_info["iswatchonly"], False)
assert_equal(addr_info["solvable"], True)
wallet.unloadwallet()
def test_disallowed_p2wsh(self):
self.log.info("Test that P2WSH output scripts with invalid witnessScripts are not migrated and do not cause migration failure")
def_wallet = self.master_node.get_wallet_rpc(self.default_wallet_name)
wallet = self.create_legacy_wallet("invalid_p2wsh")
invalid_addrs = []
# For a P2WSH output script stored in the legacy wallet's mapScripts, both the native P2WSH
# and the P2SH-P2WSH are detected by IsMine. We need to verify that descriptors for both
# output scripts are added to the resulting descriptor wallet.
# However, this cannot be done using a multisig as wallet migration treats multisigs specially.
# Instead, this is tested by importing a wsh(pkh()) script. But importing this directly will
# insert the wsh() into setWatchOnly which means that the setWatchOnly migration ends up handling
# this case, which we do not want.
# In order to get the wsh(pkh()) into only mapScripts and not setWatchOnly, we need to utilize
# importmulti and wrap the wsh(pkh()) inside of a sh(). This will insert the sh(wsh(pkh())) into
# setWatchOnly but not the wsh(pkh()).
# Furthermore, migration should not migrate the wsh(pkh()) if the key is uncompressed.
comp_wif, comp_pubkey = generate_keypair(compressed=True, wif=True)
comp_pkh_script = key_to_p2pkh_script(comp_pubkey).hex()
comp_wsh_pkh_script = script_to_p2wsh_script(comp_pkh_script).hex()
comp_sh_wsh_pkh_script = script_to_p2sh_script(comp_wsh_pkh_script).hex()
comp_wsh_pkh_addr = script_to_p2wsh(comp_pkh_script)
uncomp_wif, uncomp_pubkey = generate_keypair(compressed=False, wif=True)
uncomp_pkh_script = key_to_p2pkh_script(uncomp_pubkey).hex()
uncomp_wsh_pkh_script = script_to_p2wsh_script(uncomp_pkh_script).hex()
uncomp_sh_wsh_pkh_script = script_to_p2sh_script(uncomp_wsh_pkh_script).hex()
uncomp_wsh_pkh_addr = script_to_p2wsh(uncomp_pkh_script)
invalid_addrs.append(uncomp_wsh_pkh_addr)
import_res = wallet.importmulti([
{
"scriptPubKey": comp_sh_wsh_pkh_script,
"timestamp": "now",
"redeemscript": comp_wsh_pkh_script,
"witnessscript": comp_pkh_script,
"keys": [
comp_wif,
],
},
{
"scriptPubKey": uncomp_sh_wsh_pkh_script,
"timestamp": "now",
"redeemscript": uncomp_wsh_pkh_script,
"witnessscript": uncomp_pkh_script,
"keys": [
uncomp_wif,
],
},
])
assert_equal(import_res[0]["success"], True)
assert_equal(import_res[1]["success"], True)
# Create a wsh(sh(pkh())) - P2SH inside of P2WSH is invalid
comp_sh_pkh_script = script_to_p2sh_script(comp_pkh_script).hex()
wsh_sh_pkh_script = script_to_p2wsh_script(comp_sh_pkh_script).hex()
wsh_sh_pkh_addr = script_to_p2wsh(comp_sh_pkh_script)
invalid_addrs.append(wsh_sh_pkh_addr)
# Import wsh(sh(pkh()))
wallet.importaddress(address=comp_sh_pkh_script, p2sh=False)
wallet.importaddress(address=wsh_sh_pkh_script, p2sh=False)
# Create a wsh(wsh(pkh())) - P2WSH inside of P2WSH is invalid
wsh_wsh_pkh_script = script_to_p2wsh_script(comp_wsh_pkh_script).hex()
wsh_wsh_pkh_addr = script_to_p2wsh(comp_wsh_pkh_script)
invalid_addrs.append(wsh_wsh_pkh_addr)
# Import wsh(wsh(pkh()))
wallet.importaddress(address=wsh_wsh_pkh_script, p2sh=False)
# The wsh(pkh()) with a compressed key is always valid, so we should see that the wallet detects it as ismine, not
# watchonly, and can provide us information about the witnessScript via "embedded"
comp_wsh_pkh_addr_info = wallet.getaddressinfo(comp_wsh_pkh_addr)
assert_equal(comp_wsh_pkh_addr_info["ismine"], True)
assert_equal(comp_wsh_pkh_addr_info["iswatchonly"], False)
assert "embedded" in comp_wsh_pkh_addr_info
# The invalid addresses are invalid, so the legcy wallet should not detect them as ismine,
# nor consider them watchonly. However, because the legacy wallet has the witnessScripts/redeemScripts,
# we should see information about those in "embedded"
for addr in invalid_addrs:
addr_info = wallet.getaddressinfo(addr)
assert_equal(addr_info["ismine"], False)
assert_equal(addr_info["iswatchonly"], False)
assert "embedded" in addr_info
# Fund those output scripts
def_wallet.send([{comp_wsh_pkh_addr: 1}] + [{k: i + 1} for i, k in enumerate(invalid_addrs)])
self.generate(self.nodes[0], 6)
assert_equal(wallet.getbalances()["mine"]["trusted"], 1)
_, wallet = self.migrate_and_get_rpc("invalid_p2wsh")
assert_equal(wallet.getbalances()["mine"]["trusted"], 1)
# After migration, the wsh(pkh()) with a compressed key is still valid and the descriptor wallet will have
# information about the witnessScript
comp_wsh_pkh_addr_info = wallet.getaddressinfo(comp_wsh_pkh_addr)
assert_equal(comp_wsh_pkh_addr_info["ismine"], True)
assert_equal(comp_wsh_pkh_addr_info["iswatchonly"], False)
assert "embedded" in comp_wsh_pkh_addr_info
# After migration, the invalid addresses should still not be detected as ismine and not watchonly.
# The descriptor wallet should not have migrated these at all, so there should additionally be no
# information in "embedded" about the witnessScripts/redeemScripts.
for addr in invalid_addrs:
addr_info = wallet.getaddressinfo(addr)
assert_equal(addr_info["ismine"], False)
assert_equal(addr_info["iswatchonly"], False)
assert "embedded" not in addr_info
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 test_taproot(self):
# It turns out that due to how signing logic works, legacy wallets that have the private key for a Taproot
# output key will be able to sign and spend those scripts, even though they would not be detected as ISMINE_SPENDABLE.
self.log.info("Test migration of Taproot scripts")
def_wallet = self.master_node.get_wallet_rpc(self.default_wallet_name)
wallet = self.create_legacy_wallet("taproot")
privkey, _ = generate_keypair(compressed=True, wif=True)
rawtr_desc = descsum_create(f"rawtr({privkey})")
rawtr_addr = self.master_node.deriveaddresses(rawtr_desc)[0]
rawtr_spk = self.master_node.validateaddress(rawtr_addr)["scriptPubKey"]
tr_desc = descsum_create(f"tr({privkey})")
tr_addr = self.master_node.deriveaddresses(tr_desc)[0]
tr_spk = self.master_node.validateaddress(tr_addr)["scriptPubKey"]
tr_script_desc = descsum_create(f"tr(9ffbe722b147f3035c87cb1c60b9a5947dd49c774cc31e94773478711a929ac0,pk({privkey}))")
tr_script_addr = self.master_node.deriveaddresses(tr_script_desc)[0]
tr_script_spk = self.master_node.validateaddress(tr_script_addr)["scriptPubKey"]
wallet.importaddress(rawtr_spk)
wallet.importaddress(tr_spk)
wallet.importaddress(tr_script_spk)
wallet.importprivkey(privkey)
txid = def_wallet.send([{rawtr_addr: 1},{tr_addr: 2}, {tr_script_addr: 3}])["txid"]
rawtr_vout = find_vout_for_address(self.master_node, txid, rawtr_addr)
tr_vout = find_vout_for_address(self.master_node, txid, tr_addr)
tr_script_vout = find_vout_for_address(self.master_node, txid, tr_script_addr)
self.generate(self.master_node, 6)
# Double check that the rawtr can be spent by the legacy wallet
send_res = wallet.send(outputs=[{rawtr_addr: 0.5}], include_watching=True, change_address=def_wallet.getnewaddress(), inputs=[{"txid": txid, "vout": rawtr_vout}])
assert_equal(send_res["complete"], True)
self.generate(self.old_node, 6)
assert_equal(wallet.getbalances()["watchonly"]["trusted"], 5.5)
# Check that the tr() cannot be spent by the legacy wallet
send_res = wallet.send(outputs=[{def_wallet.getnewaddress(): 4}], include_watching=True, inputs=[{"txid": txid, "vout": tr_vout}, {"txid": txid, "vout": tr_script_vout}])
assert_equal(send_res["complete"], False)
res, wallet = self.migrate_and_get_rpc("taproot")
# The rawtr should be migrated
assert_equal(wallet.getbalances()["mine"], {"trusted": 0.5, "untrusted_pending": 0, "immature": 0})
assert_equal(wallet.getaddressinfo(rawtr_addr)["ismine"], True)
assert_equal(wallet.getaddressinfo(tr_addr)["ismine"], False)
assert_equal(wallet.getaddressinfo(tr_script_addr)["ismine"], False)
# The miniscript with some keys should be in the watchonly wallet
assert "taproot_watchonly" in self.master_node.listwallets()
watchonly = self.master_node.get_wallet_rpc("taproot_watchonly")
assert_equal(watchonly.getbalances()["mine"], {"trusted": 5, "untrusted_pending": 0, "immature": 0})
assert_equal(watchonly.getaddressinfo(rawtr_addr)["ismine"], False)
assert_equal(watchonly.getaddressinfo(tr_addr)["ismine"], True)
assert_equal(watchonly.getaddressinfo(tr_script_addr)["ismine"], True)
def test_solvable_no_privs(self):
self.log.info("Test migrating a multisig that we do not have any private keys for")
wallet = self.create_legacy_wallet("multisig_noprivs")
privkey, pubkey = generate_keypair(compressed=True, wif=True)
add_ms_res = wallet.addmultisigaddress(nrequired=1, keys=[pubkey.hex()])
addr = add_ms_res["address"]
# The multisig address should be ISMINE_NO but we should have the script info
addr_info = wallet.getaddressinfo(addr)
assert_equal(addr_info["ismine"], False)
assert "hex" in addr_info
migrate_res, wallet = self.migrate_and_get_rpc("multisig_noprivs")
assert_equal(migrate_res["solvables_name"], "multisig_noprivs_solvables")
solvables = self.master_node.get_wallet_rpc(migrate_res["solvables_name"])
# The multisig should not be in the spendable wallet
addr_info = wallet.getaddressinfo(addr)
assert_equal(addr_info["ismine"], False)
assert "hex" not in addr_info
# The multisig address should be in the solvables wallet
addr_info = solvables.getaddressinfo(addr)
assert_equal(addr_info["ismine"], True)
assert "hex" in addr_info
def run_test(self):
self.master_node = self.nodes[0]
self.old_node = self.nodes[1]
@ -1087,7 +1375,11 @@ class WalletMigrationTest(BitcoinTestFramework):
self.test_blank()
self.test_migrate_simple_watch_only()
self.test_manual_keys_import()
self.test_p2wsh()
self.test_disallowed_p2wsh()
self.test_miniscript()
self.test_taproot()
self.test_solvable_no_privs()
if __name__ == '__main__':
WalletMigrationTest(__file__).main()