wallet: parallel fast rescan (approx 5x speed up with 8 threads) #34400

pull Eunovo wants to merge 11 commits into bitcoin:master from Eunovo:new-rescan changing 13 files +607 −261
  1. Eunovo commented at 7:46 PM on January 24, 2026: contributor

    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 5x speedup at 8 threads.

    Prerequisite PRs:

    • #34667 - modify the fast-rescan test to ensure that it fails when the filter is not updated properly.
    • #34681 - refactor CWallet::ScanForWalletTransactions to prepare for the work in this PR.

    Benchmarks: NOTE: to reproduce, please tune your system with pyperf system tune

    EDIT Set up your node to use block filters by setting blockfilterindex=1 in your bitcoin.conf file and ensure your blockfilterindex is synced to tip before attempting to reproduce.

    Using the following command on mainnet with a wallet with no scripts and hyperfine version 1.20.0:

    hyperfine --show-output --export-markdown results.md --export-json results.json  \
    --sort command \
    --runs 3 \
    -L commit 68f030ee,new-rescan \
    -L num_threads 1,2,3,4,5,6,7,8,9,16 \
    --prepare 'git checkout {commit} && cmake --build build -j 20 && build/bin/bitcoind -blockfilterindex=1 -walletpar={num_threads} && sleep 10 && build/bin/bitcoin-cli loadwalllet <wallet-name>' \
    --conclude 'build/bin/bitcoin-cli stop && sleep 10' \
    'build/bin/bitcoin-cli rescanblockchain 500000 900000'
    

    I obtained the following results:

    Command Mean [s] Min [s] Max [s] Relative
    build/bin/bitcoin-cli rescanblockchain 500000 900000 (commit = baseline, num_threads = 1) 532.395 ± 0.883 531.662 533.376 5.12 ± 0.01
    build/bin/bitcoin-cli rescanblockchain 500000 900000 (commit = new-rescan, num_threads = 1) 540.736 ± 1.016 539.906 541.869 5.20 ± 0.01
    build/bin/bitcoin-cli rescanblockchain 500000 900000 (commit = new-rescan, num_threads = 2) 292.880 ± 1.045 292.043 294.051 2.82 ± 0.01
    build/bin/bitcoin-cli rescanblockchain 500000 900000 (commit = new-rescan, num_threads = 3) 204.811 ± 0.331 204.433 205.047 1.97 ± 0.00
    build/bin/bitcoin-cli rescanblockchain 500000 900000 (commit = new-rescan, num_threads = 4) 161.883 ± 0.424 161.456 162.304 1.56 ± 0.00
    build/bin/bitcoin-cli rescanblockchain 500000 900000 (commit = new-rescan, num_threads = 5) 137.347 ± 0.339 136.995 137.672 1.32 ± 0.00
    build/bin/bitcoin-cli rescanblockchain 500000 900000 (commit = new-rescan, num_threads = 6) 121.098 ± 0.410 120.739 121.544 1.16 ± 0.00
    build/bin/bitcoin-cli rescanblockchain 500000 900000 (commit = new-rescan, num_threads = 7) 109.306 ± 0.319 108.938 109.514 1.05 ± 0.00
    build/bin/bitcoin-cli rescanblockchain 500000 900000 (commit = new-rescan, num_threads = 8) 104.008 ± 0.127 103.934 104.154 1.00
    build/bin/bitcoin-cli rescanblockchain 500000 900000 (commit = new-rescan, num_threads = 9) 104.788 ± 1.882 103.430 106.937 1.01 ± 0.02
    build/bin/bitcoin-cli rescanblockchain 500000 900000 (commit = new-rescan, num_threads = 16) 103.978 ± 0.161 103.801 104.116 1.00 ± 0.00

    <details> <summary>System information:</summary>

    Architecture:             x86_64
      CPU op-mode(s):         32-bit, 64-bit
      Address sizes:          46 bits physical, 48 bits virtual
      Byte Order:             Little Endian
    CPU(s):                   20
      On-line CPU(s) list:    0-19
    Vendor ID:                GenuineIntel
      Model name:             Intel(R) Core(TM) Ultra 7 265
        CPU family:           6
        Model:                198
        Thread(s) per core:   1
        Core(s) per socket:   1
        Socket(s):            20
        Stepping:             2
        CPU(s) scaling MHz:   41%
        CPU max MHz:          4800.0000
        CPU min MHz:          800.0000
        BogoMIPS:             4761.60
    

    </details>

    The improvements seem to peak at 5x despite the machine having an excess number of Cores (20).

    Further benchmarks were performed using a python script with custom chains designed with payments at specified intervals, and the following graph was produced:

    <img width="4760" height="8950" alt="benchmark_comparison" src="https://github.com/user-attachments/assets/43ab59be-d3e2-4cd0-8035-f0ff076ae315" />

    <details> <summary>This graph was produced from a laptop with the following CPU specifications:</summary>

    Architecture:                x86_64
      CPU op-mode(s):            32-bit, 64-bit
      Address sizes:             48 bits physical, 48 bits virtual
      Byte Order:                Little Endian
    CPU(s):                      16
      On-line CPU(s) list:       0-15
    Vendor ID:                   AuthenticAMD
      Model name:                AMD Ryzen 9 8945HS w/ Radeon 780M Graphics
        CPU family:              25
        Model:                   117
        Thread(s) per core:      2
        Core(s) per socket:      8
        Socket(s):               1
        Stepping:                2
        Frequency boost:         enabled
        CPU(s) scaling MHz:      63%
        CPU max MHz:             5263.0000
        CPU min MHz:             400.0000
    

    </details>

    All materials for the custom benchmarks can be found here.

    Although 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 amount of block hashes that can be held in memory for processing to 1000 (not configurable by the user).

    All benchmarks were performed against https://github.com/bitcoin/bitcoin/pull/34400/commits/68f030eef7b14d5ac6372a12864a813317ae0f4f instead of master because -walletpar is implemented here, and the benchmark scripts would otherwise break, or more complicated scripts would be required to accommodate master.

  2. DrahtBot commented at 7:46 PM on January 24, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/34400.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    Concept ACK w0xlt, ismaelsadeeq

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #34917 (wallet: mark bip125-replaceable deprecated, remove walletrbf argument by rkrux)
    • #33008 (wallet: support bip388 policy with external signer by Sjors)
    • #32489 (wallet: Add exportwatchonlywallet RPC to export a watchonly version of a wallet by achow101)
    • #31260 (scripted-diff: Type-safe settings retrieval by ryanofsky)
    • #30343 (wallet, logging: Replace WalletLogPrintf() with LogInfo() by ryanofsky)
    • #27865 (wallet: Track no-longer-spendable TXOs separately by achow101)
    • #25722 (refactor: Use util::Result class for wallet loading by ryanofsky)

    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.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

    LLM Linter (✨ experimental)

    Possible typos and grammar issues:

    • @pre ... (and the optional stop_block) ... -> replace stop_block with max_height (or the actual stopping parameter) [the comment refers to stop_block, but the function signature/documentation uses max_height; readers may be unable to tell what the “stop” condition actually is without guessing].

    <sup>2026-04-14 12:49:01</sup>

  3. DrahtBot added the label CI failed on Jan 24, 2026
  4. DrahtBot commented at 8:44 PM on January 24, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task test max 6 ancestor commits: https://github.com/bitcoin/bitcoin/actions/runs/21320629356/job/61369934184</sub> <sub>LLM reason (✨ experimental): Compilation failed due to an unused private member (m_thread_pool) in wallet.h being treated as an error under -Werror.</sub>

    <details><summary>Hints</summary>

    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.

    </details>

  5. Eunovo force-pushed on Jan 25, 2026
  6. DrahtBot removed the label CI failed on Jan 25, 2026
  7. Eunovo renamed this:
    Parallel Fast Rescan (approx 5x speed up with 16 threads)
    wallet: parallel fast rescan (approx 5x speed up with 16 threads)
    on Jan 27, 2026
  8. DrahtBot added the label Wallet on Jan 27, 2026
  9. luke-jr commented at 4:50 AM on January 29, 2026: member

    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?

  10. Eunovo commented at 8:42 AM on January 29, 2026: contributor

    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?

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

  11. DrahtBot added the label Needs rebase on Feb 4, 2026
  12. in src/wallet/scan.cpp:203 in ef847e8bce outdated
     198 | +        // If m_max_blockqueue_size blocks have been filtered,
     199 | +        // stop reading more blocks for now, to give the
     200 | +        // main scanning loop a chance to update progress
     201 | +        // and erase some blocks from the queue.
     202 | +        if (m_continue && completed < m_max_blockqueue_size) m_continue = ReadBlockHash(result);
     203 | +        else if (!futures.empty()) thread_pool->ProcessTask();
    


    bvbfan commented at 8:59 AM on February 8, 2026:

    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.


    Eunovo commented at 11:40 AM on February 8, 2026:

    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.


    bvbfan commented at 6:29 PM on February 9, 2026:

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


    Eunovo commented at 9:34 AM on February 10, 2026:

    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.


    Eunovo commented at 12:59 PM on April 14, 2026:

    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.

  13. Eunovo force-pushed on Feb 8, 2026
  14. DrahtBot removed the label Needs rebase on Feb 8, 2026
  15. Eunovo force-pushed on Feb 11, 2026
  16. Eunovo commented at 5:20 PM on February 11, 2026: contributor

    #33689 has been merged; the cherry-picked Threadpool commit has been removed.

  17. furszy commented at 8:43 PM on February 11, 2026: member

    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. Similar to what you did in 633531614f69de49733642fd19cc9eba830fbdea, but into a separate PR so we can first land some good building blocks for this to happen.

    Some quick pseudo-code structuring how I imagine it, which is similar to yours:

    Scan(wallet, start_block_hash, end_block_hash, fn_filter_block, fn_process_block, interrupt) {
         it_current_hash = start_block_hash;
    
         while (it_current_hash != end_block_hash || interrupt) {
             // Skip block if needed (this function contains the BlockFilterIndex check if enabled)
             if (fn_filter_block(it_current_hash)) continue;
        
             // (this is more or less how we currently do it, we fetch the block and the next block hash at the same time)
             block = chain.find_block(it_current_hash).next_block(it_current_hash);
        
             // (inside this function the wallet will digest the block update the filter and save progress if needed)
             fn_process_block(block);
          }
    }
    
  18. w0xlt commented at 8:12 AM on February 21, 2026: contributor

    Concept ACK

  19. Eunovo force-pushed on Feb 26, 2026
  20. Eunovo commented at 4:21 PM on February 26, 2026: contributor

    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. Similar to what you did in 6335316, but into a separate PR so we can first land some good building blocks for this to happen.

    I moved the test change into #34667 and the ScanForWalletTransactions refactor into #34681. I'll be putting this PR in draft while those PRs are open.

  21. Eunovo marked this as a draft on Feb 26, 2026
  22. ismaelsadeeq commented at 2:34 PM on March 13, 2026: member

    Concept ACK

    I attempted to reproduce the benchmarks for this But I did not use hyperfine, I used time

    System: AMD Ryzen 7 7700, 16 cores, 64GB RAM, mainnet, blocks 500000–900000, 3 runs averaged.

    Baseline (881ebc4730ad15bd26e3e32ee3c9ba9d6e05552d): single-threaded fast rescan, -walletpar has no effect — consistently ~283s regardless of thread count.

    Parallel scan (48154b87e2fb303ca0f3d46da29a3fbbf8758a06): fast rescan with threadpool parallelism enabled via -walletpar.

    num_threads baseline parallel speedup
    1 282.618s 288.214s 0.98x (slight overhead)
    2 282.618s 187.485s 1.51x
    4 282.618s 119.575s 2.37x
    8 282.618s 81.565s 3.47x
    16 282.618s 62.965s 4.53x
    • Speedup scales well up to 16 threads, going from ~283s down to ~63s — a 4.5x improvement.
    • 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.

    Steps to reproduce

    1. Restart bitcoind with -blockfilterindex=1 till it's done.
    2. create a new wallet test
    3. Stop the node
    4. Save the script below as bench_script.sh

    <details> <summary>script</summary>

    #!/usr/bin/env bash
    set -euo pipefail
    
    COMMITS=(881ebc4730ad15bd26e3e32ee3c9ba9d6e05552d 48154b87e2fb303ca0f3d46da29a3fbbf8758a06)
    THREADS=(1 2 4 8 16)
    RUNS=3
    WALLET_NAME="test"
    DATADIR="$HOME/.bitcoin"
    RESULTS_CSV="results.csv"
    RESULTS_MD="results.md"
    
    echo "commit,num_threads,run,seconds" > "$RESULTS_CSV"
    
    for commit in "${COMMITS[@]}"; do
      short="${commit:0:7}"
      git checkout "$commit"
      cmake --build build -j 20
    
      for num_threads in "${THREADS[@]}"; do
        echo "=== commit=$short num_threads=$num_threads ==="
    
        for run in $(seq 1 $RUNS); do
          echo "  run $run/$RUNS"
    
          # start node
          build/bin/bitcoind -blockfilterindex=1 -walletpar="$num_threads" -daemonwait
          build/bin/bitcoin-cli loadwallet "$WALLET_NAME"
    
          build/bin/bitcoin-cli rescanblockchain 500000 900000
    
          # parse timing from debug log: "Rescan completed in 284737ms"
          ms=$(grep "Rescan completed in" "$DATADIR/debug.log" | tail -1 | grep -oP '\d+(?=ms)')
          seconds=$(python3 -c "print(f'{$ms / 1000:.3f}')")
    
          echo "$short,$num_threads,$run,$seconds" >> "$RESULTS_CSV"
          echo "  -> ${seconds}s"
    
          build/bin/bitcoin-cli stop
          # wait for clean shutdown
          while build/bin/bitcoin-cli ping 2>/dev/null; do sleep 1; done
          sleep 5
        done
      done
    done
    
    # markdown table with averages
    python3 - "$RESULTS_CSV" "$RESULTS_MD" <<'EOF'
    import sys, csv
    from collections import defaultdict
    
    infile, outfile = sys.argv[1], sys.argv[2]
    
    rows = list(csv.DictReader(open(infile)))
    groups = defaultdict(list)
    for r in rows:
        groups[(r['commit'], r['num_threads'])].append(float(r['seconds']))
    
    with open(outfile, 'w') as f:
        f.write("| commit | num_threads | run1 | run2 | run3 | mean |\n")
        f.write("|--------|-------------|------|------|------|------|\n")
        for (commit, threads), times in sorted(groups.items()):
            mean = sum(times) / len(times)
            runs = " | ".join(f"{t:.3f}" for t in times)
            f.write(f"| {commit} | {threads} | {runs} | {mean:.3f} |\n")
    
    print(f"Written {outfile}")
    EOF
    
    echo "Done. Results in $RESULTS_CSV and $RESULTS_MD"
    

    </details>

    1. Make the script executable chmod +x bench_script.sh
    2. Execute the script ./bench_script.sh
    3. 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.

    The current steps to reproduce in the description are stale because the commit hashes have changed since your force pushes.

  23. in src/wallet/scan.cpp:191 in 48154b87e2
     195 | +        // Submit jobs to the threadpool in batches of at most `workers_count` size.
     196 | +        // This prevents over-submission: if we queued all jobs upfront and the filtered
     197 | +        // block range is smaller than expected, worker threads would process blocks
     198 | +        // that get discarded, wasting CPU cycles.
     199 | +        const size_t job_gap = workers_count - futures.size();
     200 | +        if (job_gap > 0 && i < m_blocks.size()) {
    


    rkrux commented at 2:03 PM on March 16, 2026:

    In 48154b87e2fb303ca0f3d46da29a3fbbf8758a06 "wallet: check blockfilters in parallel"

    These two conditions in this check seem redundant with the same two conditions in the following for loop.


    Eunovo commented at 1:43 PM on March 18, 2026:

    Fixed.

  24. in src/wallet/scan.cpp:194 in 48154b87e2
     198 | +        // that get discarded, wasting CPU cycles.
     199 | +        const size_t job_gap = workers_count - futures.size();
     200 | +        if (job_gap > 0 && i < m_blocks.size()) {
     201 | +            for (size_t j = 0; j < job_gap && i < m_blocks.size(); ++j, ++i) {
     202 | +                auto block = m_blocks[i];
     203 | +                futures.emplace_back(*thread_pool->Submit([&filter, block = std::move(block)]() {
    


    rkrux commented at 2:08 PM on March 16, 2026:

    In 48154b87e2fb303ca0f3d46da29a3fbbf8758a06 "wallet: check blockfilters in parallel"

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

    https://github.com/bitcoin/bitcoin/blob/ff7cdf633e375f151cccbcc78c7add161b3d29b8/src/util/threadpool.h#L199-L220

    diff --git a/src/wallet/scan.cpp b/src/wallet/scan.cpp
    index 6b776930f1..2295a242a1 100644
    --- a/src/wallet/scan.cpp
    +++ b/src/wallet/scan.cpp
    @@ -147,6 +147,20 @@ std::optional<std::pair<size_t, size_t>> ChainScanner::ReadNextBlocks(const std:
             return std::make_pair<size_t, size_t>(0, m_blocks.size());
         }
         filter->UpdateIfNeeded();
    +
    +    auto block_matcher = [&filter](uint256 block_hash) {
    +        const auto matches_block{filter->MatchesBlock(block_hash)};
    +        if (matches_block.has_value()) {
    +            if (*matches_block) {
    +                return FilterRes::FILTER_MATCH;
    +            } else {
    +                return FilterRes::FILTER_NO_MATCH;
    +            }
    +        } else {
    +            return FilterRes::FILTER_NO_FILTER;
    +        }
    +    }
    +
         auto* thread_pool = m_wallet.m_thread_pool;
         // ThreadPool pointer should never be null here
         // during normal operation because it should
    @@ -187,23 +201,13 @@ std::optional<std::pair<size_t, size_t>> ChainScanner::ReadNextBlocks(const std:
             // This prevents over-submission: if we queued all jobs upfront and the filtered
             // block range is smaller than expected, worker threads would process blocks
             // that get discarded, wasting CPU cycles.
    -        const size_t job_gap = workers_count - futures.size();
    -        if (job_gap > 0 && i < m_blocks.size()) {
    -            for (size_t j = 0; j < job_gap && i < m_blocks.size(); ++j, ++i) {
    -                auto block = m_blocks[i];
    -                futures.emplace_back(*thread_pool->Submit([&filter, block = std::move(block)]() {
    -                    const auto matches_block{filter->MatchesBlock(block.first)};
    -                    if (matches_block.has_value()) {
    -                        if (*matches_block) {
    -                            return FilterRes::FILTER_MATCH;
    -                        } else {
    -                            return FilterRes::FILTER_NO_MATCH;
    -                        }
    -                    } else {
    -                        return FilterRes::FILTER_NO_FILTER;
    -                    }
    -                }));
    +        auto to_submit_jobs_count = std::min(workers_count - futures.size(), m_blocks - i);
    +        if (to_submit_jobs_count) {
    +            std::vector<std::function<FilterRes(uint256)>> block_matchers;
    +            for (; i < to_submit_jobs_count; ++i) {
    +                block_matchers.emplace_back(block_matcher(m_blocks[i].first));
                 }
    +            futures.emplace_back(*thread_pool->Submit(std::move(block_matchers)));
             }
     
             // If m_max_blockqueue_size blocks have been filtered,
    
    

    Eunovo commented at 12:18 PM on March 18, 2026:

    I will check this when I rebase on master.


    Eunovo commented at 12:50 PM on April 14, 2026:

    Done.

  25. in src/wallet/scan.cpp:220 in 48154b87e2 outdated
     224 | +            if (next_block) m_blocks.emplace_back(*next_block);
     225 | +        }
     226 | +        else if (!futures.empty()) {
     227 | +            // Join work processing instead of waiting idly.
     228 | +            thread_pool->ProcessTask();
     229 | +        }
    


    rkrux commented at 2:19 PM on March 16, 2026:

    In 48154b8 "wallet: check blockfilters in parallel"

    I'm doubtful that putting the controller (non-worker) thread to process the threadpool tasks is helpful.

    This 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?


    Eunovo commented at 12:05 PM on March 18, 2026:

    I'm doubtful that putting the controller (non-worker) thread to process the threadpool tasks is helpful.

    It is helpful. I had better results when I put the thread to work vs when I didn't.

    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?

    True, 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


    rkrux commented at 12:16 PM on March 18, 2026:

    I was doubtful of the helpfulness of using processtask because of this cross-wallet/rpc operation.

    this creates an argument to ditch the shared threadpool and create one at the begining of the scan process

    Interesting, I had thought of not using the processtask function in the current thread pool setup as a way. But an intra-wallet scan operation specific threadpool seems like a good alternative to consider. I wll think of its implications.


    Eunovo commented at 12:54 PM on April 14, 2026:

    I have changed the implementation substantially. I have moved away from a shared ThreadPool to one that is initialized before scanning and discarded after.

    ProcessTask() is now only used when the main thread needs to complete the queued up jobs before the processing the selected blocks.

  26. in src/wallet/scan.cpp:183 in 48154b87e2
     187 | +            }
     188 |  
     189 | -    const auto& [block_hash, block_height] = m_blocks[0];
     190 | -    auto matches_block{filter->MatchesBlock(block_hash)};
     191 | +            if (!range.has_value()) range = std::make_pair(current_block_index, current_block_index + 1);
     192 | +            else range->second = current_block_index + 1;
    


    rkrux commented at 2:57 PM on March 16, 2026:

    In 48154b8 "wallet: check blockfilters in parallel"

    It appears that current_block_index + 1 is equal to completed due to a int current_block_index = completed - 1; above.


    Eunovo commented at 1:43 PM on March 18, 2026:

    Fixed.

  27. rkrux commented at 3:05 PM on March 16, 2026: contributor

    I've looked only at the 48154b8 "wallet: check blockfilters in parallel" commit partially.

  28. Eunovo force-pushed on Mar 18, 2026
  29. achow101 referenced this in commit 696b5457c5 on Mar 24, 2026
  30. DrahtBot added the label Needs rebase on Mar 24, 2026
  31. Eunovo force-pushed on Apr 6, 2026
  32. DrahtBot removed the label Needs rebase on Apr 6, 2026
  33. DrahtBot added the label CI failed on Apr 6, 2026
  34. Eunovo force-pushed on Apr 12, 2026
  35. Eunovo force-pushed on Apr 12, 2026
  36. wallet: move scanning logic to wallet/scan.cpp
    Move rescan logic to new class to allow the scanning loop
    to be simplified by delegating some logic to member
    functions in future commits.
    
    CWallet::ScanForWalletTransactions impl is moved to scan.cpp
    to prevent circular dependency of the form
    "wallet/wallet -> wallet/scan -> wallet/wallet".
    16b1ca5efa
  37. wallet/scan: extract block filtering logic to ShouldFetchBlock method
    Pure extraction of block filter matching logic into a dedicated method.
    This prepares for further refactoring of the scanning loop.
    ddb5a07eb1
  38. wallet/scan: extract block scanning logic to ScanBlock method
    Pure extraction of block transaction processing logic into a dedicated
    method. This isolates the logic for fetching blocks and syncing their
    transactions.
    4612fb4b2f
  39. wallet/scan: extract block iteration logic into ReadNextBlock
    Extract block reading logic into ReadNextBlock method which returns
    std::optional<pair<hash, height>>.
    Introduce m_next_block member to track iteration state, consolidating
    the loop termination logic into ReadNextBlock.
    0371771e7b
  40. wallet/scan: extract progress tracking helper methods
    Extract UpdateProgress, UpdateTipIfChanged, and LogScanStatus methods.
    Move progress tracking variables to class members. This simplifies the
    main scanning loop and groups related progress tracking logic together.
    30fc35fa72
  41. wallet/scan: simplify ChainScanner::Scan main loop
    Extract ProcessBlock method and simplify the main scanning loop. This
    final refactoring demonstrates the clean separation of concerns with
    each helper method handling a specific aspect of the scanning process.
    e98e3a76eb
  42. wallet: Add wallet parallel processing threads param
    This parameter will be used in a future commit to determine
    the number of threads to use for parallel fast-rescan.
    
    `8` threads is choosen as a reasonable `MAX_WALLETPAR`,
    benchmarks show 5x improvement on fast-rescan with `8` threads.
    68f030eef7
  43. Eunovo force-pushed on Apr 12, 2026
  44. wallet/scan: combine block iteration and filtering in ReadNextBlocks
    This commit refactors the block filtering logic from ShouldFetchBlock
    into a new ReadNextBlocks method that works with a queue of blocks
    (m_blocks). This prepares the code for parallel block filter checking
    while keeping the current single-threaded behaviour.
    ceb9cbb4aa
  45. Eunovo force-pushed on Apr 12, 2026
  46. Eunovo renamed this:
    wallet: parallel fast rescan (approx 5x speed up with 16 threads)
    wallet: parallel fast rescan (approx 5x speed up with 8 threads)
    on Apr 13, 2026
  47. wallet/scan: introduce FilterExecutor for block filter evaluation
    This commit extracts that logic into a FilterExecutor interface with an InlineFilterExecutor
    implementation that runs filter checks synchronously on the calling thread.
    
    A future ParallelFilterExecutor will slot in to use a ThreadPool when m_wallet_par > 1.
    This will limit the parallel execution code to ParallelFilterExecutor; keeping ReadNextBlocks clean.
    6cf277998e
  48. wallet: check blockfilters in parallel
    This commit implements parallel block filter checking using the wallet
    threadpool. The main thread reads block hashes and queues them while
    worker threads check filters in parallel.
    
    Synchronization:
    - Operations requiring cs_wallet (GetLastBlockHeight, SyncTransaction)
      remain on the main thread since cs_wallet is a RecursiveMutex and
      ScanForWalletTransactions is called from AttachChain which locks cs_wallet
    
    Thread safety:
    - All futures  are waited on before returning to
      avoid data races on `FastWalletRescanFilter::m_filter_set`.
    
    Benchmarks show considerable improvement (approx 5x with 8 threads).
    590b821ac1
  49. wallet: save and log scan progress every `INTERVAL_TIME`
    This commit stops the submission of blocks to the filter executor
    to allow progress updates and give ScanBlock() a chance to save progress.
    b62e9e7391
  50. Eunovo force-pushed on Apr 14, 2026
  51. DrahtBot removed the label CI failed on Apr 14, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-04-17 09:12 UTC

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