From f3f8bcbd1df3d66d640885a47fa12308d7b1485c Mon Sep 17 00:00:00 2001 From: Ava Chow Date: Fri, 22 Dec 2023 17:07:18 -0500 Subject: [PATCH] wallet: Add addhdkey RPC --- src/wallet/rpc/wallet.cpp | 77 ++++++++++++++++++++++++++++++++++++ test/functional/wallet_hd.py | 54 +++++++++++++++++++++++++ 2 files changed, 131 insertions(+) diff --git a/src/wallet/rpc/wallet.cpp b/src/wallet/rpc/wallet.cpp index b6c9179d19d..8aa15c7ec5c 100644 --- a/src/wallet/rpc/wallet.cpp +++ b/src/wallet/rpc/wallet.cpp @@ -840,6 +840,82 @@ static RPCMethod createwalletdescriptor() }; } +RPCMethod addhdkey() +{ + return RPCMethod{ + "addhdkey", + "Add a BIP 32 HD key to the wallet that can be used with 'createwalletdescriptor'\n", + { + {"hdkey", RPCArg::Type::STR, RPCArg::DefaultHint{"Automatically generated new key"}, "The BIP 32 extended private key to add. If none is provided, a randomly generated one will be added."}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR, "xpub", "The xpub of the HD key that was added to the wallet"} + }, + }, + RPCExamples{ + HelpExampleCli("addhdkey", "xprv") + HelpExampleRpc("addhdkey", "xprv") + }, + [&](const RPCMethod& self, const JSONRPCRequest& request) -> UniValue + { + std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); + if (!wallet) return UniValue::VNULL; + + if (wallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS)) { + throw JSONRPCError(RPC_WALLET_ERROR, "addhdkey is not available for wallets without private keys"); + } + + EnsureWalletIsUnlocked(*wallet); + + CExtKey hdkey; + if (request.params[0].isNull()) { + CKey seed_key = GenerateRandomKey(); + hdkey.SetSeed(seed_key); + } else { + hdkey = DecodeExtKey(request.params[0].get_str()); + if (!hdkey.key.IsValid()) { + // Check if the user gave us an xpub and give a more descriptive error if so + CExtPubKey xpub = DecodeExtPubKey(request.params[0].get_str()); + if (xpub.pubkey.IsValid()) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Extended public key (xpub) provided, but extended private key (xprv) is required"); + } else { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Could not parse HD key"); + } + } + } + + LOCK(wallet->cs_wallet); + std::string desc_str = "unused(" + EncodeExtKey(hdkey) + ")"; + FlatSigningProvider keys; + std::string error; + std::vector> descs = Parse(desc_str, keys, error, false); + CHECK_NONFATAL(!descs.empty()); + WalletDescriptor w_desc(std::move(descs.at(0)), GetTime(), 0, 0, 0); + if (wallet->GetDescriptorScriptPubKeyMan(w_desc) != nullptr) { + throw JSONRPCError(RPC_WALLET_ERROR, "HD key already exists"); + } + + auto spkm = wallet->AddWalletDescriptor(w_desc, keys, /*label=*/"", /*internal=*/false); + if (!spkm) { + throw JSONRPCError(RPC_WALLET_ERROR, util::ErrorString(spkm).original); + } + + UniValue response(UniValue::VOBJ); + const DescriptorScriptPubKeyMan& desc_spkm = spkm->get(); + LOCK(desc_spkm.cs_desc_man); + std::set pubkeys; + std::set extpubs; + desc_spkm.GetWalletDescriptor().descriptor->GetPubKeys(pubkeys, extpubs); + CHECK_NONFATAL(pubkeys.size() == 0); + CHECK_NONFATAL(extpubs.size() == 1); + response.pushKV("xpub", EncodeExtPubKey(*extpubs.begin())); + + return response; + }, + }; +} + // addresses RPCMethod getaddressinfo(); RPCMethod getnewaddress(); @@ -907,6 +983,7 @@ std::span GetWalletRPCCommands() {"rawtransactions", &fundrawtransaction}, {"wallet", &abandontransaction}, {"wallet", &abortrescan}, + {"wallet", &addhdkey}, {"wallet", &backupwallet}, {"wallet", &bumpfee}, {"wallet", &psbtbumpfee}, diff --git a/test/functional/wallet_hd.py b/test/functional/wallet_hd.py index c130c731ce5..e5227e6f496 100755 --- a/test/functional/wallet_hd.py +++ b/test/functional/wallet_hd.py @@ -7,10 +7,12 @@ import shutil from test_framework.blocktools import COINBASE_MATURITY +from test_framework.descriptors import descsum_create from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, wallet_importprivkey, + assert_raises_rpc_error, ) @@ -25,6 +27,56 @@ class WalletHDTest(BitcoinTestFramework): def skip_test_if_missing_module(self): self.skip_if_no_wallet() + def test_addhdkey(self): + self.log.info("Test addhdkey") + def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name) + self.nodes[0].createwallet("hdkey") + wallet = self.nodes[0].get_wallet_rpc("hdkey") + + assert_equal(len(wallet.gethdkeys()), 1) + + wallet.addhdkey() + xpub_info = wallet.gethdkeys() + assert_equal(len(xpub_info), 2) + for x in xpub_info: + if len(x["descriptors"]) == 1 and x["descriptors"][0]["desc"].startswith("unused("): + break + else: + assert False, "Did not find HD key with no descriptors" + + imp_xpub_info = def_wallet.gethdkeys(private=True)[0] + imp_xpub = imp_xpub_info["xpub"] + imp_xprv = imp_xpub_info["xprv"] + + assert_raises_rpc_error(-5, "Extended public key (xpub) provided, but extended private key (xprv) is required", wallet.addhdkey, imp_xpub) + add_res = wallet.addhdkey(imp_xprv) + expected_unused_desc = descsum_create(f"unused({imp_xpub})") + assert_equal(add_res["xpub"], imp_xpub) + xpub_info = wallet.gethdkeys() + assert_equal(len(xpub_info), 3) + for x in xpub_info: + if x["xpub"] == imp_xpub: + assert_equal(len(x["descriptors"]), 1) + assert_equal(x["descriptors"][0]["desc"], expected_unused_desc) + break + else: + assert False, "Added HD key was not found in wallet" + + for d in wallet.listdescriptors()["descriptors"]: + if d["desc"] == expected_unused_desc: + assert_equal(d["active"], False) + break + else: + assert False, "Added HD key's descriptor was not found in wallet" + + assert_raises_rpc_error(-4, "HD key already exists", wallet.addhdkey, imp_xprv) + + def test_addhdkey_noprivs(self): + self.log.info("Test addhdkey is not available for wallets without privkeys") + self.nodes[0].createwallet("hdkey_noprivs", disable_private_keys=True) + wallet = self.nodes[0].get_wallet_rpc("hdkey_noprivs") + assert_raises_rpc_error(-4, "addhdkey is not available for wallets without private keys", wallet.addhdkey) + def run_test(self): # Make sure we use hd, keep masterkeyid hd_fingerprint = self.nodes[1].getaddressinfo(self.nodes[1].getnewaddress())['hdmasterfingerprint'] @@ -124,6 +176,8 @@ class WalletHDTest(BitcoinTestFramework): assert_equal(keypath[0:14], "m/84h/1h/0h/1/") + self.test_addhdkey() + self.test_addhdkey_noprivs() if __name__ == '__main__': WalletHDTest(__file__).main()