From 62c261c6f7f8d5bd42fa3b7aee881cd7acc8f7bd Mon Sep 17 00:00:00 2001 From: rkrux Date: Tue, 1 Apr 2025 19:56:27 +0530 Subject: [PATCH 1/2] descriptor: list private descriptors if not all keys present When parsing descriptors with multiple keys (tr, wsh, sh, miniscript), we might not have all the private keys but only few of them (rest being public keys). `listdescriptors(private=true)` RPC should not fail in such scenario and instead return those partial private keys, using public keys for the rest. --- src/script/descriptor.cpp | 61 ++++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp index e132a0ba224..970f915efee 100644 --- a/src/script/descriptor.cpp +++ b/src/script/descriptor.cpp @@ -635,13 +635,26 @@ public: virtual bool ToStringSubScriptHelper(const SigningProvider* arg, std::string& ret, const StringType type, const DescriptorCache* cache = nullptr) const { size_t pos = 0; + bool has_any_priv = false; for (const auto& scriptarg : m_subdescriptor_args) { if (pos++) ret += ","; std::string tmp; - if (!scriptarg->ToStringHelper(arg, tmp, type, cache)) return false; + bool success = scriptarg->ToStringHelper(arg, tmp, type, cache); + if (type == StringType::PRIVATE) { + if (success) { + has_any_priv = true; + } else { + tmp = ""; + if (!scriptarg->ToStringHelper(arg, tmp, StringType::PUBLIC, cache)) { + return false; + } + } + } else if (!success) { + return false; + } ret += tmp; } - return true; + return (type == StringType::PRIVATE) ? has_any_priv : true; } // NOLINTNEXTLINE(misc-no-recursion) @@ -650,6 +663,7 @@ public: std::string extra = ToStringExtra(); size_t pos = extra.size() > 0 ? 1 : 0; std::string ret = m_name + "(" + extra; + bool has_any_priv = false; for (const auto& pubkey : m_pubkey_args) { if (pos++) ret += ","; std::string tmp; @@ -658,7 +672,11 @@ public: if (!pubkey->ToNormalizedString(*arg, tmp, cache)) return false; break; case StringType::PRIVATE: - if (!pubkey->ToPrivateString(*arg, tmp)) return false; + if (!pubkey->ToPrivateString(*arg, tmp)) { + tmp = pubkey->ToString(); + } else { + has_any_priv = true; + } break; case StringType::PUBLIC: tmp = pubkey->ToString(); @@ -670,7 +688,21 @@ public: ret += tmp; } std::string subscript; - if (!ToStringSubScriptHelper(arg, subscript, type, cache)) return false; + bool is_subscript_successful = ToStringSubScriptHelper(arg, subscript, type, cache); + + if (type == StringType::PRIVATE) { + if (!has_any_priv) { + if (subscript.empty()) { + return false; + } + if (!is_subscript_successful) { + return false; + } + } + } else if (!is_subscript_successful) { + return false; + } + if (pos && subscript.size()) ret += ','; out = std::move(ret) + std::move(subscript) + ")"; return true; @@ -1181,6 +1213,7 @@ protected: bool ToStringSubScriptHelper(const SigningProvider* arg, std::string& ret, const StringType type, const DescriptorCache* cache = nullptr) const override { if (m_depths.empty()) return true; + bool has_any_subscript_key = false; std::vector path; for (size_t pos = 0; pos < m_depths.size(); ++pos) { if (pos) ret += ','; @@ -1189,7 +1222,19 @@ protected: path.push_back(false); } std::string tmp; - if (!m_subdescriptor_args[pos]->ToStringHelper(arg, tmp, type, cache)) return false; + bool is_subdescriptor_successful = m_subdescriptor_args[pos]->ToStringHelper(arg, tmp, type, cache); + if (type == StringType::PRIVATE) { + if (is_subdescriptor_successful) { + has_any_subscript_key = true; + } else { + tmp = ""; + if (!m_subdescriptor_args[pos]->ToStringHelper(arg, tmp, StringType::PUBLIC, cache)) { + return false; + } + } + } else if (!is_subdescriptor_successful) { + return false; + } ret += tmp; while (!path.empty() && path.back()) { if (path.size() > 1) ret += '}'; @@ -1197,7 +1242,7 @@ protected: } if (!path.empty()) path.back() = true; } - return true; + return (type == StringType::PRIVATE) ? has_any_subscript_key : true; } public: TRDescriptor(std::unique_ptr internal_key, std::vector> descs, std::vector depths) : @@ -1290,7 +1335,9 @@ public: { std::string ret; if (m_private) { - if (!m_pubkeys[key]->ToPrivateString(*m_arg, ret)) return {}; + if (!m_pubkeys[key]->ToPrivateString(*m_arg, ret)) { + ret = m_pubkeys[key]->ToString(); + } } else { ret = m_pubkeys[key]->ToString(); } From a0c73846aa6a38219309f948a4be5c39db2ad48d Mon Sep 17 00:00:00 2001 From: rkrux Date: Tue, 1 Apr 2025 19:56:41 +0530 Subject: [PATCH 2/2] test: listdescriptors(private=true) if not all private keys present --- test/functional/wallet_listdescriptors.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/functional/wallet_listdescriptors.py b/test/functional/wallet_listdescriptors.py index 79ebbb1d2bd..f280b321046 100755 --- a/test/functional/wallet_listdescriptors.py +++ b/test/functional/wallet_listdescriptors.py @@ -32,6 +32,26 @@ class ListDescriptorsTest(BitcoinTestFramework): def init_wallet(self, *, node): return + def test_listdescriptors_with_partial_privkeys(self): + self.nodes[0].createwallet(wallet_name="partialkeys_descs", blank=True) + wallet = self.nodes[0].get_wallet_rpc("partialkeys_descs") + + self.log.info("Test listdescriptors(private=true) with descriptors containing partial private keys") + descs = [ + descsum_create("wsh(multi(2,tprv8ZgxMBicQKsPdzuc344mDaeUk5zseMcRK9Hst8xodskNu3YbQG5NxLa2X17PUU5yXQhptiBE7F5W5cgEmsfQg4Y21Y18w4DJhLxSb8CurDf,tpubD6NzVbkrYhZ4YiCvExLvH4yh1k3jFGf5irm6TsrArY8GYdEhYVdztQTBtTirmRc6XfSJpH9tayUdnngaJZKDaa2zbqEY29DfcGZW8iRVGUY))"), + descsum_create("wsh(thresh(2,pk(tprv8ZgxMBicQKsPdzuc344mDaeUk5zseMcRK9Hst8xodskNu3YbQG5NxLa2X17PUU5yXQhptiBE7F5W5cgEmsfQg4Y21Y18w4DJhLxSb8CurDf),s:pk(tpubD6NzVbkrYhZ4YiCvExLvH4yh1k3jFGf5irm6TsrArY8GYdEhYVdztQTBtTirmRc6XfSJpH9tayUdnngaJZKDaa2zbqEY29DfcGZW8iRVGUY),sln:older(2)))"), + descsum_create("tr(03d1d1110030000000000120000000000000000000000000001370010912cd08cc,pk(cNKAo2ZRsaWKcP481cEfj3astPyBrfq56JBtLeRhHUvTSuk2z4MR))"), + descsum_create("tr(cNKAo2ZRsaWKcP481cEfj3astPyBrfq56JBtLeRhHUvTSuk2z4MR,pk(03d1d1110030000000000120000000000000000000000000001370010912cd08cc))") + ] + importdescriptors_request = list(map(lambda desc: {"desc": desc, "timestamp": "now"}, descs)) + res = wallet.importdescriptors(importdescriptors_request) + for imported_desc in res: + assert_equal(imported_desc["success"], True) + res = wallet.listdescriptors(private=True) + for listed_desc in res["descriptors"]: + # descriptors are not returned in the order they were present in the import request + assert_equal(listed_desc["desc"], next(desc for desc in descs if listed_desc["desc"] == desc)) + def run_test(self): node = self.nodes[0] assert_raises_rpc_error(-18, 'No wallet is loaded.', node.listdescriptors) @@ -133,6 +153,7 @@ class ListDescriptorsTest(BitcoinTestFramework): ] } assert_equal(expected, wallet.listdescriptors()) + self.test_listdescriptors_with_partial_privkeys() if __name__ == '__main__':