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