Coverage Report

Created: 2026-10-07 13:27

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/tmp/bitcoin/src/wallet/test/rescan_tests.cpp
Line
Count
Source
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
BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup)
40
1
{
41
    // Cap last block file size, and mine new block in a new block file.
42
1
    CBlockIndex* oldTip = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip());
43
1
    WITH_LOCK(::cs_main, m_node.chainman->m_blockman.GetBlockFileInfo(oldTip->GetBlockPos().nFile)->nSize = MAX_BLOCKFILE_SIZE);
44
1
    CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
45
1
    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
1
        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
1
            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
1
        BOOST_CHECK(result.last_failed_block.IsNull());
62
1
        BOOST_CHECK(result.last_scanned_block.IsNull());
63
1
        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
1
        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
7
        reserver.setNow([&] { fake_time += 60s; return fake_time; });
81
1
        reserver.reserve();
82
83
1
        {
84
1
            CBlockLocator locator;
85
1
            BOOST_CHECK(WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator));
86
1
            BOOST_REQUIRE(!locator.IsNull());
87
1
            BOOST_CHECK(locator.vHave.front() == newTip->GetBlockHash());
88
1
        }
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
1
        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
1
            BOOST_CHECK(WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator));
100
1
            BOOST_REQUIRE(!locator.IsNull());
101
1
            BOOST_CHECK(locator.vHave.front() == newTip->GetBlockHash());
102
1
        }
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
1
    }
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
1
        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
1
            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
1
    }
141
1
    m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number});
142
143
    // Verify Scan scans no blocks.
144
1
    {
145
1
        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
1
            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
1
        BOOST_CHECK(result.last_scanned_block.IsNull());
159
1
        BOOST_CHECK(!result.last_scanned_height);
160
1
        BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 0);
161
1
    }
162
1
}
163
164
BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_reorged_block, TestChain100Setup)
165
1
{
166
1
    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
1
    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
1
    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
1
    BOOST_REQUIRE(m_node.chainman->ActiveChainstate().InvalidateBlock(state, stale_block));
179
1
    const CScript replacement_script{GetScriptForRawPubKey(GenerateRandomKey().GetPubKey())};
180
1
    CreateAndProcessBlock({}, replacement_script);
181
1
    CreateAndProcessBlock({}, replacement_script);
182
1
    BOOST_REQUIRE(filter_index.BlockUntilSyncedToCurrentChain());
183
1
    {
184
1
        LOCK(Assert(m_node.chainman)->GetMutex());
185
1
        BOOST_REQUIRE(!m_node.chainman->ActiveChain().Contains(*stale_block));
186
1
        BOOST_REQUIRE_EQUAL(m_node.chainman->ActiveChain().Height(), stale_height + 1);
187
1
    }
188
189
1
    {
190
1
        BlockFilter filter;
191
1
        BOOST_REQUIRE(filter_index.LookupFilter(stale_block, filter));
192
1
    }
193
194
    // Test wallet whose scripts do not match the stale block's filter.
195
1
    {
196
1
        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
1
            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
1
        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
1
        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
1
            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
1
        BOOST_CHECK(result.last_scanned_block.IsNull());
228
1
        BOOST_CHECK(!result.last_scanned_height);
229
1
        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
1
    }
239
1
    m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number});
240
241
1
    {
242
1
        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
1
            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
1
        BOOST_CHECK(result.last_scanned_block.IsNull());
256
1
        BOOST_CHECK(!result.last_scanned_height);
257
1
        BOOST_CHECK(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.empty()));
258
1
    }
259
260
1
    StopIndex(filter_index, m_node);
261
1
    BOOST_REQUIRE(DestroyBlockFilterIndex(BlockFilterType::BASIC));
262
1
}
263
264
BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_abort, TestChain100Setup)
265
1
{
266
1
    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
1
        wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
273
1
        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
1
    BOOST_CHECK(reserver.reserve());
281
1
    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
1
    BOOST_CHECK(result.last_scanned_block.IsNull());
290
1
    BOOST_CHECK(!result.last_scanned_height);
291
1
    BOOST_CHECK(result.last_failed_block.IsNull());
292
1
}
293
294
BOOST_FIXTURE_TEST_CASE(wallet_rescan_reserver, TestingSetup)
295
1
{
296
1
    CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
297
298
    // No scan in progress: accessors report idle state.
299
1
    BOOST_CHECK(!wallet.Scanner().IsScanning());
300
1
    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
1
        BOOST_CHECK(first_reserver.reserve());
306
1
        BOOST_CHECK(first_reserver.isReserved());
307
1
        BOOST_CHECK(wallet.Scanner().IsScanning());
308
1
        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
1
        BOOST_CHECK(!second_reserver.reserve());
314
1
        BOOST_CHECK(!second_reserver.isReserved());
315
1
    }
316
    // Destroying the reserver (RAII) clears the scanning state.
317
1
    BOOST_CHECK(!wallet.Scanner().IsScanning());
