wallet_backwards_compatibility.py raw

   1  #!/usr/bin/env python3
   2  # Copyright (c) 2018-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  """Backwards compatibility functional test
   6  
   7  Test various backwards compatibility scenarios. Requires previous releases binaries,
   8  see test/README.md.
   9  
  10  Due to RPC changes introduced in various versions the below tests
  11  won't work for older versions without some patches or workarounds.
  12  
  13  Use only the latest patch version of each release, unless a test specifically
  14  needs an older patch version.
  15  """
  16  
  17  import os
  18  import shutil
  19  
  20  from test_framework.blocktools import COINBASE_MATURITY
  21  from test_framework.test_framework import LimenkaTestFramework
  22  from test_framework.descriptors import descsum_create
  23  
  24  from test_framework.util import (
  25      assert_equal,
  26      assert_raises_rpc_error,
  27  )
  28  
  29  
  30  class BackwardsCompatibilityTest(LimenkaTestFramework):
  31      def add_options(self, parser):
  32          self.add_wallet_options(parser)
  33  
  34      def set_test_params(self):
  35          self.setup_clean_chain = True
  36          self.num_nodes = 11
  37          # Add new version after each release:
  38          self.extra_args = [
  39              ["-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # Pre-release: use to mine blocks. noban for immediate tx relay
  40              ["-nowallet", "-walletrbf=1", "-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # Pre-release: use to receive coins, swap wallets, etc
  41              ["-nowallet", "-walletrbf=1", "-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # v25.0
  42              ["-nowallet", "-walletrbf=1", "-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # v24.0.1
  43              ["-nowallet", "-walletrbf=1", "-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # v23.0
  44              ["-nowallet", "-walletrbf=1", "-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # v22.0
  45              ["-nowallet", "-walletrbf=1", "-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # v0.21.0
  46              ["-nowallet", "-walletrbf=1", "-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # v0.20.1
  47              ["-nowallet", "-walletrbf=1", "-addresstype=bech32", "-whitelist=noban@127.0.0.1"], # v0.19.1
  48              ["-nowallet", "-walletrbf=1", "-addresstype=bech32", "-whitelist=127.0.0.1"], # v0.18.1
  49              ["-nowallet", "-walletrbf=1", "-addresstype=bech32", "-whitelist=127.0.0.1"], # v0.17.2
  50          ]
  51          self.wallet_names = [self.default_wallet_name]
  52  
  53      def skip_test_if_missing_module(self):
  54          self.skip_if_no_wallet()
  55          self.skip_if_no_previous_releases()
  56  
  57      def setup_nodes(self):
  58          self.add_nodes(self.num_nodes, extra_args=self.extra_args, versions=[
  59              None,
  60              None,
  61              250000,
  62              240001,
  63              230000,
  64              220000,
  65              210000,
  66              200100,
  67              190100,
  68              180100,
  69              170200,
  70          ])
  71  
  72          self.start_nodes()
  73          self.import_deterministic_coinbase_privkeys()
  74  
  75      def split_version(self, node):
  76          major = node.version // 10000
  77          minor = (node.version % 10000) // 100
  78          patch = (node.version % 100)
  79          return (major, minor, patch)
  80  
  81      def major_version_equals(self, node, major):
  82          node_major, _, _ = self.split_version(node)
  83          return node_major == major
  84  
  85      def major_version_less_than(self, node, major):
  86          node_major, _, _ = self.split_version(node)
  87          return node_major < major
  88  
  89      def major_version_at_least(self, node, major):
  90          node_major, _, _ = self.split_version(node)
  91          return node_major >= major
  92  
  93      def test_v19_addmultisigaddress(self):
  94          if not self.is_bdb_compiled():
  95              return
  96          # Specific test for addmultisigaddress using v19
  97          # See #18075
  98          self.log.info("Testing 0.19 addmultisigaddress case (#18075)")
  99          node_master = self.nodes[1]
 100          node_v19 = self.nodes[self.num_nodes - 4]
 101          node_v19.rpc.createwallet(wallet_name="w1_v19")
 102          wallet = node_v19.get_wallet_rpc("w1_v19")
 103          info = wallet.getwalletinfo()
 104          assert info['private_keys_enabled']
 105          assert info['keypoolsize'] > 0
 106          # Use addmultisigaddress (see #18075)
 107          address_18075 = wallet.rpc.addmultisigaddress(1, ["0296b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52", "037211a824f55b505228e4c3d5194c1fcfaa15a456abdf37f9b9d97a4040afc073"], "", "legacy")["address"]
 108          assert wallet.getaddressinfo(address_18075)["solvable"]
 109          node_v19.unloadwallet("w1_v19")
 110  
 111          # Copy the 0.19 wallet to the last Limenka version and open it:
 112          shutil.copytree(
 113              os.path.join(node_v19.wallets_path, "w1_v19"),
 114              os.path.join(node_master.wallets_path, "w1_v19")
 115          )
 116          node_master.loadwallet("w1_v19")
 117          wallet = node_master.get_wallet_rpc("w1_v19")
 118          assert wallet.getaddressinfo(address_18075)["solvable"]
 119  
 120          # Now copy that same wallet back to 0.19 to make sure no automatic upgrade breaks it
 121          node_master.unloadwallet("w1_v19")
 122          shutil.rmtree(os.path.join(node_v19.wallets_path, "w1_v19"))
 123          shutil.copytree(
 124              os.path.join(node_master.wallets_path, "w1_v19"),
 125              os.path.join(node_v19.wallets_path, "w1_v19")
 126          )
 127          node_v19.loadwallet("w1_v19")
 128          wallet = node_v19.get_wallet_rpc("w1_v19")
 129          assert wallet.getaddressinfo(address_18075)["solvable"]
 130  
 131      def run_test(self):
 132          node_miner = self.nodes[0]
 133          node_master = self.nodes[1]
 134          node_v21 = self.nodes[self.num_nodes - 5]
 135          node_v17 = self.nodes[self.num_nodes - 1]
 136  
 137          legacy_nodes = self.nodes[2:] # Nodes that support legacy wallets
 138          legacy_only_nodes = self.nodes[-4:] # Nodes that only support legacy wallets
 139          descriptors_nodes = self.nodes[2:-4] # Nodes that support descriptor wallets
 140  
 141          self.generatetoaddress(node_miner, COINBASE_MATURITY + 1, node_miner.getnewaddress())
 142  
 143          # Sanity check the test framework:
 144          res = node_v17.getblockchaininfo()
 145          assert_equal(res['blocks'], COINBASE_MATURITY + 1)
 146  
 147          self.log.info("Test wallet backwards compatibility...")
 148          # Create a number of wallets and open them in older versions:
 149  
 150          # w1: regular wallet, created on master: update this test when default
 151          #     wallets can no longer be opened by older versions.
 152          node_master.createwallet(wallet_name="w1")
 153          wallet = node_master.get_wallet_rpc("w1")
 154          info = wallet.getwalletinfo()
 155          assert info['private_keys_enabled']
 156          assert info['keypoolsize'] > 0
 157          # Create a confirmed transaction, receiving coins
 158          address = wallet.getnewaddress()
 159          node_miner.sendtoaddress(address, 10)
 160          self.sync_mempools()
 161          self.generate(node_miner, 1)
 162          # Create a conflicting transaction using RBF
 163          return_address = node_miner.getnewaddress()
 164          tx1_id = node_master.sendtoaddress(return_address, 1)
 165          tx2_id = node_master.bumpfee(tx1_id)["txid"]
 166          # Confirm the transaction
 167          self.sync_mempools()
 168          self.generate(node_miner, 1)
 169          # Create another conflicting transaction using RBF
 170          tx3_id = node_master.sendtoaddress(return_address, 1)
 171          tx4_id = node_master.bumpfee(tx3_id)["txid"]
 172          self.sync_mempools()
 173          # Abandon transaction, but don't confirm
 174          node_master.abandontransaction(tx3_id)
 175  
 176          # w2: wallet with private keys disabled, created on master: update this
 177          #     test when default wallets private keys disabled can no longer be
 178          #     opened by older versions.
 179          node_master.createwallet(wallet_name="w2", disable_private_keys=True)
 180          wallet = node_master.get_wallet_rpc("w2")
 181          info = wallet.getwalletinfo()
 182          assert info['private_keys_enabled'] == False
 183          assert info['keypoolsize'] == 0
 184  
 185          # w3: blank wallet, created on master: update this
 186          #     test when default blank wallets can no longer be opened by older versions.
 187          node_master.createwallet(wallet_name="w3", blank=True)
 188          wallet = node_master.get_wallet_rpc("w3")
 189          info = wallet.getwalletinfo()
 190          assert info['private_keys_enabled']
 191          assert info['keypoolsize'] == 0
 192  
 193          # Unload wallets and copy to older nodes:
 194          node_master_wallets_dir = node_master.wallets_path
 195          node_master.unloadwallet("w1")
 196          node_master.unloadwallet("w2")
 197          node_master.unloadwallet("w3")
 198  
 199          for node in legacy_nodes:
 200              # Copy wallets to previous version
 201              for wallet in os.listdir(node_master_wallets_dir):
 202                  dest = node.wallets_path / wallet
 203                  source = node_master_wallets_dir / wallet
 204                  if self.major_version_equals(node, 16):
 205                      # 0.16 node expect the wallet to be in the wallet dir but as a plain file rather than in directories
 206                      shutil.copyfile(source / "wallet.dat", dest)
 207                  else:
 208                      shutil.copytree(source, dest)
 209  
 210          self.test_v19_addmultisigaddress()
 211  
 212          self.log.info("Test that a wallet made on master can be opened on:")
 213          # In descriptors wallet mode, run this test on the nodes that support descriptor wallets
 214          # In legacy wallets mode, run this test on the nodes that support legacy wallets
 215          for node in descriptors_nodes if self.options.descriptors else legacy_nodes:
 216              self.log.info(f"- {node.version}")
 217              for wallet_name in ["w1", "w2", "w3"]:
 218                  if self.major_version_less_than(node, 18) and wallet_name == "w3":
 219                      # Blank wallets were introduced in v0.18.0. We test the loading error below.
 220                      continue
 221                  if self.major_version_less_than(node, 22) and wallet_name == "w1" and self.options.descriptors:
 222                      # Descriptor wallets created after 0.21 have taproot descriptors which 0.21 does not support, tested below
 223                      continue
 224                  # Also try to reopen on master after opening on old
 225                  for n in [node, node_master]:
 226                      n.loadwallet(wallet_name)
 227                      wallet = n.get_wallet_rpc(wallet_name)
 228                      info = wallet.getwalletinfo()
 229                      if wallet_name == "w1":
 230                          assert info['private_keys_enabled'] == True
 231                          assert info['keypoolsize'] > 0
 232                          txs = wallet.listtransactions()
 233                          assert_equal(len(txs), 5)
 234                          assert_equal(txs[1]["txid"], tx1_id)
 235                          assert_equal(txs[2]["walletconflicts"], [tx1_id])
 236                          assert_equal(txs[1]["replaced_by_txid"], tx2_id)
 237                          assert not txs[1]["abandoned"]
 238                          assert_equal(txs[1]["confirmations"], -1)
 239                          assert_equal(txs[2]["blockindex"], 1)
 240                          assert txs[3]["abandoned"]
 241                          assert_equal(txs[4]["walletconflicts"], [tx3_id])
 242                          assert_equal(txs[3]["replaced_by_txid"], tx4_id)
 243                          assert not hasattr(txs[3], "blockindex")
 244                      elif wallet_name == "w2":
 245                          assert info['private_keys_enabled'] == False
 246                          assert info['keypoolsize'] == 0
 247                      else:
 248                          assert info['private_keys_enabled'] == True
 249                          assert info['keypoolsize'] == 0
 250  
 251                      # Copy back to master
 252                      wallet.unloadwallet()
 253                      if n == node:
 254                          shutil.rmtree(node_master.wallets_path / wallet_name)
 255                          shutil.copytree(
 256                              n.wallets_path / wallet_name,
 257                              node_master.wallets_path / wallet_name,
 258                          )
 259  
 260          # Check that descriptor wallets don't work on legacy only nodes
 261          if self.options.descriptors:
 262              self.log.info("Test descriptor wallet incompatibility on:")
 263              for node in legacy_only_nodes:
 264                  # RPC loadwallet failure causes limenkad to exit in <= 0.17, in addition to the RPC
 265                  # call failure, so the following test won't work:
 266                  # assert_raises_rpc_error(-4, "Wallet loading failed.", node_v17.loadwallet, 'w3')
 267                  if self.major_version_less_than(node, 18):
 268                      continue
 269                  self.log.info(f"- {node.version}")
 270                  # Descriptor wallets appear to be corrupted wallets to old software
 271                  assert self.major_version_at_least(node, 18) and self.major_version_less_than(node, 21)
 272                  for wallet_name in ["w1", "w2", "w3"]:
 273                      assert_raises_rpc_error(-4, "Wallet file verification failed: wallet.dat corrupt, salvage failed", node.loadwallet, wallet_name)
 274  
 275          # Instead, we stop node and try to launch it with the wallet:
 276          self.stop_node(node_v17.index)
 277          if self.options.descriptors:
 278              self.log.info("Test descriptor wallet incompatibility with 0.17")
 279              # Descriptor wallets appear to be corrupted wallets to old software
 280              node_v17.assert_start_raises_init_error(["-wallet=w1"], "Error: wallet.dat corrupt, salvage failed")
 281              node_v17.assert_start_raises_init_error(["-wallet=w2"], "Error: wallet.dat corrupt, salvage failed")
 282              node_v17.assert_start_raises_init_error(["-wallet=w3"], "Error: wallet.dat corrupt, salvage failed")
 283          else:
 284              self.log.info("Test blank wallet incompatibility with v17")
 285              node_v17.assert_start_raises_init_error(["-wallet=w3"], "Error: Error loading w3: Wallet requires newer version of Limenka")
 286          self.start_node(node_v17.index)
 287  
 288          # When descriptors are enabled, w1 cannot be opened by 0.21 since it contains a taproot descriptor
 289          if self.options.descriptors:
 290              self.log.info("Test that 0.21 cannot open wallet containing tr() descriptors")
 291              assert_raises_rpc_error(-1, "map::at", node_v21.loadwallet, "w1")
 292  
 293          self.log.info("Test that a wallet can upgrade to and downgrade from master, from:")
 294          for node in descriptors_nodes if self.options.descriptors else legacy_nodes:
 295              self.log.info(f"- {node.version}")
 296              wallet_name = f"up_{node.version}"
 297              if self.major_version_less_than(node, 17):
 298                  # createwallet is only available in 0.17+
 299                  self.restart_node(node.index, extra_args=[f"-wallet={wallet_name}"])
 300                  wallet_prev = node.get_wallet_rpc(wallet_name)
 301                  address = wallet_prev.getnewaddress('', "bech32")
 302                  addr_info = wallet_prev.validateaddress(address)
 303              else:
 304                  if self.major_version_at_least(node, 21):
 305                      node.rpc.createwallet(wallet_name=wallet_name, descriptors=self.options.descriptors)
 306                  else:
 307                      node.rpc.createwallet(wallet_name=wallet_name)
 308                  wallet_prev = node.get_wallet_rpc(wallet_name)
 309                  address = wallet_prev.getnewaddress('', "bech32")
 310                  addr_info = wallet_prev.getaddressinfo(address)
 311  
 312              hdkeypath = addr_info["hdkeypath"].replace("'", "h")
 313              pubkey = addr_info["pubkey"]
 314  
 315              # Make a backup of the wallet file
 316              backup_path = os.path.join(self.options.tmpdir, f"{wallet_name}.dat")
 317              wallet_prev.backupwallet(backup_path)
 318  
 319              # Remove the wallet from old node
 320              if self.major_version_at_least(node, 17):
 321                  wallet_prev.unloadwallet()
 322              else:
 323                  self.stop_node(node.index)
 324  
 325              # Restore the wallet to master
 326              load_res = node_master.restorewallet(wallet_name, backup_path)
 327  
 328              # Make sure this wallet opens without warnings
 329              if not self.options.descriptors:
 330                  assert "warnings" not in load_res
 331              else:
 332                  assert "warnings" not in load_res
 333  
 334              wallet = node_master.get_wallet_rpc(wallet_name)
 335              info = wallet.getaddressinfo(address)
 336              descriptor = f"wpkh([{info['hdmasterfingerprint']}{hdkeypath[1:]}]{pubkey})"
 337              assert_equal(info["desc"], descsum_create(descriptor))
 338  
 339              # Make backup so the wallet can be copied back to old node
 340              down_wallet_name = f"re_down_{node.version}"
 341              down_backup_path = os.path.join(self.options.tmpdir, f"{down_wallet_name}.dat")
 342              wallet.backupwallet(down_backup_path)
 343  
 344              # Check that taproot descriptors can be added to 0.21 wallets
 345              # This must be done after the backup is created so that 0.21 can still load
 346              # the backup
 347              if self.options.descriptors and self.major_version_equals(node, 21):
 348                  assert_raises_rpc_error(-12, "No bech32m addresses available", wallet.getnewaddress, address_type="bech32m")
 349                  xpubs = wallet.gethdkeys(active_only=True)
 350                  assert_equal(len(xpubs), 1)
 351                  assert_equal(len(xpubs[0]["descriptors"]), 6)
 352                  wallet.createwalletdescriptor("bech32m")
 353                  xpubs = wallet.gethdkeys(active_only=True)
 354                  assert_equal(len(xpubs), 1)
 355                  assert_equal(len(xpubs[0]["descriptors"]), 8)
 356                  tr_descs = [desc["desc"] for desc in xpubs[0]["descriptors"] if desc["desc"].startswith("tr(")]
 357                  assert_equal(len(tr_descs), 2)
 358                  for desc in tr_descs:
 359                      assert info["hdmasterfingerprint"] in desc
 360                  wallet.getnewaddress(address_type="bech32m")
 361  
 362              wallet.unloadwallet()
 363  
 364              # Check that no automatic upgrade broke the downgrading the wallet
 365              if self.major_version_less_than(node, 17):
 366                  # loadwallet is only available in 0.17+
 367                  shutil.copyfile(
 368                      down_backup_path,
 369                      node.wallets_path / down_wallet_name
 370                  )
 371                  self.start_node(node.index, extra_args=[f"-wallet={down_wallet_name}"])
 372                  wallet_res = node.get_wallet_rpc(down_wallet_name)
 373                  info = wallet_res.validateaddress(address)
 374                  assert_equal(info, addr_info)
 375              else:
 376                  target_dir = node.wallets_path / down_wallet_name
 377                  os.makedirs(target_dir, exist_ok=True)
 378                  shutil.copyfile(
 379                      down_backup_path,
 380                      target_dir / "wallet.dat"
 381                  )
 382                  node.loadwallet(down_wallet_name)
 383                  wallet_res = node.get_wallet_rpc(down_wallet_name)
 384                  info = wallet_res.getaddressinfo(address)
 385                  assert_equal(info, addr_info)
 386  
 387  if __name__ == '__main__':
 388      BackwardsCompatibilityTest(__file__).main()
 389