Branch data Line data Source code
1 : : // Copyright (c) 2012-present The Bitcoin Core developers
2 : : // Distributed under the MIT software license, see the accompanying
3 : : // file COPYING or https://opensource.org/license/mit.
4 : :
5 : : #include <blockfilter.h>
6 : : #include <chain.h>
7 : : #include <index/blockfilterindex.h>
8 : : #include <interfaces/chain.h>
9 : : #include <node/blockstorage.h>
10 : : #include <test/util/common.h>
11 : : #include <test/util/logging.h>
12 : : #include <test/util/setup_common.h>
13 : : #include <test/util/validation.h>
14 : : #include <util/byte_units.h>
15 : : #include <util/check.h>
16 : : #include <validation.h>
17 : : #include <wallet/context.h>
18 : : #include <wallet/receive.h>
19 : : #include <wallet/scan.h>
20 : : #include <wallet/test/util.h>
21 : : #include <wallet/test/wallet_test_fixture.h>
22 : : #include <wallet/wallet.h>
23 : : #include <wallet/walletdb.h>
24 : :
25 : : #include <boost/test/unit_test.hpp>
26 : :
27 : : #include <cassert>
28 : : #include <chrono>
29 : : #include <memory>
30 : : #include <string>
31 : : #include <utility>
32 : :
33 : : using node::MAX_BLOCKFILE_SIZE;
34 : :
35 : : namespace wallet {
36 : :
37 : : BOOST_FIXTURE_TEST_SUITE(rescan_tests, WalletTestingSetup)
38 : :
39 [ + - + - : 7 : BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup)
+ - + - -
+ + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- ]
40 : : {
41 : : // Cap last block file size, and mine new block in a new block file.
42 [ - + + - : 4 : CBlockIndex* oldTip = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip());
- + + - ]
43 [ + - + - ]: 3 : WITH_LOCK(::cs_main, m_node.chainman->m_blockman.GetBlockFileInfo(oldTip->GetBlockPos().nFile)->nSize = MAX_BLOCKFILE_SIZE);
44 [ + - ]: 2 : CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
45 [ - + + - : 4 : CBlockIndex* newTip = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip());
- + + - ]
46 : :
47 : : // Verify Scan fails to read an unknown start block.
48 : 1 : {
49 [ + - + - ]: 2 : CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
50 : 1 : {
51 [ + - ]: 1 : LOCK(wallet.cs_wallet);
52 [ - + + - ]: 1 : LOCK(Assert(m_node.chainman)->GetMutex());
53 [ + - ]: 1 : wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
54 [ + - - + : 2 : wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
+ - - + +
- ]
55 [ + - ]: 1 : }
56 [ + - ]: 1 : AddKey(wallet, coinbaseKey);
57 [ + - ]: 1 : WalletRescanReserver reserver(wallet);
58 [ + - ]: 1 : reserver.reserve();
59 [ + - + - ]: 1 : ScanResult result = wallet.Scanner().Scan(/*start_block=*/{}, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false);
60 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.status, ScanResult::FAILURE);
61 [ + - + - : 3 : BOOST_CHECK(result.last_failed_block.IsNull());
+ - ]
62 [ + - + - : 3 : BOOST_CHECK(result.last_scanned_block.IsNull());
+ - ]
63 [ + - + - : 2 : BOOST_CHECK(!result.last_scanned_height);
+ - ]
64 [ + - + - : 1 : BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 0);
+ - ]
65 : 1 : }
66 : :
67 : : // Verify Scan picks up transactions in both the old
68 : : // and new block files.
69 : 1 : {
70 [ + - + - ]: 2 : CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
71 : 1 : {
72 [ + - ]: 1 : LOCK(wallet.cs_wallet);
73 [ - + + - ]: 1 : LOCK(Assert(m_node.chainman)->GetMutex());
74 [ + - ]: 1 : wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
75 [ + - ]: 1 : wallet.SetLastBlockProcessed(newTip->nHeight, newTip->GetBlockHash());
76 [ + - ]: 1 : }
77 [ + - ]: 1 : AddKey(wallet, coinbaseKey);
78 : 1 : WalletRescanReserver reserver(wallet);
79 : 1 : std::chrono::steady_clock::time_point fake_time;
80 : 8 : reserver.setNow([&] { fake_time += 60s; return fake_time; });
81 [ + - ]: 1 : reserver.reserve();
82 : :
83 : 1 : {
84 : 1 : CBlockLocator locator;
85 [ + - + - : 3 : BOOST_CHECK(WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator));
+ - + - +
- ]
86 [ + - + - : 2 : BOOST_REQUIRE(!locator.IsNull());
+ - ]
87 [ + - + - ]: 2 : BOOST_CHECK(locator.vHave.front() == newTip->GetBlockHash());
88 : 0 : }
89 : :
90 [ + - + - ]: 1 : ScanResult result = wallet.Scanner().Scan(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*save_progress=*/true);
91 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS);
92 [ + - + - : 3 : BOOST_CHECK(result.last_failed_block.IsNull());
+ - ]
93 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.last_scanned_block, newTip->GetBlockHash());
94 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(*result.last_scanned_height, newTip->nHeight);
95 [ + - + - : 1 : BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 100 * COIN);
+ - ]
96 : :
97 : 1 : {
98 : 1 : CBlockLocator locator;
99 [ + - + - : 3 : BOOST_CHECK(WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator));
+ - + - +
- ]
100 [ + - + - : 2 : BOOST_REQUIRE(!locator.IsNull());
+ - ]
101 [ + - + - ]: 2 : BOOST_CHECK(locator.vHave.front() == newTip->GetBlockHash());
102 : 0 : }
103 : 1 : }
104 : :
105 : : // Prune the older block file.
106 : 1 : int file_number;
107 : 1 : {
108 : 1 : LOCK(cs_main);
109 : 1 : file_number = oldTip->GetBlockPos().nFile;
110 [ - + + - ]: 1 : Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(file_number);
111 : 0 : }
112 [ + - ]: 1 : m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number});
113 : :
114 : : // Verify Scan only picks transactions in the new block
115 : : // file.
116 : 1 : {
117 [ + - + - ]: 2 : CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
118 : 1 : {
119 [ + - ]: 1 : LOCK(wallet.cs_wallet);
120 [ - + + - ]: 1 : LOCK(Assert(m_node.chainman)->GetMutex());
121 [ + - ]: 1 : wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
122 [ + - - + : 2 : wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
+ - - + +
- ]
123 [ + - ]: 1 : }
124 [ + - ]: 1 : AddKey(wallet, coinbaseKey);
125 [ + - ]: 1 : WalletRescanReserver reserver(wallet);
126 [ + - ]: 1 : reserver.reserve();
127 [ + - + - ]: 1 : ScanResult result = wallet.Scanner().Scan(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*save_progress=*/false);
128 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.status, ScanResult::FAILURE);
129 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.last_failed_block, oldTip->GetBlockHash());
130 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.last_scanned_block, newTip->GetBlockHash());
131 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(*result.last_scanned_height, newTip->nHeight);
132 [ + - + - : 1 : BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 50 * COIN);
+ - ]
133 : 1 : }
134 : :
135 : : // Prune the remaining block file.
136 : 1 : {
137 : 1 : LOCK(cs_main);
138 : 1 : file_number = newTip->GetBlockPos().nFile;
139 [ - + + - ]: 1 : Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(file_number);
140 : 0 : }
141 [ + - ]: 1 : m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number});
142 : :
143 : : // Verify Scan scans no blocks.
144 : 1 : {
145 [ + - + - ]: 2 : CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
146 : 1 : {
147 [ + - ]: 1 : LOCK(wallet.cs_wallet);
148 [ - + + - ]: 1 : LOCK(Assert(m_node.chainman)->GetMutex());
149 [ + - ]: 1 : wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
150 [ + - - + : 2 : wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
+ - - + +
- ]
151 [ + - ]: 1 : }
152 [ + - ]: 1 : AddKey(wallet, coinbaseKey);
153 [ + - ]: 1 : WalletRescanReserver reserver(wallet);
154 [ + - ]: 1 : reserver.reserve();
155 [ + - + - ]: 1 : ScanResult result = wallet.Scanner().Scan(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*save_progress=*/false);
156 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.status, ScanResult::FAILURE);
157 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.last_failed_block, newTip->GetBlockHash());
158 [ + - + - : 3 : BOOST_CHECK(result.last_scanned_block.IsNull());
+ - ]
159 [ + - + - : 2 : BOOST_CHECK(!result.last_scanned_height);
+ - ]
160 [ + - + - : 1 : BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 0);
+ - ]
161 : 1 : }
162 : 1 : }
163 : :
164 [ + - + - : 7 : BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_reorged_block, TestChain100Setup)
+ - + - -
+ + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- ]
165 : : {
166 [ + - + - ]: 3 : BOOST_REQUIRE(InitBlockFilterIndex([&]{ return interfaces::MakeChain(m_node); }, BlockFilterType::BASIC, 1_MiB, /*f_memory=*/true));
167 [ - + ]: 1 : BlockFilterIndex& filter_index{*Assert(GetBlockFilterIndex(BlockFilterType::BASIC))};
168 [ + - + - ]: 2 : BOOST_REQUIRE(filter_index.Init());
169 : 1 : filter_index.Sync();
170 : :
171 : : // Reorg the tip out of the active chain: invalidate it, then mine a
172 : : // longer replacement branch paying a script unrelated to the wallets
173 : : // below.
174 [ - + + - : 4 : CBlockIndex* stale_block = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip());
- + + - ]
175 : 1 : const uint256 stale_hash{stale_block->GetBlockHash()};
176 : 1 : const int stale_height{stale_block->nHeight};
177 [ + - ]: 1 : BlockValidationState state;
178 [ + - + - : 2 : BOOST_REQUIRE(m_node.chainman->ActiveChainstate().InvalidateBlock(state, stale_block));
+ - + - ]
179 [ + - + - ]: 1 : const CScript replacement_script{GetScriptForRawPubKey(GenerateRandomKey().GetPubKey())};
180 [ + - ]: 2 : CreateAndProcessBlock({}, replacement_script);
181 [ + - ]: 2 : CreateAndProcessBlock({}, replacement_script);
182 [ + - + - : 2 : BOOST_REQUIRE(filter_index.BlockUntilSyncedToCurrentChain());
+ - - + ]
183 : 1 : {
184 [ - + + - ]: 1 : LOCK(Assert(m_node.chainman)->GetMutex());
185 [ + - + - : 2 : BOOST_REQUIRE(!m_node.chainman->ActiveChain().Contains(*stale_block));
+ - + - ]
186 [ + - + - : 1 : BOOST_REQUIRE_EQUAL(m_node.chainman->ActiveChain().Height(), stale_height + 1);
- + + - +
- ]
187 : 0 : }
188 : :
189 : 1 : {
190 [ + - ]: 1 : BlockFilter filter;
191 [ + - + - : 2 : BOOST_REQUIRE(filter_index.LookupFilter(stale_block, filter));
+ - ]
192 : 0 : }
193 : :
194 : : // Test wallet whose scripts do not match the stale block's filter.
195 : 1 : {
196 [ + - + - : 2 : CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
+ - ]
197 : 1 : {
198 [ + - ]: 1 : LOCK(wallet.cs_wallet);
199 [ - + + - ]: 1 : LOCK(Assert(m_node.chainman)->GetMutex());
200 [ + - ]: 1 : wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
201 [ + - - + : 2 : wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
+ - - + +
- ]
202 [ + - ]: 1 : }
203 [ + - ]: 1 : WalletRescanReserver reserver(wallet);
204 [ + - ]: 1 : reserver.reserve();
205 [ + - + - ]: 1 : ScanResult result = wallet.Scanner().Scan(stale_hash, stale_height, /*max_height=*/{}, reserver, /*save_progress=*/false);
206 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS);
207 [ + - + - : 3 : BOOST_CHECK(result.last_failed_block.IsNull());
+ - ]
208 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.last_scanned_block, stale_hash);
209 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(*result.last_scanned_height, stale_height);
210 : 1 : }
211 : :
212 : : // Test wallet whose scripts do match the stale block's filter.
213 : 1 : {
214 [ + - + - : 2 : CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
+ - ]
215 : 1 : {
216 [ + - ]: 1 : LOCK(wallet.cs_wallet);
217 [ - + + - ]: 1 : LOCK(Assert(m_node.chainman)->GetMutex());
218 [ + - ]: 1 : wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
219 [ + - - + : 2 : wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
+ - - + +
- ]
220 [ + - ]: 1 : }
221 [ + - ]: 1 : AddKey(wallet, coinbaseKey); // the stale block's coinbase pays coinbaseKey
222 [ + - ]: 1 : WalletRescanReserver reserver(wallet);
223 [ + - ]: 1 : reserver.reserve();
224 [ + - + - ]: 1 : ScanResult result = wallet.Scanner().Scan(stale_hash, stale_height, /*max_height=*/{}, reserver, /*save_progress=*/false);
225 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.status, ScanResult::FAILURE);
226 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.last_failed_block, stale_hash);
227 [ + - + - : 3 : BOOST_CHECK(result.last_scanned_block.IsNull());
+ - ]
228 [ + - + - : 2 : BOOST_CHECK(!result.last_scanned_height);
+ - ]
229 [ + - + - : 3 : BOOST_CHECK(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.empty()));
+ - ]
230 : 1 : }
231 : :
232 : : // Prune the stale block's file — the block is now not active AND unreadable.
233 : 1 : int file_number;
234 : 1 : {
235 [ + - ]: 1 : LOCK(cs_main);
236 : 1 : file_number = stale_block->GetBlockPos().nFile;
237 [ - + + - ]: 1 : Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(file_number);
238 : 0 : }
239 [ + - + - ]: 1 : m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number});
240 : :
241 : 1 : {
242 [ + - + - : 2 : CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
+ - ]
243 : 1 : {
244 [ + - ]: 1 : LOCK(wallet.cs_wallet);
245 [ - + + - ]: 1 : LOCK(Assert(m_node.chainman)->GetMutex());
246 [ + - ]: 1 : wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
247 [ + - - + : 2 : wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
+ - - + +
- ]
248 [ + - ]: 1 : }
249 [ + - ]: 1 : AddKey(wallet, coinbaseKey);
250 [ + - ]: 1 : WalletRescanReserver reserver(wallet);
251 [ + - ]: 1 : reserver.reserve();
252 [ + - + - ]: 1 : ScanResult result = wallet.Scanner().Scan(stale_hash, stale_height, /*max_height=*/{}, reserver, /*save_progress=*/false);
253 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.status, ScanResult::FAILURE);
254 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.last_failed_block, stale_hash);
255 [ + - + - : 3 : BOOST_CHECK(result.last_scanned_block.IsNull());
+ - ]
256 [ + - + - : 2 : BOOST_CHECK(!result.last_scanned_height);
+ - ]
257 [ + - + - : 3 : BOOST_CHECK(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.empty()));
+ - ]
258 : 1 : }
259 : :
260 [ + - ]: 1 : StopIndex(filter_index, m_node);
261 [ + - + - : 2 : BOOST_REQUIRE(DestroyBlockFilterIndex(BlockFilterType::BASIC));
+ - ]
262 : 2 : }
263 : :
264 [ + - + - : 7 : BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_abort, TestChain100Setup)
+ - + - -
+ + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- ]
265 : : {
266 [ + - + - ]: 2 : CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
267 : 1 : uint256 genesis_hash;
268 : 1 : {
269 [ + - ]: 1 : LOCK(wallet.cs_wallet);
270 [ - + + - ]: 1 : LOCK(Assert(m_node.chainman)->GetMutex());
271 [ + - ]: 1 : wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
272 [ + - - + : 2 : wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
+ - - + +
- ]
273 [ + - - + : 2 : genesis_hash = m_node.chainman->ActiveChain().Genesis()->GetBlockHash();
+ - ]
274 [ + - ]: 1 : }
275 : :
276 : : // An abort requested while no rescan is held is stale and must
277 : : // not cancel a later scan.
278 [ + - + - ]: 1 : wallet.Scanner().Abort();
279 [ + - ]: 1 : WalletRescanReserver reserver(wallet);
280 [ + - + - : 2 : BOOST_CHECK(reserver.reserve());
+ - + - ]
281 [ + - + - : 2 : BOOST_CHECK(!wallet.Scanner().IsAborting());
+ - + - ]
282 : :
283 : : // An abort requested after the reservation but before the scan starts
284 : : // (e.g. while importdescriptors is still deriving keys) must cancel the
285 : : // scan.
286 [ + - + - ]: 1 : wallet.Scanner().Abort();
287 [ + - + - ]: 1 : ScanResult result = wallet.Scanner().Scan(genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false);
288 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.status, ScanResult::USER_ABORT);
289 [ + - + - : 3 : BOOST_CHECK(result.last_scanned_block.IsNull());
+ - ]
290 [ + - + - : 2 : BOOST_CHECK(!result.last_scanned_height);
+ - ]
291 [ + - + - ]: 3 : BOOST_CHECK(result.last_failed_block.IsNull());
292 : 1 : }
293 : :
294 [ + - + - : 7 : BOOST_FIXTURE_TEST_CASE(wallet_rescan_reserver, TestingSetup)
+ - + - -
+ + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- ]
295 : : {
296 [ + - + - ]: 2 : CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
297 : :
298 : : // No scan in progress: accessors report idle state.
299 [ + - + - : 2 : BOOST_CHECK(!wallet.Scanner().IsScanning());
+ - + - ]
300 [ + - + - : 2 : BOOST_CHECK(wallet.Scanner().ScanningDuration() == SteadyClock::duration{});
+ - + - ]
301 [ + - + - : 1 : BOOST_CHECK_EQUAL(wallet.Scanner().ScanningProgress(), 0.0);
+ - ]
302 : :
303 : 1 : {
304 [ + - ]: 1 : WalletRescanReserver first_reserver(wallet);
305 [ + - + - : 2 : BOOST_CHECK(first_reserver.reserve());
+ - + - ]
306 [ + - + - : 2 : BOOST_CHECK(first_reserver.isReserved());
+ - + - ]
307 [ + - + - : 2 : BOOST_CHECK(wallet.Scanner().IsScanning());
+ - + - ]
308 [ + - + - : 2 : BOOST_CHECK(!wallet.Scanner().IsScanningWithPassphrase());
+ - + - ]
309 [ + - + - : 1 : BOOST_CHECK_EQUAL(wallet.Scanner().ScanningProgress(), 0.0);
+ - ]
310 : :
311 : : // Only one reservation can be held at a time.
312 [ + - ]: 1 : WalletRescanReserver second_reserver(wallet);
313 [ + - + - : 2 : BOOST_CHECK(!second_reserver.reserve());
+ - + - ]
314 [ + - + - : 2 : BOOST_CHECK(!second_reserver.isReserved());
+ - ]
315 : 1 : }
316 : : // Destroying the reserver (RAII) clears the scanning state.
317 [ + - + - : 2 : BOOST_CHECK(!wallet.Scanner().IsScanning());
+ - + - ]
318 : :
319 : 1 : {
320 [ + - ]: 1 : WalletRescanReserver passphrase_reserver(wallet);
321 [ + - + - : 2 : BOOST_CHECK(passphrase_reserver.reserve(/*with_passphrase=*/true));
+ - + - ]
322 [ + - + - : 2 : BOOST_CHECK(wallet.Scanner().IsScanningWithPassphrase());
+ - ]
323 : 1 : }
324 [ + - + - : 2 : BOOST_CHECK(!wallet.Scanner().IsScanningWithPassphrase());
+ - ]
325 : 1 : }
326 : :
327 [ + - + - : 7 : BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_bounded, TestChain100Setup)
+ - + - -
+ + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- ]
328 : : {
329 : 1 : uint256 genesis_hash, max_hash, tip_hash;
330 : 1 : int max_height, tip_height;
331 : 1 : {
332 [ - + ]: 1 : LOCK(Assert(m_node.chainman)->GetMutex());
333 [ + - - + ]: 2 : genesis_hash = m_node.chainman->ActiveChain().Genesis()->GetBlockHash();
334 [ + - - + ]: 1 : tip_height = m_node.chainman->ActiveChain().Height();
335 [ + - - + ]: 2 : tip_hash = m_node.chainman->ActiveChain().Tip()->GetBlockHash();
336 : 1 : max_height = tip_height - 2;
337 [ + - + - : 2 : max_hash = m_node.chainman->ActiveChain()[max_height]->GetBlockHash();
+ - ]
338 : 0 : }
339 : :
340 : : // A scan with max_height set stops exactly at max_height and does not
341 : : // sync any blocks beyond it.
342 : 1 : {
343 [ + - + - ]: 2 : CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
344 : 1 : {
345 [ + - ]: 1 : LOCK(wallet.cs_wallet);
346 [ + - ]: 1 : wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
347 [ + - ]: 1 : wallet.SetLastBlockProcessed(tip_height, tip_hash);
348 : 0 : }
349 [ + - ]: 1 : AddKey(wallet, coinbaseKey);
350 [ + - ]: 1 : WalletRescanReserver reserver(wallet);
351 [ + - ]: 1 : reserver.reserve();
352 [ + - + - ]: 1 : ScanResult result = wallet.Scanner().Scan(genesis_hash, /*start_height=*/0, max_height, reserver, /*save_progress=*/false);
353 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS);
354 [ + - + - : 3 : BOOST_CHECK(result.last_failed_block.IsNull());
+ - ]
355 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.last_scanned_block, max_hash);
356 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(*result.last_scanned_height, max_height);
357 : : // One coinbase per block from height 1 through max_height.
358 [ + - + - : 2 : BOOST_CHECK_EQUAL(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.size()), static_cast<size_t>(max_height));
+ - ]
359 : 1 : }
360 : :
361 : : // A single-block range (start == max_height == tip) scans exactly that
362 : : // block.
363 : 1 : {
364 [ + - + - ]: 2 : CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
365 : 1 : {
366 [ + - ]: 1 : LOCK(wallet.cs_wallet);
367 [ + - ]: 1 : wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
368 [ + - ]: 1 : wallet.SetLastBlockProcessed(tip_height, tip_hash);
369 : 0 : }
370 [ + - ]: 1 : AddKey(wallet, coinbaseKey);
371 [ + - ]: 1 : WalletRescanReserver reserver(wallet);
372 [ + - ]: 1 : reserver.reserve();
373 [ + - + - ]: 1 : ScanResult result = wallet.Scanner().Scan(tip_hash, tip_height, tip_height, reserver, /*save_progress=*/false);
374 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS);
375 [ + - + - : 3 : BOOST_CHECK(result.last_failed_block.IsNull());
+ - ]
376 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.last_scanned_block, tip_hash);
377 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(*result.last_scanned_height, tip_height);
378 [ + - + - : 2 : BOOST_CHECK_EQUAL(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.size()), 1U);
+ - ]
379 : 1 : }
380 : 1 : }
381 : :
382 [ + - + - : 7 : BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_tip_extension, TestChain100Setup)
+ - + - -
+ + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- ]
383 : : {
384 [ + - + - ]: 2 : CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
385 : 1 : uint256 genesis_hash;
386 : 1 : int start_tip_height{0};
387 : 1 : {
388 [ + - ]: 1 : LOCK(wallet.cs_wallet);
389 [ - + + - ]: 1 : LOCK(Assert(m_node.chainman)->GetMutex());
390 [ + - ]: 1 : wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
391 [ + - - + ]: 1 : start_tip_height = m_node.chainman->ActiveChain().Height();
392 [ + - - + : 2 : wallet.SetLastBlockProcessed(start_tip_height, m_node.chainman->ActiveChain().Tip()->GetBlockHash());
+ - ]
393 [ + - - + : 2 : genesis_hash = m_node.chainman->ActiveChain().Genesis()->GetBlockHash();
+ - ]
394 [ + - ]: 1 : }
395 [ + - ]: 1 : AddKey(wallet, coinbaseKey);
396 : :
397 : : // Connect a block while the scan is running (the handler fires on the
398 : : // scanning thread as the scan starts) and advance the wallet's tip, as
399 : : // the blockConnected notification would. The scan must pick up the new
400 : : // tip instead of stopping at the height it started with.
401 : 1 : uint256 new_tip_hash;
402 : 1 : bool extended{false};
403 : 5 : auto handler = wallet.ShowProgress.connect([&](const std::string&, int) {
404 [ + + ]: 4 : if (extended) return;
405 : 1 : extended = true;
406 [ + - ]: 2 : CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
407 : 1 : LOCK(wallet.cs_wallet);
408 [ - + + - ]: 1 : LOCK(Assert(m_node.chainman)->GetMutex());
409 [ + - - + ]: 1 : const CBlockIndex* new_tip = m_node.chainman->ActiveChain().Tip();
410 : 1 : new_tip_hash = new_tip->GetBlockHash();
411 [ + - ]: 1 : wallet.SetLastBlockProcessed(new_tip->nHeight, new_tip_hash);
412 [ + - ]: 3 : });
413 : :
414 [ + - ]: 1 : WalletRescanReserver reserver(wallet);
415 [ + - ]: 1 : reserver.reserve();
416 [ + - + - ]: 1 : ScanResult result = wallet.Scanner().Scan(genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false);
417 : 1 : handler.disconnect();
418 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS);
419 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.last_scanned_block, new_tip_hash);
420 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(*result.last_scanned_height, start_tip_height + 1);
421 [ + - ]: 2 : }
422 : :
423 [ + - + - : 7 : BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_no_progress_saved, TestChain100Setup)
+ - + - -
+ + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- ]
424 : : {
425 [ + - + - ]: 2 : CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
426 : 1 : uint256 genesis_hash, tip_hash;
427 : 1 : int max_height;
428 : 1 : {
429 [ + - ]: 1 : LOCK(wallet.cs_wallet);
430 [ - + + - ]: 1 : LOCK(Assert(m_node.chainman)->GetMutex());
431 [ + - ]: 1 : wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
432 [ + - - + ]: 2 : tip_hash = m_node.chainman->ActiveChain().Tip()->GetBlockHash();
433 [ + - - + : 1 : wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), tip_hash);
+ - ]
434 [ + - - + ]: 2 : genesis_hash = m_node.chainman->ActiveChain().Genesis()->GetBlockHash();
435 [ + - - + : 1 : max_height = m_node.chainman->ActiveChain().Height() - 2;
+ - ]
436 [ + - ]: 1 : }
437 [ + - ]: 1 : AddKey(wallet, coinbaseKey);
438 : :
439 : 1 : WalletRescanReserver reserver(wallet);
440 : : // Advance the clock on every call so that every scanned block would be
441 : : // eligible for a progress write if save_progress were set.
442 : 1 : std::chrono::steady_clock::time_point fake_time;
443 : 202 : reserver.setNow([&] { fake_time += 60s; return fake_time; });
444 [ + - ]: 1 : reserver.reserve();
445 : :
446 [ + - + - ]: 1 : ScanResult result = wallet.Scanner().Scan(genesis_hash, /*start_height=*/0, max_height, reserver, /*save_progress=*/false);
447 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS);
448 : :
449 : : // With save_progress=false the scan must not touch the wallet's best
450 : : // block record: it still points at the tip written when the descriptor
451 : : // was added, not at any block the scan visited.
452 : 1 : CBlockLocator locator;
453 [ + - + - : 3 : BOOST_CHECK(WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator));
+ - + - +
- ]
454 [ + - + - : 2 : BOOST_CHECK(!locator.IsNull());
+ - ]
455 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(locator.vHave.front(), tip_hash);
456 : 1 : }
457 : :
458 [ + - + - : 7 : BOOST_FIXTURE_TEST_CASE(rescan_from_time, TestChain100Setup)
+ - + - -
+ + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- ]
459 : : {
460 : : // Cap last block file size, and mine new block in a new block file.
461 [ - + + - : 4 : CBlockIndex* old_tip = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip());
- + + - ]
462 [ + - + - ]: 3 : WITH_LOCK(::cs_main, m_node.chainman->m_blockman.GetBlockFileInfo(old_tip->GetBlockPos().nFile)->nSize = MAX_BLOCKFILE_SIZE);
463 [ + - ]: 2 : CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
464 [ - + + - : 4 : CBlockIndex* new_tip = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip());
- + + - ]
465 : :
466 : : // Prune the older block file.
467 : 1 : int file_number;
468 : 1 : {
469 : 1 : LOCK(cs_main);
470 : 1 : file_number = old_tip->GetBlockPos().nFile;
471 [ - + + - ]: 1 : Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(file_number);
472 : 0 : }
473 [ + - ]: 1 : m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number});
474 : :
475 [ + - + - ]: 2 : CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
476 : 1 : {
477 [ + - ]: 1 : LOCK(wallet.cs_wallet);
478 [ - + + - ]: 1 : LOCK(Assert(m_node.chainman)->GetMutex());
479 [ + - ]: 1 : wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
480 [ + - - + : 2 : wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
+ - - + +
- ]
481 [ + - ]: 1 : }
482 [ + - ]: 1 : AddKey(wallet, coinbaseKey);
483 [ + - ]: 1 : WalletRescanReserver reserver(wallet);
484 [ + - ]: 1 : reserver.reserve();
485 : :
486 : : // Blocks before the prune point cannot be read: the returned timestamp
487 : : // is moved past the last unreadable block, telling the caller from when
488 : : // the rescan is actually complete.
489 [ + - - + : 4 : const int64_t genesis_time{WITH_LOCK(::cs_main, return m_node.chainman->ActiveChain().Genesis()->GetBlockTime())};
+ - ]
490 [ + - + - : 2 : BOOST_CHECK_EQUAL(wallet.Scanner().ScanFromTime(genesis_time, reserver),
+ - + - +
- ]
491 : : WITH_LOCK(::cs_main, return old_tip->GetBlockTimeMax()) + TIMESTAMP_WINDOW + 1);
492 : :
493 : 1 : bool scan_logged{false};
494 [ + - + - ]: 3 : DebugLogHelper scan_check{"Rescan started from block", [&](const std::string* s) {
495 [ - + ]: 1 : if (s) scan_logged = true;
496 : 1 : return false;
497 [ + - ]: 1 : }};
498 : : // A timestamp past the tip requires no scanning and is returned unchanged.
499 [ + - ]: 2 : const int64_t future_time{WITH_LOCK(::cs_main, return new_tip->GetBlockTimeMax()) + TIMESTAMP_WINDOW + 1};
500 [ + - + - : 2 : BOOST_CHECK(!scan_logged);
+ - ]
501 [ + - + - : 1 : BOOST_CHECK_EQUAL(wallet.Scanner().ScanFromTime(future_time, reserver), future_time);
+ - + - ]
502 : 1 : }
503 : :
504 [ + - + - : 7 : BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_missing_filter, TestChain100Setup)
+ - + - -
+ + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- ]
505 : : {
506 : : // Enable the block filter index but do not sync it: no filters are
507 : : // available, so the scan must inspect every block rather than treat
508 : : // the missing filters as misses and skip blocks.
509 [ + - + - ]: 3 : BOOST_REQUIRE(InitBlockFilterIndex([&]{ return interfaces::MakeChain(m_node); }, BlockFilterType::BASIC, 1_MiB, /*f_memory=*/true));
510 [ - + ]: 1 : BlockFilterIndex& filter_index{*Assert(GetBlockFilterIndex(BlockFilterType::BASIC))};
511 [ + - + - ]: 2 : BOOST_REQUIRE(filter_index.Init());
512 : :
513 : 1 : {
514 [ + - + - ]: 2 : CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
515 : 1 : uint256 genesis_hash, tip_hash;
516 : 1 : int tip_height;
517 : 1 : {
518 [ + - ]: 1 : LOCK(wallet.cs_wallet);
519 [ - + + - ]: 1 : LOCK(Assert(m_node.chainman)->GetMutex());
520 [ + - ]: 1 : wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
521 [ + - - + ]: 2 : genesis_hash = m_node.chainman->ActiveChain().Genesis()->GetBlockHash();
522 [ + - - + ]: 1 : tip_height = m_node.chainman->ActiveChain().Height();
523 [ + - - + ]: 1 : auto tip{m_node.chainman->ActiveChain().Tip()};
524 : 1 : tip_hash = tip->GetBlockHash();
525 [ + - ]: 1 : wallet.SetLastBlockProcessed(tip_height, tip_hash);
526 [ + - ]: 1 : BlockFilter filter;
527 [ + - + - : 2 : BOOST_REQUIRE(!filter_index.LookupFilter(tip, filter));
+ - ]
528 [ + - + - ]: 2 : }
529 [ + - ]: 1 : AddKey(wallet, coinbaseKey);
530 [ + - ]: 1 : WalletRescanReserver reserver(wallet);
531 [ + - ]: 1 : reserver.reserve();
532 : 1 : bool fast_scan_logged{false};
533 [ + - + - ]: 4 : DebugLogHelper scan_check{"fast variant using block filters", [&](const std::string* s) {
534 [ + + ]: 2 : if (s) fast_scan_logged = true;
535 : 2 : return false;
536 [ + - ]: 1 : }};
537 [ + - + - ]: 1 : ScanResult result = wallet.Scanner().Scan(genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false);
538 [ + - + - : 2 : BOOST_REQUIRE(fast_scan_logged);
+ - ]
539 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS);
540 [ + - + - : 3 : BOOST_CHECK(result.last_failed_block.IsNull());
+ - ]
541 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(result.last_scanned_block, tip_hash);
542 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(*result.last_scanned_height, tip_height);
543 : : // One coinbase per block from height 1 through the tip.
544 [ + - + - : 2 : BOOST_CHECK_EQUAL(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.size()), static_cast<size_t>(tip_height));
+ - ]
545 : 1 : }
546 : :
547 : 1 : StopIndex(filter_index, m_node);
548 [ + - + - ]: 2 : BOOST_REQUIRE(DestroyBlockFilterIndex(BlockFilterType::BASIC));
549 : 1 : }
550 : :
551 : : //! Test the rescan that loading a wallet performs when the wallet is behind
552 : : //! the chain tip: it scans from the wallet's recorded best block - a
553 : : //! mid-chain start - with cs_wallet held.
554 [ + - + - : 7 : BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_attach_chain, TestChain100Setup)
+ - + - -
+ + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- + - + -
+ - + - +
- + - - +
+ - + - +
- + - + -
+ - - + +
- ]
555 : : {
556 : : // Do not wait for sqlite to flush data to disk to improve performance
557 [ + - + - ]: 2 : m_args.ForceSetArg("-unsafesqlitesync", "1");
558 : :
559 : : // Create a wallet owning the coinbases, and unload it at the current tip.
560 : 1 : WalletContext context;
561 : 1 : context.args = &m_args;
562 [ + - ]: 1 : context.chain = m_node.chain.get();
563 [ + - ]: 1 : auto wallet = TestCreateWallet(context);
564 [ + - ]: 1 : AddKey(*wallet, coinbaseKey);
565 [ + - ]: 1 : TestUnloadWallet(std::move(wallet));
566 : :
567 : : // Extend the chain while the wallet is not loaded.
568 : : constexpr int NEW_BLOCKS{5};
569 [ + + ]: 6 : for (int i = 0; i < NEW_BLOCKS; ++i) {
570 [ + - + - : 10 : CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
+ - ]
571 : : }
572 : :
573 : 1 : int tip_height;
574 : 1 : uint256 tip_hash;
575 : 1 : {
576 [ - + + - ]: 1 : LOCK(Assert(m_node.chainman)->GetMutex());
577 [ + - - + ]: 1 : tip_height = m_node.chainman->ActiveChain().Height();
578 [ + - - + : 2 : tip_hash = m_node.chainman->ActiveChain().Tip()->GetBlockHash();
+ - ]
579 : 0 : }
580 : :
581 : : // Loading the wallet must rescan the extension from the recorded best
582 : : // block and find its coinbases.
583 [ + - - + ]: 2 : wallet = TestLoadWallet(context);
584 : 1 : {
585 [ + - ]: 1 : LOCK(wallet->cs_wallet);
586 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(wallet->GetLastBlockHeight(), tip_height);
587 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(wallet->GetLastBlockHash(), tip_hash);
588 : : // The extension's coinbases plus the one of the recorded best block:
589 : : // the load rescan starts mid-chain, at that block inclusive.
590 [ + - + - : 1 : BOOST_CHECK_EQUAL(wallet->mapWallet.size(), static_cast<size_t>(NEW_BLOCKS + 1));
+ - ]
591 : 0 : }
592 [ + - ]: 1 : TestUnloadWallet(std::move(wallet));
593 : 1 : }
594 : :
595 : : BOOST_AUTO_TEST_SUITE_END()
596 : :
597 : : } // namespace wallet
|