Skip to content

Commit abe4fed

Browse files
committed
Merge bitcoin/bitcoin#28125: wallet: bugfix, disallow migration of invalid scripts
8e7e3e6 test: wallet, verify migration doesn't crash for an invalid script (furszy) 1de8a23 wallet: disallow migration of invalid or not-watched scripts (furszy) Pull request description: Fixing #28057. The legacy wallet allows to import any raw script (#28126), without checking if it was valid or not. Appending it to the watch-only set. This causes a crash in the migration process because we are only expecting to find valid scripts inside the legacy spkm. These stored scripts internally map to `ISMINE_NO` (same as if they weren't stored at all..). So we need to check for these special case, and take into account that the legacy spkm could be storing invalid not watched scripts. Which, in code words, means `IsMineInner()` returning `IsMineResult::INVALID` for them. Note: To verify this, can run the test commit on top of master. `wallet_migration.py` will crash without the bugfix commit. ACKs for top commit: achow101: ACK 8e7e3e6 Tree-SHA512: c2070e8ba78037a8f573b05bf6caa672803188f05429adf5b93f9fc1493faedadecdf018dee9ead27c656710558c849c5da8ca5f6f3bc9c23b3c4275d2fb50c7
2 parents 53313c4 + 8e7e3e6 commit abe4fed

File tree

4 files changed

+66
-1
lines changed

4 files changed

+66
-1
lines changed

src/wallet/scriptpubkeyman.cpp

+16-1
Original file line numberDiff line numberDiff line change
@@ -1716,8 +1716,23 @@ std::unordered_set<CScript, SaltedSipHasher> LegacyScriptPubKeyMan::GetScriptPub
17161716
}
17171717

17181718
// All watchonly scripts are raw
1719-
spks.insert(setWatchOnly.begin(), setWatchOnly.end());
1719+
for (const CScript& script : setWatchOnly) {
1720+
// As the legacy wallet allowed to import any script, we need to verify the validity here.
1721+
// LegacyScriptPubKeyMan::IsMine() return 'ISMINE_NO' for invalid or not watched scripts (IsMineResult::INVALID or IsMineResult::NO).
1722+
// e.g. a "sh(sh(pkh()))" which legacy wallets allowed to import!.
1723+
if (IsMine(script) != ISMINE_NO) spks.insert(script);
1724+
}
1725+
1726+
return spks;
1727+
}
17201728

1729+
std::unordered_set<CScript, SaltedSipHasher> LegacyScriptPubKeyMan::GetNotMineScriptPubKeys() const
1730+
{
1731+
LOCK(cs_KeyStore);
1732+
std::unordered_set<CScript, SaltedSipHasher> spks;
1733+
for (const CScript& script : setWatchOnly) {
1734+
if (IsMine(script) == ISMINE_NO) spks.insert(script);
1735+
}
17211736
return spks;
17221737
}
17231738

src/wallet/scriptpubkeyman.h

+6
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,12 @@ class LegacyScriptPubKeyMan : public ScriptPubKeyMan, public FillableSigningProv
525525
std::set<CKeyID> GetKeys() const override;
526526
std::unordered_set<CScript, SaltedSipHasher> GetScriptPubKeys() const override;
527527

528+
/**
529+
* Retrieves scripts that were imported by bugs into the legacy spkm and are
530+
* simply invalid, such as a sh(sh(pkh())) script, or not watched.
531+
*/
532+
std::unordered_set<CScript, SaltedSipHasher> GetNotMineScriptPubKeys() const;
533+
528534
/** Get the DescriptorScriptPubKeyMans (with private keys) that have the same scriptPubKeys as this LegacyScriptPubKeyMan.
529535
* Does not modify this ScriptPubKeyMan. */
530536
std::optional<MigrationData> MigrateToDescriptor();

src/wallet/wallet.cpp

+14
Original file line numberDiff line numberDiff line change
@@ -3860,6 +3860,13 @@ bool CWallet::ApplyMigrationData(MigrationData& data, bilingual_str& error)
38603860
return false;
38613861
}
38623862

