|
| 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