Skip to content

Commit 66705f3

Browse files
committed
Merge remote-tracking branch 'Eunovo/list-descriptors-with-partial-keys' into 2025/06/musig2-power
2 parents 57d7e7c + d82dcf2 commit 66705f3

10 files changed

Lines changed: 147 additions & 67 deletions

File tree

src/script/descriptor.cpp

Lines changed: 68 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,9 @@ struct PubkeyProvider
204204
/** Get the descriptor string form. */
205205
virtual std::string ToString(StringType type=StringType::PUBLIC) const = 0;
206206

207-
/** Get the descriptor string form including private data (if available in arg). */
207+
/** Get the descriptor string form including private data (if available in arg).
208+
* If no private data is available, the string will be the same as ToString(StringType::PUBLIC).
209+
*/
208210
virtual bool ToPrivateString(const SigningProvider& arg, std::string& out) const = 0;
209211

210212
/** Get the descriptor string form with the xpub at the last hardened derivation,
@@ -260,9 +262,9 @@ class OriginPubkeyProvider final : public PubkeyProvider
260262
bool ToPrivateString(const SigningProvider& arg, std::string& ret) const override
261263
{
262264
std::string sub;
263-
if (!m_provider->ToPrivateString(arg, sub)) return false;
265+
bool has_priv_key{m_provider->ToPrivateString(arg, sub)};
264266
ret = "[" + OriginString(StringType::PUBLIC) + "]" + std::move(sub);
265-
return true;
267+
return has_priv_key;
266268
}
267269
bool ToNormalizedString(const SigningProvider& arg, std::string& ret, const DescriptorCache* cache) const override
268270
{
@@ -329,7 +331,10 @@ class ConstPubkeyProvider final : public PubkeyProvider
329331
bool ToPrivateString(const SigningProvider& arg, std::string& ret) const override
330332
{
331333
std::optional<CKey> key = GetPrivKey(arg);
332-
if (!key) return false;
334+
if (!key) {
335+
ret = ToString(StringType::PUBLIC);
336+
return false;
337+
}
333338
ret = EncodeSecret(*key);
334339
return true;
335340
}
@@ -492,7 +497,10 @@ class BIP32PubkeyProvider final : public PubkeyProvider
492497
bool ToPrivateString(const SigningProvider& arg, std::string& out) const override
493498
{
494499
CExtKey key;
495-
if (!GetExtKey(arg, key)) return false;
500+
if (!GetExtKey(arg, key)) {
501+
out = ToString(StringType::PUBLIC);
502+
return false;
503+
}
496504
out = EncodeExtKey(key) + FormatHDKeypath(m_path, /*apostrophe=*/m_apostrophe);
497505
if (IsRange()) {
498506
out += "/*";
@@ -841,6 +849,31 @@ class DescriptorImpl : public Descriptor
841849
return true;
842850
}
843851

852+
bool IsWatchOnly(const SigningProvider& arg) const override
853+
{
854+
size_t privkey_count{0};
855+
std::set<CPubKey> pubkeys;
856+
std::set<CExtPubKey> ext_pubkeys;
857+
this->GetPubKeys(pubkeys, ext_pubkeys);
858+
auto output_type{this->GetOutputType()};
859+
for (auto pubkey : pubkeys) {
860+
CKey key;
861+
if (arg.GetKey(pubkey.GetID(), key)) {
862+
privkey_count += 1;
863+
} else if (output_type.has_value() && *output_type == OutputType::BECH32M && arg.GetKeyByXOnly(XOnlyPubKey(pubkey), key)) {
864+
privkey_count += 1;
865+
}
866+
}
867+
for (auto extpubkey : ext_pubkeys) {
868+
CKey dummy;
869+
if (arg.GetKey(extpubkey.pubkey.GetID(), dummy)) {
870+
privkey_count += 1;
871+
}
872+
}
873+
size_t pubkey_count{pubkeys.size() + ext_pubkeys.size()};
874+
return (privkey_count == 0 || pubkey_count > privkey_count);
875+
}
876+
844877
// NOLINTNEXTLINE(misc-no-recursion)
845878
bool IsRange() const final
846879
{
@@ -854,24 +887,27 @@ class DescriptorImpl : public Descriptor
854887
}
855888

