{
 "number": 34400,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/34400",
 "title": "wallet: parallel fast rescan (approx 8x speed up with 8 threads)",
 "author": "Eunovo",
 "author_association": "CONTRIBUTOR",
 "created_at": "2026-01-24T19:46:16Z",
 "updated_at": "2026-08-26T14:44:38Z",
 "age_days": 235,
 "draft": true,
 "labels": [
  "Wallet",
  "Needs rebase"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "ae888e0dc9f3c660586887596eb06eb0e2560aaf",
 "head_ref": "new-rescan",
 "head_repo": "Eunovo/bitcoin",
 "head_history": [
  {
   "t": "2026-01-25T00:16:03Z",
   "sha": "ef847e8bcee3d7e09051eaa956ce8086556a97f0"
  },
  {
   "t": "2026-02-08T12:28:55Z",
   "sha": "49ae44cefe695b8d107105f5ae444a57566ef9db"
  },
  {
   "t": "2026-02-11T17:19:30Z",
   "sha": "2f23481b72bb78c612071925ed5a791669d54dfe"
  },
  {
   "t": "2026-02-26T16:14:36Z",
   "sha": "48154b87e2fb303ca0f3d46da29a3fbbf8758a06"
  },
  {
   "t": "2026-03-18T12:42:36Z",
   "sha": "32609391d5d2ed4f9a88bd8d4821515ab146f804"
  },
  {
   "t": "2026-04-06T08:31:09Z",
   "sha": "22fed002ea5312b9844fab85ebc417dd81535c27"
  },
  {
   "t": "2026-04-12T17:59:08Z",
   "sha": "587d48386e8fd45a7305dffb2b9d2f603c450a82"
  },
  {
   "t": "2026-04-12T18:56:58Z",
   "sha": "6343fba2b34caeaae2e61de9409b536867431b7a"
  },
  {
   "t": "2026-04-12T19:16:30Z",
   "sha": "a733867087a64269046a22548402cb8ba8f79c34"
  },
  {
   "t": "2026-04-12T21:27:46Z",
   "sha": "87b8f03858ee98465696e57c6ae3a4382359c03c"
  },
  {
   "t": "2026-04-14T12:48:26Z",
   "sha": "b62e9e739113fe58a86d23a00f8e9b1b561cd29f"
  },
  {
   "t": "2026-07-14T07:56:38Z",
   "sha": "29c94bc77249c35833dd41ea8b533f15830da58c"
  },
  {
   "t": "2026-07-14T13:41:21Z",
   "sha": "b92a665d87f3068a3f102194a351d4198b9fe61a"
  },
  {
   "t": "2026-07-21T10:05:44Z",
   "sha": "061ceb14721bf82443de299e95886168f4e6feb0"
  },
  {
   "t": "2026-07-21T15:00:18Z",
   "sha": "bf45a678411f000b60a0d8f20363e5e0c2bc64bf"
  },
  {
   "t": "2026-07-22T20:12:08Z",
   "sha": "ae888e0dc9f3c660586887596eb06eb0e2560aaf"
  }
 ],
 "additions": 1451,
 "deletions": 388,
 "changed_files": 18,
 "commit_count": 15,
 "size_bucket": "XL",
 "mergeable_state": "dirty",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "w0xlt",
      "url": "https://github.com/bitcoin/bitcoin/pull/34400#issuecomment-3938433282"
     },
     {
      "login": "ismaelsadeeq",
      "url": "https://github.com/bitcoin/bitcoin/pull/34400#pullrequestreview-3944480721"
     },
     {
      "login": "rkrux",
      "url": "https://github.com/bitcoin/bitcoin/pull/34400#issuecomment-5427024289"
     }
    ]
   },
   "conflicts": [
    {
     "number": 35901,
     "title": "wallet: Fix ScanForWalletTransactions missing tx when look-ahead pool expands mid-block",
     "author": "pablomartin4btc"
    },
    {
     "number": 35852,
     "title": "scripted-diff: Use inline const(expr) over static constexpr in headers",
     "author": "maflcko"
    },
    {
     "number": 35752,
     "title": "wallet: make encryption state updates atomic",
     "author": "l0rinc"
    },
    {
     "number": 35716,
     "title": "wallet: Replace mapWallet and wtxOrdered with a boost::multi_index",
     "author": "achow101"
    },
    {
     "number": 34909,
     "title": "wallet, refactor: modularise wallet by extracting out legacy wallet migration",
     "author": "rkrux"
    },
    {
     "number": 34907,
     "title": "wallet, test: make wallet_fast_rescan robust",
     "author": "rkrux"
    },
    {
     "number": 34861,
     "title": "wallet: Add importdescriptors interface",
     "author": "polespinasa"
    },
    {
     "number": 34681,
     "title": "wallet: move rescan logic into ChainScanner and wallet/scan",
     "author": "Eunovo"
    },
    {
     "number": 32857,
     "title": "wallet: allow skipping script paths",
     "author": "Sjors"
    },
    {
     "number": 31668,
     "title": "Added rescan option for import descriptors",
     "author": "saikiran57"
    },
    {
     "number": 30343,
     "title": "wallet, logging: Replace WalletLogPrintf() with LogInfo()",
     "author": "ryanofsky"
    },
    {
     "number": 29278,
     "title": "Wallet:  Add `maxfeerate` wallet startup option",
     "author": "ismaelsadeeq"
    },
    {
     "number": 27865,
     "title": "wallet: Track no-longer-spendable TXOs separately",
     "author": "achow101"
    }
   ]
  }
 },
 "acks_parsed": {
  "w0xlt": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-02-21T08:12:16Z",
   "stale": false
  },
  "ismaelsadeeq": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-03-13T14:34:53Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 2,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "bvbfan",
   "furszy",
   "ismaelsadeeq",
   "l0rinc",
   "luke-jr",
   "rkrux",
   "w0xlt"
  ]
 },
 "signals": {
  "needs_rebase": true,
  "ci_failed": false,
  "mergeable_state": "dirty",
  "last_author_activity": "2026-07-22T20:12:08Z",
  "last_reviewer_activity": "2026-08-26T14:44:34Z",
  "last_reviewer": "rkrux",
  "author_silent_days": 56,
  "waiting_on_author_days": 22,
  "days_since_update": 22
 },
 "refs": {
  "mentioned": [
   34681
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 34681,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-09-14",
    "title": "wallet: move rescan logic into ChainScanner and wallet/scan"
   }
  ],
  "conflicts": [
   35901,
   35852,
   35752,
   35716,
   34909,
   34907,
   34861,
   34681,
   32857,
   31668,
   30343,
   29278,
   27865
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/wallet/scan.cpp"
 ],
 "body": "This PR speeds up wallet fast-rescan by executing the filter checks in parallel while ensuring that the filters are updated properly so that no output scripts are missed.  Benchmarks, outlined below, show considerable improvement that tapers off at around `8x` speedup at `8` threads.\n\n### Prerequisite PRs\n- [x] https://github.com/bitcoin/bitcoin/pull/34667 - modify the fast-rescan test to ensure that it fails when the filter is not updated properly.\n- [ ] https://github.com/bitcoin/bitcoin/pull/34681 - refactor `CWallet::ScanForWalletTransactions` to prepare for the work in this PR.\n\n### Benchmarks\n**NOTE: to reproduce, please tune your system with `pyperf system tune`**\n\n_EDIT\nSet up your node to use block filters by setting `blockfilterindex=1` in your bitcoin.conf file and ensure your blockfilterindex is synced to the tip before attempting to reproduce._\n\nUsing the following command on mainnet with a wallet with no scripts and hyperfine version `1.20.0`:\n- On master\n```\nhyperfine --show-output --export-markdown master.md --export-json master.json  \\\n--sort command \\\n--runs 3 \\\n--prepare 'cmake --build build -j 20 && build/bin/bitcoind -blockfilterindex=1 -networkactive=0 && sleep 10 && build/bin/bitcoin-cli loadwalllet <wallet-name>' \\\n--conclude 'build/bin/bitcoin-cli stop && sleep 10' \\\n'build/bin/bitcoin-cli rescanblockchain 700000 900000'\n```\n\n- On this PR:\n```\nhyperfine --show-output --export-markdown results.md --export-json results.json  \\\n--sort command \\\n--runs 3 \\\n-L num_threads 1,2,3,4,5,6,7,8,9,16 \\\n--prepare 'cmake --build build -j 20 && build/bin/bitcoind -blockfilterindex=1 -networkactive=0 -walletpar={num_threads} && sleep 10 && build/bin/bitcoin-cli loadwalllet <wallet-name>' \\\n--conclude 'build/bin/bitcoin-cli stop && sleep 10' \\\n'build/bin/bitcoin-cli rescanblockchain 700000 900000'\n```\n\nTable 1 was obtained on  a machine with the following specifications\n\n```\nArchitecture:             x86_64\n  CPU op-mode(s):         32-bit, 64-bit\n  Address sizes:          46 bits physical, 48 bits virtual\n  Byte Order:             Little Endian\nCPU(s):                   20\n  On-line CPU(s) list:    0-19\nVendor ID:                GenuineIntel\n  Model name:             Intel(R) Core(TM) Ultra 7 265\n    CPU family:           6\n    Model:                198\n    Thread(s) per core:   1\n    Core(s) per socket:   1\n    Socket(s):            20\n    Stepping:             2\n    CPU(s) scaling MHz:   41%\n    CPU max MHz:          4800.0000\n    CPU min MHz:          800.0000\n    BogoMIPS:             4761.60\n```\n\n| Branch | Mean [s] | Min [s] | Max [s] |\n|:---|---:|---:|---:|\n| master | 272.222 \u00b1 0.183 | 272.064 | 272.423 |\n| new-rescan (num_threads = 1) | 274.964 \u00b1 0.593 | 274.547 | 275.643 |\n| new-rescan (num_threads = 2) | 131.177 \u00b1 0.201 | 131.026 | 131.405 |\n| new-rescan (num_threads = 4) | 65.633 \u00b1 0.203 | 65.423 | 65.829 |\n| new -rescan (num_threads = 6) | 44.129 \u00b1 0.084 | 44.067 | 44.224 |\n| new-rescan (num_threads = 8) | 34.790 \u00b1 0.048 | 34.761 | 34.845 |\n| new-rescan (num_threads = 10) | 34.762 \u00b1 0.178 | 34.633 | 34.965 |\n| new-rescan (num_threads = 16)| 34.813 \u00b1 0.136 | 34.691 | 34.959 |\n\n_Table 1. Table of results of a mainnet benchmark of scanning 200000 blocks. The improvements seem to peak at `8x` speedup despite the machine having an excess number of Cores (`20`)._\n\n### Worst Case\nThis parallel fast rescan checks the filters for a series of blocks in parallel. One of the following cases can occur:\n- No blocks matched; the wallet can skip this series of blocks\n- The last block in the series matched; the wallet scans the last block and updates the filters if the wallet scripts have changed.\n- One of the blocks before the last block matched; the wallet scans this block, updates the filters if new scripts are added, and rechecks filters for the blocks after the matched block. This is the **worst-case** scenario.\n\nThis Python script patch [python script](https://github.com/Eunovo/rescan-benchmark/blob/main/rescan_benchmark.patch) creates custom chains designed with payments at specified intervals to observe the performance of parallel fast rescan in two scenarios:\n- the payments are made to the next index in the descriptor range, the expected case.\n- the payments are made to the last index in the descriptor range, the worst case.\n\n Fig 1 shown below was produced using this patch on a machine with the following specifications\n\n```\nArchitecture:                x86_64\n  CPU op-mode(s):            32-bit, 64-bit\n  Address sizes:             48 bits physical, 48 bits virtual\n  Byte Order:                Little Endian\nCPU(s):                      16\n  On-line CPU(s) list:       0-15\nVendor ID:                   AuthenticAMD\n  Model name:                AMD Ryzen 9 8945HS w/ Radeon 780M Graphics\n    CPU family:              25\n    Model:                   117\n    Thread(s) per core:      2\n    Core(s) per socket:      8\n    Socket(s):               1\n    Stepping:                2\n    Frequency boost:         enabled\n    CPU(s) scaling MHz:      63%\n    CPU max MHz:             5263.0000\n    CPU min MHz:             400.0000\n```\n\n_Fig 1. Time to scan a 5000-block chain on Regtest with payments at varying intervals, comparing parallel Fast Rescan against serial Fast Rescan (baseline). Parallel Fast Rescan outperforms serial Fast Rescan at longer payment intervals, in both the expected and worst case. The Slow Rescan graph is included separately to check whether this PR causes any regression in Slow Rescan performance. The gap between the worst-case and expected-case runtimes on the Slow Rescan graph comes from the worst case triggering a `KEYPOOL_SIZE` `TopUp` each time a new address is found on-chain. These benchmarks use [this commit](https://github.com/bitcoin/bitcoin/pull/34400/commits/33faeab50cdaff016de19967ecee9aa145a9dc79) as a baseline instead of master, since master lacks the `-walletpar` config parameter. All materials for this custom benchmark are available [here](https://github.com/Eunovo/rescan-benchmark)._\n\nAlthough not explicitly checked with Valgrind, hyperfine reported that memory usage stayed the same across all runs. I'm not sure to what degree Hyperfine's memory usage report can be trusted, but the PR limits the number of block hashes that can be held in memory for processing to `1000` (not configurable by the user).",
 "commits": [
  {
   "sha": "217efa59c734d9ff40ccbb8ad51ae6c75c66dbbb",
   "date": "2026-07-13T10:19:26Z",
   "message": "wallet/tests: pin ScanForWalletTransactions behavior\n\nAdd unit tests locking in currently untested rescan behavior, so that\nthe upcoming ChainScanner refactor can be reviewed against them:\n\n- a reorged-out block that the block filter does not match is skipped\n  and the scan ends successfully at the reorg point; a reorged-out\n  block that must be inspected fails the scan\n- rescan reservation lifecycle: single reservation at a time, RAII\n  release, the with_passphrase flag, and idle accessor values\n- bounded scans stop exactly at max_height, including a single-block\n  range (progress start == end)\n- a tip extension while the scan is running is picked up instead of\n  stopping at the height the scan started with\n- save_progress=false must not touch the wallet's best block record\n- RescanFromTime moves the returned timestamp past unreadable (pruned)\n  blocks and returns it unchanged when nothing needs scanning\n- missing block filters must not cause blocks to be skipped\n- loading a wallet that is behind the chain tip rescans from its\n  recorded best block (inclusive), the path loadwallet takes"
  },
  {
   "sha": "65d9e2fab909f764235c57106411cfb40562bbd0",
   "date": "2026-07-13T11:09:02Z",
   "message": "wallet: introduce ChainScanner as a CWallet member\n\nMove scan state atomics (abort, scanning, passphrase, start time,\nprogress) into ChainScanner and expose it via Scanner(). All\ncallers use Scanner().Scan() directly.\n\nThe newly added `m_scanner` is an incomplete type so CWallet's\nconstructor and destructor is moved into wallet.cpp where\nthe type is complete.\n\nThis change introduces a new circular dependency of the form\n\"wallet/scan -> wallet/wallet -> wallet/scan\" which is added to\n`EXPECTED_CIRCULAR_DEPENDENCIES`."
  },
  {
   "sha": "0f6591dc4357fa9e359f0dabe16de7792e2f5203",
   "date": "2026-07-13T11:09:07Z",
   "message": "wallet/scan: move RescanFromTime to ChainScanner as ScanFromTime\n\nCallers now reach this via Scanner().ScanFromTime() rather than\na CWallet member function, keeping all scan logic in ChainScanner."
  },
  {
   "sha": "9295cab7b7100c2bc084b295c4094b9c0ab1f6fb",
   "date": "2026-07-13T11:09:07Z",
   "message": "wallet/scan: move WalletRescanReserver to scan files"
  },
  {
   "sha": "9eb81e1f7327dec574df5999c96ef97839c671b9",
   "date": "2026-07-13T11:09:07Z",
   "message": "wallet/scan: extract block filter matching to ShouldFetchBlock"
  },
  {
   "sha": "6b6690e55a660f177356bbb2223f4ef9da68e1d1",
   "date": "2026-07-13T11:09:07Z",
   "message": "wallet/scan: extract block scanning logic to ScanBlock"
  },
  {
   "sha": "86db8544145b5c97f788635f78cda256057d0aa1",
   "date": "2026-07-13T11:09:07Z",
   "message": "wallet/scan: extract QueueNextBlock\n\nDequeue the current block at the top of the Scan loop and extract the\nlookup of its chain position and the queueing of its active-chain\nsuccessor into QueueNextBlock.\n\nSince the current block is now dequeued at the top of the loop, update\nprogress_current there as well so the reported progress keeps referring\nto the block being processed, as before."
  },
  {
   "sha": "b4953b1026291876b4ce49e3ab80c70180a5de01",
   "date": "2026-07-13T11:09:07Z",
   "message": "wallet/scan: extract progress tracking helpers from `ChainScanner::Scan`"
  },
  {
   "sha": "3ffb908812ffc61a330e85aafba999f7614dd32f",
   "date": "2026-07-13T11:26:54Z",
   "message": "wallet/scan: combine block iteration and filtering in ReadNextBlocks\n\nThis commit refactors the block filtering logic from ShouldFetchBlock\ninto a new ReadNextBlocks method that returns the blocks that are ready\nto be scanned. This prepares the code for pipelined and parallel block\nfilter checking while keeping the current single-threaded, one block at\na time behaviour.\n\nState for a single Scan() call lives in a ScanContext that is local to\nScan() and passed through the helpers. The QueueNextBlock helper is\nabsorbed into the new ReadNextBlock, its only caller: consuming the\npending block, recording whether it is still in the active chain, and\nqueueing its successor are one operation. Read blocks carry the\nstill-active flag, so a reorged-out block that must be inspected fails\nthe scan while a filter-skipped one stays skipped.\n\nProgress is still updated and logged for filter-skipped ranges from the\nscan loop, keeping the previous per-block reporting cadence."
  },
  {
   "sha": "33faeab50cdaff016de19967ecee9aa145a9dc79",
   "date": "2026-07-13T11:26:54Z",
   "message": "wallet: Add wallet parallel processing threads param\n\nThis parameter will be used in a future commit to determine\nthe number of threads to use for parallel fast-rescan.\n\n8 threads is chosen as a reasonable MAX_WALLETPAR,\nbenchmarks show 8x improvement on fast-rescan with 8 threads.\n\nThe member lives with the other public wallet options so that\ntests can configure it directly."
  },
  {
   "sha": "4534de0811bd158beec6d843e580d671197f1e56",
   "date": "2026-07-22T20:11:17Z",
   "message": "wallet/scan: check blockfilters in parallel\n\nThis commit implements parallel block filter checking. ParallelFilterChecker\nowns the pipeline: the scan loop pushes read blocks into its queue, the\nexecutor submits batches of checks to its thread pool, and TryPop()\npops the front block together with its verdict, in block order.\n\nEach task checks a span of up to FILTER_TASK_SPAN blocks; batching\namortizes the scheduling cost of a task, which may otherwise rival\nthe cost of the checks themselves. In-flight work is capped at\nWorkersCount()*2 span tasks so a scan that ends early does not leave\nworkers checking blocks that get discarded, and the queue is bounded by\nMAX_BLOCKQUEUE_SIZE to limit the block hashes held in memory. When the\npipeline is saturated the main thread helps with queued checks and then\nblocks on the oldest verdict instead of spinning.\n\nSynchronization:\n- Operations requiring cs_wallet (GetLastBlockHeight, SyncTransaction)\n  remain on the main thread since cs_wallet is a RecursiveMutex and\n  Scan is called from AttachChain which locks cs_wallet\n- Workers only read the wallet's filter set; the set is only updated\n  after a keypool top-up (FastWalletRescanFilter::NeedsUpdate), and\n  FilterExecutor::Reset() first drains every submitted check and\n  discards undelivered verdicts, which were computed against the old\n  scripts (documented on FastWalletRescanFilter)"
  },
  {
   "sha": "1cfd6a39163cfab1a64a1d154d5885803612d5f8",
   "date": "2026-07-22T20:11:21Z",
   "message": "wallet/scan: patch parallel filter verdicts with top-up deltas\n\nA keypool top-up previously made the parallel checker discard every\nundelivered verdict and re-check the queued blocks against the updated\nfilter set. Every scanned block triggers a top-up, so the in-flight\nwindow is discarded and re-checked once per block, and each re-check\nhashes the wallet's entire filter set.\n\nThe filter set only ever grows, so undelivered verdicts don't need to\nbe discarded: a MATCH computed against the old set stays valid, and a\nNO_MATCH can only be upgraded by the newly derived scripts. Have\nUpdateIfNeeded() return the scripts it adds and patch the undelivered\nNO_MATCH verdicts against just that delta, keeping every completed\ncheck. The re-checks are re-queued to the thread pool as span tasks,\nso they run in parallel like fresh checks instead of stalling the scan\nthread while the workers idle. Since the re-queued spans take the\nplace of the collected ones in the in-flight window, the number of\nverdicts a top-up has to patch stays bounded by Submit()'s cap."
  },
  {
   "sha": "eab65562b3b46d2ee5c34717284bd28ba33854f7",
   "date": "2026-07-22T20:11:21Z",
   "message": "wallet/scan: save and log scan progress every `INTERVAL_TIME`\n\nPause the submission of filter checks at interval boundaries so that\nReadNextBlocks returns and the scan loop can log progress and give\nScanBlock() a chance to save progress. The scan loop resets the\ninterval for scanned blocks, skipped ranges, and empty batches, so a\npaused pipeline always resumes."
  },
  {
   "sha": "8027e1cef9ab5a3fc00d2c657f9d49845563b3ed",
   "date": "2026-07-22T20:11:21Z",
   "message": "wallet/tests: exercise parallel fast-rescan\n\nRun the filter-dependent scan tests with parallel\nfilter checking (-walletpar 1 and 4).\n\nThe wallet loading rescan test also runs on the parallel path now, with\na log hook asserting that the multi-threaded variant actually engaged:\nthis covers a rescan that starts from a mid-chain block with cs_wallet\nheld."
  },
  {
   "sha": "ae888e0dc9f3c660586887596eb06eb0e2560aaf",
   "date": "2026-07-22T20:11:21Z",
   "message": "tests: update wallet_fast_rescan to cover serial fast scan\n\nThe wallet now uses  parallel scan by default, so existing functional tests now exercise the parallel scan logic.\n`wallet_fast_rescan` is updated to test both serial and parallel fast scan."
  }
 ],
 "timeline": [
  {
   "t": "2026-01-25T00:16:03Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "ef847e8bcee3d7e09051eaa956ce8086556a97f0"
  },
  {
   "t": "2026-01-29T04:50:41Z",
   "kind": "comment",
   "who": "luke-jr",
   "assoc": "CONTRIBUTOR",
   "text": "I would have expected rescanning to be I/O bound rather than CPU, in which case parallelization could make things worse (more random seeking). Have you benchmarked this on a non-SSD?"
  },
  {
   "t": "2026-01-29T08:42:30Z",
   "kind": "comment",
   "who": "Eunovo",
   "assoc": "CONTRIBUTOR",
   "text": "[quoted text omitted]\n\nFast rescan checks block filters, which involves considerable hashing. This PR parallelises the checking of block filters, and my benchmarks show considerable improvements in rescan speeds with block filters. Slow rescan, which is I/O bound, remains the same. I expect the speedup to be transferable to non-SSD machines, but I haven't benchmarked this."
  },
  {
   "t": "2026-02-08T08:59:21Z",
   "kind": "review_comment",
   "who": "bvbfan",
   "assoc": "CONTRIBUTOR",
   "path": "src/wallet/scan.cpp",
   "commit": "2f23481b72bb78c612071925ed5a791669d54dfe",
   "in_reply_to": null,
   "text": "This slows down the scanning no? All workers already process submit task in its own `WorkThread` just randomly trying to acquire mutex from scanning thread is non sense to me."
  },
  {
   "t": "2026-02-08T11:40:53Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "CONTRIBUTOR",
   "path": "src/wallet/scan.cpp",
   "commit": "2f23481b72bb78c612071925ed5a791669d54dfe",
   "in_reply_to": 2778932090,
   "text": "Are you referring to the `ThreadPool::m_mutex`? This mutex is not held during task processing. It is only briefly held to access the work queue. Calling `ProcessTask()` from the main thread does not slow down scanning; it gives the main thread work to do instead of wasting cycles waiting for results."
  },
  {
   "t": "2026-02-08T12:28:55Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "49ae44cefe695b8d107105f5ae444a57566ef9db"
  },
  {
   "t": "2026-02-09T18:29:24Z",
   "kind": "review_comment",
   "who": "bvbfan",
   "assoc": "CONTRIBUTOR",
   "path": "src/wallet/scan.cpp",
   "commit": "2f23481b72bb78c612071925ed5a791669d54dfe",
   "in_reply_to": 2778932090,
   "text": "Yep it's not held during task execution, but if main thread do a task, it cannot put new tasks to queue i.e. workers \"fight\" itself to read something and do nothing. The idea is main thread submit tasks faster than workers could finish to keep all of them busy otherwise there is no difference between 3 and 16 thread (~13 threads do nothing)."
  },
  {
   "t": "2026-02-10T09:34:04Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "CONTRIBUTOR",
   "path": "src/wallet/scan.cpp",
   "commit": "2f23481b72bb78c612071925ed5a791669d54dfe",
   "in_reply_to": 2778932090,
   "text": "The main thread intentionally submits only up to `WORKERS_COUNT` tasks before waiting, rather than continuously submitting. This allows it to pause and update filters whenever a payment is found, preventing unnecessary work on wallets with many transactions packed into a short block range."
  },
  {
   "t": "2026-02-11T17:19:30Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "2f23481b72bb78c612071925ed5a791669d54dfe"
  },
  {
   "t": "2026-02-11T17:20:49Z",
   "kind": "comment",
   "who": "Eunovo",
   "assoc": "CONTRIBUTOR",
   "text": "https://github.com/bitcoin/bitcoin/pull/33689 has been merged; the cherry-picked Threadpool commit has been removed."
  },
  {
   "t": "2026-02-11T20:43:14Z",
   "kind": "review",
   "who": "furszy",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "2f23481b72bb78c612071925ed5a791669d54dfe",
   "text": "I like the PR conceptually but I think it would be nice to first improve the current scanning code structure, then land the parallelization feature. The current code mixes a lot responsibilities.\nSimilar to what you did in 633531614f69de49733642fd19cc9eba830fbdea, but into a separate PR so we can first land some good building blocks for this to happen.\n\nSome quick pseudo-code structuring how I imagine it, which is similar to yours:\n```\nScan(wallet, start_block_hash, end_block_hash, fn_filter_block, fn_process_block, interrupt) {\n     it_current_hash = start_block_hash;\n\n     while (it_current_hash != end_block_hash || interrupt) {\n         // Skip block if needed (this function contains the BlockFilterIndex check if enabled)\n         if (fn_filter_block(it_current_hash)) continue;\n\n         // (this is more or less how we currently do it, we fetch the block and the next block hash at the same time)\n         block = chain.find_block(it_current_hash).next_block(it_current_hash);\n\n         // (inside this function the wallet will digest the block update the filter and save progress if needed)\n         fn_process_block(block);\n      }\n}\n```"
  },
  {
   "t": "2026-02-21T08:12:16Z",
   "kind": "comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "text": "Concept ACK"
  },
  {
   "t": "2026-02-26T16:14:36Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "48154b87e2fb303ca0f3d46da29a3fbbf8758a06"
  },
  {
   "t": "2026-02-26T16:21:49Z",
   "kind": "comment",
   "who": "Eunovo",
   "assoc": "CONTRIBUTOR",
   "text": "[quoted text omitted]\n\nI moved the test change into https://github.com/bitcoin/bitcoin/pull/34667 and the  `ScanForWalletTransactions` refactor into https://github.com/bitcoin/bitcoin/pull/34681.\nI'll be putting this PR in draft while those PRs are open."
  },
  {
   "t": "2026-03-13T14:34:53Z",
   "kind": "review",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "48154b87e2fb303ca0f3d46da29a3fbbf8758a06",
   "text": "Concept ACK\n\nI attempted to reproduce the benchmarks for this\nBut I did not use hyperfine, I used time\n\nSystem: AMD Ryzen 7 7700, 16 cores, 64GB RAM, mainnet, blocks 500000\u2013900000, 3 runs averaged.\n\nBaseline (881ebc4730ad15bd26e3e32ee3c9ba9d6e05552d): single-threaded fast rescan, `-walletpar` has no effect \u2014 consistently ~283s regardless of thread count.\n\nParallel scan (48154b87e2fb303ca0f3d46da29a3fbbf8758a06): fast rescan with threadpool parallelism enabled via `-walletpar`.\n\n| num_threads | baseline | parallel | speedup |\n|-------------|----------|----------|---------|\n| 1           | 282.618s | 288.214s | 0.98x (slight overhead) |\n| 2           | 282.618s | 187.485s | 1.51x |\n| 4           | 282.618s | 119.575s | 2.37x |\n| 8           | 282.618s | 81.565s  | 3.47x |\n| 16          | 282.618s | 62.965s  | 4.53x |\n\n- Speedup scales well up to 16 threads, going from ~283s down to ~63s \u2014 a 4.5x improvement.\n- Results are very consistent across runs (low variance), because the machine is bare metal and no other running processes apart from bitcoind are present during the benchmark runs.\n\nSteps to reproduce\n1. Restart bitcoind with `-blockfilterindex=1 ` till it's done.\n2. create a new wallet `test`\n3. Stop the node\n4. Save the script below as `bench_script.sh`\n\nscript\n\n```sh\n#!/usr/bin/env bash\nset -euo pipefail\n\nCOMMITS=(881ebc4730ad15bd26e3e32ee3c9ba9d6e05552d 48154b87e2fb303ca0f3d46da29a3fbbf8758a06)\nTHREADS=(1 2 4 8 16)\nRUNS=3\nWALLET_NAME=\"test\"\nDATADIR=\"$HOME/.bitcoin\"\nRESULTS_CSV=\"results.csv\"\nRESULTS_MD=\"results.md\"\n\necho \"commit,num_threads,run,seconds\" > \"$RESULTS_CSV\"\n\nfor commit in \"${COMMITS[@]}\"; do\n  short=\"${commit:0:7}\"\n  git checkout \"$commit\"\n  cmake --build build -j 20\n\n  for num_threads in \"${THREADS[@]}\"; do\n    echo \"=== commit=$short num_threads=$num_threads ===\"\n\n    for run in $(seq 1 $RUNS); do\n      echo \"  run $run/$RUNS\"\n\n      # start node\n      build/bin/bitcoind -blockfilterindex=1 -walletpar=\"$num_threads\" -daemonwait\n      build/bin/bitcoin-cli loadwallet \"$WALLET_NAME\"\n\n      build/bin/bitcoin-cli rescanblockchain 500000 900000\n\n      # parse timing from debug log: \"Rescan completed in 284737ms\"\n      ms=$(grep \"Rescan completed in\" \"$DATADIR/debug.log\" | tail -1 | grep -oP '\\d+(?=ms)')\n      seconds=$(python3 -c \"print(f'{$ms / 1000:.3f}')\")\n\n      echo \"$short,$num_threads,$run,$seconds\" >> \"$RESULTS_CSV\"\n      echo \"  -> ${seconds}s\"\n\n      build/bin/bitcoin-cli stop\n      # wait for clean shutdown\n      while build/bin/bitcoin-cli ping 2>/dev/null; do sleep 1; done\n      sleep 5\n    done\n  done\ndone\n\n# markdown table with averages\npython3 - \"$RESULTS_CSV\" \"$RESULTS_MD\" <<'EOF'\nimport sys, csv\nfrom collections import defaultdict\n\ninfile, outfile = sys.argv[1], sys.argv[2]\n\nrows = list(csv.DictReader(open(infile)))\ngroups = defaultdict(list)\nfor r in rows:\n    groups[(r['commit'], r['num_threads'])].append(float(r['seconds']))\n\nwith open(outfile, 'w') as f:\n    f.write(\"| commit | num_threads | run1 | run2 | run3 | mean |\\n\")\n    f.write(\"|--------|-------------|------|------|------|------|\\n\")\n    for (commit, threads), times in sorted(groups.items()):\n        mean = sum(times) / len(times)\n        runs = \" | \".join(f\"{t:.3f}\" for t in times)\n        f.write(f\"| {commit} | {threads} | {runs} | {mean:.3f} |\\n\")\n\nprint(f\"Written {outfile}\")\nEOF\n\necho \"Done. Results in $RESULTS_CSV and $RESULTS_MD\"\n```\n\n6. Make the script executable `chmod +x bench_script.sh`\n7. Execute the script `./bench_script.sh`\n8. You can go a step further by using a top like btop https://github.com/aristocratos/btop to monitor the resource usage and how it will be well utilized when rescanning in parallel.\n\nThe current steps to reproduce in the description are stale because the commit hashes have changed since your force pushes."
  },
  {
   "t": "2026-03-16T14:03:17Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "CONTRIBUTOR",
   "path": "src/wallet/scan.cpp",
   "commit": "48154b87e2fb303ca0f3d46da29a3fbbf8758a06",
   "in_reply_to": null,
   "text": "In 48154b87e2fb303ca0f3d46da29a3fbbf8758a06 \"wallet: check blockfilters in parallel\"\n\nThese two conditions in this check seem redundant with the same two conditions in the following for loop."
  },
  {
   "t": "2026-03-16T14:08:54Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "CONTRIBUTOR",
   "path": "src/wallet/scan.cpp",
   "commit": "48154b87e2fb303ca0f3d46da29a3fbbf8758a06",
   "in_reply_to": null,
   "text": "In 48154b87e2fb303ca0f3d46da29a3fbbf8758a06 \"wallet: check blockfilters in parallel\"\n\nSo it appears this is a flow where multiple tasks can be submitted in one go. There is an overload method of `Submit` that accepts a range of tasks & pushes all of them in the queue within one acquisition of queue lock, while notifying all the waiting workers. I think this workflow can be benefitted with this method, an untested code snippet is below because this branch is not rebased over master that contains the ranged overload.\n\nhttps://github.com/bitcoin/bitcoin/blob/ff7cdf633e375f151cccbcc78c7add161b3d29b8/src/util/threadpool.h#L199-L220\n\n```diff\ndiff --git a/src/wallet/scan.cpp b/src/wallet/scan.cpp\nindex 6b776930f1..2295a242a1 100644\n--- a/src/wallet/scan.cpp\n+++ b/src/wallet/scan.cpp\n@@ -147,6 +147,20 @@ std::optional<std::pair<size_t, size_t>> ChainScanner::ReadNextBlocks(const std:\n         return std::make_pair<size_t, size_t>(0, m_blocks.size());\n     }\n     filter->UpdateIfNeeded();\n+\n+    auto block_matcher = [&filter](uint256 block_hash) {\n+        const auto matches_block{filter->MatchesBlock(block_hash)};\n+        if (matches_block.has_value()) {\n+            if (*matches_block) {\n+                return FilterRes::FILTER_MATCH;\n+            } else {\n+                return FilterRes::FILTER_NO_MATCH;\n+            }\n+        } else {\n+            return FilterRes::FILTER_NO_FILTER;\n+        }\n+    }\n+\n     auto* thread_pool = m_wallet.m_thread_pool;\n     // ThreadPool pointer should never be null here\n     // during normal operation because it should\n@@ -187,23 +201,13 @@ std::optional<std::pair<size_t, size_t>> ChainScanner::ReadNextBlocks(const std:\n         // This prevents over-submission: if we queued all jobs upfront and the filtered\n         // block range is smaller than expected, worker threads would process blocks\n         // that get discarded, wasting CPU cycles.\n-        const size_t job_gap = workers_count - futures.size();\n-        if (job_gap > 0 && i < m_blocks.size()) {\n-            for (size_t j = 0; j < job_gap && i < m_blocks.size(); ++j, ++i) {\n-                auto block = m_blocks[i];\n-                futures.emplace_back(*thread_pool->Submit([&filter, block = std::move(block)]() {\n-                    const auto matches_block{filter->MatchesBlock(block.first)};\n-                    if (matches_block.has_value()) {\n-                        if (*matches_block) {\n-                            return FilterRes::FILTER_MATCH;\n-                        } else {\n-                            return FilterRes::FILTER_NO_MATCH;\n-                        }\n-                    } else {\n-                        return FilterRes::FILTER_NO_FILTER;\n-                    }\n-                }));\n+        auto to_submit_jobs_count = std::min(workers_count - futures.size(), m_blocks - i);\n+        if (to_submit_jobs_count) {\n+            std::vector<std::function<FilterRes(uint256)>> block_matchers;\n+            for (; i < to_submit_jobs_count; ++i) {\n+                block_matchers.emplace_back(block_matcher(m_blocks[i].first));\n             }\n+            futures.emplace_back(*thread_pool->Submit(std::move(block_matchers)));\n         }\n\n         // If m_max_blockqueue_size blocks have been filtered,\n\n```"
  },
  {
   "t": "2026-03-16T14:19:14Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "CONTRIBUTOR",
   "path": "src/wallet/scan.cpp",
   "commit": "32609391d5d2ed4f9a88bd8d4821515ab146f804",
   "in_reply_to": null,
   "text": "In 48154b8 \"wallet: check blockfilters in parallel\"\n\nI'm doubtful that putting the controller (non-worker) thread to process the threadpool tasks is helpful.\n\nThis threadpool is shared across wallets. It could be the case that multiple RPCs of different wallets could be called simultaneously. ProcessTask picks the first item from a shared queue in the threadpool. Can't it happen that this controller thread picks up the task of another RPC of another wallet, thereby distorting results (from a RPC latency point of view) of this one?"
  },
  {
   "t": "2026-03-16T14:57:14Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "CONTRIBUTOR",
   "path": "src/wallet/scan.cpp",
   "commit": "48154b87e2fb303ca0f3d46da29a3fbbf8758a06",
   "in_reply_to": null,
   "text": "In 48154b8 \"wallet: check blockfilters in parallel\"\n\nIt appears that `current_block_index + 1` is equal to `completed` due to a `int current_block_index = completed - 1;` above."
  },
  {
   "t": "2026-03-16T15:05:58Z",
   "kind": "review",
   "who": "rkrux",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "48154b87e2fb303ca0f3d46da29a3fbbf8758a06",
   "text": "I've looked only at the 48154b8 \"wallet: check blockfilters in parallel\" commit partially."
  },
  {
   "t": "2026-03-18T12:05:32Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "CONTRIBUTOR",
   "path": "src/wallet/scan.cpp",
   "commit": "32609391d5d2ed4f9a88bd8d4821515ab146f804",
   "in_reply_to": 2940686723,
   "text": "[quoted text omitted]\n\nIt is helpful. I had better results when I put the thread to work vs when I didn't.\n\n[quoted text omitted]\nTrue, this creates an argument to ditch the shared threadpool and create one at the begining of the scan process; the same way we initialise script threads in `ConnectBlock`"
  },
  {
   "t": "2026-03-18T12:16:27Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "CONTRIBUTOR",
   "path": "src/wallet/scan.cpp",
   "commit": "32609391d5d2ed4f9a88bd8d4821515ab146f804",
   "in_reply_to": 2940686723,
   "text": "I was doubtful of the helpfulness of using processtask because of this cross-wallet/rpc operation.\n\n[quoted text omitted]\nInteresting, I had thought of not using the processtask function in the current thread pool setup as a way.\nBut an intra-wallet scan operation specific threadpool seems like a good alternative to consider.\nI wll think of its implications."
  },
  {
   "t": "2026-03-18T12:18:18Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "CONTRIBUTOR",
   "path": "src/wallet/scan.cpp",
   "commit": "48154b87e2fb303ca0f3d46da29a3fbbf8758a06",
   "in_reply_to": 2940624378,
   "text": "I will check this when I rebase on master."
  },
  {
   "t": "2026-03-18T12:42:36Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "32609391d5d2ed4f9a88bd8d4821515ab146f804"
  },
  {
   "t": "2026-03-18T13:43:12Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "CONTRIBUTOR",
   "path": "src/wallet/scan.cpp",
   "commit": "48154b87e2fb303ca0f3d46da29a3fbbf8758a06",
   "in_reply_to": 2940590925,
   "text": "Fixed."
  },
  {
   "t": "2026-03-18T13:43:45Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "CONTRIBUTOR",
   "path": "src/wallet/scan.cpp",
   "commit": "48154b87e2fb303ca0f3d46da29a3fbbf8758a06",
   "in_reply_to": 2940920289,
   "text": "Fixed."
  },
  {
   "t": "2026-04-06T08:31:09Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "22fed002ea5312b9844fab85ebc417dd81535c27"
  },
  {
   "t": "2026-04-12T17:59:08Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "587d48386e8fd45a7305dffb2b9d2f603c450a82"
  },
  {
   "t": "2026-04-12T18:56:58Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "6343fba2b34caeaae2e61de9409b536867431b7a"
  },
  {
   "t": "2026-04-12T19:16:30Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "a733867087a64269046a22548402cb8ba8f79c34"
  },
  {
   "t": "2026-04-12T21:27:46Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "87b8f03858ee98465696e57c6ae3a4382359c03c"
  },
  {
   "t": "2026-04-14T12:48:26Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "b62e9e739113fe58a86d23a00f8e9b1b561cd29f"
  },
  {
   "t": "2026-04-14T12:50:47Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "CONTRIBUTOR",
   "path": "src/wallet/scan.cpp",
   "commit": "48154b87e2fb303ca0f3d46da29a3fbbf8758a06",
   "in_reply_to": 2940624378,
   "text": "Done."
  },
  {
   "t": "2026-04-14T12:54:59Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "CONTRIBUTOR",
   "path": "src/wallet/scan.cpp",
   "commit": "32609391d5d2ed4f9a88bd8d4821515ab146f804",
   "in_reply_to": 2940686723,
   "text": "I have changed the implementation substantially. I have moved away from a shared ThreadPool to one that is initialized before scanning and discarded after.\n\n`ProcessTask()` is now only used when the main thread needs to complete the queued up jobs before the processing the selected blocks."
  },
  {
   "t": "2026-04-14T12:59:29Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "CONTRIBUTOR",
   "path": "src/wallet/scan.cpp",
   "commit": "2f23481b72bb78c612071925ed5a791669d54dfe",
   "in_reply_to": 2778932090,
   "text": "I have changed the implementation so that the main thread only joins task processing when it needs to clear the work queue (after a range of blocks to fetch and scan has been determined) before processing blocks. The number of blocks  that can be submitted at once has been raised to `2 * WORKERS_COUNT`, it is still kept low intentionally to reduce wasted work."
  },
  {
   "t": "2026-04-20T15:38:50Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "CONTRIBUTOR",
   "text": "I have measured it on an `Intel(R) Core(TM) i7-7700 CPU` with 8 cores, 64Gi RAM and a HDD:\n\n```bash\nhyperfine --show-output \\\n--export-markdown pr34400-results.md \\\n--export-json pr34400-results.json \\\n--sort command \\\n--runs 3 \\\n-L commit 68f030eef7b14d5ac6372a12864a813317ae0f4f,b62e9e739113fe58a86d23a00f8e9b1b561cd29f \\\n-L num_threads 1,2,3,4,5,6,7,8,9,16 \\\n--prepare 'git checkout -q {commit} && cmake -S . -B build && cmake --build build --parallel \"$(nproc)\" && build/bin/bitcoind -daemonwait -datadir=/mnt/my_storage/BitcoinData -connect=0 -listen=0 -dnsseed=0 -blockfilterindex=1 -wallet=bench_pr34400_20260416 -walletpar={num_threads} && build/bin/bitcoin-cli -rpcwait -datadir=/mnt/my_storage/BitcoinData -rpcwallet=bench_pr34400_20260416 getwalletinfo > /dev/null' \\\n--conclude 'build/bin/bitcoin-cli -rpcwait -datadir=/mnt/my_storage/BitcoinData stop && sleep 10' \\\n'rescanblockchain 500000 900000'\n```\n\n| Command | Mean [s] | Min [s] | Max [s] | Relative |\n|:---|---:|---:|---:|---:|\n| `rescanblockchain 500000 900000 (commit = 68f030eef7b14d5ac6372a12864a813317ae0f4f, num_threads = 1)` | 572.300 \u00b1 64.726 | 534.060 | 647.032 | 3.42 \u00b1 0.39 |\n| `rescanblockchain 500000 900000 (commit = b62e9e739113fe58a86d23a00f8e9b1b561cd29f, num_threads = 1)` | 545.611 \u00b1 14.138 | 536.478 | 561.897 | 3.26 \u00b1 0.10 |\n| `rescanblockchain 500000 900000 (commit = 68f030eef7b14d5ac6372a12864a813317ae0f4f, num_threads = 2)` | 534.545 \u00b1 0.569 | 534.055 | 535.168 | 3.20 \u00b1 0.05 |\n| `rescanblockchain 500000 900000 (commit = b62e9e739113fe58a86d23a00f8e9b1b561cd29f, num_threads = 2)` | 303.284 \u00b1 6.724 | 299.207 | 311.045 | 1.81 \u00b1 0.05 |\n| `rescanblockchain 500000 900000 (commit = 68f030eef7b14d5ac6372a12864a813317ae0f4f, num_threads = 3)` | 534.895 \u00b1 0.740 | 534.298 | 535.723 | 3.20 \u00b1 0.05 |\n| `rescanblockchain 500000 900000 (commit = b62e9e739113fe58a86d23a00f8e9b1b561cd29f, num_threads = 3)` | 222.130 \u00b1 4.551 | 216.878 | 224.899 | 1.33 \u00b1 0.03 |\n| `rescanblockchain 500000 900000 (commit = 68f030eef7b14d5ac6372a12864a813317ae0f4f, num_threads = 4)` | 534.069 \u00b1 0.546 | 533.447 | 534.473 | 3.19 \u00b1 0.05 |\n| `rescanblockchain 500000 900000 (commit = b62e9e739113fe58a86d23a00f8e9b1b561cd29f, num_threads = 4)` | 200.326 \u00b1 2.305 | 197.771 | 202.248 | 1.20 \u00b1 0.02 |\n| `rescanblockchain 500000 900000 (commit = 68f030eef7b14d5ac6372a12864a813317ae0f4f, num_threads = 5)` | 534.699 \u00b1 0.582 | 534.047 | 535.167 | 3.20 \u00b1 0.05 |\n| `rescanblockchain 500000 900000 (commit = b62e9e739113fe58a86d23a00f8e9b1b561cd29f, num_threads = 5)` | 186.884 \u00b1 4.476 | 182.887 | 191.720 | 1.12 \u00b1 0.03 |\n| `rescanblockchain 500000 900000 (commit = 68f030eef7b14d5ac6372a12864a813317ae0f4f, num_threads = 6)` | 535.054 \u00b1 0.270 | 534.819 | 535.349 | 3.20 \u00b1 0.05 |\n| `rescanblockchain 500000 900000 (commit = b62e9e739113fe58a86d23a00f8e9b1b561cd29f, num_threads = 6)` | 175.091 \u00b1 0.531 | 174.640 | 175.677 | 1.05 \u00b1 0.02 |\n| `rescanblockchain 500000 900000 (commit = 68f030eef7b14d5ac6372a12864a813317ae0f4f, num_threads = 7)` | 534.670 \u00b1 1.045 | 533.600 | 535.688 | 3.20 \u00b1 0.05 |\n| `rescanblockchain 500000 900000 (commit = b62e9e739113fe58a86d23a00f8e9b1b561cd29f, num_threads = 7)` | 167.224 \u00b1 2.574 | 165.724 | 170.196 | 1.00 |\n| `rescanblockchain 500000 900000 (commit = 68f030eef7b14d5ac6372a12864a813317ae0f4f, num_threads = 8)` | 535.010 \u00b1 0.121 | 534.875 | 535.105 | 3.20 \u00b1 0.05 |\n| `rescanblockchain 500000 900000 (commit = b62e9e739113fe58a86d23a00f8e9b1b561cd29f, num_threads = 8)` | 172.263 \u00b1 1.808 | 171.188 | 174.350 | 1.03 \u00b1 0.02 |\n| `rescanblockchain 500000 900000 (commit = 68f030eef7b14d5ac6372a12864a813317ae0f4f, num_threads = 9)` | 535.058 \u00b1 0.383 | 534.660 | 535.425 | 3.20 \u00b1 0.05 |\n| `rescanblockchain 500000 900000 (commit = b62e9e739113fe58a86d23a00f8e9b1b561cd29f, num_threads = 9)` | 171.178 \u00b1 0.138 | 171.053 | 171.326 | 1.02 \u00b1 0.02 |\n| `rescanblockchain 500000 900000 (commit = 68f030eef7b14d5ac6372a12864a813317ae0f4f, num_threads = 16)` | 535.238 \u00b1 0.204 | 535.013 | 535.412 | 3.20 \u00b1 0.05 |\n| `rescanblockchain 500000 900000 (commit = b62e9e739113fe58a86d23a00f8e9b1b561cd29f, num_threads = 16)` | 172.619 \u00b1 2.656 | 171.043 | 175.685 | 1.03 \u00b1 0.02 |\n\nHyperfine relative speed comparison\n\n```bash\n3.42 \u00b1  0.39  build/bin/bitcoin-cli -rpcwait -datadir=/mnt/my_storage/BitcoinData -rpcwallet=bench_pr34400_20260416 rescanblockchain 500000 900000 (commit = 68f030eef7b14d5ac6372a12864a813317ae0f4f, num_threads = 1)\n3.26 \u00b1  0.10  build/bin/bitcoin-cli -rpcwait -datadir=/mnt/my_storage/BitcoinData -rpcwallet=bench_pr34400_20260416 rescanblockchain 500000 900000 (commit = b62e9e739113fe58a86d23a00f8e9b1b561cd29f, num_threads = 1)\n3.20 \u00b1  0.05  build/bin/bitcoin-cli -rpcwait -datadir=/mnt/my_storage/BitcoinData -rpcwallet=bench_pr34400_20260416 rescanblockchain 500000 900000 (commit = 68f030eef7b14d5ac6372a12864a813317ae0f4f, num_threads = 2)\n1.81 \u00b1  0.05  build/bin/bitcoin-cli -rpcwait -datadir=/mnt/my_storage/BitcoinData -rpcwallet=bench_pr34400_20260416 rescanblockchain 500000 900000 (commit = b62e9e739113fe58a86d23a00f8e9b1b561cd29f, num_threads = 2)\n3.20 \u00b1  0.05  build/bin/bitcoin-cli -rpcwait -datadir=/mnt/my_storage/BitcoinData -rpcwallet=bench_pr34400_20260416 rescanblockchain 500000 900000 (commit = 68f030eef7b14d5ac6372a12864a813317ae0f4f, num_threads = 3)\n1.33 \u00b1  0.03  build/bin/bitcoin-cli -rpcwait -datadir=/mnt/my_storage/BitcoinData -rpcwallet=bench_pr34400_20260416 rescanblockchain 500000 900000 (commit = b62e9e739113fe58a86d23a00f8e9b1b561cd29f, num_threads = 3)\n3.19 \u00b1  0.05  build/bin/bitcoin-cli -rpcwait -datadir=/mnt/my_storage/BitcoinData -rpcwallet=bench_pr34400_20260416 rescanblockchain 500000 900000 (commit = 68f030eef7b14d5ac6372a12864a813317ae0f4f, num_threads = 4)\n1.20 \u00b1  0.02  build/bin/bitcoin-cli -rpcwait -datadir=/mnt/my_storage/BitcoinData -rpcwallet=bench_pr34400_20260416 rescanblockchain 500000 900000 (commit = b62e9e739113fe58a86d23a00f8e9b1b561cd29f, num_threads = 4)\n3.20 \u00b1  0.05  build/bin/bitcoin-cli -rpcwait -datadir=/mnt/my_storage/BitcoinData -rpcwallet=bench_pr34400_20260416 rescanblockchain 500000 900000 (commit = 68f030eef7b14d5ac6372a12864a813317ae0f4f, num_threads = 5)\n1.12 \u00b1  0.03  build/bin/bitcoin-cli -rpcwait -datadir=/mnt/my_storage/BitcoinData -rpcwallet=bench_pr34400_20260416 rescanblockchain 500000 900000 (commit = b62e9e739113fe58a86d23a00f8e9b1b561cd29f, num_threads = 5)\n3.20 \u00b1  0.05  build/bin/bitcoin-cli -rpcwait -datadir=/mnt/my_storage/BitcoinData -rpcwallet=bench_pr34400_20260416 rescanblockchain 500000 900000 (commit = 68f030eef7b14d5ac6372a12864a813317ae0f4f, num_threads = 6)\n1.05 \u00b1  0.02  build/bin/bitcoin-cli -rpcwait -datadir=/mnt/my_storage/BitcoinData -rpcwallet=bench_pr34400_20260416 rescanblockchain 500000 900000 (commit = b62e9e739113fe58a86d23a00f8e9b1b561cd29f, num_threads = 6)\n3.20 \u00b1  0.05  build/bin/bitcoin-cli -rpcwait -datadir=/mnt/my_storage/BitcoinData -rpcwallet=bench_pr34400_20260416 rescanblockchain 500000 900000 (commit = 68f030eef7b14d5ac6372a12864a813317ae0f4f, num_threads = 7)\n1.00          build/bin/bitcoin-cli -rpcwait -datadir=/mnt/my_storage/BitcoinData -rpcwallet=bench_pr34400_20260416 rescanblockchain 500000 900000 (commit = b62e9e739113fe58a86d23a00f8e9b1b561cd29f, num_threads = 7)\n3.20 \u00b1  0.05  build/bin/bitcoin-cli -rpcwait -datadir=/mnt/my_storage/BitcoinData -rpcwallet=bench_pr34400_20260416 rescanblockchain 500000 900000 (commit = 68f030eef7b14d5ac6372a12864a813317ae0f4f, num_threads = 8)\n1.03 \u00b1  0.02  build/bin/bitcoin-cli -rpcwait -datadir=/mnt/my_storage/BitcoinData -rpcwallet=bench_pr34400_20260416 rescanblockchain 500000 900000 (commit = b62e9e739113fe58a86d23a00f8e9b1b561cd29f, num_threads = 8)\n3.20 \u00b1  0.05  build/bin/bitcoin-cli -rpcwait -datadir=/mnt/my_storage/BitcoinData -rpcwallet=bench_pr34400_20260416 rescanblockchain 500000 900000 (commit = 68f030eef7b14d5ac6372a12864a813317ae0f4f, num_threads = 9)\n1.02 \u00b1  0.02  build/bin/bitcoin-cli -rpcwait -datadir=/mnt/my_storage/BitcoinData -rpcwallet=bench_pr34400_20260416 rescanblockchain 500000 900000 (commit = b62e9e739113fe58a86d23a00f8e9b1b561cd29f, num_threads = 9)\n3.20 \u00b1  0.05  build/bin/bitcoin-cli -rpcwait -datadir=/mnt/my_storage/BitcoinData -rpcwallet=bench_pr34400_20260416 rescanblockchain 500000 900000 (commit = 68f030eef7b14d5ac6372a12864a813317ae0f4f, num_threads = 16)\n1.03 \u00b1  0.02  build/bin/bitcoin-cli -rpcwait -datadir=/mnt/my_storage/BitcoinData -rpcwallet=bench_pr34400_20260416 rescanblockchain 500000 900000 (commit = b62e9e739113fe58a86d23a00f8e9b1b561cd29f, num_threads = 16)\n```"
  },
  {
   "t": "2026-07-14T07:56:38Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "29c94bc77249c35833dd41ea8b533f15830da58c"
  },
  {
   "t": "2026-07-14T13:41:21Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "b92a665d87f3068a3f102194a351d4198b9fe61a"
  },
  {
   "t": "2026-07-15T18:43:56Z",
   "kind": "comment",
   "who": "Eunovo",
   "assoc": "CONTRIBUTOR",
   "text": "I've made some improvements which have increased the speedup from 5x at 8 threads to 8x at 8 threads. 8x speedup seems to be the peak for now. The results have been updated in the PR description, https://github.com/bitcoin/bitcoin/pull/34400#issue-3851923967"
  },
  {
   "t": "2026-07-21T10:05:44Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "061ceb14721bf82443de299e95886168f4e6feb0"
  },
  {
   "t": "2026-07-21T15:00:18Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "bf45a678411f000b60a0d8f20363e5e0c2bc64bf"
  },
  {
   "t": "2026-07-22T12:53:20Z",
   "kind": "comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "text": "I re-ran my script with the recent changes.\n\nRescan of blocks 500000-900000 (mean of 3 runs).\n\n| commit | threads | run1 | run2 | run3 | mean (s) | speedup |\n|--------|--------:|-----:|-----:|-----:|---------:|--------:|\n| b36c2d7 (master) | 1 | 282.488 | 279.832 | 280.120 | 280.813 | 1.00\u00d7 |\n| bf45a67 | 1 | 282.495 | 280.959 | 280.725 | 281.393 | 1.00\u00d7 |\n| bf45a67 | 2 | 139.017 | 138.740 | 137.590 | 138.449 | 2.03\u00d7 |\n| bf45a67 | 4 | 73.558 | 72.770 | 73.283 | 73.204 | 3.84\u00d7 |\n| bf45a67 | 8 | 42.863 | 43.172 | 42.746 | 42.927 | 6.54\u00d7 |\n| bf45a67 | 16 | 42.481 | 42.943 | 42.425 | 42.616 | 6.59\u00d7 |\n\nI got a 6\u00d7 speedup relative to master."
  },
  {
   "t": "2026-07-22T20:12:08Z",
   "kind": "force_push",
   "who": "Eunovo",
   "commit": "ae888e0dc9f3c660586887596eb06eb0e2560aaf"
  },
  {
   "t": "2026-08-26T14:44:34Z",
   "kind": "comment",
   "who": "rkrux",
   "assoc": "CONTRIBUTOR",
   "text": "Strong Concept ACK ae888e0, forgot to share it in the prior review.\n\nI will get around to re-reviewing the prerequisite #34681 so that this can be moved forward."
  }
 ],
 "labels_log": [
  {
   "t": "2026-01-24T20:44:41Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-01-25T01:26:46Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-01-27T14:38:19Z",
   "action": "labeled",
   "label": "Wallet",
   "who": "DrahtBot"
  },
  {
   "t": "2026-02-04T20:20:35Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-02-08T13:09:34Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-03-24T01:40:22Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-06T08:57:48Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-06T09:27:06Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-14T13:38:02Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-13T21:43:51Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-14T08:58:17Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-14T09:27:31Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-07-14T14:42:52Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-14T17:53:55Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-01-27T14:38:15Z",
   "kind": "renamed",
   "who": "Eunovo",
   "from": "Parallel Fast Rescan (approx 5x speed up with 16 threads)",
   "to": "wallet: parallel fast rescan (approx 5x speed up with 16 threads)"
  },
  {
   "t": "2026-02-26T16:22:10Z",
   "kind": "convert_to_draft",
   "who": "Eunovo"
  },
  {
   "t": "2026-04-13T08:37:41Z",
   "kind": "renamed",
   "who": "Eunovo",
   "from": "wallet: parallel fast rescan (approx 5x speed up with 16 threads)",
   "to": "wallet: parallel fast rescan (approx 5x speed up with 8 threads)"
  },
  {
   "t": "2026-07-15T18:40:53Z",
   "kind": "renamed",
   "who": "Eunovo",
   "from": "wallet: parallel fast rescan (approx 5x speed up with 8 threads)",
   "to": "wallet: parallel fast rescan (approx 8x speed up with 8 threads)"
  }
 ],
 "text_chars": 35918,
 "text_tokens_estimate": 8979,
 "changed_paths": [],
 "files": [],
 "test_lines": null,
 "git": null,
 "input_hash": "896ca10bba6d73b9",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}