From 6e3a0afc2fac3b7a5bb3324c8f8667acfb6af3b3 Mon Sep 17 00:00:00 2001 From: rkrux Date: Fri, 23 Jan 2026 14:21:21 +0530 Subject: [PATCH 1/2] wallet: fix `gethdkeys` RPC for descriptors with partial xprvs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A non-watch-only wallet allows to import descriptors with partial private keys, eg: a multisig descriptor with one private key and one public key. In case an xpub is imported in any such descriptors whose private key the wallet doesn't have, then the `gethdkeys` RPC throws an unhandled error like below when the private keys are requested. This fix ensures that such calls are properly handled by conditionally finding the corresponding xprv. Some related documentation of this RPC is also updated. ``` ➜ bitcoincli -named gethdkeys private=true error code: -1 error message: map::at: key not found ``` --- src/wallet/rpc/wallet.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wallet/rpc/wallet.cpp b/src/wallet/rpc/wallet.cpp index 49808a8d696..f15ba83bbb3 100644 --- a/src/wallet/rpc/wallet.cpp +++ b/src/wallet/rpc/wallet.cpp @@ -660,7 +660,7 @@ RPCHelpMan gethdkeys() {RPCResult::Type::ARR, "descriptors", "Array of descriptor objects that use this HD key", { {RPCResult::Type::OBJ, "", "", { - {RPCResult::Type::STR, "desc", "Descriptor string representation"}, + {RPCResult::Type::STR, "desc", "Descriptor string public representation"}, {RPCResult::Type::BOOL, "active", "Whether this descriptor is currently used to generate new addresses"}, }}, }}, @@ -707,7 +707,7 @@ RPCHelpMan gethdkeys() w_desc.descriptor->GetPubKeys(desc_pubkeys, desc_xpubs); for (const CExtPubKey& xpub : desc_xpubs) { std::string desc_str; - bool ok = desc_spkm->GetDescriptorString(desc_str, false); + bool ok = desc_spkm->GetDescriptorString(desc_str, /*priv=*/false); CHECK_NONFATAL(ok); wallet_xpubs[xpub].emplace(desc_str, wallet->IsActiveScriptPubKeyMan(*spkm), desc_spkm->HasPrivKey(xpub.pubkey.GetID())); if (std::optional key = priv ? desc_spkm->GetKey(xpub.pubkey.GetID()) : std::nullopt) { @@ -731,7 +731,7 @@ RPCHelpMan gethdkeys() UniValue xpub_info(UniValue::VOBJ); xpub_info.pushKV("xpub", EncodeExtPubKey(xpub)); xpub_info.pushKV("has_private", has_xprv); - if (priv) { + if (priv && has_xprv) { xpub_info.pushKV("xprv", EncodeExtKey(wallet_xprvs.at(xpub))); } xpub_info.pushKV("descriptors", std::move(descriptors)); From 43c528aba925dab2565d069285110238ef63458d Mon Sep 17 00:00:00 2001 From: rkrux Date: Thu, 22 Jan 2026 17:18:26 +0530 Subject: [PATCH 2/2] wallet, test: update `gethdkeys` functional test Update the `test_ranged_multisig` test case to verify the partial xprv fix in the `gethdkeys` RPC. Also, update some existing variable names. --- test/functional/wallet_gethdkeys.py | 50 +++++++++++++++++++---------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/test/functional/wallet_gethdkeys.py b/test/functional/wallet_gethdkeys.py index 9325627917a..d25ed4ed24a 100755 --- a/test/functional/wallet_gethdkeys.py +++ b/test/functional/wallet_gethdkeys.py @@ -10,6 +10,7 @@ from test_framework.util import ( assert_equal, assert_raises_rpc_error, assert_not_equal, + assert_greater_than, ) from test_framework.wallet_util import WalletUnlock @@ -130,31 +131,46 @@ class WalletGetHDKeyTest(BitcoinTestFramework): def test_ranged_multisig(self): self.log.info("HD keys of a multisig appear in gethdkeys") + def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name) + outside_wallet_xpub = def_wallet.gethdkeys()[0]["xpub"] + self.nodes[0].createwallet("ranged_multisig") wallet = self.nodes[0].get_wallet_rpc("ranged_multisig") - xpub1 = wallet.gethdkeys()[0]["xpub"] - xprv1 = wallet.gethdkeys(private=True)[0]["xprv"] - xpub2 = def_wallet.gethdkeys()[0]["xpub"] + hdkeys_info = wallet.gethdkeys(private=True) + assert_equal(len(hdkeys_info), 1) + within_wallet_xprv = hdkeys_info[0]["xprv"] + within_wallet_xpub = hdkeys_info[0]["xpub"] - prv_multi_desc = descsum_create(f"wsh(multi(2,{xprv1}/*,{xpub2}/*))") - pub_multi_desc = descsum_create(f"wsh(multi(2,{xpub1}/*,{xpub2}/*))") + prv_multi_desc = descsum_create(f"wsh(multi(2,{within_wallet_xprv}/*,{outside_wallet_xpub}/*))") + pub_multi_desc = descsum_create(f"wsh(multi(2,{within_wallet_xpub}/*,{outside_wallet_xpub}/*))") assert_equal(wallet.importdescriptors([{"desc": prv_multi_desc, "timestamp": "now"}])[0]["success"], True) - xpub_info = wallet.gethdkeys() - assert_equal(len(xpub_info), 2) - for x in xpub_info: - if x["xpub"] == xpub1: - found_desc = next((d for d in xpub_info[0]["descriptors"] if d["desc"] == pub_multi_desc), None) - assert found_desc is not None + rpcs_req_resp = [[False, wallet.gethdkeys()], [True, wallet.gethdkeys(private=True)]] + for rpc_req_resp in rpcs_req_resp: + requested_private, hdkeys_response = rpc_req_resp + assert_equal(len(hdkeys_response), 2) + + for hdkeys_info in hdkeys_response: + if hdkeys_info["xpub"] == within_wallet_xpub: + assert_equal(hdkeys_info["has_private"], True) + if requested_private: + assert_equal(hdkeys_info["xprv"], within_wallet_xprv) + else: + assert_equal("xprv" not in hdkeys_info, True) + assert_greater_than(len(hdkeys_info["descriptors"]), 1) # within wallet xpub by default is part of multiple descriptors + found_desc = next((d for d in hdkeys_info["descriptors"] if d["desc"] == pub_multi_desc), None) + elif hdkeys_info["xpub"] == outside_wallet_xpub: + assert_equal(hdkeys_info["has_private"], False) + assert_equal("xprv" not in hdkeys_info, True) + assert_equal(len(hdkeys_info["descriptors"]), 1) # outside wallet xpub is part of only the imported descriptor + found_desc = hdkeys_info["descriptors"][0] + else: + assert False + + assert_equal(found_desc["desc"], pub_multi_desc) assert_equal(found_desc["active"], False) - elif x["xpub"] == xpub2: - assert_equal(len(x["descriptors"]), 1) - assert_equal(x["descriptors"][0]["desc"], pub_multi_desc) - assert_equal(x["descriptors"][0]["active"], False) - else: - assert False def test_mixed_multisig(self): self.log.info("Non-HD keys of a multisig do not appear in gethdkeys")