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 +378 −7
  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:

    verify_balance a bool which is false by defualt.

    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 verify_balance is used, they are:

    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.

    Implementation:

    • Add FindCoinsByScript() for scanning UTXO set for coins belonging to output_scripts and expose it to node interface through findCoinsByScript().
    • Add GetWalletUTXOSetBalance() which calculates the wallet’s total spendable balance by summing all UTXOs in the blockchain’s UTXO set that belong to the wallet’s scriptPubKeys, ignoring immature coinbase outputs.
    • Add std::optional<int64_t> endTime to RescanFromTime() which lets the rescan stop at a specific block height, limiting the scan to a defined time range instead of scanning from startTime all the way to the blockchain tip.
    • Add IncrementalRescan() this function scans the wallet’s transaction history in chunks of blocks from tip backwards to try and reconcile the wallet’s trusted balance with the actual UTXO-set balance, stopping early if a chunk produces a balance that matches the target. It is used only when there’s a discrepancy between the wallet’s trusted balance and the UTXO-set balance, performing an incremental rescan to identify when the balances align and recording how many chunks were scanned.

    importescriptors with incremental example

    0bitcoin-cli importdescriptors '[{
    1    "desc": "wpkh([f5b1c3d7/84h/0h/0h]xpub6CUGRUonZSQ4TWtTMmzXdrXDtypWKiKpJrZT4S9vPz4ZZ6f4a1V4f3ChFy8hRk4M6mh4J4LR6WcJbNf2oyMFGmQH6Eo1QvLKMjR18q3fTq5/0/*)#abcd1234",
    2    "timestamp": "now"
    3}]' true
    

    Example Output

    0[
    1  {
    2    "info": {
    3      "utxo_check": true,
    4      "scanned_blocks": 2000,
    5      "scanned_chunks": 2
    6    }
    7  }
    8]
    

    Benchmarks Here’s a benchmark result for normal rescan and incremental rescan as suggested by fjahr.

    ns/op op/s err% total benchmark
    13,481.56 74,175.41 0.9% 0.01 BenchmarkIncrementalRescans
    13,450.49 74,346.72 1.4% 0.01 BenchmarkIncrementalRescans
    13,468.04 74,249.85 1.5% 0.01 BenchmarkIncrementalRescans
    2,274,875.00 439.58 1.7% 0.03 BenchmarkRescanFull
    2,301,375.00 434.52 1.4% 0.03 BenchmarkRescanFull
    2,306,334.00 433.59 3.0% 0.03 BenchmarkRescanFull

    Here’s the code for the benchmark. Not 100 percent sure if I wrote it correctly. It’s my first time writting benchmark, took inspiration from how src/bench/wallet_balance.cpp was written.

    A big thanks to fjahr for suggesting this approach comment

  2. DrahtBot added the label RPC/REST/ZMQ on Sep 15, 2025
  3. 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 copy-paste <!–meta-tag:bot-skip–> into the comment that the bot should ignore.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #bitcoin-core/gui/911 (Adds non-mempool wallet balance to overview by ajtowns)
    • #34049 (rpc: Disallow captures in RPCMethodImpl by ajtowns)
    • #33671 (wallet: Add separate balance info for non-mempool wallet txs by ajtowns)
    • #32861 (Have createwalletdescriptor auto-detect an unused(KEY) by Sjors)
    • #32784 (wallet: derivehdkey RPC to get xpub at arbitrary path by Sjors)
    • #31668 (Added rescan option for import descriptors by saikiran57)
    • #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 the output_scripts [preposition “from” is incorrect here; “for” makes the intention clear]
    • “If true, scans the UTXO set balance and compare with wallet balance and triggers incremental rescan if discrepancy is found.” -> “If true, scans the UTXO set balance and compares with wallet balance and triggers incremental rescan if a discrepancy is found.” [“compare” should be “compares” to agree with singular subject; added “a” before “discrepancy” for clarity]
    • “# Mine 1000 total 1000 more blocksthen send the second tx” -> “# Mine 1000 total, 1000 more blocks, then send the second tx” [“blocksthen” is a typo; needs separation (“blocks, then”) to be comprehensible]

    2026-01-24 06:33:29

  4. musaHaruna force-pushed on Sep 16, 2025
  5. 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.
  6. 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.

  7. 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.

  8. 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.

  9. 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.

  10. 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
  11. 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.

  12. 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.
  13. 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…

  14. musaHaruna force-pushed on Sep 19, 2025
  15. musaHaruna force-pushed on Sep 19, 2025
  16. musaHaruna force-pushed on Sep 19, 2025
  17. 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
  18. musaHaruna force-pushed on Sep 19, 2025
  19. musaHaruna force-pushed on Sep 19, 2025
  20. 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.
  21. in src/wallet/rpc/backup.cpp:516 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.
  22. 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
  23. in test/functional/wallet_importdescriptors.py:106 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.
  24. 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.
  25. 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
  26. in src/wallet/rpc/backup.cpp:363 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 code in the entire PR.
  27. 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
  28. 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
  29. in src/wallet/rpc/backup.cpp:511 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!!!
  30. 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.

  31. musaHaruna force-pushed on Nov 22, 2025
  32. musaHaruna force-pushed on Nov 22, 2025
  33. DrahtBot added the label CI failed on Nov 22, 2025
  34. 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.

  35. DrahtBot removed the label CI failed on Nov 22, 2025
  36. DrahtBot added the label Needs rebase on Jan 3, 2026
  37. musaHaruna force-pushed on Jan 23, 2026
  38. musaHaruna force-pushed on Jan 23, 2026
  39. DrahtBot added the label CI failed on Jan 23, 2026
  40. DrahtBot commented at 11:30 pm on January 23, 2026: contributor

    🚧 At least one of the CI tasks failed. Task lint: https://github.com/bitcoin/bitcoin/actions/runs/21304319223/job/61328911155 LLM reason (✨ experimental): Trailing whitespace detected by the lint check, causing the CI to fail.

    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. node: add and implement `FindCoinsByScript()` for UTXO-set scan by script
    Add `FindCoinsByScript()` which implements a  DB-backed scan
    that iterates the on-disk UTXO DB with a `CCoinViewCursor` and wire it into the node
    interface as `Node::getCoinsByScript()`.
    69c06c8d11
  42. musaHaruna force-pushed on Jan 23, 2026
  43. musaHaruna force-pushed on Jan 23, 2026
  44. musaHaruna force-pushed on Jan 23, 2026
  45. musaHaruna commented at 0:03 am on January 24, 2026: contributor

    Rebased and resolves conflict with 33135 in test/functional/wallet_importdescriptor.

    Also made some improvments to incremental rescan code to make it more cleaner and readable than the previous version.

    Also updated the PR description with benchmarks for normal rescan and incremental rescan as suggested by @fjahr. @rkrux Do you mean overwrite the original timestamp?

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

  46. DrahtBot removed the label Needs rebase on Jan 24, 2026
  47. 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.
    19f45ffa26
  48. wallet/rpc: add `verify_balance` arg & docs for importdescriptors
    Add a new optional boolean RPC argument `verify_balance` to
    importdescriptors and document the response fields used when the
    `verify_balance` check is enabled.
    e87e518e6b
  49. wallet: extend` RescanFromTime()` with optional endTime to limit rescan range
    Add an optional endTime parameter to `CWallet::RescanFromTime()`.
    When provided, the method finds the approximate end height and calls
    `ScanForWalletTransactions()` with a bounded [start, end] range.
    This makes incremental rescans possible while preserving existing
    behavior when no endTime is specified.
    005b545f0b
  50. 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.
    6137619f17
  51. test: add importdescriptors `verify_balance` flag behavior 363a240b0c
  52. musaHaruna force-pushed on Jan 24, 2026
  53. DrahtBot removed the label CI failed on Jan 24, 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-02-01 15:13 UTC

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