3863+
// Get all invalid or non-watched scripts that will not be migrated
3864+
std::set<CTxDestination> not_migrated_dests;
3865+
for (const auto& script : legacy_spkm->GetNotMineScriptPubKeys()) {
3866+
CTxDestination dest;
3867+
if (ExtractDestination(script, dest)) not_migrated_dests.emplace(dest);
3868+
}
3869+
38633870
for (auto& desc_spkm : data.desc_spkms) {
38643871
if (m_spk_managers.count(desc_spkm->GetID()) > 0) {
38653872
error = _("Error: Duplicate descriptors created during migration. Your wallet may be corrupted.");
@@ -3966,6 +3973,13 @@ bool CWallet::ApplyMigrationData(MigrationData& data, bilingual_str& error)
39663973
continue;
39673974
}
39683975
}
3976+
3977+
// Skip invalid/non-watched scripts that will not be migrated
3978+
if (not_migrated_dests.count(addr_pair.first) > 0) {
3979+
dests_to_delete.push_back(addr_pair.first);
3980+
continue;
3981+
}
3982+
39693983
// Not ours, not in watchonly wallet, and not in solvable
39703984
error = _("Error: Address book data in wallet cannot be identified to belong to migrated wallets");
39713985
return false;

test/functional/wallet_migration.py

+30
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import random
88
import shutil
9+
from test_framework.address import script_to_p2sh
910
from test_framework.descriptors import descsum_create
1011
from test_framework.test_framework import BitcoinTestFramework
1112
from test_framework.messages import COIN, CTransaction, CTxOut
@@ -683,6 +684,21 @@ def send_to_script(script, amount):
683684
wallet.rpc.importaddress(address=script_wsh_pkh.hex(), label="raw_spk2", rescan=True, p2sh=False)
684685
assert_equal(wallet.getbalances()['watchonly']['trusted'], 5)
685686

687+
# Import sh(pkh()) script, by using importaddress(), with the p2sh flag enabled.
688+
# This will wrap the script under another sh level, which is invalid!, and store it inside the wallet.
689+
# The migration process must skip the invalid scripts and the addressbook records linked to them.
690+
# They are not being watched by the current wallet, nor should be watched by the migrated one.
691+
label_sh_pkh = "raw_sh_pkh"
692+
script_pkh = key_to_p2pkh_script(df_wallet.getaddressinfo(df_wallet.getnewaddress())["pubkey"])
693+
script_sh_pkh = script_to_p2sh_script(script_pkh)
694+
addy_script_sh_pkh = script_to_p2sh(script_pkh) # valid script address
695+
addy_script_double_sh_pkh = script_to_p2sh(script_sh_pkh) # invalid script address
696+
697+
# Note: 'importaddress()' will add two scripts, a valid one sh(pkh()) and an invalid one 'sh(sh(pkh()))'.
698+
# Both of them will be stored with the same addressbook label. And only the latter one should
699+
# be discarded during migration. The first one must be migrated.
700+
wallet.rpc.importaddress(address=script_sh_pkh.hex(), label=label_sh_pkh, rescan=False, p2sh=True)
701+
686702
# Migrate wallet and re-check balance
687703
info_migration = wallet.migratewallet()
688704
wallet_wo = self.nodes[0].get_wallet_rpc(info_migration["watchonly_name"])
@@ -692,6 +708,20 @@ def send_to_script(script, amount):
692708
# The watch-only scripts are no longer part of the main wallet
693709
assert_equal(wallet.getbalances()['mine']['trusted'], 0)
694710

711+
# The invalid sh(sh(pk())) script label must not be part of the main wallet anymore
712+
assert label_sh_pkh not in wallet.listlabels()
713+
# But, the standard sh(pkh()) script should be part of the watch-only wallet.
714+
addrs_by_label = wallet_wo.getaddressesbylabel(label_sh_pkh)
715+
assert addy_script_sh_pkh in addrs_by_label
716+
assert addy_script_double_sh_pkh not in addrs_by_label
717+
718+
# Also, the watch-only wallet should have the descriptor for the standard sh(pkh())
719+
desc = descsum_create(f"addr({addy_script_sh_pkh})")
720+
assert next(it['desc'] for it in wallet_wo.listdescriptors()['descriptors'] if it['desc'] == desc)
721+
# And doesn't have a descriptor for the invalid one
722+
desc_invalid = descsum_create(f"addr({addy_script_double_sh_pkh})")
723+
assert_equal(next((it['desc'] for it in wallet_wo.listdescriptors()['descriptors'] if it['desc'] == desc_invalid), None), None)
724+
695725
# Just in case, also verify wallet restart
696726
self.nodes[0].unloadwallet(info_migration["watchonly_name"])
697727
self.nodes[0].loadwallet(info_migration["watchonly_name"])

0 commit comments

Comments
 (0)