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