Skip to content

Commit 3b09d0e

Browse files
committed
tests: Test for gethdkeys
1 parent 5febe28 commit 3b09d0e

File tree

2 files changed

+186
-0
lines changed

2 files changed

+186
-0
lines changed

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@
181181
'wallet_keypool_topup.py --legacy-wallet',
182182
'wallet_keypool_topup.py --descriptors',
183183
'wallet_fast_rescan.py --descriptors',
184+
'wallet_gethdkeys.py --descriptors',
184185
'interface_zmq.py',
185186
'rpc_invalid_address_message.py',
186187
'rpc_validateaddress.py',

test/functional/wallet_gethdkeys.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2023 The Bitcoin Core developers
3+
# Distributed under the MIT software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
"""Test wallet gethdkeys RPC."""
6+
7+
from test_framework.descriptors import descsum_create
8+
from test_framework.test_framework import BitcoinTestFramework
9+
from test_framework.util import (
10+
assert_equal,
11+
assert_raises_rpc_error,
12+
)
13+
from test_framework.wallet_util import WalletUnlock
14+
15+
16+
class WalletGetHDKeyTest(BitcoinTestFramework):
17+
def add_options(self, parser):
18+
self.add_wallet_options(parser, descriptors=True, legacy=False)
19+
20+
def set_test_params(self):
21+
self.setup_clean_chain = True
22+
self.num_nodes = 1
23+
24+
def skip_test_if_missing_module(self):
25+
self.skip_if_no_wallet()
26+
27+
def run_test(self):
28+
self.test_basic_gethdkeys()
29+
self.test_ranged_imports()
30+
self.test_lone_key_imports()
31+
self.test_ranged_multisig()
32+
self.test_mixed_multisig()
33+
34+
def test_basic_gethdkeys(self):
35+
self.log.info("Test gethdkeys basics")
36+
self.nodes[0].createwallet("basic")
37+
wallet = self.nodes[0].get_wallet_rpc("basic")
38+
xpub_info = wallet.gethdkeys()
39+
assert_equal(len(xpub_info), 1)
40+
assert_equal(xpub_info[0]["has_private"], True)
41+
42+
assert "xprv" not in xpub_info[0]
43+
xpub = xpub_info[0]["xpub"]
44+
45+
xpub_info = wallet.gethdkeys(private=True)
46+
xprv = xpub_info[0]["xprv"]
47+
assert_equal(xpub_info[0]["xpub"], xpub)
48+
assert_equal(xpub_info[0]["has_private"], True)
49+
50+
descs = wallet.listdescriptors(True)
51+
for desc in descs["descriptors"]:
52+
assert xprv in desc["desc"]
53+
54+
self.log.info("HD pubkey can be retrieved from encrypted wallets")
55+
prev_xprv = xprv
56+
wallet.encryptwallet("pass")
57+
# HD key is rotated on encryption, there should now be 2 HD keys
58+
assert_equal(len(wallet.gethdkeys()), 2)
59+
# New key is active, should be able to get only that one and its descriptors
60+
xpub_info = wallet.gethdkeys(active_only=True)
61+
assert_equal(len(xpub_info), 1)
62+
assert xpub_info[0]["xpub"] != xpub
63+
assert "xprv" not in xpub_info[0]
64+
assert_equal(xpub_info[0]["has_private"], True)
65+
66+
self.log.info("HD privkey can be retrieved from encrypted wallets")
67+
assert_raises_rpc_error(-13, "Error: Please enter the wallet passphrase with walletpassphrase first", wallet.gethdkeys, private=True)
68+
with WalletUnlock(wallet, "pass"):
69+
xpub_info = wallet.gethdkeys(active_only=True, private=True)[0]
70+
assert xpub_info["xprv"] != xprv
71+
for desc in wallet.listdescriptors(True)["descriptors"]:
72+
if desc["active"]:
73+
# After encrypting, HD key was rotated and should appear in all active descriptors
74+
assert xpub_info["xprv"] in desc["desc"]
75+
else:
76+
# Inactive descriptors should have the previous HD key
77+
assert prev_xprv in desc["desc"]
78+
79+
def test_ranged_imports(self):
80+
self.log.info("Keys of imported ranged descriptors appear in gethdkeys")
81+
def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
82+
self.nodes[0].createwallet("imports")
83+
wallet = self.nodes[0].get_wallet_rpc("imports")
84+
85+
xpub_info = wallet.gethdkeys()
86+
assert_equal(len(xpub_info), 1)
87+
active_xpub = xpub_info[0]["xpub"]
88+
89+
import_xpub = def_wallet.gethdkeys(active_only=True)[0]["xpub"]
90+
desc_import = def_wallet.listdescriptors(True)["descriptors"]
91+
for desc in desc_import:
92+
desc["active"] = False
93+
wallet.importdescriptors(desc_import)
94+
assert_equal(wallet.gethdkeys(active_only=True), xpub_info)
95+
96+
xpub_info = wallet.gethdkeys()
97+
assert_equal(len(xpub_info), 2)
98+
for x in xpub_info:
99+
if x["xpub"] == active_xpub:
100+
for desc in x["descriptors"]:
101+
assert_equal(desc["active"], True)
102+
elif x["xpub"] == import_xpub:
103+
for desc in x["descriptors"]:
104+
assert_equal(desc["active"], False)
105+
else:
106+
assert False
107+
108+
109+
def test_lone_key_imports(self):
110+
self.log.info("Non-HD keys do not appear in gethdkeys")
111+
self.nodes[0].createwallet("lonekey", blank=True)
112+
wallet = self.nodes[0].get_wallet_rpc("lonekey")
113+
114+
assert_equal(wallet.gethdkeys(), [])
115+
wallet.importdescriptors([{"desc": descsum_create("wpkh(cTe1f5rdT8A8DFgVWTjyPwACsDPJM9ff4QngFxUixCSvvbg1x6sh)"), "timestamp": "now"}])
116+
assert_equal(wallet.gethdkeys(), [])
117+
118+
self.log.info("HD keys of non-ranged descriptors should appear in gethdkeys")
119+
def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
120+
xpub_info = def_wallet.gethdkeys(private=True)
121+
xpub = xpub_info[0]["xpub"]
122+
xprv = xpub_info[0]["xprv"]
123+
prv_desc = descsum_create(f"wpkh({xprv})")
124+
pub_desc = descsum_create(f"wpkh({xpub})")
125+
assert_equal(wallet.importdescriptors([{"desc": prv_desc, "timestamp": "now"}])[0]["success"], True)
126+
xpub_info = wallet.gethdkeys()
127+
assert_equal(len(xpub_info), 1)
128+
assert_equal(xpub_info[0]["xpub"], xpub)
129+
assert_equal(len(xpub_info[0]["descriptors"]), 1)
130+
assert_equal(xpub_info[0]["descriptors"][0]["desc"], pub_desc)
131+
assert_equal(xpub_info[0]["descriptors"][0]["active"], False)
132+
133+
def test_ranged_multisig(self):
134+
self.log.info("HD keys of a multisig appear in gethdkeys")
135+
def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
136+
self.nodes[0].createwallet("ranged_multisig")
137+
wallet = self.nodes[0].get_wallet_rpc("ranged_multisig")
138+
139+
xpub1 = wallet.gethdkeys()[0]["xpub"]
140+
xprv1 = wallet.gethdkeys(private=True)[0]["xprv"]
141+
xpub2 = def_wallet.gethdkeys()[0]["xpub"]
142+
143+
prv_multi_desc = descsum_create(f"wsh(multi(2,{xprv1}/*,{xpub2}/*))")
144+
pub_multi_desc = descsum_create(f"wsh(multi(2,{xpub1}/*,{xpub2}/*))")
145+
assert_equal(wallet.importdescriptors([{"desc": prv_multi_desc, "timestamp": "now"}])[0]["success"], True)
146+
147+
xpub_info = wallet.gethdkeys()
148+
assert_equal(len(xpub_info), 2)
149+
for x in xpub_info:
150+
if x["xpub"] == xpub1:
151+
found_desc = next((d for d in xpub_info[0]["descriptors"] if d["desc"] == pub_multi_desc), None)
152+
assert found_desc is not None
153+
assert_equal(found_desc["active"], False)
154+
elif x["xpub"] == xpub2:
155+
assert_equal(len(x["descriptors"]), 1)
156+
assert_equal(x["descriptors"][0]["desc"], pub_multi_desc)
157+
assert_equal(x["descriptors"][0]["active"], False)
158+
else:
159+
assert False
160+
161+
def test_mixed_multisig(self):
162+
self.log.info("Non-HD keys of a multisig do not appear in gethdkeys")
163+
def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
164+
self.nodes[0].createwallet("single_multisig")
165+
wallet = self.nodes[0].get_wallet_rpc("single_multisig")
166+
167+
xpub = wallet.gethdkeys()[0]["xpub"]
168+
xprv = wallet.gethdkeys(private=True)[0]["xprv"]
169+
pub = def_wallet.getaddressinfo(def_wallet.getnewaddress())["pubkey"]
170+
171+
prv_multi_desc = descsum_create(f"wsh(multi(2,{xprv},{pub}))")
172+
pub_multi_desc = descsum_create(f"wsh(multi(2,{xpub},{pub}))")
173+
import_res = wallet.importdescriptors([{"desc": prv_multi_desc, "timestamp": "now"}])
174+
assert_equal(import_res[0]["success"], True)
175+
176+
xpub_info = wallet.gethdkeys()
177+
assert_equal(len(xpub_info), 1)
178+
assert_equal(xpub_info[0]["xpub"], xpub)
179+
found_desc = next((d for d in xpub_info[0]["descriptors"] if d["desc"] == pub_multi_desc), None)
180+
assert found_desc is not None
181+
assert_equal(found_desc["active"], False)
182+
183+
184+
if __name__ == '__main__':
185+
WalletGetHDKeyTest().main()

0 commit comments

Comments
 (0)