p2p: avoid orphanage abort at high peer counts #35919

pull l0rinc wants to merge 4 commits into bitcoin:master from l0rinc:l0rinc/p2p-handle-zero-orphanage-latency-share changing 4 files +36 −19
  1. l0rinc commented at 8:40 PM on August 6, 2026: contributor

    Problem: The orphanage divides a global latency limit among peers with orphan announcements. When an orphan announcement raises the number of represented peers above 3,000, integer division gives each peer an allowance of zero. Trimming then triggers an assertion and aborts the node. This is well above the default connection limit of 200. Stock Linux nodes can be configured to reach this threshold with enough file descriptors, while select-based builds such as macOS are capped below it.

    Fix: Use a minimum per-peer allowance of one while trimming and keep peers at that allowance eligible for eviction. This allows the orphanage to return to its global latency limit.

  2. DrahtBot added the label P2P on Aug 6, 2026
  3. DrahtBot commented at 8:40 PM on August 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/35919.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

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

    Type Reviewers
    Stale ACK jeanpablojp, brunoerg, danielabrozzoni

    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:

    • #36015 (txorphanage: bound orphan memory by storing transactions serialized by brunoerg)
    • #35511 (RFC: consensus: Make CAmount a class by hodlinator)

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

  4. DrahtBot closed this on Aug 7, 2026

  5. DrahtBot reopened this on Aug 7, 2026

  6. jeanpablojp commented at 9:50 AM on August 10, 2026: contributor

    ACK 1ac1f10e7b0e06e8d01a35cabf5bef49ea43c69a

    Reproduced the abort on master 128456b62d with three peers against a global limit of 2. The two halves of the fix depend on each other: with only the clamp and the old > filter, every peer sits at exactly its floored share, the heap comes out empty and the loop pops from it, which segfaults here. So >= is not cleanup. Full unit suite and p2p_orphan_handling.py pass on the merge.

    nit: the comment on MaxPeerLatencyScore() (txorphanage.cpp:210) still says the number of peers times that value adds up to MaxGlobalLatencyScore(), and that keeping every peer below 1 keeps the global limit. LimitOrphans() now enforces a floor of 1 instead, so that stops matching what runs once there are more peers than slots.

    nit: the new test passes whichever peer gets trimmed. BOOST_CHECK(!orphanage->HaveTxFromPeer(txns.at(2)->GetWitnessHash(), 2)) pins it, and passes as written.

    nit: #include <util/check.h> in orphanage_tests.cpp is added by de6342cd45 and nothing uses it after 1ac1f10e7b. test_bitcoin builds with the line removed.

  7. l0rinc force-pushed on Aug 11, 2026
  8. l0rinc commented at 2:27 AM on August 11, 2026: contributor

    Thanks @jeanpablojp, rebased and took all three suggestions (code comment, include and new test) - added you as co-author for the first commit.

  9. jeanpablojp commented at 3:07 PM on August 11, 2026: contributor

    re-ACK 5f4429c20c5cae0eb59baa867e131cc939e92d5d

    Thanks for the co-author credit.

  10. in src/node/txorphanage.cpp:450 in 5f4429c20c outdated
     446 | @@ -448,17 +447,17 @@ void TxOrphanageImpl::LimitOrphans()
     447 |  
     448 |      // Even though it's possible for MaxPeerLatencyScore to increase within this call to LimitOrphans
     449 |      // (e.g. if a peer's orphans are removed entirely, changing the number of peers), use consistent limits throughout.
     450 | -    const auto max_lat{MaxPeerLatencyScore()};
     451 | +    const auto max_lat{std::max<TxOrphanage::Count>(MaxPeerLatencyScore(), 1)};
    


    brunoerg commented at 3:22 PM on September 14, 2026:

    It's worthing changing the fuzz targets to reach the zero-share path. See:

    diff --git a/src/test/fuzz/txorphan.cpp b/src/test/fuzz/txorphan.cpp
    index 9466f77624..90f9fe4fa0 100644
    --- a/src/test/fuzz/txorphan.cpp
    +++ b/src/test/fuzz/txorphan.cpp
    @@ -471,7 +471,7 @@ FUZZ_TARGET(txorphanage_sim)
         // 3. Initialize real orphanage
         //
     
    -    auto max_global_latency_score = provider.ConsumeIntegralInRange<node::TxOrphanage::Count>(NUM_PEERS, MAX_ANN);
    +    auto max_global_latency_score = provider.ConsumeIntegralInRange<node::TxOrphanage::Count>(1, MAX_ANN);
         auto reserved_peer_usage = provider.ConsumeIntegralInRange<node::TxOrphanage::Usage>(1, total_usage);
         auto real = node::MakeTxOrphanage(max_global_latency_score, reserved_peer_usage);
     
    @@ -684,8 +684,8 @@ FUZZ_TARGET(txorphanage_sim)
                 }
             }
             // Always trim after each command if needed.
    -        const auto max_ann = max_global_latency_score / std::max<unsigned>(1, count_peers_fn());
    -        const auto max_mem = reserved_peer_usage;
    +        const auto max_ann = std::max<unsigned>(1, max_global_latency_score / std::max<unsigned>(1, count_peers_fn()));
    +       const auto max_mem = reserved_peer_usage;
             while (true) {
                 // Count global usage and number of peers.
                 node::TxOrphanage::Usage total_usage{0};
    @@ -713,7 +713,7 @@ FUZZ_TARGET(txorphanage_sim)
                     }
                 }
                 assert(worst_peer != unsigned(-1));
    -            assert(ByRatio{worst_dos_score} > ByRatio{FeeFrac(1, 1)});
    +            assert(ByRatio{worst_dos_score} >= ByRatio{FeeFrac(1, 1)});
                 // Find oldest announcement from worst_peer, preferring non-reconsiderable ones.
                 bool done{false};
                 for (int reconsider = 0; reconsider < 2; ++reconsider) {
    
    

    l0rinc commented at 5:34 PM on September 15, 2026:

    Thanks, widened the simulation's limits and matched its trimming model to the minimum share of one, added you as coauthor.

  11. brunoerg approved
  12. brunoerg commented at 3:41 PM on September 14, 2026: contributor

    code review ACK 5f4429c20c5cae0eb59baa867e131cc939e92d5d

  13. in src/test/orphanage_tests.cpp:351 in fb3e7d549c
     346 | +        for (auto peer{0U}; peer < global_limit; ++peer) orphanage->AddTx(txns.at(peer), peer);
     347 | +
     348 | +        BOOST_CHECK_EQUAL(orphanage->MaxPeerLatencyScore(), 1);
     349 | +        test_only_CheckFailuresAreExceptionsNotAborts mock_checks{};
     350 | +        BOOST_CHECK_EXCEPTION(orphanage->AddTx(txns.at(2), /*peer=*/2), NonFatalCheckError, HasReason{"max_peer_latency_score > 0"}); // TODO: A zero share must not abort trimming.
     351 | +        BOOST_CHECK( orphanage->HaveTxFromPeer(txns.at(2)->GetWitnessHash(), 2)); // TODO: Trim the newly added peer.
    


    danielabrozzoni commented at 4:14 PM on September 14, 2026:

    nit: extra whitespace here


    l0rinc commented at 5:09 PM on September 15, 2026:

    It's not extra, in the next commit it're replaced by a !, this way the diff is simpler :)

  14. in src/test/orphanage_tests.cpp:352 in 5f4429c20c
     347 | +        BOOST_CHECK_EQUAL(orphanage->MaxPeerLatencyScore(), 1);
     348 | +        BOOST_CHECK(orphanage->AddTx(txns.at(2), /*peer=*/2));
     349 | +        BOOST_CHECK(!orphanage->HaveTxFromPeer(txns.at(2)->GetWitnessHash(), 2));
     350 | +
     351 | +        BOOST_CHECK_EQUAL(orphanage->TotalLatencyScore(), global_limit);
     352 | +        BOOST_CHECK_EQUAL(orphanage->CountAnnouncements(), global_limit);
    


    danielabrozzoni commented at 10:00 PM on September 14, 2026:

    nit: (Found with Fable 5): every other block in the test ends with a orphanage->SanityCheck();, you should add it there too. It will initially fail, because the orphanage is not properly trimmed.

    In the first commit:

    diff --git a/src/node/txorphanage.cpp b/src/node/txorphanage.cpp
    index 58880139b3..3285cf51f5 100644
    --- a/src/node/txorphanage.cpp
    +++ b/src/node/txorphanage.cpp
    @@ -765,7 +765,7 @@ void TxOrphanageImpl::SanityCheck() const
             TxOrphanage::Count{0}, [](TxOrphanage::Count sum, const auto pair) { return sum + pair.second.m_total_latency_score; });
         assert(summed_peer_latency_score >= m_unique_rounded_input_scores + m_orphans.size());
     
    -    assert(!NeedsTrim());
    +    Assert(!NeedsTrim());
     }
     
     TxOrphanage::Count TxOrphanageImpl::MaxGlobalLatencyScore() const { return m_max_global_latency_score; }
    diff --git a/src/test/orphanage_tests.cpp b/src/test/orphanage_tests.cpp
    index 8a2c2b81ec..07fec603a8 100644
    --- a/src/test/orphanage_tests.cpp
    +++ b/src/test/orphanage_tests.cpp
    @@ -352,6 +352,8 @@ BOOST_AUTO_TEST_CASE(peer_dos_limits)
     
             BOOST_CHECK_GT(orphanage->TotalLatencyScore(), global_limit); // TODO: Trim back to the global limit.
             BOOST_CHECK_GT(orphanage->CountAnnouncements(), global_limit); // TODO: Trim back to the global limit.
    +
    +        BOOST_CHECK_EXCEPTION(orphanage->SanityCheck(), NonFatalCheckError, HasReason{"!NeedsTrim()"}); // TODO: should pass sanity checks
         }
     
         // Test eviction of multiple transactions at a time
    

    Then you change it to orphanage->SanityCheck(); in the second one.


    l0rinc commented at 5:36 PM on September 15, 2026:

    Thanks, added the sanity check after trimming back to the limit (was originally skipped since the characterization test couldn't include it yet).

  15. in src/node/txorphanage.cpp:162 in 5f4429c20c outdated
     158 | @@ -159,7 +159,7 @@ class TxOrphanageImpl final : public TxOrphanage {
     159 |          * A peer having a DoS score > 1 does not necessarily mean that something is wrong, since we
     160 |          * do not trim unless the orphanage exceeds global limits, but it means that this peer will
     161 |          * be selected for trimming sooner. If the global latency score or global memory usage
     162 | -        * limits are exceeded, it must be that there is a peer whose DoS score > 1. */
     163 | +        * limits are exceeded, it must be that there is a peer whose DoS score >= 1. */
    


    danielabrozzoni commented at 10:19 PM on September 14, 2026:

    Might be worth it to clarify why it might be equal? Something like:

    Note that the per-peer latency limit is floored at 1, so if there are more peers with orphans than the global latency limit, the global limit is exceeded even if every peer's DoS score is exactly 1.


    l0rinc commented at 10:51 PM on September 15, 2026:

    Added a whole new commit with doc adjustments, thanks!

  16. in src/node/txorphanage.cpp:7 in 5f4429c20c outdated
       6 | @@ -7,6 +7,7 @@
       7 |  #include <consensus/validation.h>
    


    danielabrozzoni commented at 11:00 AM on September 15, 2026:

    (comment placed in random location), in src/node/txorphanage.h, we say: https://github.com/bitcoin/bitcoin/blob/51ddab532cb38213e2258c24c492bc8a392ffc90/src/node/txorphanage.h#L35

    "as long as they don't exceed their limits" is no longer accurate: a peer exactly at his limit isn't protected anymore if the number of peers holding orphans is higher than the global limit. You could rephrase this as "as long as they stay strictly below their limits"


    l0rinc commented at 10:51 PM on September 15, 2026:

    Good point, fixed, thanks!

  17. danielabrozzoni commented at 11:01 AM on September 15, 2026: member

    light ACK 5f4429c20c5cae0eb59baa867e131cc939e92d5d

    I'm light ACKing because it's my first time looking into the orphanage. Code looks good, I left a couple of non-blocking ideas.

  18. test: characterize orphanage zero-share behavior
    The orphanage calculates each peer's latency share by dividing the
    global latency score by the number of peers with orphan announcements.
    When the peer count exceeds the global score, integer division produces
    a zero share and `LimitOrphans()` aborts while calculating a peer's DoS
    score.
    
    Use `Assert()` for both resource preconditions so the test can catch the
    latency failure while keeping the paired checks consistent.
    
    Co-authored-by: JP <jeanpablo.jp@hotmail.com>
    8272ff3c9a
  19. p2p: handle zero orphanage latency share
    Floor the per-peer latency share at one while `LimitOrphans()` selects
    trimming candidates, so the latency component of each peer's DoS score
    has a positive denominator. Include peers with a score of exactly one
    because the orphanage may exceed its global limit while every peer is at
    that share.
    
    Update the characterized expectations and run the full sanity check now
    that trimming completes.
    c04d549331
  20. test: expand orphanage zero-share fuzz coverage
    Allow `txorphanage_sim` to use global latency scores below `NUM_PEERS`,
    floor its simulated per-peer share at one, and keep score-one peers
    eligible for trimming. This makes the regression reachable by fuzzing
    while preserving the accessor's raw divided share.
    
    Co-authored-by: Bruno Garcia <brunoely.gc@gmail.com>
    ba1c078275
  21. doc: clarify orphanage eviction guarantees
    Document that dividing the global latency score can produce a zero
    per-peer share and that `LimitOrphans()` floors this value while
    selecting candidates. Explain why peers with a DoS score of exactly one
    must remain eligible for trimming.
    
    Replace the unconditional cross-peer protection claim with the actual
    boundary: peers below their individual limits are excluded from
    eviction, while peers at a limit may be selected when a global limit is
    exceeded.
    57a9031665
  22. l0rinc force-pushed on Sep 15, 2026
  23. l0rinc commented at 10:53 PM on September 15, 2026: contributor

    Rebased, adjusted the commit structure, the PR description and pushed a version incorporating the review feedback:

    • The three-peer regression keeps the characterization and fix assertions aligned, and the fix runs SanityCheck() after trimming.
    • txorphanage_sim now exercises global latency scores below the peer count and models the minimum share and score-one eligibility.
    • The eviction comments now distinguish peers below their limits from peers exactly at a limit.
    • Both GetDosScore() resource preconditions now use Assert().

    Thanks @brunoerg and @danielabrozzoni for the suggestions.


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

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