/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 | | } |