From 7e86541f723d62c7ec6768f7f592c09ba2047d9e Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Wed, 7 Aug 2024 16:28:57 -0400 Subject: [PATCH 01/14] descriptors: Add PubkeyProvider::Clone --- src/script/descriptor.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp index 0987db194c4..46f3e9677f9 100644 --- a/src/script/descriptor.cpp +++ b/src/script/descriptor.cpp @@ -219,6 +219,9 @@ public: virtual std::optional GetRootPubKey() const = 0; /** Return the extended public key for this PubkeyProvider, if it has one. */ virtual std::optional GetRootExtPubKey() const = 0; + + /** Make a deep copy of this PubkeyProvider */ + virtual std::unique_ptr Clone() const = 0; }; class OriginPubkeyProvider final : public PubkeyProvider @@ -280,6 +283,10 @@ public: { return m_provider->GetRootExtPubKey(); } + std::unique_ptr Clone() const override + { + return std::make_unique(m_expr_index, m_origin, m_provider->Clone(), m_apostrophe); + } }; /** An object representing a parsed constant public key in a descriptor. */ @@ -333,6 +340,10 @@ public: { return std::nullopt; } + std::unique_ptr Clone() const override + { + return std::make_unique(m_expr_index, m_pubkey, m_xonly); + } }; enum class DeriveType { @@ -556,6 +567,10 @@ public: { return m_root_extkey; } + std::unique_ptr Clone() const override + { + return std::make_unique(m_expr_index, m_root_extkey, m_path, m_derive, m_apostrophe); + } }; /** Base class for all Descriptor implementations. */ From 0d55deae157f4f8226b2419d55e7dc0dfb6e4aec Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Wed, 7 Aug 2024 16:28:59 -0400 Subject: [PATCH 02/14] descriptors: Add DescriptorImpl::Clone --- src/script/descriptor.cpp | 81 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp index 46f3e9677f9..08416f0d5f2 100644 --- a/src/script/descriptor.cpp +++ b/src/script/descriptor.cpp @@ -786,6 +786,8 @@ public: arg->GetPubKeys(pubkeys, ext_pubs); } } + + virtual std::unique_ptr Clone() const = 0; }; /** A parsed addr(A) descriptor. */ @@ -807,6 +809,10 @@ public: bool ToPrivateString(const SigningProvider& arg, std::string& out) const final { return false; } std::optional ScriptSize() const override { return GetScriptForDestination(m_destination).size(); } + std::unique_ptr Clone() const override + { + return std::make_unique(m_destination); + } }; /** A parsed raw(H) descriptor. */ @@ -830,6 +836,11 @@ public: bool ToPrivateString(const SigningProvider& arg, std::string& out) const final { return false; } std::optional ScriptSize() const override { return m_script.size(); } + + std::unique_ptr Clone() const override + { + return std::make_unique(m_script); + } }; /** A parsed pk(P) descriptor. */ @@ -865,6 +876,11 @@ public: } std::optional MaxSatisfactionElems() const override { return 1; } + + std::unique_ptr Clone() const override + { + return std::make_unique(m_pubkey_args.at(0)->Clone(), m_xonly); + } }; /** A parsed pkh(P) descriptor. */ @@ -894,6 +910,11 @@ public: } std::optional MaxSatisfactionElems() const override { return 2; } + + std::unique_ptr Clone() const override + { + return std::make_unique(m_pubkey_args.at(0)->Clone()); + } }; /** A parsed wpkh(P) descriptor. */ @@ -923,6 +944,11 @@ public: } std::optional MaxSatisfactionElems() const override { return 2; } + + std::unique_ptr Clone() const override + { + return std::make_unique(m_pubkey_args.at(0)->Clone()); + } }; /** A parsed combo(P) descriptor. */ @@ -947,6 +973,10 @@ protected: public: ComboDescriptor(std::unique_ptr prov) : DescriptorImpl(Vector(std::move(prov)), "combo") {} bool IsSingleType() const final { return false; } + std::unique_ptr Clone() const override + { + return std::make_unique(m_pubkey_args.at(0)->Clone()); + } }; /** A parsed multi(...) or sortedmulti(...) descriptor */ @@ -985,6 +1015,14 @@ public: } std::optional MaxSatisfactionElems() const override { return 1 + m_threshold; } + + std::unique_ptr Clone() const override + { + std::vector> providers; + providers.reserve(m_pubkey_args.size()); + std::transform(m_pubkey_args.begin(), m_pubkey_args.end(), providers.begin(), [](const std::unique_ptr& p) { return p->Clone(); }); + return std::make_unique(m_threshold, std::move(providers), m_sorted); + } }; /** A parsed (sorted)multi_a(...) descriptor. Always uses x-only pubkeys. */ @@ -1021,6 +1059,16 @@ public: } std::optional MaxSatisfactionElems() const override { return m_pubkey_args.size(); } + + std::unique_ptr Clone() const override + { + std::vector> providers; + providers.reserve(m_pubkey_args.size()); + for (const auto& arg : m_pubkey_args) { + providers.push_back(arg->Clone()); + } + return std::make_unique(m_threshold, std::move(providers), m_sorted); + } }; /** A parsed sh(...) descriptor. */ @@ -1066,6 +1114,11 @@ public: if (const auto sub_elems = m_subdescriptor_args[0]->MaxSatisfactionElems()) return 1 + *sub_elems; return {}; } + + std::unique_ptr Clone() const override + { + return std::make_unique(m_subdescriptor_args.at(0)->Clone()); + } }; /** A parsed wsh(...) descriptor. */ @@ -1102,6 +1155,11 @@ public: if (const auto sub_elems = m_subdescriptor_args[0]->MaxSatisfactionElems()) return 1 + *sub_elems; return {}; } + + std::unique_ptr Clone() const override + { + return std::make_unique(m_subdescriptor_args.at(0)->Clone()); + } }; /** A parsed tr(...) descriptor. */ @@ -1167,6 +1225,14 @@ public: // FIXME: See above, we assume keypath spend. return 1; } + + std::unique_ptr Clone() const override + { + std::vector> subdescs; + subdescs.reserve(m_subdescriptor_args.size()); + std::transform(m_subdescriptor_args.begin(), m_subdescriptor_args.end(), subdescs.begin(), [](const std::unique_ptr& d) { return d->Clone(); }); + return std::make_unique(m_pubkey_args.at(0)->Clone(), std::move(subdescs), m_depths); + } }; /* We instantiate Miniscript here with a simple integer as key type. @@ -1285,6 +1351,16 @@ public: std::optional MaxSatisfactionElems() const override { return m_node->GetStackSize(); } + + std::unique_ptr Clone() const override + { + std::vector> providers; + providers.reserve(m_pubkey_args.size()); + for (const auto& arg : m_pubkey_args) { + providers.push_back(arg->Clone()); + } + return std::make_unique(std::move(providers), miniscript::MakeNodeRef(*m_node)); + } }; /** A parsed rawtr(...) descriptor. */ @@ -1315,6 +1391,11 @@ public: // See above, we assume keypath spend. return 1; } + + std::unique_ptr Clone() const override + { + return std::make_unique(m_pubkey_args.at(0)->Clone()); + } }; //////////////////////////////////////////////////////////////////////////// From a5f39b103461a98689fd5d382e8da29037f55bea Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Wed, 7 Aug 2024 16:29:01 -0400 Subject: [PATCH 03/14] descriptors: Change ParseScript to return vector of descriptors To prepare for returning multipath descriptors which will be a shorthand for specifying multiple descriptors, change ParseScript's signature to return a vector. --- src/script/descriptor.cpp | 358 ++++++++++++++++++++++++++------------ 1 file changed, 248 insertions(+), 110 deletions(-) diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp index 08416f0d5f2..bb4bc2656f2 100644 --- a/src/script/descriptor.cpp +++ b/src/script/descriptor.cpp @@ -1446,14 +1446,15 @@ enum class ParseScriptContext { } /** Parse a public key that excludes origin information. */ -std::unique_ptr ParsePubkeyInner(uint32_t key_exp_index, const Span& sp, ParseScriptContext ctx, FlatSigningProvider& out, bool& apostrophe, std::string& error) +std::vector> ParsePubkeyInner(uint32_t key_exp_index, const Span& sp, ParseScriptContext ctx, FlatSigningProvider& out, bool& apostrophe, std::string& error) { + std::vector> ret; bool permit_uncompressed = ctx == ParseScriptContext::TOP || ctx == ParseScriptContext::P2SH; auto split = Split(sp, '/'); std::string str(split[0].begin(), split[0].end()); if (str.size() == 0) { error = "No key provided"; - return nullptr; + return {}; } if (split.size() == 1) { if (IsHex(str)) { @@ -1461,35 +1462,38 @@ std::unique_ptr ParsePubkeyInner(uint32_t key_exp_index, const S CPubKey pubkey(data); if (pubkey.IsValid() && !pubkey.IsValidNonHybrid()) { error = "Hybrid public keys are not allowed"; - return nullptr; + return {}; } if (pubkey.IsFullyValid()) { if (permit_uncompressed || pubkey.IsCompressed()) { - return std::make_unique(key_exp_index, pubkey, false); + ret.emplace_back(std::make_unique(key_exp_index, pubkey, false)); + return ret; } else { error = "Uncompressed keys are not allowed"; - return nullptr; + return {}; } } else if (data.size() == 32 && ctx == ParseScriptContext::P2TR) { unsigned char fullkey[33] = {0x02}; std::copy(data.begin(), data.end(), fullkey + 1); pubkey.Set(std::begin(fullkey), std::end(fullkey)); if (pubkey.IsFullyValid()) { - return std::make_unique(key_exp_index, pubkey, true); + ret.emplace_back(std::make_unique(key_exp_index, pubkey, true)); + return ret; } } error = strprintf("Pubkey '%s' is invalid", str); - return nullptr; + return {}; } CKey key = DecodeSecret(str); if (key.IsValid()) { if (permit_uncompressed || key.IsCompressed()) { CPubKey pubkey = key.GetPubKey(); out.keys.emplace(pubkey.GetID(), key); - return std::make_unique(key_exp_index, pubkey, ctx == ParseScriptContext::P2TR); + ret.emplace_back(std::make_unique(key_exp_index, pubkey, ctx == ParseScriptContext::P2TR)); + return ret; } else { error = "Uncompressed keys are not allowed"; - return nullptr; + return {}; } } } @@ -1497,7 +1501,7 @@ std::unique_ptr ParsePubkeyInner(uint32_t key_exp_index, const S CExtPubKey extpubkey = DecodeExtPubKey(str); if (!extkey.key.IsValid() && !extpubkey.pubkey.IsValid()) { error = strprintf("key '%s' is not valid", str); - return nullptr; + return {}; } KeyPath path; DeriveType type = DeriveType::NO; @@ -1509,21 +1513,23 @@ std::unique_ptr ParsePubkeyInner(uint32_t key_exp_index, const S split.pop_back(); type = DeriveType::HARDENED; } - if (!ParseKeyPath(split, path, apostrophe, error)) return nullptr; + if (!ParseKeyPath(split, path, apostrophe, error)) return {}; if (extkey.key.IsValid()) { extpubkey = extkey.Neuter(); out.keys.emplace(extpubkey.pubkey.GetID(), extkey.key); } - return std::make_unique(key_exp_index, extpubkey, std::move(path), type, apostrophe); + ret.emplace_back(std::make_unique(key_exp_index, extpubkey, std::move(path), type, apostrophe)); + return ret; } /** Parse a public key including origin information (if enabled). */ -std::unique_ptr ParsePubkey(uint32_t key_exp_index, const Span& sp, ParseScriptContext ctx, FlatSigningProvider& out, std::string& error) +std::vector> ParsePubkey(uint32_t key_exp_index, const Span& sp, ParseScriptContext ctx, FlatSigningProvider& out, std::string& error) { + std::vector> ret; auto origin_split = Split(sp, ']'); if (origin_split.size() > 2) { error = "Multiple ']' characters found for a single pubkey"; - return nullptr; + return {}; } // This is set if either the origin or path suffix contains a hardened derivation. bool apostrophe = false; @@ -1533,27 +1539,31 @@ std::unique_ptr ParsePubkey(uint32_t key_exp_index, const Span(key_exp_index, std::move(info), std::move(provider), apostrophe); + if (!ParseKeyPath(slash_split, info.path, apostrophe, error)) return {}; + auto providers = ParsePubkeyInner(key_exp_index, origin_split[1], ctx, out, apostrophe, error); + if (providers.empty()) return {}; + ret.reserve(providers.size()); + for (auto& prov : providers) { + ret.emplace_back(std::make_unique(key_exp_index, info, std::move(prov), apostrophe)); + } + return ret; } std::unique_ptr InferPubkey(const CPubKey& pubkey, ParseScriptContext ctx, const SigningProvider& provider) @@ -1595,8 +1605,8 @@ struct KeyParser { FlatSigningProvider* m_out; //! Must not be nullptr if parsing from Script. const SigningProvider* m_in; - //! List of keys contained in the Miniscript. - mutable std::vector> m_keys; + //! List of multipath expanded keys contained in the Miniscript. + mutable std::vector>> m_keys; //! Used to detect key parsing errors within a Miniscript. mutable std::string m_key_parsing_error; //! The script context we're operating within (Tapscript or P2WSH). @@ -1609,7 +1619,7 @@ struct KeyParser { : m_out(out), m_in(in), m_script_ctx(ctx), m_offset(offset) {} bool KeyCompare(const Key& a, const Key& b) const { - return *m_keys.at(a) < *m_keys.at(b); + return *m_keys.at(a).at(0) < *m_keys.at(b).at(0); } ParseScriptContext ParseContext() const { @@ -1625,14 +1635,14 @@ struct KeyParser { assert(m_out); Key key = m_keys.size(); auto pk = ParsePubkey(m_offset + key, {&*begin, &*end}, ParseContext(), *m_out, m_key_parsing_error); - if (!pk) return {}; - m_keys.push_back(std::move(pk)); + if (pk.empty()) return {}; + m_keys.emplace_back(std::move(pk)); return key; } std::optional ToString(const Key& key) const { - return m_keys.at(key)->ToString(); + return m_keys.at(key).at(0)->ToString(); } template std::optional FromPKBytes(I begin, I end) const @@ -1643,13 +1653,15 @@ struct KeyParser { XOnlyPubKey pubkey; std::copy(begin, end, pubkey.begin()); if (auto pubkey_provider = InferPubkey(pubkey.GetEvenCorrespondingCPubKey(), ParseContext(), *m_in)) { - m_keys.push_back(std::move(pubkey_provider)); + m_keys.emplace_back(); + m_keys.back().push_back(std::move(pubkey_provider)); return key; } } else if (!miniscript::IsTapscript(m_script_ctx)) { CPubKey pubkey(begin, end); if (auto pubkey_provider = InferPubkey(pubkey, ParseContext(), *m_in)) { - m_keys.push_back(std::move(pubkey_provider)); + m_keys.emplace_back(); + m_keys.back().push_back(std::move(pubkey_provider)); return key; } } @@ -1667,7 +1679,8 @@ struct KeyParser { if (m_in->GetPubKey(keyid, pubkey)) { if (auto pubkey_provider = InferPubkey(pubkey, ParseContext(), *m_in)) { Key key = m_keys.size(); - m_keys.push_back(std::move(pubkey_provider)); + m_keys.emplace_back(); + m_keys.back().push_back(std::move(pubkey_provider)); return key; } } @@ -1681,44 +1694,54 @@ struct KeyParser { /** Parse a script in a particular context. */ // NOLINTNEXTLINE(misc-no-recursion) -std::unique_ptr ParseScript(uint32_t& key_exp_index, Span& sp, ParseScriptContext ctx, FlatSigningProvider& out, std::string& error) +std::vector> ParseScript(uint32_t& key_exp_index, Span& sp, ParseScriptContext ctx, FlatSigningProvider& out, std::string& error) { using namespace script; + std::vector> ret; auto expr = Expr(sp); if (Func("pk", expr)) { - auto pubkey = ParsePubkey(key_exp_index, expr, ctx, out, error); - if (!pubkey) { + auto pubkeys = ParsePubkey(key_exp_index, expr, ctx, out, error); + if (pubkeys.empty()) { error = strprintf("pk(): %s", error); - return nullptr; + return {}; } ++key_exp_index; - return std::make_unique(std::move(pubkey), ctx == ParseScriptContext::P2TR); + for (auto& pubkey : pubkeys) { + ret.emplace_back(std::make_unique(std::move(pubkey), ctx == ParseScriptContext::P2TR)); + } + return ret; } if ((ctx == ParseScriptContext::TOP || ctx == ParseScriptContext::P2SH || ctx == ParseScriptContext::P2WSH) && Func("pkh", expr)) { - auto pubkey = ParsePubkey(key_exp_index, expr, ctx, out, error); - if (!pubkey) { + auto pubkeys = ParsePubkey(key_exp_index, expr, ctx, out, error); + if (pubkeys.empty()) { error = strprintf("pkh(): %s", error); - return nullptr; + return {}; } ++key_exp_index; - return std::make_unique(std::move(pubkey)); + for (auto& pubkey : pubkeys) { + ret.emplace_back(std::make_unique(std::move(pubkey))); + } + return ret; } else if (ctx != ParseScriptContext::P2TR && Func("pkh", expr)) { // Under Taproot, always the Miniscript parser deal with it. error = "Can only have pkh at top level, in sh(), wsh(), or in tr()"; - return nullptr; + return {}; } if (ctx == ParseScriptContext::TOP && Func("combo", expr)) { - auto pubkey = ParsePubkey(key_exp_index, expr, ctx, out, error); - if (!pubkey) { + auto pubkeys = ParsePubkey(key_exp_index, expr, ctx, out, error); + if (pubkeys.empty()) { error = strprintf("combo(): %s", error); - return nullptr; + return {}; } ++key_exp_index; - return std::make_unique(std::move(pubkey)); + for (auto& pubkey : pubkeys) { + ret.emplace_back(std::make_unique(std::move(pubkey))); + } + return ret; } else if (Func("combo", expr)) { error = "Can only have combo() at top level"; - return nullptr; + return {}; } const bool multi = Func("multi", expr); const bool sortedmulti = !multi && Func("sortedmulti", expr); @@ -1728,118 +1751,157 @@ std::unique_ptr ParseScript(uint32_t& key_exp_index, Span> providers; + std::vector>> providers; // List of multipath expanded pubkeys if (!ParseUInt32(std::string(threshold.begin(), threshold.end()), &thres)) { error = strprintf("Multi threshold '%s' is not valid", std::string(threshold.begin(), threshold.end())); - return nullptr; + return {}; } size_t script_size = 0; + size_t max_providers_len = 0; while (expr.size()) { if (!Const(",", expr)) { error = strprintf("Multi: expected ',', got '%c'", expr[0]); - return nullptr; + return {}; } auto arg = Expr(expr); - auto pk = ParsePubkey(key_exp_index, arg, ctx, out, error); - if (!pk) { + auto pks = ParsePubkey(key_exp_index, arg, ctx, out, error); + if (pks.empty()) { error = strprintf("Multi: %s", error); - return nullptr; + return {}; } - script_size += pk->GetSize() + 1; - providers.emplace_back(std::move(pk)); + script_size += pks.at(0)->GetSize() + 1; + max_providers_len = std::max(max_providers_len, pks.size()); + providers.emplace_back(std::move(pks)); key_exp_index++; } if ((multi || sortedmulti) && (providers.empty() || providers.size() > MAX_PUBKEYS_PER_MULTISIG)) { error = strprintf("Cannot have %u keys in multisig; must have between 1 and %d keys, inclusive", providers.size(), MAX_PUBKEYS_PER_MULTISIG); - return nullptr; + return {}; } else if ((multi_a || sortedmulti_a) && (providers.empty() || providers.size() > MAX_PUBKEYS_PER_MULTI_A)) { error = strprintf("Cannot have %u keys in multi_a; must have between 1 and %d keys, inclusive", providers.size(), MAX_PUBKEYS_PER_MULTI_A); - return nullptr; + return {}; } else if (thres < 1) { error = strprintf("Multisig threshold cannot be %d, must be at least 1", thres); - return nullptr; + return {}; } else if (thres > providers.size()) { error = strprintf("Multisig threshold cannot be larger than the number of keys; threshold is %d but only %u keys specified", thres, providers.size()); - return nullptr; + return {}; } if (ctx == ParseScriptContext::TOP) { if (providers.size() > 3) { error = strprintf("Cannot have %u pubkeys in bare multisig; only at most 3 pubkeys", providers.size()); - return nullptr; + return {}; } } if (ctx == ParseScriptContext::P2SH) { // This limits the maximum number of compressed pubkeys to 15. if (script_size + 3 > MAX_SCRIPT_ELEMENT_SIZE) { error = strprintf("P2SH script is too large, %d bytes is larger than %d bytes", script_size + 3, MAX_SCRIPT_ELEMENT_SIZE); - return nullptr; + return {}; } } - if (multi || sortedmulti) { - return std::make_unique(thres, std::move(providers), sortedmulti); - } else { - return std::make_unique(thres, std::move(providers), sortedmulti_a); + + // Make sure all vecs are of the same length, or exactly length 1 + // For length 1 vectors, clone key providers until vector is the same length + for (auto& vec : providers) { + if (vec.size() == 1) { + for (size_t i = 1; i < max_providers_len; ++i) { + vec.emplace_back(vec.at(0)->Clone()); + } + } else if (vec.size() != max_providers_len) { + error = strprintf("multi(): Multipath derivation paths have mismatched lengths"); + return {}; + } } + + // Build the final descriptors vector + for (size_t i = 0; i < max_providers_len; ++i) { + // Build final pubkeys vectors by retrieving the i'th subscript for each vector in subscripts + std::vector> pubs; + pubs.reserve(providers.size()); + for (auto& pub : providers) { + pubs.emplace_back(std::move(pub.at(i))); + } + if (multi || sortedmulti) { + ret.emplace_back(std::make_unique(thres, std::move(pubs), sortedmulti)); + } else { + ret.emplace_back(std::make_unique(thres, std::move(pubs), sortedmulti_a)); + } + } + return ret; } else if (multi || sortedmulti) { error = "Can only have multi/sortedmulti at top level, in sh(), or in wsh()"; - return nullptr; + return {}; } else if (multi_a || sortedmulti_a) { error = "Can only have multi_a/sortedmulti_a inside tr()"; - return nullptr; + return {}; } if ((ctx == ParseScriptContext::TOP || ctx == ParseScriptContext::P2SH) && Func("wpkh", expr)) { - auto pubkey = ParsePubkey(key_exp_index, expr, ParseScriptContext::P2WPKH, out, error); - if (!pubkey) { + auto pubkeys = ParsePubkey(key_exp_index, expr, ParseScriptContext::P2WPKH, out, error); + if (pubkeys.empty()) { error = strprintf("wpkh(): %s", error); - return nullptr; + return {}; } key_exp_index++; - return std::make_unique(std::move(pubkey)); + for (auto& pubkey : pubkeys) { + ret.emplace_back(std::make_unique(std::move(pubkey))); + } + return ret; } else if (Func("wpkh", expr)) { error = "Can only have wpkh() at top level or inside sh()"; - return nullptr; + return {}; } if (ctx == ParseScriptContext::TOP && Func("sh", expr)) { - auto desc = ParseScript(key_exp_index, expr, ParseScriptContext::P2SH, out, error); - if (!desc || expr.size()) return nullptr; - return std::make_unique(std::move(desc)); + auto descs = ParseScript(key_exp_index, expr, ParseScriptContext::P2SH, out, error); + if (descs.empty() || expr.size()) return {}; + std::vector> ret; + ret.reserve(descs.size()); + for (auto& desc : descs) { + ret.push_back(std::make_unique(std::move(desc))); + } + return ret; } else if (Func("sh", expr)) { error = "Can only have sh() at top level"; - return nullptr; + return {}; } if ((ctx == ParseScriptContext::TOP || ctx == ParseScriptContext::P2SH) && Func("wsh", expr)) { - auto desc = ParseScript(key_exp_index, expr, ParseScriptContext::P2WSH, out, error); - if (!desc || expr.size()) return nullptr; - return std::make_unique(std::move(desc)); + auto descs = ParseScript(key_exp_index, expr, ParseScriptContext::P2WSH, out, error); + if (descs.empty() || expr.size()) return {}; + for (auto& desc : descs) { + ret.emplace_back(std::make_unique(std::move(desc))); + } + return ret; } else if (Func("wsh", expr)) { error = "Can only have wsh() at top level or inside sh()"; - return nullptr; + return {}; } if (ctx == ParseScriptContext::TOP && Func("addr", expr)) { CTxDestination dest = DecodeDestination(std::string(expr.begin(), expr.end())); if (!IsValidDestination(dest)) { error = "Address is not valid"; - return nullptr; + return {}; } - return std::make_unique(std::move(dest)); + ret.emplace_back(std::make_unique(std::move(dest))); + return ret; } else if (Func("addr", expr)) { error = "Can only have addr() at top level"; - return nullptr; + return {}; } if (ctx == ParseScriptContext::TOP && Func("tr", expr)) { auto arg = Expr(expr); - auto internal_key = ParsePubkey(key_exp_index, arg, ParseScriptContext::P2TR, out, error); - if (!internal_key) { + auto internal_keys = ParsePubkey(key_exp_index, arg, ParseScriptContext::P2TR, out, error); + if (internal_keys.empty()) { error = strprintf("tr(): %s", error); - return nullptr; + return {}; } + size_t max_providers_len = internal_keys.size(); ++key_exp_index; - std::vector> subscripts; //!< list of script subexpressions + std::vector>> subscripts; //!< list of multipath expanded script subexpressions std::vector depths; //!< depth in the tree of each subexpression (same length subscripts) if (expr.size()) { if (!Const(",", expr)) { error = strprintf("tr: expected ',', got '%c'", expr[0]); - return nullptr; + return {}; } /** The path from the top of the tree to what we're currently processing. * branches[i] == false: left branch in the i'th step from the top; true: right branch. @@ -1853,19 +1915,20 @@ std::unique_ptr ParseScript(uint32_t& key_exp_index, Span TAPROOT_CONTROL_MAX_NODE_COUNT) { error = strprintf("tr() supports at most %i nesting levels", TAPROOT_CONTROL_MAX_NODE_COUNT); - return nullptr; + return {}; } } // Process the actual script expression. auto sarg = Expr(expr); subscripts.emplace_back(ParseScript(key_exp_index, sarg, ParseScriptContext::P2TR, out, error)); - if (!subscripts.back()) return nullptr; + if (subscripts.back().empty()) return {}; + max_providers_len = std::max(max_providers_len, subscripts.back().size()); depths.push_back(branches.size()); // Process closing braces; one is expected for every right branch we were in. while (branches.size() && branches.back()) { if (!Const("}", expr)) { error = strprintf("tr(): expected '}' after script expression"); - return nullptr; + return {}; } branches.pop_back(); // move up one level after encountering '}' } @@ -1873,7 +1936,7 @@ std::unique_ptr ParseScript(uint32_t& key_exp_index, Span ParseScript(uint32_t& key_exp_index, Span(std::move(internal_key), std::move(subscripts), std::move(depths)); + + // Make sure all vecs are of the same length, or exactly length 1 + // For length 1 vectors, clone subdescs until vector is the same length + for (auto& vec : subscripts) { + if (vec.size() == 1) { + for (size_t i = 1; i < max_providers_len; ++i) { + vec.emplace_back(vec.at(0)->Clone()); + } + } else if (vec.size() != max_providers_len) { + error = strprintf("tr(): Multipath subscripts have mismatched lengths"); + return {}; + } + } + + if (internal_keys.size() > 1 && internal_keys.size() != max_providers_len) { + error = strprintf("tr(): Multipath internal key mismatches multipath subscripts lengths"); + return {}; + } + + while (internal_keys.size() < max_providers_len) { + internal_keys.emplace_back(internal_keys.at(0)->Clone()); + } + + // Build the final descriptors vector + for (size_t i = 0; i < max_providers_len; ++i) { + // Build final subscripts vectors by retrieving the i'th subscript for each vector in subscripts + std::vector> this_subs; + this_subs.reserve(subscripts.size()); + for (auto& subs : subscripts) { + this_subs.emplace_back(std::move(subs.at(i))); + } + ret.emplace_back(std::make_unique(std::move(internal_keys.at(i)), std::move(this_subs), depths)); + } + return ret; + + } else if (Func("tr", expr)) { error = "Can only have tr at top level"; - return nullptr; + return {}; } if (ctx == ParseScriptContext::TOP && Func("rawtr", expr)) { auto arg = Expr(expr); if (expr.size()) { error = strprintf("rawtr(): only one key expected."); - return nullptr; + return {}; + } + auto output_keys = ParsePubkey(key_exp_index, arg, ParseScriptContext::P2TR, out, error); + if (output_keys.empty()) { + error = strprintf("rawtr(): %s", error); + return {}; } - auto output_key = ParsePubkey(key_exp_index, arg, ParseScriptContext::P2TR, out, error); - if (!output_key) return nullptr; ++key_exp_index; - return std::make_unique(std::move(output_key)); + for (auto& pubkey : output_keys) { + ret.emplace_back(std::make_unique(std::move(pubkey))); + } + return ret; } else if (Func("rawtr", expr)) { error = "Can only have rawtr at top level"; - return nullptr; + return {}; } if (ctx == ParseScriptContext::TOP && Func("raw", expr)) { std::string str(expr.begin(), expr.end()); if (!IsHex(str)) { error = "Raw script is not hex"; - return nullptr; + return {}; } auto bytes = ParseHex(str); - return std::make_unique(CScript(bytes.begin(), bytes.end())); + ret.emplace_back(std::make_unique(CScript(bytes.begin(), bytes.end()))); + return ret; } else if (Func("raw", expr)) { error = "Can only have raw() at top level"; - return nullptr; + return {}; } // Process miniscript expressions. { @@ -1923,12 +2028,12 @@ std::unique_ptr ParseScript(uint32_t& key_exp_index, SpanIsSane() || node->IsNotSatisfiable()) { // Try to find the first insane sub for better error reporting. @@ -1953,24 +2058,52 @@ std::unique_ptr ParseScript(uint32_t& key_exp_index, Span(std::move(parser.m_keys), std::move(node)); + // Make sure all vecs are of the same length, or exactly length 1 + // For length 1 vectors, clone subdescs until vector is the same length + size_t num_multipath = std::max_element(parser.m_keys.begin(), parser.m_keys.end(), + [](const std::vector>& a, const std::vector>& b) { + return a.size() < b.size(); + })->size(); + + for (auto& vec : parser.m_keys) { + if (vec.size() == 1) { + for (size_t i = 1; i < num_multipath; ++i) { + vec.emplace_back(vec.at(0)->Clone()); + } + } else if (vec.size() != num_multipath) { + error = strprintf("Miniscript: Multipath derivation paths have mismatched lengths"); + return {}; + } + } + + // Build the final descriptors vector + for (size_t i = 0; i < num_multipath; ++i) { + // Build final pubkeys vectors by retrieving the i'th subscript for each vector in subscripts + std::vector> pubs; + pubs.reserve(parser.m_keys.size()); + for (auto& pub : parser.m_keys) { + pubs.emplace_back(std::move(pub.at(i))); + } + ret.emplace_back(std::make_unique(std::move(pubs), node)); + } + return ret; } } if (ctx == ParseScriptContext::P2SH) { error = "A function is needed within P2SH"; - return nullptr; + return {}; } else if (ctx == ParseScriptContext::P2WSH) { error = "A function is needed within P2WSH"; - return nullptr; + return {}; } error = strprintf("'%s' is not a valid descriptor function", std::string(expr.begin(), expr.end())); - return nullptr; + return {}; } std::unique_ptr InferMultiA(const CScript& script, ParseScriptContext ctx, const SigningProvider& provider) @@ -2108,7 +2241,12 @@ std::unique_ptr InferScript(const CScript& script, ParseScriptCo KeyParser parser(/* out = */nullptr, /* in = */&provider, /* ctx = */script_ctx); auto node = miniscript::FromScript(script, parser); if (node && node->IsSane()) { - return std::make_unique(std::move(parser.m_keys), std::move(node)); + std::vector> keys; + keys.reserve(parser.m_keys.size()); + for (auto& key : parser.m_keys) { + keys.emplace_back(std::move(key.at(0))); + } + return std::make_unique(std::move(keys), std::move(node)); } } @@ -2169,7 +2307,7 @@ std::unique_ptr Parse(const std::string& descriptor, FlatSigningProv if (!CheckChecksum(sp, require_checksum, error)) return nullptr; uint32_t key_exp_index = 0; auto ret = ParseScript(key_exp_index, sp, ParseScriptContext::TOP, out, error); - if (sp.size() == 0 && ret) return std::unique_ptr(std::move(ret)); + if (sp.size() == 0 && !ret.empty()) return std::unique_ptr(std::move(ret.at(0))); return nullptr; } From 0d640c6f02bc20e5c1be773443dd74d8806d953b Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Wed, 7 Aug 2024 16:29:03 -0400 Subject: [PATCH 04/14] descriptors: Have ParseKeypath handle multipath specifiers Multipath specifiers are derivation path indexes of the form `` used for specifying multiple derivation paths for a descriptor. Only one multipath specifier is allowed per PubkeyProvider. This is syntactic sugar which is parsed into multiple distinct descriptors. One descriptor will have all of the `i` paths, the second all of the `j` paths, the third all of the `k` paths, and so on. ParseKeypath will always return a vector of keypaths with the same size as the multipath specifier. The callers of this function are updated to deal with this case and return multiple PubkeyProviders. Their callers have also been updated to handle vectors of PubkeyProviders. Co-Authored-By: furszy --- src/script/descriptor.cpp | 109 ++++++++++++++++++++++++++++++-------- 1 file changed, 86 insertions(+), 23 deletions(-) diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp index bb4bc2656f2..ab77bb63841 100644 --- a/src/script/descriptor.cpp +++ b/src/script/descriptor.cpp @@ -1410,37 +1410,96 @@ enum class ParseScriptContext { P2TR, //!< Inside tr() (either internal key, or BIP342 script leaf) }; +std::optional ParseKeyPathNum(Span elem, bool& apostrophe, std::string& error) +{ + bool hardened = false; + if (elem.size() > 0) { + const char last = elem[elem.size() - 1]; + if (last == '\'' || last == 'h') { + elem = elem.first(elem.size() - 1); + hardened = true; + apostrophe = last == '\''; + } + } + uint32_t p; + if (!ParseUInt32(std::string(elem.begin(), elem.end()), &p)) { + error = strprintf("Key path value '%s' is not a valid uint32", std::string(elem.begin(), elem.end())); + return std::nullopt; + } else if (p > 0x7FFFFFFFUL) { + error = strprintf("Key path value %u is out of range", p); + return std::nullopt; + } + + return std::make_optional(p | (((uint32_t)hardened) << 31)); +} + /** - * Parse a key path, being passed a split list of elements (the first element is ignored). + * Parse a key path, being passed a split list of elements (the first element is ignored because it is always the key). * * @param[in] split BIP32 path string, using either ' or h for hardened derivation - * @param[out] out the key path + * @param[out] out Vector of parsed key paths * @param[out] apostrophe only updated if hardened derivation is found * @param[out] error parsing error message + * @param[in] allow_multipath Allows the parsed path to use the multipath specifier * @returns false if parsing failed **/ -[[nodiscard]] bool ParseKeyPath(const std::vector>& split, KeyPath& out, bool& apostrophe, std::string& error) +[[nodiscard]] bool ParseKeyPath(const std::vector>& split, std::vector& out, bool& apostrophe, std::string& error, bool allow_multipath) { + KeyPath path; + std::optional multipath_segment_index; + std::vector multipath_values; + std::unordered_set seen_multipath; + for (size_t i = 1; i < split.size(); ++i) { - Span elem = split[i]; - bool hardened = false; - if (elem.size() > 0) { - const char last = elem[elem.size() - 1]; - if (last == '\'' || last == 'h') { - elem = elem.first(elem.size() - 1); - hardened = true; - apostrophe = last == '\''; + const Span& elem = split[i]; + + // Check if element contain multipath specifier + if (!elem.empty() && elem.front() == '<' && elem.back() == '>') { + if (!allow_multipath) { + error = strprintf("Key path value '%s' specifies multipath in a section where multipath is not allowed", std::string(elem.begin(), elem.end())); + return false; } + if (multipath_segment_index) { + error = "Multiple multipath key path specifiers found"; + return false; + } + + // Parse each possible value + std::vector> nums = Split(Span(elem.begin()+1, elem.end()-1), ";"); + if (nums.size() < 2) { + error = "Multipath key path specifiers must have at least two items"; + return false; + } + + for (const auto& num : nums) { + const auto& op_num = ParseKeyPathNum(num, apostrophe, error); + if (!op_num) return false; + auto [_, inserted] = seen_multipath.insert(*op_num); + if (!inserted) { + error = strprintf("Duplicated key path value %u in multipath specifier", *op_num); + return false; + } + multipath_values.emplace_back(*op_num); + } + + path.emplace_back(); // Placeholder for multipath segment + multipath_segment_index = path.size()-1; + } else { + const auto& op_num = ParseKeyPathNum(elem, apostrophe, error); + if (!op_num) return false; + path.emplace_back(*op_num); } - uint32_t p; - if (!ParseUInt32(std::string(elem.begin(), elem.end()), &p)) { - error = strprintf("Key path value '%s' is not a valid uint32", std::string(elem.begin(), elem.end())); - return false; - } else if (p > 0x7FFFFFFFUL) { - error = strprintf("Key path value %u is out of range", p); - return false; + } + + if (!multipath_segment_index) { + out.emplace_back(std::move(path)); + } else { + // Replace the multipath placeholder with each value while generating paths + for (size_t i = 0; i < multipath_values.size(); i++) { + KeyPath branch_path = path; + branch_path[*multipath_segment_index] = multipath_values[i]; + out.emplace_back(std::move(branch_path)); } - out.push_back(p | (((uint32_t)hardened) << 31)); } return true; } @@ -1503,7 +1562,7 @@ std::vector> ParsePubkeyInner(uint32_t key_exp_i error = strprintf("key '%s' is not valid", str); return {}; } - KeyPath path; + std::vector paths; DeriveType type = DeriveType::NO; if (split.back() == Span{"*"}.first(1)) { split.pop_back(); @@ -1513,12 +1572,14 @@ std::vector> ParsePubkeyInner(uint32_t key_exp_i split.pop_back(); type = DeriveType::HARDENED; } - if (!ParseKeyPath(split, path, apostrophe, error)) return {}; + if (!ParseKeyPath(split, paths, apostrophe, error, /*allow_multipath=*/true)) return {}; if (extkey.key.IsValid()) { extpubkey = extkey.Neuter(); out.keys.emplace(extpubkey.pubkey.GetID(), extkey.key); } - ret.emplace_back(std::make_unique(key_exp_index, extpubkey, std::move(path), type, apostrophe)); + for (auto& path : paths) { + ret.emplace_back(std::make_unique(key_exp_index, extpubkey, std::move(path), type, apostrophe)); + } return ret; } @@ -1556,7 +1617,9 @@ std::vector> ParsePubkey(uint32_t key_exp_index, static_assert(sizeof(info.fingerprint) == 4, "Fingerprint must be 4 bytes"); assert(fpr_bytes.size() == 4); std::copy(fpr_bytes.begin(), fpr_bytes.end(), info.fingerprint); - if (!ParseKeyPath(slash_split, info.path, apostrophe, error)) return {}; + std::vector path; + if (!ParseKeyPath(slash_split, path, apostrophe, error, /*allow_multipath=*/false)) return {}; + info.path = path.at(0); auto providers = ParsePubkeyInner(key_exp_index, origin_split[1], ctx, out, apostrophe, error); if (providers.empty()) return {}; ret.reserve(providers.size()); From 1bbf46e2dae4599d04c79aaacf7c5db00b2e707f Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Wed, 7 Aug 2024 16:29:06 -0400 Subject: [PATCH 05/14] descriptors: Change Parse to return vector of descriptors When given a descriptor which contins a multipath derivation specifier, a vector of descriptors will be returned. --- src/bench/descriptors.cpp | 4 +- src/bench/wallet_ismine.cpp | 4 +- src/qt/test/wallettests.cpp | 6 ++- src/rpc/mining.cpp | 57 ++++++++++++------------ src/rpc/output_script.cpp | 37 ++++++++++----- src/rpc/util.cpp | 22 ++++----- src/script/descriptor.cpp | 15 +++++-- src/script/descriptor.h | 4 +- src/test/descriptor_tests.cpp | 19 ++++---- src/test/fuzz/descriptor_parse.cpp | 30 ++++++++++--- src/wallet/rpc/backup.cpp | 10 +++-- src/wallet/rpc/spend.cpp | 8 ++-- src/wallet/scriptpubkeyman.cpp | 10 +++-- src/wallet/test/fuzz/notifications.cpp | 2 +- src/wallet/test/fuzz/scriptpubkeyman.cpp | 6 +-- src/wallet/test/ismine_tests.cpp | 7 +-- src/wallet/test/psbt_wallet_tests.cpp | 5 ++- src/wallet/test/util.cpp | 5 ++- src/wallet/test/wallet_tests.cpp | 5 ++- src/wallet/wallet.cpp | 21 ++++----- src/wallet/walletutil.cpp | 4 +- src/wallet/walletutil.h | 8 +++- 22 files changed, 178 insertions(+), 111 deletions(-) diff --git a/src/bench/descriptors.cpp b/src/bench/descriptors.cpp index 5d6bcb8ce87..26976fa6693 100644 --- a/src/bench/descriptors.cpp +++ b/src/bench/descriptors.cpp @@ -18,12 +18,12 @@ static void ExpandDescriptor(benchmark::Bench& bench) const std::pair range = {0, 1000}; FlatSigningProvider provider; std::string error; - auto desc = Parse(desc_str, provider, error); + auto descs = Parse(desc_str, provider, error); bench.run([&] { for (int i = range.first; i <= range.second; ++i) { std::vector scripts; - bool success = desc->Expand(i, provider, scripts, provider); + bool success = descs[0]->Expand(i, provider, scripts, provider); assert(success); } }); diff --git a/src/bench/wallet_ismine.cpp b/src/bench/wallet_ismine.cpp index 753404b5261..4189c51db06 100644 --- a/src/bench/wallet_ismine.cpp +++ b/src/bench/wallet_ismine.cpp @@ -43,8 +43,8 @@ static void WalletIsMine(benchmark::Bench& bench, bool legacy_wallet, int num_co key.MakeNewKey(/*fCompressed=*/true); FlatSigningProvider keys; std::string error; - std::unique_ptr desc = Parse("combo(" + EncodeSecret(key) + ")", keys, error, /*require_checksum=*/false); - WalletDescriptor w_desc(std::move(desc), /*creation_time=*/0, /*range_start=*/0, /*range_end=*/0, /*next_index=*/0); + std::vector> desc = Parse("combo(" + EncodeSecret(key) + ")", keys, error, /*require_checksum=*/false); + WalletDescriptor w_desc(std::move(desc.at(0)), /*creation_time=*/0, /*range_start=*/0, /*range_end=*/0, /*next_index=*/0); auto spkm = wallet->AddWalletDescriptor(w_desc, keys, /*label=*/"", /*internal=*/false); assert(spkm); } diff --git a/src/qt/test/wallettests.cpp b/src/qt/test/wallettests.cpp index 603df0b15f2..8fdb56dd7ea 100644 --- a/src/qt/test/wallettests.cpp +++ b/src/qt/test/wallettests.cpp @@ -218,8 +218,10 @@ std::shared_ptr SetupDescriptorsWallet(interfaces::Node& node, TestChai // Add the coinbase key FlatSigningProvider provider; std::string error; - std::unique_ptr desc = Parse("combo(" + EncodeSecret(test.coinbaseKey) + ")", provider, error, /* require_checksum=*/ false); - assert(desc); + auto descs = Parse("combo(" + EncodeSecret(test.coinbaseKey) + ")", provider, error, /* require_checksum=*/ false); + assert(!descs.empty()); + assert(descs.size() == 1); + auto& desc = descs.at(0); WalletDescriptor w_desc(std::move(desc), 0, 0, 1, 1); if (!wallet->AddWalletDescriptor(w_desc, provider, "", false)) assert(false); CTxDestination dest = GetDestinationForKey(test.coinbaseKey.GetPubKey(), wallet->m_default_address_type); diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 7e420dcd9b5..1ad7ee36db5 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -180,35 +180,36 @@ static UniValue generateBlocks(ChainstateManager& chainman, Mining& miner, const static bool getScriptFromDescriptor(const std::string& descriptor, CScript& script, std::string& error) { FlatSigningProvider key_provider; - const auto desc = Parse(descriptor, key_provider, error, /* require_checksum = */ false); - if (desc) { - if (desc->IsRange()) { - throw JSONRPCError(RPC_INVALID_PARAMETER, "Ranged descriptor not accepted. Maybe pass through deriveaddresses first?"); - } - - FlatSigningProvider provider; - std::vector scripts; - if (!desc->Expand(0, key_provider, scripts, provider)) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Cannot derive script without private keys"); - } - - // Combo descriptors can have 2 or 4 scripts, so we can't just check scripts.size() == 1 - CHECK_NONFATAL(scripts.size() > 0 && scripts.size() <= 4); - - if (scripts.size() == 1) { - script = scripts.at(0); - } else if (scripts.size() == 4) { - // For uncompressed keys, take the 3rd script, since it is p2wpkh - script = scripts.at(2); - } else { - // Else take the 2nd script, since it is p2pkh - script = scripts.at(1); - } - - return true; - } else { - return false; + const auto descs = Parse(descriptor, key_provider, error, /* require_checksum = */ false); + if (descs.empty()) return false; + if (descs.size() > 1) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Multipath descriptor not accepted"); } + const auto& desc = descs.at(0); + if (desc->IsRange()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Ranged descriptor not accepted. Maybe pass through deriveaddresses first?"); + } + + FlatSigningProvider provider; + std::vector scripts; + if (!desc->Expand(0, key_provider, scripts, provider)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Cannot derive script without private keys"); + } + + // Combo descriptors can have 2 or 4 scripts, so we can't just check scripts.size() == 1 + CHECK_NONFATAL(scripts.size() > 0 && scripts.size() <= 4); + + if (scripts.size() == 1) { + script = scripts.at(0); + } else if (scripts.size() == 4) { + // For uncompressed keys, take the 3rd script, since it is p2wpkh + script = scripts.at(2); + } else { + // Else take the 2nd script, since it is p2pkh + script = scripts.at(1); + } + + return true; } static RPCHelpMan generatetodescriptor() diff --git a/src/rpc/output_script.cpp b/src/rpc/output_script.cpp index 65a9be2762f..551a7f78072 100644 --- a/src/rpc/output_script.cpp +++ b/src/rpc/output_script.cpp @@ -175,7 +175,11 @@ static RPCHelpMan getdescriptorinfo() RPCResult{ RPCResult::Type::OBJ, "", "", { - {RPCResult::Type::STR, "descriptor", "The descriptor in canonical form, without private keys"}, + {RPCResult::Type::STR, "descriptor", "The descriptor in canonical form, without private keys. For a multipath descriptor, only the first will be returned."}, + {RPCResult::Type::ARR, "multipath_expansion", /*optional=*/true, "All descriptors produced by expanding multipath derivation elements. Only if the provided descriptor specifies multipath derivation elements.", + { + {RPCResult::Type::STR, "", ""}, + }}, {RPCResult::Type::STR, "checksum", "The checksum for the input descriptor"}, {RPCResult::Type::BOOL, "isrange", "Whether the descriptor is ranged"}, {RPCResult::Type::BOOL, "issolvable", "Whether the descriptor is solvable"}, @@ -191,16 +195,25 @@ static RPCHelpMan getdescriptorinfo() { FlatSigningProvider provider; std::string error; - auto desc = Parse(request.params[0].get_str(), provider, error); - if (!desc) { + auto descs = Parse(request.params[0].get_str(), provider, error); + if (descs.empty()) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error); } UniValue result(UniValue::VOBJ); - result.pushKV("descriptor", desc->ToString()); + result.pushKV("descriptor", descs.at(0)->ToString()); + + if (descs.size() > 1) { + UniValue multipath_descs(UniValue::VARR); + for (const auto& d : descs) { + multipath_descs.push_back(d->ToString()); + } + result.pushKV("multipath_expansion", multipath_descs); + } + result.pushKV("checksum", GetDescriptorChecksum(request.params[0].get_str())); - result.pushKV("isrange", desc->IsRange()); - result.pushKV("issolvable", desc->IsSolvable()); + result.pushKV("isrange", descs.at(0)->IsRange()); + result.pushKV("issolvable", descs.at(0)->IsSolvable()); result.pushKV("hasprivatekeys", provider.keys.size() > 0); return result; }, @@ -221,7 +234,8 @@ static RPCHelpMan deriveaddresses() " tr(,multi_a(,,,...)) P2TR-multisig outputs for the given threshold and pubkeys\n" "\nIn the above, either refers to a fixed public key in hexadecimal notation, or to an xpub/xprv optionally followed by one\n" "or more path elements separated by \"/\", where \"h\" represents a hardened child key.\n" - "For more information on output descriptors, see the documentation in the doc/descriptors.md file.\n"}, + "For more information on output descriptors, see the documentation in the doc/descriptors.md file.\n" + "Note that only descriptors that specify a single derivation path can be derived.\n"}, { {"descriptor", RPCArg::Type::STR, RPCArg::Optional::NO, "The descriptor."}, {"range", RPCArg::Type::RANGE, RPCArg::Optional::OMITTED, "If a ranged descriptor is used, this specifies the end or the range (in [begin,end] notation) to derive."}, @@ -250,11 +264,14 @@ static RPCHelpMan deriveaddresses() FlatSigningProvider key_provider; std::string error; - auto desc = Parse(desc_str, key_provider, error, /* require_checksum = */ true); - if (!desc) { + auto descs = Parse(desc_str, key_provider, error, /* require_checksum = */ true); + if (descs.empty()) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error); } - + if (descs.size() > 1) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Descriptor with multipath derivation path specifiers are not allowed"); + } + auto& desc = descs.at(0); if (!desc->IsRange() && request.params.size() > 1) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Range should not be specified for an un-ranged descriptor"); } diff --git a/src/rpc/util.cpp b/src/rpc/util.cpp index 4df4466c494..ee3dfa45ccf 100644 --- a/src/rpc/util.cpp +++ b/src/rpc/util.cpp @@ -1337,24 +1337,26 @@ std::vector EvalDescriptorStringOrObject(const UniValue& scanobject, Fl } std::string error; - auto desc = Parse(desc_str, provider, error); - if (!desc) { + auto descs = Parse(desc_str, provider, error); + if (descs.empty()) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error); } - if (!desc->IsRange()) { + if (!descs.at(0)->IsRange()) { range.first = 0; range.second = 0; } std::vector ret; for (int i = range.first; i <= range.second; ++i) { - std::vector scripts; - if (!desc->Expand(i, provider, scripts, provider)) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Cannot derive script without private keys: '%s'", desc_str)); + for (const auto& desc : descs) { + std::vector scripts; + if (!desc->Expand(i, provider, scripts, provider)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Cannot derive script without private keys: '%s'", desc_str)); + } + if (expand_priv) { + desc->ExpandPrivate(/*pos=*/i, provider, /*out=*/provider); + } + std::move(scripts.begin(), scripts.end(), std::back_inserter(ret)); } - if (expand_priv) { - desc->ExpandPrivate(/*pos=*/i, provider, /*out=*/provider); - } - std::move(scripts.begin(), scripts.end(), std::back_inserter(ret)); } return ret; } diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp index ab77bb63841..ac46f9d29e8 100644 --- a/src/script/descriptor.cpp +++ b/src/script/descriptor.cpp @@ -2364,14 +2364,21 @@ bool CheckChecksum(Span& sp, bool require_checksum, std::string& err return true; } -std::unique_ptr Parse(const std::string& descriptor, FlatSigningProvider& out, std::string& error, bool require_checksum) +std::vector> Parse(const std::string& descriptor, FlatSigningProvider& out, std::string& error, bool require_checksum) { Span sp{descriptor}; - if (!CheckChecksum(sp, require_checksum, error)) return nullptr; + if (!CheckChecksum(sp, require_checksum, error)) return {}; uint32_t key_exp_index = 0; auto ret = ParseScript(key_exp_index, sp, ParseScriptContext::TOP, out, error); - if (sp.size() == 0 && !ret.empty()) return std::unique_ptr(std::move(ret.at(0))); - return nullptr; + if (sp.size() == 0 && !ret.empty()) { + std::vector> descs; + descs.reserve(ret.size()); + for (auto& r : ret) { + descs.emplace_back(std::unique_ptr(std::move(r))); + } + return descs; + } + return {}; } std::string GetDescriptorChecksum(const std::string& descriptor) diff --git a/src/script/descriptor.h b/src/script/descriptor.h index e78a7753302..473649a3144 100644 --- a/src/script/descriptor.h +++ b/src/script/descriptor.h @@ -173,9 +173,9 @@ struct Descriptor { * is set, the checksum is mandatory - otherwise it is optional. * * If a parse error occurs, or the checksum is missing/invalid, or anything - * else is wrong, `nullptr` is returned. + * else is wrong, an empty vector is returned. */ -std::unique_ptr Parse(const std::string& descriptor, FlatSigningProvider& out, std::string& error, bool require_checksum = false); +std::vector> Parse(const std::string& descriptor, FlatSigningProvider& out, std::string& error, bool require_checksum = false); /** Get the checksum for a `descriptor`. * diff --git a/src/test/descriptor_tests.cpp b/src/test/descriptor_tests.cpp index e6821dd3218..f9fbbf8cabe 100644 --- a/src/test/descriptor_tests.cpp +++ b/src/test/descriptor_tests.cpp @@ -25,8 +25,8 @@ void CheckUnparsable(const std::string& prv, const std::string& pub, const std:: std::string error; auto parse_priv = Parse(prv, keys_priv, error); auto parse_pub = Parse(pub, keys_pub, error); - BOOST_CHECK_MESSAGE(!parse_priv, prv); - BOOST_CHECK_MESSAGE(!parse_pub, pub); + BOOST_CHECK_MESSAGE(parse_priv.empty(), prv); + BOOST_CHECK_MESSAGE(parse_pub.empty(), pub); BOOST_CHECK_EQUAL(error, expected_error); } @@ -139,19 +139,22 @@ void DoCheck(std::string prv, std::string pub, const std::string& norm_pub, int std::set> left_paths = paths; std::string error; - std::unique_ptr parse_priv; - std::unique_ptr parse_pub; + std::vector> parse_privs; + std::vector> parse_pubs; // Check that parsing succeeds. if (replace_apostrophe_with_h_in_prv) { prv = UseHInsteadOfApostrophe(prv); } - parse_priv = Parse(prv, keys_priv, error); - BOOST_CHECK_MESSAGE(parse_priv, error); + parse_privs = Parse(prv, keys_priv, error); + BOOST_CHECK_MESSAGE(!parse_privs.empty(), error); if (replace_apostrophe_with_h_in_pub) { pub = UseHInsteadOfApostrophe(pub); } - parse_pub = Parse(pub, keys_pub, error); - BOOST_CHECK_MESSAGE(parse_pub, error); + parse_pubs = Parse(pub, keys_pub, error); + BOOST_CHECK_MESSAGE(!parse_pubs.empty(), error); + + auto& parse_priv = parse_privs.at(0); + auto& parse_pub = parse_pubs.at(0); // We must be able to estimate the max satisfaction size for any solvable descriptor top descriptor (but combo). const bool is_nontop_or_nonsolvable{!parse_priv->IsSolvable() || !parse_priv->GetOutputType()}; diff --git a/src/test/fuzz/descriptor_parse.cpp b/src/test/fuzz/descriptor_parse.cpp index b9a5560ffb9..14322effbef 100644 --- a/src/test/fuzz/descriptor_parse.cpp +++ b/src/test/fuzz/descriptor_parse.cpp @@ -15,14 +15,24 @@ MockedDescriptorConverter MOCKED_DESC_CONVERTER; /** Test a successfully parsed descriptor. */ -static void TestDescriptor(const Descriptor& desc, FlatSigningProvider& sig_provider, std::string& dummy) +static void TestDescriptor(const Descriptor& desc, FlatSigningProvider& sig_provider, std::string& dummy, std::optional& is_ranged, std::optional& is_solvable) { // Trivial helpers. (void)desc.IsRange(); - const bool is_solvable{desc.IsSolvable()}; (void)desc.IsSingleType(); (void)desc.GetOutputType(); + if (is_ranged.has_value()) { + assert(desc.IsRange() == *is_ranged); + } else { + is_ranged = desc.IsRange(); + } + if (is_solvable.has_value()) { + assert(desc.IsSolvable() == *is_solvable); + } else { + is_solvable = desc.IsSolvable(); + } + // Serialization to string representation. (void)desc.ToString(); (void)desc.ToPrivateString(sig_provider, dummy); @@ -48,7 +58,7 @@ static void TestDescriptor(const Descriptor& desc, FlatSigningProvider& sig_prov const auto max_sat_nonmaxsig{desc.MaxSatisfactionWeight(true)}; const auto max_elems{desc.MaxSatisfactionElems()}; // We must be able to estimate the max satisfaction size for any solvable descriptor (but combo). - const bool is_nontop_or_nonsolvable{!is_solvable || !desc.GetOutputType()}; + const bool is_nontop_or_nonsolvable{!*is_solvable || !desc.GetOutputType()}; const bool is_input_size_info_set{max_sat_maxsig && max_sat_nonmaxsig && max_elems}; assert(is_input_size_info_set || is_nontop_or_nonsolvable); } @@ -77,7 +87,12 @@ FUZZ_TARGET(mocked_descriptor_parse, .init = initialize_mocked_descriptor_parse) FlatSigningProvider signing_provider; std::string error; const auto desc = Parse(*descriptor, signing_provider, error); - if (desc) TestDescriptor(*desc, signing_provider, error); + std::optional is_ranged; + std::optional is_solvable; + for (const auto& d : desc) { + assert(d); + TestDescriptor(*d, signing_provider, error, is_ranged, is_solvable); + } } } @@ -91,6 +106,11 @@ FUZZ_TARGET(descriptor_parse, .init = initialize_descriptor_parse) std::string error; for (const bool require_checksum : {true, false}) { const auto desc = Parse(descriptor, signing_provider, error, require_checksum); - if (desc) TestDescriptor(*desc, signing_provider, error); + std::optional is_ranged; + std::optional is_solvable; + for (const auto& d : desc) { + assert(d); + TestDescriptor(*d, signing_provider, error, is_ranged, is_solvable); + } } } diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp index 8cddb8b0990..d92c2b6770a 100644 --- a/src/wallet/rpc/backup.cpp +++ b/src/wallet/rpc/backup.cpp @@ -1061,10 +1061,11 @@ static UniValue ProcessImportDescriptor(ImportData& import_data, std::mapGetOutputType() == OutputType::BECH32M) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Bech32m descriptors cannot be imported into legacy wallets"); } @@ -1452,10 +1453,11 @@ static UniValue ProcessDescriptorImport(CWallet& wallet, const UniValue& data, c // Parse descriptor string FlatSigningProvider keys; std::string error; - auto parsed_desc = Parse(descriptor, keys, error, /* require_checksum = */ true); - if (!parsed_desc) { + auto parsed_descs = Parse(descriptor, keys, error, /* require_checksum = */ true); + if (parsed_descs.empty()) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error); } + auto& parsed_desc = parsed_descs.at(0); // Range check int64_t range_start = 0, range_end = 1, next_index = 0; diff --git a/src/wallet/rpc/spend.cpp b/src/wallet/rpc/spend.cpp index ac2a4826f05..ba03804270e 100644 --- a/src/wallet/rpc/spend.cpp +++ b/src/wallet/rpc/spend.cpp @@ -660,11 +660,13 @@ CreatedTransactionResult FundTransaction(CWallet& wallet, const CMutableTransact FlatSigningProvider desc_out; std::string error; std::vector scripts_temp; - std::unique_ptr desc = Parse(desc_str, desc_out, error, true); - if (!desc) { + auto descs = Parse(desc_str, desc_out, error, true); + if (descs.empty()) { throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("Unable to parse descriptor '%s': %s", desc_str, error)); } - desc->Expand(0, desc_out, scripts_temp, desc_out); + for (auto& desc : descs) { + desc->Expand(0, desc_out, scripts_temp, desc_out); + } coinControl.m_external_provider.Merge(std::move(desc_out)); } } diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp index c64aff5fe25..6b57f8c3477 100644 --- a/src/wallet/scriptpubkeyman.cpp +++ b/src/wallet/scriptpubkeyman.cpp @@ -1812,8 +1812,9 @@ std::optional LegacyScriptPubKeyMan::MigrateToDescriptor() std::string desc_str = "combo(" + origin_str + HexStr(key.GetPubKey()) + ")"; FlatSigningProvider keys; std::string error; - std::unique_ptr desc = Parse(desc_str, keys, error, false); - WalletDescriptor w_desc(std::move(desc), creation_time, 0, 0, 0); + std::vector> descs = Parse(desc_str, keys, error, false); + CHECK_NONFATAL(descs.size() == 1); // It shouldn't be possible to have an invalid or multipath descriptor + WalletDescriptor w_desc(std::move(descs.at(0)), creation_time, 0, 0, 0); // Make the DescriptorScriptPubKeyMan and get the scriptPubKeys auto desc_spk_man = std::unique_ptr(new DescriptorScriptPubKeyMan(m_storage, w_desc, m_keypool_size)); @@ -1856,9 +1857,10 @@ std::optional LegacyScriptPubKeyMan::MigrateToDescriptor() std::string desc_str = "combo(" + xpub + "/0h/" + ToString(i) + "h/*h)"; FlatSigningProvider keys; std::string error; - std::unique_ptr desc = Parse(desc_str, keys, error, false); + std::vector> descs = Parse(desc_str, keys, error, false); + CHECK_NONFATAL(descs.size() == 1); // It shouldn't be possible to have an invalid or multipath descriptor uint32_t chain_counter = std::max((i == 1 ? chain.nInternalChainCounter : chain.nExternalChainCounter), (uint32_t)0); - WalletDescriptor w_desc(std::move(desc), 0, 0, chain_counter, 0); + WalletDescriptor w_desc(std::move(descs.at(0)), 0, 0, chain_counter, 0); // Make the DescriptorScriptPubKeyMan and get the scriptPubKeys auto desc_spk_man = std::unique_ptr(new DescriptorScriptPubKeyMan(m_storage, w_desc, m_keypool_size)); diff --git a/src/wallet/test/fuzz/notifications.cpp b/src/wallet/test/fuzz/notifications.cpp index 792079e6c65..a7015f6685b 100644 --- a/src/wallet/test/fuzz/notifications.cpp +++ b/src/wallet/test/fuzz/notifications.cpp @@ -68,7 +68,7 @@ void ImportDescriptors(CWallet& wallet, const std::string& seed_insecure) FlatSigningProvider keys; std::string error; - auto parsed_desc = Parse(descriptor, keys, error, /*require_checksum=*/false); + auto parsed_desc = std::move(Parse(descriptor, keys, error, /*require_checksum=*/false).at(0)); assert(parsed_desc); assert(error.empty()); assert(parsed_desc->IsRange()); diff --git a/src/wallet/test/fuzz/scriptpubkeyman.cpp b/src/wallet/test/fuzz/scriptpubkeyman.cpp index 835470aeae9..2cc54b17170 100644 --- a/src/wallet/test/fuzz/scriptpubkeyman.cpp +++ b/src/wallet/test/fuzz/scriptpubkeyman.cpp @@ -69,10 +69,10 @@ static std::optional> CreateWal FlatSigningProvider keys; std::string error; - std::unique_ptr parsed_desc{Parse(desc_str.value(), keys, error, false)}; - if (!parsed_desc) return std::nullopt; + std::vector> parsed_descs = Parse(desc_str.value(), keys, error, false); + if (parsed_descs.empty()) return std::nullopt; - WalletDescriptor w_desc{std::move(parsed_desc), /*creation_time=*/0, /*range_start=*/0, /*range_end=*/1, /*next_index=*/1}; + WalletDescriptor w_desc{std::move(parsed_descs.at(0)), /*creation_time=*/0, /*range_start=*/0, /*range_end=*/1, /*next_index=*/1}; return std::make_pair(w_desc, keys); } diff --git a/src/wallet/test/ismine_tests.cpp b/src/wallet/test/ismine_tests.cpp index dfad0e21269..c446c0f8d85 100644 --- a/src/wallet/test/ismine_tests.cpp +++ b/src/wallet/test/ismine_tests.cpp @@ -25,13 +25,14 @@ wallet::ScriptPubKeyMan* CreateDescriptor(CWallet& keystore, const std::string& FlatSigningProvider keys; std::string error; - std::unique_ptr parsed_desc = Parse(desc_str, keys, error, false); - BOOST_CHECK(success == (parsed_desc != nullptr)); + auto parsed_descs = Parse(desc_str, keys, error, false); + BOOST_CHECK(success == (!parsed_descs.empty())); if (!success) return nullptr; + auto& desc = parsed_descs.at(0); const int64_t range_start = 0, range_end = 1, next_index = 0, timestamp = 1; - WalletDescriptor w_desc(std::move(parsed_desc), timestamp, range_start, range_end, next_index); + WalletDescriptor w_desc(std::move(desc), timestamp, range_start, range_end, next_index); LOCK(keystore.cs_wallet); diff --git a/src/wallet/test/psbt_wallet_tests.cpp b/src/wallet/test/psbt_wallet_tests.cpp index b5a3b22c544..817c4f5feaa 100644 --- a/src/wallet/test/psbt_wallet_tests.cpp +++ b/src/wallet/test/psbt_wallet_tests.cpp @@ -21,8 +21,9 @@ static void import_descriptor(CWallet& wallet, const std::string& descriptor) AssertLockHeld(wallet.cs_wallet); FlatSigningProvider provider; std::string error; - std::unique_ptr desc = Parse(descriptor, provider, error, /* require_checksum=*/ false); - assert(desc); + auto descs = Parse(descriptor, provider, error, /* require_checksum=*/ false); + assert(descs.size() == 1); + auto& desc = descs.at(0); WalletDescriptor w_desc(std::move(desc), 0, 0, 10, 0); wallet.AddWalletDescriptor(w_desc, provider, "", false); } diff --git a/src/wallet/test/util.cpp b/src/wallet/test/util.cpp index b21a9a601d1..23d8eaf09cc 100644 --- a/src/wallet/test/util.cpp +++ b/src/wallet/test/util.cpp @@ -31,8 +31,9 @@ std::unique_ptr CreateSyncedWallet(interfaces::Chain& chain, CChain& cc FlatSigningProvider provider; std::string error; - std::unique_ptr desc = Parse("combo(" + EncodeSecret(key) + ")", provider, error, /* require_checksum=*/ false); - assert(desc); + auto descs = Parse("combo(" + EncodeSecret(key) + ")", provider, error, /* require_checksum=*/ false); + assert(descs.size() == 1); + auto& desc = descs.at(0); WalletDescriptor w_desc(std::move(desc), 0, 0, 1, 1); if (!wallet->AddWalletDescriptor(w_desc, provider, "", false)) assert(false); } diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp index 53f3bcc4217..7cf90edea5f 100644 --- a/src/wallet/test/wallet_tests.cpp +++ b/src/wallet/test/wallet_tests.cpp @@ -65,8 +65,9 @@ static void AddKey(CWallet& wallet, const CKey& key) LOCK(wallet.cs_wallet); FlatSigningProvider provider; std::string error; - std::unique_ptr desc = Parse("combo(" + EncodeSecret(key) + ")", provider, error, /* require_checksum=*/ false); - assert(desc); + auto descs = Parse("combo(" + EncodeSecret(key) + ")", provider, error, /* require_checksum=*/ false); + assert(descs.size() == 1); + auto& desc = descs.at(0); WalletDescriptor w_desc(std::move(desc), 0, 0, 1, 1); if (!wallet.AddWalletDescriptor(w_desc, provider, "", false)) assert(false); } diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index d569c64b433..2ff5a529841 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -3743,10 +3743,11 @@ void CWallet::SetupDescriptorScriptPubKeyMans() const std::string& desc_str = desc_val.getValStr(); FlatSigningProvider keys; std::string desc_error; - std::unique_ptr desc = Parse(desc_str, keys, desc_error, false); - if (desc == nullptr) { + auto descs = Parse(desc_str, keys, desc_error, false); + if (descs.empty()) { throw std::runtime_error(std::string(__func__) + ": Invalid descriptor \"" + desc_str + "\" (" + desc_error + ")"); } + auto& desc = descs.at(0); if (!desc->GetOutputType()) { continue; } @@ -4285,12 +4286,12 @@ bool DoMigration(CWallet& wallet, WalletContext& context, bilingual_str& error, // Parse the descriptor FlatSigningProvider keys; std::string parse_err; - std::unique_ptr desc = Parse(desc_str, keys, parse_err, /* require_checksum */ true); - assert(desc); // It shouldn't be possible to have the LegacyScriptPubKeyMan make an invalid descriptor - assert(!desc->IsRange()); // It shouldn't be possible to have LegacyScriptPubKeyMan make a ranged watchonly descriptor + std::vector> descs = Parse(desc_str, keys, parse_err, /* require_checksum */ true); + assert(descs.size() == 1); // It shouldn't be possible to have the LegacyScriptPubKeyMan make an invalid descriptor or a multipath descriptors + assert(!descs.at(0)->IsRange()); // It shouldn't be possible to have LegacyScriptPubKeyMan make a ranged watchonly descriptor // Add to the wallet - WalletDescriptor w_desc(std::move(desc), creation_time, 0, 0, 0); + WalletDescriptor w_desc(std::move(descs.at(0)), creation_time, 0, 0, 0); data->watchonly_wallet->AddWalletDescriptor(w_desc, keys, "", false); } @@ -4322,12 +4323,12 @@ bool DoMigration(CWallet& wallet, WalletContext& context, bilingual_str& error, // Parse the descriptor FlatSigningProvider keys; std::string parse_err; - std::unique_ptr desc = Parse(desc_str, keys, parse_err, /* require_checksum */ true); - assert(desc); // It shouldn't be possible to have the LegacyScriptPubKeyMan make an invalid descriptor - assert(!desc->IsRange()); // It shouldn't be possible to have LegacyScriptPubKeyMan make a ranged watchonly descriptor + std::vector> descs = Parse(desc_str, keys, parse_err, /* require_checksum */ true); + assert(descs.size() == 1); // It shouldn't be possible to have the LegacyScriptPubKeyMan make an invalid descriptor or a multipath descriptors + assert(!descs.at(0)->IsRange()); // It shouldn't be possible to have LegacyScriptPubKeyMan make a ranged watchonly descriptor // Add to the wallet - WalletDescriptor w_desc(std::move(desc), creation_time, 0, 0, 0); + WalletDescriptor w_desc(std::move(descs.at(0)), creation_time, 0, 0, 0); data->solvable_wallet->AddWalletDescriptor(w_desc, keys, "", false); } diff --git a/src/wallet/walletutil.cpp b/src/wallet/walletutil.cpp index 0de2617d456..53e65d0194a 100644 --- a/src/wallet/walletutil.cpp +++ b/src/wallet/walletutil.cpp @@ -94,8 +94,8 @@ WalletDescriptor GenerateWalletDescriptor(const CExtPubKey& master_key, const Ou // Make the descriptor FlatSigningProvider keys; std::string error; - std::unique_ptr desc = Parse(desc_str, keys, error, false); - WalletDescriptor w_desc(std::move(desc), creation_time, 0, 0, 0); + std::vector> desc = Parse(desc_str, keys, error, false); + WalletDescriptor w_desc(std::move(desc.at(0)), creation_time, 0, 0, 0); return w_desc; } diff --git a/src/wallet/walletutil.h b/src/wallet/walletutil.h index 38926c1eb81..3416f1dcd05 100644 --- a/src/wallet/walletutil.h +++ b/src/wallet/walletutil.h @@ -96,10 +96,14 @@ public: { std::string error; FlatSigningProvider keys; - descriptor = Parse(str, keys, error, true); - if (!descriptor) { + auto descs = Parse(str, keys, error, true); + if (descs.empty()) { throw std::ios_base::failure("Invalid descriptor: " + error); } + if (descs.size() > 1) { + throw std::ios_base::failure("Can't load a multipath descriptor from databases"); + } + descriptor = std::move(descs.at(0)); id = DescriptorID(*descriptor); } From a90eee444c965bbd7bcddf9656eca9cee14c3aec Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Wed, 7 Aug 2024 16:29:07 -0400 Subject: [PATCH 06/14] tests: Add unit tests for multipath descriptors --- src/test/descriptor_tests.cpp | 349 ++++++++++++++++++++++++++++++++-- 1 file changed, 338 insertions(+), 11 deletions(-) diff --git a/src/test/descriptor_tests.cpp b/src/test/descriptor_tests.cpp index f9fbbf8cabe..73c16cc2e7b 100644 --- a/src/test/descriptor_tests.cpp +++ b/src/test/descriptor_tests.cpp @@ -133,7 +133,8 @@ void DoCheck(std::string prv, std::string pub, const std::string& norm_pub, int const std::vector>& scripts, const std::optional& type, std::optional op_desc_id = std::nullopt, const std::set>& paths = ONLY_EMPTY, bool replace_apostrophe_with_h_in_prv=false, bool replace_apostrophe_with_h_in_pub=false, uint32_t spender_nlocktime=0, uint32_t spender_nsequence=CTxIn::SEQUENCE_FINAL, - std::map, std::vector> preimages={}) + std::map, std::vector> preimages={}, + std::optional expected_prv = std::nullopt, std::optional expected_pub = std::nullopt, int desc_index = 0) { FlatSigningProvider keys_priv, keys_pub; std::set> left_paths = paths; @@ -153,8 +154,8 @@ void DoCheck(std::string prv, std::string pub, const std::string& norm_pub, int parse_pubs = Parse(pub, keys_pub, error); BOOST_CHECK_MESSAGE(!parse_pubs.empty(), error); - auto& parse_priv = parse_privs.at(0); - auto& parse_pub = parse_pubs.at(0); + auto& parse_priv = parse_privs.at(desc_index); + auto& parse_pub = parse_pubs.at(desc_index); // We must be able to estimate the max satisfaction size for any solvable descriptor top descriptor (but combo). const bool is_nontop_or_nonsolvable{!parse_priv->IsSolvable() || !parse_priv->GetOutputType()}; @@ -176,11 +177,17 @@ void DoCheck(std::string prv, std::string pub, const std::string& norm_pub, int BOOST_CHECK(keys_priv.keys.size()); BOOST_CHECK(!keys_pub.keys.size()); - // Check that both versions serialize back to the public version. + // If expected_pub is provided, check that the serialize matches that. + // Otherwise check that they serialize back to the public version. std::string pub1 = parse_priv->ToString(); std::string pub2 = parse_pub->ToString(); - BOOST_CHECK_MESSAGE(EqualDescriptor(pub, pub1), "Private ser: " + pub1 + " Public desc: " + pub); - BOOST_CHECK_MESSAGE(EqualDescriptor(pub, pub2), "Public ser: " + pub2 + " Public desc: " + pub); + if (expected_pub) { + BOOST_CHECK_MESSAGE(EqualDescriptor(*expected_pub, pub1), "Private ser: " + pub1 + " Public desc: " + *expected_pub); + BOOST_CHECK_MESSAGE(EqualDescriptor(*expected_pub, pub2), "Public ser: " + pub2 + " Public desc: " + *expected_pub); + } else { + BOOST_CHECK_MESSAGE(EqualDescriptor(pub, pub1), "Private ser: " + pub1 + " Public desc: " + pub); + BOOST_CHECK_MESSAGE(EqualDescriptor(pub, pub2), "Public ser: " + pub2 + " Public desc: " + pub); + } // Check that the COMPAT identifier did not change if (op_desc_id) { @@ -191,10 +198,19 @@ void DoCheck(std::string prv, std::string pub, const std::string& norm_pub, int if (!(flags & MISSING_PRIVKEYS)) { std::string prv1; BOOST_CHECK(parse_priv->ToPrivateString(keys_priv, prv1)); - BOOST_CHECK_MESSAGE(EqualDescriptor(prv, prv1), "Private ser: " + prv1 + " Private desc: " + prv); + if (expected_prv) { + BOOST_CHECK_MESSAGE(EqualDescriptor(*expected_prv, prv1), "Private ser: " + prv1 + "Private desc: " + *expected_prv); + } else { + BOOST_CHECK_MESSAGE(EqualDescriptor(prv, prv1), "Private ser: " + prv1 + " Private desc: " + prv); + } BOOST_CHECK(!parse_priv->ToPrivateString(keys_pub, prv1)); BOOST_CHECK(parse_pub->ToPrivateString(keys_priv, prv1)); - BOOST_CHECK_MESSAGE(EqualDescriptor(prv, prv1), "Private ser: " + prv1 + " Private desc: " + prv); + if (expected_prv) { + BOOST_CHECK(EqualDescriptor(*expected_prv, prv1)); + BOOST_CHECK_MESSAGE(EqualDescriptor(*expected_prv, prv1), "Private ser: " + prv1 + " Private desc: " + *expected_prv); + } else { + BOOST_CHECK_MESSAGE(EqualDescriptor(prv, prv1), "Private ser: " + prv1 + " Private desc: " + prv); + } BOOST_CHECK(!parse_pub->ToPrivateString(keys_pub, prv1)); } @@ -375,18 +391,42 @@ void DoCheck(std::string prv, std::string pub, const std::string& norm_pub, int void Check(const std::string& prv, const std::string& pub, const std::string& norm_pub, int flags, const std::vector>& scripts, const std::optional& type, std::optional op_desc_id = std::nullopt, const std::set>& paths = ONLY_EMPTY, uint32_t spender_nlocktime=0, - uint32_t spender_nsequence=CTxIn::SEQUENCE_FINAL, std::map, std::vector> preimages={}) + uint32_t spender_nsequence=CTxIn::SEQUENCE_FINAL, std::map, std::vector> preimages={}, + std::optional expected_prv = std::nullopt, std::optional expected_pub = std::nullopt, int desc_index = 0) { // Do not replace apostrophes with 'h' in prv and pub DoCheck(prv, pub, norm_pub, flags, scripts, type, op_desc_id, paths, /*replace_apostrophe_with_h_in_prv=*/false, /*replace_apostrophe_with_h_in_pub=*/false, /*spender_nlocktime=*/spender_nlocktime, - /*spender_nsequence=*/spender_nsequence, /*preimages=*/preimages); + /*spender_nsequence=*/spender_nsequence, /*preimages=*/preimages, + expected_prv, expected_pub, desc_index); // Replace apostrophes with 'h' both in prv and in pub, if apostrophes are found in both if (prv.find('\'') != std::string::npos && pub.find('\'') != std::string::npos) { DoCheck(prv, pub, norm_pub, flags, scripts, type, op_desc_id, paths, /*replace_apostrophe_with_h_in_prv=*/true, /*replace_apostrophe_with_h_in_pub=*/true, /*spender_nlocktime=*/spender_nlocktime, - /*spender_nsequence=*/spender_nsequence, /*preimages=*/preimages); + /*spender_nsequence=*/spender_nsequence, /*preimages=*/preimages, + expected_prv, expected_pub, desc_index); + } +} + +void CheckMultipath(const std::string& prv, + const std::string& pub, + const std::vector& expanded_prvs, + const std::vector& expanded_pubs, + const std::vector& expanded_norm_pubs, + int flags, + const std::vector>>& scripts, + const std::optional& type, + const std::vector>>& paths) +{ + assert(expanded_prvs.size() == expanded_pubs.size()); + assert(expanded_prvs.size() == expanded_norm_pubs.size()); + assert(expanded_prvs.size() == scripts.size()); + assert(expanded_prvs.size() == paths.size()); + for (size_t i = 0; i < expanded_prvs.size(); ++i) { + Check(prv, pub, expanded_norm_pubs.at(i), flags, scripts.at(i), type, std::nullopt, paths.at(i), + /*spender_nlocktime=*/0, /*spender_nsequence=*/CTxIn::SEQUENCE_FINAL, /*preimages=*/{}, + expanded_prvs.at(i), expanded_pubs.at(i), i); } } @@ -495,6 +535,293 @@ BOOST_AUTO_TEST_CASE(descriptor_test) CheckUnparsable("pkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/1aa)", "pkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1aa)", "pkh(): Key path value '1aa' is not a valid uint32"); // Path is not valid uint Check("pkh([01234567/10/20]xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/2147483647'/0)", "pkh([01234567/10/20]xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/2147483647'/0)", "pkh([01234567/10/20/2147483647h]xpub69H7F5dQzmVd3vPuLKtcXJziMEQByuDidnX3YdwgtNsecY5HRGtAAQC5mXTt4dsv9RzyjgDjAQs9VGVV6ydYCHnprc9vvaA5YtqWyL6hyds/0)", HARDENED, {{"76a914ebdc90806a9c4356c1c88e42216611e1cb4c1c1788ac"}}, OutputType::LEGACY, /*op_desc_id=*/std::nullopt, {{10, 20, 0xFFFFFFFFUL, 0}}); + // Multipath versions with BIP32 derivations + CheckMultipath("pk(xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/<0;1>)", + "pk(xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/<0;1>)", + { + "pk(xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0)", + "pk(xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/1)", + }, + { + "pk(xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0)", + "pk(xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/1)", + }, + { + "pk(xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0)", + "pk(xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/1)", + }, + DEFAULT, + { + {{"210379e45b3cf75f9c5f9befd8e9506fb962f6a9d185ac87001ec44a8d3df8d4a9e3ac"}}, + {{"21034f8d02282ac6786737d0f37f0df7655f49daa24843bc7de3f4ea88603d26d10aac"}}, + }, + std::nullopt, + { + {{0}}, + {{1}}, + } + ); + CheckMultipath("pkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/<2147483647h;0>/0)", + "pkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/<2147483647h;0>/0)", + { + "pkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/2147483647h/0)", + "pkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/0/0)", + }, + { + "pkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/2147483647h/0)", + "pkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/0/0)", + }, + { + "pkh([bd16bee5/2147483647h]xpub69H7F5dQzmVd3vPuLKtcXJziMEQByuDidnX3YdwgtNsecY5HRGtAAQC5mXTt4dsv9RzyjgDjAQs9VGVV6ydYCHnprc9vvaA5YtqWyL6hyds/0)", + "pkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/0/0)", + }, + HARDENED, + { + {{"76a914ebdc90806a9c4356c1c88e42216611e1cb4c1c1788ac"}}, + {{"76a914f103317b9f0b758a62cb3879281d23e3b1deb90d88ac"}}, + }, + OutputType::LEGACY, + { + {{0xFFFFFFFFUL,0}}, + {{0,0}}, + } + ); + CheckMultipath("wpkh([ffffffff/13h]xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt/<1;3>/2/*)", + "wpkh([ffffffff/13h]xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/<1;3>/2/*)", + { + "wpkh([ffffffff/13h]xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt/1/2/*)", + "wpkh([ffffffff/13h]xprv9vHkqa6EV4sPZHYqZznhT2NPtPCjKuDKGY38FBWLvgaDx45zo9WQRUT3dKYnjwih2yJD9mkrocEZXo1ex8G81dwSM1fwqWpWkeS3v86pgKt/3/2/*)", + }, + { + "wpkh([ffffffff/13h]xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/1/2/*)", + "wpkh([ffffffff/13h]xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/3/2/*)", + }, + { + "wpkh([ffffffff/13h]xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/1/2/*)", + "wpkh([ffffffff/13h]xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/3/2/*)", + }, + RANGE, + { + {{"0014326b2249e3a25d5dc60935f044ee835d090ba859"},{"0014af0bd98abc2f2cae66e36896a39ffe2d32984fb7"},{"00141fa798efd1cbf95cebf912c031b8a4a6e9fb9f27"}}, + {{"001426183882ef9c76b9a44386e9b387f33cee7c3a2d"},{"001447c1b9dc215c3f8b47e572981eb97528768cde4e"},{"00146e92cbaa397f9caeccf9a049460258af6ccd67e2"}}, + }, + OutputType::BECH32, + { + {{0x8000000DUL, 1, 2, 0}, {0x8000000DUL, 1, 2, 1}, {0x8000000DUL, 1, 2, 2}}, + {{0x8000000DUL, 3, 2, 0}, {0x8000000DUL, 3, 2, 1}, {0x8000000DUL, 3, 2, 2}}, + } + ); + CheckMultipath("sh(wpkh(xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi/<10;100h>/20/30/40/*h))", + "sh(wpkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/<10;100h>/20/30/40/*h))", + { + "sh(wpkh(xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi/10/20/30/40/*h))", + "sh(wpkh(xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi/100h/20/30/40/*h))", + }, + { + "sh(wpkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/10/20/30/40/*h))", + "sh(wpkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/100h/20/30/40/*h))", + }, + { + "sh(wpkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/10/20/30/40/*h))", + "sh(wpkh(xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8/100h/20/30/40/*h))", + }, + RANGE | HARDENED | DERIVE_HARDENED, + { + {{"a9149a4d9901d6af519b2a23d4a2f51650fcba87ce7b87"},{"a914bed59fc0024fae941d6e20a3b44a109ae740129287"},{"a9148483aa1116eb9c05c482a72bada4b1db24af654387"}}, + {{"a91470192039cb9529aadf4e53e46d9ac6a13790865787"},{"a914855859faffabf1e4ed2bb7411ab66f4599b1abd287"},{"a9148f2cfd4b486de247c44684160da164617ccf2c2687"}}, + }, + OutputType::P2SH_SEGWIT, + { + {{10, 20, 30, 40, 0x80000000UL}, {10, 20, 30, 40, 0x80000001UL}, {10, 20, 30, 40, 0x80000002UL}}, + {{0x80000064UL, 20, 30, 40, 0x80000000UL}, {0x80000064UL, 20, 30, 40, 0x80000001UL}, {0x80000064UL, 20, 30, 40, 0x80000002UL}}, + } + ); + CheckMultipath("multi(2,xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc/<1;2>/*,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/<3;4>/0/*)", + "multi(2,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/<1;2>/*,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/<3;4>/0/*)", + { + "multi(2,xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc/1/*,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/3/0/*)", + "multi(2,xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc/2/*,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/4/0/*)", + }, + { + "multi(2,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/3/0/*)", + "multi(2,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/2/*,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/4/0/*)", + }, + { + "multi(2,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/*,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/3/0/*)", + "multi(2,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/2/*,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/4/0/*)", + }, + RANGE, + { + {{"522103095e95d8c50ae3f3fea93fa8e983f710489f60ff681a658c06eba64622c824b121020443e9e729b42628913f1a69b46b7d43ff87c46e86140e12ee420d7e2e8caf8c52ae"},{"5221027512d6bd74e24eeb1ad752d5be800adc5886ded11c5293a9a701db83658b526a2102371e912dea5fefa56158908fe4c9f66bc925a8939b10f3821e8f8be797b9ca8252ae"},{"522102cc9fd211dc0a1c8bb7a106ff831be0e253bc992f21d08fb8a6fd43fae51b9b892103e43eddc68afc9746c9d09ce0bf8067b4f2416287abbc422ed1ac300673b1104952ae"}}, + {{"5221031c0517fff3d483f06ca769bd2326bf30aca1c4de278e676e6ef760c3301244c6210316e171ff4f82dc62ad3f0d84c97865034fc5041eaa508b48c1d7af77f301c8bd52ae"},{"52210240f010ccff4202ade2ef87756f6b9af57bbf5ebcb0393b949e6e5d45d30bff36210229057a7e03510b8cb66727fab3f47a52a02ea94eae03e7c2e81b72a26781bfde52ae"},{"5221034052522058a07b647bd08fa1a9eaedae0222eac76ddd122ff8096ec969398de721038cb8180dd4c956848bcf191e45aaf297146207559fb8737881156aadaf13704152ae"}}, + }, + std::nullopt, + { + {{1, 0}, {1, 1}, {1, 2}, {3, 0, 0}, {3, 0, 1}, {3, 0, 2}}, + {{2, 0}, {2, 1}, {2, 2}, {4, 0, 0}, {4, 0, 1}, {4, 0, 2}}, + } + ); + CheckMultipath("pkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/<0;1;2>)", + "pkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/<0;1;2>)", + { + "pkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/0)", + "pkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/1)", + "pkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/2)", + }, + { + "pkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/0)", + "pkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1)", + "pkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/2)", + }, + { + "pkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/0)", + "pkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1)", + "pkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/2)", + }, + DEFAULT, + { + {{"76a9145a61ff8eb7aaca3010db97ebda76121610b7809688ac"}}, + {{"76a9142f792a782cf4adbb321fe646c8e220563649b8fa88ac"}}, + {{"76a914dcc5b93b52177d78f97b3f2d259b9a86ee1403b188ac"}}, + }, + OutputType::LEGACY, + { + {{0}}, + {{1}}, + {{2}}, + } + ); + CheckMultipath("sh(multi(2,xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc/<1;2;3>/0/*,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0/*,xprv9s21ZrQH143K3jUwNHoqQNrtzJnJmx4Yup8NkNLdVQCymYbPbJXnPhwkfTfxZfptcs3rLAPUXS39oDLgrNKQGwbGsEmJJ8BU3RzQuvShEG4/0/0/<3;4;5>/*))", + "sh(multi(2,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/<1;2;3>/0/*,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0/*,xpub661MyMwAqRbcGDZQUKLqmWodYLcoBQnQH33yYkkF3jjxeLvY8qr2wWGEWkiKFaaQfJCoi3HeEq3Dc5DptfbCyjD38fNhSqtKc1UHaP4ba3t/0/0/<3;4;5>/*))", + { + "sh(multi(2,xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc/1/0/*,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0/*,xprv9s21ZrQH143K3jUwNHoqQNrtzJnJmx4Yup8NkNLdVQCymYbPbJXnPhwkfTfxZfptcs3rLAPUXS39oDLgrNKQGwbGsEmJJ8BU3RzQuvShEG4/0/0/3/*))", + "sh(multi(2,xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc/2/0/*,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0/*,xprv9s21ZrQH143K3jUwNHoqQNrtzJnJmx4Yup8NkNLdVQCymYbPbJXnPhwkfTfxZfptcs3rLAPUXS39oDLgrNKQGwbGsEmJJ8BU3RzQuvShEG4/0/0/4/*))", + "sh(multi(2,xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc/3/0/*,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0/*,xprv9s21ZrQH143K3jUwNHoqQNrtzJnJmx4Yup8NkNLdVQCymYbPbJXnPhwkfTfxZfptcs3rLAPUXS39oDLgrNKQGwbGsEmJJ8BU3RzQuvShEG4/0/0/5/*))", + }, + { + "sh(multi(2,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/0/*,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0/*,xpub661MyMwAqRbcGDZQUKLqmWodYLcoBQnQH33yYkkF3jjxeLvY8qr2wWGEWkiKFaaQfJCoi3HeEq3Dc5DptfbCyjD38fNhSqtKc1UHaP4ba3t/0/0/3/*))", + "sh(multi(2,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/2/0/*,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0/*,xpub661MyMwAqRbcGDZQUKLqmWodYLcoBQnQH33yYkkF3jjxeLvY8qr2wWGEWkiKFaaQfJCoi3HeEq3Dc5DptfbCyjD38fNhSqtKc1UHaP4ba3t/0/0/4/*))", + "sh(multi(2,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/3/0/*,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0/*,xpub661MyMwAqRbcGDZQUKLqmWodYLcoBQnQH33yYkkF3jjxeLvY8qr2wWGEWkiKFaaQfJCoi3HeEq3Dc5DptfbCyjD38fNhSqtKc1UHaP4ba3t/0/0/5/*))", + }, + { + "sh(multi(2,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/0/*,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0/*,xpub661MyMwAqRbcGDZQUKLqmWodYLcoBQnQH33yYkkF3jjxeLvY8qr2wWGEWkiKFaaQfJCoi3HeEq3Dc5DptfbCyjD38fNhSqtKc1UHaP4ba3t/0/0/3/*))", + "sh(multi(2,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/2/0/*,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0/*,xpub661MyMwAqRbcGDZQUKLqmWodYLcoBQnQH33yYkkF3jjxeLvY8qr2wWGEWkiKFaaQfJCoi3HeEq3Dc5DptfbCyjD38fNhSqtKc1UHaP4ba3t/0/0/4/*))", + "sh(multi(2,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/3/0/*,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0/*,xpub661MyMwAqRbcGDZQUKLqmWodYLcoBQnQH33yYkkF3jjxeLvY8qr2wWGEWkiKFaaQfJCoi3HeEq3Dc5DptfbCyjD38fNhSqtKc1UHaP4ba3t/0/0/5/*))", + }, + RANGE, + { + {{"a914689cdf7de5836ec04fb971d128cc84858f73e11487"},{"a9142ea7dbaf0a77ee19f080cdacb3e13560e3cd9cf587"},{"a9143da854021f58f5e2d3ff6bb4fcd0ced877deb34987"}}, + {{"a9143dd613d162e89b83369bbf08e5f1977cfdc9b02787"},{"a91449eef5d3df5c465b20a630c66058fe689082d8e187"},{"a91492be56babf54ea2109c577f799ba6d73948e8c3287"}}, + {{"a9140093ca92097bdf557fbb0570bb77e1efd2e7529c87"},{"a914e4d0419d3d2ce8f921a800796811ff5462bb151887"},{"a914997bf69841ac444190dc02f5e6031dd6f8feab4587"}}, + }, + OutputType::LEGACY, + { + {{1, 0, 0}, {1, 0, 1}, {1, 0, 2}, {0, 0}, {0, 1}, {0, 2}, {0, 0, 3, 0}, {0, 0, 3, 1}, {0, 0, 3, 2}}, + {{2, 0, 0}, {2, 0, 1}, {2, 0, 2}, {0, 0}, {0, 1}, {0, 2}, {0, 0, 4, 0}, {0, 0, 4, 1}, {0, 0, 4, 2}}, + {{3, 0, 0}, {3, 0, 1}, {3, 0, 2}, {0, 0}, {0, 1}, {0, 2}, {0, 0, 5, 0}, {0, 0, 5, 1}, {0, 0, 5, 2}}, + } + ); + CheckMultipath("tr(xprv9s21ZrQH143K2Zu2kTVKcQi9nKhfgJUkYqG73wXsHuhATm1wkt6kcSZeTYEw2PL7krZtJopEYDvBdYWdAai3n3TWUTCVfHvPHqTYJv7smYe/<6;7;8>/*,{pk(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc/<1;2;3>/0/*),pk(xprv9s21ZrQH143K3jUwNHoqQNrtzJnJmx4Yup8NkNLdVQCymYbPbJXnPhwkfTfxZfptcs3rLAPUXS39oDLgrNKQGwbGsEmJJ8BU3RzQuvShEG4/0/0/<3;4;5>/*)})", + "tr(xpub661MyMwAqRbcF3yVrV2KyYetLMYA5mCbv4BhrKwUrFE9LZM6JRR1AEt8Jq4V4C8LwtTke6YEEdCZqgXp85YRk2j74EfJKhe3QybQ9kcUjs4/<6;7;8>/*,{pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/<1;2;3>/0/*),pk(xpub661MyMwAqRbcGDZQUKLqmWodYLcoBQnQH33yYkkF3jjxeLvY8qr2wWGEWkiKFaaQfJCoi3HeEq3Dc5DptfbCyjD38fNhSqtKc1UHaP4ba3t/0/0/<3;4;5>/*)})", + { + "tr(xprv9s21ZrQH143K2Zu2kTVKcQi9nKhfgJUkYqG73wXsHuhATm1wkt6kcSZeTYEw2PL7krZtJopEYDvBdYWdAai3n3TWUTCVfHvPHqTYJv7smYe/6/*,{pk(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc/1/0/*),pk(xprv9s21ZrQH143K3jUwNHoqQNrtzJnJmx4Yup8NkNLdVQCymYbPbJXnPhwkfTfxZfptcs3rLAPUXS39oDLgrNKQGwbGsEmJJ8BU3RzQuvShEG4/0/0/3/*)})", + "tr(xprv9s21ZrQH143K2Zu2kTVKcQi9nKhfgJUkYqG73wXsHuhATm1wkt6kcSZeTYEw2PL7krZtJopEYDvBdYWdAai3n3TWUTCVfHvPHqTYJv7smYe/7/*,{pk(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc/2/0/*),pk(xprv9s21ZrQH143K3jUwNHoqQNrtzJnJmx4Yup8NkNLdVQCymYbPbJXnPhwkfTfxZfptcs3rLAPUXS39oDLgrNKQGwbGsEmJJ8BU3RzQuvShEG4/0/0/4/*)})", + "tr(xprv9s21ZrQH143K2Zu2kTVKcQi9nKhfgJUkYqG73wXsHuhATm1wkt6kcSZeTYEw2PL7krZtJopEYDvBdYWdAai3n3TWUTCVfHvPHqTYJv7smYe/8/*,{pk(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc/3/0/*),pk(xprv9s21ZrQH143K3jUwNHoqQNrtzJnJmx4Yup8NkNLdVQCymYbPbJXnPhwkfTfxZfptcs3rLAPUXS39oDLgrNKQGwbGsEmJJ8BU3RzQuvShEG4/0/0/5/*)})", + }, + { + "tr(xpub661MyMwAqRbcF3yVrV2KyYetLMYA5mCbv4BhrKwUrFE9LZM6JRR1AEt8Jq4V4C8LwtTke6YEEdCZqgXp85YRk2j74EfJKhe3QybQ9kcUjs4/6/*,{pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/0/*),pk(xpub661MyMwAqRbcGDZQUKLqmWodYLcoBQnQH33yYkkF3jjxeLvY8qr2wWGEWkiKFaaQfJCoi3HeEq3Dc5DptfbCyjD38fNhSqtKc1UHaP4ba3t/0/0/3/*)})", + "tr(xpub661MyMwAqRbcF3yVrV2KyYetLMYA5mCbv4BhrKwUrFE9LZM6JRR1AEt8Jq4V4C8LwtTke6YEEdCZqgXp85YRk2j74EfJKhe3QybQ9kcUjs4/7/*,{pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/2/0/*),pk(xpub661MyMwAqRbcGDZQUKLqmWodYLcoBQnQH33yYkkF3jjxeLvY8qr2wWGEWkiKFaaQfJCoi3HeEq3Dc5DptfbCyjD38fNhSqtKc1UHaP4ba3t/0/0/4/*)})", + "tr(xpub661MyMwAqRbcF3yVrV2KyYetLMYA5mCbv4BhrKwUrFE9LZM6JRR1AEt8Jq4V4C8LwtTke6YEEdCZqgXp85YRk2j74EfJKhe3QybQ9kcUjs4/8/*,{pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/3/0/*),pk(xpub661MyMwAqRbcGDZQUKLqmWodYLcoBQnQH33yYkkF3jjxeLvY8qr2wWGEWkiKFaaQfJCoi3HeEq3Dc5DptfbCyjD38fNhSqtKc1UHaP4ba3t/0/0/5/*)})", + }, + { + "tr(xpub661MyMwAqRbcF3yVrV2KyYetLMYA5mCbv4BhrKwUrFE9LZM6JRR1AEt8Jq4V4C8LwtTke6YEEdCZqgXp85YRk2j74EfJKhe3QybQ9kcUjs4/6/*,{pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/0/*),pk(xpub661MyMwAqRbcGDZQUKLqmWodYLcoBQnQH33yYkkF3jjxeLvY8qr2wWGEWkiKFaaQfJCoi3HeEq3Dc5DptfbCyjD38fNhSqtKc1UHaP4ba3t/0/0/3/*)})", + "tr(xpub661MyMwAqRbcF3yVrV2KyYetLMYA5mCbv4BhrKwUrFE9LZM6JRR1AEt8Jq4V4C8LwtTke6YEEdCZqgXp85YRk2j74EfJKhe3QybQ9kcUjs4/7/*,{pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/2/0/*),pk(xpub661MyMwAqRbcGDZQUKLqmWodYLcoBQnQH33yYkkF3jjxeLvY8qr2wWGEWkiKFaaQfJCoi3HeEq3Dc5DptfbCyjD38fNhSqtKc1UHaP4ba3t/0/0/4/*)})", + "tr(xpub661MyMwAqRbcF3yVrV2KyYetLMYA5mCbv4BhrKwUrFE9LZM6JRR1AEt8Jq4V4C8LwtTke6YEEdCZqgXp85YRk2j74EfJKhe3QybQ9kcUjs4/8/*,{pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/3/0/*),pk(xpub661MyMwAqRbcGDZQUKLqmWodYLcoBQnQH33yYkkF3jjxeLvY8qr2wWGEWkiKFaaQfJCoi3HeEq3Dc5DptfbCyjD38fNhSqtKc1UHaP4ba3t/0/0/5/*)})", + }, + XONLY_KEYS | RANGE, + { + {{"5120993e5b1d71d14cbb0a90c57ea0fed1d5bf77d5804cee206c3dbd7e4d2c67d869"},{"51207b8f629f6d406b92ffa6284f5545085eafb837c469018b715755f619b587163b"},{"512061f52925826e51e4615007557ddbea55b22c817909d7ebcfd3c454c634643ece"}}, + {{"5120633808b2156d0a6597e8b07f59c387bb4c2d5c02c4cb98f1802748e64c6abf5f"},{"5120fc5f06ded29328c170bf7e49e71c9cc8699befa2bf0a2a80802a1f32ab72d291"},{"5120fd05e2227e0dac972dff9941e332db8461bedc320c2a74def44e469ddbad9d21"}}, + {{"51205d19538c7c0901520eb712d079ae6eebed4f691021da466dc24e9575d9815ad0"},{"5120b9fc348ede2b7b9fb1f84c21741bb36bb3fa0905d0bc9417e07145d3142673f7"},{"51203a655bc5181b12efac82a5a5d1d0969b2ceb92c6fc37f505fdf00ee8afa09b33"}}, + }, + OutputType::BECH32M, + { + {{6, 0}, {6, 1}, {6, 2}, {1, 0, 0}, {1, 0, 1}, {1, 0, 2}, {0, 0, 3, 0}, {0, 0, 3, 1}, {0, 0, 3, 2}}, + {{7, 0}, {7, 1}, {7, 2}, {2, 0, 0}, {2, 0, 1}, {2, 0, 2}, {0, 0, 4, 0}, {0, 0, 4, 1}, {0, 0, 4, 2}}, + {{8, 0}, {8, 1}, {8, 2}, {3, 0, 0}, {3, 0, 1}, {3, 0, 2}, {0, 0, 5, 0}, {0, 0, 5, 1}, {0, 0, 5, 2}}, + } + ); + CheckMultipath("tr(xprv9s21ZrQH143K2Zu2kTVKcQi9nKhfgJUkYqG73wXsHuhATm1wkt6kcSZeTYEw2PL7krZtJopEYDvBdYWdAai3n3TWUTCVfHvPHqTYJv7smYe/6/*,{pk(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc/<1;2;3>/0/*),pk(xprv9s21ZrQH143K3jUwNHoqQNrtzJnJmx4Yup8NkNLdVQCymYbPbJXnPhwkfTfxZfptcs3rLAPUXS39oDLgrNKQGwbGsEmJJ8BU3RzQuvShEG4/0/0/<3;4;5>/*)})", + "tr(xpub661MyMwAqRbcF3yVrV2KyYetLMYA5mCbv4BhrKwUrFE9LZM6JRR1AEt8Jq4V4C8LwtTke6YEEdCZqgXp85YRk2j74EfJKhe3QybQ9kcUjs4/6/*,{pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/<1;2;3>/0/*),pk(xpub661MyMwAqRbcGDZQUKLqmWodYLcoBQnQH33yYkkF3jjxeLvY8qr2wWGEWkiKFaaQfJCoi3HeEq3Dc5DptfbCyjD38fNhSqtKc1UHaP4ba3t/0/0/<3;4;5>/*)})", + { + "tr(xprv9s21ZrQH143K2Zu2kTVKcQi9nKhfgJUkYqG73wXsHuhATm1wkt6kcSZeTYEw2PL7krZtJopEYDvBdYWdAai3n3TWUTCVfHvPHqTYJv7smYe/6/*,{pk(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc/1/0/*),pk(xprv9s21ZrQH143K3jUwNHoqQNrtzJnJmx4Yup8NkNLdVQCymYbPbJXnPhwkfTfxZfptcs3rLAPUXS39oDLgrNKQGwbGsEmJJ8BU3RzQuvShEG4/0/0/3/*)})", + "tr(xprv9s21ZrQH143K2Zu2kTVKcQi9nKhfgJUkYqG73wXsHuhATm1wkt6kcSZeTYEw2PL7krZtJopEYDvBdYWdAai3n3TWUTCVfHvPHqTYJv7smYe/6/*,{pk(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc/2/0/*),pk(xprv9s21ZrQH143K3jUwNHoqQNrtzJnJmx4Yup8NkNLdVQCymYbPbJXnPhwkfTfxZfptcs3rLAPUXS39oDLgrNKQGwbGsEmJJ8BU3RzQuvShEG4/0/0/4/*)})", + "tr(xprv9s21ZrQH143K2Zu2kTVKcQi9nKhfgJUkYqG73wXsHuhATm1wkt6kcSZeTYEw2PL7krZtJopEYDvBdYWdAai3n3TWUTCVfHvPHqTYJv7smYe/6/*,{pk(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc/3/0/*),pk(xprv9s21ZrQH143K3jUwNHoqQNrtzJnJmx4Yup8NkNLdVQCymYbPbJXnPhwkfTfxZfptcs3rLAPUXS39oDLgrNKQGwbGsEmJJ8BU3RzQuvShEG4/0/0/5/*)})", + }, + { + "tr(xpub661MyMwAqRbcF3yVrV2KyYetLMYA5mCbv4BhrKwUrFE9LZM6JRR1AEt8Jq4V4C8LwtTke6YEEdCZqgXp85YRk2j74EfJKhe3QybQ9kcUjs4/6/*,{pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/0/*),pk(xpub661MyMwAqRbcGDZQUKLqmWodYLcoBQnQH33yYkkF3jjxeLvY8qr2wWGEWkiKFaaQfJCoi3HeEq3Dc5DptfbCyjD38fNhSqtKc1UHaP4ba3t/0/0/3/*)})", + "tr(xpub661MyMwAqRbcF3yVrV2KyYetLMYA5mCbv4BhrKwUrFE9LZM6JRR1AEt8Jq4V4C8LwtTke6YEEdCZqgXp85YRk2j74EfJKhe3QybQ9kcUjs4/6/*,{pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/2/0/*),pk(xpub661MyMwAqRbcGDZQUKLqmWodYLcoBQnQH33yYkkF3jjxeLvY8qr2wWGEWkiKFaaQfJCoi3HeEq3Dc5DptfbCyjD38fNhSqtKc1UHaP4ba3t/0/0/4/*)})", + "tr(xpub661MyMwAqRbcF3yVrV2KyYetLMYA5mCbv4BhrKwUrFE9LZM6JRR1AEt8Jq4V4C8LwtTke6YEEdCZqgXp85YRk2j74EfJKhe3QybQ9kcUjs4/6/*,{pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/3/0/*),pk(xpub661MyMwAqRbcGDZQUKLqmWodYLcoBQnQH33yYkkF3jjxeLvY8qr2wWGEWkiKFaaQfJCoi3HeEq3Dc5DptfbCyjD38fNhSqtKc1UHaP4ba3t/0/0/5/*)})", + }, + { + "tr(xpub661MyMwAqRbcF3yVrV2KyYetLMYA5mCbv4BhrKwUrFE9LZM6JRR1AEt8Jq4V4C8LwtTke6YEEdCZqgXp85YRk2j74EfJKhe3QybQ9kcUjs4/6/*,{pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/1/0/*),pk(xpub661MyMwAqRbcGDZQUKLqmWodYLcoBQnQH33yYkkF3jjxeLvY8qr2wWGEWkiKFaaQfJCoi3HeEq3Dc5DptfbCyjD38fNhSqtKc1UHaP4ba3t/0/0/3/*)})", + "tr(xpub661MyMwAqRbcF3yVrV2KyYetLMYA5mCbv4BhrKwUrFE9LZM6JRR1AEt8Jq4V4C8LwtTke6YEEdCZqgXp85YRk2j74EfJKhe3QybQ9kcUjs4/6/*,{pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/2/0/*),pk(xpub661MyMwAqRbcGDZQUKLqmWodYLcoBQnQH33yYkkF3jjxeLvY8qr2wWGEWkiKFaaQfJCoi3HeEq3Dc5DptfbCyjD38fNhSqtKc1UHaP4ba3t/0/0/4/*)})", + "tr(xpub661MyMwAqRbcF3yVrV2KyYetLMYA5mCbv4BhrKwUrFE9LZM6JRR1AEt8Jq4V4C8LwtTke6YEEdCZqgXp85YRk2j74EfJKhe3QybQ9kcUjs4/6/*,{pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/3/0/*),pk(xpub661MyMwAqRbcGDZQUKLqmWodYLcoBQnQH33yYkkF3jjxeLvY8qr2wWGEWkiKFaaQfJCoi3HeEq3Dc5DptfbCyjD38fNhSqtKc1UHaP4ba3t/0/0/5/*)})", + }, + XONLY_KEYS | RANGE, + { + {{"5120993e5b1d71d14cbb0a90c57ea0fed1d5bf77d5804cee206c3dbd7e4d2c67d869"},{"51207b8f629f6d406b92ffa6284f5545085eafb837c469018b715755f619b587163b"},{"512061f52925826e51e4615007557ddbea55b22c817909d7ebcfd3c454c634643ece"}}, + {{"5120c481a8ada38d1070094f62af526d4f8aae2eb1e44d1fd961be6a25198b4da77b"},{"512034a2d31c091905e62def62b575b88beff41723d83acb02dfada2e73d9c529b40"},{"5120e0ecc278655b092962ded92a5781bd8e86e8408055de05f121e107fa211e5dfb"}}, + {{"51206052cff5efc848e4b38a947803943eb1eb0076523eec1041969851ebcd265555"},{"512009ed83d758c0bdd36e225c961810761c7a360533434a41a17bba709e331e6cd1"},{"5120fcd77851ebaac37564b87e9b351c54492a8fbb1d6afdf7f3a9317703a002b22b"}}, + }, + OutputType::BECH32M, + { + {{6, 0}, {6, 1}, {6, 2}, {1, 0, 0}, {1, 0, 1}, {1, 0, 2}, {0, 0, 3, 0}, {0, 0, 3, 1}, {0, 0, 3, 2}}, + {{6, 0}, {6, 1}, {6, 2}, {2, 0, 0}, {2, 0, 1}, {2, 0, 2}, {0, 0, 4, 0}, {0, 0, 4, 1}, {0, 0, 4, 2}}, + {{6, 0}, {6, 1}, {6, 2}, {3, 0, 0}, {3, 0, 1}, {3, 0, 2}, {0, 0, 5, 0}, {0, 0, 5, 1}, {0, 0, 5, 2}}, + } + ); + CheckMultipath("wsh(or_d(pk([2557c640/48h/1h/0h/2h]xprv9ws7hGFQPbDga6QrETbFM7Gqc7m15UNoJ7KF5kkDhyZCBcANAqRMUCytQ4JM1nLSYvGyFjg6TvBEfNrN3znaFdb67jQoQ7z9kFnd4BUUJiE/<0;1>/*),and_v(v:pkh([00aabb22/48h/1h/0h/2h]xprv9ws7hGFQPbDgbCvNcYVfGeGK8UTSFmAho4iAXZf13yQVJmHuKHN9oMXCv7zsJn8Dcqvqy2iugFWAhDdUUX6r5VLNWkRTpxVoQJ6DbzY9eYa/<0;1>/*),older(2))))", + "wsh(or_d(pk([2557c640/48h/1h/0h/2h]xpub6ArU6mnJDxmynaVKLV8FiFDaA9bVUw6efLEqt99qGK6B4QVWiNjc21JNFKkXNjgT8NCUmpFpSSBrYFtWEAqGirbqT4J1bRFpWyAnYdzmZUm/<0;1>/*),and_v(v:pkh([00aabb22/48h/1h/0h/2h]xpub6ArU6mnJDxmyogzqia2fdnD3gWHvfDtZAHdmKx4ccJwUBZd3rpgQM9qgmPAn1mqT2yh81uvGGohMkg3fNLoXZzn7sRo4a1X3KnCAVot2yuS/<0;1>/*),older(2))))", + { + "wsh(or_d(pk([2557c640/48h/1h/0h/2h]xprv9ws7hGFQPbDga6QrETbFM7Gqc7m15UNoJ7KF5kkDhyZCBcANAqRMUCytQ4JM1nLSYvGyFjg6TvBEfNrN3znaFdb67jQoQ7z9kFnd4BUUJiE/0/*),and_v(v:pkh([00aabb22/48h/1h/0h/2h]xprv9ws7hGFQPbDgbCvNcYVfGeGK8UTSFmAho4iAXZf13yQVJmHuKHN9oMXCv7zsJn8Dcqvqy2iugFWAhDdUUX6r5VLNWkRTpxVoQJ6DbzY9eYa/0/*),older(2))))", + "wsh(or_d(pk([2557c640/48h/1h/0h/2h]xprv9ws7hGFQPbDga6QrETbFM7Gqc7m15UNoJ7KF5kkDhyZCBcANAqRMUCytQ4JM1nLSYvGyFjg6TvBEfNrN3znaFdb67jQoQ7z9kFnd4BUUJiE/1/*),and_v(v:pkh([00aabb22/48h/1h/0h/2h]xprv9ws7hGFQPbDgbCvNcYVfGeGK8UTSFmAho4iAXZf13yQVJmHuKHN9oMXCv7zsJn8Dcqvqy2iugFWAhDdUUX6r5VLNWkRTpxVoQJ6DbzY9eYa/1/*),older(2))))", + }, + { + "wsh(or_d(pk([2557c640/48h/1h/0h/2h]xpub6ArU6mnJDxmynaVKLV8FiFDaA9bVUw6efLEqt99qGK6B4QVWiNjc21JNFKkXNjgT8NCUmpFpSSBrYFtWEAqGirbqT4J1bRFpWyAnYdzmZUm/0/*),and_v(v:pkh([00aabb22/48h/1h/0h/2h]xpub6ArU6mnJDxmyogzqia2fdnD3gWHvfDtZAHdmKx4ccJwUBZd3rpgQM9qgmPAn1mqT2yh81uvGGohMkg3fNLoXZzn7sRo4a1X3KnCAVot2yuS/0/*),older(2))))", + "wsh(or_d(pk([2557c640/48h/1h/0h/2h]xpub6ArU6mnJDxmynaVKLV8FiFDaA9bVUw6efLEqt99qGK6B4QVWiNjc21JNFKkXNjgT8NCUmpFpSSBrYFtWEAqGirbqT4J1bRFpWyAnYdzmZUm/1/*),and_v(v:pkh([00aabb22/48h/1h/0h/2h]xpub6ArU6mnJDxmyogzqia2fdnD3gWHvfDtZAHdmKx4ccJwUBZd3rpgQM9qgmPAn1mqT2yh81uvGGohMkg3fNLoXZzn7sRo4a1X3KnCAVot2yuS/1/*),older(2))))" + }, + { + "wsh(or_d(pk([2557c640/48h/1h/0h/2h]xpub6ArU6mnJDxmynaVKLV8FiFDaA9bVUw6efLEqt99qGK6B4QVWiNjc21JNFKkXNjgT8NCUmpFpSSBrYFtWEAqGirbqT4J1bRFpWyAnYdzmZUm/0/*),and_v(v:pkh([00aabb22/48h/1h/0h/2h]xpub6ArU6mnJDxmyogzqia2fdnD3gWHvfDtZAHdmKx4ccJwUBZd3rpgQM9qgmPAn1mqT2yh81uvGGohMkg3fNLoXZzn7sRo4a1X3KnCAVot2yuS/0/*),older(2))))", + "wsh(or_d(pk([2557c640/48h/1h/0h/2h]xpub6ArU6mnJDxmynaVKLV8FiFDaA9bVUw6efLEqt99qGK6B4QVWiNjc21JNFKkXNjgT8NCUmpFpSSBrYFtWEAqGirbqT4J1bRFpWyAnYdzmZUm/1/*),and_v(v:pkh([00aabb22/48h/1h/0h/2h]xpub6ArU6mnJDxmyogzqia2fdnD3gWHvfDtZAHdmKx4ccJwUBZd3rpgQM9qgmPAn1mqT2yh81uvGGohMkg3fNLoXZzn7sRo4a1X3KnCAVot2yuS/1/*),older(2))))" + }, + RANGE, + { + {{"0020538436a60f2a638ea9e1e1342e9b93374aa7ec559ff0a805b3a185d4ba855d7f"},{"00203a588d107d604b6913201c7c1e1722f07a0f8fb3a382744f17b9ae5f6ccfcdd7"},{"0020d30fb375f7c491a208e77c7b5d0996ca14cf4a770c2ab5981f915c0e4565c74a"}}, + {{"002072b5fc3a691c48fdbaf485f27e787b4094055d4b434c90c81ed1090f3d48733b"},{"0020a9ccdf4496e5d60db4704b27494d9d74f54a16c180ff954a43ce5e3aa465113a"},{"0020d17e21820a0069ca87049513eca763f08a74b586724441e7d76fc5142bcc327c"}}, + }, + OutputType::BECH32, + { + {{0x80000000UL + 48, 0x80000000UL + 1, 0x80000000UL, 0x80000000UL + 2, 0, 0}, {0x80000000UL + 48, 0x80000000UL + 1, 0x80000000UL, 0x80000000UL + 2, 0, 1}, {0x80000000UL + 48, 0x80000000UL + 1, 0x80000000UL, 0x80000000UL + 2, 0, 2}}, + {{0x80000000UL + 48, 0x80000000UL + 1, 0x80000000UL, 0x80000000UL + 2, 1, 0}, {0x80000000UL + 48, 0x80000000UL + 1, 0x80000000UL, 0x80000000UL + 2, 1, 1}, {0x80000000UL + 48, 0x80000000UL + 1, 0x80000000UL, 0x80000000UL + 2, 1, 2}}, + } + ); + CheckUnparsable("pkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/<0;1>/<2;3>)", "pkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/<0;1>/<2;3>)", "pkh(): Multiple multipath key path specifiers found"); + CheckUnparsable("pkh([deadbeef/<0;1>]xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/0)", "pkh([deadbeef/<0;1>]xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/0)", "pkh(): Key path value \'<0;1>\' specifies multipath in a section where multipath is not allowed"); + CheckUnparsable("tr(xprv9s21ZrQH143K2Zu2kTVKcQi9nKhfgJUkYqG73wXsHuhATm1wkt6kcSZeTYEw2PL7krZtJopEYDvBdYWdAai3n3TWUTCVfHvPHqTYJv7smYe/6/*,{pk(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc/<1;2;3>/0/*),pk(xprv9s21ZrQH143K3jUwNHoqQNrtzJnJmx4Yup8NkNLdVQCymYbPbJXnPhwkfTfxZfptcs3rLAPUXS39oDLgrNKQGwbGsEmJJ8BU3RzQuvShEG4/0/0/<3;4>/*)})", "tr(xpub6B4sSbNr8XFYXqqKB7PeUemqgEaVtCLjgd5Lf2VYtezSHozC7ffCvVNCyu9TCgHntRQdimjV3tHbxmNfocxtuh6saNtZEw91gjXLRhQ3Yar/6/*,{pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/<1;2;3>/0/*),pk(xpub6AhFhZJJGt9YB8i85RfrJ8jT3T2FF5EejDCXqXfm1DAczFEXkk8HD3CXTg2TmKM8wTbSnSw3wPg5JuyLitUrpRmkjn2BQXyZnqJx16AGy94/0/0/<3;4>/*)})", "tr(): Multipath subscripts have mismatched lengths"); + CheckUnparsable("tr(xprv9s21ZrQH143K2Zu2kTVKcQi9nKhfgJUkYqG73wXsHuhATm1wkt6kcSZeTYEw2PL7krZtJopEYDvBdYWdAai3n3TWUTCVfHvPHqTYJv7smYe/<6;7;8;9>/*,{pk(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc/<1;2;3>/0/*),pk(xprv9s21ZrQH143K3jUwNHoqQNrtzJnJmx4Yup8NkNLdVQCymYbPbJXnPhwkfTfxZfptcs3rLAPUXS39oDLgrNKQGwbGsEmJJ8BU3RzQuvShEG4/0/0/<3;4;5>/*)})", "tr(xpub661MyMwAqRbcF3yVrV2KyYetLMYA5mCbv4BhrKwUrFE9LZM6JRR1AEt8Jq4V4C8LwtTke6YEEdCZqgXp85YRk2j74EfJKhe3QybQ9kcUjs4/<6;7;8;9>/*,{pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/<1;2;3>/0/*),pk(xpub661MyMwAqRbcGDZQUKLqmWodYLcoBQnQH33yYkkF3jjxeLvY8qr2wWGEWkiKFaaQfJCoi3HeEq3Dc5DptfbCyjD38fNhSqtKc1UHaP4ba3t/0/0/<3;4;5>/*)})", "tr(): Multipath subscripts have mismatched lengths"); + CheckUnparsable("tr(xprv9s21ZrQH143K2Zu2kTVKcQi9nKhfgJUkYqG73wXsHuhATm1wkt6kcSZeTYEw2PL7krZtJopEYDvBdYWdAai3n3TWUTCVfHvPHqTYJv7smYe/<6;7>/*,{pk(xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc/<1;2;3>/0/*),pk(xprv9s21ZrQH143K3jUwNHoqQNrtzJnJmx4Yup8NkNLdVQCymYbPbJXnPhwkfTfxZfptcs3rLAPUXS39oDLgrNKQGwbGsEmJJ8BU3RzQuvShEG4/0/0/<3;4;5>/*)})", "tr(xpub661MyMwAqRbcF3yVrV2KyYetLMYA5mCbv4BhrKwUrFE9LZM6JRR1AEt8Jq4V4C8LwtTke6YEEdCZqgXp85YRk2j74EfJKhe3QybQ9kcUjs4/<6;7>/*,{pk(xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/<1;2;3>/0/*),pk(xpub661MyMwAqRbcGDZQUKLqmWodYLcoBQnQH33yYkkF3jjxeLvY8qr2wWGEWkiKFaaQfJCoi3HeEq3Dc5DptfbCyjD38fNhSqtKc1UHaP4ba3t/0/0/<3;4;5>/*)})", "tr(): Multipath internal key mismatches multipath subscripts lengths"); + CheckUnparsable("sh(multi(2,xprvA1RpRA33e1JQ7ifknakTFpgNXPmW2YvmhqLQYMmrj4xJXXWYpDPS3xz7iAxn8L39njGVyuoseXzU6rcxFLJ8HFsTjSyQbLYnMpCqE2VbFWc/<1;2;3>/0/*,xprv9uPDJpEQgRQfDcW7BkF7eTya6RPxXeJCqCJGHuCJ4GiRVLzkTXBAJMu2qaMWPrS7AANYqdq6vcBcBUdJCVVFceUvJFjaPdGZ2y9WACViL4L/0/*,xprv9s21ZrQH143K3jUwNHoqQNrtzJnJmx4Yup8NkNLdVQCymYbPbJXnPhwkfTfxZfptcs3rLAPUXS39oDLgrNKQGwbGsEmJJ8BU3RzQuvShEG4/0/0/<3;4>/*))", "sh(multi(2,xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL/<1;2;3>/0/*,xpub68NZiKmJWnxxS6aaHmn81bvJeTESw724CRDs6HbuccFQN9Ku14VQrADWgqbhhTHBaohPX4CjNLf9fq9MYo6oDaPPLPxSb7gwQN3ih19Zm4Y/0/*,xpub661MyMwAqRbcGDZQUKLqmWodYLcoBQnQH33yYkkF3jjxeLvY8qr2wWGEWkiKFaaQfJCoi3HeEq3Dc5DptfbCyjD38fNhSqtKc1UHaP4ba3t/0/0/<3;4>/*))", "multi(): Multipath derivation paths have mismatched lengths"); + CheckUnparsable("wpkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/<0>/*)", "wpkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/<0>/*)", "wpkh(): Multipath key path specifiers must have at least two items"); + CheckUnparsable("wsh(andor(pk(xprv9xGFvhWa1Koc8dmeEG5JXVfMaNBkioYscFGmn7yx8YnhFQYeydFfudxdKRzR5p7v1kip85ohB6eUQbPpAee9cFZu9M85G9X4ovPP4xw4xbM/0'/<0;1;2;3>/*),older(10000),pk(xprv9x9bas78RYwopceXTStT8vDuTiu6g1u91L6sG3DhHfDDXKPrYdcHcDuDw4Hv1kjZBWKoZnobUHrdoFxBPUMBTMruUs8HwzL8GxGA95MmZ7v/8/<0;1;2>/*)))", "wsh(andor(pk(xpub6BFcLD3TqhMuM7r7LHcJtdc68Q2F8GGiyUCNaWPZgtKg8CsoXAZvTSH7AhaCPnuuewjwzA2gbAm1y6uaDNNxa7JqTiL76cdioT5rxjgxWXF/0'/<0;1;2;3>/*),older(10000),pk(xpub6B8wzNe2FvW736izZURTW4Ae1kjb5UczNZ2U4RdJqzkCQ7j16AvYA2DhnL8Kb5FeWAZJ43NnGPdjpeSKvAeM8YGaqhCzpD743Uv6S87hfAt/8/<0;1;2>/*)))", "Miniscript: Multipath derivation paths have mismatched lengths"); + CheckUnparsable("wpkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/<>/*)", "wpkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/<>/*)", "wpkh(): Multipath key path specifiers must have at least two items"); + CheckUnparsable("wpkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/<0/*)", "wpkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/<0/*)", "wpkh(): Key path value '<0' is not a valid uint32"); + CheckUnparsable("wpkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/0>/*)", "wpkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/0>/*)", "wpkh(): Key path value '0>' is not a valid uint32"); + CheckUnparsable("wpkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/<0;>/*)", "wpkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/<0;>/*)", "wpkh(): Key path value '' is not a valid uint32"); + CheckUnparsable("wpkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/<;1>/*)", "wpkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/<;1>/*)", "wpkh(): Key path value '' is not a valid uint32"); + CheckUnparsable("wpkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/<0;1;>/*)", "wpkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/<0;1;>/*)", "wpkh(): Key path value '' is not a valid uint32"); + CheckUnparsable("wpkh(xprv9s21ZrQH143K31xYSDQpPDxsXRTUcvj2iNHm5NUtrGiGG5e2DtALGdso3pGz6ssrdK4PFmM8NSpSBHNqPqm55Qn3LqFtT2emdEXVYsCzC2U/<1;1>/*)", "wpkh(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/<1;1>/*)", "wpkh(): Duplicated key path value 1 in multipath specifier"); + // Multisig constructions Check("multi(1,L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1,5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss)", "multi(1,03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd,04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235)", "multi(1,03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd,04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235)", SIGNABLE, {{"512103a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd4104a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea23552ae"}}, std::nullopt, /*op_desc_id=*/uint256S("b147e25eb4a9d3da4e86ed8e970d817563ae2cb9c71a756b11cfdeb4dc11b70c")); Check("sortedmulti(1,L4rK1yDtCWekvXuE6oXD9jCYfFNV2cWRpVuPLBcCU2z8TrisoyY1,5KYZdUEo39z3FPrtuX2QbbwGnNP5zTd7yyr2SC1j299sBCnWjss)", "sortedmulti(1,03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd,04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235)", "sortedmulti(1,03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd,04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235)", SIGNABLE, {{"512103a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd4104a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea23552ae"}}, std::nullopt, /*op_desc_id=*/uint256S("62b59d1e32a62176ef7a17538f3b80c7d1afc53e5644eb753525bdb5d556486c")); From 360456cd221501fde3efe11bdba5c6d999dbb323 Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Wed, 7 Aug 2024 16:29:09 -0400 Subject: [PATCH 07/14] tests: Multipath descriptors for getdescriptorinfo --- test/functional/rpc_getdescriptorinfo.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/test/functional/rpc_getdescriptorinfo.py b/test/functional/rpc_getdescriptorinfo.py index 2eb36f260c6..d229bd7c13d 100755 --- a/test/functional/rpc_getdescriptorinfo.py +++ b/test/functional/rpc_getdescriptorinfo.py @@ -19,10 +19,15 @@ class DescriptorTest(BitcoinTestFramework): self.extra_args = [["-disablewallet"]] self.wallet_names = [] - def test_desc(self, desc, isrange, issolvable, hasprivatekeys): + def test_desc(self, desc, isrange, issolvable, hasprivatekeys, expanded_descs=None): info = self.nodes[0].getdescriptorinfo(desc) assert_equal(info, self.nodes[0].getdescriptorinfo(descsum_create(desc))) - assert_equal(info['descriptor'], descsum_create(desc)) + if expanded_descs is not None: + assert_equal(info["descriptor"], descsum_create(expanded_descs[0])) + assert_equal(info["multipath_expansion"], [descsum_create(x) for x in expanded_descs]) + else: + assert_equal(info['descriptor'], descsum_create(desc)) + assert "multipath_expansion" not in info assert_equal(info['isrange'], isrange) assert_equal(info['issolvable'], issolvable) assert_equal(info['hasprivatekeys'], hasprivatekeys) @@ -60,6 +65,11 @@ class DescriptorTest(BitcoinTestFramework): self.test_desc("pkh([d34db33f/44h/0h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/*)", isrange=True, issolvable=True, hasprivatekeys=False) # A set of *1-of-2* P2WSH multisig outputs where the first multisig key is the *1/0/`i`* child of the first specified xpub and the second multisig key is the *0/0/`i`* child of the second specified xpub, and `i` is any number in a configurable range (`0-1000` by default). self.test_desc("wsh(multi(1,tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/0/*,tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/0/*))", isrange=True, issolvable=True, hasprivatekeys=False) + # A multipath descriptor + self.test_desc("wpkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/<0;1>/*)", isrange=True, issolvable=True, hasprivatekeys=False, + expanded_descs=["wpkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0/*)", "wpkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/*)"]) + self.test_desc("wsh(multi(1,tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/<1;2>/0/*,tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/<2;3>/0/*))", isrange=True, issolvable=True, hasprivatekeys=False, + expanded_descs=["wsh(multi(1,tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/0/*,tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/2/0/*))", "wsh(multi(1,tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/2/0/*,tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/3/0/*))"]) if __name__ == '__main__': From cddc0ba9a9dca3ca5873d768b3b504cdb2ab947b Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Wed, 7 Aug 2024 16:29:12 -0400 Subject: [PATCH 08/14] rpc: Have deriveaddresses derive receiving and change When given a multipath descriptor, derive all of the descriptors. The derived addresses will be returned in an object consisting of multiple arrays. For compatibility, when given a single path descriptor, the addresses are provided in a single array as before. --- src/rpc/output_script.cpp | 97 ++++++++++++++++++++++++--------------- 1 file changed, 61 insertions(+), 36 deletions(-) diff --git a/src/rpc/output_script.cpp b/src/rpc/output_script.cpp index 551a7f78072..dca4d278255 100644 --- a/src/rpc/output_script.cpp +++ b/src/rpc/output_script.cpp @@ -220,6 +220,40 @@ static RPCHelpMan getdescriptorinfo() }; } +static UniValue DeriveAddresses(const Descriptor* desc, int64_t range_begin, int64_t range_end, FlatSigningProvider& key_provider) +{ + UniValue addresses(UniValue::VARR); + + for (int64_t i = range_begin; i <= range_end; ++i) { + FlatSigningProvider provider; + std::vector scripts; + if (!desc->Expand(i, key_provider, scripts, provider)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Cannot derive script without private keys"); + } + + for (const CScript& script : scripts) { + CTxDestination dest; + if (!ExtractDestination(script, dest)) { + // ExtractDestination no longer returns true for P2PK since it doesn't have a corresponding address + // However combo will output P2PK and should just ignore that script + if (scripts.size() > 1 && std::get_if(&dest)) { + continue; + } + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Descriptor does not have a corresponding address"); + } + + addresses.push_back(EncodeDestination(dest)); + } + } + + // This should not be possible, but an assert seems overkill: + if (addresses.empty()) { + throw JSONRPCError(RPC_MISC_ERROR, "Unexpected empty result"); + } + + return addresses; +} + static RPCHelpMan deriveaddresses() { const std::string EXAMPLE_DESCRIPTOR = "wpkh([d34db33f/84h/0h/0h]xpub6DJ2dNUysrn5Vt36jH2KLBT2i1auw1tTSSomg8PhqNiUtx8QX2SvC9nrHu81fT41fvDUnhMjEzQgXnQjKEu3oaqMSzhSrHMxyyoEAmUHQbY/0/*)#cjjspncu"; @@ -234,17 +268,29 @@ static RPCHelpMan deriveaddresses() " tr(,multi_a(,,,...)) P2TR-multisig outputs for the given threshold and pubkeys\n" "\nIn the above, either refers to a fixed public key in hexadecimal notation, or to an xpub/xprv optionally followed by one\n" "or more path elements separated by \"/\", where \"h\" represents a hardened child key.\n" - "For more information on output descriptors, see the documentation in the doc/descriptors.md file.\n" - "Note that only descriptors that specify a single derivation path can be derived.\n"}, + "For more information on output descriptors, see the documentation in the doc/descriptors.md file.\n"}, { {"descriptor", RPCArg::Type::STR, RPCArg::Optional::NO, "The descriptor."}, {"range", RPCArg::Type::RANGE, RPCArg::Optional::OMITTED, "If a ranged descriptor is used, this specifies the end or the range (in [begin,end] notation) to derive."}, }, - RPCResult{ - RPCResult::Type::ARR, "", "", - { - {RPCResult::Type::STR, "address", "the derived addresses"}, - } + { + RPCResult{"for single derivation descriptors", + RPCResult::Type::ARR, "", "", + { + {RPCResult::Type::STR, "address", "the derived addresses"}, + } + }, + RPCResult{"for multipath descriptors", + RPCResult::Type::ARR, "", "The derived addresses for each of the multipath expansions of the descriptor, in multipath specifier order", + { + { + RPCResult::Type::ARR, "", "The derived addresses for a multipath descriptor expansion", + { + {RPCResult::Type::STR, "address", "the derived address"}, + }, + }, + }, + }, }, RPCExamples{ "First three native segwit receive addresses\n" + @@ -268,9 +314,6 @@ static RPCHelpMan deriveaddresses() if (descs.empty()) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error); } - if (descs.size() > 1) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Descriptor with multipath derivation path specifiers are not allowed"); - } auto& desc = descs.at(0); if (!desc->IsRange() && request.params.size() > 1) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Range should not be specified for an un-ranged descriptor"); @@ -280,36 +323,18 @@ static RPCHelpMan deriveaddresses() throw JSONRPCError(RPC_INVALID_PARAMETER, "Range must be specified for a ranged descriptor"); } - UniValue addresses(UniValue::VARR); + UniValue addresses = DeriveAddresses(desc.get(), range_begin, range_end, key_provider); - for (int64_t i = range_begin; i <= range_end; ++i) { - FlatSigningProvider provider; - std::vector scripts; - if (!desc->Expand(i, key_provider, scripts, provider)) { - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Cannot derive script without private keys"); - } - - for (const CScript& script : scripts) { - CTxDestination dest; - if (!ExtractDestination(script, dest)) { - // ExtractDestination no longer returns true for P2PK since it doesn't have a corresponding address - // However combo will output P2PK and should just ignore that script - if (scripts.size() > 1 && std::get_if(&dest)) { - continue; - } - throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Descriptor does not have a corresponding address"); - } - - addresses.push_back(EncodeDestination(dest)); - } + if (descs.size() == 1) { + return addresses; } - // This should not be possible, but an assert seems overkill: - if (addresses.empty()) { - throw JSONRPCError(RPC_MISC_ERROR, "Unexpected empty result"); + UniValue ret(UniValue::VARR); + ret.push_back(addresses); + for (size_t i = 1; i < descs.size(); ++i) { + ret.push_back(DeriveAddresses(descs.at(i).get(), range_begin, range_end, key_provider)); } - - return addresses; + return ret; }, }; } From 16922455253f47fae0466c4ec6c3adfadcfe9182 Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Wed, 7 Aug 2024 16:29:14 -0400 Subject: [PATCH 09/14] tests: Multipath descriptors for scantxoutset and deriveaddresses --- test/functional/rpc_deriveaddresses.py | 3 +++ test/functional/rpc_scantxoutset.py | 1 + 2 files changed, 4 insertions(+) diff --git a/test/functional/rpc_deriveaddresses.py b/test/functional/rpc_deriveaddresses.py index 64994d6bb30..0bb0e2b2236 100755 --- a/test/functional/rpc_deriveaddresses.py +++ b/test/functional/rpc_deriveaddresses.py @@ -29,6 +29,9 @@ class DeriveaddressesTest(BitcoinTestFramework): assert_equal(self.nodes[0].deriveaddresses(ranged_descriptor, [1, 2]), ["bcrt1qhku5rq7jz8ulufe2y6fkcpnlvpsta7rq4442dy", "bcrt1qpgptk2gvshyl0s9lqshsmx932l9ccsv265tvaq"]) assert_equal(self.nodes[0].deriveaddresses(ranged_descriptor, 2), [address, "bcrt1qhku5rq7jz8ulufe2y6fkcpnlvpsta7rq4442dy", "bcrt1qpgptk2gvshyl0s9lqshsmx932l9ccsv265tvaq"]) + ranged_descriptor = descsum_create("wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/<0;1>/*)") + assert_equal(self.nodes[0].deriveaddresses(ranged_descriptor, [1, 2]), [["bcrt1q7c8mdmdktrzs8xgpjmqw90tjn65j5a3yj04m3n", "bcrt1qs6n37uzu0v0qfzf0r0csm0dwa7prc0v5uavgy0"], ["bcrt1qhku5rq7jz8ulufe2y6fkcpnlvpsta7rq4442dy", "bcrt1qpgptk2gvshyl0s9lqshsmx932l9ccsv265tvaq"]]) + assert_raises_rpc_error(-8, "Range should not be specified for an un-ranged descriptor", self.nodes[0].deriveaddresses, descsum_create("wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/0)"), [0, 2]) assert_raises_rpc_error(-8, "Range must be specified for a ranged descriptor", self.nodes[0].deriveaddresses, descsum_create("wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*)")) diff --git a/test/functional/rpc_scantxoutset.py b/test/functional/rpc_scantxoutset.py index 9f77f209efc..0ea424ebb0e 100755 --- a/test/functional/rpc_scantxoutset.py +++ b/test/functional/rpc_scantxoutset.py @@ -110,6 +110,7 @@ class ScantxoutsetTest(BitcoinTestFramework): assert_equal(self.nodes[0].scantxoutset("start", [{"desc": "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/*)", "range": 1499}])['total_amount'], Decimal("12.288")) assert_equal(self.nodes[0].scantxoutset("start", [{"desc": "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/*)", "range": 1500}])['total_amount'], Decimal("28.672")) assert_equal(self.nodes[0].scantxoutset("start", [{"desc": "combo(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/*)", "range": [1500, 1500]}])['total_amount'], Decimal("16.384")) + assert_equal(self.nodes[0].scantxoutset("start", [ {"desc": "pkh(tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/1/1/<0;1>)"}])["total_amount"], Decimal("12.288")) # Test the reported descriptors for a few matches assert_equal(descriptors(self.nodes[0].scantxoutset("start", [{"desc": "combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/0h/0h/*)", "range": 1499}])), ["pkh([0c5f9a1e/0h/0h/0]026dbd8b2315f296d36e6b6920b1579ca75569464875c7ebe869b536a7d9503c8c)#rthll0rg", "pkh([0c5f9a1e/0h/0h/1]033e6f25d76c00bedb3a8993c7d5739ee806397f0529b1b31dda31ef890f19a60c)#mcjajulr"]) From 64dfe3ce4bed9ac168d0b08def8af7485db94ef1 Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Wed, 7 Aug 2024 16:29:15 -0400 Subject: [PATCH 10/14] wallet: Move internal to be per key when importing Instead of applying internal-ness to all keys being imported at the same time, apply it on a per key basis. So each key that is imported will carry with it whether it is for the change keypool. --- src/qt/test/wallettests.cpp | 2 +- src/wallet/rpc/backup.cpp | 16 +++++++++------- src/wallet/scriptpubkeyman.cpp | 4 ++-- src/wallet/scriptpubkeyman.h | 2 +- src/wallet/wallet.cpp | 4 ++-- src/wallet/wallet.h | 2 +- 6 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/qt/test/wallettests.cpp b/src/qt/test/wallettests.cpp index 8fdb56dd7ea..6a573d284ca 100644 --- a/src/qt/test/wallettests.cpp +++ b/src/qt/test/wallettests.cpp @@ -199,7 +199,7 @@ std::shared_ptr SetupLegacyWatchOnlyWallet(interfaces::Node& node, Test wallet->SetupLegacyScriptPubKeyMan(); // Add watched key CPubKey pubKey = test.coinbaseKey.GetPubKey(); - bool import_keys = wallet->ImportPubKeys({pubKey.GetID()}, {{pubKey.GetID(), pubKey}} , /*key_origins=*/{}, /*add_keypool=*/false, /*internal=*/false, /*timestamp=*/1); + bool import_keys = wallet->ImportPubKeys({{pubKey.GetID(), false}}, {{pubKey.GetID(), pubKey}} , /*key_origins=*/{}, /*add_keypool=*/false, /*timestamp=*/1); assert(import_keys); wallet->SetLastBlockProcessed(105, WITH_LOCK(node.context()->chainman->GetMutex(), return node.context()->chainman->ActiveChain().Tip()->GetBlockHash())); } diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp index d92c2b6770a..e7ce293da09 100644 --- a/src/wallet/rpc/backup.cpp +++ b/src/wallet/rpc/backup.cpp @@ -471,7 +471,7 @@ RPCHelpMan importpubkey() pwallet->ImportScriptPubKeys(strLabel, script_pub_keys, /*have_solving_data=*/true, /*apply_label=*/true, /*timestamp=*/1); - pwallet->ImportPubKeys({pubKey.GetID()}, {{pubKey.GetID(), pubKey}} , /*key_origins=*/{}, /*add_keypool=*/false, /*internal=*/false, /*timestamp=*/1); + pwallet->ImportPubKeys({{pubKey.GetID(), false}}, {{pubKey.GetID(), pubKey}} , /*key_origins=*/{}, /*add_keypool=*/false, /*timestamp=*/1); } if (fRescan) { @@ -915,7 +915,7 @@ static std::string RecurseImportData(const CScript& script, ImportData& import_d NONFATAL_UNREACHABLE(); } -static UniValue ProcessImportLegacy(ImportData& import_data, std::map& pubkey_map, std::map& privkey_map, std::set& script_pub_keys, bool& have_solving_data, const UniValue& data, std::vector& ordered_pubkeys) +static UniValue ProcessImportLegacy(ImportData& import_data, std::map& pubkey_map, std::map& privkey_map, std::set& script_pub_keys, bool& have_solving_data, const UniValue& data, std::vector>& ordered_pubkeys) { UniValue warnings(UniValue::VARR); @@ -981,7 +981,7 @@ static UniValue ProcessImportLegacy(ImportData& import_data, std::map& pubkey_map, std::map& privkey_map, std::set& script_pub_keys, bool& have_solving_data, const UniValue& data, std::vector& ordered_pubkeys) +static UniValue ProcessImportDescriptor(ImportData& import_data, std::map& pubkey_map, std::map& privkey_map, std::set& script_pub_keys, bool& have_solving_data, const UniValue& data, std::vector>& ordered_pubkeys) { + const bool internal = data.exists("internal") ? data["internal"].get_bool() : false; + UniValue warnings(UniValue::VARR); const std::string& descriptor = data["desc"].get_str(); @@ -1092,7 +1094,7 @@ static UniValue ProcessImportDescriptor(ImportData& import_data, std::mapExpand(i, keys, scripts_temp, out_keys); std::copy(scripts_temp.begin(), scripts_temp.end(), std::inserter(script_pub_keys, script_pub_keys.end())); for (const auto& key_pair : out_keys.pubkeys) { - ordered_pubkeys.push_back(key_pair.first); + ordered_pubkeys.emplace_back(key_pair.first, internal); } for (const auto& x : out_keys.scripts) { @@ -1167,7 +1169,7 @@ static UniValue ProcessImport(CWallet& wallet, const UniValue& data, const int64 std::map pubkey_map; std::map privkey_map; std::set script_pub_keys; - std::vector ordered_pubkeys; + std::vector> ordered_pubkeys; bool have_solving_data; if (data.exists("scriptPubKey") && data.exists("desc")) { @@ -1200,7 +1202,7 @@ static UniValue ProcessImport(CWallet& wallet, const UniValue& data, const int64 if (!wallet.ImportPrivKeys(privkey_map, timestamp)) { throw JSONRPCError(RPC_WALLET_ERROR, "Error adding key to wallet"); } - if (!wallet.ImportPubKeys(ordered_pubkeys, pubkey_map, import_data.key_origins, add_keypool, internal, timestamp)) { + if (!wallet.ImportPubKeys(ordered_pubkeys, pubkey_map, import_data.key_origins, add_keypool, timestamp)) { throw JSONRPCError(RPC_WALLET_ERROR, "Error adding address to wallet"); } if (!wallet.ImportScriptPubKeys(label, script_pub_keys, have_solving_data, !internal, timestamp)) { diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp index 6b57f8c3477..7bce68a618d 100644 --- a/src/wallet/scriptpubkeyman.cpp +++ b/src/wallet/scriptpubkeyman.cpp @@ -1623,13 +1623,13 @@ bool LegacyScriptPubKeyMan::ImportPrivKeys(const std::map& privkey return true; } -bool LegacyScriptPubKeyMan::ImportPubKeys(const std::vector& ordered_pubkeys, const std::map& pubkey_map, const std::map>& key_origins, const bool add_keypool, const bool internal, const int64_t timestamp) +bool LegacyScriptPubKeyMan::ImportPubKeys(const std::vector>& ordered_pubkeys, const std::map& pubkey_map, const std::map>& key_origins, const bool add_keypool, const int64_t timestamp) { WalletBatch batch(m_storage.GetDatabase()); for (const auto& entry : key_origins) { AddKeyOriginWithDB(batch, entry.second.first, entry.second.second); } - for (const CKeyID& id : ordered_pubkeys) { + for (const auto& [id, internal] : ordered_pubkeys) { auto entry = pubkey_map.find(id); if (entry == pubkey_map.end()) { continue; diff --git a/src/wallet/scriptpubkeyman.h b/src/wallet/scriptpubkeyman.h index 4d9f7bb1fac..c5d37138a33 100644 --- a/src/wallet/scriptpubkeyman.h +++ b/src/wallet/scriptpubkeyman.h @@ -484,7 +484,7 @@ public: bool ImportScripts(const std::set scripts, int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore); bool ImportPrivKeys(const std::map& privkey_map, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore); - bool ImportPubKeys(const std::vector& ordered_pubkeys, const std::map& pubkey_map, const std::map>& key_origins, const bool add_keypool, const bool internal, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore); + bool ImportPubKeys(const std::vector>& ordered_pubkeys, const std::map& pubkey_map, const std::map>& key_origins, const bool add_keypool, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore); bool ImportScriptPubKeys(const std::set& script_pub_keys, const bool have_solving_data, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_KeyStore); /* Returns true if the wallet can generate new keys */ diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 2ff5a529841..79287a9aff6 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -1773,14 +1773,14 @@ bool CWallet::ImportPrivKeys(const std::map& privkey_map, const in return spk_man->ImportPrivKeys(privkey_map, timestamp); } -bool CWallet::ImportPubKeys(const std::vector& ordered_pubkeys, const std::map& pubkey_map, const std::map>& key_origins, const bool add_keypool, const bool internal, const int64_t timestamp) +bool CWallet::ImportPubKeys(const std::vector>& ordered_pubkeys, const std::map& pubkey_map, const std::map>& key_origins, const bool add_keypool, const int64_t timestamp) { auto spk_man = GetLegacyScriptPubKeyMan(); if (!spk_man) { return false; } LOCK(spk_man->cs_KeyStore); - return spk_man->ImportPubKeys(ordered_pubkeys, pubkey_map, key_origins, add_keypool, internal, timestamp); + return spk_man->ImportPubKeys(ordered_pubkeys, pubkey_map, key_origins, add_keypool, timestamp); } bool CWallet::ImportScriptPubKeys(const std::string& label, const std::set& script_pub_keys, const bool have_solving_data, const bool apply_label, const int64_t timestamp) diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 5bc888462f3..7416735eacb 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -686,7 +686,7 @@ public: bool ImportScripts(const std::set scripts, int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool ImportPrivKeys(const std::map& privkey_map, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); - bool ImportPubKeys(const std::vector& ordered_pubkeys, const std::map& pubkey_map, const std::map>& key_origins, const bool add_keypool, const bool internal, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); + bool ImportPubKeys(const std::vector>& ordered_pubkeys, const std::map& pubkey_map, const std::map>& key_origins, const bool add_keypool, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); bool ImportScriptPubKeys(const std::string& label, const std::set& script_pub_keys, const bool have_solving_data, const bool apply_label, const int64_t timestamp) EXCLUSIVE_LOCKS_REQUIRED(cs_wallet); /** Updates wallet birth time if 'time' is below it */ From 32dcbca3fb918bc899a0637f876db31c3419aafd Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Wed, 7 Aug 2024 16:29:17 -0400 Subject: [PATCH 11/14] rpc: Allow importmulti to import multipath descriptors correctly Multipath descriptors will be imported as multiple separate descriptors. When there are exactly 2 multipath items, the first descriptor will be for receiving addreses, and the second for change addresses. When importing a multipath descriptor, 'internal' cannot be specified. --- src/wallet/rpc/backup.cpp | 60 ++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp index e7ce293da09..644a7b10819 100644 --- a/src/wallet/rpc/backup.cpp +++ b/src/wallet/rpc/backup.cpp @@ -1056,8 +1056,6 @@ static UniValue ProcessImportLegacy(ImportData& import_data, std::map& pubkey_map, std::map& privkey_map, std::set& script_pub_keys, bool& have_solving_data, const UniValue& data, std::vector>& ordered_pubkeys) { - const bool internal = data.exists("internal") ? data["internal"].get_bool() : false; - UniValue warnings(UniValue::VARR); const std::string& descriptor = data["desc"].get_str(); @@ -1067,18 +1065,25 @@ static UniValue ProcessImportDescriptor(ImportData& import_data, std::mapGetOutputType() == OutputType::BECH32M) { + if (parsed_descs.at(0)->GetOutputType() == OutputType::BECH32M) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Bech32m descriptors cannot be imported into legacy wallets"); } - have_solving_data = parsed_desc->IsSolvable(); + std::optional internal; + if (data.exists("internal")) { + if (parsed_descs.size() > 1) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Cannot have multipath descriptor while also specifying \'internal\'"); + } + internal = data["internal"].get_bool(); + } + + have_solving_data = parsed_descs.at(0)->IsSolvable(); const bool watch_only = data.exists("watchonly") ? data["watchonly"].get_bool() : false; int64_t range_start = 0, range_end = 0; - if (!parsed_desc->IsRange() && data.exists("range")) { + if (!parsed_descs.at(0)->IsRange() && data.exists("range")) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Range should not be specified for an un-ranged descriptor"); - } else if (parsed_desc->IsRange()) { + } else if (parsed_descs.at(0)->IsRange()) { if (!data.exists("range")) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Descriptor is ranged, please specify the range"); } @@ -1087,25 +1092,34 @@ static UniValue ProcessImportDescriptor(ImportData& import_data, std::map scripts_temp; - parsed_desc->Expand(i, keys, scripts_temp, out_keys); - std::copy(scripts_temp.begin(), scripts_temp.end(), std::inserter(script_pub_keys, script_pub_keys.end())); - for (const auto& key_pair : out_keys.pubkeys) { - ordered_pubkeys.emplace_back(key_pair.first, internal); + for (size_t j = 0; j < parsed_descs.size(); ++j) { + const auto& parsed_desc = parsed_descs.at(j); + bool desc_internal = internal.has_value() && internal.value(); + if (parsed_descs.size() == 2) { + desc_internal = j == 1; + } else if (parsed_descs.size() > 2) { + CHECK_NONFATAL(!desc_internal); } + // Expand all descriptors to get public keys and scripts, and private keys if available. + for (int i = range_start; i <= range_end; ++i) { + FlatSigningProvider out_keys; + std::vector scripts_temp; + parsed_desc->Expand(i, keys, scripts_temp, out_keys); + std::copy(scripts_temp.begin(), scripts_temp.end(), std::inserter(script_pub_keys, script_pub_keys.end())); + for (const auto& key_pair : out_keys.pubkeys) { + ordered_pubkeys.emplace_back(key_pair.first, desc_internal); + } - for (const auto& x : out_keys.scripts) { - import_data.import_scripts.emplace(x.second); + for (const auto& x : out_keys.scripts) { + import_data.import_scripts.emplace(x.second); + } + + parsed_desc->ExpandPrivate(i, keys, out_keys); + + std::copy(out_keys.pubkeys.begin(), out_keys.pubkeys.end(), std::inserter(pubkey_map, pubkey_map.end())); + std::copy(out_keys.keys.begin(), out_keys.keys.end(), std::inserter(privkey_map, privkey_map.end())); + import_data.key_origins.insert(out_keys.origins.begin(), out_keys.origins.end()); } - - parsed_desc->ExpandPrivate(i, keys, out_keys); - - std::copy(out_keys.pubkeys.begin(), out_keys.pubkeys.end(), std::inserter(pubkey_map, pubkey_map.end())); - std::copy(out_keys.keys.begin(), out_keys.keys.end(), std::inserter(privkey_map, privkey_map.end())); - import_data.key_origins.insert(out_keys.origins.begin(), out_keys.origins.end()); } for (size_t i = 0; i < priv_keys.size(); ++i) { From f97d5c137d605ac48f1122a836c9aa5f834957ba Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Wed, 7 Aug 2024 16:29:19 -0400 Subject: [PATCH 12/14] wallet, rpc: Allow importdescriptors to import multipath descriptors Multipath descriptors will be imported as multiple separate descriptors. When there are 2 multipath items, the first descriptor will be for receiving addresses and the second for change. This mirrors importmulti. --- src/wallet/rpc/backup.cpp | 132 ++++++++++++++++++++++---------------- 1 file changed, 76 insertions(+), 56 deletions(-) diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp index 644a7b10819..481eeb4581f 100644 --- a/src/wallet/rpc/backup.cpp +++ b/src/wallet/rpc/backup.cpp @@ -1463,7 +1463,6 @@ static UniValue ProcessDescriptorImport(CWallet& wallet, const UniValue& data, c const std::string& descriptor = data["desc"].get_str(); const bool active = data.exists("active") ? data["active"].get_bool() : false; - const bool internal = data.exists("internal") ? data["internal"].get_bool() : false; const std::string label{LabelFromValue(data["label"])}; // Parse descriptor string @@ -1473,13 +1472,19 @@ static UniValue ProcessDescriptorImport(CWallet& wallet, const UniValue& data, c if (parsed_descs.empty()) { throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, error); } - auto& parsed_desc = parsed_descs.at(0); + std::optional internal; + if (data.exists("internal")) { + if (parsed_descs.size() > 1) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Cannot have multipath descriptor while also specifying \'internal\'"); + } + internal = data["internal"].get_bool(); + } // Range check int64_t range_start = 0, range_end = 1, next_index = 0; - if (!parsed_desc->IsRange() && data.exists("range")) { + if (!parsed_descs.at(0)->IsRange() && data.exists("range")) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Range should not be specified for an un-ranged descriptor"); - } else if (parsed_desc->IsRange()) { + } else if (parsed_descs.at(0)->IsRange()) { if (data.exists("range")) { auto range = ParseDescriptorRange(data["range"]); range_start = range.first; @@ -1501,10 +1506,15 @@ static UniValue ProcessDescriptorImport(CWallet& wallet, const UniValue& data, c } // Active descriptors must be ranged - if (active && !parsed_desc->IsRange()) { + if (active && !parsed_descs.at(0)->IsRange()) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Active descriptors must be ranged"); } + // Multipath descriptors should not have a label + if (parsed_descs.size() > 1 && data.exists("label")) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Multipath descriptors should not have a label"); + } + // Ranged descriptors should not have a label if (data.exists("range") && data.exists("label")) { throw JSONRPCError(RPC_INVALID_PARAMETER, "Ranged descriptors should not have a label"); @@ -1516,7 +1526,7 @@ static UniValue ProcessDescriptorImport(CWallet& wallet, const UniValue& data, c } // Combo descriptor check - if (active && !parsed_desc->IsSingleType()) { + if (active && !parsed_descs.at(0)->IsSingleType()) { throw JSONRPCError(RPC_WALLET_ERROR, "Combo descriptors cannot be set to active"); } @@ -1525,61 +1535,70 @@ static UniValue ProcessDescriptorImport(CWallet& wallet, const UniValue& data, c throw JSONRPCError(RPC_WALLET_ERROR, "Cannot import private keys to a wallet with private keys disabled"); } - // Need to ExpandPrivate to check if private keys are available for all pubkeys - FlatSigningProvider expand_keys; - std::vector scripts; - if (!parsed_desc->Expand(0, keys, scripts, expand_keys)) { - throw JSONRPCError(RPC_WALLET_ERROR, "Cannot expand descriptor. Probably because of hardened derivations without private keys provided"); - } - parsed_desc->ExpandPrivate(0, keys, expand_keys); - - // Check if all private keys are provided - bool have_all_privkeys = !expand_keys.keys.empty(); - for (const auto& entry : expand_keys.origins) { - const CKeyID& key_id = entry.first; - CKey key; - if (!expand_keys.GetKey(key_id, key)) { - have_all_privkeys = false; - break; + for (size_t j = 0; j < parsed_descs.size(); ++j) { + auto parsed_desc = std::move(parsed_descs[j]); + bool desc_internal = internal.has_value() && internal.value(); + if (parsed_descs.size() == 2) { + desc_internal = j == 1; + } else if (parsed_descs.size() > 2) { + CHECK_NONFATAL(!desc_internal); } - } - - // If private keys are enabled, check some things. - if (!wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { - if (keys.keys.empty()) { - throw JSONRPCError(RPC_WALLET_ERROR, "Cannot import descriptor without private keys to a wallet with private keys enabled"); - } - if (!have_all_privkeys) { - warnings.push_back("Not all private keys provided. Some wallet functionality may return unexpected errors"); - } - } - - WalletDescriptor w_desc(std::move(parsed_desc), timestamp, range_start, range_end, next_index); - - // Check if the wallet already contains the descriptor - auto existing_spk_manager = wallet.GetDescriptorScriptPubKeyMan(w_desc); - if (existing_spk_manager) { - if (!existing_spk_manager->CanUpdateToWalletDescriptor(w_desc, error)) { - throw JSONRPCError(RPC_INVALID_PARAMETER, error); + // Need to ExpandPrivate to check if private keys are available for all pubkeys + FlatSigningProvider expand_keys; + std::vector scripts; + if (!parsed_desc->Expand(0, keys, scripts, expand_keys)) { + throw JSONRPCError(RPC_WALLET_ERROR, "Cannot expand descriptor. Probably because of hardened derivations without private keys provided"); } - } + parsed_desc->ExpandPrivate(0, keys, expand_keys); - // Add descriptor to the wallet - auto spk_manager = wallet.AddWalletDescriptor(w_desc, keys, label, internal); - if (spk_manager == nullptr) { - throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Could not add descriptor '%s'", descriptor)); - } + // Check if all private keys are provided + bool have_all_privkeys = !expand_keys.keys.empty(); + for (const auto& entry : expand_keys.origins) { + const CKeyID& key_id = entry.first; + CKey key; + if (!expand_keys.GetKey(key_id, key)) { + have_all_privkeys = false; + break; + } + } - // Set descriptor as active if necessary - if (active) { - if (!w_desc.descriptor->GetOutputType()) { - warnings.push_back("Unknown output type, cannot set descriptor to active."); + // If private keys are enabled, check some things. + if (!wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + if (keys.keys.empty()) { + throw JSONRPCError(RPC_WALLET_ERROR, "Cannot import descriptor without private keys to a wallet with private keys enabled"); + } + if (!have_all_privkeys) { + warnings.push_back("Not all private keys provided. Some wallet functionality may return unexpected errors"); + } + } + + WalletDescriptor w_desc(std::move(parsed_desc), timestamp, range_start, range_end, next_index); + + // Check if the wallet already contains the descriptor + auto existing_spk_manager = wallet.GetDescriptorScriptPubKeyMan(w_desc); + if (existing_spk_manager) { + if (!existing_spk_manager->CanUpdateToWalletDescriptor(w_desc, error)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, error); + } + } + + // Add descriptor to the wallet + auto spk_manager = wallet.AddWalletDescriptor(w_desc, keys, label, desc_internal); + if (spk_manager == nullptr) { + throw JSONRPCError(RPC_WALLET_ERROR, strprintf("Could not add descriptor '%s'", descriptor)); + } + + // Set descriptor as active if necessary + if (active) { + if (!w_desc.descriptor->GetOutputType()) { + warnings.push_back("Unknown output type, cannot set descriptor to active."); + } else { + wallet.AddActiveScriptPubKeyMan(spk_manager->GetID(), *w_desc.descriptor->GetOutputType(), desc_internal); + } } else { - wallet.AddActiveScriptPubKeyMan(spk_manager->GetID(), *w_desc.descriptor->GetOutputType(), internal); - } - } else { - if (w_desc.descriptor->GetOutputType()) { - wallet.DeactivateScriptPubKeyMan(spk_manager->GetID(), *w_desc.descriptor->GetOutputType(), internal); + if (w_desc.descriptor->GetOutputType()) { + wallet.DeactivateScriptPubKeyMan(spk_manager->GetID(), *w_desc.descriptor->GetOutputType(), desc_internal); + } } } @@ -1596,6 +1615,7 @@ RPCHelpMan importdescriptors() { return RPCHelpMan{"importdescriptors", "\nImport descriptors. This will trigger a rescan of the blockchain based on the earliest timestamp of all descriptors being imported. Requires a new wallet backup.\n" + "When importing descriptors with multipath key expressions, if the multipath specifier contains exactly two elements, the descriptor produced from the second elements will be imported as an internal descriptor.\n" "\nNote: This call can take over an hour to complete if using an early timestamp; during that time, other rpc calls\n" "may report that the imported keys, addresses or scripts exist but related transactions are still missing.\n" "The rescan is significantly faster if block filters are available (using startup option \"-blockfilterindex=1\").\n", From 0019f61fc546b4d5f42eb4086f42560863fe0efb Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Wed, 7 Aug 2024 16:29:21 -0400 Subject: [PATCH 13/14] tests: Test importing of multipath descriptors Test that both importmulti and importdescriptors behave as expected when importing a multipath descriptor. --- test/functional/wallet_importdescriptors.py | 52 +++++++++++++++++++++ test/functional/wallet_importmulti.py | 37 +++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/test/functional/wallet_importdescriptors.py b/test/functional/wallet_importdescriptors.py index f9d05a2fe40..bf68980c70c 100755 --- a/test/functional/wallet_importdescriptors.py +++ b/test/functional/wallet_importdescriptors.py @@ -16,6 +16,7 @@ variants. and test the values returned.""" import concurrent.futures +import time from test_framework.authproxy import JSONRPCException from test_framework.blocktools import COINBASE_MATURITY @@ -708,5 +709,56 @@ class ImportDescriptorsTest(BitcoinTestFramework): assert_equal(temp_wallet.getbalance(), encrypted_wallet.getbalance()) + self.log.info("Multipath descriptors") + self.nodes[1].createwallet(wallet_name="multipath", descriptors=True, blank=True) + w_multipath = self.nodes[1].get_wallet_rpc("multipath") + self.nodes[1].createwallet(wallet_name="multipath_split", descriptors=True, blank=True) + w_multisplit = self.nodes[1].get_wallet_rpc("multipath_split") + timestamp = int(time.time()) + + self.test_importdesc({"desc": descsum_create(f"wpkh({xpriv}/<10;20>/0/*)"), + "active": True, + "range": 10, + "timestamp": "now", + "label": "some label"}, + success=False, + error_code=-8, + error_message="Multipath descriptors should not have a label", + wallet=w_multipath) + self.test_importdesc({"desc": descsum_create(f"wpkh({xpriv}/<10;20>/0/*)"), + "active": True, + "range": 10, + "timestamp": timestamp, + "internal": True}, + success=False, + error_code=-5, + error_message="Cannot have multipath descriptor while also specifying \'internal\'", + wallet=w_multipath) + + self.test_importdesc({"desc": descsum_create(f"wpkh({xpriv}/<10;20>/0/*)"), + "active": True, + "range": 10, + "timestamp": timestamp}, + success=True, + wallet=w_multipath) + + self.test_importdesc({"desc": descsum_create(f"wpkh({xpriv}/10/0/*)"), + "active": True, + "range": 10, + "timestamp": timestamp}, + success=True, + wallet=w_multisplit) + self.test_importdesc({"desc": descsum_create(f"wpkh({xpriv}/20/0/*)"), + "active": True, + "range": 10, + "internal": True, + "timestamp": timestamp}, + success=True, + wallet=w_multisplit) + for _ in range(0, 10): + assert_equal(w_multipath.getnewaddress(address_type="bech32"), w_multisplit.getnewaddress(address_type="bech32")) + assert_equal(w_multipath.getrawchangeaddress(address_type="bech32"), w_multisplit.getrawchangeaddress(address_type="bech32")) + assert_equal(sorted(w_multipath.listdescriptors()["descriptors"], key=lambda x: x["desc"]), sorted(w_multisplit.listdescriptors()["descriptors"], key=lambda x: x["desc"])) + if __name__ == '__main__': ImportDescriptorsTest().main() diff --git a/test/functional/wallet_importmulti.py b/test/functional/wallet_importmulti.py index 31013f6323f..0c24c01b0d8 100755 --- a/test/functional/wallet_importmulti.py +++ b/test/functional/wallet_importmulti.py @@ -896,6 +896,43 @@ class ImportMultiTest(BitcoinTestFramework): ) assert result[0]['success'] + self.log.info("Multipath descriptors") + self.nodes[1].createwallet(wallet_name="multipath", blank=True, disable_private_keys=True) + w_multipath = self.nodes[1].get_wallet_rpc("multipath") + self.nodes[1].createwallet(wallet_name="multipath_split", blank=True, disable_private_keys=True) + w_multisplit = self.nodes[1].get_wallet_rpc("multipath_split") + + res = w_multipath.importmulti([{"desc": descsum_create(f"wpkh({xpub}/<10;20>/0/*)"), + "keypool": True, + "range": 10, + "timestamp": "now", + "internal": True}]) + assert_equal(res[0]["success"], False) + assert_equal(res[0]["error"]["code"], -5) + assert_equal(res[0]["error"]["message"], "Cannot have multipath descriptor while also specifying 'internal'") + + res = w_multipath.importmulti([{"desc": descsum_create(f"wpkh({xpub}/<10;20>/0/*)"), + "keypool": True, + "range": 10, + "timestamp": "now"}]) + assert_equal(res[0]["success"], True) + + res = w_multisplit.importmulti([{"desc": descsum_create(f"wpkh({xpub}/10/0/*)"), + "keypool": True, + "range": 10, + "timestamp": "now"}]) + assert_equal(res[0]["success"], True) + res = w_multisplit.importmulti([{"desc": descsum_create(f"wpkh({xpub}/20/0/*)"), + "keypool": True, + "range": 10, + "internal": True, + "timestamp": timestamp}]) + assert_equal(res[0]["success"], True) + + for _ in range(0, 9): + assert_equal(w_multipath.getnewaddress(address_type="bech32"), w_multisplit.getnewaddress(address_type="bech32")) + assert_equal(w_multipath.getrawchangeaddress(address_type="bech32"), w_multisplit.getrawchangeaddress(address_type="bech32")) + if __name__ == '__main__': ImportMultiTest().main() From a0abcbd3822bd17a1d73c42ccd5b040a150b0501 Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Wed, 7 Aug 2024 16:29:25 -0400 Subject: [PATCH 14/14] doc: Mention multipath specifier --- doc/descriptors.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/doc/descriptors.md b/doc/descriptors.md index 3b94ec03e4a..14380b9c46c 100644 --- a/doc/descriptors.md +++ b/doc/descriptors.md @@ -97,6 +97,7 @@ Descriptors consist of several types of expressions. The top level expression is - [WIF](https://en.bitcoin.it/wiki/Wallet_import_format) encoded private keys may be specified instead of the corresponding public key, with the same meaning. - `xpub` encoded extended public key or `xprv` encoded extended private key (as defined in [BIP 32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)). - Followed by zero or more `/NUM` unhardened and `/NUM'` hardened BIP32 derivation steps. + - No more than one of these derivation steps may be of the form `` (including hardened indicators with either or both `NUM`). If such specifiers are included, the descriptor will be parsed as multiple descriptors where the first descriptor uses all of the first `NUM` in the pair, and the second descriptor uses the second `NUM` in the pair for all `KEY` expressions, and so on. - Optionally followed by a single `/*` or `/*'` final step to denote all (direct) unhardened or hardened children. - The usage of hardened derivation steps requires providing the private key. @@ -256,6 +257,30 @@ Note how the first key is an xprv private key with a specific derivation path, while the other two are public keys. +### Specifying receiving and change descriptors in one descriptor + +Since receiving and change addresses are frequently derived from the same +extended key(s) but with a single derivation index changed, it is convenient +to be able to specify a descriptor that can derive at the two different +indexes. Thus a single tuple of indexes is allowed in each derivation path +following the extended key. When this descriptor is parsed, multiple descriptors +will be produced, the first one will use the first index in the tuple for all +key expressions, the second will use the second index, the third will use the +third index, and so on.. + +For example, a descriptor of the form: + + multi(2,xpub.../<0;1;2>/0/*,xpub.../<2;3;4>/*) + +will expand to the two descriptors + + multi(2,xpub.../0/0/*,xpub.../2/*) + multi(2,xpub.../1/0/*,xpub.../3/*) + multi(2,xpub.../2/0/*,xpub.../4*) + +When this tuple contains only two elements, wallet implementations can use the +first descriptor for receiving addresses and the second descriptor for change addresses. + ### Compatibility with old wallets In order to easily represent the sets of scripts currently supported by