Skip to content

Commit a42a034

Browse files
committed
Merge remote-tracking branch 'Eunovo/list-descriptors-with-partial-keys' into 2025/06/musig2-power
2 parents 1edcfe7 + 3297f24 commit a42a034

10 files changed

Lines changed: 171 additions & 51 deletions

File tree

src/script/descriptor.cpp

Lines changed: 81 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,11 @@ 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 the private data is not available, the output string in the "out" parameter
209+
* will not contain any private key information,
210+
* and this function will return "false".
211+
*/
208212
virtual bool ToPrivateString(const SigningProvider& arg, std::string& out) const = 0;
209213

210214
/** Get the descriptor string form with the xpub at the last hardened derivation,
@@ -260,9 +264,9 @@ class OriginPubkeyProvider final : public PubkeyProvider
260264
bool ToPrivateString(const SigningProvider& arg, std::string& ret) const override
261265
{
262266
std::string sub;
263-
if (!m_provider->ToPrivateString(arg, sub)) return false;
267+
bool has_priv_key{m_provider->ToPrivateString(arg, sub)};
264268
ret = "[" + OriginString(StringType::PUBLIC) + "]" + std::move(sub);
265-
return true;
269+
return has_priv_key;
266270
}
267271
bool ToNormalizedString(const SigningProvider& arg, std::string& ret, const DescriptorCache* cache) const override
268272
{
@@ -329,7 +333,10 @@ class ConstPubkeyProvider final : public PubkeyProvider
329333
bool ToPrivateString(const SigningProvider& arg, std::string& ret) const override
330334
{
331335
std::optional<CKey> key = GetPrivKey(arg);
332-
if (!key) return false;
336+
if (!key) {
337+
ret = ToString(StringType::PUBLIC);
338+
return false;
339+
}
333340
ret = EncodeSecret(*key);
334341
return true;
335342
}
@@ -492,7 +499,10 @@ class BIP32PubkeyProvider final : public PubkeyProvider
492499
bool ToPrivateString(const SigningProvider& arg, std::string& out) const override
493500
{
494501
CExtKey key;
495-
if (!GetExtKey(arg, key)) return false;
502+
if (!GetExtKey(arg, key)) {
503+
out = ToString(StringType::PUBLIC);
504+
return false;
505+
}
496506
out = EncodeExtKey(key) + FormatHDKeypath(m_path, /*apostrophe=*/m_apostrophe);
497507
if (IsRange()) {
498508
out += "/*";
@@ -710,17 +720,14 @@ class MuSigPubkeyProvider final : public PubkeyProvider
710720
std::string tmp;
711721
if (pubkey->ToPrivateString(arg, tmp)) {
712722
any_privkeys = true;
713-
out += tmp;
714-
} else {
715-
out += pubkey->ToString();
716723
}
724+
out += tmp;
717725
}
718726
out += ")";
719727
out += FormatHDKeypath(m_path);
720728
if (IsRangedDerivation()) {
721729
out += "/*";
722730
}
723-
if (!any_privkeys) out.clear();
724731
return any_privkeys;
725732
}
726733
bool ToNormalizedString(const SigningProvider& arg, std::string& out, const DescriptorCache* cache = nullptr) const override
@@ -837,6 +844,25 @@ class DescriptorImpl : public Descriptor
837844
return true;
838845
}
839846

