mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-09-11 21:20:39 +02:00
Add an UnusedKey filter to GetHDPubKeys() so the new RPC can prefer unused(KEY) descriptors before falling back to active descriptors. Co-authored-by: w0xlt <94266259+w0xlt@users.noreply.github.com>
236 lines
9.1 KiB
Python
Executable File
236 lines
9.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# Copyright (c) 2025-Present The Bitcoin Core developers
|
|
# Distributed under the MIT software license, see the accompanying
|
|
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
"""Test wallet derivehdkey RPC."""
|
|
|
|
from test_framework.test_framework import BitcoinTestFramework
|
|
from test_framework.util import (
|
|
assert_equal,
|
|
assert_raises_rpc_error,
|
|
)
|
|
from test_framework.wallet_util import WalletUnlock
|
|
|
|
|
|
class WalletDeriveHDKeyTest(BitcoinTestFramework):
|
|
def set_test_params(self):
|
|
self.setup_clean_chain = True
|
|
self.num_nodes = 2
|
|
|
|
def skip_test_if_missing_module(self):
|
|
self.skip_if_no_wallet()
|
|
|
|
def run_test(self):
|
|
self.test_basic_derivehdkey()
|
|
self.test_multiple_unused_keys()
|
|
self.test_active_descriptor()
|
|
self.test_inactive_descriptor()
|
|
self.test_noprivs_blank()
|
|
self.test_compare()
|
|
|
|
def test_basic_derivehdkey(self):
|
|
self.log.info("Test derivehdkey basics")
|
|
self.nodes[0].createwallet("basic", blank=True)
|
|
wallet = self.nodes[0].get_wallet_rpc("basic")
|
|
assert_raises_rpc_error(
|
|
-8,
|
|
"Derivation path requires at least one hardened step",
|
|
wallet.derivehdkey,
|
|
"m",
|
|
)
|
|
assert_raises_rpc_error(
|
|
-8,
|
|
"Derivation path requires at least one hardened step",
|
|
wallet.derivehdkey,
|
|
"m/87/0/0",
|
|
)
|
|
# A BIP32 keypath element must be in 0..2^31-1. The
|
|
# value 2^31 (2147483648 == 0x80000000) is out of range.
|
|
for path in ["m/2147483648", "m/2147483648h", "m/2147483648'"]:
|
|
assert_raises_rpc_error(
|
|
-8,
|
|
"Invalid BIP32 keypath",
|
|
wallet.derivehdkey,
|
|
path,
|
|
)
|
|
assert_raises_rpc_error(
|
|
-5,
|
|
"No active or unused(KEY) descriptor found",
|
|
wallet.derivehdkey,
|
|
"m/87h",
|
|
)
|
|
wallet.addhdkey()
|
|
xpub_info = wallet.derivehdkey("m/87h")
|
|
assert "xprv" not in xpub_info
|
|
xpub = xpub_info["xpub"]
|
|
root_fingerprint = wallet.derivehdkey("m/87h/0h")["origin"][1:9]
|
|
assert_equal(xpub_info["origin"], f"[{root_fingerprint}/87h]")
|
|
too_deep_path = "m/" + "/".join(["0h"] * 256)
|
|
assert_raises_rpc_error(
|
|
-8,
|
|
"Unable to derive HD key at the requested path",
|
|
wallet.derivehdkey,
|
|
too_deep_path,
|
|
)
|
|
|
|
xpub_info = wallet.derivehdkey("m/87h/0h/0h/0")
|
|
xpub_priv_info = wallet.derivehdkey("m/87h/0h/0h/0", private=True)
|
|
xprv = xpub_priv_info["xprv"]
|
|
assert_equal(xpub_priv_info["xpub"], xpub_info["xpub"])
|
|
|
|
self.log.info("HD pubkey can be retrieved from encrypted wallets")
|
|
prev_xprv = xprv
|
|
wallet.encryptwallet("pass")
|
|
assert_raises_rpc_error(
|
|
-13,
|
|
"Error: Please enter the wallet passphrase with walletpassphrase first",
|
|
wallet.derivehdkey,
|
|
"m/87h",
|
|
)
|
|
with WalletUnlock(wallet, "pass"):
|
|
xpub_info = wallet.derivehdkey("m/87h")
|
|
# Only automatically generated descriptors are rotated on
|
|
# encryption, unused(KEY) is not.
|
|
assert_equal(xpub_info["xpub"], xpub)
|
|
assert "xprv" not in xpub_info
|
|
|
|
self.log.info("HD privkey can be retrieved from encrypted wallets")
|
|
assert_raises_rpc_error(
|
|
-13,
|
|
"Error: Please enter the wallet passphrase with walletpassphrase first",
|
|
wallet.derivehdkey,
|
|
"m/87h/0h/0h/0",
|
|
private=True,
|
|
)
|
|
with WalletUnlock(wallet, "pass"):
|
|
xpub_info = wallet.derivehdkey("m/87h/0h/0h/0", private=True)
|
|
# Unused(KEY) is preferred over active descriptors and is not
|
|
# rotated on encryption.
|
|
assert_equal(xpub_info["xprv"], prev_xprv)
|
|
|
|
def test_multiple_unused_keys(self):
|
|
self.log.info("Test multiple unused(KEY) descriptors")
|
|
self.nodes[0].createwallet("multiple_unused_keys", blank=True)
|
|
wallet = self.nodes[0].get_wallet_rpc("multiple_unused_keys")
|
|
master_xpub_1 = wallet.addhdkey()['xpub']
|
|
master_xpub_2 = wallet.addhdkey()['xpub']
|
|
assert_raises_rpc_error(
|
|
-5,
|
|
"Unable to determine which HD key to use. Please specify with 'hdkey'",
|
|
wallet.derivehdkey,
|
|
"m/87h",
|
|
)
|
|
xpub_info_1 = wallet.derivehdkey("m/87h", hdkey=master_xpub_1)
|
|
xpub_info_2 = wallet.derivehdkey("m/87h", hdkey=master_xpub_2)
|
|
assert xpub_info_1["xpub"] != xpub_info_2["xpub"]
|
|
|
|
assert_raises_rpc_error(
|
|
-5,
|
|
"Unable to parse HD key. Please provide a valid xpub",
|
|
wallet.derivehdkey,
|
|
"m/87h",
|
|
hdkey="bad",
|
|
)
|
|
|
|
def test_active_descriptor(self):
|
|
self.log.info("Test derivation in wallet with regular descriptors")
|
|
self.nodes[0].createwallet("active_descriptor")
|
|
wallet = self.nodes[0].get_wallet_rpc("active_descriptor")
|
|
xpub_info = wallet.derivehdkey("m/44h/1h/0h")
|
|
active_xpub = wallet.gethdkeys(active_only=True)[0]["xpub"]
|
|
assert_equal(wallet.derivehdkey("m/44h/1h/0h", hdkey=active_xpub), xpub_info)
|
|
|
|
# Get the activate wpkh() receive descriptor
|
|
desc = list(filter(lambda d:
|
|
d["active"] and not d["internal"] and d["desc"][0:3] == "pkh",
|
|
wallet.listdescriptors()["descriptors"])
|
|
)[0]["desc"]
|
|
self.log.debug(desc)
|
|
|
|
assert(xpub_info["origin"] in desc)
|
|
assert(xpub_info["xpub"] in desc)
|
|
|
|
self.log.info("Test unused(KEY) descriptors are preferred over active descriptors")
|
|
master_xpub = wallet.addhdkey()["xpub"]
|
|
assert_equal(
|
|
wallet.derivehdkey("m/87h", hdkey=master_xpub)["xpub"],
|
|
wallet.derivehdkey("m/87h")["xpub"],
|
|
)
|
|
|
|
def test_inactive_descriptor(self):
|
|
self.log.info("Test that the HD key of a used inactive descriptor is rejected")
|
|
# Mint a spendable descriptor in a throwaway wallet, so we can import it
|
|
# into the wallet under test and deactivate it there.
|
|
self.nodes[0].createwallet(wallet_name="hdkey_source")
|
|
source = self.nodes[0].get_wallet_rpc("hdkey_source")
|
|
desc = next(d["desc"] for d in source.listdescriptors(private=True)["descriptors"]
|
|
if d["active"] and not d["internal"] and d["desc"].startswith("pkh("))
|
|
|
|
self.nodes[0].createwallet(wallet_name="inactive_descriptor", blank=True)
|
|
wallet = self.nodes[0].get_wallet_rpc("inactive_descriptor")
|
|
request = {"desc": desc, "range": [0, 9], "timestamp": "now", "internal": False}
|
|
assert_equal(wallet.importdescriptors([{**request, "active": True}])[0]["success"], True)
|
|
|
|
# While the descriptor is active its HD key can be used for derivation.
|
|
xpub = wallet.gethdkeys(active_only=True)[0]["xpub"]
|
|
wallet.derivehdkey("m/87h", hdkey=xpub)
|
|
|
|
# Once deactivated the descriptor is neither active nor unused(KEY), so
|
|
# its HD key is no longer a candidate.
|
|
assert_equal(wallet.importdescriptors([{**request, "active": False}])[0]["success"], True)
|
|
assert_raises_rpc_error(
|
|
-5,
|
|
"HD key is not used by an active or unused(KEY) descriptor",
|
|
wallet.derivehdkey,
|
|
"m/87h",
|
|
hdkey=xpub,
|
|
)
|
|
|
|
def test_noprivs_blank(self):
|
|
self.log.info("Test derivehdkey on wallet without private keys")
|
|
self.nodes[0].createwallet(wallet_name="noprivs", disable_private_keys=True)
|
|
wallet = self.nodes[0].get_wallet_rpc("noprivs")
|
|
assert_raises_rpc_error(
|
|
-4,
|
|
"derivehdkey is not available for watch-only wallets",
|
|
wallet.derivehdkey,
|
|
"m/87h",
|
|
)
|
|
|
|
self.log.info("Test derivehdkey on blank wallet")
|
|
self.nodes[0].createwallet(wallet_name="blank", blank=True)
|
|
wallet = self.nodes[0].get_wallet_rpc("blank")
|
|
assert_raises_rpc_error(
|
|
-5,
|
|
"No active or unused(KEY) descriptor found",
|
|
wallet.derivehdkey,
|
|
"m/87h",
|
|
)
|
|
|
|
def test_compare(self):
|
|
self.log.info(
|
|
"Compare with result of createwalletdescriptor")
|
|
self.nodes[0].createwallet(wallet_name="w1", blank=True)
|
|
wallet = self.nodes[0].get_wallet_rpc("w1")
|
|
master_xpub = wallet.addhdkey()["xpub"]
|
|
|
|
# Derive xpub for legacy descriptor
|
|
xpub_info = wallet.derivehdkey("m/44h/1h/0h")
|
|
|
|
# Generate legacy descriptor
|
|
wallet.createwalletdescriptor(type="legacy", hdkey=master_xpub)
|
|
|
|
# Get the activate wpkh() receive descriptor
|
|
desc = list(filter(lambda d:
|
|
d["active"] and not d["internal"] and d["desc"][0:3] == "pkh",
|
|
wallet.listdescriptors()["descriptors"])
|
|
)[0]["desc"]
|
|
self.log.debug(desc)
|
|
|
|
assert(xpub_info["origin"] in desc)
|
|
assert(xpub_info["xpub"] in desc)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
WalletDeriveHDKeyTest(__file__).main()
|