wallet_inactive_hdchains.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2021-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  """
   6  Test Inactive HD Chains.
   7  """
   8  import shutil
   9  
  10  from test_framework.authproxy import JSONRPCException
  11  from test_framework.test_framework import LimenkaTestFramework
  12  from test_framework.wallet_util import (
  13      get_generate_key,
  14  )
  15  
  16  
  17  class InactiveHDChainsTest(LimenkaTestFramework):
  18      def add_options(self, parser):
  19          self.add_wallet_options(parser, descriptors=False)
  20  
  21      def set_test_params(self):
  22          self.setup_clean_chain = True
  23          self.num_nodes = 2
  24          self.extra_args = [["-keypool=10"], ["-nowallet", "-keypool=10"]]
  25  
  26      def skip_test_if_missing_module(self):
  27          self.skip_if_no_wallet()
  28          self.skip_if_no_bdb()
  29          self.skip_if_no_previous_releases()
  30  
  31      def setup_nodes(self):
  32          self.add_nodes(self.num_nodes, extra_args=self.extra_args, versions=[
  33              None,
  34              170200, # 0.17.2 Does not have the key metadata upgrade
  35          ])
  36  
  37          self.start_nodes()
  38          self.init_wallet(node=0)
  39  
  40      def prepare_wallets(self, wallet_basename, encrypt=False):
  41          self.nodes[0].createwallet(wallet_name=f"{wallet_basename}_base", descriptors=False, blank=True)
  42          self.nodes[0].createwallet(wallet_name=f"{wallet_basename}_test", descriptors=False, blank=True)
  43          base_wallet = self.nodes[0].get_wallet_rpc(f"{wallet_basename}_base")
  44          test_wallet = self.nodes[0].get_wallet_rpc(f"{wallet_basename}_test")
  45  
  46          # Setup both wallets with the same HD seed
  47          seed = get_generate_key()
  48          base_wallet.sethdseed(True, seed.privkey)
  49          test_wallet.sethdseed(True, seed.privkey)
  50  
  51          if encrypt:
  52              # Encrypting will generate a new HD seed and flush the keypool
  53              test_wallet.encryptwallet("pass")
  54          else:
  55              # Generate a new HD seed on the test wallet
  56              test_wallet.sethdseed()
  57  
  58          return base_wallet, test_wallet
  59  
  60      def do_inactive_test(self, base_wallet, test_wallet, encrypt=False):
  61          default = self.nodes[0].get_wallet_rpc(self.default_wallet_name)
  62  
  63          # The first address should be known by both wallets.
  64          addr1 = base_wallet.getnewaddress()
  65          assert test_wallet.getaddressinfo(addr1)["ismine"]
  66          # The address at index 9 is the first address that the test wallet will not know initially
  67          for _ in range(0, 9):
  68              base_wallet.getnewaddress()
  69          addr2 = base_wallet.getnewaddress()
  70          assert not test_wallet.getaddressinfo(addr2)["ismine"]
  71  
  72          # Send to first address on the old seed
  73          txid = default.sendtoaddress(addr1, 10)
  74          self.generate(self.nodes[0], 1)
  75  
  76          # Wait for the test wallet to see the transaction
  77          def is_tx_available(txid):
  78              try:
  79                  test_wallet.gettransaction(txid)
  80                  return True
  81              except JSONRPCException:
  82                  return False
  83          self.nodes[0].wait_until(lambda: is_tx_available(txid), timeout=10, check_interval=0.1)
  84  
  85          if encrypt:
  86              # The test wallet will not be able to generate the topped up keypool
  87              # until it is unlocked. So it still should not know about the second address
  88              assert not test_wallet.getaddressinfo(addr2)["ismine"]
  89              test_wallet.walletpassphrase("pass", 1)
  90  
  91          # The test wallet should now know about the second address as it
  92          # should have generated it in the inactive chain's keypool
  93          assert test_wallet.getaddressinfo(addr2)["ismine"]
  94  
  95          # Send to second address on the old seed
  96          txid = default.sendtoaddress(addr2, 10)
  97          self.generate(self.nodes[0], 1)
  98          test_wallet.gettransaction(txid)
  99  
 100      def test_basic(self):
 101          self.log.info("Test basic case for inactive HD chains")
 102          self.do_inactive_test(*self.prepare_wallets("basic"))
 103  
 104      def test_encrypted_wallet(self):
 105          self.log.info("Test inactive HD chains when wallet is encrypted")
 106          self.do_inactive_test(*self.prepare_wallets("enc", encrypt=True), encrypt=True)
 107  
 108      def test_without_upgraded_keymeta(self):
 109          # Test that it is possible to top up inactive hd chains even if there is no key origin
 110          # in CKeyMetadata. This tests for the segfault reported in
 111          # https://github.com/limenka/limenka/issues/21605
 112          self.log.info("Test that topping up inactive HD chains does not need upgraded key origin")
 113  
 114          self.nodes[0].createwallet(wallet_name="keymeta_base", descriptors=False, blank=True)
 115          # Createwallet is overridden in the test framework so that the descriptor option can be filled
 116          # depending on the test's cli args. However we don't want to do that when using old nodes that
 117          # do not support descriptors. So we use the createwallet_passthrough function.
 118          self.nodes[1].createwallet_passthrough(wallet_name="keymeta_test")
 119          base_wallet = self.nodes[0].get_wallet_rpc("keymeta_base")
 120          test_wallet = self.nodes[1].get_wallet_rpc("keymeta_test")
 121  
 122          # Setup both wallets with the same HD seed
 123          seed = get_generate_key()
 124          base_wallet.sethdseed(True, seed.privkey)
 125          test_wallet.sethdseed(True, seed.privkey)
 126  
 127          # Encrypting will generate a new HD seed and flush the keypool
 128          test_wallet.encryptwallet("pass")
 129  
 130          # Copy test wallet to node 0
 131          test_wallet.unloadwallet()
 132          test_wallet_dir = self.nodes[1].wallets_path / "keymeta_test"
 133          new_test_wallet_dir = self.nodes[0].wallets_path / "keymeta_test"
 134          shutil.copytree(test_wallet_dir, new_test_wallet_dir)
 135          self.nodes[0].loadwallet("keymeta_test")
 136          test_wallet = self.nodes[0].get_wallet_rpc("keymeta_test")
 137  
 138          self.do_inactive_test(base_wallet, test_wallet, encrypt=True)
 139  
 140      def run_test(self):
 141          self.generate(self.nodes[0], 101)
 142  
 143          self.test_basic()
 144          self.test_encrypted_wallet()
 145          self.test_without_upgraded_keymeta()
 146  
 147  
 148  if __name__ == '__main__':
 149      InactiveHDChainsTest(__file__).main()
 150