Coverage Report

Created: 2026-09-14 20:36

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/tmp/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
639
{
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
639
    int start_height = 0;
24
639
    uint256 start_block;
25
639
    bool start = m_wallet.chain().findFirstBlockWithTimeAndHeight(startTime - TIMESTAMP_WINDOW, 0, FoundBlock().hash(start_block).height(start_height));
26
639
    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);
27
28
639
    if (start) {
29
        // TODO: this should take into account failure by ScanResult::USER_ABORT
30
638
        ScanResult result = Scan(start_block, start_height, /*max_height=*/{}, reserver, /*save_progress=*/false);
31
638
        if (result.status == ScanResult::FAILURE) {
32
3
            int64_t time_max;
33
3
            CHECK_NONFATAL(m_wallet.chain().findBlock(result.last_failed_block, FoundBlock().maxTime(time_max)));
34
3
            return time_max + TIMESTAMP_WINDOW + 1;
35
3
        }
36
638
    }
37
636
    return startTime;
38
639
}
39
40
1.11k
bool WalletRescanReserver::reserve(bool with_passphrase) {
41
1.11k
    assert(!m_could_reserve);
42
1.11k
    if (!m_wallet.Scanner().TryReserve(with_passphrase)) {
43
2
        return false;
44
2
    }
45
1.11k
    m_could_reserve = true;
46
1.11k
    return true;
47
1.11k
}
48
49
731
bool WalletRescanReserver::isReserved() const {
50
731
    return (m_could_reserve && m_wallet.Scanner().IsScanning());
51
731
}
52
53
1.11k
WalletRescanReserver::~WalletRescanReserver() {
54
1.11k
    if (m_could_reserve) {
55
1.11k
        m_wallet.Scanner().Release();
56
1.11k
    }
57
1.11k
}
58
59
1.11k
bool ChainScanner::TryReserve(bool with_passphrase) {
60
1.11k
    if (m_scanning.exchange(true)) return false;
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
1.11k
    m_abort = false;
65
1.11k
    m_scanning_with_passphrase = with_passphrase;
66
1.11k
    m_scanning_start = SteadyClock::now();
67
1.11k
    m_scanning_progress = 0;
68
1.11k
    return true;
69
1.11k
}
70
71
1.11k
void ChainScanner::Release() {
72
1.11k
    m_scanning = false;
73
1.11k
    m_scanning_with_passphrase = false;
74
1.11k
}
75
76
namespace {
77
class FastWalletRescanFilter
78
{
79
public:
80
8
    FastWalletRescanFilter(const CWallet& wallet) : m_wallet(wallet)
81
8
    {
82
        // create initial filter with scripts from all ScriptPubKeyMans
83
39
        for (auto spkm : m_wallet.GetAllScriptPubKeyMans()) {
84
39
            auto desc_spkm{dynamic_cast<DescriptorScriptPubKeyMan*>(spkm)};
85
39
            assert(desc_spkm != nullptr);
86
39
            AddScriptPubKeys(desc_spkm);
87
            // save each range descriptor's end for possible future filter updates
88
39
            if (desc_spkm->IsHDEnabled()) {
89
32
                m_last_range_ends.emplace(desc_spkm->GetID(), desc_spkm->GetEndRange());
90
32
            }
91
39
        }
92
8
    }
93
94
    void UpdateIfNeeded()
95
719
    {
96
        // repopulate filter with new scripts if top-up has happened since last iteration
97
4.92k
        for (const auto& [desc_spkm_id, last_range_end] : m_last_range_ends) {
98
4.92k
            auto desc_spkm{dynamic_cast<DescriptorScriptPubKeyMan*>(m_wallet.GetScriptPubKeyMan(desc_spkm_id))};
99
4.92k
            assert(desc_spkm != nullptr);
100
4.92k
            int32_t current_range_end{desc_spkm->GetEndRange()};
101
4.92k
            if (current_range_end > last_range_end) {
102
64
                AddScriptPubKeys(desc_spkm, last_range_end);
103
64
                m_last_range_ends.at(desc_spkm->GetID()) = current_range_end;
104
64
            }
105
4.92k
        }
106
719
    }
107
108
    std::optional<bool> MatchesBlock(const uint256& block_hash) const
109
719
    {
110
719
        return m_wallet.chain().blockFilterMatchesAny(BlockFilterType::BASIC, block_hash, m_filter_set);
111
719
    }
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
103
    {
126
9.61k
        for (const auto& script_pub_key : desc_spkm->GetScriptPubKeys(last_range_end)) {
127
9.61k
            m_filter_set.emplace(script_pub_key.begin(), script_pub_key.end());
128
9.61k
        }
129
103
    }
130
};
131
132
719
static bool ShouldFetchBlock(const FastWalletRescanFilter& filter, const uint256& block_hash, int block_height) {
133
719
    auto matches_block{filter.MatchesBlock(block_hash)};
134
719
    if (matches_block.has_value()) {
135
618
        if (*matches_block) {
136
66
            LogDebug(BCLog::SCAN, "Fast rescan: inspect block %d [%s] (filter matched)\n", block_height, block_hash.ToString());
137
66
            return true;
138
552
        } else {
139
552
            return false;
140
552
        }
141
618
    } else {
142
101
        LogDebug(BCLog::SCAN, "Fast rescan: inspect block %d [%s] (WARNING: block filter not found!)\n", block_height, block_hash.ToString());
143
101
        return true;
144
101
    }
145
719
}
146
} // namespace
147
148
81.7k
bool ChainScanner::QueueNextBlock(const uint256& block_hash, int block_height, std::optional<std::pair<uint256, int>>& next_block, std::optional<int> max_height) {
149
81.7k
    bool block_still_active = false;
150
81.7k
    bool has_next_block = false;
151
81.7k
    uint256 next_block_hash;
152
81.7k
    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
81.7k
    if (has_next_block && (!max_height || block_height < *max_height)) {
159
81.0k
        next_block = {{next_block_hash, block_height + 1}};
160
81.0k
    }
161
162
81.7k
    return block_still_active;
163
81.7k
}
164
165
81.7k
void ChainScanner::UpdateProgress(const LoopState& state, double progress_current, int block_height) {
166
81.7k
    m_scanning_progress = 0;
167
81.7k
    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
81.7k
    if (progress_diff <= 0.0) return;
171
81.5k
    m_scanning_progress = (progress_current - state.progress_begin) / progress_diff;
172
173
81.5k
    if (block_height % 100 == 0) {
174
904
        m_wallet.ShowProgress(strprintf("[%s] %s", m_wallet.DisplayName(), _("Rescanning…")),
175
904
                              std::max(1, std::min(99, (int)(m_scanning_progress.load() * 100))));
176
904
    }
177
81.5k
}
178
179
80.6k
void ChainScanner::UpdateTipIfChanged(LoopState& state) {
180
80.6k
    const uint256 new_tip = WITH_LOCK(m_wallet.cs_wallet, return m_wallet.GetLastBlockHash());
181
80.6k
    if (new_tip != state.tip_hash) {
182
0
        state.tip_hash = new_tip;
183
0
        state.progress_end = m_wallet.chain().guessVerificationProgress(state.tip_hash);
184
0
    }
185
80.6k
}
186
187
81.2k
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
81.2k
    CBlock block;
