Skip to content

Conversation

@musaHaruna
Copy link
Contributor

@musaHaruna musaHaruna commented Sep 15, 2025

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

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.
@DrahtBot
Copy link
Contributor

DrahtBot commented Sep 15, 2025

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)
  • #34049 (rpc: Disallow captures in RPCMethodImpl 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)
  • #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 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

@fjahr
Copy link
Contributor

fjahr commented Sep 16, 2025

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.

@musaHaruna
Copy link
Contributor Author

musaHaruna commented Sep 16, 2025

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.

@musaHaruna
Copy link
Contributor Author

musaHaruna commented Sep 16, 2025

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.

@fjahr
Copy link
Contributor

fjahr commented Sep 16, 2025

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.

@musaHaruna
Copy link
Contributor Author

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.

@musaHaruna musaHaruna changed the title 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 Sep 17, 2025
@fjahr
Copy link
Contributor

fjahr commented Sep 18, 2025

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.

Copy link
Member

@luke-jr luke-jr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

{
{"dummy", RPCArg::Type::STR, RPCArg::Optional::OMITTED, "Remains for backward compatibility. Must be excluded or set to \"*\"."},
{"minconf", RPCArg::Type::NUM, RPCArg::Default{0}, "Only include transactions confirmed at least this many times."},
{"include_watchonly", RPCArg::Type::BOOL, RPCArg::Default{false}, "No longer used"},
{"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."},
{"scan_utxoset", RPCArg::Type::BOOL, RPCArg::Default{false}, "If true, scan the UTXO set and return scanned UTXO balance alongside the trusted balance."},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be a positional parameter, at least.

@musaHaruna musaHaruna force-pushed the feature/scan-utxoset-balance-check branch 3 times, most recently from 3e5ead7 to 1c23d97 Compare September 19, 2025 14:39
@musaHaruna musaHaruna changed the title 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 Sep 19, 2025
@musaHaruna musaHaruna force-pushed the feature/scan-utxoset-balance-check branch 2 times, most recently from e45f79b to 82a222b Compare September 19, 2025 15:24
Copy link
Contributor

@rkrux rkrux left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK and cursory review 1ed236e.

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


CAmount utxo_scanned_balance = GetWalletUTXOSetBalance(wallet);

if (utxo_scanned_balance != bal.m_mine_trusted) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 82a222b "rpc: extend importdescriptors with UTXO check and incremental rescan"

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

diff --git a/src/wallet/rpc/backup.cpp b/src/wallet/rpc/backup.cpp
index 4146a65876..cf8269b2ef 100644
--- a/src/wallet/rpc/backup.cpp
+++ b/src/wallet/rpc/backup.cpp
@@ -507,7 +507,6 @@ RPCHelpMan importdescriptors()
     bool have_prune_boundary = false;
     int64_t min_trial_start_time = 0;
 
-
     UniValue utxo_diff_obj;
 
     if (do_scan_utxoset && rescan) {
@@ -516,53 +515,53 @@ RPCHelpMan importdescriptors()
 
         CAmount utxo_scanned_balance = GetWalletUTXOSetBalance(wallet);
 
-    if (utxo_scanned_balance != bal.m_mine_trusted) {
-        // Incremental-rescan chunking parameters
-        const int chunk_blocks = 1000;
-        const int64_t avg_block_time = 600; // seconds per block (approx)
+        if (utxo_scanned_balance != bal.m_mine_trusted) {
+            // Incremental-rescan chunking parameters
+            const int chunk_blocks = 1000;
+            const int64_t avg_block_time = 600; // seconds per block (approx)
 
-        // Get tip time and height
-        int64_t tip_time = 0;
-        int tip_height = 0;
-        {
-        LOCK(pwallet->cs_wallet);
-        CHECK_NONFATAL(pwallet->chain().findBlock(pwallet->GetLastBlockHash(), FoundBlock().time(tip_time).height(tip_height)));
-        }
+            // Get tip time and height
+            int64_t tip_time = 0;
+            int tip_height = 0;
+            {
+                LOCK(pwallet->cs_wallet);
+                CHECK_NONFATAL(pwallet->chain().findBlock(pwallet->GetLastBlockHash(), FoundBlock().time(tip_time).height(tip_height)));
+            }
 
-        // If pruned, compute an approximate earliest start time based on prune height
-        bool is_pruned = pwallet->chain().havePruned();
-        std::optional<int> prune_height_opt = pwallet->chain().getPruneHeight();
-        have_prune_boundary = false;
-        min_trial_start_time = 0;
-        if (is_pruned && prune_height_opt.has_value()) {
-            const int prune_height = prune_height_opt.value();
-            int64_t blocks_diff = tip_height - prune_height;
-            if (blocks_diff < 0) blocks_diff = 0;
-            int64_t prune_time_est = tip_time - blocks_diff * avg_block_time;
-            if (prune_time_est < 0) prune_time_est = 0;
-            min_trial_start_time = prune_time_est;
-            have_prune_boundary = true;
-        }
+            // If pruned, compute an approximate earliest start time based on prune height
+            bool is_pruned = pwallet->chain().havePruned();
+            std::optional<int> prune_height_opt = pwallet->chain().getPruneHeight();
+            have_prune_boundary = false;
+            min_trial_start_time = 0;
+            if (is_pruned && prune_height_opt.has_value()) {
+                const int prune_height = prune_height_opt.value();
+                int64_t blocks_diff = tip_height - prune_height;
+                if (blocks_diff < 0) blocks_diff = 0;
+                int64_t prune_time_est = tip_time - blocks_diff * avg_block_time;
+                if (prune_time_est < 0) prune_time_est = 0;
+                min_trial_start_time = prune_time_est;
+                have_prune_boundary = true;
+            }
 
-        // Attempt incremental rescans using the helper defined above.
-        int out_chunks_tried = 0;
-        int64_t out_lowest_ts = 0;
-        UniValue early = IncrementalRescansNonOverlap(wallet, tip_time, tip_height, chunk_blocks, avg_block_time, have_prune_boundary,
-                                                        min_trial_start_time, utxo_scanned_balance, reserver, response, out_chunks_tried, out_lowest_ts);
+            // Attempt incremental rescans using the helper defined above.
+            int out_chunks_tried = 0;
+            int64_t out_lowest_ts = 0;
+            UniValue early = IncrementalRescansNonOverlap(wallet, tip_time, tip_height, chunk_blocks, avg_block_time, have_prune_boundary,
+                                                            min_trial_start_time, utxo_scanned_balance, reserver, response, out_chunks_tried, out_lowest_ts);
 
-        if (!early.isNull()) {
-            // Matched and response already annotated by helper.
-            return early;
-        }
+            if (!early.isNull()) {
+                // Matched and response already annotated by helper.
+                return early;
+            }
 
-        if (have_prune_boundary) {
-            // Set the fallback rescan start to the prune boundary (instead of 0)
-            lowest_timestamp = min_trial_start_time;
-        } else {
-            // Non-pruned node: incremental attempts scanned back to timestamp 0 (genesis)
-            lowest_timestamp = 0;
+            if (have_prune_boundary) {
+                // Set the fallback rescan start to the prune boundary (instead of 0)
+                lowest_timestamp = min_trial_start_time;
+            } else {
+                // Non-pruned node: incremental attempts scanned back to timestamp 0 (genesis)
+                lowest_timestamp = 0;
+            }
         }
-    }
 
     }
 

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.


UniValue utxo_diff_obj;

if (do_scan_utxoset && rescan) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 82a222b "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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

},
},
RPCArgOptions{.oneline_description="requests"}},
{"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."}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fa3ac73 "wallet/rpc: add scan_utxoset arg & docs for importdescriptors"