847+
// NOLINTNEXTLINE(misc-no-recursion)
848+
bool HavePrivateKeys(const SigningProvider& arg) const override
849+
{
850+
if (m_pubkey_args.empty() && m_subdescriptor_args.empty()) return false;
851+
852+
for (const auto& sub: m_subdescriptor_args) {
853+
if (!sub->HavePrivateKeys(arg)) return false;
854+
}
855+
856+
FlatSigningProvider tmp_provider;
857+
for (const auto& pubkey : m_pubkey_args) {
858+
tmp_provider.keys.clear();
859+
pubkey->GetPrivKey(0, arg, tmp_provider);
860+
if (tmp_provider.keys.empty()) return false;
861+
}
862+
863+
return true;
864+
}
865+
840866
// NOLINTNEXTLINE(misc-no-recursion)
841867
bool IsRange() const final
842868
{
@@ -853,13 +879,19 @@ class DescriptorImpl : public Descriptor
853879
virtual bool ToStringSubScriptHelper(const SigningProvider* arg, std::string& ret, const StringType type, const DescriptorCache* cache = nullptr) const
854880
{
855881
size_t pos = 0;
882+
bool is_private{type == StringType::PRIVATE};
883+
// For private string output, track if at least one key has a private key available.
884+
// Initialize to true for non-private types.
885+
bool any_success{!is_private};
856886
for (const auto& scriptarg : m_subdescriptor_args) {
857887
if (pos++) ret += ",";
858888
std::string tmp;
859-
if (!scriptarg->ToStringHelper(arg, tmp, type, cache)) return false;
889+
bool subscript_res{scriptarg->ToStringHelper(arg, tmp, type, cache)};
890+
if (!is_private && !subscript_res) return false;
891+
any_success = any_success || subscript_res;
860892
ret += tmp;
861893
}
862-
return true;
894+
return any_success;
863895
}
864896

865897
// NOLINTNEXTLINE(misc-no-recursion)
@@ -868,6 +900,11 @@ class DescriptorImpl : public Descriptor
868900
std::string extra = ToStringExtra();
869901
size_t pos = extra.size() > 0 ? 1 : 0;
870902
std::string ret = m_name + "(" + extra;
903+
bool is_private{type == StringType::PRIVATE};
904+
// For private string output, track if at least one key has a private key available.
905+
// Initialize to true for non-private types.
906+
bool any_success{!is_private};
907+
871908
for (const auto& pubkey : m_pubkey_args) {
872909
if (pos++) ret += ",";
873910
std::string tmp;
@@ -876,7 +913,7 @@ class DescriptorImpl : public Descriptor
876913
if (!pubkey->ToNormalizedString(*arg, tmp, cache)) return false;
877914
break;
878915
case StringType::PRIVATE:
879-
if (!pubkey->ToPrivateString(*arg, tmp)) return false;
916+
any_success = pubkey->ToPrivateString(*arg, tmp) || any_success;
880917
break;
881918
case StringType::PUBLIC:
882919
tmp = pubkey->ToString();
@@ -888,10 +925,12 @@ class DescriptorImpl : public Descriptor
888925
ret += tmp;
889926
}
890927
std::string subscript;
891-
if (!ToStringSubScriptHelper(arg, subscript, type, cache)) return false;
928+
bool subscript_res{ToStringSubScriptHelper(arg, subscript, type, cache)};
929+
if (!is_private && !subscript_res) return false;
930+
any_success = any_success || subscript_res;
892931
if (pos && subscript.size()) ret += ',';
893932
out = std::move(ret) + std::move(subscript) + ")";
894-
return true;
933+
return any_success;
895934
}
896935

897936
std::string ToString(bool compat_format) const final
@@ -903,9 +942,9 @@ class DescriptorImpl : public Descriptor
903942

904943
bool ToPrivateString(const SigningProvider& arg, std::string& out) const override
905944
{
906-
bool ret = ToStringHelper(&arg, out, StringType::PRIVATE);
945+
bool has_priv_key{ToStringHelper(&arg, out, StringType::PRIVATE)};
907946
out = AddChecksum(out);
908-
return ret;
947+
return has_priv_key;
909948
}
910949

