Skip to content

Commit

Permalink
wallet: Remove IsMine from migration
Browse files Browse the repository at this point in the history
As IsMine will be removed, the relevant components of IsMine are inlined
into the migration functions.
  • Loading branch information
achow101 committed Nov 20, 2024
1 parent 82d9743 commit a5a81cf
Showing 1 changed file with 174 additions and 28 deletions.
202 changes: 174 additions & 28 deletions src/wallet/scriptpubkeyman.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1717,42 +1717,193 @@ std::unordered_set<CScript, SaltedSipHasher> LegacyDataSPKM::GetScriptPubKeys()
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()) {
// Lambda helper to check that all keys found by the solver are compressed
const auto& all_keys_compressed = [](const std::vector<valtype>& keys) -> bool {
return std::all_of(keys.cbegin(), keys.cend(),
[](const auto& key) { return key.size() == CPubKey::COMPRESSED_SIZE; });
};

// mapScripts is iterated to compute all additional spendable output scripts that utilize the contained scripts
// as redeemScripts and witnessScripts.
//
// mapScripts contains redeemScripts and witnessScripts. It may also contain output scripts which,
// in addition to being treated as output scripts, are also treated as redeemScripts and witnessScripts.
// All scripts in mapScripts are treated as redeemScripts, unless that script is also a P2SH.
// A script is only treated as a witnessScript if there its corresponding P2WSH output script is in the map.
for (const auto& [_, script] : mapScripts) {
std::vector<std::vector<unsigned char>> solutions;
TxoutType type = Solver(script, solutions);
switch (type) {
// We don't care about these types because they are not spendable
case TxoutType::NONSTANDARD:
case TxoutType::NULL_DATA:
case TxoutType::WITNESS_UNKNOWN:
case TxoutType::ANCHOR:
// P2TR are not spendable as the legacy wallet never supported them.
case TxoutType::WITNESS_V1_TAPROOT:
// These are only spendable if the witness script is also spendable as a scriptPubKey
// We will check these later after "spks" has been updated with the computed output scripts from mapScripts.
case TxoutType::WITNESS_V0_SCRIPTHASH:
// For P2SH to be spendable, we need to have the redeemScript, and if we have it, it will be handled when
// this loop gets to it. A P2SH script itself cannot be nested in anything, so we can skip them.
case TxoutType::SCRIPTHASH:
break;
// Any P2PK or P2PKH scripts found in mapScripts can be spent as P2SH-P2PK or P2SH-P2PKH respectively,
// if we have the private key.
// Since all private keys were iterated earlier and their corresponding P2PK and P2PKH scripts inserted
// to "spks", we can simply check whether this P2PK or P2PKH script is in "spk" to determine spendability.
case TxoutType::PUBKEY:
case TxoutType::PUBKEYHASH:
if (spks.count(script) > 0) {
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) {
break;
// P2WPKH scripts are only spendable if we have the private key.
case TxoutType::WITNESS_V0_KEYHASH:
{
CKeyID key_id{uint160(solutions[0])};
CPubKey pubkey;
if (GetPubKey(key_id, pubkey) && pubkey.IsCompressed() && HaveKey(key_id)) {
spks.insert(script);
// Also insert P2SH-P2WPKH output script
spks.insert(GetScriptForDestination(ScriptHash(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);
}
break;
}
// Multisig scripts are spendable if they are inside of a P2SH or P2WSH, and we have all of the private keys.
// Bare multisigs are never considered spendable
case TxoutType::MULTISIG:
{
std::vector<std::vector<unsigned char>> keys(solutions.begin() + 1, solutions.begin() + solutions.size() - 1);
if (!HaveKeys(keys, *this)) {
break;
}
// Insert P2SH-Multisig
spks.insert(GetScriptForDestination(ScriptHash(script)));
// P2WSH-Multisig output scripts are spendable if the P2WSH output script is also in mapScripts,
// and all keys are compressed
CScript ms_wsh = GetScriptForDestination(WitnessV0ScriptHash(script));
if (HaveCScript(CScriptID(ms_wsh)) && all_keys_compressed(keys)) {
spks.insert(ms_wsh);
spks.insert(GetScriptForDestination(ScriptHash(ms_wsh)));
}
break;
}
}
}

// Enum to track the execution context of a script, similar to the script interpreter's SigVersion.
// It is separate to distinguish between top level scriptPubKey execution and P2SH redeemScript execution
// which SigVersion does not distinguish. It also excludes Taproot and Tapscript as the legacy wallet
// never supported those.
enum class ScriptContext {
TOP,
P2SH,
P2WSH,
};
// Lambda helper function to determine whether a script is valid, mainly looking at key compression requirements.
std::function<bool(const CScript&, const ScriptContext)> is_valid_script = [&](const CScript& script, const ScriptContext ctx) -> bool {
std::vector<valtype> solutions;
TxoutType spk_type = Solver(script, solutions);

CKeyID keyID;
switch (spk_type) {
// Scripts with no nesting (arbitrary, unknown scripts) are always valid.
case TxoutType::NONSTANDARD:
case TxoutType::NULL_DATA:
case TxoutType::WITNESS_UNKNOWN:
case TxoutType::ANCHOR:
// Taproot output scripts are always valid. Legacy wallets did not support Taproot spending so no nested inspection is required.
case TxoutType::WITNESS_V1_TAPROOT:
return ctx == ScriptContext::TOP;
// P2PK in any nesting level is valid, but inside of P2WSH, the pubkey must also be compressed.
case TxoutType::PUBKEY:
if (ctx == ScriptContext::P2WSH && solutions[0].size() != CPubKey::COMPRESSED_SIZE) return false;
return true;
// P2WPKH is allowed as an output script or in P2SH, but not inside of P2WSH
case TxoutType::WITNESS_V0_KEYHASH:
return ctx != ScriptContext::P2WSH;
// P2PKH in any nesting level is valid, but inside of P2WSH, the pubkey must also be compressed.
// If the pubkey cannot be retrieved to check for compression, then the P2WSH-P2PKH is allowed.
case TxoutType::PUBKEYHASH:
if (ctx == ScriptContext::P2WSH) {
CPubKey pubkey;
if (GetPubKey(CKeyID(uint160(solutions[0])), pubkey) && !pubkey.IsCompressed()) {
return false;
}
}
return true;
// P2SH is allowed only as an output script.
// If the redeemScript is known, it must also be a valid script within P2SH.
// If the redeemScript is not known, the P2SH output script is valid.
case TxoutType::SCRIPTHASH:
{
if (ctx != ScriptContext::TOP) return false;
CScriptID script_id = CScriptID(uint160(solutions[0]));
CScript subscript;
if (GetCScript(script_id, subscript)) {
return is_valid_script(subscript, ScriptContext::P2SH);
}
return true;
}
// P2WSH is allowed as an output script or inside of P2SH, but not P2WSH.
// If the witnessScript is known, it must also be a valid script within P2WSH.
// If the witnessScript is not known, the P2WSH output script is valid.
case TxoutType::WITNESS_V0_SCRIPTHASH:
{
if (ctx == ScriptContext::P2WSH) return false;
// CScriptID is the hash160 of the script. For P2WSH, we already have the SHA256 of the script,
// so only RIPEMD160 of that is required to get the CScriptID for lookup.
CScriptID script_id{RIPEMD160(solutions[0])};
CScript subscript;
if (GetCScript(script_id, subscript)) {
return is_valid_script(subscript, ScriptContext::P2WSH);
}
return true;
}
// Multisig in any nesting level is valid, but inside of P2WSH, all pubkeys must be compressed.
case TxoutType::MULTISIG:
{
if (ctx == ScriptContext::P2WSH) {
std::vector<std::vector<unsigned char>> keys(solutions.begin() + 1, solutions.begin() + solutions.size() - 1);
if (!all_keys_compressed(keys)) {
return false;
}
}
return true;
}
}
assert(false);
};
// Iterate again for all the P2WSH scripts
for (const auto& [id, script] : mapScripts) {
std::vector<std::vector<unsigned char>> solutions;
TxoutType type = Solver(script, solutions);
if (type == TxoutType::WITNESS_V0_SCRIPTHASH) {
CScript witness_script;
int wit_ver = -1;
std::vector<unsigned char> wit_prog;
CScriptID script_id{RIPEMD160(solutions[0])};
// For a P2WSH output script to be spendable, we must know and inspect its witnessScript.
if (GetCScript(script_id, witness_script) &&
!witness_script.IsPayToScriptHash() && // P2SH inside of P2WSH is not allowed
!witness_script.IsWitnessProgram(wit_ver, wit_prog) && // Witness programs are not allowed inside of P2WSH
// We only allow scripts that we would consider spendable as an output script.
// Note that while this would exclude P2WSH-multisigs, we are already handling those in the first loop.
spks.count(witness_script) > 0 &&
// Pubkeys must be compressed.
is_valid_script(witness_script, ScriptContext::P2WSH)) {
spks.insert(script);
spks.insert(GetScriptForDestination(ScriptHash(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);
if (is_valid_script(script, ScriptContext::TOP)) spks.insert(script);
}

return spks;
Expand Down Expand Up @@ -1845,7 +1996,6 @@ std::optional<MigrationData> LegacyDataSPKM::MigrateToDescriptor()
for (const CScript& spk : desc_spks) {
size_t erased = spks.erase(spk);
assert(erased == 1);
assert(IsMine(spk) == ISMINE_SPENDABLE);
}

out.desc_spkms.push_back(std::move(desc_spk_man));
Expand Down Expand Up @@ -1891,7 +2041,6 @@ std::optional<MigrationData> LegacyDataSPKM::MigrateToDescriptor()
for (const CScript& spk : desc_spks) {
size_t erased = spks.erase(spk);
assert(erased == 1);
assert(IsMine(spk) == ISMINE_SPENDABLE);
}

out.desc_spkms.push_back(std::move(desc_spk_man));
Expand Down Expand Up @@ -1963,7 +2112,6 @@ std::optional<MigrationData> LegacyDataSPKM::MigrateToDescriptor()
for (const CScript& desc_spk : desc_spks) {
auto del_it = spks.find(desc_spk);
assert(del_it != spks.end());
assert(IsMine(desc_spk) != ISMINE_NO);
it = spks.erase(del_it);
}
}
Expand Down Expand Up @@ -1998,8 +2146,6 @@ std::optional<MigrationData> LegacyDataSPKM::MigrateToDescriptor()
// * 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);
continue;
}
assert(IsMine(sh_spk) == ISMINE_NO && IsMine(witprog) == ISMINE_NO && IsMine(sh_wsh_spk) == ISMINE_NO);
Expand Down

0 comments on commit a5a81cf

Please sign in to comment.