Skip to content

Commit 96d30ed

Browse files
committed
Merge bitcoin/bitcoin#31495: wallet: Utilize IsMine() and CanProvide() in migration to cover edge cases
af76664 test: Test migration of a solvable script with no privkeys (Ava Chow) 17f01b0 test: Test migration of taproot output scripts (Ava Chow) 1eb9a2a test: Test migration of miniscript in legacy wallets (Ava Chow) e8c3efc wallet migration: Determine Solvables with CanProvide (Ava Chow) fa1b7cd migration: Skip descriptors which do not parse (Ava Chow) 440ea1a legacy spkm: use IsMine() to extract watched output scripts (Ava Chow) b777e84 legacy spkm: Move CanProvide to LegacyDataSPKM (Ava Chow) b1ab927 tests: Test migration of additional P2WSH scripts (Ava Chow) c39b3cf test: Extra verification that migratewallet migrates (Ava Chow) Pull request description: The legacy wallet `IsMine()` is essentially a black box that would tell us whether the wallet is watching an output script. In order to migrate legacy wallets to descriptor wallets, we need to be able to compute all of the output scripts that a legacy wallet would watch. The original approach for this was to understand `IsMine()` and write a function which would be its inverse. This was partially done in the original migration code, and attempted to be completed in #30328. However, further analysis of `IsMine()` has continued to reveal additional edge cases which make writing an inverse function increasingly difficult to verify correctness. This PR instead changes migration to utilize `IsMine()` to produce the output scripts by first computing a superset of all of the output scripts that `IsMine()` would watch and testing each script against `IsMine()` to filter for the ones that actually are watched. The superset is constructed by computing all possible output scripts for the keys and scripts in the wallet - for keys, every key could be a P2PK, P2PKH, P2WPKH, and P2SH-P2WPKH; for scripts, every script could be an output script, the redeemScript of a P2SH, the witnessScript of a P2WSH, and the witnessScript of a P2SH-P2WSH. Additionally, the legacy wallet can contain scripts that are redeemScripts and witnessScripts, while not watching for any output script utilizing that script. These are known as solvable scripts and are migrated to a separate "solvables" wallet. The previous approach to identifying these solvables was similar to identifying output scripts - finding known solvable conditions and computing the scripts. However, this also can miss scripts, so the solvables are now identified in a manner similar to the output scripts but using the function `CanProvide()`. Using the same superset as before, all output scripts which are `ISMINE_NO` are put through `CanProvide()` which will perform a dummy signing and then a key lookup to determine whether the legacy wallet could provide any solving data for the output script. The scripts that pass will have their descriptors inferred and the script included in the solvables wallet. The main downside of this approach is that `IsMine()` and `CanProvide()` can no longer be deleted. They will need to be refactored to be migration only code instead in #28710. Lastly, I've added 2 test cases for the edge cases that prompted this change of approach. In particular, miniscript witnessScripts and `rawtr()` output scripts are solvable and signable in a legacy wallet, although never `ISMINE_SPENDABLE`. ACKs for top commit: sipa: Code review ACK af76664; I did not review the tests in detail. brunoerg: code review ACK af76664 rkrux: ACK af76664 Tree-SHA512: 7f58a90de6f38fe9801fb6c2a520627072c8d66358652ad0872ff59deb678a82664b99babcfd874288bebcb1487d099a77821f03ae063c2b4cbf2d316e77d141
2 parents c242fa5 + af76664 commit 96d30ed

File tree

3 files changed

+419
-109
lines changed

3 files changed

+419
-109
lines changed

src/wallet/scriptpubkeyman.cpp

Lines changed: 107 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -597,7 +597,7 @@ std::unique_ptr<SigningProvider> LegacyDataSPKM::GetSolvingProvider(const CScrip
597597
return std::make_unique<LegacySigningProvider>(*this);
598598
}
599599

