Coverage Report

Created: 2026-10-07 20:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/root/bitcoin/src/wallet/scan.cpp
Line
Count
Source
1
// Copyright (c) 2026-present The Bitcoin Core developers
2
// Distributed under the MIT software license, see the accompanying
3
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4
5
#include <chain.h>
6
#include <interfaces/chain.h>
7
#include <logging.h>
8
#include <primitives/block.h>
9
#include <sync.h>
10
#include <util/check.h>
11
#include <wallet/scan.h>
12
#include <wallet/wallet.h>
13
14
using interfaces::FoundBlock;
15
16
namespace wallet {
17
18
int64_t ChainScanner::ScanFromTime(int64_t startTime, const WalletRescanReserver& reserver)
19
0
{
20
    // Find starting block. May be null if nCreateTime is greater than the
21
    // highest blockchain timestamp, in which case there is nothing that needs
22
    // to be scanned.
23
0
    int start_height = 0;
24
0
    uint256 start_block;
25
0
    bool start = m_wallet.chain().findFirstBlockWithTimeAndHeight(startTime - TIMESTAMP_WINDOW, 0, FoundBlock().hash(start_block).height(start_height));
26
0
    m_wallet.WalletLogPrintf("%s: Rescanning last %i blocks\n", __func__, start ? WITH_LOCK(m_wallet.cs_wallet, return m_wallet.GetLastBlockHeight()) - start_height + 1 : 0);
  Branch (26:75): [True: 0, False: 0]
27
28
0
    if (start) {
  Branch (28:9): [True: 0, False: 0]
29
        // TODO: this should take into account failure by ScanResult::USER_ABORT
30
0
        ScanResult result = Scan(start_block, start_height, /*max_height=*/{}, reserver, /*save_progress=*/false);
31
0
        if (result.status == ScanResult::FAILURE) {
  Branch (31:13): [True: 0, False: 0]
32
0
            int64_t time_max;
33
0
            CHECK_NONFATAL(m_wallet.chain().findBlock(result.last_failed_block, FoundBlock().maxTime(time_max)));
34
0
            return time_max + TIMESTAMP_WINDOW + 1;
35
0
        }
36
0
    }
37
0
    return startTime;
38
0
}
39
40
0
bool WalletRescanReserver::reserve(bool with_passphrase) {
41
0
    assert(!m_could_reserve);
  Branch (41:5): [True: 0, False: 0]
42
0
    if (!m_wallet.Scanner().TryReserve(with_passphrase)) {
  Branch (42:9): [True: 0, False: 0]
43
0
        return false;
44
0
    }
45
0
    m_could_reserve = true;
46
0
    return true;
47
0
}
48
49
0
bool WalletRescanReserver::isReserved() const {
50
0
    return (m_could_reserve && m_wallet.Scanner().IsScanning());
  Branch (50:13): [True: 0, False: 0]
  Branch (50:32): [True: 0, False: 0]
51
0
}
52
53
0
WalletRescanReserver::~WalletRescanReserver() {
54
0
    if (m_could_reserve) {
  Branch (54:9): [True: 0, False: 0]
55
0
        m_wallet.Scanner().Release();
56
0
    }
57
0
}
58
59
0
bool ChainScanner::TryReserve(bool with_passphrase) {
60
0
    if (m_scanning.exchange(true)) return false;
  Branch (60:9): [True: 0, False: 0]
61
    // Discard any abort request left over from previous reservation, so
62
    // that an abort requested while the reservation is held always applies
63
    // to abort this rescan, even if it arrives before the scan loop starts.
64
0
    m_abort = false;
65
0
    m_scanning_with_passphrase = with_passphrase;
66
0
    m_scanning_start = SteadyClock::now();
67
0
    m_scanning_progress = 0;
68
0
    return true;
69
0
}
70
71
0
void ChainScanner::Release() {
72
0
    m_scanning = false;
73
0
    m_scanning_with_passphrase = false;
74
0
}
75
76
namespace {
77
class FastWalletRescanFilter
78
{
79
public:
80
0
    FastWalletRescanFilter(const CWallet& wallet) : m_wallet(wallet)
81
0
    {
82
        // create initial filter with scripts from all ScriptPubKeyMans
83
0
        for (auto spkm : m_wallet.GetAllScriptPubKeyMans()) {
  Branch (83:24): [True: 0, False: 0]
84
0
            auto desc_spkm{dynamic_cast<DescriptorScriptPubKeyMan*>(spkm)};
85
0
            assert(desc_spkm != nullptr);
  Branch (85:13): [True: 0, False: 0]
86
0
            AddScriptPubKeys(desc_spkm);
87
            // save each range descriptor's end for possible future filter updates
88
0
            if (desc_spkm->IsHDEnabled()) {
  Branch (88:17): [True: 0, False: 0]
89
0
                m_last_range_ends.emplace(desc_spkm->GetID(), desc_spkm->GetEndRange());
90
0
            }
91
0
        }
92
0
    }
93
94
    void UpdateIfNeeded()
95
0
    {
96
        // repopulate filter with new scripts if top-up has happened since last iteration
97
0
        for (const auto& [desc_spkm_id, last_range_end] : m_last_range_ends) {
  Branch (97:57): [True: 0, False: 0]
98
0
            auto desc_spkm{dynamic_cast<DescriptorScriptPubKeyMan*>(m_wallet.GetScriptPubKeyMan(desc_spkm_id))};
99
0
            assert(desc_spkm != nullptr);
  Branch (99:13): [True: 0, False: 0]
100
0
            int32_t current_range_end{desc_spkm->GetEndRange()};
101
0
            if (current_range_end > last_range_end) {
  Branch (101:17): [True: 0, False: 0]
102
0
                AddScriptPubKeys(desc_spkm, last_range_end);
103
0
                m_last_range_ends.at(desc_spkm->GetID()) = current_range_end;
104
0
            }
105
0
        }
106
0
    }
107
108
    std::optional<bool> MatchesBlock(const uint256& block_hash) const
109
0
    {
110
0
        return m_wallet.chain().blockFilterMatchesAny(BlockFilterType::BASIC, block_hash, m_filter_set);
111
0
    }
112
113
private:
114
    const CWallet& m_wallet;
115
    /** Map for keeping track of each range descriptor's last seen end range.
116
      * This information is used to detect whether new addresses were derived
117
      * (that is, if the current end range is larger than the saved end range)
118
      * after processing a block and hence a filter set update is needed to
119
      * take possible keypool top-ups into account.
120
      */
121
    std::map<uint256, int32_t> m_last_range_ends;
122
    GCSFilter::ElementSet m_filter_set;
123
124
    void AddScriptPubKeys(const DescriptorScriptPubKeyMan* desc_spkm, int32_t last_range_end = 0)
125
0
    {
126
0
        for (const auto& script_pub_key : desc_spkm->GetScriptPubKeys(last_range_end)) {
  Branch (126:41): [True: 0, False: 0]
127
0
            m_filter_set.emplace(script_pub_key.begin(), script_pub_key.end());
128
0
        }
129
0
    }
130
};
131
132
0
static bool ShouldFetchBlock(const FastWalletRescanFilter& filter, const uint256& block_hash, int block_height) {
133
0
    auto matches_block{filter.MatchesBlock(block_hash)};
134
0
    if (matches_block.has_value()) {
  Branch (134:9): [True: 0, False: 0]
135
0
        if (*matches_block) {
  Branch (135:13): [True: 0, False: 0]
136
0
            LogDebug(BCLog::SCAN, "Fast rescan: inspect block %d [%s] (filter matched)\n", block_height, block_hash.ToString());
137
0
            return true;
138
0
        } else {
139
0
            return false;
140
0
        }
141
0
    } else {
142
0
        LogDebug(BCLog::SCAN, "Fast rescan: inspect block %d [%s] (WARNING: block filter not found!)\n", block_height, block_hash.ToString());
143
0
        return true;
144
0
    }
145
0
}
146
} // namespace
147
148
0
bool ChainScanner::QueueNextBlock(const uint256& block_hash, int block_height, std::optional<std::pair<uint256, int>>& next_block, std::optional<int> max_height) {
149
0
    bool block_still_active = false;
150
0
    bool has_next_block = false;
151
0
    uint256 next_block_hash;
152
0
    m_wallet.chain().findBlock(block_hash, FoundBlock().inActiveChain(block_still_active).nextBlock(FoundBlock().inActiveChain(has_next_block).hash(next_block_hash)));
153
154
    // Queue the next block if it exists and is within range. Whether the scan
155
    // has caught up with the wallet's tip is checked after the current block
156
    // is processed, so blocks connected while it was being processed are not
157
    // missed.
158
0
    if (has_next_block && (!max_height || block_height < *max_height)) {
  Branch (158:9): [True: 0, False: 0]
  Branch (158:28): [True: 0, False: 0]
  Branch (158:43): [True: 0, False: 0]
159
0
        next_block = {{next_block_hash, block_height + 1}};
160
0
    }
161
162
0
    return block_still_active;
163
0
}
164
165
0
void ChainScanner::UpdateProgress(const LoopState& state, double progress_current, int block_height) {
166
0
    m_scanning_progress = 0;
167
0
    double progress_diff = state.progress_end - state.progress_begin;
168
169
    // avoid divide-by-zero for single block scan range (i.e. start and stop hashes are equal)
170
0
    if (progress_diff <= 0.0) return;
  Branch (170:9): [True: 0, False: 0]
171
0
    m_scanning_progress = (progress_current - state.progress_begin) / progress_diff;
172
173
0
    if (block_height % 100 == 0) {
  Branch (173:9): [True: 0, False: 0]
174
0
        m_wallet.ShowProgress(strprintf("[%s] %s", m_wallet.DisplayName(), _("Rescanning…")),
175
0
                              std::max(1, std::min(99, (int)(m_scanning_progress.load() * 100))));
176
0
    }
177
0
}
178
179
0
void ChainScanner::UpdateTipIfChanged(LoopState& state) {
180
0
    const uint256 new_tip = WITH_LOCK(m_wallet.cs_wallet, return m_wallet.GetLastBlockHash());
181
0
    if (new_tip != state.tip_hash) {
  Branch (181:9): [True: 0, False: 0]
182
0
        state.tip_hash = new_tip;
183
0
        state.progress_end = m_wallet.chain().guessVerificationProgress(state.tip_hash);
184
0
    }
185
0
}
186
187
0
bool ChainScanner::ScanBlock(const uint256& block_hash, int block_height, bool save_progress) {
188
    // Read block data and locator if needed (the locator is usually null unless we need to save progress)
189
0
    CBlock block;
190
0
    CBlockLocator loc;
191
    // Find block
192
0
    FoundBlock found_block{FoundBlock().data(block)};
193
0
    if (save_progress) found_block.locator(loc);
  Branch (193:9): [True: 0, False: 0]
194
0
    m_wallet.chain().findBlock(block_hash, found_block);
195
196
0
    if (block.IsNull()) return false;
  Branch (196:9): [True: 0, False: 0]
197
198
0
    {
199
        // cs_wallet is a RecursiveMutex; ScanBlock may be called
200
        // with cs_wallet already held as in AttachChain or without it.
201
0
        LOCK(m_wallet.cs_wallet);
202
0
        for (size_t posInBlock = 0; posInBlock < block.vtx.size(); ++posInBlock) {
  Branch (202:37): [True: 0, False: 0]
203
0
            m_wallet.SyncTransaction(
204
0
                block.vtx[posInBlock], TxStateConfirmed{block_hash, block_height,
205
0
                static_cast<int>(posInBlock)},
206
0
                /*rescanning_old_block=*/true);
207
0
        }
208
209
0
        if (!loc.IsNull()) {
  Branch (209:13): [True: 0, False: 0]
210
0
            m_wallet.WalletLogPrintf("Saving scan progress %d.\n", block_height);
211
0
            WalletBatch batch(m_wallet.GetDatabase());
212
0
            batch.WriteBestBlock(loc);
213
0
        }
214
0
    }
215
0
    return true;
216
0
}
217
218
ScanResult ChainScanner::Scan(const uint256& start_block, int start_height, std::optional<int> max_height,
219
0
                              const WalletRescanReserver& reserver, bool save_progress) {
220
0
    constexpr auto INTERVAL_TIME{60s};
221
0
    auto current_time{reserver.now()};
222
0
    auto start_time{reserver.now()};
223
224
0
    assert(reserver.isReserved());
  Branch (224:5): [True: 0, False: 0]
225
0
    auto& chain = m_wallet.chain();
226
227
0
    std::unique_ptr<FastWalletRescanFilter> fast_rescan_filter;
228
0
    if (chain.hasBlockFilterIndex(BlockFilterType::BASIC)) fast_rescan_filter = std::make_unique<FastWalletRescanFilter>(m_wallet);
  Branch (228:9): [True: 0, False: 0]
229
230
0
    m_wallet.WalletLogPrintf("Rescan started from block %s... (%s)\n", start_block.ToString(),
231
0
                fast_rescan_filter ? "fast variant using block filters" : "slow variant inspecting all blocks");
  Branch (231:17): [True: 0, False: 0]
232
233
    // show rescan progress in GUI as dialog or on splashscreen, if rescan required on startup (e.g. due to corruption)
234
0
    m_wallet.ShowProgress(strprintf("[%s] %s", m_wallet.DisplayName(), _("Rescanning…")), 0);
235
236
0
    ScanResult result;
237
0
    LoopState state;
238
0
    state.tip_hash = WITH_LOCK(m_wallet.cs_wallet, return m_wallet.GetLastBlockHash());
239
0
    uint256 end_hash = state.tip_hash;
240
0
    if (max_height) chain.findAncestorByHeight(state.tip_hash, *max_height, FoundBlock().hash(end_hash));
  Branch (240:9): [True: 0, False: 0]
241
0
    state.progress_begin = chain.guessVerificationProgress(start_block);
242
0
    state.progress_end = chain.guessVerificationProgress(end_hash);
243
0
    double progress_current = state.progress_begin;
244
0
    std::optional<std::pair<uint256, int>> next_block = {{start_block, start_height}};
245
0
    int block_height = start_height;
246
0
    while (!m_abort && !chain.shutdownRequested()) {
  Branch (246:12): [True: 0, False: 0]
  Branch (246:24): [True: 0, False: 0]
247
0
        if (!next_block) break;
  Branch (247:13): [True: 0, False: 0]
248
249
0
        const uint256 block_hash = next_block->first;
250
0
        block_height = next_block->second;
251
0
        next_block.reset();
252
        // Look up the current block's position separately from reading its
253
        // data below, because reading is slow and there might be a reorg
254
        // while it is read.
255
0
        const bool block_still_active = QueueNextBlock(block_hash, block_height, next_block, max_height);
256
257
0
        progress_current = chain.guessVerificationProgress(block_hash);
258
0
        UpdateProgress(state, progress_current, block_height);
259
260
0
        bool next_interval = reserver.now() >= current_time + INTERVAL_TIME;
261
0
        if (next_interval) {
  Branch (261:13): [True: 0, False: 0]
262
0
            current_time = reserver.now();
263
0
            m_wallet.WalletLogPrintf("Still rescanning. At block %d. Progress=%f\n", block_height, progress_current);
264
0
        }
265
266
0
        bool fetch_block{true};
267
0
        if (fast_rescan_filter) {
  Branch (267:13): [True: 0, False: 0]
268
0
            fast_rescan_filter->UpdateIfNeeded();
269
0
            fetch_block = ShouldFetchBlock(*fast_rescan_filter, block_hash, block_height);
270
0
        }
271
272
0
        if (fetch_block && !block_still_active) {
  Branch (272:13): [True: 0, False: 0]
  Branch (272:28): [True: 0, False: 0]
273
            // Abort scan if a block that needs to be inspected is no longer
274
            // active, to prevent marking transactions as coming from the
275
            // wrong block. A block skipped by the filter can stay skipped:
276
            // it has no successor in the active chain, so the scan ends
277
            // successfully at the reorg point and the replacement blocks are
278
            // handled by blockConnected notifications.
279
0
            result.last_failed_block = block_hash;
280
0
            result.status = ScanResult::FAILURE;
281
0
            break;
282
0
        }
283
0
        if (!fetch_block || ScanBlock(block_hash, block_height, save_progress && next_interval)) {
  Branch (283:13): [True: 0, False: 0]
  Branch (283:29): [True: 0, False: 0]
  Branch (283:65): [True: 0, False: 0]
  Branch (283:82): [True: 0, False: 0]
284
            // scanned the block, or skipped it via the filter: record it as
285
            // the most recent successfully scanned block
286
0
            result.last_scanned_block = block_hash;
287
0
            result.last_scanned_height = block_height;
288
0
        } else {
289
            // could not scan block, keep scanning but record this block as the most recent failure
290
0
            result.last_failed_block = block_hash;
291
0
            result.status = ScanResult::FAILURE;
292
0
        }
293
294
        // Stop scanning once the wallet's tip is reached, re-reading the height
295
        // after the block was processed so a tip extension that happened
296
        // meanwhile is picked up. If scanning with cs_wallet locked (AttachChain),
297
        // blocks connected during rescan are handled after scanning is complete
298
        // via blockConnected notifications. Without the lock, newly added blocks
299
        // are re-processed here if the notifications were handled and the last
300
        // block height was updated.
301
0
        if (block_height >= WITH_LOCK(m_wallet.cs_wallet, return m_wallet.GetLastBlockHeight())) {
  Branch (301:13): [True: 0, False: 0]
302
0
            break;
303
0
        }
304
305
0
        if (!max_height) UpdateTipIfChanged(state);
  Branch (305:13): [True: 0, False: 0]
306
0
    }
307
0
    if (!max_height) {
  Branch (307:9): [True: 0, False: 0]
308
0
        m_wallet.WalletLogPrintf("Scanning current mempool transactions.\n");
309
0
        WITH_LOCK(m_wallet.cs_wallet, chain.requestMempoolTransactions(m_wallet));
310
0
    }
311
0
    m_wallet.ShowProgress(strprintf("[%s] %s", m_wallet.DisplayName(), _("Rescanning…")), 100); // hide progress dialog in GUI
312
0
    if (m_abort) {
  Branch (312:9): [True: 0, False: 0]
313
0
        m_wallet.WalletLogPrintf("Rescan aborted at block %d. Progress=%f\n", block_height, progress_current);
314
0
        result.status = ScanResult::USER_ABORT;
315
0
    } else if (chain.shutdownRequested()) {
  Branch (315:16): [True: 0, False: 0]
316
0
        m_wallet.WalletLogPrintf("Rescan interrupted by shutdown request at block %d. Progress=%f\n", block_height, progress_current);
317
0
        result.status = ScanResult::USER_ABORT;
318
0
    } else {
319
0
        m_wallet.WalletLogPrintf("Rescan completed in %15dms\n", Ticks<std::chrono::milliseconds>(reserver.now() - start_time));
320
0
    }
321
0
    return result;
322
0
}
323
}