/ test / functional / wallet_hd.py
wallet_hd.py
  1  #!/usr/bin/env python3
  2  # Copyright (c) 2016-2022 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.test_framework import BitcoinTestFramework
 11  from test_framework.util import (
 12      assert_equal,
 13      wallet_importprivkey,
 14  )
 15  
 16  
 17  class WalletHDTest(BitcoinTestFramework):
 18      def set_test_params(self):
 19          self.setup_clean_chain = True
 20          self.num_nodes = 2
 21          self.extra_args = [[], ['-keypool=0']]
 22          # whitelist peers to speed up tx relay / mempool sync
 23          self.noban_tx_relay = True
 24  
 25      def skip_test_if_missing_module(self):
 26          self.skip_if_no_wallet()
 27  
 28      def run_test(self):
 29          # Make sure we use hd, keep masterkeyid
 30          hd_fingerprint = self.nodes[1].getaddressinfo(self.nodes[1].getnewaddress())['hdmasterfingerprint']
 31          assert_equal(len(hd_fingerprint), 8)
 32  
 33          # create an internal key
 34          change_addr = self.nodes[1].getrawchangeaddress()
 35          change_addrV = self.nodes[1].getaddressinfo(change_addr)
 36          assert_equal(change_addrV["hdkeypath"], "m/84h/1h/0h/1/0")
 37  
 38          # Import a non-HD private key in the HD wallet
 39          non_hd_add = 'bcrt1qmevj8zfx0wdvp05cqwkmr6mxkfx60yezwjksmt'
 40          non_hd_key = 'cS9umN9w6cDMuRVYdbkfE4c7YUFLJRoXMfhQ569uY4odiQbVN8Rt'
 41          wallet_importprivkey(self.nodes[1], non_hd_key, "now")
 42  
 43          # This should be enough to keep the master key and the non-HD key
 44          self.nodes[1].backupwallet(self.nodes[1].datadir_path / "hd.bak")
 45  
 46          # Derive some HD addresses and remember the last
 47          # Also send funds to each add
 48          self.generate(self.nodes[0], COINBASE_MATURITY + 1)
 49          hd_add = None
 50          NUM_HD_ADDS = 10
 51          for i in range(1, NUM_HD_ADDS + 1):
 52              hd_add = self.nodes[1].getnewaddress()
 53              hd_info = self.nodes[1].getaddressinfo(hd_add)
 54              assert_equal(hd_info["hdkeypath"], "m/84h/1h/0h/0/" + str(i))
 55              assert_equal(hd_info["hdmasterfingerprint"], hd_fingerprint)
 56              self.nodes[0].sendtoaddress(hd_add, 1)
 57              self.generate(self.nodes[0], 1)
 58          self.nodes[0].sendtoaddress(non_hd_add, 1)
 59          self.generate(self.nodes[0], 1)
 60  
 61          # create an internal key (again)
 62          change_addr = self.nodes[1].getrawchangeaddress()
 63          change_addrV = self.nodes[1].getaddressinfo(change_addr)
 64          assert_equal(change_addrV["hdkeypath"], "m/84h/1h/0h/1/1")
 65  
 66          self.sync_all()
 67          assert_equal(self.nodes[1].getbalance(), NUM_HD_ADDS + 1)
 68  
 69          self.log.info("Restore backup ...")
 70          self.stop_node(1)
 71          # we need to delete the complete chain directory
 72          # otherwise node1 would auto-recover all funds in flag the keypool keys as used
 73          shutil.rmtree(self.nodes[1].blocks_path)
 74          shutil.rmtree(self.nodes[1].chain_path / "chainstate")
 75          shutil.copyfile(
 76              self.nodes[1].datadir_path / "hd.bak",
 77              self.nodes[1].wallets_path / self.default_wallet_name / self.wallet_data_filename
 78          )
 79          self.start_node(1)
 80  
 81          # Assert that derivation is deterministic
 82          hd_add_2 = None
 83          for i in range(1, NUM_HD_ADDS + 1):
 84              hd_add_2 = self.nodes[1].getnewaddress()
 85              hd_info_2 = self.nodes[1].getaddressinfo(hd_add_2)
 86              assert_equal(hd_info_2["hdkeypath"], "m/84h/1h/0h/0/" + str(i))
 87              assert_equal(hd_info_2["hdmasterfingerprint"], hd_fingerprint)
 88          assert_equal(hd_add, hd_add_2)
 89          self.connect_nodes(0, 1)
 90          self.sync_all()
 91  
 92          # Needs rescan
 93          self.nodes[1].rescanblockchain()
 94          assert_equal(self.nodes[1].getbalance(), NUM_HD_ADDS + 1)
 95  
 96          # Try a RPC based rescan
 97          self.stop_node(1)
 98          shutil.rmtree(self.nodes[1].blocks_path)
 99          shutil.rmtree(self.nodes[1].chain_path / "chainstate")
100          shutil.copyfile(
101              self.nodes[1].datadir_path / "hd.bak",
102              self.nodes[1].wallets_path / self.default_wallet_name / self.wallet_data_filename
103          )
104          self.start_node(1, extra_args=self.extra_args[1])
105          self.connect_nodes(0, 1)
106          self.sync_all()
107          # Wallet automatically scans blocks older than key on startup
108          assert_equal(self.nodes[1].getbalance(), NUM_HD_ADDS + 1)
109          out = self.nodes[1].rescanblockchain(0, 1)
110          assert_equal(out['start_height'], 0)
111          assert_equal(out['stop_height'], 1)
112          out = self.nodes[1].rescanblockchain()
113          assert_equal(out['start_height'], 0)
114          assert_equal(out['stop_height'], self.nodes[1].getblockcount())
115          assert_equal(self.nodes[1].getbalance(), NUM_HD_ADDS + 1)
116  
117          # send a tx and make sure its using the internal chain for the changeoutput
118          txid = self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), 1)
119          outs = self.nodes[1].gettransaction(txid=txid, verbose=True)['decoded']['vout']
120          keypath = ""
121          for out in outs:
122              if out['value'] != 1:
123                  keypath = self.nodes[1].getaddressinfo(out['scriptPubKey']['address'])['hdkeypath']
124  
125          assert_equal(keypath[0:14], "m/84h/1h/0h/1/")
126  
127  
128  if __name__ == '__main__':
129      WalletHDTest(__file__).main()