#!/usr/bin/env python3 # Copyright (c) 2013 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 the 'seeds' argument to the importdescriptors RPC Test importingi seeds by using the BIP 93 test vectors to verify that imported seeds are compatible with descriptors containing the corresponding xpubs, that the wallet is able to recognize and send funds, and that the wallet can derive addresses, when given only seeds as private data.""" import time from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_raises_rpc_error, ) class ImportDescriptorsTest(BitcoinTestFramework): def add_options(self, parser): self.add_wallet_options(parser, legacy=False) def set_test_params(self): self.num_nodes = 1 self.setup_clean_chain = True self.wallet_names = [] def skip_test_if_missing_module(self): self.skip_if_no_wallet() def run_test(self): test_start = int(time.time()) # Spend/receive tests self.nodes[0].createwallet(wallet_name='w0', descriptors=True) self.nodes[0].createwallet(wallet_name='w1', descriptors=True, blank=True) w0 = self.nodes[0].get_wallet_rpc('w0') w1 = self.nodes[0].get_wallet_rpc('w1') self.generatetoaddress(self.nodes[0], 2, w0.getnewaddress()) self.generate(self.nodes[0], 100) # Test 1: send coins to wallet, check they are not received, then import # the descriptor and make sure they are recognized. Send them # back and repeat. Uses single codex32 seed. # # xpub converted from BIP 93 test vector 1 xpriv using rust-bitcoin xpub = "tpubD6NzVbkrYhZ4YAqhvsGTCD5axU32P9MH7ySPr38icriLyJc4KcCvwVzE3rsi" \ "XaAHBC8QtYWhiBGdc6aZRmroQShGcWygQfErbvLULfJSi8j" descriptors = [ f"wsh(pk({xpub}/55/*))", f"tr({xpub}/1/2/3/4/5/*)", f"pkh({xpub}/*)", f"wpkh({xpub}/*)", f"rawtr({xpub}/1/2/3/*)", ] assert_raises_rpc_error(-4, "This wallet has no available keys", w1.getnewaddress) for descriptor in descriptors: descriptor_chk = w0.getdescriptorinfo(descriptor)["descriptor"] addr = w0.deriveaddresses(descriptor_chk, range=[0, 20])[0] assert w0.getbalance() > 99 # sloppy balance checks, to account for fees w0.sendtoaddress(addr, 95) self.generate(self.nodes[0], 1) assert w0.getbalance() < 5 w1.importdescriptors( [{"desc": descriptor_chk, "timestamp": test_start, "range": 0, "active": True}], [["ms10testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlw"]], ) assert w1.getbalance() > 94 w1.sendtoaddress(w0.getnewaddress(), 95, "", "", True) self.generate(self.nodes[0], 1) assert w0.getbalance() > 99 w1.getnewaddress() # no failure now # Test 2: deriveaddresses on hardened keys fails before import, succeeds after. # Uses single codex32 seed in 2 shares. # # xpub converted from BIP 93 test vector 2 xpriv using rust-bitcoin self.nodes[0].createwallet(wallet_name='w2', descriptors=True, blank=True) w2 = self.nodes[0].get_wallet_rpc('w2') xpub = "tpubD6NzVbkrYhZ4Wf289qp46iFM6zACTdXTqqrA3pKUV8bF8SNBcYS8xvVPZg43" \ "6YhSuCqTKLfnDkmwi9TE6fa5cvxm3NHRCBbgJoC6YgsQBFY" descriptor = f"tr([fab6868a/1h/2]{xpub}/1h/2/*h)" descriptor_chk = w2.getdescriptorinfo(descriptor)["descriptor"] assert_raises_rpc_error( -4, "This wallet has no available keys", w2.getnewaddress, address_type="bech32m", ) # Try importing descriptor with wrong seed err = w2.importdescriptors( [{"desc": descriptor_chk, "timestamp": test_start, "active": True, "range": [0, 20]}], [["ms10testsxxxxxxxxxxxxxxxxxxxxxxxxxx4nzvca9cmczlw"]], ) assert "Cannot expand descriptor." in err[0]["error"]["message"] assert_raises_rpc_error( -4, "This wallet has no available keys", w2.getnewaddress, address_type="bech32m", ) # Try various failure cases assert_raises_rpc_error( -5, "single share must be the S share", w2.importdescriptors, [{"desc": descriptor_chk, "timestamp": test_start, "active": True, "range": [0, 20]}], [["MS12NAMEA320ZYXWVUTSRQPNMLKJHGFEDCAXRPP870HKKQRM"]], ) assert_raises_rpc_error( -5, "two input shares had the same index", w2.importdescriptors, [{"desc": descriptor_chk, "timestamp": test_start, "active": True, "range": [0, 20]}], [[ "MS12NAMEA320ZYXWVUTSRQPNMLKJHGFEDCAXRPP870HKKQRM", "MS12NAMEA320ZYXWVUTSRQPNMLKJHGFEDCAXRPP870HKKQRM", ]], ) assert_raises_rpc_error( -5, "input shares had inconsistent seed IDs", w2.importdescriptors, [{"desc": descriptor_chk, "timestamp": test_start, "active": True, "range": [0, 20]}], [[ "MS12NAMEA320ZYXWVUTSRQPNMLKJHGFEDCAXRPP870HKKQRM", "ms13cashcacdefghjklmnpqrstuvwxyz023949xq35my48dr", ]], ) # Do it correctly w2.importdescriptors( [{"desc": descriptor_chk, "timestamp": test_start, "active": True, "range": [0, 20]}], [[ "MS12NAMEA320ZYXWVUTSRQPNMLKJHGFEDCAXRPP870HKKQRM", "MS12NAMECACDEFGHJKLMNPQRSTUVWXYZ023FTR2GDZMPY6PN", ]], ) # getnewaddress no longer fails. Annoyingl, deriveaddresses will w2.getnewaddress(address_type="bech32m") assert_raises_rpc_error( -5, "Cannot derive script without private keys", w2.deriveaddresses, descriptor_chk, 0, ) # Do it again, to see if nothing breaks w2.importdescriptors( [{"desc": descriptor_chk, "timestamp": test_start, "active": True, "range": [0, 20]}], [[ "MS12NAMEA320ZYXWVUTSRQPNMLKJHGFEDCAXRPP870HKKQRM", "MS12NAMECACDEFGHJKLMNPQRSTUVWXYZ023FTR2GDZMPY6PN", ]], ) # Test 3: multiple seeds, multiple descriptors # # xpubs converted from BIP 93 test vector 3, 4 and 5 xprivs using rust-bitcoin self.nodes[0].createwallet(wallet_name='w3', descriptors=True, blank=True) w3 = self.nodes[0].get_wallet_rpc('w3') xpub1 = "tpubD6NzVbkrYhZ4WNNA2qNKYbaxKR3TYtP2n5bNSj6JKzYsVUPxahe2vWJKwiX2" \ "wfoTJyERQNJ8YnmJvprMHygyaXziTdyFVsSGNmfQtDCCSJ3" # vector 3 xpub2 = "tpubD6NzVbkrYhZ4Y9KL2R346X9ZwcN16c37vjXuZEhDV2LaMt84zqVbKVbVAw1z" \ "nMksNtdKnSRZQXyBL9qJaNnq9BkjtRBdsQbxkTbSGZGrcG6" # vector 4 xpub3 = "tpubD6NzVbkrYhZ4Ykomd4u92cmRCkhZtctLkKU3vCVi7DKBAopRDWVpq6wEGoq7" \ "xYbCQQjEGM8KkqxvQDoLa3sdfpzTBv1yodq4FKwrCdxweHE" # vector 5 descriptor1 = f"rawtr({xpub1}/1/2h/*)" descriptor1_chk = w3.getdescriptorinfo(descriptor1)["descriptor"] descriptor2 = f"wpkh({xpub2}/1h/2/*)" descriptor2_chk = w3.getdescriptorinfo(descriptor2)["descriptor"] descriptor3 = f"pkh({xpub3}/1h/2/3/4/5/6/7/8/9/10/*)" descriptor3_chk = w3.getdescriptorinfo(descriptor3)["descriptor"] assert_raises_rpc_error( -4, "This wallet has no available keys", w3.getnewaddress, address_type="bech32m", ) assert_raises_rpc_error( -4, "This wallet has no available keys", w3.getnewaddress, address_type="bech32", ) assert_raises_rpc_error( -4, "This wallet has no available keys", w3.getnewaddress, address_type="legacy", ) # First try without enough input shares. assert_raises_rpc_error( -5, "did not have enough input shares", w3.importdescriptors, [ {"desc": descriptor1_chk, "timestamp": test_start, "active": True, "range": 10}, {"desc": descriptor2_chk, "timestamp": test_start, "active": True, "range": 15}, ], [[ "ms13casheekgpemxzshcrmqhaydlp6yhms3ws7320xyxsar9", "ms13cashf8jh6sdrkpyrsp5ut94pj8ktehhw2hfvyrj48704", ], [ "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqqtum9pgv99ycma", ]], ) # Wallet still doesn't work, even the descriptor whose seed was correctly specified assert_raises_rpc_error( -4, "This wallet has no available keys", w3.getnewaddress, address_type="bech32", ) # Do it properly w3.importdescriptors( [ {"desc": descriptor1_chk, "timestamp": test_start, "active": True, "range": 10}, {"desc": descriptor2_chk, "timestamp": test_start, "active": True, "range": 15}, {"desc": descriptor3_chk, "timestamp": test_start, "active": True, "range": 15}, ], [[ "ms13cashd0wsedstcdcts64cd7wvy4m90lm28w4ffupqs7rm", "ms13casheekgpemxzshcrmqhaydlp6yhms3ws7320xyxsar9", "ms13cashf8jh6sdrkpyrsp5ut94pj8ktehhw2hfvyrj48704", ], [ "ms10leetsllhdmn9m42vcsamx24zrxgs3qrl7ahwvhw4fnzrhve25gvezzyqqtum9pgv99ycma", ]], ) # All good now for the two descriptors that had seeds w3.getnewaddress(address_type="bech32") w3.getnewaddress(address_type="bech32m") # but the one without a seed still doesn't work assert_raises_rpc_error( -12, "No legacy addresses available", w3.getnewaddress, address_type="legacy", ) # Ok, try to import the legacy one separately. w3.importdescriptors( [{"desc": descriptor3_chk, "timestamp": test_start, "active": True, "range": 15}], [["MS100C8VSM32ZXFGUHPCHTLUPZRY9X8GF2TVDW0S3JN54KHCE6MUA7LQPZYGSFJD" # concat string "6AN074RXVCEMLH8WU3TK925ACDEFGHJKLMNPQRSTUVWXY06FHPV80UNDVARHRAK"]], ) # And all is well! w3.getnewaddress(address_type="bech32") w3.getnewaddress(address_type="bech32m") w3.getnewaddress(address_type="legacy") if __name__ == '__main__': ImportDescriptorsTest(__file__).main()