911950
bool ToNormalizedString(const SigningProvider& arg, std::string& out, const DescriptorCache* cache) const override final
@@ -1398,24 +1437,38 @@ class TRDescriptor final : public DescriptorImpl
13981437
}
13991438
bool ToStringSubScriptHelper(const SigningProvider* arg, std::string& ret, const StringType type, const DescriptorCache* cache = nullptr) const override
14001439
{
1401-
if (m_depths.empty()) return true;
1440+
if (m_depths.empty()) {
1441+
// If there are no sub-descriptors and a PRIVATE string
1442+
// is requested, return `false` to indicate that the presence
1443+
// of a private key depends solely on the internal key (which is checked
1444+
// in the caller), not on any sub-descriptor. This ensures correct behavior for
1445+
// descriptors like tr(internal_key) when checking for private keys.
1446+
return type != StringType::PRIVATE;
1447+
}
14021448
std::vector<bool> path;
1449+
bool is_private{type == StringType::PRIVATE};
1450+
// For private string output, track if at least one key has a private key available.
1451+
// Initialize to true for non-private types.
1452+
bool any_success{!is_private};
1453+
14031454
for (size_t pos = 0; pos < m_depths.size(); ++pos) {
14041455
if (pos) ret += ',';
14051456
while ((int)path.size() <= m_depths[pos]) {
14061457
if (path.size()) ret += '{';
14071458
path.push_back(false);
14081459
}
14091460
std::string tmp;
1410-
if (!m_subdescriptor_args[pos]->ToStringHelper(arg, tmp, type, cache)) return false;
1461+
bool subscript_res{m_subdescriptor_args[pos]->ToStringHelper(arg, tmp, type, cache)};
1462+
if (!is_private && !subscript_res) return false;
1463+
any_success = any_success || subscript_res;
14111464
ret += tmp;
14121465
while (!path.empty() && path.back()) {
14131466
if (path.size() > 1) ret += '}';
14141467
path.pop_back();
14151468
}
14161469
if (!path.empty()) path.back() = true;
14171470
}
1418-
return true;
1471+
return any_success;
14191472
}
14201473
public:
14211474
TRDescriptor(std::unique_ptr<PubkeyProvider> internal_key, std::vector<std::unique_ptr<DescriptorImpl>> descs, std::vector<int> depths) :
@@ -1508,15 +1561,16 @@ class StringMaker {
15081561
const DescriptorCache* cache LIFETIMEBOUND)
15091562
: m_arg(arg), m_pubkeys(pubkeys), m_type(type), m_cache(cache) {}
15101563