190
81.2k
    CBlockLocator loc;
191
    // Find block
192
81.2k
    FoundBlock found_block{FoundBlock().data(block)};
193
81.2k
    if (save_progress) found_block.locator(loc);
194
81.2k
    m_wallet.chain().findBlock(block_hash, found_block);
195
196
81.2k
    if (block.IsNull()) return false;
197
198
80.9k
    {
199
        // cs_wallet is a RecursiveMutex; ScanBlock may be called
200
        // with cs_wallet already held as in AttachChain or without it.
201
80.9k
        LOCK(m_wallet.cs_wallet);
202
183k
        for (size_t posInBlock = 0; posInBlock < block.vtx.size(); ++posInBlock) {
203
102k
            m_wallet.SyncTransaction(
204
102k
                block.vtx[posInBlock], TxStateConfirmed{block_hash, block_height,
205
102k
                static_cast<int>(posInBlock)},
206
102k
                /*rescanning_old_block=*/true);
207
102k
        }
208
209
80.9k
        if (!loc.IsNull()) {
210
2
            m_wallet.WalletLogPrintf("Saving scan progress %d.\n", block_height);
211
2
            WalletBatch batch(m_wallet.GetDatabase());
212
2
            batch.WriteBestBlock(loc);
213
2
        }
214
80.9k
    }
215
80.9k
    return true;
