Skip to content

Commit c1959d3

Browse files
committed
fixup: derive BIP388 for some basic descriptor types
1 parent 9e1bd7a commit c1959d3

File tree

2 files changed

+149
-11
lines changed

2 files changed

+149
-11
lines changed

src/wallet/wallet.cpp

Lines changed: 130 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@
3535
#include <psbt.h>
3636
#include <pubkey.h>
3737
#include <random.h>
38+
#include <regex>
3839
#include <script/descriptor.h>
40+
#include <script/parsing.h>
3941
#include <script/interpreter.h>
4042
#include <script/script.h>
4143
#include <script/sign.h>
@@ -2579,29 +2581,146 @@ util::Result<void> CWallet::DisplayAddress(const CTxDestination& dest)
25792581
return util::Error{_("There is no ScriptPubKeyManager for this address")};
25802582
}
25812583

2582-
util::Result<std::string> CWallet::RegisterPolicy(std::optional<std::string> name)
2584+
// TODO:
2585+
// - apply furth BIP388 restrictions
2586+
// - avoid string parsing, add properties to Descriptor instead
2587+
bool CWallet::IsCandidateForBIP388Policy(DescriptorScriptPubKeyMan& spkm)
25832588
{
2584-
// TODO: use wallet name as default
2585-
const std::string policy_name{name.has_value() ? *name : "Core"};
2589+
std::string desc;
2590+
if (!Assume(spkm.GetDescriptorString(desc, /*priv=*/false))) return false;
25862591

2587-
// TODO: derive policy from descriptor (fail if not possible)
2588-
const std::string descriptor_template{"wsh(multi(@0,@1))"};
2592+
std::span<const char> sp{desc};
25892593

2590-
// TODO: extract key information
2591-
const std::vector<std::string> keys_info{
2592-
"[00000001/47h/1h/0h]tpubD6NzVbkrYhZ4YNXVQbNhMK1WqguFsUXceaVJKbmno2aZ3B6QfbMeraaYvnBSGpV3vxLyTTK9DYT1yoEck4XUScMzXoQ2U2oSmE2JyMedq3H",
2593-
"[00000001/47h/1h/0h]tpubDAXcJ7s7ZwicqjprRaEWdPoHKrCS215qxGYxpusRLLmJuT69ZSicuGdSfyvyKpvUNYBW1s2U3NSrT6vrCYB9e6nZUEvrqnwXPF8ArTCRXMY"
2594-
};
2594+
if (!(script::Const("tr(", sp, /*skip=*/true) || script::Const("wsh(", sp, /*skip=*/true))) return false;
2595+
// Must be MuSig2, have a leaf script or segwit multisig
2596+
if (desc.find(',') == std::string::npos) return false;
2597+
2598+
return true;
2599+
}
2600+
2601+
util::Result<std::pair<std::string, std::vector<std::string>>> CWallet::DerivePolicy(std::optional<std::pair<DescriptorScriptPubKeyMan&, DescriptorScriptPubKeyMan&>> spk_pair)
2602+
{
2603+
std::string receive_descriptor;
2604+
std::string change_descriptor;
2605+
2606+
DescriptorScriptPubKeyMan* receive{nullptr};
2607+
DescriptorScriptPubKeyMan* change{nullptr};
2608+
2609+
if (spk_pair) {
2610+
if (!IsCandidateForBIP388Policy(spk_pair->first) || !IsCandidateForBIP388Policy(spk_pair->second)) {
2611+
return util::Error{_("Provided descriptors are not compatible with BIP388")};
2612+
}
2613+
receive = &spk_pair->first;
2614+
change = &spk_pair->second;
2615+
} else {
2616+
for (bool internal : {false, true}) {
2617+
// TODO: support P2SH.
2618+
for (const OutputType type : {OutputType::BECH32M, OutputType::BECH32}) {
2619+
// Only look for a single candidate
2620+
if (!internal && !receive_descriptor.empty()) continue;
2621+
if (internal && !change_descriptor.empty()) continue;
2622+
2623+
auto spk_man = GetScriptPubKeyMan(type, internal);
2624+
if (!Assume(spk_man)) continue;
2625+
auto desc_spk_man = dynamic_cast<DescriptorScriptPubKeyMan *>(spk_man);
2626+
if (!Assume(desc_spk_man)) continue;
2627+
2628+
if (!IsCandidateForBIP388Policy(*desc_spk_man)) continue;
2629+
2630+
if (internal) {
2631+
change = desc_spk_man;
2632+
} else {
2633+
receive = desc_spk_man;
2634+
}
2635+
}
2636+
}
2637+
}
2638+
2639+
if (!receive || !change) {
2640+
return util::Error{_("No suitable descriptors found or provided for BIP388 policy")};
2641+
}
2642+
2643+
bool res{receive->GetDescriptorString(receive_descriptor, /*priv=*/false)};
2644+
res &= change->GetDescriptorString(change_descriptor, /*priv=*/false);
2645+
if (!Assume(res)) {
2646+
return util::Error{_("Failed to get descriptor strings")};
2647+
}
2648+
2649+
// TODO: render (candidate) policy directly from Descriptor class and extract
2650+
// keys there.
2651+
2652+
// Lookup keys in the receive descriptor.
2653+
std::vector<std::string> keys_info;
2654+
const std::regex key_regex{R"(((?:\[[a-f\dh'/]{8,}\])?[\w]{10,})[,\)\/])"};
2655+
auto search_begin = std::sregex_token_iterator(receive_descriptor.begin(), receive_descriptor.end(), key_regex, 1);
2656+
auto search_end = std::sregex_token_iterator();
2657+
2658+
for (std::regex_token_iterator i = search_begin; i != search_end; ++i) {
2659+
std::string key{i->str()};
2660+
if (std::find(keys_info.begin(), keys_info.end(), key) != keys_info.end()) continue;
2661+
keys_info.push_back(key);
2662+
}
2663+
2664+
// Replace keys in descriptor swith @1, etc
2665+
// - convert /0/* in the receive descriptor to /**
2666+
// - convert /1/* in the change descriptor to /**
2667+
// - TODO: support arbitrary /<NUM;NUM>/*
2668+
// - also drop checksum
2669+
std::string descriptor_template{receive_descriptor.substr(0, receive_descriptor.size() - 9)};
2670+
std::string descriptor_template_check{change_descriptor.substr(0, change_descriptor.size() - 9)};
2671+
uint32_t key_index = 0;
2672+
for (auto& key_info : keys_info) {
2673+
util::ReplaceAll(descriptor_template, key_info, strprintf("@%d", key_index), /*regex=*/false);
2674+
util::ReplaceAll(descriptor_template_check, key_info, strprintf("@%d", key_index), /*regex=*/false);
2675+
key_index++;
2676+
}
2677+
2678+
// Replace /0/* with /@0/**
2679+
util::ReplaceAll(descriptor_template, "/0/*", "/**", /*regex=*/false);
2680+
2681+
// Replace /1/* with /@0/**
2682+
util::ReplaceAll(descriptor_template_check, "/1/*", "/**", /*regex=*/false);
2683+
2684+
// The receive and change descriptor should now be identical
2685+
if (descriptor_template != descriptor_template_check) {
2686+
return util::Error{Untranslated(strprintf(
2687+
"Receive and change descriptors incompatible for BIP388 policy registration:\n%s\n%s\nKey info:\n%s",
2688+
descriptor_template.c_str(),
2689+
descriptor_template_check.c_str(),
2690+
util::Join(keys_info, "\n")
2691+
))};
2692+
}
2693+
2694+
// Replace h with ' in key info:
2695+
for (std::string& key : keys_info) {
2696+
util::ReplaceAll(key, "h/", "'/", /*regex=*/false);
2697+
util::ReplaceAll(key, "h]", "']", /*regex=*/false);
2698+
}
2699+
2700+
return std::pair<std::string, std::vector<std::string>>{descriptor_template, keys_info};
2701+
}
2702+
2703+
util::Result<std::string> CWallet::RegisterPolicy(std::optional<std::string> name)
2704+
{
2705+
const std::string policy_name{name.has_value() ? *name : m_name};
2706+
2707+
// A wallet with multiple descriptors could have multiple BIP388 policies,
2708+
// but this is currently unsupported. DerivePolicy just picks one.
2709+
const auto res{DerivePolicy(/*spk_pair=*/std::nullopt)};
2710+
if (!res) return util::Error{util::ErrorString(res)};
2711+
const std::string& descriptor_template{res->first};
2712+
const std::vector<std::string>& keys_info{res->second};
25952713

25962714
for (const auto& spk_man : GetActiveScriptPubKeyMans()) {
25972715
auto signer_spk_man = dynamic_cast<ExternalSignerScriptPubKeyMan *>(spk_man);
25982716
if (signer_spk_man == nullptr) {
25992717
continue;
26002718
}
26012719
auto signer{ExternalSignerScriptPubKeyMan::GetExternalSigner()};
2602-
if (!signer) return util::ErrorString(signer).original;
2720+
if (!signer) return util::Error{util::ErrorString(signer)};
26032721

26042722
util::Result<std::string> res{signer_spk_man->RegisterPolicy(*signer, policy_name, descriptor_template, keys_info)};
2723+
if (!res) return util::Error{util::ErrorString(res)};
26052724

26062725
if (res) {
26072726
// Store hmac in wallet

src/wallet/wallet.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,25 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
578578
/** Display address on an external signer. */
579579
util::Result<void> DisplayAddress(const CTxDestination& dest) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
580580

581+
/** Determine if the SPKM descriptor is compatible with BIP388 */
582+
bool IsCandidateForBIP388Policy(DescriptorScriptPubKeyMan& spkm) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
583+
584+
/**
585+
* Derive a BIP388 policy from a pair of descriptor scriptpubkey manangers.
586+
*
587+
* Ignores trivial single sig policies: pkh(KEY), wpkh(KEY), sh(wpkh(KEY))
588+
* and tr(KEY)
589+
*
590+
* If no SKPM pair is provided, look for the first suitable pair.
591+
*
592+
* @param[in] spk_pair The receive and change SKPM to use.
593+
*
594+
* @return a string containing the BIP388 policy and array of strings
595+
* containing the key information. An error if no suitable
596+
* descriptors were found.
597+
*/
598+
util::Result<std::pair<std::string, std::vector<std::string>>> DerivePolicy(std::optional<std::pair<DescriptorScriptPubKeyMan&, DescriptorScriptPubKeyMan&>> spk_pair) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
599+
581600
/** Register BIP388 on an external signer. Store and return the resulting hmac. */
582601
util::Result<std::string> RegisterPolicy(std::optional<std::string> name) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
583602

0 commit comments

Comments
 (0)