Merge bitcoin/bitcoin#34379: wallet: fix gethdkeys RPC for descriptors with partial xprvs

43c528aba9 wallet, test: update `gethdkeys` functional test (rkrux)
6e3a0afc2f wallet: fix `gethdkeys` RPC for descriptors with partial xprvs (rkrux)

Pull request description:

  Fixes #34378

  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 and the related functional test is accordingly updated.

  ```
  ➜ bitcoincli -named gethdkeys private=true
  error code: -1
  error message:
  map::at:  key not found
  ```

ACKs for top commit:
  achow101:
    ACK 43c528aba9
  w0xlt:
    ACK 43c528aba9
  Eunovo:
    Tested ACK 43c528aba9

Tree-SHA512: 106e02ee368a3fa94d116f54f2fa71f9886e4753e331b91ce13a346559d5497ef6cd7ddaa8736fcfe1a7ac427a44d647fb75e523eb72f917fa866adbe0dbad30
This commit is contained in:
Ava Chow
2026-04-01 14:32:49 -07:00
2 changed files with 36 additions and 20 deletions

View File

@@ -658,7 +658,7 @@ RPCMethod 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"},
}},
}},
@@ -705,7 +705,7 @@ RPCMethod 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<CKey> key = priv ? desc_spkm->GetKey(xpub.pubkey.GetID()) : std::nullopt) {
@@ -729,7 +729,7 @@ RPCMethod 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));

View File

@@ -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")