wallet, rpc: add UTXO set check and incremental rescan to importdescriptors #33392

pull musaHaruna wants to merge 6 commits into bitcoin:master from musaHaruna:feature/scan-utxoset-balance-check changing 11 files +411 −6
  1. musaHaruna commented at 4:17 pm on September 15, 2025: contributor

    Fixes #28898

    When importing descriptors, users may accidentally provide an incorrect birthdate (timestamp). This can cause the wallet to miss relevant historical transactions, leading to incorrect or incomplete balances. Currently, the wallet only relies on rescans starting from the provided timestamp.

    This PR extends the importdescriptors RPC with a new optional argument: scan_utxoset (bool, default=false): If enabled, the wallet will compare its calculated trusted balance against UTXO set balance (by generating the scriptpukeys of the wallet and comparing it with the chains UTXO set scriptpubkeys to get the accurate balance belonging to the wallet and comparing it with the wallet trusted balance).

    If the balances match, import continues as normal. If a discrepancy is detected, the wallet will attempt incremental rescans in chunks of recent blocks until the missing history is found. If the wallet is pruned, incremental rescans will not go earlier than the prune boundary.

    Additional information is returned in the RPC response under an "info" object when scan_utxoset is used, such as: utxo_check: whether the UTXO set matched the wallet balance scanned_chunks: number of incremental rescan chunks attempted scanned_blocks: approximate number of blocks scanned during incremental rescans

    This helps detect and fix balance mismatches caused by wrong descriptor timestamps.

    A big thanks to fjahr for suggesting this approach comment

  2. node: add and implement FindCoinsByScript for UTXO-set scan by script
    Add node::FindCoinsByScript and wire it into the node interface as
    Node::getCoinsByScript(), then implement the DB-backed scan that iterates
    the on-disk UTXO DB with a CCoinsViewCursor.
    ba9f134734
  3. DrahtBot added the label RPC/REST/ZMQ on Sep 15, 2025
  4. DrahtBot commented at 4:17 pm on September 15, 2025: contributor

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

    Code Coverage & Benchmarks

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

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK rkrux

    If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #bitcoin-core/gui/911 (Adds non-mempool wallet balance to overview by ajtowns)
    • #33671 (wallet: Add separate balance info for non-mempool wallet txs by ajtowns)
    • #33135 (wallet: warn against accidental unsafe older() import by Sjors)
    • #32861 (Have createwalletdescriptor auto-detect an unused(KEY) by Sjors)
    • #30343 (wallet, logging: Replace WalletLogPrintf() with LogInfo() by ryanofsky)
    • #29136 (wallet: addhdkey RPC to add just keys to wallets via new unused(KEY) descriptor 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.

    LLM Linter (✨ experimental)

    Possible typos and grammar issues:

      • //! Scan UTXO set from coins belonging to the output_scripts -> + //! Scan UTXO set for coins belonging to output_scripts [“from coins belonging to the output_scripts” is ungrammatical/awkward; “for coins belonging to output_scripts” is clearer and consistent with other comments]
      •    # Mine 1000 total 1000 more blocksthen send the second tx -> +        # Mine 1000 more blocks then send the second tx [“1000 total 1000 more blocksthen” has duplicated/missing words and “blocksthen” is a typo; corrected to a clear sentence]
        

    drahtbot_id_5_m

  5. musaHaruna force-pushed on Sep 16, 2025
  6. fjahr commented at 2:17 pm on September 16, 2025: contributor
    It seems kind of weird to me to add this as an option to getbalance. The problem is in the importdescriptors call, which is using the wrong birthdate. Have you looked into making this an option of importdescriptors? If the performance isn’t too bad, this could even be on by default. If that is possible that would seem preferred, since you also write “However, users may not realize that their wallet balance is incomplete.”, if they don’t know they likely also won’t call the getbalance with this option. So ideally we could “force” this on users in a way that they don’t feel (much) negative impact from it. If we can’t do that then I am not sure this is of much use since the users could just call rescanblockchain directly if they are aware that there is a problem with their balances.
  7. musaHaruna commented at 4:13 pm on September 16, 2025: contributor

    Have you looked into making this an option of importdescriptors?

    I have not looked into making it an option for importdecriptors, but I will look into it, and get back to you on that.

    Just thinking on a high level and somewhat naively because am a new contributor with little knowledge about the codebase, what if we can atomatically cross check the balance by scanning the utxoset in importdescriptor before triggering the rescan, incase of wrong birthtime, seeing that even if they use the new flags in getbalance, they might/will eventually have to run rescanblockchain.

    Thanks for the suggestion. I really appreciate it.

  8. musaHaruna commented at 7:45 pm on September 16, 2025: contributor

    I have not looked into making it an option for importdecriptors, but I will look into it, and get back to you on that.

    Yes, I have looked into adding the option to importdescriptors by introducing a scan_utxoset flag and it’s possible. The idea is that when enabled, the wallet would scan the UTXO set immediately after import to verify balances against chainstate, and if a discrepancy is detected (for example due to an incorrect birthdate), it could automatically trigger a full rescan from height 0 to restore missing history. This way, users wouldn’t have to manually diagnose incomplete balances — the import flow itself would handle it. What Do you think this approach?.

    I don’t yet know the full impact on performance, but I’m thinking of making the trade-offs very clear in the RPC docs so users can decide whether to enable it.

  9. fjahr commented at 8:53 pm on September 16, 2025: contributor

    Yes, I have looked into adding the option to importdescriptors by introducing a scan_utxoset flag and it’s possible. The idea is that when enabled, the wallet would scan the UTXO set immediately after import to verify balances against chainstate, and if a discrepancy is detected (for example due to an incorrect birthdate), it could automatically trigger a full rescan from height 0 to restore missing history. This way, users wouldn’t have to manually diagnose incomplete balances — the import flow itself would handle it. What Do you think this approach?.

    Cool, this is pretty much what I had in mind, I guess I would slightly prefer that full rescan isn’t started by default and rather the user receives a clear hint that there might be some funds missing and if they want to make sure to get them they should run a full rescan, basically just like what you are doing now in the return from getbalance. But I don’t have a strong preference there, either sound fine, maybe wait for some more conceptual feedback from other reviewers.

    I don’t yet know the full impact on performance, but I’m thinking of making the trade-offs very clear in the RPC docs so users can decide whether to enable it.

    Maybe run some benchmarks on the utxo set scan and full rescan and add them to the PR here. I don’t have a good feeling for what the relation is and this might influence what reviewers think is better concerning running a full rescan automatically or just returning a warning/hint from importdescriptors.

    Another idea concerning performance: It seems likely that if the birth date is wrong the user might be off by just a few days or weeks, rather than they have a wallet with the satoshi coins. So instead of a full rescan from the start of the chain the rescan could move backwards from the originally supplied birth date and scan the chain in 1000 (or so) block increments and it could stop once the balance matches the one from the utxo set scan. A nice side effect of this is that it means the rescan is also pruning compatible (for as many blocks that are available) but I guess this could be also achieved by starting from pruneheight instead of 0 if pruning is enabled. It’s something you need to keep in mind either way. This would also need benchmarks but I can’t imagine moving backwards with somewhat large increments would make the process much slower. And ideally the process would exit early in the most common scenario.

  10. musaHaruna commented at 9:27 am on September 17, 2025: contributor

    Maybe run some benchmarks on the utxo set scan and full rescan and add them to the PR here. I don’t have a good feeling for what the relation is and this might influence what reviewers think is better concerning running a full rescan automatically or just returning a warning/hint from importdescriptors.

    I will run some benchmarks on both utxo set scan and full rescan, update the whole PR to use the new approach you suggested i.e to add the feature on importdescriptors directly, which I honestly think is better.

    Another idea concerning performance: It seems likely that if the birth date is wrong the user might be off by just a few days or weeks, rather than they have a wallet with the satoshi coins. So instead of a full rescan from the start of the chain the rescan could move backwards from the originally supplied birth date and scan the chain in 1000 (or so) block increments and it could stop once the balance matches the one from the utxo set scan. A nice side effect of this is that it means the rescan is also pruning compatible (for as many blocks that are available) but I guess this could be also achieved by starting from pruneheight instead of 0 if pruning is enabled. It’s something you need to keep in mind either way. This would also need benchmarks but I can’t imagine moving backwards with somewhat large increments would make the process much slower. And ideally the process would exit early in the most common scenario.

    Just to make sure we’re on the same page — my understanding is that the idea is to:

    Use chunked backward rescans starting from the supplied birthdate (e.g. 1000-block increments), and stop as soon as the wallet balance matches the UTXO-set scan. This handles the common case where the birthdate is only off by a few days or weeks, so we don’t need to rescan the entire chain.

    At the same time, respect the pruneheight as the lower bound if pruning is enabled. That means we only scan back as far as blocks are available, and if the discrepancy still isn’t resolved at that point, we’d warn the user that a reindex is required.

  11. musaHaruna renamed this:
    rpc: add scan_utxoset option to getbalance(s) to verify wallet balance accuracy
    wallet/rpc: add scan_utxoset option to getbalance(s) to verify wallet balance accuracy
    on Sep 17, 2025
  12. fjahr commented at 12:58 pm on September 18, 2025: contributor

    Just to make sure we’re on the same page — my understanding is that the idea is to:

    Use chunked backward rescans starting from the supplied birthdate (e.g. 1000-block increments), and stop as soon as the wallet balance matches the UTXO-set scan. This handles the common case where the birthdate is only off by a few days or weeks, so we don’t need to rescan the entire chain.

    Right, so for example if the supplied birthdate was blockheight 800,000 but there is a discrepancy, then, instead rescanning from 0 to 800k right away, scan 799k - 800k and check the balance, exit if it’s a match, continue with 798k - 799k and so on. This shouldn’t be too complex to implement since the rescanblockchain RPC already takes a start and an end height.

    At the same time, respect the pruneheight as the lower bound if pruning is enabled. That means we only scan back as far as blocks are available, and if the discrepancy still isn’t resolved at that point, we’d warn the user that a reindex is required.

    Yeah, there would be some kind of safe abort if the approach for walking backwards runs into unavailable blocks, just like rescanblockchain does as well.

  13. in src/wallet/rpc/coins.cpp:234 in 93baccaa35 outdated
    231                 {
    232                     {"dummy", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Remains for backward compatibility. Must be excluded or set to \"*\"."},
    233                     {"minconf", RPCArg::Type::NUM, RPCArg::Default{0}, "Only include transactions confirmed at least this many times."},
    234                     {"include_watchonly", RPCArg::Type::BOOL, RPCArg::Default{false}, "No longer used"},
    235                     {"avoid_reuse", RPCArg::Type::BOOL, RPCArg::Default{true}, "(only available if avoid_reuse wallet flag is set) Do not include balance in dirty outputs; addresses are considered dirty if they have previously been used in a transaction."},
    236+                    {"scan_utxoset", RPCArg::Type::BOOL, RPCArg::Default{false}, "If true, scan the UTXO set and return scanned UTXO balance alongside the trusted balance."},
    


    luke-jr commented at 10:31 pm on September 18, 2025:
    This shouldn’t be a positional parameter, at least.
  14. luke-jr commented at 10:32 pm on September 18, 2025: member

    I agree getbalance* is the wrong place for this kind of check.

    But it also will fail to detect incorrect birthdates if the TXOs are spent, so it can’t be relied on either…

  15. musaHaruna force-pushed on Sep 19, 2025
  16. musaHaruna force-pushed on Sep 19, 2025
  17. musaHaruna force-pushed on Sep 19, 2025
  18. musaHaruna renamed this:
    wallet/rpc: add scan_utxoset option to getbalance(s) to verify wallet balance accuracy
    wallet, rpc: add UTXO set check and incremental rescan to importdescriptors
    on Sep 19, 2025
  19. musaHaruna force-pushed on Sep 19, 2025
  20. musaHaruna force-pushed on Sep 19, 2025
  21. in src/wallet/rpc/backup.cpp:519 in 82a222b00d outdated
    514+        // Compare wallet trusted balance with chainstate-scanned spendable balance.
    515+        const auto bal = GetBalance(wallet);
    516+
    517+        CAmount utxo_scanned_balance = GetWalletUTXOSetBalance(wallet);
    518+
    519+    if (utxo_scanned_balance != bal.m_mine_trusted) {
    


    rkrux commented at 12:54 pm on November 11, 2025:

    In 82a222b00d6497dc22e85205dae577a1adf15989 “rpc: extend importdescriptors with UTXO check and incremental rescan”

    The lack of nesting in the if blocks makes the code harder to read.

      0diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp
      1index 4146a65876..cf8269b2ef 100644
      2--- a/src/wallet/rpc/backup.cpp
      3+++ b/src/wallet/rpc/backup.cpp
      4@@ -507,7 +507,6 @@ RPCHelpMan importdescriptors()
      5     bool have_prune_boundary = false;
      6     int64_t min_trial_start_time = 0;
      7 
      8-
      9     UniValue utxo_diff_obj;
     10 
     11     if (do_scan_utxoset && rescan) {
     12@@ -516,53 +515,53 @@ RPCHelpMan importdescriptors()
     13 
     14         CAmount utxo_scanned_balance = GetWalletUTXOSetBalance(wallet);
     15 
     16-    if (utxo_scanned_balance != bal.m_mine_trusted) {
     17-        // Incremental-rescan chunking parameters
     18-        const int chunk_blocks = 1000;
     19-        const int64_t avg_block_time = 600; // seconds per block (approx)
     20+        if (utxo_scanned_balance != bal.m_mine_trusted) {
     21+            // Incremental-rescan chunking parameters
     22+            const int chunk_blocks = 1000;
     23+            const int64_t avg_block_time = 600; // seconds per block (approx)
     24 
     25-        // Get tip time and height
     26-        int64_t tip_time = 0;
     27-        int tip_height = 0;
     28-        {
     29-        LOCK(pwallet->cs_wallet);
     30-        CHECK_NONFATAL(pwallet->chain().findBlock(pwallet->GetLastBlockHash(), FoundBlock().time(tip_time).height(tip_height)));
     31-        }
     32+            // Get tip time and height
     33+            int64_t tip_time = 0;
     34+            int tip_height = 0;
     35+            {
     36+                LOCK(pwallet->cs_wallet);
     37+                CHECK_NONFATAL(pwallet->chain().findBlock(pwallet->GetLastBlockHash(), FoundBlock().time(tip_time).height(tip_height)));
     38+            }
     39 
     40-        // If pruned, compute an approximate earliest start time based on prune height
     41-        bool is_pruned = pwallet->chain().havePruned();
     42-        std::optional<int> prune_height_opt = pwallet->chain().getPruneHeight();
     43-        have_prune_boundary = false;
     44-        min_trial_start_time = 0;
     45-        if (is_pruned && prune_height_opt.has_value()) {
     46-            const int prune_height = prune_height_opt.value();
     47-            int64_t blocks_diff = tip_height - prune_height;
     48-            if (blocks_diff < 0) blocks_diff = 0;
     49-            int64_t prune_time_est = tip_time - blocks_diff * avg_block_time;
     50-            if (prune_time_est < 0) prune_time_est = 0;
     51-            min_trial_start_time = prune_time_est;
     52-            have_prune_boundary = true;
     53-        }
     54+            // If pruned, compute an approximate earliest start time based on prune height
     55+            bool is_pruned = pwallet->chain().havePruned();
     56+            std::optional<int> prune_height_opt = pwallet->chain().getPruneHeight();
     57+            have_prune_boundary = false;
     58+            min_trial_start_time = 0;
     59+            if (is_pruned && prune_height_opt.has_value()) {
     60+                const int prune_height = prune_height_opt.value();
     61+                int64_t blocks_diff = tip_height - prune_height;
     62+                if (blocks_diff < 0) blocks_diff = 0;
     63+                int64_t prune_time_est = tip_time - blocks_diff * avg_block_time;
     64+                if (prune_time_est < 0) prune_time_est = 0;
     65+                min_trial_start_time = prune_time_est;
     66+                have_prune_boundary = true;
     67+            }
     68 
     69-        // Attempt incremental rescans using the helper defined above.
     70-        int out_chunks_tried = 0;
     71-        int64_t out_lowest_ts = 0;
     72-        UniValue early = IncrementalRescansNonOverlap(wallet, tip_time, tip_height, chunk_blocks, avg_block_time, have_prune_boundary,
     73-                                                        min_trial_start_time, utxo_scanned_balance, reserver, response, out_chunks_tried, out_lowest_ts);
     74+            // Attempt incremental rescans using the helper defined above.
     75+            int out_chunks_tried = 0;
     76+            int64_t out_lowest_ts = 0;
     77+            UniValue early = IncrementalRescansNonOverlap(wallet, tip_time, tip_height, chunk_blocks, avg_block_time, have_prune_boundary,
     78+                                                            min_trial_start_time, utxo_scanned_balance, reserver, response, out_chunks_tried, out_lowest_ts);
     79 
     80-        if (!early.isNull()) {
     81-            // Matched and response already annotated by helper.
     82-            return early;
     83-        }
     84+            if (!early.isNull()) {
     85+                // Matched and response already annotated by helper.
     86+                return early;
     87+            }
     88 
     89-        if (have_prune_boundary) {
     90-            // Set the fallback rescan start to the prune boundary (instead of 0)
     91-            lowest_timestamp = min_trial_start_time;
     92-        } else {
     93-            // Non-pruned node: incremental attempts scanned back to timestamp 0 (genesis)
     94-            lowest_timestamp = 0;
     95+            if (have_prune_boundary) {
     96+                // Set the fallback rescan start to the prune boundary (instead of 0)
     97+                lowest_timestamp = min_trial_start_time;
     98+            } else {
     99+                // Non-pruned node: incremental attempts scanned back to timestamp 0 (genesis)
    100+                lowest_timestamp = 0;
    101+            }
    102         }
    103-    }
    104 
    105     }
    106 
    

    musaHaruna commented at 11:26 am on November 22, 2025:
    Fixed.
  22. in src/wallet/rpc/backup.cpp:515 in 82a222b00d outdated
    508+    int64_t min_trial_start_time = 0;
    509+
    510+
    511+    UniValue utxo_diff_obj;
    512+
    513+    if (do_scan_utxoset && rescan) {
    


    rkrux commented at 1:08 pm on November 11, 2025:

    In https://github.com/bitcoin/bitcoin/commit/82a222b00d6497dc22e85205dae577a1adf15989 “rpc: extend importdescriptors with UTXO check and incremental rescan”

    Why is the UTXO set balance check (and the corresponding incremental block scan incase of balance mismatch) done before the usual rescan that’s supposed to be done upon a successful descriptor import? Should this not be done after the usual rescan to find any transactions in the older blocks that might have been missed in the rescan because of incorrect timestamp added by the user?


    musaHaruna commented at 11:27 am on November 22, 2025:
    The UTXO set check is performed before the main rescan to attempt incremental reconciliation of missing funds in small block chunks, potentially reducing the amount of blocks that need to be rescanned in the case of a large blockchain. Doing it after the main rescan would generally be redundant, because the rescan will already incorporate transactions missed due to incorrect timestamps, and any discrepancy with the UTXO set would likely have been resolved. The current order allows us to detect and fix large balance mismatches earlier in the RPC, without necessarily scanning the entire chain from genesis immediately.
  23. in src/wallet/rpc/backup.cpp:326 in fa3ac7378c outdated
    322@@ -323,6 +323,7 @@ RPCHelpMan importdescriptors()
    323                             },
    324                         },
    325                         RPCArgOptions{.oneline_description="requests"}},
    326+                        {"scan_utxoset", RPCArg::Type::BOOL, RPCArg::Default{false}, "If true, scans the UTXO set balance and compare with wallet balance and triggers incremental rescan if discrepency is found."}
    


    rkrux commented at 1:11 pm on November 11, 2025:

    In fa3ac7378ca016c598d51fa00a9cc02103754569 “wallet/rpc: add scan_utxoset arg & docs for importdescriptors”

    0s/"scan_utxoset"/"verify"
    

    From a user’s POV, this feels to me more like a verification step that the wallet might do if set by the user. Let’s just call it that to keep it simple for the user?


    rkrux commented at 1:43 pm on November 11, 2025:
    0s/discrepency/discrepancy
    

    musaHaruna commented at 11:28 am on November 22, 2025:
    Yeah I agree I have changed it to verify_balance I thought just verify is till a bit vague
  24. in test/functional/wallet_importdescriptors.py:104 in 1ed236e3c6 outdated
     99+        wallet_no_scan = node.get_wallet_rpc('watch_only_no_scan')
    100+        wallet_with_scan = node.get_wallet_rpc('watch_only_with_scan')
    101+
    102+        # Blank wallets don't have a birth time
    103+        assert 'birthtime' not in wallet_no_scan.getwalletinfo()
    104+        assert 'birthtime' not in wallet_with_scan.getwalletinfo()
    


    rkrux commented at 1:11 pm on November 11, 2025:

    In 1ed236e3c686582e35f20ab668cd6d93316fae2c “test: add functional test for importdescriptors scan_utxo flag”

    What’s the reason to assert this at only this stage?


    musaHaruna commented at 12:44 pm on November 22, 2025:
    This assertion ensures that the newly created watch-only wallets start from a completely clean state with no prior transaction history or birthtime. That way, we can be sure that any transactions discovered after the import come solely from the descriptor import and the UTXO scan, not from pre-existing wallet data.
  25. in src/wallet/rpc/backup.cpp:553 in 82a222b00d outdated
    548+        int out_chunks_tried = 0;
    549+        int64_t out_lowest_ts = 0;
    550+        UniValue early = IncrementalRescansNonOverlap(wallet, tip_time, tip_height, chunk_blocks, avg_block_time, have_prune_boundary,
    551+                                                        min_trial_start_time, utxo_scanned_balance, reserver, response, out_chunks_tried, out_lowest_ts);
    552+
    553+        if (!early.isNull()) {
    


    rkrux commented at 1:22 pm on November 11, 2025:

    In https://github.com/bitcoin/bitcoin/commit/82a222b00d6497dc22e85205dae577a1adf15989 “rpc: extend importdescriptors with UTXO check and incremental rescan”

    Shouldn’t the timestamp of the imported descriptors be updated as well in this case to avoid data inconsistency?


    musaHaruna commented at 11:45 am on November 22, 2025:
    Yeah, I think it should be, I will look into how I can do that, am open to suggestions on how that can be done.
  26. in src/wallet/rpc/backup.cpp:344 in fa3ac7378c outdated
    338@@ -338,21 +339,28 @@ RPCHelpMan importdescriptors()
    339                             {
    340                                 {RPCResult::Type::ELISION, "", "JSONRPC error"},
    341                             }},
    342+                            {RPCResult::Type::OBJ, "info", /*optional=*/true, "Optional informational fields. When present, this object will contain details about incremental rescans (examples below).",
    343+                            {
    344+                                {RPCResult::Type::STR, "utxo_check", /*optional=*/true, "Status of the UTXO check. Example values: 'matched' (wallet DB matches UTXO set)."},
    


    rkrux commented at 1:46 pm on November 11, 2025:

    In https://github.com/bitcoin/bitcoin/commit/fa3ac7378ca016c598d51fa00a9cc02103754569 “wallet/rpc: add scan_utxoset arg & docs for importdescriptors”

    This string property seems unnecessary if it is supposed to have only one value all the time.


    musaHaruna commented at 11:32 am on November 22, 2025:
    Fixed
  27. in src/wallet/rpc/backup.cpp:358 in fa3ac7378c outdated
    360 {
    361     std::shared_ptr<CWallet> const pwallet = GetWalletForJSONRPCRequest(main_request);
    362     if (!pwallet) return UniValue::VNULL;
    363     CWallet& wallet{*pwallet};
    364 
    365-    // Make sure the results are valid at least up to the most recent block
    


    rkrux commented at 1:51 pm on November 11, 2025:

    In https://github.com/bitcoin/bitcoin/commit/fa3ac7378ca016c598d51fa00a9cc02103754569 “wallet/rpc: add scan_utxoset arg & docs for importdescriptors”

    Why is this comment cut midway?


    musaHaruna commented at 12:47 pm on November 22, 2025:
    I think this is part of backupwallet RPC and I don’t think I touch the backupwallet rpc codenthe entire PR.
  28. in src/wallet/rpc/backup.cpp:346 in fa3ac7378c outdated
    338@@ -338,21 +339,28 @@ RPCHelpMan importdescriptors()
    339                             {
    340                                 {RPCResult::Type::ELISION, "", "JSONRPC error"},
    341                             }},
    342+                            {RPCResult::Type::OBJ, "info", /*optional=*/true, "Optional informational fields. When present, this object will contain details about incremental rescans (examples below).",
    343+                            {
    344+                                {RPCResult::Type::STR, "utxo_check", /*optional=*/true, "Status of the UTXO check. Example values: 'matched' (wallet DB matches UTXO set)."},
    345+                                {RPCResult::Type::NUM, "scanned_chunks", /*optional=*/true, "If incremental rescans were performed, the number of chunks scanned before a match was found."},
    346+                                {RPCResult::Type::NUM, "scanned_blocks", /*optional=*/true, "If incremental rescans were performed, the approximate number of blocks scanned (scanned_chunks * chunk_size)."},
    


    rkrux commented at 1:58 pm on November 11, 2025:

    In https://github.com/bitcoin/bitcoin/commit/fa3ac7378ca016c598d51fa00a9cc02103754569 “wallet/rpc: add scan_utxoset arg & docs for importdescriptors”

    I think these don’t need to be marked optional when the info object is already marked so. Unless these properties appear optionally inside the info object that I think is not the case.


    musaHaruna commented at 11:32 am on November 22, 2025:
    Fixed
  29. in src/wallet/receive.cpp:290 in ac06ddb8b9 outdated
    285+        for (const auto& script : spkm->GetScriptPubKeys()) {
    286+            if (has_privkeys) {
    287+                output_scripts_mine.emplace(script);
    288+            } else {
    289+                output_scripts_watchonly.emplace(script);
    290+            }
    


    rkrux commented at 2:03 pm on November 11, 2025:

    In ac06ddb8b9e33b7a594af9e38e00f2a8410947b7 “wallet: add GetWalletUTXOSetBalance to calculate balance from UTXO set”

    I don’t think the bifurcation between output_scripts_mine and output_scripts_watchonly needs to be done anymore because the watch only property in the descriptor wallets is at the wallet level now. Either the whole wallet will be watch-only or all of it will be not. Ref: #32618


    musaHaruna commented at 11:32 am on November 22, 2025:
    Fixed
  30. in src/wallet/rpc/backup.cpp:512 in 82a222b00d outdated
    505     }
    506 
    507+    bool have_prune_boundary = false;
    508+    int64_t min_trial_start_time = 0;
    509+
    510+
    


    rkrux commented at 2:08 pm on November 11, 2025:

    In https://github.com/bitcoin/bitcoin/commit/82a222b00d6497dc22e85205dae577a1adf15989 “rpc: extend importdescriptors with UTXO check and incremental rescan”

    Can early return from the function here in case rescan is false.


    musaHaruna commented at 11:33 am on November 22, 2025:
    Fixed. Thank you so much for reviewing the code!!!
  31. rkrux commented at 2:27 pm on November 11, 2025: contributor

    Concept ACK and cursory review 1ed236e3c686582e35f20ab668cd6d93316fae2c.

    The intent of the PR and the corresponding issue #28898 seems fine to me.

  32. wallet: add GetWalletUTXOSetBalance to calculate balance from UTXO set
    Introduce a new helper `GetWalletUTXOSetBalance(const CWallet&)` that
    derives the wallet’s balance directly from the UTXO set rather than from
    the wallet’s transaction history.
    
    Usefull for verifying wallet balance correctness against the
    chainstate without requiring a full rescan.
    6684e3d4e9
  33. wallet/rpc: add scan_utxoset arg & docs for importdescriptors
    Add a new optional boolean RPC argument `scan_utxoset` to
    importdescriptors and document the response fields used when the
    scan_utxoset check is enabled.
    782a159815
  34. wallet: extend RescanFromTime with optional endTime to limit rescan range
    Add a new overload of CWallet::RescanFromTime that accepts an optional
    endTime parameter. When provided, the method finds the approximate end
    height and calls ScanForWalletTransactions with a bounded [start, end]
    range. This makes incremental or windowed rescans possible while preserving
    existing behavior when no endTime is specified.
    87422b337c
  35. musaHaruna force-pushed on Nov 22, 2025
  36. rpc: extend importdescriptors with UTXO check and incremental rescan
    Extend the importdescriptors RPC with an optional `scan_utxoset` parameter.
    When enabled, the wallet balance is compared against a balance derived from
    the UTXO set. If a discrepancy is found, the wallet attempts incremental,
    non-overlapping rescans in fixed-size block chunks until the balances match,
    allowing for faster recovery in many cases. When a match is detected, the
    RPC returns early with an `info` object describing the UTXO check and rescan
    progress. If no match is found, the code falls back to the existing full
    rescan behavior starting from the earliest descriptor timestamp (or the
    prune boundary if applicable). Responses are updated to include optional
    `info` fields with `utxo_check`, `scanned_chunks`, and `scanned_blocks`
    metadata.
    0ef7985413
  37. test: add functional test for importdescriptors scan_utxo flag
    Add a functional test that verifies `importdescriptors`
    behavior with and without `scan_utxoset`, ensuring wallets correctly detect
    UTXOs and transactions depending on the timestamp used. The test covers both
    `timestamp="now"` (no history vs. UTXO set scan) and accurate historical timestamps
    (both imports discover full history), and also asserts returned metadata fields
    (`success`, `utxo_check`, `scanned_chunks`, `scanned_blocks`).
    3e42097ded
  38. musaHaruna force-pushed on Nov 22, 2025
  39. DrahtBot added the label CI failed on Nov 22, 2025
  40. DrahtBot commented at 11:43 am on November 22, 2025: contributor

    🚧 At least one of the CI tasks failed. Task Windows-cross to x86_64: https://github.com/bitcoin/bitcoin/actions/runs/19594797010/job/56117977131 LLM reason (✨ experimental): Compilation failed: wallet::CWallet::ResubmitWalletTransactions is called with a bool where a node::TxBroadcast enum is required.

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

  41. DrahtBot removed the label CI failed on Nov 22, 2025

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: 2025-12-02 18:12 UTC

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