validation, net: Process blocks asynchronously and reduce cs_main contention #36244

pull w0xlt wants to merge 20 commits into bitcoin:master from w0xlt:validation/async-block-processing changing 41 files +3128 −288
  1. w0xlt commented at 7:42 AM on September 14, 2026: contributor

    The shared P2P message thread currently waits for block processing to finish before it can process messages from other peers.

    This PR moves block processing to a dedicated worker after the initial checks, following the approach of #18963 and #16324:

    • Initial checks still run on the P2P message thread and decide whether a block should enter the processing queue.
    • A single worker processes queued blocks one at a time, in the order they are added to the queue.
    • Each peer can have only one block queued or being processed at a time. The node handles the processing result and any required peer penalties before processing that peer’s next incoming message.
    • Tracking block requests and reconstructing blocks from compact-block transaction responses no longer require cs_main, the chain lock.
    • When cs_main is busy, SendMessages() defers work such as requesting headers, blocks or transactions and announcing new blocks or transactions. This allows the P2P message thread to move on to other peers.

    A controlled 10,000-block IBD benchmark showed these response-time improvements, measured as the median of four per-run medians:

    Probe Download connections Baseline → This branch Reduction
    PING/PONG 1 41.8 → 0.77 ms 98%
    Block serving 1 39.0 → 26.5 ms 32%
    Block serving 4 107.0 → 58.5 ms 45%

    With four download connections, the median time to download and validate the 10,000 tested blocks during IBD fell by approximately 5%, from 473.6 s to 450.3 s.

    The comparison used matching Clang 18 Release builds of baseline 4519933391dd and this branch, mainnet blocks 910,489 - 920,488, one local source node, and -assumevalid=0 -blocksonly=1. Each setup ran four times per build. PINGs ran at 10/s; the block probe requested the same previously validated block after warmup, at most once per second, timing receipt of the full block.

    These results apply to this controlled IBD workload; they do not establish gains for other services, hardware, or network conditions. Performance outside IBD was not measured.

    Tests and test infrastructure account for approximately 67% of the diff. Production code and build integration account for 1,132 changed lines (883 additions and 249 deletions).

    I’m opening this PR as a draft primarily to gather feedback on the overall approach.

  2. DrahtBot commented at 7:42 AM on September 14, 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/36244.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process. A summary of reviews will appear here.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

    LLM Linter (✨ experimental)

    Possible places where named args for integral literals may be used (e.g. func(x, /*named_arg=*/0) in C++, and func(x, named_arg=0) in Python):

    • ProcessNewBlock(..., true, true) in src/test/blockfilter_index_tests.cpp
    • ProcessNewBlock(..., true, true) in src/test/peerman_tests.cpp
    • ProcessNewBlock(..., true, true) in src/test/util/mining.cpp
    • ProcessNewBlock(..., true, true) in src/test/validation_block_tests.cpp
    • ProcessNewBlock(..., true, true) in src/test/validation_chainstate_tests.cpp
    • ProcessNewBlock(..., true, true) in src/test/validationinterface_tests.cpp

    Possible places where comparison-specific test macros should replace generic comparisons:

    • [src/test/validation_queue_tests.cpp] BOOST_CHECK_THROW(queue.Start(), std::logic_error); -> Consider BOOST_CHECK_EXCEPTION(queue.Start(), std::logic_error, HasReason("...")) if the exact failure condition/message is known.
    • [src/test/validation_queue_tests.cpp] BOOST_CHECK_THROW(stopped.Start(), std::logic_error); -> Consider BOOST_CHECK_EXCEPTION(stopped.Start(), std::logic_error, HasReason("...")) if the exact failure condition/message is known.
    • [src/test/validation_queue_tests.cpp] BOOST_CHECK_THROW(interrupted.Start(), std::logic_error); -> Consider BOOST_CHECK_EXCEPTION(interrupted.Start(), std::logic_error, HasReason("...")) if the exact failure condition/message is known.
    • [src/test/validationinterface_tests.cpp] BOOST_CHECK_THROW(m_node.chainman->StartBlockProcessing(), std::logic_error); -> Consider BOOST_CHECK_EXCEPTION(m_node.chainman->StartBlockProcessing(), std::logic_error, HasReason("...")) if the exact failure condition/message is known.

    <sup>2026-09-14 08:03:12</sup>

  3. validation: Make block check caches atomic
    Group the three successful block-check caches in a CBlock-specific,
    value-copyable type. Use atomic loads and stores so immutable blocks can
    be checked and copied concurrently with independent validation states.
    Keep the payload copy and move operations compiler-generated.
    
    The bits memoize successful checks; they do not publish payload changes
    or guarantee exactly-once checking. Callers must keep block contents and
    effective consensus parameters unchanged while sharing a block. Copies
    may observe the independently valid success bits at different times.
    
    Keep the initial ProcessNewBlock lock placement unchanged.
    4c837cab18
  4. validation: Check blocks before taking cs_main
    Serialize the initial CheckBlock call with a private manager mutex and
    release it before waiting for cs_main. Keep initial rejection handling
    and synchronous validation notifications under cs_main, with master
    acceptance, storage, import and activation locking unchanged.
    
    This narrows the initial checking lock; it does not unlock acceptance or
    block-file writes or promise FIFO acceptance for concurrent callers.
    69cd2f83bc
  5. refactor: Separate block processing completion
    Separate ProcessNewBlock execution from subsequent peer-state updates,
    while continuing to call both synchronously at all three submission sites.
    
    Keep processing success distinct from new_block. Only new_block controls
    the existing timestamp and request housekeeping, including write errors.
    Move optimistic compact reconstruction's additional transaction-valid
    request cleanup into the continuation, using a fresh index lookup under
    cs_main. Other submission paths do not gain that cleanup.
    7cd60b561f
  6. refactor: Extract block storage admission 9649d641fa
  7. refactor: Separate block admission from persistence
    Move header acceptance, storage policy, block/contextual checks, and
    invalid-block handling into PreWriteCheckBlock(). Return admission success
    separately from whether block data should be stored, distinguishing
    rejections, successful no-ops, and blocks eligible for persistence.
    
    Keep AcceptBlock() responsible for synchronous persistence with its
    existing locking and new_block semantics. Preserve ProcessNewBlock()'s
    initial check, notification ordering, and chain activation.
    95e72d2268
  8. refactor: Extract block persistence
    Move block notifications, disk writes, index updates and flushing into
    StoreBlock(), called synchronously by AcceptBlock() after admission.
    Preserve cs_main locking, the existing disk-position import path, and
    new_block assignment before storage.
    
    This exposes the persistence stage for reuse by a future validation worker.
    26185d2c7a
  9. refactor: Expose initial block validation state
    Pass a caller-owned BlockValidationState through ProcessNewBlock's
    CheckBlock and AcceptBlock stages, keeping chain-activation states local.
    Update callers to supply fresh states while preserving synchronous
    BlockChecked delivery, processing success, and new_block semantics.
    
    Cover initial rejection, valid and duplicate submissions, and a block
    that passes admission but fails validation when connected. This prepares
    callers to distinguish initial validation results from future completion,
    following the API preparation in PR #18963.
    53261fca6c
  10. refactor: Return block processing outcomes together
    Move BlockProcessingResult from PeerManagerImpl into the validation API
    and return it from ProcessNewBlock instead of returning bool and writing
    new_block through an output parameter. Update all callers while keeping
    validation, notifications, and peer completion synchronous.
    
    Preserve both outcomes independently, including new_block remaining true
    on a write failure. Neither field implies full consensus validity.
    Extend the initial-state regression test to check both result fields and
    exercise a real block-file write failure. Capture and assert its expected
    error notification to keep successful test output clear.
    
    This prepares the API for a future carrying both completion outcomes.
    3ce7c7ad3e
  11. refactor: Return block processing outcomes through a future
    Wrap BlockProcessingResult in a future, fulfilling its promise at each
    existing return. Keep validation synchronous and fulfill successful
    completion after active and historical chainstate activation.
    
    Update callers to consume the ready future before their existing
    completion actions, preserving initial-state reporting, both result
    fields, and notification ordering. Explicitly discard unused test
    results for libc++ compatibility. Check future validity and immediate
    readiness in the existing outcome regression scenarios.
    124fc6be47
  12. net: Track pending block processing per peer
    Retain completion futures for full blocks, completed compact blocks,
    and optimistic reconstruction. Poll without waiting, pausing new-message
    dequeuing and the normal send loop for the submitting peer while its
    completion is pending. Continue servicing previously queued getdata and
    orphan work before checking for pending block processing.
    
    Keep records in the peer manager by peer ID and poll them even when no
    peers remain, so download cleanup survives source disconnection. Preserve
    newer source attribution, check optimistic transaction validity at
    completion, and apply punishment before dequeuing another message.
    Handle failed futures without letting exceptions escape the message loop.
    
    Add controlled-future coverage for peer pause/resumption, queued getdata
    work, ordinary messages from other peers, disconnection, completion
    ordering, and errors. Validation still executes synchronously and returns
    ready futures.
    a96fca16fb
  13. validation: Add block processing queue
    Add a single-worker FIFO with owned blocks, processing callbacks, and
    completion futures. Accept submissions while running and complete futures
    with either a result or the callback's exception. Report inactive or
    stopping queues without retaining the submitted work.
    
    Make ChainstateManager own the queue. Support explicit startup, interruption,
    and draining shutdown, and stop before node and test dependencies are
    removed. Keep the kernel context alive throughout manager destruction.
    Move BlockProcessingResult into the queue header without changing its API.
    
    Add tests covering lifecycle, ownership, exceptions, FIFO processing,
    concurrent producers and shutdown, and callback submission. Production
    startup leaves the worker inactive and ProcessNewBlock remains synchronous;
    validation admission, worker processing, and completion wakeups follow
    separately.
    5e2ae8e0ee
  14. net: Account for blocks pending processing
    Exclude received blocks awaiting processing completion from automatic
    download selection and compact-block reconstruction. Avoid attributing
    download stalls to peers when local processing holds back or advances the
    download window.
    f041ae5ca1
  15. validation: Route admitted blocks through the processing queue
    Run initial validation and admission on the caller, then submit blocks needing
    storage for persistence and chain activation. Retain inline execution before
    the queue is started for kernel, fuzz, and other standalone callers.
    
    Return initial errors directly and deferred cached-invalid outcomes through the
    future so peer attribution remains tied to each submission. Notify listeners
    when completions become available and adapt blocking callers to the split
    result contract.
    b482714647
  16. validation: Process admitted blocks on a worker
    Start the block-processing queue during node and ordinary test setup. Keep fuzz
    and kernel callers on the inline path, and stop the worker before its callback,
    networking, mempool, and index dependencies are destroyed.
    
    Add coverage for queued parent invalidation, duplicate rejection attribution,
    pending downloads and stalling, disconnected sources, and worker-gate and
    subscriber lifetimes.
    f0d7860a17
  17. validation: Queue BlockChecked notifications
    Deliver BlockChecked through the validation task runner, retaining the
    block and validation state until delivery. Keep peer completions pending
    until a queued marker has run so validation feedback precedes source
    cleanup and message processing resumption.
    
    Wait for queued results before unregistering mining catchers. Adapt the
    network fuzz targets to a scoped scheduler and serial runner, with
    explicit callback and completion draining between messages, so callbacks
    can acquire cs_main without blocking the emitter under that lock.
    
    Cover queued ownership, callback ordering, peer teardown, and the fuzz
    runner's deferred delivery.
    c93b210876
  18. net: Protect block sources with a dedicated mutex
    Move mapBlockSource from cs_main to a PeerManager bookkeeping mutex,
    following the source-attribution change in PR #18963. Source insertion and
    ordinary completion cleanup can run while validation holds cs_main.
    
    Acquire cs_main before the new mutex where chain state is also needed,
    matching the modern PeerManager lock order. Preserve source ownership,
    compact-block punishment exceptions, and callback ordering. Add a
    regression that completes source cleanup while another thread holds
    cs_main.
    4aacbe338c
  19. net: Move block download tracking into Peer
    Move per-peer in-flight blocks and download/stalling timers out of
    CNodeState, following PR #16324. Protect them, mapBlocksInFlight, and the
    downloading-peer count with the block bookkeeping mutex.
    
    Serialize request registration and cleanup with peer removal, retaining
    the Peer until its request entries are cleared. Keep cs_main for chain
    dependent reads and preserve request selection, limits, and timeouts.
    
    When new_block is true, skip the redundant optimistic validity recheck
    and its chain-lock acquisition after request cleanup. Retain the recheck
    for completions with new_block=false.
    
    Add a regression that registers and completes requests for both full
    blocks and optimistic reconstructions while another thread holds
    cs_main. Freeze its node clock so the completion timestamp assertion
    cannot race a wall-clock second boundary.
    f05446f295
  20. net: Avoid unnecessary cs_main waits in message processing
    Skip taking the chain lock when a peer has no orphan transactions to
    reconsider. Defer the remaining send work when cs_main is unavailable so
    the shared message handler can service other peers.
    
    Keep fee-filter calculation inside the successful chain-lock scope so
    block validation cannot acquire the mempool lock before the calculation.
    Require cs_main in the helper's lock annotation.
    
    Add a regression covering PING/PONG output, punishment, and deferred
    header requests while another thread holds cs_main.
    d1e2118b0f
  21. net: Remove cs_main from blocktxn reconstruction
    Use the validated header already stored with the download request to
    determine SegWit activation. This lets BLOCKTXN bookkeeping and
    reconstruction proceed under m_block_mutex while another block is being
    validated. Release the lock before the existing admission checks.
    
    Add a regression covering unexpected responses and reconstruction
    failures while another thread holds cs_main, including full-block
    fallback.
    1bda769c9e
  22. net: Clear optimistic block requests after admission
    Clear other peers' requests after successful initial admission of an
    optimistically reconstructed compact block, following #16324. Pending
    processing still suppresses automatic downloads and pauses the source
    until its completion and validation callbacks have been handled.
    
    Remove the optimistic flag from pending completions and the remaining
    completion-time validity recheck under cs_main. Cover admission with a
    parked worker, queued duplicates, source disconnection, initial rejection,
    and interrupted submission.
    
    Validation: 41 unit tests passed in regular and ThreadSanitizer builds;
    six compact-block, invalid-block and unrequested-block functional tests
    passed.
    34d9d3d6ee
  23. w0xlt force-pushed on Sep 14, 2026
  24. w0xlt marked this as a draft on Sep 14, 2026
  25. DrahtBot added the label Needs rebase on Sep 14, 2026
  26. DrahtBot commented at 3:47 PM on September 14, 2026: contributor

    <!--cf906140f33d8803c4a75a2196329ecb-->

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


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-09-14 17:51 UTC

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