diff --git a/src/script/descriptor.cpp b/src/script/descriptor.cpp index c530c888c43..f771ee52787 100644 --- a/src/script/descriptor.cpp +++ b/src/script/descriptor.cpp @@ -204,7 +204,11 @@ public: /** Get the descriptor string form. */ virtual std::string ToString(StringType type=StringType::PUBLIC) const = 0; - /** Get the descriptor string form including private data (if available in arg). */ + /** Get the descriptor string form including private data (if available in arg). + * If the private data is not available, the output string in the "out" parameter + * will not contain any private key information, + * and this function will return "false". + */ virtual bool ToPrivateString(const SigningProvider& arg, std::string& out) const = 0; /** Get the descriptor string form with the xpub at the last hardened derivation, @@ -260,9 +264,9 @@ public: bool ToPrivateString(const SigningProvider& arg, std::string& ret) const override { std::string sub; - if (!m_provider->ToPrivateString(arg, sub)) return false; + bool has_priv_key{m_provider->ToPrivateString(arg, sub)}; ret = "[" + OriginString(StringType::PUBLIC) + "]" + std::move(sub); - return true; + return has_priv_key; } bool ToNormalizedString(const SigningProvider& arg, std::string& ret, const DescriptorCache* cache) const override { @@ -329,7 +333,10 @@ public: bool ToPrivateString(const SigningProvider& arg, std::string& ret) const override { std::optional key = GetPrivKey(arg); - if (!key) return false; + if (!key) { + ret = ToString(StringType::PUBLIC); + return false; + } ret = EncodeSecret(*key); return true; } @@ -492,7 +499,10 @@ public: bool ToPrivateString(const SigningProvider& arg, std::string& out) const override { CExtKey key; - if (!GetExtKey(arg, key)) return false; + if (!GetExtKey(arg, key)) { + out = ToString(StringType::PUBLIC); + return false; + } out = EncodeExtKey(key) + FormatHDKeypath(m_path, /*apostrophe=*/m_apostrophe); if (IsRange()) { out += "/*"; @@ -710,17 +720,14 @@ public: std::string tmp; if (pubkey->ToPrivateString(arg, tmp)) { any_privkeys = true; - out += tmp; - } else { - out += pubkey->ToString(); } + out += tmp; } out += ")"; out += FormatHDKeypath(m_path); if (IsRangedDerivation()) { out += "/*"; } - if (!any_privkeys) out.clear(); return any_privkeys; } bool ToNormalizedString(const SigningProvider& arg, std::string& out, const DescriptorCache* cache = nullptr) const override @@ -837,6 +844,25 @@ public: return true; } + // NOLINTNEXTLINE(misc-no-recursion) + bool HavePrivateKeys(const SigningProvider& arg) const override + { + if (m_pubkey_args.empty() && m_subdescriptor_args.empty()) return false; + + for (const auto& sub: m_subdescriptor_args) { + if (!sub->HavePrivateKeys(arg)) return false; + } + + FlatSigningProvider tmp_provider; + for (const auto& pubkey : m_pubkey_args) { + tmp_provider.keys.clear(); + pubkey->GetPrivKey(0, arg, tmp_provider); + if (tmp_provider.keys.empty()) return false; + } + + return true; + } + // NOLINTNEXTLINE(misc-no-recursion) bool IsRange() const final { @@ -853,13 +879,19 @@ public: virtual bool ToStringSubScriptHelper(const SigningProvider* arg, std::string& ret, const StringType type, const DescriptorCache* cache = nullptr) const { size_t pos = 0; + bool is_private{type == StringType::PRIVATE}; + // For private string output, track if at least one key has a private key available. + // Initialize to true for non-private types. + bool any_success{!is_private}; for (const auto& scriptarg : m_subdescriptor_args) { if (pos++) ret += ","; std::string tmp; - if (!scriptarg->ToStringHelper(arg, tmp, type, cache)) return false; + bool subscript_res{scriptarg->ToStringHelper(arg, tmp, type, cache)}; + if (!is_private && !subscript_res) return false; + any_success = any_success || subscript_res; ret += tmp; } - return true; + return any_success; } // NOLINTNEXTLINE(misc-no-recursion) @@ -868,6 +900,11 @@ public: std::string extra = ToStringExtra(); size_t pos = extra.size() > 0 ? 1 : 0; std::string ret = m_name + "(" + extra; + bool is_private{type == StringType::PRIVATE}; + // For private string output, track if at least one key has a private key available. + // Initialize to true for non-private types. + bool any_success{!is_private}; + for (const auto& pubkey : m_pubkey_args) { if (pos++) ret += ","; std::string tmp; @@ -876,7 +913,7 @@ public: if (!pubkey->ToNormalizedString(*arg, tmp, cache)) return false; break; case StringType::PRIVATE: - if (!pubkey->ToPrivateString(*arg, tmp)) return false; + any_success = pubkey->ToPrivateString(*arg, tmp) || any_success; break; case StringType::PUBLIC: tmp = pubkey->ToString(); @@ -888,10 +925,12 @@ public: ret += tmp; } std::string subscript; - if (!ToStringSubScriptHelper(arg, subscript, type, cache)) return false; + bool subscript_res{ToStringSubScriptHelper(arg, subscript, type, cache)}; + if (!is_private && !subscript_res) return false; + any_success = any_success || subscript_res; if (pos && subscript.size()) ret += ','; out = std::move(ret) + std::move(subscript) + ")"; - return true; + return any_success; } std::string ToString(bool compat_format) const final @@ -903,9 +942,9 @@ public: bool ToPrivateString(const SigningProvider& arg, std::string& out) const override { - bool ret = ToStringHelper(&arg, out, StringType::PRIVATE); + bool has_priv_key{ToStringHelper(&arg, out, StringType::PRIVATE)}; out = AddChecksum(out); - return ret; + return has_priv_key; } bool ToNormalizedString(const SigningProvider& arg, std::string& out, const DescriptorCache* cache) const override final @@ -1396,8 +1435,20 @@ protected: } bool ToStringSubScriptHelper(const SigningProvider* arg, std::string& ret, const StringType type, const DescriptorCache* cache = nullptr) const override { - if (m_depths.empty()) return true; + if (m_depths.empty()) { + // If there are no sub-descriptors and a PRIVATE string + // is requested, return `false` to indicate that the presence + // of a private key depends solely on the internal key (which is checked + // in the caller), not on any sub-descriptor. This ensures correct behavior for + // descriptors like tr(internal_key) when checking for private keys. + return type != StringType::PRIVATE; + } std::vector path; + bool is_private{type == StringType::PRIVATE}; + // For private string output, track if at least one key has a private key available. + // Initialize to true for non-private types. + bool any_success{!is_private}; + for (size_t pos = 0; pos < m_depths.size(); ++pos) { if (pos) ret += ','; while ((int)path.size() <= m_depths[pos]) { @@ -1405,7 +1456,9 @@ protected: path.push_back(false); } std::string tmp; - if (!m_subdescriptor_args[pos]->ToStringHelper(arg, tmp, type, cache)) return false; + bool subscript_res{m_subdescriptor_args[pos]->ToStringHelper(arg, tmp, type, cache)}; + if (!is_private && !subscript_res) return false; + any_success = any_success || subscript_res; ret += tmp; while (!path.empty() && path.back()) { if (path.size() > 1) ret += '}'; @@ -1413,7 +1466,7 @@ protected: } if (!path.empty()) path.back() = true; } - return true; + return any_success; } public: TRDescriptor(std::unique_ptr internal_key, std::vector> descs, std::vector depths) : @@ -1506,15 +1559,16 @@ public: const DescriptorCache* cache LIFETIMEBOUND) : m_arg(arg), m_pubkeys(pubkeys), m_type(type), m_cache(cache) {} - std::optional ToString(uint32_t key) const + std::optional ToString(uint32_t key, bool& has_priv_key) const { std::string ret; + has_priv_key = false; switch (m_type) { case DescriptorImpl::StringType::PUBLIC: ret = m_pubkeys[key]->ToString(); break; case DescriptorImpl::StringType::PRIVATE: - if (!m_pubkeys[key]->ToPrivateString(*m_arg, ret)) return {}; + has_priv_key = m_pubkeys[key]->ToPrivateString(*m_arg, ret); break; case DescriptorImpl::StringType::NORMALIZED: if (!m_pubkeys[key]->ToNormalizedString(*m_arg, ret, m_cache)) return {}; @@ -1571,11 +1625,15 @@ public: bool ToStringHelper(const SigningProvider* arg, std::string& out, const StringType type, const DescriptorCache* cache = nullptr) const override { - if (const auto res = m_node->ToString(StringMaker(arg, m_pubkey_args, type, cache))) { - out = *res; - return true; + bool has_priv_key{false}; + auto res = m_node->ToString(StringMaker(arg, m_pubkey_args, type, cache), has_priv_key); + if (res) out = *res; + if (type == StringType::PRIVATE) { + Assume(res.has_value()); + return has_priv_key; + } else { + return res.has_value(); } - return false; } bool IsSolvable() const override { return true; } @@ -2113,7 +2171,7 @@ struct KeyParser { return key; } - std::optional ToString(const Key& key) const + std::optional ToString(const Key& key, bool&) const { return m_keys.at(key).at(0)->ToString(); } @@ -2510,7 +2568,7 @@ std::vector> ParseScript(uint32_t& key_exp_index // Try to find the first insane sub for better error reporting. auto insane_node = node.get(); if (const auto sub = node->FindInsaneSub()) insane_node = sub; - if (const auto str = insane_node->ToString(parser)) error = *str; + error = *insane_node->ToString(parser); if (!insane_node->IsValid()) { error += " is invalid"; } else if (!node->IsSane()) { diff --git a/src/script/descriptor.h b/src/script/descriptor.h index 8c8dff8a853..fe33476da52 100644 --- a/src/script/descriptor.h +++ b/src/script/descriptor.h @@ -111,7 +111,20 @@ struct Descriptor { /** Whether this descriptor will return one scriptPubKey or multiple (aka is or is not combo) */ virtual bool IsSingleType() const = 0; - /** Convert the descriptor to a private string. This fails if the provided provider does not have the relevant private keys. */ + /** Whether the given provider has all private keys required by this descriptor. + * @return `false` if the descriptor doesn't have any keys or subdescriptors, + * or if the provider does not have all private keys required by + * the descriptor. + */ + virtual bool HavePrivateKeys(const SigningProvider& provider) const = 0; + + /** Convert the descriptor to a private string. This uses public keys if the relevant private keys are not in the SigningProvider. + * If none of the relevant private keys are available, the output string in the "out" parameter will not contain any private key information, + * and this function will return "false". + * @param[in] provider The SigningProvider to query for private keys. + * @param[out] out The resulting descriptor string, containing private keys if available. + * @returns true if at least one private key available. + */ virtual bool ToPrivateString(const SigningProvider& provider, std::string& out) const = 0; /** 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. */ diff --git a/src/script/miniscript.h b/src/script/miniscript.h index 955ede236fe..74600292b01 100644 --- a/src/script/miniscript.h +++ b/src/script/miniscript.h @@ -843,6 +843,12 @@ public: template std::optional ToString(const CTx& ctx) const { + bool dummy{false}; + return ToString(ctx, dummy); + } + + template + std::optional ToString(const CTx& ctx, bool& has_priv_key) const { // To construct the std::string representation for a Miniscript object, we use // the TreeEvalMaybe algorithm. The State is a boolean: whether the parent node is a // wrapper. If so, non-wrapper expressions must be prefixed with a ":". @@ -855,10 +861,16 @@ public: (node.fragment == Fragment::OR_I && node.subs[0]->fragment == Fragment::JUST_0) || (node.fragment == Fragment::OR_I && node.subs[1]->fragment == Fragment::JUST_0)); }; + auto toString = [&ctx, &has_priv_key](Key key) -> std::optional { + bool fragment_has_priv_key{false}; + auto key_str{ctx.ToString(key, fragment_has_priv_key)}; + if (key_str) has_priv_key = has_priv_key || fragment_has_priv_key; + return key_str; + }; // The upward function computes for a node, given whether its parent is a wrapper, // and the string representations of its child nodes, the string representation of the node. const bool is_tapscript{IsTapscript(m_script_ctx)}; - auto upfn = [&ctx, is_tapscript](bool wrapped, const Node& node, std::span subs) -> std::optional { + auto upfn = [is_tapscript, &toString](bool wrapped, const Node& node, std::span subs) -> std::optional { std::string ret = wrapped ? ":" : ""; switch (node.fragment) { @@ -867,13 +879,13 @@ public: case Fragment::WRAP_C: if (node.subs[0]->fragment == Fragment::PK_K) { // pk(K) is syntactic sugar for c:pk_k(K) - auto key_str = ctx.ToString(node.subs[0]->keys[0]); + auto key_str = toString(node.subs[0]->keys[0]); if (!key_str) return {}; return std::move(ret) + "pk(" + std::move(*key_str) + ")"; } if (node.subs[0]->fragment == Fragment::PK_H) { // pkh(K) is syntactic sugar for c:pk_h(K) - auto key_str = ctx.ToString(node.subs[0]->keys[0]); + auto key_str = toString(node.subs[0]->keys[0]); if (!key_str) return {}; return std::move(ret) + "pkh(" + std::move(*key_str) + ")"; } @@ -894,12 +906,12 @@ public: } switch (node.fragment) { case Fragment::PK_K: { - auto key_str = ctx.ToString(node.keys[0]); + auto key_str = toString(node.keys[0]); if (!key_str) return {}; return std::move(ret) + "pk_k(" + std::move(*key_str) + ")"; } case Fragment::PK_H: { - auto key_str = ctx.ToString(node.keys[0]); + auto key_str = toString(node.keys[0]); if (!key_str) return {}; return std::move(ret) + "pk_h(" + std::move(*key_str) + ")"; } @@ -925,7 +937,7 @@ public: CHECK_NONFATAL(!is_tapscript); auto str = std::move(ret) + "multi(" + util::ToString(node.k); for (const auto& key : node.keys) { - auto key_str = ctx.ToString(key); + auto key_str = toString(key); if (!key_str) return {}; str += "," + std::move(*key_str); } @@ -935,7 +947,7 @@ public: CHECK_NONFATAL(is_tapscript); auto str = std::move(ret) + "multi_a(" + util::ToString(node.k); for (const auto& key : node.keys) { - auto key_str = ctx.ToString(key); + auto key_str = toString(key); if (!key_str) return {}; str += "," + std::move(*key_str); } diff --git a/src/test/descriptor_tests.cpp b/src/test/descriptor_tests.cpp index 04794b73ba4..8ff44d13dcc 100644 --- a/src/test/descriptor_tests.cpp +++ b/src/test/descriptor_tests.cpp @@ -49,7 +49,7 @@ constexpr int SIGNABLE = 1 << 3; // We can sign with this descriptor (this is no constexpr int DERIVE_HARDENED = 1 << 4; // The final derivation is hardened, i.e. ends with *' or *h constexpr int MIXED_PUBKEYS = 1 << 5; constexpr int XONLY_KEYS = 1 << 6; // X-only pubkeys are in use (and thus inferring/caching may swap parity of pubkeys/keyids) -constexpr int MISSING_PRIVKEYS = 1 << 7; // Not all private keys are available, so ToPrivateString will fail. +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`. constexpr int SIGNABLE_FAILS = 1 << 8; // We can sign with this descriptor, but actually trying to sign will fail constexpr int MUSIG = 1 << 9; // This is a MuSig so key counts will have an extra key 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 } else { BOOST_CHECK_MESSAGE(EqualDescriptor(prv, prv1), "Private ser: " + prv1 + " Private desc: " + prv); } + BOOST_CHECK(!parse_priv->HavePrivateKeys(keys_pub)); + BOOST_CHECK(parse_pub->HavePrivateKeys(keys_priv)); + BOOST_CHECK(!parse_priv->ToPrivateString(keys_pub, prv1)); BOOST_CHECK(parse_pub->ToPrivateString(keys_priv, prv1)); if (expected_prv) { @@ -261,6 +264,12 @@ void DoCheck(std::string prv, std::string pub, const std::string& norm_pub, int parse_pub->ExpandPrivate(0, keys_priv, pub_prov); BOOST_CHECK_MESSAGE(EqualSigningProviders(priv_prov, pub_prov), "Private desc: " + prv + " Pub desc: " + pub); + } else if (keys_priv.keys.size() > 0) { + // If there is at least one private key, ToPrivateString() should return true and include that key + std::string prv_str; + BOOST_CHECK(parse_priv->ToPrivateString(keys_priv, prv_str)); + size_t checksum_len = 9; // Including the '#' character + BOOST_CHECK_MESSAGE(prv == prv_str.substr(0, prv_str.length() - checksum_len), prv); } // Check that private can produce the normalized descriptors diff --git a/src/test/fuzz/miniscript.cpp b/src/test/fuzz/miniscript.cpp index c6f8202d6bb..85797a1a6c7 100644 --- a/src/test/fuzz/miniscript.cpp +++ b/src/test/fuzz/miniscript.cpp @@ -125,10 +125,14 @@ struct ParserContext { return a < b; } - std::optional ToString(const Key& key) const + std::optional ToString(const Key& key, bool& has_priv_key) const { + has_priv_key = false; auto it = TEST_DATA.dummy_key_idx_map.find(key); - if (it == TEST_DATA.dummy_key_idx_map.end()) return {}; + if (it == TEST_DATA.dummy_key_idx_map.end()) { + return HexStr(key); + } + has_priv_key = true; uint8_t idx = it->second; return HexStr(std::span{&idx, 1}); } diff --git a/src/test/miniscript_tests.cpp b/src/test/miniscript_tests.cpp index 70563d514f1..5b7f58a0e72 100644 --- a/src/test/miniscript_tests.cpp +++ b/src/test/miniscript_tests.cpp @@ -188,7 +188,7 @@ struct KeyConverter { return g_testdata->pkmap.at(keyid); } - std::optional ToString(const Key& key) const { + std::optional ToString(const Key& key, bool&) const { return HexStr(ToPKBytes(key)); } diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp index 6217358c870..2aa1e9f71eb 100644 --- a/src/wallet/rpc/backup.cpp +++ b/src/wallet/rpc/backup.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -497,6 +498,9 @@ RPCHelpMan listdescriptors() if (!wallet) return UniValue::VNULL; const bool priv = !request.params[0].isNull() && request.params[0].get_bool(); + if (wallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && priv) { + throw JSONRPCError(RPC_WALLET_ERROR, "Can't get private descriptor string for watch-only wallets"); + } if (priv) { EnsureWalletIsUnlocked(*wallet); } @@ -523,9 +527,7 @@ RPCHelpMan listdescriptors() LOCK(desc_spk_man->cs_desc_man); const auto& wallet_descriptor = desc_spk_man->GetWalletDescriptor(); std::string descriptor; - if (!desc_spk_man->GetDescriptorString(descriptor, priv)) { - throw JSONRPCError(RPC_WALLET_ERROR, "Can't get descriptor string."); - } + CHECK_NONFATAL(desc_spk_man->GetDescriptorString(descriptor, priv)); const bool is_range = wallet_descriptor.descriptor->IsRange(); wallet_descriptors.push_back({ descriptor, diff --git a/src/wallet/scriptpubkeyman.cpp b/src/wallet/scriptpubkeyman.cpp index a6c254847ca..8a1c0a45a56 100644 --- a/src/wallet/scriptpubkeyman.cpp +++ b/src/wallet/scriptpubkeyman.cpp @@ -717,10 +717,9 @@ std::optional LegacyDataSPKM::MigrateToDescriptor() std::vector desc_spks; - // Make the descriptor string with private keys - std::string desc_str; - bool watchonly = !desc->ToPrivateString(*this, desc_str); - if (watchonly && !m_storage.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + // If we can't provide all private keys for this inferred descriptor, + // but this wallet is not watch-only, migrate it to the watch-only wallet. + if (!desc->HavePrivateKeys(*this) && !m_storage.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { out.watch_descs.emplace_back(desc->ToString(), creation_time); // Get the scriptPubKeys without writing this to the wallet diff --git a/src/wallet/test/walletload_tests.cpp b/src/wallet/test/walletload_tests.cpp index 0cc5a88cfdb..ec31cda3895 100644 --- a/src/wallet/test/walletload_tests.cpp +++ b/src/wallet/test/walletload_tests.cpp @@ -26,6 +26,7 @@ public: bool IsRange() const override { return false; } bool IsSolvable() const override { return false; } bool IsSingleType() const override { return true; } + bool HavePrivateKeys(const SigningProvider&) const override { return false; } bool ToPrivateString(const SigningProvider& provider, std::string& out) const override { return false; } bool ToNormalizedString(const SigningProvider& provider, std::string& out, const DescriptorCache* cache = nullptr) const override { return false; } bool Expand(int pos, const SigningProvider& provider, std::vector& output_scripts, FlatSigningProvider& out, DescriptorCache* write_cache = nullptr) const override { return false; }; diff --git a/test/functional/wallet_listdescriptors.py b/test/functional/wallet_listdescriptors.py index 107f2e59426..e7d33cc2dc7 100755 --- a/test/functional/wallet_listdescriptors.py +++ b/test/functional/wallet_listdescriptors.py @@ -107,7 +107,7 @@ class ListDescriptorsTest(BitcoinTestFramework): 'desc': descsum_create('wpkh(' + xpub_acc + ')'), 'timestamp': TIME_GENESIS_BLOCK, }]) - assert_raises_rpc_error(-4, 'Can\'t get descriptor string', watch_only_wallet.listdescriptors, True) + assert_raises_rpc_error(-4, 'Can\'t get private descriptor string for watch-only wallets', watch_only_wallet.listdescriptors, True) self.log.info('Test non-active non-range combo descriptor') node.createwallet(wallet_name='w4', blank=True) @@ -122,7 +122,7 @@ class ListDescriptorsTest(BitcoinTestFramework): {'active': False, 'desc': 'combo(0227d85ba011276cf25b51df6a188b75e604b38770a462b2d0e9fb2fc839ef5d3f)#np574htj', 'timestamp': TIME_GENESIS_BLOCK}, - ] + ], } assert_equal(expected, wallet.listdescriptors()) @@ -146,6 +146,36 @@ class ListDescriptorsTest(BitcoinTestFramework): } assert_equal(expected, wallet.listdescriptors()) + self.log.info('Test descriptor with missing private keys') + node.createwallet(wallet_name='w6', blank=True) + wallet = node.get_wallet_rpc('w6') + + expected_descs = { + descsum_create('tr(' + node.get_deterministic_priv_key().key + + ',{pk(03cdabb7f2dce7bfbd8a0b9570c6fd1e712e5d64045e9d6b517b3d5072251dc204)' + + ',pk([d34db33f/44h/0h/0h]tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0)})'), + descsum_create('wsh(and_v(v:ripemd160(095ff41131e5946f3c85f79e44adbcf8e27e080e),multi(1,' + node.get_deterministic_priv_key().key + + ',tpubD6NzVbkrYhZ4WaWSyoBvQwbpLkojyoTZPRsgXELWz3Popb3qkjcJyJUGLnL4qHHoQvao8ESaAstxYSnhyswJ76uZPStJRJCTKvosUCJZL5B/0)))'), + descsum_create('tr(03dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659,pk(musig(tprv8ZgxMBicQKsPeNLUGrbv3b7qhUk1LQJZAGMuk9gVuKh9sd4BWGp1eMsehUni6qGb8bjkdwBxCbgNGdh2bYGACK5C5dRTaif9KBKGVnSezxV,tpubD6NzVbkrYhZ4XcACN3PEwNjRpR1g4tZjBVk5pdMR2B6dbd3HYhdGVZNKofAiFZd9okBserZvv58A6tBX4pE64UpXGNTSesfUW7PpW36HuKz)/7/8/*))'), + descsum_create('tr(03dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659,pk(musig(tprv8ZgxMBicQKsPeNLUGrbv3b7qhUk1LQJZAGMuk9gVuKh9sd4BWGp1eMsehUni6qGb8bjkdwBxCbgNGdh2bYGACK5C5dRTaif9KBKGVnSezxV/10,tpubD6NzVbkrYhZ4XcACN3PEwNjRpR1g4tZjBVk5pdMR2B6dbd3HYhdGVZNKofAiFZd9okBserZvv58A6tBX4pE64UpXGNTSesfUW7PpW36HuKz/11)/*))'), + descsum_create('tr(03dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659,{pk(musig(tpubD6NzVbkrYhZ4Wo2WcFSgSqRD9QWkGxddo6WSqsVBx7uQ8QEtM7WncKDRjhFEexK119NigyCsFygA4b7sAPQxqebyFGAZ9XVV1BtcgNzbCRR,tprv8ZgxMBicQKsPen4PGtDwURYnCtVMDejyE8vVwMGhQWfVqB2FBPdekhTacDW4vmsKTsgC1wsncVqXiZdX2YFGAnKoLXYf42M78fQJFzuDYFN)/12/*),pk(musig(tprv8ZgxMBicQKsPeNLUGrbv3b7qhUk1LQJZAGMuk9gVuKh9sd4BWGp1eMsehUni6qGb8bjkdwBxCbgNGdh2bYGACK5C5dRTaif9KBKGVnSezxV,tpubD6NzVbkrYhZ4XWb6fGPjyhgLxapUhXszv7ehQYrQWDgDX4nYWcNcbgWcM2RhYo9s2mbZcfZJ8t5LzYcr24FK79zVybsw5Qj3Rtqug8jpJMy)/13/*)})'), + descsum_create('tr(03dff1d77f2a671c5f36183726db2341be58feae1da2deced843240f7b502ba659,{pk(musig(tpubD6NzVbkrYhZ4Wo2WcFSgSqRD9QWkGxddo6WSqsVBx7uQ8QEtM7WncKDRjhFEexK119NigyCsFygA4b7sAPQxqebyFGAZ9XVV1BtcgNzbCRR,tpubD6NzVbkrYhZ4Wc3i6L6N1Pp7cyVeyMcdLrFGXGDGzCfdCa5F4Zs3EY46N72Ws8QDEUYBVwXfDfda2UKSseSdU1fsBegJBhGCZyxkf28bkQ6)/12/*),pk(musig(tprv8ZgxMBicQKsPeNLUGrbv3b7qhUk1LQJZAGMuk9gVuKh9sd4BWGp1eMsehUni6qGb8bjkdwBxCbgNGdh2bYGACK5C5dRTaif9KBKGVnSezxV,tpubD6NzVbkrYhZ4XWb6fGPjyhgLxapUhXszv7ehQYrQWDgDX4nYWcNcbgWcM2RhYo9s2mbZcfZJ8t5LzYcr24FK79zVybsw5Qj3Rtqug8jpJMy)/13/*)})') + } + + descs_to_import = [] + for desc in expected_descs: + descs_to_import.append({'desc': desc, 'timestamp': TIME_GENESIS_BLOCK}) + + wallet.importdescriptors(descs_to_import) + result = wallet.listdescriptors(True) + actual_descs = [d['desc'] for d in result['descriptors']] + + assert_equal(len(actual_descs), len(expected_descs)) + for desc in actual_descs: + if desc not in expected_descs: + raise AssertionError(f"{desc} missing") + + if __name__ == '__main__': ListDescriptorsTest(__file__).main()