net: optimize compact block extra tx iteration #35670

pull l0rinc wants to merge 1 commits into bitcoin:master from l0rinc:l0rinc/cmpctblock-grow-extra-tx-cache changing 1 files +7 −5
  1. l0rinc commented at 7:30 PM on July 6, 2026: contributor

    Problem: vExtraTxnForCompact gives compact block reconstruction one more source for recently removed transactions. Before this PR, the first insertion resized the cache to its configured capacity, so before the cache was full, PartiallyDownloadedBlock::InitData() also scanned default {Wtxid::ZERO, nullptr} entries that had never been added to the cache. Those unused entries could still participate in reconstruction short-id matching.

    Fix: Reserve the configured capacity and append entries until the cache is full. Once full, keep the same ring-buffer overwrite behavior, configured maximum, and replacement order.

    Risk: Triggering the affected duplicate-match branch requires a block-specific 6-byte short-id collision while the extra-txn cache still contains default null slots. With a 16-thread benchmark of the CBlockHeaderAndShortTxIDs nonce-grinding path, the 50% collision time was ~178 days on my machine.

  2. net: optimize compact block extra tx iteration
    `vExtraTxnForCompact` was resized to its configured capacity on the first insertion.
    Before the ring was filled, compact block reconstruction scanned default `{Wtxid::ZERO, nullptr}` entries created for unused slots.
    
    Reserve the configured capacity and append entries until the cache is full, then keep the same ring-buffer overwrite behavior.
    This preserves the configured maximum and replacement order while keeping reconstruction from scanning unused capacity.
    1a3cbf1bd2
  3. DrahtBot added the label P2P on Jul 6, 2026
  4. DrahtBot commented at 7:41 PM on July 6, 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/35670.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    ACK darosior, davidgumberg, w0xlt

    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.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  5. darosior approved
  6. darosior commented at 2:09 PM on July 7, 2026: member

    utACK 1a3cbf1bd2c0d4d350063bf5162c3e1c2ec2df30

    Scanning -blockreconstructionextratxn empty values when reconstructing doesn't make sense.

  7. fanquake commented at 2:10 PM on July 7, 2026: member
  8. in src/net_processing.cpp:1910 in 1a3cbf1bd2
    1906 | @@ -1907,11 +1907,13 @@ std::vector<CTransactionRef> PeerManagerImpl::AbortPrivateBroadcast(const uint25
    1907 |  
    1908 |  void PeerManagerImpl::AddToCompactExtraTransactions(const CTransactionRef& tx)
    1909 |  {
    1910 | -    if (m_opts.max_extra_txs <= 0)
    1911 | -        return;
    1912 | -    if (!vExtraTxnForCompact.size())
    1913 | -        vExtraTxnForCompact.resize(m_opts.max_extra_txs);
    1914 | -    vExtraTxnForCompact[vExtraTxnForCompactIt] = std::make_pair(tx->GetWitnessHash(), tx);
    1915 | +    if (m_opts.max_extra_txs == 0) return;
    


    davidgumberg commented at 5:36 PM on July 8, 2026:

    Probably leave this line as is since otherwise we are relying on this clamp always existing:

    https://github.com/bitcoin/bitcoin/blob/32941e131479b4aec1eb606d68a9bcc3f5d7cf3d/src/node/peerman_args.cpp#L20

    And there are no tests that enforce this is how -blockreconstructionextratxn works AFAICT.


    darosior commented at 5:51 PM on July 8, 2026:

    l0rinc commented at 5:51 PM on July 8, 2026:

    I don’t think keeping <= 0 adds protection here, since max_extra_txs is already unsigned. If the args clamp were removed, a negative value would wrap before reaching this guard, so both <= 0 and == 0 would miss it. The local invariant here is just the disabled/zero case before the modulo below. Let me know if I misunderstood what you meant.


    davidgumberg commented at 6:10 PM on July 8, 2026:

    Ah thanks, missed that it was unsigned 🤦

  9. in src/net_processing.cpp:1915 in 1a3cbf1bd2
    1915 | +    if (m_opts.max_extra_txs == 0) return;
    1916 | +    if (vExtraTxnForCompact.size() < m_opts.max_extra_txs) {
    1917 | +        if (vExtraTxnForCompact.empty()) vExtraTxnForCompact.reserve(m_opts.max_extra_txs);
    1918 | +        vExtraTxnForCompact.emplace_back(tx->GetWitnessHash(), tx);
    1919 | +    } else {
    1920 | +        vExtraTxnForCompact[vExtraTxnForCompactIt] = std::make_pair(tx->GetWitnessHash(), tx);
    


    davidgumberg commented at 5:56 PM on July 8, 2026:

    non-blocking-nit:

            vExtraTxnForCompact.at(vExtraTxnForCompactIt) = std::make_pair(tx->GetWitnessHash(), tx);
    

    This is a bit safer I thinks.


    l0rinc commented at 6:02 PM on July 8, 2026:

    Thanks, I prefer leaving that as-is to simplify review.

  10. in src/net_processing.cpp:1912 in 1a3cbf1bd2
    1912 | -    if (!vExtraTxnForCompact.size())
    1913 | -        vExtraTxnForCompact.resize(m_opts.max_extra_txs);
    1914 | -    vExtraTxnForCompact[vExtraTxnForCompactIt] = std::make_pair(tx->GetWitnessHash(), tx);
    1915 | +    if (m_opts.max_extra_txs == 0) return;
    1916 | +    if (vExtraTxnForCompact.size() < m_opts.max_extra_txs) {
    1917 | +        if (vExtraTxnForCompact.empty()) vExtraTxnForCompact.reserve(m_opts.max_extra_txs);
    


    davidgumberg commented at 6:06 PM on July 8, 2026:

    non-blocking-observation-feel-free-to-disregard:

    It seems a little awkward that we are tying together size and capacity in this way, this feels like something that should be happening somewhere in a constructor instead of happening inside of the Add function the first time it's invoked with a check to see if this is the first time Add is being called, but given what already exists I think this is a reasonable solution.


    l0rinc commented at 6:13 PM on July 8, 2026:

    should be happening somewhere in a constructor

    That would make the memory allocation eager, this is meant to do lazy init.

    given what already exists I think this is a reasonable solution

    Yeah, the review is simpler if this PR only focuses on one problem at the time: which is uninitialized iteration here.


    davidgumberg commented at 6:25 PM on July 8, 2026:

    I agree that this approach is lazy with respect to startup, it is "eager" with respect to the extrapool's use in the sense that the first time you add something to the extrapool you allocate memory for the whole pool even if it might take you much more time before you need that memory to be allocated.

    Maybe the right approach here is actually to drop the reserve, it lets us delete a line that doesn't really gain us much, and lets us get even lazier with the allocation.


    l0rinc commented at 6:32 PM on July 8, 2026:

    it lets us delete a line that doesn't really gain us much

    Not sure I follow. The reserve is needed to avoid copying the data as we populate it until it fills (in which case it will likely contain more elements than needed). For 100 elements it doesn't matter that much, but as https://github.com/bitcoinknots/bitcoin/blob/f41f01e1e6de7025d52a865bef97f2a67277f0f3/src/net_processing.h#L33 shows, that default isn't always the case. I don't see what advantage removing it would bring, especially in relation to this PR.


    davidgumberg commented at 6:55 PM on July 8, 2026:

    The reserve is needed to avoid copying the data as we populate it until it fills (in which case it will likely contain more elements than needed).

    This is minimal, even if someone were using an extrapool sized at 32,768 as you linked, and every tx was 10KiB, the final copy of 160MiB would take 12ms on an RPi4.

    I don't see what advantage removing it would bring, especially in relation to this PR.

    Since this line is being added in this PR, it seems like there should be a justification for what it is doing that std::vector doesn't already do well enough.


    l0rinc commented at 8:19 PM on July 8, 2026:

    Since this line is being added in this PR

    I see it more as replacing the old first-use allocation shape than adding a new one:

    • previously resize(m_opts.max_extra_txs) allocated storage and made all configured slots part of the vector, creating default entries that reconstruction would later iterate.
    • now reserve(m_opts.max_extra_txs) keeps the same first-use capacity reservation, but leaves size() tracking only entries that were actually added.
  11. davidgumberg commented at 7:46 PM on July 8, 2026: contributor

    crACK https://github.com/bitcoin/bitcoin/commit/1a3cbf1bd2c0d4d350063bf5162c3e1c2ec2df30

    I would prefer this PR without the reserve call:

    if (vExtraTxnForCompact.empty()) vExtraTxnForCompact.reserve(m_opts.max_extra_txs);
    

    (see the discussion above: #35670 (review))

    Since I don't think it is that helpful, but it is also not very harmful, so merging as-is seems fine.

  12. w0xlt commented at 10:26 PM on July 8, 2026: contributor

    ACK 1a3cbf1bd2c0d4d350063bf5162c3e1c2ec2df30

    There is no reason to scan {Wtxid::ZERO, nullptr} slots during compact block reconstruction.


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-07-09 06:47 UTC