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