mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-06-11 15:19:19 +02:00
Merge bitcoin/bitcoin#29136: wallet: addhdkey RPC to add just keys to wallets via new unused(KEY) descriptor
a39cc16b43doc: Release note for addhdkey (Ava Chow)89b9a01b4ewallet, rpc: Disallow importing unused() to wallets without privkeys (Ava Chow)35bbee6374wallet, rpc: Disallow import of unused() if key already exists (Ava Chow)f3f8bcbd1dwallet: Add addhdkey RPC (Ava Chow)82bc280de4test: Simple test for importing unused(KEY) (Ava Chow)80c29bc6f1descriptor: Add unused(KEY) descriptor (Ava Chow) Pull request description: It is sometimes useful for the wallet to have keys that it can sign with but are not (initially) involved in any scripts, e.g. for setting up a multisig. Ryanofsky [suggested](https://github.com/bitcoin/bitcoin/pull/26728#issuecomment-1867721948) A `unused(KEY)` descriptor which allows for a key to be specified, but produces no scripts. These can be imported into the wallet, and subsequently retrieved with `gethdkeys`. Additionally, `listdescriptors` will output these descriptors so that they can be easily backed up. In order to make it easier for people to add HD keys to their wallet, and to generate a new one if they want to rotate their descriptors, an `addhdkey` RPC is also added. Without arguments, it will generate a new HD key and add it to the wallet via a `unused(KEY)` descriptor. If provided a private key, it will construct the descriptor and add it to the wallet. See also: https://github.com/bitcoin/bitcoin/pull/26728#issuecomment-1866961865 Based on #29130 as `gethdkeys` is useful for testing this. ACKs for top commit: Sjors: utACKa39cc16rkrux: lgtm ACKa39cc16b43Tree-SHA512: c1288c792ab01ca2eaddd24b0e7d11c259cd59e79042465d0d1eb656fd559c1200dc19750b4d84acc762b5b599935a06df214c18226e662087842ea91ec3011b
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -65,6 +65,59 @@ class ImportDescriptorsTest(BitcoinTestFramework):
|
||||
assert_equal(result[0]['error']['code'], error_code)
|
||||
assert_equal(result[0]['error']['message'], error_message)
|
||||
|
||||
def test_import_unused_key(self):
|
||||
self.log.info("Test import of unused(KEY)")
|
||||
self.nodes[0].createwallet(wallet_name="import_unused", blank=True)
|
||||
wallet = self.nodes[0].get_wallet_rpc("import_unused")
|
||||
|
||||
assert_equal(len(wallet.gethdkeys()), 0)
|
||||
|
||||
xprv = "tprv8ZgxMBicQKsPeuVhWwi6wuMQGfPKi9Li5GtX35jVNknACgqe3CY4g5xgkfDDJcmtF7o1QnxWDRYw4H5P26PXq7sbcUkEqeR4fg3Kxp2tigg"
|
||||
xpub = "tpubD6NzVbkrYhZ4YNXVQbNhMK1WqguFsUXceaVJKbmno2aZ3B6QfbMeraaYvnBSGpV3vxLyTTK9DYT1yoEck4XUScMzXoQ2U2oSmE2JyMedq3H"
|
||||
self.test_importdesc({"desc":descsum_create(f"unused({xpub})"),
|
||||
"timestamp": "now"},
|
||||
success=False,
|
||||
error_code=-4,
|
||||
error_message='Cannot import descriptor without private keys to a wallet with private keys enabled',
|
||||
wallet=wallet)
|
||||
self.test_importdesc({"timestamp": "now", "desc": descsum_create(f"unused({xprv})")},
|
||||
success=True,
|
||||
wallet=wallet)
|
||||
hdkeys = wallet.gethdkeys()
|
||||
assert_equal(len(hdkeys), 1)
|
||||
assert_equal(hdkeys[0]["xpub"], xpub)
|
||||
wallet.unloadwallet()
|
||||
|
||||
def test_import_unused_key_existing(self):
|
||||
self.log.info("Test import of unused(KEY) with existing KEY")
|
||||
self.nodes[0].createwallet(wallet_name="import_existing_unused")
|
||||
wallet = self.nodes[0].get_wallet_rpc("import_existing_unused")
|
||||
|
||||
hdkeys = wallet.gethdkeys(private=True)
|
||||
assert_equal(len(hdkeys), 1)
|
||||
xprv = hdkeys[0]["xprv"]
|
||||
|
||||
self.test_importdesc({"timestamp": "now", "desc": descsum_create(f"unused({xprv})")},
|
||||
success=False,
|
||||
error_code=-4,
|
||||
error_message="Cannot import an unused() descriptor when its private key is already in the wallet",
|
||||
wallet=wallet)
|
||||
wallet.unloadwallet()
|
||||
|
||||
def test_import_unused_noprivs(self):
|
||||
self.log.info("Test import of unused(KEY) to wallet without privkeys")
|
||||
self.nodes[0].createwallet(wallet_name="import_unused_noprivs", disable_private_keys=True)
|
||||
wallet = self.nodes[0].get_wallet_rpc("import_unused_noprivs")
|
||||
|
||||
xpub = "tpubD6NzVbkrYhZ4YNXVQbNhMK1WqguFsUXceaVJKbmno2aZ3B6QfbMeraaYvnBSGpV3vxLyTTK9DYT1yoEck4XUScMzXoQ2U2oSmE2JyMedq3H"
|
||||
self.test_importdesc({"timestamp": "now", "desc": descsum_create(f"unused({xpub})")},
|
||||
success=False,
|
||||
error_code=-4,
|
||||
error_message="Cannot import unused() to wallet without private keys enabled",
|
||||
wallet=wallet)
|
||||
wallet.unloadwallet()
|
||||
|
||||
|
||||
def run_test(self):
|
||||
self.log.info('Setting up wallets')
|
||||
self.nodes[0].createwallet(wallet_name='w0', disable_private_keys=False)
|
||||
@@ -822,5 +875,9 @@ class ImportDescriptorsTest(BitcoinTestFramework):
|
||||
)
|
||||
|
||||
|
||||
self.test_import_unused_key()
|
||||
self.test_import_unused_key_existing()
|
||||
self.test_import_unused_noprivs()
|
||||
|
||||
if __name__ == '__main__':
|
||||
ImportDescriptorsTest(__file__).main()
|
||||
|
||||
Reference in New Issue
Block a user