600-
bool LegacyScriptPubKeyMan::CanProvide(const CScript& script, SignatureData& sigdata)
600+
bool LegacyDataSPKM::CanProvide(const CScript& script, SignatureData& sigdata)
601601
{
602602
IsMineResult ismine = IsMineInner(*this, script, IsMineSigVersion::TOP, /* recurse_scripthash= */ false);
603603
if (ismine == IsMineResult::SPENDABLE || ismine == IsMineResult::WATCH_ONLY) {
@@ -1706,59 +1706,62 @@ std::set<CKeyID> LegacyScriptPubKeyMan::GetKeys() const
17061706
return set_address;
17071707
}
17081708

1709-
std::unordered_set<CScript, SaltedSipHasher> LegacyDataSPKM::GetScriptPubKeys() const
1709+
std::unordered_set<CScript, SaltedSipHasher> LegacyDataSPKM::GetCandidateScriptPubKeys() const
17101710
{
17111711
LOCK(cs_KeyStore);
1712-
std::unordered_set<CScript, SaltedSipHasher> spks;
1712+
std::unordered_set<CScript, SaltedSipHasher> candidate_spks;
17131713

1714-
// All keys have at least P2PK and P2PKH
1715-
for (const auto& key_pair : mapKeys) {
1716-
const CPubKey& pub = key_pair.second.GetPubKey();
1717-
spks.insert(GetScriptForRawPubKey(pub));
1718-
spks.insert(GetScriptForDestination(PKHash(pub)));
1714+
// For every private key in the wallet, there should be a P2PK, P2PKH, P2WPKH, and P2SH-P2WPKH
1715+
const auto& add_pubkey = [&candidate_spks](const CPubKey& pub) -> void {
1716+
candidate_spks.insert(GetScriptForRawPubKey(pub));
1717+
candidate_spks.insert(GetScriptForDestination(PKHash(pub)));
1718+
1719+
CScript wpkh = GetScriptForDestination(WitnessV0KeyHash(pub));
1720+
candidate_spks.insert(wpkh);
1721+
candidate_spks.insert(GetScriptForDestination(ScriptHash(wpkh)));
1722+
};
1723+
for (const auto& [_, key] : mapKeys) {
1724+
add_pubkey(key.GetPubKey());
17191725
}
1720-
for (const auto& key_pair : mapCryptedKeys) {
1721-
const CPubKey& pub = key_pair.second.first;
1722-
spks.insert(GetScriptForRawPubKey(pub));
1723-
spks.insert(GetScriptForDestination(PKHash(pub)));
1724-
}
1725-
1726-
// For every script in mapScript, only the ISMINE_SPENDABLE ones are being tracked.
1727-
// The watchonly ones will be in setWatchOnly which we deal with later
1728-
// For all keys, if they have segwit scripts, those scripts will end up in mapScripts
1729-
for (const auto& script_pair : mapScripts) {
1730-
const CScript& script = script_pair.second;
1731-
if (IsMine(script) == ISMINE_SPENDABLE) {
1732-
// Add ScriptHash for scripts that are not already P2SH
1733-
if (!script.IsPayToScriptHash()) {
1734-
spks.insert(GetScriptForDestination(ScriptHash(script)));
1735-
}
1736-
// For segwit scripts, we only consider them spendable if we have the segwit spk
1737-
int wit_ver = -1;
1738-
std::vector<unsigned char> witprog;
1739-
if (script.IsWitnessProgram(wit_ver, witprog) && wit_ver == 0) {
1740-
spks.insert(script);
1741-
}
1742-
} else {
1743-
// Multisigs are special. They don't show up as ISMINE_SPENDABLE unless they are in a P2SH
1744-
// So check the P2SH of a multisig to see if we should insert it
1745-
std::vector<std::vector<unsigned char>> sols;
1746-
TxoutType type = Solver(script, sols);
1747-
if (type == TxoutType::MULTISIG) {
1748-
CScript ms_spk = GetScriptForDestination(ScriptHash(script));
1749-
if (IsMine(ms_spk) != ISMINE_NO) {
1750-
spks.insert(ms_spk);
1751-
}
1752-
}
1753-
}
1726+
for (const auto& [_, ckeypair] : mapCryptedKeys) {
1727+
add_pubkey(ckeypair.first);
17541728
}
17551729

1756-
// All watchonly scripts are raw
1757-
for (const CScript& script : setWatchOnly) {
1758-
// As the legacy wallet allowed to import any script, we need to verify the validity here.
1759-
// LegacyScriptPubKeyMan::IsMine() return 'ISMINE_NO' for invalid or not watched scripts (IsMineResult::INVALID or IsMineResult::NO).
1760-
// e.g. a "sh(sh(pkh()))" which legacy wallets allowed to import!.
1761-
if (IsMine(script) != ISMINE_NO) spks.insert(script);
1730+
// mapScripts contains all redeemScripts and witnessScripts. Therefore each script in it has
1731+
// itself, P2SH, P2WSH, and P2SH-P2WSH as a candidate.
1732+
// Invalid scripts such as P2SH-P2SH and P2WSH-P2SH, among others, will be added as candidates.
1733+
// Callers of this function will need to remove such scripts.
1734+
const auto& add_script = [&candidate_spks](const CScript& script) -> void {
1735+
candidate_spks.insert(script);
1736+
candidate_spks.insert(GetScriptForDestination(ScriptHash(script)));
1737+
1738+
CScript wsh = GetScriptForDestination(WitnessV0ScriptHash(script));
1739+
candidate_spks.insert(wsh);
1740+
candidate_spks.insert(GetScriptForDestination(ScriptHash(wsh)));
1741+
};
1742+
for (const auto& [_, script] : mapScripts) {
1743+
add_script(script);
1744+
}
1745+
1746+
// Although setWatchOnly should only contain output scripts, we will also include each script's
1747+
// P2SH, P2WSH, and P2SH-P2WSH as a precaution.
1748+
for (const auto& script : setWatchOnly) {
1749+
add_script(script);
1750+
}
1751+
1752+
return candidate_spks;
1753+
}
1754+
1755+
std::unordered_set<CScript, SaltedSipHasher> LegacyDataSPKM::GetScriptPubKeys() const
1756+
{
1757+
// Run IsMine() on each candidate output script. Any script that is not ISMINE_NO is an output
1758+
// script to return.
1759+
// This both filters out things that are not watched by the wallet, and things that are invalid.
1760+
std::unordered_set<CScript, SaltedSipHasher> spks;
1761+
for (const CScript& script : GetCandidateScriptPubKeys()) {
1762+
if (IsMine(script) != ISMINE_NO) {
1763+
spks.insert(script);
1764+
}
17621765
}
17631766

17641767
return spks;
@@ -1931,6 +1934,21 @@ std::optional<MigrationData> LegacyDataSPKM::MigrateToDescriptor()
19311934

19321935
// InferDescriptor as that will get us all the solving info if it is there
19331936
std::unique_ptr<Descriptor> desc = InferDescriptor(spk, *GetSolvingProvider(spk));
1937+
1938+
// Past bugs in InferDescriptor have caused it to create descriptors which cannot be re-parsed.
1939+
// Re-parse the descriptors to detect that, and skip any that do not parse.
1940+
{
1941+
std::string desc_str = desc->ToString();
1942+
FlatSigningProvider parsed_keys;
1943+
std::string parse_error;
1944+
std::vector<std::unique_ptr<Descriptor>> parsed_descs = Parse(desc_str, parsed_keys, parse_error);
1945+
if (parsed_descs.empty()) {
1946+
// Remove this scriptPubKey from the set
1947+
it = spks.erase(it);
1948+
continue;
1949+
}
1950+
}
1951+
19341952
// Get the private keys for this descriptor
19351953
std::vector<CScript> scripts;
19361954
FlatSigningProvider keys;
@@ -1980,10 +1998,29 @@ std::optional<MigrationData> LegacyDataSPKM::MigrateToDescriptor()
19801998
}
19811999
}
19822000

1983-
// Multisigs are special. They don't show up as ISMINE_SPENDABLE unless they are in a P2SH
1984-
// So we have to check if any of our scripts are a multisig and if so, add the P2SH
1985-
for (const auto& script_pair : mapScripts) {
1986-
const CScript script = script_pair.second;
2001+
// Make sure that we have accounted for all scriptPubKeys
2002+
if (!Assume(spks.empty())) {
2003+
LogPrintf("%s\n", STR_INTERNAL_BUG("Error: Some output scripts were not migrated.\n"));
2004+
return std::nullopt;
2005+
}
2006+
2007+
// Legacy wallets can also contain scripts whose P2SH, P2WSH, or P2SH-P2WSH it is not watching for
2008+
// but can provide script data to a PSBT spending them. These "solvable" output scripts will need to
2009+
// be put into the separate "solvables" wallet.
2010+
// These can be detected by going through the entire candidate output scripts, finding the ISMINE_NO scripts,
2011+
// and checking CanProvide() which will dummy sign.
2012+
for (const CScript& script : GetCandidateScriptPubKeys()) {
2013+
// Since we only care about P2SH, P2WSH, and P2SH-P2WSH, filter out any scripts that are not those
2014+
if (!script.IsPayToScriptHash() && !script.IsPayToWitnessScriptHash()) {
2015+
continue;
2016+
}
2017+
if (IsMine(script) != ISMINE_NO) {
2018+
continue;
2019+
}
2020+
SignatureData dummy_sigdata;
2021+
if (!CanProvide(script, dummy_sigdata)) {
2022+
continue;
2023+
}
19872024

19882025
// Get birthdate from script meta
19892026
uint64_t creation_time = 0;
@@ -1992,45 +2029,28 @@ std::optional<MigrationData> LegacyDataSPKM::MigrateToDescriptor()
19922029
creation_time = it->second.nCreateTime;
19932030
}
19942031

1995-
std::vector<std::vector<unsigned char>> sols;
1996-
TxoutType type = Solver(script, sols);
1997-
if (type == TxoutType::MULTISIG) {
1998-
CScript sh_spk = GetScriptForDestination(ScriptHash(script));
1999-
CTxDestination witdest = WitnessV0ScriptHash(script);
2000-
CScript witprog = GetScriptForDestination(witdest);
2001-
CScript sh_wsh_spk = GetScriptForDestination(ScriptHash(witprog));
2002-
2003-
// We only want the multisigs that we have not already seen, i.e. they are not watchonly and not spendable
2004-
// For P2SH, a multisig is not ISMINE_NO when:
2005-
// * All keys are in the wallet
2006-
// * The multisig itself is watch only
2007-
// * The P2SH is watch only
2008-
// For P2SH-P2WSH, if the script is in the wallet, then it will have the same conditions as P2SH.
2009-
// For P2WSH, a multisig is not ISMINE_NO when, other than the P2SH conditions:
2010-
// * The P2WSH script is in the wallet and it is being watched
2011-
std::vector<std::vector<unsigned char>> keys(sols.begin() + 1, sols.begin() + sols.size() - 1);
2012-
if (HaveWatchOnly(sh_spk) || HaveWatchOnly(script) || HaveKeys(keys, *this) || (HaveCScript(CScriptID(witprog)) && HaveWatchOnly(witprog))) {
2013-
// The above emulates IsMine for these 3 scriptPubKeys, so double check that by running IsMine
2014-
assert(IsMine(sh_spk) != ISMINE_NO || IsMine(witprog) != ISMINE_NO || IsMine(sh_wsh_spk) != ISMINE_NO);
2015-
continue;
2016-
}
2017-
assert(IsMine(sh_spk) == ISMINE_NO && IsMine(witprog) == ISMINE_NO && IsMine(sh_wsh_spk) == ISMINE_NO);
2018-
2019-
std::unique_ptr<Descriptor> sh_desc = InferDescriptor(sh_spk, *GetSolvingProvider(sh_spk));
2020-
out.solvable_descs.emplace_back(sh_desc->ToString(), creation_time);
2032+
// InferDescriptor as that will get us all the solving info if it is there
2033+
std::unique_ptr<Descriptor> desc = InferDescriptor(script, *GetSolvingProvider(script));
2034+
if (!desc->IsSolvable()) {
2035+
// The wallet was able to provide some information, but not enough to make a descriptor that actually
2036+
// contains anything useful. This is probably because the script itself is actually unsignable (e.g. P2WSH-P2WSH).
2037+
continue;
2038+
}
20212039

2022-
const auto desc = InferDescriptor(witprog, *this);
2023-
if (desc->IsSolvable()) {
2024-
std::unique_ptr<Descriptor> wsh_desc = InferDescriptor(witprog, *GetSolvingProvider(witprog));
2025-
out.solvable_descs.emplace_back(wsh_desc->ToString(), creation_time);
2026-
std::unique_ptr<Descriptor> sh_wsh_desc = InferDescriptor(sh_wsh_spk, *GetSolvingProvider(sh_wsh_spk));
2027-
out.solvable_descs.emplace_back(sh_wsh_desc->ToString(), creation_time);
2040+
// Past bugs in InferDescriptor have caused it to create descriptors which cannot be re-parsed
2041+
// Re-parse the descriptors to detect that, and skip any that do not parse.
2042+
{
2043+
std::string desc_str = desc->ToString();
2044+
FlatSigningProvider parsed_keys;
2045+
std::string parse_error;
2046+
std::vector<std::unique_ptr<Descriptor>> parsed_descs = Parse(desc_str, parsed_keys, parse_error, false);
2047+
if (parsed_descs.empty()) {
2048+
continue;
20282049
}
20292050
}
2030-
}
20312051

2032-
// Make sure that we have accounted for all scriptPubKeys
2033-
assert(spks.size() == 0);
2052+
out.solvable_descs.emplace_back(desc->ToString(), creation_time);
2053+
}
20342054

20352055
// Finalize transaction
20362056
if (!batch.TxnCommit()) {

src/wallet/scriptpubkeyman.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,10 @@ class LegacyDataSPKM : public ScriptPubKeyMan, public FillableSigningProvider
303303
virtual bool AddKeyPubKeyInner(const CKey& key, const CPubKey &pubkey);
304304
bool AddCryptedKeyInner(const CPubKey &vchPubKey, const std::vector<unsigned char> &vchCryptedSecret);
305305

306+
// Helper function to retrieve a conservative superset of all output scripts that may be relevant to this LegacyDataSPKM.
307+
// It may include scripts that are invalid or not actually watched by this LegacyDataSPKM.
308+
// Used only in migration.
309+
std::unordered_set<CScript, SaltedSipHasher> GetCandidateScriptPubKeys() const;
306310
public:
307311
using ScriptPubKeyMan::ScriptPubKeyMan;
308312

@@ -319,6 +323,7 @@ class LegacyDataSPKM : public ScriptPubKeyMan, public FillableSigningProvider
319323
uint256 GetID() const override { return uint256::ONE; }
320324
// TODO: Remove IsMine when deleting LegacyScriptPubKeyMan
321325
isminetype IsMine(const CScript& script) const override;
326+
bool CanProvide(const CScript& script, SignatureData& sigdata) override;
322327

323328
// FillableSigningProvider overrides
324329
bool HaveKey(const CKeyID &address) const override;
@@ -488,8 +493,6 @@ class LegacyScriptPubKeyMan : public LegacyDataSPKM
488493

489494
bool CanGetAddresses(bool internal = false) const override;
490495

491-
bool CanProvide(const CScript& script, SignatureData& sigdata) override;
492-
493496
bool SignTransaction(CMutableTransaction& tx, const std::map<COutPoint, Coin>& coins, int sighash, std::map<int, bilingual_str>& input_errors) const override;
494497
SigningResult SignMessage(const std::string& message, const PKHash& pkhash, std::string& str_sig) const override;
495498
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;

0 commit comments

Comments
 (0)