wallet_hd.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2016-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 Hierarchical Deterministic wallet function."""
   6  
   7  import shutil
   8  
   9  from test_framework.blocktools import COINBASE_MATURITY
  10  from test_framework.descriptors import descsum_create
  11  from test_framework.test_framework import BitcoinTestFramework
  12  from test_framework.util import (
  13      assert_equal,
  14      wallet_importprivkey,
  15      assert_raises_rpc_error,
  16  )
  17  
  18  
  19  class WalletHDTest(BitcoinTestFramework):
  20      def set_test_params(self):
  21          self.setup_clean_chain = True
  22          self.num_nodes = 2
  23          self.extra_args = [[], ['-keypool=0']]
  24          # whitelist peers to speed up tx relay / mempool sync
  25          self.noban_tx_relay = True
  26  
  27      def skip_test_if_missing_module(self):
  28          self.skip_if_no_wallet()
  29  
  30      def test_addhdkey(self):
  31          self.log.info("Test addhdkey")
  32          def_wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
  33          self.nodes[0].createwallet("hdkey")
  34          wallet = self.nodes[0].get_wallet_rpc("hdkey")
  35  
  36          assert_equal(len(wallet.gethdkeys()), 1)
  37  
  38          wallet.addhdkey()
  39          xpub_info = wallet.gethdkeys()
  40          assert_equal(len(xpub_info), 2)
  41          for x in xpub_info:
  42              if len(x["descriptors"]) == 1 and x["descriptors"][0]["desc"].startswith("unused("):
  43                  break
  44          else:
  45              assert False, "Did not find HD key with no descriptors"
  46  
  47          imp_xpub_info = def_wallet.gethdkeys(private=True)[0]
  48          imp_xpub = imp_xpub_info["xpub"]
  49          imp_xprv = imp_xpub_info["xprv"]
  50  
  51          assert_raises_rpc_error(-5, "Extended public key (xpub) provided, but extended private key (xprv) is required", wallet.addhdkey, imp_xpub)
  52          add_res = wallet.addhdkey(imp_xprv)
  53          expected_unused_desc = descsum_create(f"unused({imp_xpub})")
  54          assert_equal(add_res["xpub"], imp_xpub)
  55          xpub_info = wallet.gethdkeys()
  56          assert_equal(len(xpub_info), 3)
  57          for x in xpub_info:
  58              if x["xpub"] == imp_xpub:
  59                  assert_equal(len(x["descriptors"]), 1)
  60                  assert_equal(x["descriptors"][0]["desc"], expected_unused_desc)
  61                  break
  62          else:
  63              assert False, "Added HD key was not found in wallet"
  64  
  65          for d in wallet.listdescriptors()["descriptors"]:
  66              if d["desc"] == expected_unused_desc:
  67                  assert_equal(d["active"], False)
  68                  break
  69          else:
  70              assert False, "Added HD key's descriptor was not found in wallet"
  71  
  72          assert_raises_rpc_error(-4, "HD key already exists", wallet.addhdkey, imp_xprv)
  73  
  74      def test_addhdkey_noprivs(self):
  75          self.log.info("Test addhdkey is not available for wallets without privkeys")
  76          self.nodes[0].createwallet("hdkey_noprivs", disable_private_keys=True)
  77          wallet = self.nodes[0].get_wallet_rpc("hdkey_noprivs")
  78          assert_raises_rpc_error(-4, "addhdkey is not available for wallets without private keys", wallet.addhdkey)
  79  
  80      def run_test(self):
  81          # Make sure we use hd, keep masterkeyid
  82          hd_fingerprint = self.nodes[1].getaddressinfo(self.nodes[1].getnewaddress())['hdmasterfingerprint']
  83          assert_equal(len(hd_fingerprint), 8)
  84  
  85          # create an internal key
  86          change_addr = self.nodes[1].getrawchangeaddress()
  87          change_addrV = self.nodes[1].getaddressinfo(change_addr)
  88          assert_equal(change_addrV["hdkeypath"], "m/84h/1h/0h/1/0")
  89  
  90          # Import a non-HD private key in the HD wallet
  91          non_hd_add = 'bcrt1qmevj8zfx0wdvp05cqwkmr6mxkfx60yezwjksmt'
  92          non_hd_key = 'cS9umN9w6cDMuRVYdbkfE4c7YUFLJRoXMfhQ569uY4odiQbVN8Rt'
  93          wallet_importprivkey(self.nodes[1], non_hd_key, "now")
  94  
  95          # This should be enough to keep the master key and the non-HD key
  96          self.nodes[1].backupwallet(self.nodes[1].datadir_path / "hd.bak")
  97  
  98          # Derive some HD addresses and remember the last
  99          # Also send funds to each add
 100          self.generate(self.nodes[0], COINBASE_MATURITY + 1)
 101          hd_add = None
 102          NUM_HD_ADDS = 10
 103          for i in range(1, NUM_HD_ADDS + 1):
 104              hd_add = self.nodes[1].getnewaddress()
 105              hd_info = self.nodes[1].getaddressinfo(hd_add)
 106              assert_equal(hd_info["hdkeypath"], "m/84h/1h/0h/0/" + str(i))
 107              assert_equal(hd_info["hdmasterfingerprint"], hd_fingerprint)
 108              self.nodes[0].sendtoaddress(hd_add, 1)
 109              self.generate(self.nodes[0], 1)
 110          self.nodes[0].sendtoaddress(non_hd_add, 1)
 111          self.generate(self.nodes[0], 1)
 112  
 113          # create an internal key (again)
 114          change_addr = self.nodes[1].getrawchangeaddress()
 115          change_addrV = self.nodes[1].getaddressinfo(change_addr)
 116          assert_equal(change_addrV["hdkeypath"], "m/84h/1h/0h/1/1")
 117  
 118          self.sync_all()
 119          assert_equal(self.nodes[1].getbalance(), NUM_HD_ADDS + 1)
 120  
 121          self.log.info("Restore backup ...")
 122          self.stop_node(1)
 123          # we need to delete the complete chain directory
 124          # otherwise node1 would auto-recover all funds in flag the keypool keys as used
 125          self.cleanup_folder(self.nodes[1].blocks_path)
 126          self.cleanup_folder(self.nodes[1].chain_path / "chainstate")
 127          shutil.copyfile(
 128              self.nodes[1].datadir_path / "hd.bak",
 129              self.nodes[1].wallets_path / self.default_wallet_name / self.wallet_data_filename
 130          )
 131          self.start_node(1)
 132  
 133          # Assert that derivation is deterministic
 134          hd_add_2 = None
 135          for i in range(1, NUM_HD_ADDS + 1):
 136              hd_add_2 = self.nodes[1].getnewaddress()
 137              hd_info_2 = self.nodes[1].getaddressinfo(hd_add_2)
 138              assert_equal(hd_info_2["hdkeypath"], "m/84h/1h/0h/0/" + str(i))
 139              assert_equal(hd_info_2["hdmasterfingerprint"], hd_fingerprint)
 140          assert_equal(hd_add, hd_add_2)
 141          self.connect_nodes(0, 1)
 142          self.sync_all()
 143  
 144          # Needs rescan
 145          self.nodes[1].rescanblockchain()
 146          assert_equal(self.nodes[1].getbalance(), NUM_HD_ADDS + 1)
 147  
 148          # Try a RPC based rescan
 149          self.stop_node(1)
 150          self.cleanup_folder(self.nodes[1].blocks_path)
 151          self.cleanup_folder(self.nodes[1].chain_path / "chainstate")
 152          shutil.copyfile(
 153              self.nodes[1].datadir_path / "hd.bak",
 154              self.nodes[1].wallets_path / self.default_wallet_name / self.wallet_data_filename
 155          )
 156          self.start_node(1, extra_args=self.extra_args[1])
 157          self.connect_nodes(0, 1)
 158          self.sync_all()
 159          # Wallet automatically scans blocks older than key on startup
 160          assert_equal(self.nodes[1].getbalance(), NUM_HD_ADDS + 1)
 161          out = self.nodes[1].rescanblockchain(0, 1)
 162          assert_equal(out['start_height'], 0)
 163          assert_equal(out['stop_height'], 1)
 164          out = self.nodes[1].rescanblockchain()
 165          assert_equal(out['start_height'], 0)
 166          assert_equal(out['stop_height'], self.nodes[1].getblockcount())
 167          assert_equal(self.nodes[1].getbalance(), NUM_HD_ADDS + 1)
 168  
 169          # send a tx and make sure its using the internal chain for the changeoutput
 170          txid = self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), 1)
 171          outs = self.nodes[1].gettransaction(txid=txid, verbose=True)['decoded']['vout']
 172          keypath = ""
 173          for out in outs:
 174              if out['value'] != 1:
 175                  keypath = self.nodes[1].getaddressinfo(out['scriptPubKey']['address'])['hdkeypath']
 176  
 177          assert_equal(keypath[0:14], "m/84h/1h/0h/1/")
 178  
 179          self.test_addhdkey()
 180          self.test_addhdkey_noprivs()
 181  
 182  if __name__ == '__main__':
 183      WalletHDTest(__file__).main()
 184