Merge bitcoin/bitcoin#34681: wallet: move rescan logic into ChainScanner and wallet/scan
What changed, and why it matters
This is a code cleanup change in Bitcoin Core's wallet. It moves the wallet's blockchain rescan logic out of the main CWallet class into a new dedicated ChainScanner class, without changing what the rescan does. The only intentional behavior change is the order of two internal checks: the code now verifies a block is still on the active chain before trying to read it, rather than after. The change is documented and tested, and appears to reduce risk rather than introduce it.
No immediate action required. Treat as routine maintenance/refactor. Reviewers should verify the documented order-of-operations change does not affect rescan failure reporting in edge cases (e.g., pruned blocks that are also no longer active).
Security signals we found
Refactor only: no new network surface, no new RPCs, no cryptographic changes
One documented behavior change: active-chain check now precedes block read in rescan loop
New test explicitly validates reorged-block handling: filter-matched stale block causes FAILURE; filter-skipped stale block returns SUCCESS
WalletRescanReserver reservation/abort semantics preserved and tested
No vendor security disclosure or CVE referenced in commit or supplied materials
Evidence from the diff
PR #34681 refactors wallet rescanning by introducing wallet::ChainScanner in wallet/scan.h/.cpp. Scan state atomics (fAbortRescan, fScanningWallet, m_scanning_with_passphrase, etc.), WalletRescanReserver, RescanFromTime, and ScanForWalletTransactions are relocated from CWallet to ChainScanner, with CWallet exposing a Scanner() accessor. The logic is decomposed into helpers: ShouldFetchBlock, ScanBlock, QueueNextBlock, UpdateProgress, UpdateTipIfChanged. The one noted behavior change is that active-chain membership is checked via QueueNextBlock before attempting to read the block, whereas previously the read happened first and the active check happened inside the locked block-processing section. New tests cover reorged blocks, bounded scans, tip extension during scan, rescan reserver state, missing filters, and attach-chain rescan.
Changed components
src/wallet/scan.cppsrc/wallet/scan.hsrc/wallet/wallet.cppsrc/wallet/wallet.hsrc/wallet/rpc/backup.cppsrc/wallet/rpc/encrypt.cppsrc/wallet/rpc/transactions.cppsrc/wallet/rpc/wallet.cppsrc/wallet/interfaces.cppsrc/wallet/test/wallet_tests.cppsrc/qt/test/wallettests.cppInspect captured patch +942 / −387
### src/qt/test/wallettests.cpp
@@ -28,6 +28,7 @@
#include <test/util/setup_common.h>
#include <validation.h>
#include <wallet/test/util.h>
+#include <wallet/scan.h>
#include <wallet/wallet.h>
#include <chrono>
@@ -179,8 +180,8 @@ void SyncUpWallet(const std::shared_ptr<CWallet>& wallet, interfaces::Node& node
{
WalletRescanReserver reserver(*wallet);
reserver.reserve();
- CWallet::ScanResult result = wallet->ScanForWalletTransactions(Params().GetConsensus().hashGenesisBlock, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false);
- QCOMPARE(result.status, CWallet::ScanResult::SUCCESS);
+ wallet::ScanResult result = wallet->Scanner().Scan(Params().GetConsensus().hashGenesisBlock, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false);
+ QCOMPARE(result.status, wallet::ScanResult::SUCCESS);
QCOMPARE(result.last_scanned_block, WITH_LOCK(node.context()->chainman->GetMutex(), return node.context()->chainman->ActiveChain().Tip()->GetBlockHash()));
QVERIFY(result.last_failed_block.IsNull());
}
### src/wallet/CMakeLists.txt
@@ -27,6 +27,7 @@ add_library(bitcoin_wallet STATIC EXCLUDE_FROM_ALL
rpc/transactions.cpp
rpc/util.cpp
rpc/wallet.cpp
+ scan.cpp
scriptpubkeyman.cpp
spend.cpp
sqlite.cpp
### src/wallet/interfaces.cpp
@@ -28,6 +28,7 @@
#include <wallet/receive.h>
#include <wallet/rpc/wallet.h>
#include <wallet/spend.h>
+#include <wallet/scan.h>
#include <wallet/wallet.h>
#include <memory>
@@ -152,7 +153,7 @@ class WalletImpl : public Wallet
{
return m_wallet->ChangeWalletPassphrase(old_wallet_passphrase, new_wallet_passphrase);
}
- void abortRescan() override { m_wallet->AbortRescan(); }
+ void abortRescan() override { m_wallet->Scanner().Abort(); }
bool backupWallet(const std::string& filename) override { return m_wallet->BackupWallet(filename); }
std::string getWalletName() override { return m_wallet->GetName(); }
util::Result<CTxDestination> getNewDestination(const OutputType type, const std::string& label) override
### src/wallet/rpc/backup.cpp
@@ -23,6 +23,7 @@
#include <util/translation.h>
#include <wallet/export.h>
#include <wallet/rpc/util.h>
+#include <wallet/scan.h>
#include <wallet/wallet.h>
#include <cstdint>
@@ -421,10 +422,10 @@ RPCMethod importdescriptors()
// Rescan the blockchain using the lowest timestamp
if (rescan) {
- int64_t scanned_time = pwallet->RescanFromTime(lowest_timestamp, reserver);
+ int64_t scanned_time = pwallet->Scanner().ScanFromTime(lowest_timestamp, reserver);
pwallet->ResubmitWalletTransactions(node::TxBroadcast::MEMPOOL_NO_BROADCAST, /*force=*/true);
- if (pwallet->IsAbortingRescan()) {
+ if (pwallet->Scanner().IsAborting()) {
throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted by user.");
}
### src/wallet/rpc/encrypt.cpp
@@ -6,6 +6,7 @@
#include <scheduler.h>
#include <wallet/context.h>
#include <wallet/rpc/util.h>
+#include <wallet/scan.h>
#include <wallet/wallet.h>
@@ -138,7 +139,7 @@ RPCMethod walletpassphrasechange()
throw JSONRPCError(RPC_WALLET_WRONG_ENC_STATE, "Error: running with an unencrypted wallet, but walletpassphrasechange was called.");
}
- if (pwallet->IsScanningWithPassphrase()) {
+ if (pwallet->Scanner().IsScanningWithPassphrase()) {
throw JSONRPCError(RPC_WALLET_ERROR, "Error: the wallet is currently being used to rescan the blockchain for related transactions. Please call `abortrescan` before changing the passphrase.");
}
@@ -203,7 +204,7 @@ RPCMethod walletlock()
throw JSONRPCError(RPC_WALLET_WRONG_ENC_STATE, "Error: running with an unencrypted wallet, but walletlock was called.");
}
- if (pwallet->IsScanningWithPassphrase()) {
+ if (pwallet->Scanner().IsScanningWithPassphrase()) {
throw JSONRPCError(RPC_WALLET_ERROR, "Error: the wallet is currently being used to rescan the blockchain for related transactions. Please call `abortrescan` before locking the wallet.");
}
@@ -260,7 +261,7 @@ RPCMethod encryptwallet()
throw JSONRPCError(RPC_WALLET_WRONG_ENC_STATE, "Error: running with an encrypted wallet, but encryptwallet was called.");
}
- if (pwallet->IsScanningWithPassphrase()) {
+ if (pwallet->Scanner().IsScanningWithPassphrase()) {
throw JSONRPCError(RPC_WALLET_ERROR, "Error: the wallet is currently being used to rescan the blockchain for related transactions. Please call `abortrescan` before encrypting the wallet.");
}
### src/wallet/rpc/transactions.cpp
@@ -12,6 +12,7 @@
#include <util/vector.h>
#include <wallet/receive.h>
#include <wallet/rpc/util.h>
+#include <wallet/scan.h>
#include <wallet/wallet.h>
using interfaces::FoundBlock;
@@ -925,14 +926,14 @@ RPCMethod rescanblockchain()
CHECK_NONFATAL(pwallet->chain().findAncestorByHeight(pwallet->GetLastBlockHash(), start_height, FoundBlock().hash(start_block)));
}
- CWallet::ScanResult result =
- pwallet->ScanForWalletTransactions(start_block, start_height, stop_height, reserver, /*save_progress=*/false);
+ ScanResult result =
+ pwallet->Scanner().Scan(start_block, start_height, stop_height, reserver, /*save_progress=*/false);
switch (result.status) {
- case CWallet::ScanResult::SUCCESS:
+ case ScanResult::SUCCESS:
break;
- case CWallet::ScanResult::FAILURE:
+ case ScanResult::FAILURE:
throw JSONRPCError(RPC_MISC_ERROR, "Rescan failed. Potentially corrupted data files.");
- case CWallet::ScanResult::USER_ABORT:
+ case ScanResult::USER_ABORT:
throw JSONRPCError(RPC_MISC_ERROR, "Rescan aborted.");
} // no default case, so the compiler can warn about missing cases
UniValue response(UniValue::VOBJ);
@@ -963,8 +964,8 @@ RPCMethod abortrescan()
std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(request);
if (!pwallet) return UniValue::VNULL;
- if (!pwallet->IsScanning() || pwallet->IsAbortingRescan()) return false;
- pwallet->AbortRescan();
+ if (!pwallet->Scanner().IsScanning() || pwallet->Scanner().IsAborting()) return false;
+ pwallet->Scanner().Abort();
return true;
},
};
### src/wallet/rpc/wallet.cpp
@@ -20,6 +20,7 @@
#include <wallet/export.h>
#include <wallet/receive.h>
#include <wallet/rpc/util.h>
+#include <wallet/scan.h>
#include <wallet/wallet.h>
#include <wallet/walletutil.h>
@@ -106,10 +107,10 @@ static RPCMethod getwalletinfo()
}
obj.pushKV("private_keys_enabled", !pwallet->IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS));
obj.pushKV("avoid_reuse", pwallet->IsWalletFlagSet(WALLET_FLAG_AVOID_REUSE));
- if (pwallet->IsScanning()) {
+ if (pwallet->Scanner().IsScanning()) {
UniValue scanning(UniValue::VOBJ);
- scanning.pushKV("duration", Ticks<std::chrono::seconds>(pwallet->ScanningDuration()));
- scanning.pushKV("progress", pwallet->ScanningProgress());
+ scanning.pushKV("duration", Ticks<std::chrono::seconds>(pwallet->Scanner().ScanningDuration()));
+ scanning.pushKV("progress", pwallet->Scanner().ScanningProgress());
obj.pushKV("scanning", std::move(scanning));
} else {
obj.pushKV("scanning", false);
### src/wallet/scan.cpp
@@ -0,0 +1,323 @@
+// Copyright (c) 2026-present The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#include <chain.h>
+#include <interfaces/chain.h>
+#include <logging.h>
+#include <primitives/block.h>
+#include <sync.h>
+#include <util/check.h>
+#include <wallet/scan.h>
+#include <wallet/wallet.h>
+
+using interfaces::FoundBlock;
+
+namespace wallet {
+
+int64_t ChainScanner::ScanFromTime(int64_t startTime, const WalletRescanReserver& reserver)
+{
+ // Find starting block. May be null if nCreateTime is greater than the
+ // highest blockchain timestamp, in which case there is nothing that needs
+ // to be scanned.
+ int start_height = 0;
+ uint256 start_block;
+ bool start = m_wallet.chain().findFirstBlockWithTimeAndHeight(startTime - TIMESTAMP_WINDOW, 0, FoundBlock().hash(start_block).height(start_height));
+ 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);
+
+ if (start) {
+ // TODO: this should take into account failure by ScanResult::USER_ABORT
+ ScanResult result = Scan(start_block, start_height, /*max_height=*/{}, reserver, /*save_progress=*/false);
+ if (result.status == ScanResult::FAILURE) {
+ int64_t time_max;
+ CHECK_NONFATAL(m_wallet.chain().findBlock(result.last_failed_block, FoundBlock().maxTime(time_max)));
+ return time_max + TIMESTAMP_WINDOW + 1;
+ }
+ }
+ return startTime;
+}
+
+bool WalletRescanReserver::reserve(bool with_passphrase) {
+ assert(!m_could_reserve);
+ if (!m_wallet.Scanner().TryReserve(with_passphrase)) {
+ return false;
+ }
+ m_could_reserve = true;
+ return true;
+}
+
+bool WalletRescanReserver::isReserved() const {
+ return (m_could_reserve && m_wallet.Scanner().IsScanning());
+}
+
+WalletRescanReserver::~WalletRescanReserver() {
+ if (m_could_reserve) {
+ m_wallet.Scanner().Release();
+ }
+}
+
+bool ChainScanner::TryReserve(bool with_passphrase) {
+ if (m_scanning.exchange(true)) return false;
+ // Discard any abort request left over from previous reservation, so
+ // that an abort requested while the reservation is held always applies
+ // to abort this rescan, even if it arrives before the scan loop starts.
+ m_abort = false;
+ m_scanning_with_passphrase = with_passphrase;
+ m_scanning_start = SteadyClock::now();
+ m_scanning_progress = 0;
+ return true;
+}
+
+void ChainScanner::Release() {
+ m_scanning = false;
+ m_scanning_with_passphrase = false;
+}
+
+namespace {
+class FastWalletRescanFilter
+{
+public:
+ FastWalletRescanFilter(const CWallet& wallet) : m_wallet(wallet)
+ {
+ // create initial filter with scripts from all ScriptPubKeyMans
+ for (auto spkm : m_wallet.GetAllScriptPubKeyMans()) {
+ auto desc_spkm{dynamic_cast<DescriptorScriptPubKeyMan*>(spkm)};
+ assert(desc_spkm != nullptr);
+ AddScriptPubKeys(desc_spkm);
+ // save each range descriptor's end for possible future filter updates
+ if (desc_spkm->IsHDEnabled()) {
+ m_last_range_ends.emplace(desc_spkm->GetID(), desc_spkm->GetEndRange());
+ }
+ }
+ }
+
+ void UpdateIfNeeded()
+ {
+ // repopulate filter with new scripts if top-up has happened since last iteration
+ for (const auto& [desc_spkm_id, last_range_end] : m_last_range_ends) {
+ auto desc_spkm{dynamic_cast<DescriptorScriptPubKeyMan*>(m_wallet.GetScriptPubKeyMan(desc_spkm_id))};
+ assert(desc_spkm != nullptr);
+ int32_t current_range_end{desc_spkm->GetEndRange()};
+ if (current_range_end > last_range_end) {
+ AddScriptPubKeys(desc_spkm, last_range_end);
+ m_last_range_ends.at(desc_spkm->GetID()) = current_range_end;
+ }
+ }
+ }
+
+ std::optional<bool> MatchesBlock(const uint256& block_hash) const
+ {
+ return m_wallet.chain().blockFilterMatchesAny(BlockFilterType::BASIC, block_hash, m_filter_set);
+ }
+
+private:
+ const CWallet& m_wallet;
+ /** Map for keeping track of each range descriptor's last seen end range.
+ * This information is used to detect whether new addresses were derived
+ * (that is, if the current end range is larger than the saved end range)
+ * after processing a block and hence a filter set update is needed to
+ * take possible keypool top-ups into account.
+ */
+ std::map<uint256, int32_t> m_last_range_ends;
+ GCSFilter::ElementSet m_filter_set;
+
+ void AddScriptPubKeys(const DescriptorScriptPubKeyMan* desc_spkm, int32_t last_range_end = 0)
+ {
+ for (const auto& script_pub_key : desc_spkm->GetScriptPubKeys(last_range_end)) {
+ m_filter_set.emplace(script_pub_key.begin(), script_pub_key.end());
+ }
+ }
+};
+
+static bool ShouldFetchBlock(const FastWalletRescanFilter& filter, const uint256& block_hash, int block_height) {
+ auto matches_block{filter.MatchesBlock(block_hash)};
+ if (matches_block.has_value()) {
+ if (*matches_block) {
+ LogDebug(BCLog::SCAN, "Fast rescan: inspect block %d [%s] (filter matched)\n", block_height, block_hash.ToString());
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ LogDebug(BCLog::SCAN, "Fast rescan: inspect block %d [%s] (WARNING: block filter not found!)\n", block_height, block_hash.ToString());
+ return true;
+ }
+}
+} // namespace
+
+bool ChainScanner::QueueNextBlock(const uint256& block_hash, int block_height, std::optional<std::pair<uint256, int>>& next_block, std::optional<int> max_height) {
+ bool block_still_active = false;
+ bool has_next_block = false;
+ uint256 next_block_hash;
+ m_wallet.chain().findBlock(block_hash, FoundBlock().inActiveChain(block_still_active).nextBlock(FoundBlock().inActiveChain(has_next_block).hash(next_block_hash)));
+
+ // Queue the next block if it exists and is within range. Whether the scan
+ // has caught up with the wallet's tip is checked after the current block
+ // is processed, so blocks connected while it was being processed are not
+ // missed.
+ if (has_next_block && (!max_height || block_height < *max_height)) {
+ next_block = {{next_block_hash, block_height + 1}};
+ }
+
+ return block_still_active;
+}
+
+void ChainScanner::UpdateProgress(const LoopState& state, double progress_current, int block_height) {
+ m_scanning_progress = 0;
+ double progress_diff = state.progress_end - state.progress_begin;
+
+ // avoid divide-by-zero for single block scan range (i.e. start and stop hashes are equal)
+ if (progress_diff <= 0.0) return;
+ m_scanning_progress = (progress_current - state.progress_begin) / progress_diff;
+
+ if (block_height % 100 == 0) {
+ m_wallet.ShowProgress(strprintf("[%s] %s", m_wallet.DisplayName(), _("Rescanning…")),
+ std::max(1, std::min(99, (int)(m_scanning_progress.load() * 100))));
+ }
+}
+
+void ChainScanner::UpdateTipIfChanged(LoopState& state) {
+ const uint256 new_tip = WITH_LOCK(m_wallet.cs_wallet, return m_wallet.GetLastBlockHash());
+ if (new_tip != state.tip_hash) {
+ state.tip_hash = new_tip;
+ state.progress_end = m_wallet.chain().guessVerificationProgress(state.tip_hash);
+ }
+}
+
+bool ChainScanner::ScanBlock(const uint256& block_hash, int block_height, bool save_progress) {
+ // Read block data and locator if needed (the locator is usually null unless we need to save progress)
+ CBlock block;
+ CBlockLocator loc;
+ // Find block
+ FoundBlock found_block{FoundBlock().data(block)};
+ if (save_progress) found_block.locator(loc);
+ m_wallet.chain().findBlock(block_hash, found_block);
+
+ if (block.IsNull()) return false;
+
+ {
+ // cs_wallet is a RecursiveMutex; ScanBlock may be called
+ // with cs_wallet already held as in AttachChain or without it.
+ LOCK(m_wallet.cs_wallet);
+ for (size_t posInBlock = 0; posInBlock < block.vtx.size(); ++posInBlock) {
+ m_wallet.SyncTransaction(
+ block.vtx[posInBlock], TxStateConfirmed{block_hash, block_height,
+ static_cast<int>(posInBlock)},
+ /*rescanning_old_block=*/true);
+ }
+
+ if (!loc.IsNull()) {
+ m_wallet.WalletLogPrintf("Saving scan progress %d.\n", block_height);
+ WalletBatch batch(m_wallet.GetDatabase());
+ batch.WriteBestBlock(loc);
+ }
+ }
+ return true;
+}
+
+ScanResult ChainScanner::Scan(const uint256& start_block, int start_height, std::optional<int> max_height,
+ const WalletRescanReserver& reserver, bool save_progress) {
+ constexpr auto INTERVAL_TIME{60s};
+ auto current_time{reserver.now()};
+ auto start_time{reserver.now()};
+
+ assert(reserver.isReserved());
+ auto& chain = m_wallet.chain();
+
+ std::unique_ptr<FastWalletRescanFilter> fast_rescan_filter;
+ if (chain.hasBlockFilterIndex(BlockFilterType::BASIC)) fast_rescan_filter = std::make_unique<FastWalletRescanFilter>(m_wallet);
+
+ m_wallet.WalletLogPrintf("Rescan started from block %s... (%s)\n", start_block.ToString(),
+ fast_rescan_filter ? "fast variant using block filters" : "slow variant inspecting all blocks");
+
+ // show rescan progress in GUI as dialog or on splashscreen, if rescan required on startup (e.g. due to corruption)
+ m_wallet.ShowProgress(strprintf("[%s] %s", m_wallet.DisplayName(), _("Rescanning…")), 0);
+
+ ScanResult result;
+ LoopState state;
+ state.tip_hash = WITH_LOCK(m_wallet.cs_wallet, return m_wallet.GetLastBlockHash());
+ uint256 end_hash = state.tip_hash;
+ if (max_height) chain.findAncestorByHeight(state.tip_hash, *max_height, FoundBlock().hash(end_hash));
+ state.progress_begin = chain.guessVerificationProgress(start_block);
+ state.progress_end = chain.guessVerificationProgress(end_hash);
+ double progress_current = state.progress_begin;
+ std::optional<std::pair<uint256, int>> next_block = {{start_block, start_height}};
+ int block_height = start_height;
+ while (!m_abort && !chain.shutdownRequested()) {
+ if (!next_block) break;
+
+ const uint256 block_hash = next_block->first;
+ block_height = next_block->second;
+ next_block.reset();
+ // Look up the current block's position separately from reading its
+ // data below, because reading is slow and there might be a reorg
+ // while it is read.
+ const bool block_still_active = QueueNextBlock(block_hash, block_height, next_block, max_height);
+
+ progress_current = chain.guessVerificationProgress(block_hash);
+ UpdateProgress(state, progress_current, block_height);
+
+ bool next_interval = reserver.now() >= current_time + INTERVAL_TIME;
+ if (next_interval) {
+ current_time = reserver.now();
+ m_wallet.WalletLogPrintf("Still rescanning. At block %d. Progress=%f\n", block_height, progress_current);
+ }
+
+ bool fetch_block{true};
+ if (fast_rescan_filter) {
+ fast_rescan_filter->UpdateIfNeeded();
+ fetch_block = ShouldFetchBlock(*fast_rescan_filter, block_hash, block_height);
+ }
+
+ if (fetch_block && !block_still_active) {
+ // Abort scan if a block that needs to be inspected is no longer
+ // active, to prevent marking transactions as coming from the
+ // wrong block. A block skipped by the filter can stay skipped:
+ // it has no successor in the active chain, so the scan ends
+ // successfully at the reorg point and the replacement blocks are
+ // handled by blockConnected notifications.
+ result.last_failed_block = block_hash;
+ result.status = ScanResult::FAILURE;
+ break;
+ }
+ if (!fetch_block || ScanBlock(block_hash, block_height, save_progress && next_interval)) {
+ // scanned the block, or skipped it via the filter: record it as
+ // the most recent successfully scanned block
+ result.last_scanned_block = block_hash;
+ result.last_scanned_height = block_height;
+ } else {
+ // could not scan block, keep scanning but record this block as the most recent failure
+ result.last_failed_block = block_hash;
+ result.status = ScanResult::FAILURE;
+ }
+
+ // Stop scanning once the wallet's tip is reached, re-reading the height
+ // after the block was processed so a tip extension that happened
+ // meanwhile is picked up. If scanning with cs_wallet locked (AttachChain),
+ // blocks connected during rescan are handled after scanning is complete
+ // via blockConnected notifications. Without the lock, newly added blocks
+ // are re-processed here if the notifications were handled and the last
+ // block height was updated.
+ if (block_height >= WITH_LOCK(m_wallet.cs_wallet, return m_wallet.GetLastBlockHeight())) {
+ break;
+ }
+
+ if (!max_height) UpdateTipIfChanged(state);
+ }
+ if (!max_height) {
+ m_wallet.WalletLogPrintf("Scanning current mempool transactions.\n");
+ WITH_LOCK(m_wallet.cs_wallet, chain.requestMempoolTransactions(m_wallet));
+ }
+ m_wallet.ShowProgress(strprintf("[%s] %s", m_wallet.DisplayName(), _("Rescanning…")), 100); // hide progress dialog in GUI
+ if (m_abort) {
+ m_wallet.WalletLogPrintf("Rescan aborted at block %d. Progress=%f\n", block_height, progress_current);
+ result.status = ScanResult::USER_ABORT;
+ } else if (chain.shutdownRequested()) {
+ m_wallet.WalletLogPrintf("Rescan interrupted by shutdown request at block %d. Progress=%f\n", block_height, progress_current);
+ result.status = ScanResult::USER_ABORT;
+ } else {
+ m_wallet.WalletLogPrintf("Rescan completed in %15dms\n", Ticks<std::chrono::milliseconds>(reserver.now() - start_time));
+ }
+ return result;
+}
+}
### src/wallet/scan.h
@@ -0,0 +1,136 @@
+// Copyright (c) 2026-present The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#ifndef BITCOIN_WALLET_SCAN_H
+#define BITCOIN_WALLET_SCAN_H
+
+#include <uint256.h>
+#include <util/time.h>
+
+#include <atomic>
+#include <functional>
+#include <optional>
+
+namespace wallet {
+class CWallet;
+
+/** Result of a wallet scan */
+struct ScanResult {
+ enum { SUCCESS, FAILURE, USER_ABORT } status = SUCCESS;
+
+ //! Hash and height of most recent block that was successfully scanned.
+ //! Unset if no blocks were scanned due to read errors or the chain
+ //! being empty.
+ uint256 last_scanned_block;
+ std::optional<int> last_scanned_height;
+
+ //! Height of the most recent block that could not be scanned due to
+ //! read errors or pruning. Will be set if status is FAILURE, unset if
+ //! status is SUCCESS, and may or may not be set if status is
+ //! USER_ABORT.
+ uint256 last_failed_block;
+};
+
+/** RAII object to check and reserve a wallet rescan */
+class WalletRescanReserver
+{
+private:
+ using Clock = std::chrono::steady_clock;
+ using NowFn = std::function<Clock::time_point()>;
+ CWallet& m_wallet;
+ bool m_could_reserve{false};
+ NowFn m_now;
+public:
+ explicit WalletRescanReserver(CWallet& w) : m_wallet(w) {}
+
+ bool reserve(bool with_passphrase = false);
+ bool isReserved() const;
+
+ Clock::time_point now() const { return m_now ? m_now() : Clock::now(); };
+
+ void setNow(NowFn now) { m_now = std::move(now); }
+
+ ~WalletRescanReserver();
+};
+
+class ChainScanner {
+private:
+ CWallet& m_wallet;
+
+ std::atomic<bool> m_abort{false};
+ std::atomic<bool> m_scanning{false};
+ std::atomic<bool> m_scanning_with_passphrase{false};
+ std::atomic<SteadyClock::time_point> m_scanning_start{SteadyClock::time_point{}};
+ std::atomic<double> m_scanning_progress{0};
+
+ //! Progress window and tip tracked across Scan loop iterations. The
+ //! current block's progress is a plain local in Scan; only the window
+ //! bounds are shared with the helpers, and UpdateTipIfChanged is the
+ //! sole mutator.
+ struct LoopState {
+ double progress_begin{0};
+ double progress_end{0};
+ uint256 tip_hash;
+ };
+
+ //! Locate block_hash in the chain, queueing its active-chain successor
+ //! into next_block if it exists and is within the scan range. Returns
+ //! whether the block itself is still in the active chain.
+ bool QueueNextBlock(const uint256& block_hash, int block_height, std::optional<std::pair<uint256, int>>& next_block, std::optional<int> max_height);
+ bool ScanBlock(const uint256& block_hash, int block_height, bool save_progress);
+ void UpdateProgress(const LoopState& state, double progress_current, int block_height);
+ void UpdateTipIfChanged(LoopState& state);
+
+ //! Only WalletRescanReserver may reserve and release scans, so that
+ //! reservations are always managed RAII-style.
+ friend class WalletRescanReserver;
+ bool TryReserve(bool with_passphrase = false);
+ void Release();
+
+public:
+ explicit ChainScanner(CWallet& wallet) : m_wallet(wallet) {}
+
+ void Abort() { m_abort = true; }
+ bool IsAborting() const { return m_abort; }
+ bool IsScanning() const { return m_scanning; }
+ bool IsScanningWithPassphrase() const { return m_scanning_with_passphrase; }
+ SteadyClock::duration ScanningDuration() const { return m_scanning ? SteadyClock::now() - m_scanning_start.load() : SteadyClock::duration{}; }
+ double ScanningProgress() const { return m_scanning ? m_scanning_progress.load() : 0; }
+
+ /** Scan active chain for relevant transactions after importing keys. Should
+ * be called whenever new keys are added to the wallet, with the oldest key
+ * creation time.
+ * @return Earliest timestamp that could be successfully scanned from. Timestamp
+ * returned will be higher than startTime if relevant blocks could not be read. */
+ int64_t ScanFromTime(int64_t startTime, const WalletRescanReserver& reserver);
+
+ /**
+ * Scan the block chain (starting in start_block) for transactions
+ * from or to us. If max_height is not set, the
+ * mempool will be scanned as well.
+ *
+ * @param[in] start_block Scan starting block. If block is not on the active
+ * chain, the scan will return SUCCESS immediately.
+ * @param[in] start_height Height of start_block
+ * @param[in] max_height Optional max scanning height. If unset there is
+ * no maximum and scanning can continue to the tip
+ *
+ * @return ScanResult returning scan information and indicating success or
+ * failure. Return status will be set to SUCCESS if scan was
+ * successful. FAILURE if a complete rescan was not possible (due to
+ * pruning or corruption). USER_ABORT if the rescan was aborted before
+ * it could complete.
+ *
+ * @pre Caller needs to make sure start_block (and the optional stop_block) are on
+ * the main chain after the addition of any new keys you want to detect
+ * transactions for.
+ */
+ ScanResult Scan(const uint256& start_block, int start_height, std::optional<int> max_height,
+ const WalletRescanReserver& reserver, bool save_progress);
+
+};
+
+} // namespace wallet
+
+#endif // BITCOIN_WALLET_SCAN_H
### src/wallet/test/util.cpp
@@ -10,6 +10,7 @@
#include <test/util/setup_common.h>
#include <validationinterface.h>
#include <wallet/context.h>
+#include <wallet/scan.h>
#include <wallet/wallet.h>
#include <wallet/walletdb.h>
@@ -40,8 +41,8 @@ std::unique_ptr<CWallet> CreateSyncedWallet(interfaces::Chain& chain, CChain& cc
}
WalletRescanReserver reserver(*wallet);
reserver.reserve();
- CWallet::ScanResult result = wallet->ScanForWalletTransactions(cchain.Genesis()->GetBlockHash(), /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false);
- assert(result.status == CWallet::ScanResult::SUCCESS);
+ ScanResult result = wallet->Scanner().Scan(cchain.Genesis()->GetBlockHash(), /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false);
+ assert(result.status == ScanResult::SUCCESS);
assert(result.last_scanned_block == cchain.Tip()->GetBlockHash());
assert(*result.last_scanned_height == cchain.Height());
assert(result.last_failed_block.IsNull());
### src/wallet/test/wallet_tests.cpp
@@ -2,6 +2,7 @@
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+#include <wallet/scan.h>
#include <wallet/wallet.h>
#include <cstdint>
@@ -10,8 +11,13 @@
#include <vector>
#include <addresstype.h>
+#include <blockfilter.h>
+#include <chain.h>
+#include <consensus/validation.h>
+#include <index/blockfilterindex.h>
#include <interfaces/chain.h>
#include <key_io.h>
+#include <logging.h>
#include <node/blockstorage.h>
#include <node/types.h>
#include <policy/policy.h>
@@ -21,6 +27,7 @@
#include <test/util/logging.h>
#include <test/util/random.h>
#include <test/util/setup_common.h>
+#include <util/byte_units.h>
#include <util/translation.h>
#include <validation.h>
#include <validationinterface.h>
@@ -98,7 +105,7 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup)
CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
CBlockIndex* newTip = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip());
- // Verify ScanForWalletTransactions fails to read an unknown start block.
+ // Verify Scan fails to read an unknown start block.
{
CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
{
@@ -110,15 +117,15 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup)
AddKey(wallet, coinbaseKey);
WalletRescanReserver reserver(wallet);
reserver.reserve();
- CWallet::ScanResult result = wallet.ScanForWalletTransactions(/*start_block=*/{}, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false);
- BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE);
+ ScanResult result = wallet.Scanner().Scan(/*start_block=*/{}, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false);
+ BOOST_CHECK_EQUAL(result.status, ScanResult::FAILURE);
BOOST_CHECK(result.last_failed_block.IsNull());
BOOST_CHECK(result.last_scanned_block.IsNull());
BOOST_CHECK(!result.last_scanned_height);
BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 0);
}
- // Verify ScanForWalletTransactions picks up transactions in both the old
+ // Verify Scan picks up transactions in both the old
// and new block files.
{
CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
@@ -141,8 +148,8 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup)
BOOST_CHECK(locator.vHave.front() == newTip->GetBlockHash());
}
- CWallet::ScanResult result = wallet.ScanForWalletTransactions(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*save_progress=*/true);
- BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS);
+ ScanResult result = wallet.Scanner().Scan(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*save_progress=*/true);
+ BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS);
BOOST_CHECK(result.last_failed_block.IsNull());
BOOST_CHECK_EQUAL(result.last_scanned_block, newTip->GetBlockHash());
BOOST_CHECK_EQUAL(*result.last_scanned_height, newTip->nHeight);
@@ -165,7 +172,7 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup)
}
m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number});
- // Verify ScanForWalletTransactions only picks transactions in the new block
+ // Verify Scan only picks transactions in the new block
// file.
{
CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
@@ -178,8 +185,8 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup)
AddKey(wallet, coinbaseKey);
WalletRescanReserver reserver(wallet);
reserver.reserve();
- CWallet::ScanResult result = wallet.ScanForWalletTransactions(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*save_progress=*/false);
- BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE);
+ ScanResult result = wallet.Scanner().Scan(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*save_progress=*/false);
+ BOOST_CHECK_EQUAL(result.status, ScanResult::FAILURE);
BOOST_CHECK_EQUAL(result.last_failed_block, oldTip->GetBlockHash());
BOOST_CHECK_EQUAL(result.last_scanned_block, newTip->GetBlockHash());
BOOST_CHECK_EQUAL(*result.last_scanned_height, newTip->nHeight);
@@ -194,7 +201,7 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup)
}
m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number});
- // Verify ScanForWalletTransactions scans no blocks.
+ // Verify Scan scans no blocks.
{
CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
{
@@ -206,15 +213,115 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup)
AddKey(wallet, coinbaseKey);
WalletRescanReserver reserver(wallet);
reserver.reserve();
- CWallet::ScanResult result = wallet.ScanForWalletTransactions(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*save_progress=*/false);
- BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::FAILURE);
+ ScanResult result = wallet.Scanner().Scan(/*start_block=*/oldTip->GetBlockHash(), /*start_height=*/oldTip->nHeight, /*max_height=*/{}, reserver, /*save_progress=*/false);
+ BOOST_CHECK_EQUAL(result.status, ScanResult::FAILURE);
BOOST_CHECK_EQUAL(result.last_failed_block, newTip->GetBlockHash());
BOOST_CHECK(result.last_scanned_block.IsNull());
BOOST_CHECK(!result.last_scanned_height);
BOOST_CHECK_EQUAL(GetBalance(wallet).m_mine_immature, 0);
}
}
+BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_reorged_block, TestChain100Setup)
+{
+ BOOST_REQUIRE(InitBlockFilterIndex([&]{ return interfaces::MakeChain(m_node); }, BlockFilterType::BASIC, 1_MiB, /*f_memory=*/true));
+ BlockFilterIndex& filter_index{*Assert(GetBlockFilterIndex(BlockFilterType::BASIC))};
+ BOOST_REQUIRE(filter_index.Init());
+ filter_index.Sync();
+
+ // Reorg the tip out of the active chain: invalidate it, then mine a
+ // longer replacement branch paying a script unrelated to the wallets
+ // below.
+ CBlockIndex* stale_block = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip());
+ const uint256 stale_hash{stale_block->GetBlockHash()};
+ const int stale_height{stale_block->nHeight};
+ BlockValidationState state;
+ BOOST_REQUIRE(m_node.chainman->ActiveChainstate().InvalidateBlock(state, stale_block));
+ const CScript replacement_script{GetScriptForRawPubKey(GenerateRandomKey().GetPubKey())};
+ CreateAndProcessBlock({}, replacement_script);
+ CreateAndProcessBlock({}, replacement_script);
+ BOOST_REQUIRE(filter_index.BlockUntilSyncedToCurrentChain());
+ {
+ LOCK(Assert(m_node.chainman)->GetMutex());
+ BOOST_REQUIRE(!m_node.chainman->ActiveChain().Contains(*stale_block));
+ BOOST_REQUIRE_EQUAL(m_node.chainman->ActiveChain().Height(), stale_height + 1);
+ }
+
+ {
+ BlockFilter filter;
+ BOOST_REQUIRE(filter_index.LookupFilter(stale_block, filter));
+ }
+
+ // Test wallet whose scripts do not match the stale block's filter.
+ {
+ CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
+ {
+ LOCK(wallet.cs_wallet);
+ LOCK(Assert(m_node.chainman)->GetMutex());
+ wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
+ wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
+ }
+ WalletRescanReserver reserver(wallet);
+ reserver.reserve();
+ ScanResult result = wallet.Scanner().Scan(stale_hash, stale_height, /*max_height=*/{}, reserver, /*save_progress=*/false);
+ BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS);
+ BOOST_CHECK(result.last_failed_block.IsNull());
+ BOOST_CHECK_EQUAL(result.last_scanned_block, stale_hash);
+ BOOST_CHECK_EQUAL(*result.last_scanned_height, stale_height);
+ }
+
+ // Test wallet whose scripts do match the stale block's filter.
+ {
+ CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
+ {
+ LOCK(wallet.cs_wallet);
+ LOCK(Assert(m_node.chainman)->GetMutex());
+ wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
+ wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
+ }
+ AddKey(wallet, coinbaseKey); // the stale block's coinbase pays coinbaseKey
+ WalletRescanReserver reserver(wallet);
+ reserver.reserve();
+ ScanResult result = wallet.Scanner().Scan(stale_hash, stale_height, /*max_height=*/{}, reserver, /*save_progress=*/false);
+ BOOST_CHECK_EQUAL(result.status, ScanResult::FAILURE);
+ BOOST_CHECK_EQUAL(result.last_failed_block, stale_hash);
+ BOOST_CHECK(result.last_scanned_block.IsNull());
+ BOOST_CHECK(!result.last_scanned_height);
+ BOOST_CHECK(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.empty()));
+ }
+
+ // Prune the stale block's file — the block is now not active AND unreadable.
+ int file_number;
+ {
+ LOCK(cs_main);
+ file_number = stale_block->GetBlockPos().nFile;
+ Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(file_number);
+ }
+ m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number});
+
+ {
+ CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
+ {
+ LOCK(wallet.cs_wallet);
+ LOCK(Assert(m_node.chainman)->GetMutex());
+ wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
+ wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
+ }
+ AddKey(wallet, coinbaseKey);
+ WalletRescanReserver reserver(wallet);
+ reserver.reserve();
+ ScanResult result = wallet.Scanner().Scan(stale_hash, stale_height, /*max_height=*/{}, reserver, /*save_progress=*/false);
+ BOOST_CHECK_EQUAL(result.status, ScanResult::FAILURE);
+ BOOST_CHECK_EQUAL(result.last_failed_block, stale_hash);
+ BOOST_CHECK(result.last_scanned_block.IsNull());
+ BOOST_CHECK(!result.last_scanned_height);
+ BOOST_CHECK(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.empty()));
+ }
+
+ filter_index.Stop();
+ BOOST_REQUIRE(DestroyBlockFilterIndex(BlockFilterType::BASIC));
+}
+
BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_abort, TestChain100Setup)
{
CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
@@ -229,22 +336,323 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_abort, TestChain100Setup)
// An abort requested while no rescan is held is stale and must
// not cancel a later scan.
- wallet.AbortRescan();
+ wallet.Scanner().Abort();
WalletRescanReserver reserver(wallet);
BOOST_CHECK(reserver.reserve());
- BOOST_CHECK(!wallet.IsAbortingRescan());
+ BOOST_CHECK(!wallet.Scanner().IsAborting());
// An abort requested after the reservation but before the scan starts
// (e.g. while importdescriptors is still deriving keys) must cancel the
// scan.
- wallet.AbortRescan();
- CWallet::ScanResult result = wallet.ScanForWalletTransactions(genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false);
- BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::USER_ABORT);
+ wallet.Scanner().Abort();
+ ScanResult result = wallet.Scanner().Scan(genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false);
+ BOOST_CHECK_EQUAL(result.status, ScanResult::USER_ABORT);
BOOST_CHECK(result.last_scanned_block.IsNull());
BOOST_CHECK(!result.last_scanned_height);
BOOST_CHECK(result.last_failed_block.IsNull());
}
+BOOST_FIXTURE_TEST_CASE(wallet_rescan_reserver, TestingSetup)
+{
+ CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
+
+ // No scan in progress: accessors report idle state.
+ BOOST_CHECK(!wallet.Scanner().IsScanning());
+ BOOST_CHECK(wallet.Scanner().ScanningDuration() == SteadyClock::duration{});
+ BOOST_CHECK_EQUAL(wallet.Scanner().ScanningProgress(), 0.0);
+
+ {
+ WalletRescanReserver first_reserver(wallet);
+ BOOST_CHECK(first_reserver.reserve());
+ BOOST_CHECK(first_reserver.isReserved());
+ BOOST_CHECK(wallet.Scanner().IsScanning());
+ BOOST_CHECK(!wallet.Scanner().IsScanningWithPassphrase());
+ BOOST_CHECK_EQUAL(wallet.Scanner().ScanningProgress(), 0.0);
+
+ // Only one reservation can be held at a time.
+ WalletRescanReserver second_reserver(wallet);
+ BOOST_CHECK(!second_reserver.reserve());
+ BOOST_CHECK(!second_reserver.isReserved());
+ }
+ // Destroying the reserver (RAII) clears the scanning state.
+ BOOST_CHECK(!wallet.Scanner().IsScanning());
+
+ {
+ WalletRescanReserver passphrase_reserver(wallet);
+ BOOST_CHECK(passphrase_reserver.reserve(/*with_passphrase=*/true));
+ BOOST_CHECK(wallet.Scanner().IsScanningWithPassphrase());
+ }
+ BOOST_CHECK(!wallet.Scanner().IsScanningWithPassphrase());
+}
+
+BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_bounded, TestChain100Setup)
+{
+ uint256 genesis_hash, max_hash, tip_hash;
+ int max_height, tip_height;
+ {
+ LOCK(Assert(m_node.chainman)->GetMutex());
+ genesis_hash = m_node.chainman->ActiveChain().Genesis()->GetBlockHash();
+ tip_height = m_node.chainman->ActiveChain().Height();
+ tip_hash = m_node.chainman->ActiveChain().Tip()->GetBlockHash();
+ max_height = tip_height - 2;
+ max_hash = m_node.chainman->ActiveChain()[max_height]->GetBlockHash();
+ }
+
+ // A scan with max_height set stops exactly at max_height and does not
+ // sync any blocks beyond it.
+ {
+ CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
+ {
+ LOCK(wallet.cs_wallet);
+ wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
+ wallet.SetLastBlockProcessed(tip_height, tip_hash);
+ }
+ AddKey(wallet, coinbaseKey);
+ WalletRescanReserver reserver(wallet);
+ reserver.reserve();
+ ScanResult result = wallet.Scanner().Scan(genesis_hash, /*start_height=*/0, max_height, reserver, /*save_progress=*/false);
+ BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS);
+ BOOST_CHECK(result.last_failed_block.IsNull());
+ BOOST_CHECK_EQUAL(result.last_scanned_block, max_hash);
+ BOOST_CHECK_EQUAL(*result.last_scanned_height, max_height);
+ // One coinbase per block from height 1 through max_height.
+ BOOST_CHECK_EQUAL(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.size()), static_cast<size_t>(max_height));
+ }
+
+ // A single-block range (start == max_height == tip) scans exactly that
+ // block.
+ {
+ CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
+ {
+ LOCK(wallet.cs_wallet);
+ wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
+ wallet.SetLastBlockProcessed(tip_height, tip_hash);
+ }
+ AddKey(wallet, coinbaseKey);
+ WalletRescanReserver reserver(wallet);
+ reserver.reserve();
+ ScanResult result = wallet.Scanner().Scan(tip_hash, tip_height, tip_height, reserver, /*save_progress=*/false);
+ BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS);
+ BOOST_CHECK(result.last_failed_block.IsNull());
+ BOOST_CHECK_EQUAL(result.last_scanned_block, tip_hash);
+ BOOST_CHECK_EQUAL(*result.last_scanned_height, tip_height);
+ BOOST_CHECK_EQUAL(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.size()), 1U);
+ }
+}
+
+BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_tip_extension, TestChain100Setup)
+{
+ CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
+ uint256 genesis_hash;
+ int start_tip_height{0};
+ {
+ LOCK(wallet.cs_wallet);
+ LOCK(Assert(m_node.chainman)->GetMutex());
+ wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
+ start_tip_height = m_node.chainman->ActiveChain().Height();
+ wallet.SetLastBlockProcessed(start_tip_height, m_node.chainman->ActiveChain().Tip()->GetBlockHash());
+ genesis_hash = m_node.chainman->ActiveChain().Genesis()->GetBlockHash();
+ }
+ AddKey(wallet, coinbaseKey);
+
+ // Connect a block while the scan is running (the handler fires on the
+ // scanning thread as the scan starts) and advance the wallet's tip, as
+ // the blockConnected notification would. The scan must pick up the new
+ // tip instead of stopping at the height it started with.
+ uint256 new_tip_hash;
+ bool extended{false};
+ auto handler = wallet.ShowProgress.connect([&](const std::string&, int) {
+ if (extended) return;
+ extended = true;
+ CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
+ LOCK(wallet.cs_wallet);
+ LOCK(Assert(m_node.chainman)->GetMutex());
+ const CBlockIndex* new_tip = m_node.chainman->ActiveChain().Tip();
+ new_tip_hash = new_tip->GetBlockHash();
+ wallet.SetLastBlockProcessed(new_tip->nHeight, new_tip_hash);
+ });
+
+ WalletRescanReserver reserver(wallet);
+ reserver.reserve();
+ ScanResult result = wallet.Scanner().Scan(genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false);
+ handler.disconnect();
+ BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS);
+ BOOST_CHECK_EQUAL(result.last_scanned_block, new_tip_hash);
+ BOOST_CHECK_EQUAL(*result.last_scanned_height, start_tip_height + 1);
+}
+
+BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_no_progress_saved, TestChain100Setup)
+{
+ CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
+ uint256 genesis_hash, tip_hash;
+ int max_height;
+ {
+ LOCK(wallet.cs_wallet);
+ LOCK(Assert(m_node.chainman)->GetMutex());
+ wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
+ tip_hash = m_node.chainman->ActiveChain().Tip()->GetBlockHash();
+ wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), tip_hash);
+ genesis_hash = m_node.chainman->ActiveChain().Genesis()->GetBlockHash();
+ max_height = m_node.chainman->ActiveChain().Height() - 2;
+ }
+ AddKey(wallet, coinbaseKey);
+
+ WalletRescanReserver reserver(wallet);
+ // Advance the clock on every call so that every scanned block would be
+ // eligible for a progress write if save_progress were set.
+ std::chrono::steady_clock::time_point fake_time;
+ reserver.setNow([&] { fake_time += 60s; return fake_time; });
+ reserver.reserve();
+
+ ScanResult result = wallet.Scanner().Scan(genesis_hash, /*start_height=*/0, max_height, reserver, /*save_progress=*/false);
+ BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS);
+
+ // With save_progress=false the scan must not touch the wallet's best
+ // block record: it still points at the tip written when the descriptor
+ // was added, not at any block the scan visited.
+ CBlockLocator locator;
+ BOOST_CHECK(WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator));
+ BOOST_CHECK(!locator.IsNull());
+ BOOST_CHECK_EQUAL(locator.vHave.front(), tip_hash);
+}
+
+BOOST_FIXTURE_TEST_CASE(rescan_from_time, TestChain100Setup)
+{
+ // Cap last block file size, and mine new block in a new block file.
+ CBlockIndex* old_tip = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip());
+ WITH_LOCK(::cs_main, m_node.chainman->m_blockman.GetBlockFileInfo(old_tip->GetBlockPos().nFile)->nSize = MAX_BLOCKFILE_SIZE);
+ CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
+ CBlockIndex* new_tip = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip());
+
+ // Prune the older block file.
+ int file_number;
+ {
+ LOCK(cs_main);
+ file_number = old_tip->GetBlockPos().nFile;
+ Assert(m_node.chainman)->m_blockman.PruneOneBlockFile(file_number);
+ }
+ m_node.chainman->m_blockman.UnlinkPrunedFiles({file_number});
+
+ CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
+ {
+ LOCK(wallet.cs_wallet);
+ LOCK(Assert(m_node.chainman)->GetMutex());
+ wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
+ wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), m_node.chainman->ActiveChain().Tip()->GetBlockHash());
+ }
+ AddKey(wallet, coinbaseKey);
+ WalletRescanReserver reserver(wallet);
+ reserver.reserve();
+
+ // Blocks before the prune point cannot be read: the returned timestamp
+ // is moved past the last unreadable block, telling the caller from when
+ // the rescan is actually complete.
+ const int64_t genesis_time{WITH_LOCK(::cs_main, return m_node.chainman->ActiveChain().Genesis()->GetBlockTime())};
+ BOOST_CHECK_EQUAL(wallet.Scanner().ScanFromTime(genesis_time, reserver),
+ WITH_LOCK(::cs_main, return old_tip->GetBlockTimeMax()) + TIMESTAMP_WINDOW + 1);
+
+ bool scan_logged{false};
+ DebugLogHelper scan_check{"Rescan started from block", [&](const std::string* s) {
+ if (s) scan_logged = true;
+ return false;
+ }};
+ // A timestamp past the tip requires no scanning and is returned unchanged.
+ const int64_t future_time{WITH_LOCK(::cs_main, return new_tip->GetBlockTimeMax()) + TIMESTAMP_WINDOW + 1};
+ BOOST_CHECK(!scan_logged);
+ BOOST_CHECK_EQUAL(wallet.Scanner().ScanFromTime(future_time, reserver), future_time);
+}
+
+BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_missing_filter, TestChain100Setup)
+{
+ // Enable the block filter index but do not sync it: no filters are
+ // available, so the scan must inspect every block rather than treat
+ // the missing filters as misses and skip blocks.
+ BOOST_REQUIRE(InitBlockFilterIndex([&]{ return interfaces::MakeChain(m_node); }, BlockFilterType::BASIC, 1_MiB, /*f_memory=*/true));
+ BlockFilterIndex& filter_index{*Assert(GetBlockFilterIndex(BlockFilterType::BASIC))};
+ BOOST_REQUIRE(filter_index.Init());
+
+ {
+ CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
+ uint256 genesis_hash, tip_hash;
+ int tip_height;
+ {
+ LOCK(wallet.cs_wallet);
+ LOCK(Assert(m_node.chainman)->GetMutex());
+ wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
+ genesis_hash = m_node.chainman->ActiveChain().Genesis()->GetBlockHash();
+ tip_height = m_node.chainman->ActiveChain().Height();
+ auto tip{m_node.chainman->ActiveChain().Tip()};
+ tip_hash = tip->GetBlockHash();
+ wallet.SetLastBlockProcessed(tip_height, tip_hash);
+ BlockFilter filter;
+ BOOST_REQUIRE(!filter_index.LookupFilter(tip, filter));
+ }
+ AddKey(wallet, coinbaseKey);
+ WalletRescanReserver reserver(wallet);
+ reserver.reserve();
+ bool fast_scan_logged{false};
+ DebugLogHelper scan_check{"fast variant using block filters", [&](const std::string* s) {
+ if (s) fast_scan_logged = true;
+ return false;
+ }};
+ ScanResult result = wallet.Scanner().Scan(genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false);
+ BOOST_REQUIRE(fast_scan_logged);
+ BOOST_CHECK_EQUAL(result.status, ScanResult::SUCCESS);
+ BOOST_CHECK(result.last_failed_block.IsNull());
+ BOOST_CHECK_EQUAL(result.last_scanned_block, tip_hash);
+ BOOST_CHECK_EQUAL(*result.last_scanned_height, tip_height);
+ // One coinbase per block from height 1 through the tip.
+ BOOST_CHECK_EQUAL(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.size()), static_cast<size_t>(tip_height));
+ }
+
+ filter_index.Stop();
+ BOOST_REQUIRE(DestroyBlockFilterIndex(BlockFilterType::BASIC));
+}
+
+//! Test the rescan that loading a wallet performs when the wallet is behind
+//! the chain tip: it scans from the wallet's recorded best block - a
+//! mid-chain start - with cs_wallet held.
+BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_attach_chain, TestChain100Setup)
+{
+ // Do not wait for sqlite to flush data to disk to improve performance
+ m_args.ForceSetArg("-unsafesqlitesync", "1");
+
+ // Create a wallet owning the coinbases, and unload it at the current tip.
+ WalletContext context;
+ context.args = &m_args;
+ context.chain = m_node.chain.get();
+ auto wallet = TestCreateWallet(context);
+ AddKey(*wallet, coinbaseKey);
+ TestUnloadWallet(std::move(wallet));
+
+ // Extend the chain while the wallet is not loaded.
+ constexpr int NEW_BLOCKS{5};
+ for (int i = 0; i < NEW_BLOCKS; ++i) {
+ CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
+ }
+
+ int tip_height;
+ uint256 tip_hash;
+ {
+ LOCK(Assert(m_node.chainman)->GetMutex());
+ tip_height = m_node.chainman->ActiveChain().Height();
+ tip_hash = m_node.chainman->ActiveChain().Tip()->GetBlockHash();
+ }
+
+ // Loading the wallet must rescan the extension from the recorded best
+ // block and find its coinbases.
+ wallet = TestLoadWallet(context);
+ {
+ LOCK(wallet->cs_wallet);
+ BOOST_CHECK_EQUAL(wallet->GetLastBlockHeight(), tip_height);
+ BOOST_CHECK_EQUAL(wallet->GetLastBlockHash(), tip_hash);
+ // The extension's coinbases plus the one of the recorded best block:
+ // the load rescan starts mid-chain, at that block inclusive.
+ BOOST_CHECK_EQUAL(wallet->mapWallet.size(), static_cast<size_t>(NEW_BLOCKS + 1));
+ }
+ TestUnloadWallet(std::move(wallet));
+}
+
// This test verifies that wallet settings can be added and removed
// concurrently, ensuring no race conditions occur during either process.
BOOST_FIXTURE_TEST_CASE(write_wallet_settings_concurrently, TestingSetup)
### src/wallet/wallet.cpp
@@ -67,6 +67,7 @@
#include <wallet/crypter.h>
#include <wallet/db.h>
#include <wallet/external_signer_scriptpubkeyman.h>
+#include <wallet/scan.h>
#include <wallet/scriptpubkeyman.h>
#include <wallet/transaction.h>
#include <wallet/types.h>
@@ -315,61 +316,6 @@ std::shared_ptr<CWallet> LoadWalletInternal(WalletContext& context, const std::s
return nullptr;
}
}
-
-class FastWalletRescanFilter
-{
-public:
- FastWalletRescanFilter(const CWallet& wallet) : m_wallet(wallet)
- {
- // create initial filter with scripts from all ScriptPubKeyMans
- for (auto spkm : m_wallet.GetAllScriptPubKeyMans()) {
- auto desc_spkm{dynamic_cast<DescriptorScriptPubKeyMan*>(spkm)};
- assert(desc_spkm != nullptr);
- AddScriptPubKeys(desc_spkm);
- // save each range descriptor's end for possible future filter updates
- if (desc_spkm->IsHDEnabled()) {
- m_last_range_ends.emplace(desc_spkm->GetID(), desc_spkm->GetEndRange());
- }
- }
- }
-
- void UpdateIfNeeded()
- {
- // repopulate filter with new scripts if top-up has happened since last iteration
- for (const auto& [desc_spkm_id, last_range_end] : m_last_range_ends) {
- auto desc_spkm{dynamic_cast<DescriptorScriptPubKeyMan*>(m_wallet.GetScriptPubKeyMan(desc_spkm_id))};
- assert(desc_spkm != nullptr);
- int32_t current_range_end{desc_spkm->GetEndRange()};
- if (current_range_end > last_range_end) {
- AddScriptPubKeys(desc_spkm, last_range_end);
- m_last_range_ends.at(desc_spkm->GetID()) = current_range_end;
- }
- }
- }
-
- std::optional<bool> MatchesBlock(const uint256& block_hash) const
- {
- return m_wallet.chain().blockFilterMatchesAny(BlockFilterType::BASIC, block_hash, m_filter_set);
- }
-
-private:
- const CWallet& m_wallet;
- /** Map for keeping track of each range descriptor's last seen end range.
- * This information is used to detect whether new addresses were derived
- * (that is, if the current end range is larger than the saved end range)
- * after processing a block and hence a filter set update is needed to
- * take possible keypool top-ups into account.
- */
- std::map<uint256, int32_t> m_last_range_ends;
- GCSFilter::ElementSet m_filter_set;
-
- void AddScriptPubKeys(const DescriptorScriptPubKeyMan* desc_spkm, int32_t last_range_end = 0)
- {
- for (const auto& script_pub_key : desc_spkm->GetScriptPubKeys(last_range_end)) {
- m_filter_set.emplace(script_pub_key.begin(), script_pub_key.end());
- }
- }
-};
} // namespace
std::shared_ptr<CWallet> LoadWallet(WalletContext& context, const std::string& name, std::optional<bool> load_on_start, const DatabaseOptions& options, DatabaseStatus& status, bilingual_str& error, std::vector<bilingual_str>& warnings)
@@ -536,6 +482,23 @@ std::shared_ptr<CWallet> RestoreWallet(WalletContext& context, const fs::path& b
return wallet;
}
+CWallet::CWallet(interfaces::Chain* chain, const std::string& name, std::unique_ptr<WalletDatabase> database)
+ : m_chain(chain),
+ m_name(name),
+ m_database(std::move(database)),
+ m_scanner(std::make_unique<ChainScanner>(*this))
+{
+}
+
+CWallet::~CWallet()
+{
+ // Should not have slots connected at this point.
+ assert(NotifyUnload.empty());
+}
+
+ChainScanner& CWallet::Scanner() { return *m_scanner; }
+const ChainScanner& CWallet::Scanner() const { return *m_scanner; }
+
/** @defgroup mapWallet
*
* @{
@@ -1847,207 +1810,6 @@ void CWallet::MaybeUpdateBirthTime(int64_t time)
}
}
-/**
- * Scan active chain for relevant transactions after importing keys. This should
- * be called whenever new keys are added to the wallet, with the oldest key
- * creation time.
- *
- * @return Earliest timestamp that could be successfully scanned from. Timestamp
- * returned will be higher than startTime if relevant blocks could not be read.
- */
-int64_t CWallet::RescanFromTime(int64_t startTime, const WalletRescanReserver& reserver)
-{
- // Find starting block. May be null if nCreateTime is greater than the
- // highest blockchain timestamp, in which case there is nothing that needs
- // to be scanned.
- int start_height = 0;
- uint256 start_block;
- bool start = chain().findFirstBlockWithTimeAndHeight(startTime - TIMESTAMP_WINDOW, 0, FoundBlock().hash(start_block).height(start_height));
- WalletLogPrintf("%s: Rescanning last %i blocks\n", __func__, start ? WITH_LOCK(cs_wallet, return GetLastBlockHeight()) - start_height + 1 : 0);
-
- if (start) {
- // TODO: this should take into account failure by ScanResult::USER_ABORT
- ScanResult result = ScanForWalletTransactions(start_block, start_height, /*max_height=*/{}, reserver, /*save_progress=*/false);
- if (result.status == ScanResult::FAILURE) {
- int64_t time_max;
- CHECK_NONFATAL(chain().findBlock(result.last_failed_block, FoundBlock().maxTime(time_max)));
- return time_max + TIMESTAMP_WINDOW + 1;
- }
- }
- return startTime;
-}
-
-/**
- * Scan the block chain (starting in start_block) for transactions
- * from or to us. If max_height is not set, the
- * mempool will be scanned as well.
- *
- * @param[in] start_block Scan starting block. If block is not on the active
- * chain, the scan will return SUCCESS immediately.
- * @param[in] start_height Height of start_block
- * @param[in] max_height Optional max scanning height. If unset there is
- * no maximum and scanning can continue to the tip
- *
- * @return ScanResult returning scan information and indicating success or
- * failure. Return status will be set to SUCCESS if scan was
- * successful. FAILURE if a complete rescan was not possible (due to
- * pruning or corruption). USER_ABORT if the rescan was aborted before
- * it could complete.
- *
- * @pre Caller needs to make sure start_block (and the optional stop_block) are on
- * the main chain after to the addition of any new keys you want to detect
- * transactions for.
- */
-CWallet::ScanResult CWallet::ScanForWalletTransactions(const uint256& start_block, int start_height, std::optional<int> max_height, const WalletRescanReserver& reserver, const bool save_progress)
-{
- constexpr auto INTERVAL_TIME{60s};
- auto current_time{reserver.now()};
- auto start_time{reserver.now()};
-
- assert(reserver.isReserved());
-
- uint256 block_hash = start_block;
- ScanResult result;
-
- std::unique_ptr<FastWalletRescanFilter> fast_rescan_filter;
- if (chain().hasBlockFilterIndex(BlockFilterType::BASIC)) fast_rescan_filter = std::make_unique<FastWalletRescanFilter>(*this);
-
- WalletLogPrintf("Rescan started from block %s... (%s)\n", start_block.ToString(),
- fast_rescan_filter ? "fast variant using block filters" : "slow variant inspecting all blocks");
-
- ShowProgress(strprintf("[%s] %s", DisplayName(), _("Rescanning…")), 0); // show rescan progress in GUI as dialog or on splashscreen, if rescan required on startup (e.g. due to corruption)
- uint256 tip_hash = WITH_LOCK(cs_wallet, return GetLastBlockHash());
- uint256 end_hash = tip_hash;
- if (max_height) chain().findAncestorByHeight(tip_hash, *max_height, FoundBlock().hash(end_hash));
- double progress_begin = chain().guessVerificationProgress(block_hash);
- double progress_end = chain().guessVerificationProgress(end_hash);
- double progress_current = progress_begin;
- int block_height = start_height;
- while (!fAbortRescan && !chain().shutdownRequested()) {
- if (progress_end - progress_begin > 0.0) {
- m_scanning_progress = (progress_current - progress_begin) / (progress_end - progress_begin);
- } else { // avoid divide-by-zero for single block scan range (i.e. start and stop hashes are equal)
- m_scanning_progress = 0;
- }
- if (block_height % 100 == 0 && progress_end - progress_begin > 0.0) {
- ShowProgress(strprintf("[%s] %s", DisplayName(), _("Rescanning…")), std::max(1, std::min(99, (int)(m_scanning_progress * 100))));
- }
-
- bool next_interval = reserver.now() >= current_time + INTERVAL_TIME;
- if (next_interval) {
- current_time = reserver.now();
- WalletLogPrintf("Still rescanning. At block %d. Progress=%f\n", block_height, progress_current);
- }
-
- bool fetch_block{true};
- if (fast_rescan_filter) {
- fast_rescan_filter->UpdateIfNeeded();
- auto matches_block{fast_rescan_filter->MatchesBlock(block_hash)};
- if (matches_block.has_value()) {
- if (*matches_block) {
- LogDebug(BCLog::SCAN, "Fast rescan: inspect block %d [%s] (filter matched)\n", block_height, block_hash.ToString());
- } else {
- result.last_scanned_block = block_hash;
- result.last_scanned_height = block_height;
- fetch_block = false;
- }
- } else {
- LogDebug(BCLog::SCAN, "Fast rescan: inspect block %d [%s] (WARNING: block filter not found!)\n", block_height, block_hash.ToString());
- }
- }
-
- // Find next block separately from reading data above, because reading
- // is slow and there might be a reorg while it is read.
- bool block_still_active = false;
- bool next_block = false;
- uint256 next_block_hash;
- chain().findBlock(block_hash, FoundBlock().inActiveChain(block_still_active).nextBlock(FoundBlock().inActiveChain(next_block).hash(next_block_hash)));
-
- if (fetch_block) {
- // Read block data and locator if needed (the locator is usually null unless we need to save progress)
- CBlock block;
- CBlockLocator loc;
- // Find block
- FoundBlock found_block{FoundBlock().data(block)};
- if (save_progress && next_interval) found_block.locator(loc);
- chain().findBlock(block_hash, found_block);
-
- if (!block.IsNull()) {
- LOCK(cs_wallet);
- if (!block_still_active) {
- // Abort scan if current block is no longer active, to prevent
- // marking transactions as coming from the wrong block.
- result.last_failed_block = block_hash;
- result.status = ScanResult::FAILURE;
- break;
- }
- for (size_t posInBlock = 0; posInBlock < block.vtx.size(); ++posInBlock) {
- SyncTransaction(block.vtx[posInBlock], TxStateConfirmed{block_hash, block_height, static_cast<int>(posInBlock)}, /*rescanning_old_block=*/true);
- }
- // scan succeeded, record block as most recent successfully scanned
- result.last_scanned_block = block_hash;
- result.last_scanned_height = block_height;
-
- if (!loc.IsNull()) {
- WalletLogPrintf("Saving scan progress %d.\n", block_height);
- WalletBatch batch(GetDatabase());
- batch.WriteBestBlock(loc);
- }
- } else {
- // could not scan block, keep scanning but record this block as the most recent failure
- result.last_failed_block = block_hash;
- result.status = ScanResult::FAILURE;
- }
- }
- if (max_height && block_height >= *max_height) {
- break;
- }
- // If rescanning was triggered with cs_wallet permanently locked (AttachChain), additional blocks that were connected during the rescan
- // aren't processed here but will be processed with the pending blockConnected notifications after the lock is released.
- // If rescanning without a permanent cs_wallet lock, additional blocks that were added during the rescan will be re-processed if
- // the notification was processed and the last block height was updated.
- if (block_height >= WITH_LOCK(cs_wallet, return GetLastBlockHeight())) {
- break;
- }
-
- {
- if (!next_block) {
- // break successfully when rescan has reached the tip, or
- // previous block is no longer on the chain due to a reorg
- break;
- }
-
- // increment block and verification progress
- block_hash = next_block_hash;
- ++block_height;
- progress_current = chain().guessVerificationProgress(block_hash);
-
- // handle updated tip hash
- const uint256 prev_tip_hash = tip_hash;
- tip_hash = WITH_LOCK(cs_wallet, return GetLastBlockHash());
- if (!max_height && prev_tip_hash != tip_hash) {
- // in case the tip has changed, update progress max
- progress_end = chain().guessVerificationProgress(tip_hash);
- }
- }
- }
- if (!max_height) {
- WalletLogPrintf("Scanning current mempool transactions.\n");
- WITH_LOCK(cs_wallet, chain().requestMempoolTransactions(*this));
- }
- ShowProgress(strprintf("[%s] %s", DisplayName(), _("Rescanning…")), 100); // hide progress dialog in GUI
- if (fAbortRescan) {
- WalletLogPrintf("Rescan aborted at block %d. Progress=%f\n", block_height, progress_current);
- result.status = ScanResult::USER_ABORT;
- } else if (chain().shutdownRequested()) {
- WalletLogPrintf("Rescan interrupted by shutdown request at block %d. Progress=%f\n", block_height, progress_current);
- result.status = ScanResult::USER_ABORT;
- } else {
- WalletLogPrintf("Rescan completed in %15dms\n", Ticks<std::chrono::milliseconds>(reserver.now() - start_time));
- }
- return result;
-}
-
bool CWallet::SubmitTxMemoryPoolAndRelay(CWalletTx& wtx,
std::string& err_string,
node::TxBroadcast broadcast_method) const
@@ -3335,7 +3097,7 @@ bool CWallet::AttachChain(const std::shared_ptr<CWallet>& walletInstance, interf
error = _("Failed to acquire rescan reserver during wallet initialization");
return false;
}
- ScanResult scan_res = walletInstance->ScanForWalletTransactions(chain.getBlockHash(rescan_height), rescan_height, /*max_height=*/{}, reserver, /*save_progress=*/true);
+ ScanResult scan_res = walletInstance->Scanner().Scan(chain.getBlockHash(rescan_height), rescan_height, /*max_height=*/{}, reserver, /*save_progress=*/true);
if (ScanResult::SUCCESS != scan_res.status) {
error = _("Failed to rescan the wallet during initialization");
return false;
### src/wallet/wallet.h
@@ -85,6 +85,7 @@ using LoadWalletFn = std::function<void(std::unique_ptr<interfaces::Wallet> wall
struct bilingual_str;
namespace wallet {
+class ChainScanner;
struct WalletContext;
//! Explicitly delete the wallet.
@@ -304,7 +305,7 @@ struct CRecipient
bool fSubtractFeeFromAmount;
};
-class WalletRescanReserver; //forward declarations for ScanForWalletTransactions/RescanFromTime
+
/**
* A CWallet maintains a set of transactions and balances, and provides the ability to create new transactions.
*/
@@ -315,12 +316,7 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
bool Unlock(const CKeyingMaterial& vMasterKeyIn);
- std::atomic<bool> fAbortRescan{false};
- std::atomic<bool> fScanningWallet{false}; // controlled by WalletRescanReserver
- std::atomic<bool> m_scanning_with_passphrase{false};
- std::atomic<SteadyClock::time_point> m_scanning_start{SteadyClock::time_point{}};
- std::atomic<double> m_scanning_progress{0};
- friend class WalletRescanReserver;
+ friend class ChainScanner;
/** The next scheduled rebroadcast of wallet transactions. */
NodeClock::time_point m_next_resend{GetDefaultNextResend()};
@@ -403,6 +399,8 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
/** Internal database handle. */
std::unique_ptr<WalletDatabase> m_database;
+ std::unique_ptr<ChainScanner> m_scanner;
+
/**
* The following is used to keep track of how far behind the wallet is
* from the chain sync, and to allow clients to block on us being caught up.
@@ -479,18 +477,8 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
unsigned int nMasterKeyMaxID = 0;
/** Construct wallet with specified name and database implementation. */
- CWallet(interfaces::Chain* chain, const std::string& name, std::unique_ptr<WalletDatabase> database)
- : m_chain(chain),
- m_name(name),
- m_database(std::move(database))
- {
- }
-
- ~CWallet()
- {
- // Should not have slots connected at this point.
- assert(NotifyUnload.empty());
- }
+ CWallet(interfaces::Chain* chain, const std::string& name, std::unique_ptr<WalletDatabase> database);
+ ~CWallet();
bool IsLocked() const override;
bool Lock();
@@ -582,15 +570,8 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
bool UnlockAllCoins() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
void ListLockedCoins(std::vector<COutPoint>& vOutpts) const EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
- /*
- * Rescan abort properties
- */
- void AbortRescan() { fAbortRescan = true; }
- bool IsAbortingRescan() const { return fAbortRescan; }
- bool IsScanning() const { return fScanningWallet; }
- bool IsScanningWithPassphrase() const { return m_scanning_with_passphrase; }
- SteadyClock::duration ScanningDuration() const { return fScanningWallet ? SteadyClock::now() - m_scanning_start.load() : SteadyClock::duration{}; }
- double ScanningProgress() const { return fScanningWallet ? (double) m_scanning_progress : 0; }
+ ChainScanner& Scanner();
+ const ChainScanner& Scanner() const;
//! Upgrade DescriptorCaches
void UpgradeDescriptorCache() EXCLUSIVE_LOCKS_REQUIRED(cs_wallet);
@@ -641,24 +622,6 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
void blockConnected(const kernel::ChainstateRole& role, const interfaces::BlockInfo& block) override;
void blockDisconnected(const interfaces::BlockInfo& block) override;
void updatedBlockTip() override;
- int64_t RescanFromTime(int64_t startTime, const WalletRescanReserver& reserver);
-
- struct ScanResult {
- enum { SUCCESS, FAILURE, USER_ABORT } status = SUCCESS;
-
- //! Hash and height of most recent block that was successfully scanned.
- //! Unset if no blocks were scanned due to read errors or the chain
- //! being empty.
- uint256 last_scanned_block;
- std::optional<int> last_scanned_height;
-
- //! Height of the most recent block that could not be scanned due to
- //! read errors or pruning. Will be set if status is FAILURE, unset if
- //! status is SUCCESS, and may or may not be set if status is
- //! USER_ABORT.
- uint256 last_failed_block;
- };
- ScanResult ScanForWalletTransactions(const uint256& start_block, int start_height, std::optional<int> max_height, const WalletRescanReserver& reserver, bool save_progress);
void transactionRemovedFromMempool(const CTransactionRef& tx, MemPoolRemovalReason reason) override;
/** Set the next time this wallet should resend transactions to 12-36 hours from now, ~1 day on average. */
void SetNextResend() { m_next_resend = GetDefaultNextResend(); }
@@ -1121,52 +1084,6 @@ class CWallet final : public WalletStorage, public interfaces::Chain::Notificati
*/
void MaybeResendWalletTxs(WalletContext& context);
-/** RAII object to check and reserve a wallet rescan */
-class WalletRescanReserver
-{
-private:
- using Clock = std::chrono::steady_clock;
- using NowFn = std::function<Clock::time_point()>;
- CWallet& m_wallet;
- bool m_could_reserve{false};
- NowFn m_now;
-public:
- explicit WalletRescanReserver(CWallet& w) : m_wallet(w) {}
-
- bool reserve(bool with_passphrase = false)
- {
- assert(!m_could_reserve);
- if (m_wallet.fScanningWallet.exchange(true)) {
- return false;
- }
- // Discard any abort request left over from previous reservation, so
- // that an abort requested while the reservation is held always applies
- // to abort this rescan, even if it arrives before the scan loop starts.
- m_wallet.fAbortRescan = false;
- m_wallet.m_scanning_with_passphrase.exchange(with_passphrase);
- m_wallet.m_scanning_start = SteadyClock::now();
- m_wallet.m_scanning_progress = 0;
- m_could_reserve = true;
- return true;
- }
-
- bool isReserved() const
- {
- return (m_could_reserve && m_wallet.fScanningWallet);
- }
-
- Clock::time_point now() const { return m_now ? m_now() : Clock::now(); };
-
- void setNow(NowFn now) { m_now = std::move(now); }
-
- ~WalletRescanReserver()
- {
- if (m_could_reserve) {
- m_wallet.fScanningWallet = false;
- m_wallet.m_scanning_with_passphrase = false;
- }
- }
-};
//! Add wallet name to persistent configuration so it will be loaded on startup.
bool AddWalletSetting(interfaces::Chain& chain, const std::string& wallet_name);
### test/functional/wallet_importdescriptors.py
@@ -236,7 +236,7 @@ def import_after_barrier(wallet, descriptors):
importing = thread.submit(w_import.importdescriptors, descriptor)
- # Keep trying because an abort before ScanForWalletTransactions starts
+ # Keep trying because an abort before wallet transaction scan starts
# is reset when the scan loop begins.
abort_succeeded = False
abort_deadline = time.time() + 30 * self.options.timeout_factor
### test/lint/lint-circular-dependencies.py
@@ -19,6 +19,7 @@
"qt/recentrequeststablemodel -> qt/walletmodel -> qt/recentrequeststablemodel",
"qt/sendcoinsdialog -> qt/walletmodel -> qt/sendcoinsdialog",
"qt/transactiontablemodel -> qt/walletmodel -> qt/transactiontablemodel",
+ "wallet/scan -> wallet/wallet -> wallet/scan",
"wallet/wallet -> wallet/walletdb -> wallet/wallet",
"kernel/coinstats -> validation -> kernel/coinstats",
"versionbits -> versionbits_impl -> versionbits",Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.