216
81.2k
}
217
218
ScanResult ChainScanner::Scan(const uint256& start_block, int start_height, std::optional<int> max_height,
219
729
                              const WalletRescanReserver& reserver, bool save_progress) {
220
729
    constexpr auto INTERVAL_TIME{60s};
221
729
    auto current_time{reserver.now()};
222
729
    auto start_time{reserver.now()};
223
224
729
    assert(reserver.isReserved());
225
729
    auto& chain = m_wallet.chain();
226
227
729
    std::unique_ptr<FastWalletRescanFilter> fast_rescan_filter;
228
729
    if (chain.hasBlockFilterIndex(BlockFilterType::BASIC)) fast_rescan_filter = std::make_unique<FastWalletRescanFilter>(m_wallet);
229
230
729
    m_wallet.WalletLogPrintf("Rescan started from block %s... (%s)\n", start_block.ToString(),
231
729
                fast_rescan_filter ? "fast variant using block filters" : "slow variant inspecting all blocks");
232
233
    // show rescan progress in GUI as dialog or on splashscreen, if rescan required on startup (e.g. due to corruption)
234
729
    m_wallet.ShowProgress(strprintf("[%s] %s", m_wallet.DisplayName(), _("Rescanning…")), 0);
235
236
729
    ScanResult result;
237
729
    LoopState state;
238
729
    state.tip_hash = WITH_LOCK(m_wallet.cs_wallet, return m_wallet.GetLastBlockHash());
239
729
    uint256 end_hash = state.tip_hash;
240
729
    if (max_height) chain.findAncestorByHeight(state.tip_hash, *max_height, FoundBlock().hash(end_hash));
241
729
    state.progress_begin = chain.guessVerificationProgress(start_block);
242
729
    state.progress_end = chain.guessVerificationProgress(end_hash);
243
729
    double progress_current = state.progress_begin;
244
729
    std::optional<std::pair<uint256, int>> next_block = {{start_block, start_height}};
245
729
    int block_height = start_height;
246
81.7k
    while (!m_abort && !chain.shutdownRequested()) {
247
81.7k
        if (!next_block) break;
248
249
81.7k
        const uint256 block_hash = next_block->first;
250
81.7k
        block_height = next_block->second;
251
81.7k
        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
81.7k
        const bool block_still_active = QueueNextBlock(block_hash, block_height, next_block, max_height);
256
257
81.7k
        progress_current = chain.guessVerificationProgress(block_hash);
258
81.7k
        UpdateProgress(state, progress_current, block_height);
259
260
81.7k
        bool next_interval = reserver.now() >= current_time + INTERVAL_TIME;
261
81.7k
        if (next_interval) {
262
101
            current_time = reserver.now();
263
101
            m_wallet.WalletLogPrintf("Still rescanning. At block %d. Progress=%f\n", block_height, progress_current);
264
101
        }
265
266
81.7k
        bool fetch_block{true};
267
81.7k
        if (fast_rescan_filter) {
268
719
            fast_rescan_filter->UpdateIfNeeded();
269
719
            fetch_block = ShouldFetchBlock(*fast_rescan_filter, block_hash, block_height);
270
719
        }
271
272
81.7k
        if (fetch_block && !block_still_active) {
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
3
            result.last_failed_block = block_hash;
280
3
            result.status = ScanResult::FAILURE;
281
3
            break;
282
3
        }
283
81.7k
        if (!fetch_block || ScanBlock(block_hash, block_height, save_progress && next_interval)) {
284
            // scanned the block, or skipped it via the filter: record it as
285
            // the most recent successfully scanned block
286
81.4k
            result.last_scanned_block = block_hash;
287
81.4k
            result.last_scanned_height = block_height;
288
81.4k
        } else {
289
            // could not scan block, keep scanning but record this block as the most recent failure
290
304
            result.last_failed_block = block_hash;
291
304
            result.status = ScanResult::FAILURE;
292
304
        }
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
81.7k
        if (block_height >= WITH_LOCK(m_wallet.cs_wallet, return m_wallet.GetLastBlockHeight())) {
302
719
            break;
303
719
        }
304
305
81.0k
        if (!max_height) UpdateTipIfChanged(state);
306
81.0k
    }
307
729
    if (!max_height) {
308
724
        m_wallet.WalletLogPrintf("Scanning current mempool transactions.\n");
309
724
        WITH_LOCK(m_wallet.cs_wallet, chain.requestMempoolTransactions(m_wallet));
310
724
    }
311
729
    m_wallet.ShowProgress(strprintf("[%s] %s", m_wallet.DisplayName(), _("Rescanning…")), 100); // hide progress dialog in GUI
312
729
    if (m_abort) {
313
2
        m_wallet.WalletLogPrintf("Rescan aborted at block %d. Progress=%f\n", block_height, progress_current);
314
2
        result.status = ScanResult::USER_ABORT;
315
727
    } else if (chain.shutdownRequested()) {
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
727
    } else {
319
727
        m_wallet.WalletLogPrintf("Rescan completed in %15dms\n", Ticks<std::chrono::milliseconds>(reserver.now() - start_time));
320
727
    }
321
729
    return result;
322
729
}
323
}