Skip to content

Commit f7e88e2

Browse files
committed
Merge bitcoin/bitcoin#32471: wallet/rpc: fix listdescriptors RPC fails to return descriptors with private key information when wallet contains descriptors missing any key
9c7e477 test: Test listdescs with priv works even with missing priv keys (Novo) ed945a6 walletrpc: reject listdes with priv key on w-only wallets (Novo) 9e5e982 descriptor: ToPrivateString() pass if at least 1 priv key exists (Novo) 5c4db25 descriptor: refactor ToPrivateString for providers (Novo) 2dc74e3 wallet/migration: use HavePrivateKeys in place of ToPrivateString (Novo) e842eb9 descriptors: add HavePrivateKeys() (Novo) Pull request description: _TLDR: Currently, `listdescriptors [private=true]` will fail for a non-watch-only wallet if any descriptor has a missing private key(e.g `tr()`, `multi()`, etc.). This PR changes that while making sure `listdescriptors [private=true]` still fails if there no private keys. Closes #32078_ In non-watch-only wallets, it's possible to import descriptors as long as at least one private key is included. It's important that users can still view these descriptors when they need to create a backup—even if some private keys are missing ([#32078 (comment)](bitcoin/bitcoin#32078 (comment))). This change makes it possible to do so. This change also helps prevent `listdescriptors true` from failing completely, because one descriptor is missing some private keys. ### Notes - The new behaviour is applied to all descriptors including miniscript descriptors - `listdescriptors true` still fails for watch-only wallets to preserve existing behaviour bitcoin/bitcoin#24361 (comment) - Wallet migration logic previously used `Descriptor::ToPrivateString()` to determine which descriptor was watchonly. This means that modifying the `ToPrivateString()` behaviour caused descriptors that were previously recognized as "watchonly" to be "non-watchonly". **In order to keep the scope of this PR limited to the RPC behaviour, this PR uses a different method to determine `watchonly` descriptors for the purpose of wallet migration.** A follow-up PR can be opened to update migration logic to exclude descriptors with some private keys from the `watchonly` migration wallet. ### Relevant PRs bitcoin/bitcoin#24361 bitcoin/bitcoin#32186 ### Testing Functional tests were added to test the new behaviour EDIT **`listdescriptors [private=true]` will still fail when there are no private keys because non-watchonly wallets must have private keys and calling `listdescriptors [private=true]` for watchonly wallet returns an error** ACKs for top commit: Sjors: ACK 9c7e477 achow101: ACK 9c7e477 w0xlt: reACK bitcoin/bitcoin@9c7e477 with minor nits rkrux: re-ACK 9c7e477 Tree-SHA512: f9b3b2c3e5425a26e158882e39e82e15b7cb13ffbfb6a5fa2868c79526e9b178fcc3cd88d3e2e286f64819d041f687353780bbcf5a355c63a136fb8179698b60
2 parents 7f5ebef + 9c7e477 commit f7e88e2

File tree

10 files changed

+176
-48
lines changed

10 files changed

+176
-48
lines changed

src/script/descriptor.cpp

