{
 "number": 36245,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36245",
 "title": "wallet: start descriptor top up at range_start instead of index 0",
 "author": "kriss39",
 "author_association": "CONTRIBUTOR",
 "created_at": "2026-09-14T08:34:00Z",
 "updated_at": "2026-09-15T01:48:50Z",
 "age_days": 3,
 "draft": false,
 "labels": [
  "Wallet"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "4d2dc3c327fe5293e481b2a80805adfeb16ba642",
 "head_ref": "fix-topup-cache-from-range-start",
 "head_repo": "kriss39/bitcoin",
 "head_history": [],
 "additions": 43,
 "deletions": 3,
 "changed_files": 2,
 "commit_count": 1,
 "size_bucket": "S",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {},
   "conflicts": [
    {
     "number": 35989,
     "title": "wallet: fix crash on importdescriptors with a range ending at 2^31-1",
     "author": "shuv-amp"
    }
   ]
  }
 },
 "acks_parsed": {},
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 0,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": []
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-09-14T08:22:45Z",
  "last_reviewer_activity": null,
  "last_reviewer": null,
  "author_silent_days": 3,
  "waiting_on_author_days": 0,
  "days_since_update": 2
 },
 "refs": {
  "mentioned": [],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [],
  "conflicts": [
   35989
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [],
 "body": "DescriptorScriptPubKeyMan::TopUpWithDB() expands the descriptor from m_max_cached_index + 1, which is 0 for a freshly imported descriptor regardless of its range_start. Importing a descriptor with \"range\": [N, M] therefore derives and watches every index in [0, M), so the cost of an import is O(M) rather than O(M - N) and the wallet treats scriptPubKeys below N as its own. Load() only expands [range_start, range_end), so those scriptPubKeys stop being IsMine the next time the wallet is opened, and a transaction picked up for one of them during the import rescan is no longer credited to the wallet.\n\nSince ParseDescriptorRange() only bounds the size of the range and not its start, \"range\": [2146483648, 2147483647] is accepted and expands 2^31 indexes into m_map_script_pub_keys before the RPC returns.\n\nStart the top up loop at range_start and record the index that was actually cached instead of counting from -1, so that Load() and TopUpWithDB() agree on m_max_cached_index for descriptors with a non-zero range_start. Before this change Load() left m_max_cached_index at range_end - range_start - 1 for such descriptors, which made the first top up after opening the wallet re-expand the stored range.\n\nImporting wpkh(xpub/0/*) with \"range\": [600000, 600009] into a blank watch-only wallet goes from 19.5s to 0.09s on this machine.",
 "commits": [
  {
   "sha": "4d2dc3c327fe5293e481b2a80805adfeb16ba642",
   "date": "2026-09-14T08:22:45Z",
   "message": "wallet: start descriptor top up at range_start instead of index 0\n\nDescriptorScriptPubKeyMan::TopUpWithDB() expands the descriptor from\nm_max_cached_index + 1, which is 0 for a freshly imported descriptor\nregardless of its range_start. Importing a descriptor with\n\"range\": [N, M] therefore derives and watches every index in [0, M),\nso the cost of an import is O(M) rather than O(M - N) and the wallet\ntreats scriptPubKeys below N as its own. Load() only expands\n[range_start, range_end), so those scriptPubKeys stop being IsMine the\nnext time the wallet is opened, and a transaction picked up for one of\nthem during the import rescan is no longer credited to the wallet.\n\nSince ParseDescriptorRange() only bounds the size of the range and not\nits start, \"range\": [2146483648, 2147483647] is accepted and expands\n2^31 indexes into m_map_script_pub_keys before the RPC returns.\n\nStart the top up loop at range_start and record the index that was\nactually cached instead of counting from -1, so that Load() and\nTopUpWithDB() agree on m_max_cached_index for descriptors with a\nnon-zero range_start. Before this change Load() left m_max_cached_index\nat range_end - range_start - 1 for such descriptors, which made the\nfirst top up after opening the wallet re-expand the stored range.\n\nImporting wpkh(xpub/0/*) with \"range\": [600000, 600009] into a blank\nwatch-only wallet goes from 19.5s to 0.09s on this machine."
  }
 ],
 "timeline": [],
 "labels_log": [
  {
   "t": "2026-09-14T08:34:04Z",
   "action": "labeled",
   "label": "Wallet",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 2817,
 "text_tokens_estimate": 704,
 "changed_paths": [
  "src/wallet/scriptpubkeyman.cpp",
  "test/functional/wallet_importdescriptors.py"
 ],
 "files": [
  {
   "path": "src/wallet/scriptpubkeyman.cpp",
   "add": 6,
   "del": 3
  },
  {
   "path": "test/functional/wallet_importdescriptors.py",
   "add": 37,
   "del": 0
  }
 ],
 "test_lines": 37,
 "git": {
  "head": "4d2dc3c327fe5293e481b2a80805adfeb16ba642",
  "head_matches_backup": true,
  "base": "d25470d992b72b1960729478e677473f5bf860a8",
  "commits": [
   {
    "sha": "4d2dc3c327",
    "subject": "wallet: start descriptor top up at range_start instead of index 0",
    "files": 2,
    "add": 43,
    "del": 3
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "c7aa96a297924cea",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}