1511-
std::optional<std::string> ToString(uint32_t key) const
1564+
std::optional<std::string> ToString(uint32_t key, bool& has_priv_key) const
15121565
{
15131566
std::string ret;
1567+
has_priv_key = false;
15141568
switch (m_type) {
15151569
case DescriptorImpl::StringType::PUBLIC:
15161570
ret = m_pubkeys[key]->ToString();
15171571
break;
15181572
case DescriptorImpl::StringType::PRIVATE:
1519-
if (!m_pubkeys[key]->ToPrivateString(*m_arg, ret)) return {};
1573+
has_priv_key = m_pubkeys[key]->ToPrivateString(*m_arg, ret);
15201574
break;
15211575
case DescriptorImpl::StringType::NORMALIZED:
15221576
if (!m_pubkeys[key]->ToNormalizedString(*m_arg, ret, m_cache)) return {};
@@ -1573,11 +1627,10 @@ class MiniscriptDescriptor final : public DescriptorImpl
15731627
bool ToStringHelper(const SigningProvider* arg, std::string& out, const StringType type,
15741628
const DescriptorCache* cache = nullptr) const override
15751629
{
1576-
if (const auto res = m_node->ToString(StringMaker(arg, m_pubkey_args, type, cache))) {
1577-
out = *res;
1578-
return true;
1579-
}
1580-
return false;
1630+
bool has_priv_key{false};
1631+
auto res = m_node->ToString(StringMaker(arg, m_pubkey_args, type, cache), has_priv_key);
1632+
if (res) out = *res;
1633+
return type == StringType::PRIVATE ? has_priv_key : res.has_value();
15811634
}
15821635

15831636
bool IsSolvable() const override { return true; }
@@ -2132,7 +2185,7 @@ struct KeyParser {
21322185
return key;
21332186
}
21342187

2135-
std::optional<std::string> ToString(const Key& key) const
2188+
std::optional<std::string> ToString(const Key& key, bool&) const
21362189
{
21372190
return m_keys.at(key).at(0)->ToString();
21382191
}
@@ -2550,7 +2603,7 @@ std::vector<std::unique_ptr<DescriptorImpl>> ParseScript(uint32_t& key_exp_index
25502603
// Try to find the first insane sub for better error reporting.
25512604
auto insane_node = node.get();
25522605
if (const auto sub = node->FindInsaneSub()) insane_node = sub;
2553-
if (const auto str = insane_node->ToString(parser)) error = *str;
2606+
error = *insane_node->ToString(parser);
25542607
if (!insane_node->IsValid()) {
25552608
error += " is invalid";
25562609
} else if (!node->IsSane()) {

src/script/descriptor.h

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,20 @@ 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 the given provider has all private keys required by this descriptor.
115+
* @return `false` if the descriptor doesn't have any keys or subdescriptors,
116+
* or if the provider does not have all private keys required by
117+
* the descriptor.
118+
*/
119+
virtual bool HavePrivateKeys(const SigningProvider& provider) const = 0;
120+
121+
/** Convert the descriptor to a private string. This uses public keys if the relevant private keys are not in the SigningProvider.
122+
* If none of the relevant private keys are available, the output string in the "out" parameter will not contain any private key information,
123+
* and this function will return "false".
124+
* @param[in] provider The SigningProvider to query for private keys.
125+
* @param[out] out The resulting descriptor string, containing private keys if available.
126+
* @returns true if at least one private key available.
127+
*/
115128
virtual bool ToPrivateString(const SigningProvider& provider, std::string& out) const = 0;
116129

117130
/** 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: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -843,6 +843,12 @@ struct Node {
843843

844844
template<typename CTx>
845845
std::optional<std::string> ToString(const CTx& ctx) const {
846+
bool dummy{false};
847+
return ToString(ctx, dummy);
848+
}
849+
850+
template<typename CTx>
851+
std::optional<std::string> ToString(const CTx& ctx, bool& has_priv_key) const {
846852
// To construct the std::string representation for a Miniscript object, we use
847853
// the TreeEvalMaybe algorithm. The State is a boolean: whether the parent node is a
848854
// wrapper. If so, non-wrapper expressions must be prefixed with a ":".
@@ -855,10 +861,16 @@ struct Node {
855861
(node.fragment == Fragment::OR_I && node.subs[0]->fragment == Fragment::JUST_0) ||
856862
(node.fragment == Fragment::OR_I && node.subs[1]->fragment == Fragment::JUST_0));
857863
};
864+
auto toString = [&ctx, &has_priv_key](Key key) -> std::optional<std::string> {
865+
bool fragment_has_priv_key{false};
866+
auto key_str{ctx.ToString(key, fragment_has_priv_key)};
867+
if (key_str) has_priv_key = has_priv_key || fragment_has_priv_key;
868+
return key_str;
869+
};
858870
// The upward function computes for a node, given whether its parent is a wrapper,
859871
// and the string representations of its child nodes, the string representation of the node.
860872
const bool is_tapscript{IsTapscript(m_script_ctx)};
861-
auto upfn = [&ctx, is_tapscript](bool wrapped, const Node& node, std::span<std::string> subs) -> std::optional<std::string> {
873+
auto upfn = [is_tapscript, &toString](bool wrapped, const Node& node, std::span<std::string> subs) -> std::optional<std::string> {
862874
std::string ret = wrapped ? ":" : "";
863875

864876
switch (node.fragment) {
@@ -867,13 +879,13 @@ struct Node {
867879
case Fragment::WRAP_C:
868880
if (node.subs[0]->fragment == Fragment::PK_K) {
869881
// pk(K) is syntactic sugar for c:pk_k(K)
870-
auto key_str = ctx.ToString(node.subs[0]->keys[0]);
882+
auto key_str = toString(node.subs[0]->keys[0]);
871883
if (!key_str) return {};
872884
return std::move(ret) + "pk(" + std::move(*key_str) + ")";
873885
}
874886
if (node.subs[0]->fragment == Fragment::PK_H) {
875887
// pkh(K) is syntactic sugar for c:pk_h(K)
876-
auto key_str = ctx.ToString(node.subs[0]->keys[0]);
888+
auto key_str = toString(node.subs[0]->keys[0]);
877889
if (!key_str) return {};
878890
return std::move(ret) + "pkh(" + std::move(*key_str) + ")";
879891
}
@@ -894,12 +906,12 @@ struct Node {
894906
}
895907
switch (node.fragment) {
896908
case Fragment::PK_K: {
897-
auto key_str = ctx.ToString(node.keys[0]);
909+
auto key_str = toString(node.keys[0]);
898910
if (!key_str) return {};
899911
return std::move(ret) + "pk_k(" + std::move(*key_str) + ")";
900912
}
901913
case Fragment::PK_H: {
902-
auto key_str = ctx.ToString(node.keys[0]);
914+
auto key_str = toString(node.keys[0]);
903915
if (!key_str) return {};
904916
return std::move(ret) + "pk_h(" + std::move(*key_str) + ")";
905917
}
@@ -925,7 +937,7 @@ struct Node {
925937
CHECK_NONFATAL(!is_tapscript);
926938
auto str = std::move(ret) + "multi(" + util::ToString(node.k);
927939
for (const auto& key : node.keys) {
928-
auto key_str = ctx.ToString(key);
940+
auto key_str = toString(key);
929941
if (!key_str) return {};
930942
str += "," + std::move(*key_str);
931943
}
@@ -935,7 +947,7 @@ struct Node {
935947
CHECK_NONFATAL(is_tapscript);
936948
auto str = std::move(ret) + "multi_a(" + util::ToString(node.k);
937949
for (const auto& key : node.keys) {
938-
auto key_str = ctx.ToString(key);
950+
auto key_str = toString(key);
939951
if (!key_str) return {};
940952
str += "," + std::move(*key_str);
941953
}

src/test/descriptor_tests.cpp

Lines changed: 10 additions & 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. ToPrivateString() will return true if there is at least one private key and HavePrivateKeys() will return `false`.
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
@@ -243,6 +243,9 @@ void DoCheck(std::string prv, std::string pub, const std::string& norm_pub, int
243243
} else {
244244
BOOST_CHECK_MESSAGE(EqualDescriptor(prv, prv1), "Private ser: " + prv1 + " Private desc: " + prv);
245245
}
246+
BOOST_CHECK(!parse_priv->HavePrivateKeys(keys_pub));
247+
BOOST_CHECK(parse_pub->HavePrivateKeys(keys_priv));
248+
246249
BOOST_CHECK(!parse_priv->ToPrivateString(keys_pub, prv1));
247250
BOOST_CHECK(parse_pub->ToPrivateString(keys_priv, prv1));
248251
if (expected_prv) {
@@ -261,6 +264,12 @@ void DoCheck(std::string prv, std::string pub, const std::string& norm_pub, int
261264
parse_pub->ExpandPrivate(0, keys_priv, pub_prov);
262265

263266
BOOST_CHECK_MESSAGE(EqualSigningProviders(priv_prov, pub_prov), "Private desc: " + prv + " Pub desc: " + pub);
267+
} else if (keys_priv.keys.size() > 0) {
268+
// If there is at least one private key, ToPrivateString() should return true and include that key
269+
std::string prv_str;
270+
BOOST_CHECK(parse_priv->ToPrivateString(keys_priv, prv_str));
271+
size_t checksum_len = 9; // Including the '#' character
272+
BOOST_CHECK_MESSAGE(prv == prv_str.substr(0, prv_str.length() - checksum_len), prv);
264273
}
265274

266275
// Check that private can produce the normalized descriptors

0 commit comments

Comments
 (0)