1 #!/usr/bin/env python3
2 # Copyright (c) 2016-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 """Test Hierarchical Deterministic wallet function."""
6 7 import shutil
8 9 from test_framework.blocktools import COINBASE_MATURITY
10 from test_framework.test_framework import LimenkaTestFramework
11 from test_framework.util import (
12 assert_equal,
13 assert_raises_rpc_error,
14 )
15 16 17 class WalletHDTest(LimenkaTestFramework):
18 def add_options(self, parser):
19 self.add_wallet_options(parser)
20 21 def set_test_params(self):
22 self.setup_clean_chain = True
23 self.num_nodes = 2
24 self.extra_args = [[], ['-keypool=0']]
25 # whitelist peers to speed up tx relay / mempool sync
26 self.noban_tx_relay = True
27 28 self.supports_cli = False
29 30 def skip_test_if_missing_module(self):
31 self.skip_if_no_wallet()
32 33 def run_test(self):
34 # Make sure we use hd, keep masterkeyid
35 hd_fingerprint = self.nodes[1].getaddressinfo(self.nodes[1].getnewaddress())['hdmasterfingerprint']
36 assert_equal(len(hd_fingerprint), 8)
37 38 # create an internal key
39 change_addr = self.nodes[1].getrawchangeaddress()
40 change_addrV = self.nodes[1].getaddressinfo(change_addr)
41 if self.options.descriptors:
42 assert_equal(change_addrV["hdkeypath"], "m/84h/1h/0h/1/0")
43 44 # Exporting the master private key should fail on a descriptor wallet
45 assert_raises_rpc_error(-4, "Only legacy wallets are supported by this command", self.nodes[1].dumpmasterprivkey)
46 else:
47 assert_equal(change_addrV["hdkeypath"], "m/0'/1'/0'") #first internal child key
48 49 # Check that the exported master private key begins with tprv
50 xprv = self.nodes[1].dumpmasterprivkey()
51 assert_equal(xprv[0:4], "tprv")
52 53 # Exporting the master private key should fail on a non-HD wallet
54 # FIXME: No way to make non-HD wallets anymore
55 #assert_raises_rpc_error(-4, "Wallet is not a HD wallet.", self.nodes[0].dumpmasterprivkey)
56 57 # Import a non-HD private key in the HD wallet
58 non_hd_add = 'bcrt1qmevj8zfx0wdvp05cqwkmr6mxkfx60yezwjksmt'
59 non_hd_key = 'cS9umN9w6cDMuRVYdbkfE4c7YUFLJRoXMfhQ569uY4odiQbVN8Rt'
60 self.nodes[1].importprivkey(non_hd_key)
61 62 # This should be enough to keep the master key and the non-HD key
63 self.nodes[1].backupwallet(self.nodes[1].datadir_path / "hd.bak")
64 #self.nodes[1].dumpwallet(self.nodes[1].datadir_path / "hd.dump")
65 66 # Derive some HD addresses and remember the last
67 # Also send funds to each add
68 self.generate(self.nodes[0], COINBASE_MATURITY + 1)
69 hd_add = None
70 NUM_HD_ADDS = 10
71 for i in range(1, NUM_HD_ADDS + 1):
72 hd_add = self.nodes[1].getnewaddress()
73 hd_info = self.nodes[1].getaddressinfo(hd_add)
74 if self.options.descriptors:
75 assert_equal(hd_info["hdkeypath"], "m/84h/1h/0h/0/" + str(i))
76 else:
77 assert_equal(hd_info["hdkeypath"], "m/0'/0'/" + str(i) + "'")
78 assert_equal(hd_info["hdmasterfingerprint"], hd_fingerprint)
79 self.nodes[0].sendtoaddress(hd_add, 1)
80 self.generate(self.nodes[0], 1)
81 self.nodes[0].sendtoaddress(non_hd_add, 1)
82 self.generate(self.nodes[0], 1)
83 84 # create an internal key (again)
85 change_addr = self.nodes[1].getrawchangeaddress()
86 change_addrV = self.nodes[1].getaddressinfo(change_addr)
87 if self.options.descriptors:
88 assert_equal(change_addrV["hdkeypath"], "m/84h/1h/0h/1/1")
89 else:
90 assert_equal(change_addrV["hdkeypath"], "m/0'/1'/1'") #second internal child key
91 92 self.sync_all()
93 assert_equal(self.nodes[1].getbalance(), NUM_HD_ADDS + 1)
94 95 self.log.info("Restore backup ...")
96 self.stop_node(1)
97 # we need to delete the complete chain directory
98 # otherwise node1 would auto-recover all funds in flag the keypool keys as used
99 shutil.rmtree(self.nodes[1].blocks_path)
100 shutil.rmtree(self.nodes[1].chain_path / "chainstate")
101 shutil.copyfile(
102 self.nodes[1].datadir_path / "hd.bak",
103 self.nodes[1].wallets_path / self.default_wallet_name / self.wallet_data_filename
104 )
105 self.start_node(1)
106 107 # Assert that derivation is deterministic
108 hd_add_2 = None
109 for i in range(1, NUM_HD_ADDS + 1):
110 hd_add_2 = self.nodes[1].getnewaddress()
111 hd_info_2 = self.nodes[1].getaddressinfo(hd_add_2)
112 if self.options.descriptors:
113 assert_equal(hd_info_2["hdkeypath"], "m/84h/1h/0h/0/" + str(i))
114 else:
115 assert_equal(hd_info_2["hdkeypath"], "m/0'/0'/" + str(i) + "'")
116 assert_equal(hd_info_2["hdmasterfingerprint"], hd_fingerprint)
117 assert_equal(hd_add, hd_add_2)
118 self.connect_nodes(0, 1)
119 self.sync_all()
120 121 # Needs rescan
122 self.nodes[1].rescanblockchain()
123 assert_equal(self.nodes[1].getbalance(), NUM_HD_ADDS + 1)
124 125 # Try a RPC based rescan
126 self.stop_node(1)
127 shutil.rmtree(self.nodes[1].blocks_path)
128 shutil.rmtree(self.nodes[1].chain_path / "chainstate")
129 shutil.copyfile(
130 self.nodes[1].datadir_path / "hd.bak",
131 self.nodes[1].wallets_path / self.default_wallet_name / self.wallet_data_filename
132 )
133 self.start_node(1, extra_args=self.extra_args[1])
134 self.connect_nodes(0, 1)
135 self.sync_all()
136 # Wallet automatically scans blocks older than key on startup
137 assert_equal(self.nodes[1].getbalance(), NUM_HD_ADDS + 1)
138 out = self.nodes[1].rescanblockchain(0, 1)
139 assert_equal(out['start_height'], 0)
140 assert_equal(out['stop_height'], 1)
141 out = self.nodes[1].rescanblockchain()
142 assert_equal(out['start_height'], 0)
143 assert_equal(out['stop_height'], self.nodes[1].getblockcount())
144 assert_equal(self.nodes[1].getbalance(), NUM_HD_ADDS + 1)
145 146 # send a tx and make sure its using the internal chain for the changeoutput
147 txid = self.nodes[1].sendtoaddress(self.nodes[0].getnewaddress(), 1)
148 outs = self.nodes[1].gettransaction(txid=txid, verbose=True)['decoded']['vout']
149 keypath = ""
150 for out in outs:
151 if out['value'] != 1:
152 keypath = self.nodes[1].getaddressinfo(out['scriptPubKey']['address'])['hdkeypath']
153 154 if self.options.descriptors:
155 assert_equal(keypath[0:14], "m/84h/1h/0h/1/")
156 else:
157 assert_equal(keypath[0:7], "m/0'/1'")
158 159 if not self.options.descriptors:
160 # Generate a new HD seed on node 1 and make sure it is set
161 orig_masterkeyid = self.nodes[1].getwalletinfo()['hdseedid']
162 self.nodes[1].sethdseed()
163 new_masterkeyid = self.nodes[1].getwalletinfo()['hdseedid']
164 assert orig_masterkeyid != new_masterkeyid
165 addr = self.nodes[1].getnewaddress()
166 # Make sure the new address is the first from the keypool
167 assert_equal(self.nodes[1].getaddressinfo(addr)['hdkeypath'], 'm/0\'/0\'/0\'')
168 self.nodes[1].keypoolrefill(1) # Fill keypool with 1 key
169 170 # Set a new HD seed on node 1 without flushing the keypool
171 new_seed = self.nodes[0].dumpprivkey(self.nodes[0].getnewaddress())
172 orig_masterkeyid = new_masterkeyid
173 self.nodes[1].sethdseed(False, new_seed)
174 new_masterkeyid = self.nodes[1].getwalletinfo()['hdseedid']
175 assert orig_masterkeyid != new_masterkeyid
176 addr = self.nodes[1].getnewaddress()
177 assert_equal(orig_masterkeyid, self.nodes[1].getaddressinfo(addr)['hdseedid'])
178 # Make sure the new address continues previous keypool
179 assert_equal(self.nodes[1].getaddressinfo(addr)['hdkeypath'], 'm/0\'/0\'/1\'')
180 181 # Check that the next address is from the new seed
182 self.nodes[1].keypoolrefill(1)
183 next_addr = self.nodes[1].getnewaddress()
184 assert_equal(new_masterkeyid, self.nodes[1].getaddressinfo(next_addr)['hdseedid'])
185 # Make sure the new address is not from previous keypool
186 assert_equal(self.nodes[1].getaddressinfo(next_addr)['hdkeypath'], 'm/0\'/0\'/0\'')
187 assert next_addr != addr
188 189 # Sethdseed parameter validity
190 assert_raises_rpc_error(-1, 'sethdseed', self.nodes[0].sethdseed, False, new_seed, 0)
191 assert_raises_rpc_error(-5, "Invalid private key", self.nodes[1].sethdseed, False, "not_wif")
192 assert_raises_rpc_error(-3, "JSON value of type string is not of expected type bool", self.nodes[1].sethdseed, "Not_bool")
193 assert_raises_rpc_error(-3, "JSON value of type bool is not of expected type string", self.nodes[1].sethdseed, False, True)
194 assert_raises_rpc_error(-5, "Already have this key", self.nodes[1].sethdseed, False, new_seed)
195 assert_raises_rpc_error(-5, "Already have this key", self.nodes[1].sethdseed, False, self.nodes[1].dumpprivkey(self.nodes[1].getnewaddress()))
196 197 self.log.info('Test sethdseed restoring with keys outside of the initial keypool')
198 self.generate(self.nodes[0], 10)
199 # Restart node 1 with keypool of 3 and a different wallet
200 self.nodes[1].createwallet(wallet_name='origin', blank=True)
201 self.restart_node(1, extra_args=['-keypool=3', '-wallet=origin'])
202 self.connect_nodes(0, 1)
203 204 # sethdseed restoring and seeing txs to addresses out of the keypool
205 origin_rpc = self.nodes[1].get_wallet_rpc('origin')
206 seed = self.nodes[0].dumpprivkey(self.nodes[0].getnewaddress())
207 origin_rpc.sethdseed(True, seed)
208 209 self.nodes[1].createwallet(wallet_name='restore', blank=True)
210 restore_rpc = self.nodes[1].get_wallet_rpc('restore')
211 restore_rpc.sethdseed(True, seed) # Set to be the same seed as origin_rpc
212 restore_rpc.sethdseed(True) # Rotate to a new seed, making original `seed` inactive
213 214 self.nodes[1].createwallet(wallet_name='restore2', blank=True)
215 restore2_rpc = self.nodes[1].get_wallet_rpc('restore2')
216 restore2_rpc.sethdseed(True, seed) # Set to be the same seed as origin_rpc
217 restore2_rpc.sethdseed(True) # Rotate to a new seed, making original `seed` inactive
218 219 # Check persistence of inactive seed by reloading restore. restore2 is still loaded to test the case where the wallet is not reloaded
220 restore_rpc.unloadwallet()
221 self.nodes[1].loadwallet('restore')
222 restore_rpc = self.nodes[1].get_wallet_rpc('restore')
223 224 # Empty origin keypool and get an address that is beyond the initial keypool
225 origin_rpc.getnewaddress()
226 origin_rpc.getnewaddress()
227 last_addr = origin_rpc.getnewaddress() # Last address of initial keypool
228 addr = origin_rpc.getnewaddress() # First address beyond initial keypool
229 230 # Check that the restored seed has last_addr but does not have addr
231 info = restore_rpc.getaddressinfo(last_addr)
232 assert_equal(info['ismine'], True)
233 info = restore_rpc.getaddressinfo(addr)
234 assert_equal(info['ismine'], False)
235 info = restore2_rpc.getaddressinfo(last_addr)
236 assert_equal(info['ismine'], True)
237 info = restore2_rpc.getaddressinfo(addr)
238 assert_equal(info['ismine'], False)
239 # Check that the origin seed has addr
240 info = origin_rpc.getaddressinfo(addr)
241 assert_equal(info['ismine'], True)
242 243 # Send a transaction to addr, which is out of the initial keypool.
244 # The wallet that has set a new seed (restore_rpc) should not detect this transaction.
245 txid = self.nodes[0].sendtoaddress(addr, 1)
246 origin_rpc.sendrawtransaction(self.nodes[0].gettransaction(txid)['hex'])
247 self.generate(self.nodes[0], 1)
248 origin_rpc.gettransaction(txid)
249 assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', restore_rpc.gettransaction, txid)
250 out_of_kp_txid = txid
251 252 # Send a transaction to last_addr, which is in the initial keypool.
253 # The wallet that has set a new seed (restore_rpc) should detect this transaction and generate 3 new keys from the initial seed.
254 # The previous transaction (out_of_kp_txid) should still not be detected as a rescan is required.
255 txid = self.nodes[0].sendtoaddress(last_addr, 1)
256 origin_rpc.sendrawtransaction(self.nodes[0].gettransaction(txid)['hex'])
257 self.generate(self.nodes[0], 1)
258 origin_rpc.gettransaction(txid)
259 restore_rpc.gettransaction(txid)
260 assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', restore_rpc.gettransaction, out_of_kp_txid)
261 restore2_rpc.gettransaction(txid)
262 assert_raises_rpc_error(-5, 'Invalid or non-wallet transaction id', restore2_rpc.gettransaction, out_of_kp_txid)
263 264 # After rescanning, restore_rpc should now see out_of_kp_txid and generate an additional key.
265 # addr should now be part of restore_rpc and be ismine
266 restore_rpc.rescanblockchain()
267 restore_rpc.gettransaction(out_of_kp_txid)
268 info = restore_rpc.getaddressinfo(addr)
269 assert_equal(info['ismine'], True)
270 restore2_rpc.rescanblockchain()
271 restore2_rpc.gettransaction(out_of_kp_txid)
272 info = restore2_rpc.getaddressinfo(addr)
273 assert_equal(info['ismine'], True)
274 275 # Check again that 3 keys were derived.
276 # Empty keypool and get an address that is beyond the initial keypool
277 origin_rpc.getnewaddress()
278 origin_rpc.getnewaddress()
279 last_addr = origin_rpc.getnewaddress()
280 addr = origin_rpc.getnewaddress()
281 282 # Check that the restored seed has last_addr but does not have addr
283 info = restore_rpc.getaddressinfo(last_addr)
284 assert_equal(info['ismine'], True)
285 info = restore_rpc.getaddressinfo(addr)
286 assert_equal(info['ismine'], False)
287 info = restore2_rpc.getaddressinfo(last_addr)
288 assert_equal(info['ismine'], True)
289 info = restore2_rpc.getaddressinfo(addr)
290 assert_equal(info['ismine'], False)
291 292 293 if __name__ == '__main__':
294 WalletHDTest(__file__).main()
295