wallet_transactiontime_rescan.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 """Test transaction time during old block rescanning
6 """
7
8 import concurrent.futures
9 import time
10
11 from test_framework.authproxy import JSONRPCException
12 from test_framework.blocktools import COINBASE_MATURITY
13 from test_framework.test_framework import LimenkaTestFramework
14 from test_framework.util import (
15 assert_equal,
16 assert_raises_rpc_error,
17 set_node_times,
18 )
19 from test_framework.wallet_util import (
20 get_generate_key,
21 )
22
23
24 class TransactionTimeRescanTest(LimenkaTestFramework):
25 def add_options(self, parser):
26 self.add_wallet_options(parser)
27
28 def set_test_params(self):
29 self.setup_clean_chain = False
30 self.num_nodes = 3
31 self.extra_args = [["-keypool=400"],
32 ["-keypool=400"],
33 []
34 ]
35
36 def skip_test_if_missing_module(self):
37 self.skip_if_no_wallet()
38
39 def run_test(self):
40 self.log.info('Prepare nodes and wallet')
41
42 minernode = self.nodes[0] # node used to mine BTC and create transactions
43 usernode = self.nodes[1] # user node with correct time
44 restorenode = self.nodes[2] # node used to restore user wallet and check time determination in ComputeSmartTime (wallet.cpp)
45
46 # time constant
47 cur_time = int(time.time())
48 ten_days = 10 * 24 * 60 * 60
49
50 # synchronize nodes and time
51 self.sync_all()
52 set_node_times(self.nodes, cur_time)
53
54 # prepare miner wallet
55 minernode.createwallet(wallet_name='default')
56 miner_wallet = minernode.get_wallet_rpc('default')
57 m1 = miner_wallet.getnewaddress()
58
59 # prepare the user wallet with 3 watch only addresses
60 wo1 = usernode.getnewaddress()
61 wo2 = usernode.getnewaddress()
62 wo3 = usernode.getnewaddress()
63
64 usernode.createwallet(wallet_name='wo', disable_private_keys=True)
65 wo_wallet = usernode.get_wallet_rpc('wo')
66
67 wo_wallet.importaddress(wo1)
68 wo_wallet.importaddress(wo2)
69 wo_wallet.importaddress(wo3)
70
71 self.log.info('Start transactions')
72
73 # check blockcount
74 assert_equal(minernode.getblockcount(), 200)
75
76 # generate some btc to create transactions and check blockcount
77 initial_mine = COINBASE_MATURITY + 1
78 self.generatetoaddress(minernode, initial_mine, m1)
79 assert_equal(minernode.getblockcount(), initial_mine + 200)
80
81 # synchronize nodes and time
82 self.sync_all()
83 set_node_times(self.nodes, cur_time + ten_days)
84 # send 10 btc to user's first watch-only address
85 self.log.info('Send 10 btc to user')
86 miner_wallet.sendtoaddress(wo1, 10)
87
88 # generate blocks and check blockcount
89 self.generatetoaddress(minernode, COINBASE_MATURITY, m1)
90 assert_equal(minernode.getblockcount(), initial_mine + 300)
91
92 # synchronize nodes and time
93 self.sync_all()
94 set_node_times(self.nodes, cur_time + ten_days + ten_days)
95 # send 5 btc to our second watch-only address
96 self.log.info('Send 5 btc to user')
97 miner_wallet.sendtoaddress(wo2, 5)
98
99 # generate blocks and check blockcount
100 self.generatetoaddress(minernode, COINBASE_MATURITY, m1)
101 assert_equal(minernode.getblockcount(), initial_mine + 400)
102
103 # synchronize nodes and time
104 self.sync_all()
105 set_node_times(self.nodes, cur_time + ten_days + ten_days + ten_days)
106 # send 1 btc to our third watch-only address
107 self.log.info('Send 1 btc to user')
108 miner_wallet.sendtoaddress(wo3, 1)
109
110 # generate more blocks and check blockcount
111 self.generatetoaddress(minernode, COINBASE_MATURITY, m1)
112 assert_equal(minernode.getblockcount(), initial_mine + 500)
113
114 self.log.info('Check user\'s final balance and transaction count')
115 assert_equal(wo_wallet.getbalance(), 16)
116 assert_equal(len(wo_wallet.listtransactions()), 3)
117
118 self.log.info('Check transaction times')
119 for tx in wo_wallet.listtransactions():
120 if tx['address'] == wo1:
121 assert_equal(tx['blocktime'], cur_time + ten_days)
122 assert_equal(tx['time'], cur_time + ten_days)
123 elif tx['address'] == wo2:
124 assert_equal(tx['blocktime'], cur_time + ten_days + ten_days)
125 assert_equal(tx['time'], cur_time + ten_days + ten_days)
126 elif tx['address'] == wo3:
127 assert_equal(tx['blocktime'], cur_time + ten_days + ten_days + ten_days)
128 assert_equal(tx['time'], cur_time + ten_days + ten_days + ten_days)
129
130 # restore user wallet without rescan
131 self.log.info('Restore user wallet on another node without rescan')
132 restorenode.createwallet(wallet_name='wo', disable_private_keys=True)
133 restorewo_wallet = restorenode.get_wallet_rpc('wo')
134
135 # for descriptor wallets, the test framework maps the importaddress RPC to the
136 # importdescriptors RPC (with argument 'timestamp'='now'), which always rescans
137 # blocks of the past 2 hours, based on the current MTP timestamp; in order to avoid
138 # importing the last address (wo3), we advance the time further and generate 10 blocks
139 if self.options.descriptors:
140 set_node_times(self.nodes, cur_time + ten_days + ten_days + ten_days + ten_days)
141 self.generatetoaddress(minernode, 10, m1)
142
143 restorewo_wallet.importaddress(wo1, rescan=False)
144 restorewo_wallet.importaddress(wo2, rescan=False)
145 restorewo_wallet.importaddress(wo3, rescan=False)
146
147 # check user has 0 balance and no transactions
148 assert_equal(restorewo_wallet.getbalance(), 0)
149 assert_equal(len(restorewo_wallet.listtransactions()), 0)
150
151 # proceed to rescan, first with an incomplete one, then with a full rescan
152 self.log.info('Rescan last history part')
153 restorewo_wallet.rescanblockchain(initial_mine + 350)
154 self.log.info('Rescan all history')
155 restorewo_wallet.rescanblockchain()
156
157 self.log.info('Check user\'s final balance and transaction count after restoration')
158 assert_equal(restorewo_wallet.getbalance(), 16)
159 assert_equal(len(restorewo_wallet.listtransactions()), 3)
160
161 self.log.info('Check transaction times after restoration')
162 for tx in restorewo_wallet.listtransactions():
163 if tx['address'] == wo1:
164 assert_equal(tx['blocktime'], cur_time + ten_days)
165 assert_equal(tx['time'], cur_time + ten_days)
166 elif tx['address'] == wo2:
167 assert_equal(tx['blocktime'], cur_time + ten_days + ten_days)
168 assert_equal(tx['time'], cur_time + ten_days + ten_days)
169 elif tx['address'] == wo3:
170 assert_equal(tx['blocktime'], cur_time + ten_days + ten_days + ten_days)
171 assert_equal(tx['time'], cur_time + ten_days + ten_days + ten_days)
172
173
174 self.log.info('Test handling of invalid parameters for rescanblockchain')
175 assert_raises_rpc_error(-8, "Invalid start_height", restorewo_wallet.rescanblockchain, -1, 10)
176 assert_raises_rpc_error(-8, "Invalid stop_height", restorewo_wallet.rescanblockchain, 1, -1)
177 assert_raises_rpc_error(-8, "stop_height must be greater than start_height", restorewo_wallet.rescanblockchain, 20, 10)
178
179 self.log.info("Test `rescanblockchain` fails when wallet is encrypted and locked")
180 usernode.createwallet(wallet_name="enc_wallet", passphrase="passphrase")
181 enc_wallet = usernode.get_wallet_rpc("enc_wallet")
182 assert_raises_rpc_error(-13, "Error: Please enter the wallet passphrase with walletpassphrase first.", enc_wallet.rescanblockchain)
183
184 if not self.options.descriptors:
185 self.log.info("Test rescanning an encrypted wallet")
186 hd_seed = get_generate_key().privkey
187
188 usernode.createwallet(wallet_name="temp_wallet", blank=True, descriptors=False)
189 temp_wallet = usernode.get_wallet_rpc("temp_wallet")
190 temp_wallet.sethdseed(seed=hd_seed)
191
192 for i in range(399):
193 temp_wallet.getnewaddress()
194
195 self.generatetoaddress(usernode, COINBASE_MATURITY + 1, temp_wallet.getnewaddress())
196 self.generatetoaddress(usernode, COINBASE_MATURITY + 1, temp_wallet.getnewaddress())
197
198 minernode.createwallet("encrypted_wallet", blank=True, passphrase="passphrase", descriptors=False)
199 encrypted_wallet = minernode.get_wallet_rpc("encrypted_wallet")
200
201 encrypted_wallet.walletpassphrase("passphrase", 99999)
202 encrypted_wallet.sethdseed(seed=hd_seed)
203
204 with concurrent.futures.ThreadPoolExecutor(max_workers=1) as thread:
205 with minernode.assert_debug_log(expected_msgs=["Rescan started from block 0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206... (slow variant inspecting all blocks)"], timeout=5):
206 rescanning = thread.submit(encrypted_wallet.rescanblockchain)
207
208 # set the passphrase timeout to 1 to test that the wallet remains unlocked during the rescan
209 minernode.cli("-rpcwallet=encrypted_wallet").walletpassphrase("passphrase", 1)
210
211 try:
212 minernode.cli("-rpcwallet=encrypted_wallet").walletlock()
213 except JSONRPCException as e:
214 assert e.error["code"] == -4 and "Error: the wallet is currently being used to rescan the blockchain for related transactions. Please call `abortrescan` before locking the wallet." in e.error["message"]
215
216 try:
217 minernode.cli("-rpcwallet=encrypted_wallet").walletpassphrasechange("passphrase", "newpassphrase")
218 except JSONRPCException as e:
219 assert e.error["code"] == -4 and "Error: the wallet is currently being used to rescan the blockchain for related transactions. Please call `abortrescan` before changing the passphrase." in e.error["message"]
220
221 assert_equal(rescanning.result(), {"start_height": 0, "stop_height": 803})
222
223 assert_equal(encrypted_wallet.getbalance(), temp_wallet.getbalance())
224
225 if __name__ == '__main__':
226 TransactionTimeRescanTest(__file__).main()
227