s/"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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/discrepency/discrepancy

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree I have changed it to verify_balance I thought just verify is till a bit vague

Comment on lines +102 to +104
# Blank wallets don't have a birth time
assert 'birthtime' not in wallet_no_scan.getwalletinfo()
assert 'birthtime' not in wallet_with_scan.getwalletinfo()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 1ed236e "test: add functional test for importdescriptors scan_utxo flag"

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

UniValue early = IncrementalRescansNonOverlap(wallet, tip_time, tip_height, chunk_blocks, avg_block_time, have_prune_boundary,
min_trial_start_time, utxo_scanned_balance, reserver, response, out_chunks_tried, out_lowest_ts);

if (!early.isNull()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 82a222b "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?

Copy link
Contributor Author

@musaHaruna musaHaruna Nov 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

}},
{RPCResult::Type::OBJ, "info", /*optional=*/true, "Optional informational fields. When present, this object will contain details about incremental rescans (examples below).",
{
{RPCResult::Type::STR, "utxo_check", /*optional=*/true, "Status of the UTXO check. Example values: 'matched' (wallet DB matches UTXO set)."},
Copy link
Contributor

@rkrux rkrux Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fa3ac73 "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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

if (!pwallet) return UniValue::VNULL;
CWallet& wallet{*pwallet};

// Make sure the results are valid at least up to the most recent block
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fa3ac73 "wallet/rpc: add scan_utxoset arg & docs for importdescriptors"

Why is this comment cut midway?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is part of backupwallet RPC and I don't think I touch the backupwallet rpc codenthe entire PR.

Comment on lines 345 to 346
{RPCResult::Type::NUM, "scanned_chunks", /*optional=*/true, "If incremental rescans were performed, the number of chunks scanned before a match was found."},
{RPCResult::Type::NUM, "scanned_blocks", /*optional=*/true, "If incremental rescans were performed, the approximate number of blocks scanned (scanned_chunks * chunk_size)."},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fa3ac73 "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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment on lines 284 to 290
bool has_privkeys = spkm->HavePrivateKeys();
for (const auto& script : spkm->GetScriptPubKeys()) {
if (has_privkeys) {
output_scripts_mine.emplace(script);
} else {
output_scripts_watchonly.emplace(script);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In ac06ddb "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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

bool have_prune_boundary = false;
int64_t min_trial_start_time = 0;


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In 82a222b "rpc: extend importdescriptors with UTXO check and incremental rescan"

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

Copy link
Contributor Author

@musaHaruna musaHaruna Nov 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.
Thank you so much for reviewing the code!!!

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.
Add a new optional boolean RPC argument `scan_utxoset` to
importdescriptors and document the response fields used when the
scan_utxoset check is enabled.
…ange

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.
@musaHaruna musaHaruna force-pushed the feature/scan-utxoset-balance-check branch from 1ed236e to 68d4cfb Compare November 22, 2025 11:24
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.
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`).
@musaHaruna musaHaruna force-pushed the feature/scan-utxoset-balance-check branch from 68d4cfb to 3e42097 Compare November 22, 2025 11:42
@DrahtBot
Copy link
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.

Hints

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.

@DrahtBot
Copy link
Contributor

DrahtBot commented Jan 3, 2026

🐙 This pull request conflicts with the target branch and needs rebase.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

wallet RPC to double-check the calculated balance

5 participants