wallet: move rescan logic into ChainScanner and wallet/scan #34681

pull Eunovo wants to merge 8 commits into bitcoin:master from Eunovo:refactor-wallet-scan changing 15 files +889 −387
  1. Eunovo commented at 11:36 AM on February 26, 2026: contributor

    Part of #34400

    This PR refactors the wallet rescan logic for improved readability and maintainability, and prepares the rescan logic for the changes in #34400.

    CWallet previously owned all rescan-related concerns: atomic state variables, the WalletRescanReserver RAII type, RescanFromTime, and the entire block-scanning loop (ScanForWalletTransactions) as a single large member function.

    This PR separates those concerns into a dedicated ChainScanner class, introduced in wallet/scan.h and wallet/scan.cpp:

    • Scan state (fAbortRescan, fScanningWallet, progress, start time, passphrase flag) moves into ChainScanner atomics, exposed through Scanner().
    • WalletRescanReserver
    • RescanFromTime becomes ChainScanner::ScanFromTime, keeping all scan entry points in one place.
    • ScanForWalletTransactions is replaced by ChainScanner::Scan and decomposed into focused helpers: ShouldFetchBlock, ScanBlock, ReadNextBlock, UpdateProgress, UpdateTipIfChanged, and ProcessBlock.

    CWallet retains only a ChainScanner member and a Scanner() accessor. Callers that previously reached into CWallet for scan state now go through Scanner().

    One slight behaviour change is introduced in this PR and is documented in the associated commit message. We previously attempted to read the block to scan before checking that it is active; now we check that it is active before the read attempt. It has no significant effect, but the change is documented.

    This PR should be reviewed with the --color-moved=dimmed-zebra git option to aid review.

  2. DrahtBot added the label Wallet on Feb 26, 2026
  3. DrahtBot commented at 11:37 AM on February 26, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/34681.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    ACK polespinasa
    Concept ACK rkrux

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #bitcoin-core/gui/954 (Add dialog to select change output when bumping fee by pablomartin4btc)
    • #35752 (wallet: make encryption state updates atomic by l0rinc)
    • #35716 (wallet: Replace mapWallet and wtxOrdered with a boost::multi_index by achow101)
    • #34909 (wallet, refactor: modularise wallet by extracting out legacy wallet migration by rkrux)
    • #34861 (wallet: Add importdescriptors interface by polespinasa)
    • #34400 (wallet: parallel fast rescan (approx 8x speed up with 8 threads) by Eunovo)
    • #33392 (wallet, rpc: add UTXO set check and incremental rescan to importdescriptors by musaHaruna)
    • #30343 (wallet, logging: Replace WalletLogPrintf() with LogInfo() by ryanofsky)
    • #29278 (Wallet: Add maxfeerate wallet startup option by ismaelsadeeq)
    • #27865 (wallet: Track no-longer-spendable TXOs separately by achow101)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

    LLM Linter (✨ experimental)

    Possible places where named args for integral literals may be used (e.g. func(x, /*named_arg=*/0) in C++, and func(x, named_arg=0) in Python):

    • InitBlockFilterIndex([&]{ return interfaces::MakeChain(m_node); }, BlockFilterType::BASIC, 1_MiB, /*f_memory=*/true) in src/wallet/test/wallet_tests.cpp
    • InitBlockFilterIndex([&]{ return interfaces::MakeChain(m_node); }, BlockFilterType::BASIC, 1_MiB, /*f_memory=*/true) in src/wallet/test/wallet_tests.cpp

    <sup>2026-07-30 13:54:43</sup>

  4. Eunovo force-pushed on Feb 26, 2026
  5. DrahtBot added the label CI failed on Feb 26, 2026
  6. DrahtBot removed the label CI failed on Feb 26, 2026
  7. in src/wallet/rpc/transactions.cpp:902 in a2930eceaa outdated
     895 | @@ -896,14 +896,14 @@ RPCHelpMan rescanblockchain()
     896 |          CHECK_NONFATAL(pwallet->chain().findAncestorByHeight(pwallet->GetLastBlockHash(), start_height, FoundBlock().hash(start_block)));
     897 |      }
     898 |  
     899 | -    CWallet::ScanResult result =
     900 | +    ScanResult result =
     901 |          pwallet->ScanForWalletTransactions(start_block, start_height, stop_height, reserver, /*fUpdate=*/true, /*save_progress=*/false);
     902 |      switch (result.status) {
     903 | -    case CWallet::ScanResult::SUCCESS:
     904 | +    case ScanResult::SUCCESS:
    


    polespinasa commented at 6:31 PM on February 27, 2026:

    in a2930eceaac1356e7bdc0438dee315f5b18377ce

    Before the scan result was in the CWallet namespace CWallet::ScanResult::SUCCESS, but now in some places we have wallet::ScanResult::SUCCESS and in others there's only ScanResult::SUCCESS. Maybe try to keep the wallet:: namespace on all occurrences to keep consistency between files.


    Eunovo commented at 9:25 AM on March 5, 2026:

    Maybe try to keep the wallet:: namespace on all occurrences to keep consistency between files.

    I'm not sure this is a good reason to use wallet:: in code already within the wallet namespace.

  8. in src/wallet/wallet.h:308 in a2930eceaa
     301 | @@ -302,7 +302,11 @@ struct CRecipient
     302 |      bool fSubtractFeeFromAmount;
     303 |  };
     304 |  
     305 | -class WalletRescanReserver; //forward declarations for ScanForWalletTransactions/RescanFromTime
     306 | +//forward declarations for ScanForWalletTransactions/RescanFromTime
     307 | +class WalletRescanReserver;
     308 | +class ChainScanner;
     309 | +struct ScanResult;
    


    polespinasa commented at 6:14 PM on March 2, 2026:

    in a2930eceaac1356e7bdc0438dee315f5b18377ce

    ScanResult and WalletRescanReserver are already declared later in the file. Why can't we move the "real" declaration up here?


    Eunovo commented at 9:56 AM on March 5, 2026:

    WalletRescanReserver needs to be forward declared because it has a member that is a reference to the CWallet instance. On the otherhand, ScanResult does not need to be forward declared. I have corrected this in the latest push. Thank you.


    polespinasa commented at 1:32 PM on March 5, 2026:

    Oh right sorry, I didn't mean WalletRescanReserver. You can remove ChainScanner forward declaration as it is only used as a friend class inside CWallet. Tested and it compiles correctly.


    Eunovo commented at 1:59 PM on March 5, 2026:

    Fixed in the latest push.

  9. in src/wallet/scan.cpp:203 in c89eb55312 outdated
     198 | +    ScanResult result;
     199 | +    double progress_begin = chain.guessVerificationProgress(m_start_block);
     200 | +    double progress_end = chain.guessVerificationProgress(end_hash);
     201 | +    double progress_current = progress_begin;
     202 | +    int block_height = m_start_height;
     203 | +    m_next_block = {{m_start_block, m_start_height}};
    


    polespinasa commented at 6:50 PM on March 2, 2026:

    In c89eb55312d ~m_next_block is unused? I think this line can be deleted~

    Nevermind, I didn't understand the code :)

    Am I right that m_next_block is used to keep track of the next block to be scanned in ReadNextBlock? And here the next block is the current block because the scanning just started and it is the first block to be scanned.


    Eunovo commented at 9:27 AM on March 5, 2026:

    You are right.

  10. in src/wallet/scan.h:35 in c7f8368246 outdated
      31 | @@ -32,6 +32,7 @@ class ChainScanner {
      32 |      bool m_save_progress;
      33 |  
      34 |      bool ShouldFetchBlock(const std::unique_ptr<FastWalletRescanFilter>& filter, const uint256& block_hash, int block_height);
      35 | +    bool ScanBlock(const uint256& block_hash, int block_height, bool save_progress);
    


    polespinasa commented at 7:48 PM on March 2, 2026:

    in c7f8368246bf7c474e65c2df07c2aaea7f468089

    I think save_progress is not a good name here anymore if it is the combination of m_save_progress and next_interval.


    Eunovo commented at 9:39 AM on March 5, 2026:

    I disagree. The parameter name represents exactly what it sounds like; it tells ScanBlock to save scan progress if set to true.

  11. in src/wallet/scan.cpp:205 in c7f8368246
     220 | -                    break;
     221 | -                }
     222 | -                for (size_t posInBlock = 0; posInBlock < block.vtx.size(); ++posInBlock) {
     223 | -                    m_wallet.SyncTransaction(block.vtx[posInBlock], TxStateConfirmed{block_hash, block_height, static_cast<int>(posInBlock)}, m_fUpdate, /*rescanning_old_block=*/true);
     224 | -                }
     225 | +            if (!block_still_active) {
    


    polespinasa commented at 7:58 PM on March 2, 2026:

    in c7f8368246bf7c474e65c2df07c2aaea7f468089

    Taking this out of ScanBlock and if (!block.isNull(){...} opens the door to a block being null and still call SyncTransaction(...).

    This is a behavior change and not only code extraction.

    I am not familiar enough to know with this part of the code to know if this matter or if this is ok. Pointing it out just in case:)


    Eunovo commented at 9:46 AM on March 5, 2026:

    Taking this out of ScanBlock and if (!block.isNull(){...} opens the door to a block being null and still call SyncTransaction(...).

    ScanBlock has an if (block.IsNull()) return false; check before calling SyncTransaction


    Eunovo commented at 9:50 AM on March 5, 2026:

    One thing that does change here is that previously, we fetch the block before checking block_still_active, and I decided to change it so that we don't fetch the block if !block_still_active. I don't see a reason to fetch the block if it's not active, and AFAICT, it doesn't change the result.


    polespinasa commented at 1:12 PM on March 5, 2026:

    ScanBlock has an if (block.IsNull()) return false; check before calling SyncTransaction

    you're right, I have missread the code, though the {...} under the if was regarding the if, didn't see the return 😅


    polespinasa commented at 1:13 PM on March 5, 2026:

    I decided to change it so that we don't fetch the block if !block_still_active

    this makes much sense!

    Thanks you can resolve this

  12. polespinasa commented at 9:25 PM on March 2, 2026: member

    Currently reviewing c89eb55312def8bee3f6d7ba645c58cd1bc1f393

    Will continue, left a few comments

  13. in src/wallet/scan.cpp:195 in c89eb55312


    polespinasa commented at 1:41 PM on March 4, 2026:

    In 2eba8426f711c12f31af244c9964a071452a36b8

    Not a change added by this PR but a nit to clean a bit the code.

    nit: I think end_hash can be removed. There shouldn't be any need to duplicate tip_hash.

    By using both in the code seems like they should have different values but they don't. Using only tip_hash should be fine.


    Eunovo commented at 1:50 PM on March 5, 2026:

    If max_height is set, end_hash will be the hash of the block at that height; otherwise, it will be the current tip_hash.


    polespinasa commented at 10:44 AM on July 27, 2026:

    in 563c2c25073c59008ff37daf4f9681abc4ac54be wallet: introduce ChainScanner as a CWallet member

    This does not match the function name anymore, maybe change it to scan_for_wallet or just scan?


    Eunovo commented at 2:09 PM on July 28, 2026:

    I think scan_for_wallet_transactions is still a better name for this test.

  14. Eunovo force-pushed on Mar 5, 2026
  15. Eunovo force-pushed on Mar 5, 2026
  16. Eunovo force-pushed on Apr 14, 2026
  17. rkrux commented at 2:14 PM on April 22, 2026: contributor

    Initial code review at e98e3a76ebbbccbe29d7c4a5f9a36be8e8969a22

    I feel RescanFromTime method should also be moved out in this PR because it's sort of a wrapper over ScanForWalletTransactions.

    Consider the last two commits from my fork's branch that builds over this branch:

  18. Eunovo commented at 9:29 AM on April 23, 2026: contributor

    I feel RescanFromTime method should also be moved out in this PR because it's sort of a wrapper over ScanForWalletTransaction

    Thanks for the review. I'm not quite convinced that RescanFromTime should be moved out. ScanForWalletTransaction was substantially large, which created the need to split its logic into multiple functions that could be in one class, ChainScanner. RescanFromTime doesn't have the same problem. The ScanForWalletTransaction function itself remains part of CWallet; its implementation was only moved to resolve a circular dependency issue.

    I think it's fine that RescanFromTime remains a member function of CWallet. It could be argued that moving it out as you show in https://github.com/rkrux/bitcoin/commit/f2ece2bcda062e13c694be178c21aeb8bca5506f makes its api less ideal, as it now requires an extra parameter.

  19. in src/wallet/scan.cpp:41 in 16b1ca5efa
      36 | + */
      37 | +ScanResult CWallet::ScanForWalletTransactions(const uint256& start_block, int start_height, std::optional<int> max_height, const WalletRescanReserver& reserver, bool fUpdate, bool save_progress)
      38 | +{
      39 | +    ChainScanner scanner{*this, reserver, start_block, start_height, max_height, fUpdate, save_progress};
      40 | +    return scanner.Scan();
      41 | +}
    


    rkrux commented at 11:50 AM on April 23, 2026:

    In 16b1ca5efa16b8ea0dcc9464bef285401c5203d4 "wallet: move scanning logic to wallet/scan.cpp" and re: #34681 (comment) -

    I don't like this ScanForWalletTransactions implementation at all. I didn't share all my thoughts earlier, following is what I think about the wallet structure now that we are adding wallet/scan.

    The ScanForWalletTransaction function itself remains part of CWallet; its implementation was only moved to resolve a circular dependency issue.

    Fair point but this in itself is a sign that there is no need for ScanForWalletTransactions anymore. Combine that with the fact it's just a wrapper over ChainScanner, the case for its removal becomes stronger.

    However, I do realise it's not easy to remove ScanForWalletTransactions straightaway because of its usage inside RescanFromTime and more importantly AttachChain, which is used a few times within wallet/wallet. The only way I see ScanForWalletTransactions getting removed is by removing its usages gradually:

    • Removing AttachChain from wallet/wallet is a big task and goes outside the scope of this PR.
    • RescanFromTime is fairly straightforward that can be done much quicker and that's why I suggested moving it within wallet/scan in this PR.

    It could be argued that moving it out as you show in https://github.com/rkrux/bitcoin/commit/f2ece2bcda062e13c694be178c21aeb8bca5506f makes its api less ideal, as it now requires an extra parameter.

    The second commit in my fork branch that I shared can be avoided because it addresses a different point of removing it from CWallet but the first commit is a simple move.

    Also, now that wallet/scan is being added that uses wallet/wallet and moves few scanning objects such as ScanForWalletTransactions (& corresponding ChainScanner) and FastWalletRescanFilter, I don't see a strong reason for why we need to have a partial split of the chain scanning stuff in wallet/wallet (such as WalletRescanReserver, ScanResult) and the remaining in wallet/scan. Moving all the chain scanning stuff to wallet/scan would remove the scope of circular dependency altogether and provide a clearer separation of concerns.

    But as mentioned earlier, it would be a bigger refactor that goes outside the scope of this PR. I'm quite excited by the chain scanning speedup we'd get in #34400 and don't want to derail this PR by suggesting that broader refactor.

    That said, I'm also fine with not moving RescanFromTime in this PR because it's a caller of ScanForWalletTransactions and its movement is not fully required in this PR.

    I will review the remaining commits soon.


    Eunovo commented at 12:35 PM on April 23, 2026:

    There are more scanning-related member functions like AbortRescan(), ScanningProgress() and ScanningDuration(), etc. It might be a good idea to move everything to ChainScanner and create a ChainScanner scanner member in CWallet and expose a const ChainScanner& getScanner() function that can be used to access all scanning-related functions and state.

  20. DrahtBot added the label Needs rebase on May 13, 2026
  21. Eunovo force-pushed on May 27, 2026
  22. DrahtBot removed the label Needs rebase on May 27, 2026
  23. Eunovo renamed this:
    wallet: refactor ScanForWalletTransactions
    wallet: move rescan logic into ChainScanner and wallet/scan
    on May 27, 2026
  24. Eunovo commented at 10:59 AM on May 27, 2026: contributor

    @rkrux @polespinasa This PR has been heavily reworked since your last look following the comment in #34681 (review). All scanning-related state and logic have been moved to wallet/scan, and CWallet now has a ChainScanner member.

  25. rkrux commented at 2:04 PM on May 28, 2026: contributor

    Great that more of the scanning logic could be extracted out from the main wallet file, I will take a look.

  26. DrahtBot added the label Needs rebase on May 29, 2026
  27. rkrux commented at 12:05 PM on June 5, 2026: contributor

    Strong Concept ACK e3258c9

    I like the removal of close to 260 lines from wallet.cpp and 90 lines from wallet.h. The presence of the scanning primitives and paraphernalia used to clutter the main wallet file and often broke my flow while reading CWallet. I also like the change in the scanning usage from wallet->xxxscanxxx to wallet->GetScanner().xxxscanxxx as I feel it encapsulates the scanning flow and logic within the Scanner, thereby making CWallet leaner and easier to wrap one's head around.

  28. in src/wallet/wallet.h:569 in d4862c4577
     573 | -    bool IsScanning() const { return fScanningWallet; }
     574 | -    bool IsScanningWithPassphrase() const { return m_scanning_with_passphrase; }
     575 | -    SteadyClock::duration ScanningDuration() const { return fScanningWallet ? SteadyClock::now() - m_scanning_start.load() : SteadyClock::duration{}; }
     576 | -    double ScanningProgress() const { return fScanningWallet ? (double) m_scanning_progress : 0; }
     577 | +    ChainScanner& GetScanner();
     578 | +    const ChainScanner& GetScanner() const;
    


    rkrux commented at 1:31 PM on June 5, 2026:

    In d4862c4577e28cedf542f1198388209a5324f94f "wallet: introduce ChainScanner as a CWallet member"

    While reading this new call, I don't believe there is anything to get as it just returns the chain scanner member variable. Maybe call it just Scanner()? Like how the chain() method is present in CWallet that I like reading because it's straight to the point.

    The Get notation seems more appropriate if a new object is retrieved every time the call is made. But here, it's just the one scanner object that's returned every time.


    Eunovo commented at 12:22 AM on June 10, 2026:

    Done.

  29. rkrux commented at 1:39 PM on June 5, 2026: contributor

    The PR description can mention that this PR should be reviewed with the --color-moved=dimmed-zebra git option to aid review.

  30. Eunovo force-pushed on Jun 10, 2026
  31. DrahtBot removed the label Needs rebase on Jun 10, 2026
  32. polespinasa commented at 7:13 AM on June 11, 2026: member

    @rkrux @polespinasa This PR has been heavily reworked since your last look following the comment in #34681 (comment). All scanning-related state and logic have been moved to wallet/scan, and CWallet now has a ChainScanner member.

    Nice, will review soon, sorry for abandoning this :/

  33. DrahtBot added the label Needs rebase on Jun 19, 2026
  34. Eunovo force-pushed on Jul 9, 2026
  35. DrahtBot removed the label Needs rebase on Jul 9, 2026
  36. Eunovo force-pushed on Jul 13, 2026
  37. Eunovo commented at 6:23 AM on July 14, 2026: contributor

    Added some unit tests to pin the previous ScanForWalletTransactions behaviour before the refactor.

  38. in src/wallet/test/wallet_tests.cpp:11 in 217efa59c7 outdated
       9 | @@ -10,6 +10,10 @@
      10 |  #include <vector>
      11 |  
    


    ismaelsadeeq commented at 10:06 AM on July 21, 2026:

    In 217efa59c734d9ff40ccbb8ad51ae6c75c66dbbb "wallet/tests: pin ScanForWalletTransactions behaviour"

    This commit message is dense and hard to follow, perhaps it will be easier to split the test cases into separate commits?


    Eunovo commented at 10:01 PM on July 23, 2026:

    I cut the commit message short. I'm not convinced that it is worth splitting each unit test into separate commits.

  39. in src/wallet/test/wallet_tests.cpp:223 in 217efa59c7
     217 | @@ -213,6 +218,85 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup)
     218 |      }
     219 |  }
     220 |  
     221 | +BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_reorged_block, TestChain100Setup)
     222 | +{
     223 | +    BOOST_REQUIRE(InitBlockFilterIndex([&]{ return interfaces::MakeChain(m_node); }, BlockFilterType::BASIC, 1_MiB, /*f_memory=*/true, /*f_wipe=*/false));
    


    ismaelsadeeq commented at 11:03 AM on July 21, 2026:

    In 217efa59c734d9ff40ccbb8ad51ae6c75c66dbbb "wallet/tests: pin ScanForWalletTransactions behaviour"

    nit: no need to pass f_wipe to false again; the default is false? Except if it was intentionally done to be explicit?

        BOOST_REQUIRE(InitBlockFilterIndex([&]{ return interfaces::MakeChain(m_node); }, BlockFilterType::BASIC, 1_MiB, /*f_memory=*/true));
    

    Eunovo commented at 10:02 PM on July 23, 2026:

    Fixed.

  40. in src/wallet/test/wallet_tests.cpp:238 in 217efa59c7 outdated
     233 | +    const int stale_height{stale_block->nHeight};
     234 | +    BlockValidationState state;
     235 | +    BOOST_REQUIRE(m_node.chainman->ActiveChainstate().InvalidateBlock(state, stale_block));
     236 | +    const CScript replacement_script{GetScriptForRawPubKey(GenerateRandomKey().GetPubKey())};
     237 | +    CreateAndProcessBlock({}, replacement_script);
     238 | +    CreateAndProcessBlock({}, replacement_script);
    


    ismaelsadeeq commented at 12:12 PM on July 21, 2026:

    In 217efa59c734d9ff40ccbb8ad51ae6c75c66dbbb "wallet/tests: pin ScanForWalletTransactions behaviour"

    It seems the chain extension is not needed at all; commenting the chain extension out and the line that requires the chain to be extended to prev height + 1 does not cause any failure.


    Eunovo commented at 10:03 PM on July 23, 2026:

    Commenting these lines out significantly changes the test. The objective is to test reorg behaviour, but if you don't extend the chain, the stale block becomes the last block processed by the wallet and ScanForWalletTransactions always succeeds once GetLastBlockHeight() is reached.

  41. in src/wallet/test/wallet_tests.cpp:267 in 217efa59c7
     262 | +        // BIP158 filter stays deterministic; a random key could rarely
     263 | +        // false-positive and flip this branch to the fetch path.
     264 | +        CKey unrelated_key;
     265 | +        const std::vector<unsigned char> unrelated_secret(32, 42);
     266 | +        unrelated_key.Set(unrelated_secret.begin(), unrelated_secret.end(), /*fCompressedIn=*/true);
     267 | +        AddKey(wallet, unrelated_key);
    


    ismaelsadeeq commented at 12:14 PM on July 21, 2026:

    In 217efa59c734d9ff40ccbb8ad51ae6c75c66dbbb "wallet/tests: pin ScanForWalletTransactions behaviour"

    Commenting this out does not cause any failure. This indicates the test has a bad mutant score.


    Eunovo commented at 10:02 PM on July 23, 2026:

    This test doesn't need a key to be added for it to test the intended behaviour. I have deleted these lines.

  42. in src/wallet/test/wallet_tests.cpp:438 in 217efa59c7
     433 | +    // the blockConnected notification would. The scan must pick up the new
     434 | +    // tip instead of stopping at the height it started with.
     435 | +    uint256 new_tip_hash;
     436 | +    int new_tip_height{0};
     437 | +    auto handler = wallet.ShowProgress.connect([&](const std::string&, int progress) {
     438 | +        if (progress != 0 || new_tip_height != 0) return;
    


    ismaelsadeeq commented at 12:04 PM on July 22, 2026:

    In 217efa59c734d9ff40ccbb8ad51ae6c75c66dbbb "wallet/tests: pin ScanForWalletTransactions behaviour"

    Is the other branch redundant? removing it not cause a failure?

            if (progress != 0 ) return;
    

    Eunovo commented at 10:02 PM on July 23, 2026:

    Fixed.

  43. in src/wallet/test/wallet_tests.cpp:439 in 217efa59c7 outdated
     434 | +    // tip instead of stopping at the height it started with.
     435 | +    uint256 new_tip_hash;
     436 | +    int new_tip_height{0};
     437 | +    auto handler = wallet.ShowProgress.connect([&](const std::string&, int progress) {
     438 | +        if (progress != 0 || new_tip_height != 0) return;
     439 | +        CreateAndProcessBlock({}, GetScriptForRawPubKey(coinbaseKey.GetPubKey()));
    


    ismaelsadeeq commented at 12:09 PM on July 22, 2026:

    In 217efa59c734d9ff40ccbb8ad51ae6c75c66dbbb "wallet/tests: pin ScanForWalletTransactions behaviour"

    Hmm, commenting out creating and processing this block does not cause any failure here.

    Same as commenting out the SetLastBlockProcessed.

    <details>

    <summary>this seems to be better</summary>

    diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp
    index e49ac5079b..92da40f17b 100644
    --- a/src/wallet/test/wallet_tests.cpp
    +++ b/src/wallet/test/wallet_tests.cpp
    @@ -418,40 +418,47 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_bounded, TestChain100Setup)
     BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_tip_extension, TestChain100Setup)
     {
         CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
    -    uint256 genesis_hash;
    +    uint256 genesis_hash, start_tip_hash;
    +    int start_tip_height;
         {
             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());
    +        start_tip_height = m_node.chainman->ActiveChain().Height();
    +        start_tip_hash = m_node.chainman->ActiveChain().Tip()->GetBlockHash();
    +        wallet.SetLastBlockProcessed(start_tip_height, start_tip_hash);
             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.
    +    // Extend the chain while the scan is already running: the handler fires on
    +    // the scanning thread from a mid-scan progress report (progress in [1,99]),
    +    // i.e. after the scan has read its starting tip. It advances the wallet's
    +    // tip as the blockConnected notification would. The scan must follow the
    +    // new tip instead of stopping at the height it started with.
         uint256 new_tip_hash;
    -    int new_tip_height{0};
    +    bool extended{false};
         auto handler = wallet.ShowProgress.connect([&](const std::string&, int progress) {
    -        if (progress != 0 || new_tip_height != 0) return;
    +        if (progress == 0 || 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();
    -        new_tip_height = new_tip->nHeight;
    -        wallet.SetLastBlockProcessed(new_tip_height, new_tip_hash);
    +        wallet.SetLastBlockProcessed(new_tip->nHeight, new_tip_hash);
         });
    
         WalletRescanReserver reserver(wallet);
         reserver.reserve();
         CWallet::ScanResult result = wallet.ScanForWalletTransactions(genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false);
         handler.disconnect();
    +    // The scan followed the mid-scan extension instead of stopping at the tip
    +    // it started with (start_tip_height); reaching start_tip_height + 1 also
    +    // confirms the handler actually connected the block.
         BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS);
    +    BOOST_CHECK_EQUAL(*result.last_scanned_height, start_tip_height + 1);
         BOOST_CHECK_EQUAL(result.last_scanned_block, new_tip_hash);
    -    BOOST_CHECK_EQUAL(*result.last_scanned_height, new_tip_height);
     }
    

    </details>


    Eunovo commented at 10:03 PM on July 23, 2026:

    Fixed.

  44. in src/wallet/test/wallet_tests.cpp:468 in 217efa59c7 outdated
     463 | +        LOCK(wallet.cs_wallet);
     464 | +        LOCK(Assert(m_node.chainman)->GetMutex());
     465 | +        wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
     466 | +        tip_hash = m_node.chainman->ActiveChain().Tip()->GetBlockHash();
     467 | +        wallet.SetLastBlockProcessed(m_node.chainman->ActiveChain().Height(), tip_hash);
     468 | +        genesis_hash = m_node.chainman->ActiveChain().Genesis()->GetBlockHash();
    


    ismaelsadeeq commented at 12:29 PM on July 22, 2026:

    In 217efa59c734d9ff40ccbb8ad51ae6c75c66dbbb "wallet/tests: pin ScanForWalletTransactions behaviour"

    This pattern and others are duplicated multiple times; can you create a static helper to be dry?


    Eunovo commented at 10:02 PM on July 23, 2026:

    Not convinced it will be worth the effort.

  45. in src/wallet/test/wallet_tests.cpp:477 in 217efa59c7 outdated
     472 | +
     473 | +    WalletRescanReserver reserver(wallet);
     474 | +    // Advance the clock on every call so that every scanned block would be
     475 | +    // eligible for a progress write if save_progress were set.
     476 | +    std::chrono::steady_clock::time_point fake_time;
     477 | +    reserver.setNow([&] { fake_time += 60s; return fake_time; });
    


    ismaelsadeeq commented at 1:36 PM on July 22, 2026:

    In 217efa59c734d9ff40ccbb8ad51ae6c75c66dbbb "wallet/tests: pin ScanForWalletTransactions behaviour"

    Commenting this does not cause any test failure.

    The test does not prove the scan actually crossed a progress-save interval. Consider asserting the rescan interval path was reached, for example by checking the existing "Still rescanning. At block" log after installing the fake clock. That keeps the test focused on save_progress=false while making the fake clock observable.

        bool progress_logged{false};
        DebugLogHelper progress_check{"Still rescanning. At block", [&](const std::string* s) {
            if (s) progress_logged = true;
            return false;
        }};
    
         CWallet::ScanResult result = wallet.ScanForWalletTransactions(genesis_hash, /*start_height=*/0, max_height, reserver, /*save_progress=*/false);
        BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS);
        BOOST_CHECK(progress_logged);
    

    This way, commenting out the reserver setNow causes failure.


    Eunovo commented at 10:02 PM on July 23, 2026:

    This is not necessary. If you ignore the save_progress parameter, this test will fail as is. If you comment out the reserver.setNow line, the test will no longer fail. The test works as is.

  46. in src/wallet/test/wallet_tests.cpp:495 in 217efa59c7 outdated
     490 | +}
     491 | +
     492 | +BOOST_FIXTURE_TEST_CASE(rescan_from_time, TestChain100Setup)
     493 | +{
     494 | +    // Cap last block file size, and mine new block in a new block file.
     495 | +    CBlockIndex* old_tip = WITH_LOCK(Assert(m_node.chainman)->GetMutex(), return m_node.chainman->ActiveChain().Tip());
    


    ismaelsadeeq commented at 2:05 PM on July 22, 2026:

    In 217efa59c734d9ff40ccbb8ad51ae6c75c66dbbb "wallet/tests: pin ScanForWalletTransactions behaviour"

    Nit, you use the same WITH_LOCK approach in other tests too?

    <details> <summary>see diff and other potential deduplications</summary>

    diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp
    index fd2dfc238c..eaace67d8e 100644
    --- a/src/wallet/test/wallet_tests.cpp
    +++ b/src/wallet/test/wallet_tests.cpp
    @@ -76,6 +76,49 @@ static void AddKey(CWallet& wallet, const CKey& key)
         Assert(wallet.AddWalletDescriptor(w_desc, provider, "", false));
     }
    
    +struct ActiveChainInfo {
    +    uint256 genesis_hash;
    +    int tip_height;
    +    uint256 tip_hash;
    +};
    +
    +struct BoundedScanInfo {
    +    ActiveChainInfo chain_info;
    +    int max_height;
    +    uint256 max_hash;
    +};
    +
    +static ActiveChainInfo GetActiveChainInfo(ChainstateManager& chainman)
    +{
    +    return WITH_LOCK(chainman.GetMutex(), return (ActiveChainInfo{
    +        chainman.ActiveChain().Genesis()->GetBlockHash(),
    +        chainman.ActiveChain().Height(),
    +        chainman.ActiveChain().Tip()->GetBlockHash()}));
    +}
    +
    +static BoundedScanInfo GetBoundedScanInfo(ChainstateManager& chainman, int blocks_before_tip)
    +{
    +    return WITH_LOCK(chainman.GetMutex(),
    +        const int max_height{chainman.ActiveChain().Height() - blocks_before_tip};
    +        return (BoundedScanInfo{
    +            {chainman.ActiveChain().Genesis()->GetBlockHash(),
    +             chainman.ActiveChain().Height(),
    +             chainman.ActiveChain().Tip()->GetBlockHash()},
    +            max_height,
    +            chainman.ActiveChain()[max_height]->GetBlockHash()});
    +    );
    +}
    +
    +static void SetupDescriptorWalletWithKey(CWallet& wallet, const CKey& key, int block_height, const uint256& block_hash)
    +{
    +    {
    +        LOCK(wallet.cs_wallet);
    +        wallet.SetWalletFlag(WALLET_FLAG_DESCRIPTORS);
    +        wallet.SetLastBlockProcessed(block_height, block_hash);
    +    }
    +    AddKey(wallet, key);
    +}
    +
     BOOST_FIXTURE_TEST_CASE(update_non_range_descriptor, TestingSetup)
     {
         CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
    @@ -362,55 +405,36 @@ BOOST_FIXTURE_TEST_CASE(wallet_rescan_reserver, TestingSetup)
    
     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();
    -    }
    +    const auto scan{GetBoundedScanInfo(*Assert(m_node.chainman), /*blocks_before_tip=*/2)};
    
         // 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);
    +        SetupDescriptorWalletWithKey(wallet, coinbaseKey, scan.chain_info.tip_height, scan.chain_info.tip_hash);
             WalletRescanReserver reserver(wallet);
             reserver.reserve();
    -        CWallet::ScanResult result = wallet.ScanForWalletTransactions(genesis_hash, /*start_height=*/0, max_height, reserver, /*save_progress=*/false);
    +        CWallet::ScanResult result = wallet.ScanForWalletTransactions(scan.chain_info.genesis_hash, /*start_height=*/0, scan.max_height, reserver, /*save_progress=*/false);
             BOOST_CHECK_EQUAL(result.status, CWallet::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);
    +        BOOST_CHECK_EQUAL(result.last_scanned_block, scan.max_hash);
    +        BOOST_CHECK_EQUAL(*result.last_scanned_height, scan.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));
    +        BOOST_CHECK_EQUAL(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.size()), static_cast<size_t>(scan.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);
    +        SetupDescriptorWalletWithKey(wallet, coinbaseKey, scan.chain_info.tip_height, scan.chain_info.tip_hash);
             WalletRescanReserver reserver(wallet);
             reserver.reserve();
    -        CWallet::ScanResult result = wallet.ScanForWalletTransactions(tip_hash, tip_height, tip_height, reserver, /*save_progress=*/false);
    +        CWallet::ScanResult result = wallet.ScanForWalletTransactions(scan.chain_info.tip_hash, scan.chain_info.tip_height, scan.chain_info.tip_height, reserver, /*save_progress=*/false);
             BOOST_CHECK_EQUAL(result.status, CWallet::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(result.last_scanned_block, scan.chain_info.tip_hash);
    +        BOOST_CHECK_EQUAL(*result.last_scanned_height, scan.chain_info.tip_height);
             BOOST_CHECK_EQUAL(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.size()), 1U);
         }
     }
    @@ -418,15 +442,8 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_bounded, TestChain100Setup)
     BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_tip_extension, TestChain100Setup)
     {
         CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
    -    uint256 genesis_hash;
    -    {
    -        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());
    -        genesis_hash = m_node.chainman->ActiveChain().Genesis()->GetBlockHash();
    -    }
    -    AddKey(wallet, coinbaseKey);
    +    const auto chain_info{GetActiveChainInfo(*Assert(m_node.chainman))};
    +    SetupDescriptorWalletWithKey(wallet, coinbaseKey, chain_info.tip_height, chain_info.tip_hash);
    
         // 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
    @@ -437,17 +454,15 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_tip_extension, TestChain100
         auto handler = wallet.ShowProgress.connect([&](const std::string&, int progress) {
             if (progress != 0 || new_tip_height != 0) return;
             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();
    -        new_tip_height = new_tip->nHeight;
    -        wallet.SetLastBlockProcessed(new_tip_height, new_tip_hash);
    +        const auto new_tip{GetActiveChainInfo(*Assert(m_node.chainman))};
    +        new_tip_hash = new_tip.tip_hash;
    +        new_tip_height = new_tip.tip_height;
    +        WITH_LOCK(wallet.cs_wallet, wallet.SetLastBlockProcessed(new_tip_height, new_tip_hash));
         });
    
         WalletRescanReserver reserver(wallet);
         reserver.reserve();
    -    CWallet::ScanResult result = wallet.ScanForWalletTransactions(genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false);
    +    CWallet::ScanResult result = wallet.ScanForWalletTransactions(chain_info.genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false);
         handler.disconnect();
         BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS);
         BOOST_CHECK_EQUAL(result.last_scanned_block, new_tip_hash);
    @@ -457,18 +472,9 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_tip_extension, TestChain100
     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);
    +    const auto chain_info{GetActiveChainInfo(*Assert(m_node.chainman))};
    +    const int max_height{chain_info.tip_height - 2};
    +    SetupDescriptorWalletWithKey(wallet, coinbaseKey, chain_info.tip_height, chain_info.tip_hash);
    
         WalletRescanReserver reserver(wallet);
         // Advance the clock on every call so that every scanned block would be
    @@ -477,7 +483,7 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_no_progress_saved, TestChai
         reserver.setNow([&] { fake_time += 60s; return fake_time; });
         reserver.reserve();
    
    -    CWallet::ScanResult result = wallet.ScanForWalletTransactions(genesis_hash, /*start_height=*/0, max_height, reserver, /*save_progress=*/false);
    +    CWallet::ScanResult result = wallet.ScanForWalletTransactions(chain_info.genesis_hash, /*start_height=*/0, max_height, reserver, /*save_progress=*/false);
         BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS);
    
         // With save_progress=false the scan must not touch the wallet's best
    @@ -486,7 +492,7 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_no_progress_saved, TestChai
         CBlockLocator locator;
         BOOST_CHECK(WalletBatch{wallet.GetDatabase()}.ReadBestBlock(locator));
         BOOST_CHECK(!locator.IsNull());
    -    BOOST_CHECK_EQUAL(locator.vHave.front(), tip_hash);
    +    BOOST_CHECK_EQUAL(locator.vHave.front(), chain_info.tip_hash);
     }
    
     BOOST_FIXTURE_TEST_CASE(rescan_from_time, TestChain100Setup)
    @@ -499,21 +505,15 @@ BOOST_FIXTURE_TEST_CASE(rescan_from_time, TestChain100Setup)
    
         // Prune the older block file.
         int file_number;
    -    {
    -        LOCK(cs_main);
    +    WITH_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);
    +    const auto chain_info{GetActiveChainInfo(*Assert(m_node.chainman))};
    +    SetupDescriptorWalletWithKey(wallet, coinbaseKey, chain_info.tip_height, chain_info.tip_hash);
         WalletRescanReserver reserver(wallet);
         reserver.reserve();
    
    @@ -540,27 +540,17 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_missing_filter, TestChain10
    
         {
             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();
    -            tip_hash = m_node.chainman->ActiveChain().Tip()->GetBlockHash();
    -            wallet.SetLastBlockProcessed(tip_height, tip_hash);
    -        }
    -        AddKey(wallet, coinbaseKey);
    +        const auto chain_info{GetActiveChainInfo(*Assert(m_node.chainman))};
    +        SetupDescriptorWalletWithKey(wallet, coinbaseKey, chain_info.tip_height, chain_info.tip_hash);
             WalletRescanReserver reserver(wallet);
             reserver.reserve();
    -        CWallet::ScanResult result = wallet.ScanForWalletTransactions(genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false);
    +        CWallet::ScanResult result = wallet.ScanForWalletTransactions(chain_info.genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false);
             BOOST_CHECK_EQUAL(result.status, CWallet::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(result.last_scanned_block, chain_info.tip_hash);
    +        BOOST_CHECK_EQUAL(*result.last_scanned_height, chain_info.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));
    +        BOOST_CHECK_EQUAL(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.size()), static_cast<size_t>(chain_info.tip_height));
         }
    
         filter_index.Stop();
    @@ -588,21 +578,15 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_attach_chain, TestChain100S
             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();
    -    }
    +    const auto chain_info{GetActiveChainInfo(*Assert(m_node.chainman))};
    
         // 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);
    +        BOOST_CHECK_EQUAL(wallet->GetLastBlockHeight(), chain_info.tip_height);
    +        BOOST_CHECK_EQUAL(wallet->GetLastBlockHash(), chain_info.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));
    

    </details>


    Eunovo commented at 10:02 PM on July 23, 2026:

    Not convinced that it is worth the effort to do this.

  47. in src/wallet/test/wallet_tests.cpp:529 in 217efa59c7
     524 | +    BOOST_CHECK_EQUAL(wallet.RescanFromTime(genesis_time, reserver),
     525 | +                      WITH_LOCK(::cs_main, return old_tip->GetBlockTimeMax()) + TIMESTAMP_WINDOW + 1);
     526 | +
     527 | +    // A timestamp past the tip requires no scanning and is returned unchanged.
     528 | +    const int64_t future_time{WITH_LOCK(::cs_main, return new_tip->GetBlockTimeMax()) + TIMESTAMP_WINDOW + 1};
     529 | +    BOOST_CHECK_EQUAL(wallet.RescanFromTime(future_time, reserver), future_time);
    


    ismaelsadeeq commented at 2:32 PM on July 22, 2026:

    In 217efa59c734d9ff40ccbb8ad51ae6c75c66dbbb "wallet/tests: pin ScanForWalletTransactions behaviour"

    A mutation changing future_time from tip_time + TIMESTAMP_WINDOW + 1 to tip_time + TIMESTAMP_WINDOW still passes, so the test does not prove the “nothing needs scanning” path. Consider asserting no rescan was started around this call, for example by checking that the existing "Rescan started" log is not emitted as well?


    Eunovo commented at 10:02 PM on July 23, 2026:

    Fixed.

  48. in src/wallet/test/wallet_tests.cpp:566 in 217efa59c7 outdated
     561 | +        BOOST_CHECK_EQUAL(*result.last_scanned_height, tip_height);
     562 | +        // One coinbase per block from height 1 through the tip.
     563 | +        BOOST_CHECK_EQUAL(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.size()), static_cast<size_t>(tip_height));
     564 | +    }
     565 | +
     566 | +    filter_index.Stop();
    


    ismaelsadeeq commented at 2:47 PM on July 22, 2026:

    In 217efa59c734d9ff40ccbb8ad51ae6c75c66dbbb "wallet/tests: pin ScanForWalletTransactions behaviour"

    Perhaps add a comment why we stop and destroy the filter here.


    Eunovo commented at 10:02 PM on July 23, 2026:

    Filter indexes are stored in a global map; if it is not destroyed, it will cause the next InitBlockFilterIndex call to fail. It is not unusual to destroy objects that were initialised in the test, so I'm not convinced it needs an explicit comment.

  49. in src/wallet/test/wallet_tests.cpp:567 in 217efa59c7 outdated
     562 | +        // One coinbase per block from height 1 through the tip.
     563 | +        BOOST_CHECK_EQUAL(WITH_LOCK(wallet.cs_wallet, return wallet.mapWallet.size()), static_cast<size_t>(tip_height));
     564 | +    }
     565 | +
     566 | +    filter_index.Stop();
     567 | +    BOOST_REQUIRE(DestroyBlockFilterIndex(BlockFilterType::BASIC));
    


    ismaelsadeeq commented at 2:49 PM on July 22, 2026:

    The test does not currently prove it is exercising the missing-filter fast-rescan path: syncing the filter index, or removing the filter index setup entirely, still passes.

    Consider asserting the scan entered the fast-rescan path and saw missing filters, e.g. by enabling BCLog::SCAN and checking the existing "fast variant using block filters" and "block filter not found" log messages.

    <details> <summary>see diff</summary>

    diff --git a/src/wallet/test/wallet_tests.cpp b/src/wallet/test/wallet_tests.cpp
    index 4fb04b9d23..ab20ae3956 100644
    --- a/src/wallet/test/wallet_tests.cpp
    +++ b/src/wallet/test/wallet_tests.cpp
    @@ -16,6 +16,7 @@
     #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>
    @@ -539,6 +540,19 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_missing_filter, TestChain10
         BOOST_REQUIRE(filter_index.Init());
    
         {
    +        const BCLog::CategoryMask previous_log_categories{LogInstance().GetCategoryMask()};
    +        LogInstance().EnableCategory(BCLog::SCAN);
    +        bool fast_rescan_logged{false};
    +        DebugLogHelper fast_rescan_check{"fast variant using block filters", [&](const std::string* s) {
    +            if (s) fast_rescan_logged = true;
    +            return false;
    +        }};
    +        bool missing_filter_logged{false};
    +        DebugLogHelper missing_filter_check{"block filter not found", [&](const std::string* s) {
    +            if (s) missing_filter_logged = true;
    +            return false;
    +        }};
    +
             CWallet wallet(m_node.chain.get(), "", CreateMockableWalletDatabase());
             uint256 genesis_hash, tip_hash;
             int tip_height;
    @@ -555,7 +569,11 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_missing_filter, TestChain10
             WalletRescanReserver reserver(wallet);
             reserver.reserve();
             CWallet::ScanResult result = wallet.ScanForWalletTransactions(genesis_hash, /*start_height=*/0, /*max_height=*/{}, reserver, /*save_progress=*/false);
    +        LogInstance().DisableCategory(BCLog::LogFlags::ALL);
    +        LogInstance().EnableCategory(BCLog::LogFlags{previous_log_categories});
             BOOST_CHECK_EQUAL(result.status, CWallet::ScanResult::SUCCESS);
    +        BOOST_CHECK(fast_rescan_logged);
    +        BOOST_CHECK(missing_filter_logged);
             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);
    
    
    

    </details>


    Eunovo commented at 10:03 PM on July 23, 2026:

    Fixed using a slightly different diff.

  50. ismaelsadeeq commented at 3:40 PM on July 22, 2026: member

    Code review 217efa59c734d9ff40ccbb8ad51ae6c75c66dbbb

    I just looked at the test commit.

    Nice test coverage; the commit message indicates that it crammed in lots of test cases.

    It will be easy to review if broken up into multiple commits.

    It seems the tests are low-level; that's why u use unit tests? are their some user-facing tests that fit better as functional tests (I should attempt to write, I know :), but may reduce duplicate work to know why or why not)?

  51. Eunovo force-pushed on Jul 23, 2026
  52. Eunovo force-pushed on Jul 23, 2026
  53. DrahtBot added the label CI failed on Jul 23, 2026
  54. Eunovo commented at 10:11 PM on July 23, 2026: contributor

    It seems the tests are low-level; that's why u use unit tests? are their some user-facing tests that fit better as functional tests (I should attempt to write, I know :), but may reduce duplicate work to know why or why not)?

    I don't think any of the added tests will be better as functional tests. The tests are intended to pin specific behaviour of the rescan functions.

  55. DrahtBot removed the label CI failed on Jul 23, 2026
  56. in src/wallet/scan.cpp:50 in 985d166f9d
      45 | +    m_could_reserve = true;
      46 | +    return true;
      47 | +}
      48 | +
      49 | +bool WalletRescanReserver::isReserved() const {
      50 | +    return m_could_reserve && m_wallet.Scanner().IsScanning();
    


    polespinasa commented at 10:16 AM on July 27, 2026:

    in 985d166f9d684d51bc5f34c4a9db4d80db92a5bb wallet/scan: move WalletRescanReserver to scan files

    consider keeping the parenthesis return ( m_could_reserve .... IsScanning()) to not break the diff with color-moved=dimmed-zebra


    Eunovo commented at 2:07 PM on July 28, 2026:

    Fixed.

  57. in src/wallet/scan.h:50 in 985d166f9d
      45 | +    explicit WalletRescanReserver(CWallet& w) : m_wallet(w) {}
      46 | +
      47 | +    bool reserve(bool with_passphrase = false);
      48 | +    bool isReserved() const;
      49 | +
      50 | +    Clock::time_point now() const { return m_now ? m_now() : Clock::now(); }
    


    polespinasa commented at 10:19 AM on July 27, 2026:

    in 985d166 wallet/scan: move WalletRescanReserver to scan files

    same, to not break the color-move filter, consider adding the ending ; and adding back the empty line under this declaration.


    Eunovo commented at 2:07 PM on July 28, 2026:

    Fixed.

  58. in src/wallet/scan.cpp:215 in da873c7e20
     239 | -            if (save_progress && next_interval) found_block.locator(loc);
     240 | -            chain.findBlock(block_hash, found_block);
     241 | -
     242 | -            if (!block.IsNull()) {
     243 | -                LOCK(m_wallet.cs_wallet);
     244 | -                if (!block_still_active) {
    


    polespinasa commented at 10:39 AM on July 27, 2026:

    in da873c7e20ae200af42077d024e3691ecf4d2e11 wallet/scan: extract block scanning logic to ScanBlock

    I think there is a behavior change, not only a move. In the original code the block_still_active check was done after reading the block data. In the new code it is check before ScanBlock, which then reads the block data.

    If block.isNull() and !block_still_active the old code would set FAILURE and continue scanning while the new breaks immediatly.

    I think the behavior change is correct, it does not make sense to scan successors of an inactive block, but mentioning that in the commit message would make things more clear.


    Eunovo commented at 2:08 PM on July 28, 2026:

    Thanks for pointing this out. The commit message has been updated.

  59. in src/wallet/scan.cpp:308 in 69c32fc327
     310 |      if (!max_height) {
     311 |          m_wallet.WalletLogPrintf("Scanning current mempool transactions.\n");
     312 |          WITH_LOCK(m_wallet.cs_wallet, chain.requestMempoolTransactions(m_wallet));
     313 |      }
     314 | -    m_wallet.ShowProgress(strprintf("[%s] %s", m_wallet.DisplayName(), _("Rescanning…")), 100); // hide progress dialog in GUI
     315 | +    m_wallet.ShowProgress(strprintf("[%s] %s", m_wallet.DisplayName(), _("Rescanning…")), 100);
    


    polespinasa commented at 10:42 AM on July 27, 2026:

    in 69c32fc327cf2b53b3f888236e8d5d62a41aa23d wallet/scan: extract progress tracking helpers from ChainScanner::Scan

    A comment was removed here, probably worth to add it back.


    Eunovo commented at 2:08 PM on July 28, 2026:

    Fixed.

  60. polespinasa commented at 10:46 AM on July 27, 2026: member

    concept ACK

  61. Eunovo force-pushed on Jul 28, 2026
  62. in src/wallet/test/wallet_tests.cpp:227 in a32f72fd60 outdated
     218 | @@ -213,6 +219,77 @@ BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions, TestChain100Setup)
     219 |      }
     220 |  }
     221 |  
     222 | +BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_reorged_block, TestChain100Setup)
     223 | +{
     224 | +    BOOST_REQUIRE(InitBlockFilterIndex([&]{ return interfaces::MakeChain(m_node); }, BlockFilterType::BASIC, 1_MiB, /*f_memory=*/true));
     225 | +    BlockFilterIndex& filter_index{*Assert(GetBlockFilterIndex(BlockFilterType::BASIC))};
     226 | +    BOOST_REQUIRE(filter_index.Init());
     227 | +    filter_index.Sync();
    


    polespinasa commented at 8:47 AM on July 29, 2026:

    in a32f72fd60ed0f4584e5337f2747b5e2690d76d3 wallet/tests: pin rescan behavior

    If I am not wrong this Sync call is redundant since we immediately call BlockUntilSyncedToCurrentChain which already ensures sync.


    Eunovo commented at 1:09 PM on July 30, 2026:

    BlockUntilSyncedToCurrentChain fails without first calling Sync

  63. in src/wallet/test/wallet_tests.cpp:585 in a32f72fd60 outdated
     580 | +//! Test the rescan that loading a wallet performs when the wallet is behind
     581 | +//! the chain tip: it scans from the wallet's recorded best block - a
     582 | +//! mid-chain start - with cs_wallet held.
     583 | +BOOST_FIXTURE_TEST_CASE(scan_for_wallet_transactions_attach_chain, TestChain100Setup)
     584 | +{
     585 | +    m_args.ForceSetArg("-unsafesqlitesync", "1");
    


    polespinasa commented at 8:48 AM on July 29, 2026:

    in a32f72fd60ed0f4584e5337f2747b5e2690d76d3 wallet/tests: pin rescan behavior

    Probably this is worth a short comment bc it is not easy to know why this is needed at first.


    Eunovo commented at 1:55 PM on July 30, 2026:

    Add a short comment.

  64. in src/wallet/scan.cpp:165 in 775e954ade outdated
     161 | @@ -162,6 +162,27 @@ bool ChainScanner::QueueNextBlock(const uint256& block_hash, int block_height, s
     162 |      return block_still_active;
     163 |  }
     164 |  
     165 | +void ChainScanner::UpdateProgress(const LoopState& state, double progress_current, int block_height) {
    


    polespinasa commented at 9:13 AM on July 29, 2026:

    in 775e954ade2aa610b2ff1b01f9443a4547e39d6b wallet/scan: extract progress tracking helpers from ChainScanner::Scan

    nit: feel free to ignore

    I think the function could be simplified, the state.progress_end - state.progress_begin > 0.0 is checked twice.

    $ git diff
    diff --git a/src/wallet/scan.cpp b/src/wallet/scan.cpp
    index 5f364801f8..ad37b752ca 100644
    --- a/src/wallet/scan.cpp
    +++ b/src/wallet/scan.cpp
    @@ -163,13 +163,13 @@ bool ChainScanner::QueueNextBlock(const uint256& block_hash, int block_height, s
     }
     
     void ChainScanner::UpdateProgress(const LoopState& state, double progress_current, int block_height) {
    -    if (state.progress_end - state.progress_begin > 0.0) {
    -        m_scanning_progress = (progress_current - state.progress_begin) / (state.progress_end - state.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 && state.progress_end - state.progress_begin > 0.0) {
    +    // avoid divide-by-zero for single block scan range (i.e. start and stop hashes are equal)
    +    m_scanning_progress = 0;
    +    double progress_diff = state.progress_end - state.progress_begin;
    +    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))));
         }
    

    Eunovo commented at 1:54 PM on July 30, 2026:

    Fixed.

  65. in src/wallet/scan.cpp:234 in 775e954ade outdated
     235 |  
     236 |      ScanResult result;
     237 | -    double progress_begin = chain.guessVerificationProgress(start_block);
     238 | -    double progress_end = chain.guessVerificationProgress(end_hash);
     239 | -    double progress_current = progress_begin;
     240 | +    LoopState state;
    


    polespinasa commented at 9:15 AM on July 29, 2026:

    in 775e954 wallet/scan: extract progress tracking helpers from ChainScanner::Scan

    nit:

    LoopState defaults progress range to 0, 0 but those values are overwritten instantly. I think having a constructor setting the hash, begin and end would be cleaner.


    Eunovo commented at 1:33 PM on July 30, 2026:

    I decided to leave this as is because the resulting code that instantiates the LoopState looks worse.

  66. polespinasa commented at 9:16 AM on July 29, 2026: member

    some more nits :)

  67. wallet/tests: pin rescan behavior
    Add unit tests locking in currently untested rescan behavior, so that
    the upcoming ChainScanner refactor can be reviewed against them.
    1a688a9679
  68. wallet: introduce ChainScanner as a CWallet member
    Move scan state atomics (abort, scanning, passphrase, start time,
    progress) into ChainScanner and expose it via Scanner(). All
    callers use Scanner().Scan() directly.
    
    The newly added `m_scanner` is an incomplete type so CWallet's
    constructor and destructor is moved into wallet.cpp where
    the type is complete.
    
    This change introduces a new circular dependency of the form
    "wallet/scan -> wallet/wallet -> wallet/scan" which is added to
    `EXPECTED_CIRCULAR_DEPENDENCIES`.
    3a972e1e83
  69. wallet/scan: move RescanFromTime to ChainScanner as ScanFromTime
    Callers now reach this via Scanner().ScanFromTime() rather than
    a CWallet member function, keeping all scan logic in ChainScanner.
    23fa9add5e
  70. wallet/scan: move WalletRescanReserver to scan files 6d836f38a4
  71. wallet/scan: extract block filter matching to ShouldFetchBlock 565d0dfc41
  72. wallet/scan: extract block scanning logic to ScanBlock
    A slight behavior change is introduced here. Previously, if a block
    could not be read and the block is not active, the scan will continue
    but record this block as the most recent failure. After this commit,
    the scan will abort with this block as the most recent failure.
    
    This happens because the `Scan()` function now checks if the block is
    active before trying to read the block from disk.
    
    The scan process already ignores reorged blocks, so this should have
    no effect on the wallet balance.
    82eac7a197
  73. wallet/scan: extract QueueNextBlock
    Dequeue the current block at the top of the Scan loop and extract the
    lookup of its chain position and the queueing of its active-chain
    successor into QueueNextBlock.
    
    Since the current block is now dequeued at the top of the loop, update
    progress_current there as well so the reported progress keeps referring
    to the block being processed, as before.
    8ea5bebaf1
  74. wallet/scan: extract progress tracking helpers from `ChainScanner::Scan` 41abd4cf61
  75. Eunovo force-pushed on Jul 30, 2026
  76. polespinasa commented at 2:19 PM on July 30, 2026: member

    ACK 41abd4cf61ecff81623dc0779c046038dced2aa3

    thanks for addressing the feedback. Code looks good to me, moving the mode seems a clean and good step.

    I didn't go super deep into the code tests.

  77. DrahtBot requested review from rkrux on Jul 30, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-08-01 07:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me