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