wallet_gethdkeys.py raw
1 #!/usr/bin/env python3
2 # Copyright (c) 2023-present 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 assert_not_equal,
13 assert_greater_than,
14 )
15 from test_framework.wallet_util import WalletUnlock
16
17
18 class WalletGetHDKeyTest(BitcoinTestFramework):
19 def set_test_params(self):
20 self.setup_clean_chain = True
21 self.num_nodes = 1
22
23 def skip_test_if_missing_module(self):
24 self.skip_if_no_wallet()
25
26 def run_test(self):
27 self.test_basic_gethdkeys()
28 self.test_ranged_imports()
29 self.test_lone_key_imports()
30 self.test_ranged_multisig()
31 self.test_mixed_multisig()
32
33 def test_basic_gethdkeys(self):
34 self.log.info("Test gethdkeys basics")
35 self.nodes[0].createwallet("basic")
36 wallet = self.nodes[0].get_wallet_rpc("basic")
37 xpub_info = wallet.gethdkeys()
38 assert_equal(len(xpub_info), 1)
39 assert_equal(xpub_info[0]["has_private"], True)
40
41 assert "xprv" not in xpub_info[0]
42 xpub = xpub_info[0]["xpub"]
43
44 xpub_info = wallet.gethdkeys(private=True)
45 xprv = xpub_info[0]["xprv"]
46 assert_equal(xpub_info[0]["xpub"], xpub)
47 assert_equal(xpub_info[0]["has_private"], True)
48
49 descs = wallet.listdescriptors(True)
50 for desc in descs["descriptors"]:
51 assert xprv in desc["desc"]
52
53 self.log.info("HD pubkey can be retrieved from encrypted wallets")
54 prev_xprv = xprv
55 wallet.encryptwallet("pass")
56 # HD key is rotated on encryption, there should now be 2 HD keys
57 assert_equal(len(wallet.gethdkeys()), 2)
58 # New key is active, should be able to get only that one and its descriptors
59 xpub_info = wallet.gethdkeys(active_only=True)
60 assert_equal(len(xpub_info), 1)
61 assert_not_equal(xpub_info[0]["xpub"], xpub)
62 assert "xprv" not in xpub_info[0]
63 assert_equal(xpub_info[0]["has_private"], True)
64
65 self.log.info("HD privkey can be retrieved from encrypted wallets")
66 assert_raises_rpc_error(-13, "Error: Please enter the wallet passphrase with walletpassphrase first", wallet.gethdkeys, private=True)
67 with WalletUnlock(wallet, "pass"):
68 xpub_info = wallet.gethdkeys(active_only=True, private=True)[0]
69 assert_not_equal(xpub_info["xprv"], xprv)
70 for desc in wallet.listdescriptors(True)["descriptors"]:
71 if desc["active"]:
72 # After encrypting, HD key was rotated and should appear in all active descriptors
73 assert xpub_info["xprv"] in desc["desc"]
74 else:
75 # Inactive descriptors should have the previous HD key
76 assert prev_xprv in desc["desc"]
77
78 def test_ranged_imports(self):
79 self.log.info("Keys of imported ranged descriptors appear in gethdkeys")
80 def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
81 self.nodes[0].createwallet("imports")
82 wallet = self.nodes[0].get_wallet_rpc("imports")
83
84 xpub_info = wallet.gethdkeys()
85 assert_equal(len(xpub_info), 1)
86 active_xpub = xpub_info[0]["xpub"]
87
88 import_xpub = def_wallet.gethdkeys(active_only=True)[0]["xpub"]
89 desc_import = def_wallet.listdescriptors(True)["descriptors"]
90 for desc in desc_import:
91 desc["active"] = False
92 wallet.importdescriptors(desc_import)
93 assert_equal(wallet.gethdkeys(active_only=True), xpub_info)
94
95 xpub_info = wallet.gethdkeys()
96 assert_equal(len(xpub_info), 2)
97 for x in xpub_info:
98 if x["xpub"] == active_xpub:
99 for desc in x["descriptors"]:
100 assert_equal(desc["active"], True)
101 elif x["xpub"] == import_xpub:
102 for desc in x["descriptors"]:
103 assert_equal(desc["active"], False)
104 else:
105 assert False
106
107
108 def test_lone_key_imports(self):
109 self.log.info("Non-HD keys do not appear in gethdkeys")
110 self.nodes[0].createwallet("lonekey", blank=True)
111 wallet = self.nodes[0].get_wallet_rpc("lonekey")
112
113 assert_equal(wallet.gethdkeys(), [])
114 wallet.importdescriptors([{"desc": descsum_create("wpkh(cTe1f5rdT8A8DFgVWTjyPwACsDPJM9ff4QngFxUixCSvvbg1x6sh)"), "timestamp": "now"}])
115 assert_equal(wallet.gethdkeys(), [])
116
117 self.log.info("HD keys of non-ranged descriptors should appear in gethdkeys")
118 def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
119 xpub_info = def_wallet.gethdkeys(private=True)
120 xpub = xpub_info[0]["xpub"]
121 xprv = xpub_info[0]["xprv"]
122 prv_desc = descsum_create(f"wpkh({xprv})")
123 pub_desc = descsum_create(f"wpkh({xpub})")
124 assert_equal(wallet.importdescriptors([{"desc": prv_desc, "timestamp": "now"}])[0]["success"], True)
125 xpub_info = wallet.gethdkeys()
126 assert_equal(len(xpub_info), 1)
127 assert_equal(xpub_info[0]["xpub"], xpub)
128 assert_equal(len(xpub_info[0]["descriptors"]), 1)
129 assert_equal(xpub_info[0]["descriptors"][0]["desc"], pub_desc)
130 assert_equal(xpub_info[0]["descriptors"][0]["active"], False)
131
132 def test_ranged_multisig(self):
133 self.log.info("HD keys of a multisig appear in gethdkeys")
134
135 def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
136 outside_wallet_xpub = def_wallet.gethdkeys()[0]["xpub"]
137
138 self.nodes[0].createwallet("ranged_multisig")
139 wallet = self.nodes[0].get_wallet_rpc("ranged_multisig")
140
141 hdkeys_info = wallet.gethdkeys(private=True)
142 assert_equal(len(hdkeys_info), 1)
143 within_wallet_xprv = hdkeys_info[0]["xprv"]
144 within_wallet_xpub = hdkeys_info[0]["xpub"]
145
146 prv_multi_desc = descsum_create(f"wsh(multi(2,{within_wallet_xprv}/*,{outside_wallet_xpub}/*))")
147 pub_multi_desc = descsum_create(f"wsh(multi(2,{within_wallet_xpub}/*,{outside_wallet_xpub}/*))")
148 assert_equal(wallet.importdescriptors([{"desc": prv_multi_desc, "timestamp": "now"}])[0]["success"], True)
149
150 rpcs_req_resp = [[False, wallet.gethdkeys()], [True, wallet.gethdkeys(private=True)]]
151 for rpc_req_resp in rpcs_req_resp:
152 requested_private, hdkeys_response = rpc_req_resp
153 assert_equal(len(hdkeys_response), 2)
154
155 for hdkeys_info in hdkeys_response:
156 if hdkeys_info["xpub"] == within_wallet_xpub:
157 assert_equal(hdkeys_info["has_private"], True)
158 if requested_private:
159 assert_equal(hdkeys_info["xprv"], within_wallet_xprv)
160 else:
161 assert_equal("xprv" not in hdkeys_info, True)
162 assert_greater_than(len(hdkeys_info["descriptors"]), 1) # within wallet xpub by default is part of multiple descriptors
163 found_desc = next((d for d in hdkeys_info["descriptors"] if d["desc"] == pub_multi_desc), None)
164 elif hdkeys_info["xpub"] == outside_wallet_xpub:
165 assert_equal(hdkeys_info["has_private"], False)
166 assert_equal("xprv" not in hdkeys_info, True)
167 assert_equal(len(hdkeys_info["descriptors"]), 1) # outside wallet xpub is part of only the imported descriptor
168 found_desc = hdkeys_info["descriptors"][0]
169 else:
170 assert False
171
172 assert_equal(found_desc["desc"], pub_multi_desc)
173 assert_equal(found_desc["active"], False)
174
175 def test_mixed_multisig(self):
176 self.log.info("Non-HD keys of a multisig do not appear in gethdkeys")
177 def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
178 self.nodes[0].createwallet("single_multisig")
179 wallet = self.nodes[0].get_wallet_rpc("single_multisig")
180
181 xpub = wallet.gethdkeys()[0]["xpub"]
182 xprv = wallet.gethdkeys(private=True)[0]["xprv"]
183 pub = def_wallet.getaddressinfo(def_wallet.getnewaddress())["pubkey"]
184
185 prv_multi_desc = descsum_create(f"wsh(multi(2,{xprv},{pub}))")
186 pub_multi_desc = descsum_create(f"wsh(multi(2,{xpub},{pub}))")
187 import_res = wallet.importdescriptors([{"desc": prv_multi_desc, "timestamp": "now"}])
188 assert_equal(import_res[0]["success"], True)
189
190 xpub_info = wallet.gethdkeys()
191 assert_equal(len(xpub_info), 1)
192 assert_equal(xpub_info[0]["xpub"], xpub)
193 found_desc = next((d for d in xpub_info[0]["descriptors"] if d["desc"] == pub_multi_desc), None)
194 assert found_desc is not None
195 assert_equal(found_desc["active"], False)
196
197
198 if __name__ == '__main__':
199 WalletGetHDKeyTest(__file__).main()
200