Skip to content

Commit 46b0e70

Browse files
apoelstraroconnor-blockstream
authored andcommitted
codex32: add functional test for seed import
1 parent ffa43d1 commit 46b0e70

File tree

2 files changed

+281
-0
lines changed

2 files changed

+281
-0
lines changed

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@
273273
'feature_nulldummy.py',
274274
'mempool_accept.py',
275275
'mempool_expiry.py',
276+
'wallet_addhdkey.py',
276277
'wallet_importdescriptors.py',
277278
'wallet_crosschain.py',
278279
'mining_basic.py',

test/functional/wallet_addhdkey.py

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2025 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 the 'codex32' argument to the 'addhdkey' RPC
6+
7+
Test importing seeds by using the BIP 93 test vectors to verify that imported
8+
seeds are compatible with descriptors containing the corresponding xpubs, that
9+
the wallet is able to recognize and send funds, and that the wallet can derive
10+
addresses, when given only seeds as private data."""
11+
12+
import time
13+
14+
from test_framework.test_framework import BitcoinTestFramework
15+
from test_framework.util import (
16+
assert_raises_rpc_error,
17+
)
18+
19+
20+
class AddHdKeyTest(BitcoinTestFramework):
21+
def set_test_params(self):
22+
self.num_nodes = 1
23+
self.setup_clean_chain = True
24+
self.wallet_names = []
25+
26+
def skip_test_if_missing_module(self):
27+
self.skip_if_no_wallet()
28+
29+
def run_test(self):
30+
test_start = int(time.time())
31+
32+
# Spend/receive tests
33+
self.nodes[0].createwallet(wallet_name='w0', descriptors=True)
34+
self.nodes[0].createwallet(wallet_name='w1', descriptors=True, blank=True)
35+
w0 = self.nodes[0].get_wallet_rpc('w0')
36+
w1 = self.nodes[0].get_wallet_rpc('w1')
37+
38+
self.generatetoaddress(self.nodes[0], 2, w0.getnewaddress())
39+
self.generate(self.nodes[0], 100)
40+
41+
# Test 1: send coins to wallet, check they are not received, then import
42+
# the descriptor and make sure they are recognized. Send them
43+
# back and repeat. Uses single codex32 seed.
44+
#
45+
# xpub converted from BIP 93 test vector 1 xpriv using rust-bitcoin
46+
xpub = "tpubD6NzVbkrYhZ4YAqhvsGTCD5axU32P9MH7ySPr38icriLyJc4KcCvwVzE3rsi" \
47+
"XaAHBC8QtYWhiBGdc6aZRmroQShGcWygQfErbvLULfJSi8j"
48+
descriptors = [
49+
f"wsh(pk({xpub}/55/*))",
50+
f"tr({xpub}/1/2/3/4/5/*)",
51+
f"pkh({xpub}/*)",
52+
f"wpkh({xpub}/*)",
53+
f"rawtr({xpub}/1/2/3/*)",
54+
]
55+
assert_raises_rpc_error(-4, "This wallet has no available keys", w1.getnewaddress)
56+
# :TODO: doesn't work
57+
for descriptor in descriptors:
58+
descriptor_chk = w0.getdescriptorinfo(descriptor)["descriptor"]
59+
addr = w0.deriveaddresses(descriptor_chk, range=[0, 20])[0]
60+
61+
assert w0.getbalance() > 99 # sloppy balance checks, to account for fees
62+
w0.sendtoaddress(addr, 95)
63+
self.generate(self.nodes[0], 1)
64+
assert w0.getbalance() < 5
65+
66+
w1.addhdkey(
67+
codex32=["ms10testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlw"],
68+
)
69+
w1.importdescriptors(
70+
[{"desc": descriptor_chk, "timestamp": test_start, "range": 0, "active": True}],
71+
)
72+
73+
assert w1.getbalance() > 94
74+
w1.sendtoaddress(w0.getnewaddress(), 95, "", "", True)
75+
self.generate(self.nodes[0], 1)
76+
assert w0.getbalance() > 99
77+
w1.getnewaddress() # no failure now
78+
79+
# Test 2: deriveaddresses on hardened keys fails before import, succeeds after.
80+
# Uses single codex32 seed in 2 shares.
81+
#
82+
# xpub converted from BIP 93 test vector 2 xpriv using rust-bitcoin
83+
self.nodes[0].createwallet(wallet_name='w2', descriptors=True, blank=True)
84+
w2 = self.nodes[0].get_wallet_rpc('w2')
85+
86+
xpub = "tpubD6NzVbkrYhZ4Wf289qp46iFM6zACTdXTqqrA3pKUV8bF8SNBcYS8xvVPZg43" \
87+
"6YhSuCqTKLfnDkmwi9TE6fa5cvxm3NHRCBbgJoC6YgsQBFY"
88+
descriptor = f"tr([fab6868a/1h/2]{xpub}/1h/2/*h)"
89+
descriptor_chk = w2.getdescriptorinfo(descriptor)["descriptor"]
90+
assert_raises_rpc_error(
91+
-4,
92+
"This wallet has no available keys",
93+
w2.getnewaddress,
94+
address_type="bech32m",
95+
)
96+
97+
# Try importing wrong seed
98+
w2.addhdkey(
99+
codex32=["ms10testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlw"],
100+
)
101+
err = w2.importdescriptors(
102+
[{"desc": descriptor_chk, "timestamp": test_start, "active": True, "range": [0, 20]}],
103+
)
104+
assert "Cannot expand descriptor." in err[0]["error"]["message"]
105+
assert_raises_rpc_error(
106+
-4,
107+
"This wallet has no available keys",
108+
w2.getnewaddress,
109+
address_type="bech32m",
110+
)
111+
112+
# Try various failure cases
113+
assert_raises_rpc_error(
114+
-5,
115+
"single share must be the S share",
116+
w2.addhdkey,
117+
codex32=["MS12NAMEA320ZYXWVUTSRQPNMLKJHGFEDCAXRPP870HKKQRM"],
118+
)
119+
120+
assert_raises_rpc_error(
121+
-5,
122+
"two input shares had the same index",
123+
w2.addhdkey,
124+
codex32=[
125+
"MS12NAMEA320ZYXWVUTSRQPNMLKJHGFEDCAXRPP870HKKQRM",
126+
"MS12NAMEA320ZYXWVUTSRQPNMLKJHGFEDCAXRPP870HKKQRM",
127+
],
128+
)
129+
130+
assert_raises_rpc_error(
131+
-5,
132+
"input shares had inconsistent seed IDs",
133+
w2.addhdkey,
134+
codex32=[
135+
"MS12NAMEA320ZYXWVUTSRQPNMLKJHGFEDCAXRPP870HKKQRM",
136+
"ms13cashcacdefghjklmnpqrstuvwxyz023949xq35my48dr",
137+
],
138+
)
139+
140+
# Do it correctly
141+
w2.addhdkey(
142+
codex32=[
143+
"MS12NAMEA320ZYXWVUTSRQPNMLKJHGFEDCAXRPP870HKKQRM",
144+
"MS12NAMECACDEFGHJKLMNPQRSTUVWXYZ023FTR2GDZMPY6PN",
145+
],
146+
)
147+
w2.importdescriptors(
148+
[{"desc": descriptor_chk, "timestamp": test_start, "active": True, "range": [0, 20]}],
149+
)
150+
# getnewaddress no longer fails. Annoyingly, deriveaddresses will
151+
# :TODO: it does work
152+
w2.getnewaddress(address_type="bech32m")
153+
assert_raises_rpc_error(
154+
-5,
155+
"Cannot derive script without private keys",
156+
w2.deriveaddresses,
157+
descriptor_chk,
158+
0,
159+
)
160+
# Do it again, to see if nothing breaks
161+
162+
# :TODO: Cannot add an hd key twice.
163+
w2.addhdkey(
164+
codex32=[
165+
"MS12NAMEA320ZYXWVUTSRQPNMLKJHGFEDCAXRPP870HKKQRM",
166+
"MS12NAMECACDEFGHJKLMNPQRSTUVWXYZ023FTR2GDZMPY6PN",
167+
],
168+
)
169+
w2.importdescriptors(
170+
[{"desc": descriptor_chk, "timestamp": test_start, "active": True, "range": [0, 20]}],
171+
)
172+
173+
# Test 3: multiple seeds, multiple descriptors
174+
#
175+
# xpubs converted from BIP 93 test vector 3, 4 and 5 xprivs using rust-bitcoin
176+
177+
self.nodes[0].createwallet(wallet_name='w3', descriptors=True, blank=True)
178+
w3 = self.nodes[0].get_wallet_rpc('w3')
179+
xpub1 = "tpubD6NzVbkrYhZ4WNNA2qNKYbaxKR3TYtP2n5bNSj6JKzYsVUPxahe2vWJKwiX2" \
180+
"wfoTJyERQNJ8YnmJvprMHygyaXziTdyFVsSGNmfQtDCCSJ3" # vector 3
181+
xpub2 = "tpubD6NzVbkrYhZ4Y9KL2R346X9ZwcN16c37vjXuZEhDV2LaMt84zqVbKVbVAw1z" \
182+
"nMksNtdKnSRZQXyBL9qJaNnq9BkjtRBdsQbxkTbSGZGrcG6" # vector 4
183+
xpub3 = "tpubD6NzVbkrYhZ4Ykomd4u92cmRCkhZtctLkKU3vCVi7DKBAopRDWVpq6wEGoq7" \
184+
"xYbCQQjEGM8KkqxvQDoLa3sdfpzTBv1yodq4FKwrCdxweHE" # vector 5
185+
186+
descriptor1 = f"rawtr({xpub1}/1/2h/*)"
187+
descriptor1_chk = w3.getdescriptorinfo(descriptor1)["descriptor"]
188+
descriptor2 = f"wpkh({xpub2}/1h/2/*)"
189+
descriptor2_chk = w3.getdescriptorinfo(descriptor2)["descriptor"]
190+
descriptor3 = f"pkh({xpub3}/1h/2/3/4/5/6/7/8/9/10/*)"
191+
descriptor3_chk = w3.getdescriptorinfo(descriptor3)["descriptor"]
192+
193+
assert_raises_rpc_error(
194+
-4,
195+
"This wallet has no available keys",
196+
w3.getnewaddress,
197+
address_type="bech32m",
198+
)
199+
assert_raises_rpc_error(
200+
-4,
201+
"This wallet has no available keys",
202+
w3.getnewaddress,
203+
address_type="bech32",
204+
)
205+
assert_raises_rpc_error(
206+
-4,
207+
"This wallet has no available keys",
208+
w3.getnewaddress,
209+
address_type="legacy",
210+
)
211+
212+
# First try without enough input shares.
213+
assert_raises_rpc_error(
214+
-5,
215+
"did not have enough input shares",
216+
w3.addhdkey,
217+
codex32=[
218+
"ms13casheekgpemxzshcrmqhaydlp6yhms3ws7320xyxsar9",
219+
"ms13cashf8jh6sdrkpyrsp5ut94pj8ktehhw2hfvyrj48704",
220+
],
221+
)
222+
# Wallet still doesn't work, even the descriptor whose seed was correctly specified
223+
assert_raises_rpc_error(
224+
-4,
225+
"This wallet has no available keys",
226+
w3.getnewaddress,
227+
address_type="bech32",
228+
)
229+
230+
# Do it properly
231+
w3.addhdkey(
232+
codex32=[
233+
"ms13cashd0wsedstcdcts64cd7wvy4m90lm28w4ffupqs7rm",
234+
"ms13casheekgpemxzshcrmqhaydlp6yhms3ws7320xyxsar9",
235+
"ms13cashf8jh6sdrkpyrsp5ut94pj8ktehhw2hfvyrj48704",
236+
],
237+
)
238+
w3.addhdkey(
239+
codex32=[
240+
"ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqqtum9pgv99ycma",
241+
],
242+
)
243+
w3.importdescriptors(
244+
[
245+
{"desc": descriptor1_chk, "timestamp": test_start, "active": True, "range": 10},
246+
{"desc": descriptor2_chk, "timestamp": test_start, "active": True, "range": 15},
247+
{"desc": descriptor3_chk, "timestamp": test_start, "active": True, "range": 15},
248+
],
249+
)
250+
# All good now for the two descriptors that had seeds
251+
# :TODO: these don't work.
252+
w3.getnewaddress(address_type="bech32")
253+
w3.getnewaddress(address_type="bech32m")
254+
# but the one without a seed still doesn't work
255+
# :TODO: it does work
256+
assert_raises_rpc_error(
257+
-12,
258+
"No legacy addresses available",
259+
w3.getnewaddress,
260+
address_type="legacy",
261+
)
262+
263+
# Ok, try to import the legacy one separately.
264+
w2.addhdkey(
265+
codex32=[
266+
"MS100C8VSM32ZXFGUHPCHTLUPZRY9X8GF2TVDW0S3JN54KHCE6MUA7LQPZYGSFJD" # concat string
267+
"6AN074RXVCEMLH8WU3TK925ACDEFGHJKLMNPQRSTUVWXY06FHPV80UNDVARHRAK"],
268+
)
269+
w3.importdescriptors(
270+
[{"desc": descriptor3_chk, "timestamp": test_start, "active": True, "range": 15}],
271+
)
272+
# And all is well!
273+
# :TODO: these don't work.
274+
w3.getnewaddress(address_type="bech32")
275+
w3.getnewaddress(address_type="bech32m")
276+
w3.getnewaddress(address_type="legacy")
277+
278+
279+
if __name__ == '__main__':
280+
AddHdKeyTest(__file__).main()

0 commit comments

Comments
 (0)