wallet_hd.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2016-2022 The Limenka 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.test_framework import LimenkaTestFramework
  11  from test_framework.util import (
  12      assert_equal,
  13      assert_raises_rpc_error,
  14  )
  15  
  16  
  17  class WalletHDTest(LimenkaTestFramework):
  18      def add_options(self, parser):
  19          self.add_wallet_options(parser)
  20  
  21      def set_test_params(self):
  22          self.setup_clean_chain = True
  23          self.num_nodes = 2
  24          self.extra_args = [[], ['-keypool=0']]
  25          # whitelist peers to speed up tx relay / mempool sync
  26          self.noban_tx_relay = True
  27  
  28          self.supports_cli = False
  29  
  30      def skip_test_if_missing_module(self):
  31          self.skip_if_no_wallet()
  32  
  33      def run_test(self):
  34          # Make sure we use hd, keep masterkeyid
  35          hd_fingerprint = self.nodes[1].getaddressinfo(self.nodes[1].getnewaddress())['hdmasterfingerprint']
  36          assert_equal(len(hd_fingerprint), 8)
  37  
  38          # create an internal key
  39          change_addr = self.nodes[1].getrawchangeaddress()
  40          change_addrV = self.nodes[1].getaddressinfo(change_addr)
  41          if self.options.descriptors:
  42              assert_equal(change_addrV["hdkeypath"], "m/84h/1h/0h/1/0")
  43  
  44              # Exporting the master private key should fail on a descriptor wallet
  45              assert_raises_rpc_error(-4, "Only legacy wallets are supported by this command", self.nodes[1].dumpmasterprivkey)
  46          else:
  47              assert_equal(change_addrV["hdkeypath"], "m/0'/1'/0'")  #first internal child key
  48  
  49              # Check that the exported master private key begins with tprv
  50              xprv = self.nodes[1].dumpmasterprivkey()
  51              assert_equal(xprv[0:4], "tprv")
  52  
  53              # Exporting the master private key should fail on a non-HD wallet
  54              # FIXME: No way to make non-HD wallets anymore
  55              #assert_raises_rpc_error(-4, "Wallet is not a HD wallet.", self.nodes[0].dumpmasterprivkey)
  56  
  57          # Import a non-HD private key in the HD wallet
  58          non_hd_add = 'bcrt1qmevj8zfx0wdvp05cqwkmr6mxkfx60yezwjksmt'
  59          non_hd_key = 'cS9umN9w6cDMuRVYdbkfE4c7YUFLJRoXMfhQ569uY4odiQbVN8Rt'
  60          self.nodes[1].importprivkey(non_hd_key)
  61  
  62          # This should be enough to keep the master key and the non-HD key
  63          self.nodes[1].backupwallet(self.nodes[1].datadir_path / "hd.bak")
  64          #self.nodes[1].dumpwallet(self.nodes[1].datadir_path / "hd.dump")
  65  
  66          # Derive some HD addresses and remember the last
  67          # Also send funds to each add
  68          self.generate(self.nodes[0], COINBASE_MATURITY + 1)
  69          hd_add = None
  70          NUM_HD_ADDS = 10
  71          for i in range(1, NUM_HD_ADDS + 1):
  72              hd_add = self.nodes[1].getnewaddress()
  73              hd_info = self.nodes[1].getaddressinfo(hd_add)
  74              if self.options.descriptors:
  75                  assert_equal(hd_info["hdkeypath"], "m/84h/1h/0h/0/" + str(i))
  76              else:
  77                  assert_equal(hd_info["hdkeypath"], "m/0'/0'/" + str(i) + "'")
  78              assert_equal(hd_info["hdmasterfingerprint"], hd_fingerprint)
  79              self.nodes[0].sendtoaddress(hd_add, 1)
  80              self.generate(self.nodes[0], 1)
  81          self.nodes[0].sendtoaddress(non_hd_add, 1)
  82          self.generate(self.nodes[0], 1)
  83  
  84          # create an internal key (again)
  85          change_addr = self.nodes[1].getrawchangeaddress()
  86          change_addrV = self.nodes[1].getaddressinfo(change_addr)
  87          if self.options.descriptors:
  88              assert_equal(change_addrV["hdkeypath"], "m/84h/1h/0h/1/1")
  89          else:
  90              assert_equal(change_addrV["hdkeypath"], "m/0'/1'/1'")  #second internal child key
  91  
  92          self.sync_all()
  93          assert_equal(self.nodes[1].getbalance(), NUM_HD_ADDS + 1)
  94  
  95          self.log.info("Restore backup ...")
  96          self.stop_node(1)
  97          # we need to delete the complete chain directory
  98          # otherwise node1 would auto-recover all funds in flag the keypool keys as used
  99          shutil.rmtree(self.nodes[1].blocks_path)
 100          shutil.rmtree(self.nodes[1].chain_path / "chainstate")
 101          shutil.copyfile(
 102              self.nodes[1].datadir_path / "hd.bak",
 103              self.nodes[1].wallets_path / self.default_wallet_name / self.wallet_data_filename
 104          )
 105          self.start_node(1)
 106  
 107          # Assert that derivation is deterministic
 108          hd_add_2 = None
 109          for i in range(1, NUM_HD_ADDS + 1):
 110              hd_add_2 = self.nodes[1].getnewaddress()
 111              hd_info_2 = self.nodes[1].getaddressinfo(hd_add_2)
 112              if self.options.descriptors:
 113                  assert_equal(hd_info_2["hdkeypath"], "m/84h/1h/0h/0/" + str(i))
 114              else:
 115                  assert_equal(hd_info_2["hdkeypath"], "m/0'/0'/" + str(i) + "'")
 116              assert_equal(hd_info_2["hdmasterfingerprint"], hd_fingerprint)
 117          assert_equal(hd_add, hd_add_2)
 118          self.connect_nodes(0, 1)
 119          self.sync_all()
 120  
 121          # Needs rescan
 122          self.nodes[1].rescanblockchain()
 123          assert_equal(self.nodes[1].getbalance(), NUM_HD_ADDS + 1)
 124  
 125          # Try a RPC based rescan
 126          self.stop_node(1)
 127          shutil.rmtree(self.nodes[1].blocks_path)
 128          shutil.rmtree(self.nodes[1].chain_path / "chainstate")
 129          shutil.copyfile(
 130              self.nodes[1].datadir_path / "hd.bak",
 131              self.nodes[1].wallets_path / self.default_wallet_name / self.wallet_data_filename
 132          )
 133          self.start_node(1, extra_args=self.extra_args[1])
 134          self.connect_nodes(0, 1)
 135          self.sync_all()
 136          # Wallet automatically scans blocks older than key on startup
 137          assert_equal(self.nodes[1].getbalance(), NUM_HD_ADDS + 1)
 138          out = self.nodes[1].rescanblockchain(0, 1)
 139          assert_equal(out['start_height'], 0)
 140          assert_equal(out['stop_height'], 1)
 141          out = self.nodes[1].rescanblockchain()
 142          assert_equal(out['start_height'], 0)
 143          assert_equal(out['stop_height'], self.nodes[1].getblockcount())
 144          assert_equal(self.nodes[1].getbalance(), NUM_HD_ADDS + 1)
 145  
 146          # send a tx and make sure its using the internal chain for the changeoutput
 147          txid = self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), 1)
 148          outs = self.nodes[1].gettransaction(txid=txid, verbose=True)['decoded']['vout']
 149          keypath = ""
 150          for out in outs:
 151              if out['value'] != 1:
 152                  keypath = self.nodes[1].getaddressinfo(out['scriptPubKey']['address'])['hdkeypath']
 153  
 154          if self.options.descriptors:
 155              assert_equal(keypath[0:14], "m/84h/1h/0h/1/")
 156          else:
 157              assert_equal(keypath[0:7], "m/0'/1'")
 158  
 159          if not self.options.descriptors:
 160              # Generate a new HD seed on node 1 and make sure it is set
 161              orig_masterkeyid = self.nodes[1].getwalletinfo()['hdseedid']
 162              self.nodes[1].sethdseed()
 163              new_masterkeyid = self.nodes[1].getwalletinfo()['hdseedid']
 164              assert orig_masterkeyid != new_masterkeyid
 165              addr = self.nodes[1].getnewaddress()
 166              # Make sure the new address is the first from the keypool
 167              assert_equal(self.nodes[1].getaddressinfo(addr)['hdkeypath'], 'm/0\'/0\'/0\'')
 168              self.nodes[1].keypoolrefill(1)  # Fill keypool with 1 key
 169  
 170              # Set a new HD seed on node 1 without flushing the keypool
 171              new_seed = self.nodes[0].dumpprivkey(self.nodes[0].getnewaddress())
 172              orig_masterkeyid = new_masterkeyid
 173              self.nodes[1].sethdseed(False, new_seed)
 174              new_masterkeyid = self.nodes[1].getwalletinfo()['hdseedid']
 175              assert orig_masterkeyid != new_masterkeyid
 176              addr = self.nodes[1].getnewaddress()
 177              assert_equal(orig_masterkeyid, self.nodes[1].getaddressinfo(addr)['hdseedid'])
 178              # Make sure the new address continues previous keypool
 179              assert_equal(self.nodes[1].getaddressinfo(addr)['hdkeypath'], 'm/0\'/0\'/1\'')
 180  
 181              # Check that the next address is from the new seed
 182              self.nodes[1].keypoolrefill(1)
 183              next_addr = self.nodes[1].getnewaddress()
 184              assert_equal(new_masterkeyid, self.nodes[1].getaddressinfo(next_addr)['hdseedid'])
 185              # Make sure the new address is not from previous keypool
 186              assert_equal(self.nodes[1].getaddressinfo(next_addr)['hdkeypath'], 'm/0\'/0\'/0\'')
 187              assert next_addr != addr
 188  
 189              # Sethdseed parameter validity
 190              assert_raises_rpc_error(-1, 'sethdseed', self.nodes[0].sethdseed, False, new_seed, 0)
 191              assert_raises_rpc_error(-5, "Invalid private key", self.nodes[1].sethdseed, False, "not_wif")
 192              assert_raises_rpc_error(-3, "JSON value of type string is not of expected type bool", self.nodes[1].sethdseed, "Not_bool")
 193              assert_raises_rpc_error(-3, "JSON value of type bool is not of expected type string", self.nodes[1].sethdseed, False, True)
 194              assert_raises_rpc_error(-5, "Already have this key", self.nodes[1].sethdseed, False, new_seed)
 195              assert_raises_rpc_error(-5, "Already have this key", self.nodes[1].sethdseed, False, self.nodes[1].dumpprivkey(self.nodes[1].getnewaddress()))
 196  
 197              self.log.info('Test sethdseed restoring with keys outside of the initial keypool')
 198              self.generate(self.nodes[0], 10)
 199              # Restart node 1 with keypool of 3 and a different wallet
 200              self.nodes[1].createwallet(wallet_name='origin', blank=True)
 201              self.restart_node(1, extra_args=['-keypool=3', '-wallet=origin'])
 202              self.connect_nodes(0, 1)
 203  
 204              # sethdseed restoring and seeing txs to addresses out of the keypool
 205              origin_rpc = self.nodes[1].get_wallet_rpc('origin')
 206              seed = self.nodes[0].dumpprivkey(self.nodes[0].getnewaddress())
 207              origin_rpc.sethdseed(True, seed)
 208  
 209              self.nodes[1].createwallet(wallet_name='restore', blank=True)
 210              restore_rpc = self.nodes[1].get_wallet_rpc('restore')
 211              restore_rpc.sethdseed(True, seed)  # Set to be the same seed as origin_rpc
 212              restore_rpc.sethdseed(True)  # Rotate to a new seed, making original `seed` inactive
 213  
 214              self.nodes[1].createwallet(wallet_name='restore2', blank=True)
 215              restore2_rpc = self.nodes[1].get_wallet_rpc('restore2')
 216              restore2_rpc.sethdseed(True, seed)  # Set to be the same seed as origin_rpc
 217              restore2_rpc.sethdseed(True)  # Rotate to a new seed, making original `seed` inactive
 218  
 219              # Check persistence of inactive seed by reloading restore. restore2 is still loaded to test the case where the wallet is not reloaded
 220              restore_rpc.unloadwallet()
 221              self.nodes[1].loadwallet('restore')
 222              restore_rpc = self.nodes[1].get_wallet_rpc('restore')
 223  
 224              # Empty origin keypool and get an address that is beyond the initial keypool
 225              origin_rpc.getnewaddress()
 226              origin_rpc.getnewaddress()
 227              last_addr = origin_rpc.getnewaddress()  # Last address of initial keypool
 228              addr = origin_rpc.getnewaddress()  # First address beyond initial keypool
 229  
 230              # Check that the restored seed has last_addr but does not have addr
 231              info = restore_rpc.getaddressinfo(last_addr)
 232              assert_equal(info['ismine'], True)
 233              info = restore_rpc.getaddressinfo(addr)
 234              assert_equal(info['ismine'], False)
 235              info = restore2_rpc.getaddressinfo(last_addr)
 236              assert_equal(info['ismine'], True)
 237              info = restore2_rpc.getaddressinfo(addr)
 238              assert_equal(info['ismine'], False)
 239              # Check that the origin seed has addr
 240              info = origin_rpc.getaddressinfo(addr)
 241              assert_equal(info['ismine'], True)
 242  
 243              # Send a transaction to addr, which is out of the initial keypool.
 244              # The wallet that has set a new seed (restore_rpc) should not detect this transaction.
 245              txid = self.nodes[0].sendtoaddress(addr, 1)
 246              origin_rpc.sendrawtransaction(self.nodes[0].gettransaction(txid)['hex'])
 247              self.generate(self.nodes[0], 1)
 248              origin_rpc.gettransaction(txid)
 249              assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', restore_rpc.gettransaction, txid)
 250              out_of_kp_txid = txid
 251  
 252              # Send a transaction to last_addr, which is in the initial keypool.
 253              # The wallet that has set a new seed (restore_rpc) should detect this transaction and generate 3 new keys from the initial seed.
 254              # The previous transaction (out_of_kp_txid) should still not be detected as a rescan is required.
 255              txid = self.nodes[0].sendtoaddress(last_addr, 1)
 256              origin_rpc.sendrawtransaction(self.nodes[0].gettransaction(txid)['hex'])
 257              self.generate(self.nodes[0], 1)
 258              origin_rpc.gettransaction(txid)
 259              restore_rpc.gettransaction(txid)
 260              assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', restore_rpc.gettransaction, out_of_kp_txid)
 261              restore2_rpc.gettransaction(txid)
 262              assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', restore2_rpc.gettransaction, out_of_kp_txid)
 263  
 264              # After rescanning, restore_rpc should now see out_of_kp_txid and generate an additional key.
 265              # addr should now be part of restore_rpc and be ismine
 266              restore_rpc.rescanblockchain()
 267              restore_rpc.gettransaction(out_of_kp_txid)
 268              info = restore_rpc.getaddressinfo(addr)
 269              assert_equal(info['ismine'], True)
 270              restore2_rpc.rescanblockchain()
 271              restore2_rpc.gettransaction(out_of_kp_txid)
 272              info = restore2_rpc.getaddressinfo(addr)
 273              assert_equal(info['ismine'], True)
 274  
 275              # Check again that 3 keys were derived.
 276              # Empty keypool and get an address that is beyond the initial keypool
 277              origin_rpc.getnewaddress()
 278              origin_rpc.getnewaddress()
 279              last_addr = origin_rpc.getnewaddress()
 280              addr = origin_rpc.getnewaddress()
 281  
 282              # Check that the restored seed has last_addr but does not have addr
 283              info = restore_rpc.getaddressinfo(last_addr)
 284              assert_equal(info['ismine'], True)
 285              info = restore_rpc.getaddressinfo(addr)
 286              assert_equal(info['ismine'], False)
 287              info = restore2_rpc.getaddressinfo(last_addr)
 288              assert_equal(info['ismine'], True)
 289              info = restore2_rpc.getaddressinfo(addr)
 290              assert_equal(info['ismine'], False)
 291  
 292  
 293  if __name__ == '__main__':
 294      WalletHDTest(__file__).main()
 295