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