Lines changed: 85 additions & 27 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
@@ -1396,24 +1435,38 @@ class TRDescriptor final : public DescriptorImpl
13961435
}
13971436
bool ToStringSubScriptHelper(const SigningProvider* arg, std::string& ret, const StringType type, const DescriptorCache* cache = nullptr) const override
13981437
{
1399-
if (m_depths.empty()) return true;
1438+
if (m_depths.empty()) {
1439+
// If there are no sub-descriptors and a PRIVATE string
1440+
// is requested, return `false` to indicate that the presence
1441+
// of a private key depends solely on the internal key (which is checked
1442+
// in the caller), not on any sub-descriptor. This ensures correct behavior for
1443+
// descriptors like tr(internal_key) when checking for private keys.
1444+
return type != StringType::PRIVATE;
1445+
}
14001446
std::vector<bool> path;
1447+
bool is_private{type == StringType::PRIVATE};
1448+
// For private string output, track if at least one key has a private key available.
1449+
// Initialize to true for non-private types.
1450+
bool any_success{!is_private};
1451+
14011452
for (size_t pos = 0; pos < m_depths.size(); ++pos) {
14021453
if (pos) ret += ',';
14031454
while ((int)path.size() <= m_depths[pos]) {
14041455
if (path.size()) ret += '{';
14051456
path.push_back(false);
14061457
}
14071458
std::string tmp;
1408-
if (!m_subdescriptor_args[pos]->ToStringHelper(arg, tmp, type, cache)) return false;
1459+
bool subscript_res{m_subdescriptor_args[pos]->ToStringHelper(arg, tmp, type, cache)};
1460+
if (!is_private && !subscript_res) return false;
1461+
any_success = any_success || subscript_res;
14091462
ret += tmp;
14101463
while (!path.empty() && path.back()) {
14111464
if (path.size() > 1) ret += '}';
14121465
path.pop_back();
14131466
}
14141467
if (!path.empty()) path.back() = true;
14151468
}
1416-
return true;
1469+
return any_success;
14171470
}
14181471
public:
14191472
TRDescriptor(std::unique_ptr<PubkeyProvider> internal_key, std::vector<std::unique_ptr<DescriptorImpl>> descs, std::vector<int> depths) :
@@ -1506,15 +1559,16 @@ class StringMaker {
15061559
const DescriptorCache* cache LIFETIMEBOUND)
15071560
: m_arg(arg), m_pubkeys(pubkeys), m_type(type), m_cache(cache) {}
15081561

1509-
std::optional<std::string> ToString(uint32_t key) const
1562+
std::optional<std::string> ToString(uint32_t key, bool& has_priv_key) const
15101563
{
15111564
std::string ret;
1565+
has_priv_key = false;
15121566
switch (m_type) {
15131567
case DescriptorImpl::StringType::PUBLIC:
15141568
ret = m_pubkeys[key]->ToString();
15151569
break;
15161570
case DescriptorImpl::StringType::PRIVATE:
1517-
if (!m_pubkeys[key]->ToPrivateString(*m_arg, ret)) return {};
1571+
has_priv_key = m_pubkeys[key]->ToPrivateString(*m_arg, ret);
15181572
break;
15191573
case DescriptorImpl::StringType::NORMALIZED:
15201574
if (!m_pubkeys[key]->ToNormalizedString(*m_arg, ret, m_cache)) return {};
@@ -1571,11 +1625,15 @@ class MiniscriptDescriptor final : public DescriptorImpl
15711625
bool ToStringHelper(const SigningProvider* arg, std::string& out, const StringType type,
15721626
const DescriptorCache* cache = nullptr) const override
15731627
{
1574-
if (const auto res = m_node->ToString(StringMaker(arg, m_pubkey_args, type, cache))) {
1575-
out = *res;
1576-
return true;
1628+
bool has_priv_key{false};
1629+
auto res = m_node->ToString(StringMaker(arg, m_pubkey_args, type, cache), has_priv_key);
1630+
if (res) out = *res;
1631+
if (type == StringType::PRIVATE) {
1632+
Assume(res.has_value());
1633+
return has_priv_key;
1634+
} else {
1635+
return res.has_value();
15771636
}
1578-
return false;
15791637
}
15801638

15811639
bool IsSolvable() const override { return true; }
@@ -2113,7 +2171,7 @@ struct KeyParser {
21132171
return key;
21142172
}
21152173

2116-
std::optional<std::string> ToString(const Key& key) const
2174+
std::optional<std::string> ToString(const Key& key, bool&) const
21172175
{
21182176
return m_keys.at(key).at(0)->ToString();
21192177
}
@@ -2510,7 +2568,7 @@ std::vector<std::unique_ptr<DescriptorImpl>> ParseScript(uint32_t& key_exp_index
25102568
// Try to find the first insane sub for better error reporting.
25112569
auto insane_node = node.get();
25122570
if (const auto sub = node->FindInsaneSub()) insane_node = sub;
2513-
if (const auto str = insane_node->ToString(parser)) error = *str;
2571+
error = *insane_node->ToString(parser);
25142572
if (!insane_node->IsValid()) {
25152573
error += " is invalid";
25162574
} 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 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)