Branch data Line data Source code
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 : 0 : int64_t ChainScanner::ScanFromTime(int64_t startTime, const WalletRescanReserver& reserver)
19 : : {
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);
27 : :
28 [ # # ]: 0 : if (start) {
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) {
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 : : }
36 : : }
37 : : return startTime;
38 : : }
39 : :
40 : 0 : bool WalletRescanReserver::reserve(bool with_passphrase) {
41 [ # # ]: 0 : assert(!m_could_reserve);
42 [ # # ]: 0 : if (!m_wallet.Scanner().TryReserve(with_passphrase)) {
43 : : return false;
44 : : }
45 : 0 : m_could_reserve = true;
46 : 0 : return true;
47 : : }
48 : :
49 : 0 : bool WalletRescanReserver::isReserved() const {
50 [ # # # # ]: 0 : return (m_could_reserve && m_wallet.Scanner().IsScanning());
51 : : }
52 : :
53 : 0 : WalletRescanReserver::~WalletRescanReserver() {
54 [ # # ]: 0 : if (m_could_reserve) {
55 : 0 : m_wallet.Scanner().Release();
56 : : }
57 : 0 : }
58 : :
59 : 0 : bool ChainScanner::TryReserve(bool with_passphrase) {
60 [ # # ]: 0 : 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 : 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 : : }
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 : : {
82 : : // create initial filter with scripts from all ScriptPubKeyMans
83 [ # # # # ]: 0 : for (auto spkm : m_wallet.GetAllScriptPubKeyMans()) {
84 [ # # ]: 0 : auto desc_spkm{dynamic_cast<DescriptorScriptPubKeyMan*>(spkm)};
85 [ # # ]: 0 : assert(desc_spkm != nullptr);
86 [ # # ]: 0 : AddScriptPubKeys(desc_spkm);
87 : : // save each range descriptor's end for possible future filter updates
88 [ # # # # ]: 0 : if (desc_spkm->IsHDEnabled()) {
89 [ # # # # : 0 : m_last_range_ends.emplace(desc_spkm->GetID(), desc_spkm->GetEndRange());
# # ]
90 : : }
91 : 0 : }
92 : 0 : }
93 : :
94 : 0 : void UpdateIfNeeded()
95 : : {
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) {
98 [ # # ]: 0 : auto desc_spkm{dynamic_cast<DescriptorScriptPubKeyMan*>(m_wallet.GetScriptPubKeyMan(desc_spkm_id))};
99 [ # # ]: 0 : assert(desc_spkm != nullptr);
100 : 0 : int32_t current_range_end{desc_spkm->GetEndRange()};
101 [ # # ]: 0 : if (current_range_end > last_range_end) {
102 : 0 : AddScriptPubKeys(desc_spkm, last_range_end);
103 : 0 : m_last_range_ends.at(desc_spkm->GetID()) = current_range_end;
104 : : }
105 : : }
106 : 0 : }
107 : :
108 : 0 : std::optional<bool> MatchesBlock(const uint256& block_hash) const
109 : : {
110 : 0 : return m_wallet.chain().blockFilterMatchesAny(BlockFilterType::BASIC, block_hash, m_filter_set);
111 : : }
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 : 0 : void AddScriptPubKeys(const DescriptorScriptPubKeyMan* desc_spkm, int32_t last_range_end = 0)
125 : : {
126 [ # # # # ]: 0 : for (const auto& script_pub_key : desc_spkm->GetScriptPubKeys(last_range_end)) {
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()) {
135 [ # # ]: 0 : if (*matches_block) {
136 [ # # # # ]: 0 : LogDebug(BCLog::SCAN, "Fast rescan: inspect block %d [%s] (filter matched)\n", block_height, block_hash.ToString());
137 : 0 : return true;
138 : : } else {
139 : : return false;
140 : : }
141 : : } 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 : : }
145 : : }
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)) {
# # ]
159 : 0 : next_block = {{next_block_hash, block_height + 1}};
160 : : }
161 : :
162 : 0 : return block_still_active;
163 : : }
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;
171 [ # # ]: 0 : m_scanning_progress = (progress_current - state.progress_begin) / progress_diff;
172 : :
173 [ # # ]: 0 : if (block_height % 100 == 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 : : }
177 : : }
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) {
182 : 0 : state.tip_hash = new_tip;
183 : 0 : state.progress_end = m_wallet.chain().guessVerificationProgress(state.tip_hash);
184 : : }
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);
194 [ # # ]: 0 : m_wallet.chain().findBlock(block_hash, found_block);
195 : :
196 [ # # ]: 0 : if (block.IsNull()) return false;
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) {
203 : 0 : m_wallet.SyncTransaction(
204 [ # # ]: 0 : block.vtx[posInBlock], TxStateConfirmed{block_hash, block_height,
205 [ # # ]: 0 : static_cast<int>(posInBlock)},
206 : : /*rescanning_old_block=*/true);
207 : : }
208 : :
209 [ # # ]: 0 : if (!loc.IsNull()) {
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 : 0 : ScanResult ChainScanner::Scan(const uint256& start_block, int start_height, std::optional<int> max_height,
219 : : 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());
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);
# # ]
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");
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));
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()) {
# # ]
247 [ # # ]: 0 : if (!next_block) break;
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) {
262 [ # # ]: 0 : current_time = reserver.now();
263 [ # # ]: 0 : m_wallet.WalletLogPrintf("Still rescanning. At block %d. Progress=%f\n", block_height, progress_current);
264 : : }
265 : :
266 : 0 : bool fetch_block{true};
267 [ # # ]: 0 : if (fast_rescan_filter) {
268 [ # # ]: 0 : fast_rescan_filter->UpdateIfNeeded();
269 [ # # ]: 0 : fetch_block = ShouldFetchBlock(*fast_rescan_filter, block_hash, block_height);
270 : : }
271 : :
272 [ # # ]: 0 : 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 : 0 : result.last_failed_block = block_hash;
280 : 0 : result.status = ScanResult::FAILURE;
281 : 0 : break;
282 : : }
283 [ # # # # : 0 : 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 : 0 : result.last_scanned_block = block_hash;
287 : 0 : result.last_scanned_height = block_height;
288 : : } 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 : : }
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())) {
# # ]
302 : : break;
303 : : }
304 : :
305 [ # # # # ]: 0 : if (!max_height) UpdateTipIfChanged(state);
306 : : }
307 [ # # ]: 0 : if (!max_height) {
308 [ # # ]: 0 : m_wallet.WalletLogPrintf("Scanning current mempool transactions.\n");
309 [ # # # # ]: 0 : WITH_LOCK(m_wallet.cs_wallet, chain.requestMempoolTransactions(m_wallet));
310 : : }
311 [ # # # # : 0 : m_wallet.ShowProgress(strprintf("[%s] %s", m_wallet.DisplayName(), _("Rescanning…")), 100); // hide progress dialog in GUI
# # ]
312 [ # # ]: 0 : if (m_abort) {
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()) {
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 : : } else {
319 [ # # # # ]: 0 : m_wallet.WalletLogPrintf("Rescan completed in %15dms\n", Ticks<std::chrono::milliseconds>(reserver.now() - start_time));
320 : : }
321 : 0 : return result;
322 : 0 : }
323 : : }
|