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 throughfindCoinsByScript(). - 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> endTimetoRescanFromTime()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