318
319
1
    {
320
1
        WalletRescanReserver passphrase_reserver(wallet);
321
1
        BOOST_CHECK(passphrase_reserver.reserve(/*with_passphrase=*/true));
322
1
        BOOST_CHECK(wallet.Scanner().IsScanningWithPassphrase());
323
1
    }
324
1
    BOOST_CHECK(!wallet.Scanner().IsScanningWithPassphrase());
325
1
}
326
327
BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_bounded, TestChain100Setup)
328
1
{
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
1
        genesis_hash = m_node.chainman->ActiveChain().Genesis()->GetBlockHash();
334
1
        tip_height = m_node.chainman->ActiveChain().Height();
335
1
        tip_hash = m_node.chainman->ActiveChain().Tip()->GetBlockHash();
336
1
        max_height = tip_height - 2;
337
1
        max_hash = m_node.chainman->ActiveChain()[max_height]->GetBlockHash();
338
1
    }
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
1
        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
1
        }
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
1
        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
1
        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
1
        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
1
        }
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
1
        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
1
        BOOST_CHECK_EQUAL(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.size()), 1U);
379
1
    }
380
1
}
381
382
BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_tip_extension, TestChain100Setup)
383
1
{
384
1
    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
1
        wallet.SetLastBlockProcessed(start_tip_height, m_node.chainman->ActiveChain().Tip()->GetBlockHash());
393
1
        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
4
    auto handler = wallet.ShowProgress.connect([&](const std::string&, int) {
404
4
        if (extended) return;
405
1
        extended = true;
406
1
        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
1
    });
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
1
}
422
423
BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_no_progress_saved, TestChain100Setup)
424
1
{
425
1
    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
1
        tip_hash = m_node.chainman->ActiveChain().Tip()->GetBlockHash();
433
1
        wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), tip_hash);
434
1
        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
201
    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
1
    BOOST_CHECK(WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator));
454
1
    BOOST_CHECK(!locator.IsNull());
455
1
    BOOST_CHECK_EQUAL(locator.vHave.front(), tip_hash);
456
1
}
457
458
BOOST_FIXTURE_TEST_CASE(rescan_from_time, TestChain100Setup)
459
1
{
460
    // Cap last block file size, and mine new block in a new block file.
461
1
    CBlockIndex* old_tip = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip());
462
1
    WITH_LOCK(::cs_main, m_node.chainman->m_blockman.GetBlockFileInfo(old_tip->GetBlockPos().nFile)->nSize = MAX_BLOCKFILE_SIZE);
463
1
    CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
464
1
    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
1
    }
473
1
    m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number});
474
475
1
    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
1
        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
1
    const int64_t genesis_time{WITH_LOCK(::cs_main, return m_node.chainman->ActiveChain().Genesis()->GetBlockTime())};
490
1
    BOOST_CHECK_EQUAL(wallet.Scanner().ScanFromTime(genesis_time, reserver),
491
1
                      WITH_LOCK(::cs_main, return old_tip->GetBlockTimeMax()) + TIMESTAMP_WINDOW + 1);
492
493
1
    bool scan_logged{false};
494
1
    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
1
    const int64_t future_time{WITH_LOCK(::cs_main, return new_tip->GetBlockTimeMax()) + TIMESTAMP_WINDOW + 1};
500
1
    BOOST_CHECK(!scan_logged);
501
1
    BOOST_CHECK_EQUAL(wallet.Scanner().ScanFromTime(future_time, reserver), future_time);
502
1
}
503
504
BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_missing_filter, TestChain100Setup)
505
1
{
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
1
    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
1
    BOOST_REQUIRE(filter_index.Init());
512
513
1
    {
514
1
        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
1
            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
1
            BOOST_REQUIRE(!filter_index.LookupFilter(tip, filter));
528
1
        }
529
1
        AddKey(wallet, coinbaseKey);
530
1
        WalletRescanReserver reserver(wallet);
531
1
        reserver.reserve();
532
1
        bool fast_scan_logged{false};
533
2
        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
2
        }};
537
1
        ScanResult result = wallet.Scanner().Scan(genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false);
538
1
        BOOST_REQUIRE(fast_scan_logged);
539
1
        BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS);
540
1
        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
1
        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
1
    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
BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_attach_chain, TestChain100Setup)
555
1
{
556
    // Do not wait for sqlite to flush data to disk to improve performance
557
1
    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
1
    constexpr int NEW_BLOCKS{5};
569
6
    for (int i = 0; i < NEW_BLOCKS; ++i) {
570
5
        CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
571
5
    }
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
1
        tip_hash = m_node.chainman->ActiveChain().Tip()->GetBlockHash();
579
1
    }
580
581
    // Loading the wallet must rescan the extension from the recorded best
582
    // block and find its coinbases.
583
1
    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
        BOOST_CHECK_EQUAL(wallet->mapWallet.size(), static_cast<size_t>(NEW_BLOCKS + 1));
591
1
    }
592
1
    TestUnloadWallet(std::move(wallet));
593
1
}
594
595
BOOST_AUTO_TEST_SUITE_END()
596
597
} // namespace wallet