LCOV - code coverage report
Current view: top level - src/wallet - scan.cpp (source / functions) Coverage Total Hit
Test: fuzz_coverage.info Lines: 0.0 % 170 0
Test Date: 2026-09-27 07:34:41 Functions: 0.0 % 21 0
Branches: 0.0 % 236 0

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

Generated by: LCOV version 2.5.0-full