856889
// NOLINTNEXTLINE(misc-no-recursion)
857-
virtual bool ToStringSubScriptHelper(const SigningProvider* arg, std::string& ret, const StringType type, const DescriptorCache* cache = nullptr) const
890+
virtual bool ToStringSubScriptHelper(const SigningProvider* arg, std::string& ret, const StringType type, bool* has_priv_key, const DescriptorCache* cache = nullptr) const
858891
{
859892
size_t pos = 0;
860893
for (const auto& scriptarg : m_subdescriptor_args) {
861894
if (pos++) ret += ",";
862895
std::string tmp;
863-
if (!scriptarg->ToStringHelper(arg, tmp, type, cache)) return false;
896+
bool subscript_has_priv_key{false};
897+
if (!scriptarg->ToStringHelper(arg, tmp, type, &subscript_has_priv_key, cache)) return false;
898+
*has_priv_key = *has_priv_key || subscript_has_priv_key;
864899
ret += tmp;
865900
}
866901
return true;
867902
}
868903

869904
// NOLINTNEXTLINE(misc-no-recursion)
870-
virtual bool ToStringHelper(const SigningProvider* arg, std::string& out, const StringType type, const DescriptorCache* cache = nullptr) const
905+
virtual bool ToStringHelper(const SigningProvider* arg, std::string& out, const StringType type, bool* has_priv_key = nullptr, const DescriptorCache* cache = nullptr) const
871906
{
872907
std::string extra = ToStringExtra();
873908
size_t pos = extra.size() > 0 ? 1 : 0;
874909
std::string ret = m_name + "(" + extra;
910+
875911
for (const auto& pubkey : m_pubkey_args) {
876912
if (pos++) ret += ",";
877913
std::string tmp;
@@ -880,7 +916,8 @@ class DescriptorImpl : public Descriptor
880916
if (!pubkey->ToNormalizedString(*arg, tmp, cache)) return false;
881917
break;
882918
case StringType::PRIVATE:
883-
if (!pubkey->ToPrivateString(*arg, tmp)) return false;
919+
assert(has_priv_key != nullptr);
920+
*has_priv_key = pubkey->ToPrivateString(*arg, tmp) || *has_priv_key;
884921
break;
885922
case StringType::PUBLIC:
886923
tmp = pubkey->ToString();
@@ -892,7 +929,9 @@ class DescriptorImpl : public Descriptor
892929
ret += tmp;
893930
}
894931
std::string subscript;
895-
if (!ToStringSubScriptHelper(arg, subscript, type, cache)) return false;
932+
bool subscript_has_priv_key{false};
933+
if (!ToStringSubScriptHelper(arg, subscript, type, &subscript_has_priv_key, cache)) return false;
934+
if (has_priv_key != nullptr) *has_priv_key = *has_priv_key || subscript_has_priv_key;
896935
if (pos && subscript.size()) ret += ',';
897936
out = std::move(ret) + std::move(subscript) + ")";
898937
return true;
@@ -907,14 +946,17 @@ class DescriptorImpl : public Descriptor
907946

908947
bool ToPrivateString(const SigningProvider& arg, std::string& out) const override
909948
{
910-
bool ret = ToStringHelper(&arg, out, StringType::PRIVATE);
949+
bool has_priv_key{false};
950+
// ToStringHelper should never fail for StringType::PRIVATE,
951+
// because it falls back to StringType::PUBLIC when no private key is available.
952+
assert(ToStringHelper(&arg, out, StringType::PRIVATE, &has_priv_key));
911953
out = AddChecksum(out);
912-
return ret;
954+
return has_priv_key;
913955
}
914956

915957
bool ToNormalizedString(const SigningProvider& arg, std::string& out, const DescriptorCache* cache) const override final
916958
{
917-
bool ret = ToStringHelper(&arg, out, StringType::NORMALIZED, cache);
959+
bool ret = ToStringHelper(&arg, out, StringType::NORMALIZED, nullptr, cache);
918960
out = AddChecksum(out);
919961
return ret;
920962
}
@@ -1390,18 +1432,21 @@ class TRDescriptor final : public DescriptorImpl
13901432
out.tr_trees[output] = builder;
13911433
return Vector(GetScriptForDestination(output));
13921434
}
1393-
bool ToStringSubScriptHelper(const SigningProvider* arg, std::string& ret, const StringType type, const DescriptorCache* cache = nullptr) const override
1435+
bool ToStringSubScriptHelper(const SigningProvider* arg, std::string& ret, const StringType type, bool* has_priv_key, const DescriptorCache* cache = nullptr) const override
13941436
{
13951437
if (m_depths.empty()) return true;
13961438
std::vector<bool> path;
1439+
13971440
for (size_t pos = 0; pos < m_depths.size(); ++pos) {
13981441
if (pos) ret += ',';
13991442
while ((int)path.size() <= m_depths[pos]) {
14001443
if (path.size()) ret += '{';
14011444
path.push_back(false);
14021445
}
14031446
std::string tmp;
1404-
if (!m_subdescriptor_args[pos]->ToStringHelper(arg, tmp, type, cache)) return false;
1447+
bool subscript_has_priv_key{false};
1448+
if (!m_subdescriptor_args[pos]->ToStringHelper(arg, tmp, type, &subscript_has_priv_key, cache)) return false;
1449+
if (has_priv_key != nullptr) *has_priv_key = *has_priv_key || subscript_has_priv_key;
14051450
ret += tmp;
14061451
while (!path.empty() && path.back()) {
14071452
if (path.size() > 1) ret += '}';
@@ -1498,11 +1543,12 @@ class StringMaker {
14981543
StringMaker(const SigningProvider* arg LIFETIMEBOUND, const std::vector<std::unique_ptr<PubkeyProvider>>& pubkeys LIFETIMEBOUND, bool priv)
14991544
: m_arg(arg), m_pubkeys(pubkeys), m_private(priv) {}
15001545

1501-
std::optional<std::string> ToString(uint32_t key) const
1546+
std::string ToString(uint32_t key, bool* has_priv_key = nullptr) const
15021547
{
15031548
std::string ret;
15041549
if (m_private) {
1505-
if (!m_pubkeys[key]->ToPrivateString(*m_arg, ret)) return {};
1550+
assert(has_priv_key != nullptr);
1551+
*has_priv_key = m_pubkeys[key]->ToPrivateString(*m_arg, ret);
15061552
} else {
15071553
ret = m_pubkeys[key]->ToString();
15081554
}
@@ -1535,13 +1581,10 @@ class MiniscriptDescriptor final : public DescriptorImpl
15351581
: DescriptorImpl(std::move(providers), "?"), m_node(std::move(node)) {}
15361582

15371583
bool ToStringHelper(const SigningProvider* arg, std::string& out, const StringType type,
1538-
const DescriptorCache* cache = nullptr) const override
1584+
bool* has_priv_key, const DescriptorCache* cache = nullptr) const override
15391585
{
1540-
if (const auto res = m_node->ToString(StringMaker(arg, m_pubkey_args, type == StringType::PRIVATE))) {
1541-
out = *res;
1542-
return true;
1543-
}
1544-
return false;
1586+
out = m_node->ToString(StringMaker(arg, m_pubkey_args, type == StringType::PRIVATE), has_priv_key);
1587+
return true;
15451588
}
15461589

15471590
bool IsSolvable() const override { return true; }
@@ -2096,7 +2139,7 @@ struct KeyParser {
20962139
return key;
20972140
}
20982141

2099-
std::optional<std::string> ToString(const Key& key) const
2142+
std::string ToString(const Key& key, bool* has_priv_key = nullptr) const
21002143
{
21012144
return m_keys.at(key).at(0)->ToString();
21022145
}
@@ -2514,7 +2557,7 @@ std::vector<std::unique_ptr<DescriptorImpl>> ParseScript(uint32_t& key_exp_index
25142557
// Try to find the first insane sub for better error reporting.
25152558
auto insane_node = node.get();
25162559
if (const auto sub = node->FindInsaneSub()) insane_node = sub;
2517-
if (const auto str = insane_node->ToString(parser)) error = *str;
2560+
error = insane_node->ToString(parser);
25182561
if (!insane_node->IsValid()) {
25192562
error += " is invalid";
25202563
} else if (!node->IsSane()) {

src/script/descriptor.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,16 @@ struct Descriptor {
111111
/** Whether this descriptor will return at most one scriptPubKey or multiple (aka is or is not combo) */
112112
virtual bool IsSingleType() const = 0;
113113

114-
/** Convert the descriptor to a private string. This fails if the provided provider does not have the relevant private keys. */
114+
/** Whether this descriptor contains any private key material */
115+
virtual bool IsWatchOnly(const SigningProvider& provider) const = 0;
116+
117+
/** Convert the descriptor to a private string. This uses public keys if the relevant private keys are not in the SigningProvider.
118+
* If none of the relevant private keys are available, the output string in the "out" parameter will not contain any private key information,
119+
* and this function will return "false".
120+
* @param[in] provider The SigningProvider to query for private keys.
121+
* @param[out] out The resulting descriptor string, containing private keys if available.
122+
* @returns true if at least one private key available.
123+
*/
115124
virtual bool ToPrivateString(const SigningProvider& provider, std::string& out) const = 0;
116125

117126
/** Convert the descriptor to a normalized string. Normalized descriptors have the xpub at the last hardened step. This fails if the provided provider does not have the private keys to derive that xpub. */

src/script/miniscript.h

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -825,7 +825,7 @@ struct Node {
825825
}
826826

827827
template<typename CTx>
828-
std::optional<std::string> ToString(const CTx& ctx) const {
828+
std::string ToString(const CTx& ctx, bool* has_priv_key = nullptr) const {
829829
// To construct the std::string representation for a Miniscript object, we use
830830
// the TreeEvalMaybe algorithm. The State is a boolean: whether the parent node is a
831831
// wrapper. If so, non-wrapper expressions must be prefixed with a ":".
@@ -838,10 +838,18 @@ struct Node {
838838
(node.fragment == Fragment::OR_I && node.subs[0]->fragment == Fragment::JUST_0) ||
839839
(node.fragment == Fragment::OR_I && node.subs[1]->fragment == Fragment::JUST_0));
840840
};
841+
auto toString = [&ctx, &has_priv_key](Key key) -> std::string {
842+
bool fragment_has_priv_key{false};
843+
auto key_str{ctx.ToString(key, &fragment_has_priv_key)};
844+
if (has_priv_key != nullptr) {
845+
*has_priv_key = *has_priv_key || fragment_has_priv_key;
846+
}
847+
return key_str;
848+
};
841849
// The upward function computes for a node, given whether its parent is a wrapper,
842850
// and the string representations of its child nodes, the string representation of the node.
843851
const bool is_tapscript{IsTapscript(m_script_ctx)};
844-
auto upfn = [&ctx, is_tapscript](bool wrapped, const Node& node, std::span<std::string> subs) -> std::optional<std::string> {
852+
auto upfn = [is_tapscript, &toString](bool wrapped, const Node& node, std::span<std::string> subs) -> std::string {
845853
std::string ret = wrapped ? ":" : "";
846854

847855
switch (node.fragment) {
@@ -850,15 +858,13 @@ struct Node {
850858
case Fragment::WRAP_C:
851859
if (node.subs[0]->fragment == Fragment::PK_K) {
852860
// pk(K) is syntactic sugar for c:pk_k(K)
853-
auto key_str = ctx.ToString(node.subs[0]->keys[0]);
854-
if (!key_str) return {};
855-
return std::move(ret) + "pk(" + std::move(*key_str) + ")";
861+
auto key_str = toString(node.subs[0]->keys[0]);
862+
return std::move(ret) + "pk(" + std::move(key_str) + ")";
856863
}
857864
if (node.subs[0]->fragment == Fragment::PK_H) {
858865
// pkh(K) is syntactic sugar for c:pk_h(K)
859-
auto key_str = ctx.ToString(node.subs[0]->keys[0]);
860-
if (!key_str) return {};
861-
return std::move(ret) + "pkh(" + std::move(*key_str) + ")";
866+
auto key_str = toString(node.subs[0]->keys[0]);
867+
return std::move(ret) + "pkh(" + std::move(key_str) + ")";
862868
}
863869
return "c" + std::move(subs[0]);
864870
case Fragment::WRAP_D: return "d" + std::move(subs[0]);
@@ -877,14 +883,12 @@ struct Node {
877883
}
878884
switch (node.fragment) {
879885
case Fragment::PK_K: {
880-
auto key_str = ctx.ToString(node.keys[0]);
881-
if (!key_str) return {};
882-
return std::move(ret) + "pk_k(" + std::move(*key_str) + ")";
886+
auto key_str = toString(node.keys[0]);
887+
return std::move(ret) + "pk_k(" + std::move(key_str) + ")";
883888
}
884889
case Fragment::PK_H: {
885-
auto key_str = ctx.ToString(node.keys[0]);
886-
if (!key_str) return {};
887-
return std::move(ret) + "pk_h(" + std::move(*key_str) + ")";
890+
auto key_str = toString(node.keys[0]);
891+
return std::move(ret) + "pk_h(" + std::move(key_str) + ")";
888892
}
889893
case Fragment::AFTER: return std::move(ret) + "after(" + util::ToString(node.k) + ")";
890894
case Fragment::OLDER: return std::move(ret) + "older(" + util::ToString(node.k) + ")";
@@ -908,19 +912,17 @@ struct Node {
908912
CHECK_NONFATAL(!is_tapscript);
909913
auto str = std::move(ret) + "multi(" + util::ToString(node.k);
910914
for (const auto& key : node.keys) {
911-
auto key_str = ctx.ToString(key);
912-
if (!key_str) return {};
913-
str += "," + std::move(*key_str);
915+
auto key_str = toString(key);
916+
str += "," + std::move(key_str);
914917
}
915918
return std::move(str) + ")";
916919
}
917920
case Fragment::MULTI_A: {
918921
CHECK_NONFATAL(is_tapscript);
919922
auto str = std::move(ret) + "multi_a(" + util::ToString(node.k);
920923
for (const auto& key : node.keys) {
921-
auto key_str = ctx.ToString(key);
922-
if (!key_str) return {};
923-
str += "," + std::move(*key_str);
924+
auto key_str = toString(key);
925+
str += "," + std::move(key_str);
924926
}
925927
return std::move(str) + ")";
926928
}
@@ -936,7 +938,7 @@ struct Node {
936938
assert(false);
937939
};
938940

939-
return TreeEvalMaybe<std::string>(false, downfn, upfn);
941+
return TreeEval<std::string>(false, downfn, upfn);
940942
}
941943

942944
private:

src/test/descriptor_tests.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ constexpr int SIGNABLE = 1 << 3; // We can sign with this descriptor (this is no
4949
constexpr int DERIVE_HARDENED = 1 << 4; // The final derivation is hardened, i.e. ends with *' or *h
5050
constexpr int MIXED_PUBKEYS = 1 << 5;
5151
constexpr int XONLY_KEYS = 1 << 6; // X-only pubkeys are in use (and thus inferring/caching may swap parity of pubkeys/keyids)
52-
constexpr int MISSING_PRIVKEYS = 1 << 7; // Not all private keys are available, so ToPrivateString will fail.
52+
constexpr int MISSING_PRIVKEYS = 1 << 7; // Not all private keys are available.
5353
constexpr int SIGNABLE_FAILS = 1 << 8; // We can sign with this descriptor, but actually trying to sign will fail
5454
constexpr int MUSIG = 1 << 9; // This is a MuSig so key counts will have an extra key
5555
constexpr int MUSIG_DERIVATION = 1 << 10; // MuSig with BIP 328 derivation from the aggregate key

src/test/fuzz/miniscript.cpp

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,13 @@ struct ParserContext {
124124
return a < b;
125125
}
126126

127-
std::optional<std::string> ToString(const Key& key) const
127+
std::string ToString(const Key& key, bool* has_priv_key) const
128128
{
129129
auto it = TEST_DATA.dummy_key_idx_map.find(key);
130-
if (it == TEST_DATA.dummy_key_idx_map.end()) return {};
130+
if (it == TEST_DATA.dummy_key_idx_map.end()) {
131+
return HexStr(key);
132+
}
133+
*has_priv_key = true;
131134
uint8_t idx = it->second;
132135
return HexStr(std::span{&idx, 1});
133136
}
@@ -1033,9 +1036,9 @@ void TestNode(const MsCtx script_ctx, const NodeRef& node, FuzzedDataProvider& p
10331036

10341037
// Check that it roundtrips to text representation
10351038
const ParserContext parser_ctx{script_ctx};
1036-
std::optional<std::string> str{node->ToString(parser_ctx)};
1037-
assert(str);
1038-
auto parsed = miniscript::FromString(*str, parser_ctx);
1039+
bool tmp{false};
1040+
std::string str{node->ToString(parser_ctx, &tmp)};
1041+
auto parsed = miniscript::FromString(str, parser_ctx);
10391042
assert(parsed);
10401043
assert(*parsed == *node);
10411044

@@ -1241,9 +1244,9 @@ FUZZ_TARGET(miniscript_string, .init = FuzzInit)
12411244
auto parsed = miniscript::FromString(str, parser_ctx);
12421245
if (!parsed) return;
12431246

1244-
const auto str2 = parsed->ToString(parser_ctx);
1245-
assert(str2);
1246-
auto parsed2 = miniscript::FromString(*str2, parser_ctx);
1247+
bool tmp{false};
1248+
const auto str2 = parsed->ToString(parser_ctx, &tmp);
1249+
auto parsed2 = miniscript::FromString(str2, parser_ctx);
12471250
assert(parsed2);
12481251
assert(*parsed == *parsed2);
12491252
}

0 commit comments

Comments
 (0)