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()