{
 "number": 35919,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/35919",
 "title": "p2p: avoid orphanage abort at high peer counts",
 "author": "l0rinc",
 "author_association": "MEMBER",
 "created_at": "2026-08-06T20:40:51Z",
 "updated_at": "2026-09-15T22:53:25Z",
 "age_days": 41,
 "draft": false,
 "labels": [
  "P2P"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "57a903166501d02e3cc29aced563a8ceed7c3027",
 "head_ref": "l0rinc/p2p-handle-zero-orphanage-latency-share",
 "head_repo": "l0rinc/bitcoin",
 "head_history": [
  {
   "t": "2026-08-11T02:25:08Z",
   "sha": "5f4429c20c5cae0eb59baa867e131cc939e92d5d"
  },
  {
   "t": "2026-09-15T22:49:20Z",
   "sha": "57a903166501d02e3cc29aced563a8ceed7c3027"
  }
 ],
 "additions": 36,
 "deletions": 19,
 "changed_files": 4,
 "commit_count": 4,
 "size_bucket": "S",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "stale_ack": [
     {
      "login": "jeanpablojp",
      "url": "https://github.com/bitcoin/bitcoin/pull/35919#issuecomment-5254985555"
     },
     {
      "login": "brunoerg",
      "url": "https://github.com/bitcoin/bitcoin/pull/35919#pullrequestreview-5199777906"
     },
     {
      "login": "danielabrozzoni",
      "url": "https://github.com/bitcoin/bitcoin/pull/35919#pullrequestreview-5200112366"
     }
    ]
   },
   "conflicts": [
    {
     "number": 36015,
     "title": "txorphanage: bound orphan memory by storing transactions serialized",
     "author": "brunoerg"
    },
    {
     "number": 35511,
     "title": "RFC: consensus: Make `CAmount` a class",
     "author": "hodlinator"
    }
   ]
  }
 },
 "acks_parsed": {
  "jeanpablojp": {
   "kind": "ack",
   "hash": "5f4429c20c5cae0eb59baa867e131cc939e92d5d",
   "t": "2026-08-11T15:07:22Z",
   "stale": true
  },
  "brunoerg": {
   "kind": "ack",
   "hash": "5f4429c20c5cae0eb59baa867e131cc939e92d5d",
   "t": "2026-09-14T15:41:50Z",
   "stale": true
  },
  "danielabrozzoni": {
   "kind": "ack",
   "hash": "5f4429c20c5cae0eb59baa867e131cc939e92d5d",
   "t": "2026-09-15T11:01:39Z",
   "stale": true
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 3,
  "concept_ack": 0,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 1,
  "changes_requested": 0,
  "distinct_reviewers": [
   "brunoerg",
   "danielabrozzoni",
   "jeanpablojp"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-09-15T22:53:25Z",
  "last_reviewer_activity": "2026-09-15T11:01:39Z",
  "last_reviewer": "danielabrozzoni",
  "author_silent_days": 1,
  "waiting_on_author_days": 0,
  "days_since_update": 1
 },
 "refs": {
  "mentioned": [],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [],
  "conflicts": [
   36015,
   35511
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/node/txorphanage.cpp",
  "src/test/orphanage_tests.cpp"
 ],
 "body": "**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.\n\n**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.",
 "commits": [
  {
   "sha": "8272ff3c9ac7b69add4cf46fbfd8a931ccf07a54",
   "date": "2026-09-15T22:43:45Z",
   "message": "test: characterize orphanage zero-share behavior\n\nThe orphanage calculates each peer's latency share by dividing the\nglobal latency score by the number of peers with orphan announcements.\nWhen the peer count exceeds the global score, integer division produces\na zero share and `LimitOrphans()` aborts while calculating a peer's DoS\nscore.\n\nUse `Assert()` for both resource preconditions so the test can catch the\nlatency failure while keeping the paired checks consistent.\n\nCo-authored-by: JP <jeanpablo.jp@hotmail.com>"
  },
  {
   "sha": "c04d54933171e223702dc4e38b6fa9f90a0299a2",
   "date": "2026-09-15T22:43:57Z",
   "message": "p2p: handle zero orphanage latency share\n\nFloor the per-peer latency share at one while `LimitOrphans()` selects\ntrimming candidates, so the latency component of each peer's DoS score\nhas a positive denominator. Include peers with a score of exactly one\nbecause the orphanage may exceed its global limit while every peer is at\nthat share.\n\nUpdate the characterized expectations and run the full sanity check now\nthat trimming completes."
  },
  {
   "sha": "ba1c078275b1d431035e1bd8df201cf3f28394b2",
   "date": "2026-09-15T22:44:09Z",
   "message": "test: expand orphanage zero-share fuzz coverage\n\nAllow `txorphanage_sim` to use global latency scores below `NUM_PEERS`,\nfloor its simulated per-peer share at one, and keep score-one peers\neligible for trimming. This makes the regression reachable by fuzzing\nwhile preserving the accessor's raw divided share.\n\nCo-authored-by: Bruno Garcia <brunoely.gc@gmail.com>"
  },
  {
   "sha": "57a903166501d02e3cc29aced563a8ceed7c3027",
   "date": "2026-09-15T22:44:21Z",
   "message": "doc: clarify orphanage eviction guarantees\n\nDocument that dividing the global latency score can produce a zero\nper-peer share and that `LimitOrphans()` floors this value while\nselecting candidates. Explain why peers with a DoS score of exactly one\nmust remain eligible for trimming.\n\nReplace the unconditional cross-peer protection claim with the actual\nboundary: peers below their individual limits are excluded from\neviction, while peers at a limit may be selected when a global limit is\nexceeded."
  }
 ],
 "timeline": [
  {
   "t": "2026-08-10T09:50:49Z",
   "kind": "comment",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "text": "ACK 1ac1f10e7b0e06e8d01a35cabf5bef49ea43c69a\n\nReproduced 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.\n\nnit: 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.\n\nnit: the new test passes whichever peer gets trimmed. `BOOST_CHECK(!orphanage->HaveTxFromPeer(txns.at(2)->GetWitnessHash(), 2))` pins it, and passes as written.\n\nnit: `#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."
  },
  {
   "t": "2026-08-11T02:25:08Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "5f4429c20c5cae0eb59baa867e131cc939e92d5d"
  },
  {
   "t": "2026-08-11T02:27:20Z",
   "kind": "comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "text": "Thanks @jeanpablojp, rebased and took all three suggestions (code comment, include and new test) - added you as co-author for the first commit."
  },
  {
   "t": "2026-08-11T15:07:22Z",
   "kind": "comment",
   "who": "jeanpablojp",
   "assoc": "CONTRIBUTOR",
   "text": "re-ACK 5f4429c20c5cae0eb59baa867e131cc939e92d5d\n\nThanks for the co-author credit."
  },
  {
   "t": "2026-09-14T15:22:50Z",
   "kind": "review_comment",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "path": "src/node/txorphanage.cpp",
   "commit": "57a903166501d02e3cc29aced563a8ceed7c3027",
   "in_reply_to": null,
   "text": "It's worthing changing the fuzz targets to reach the zero-share path. See:\n\n```diff\ndiff --git a/src/test/fuzz/txorphan.cpp b/src/test/fuzz/txorphan.cpp\nindex 9466f77624..90f9fe4fa0 100644\n--- a/src/test/fuzz/txorphan.cpp\n+++ b/src/test/fuzz/txorphan.cpp\n@@ -471,7 +471,7 @@ FUZZ_TARGET(txorphanage_sim)\n     // 3. Initialize real orphanage\n     //\n\n-    auto max_global_latency_score = provider.ConsumeIntegralInRange<node::TxOrphanage::Count>(NUM_PEERS, MAX_ANN);\n+    auto max_global_latency_score = provider.ConsumeIntegralInRange<node::TxOrphanage::Count>(1, MAX_ANN);\n     auto reserved_peer_usage = provider.ConsumeIntegralInRange<node::TxOrphanage::Usage>(1, total_usage);\n     auto real = node::MakeTxOrphanage(max_global_latency_score, reserved_peer_usage);\n\n@@ -684,8 +684,8 @@ FUZZ_TARGET(txorphanage_sim)\n             }\n         }\n         // Always trim after each command if needed.\n-        const auto max_ann = max_global_latency_score / std::max<unsigned>(1, count_peers_fn());\n-        const auto max_mem = reserved_peer_usage;\n+        const auto max_ann = std::max<unsigned>(1, max_global_latency_score / std::max<unsigned>(1, count_peers_fn()));\n+       const auto max_mem = reserved_peer_usage;\n         while (true) {\n             // Count global usage and number of peers.\n             node::TxOrphanage::Usage total_usage{0};\n@@ -713,7 +713,7 @@ FUZZ_TARGET(txorphanage_sim)\n                 }\n             }\n             assert(worst_peer != unsigned(-1));\n-            assert(ByRatio{worst_dos_score} > ByRatio{FeeFrac(1, 1)});\n+            assert(ByRatio{worst_dos_score} >= ByRatio{FeeFrac(1, 1)});\n             // Find oldest announcement from worst_peer, preferring non-reconsiderable ones.\n             bool done{false};\n             for (int reconsider = 0; reconsider < 2; ++reconsider) {\n\n```"
  },
  {
   "t": "2026-09-14T15:41:50Z",
   "kind": "review",
   "who": "brunoerg",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "5f4429c20c5cae0eb59baa867e131cc939e92d5d",
   "text": "code review ACK 5f4429c20c5cae0eb59baa867e131cc939e92d5d"
  },
  {
   "t": "2026-09-14T16:14:22Z",
   "kind": "review_comment",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "path": "src/test/orphanage_tests.cpp",
   "commit": "fb3e7d549cb6941faa65697564bf6f4d995510ca",
   "in_reply_to": null,
   "text": "nit: extra whitespace here"
  },
  {
   "t": "2026-09-14T22:00:06Z",
   "kind": "review_comment",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "path": "src/test/orphanage_tests.cpp",
   "commit": "5f4429c20c5cae0eb59baa867e131cc939e92d5d",
   "in_reply_to": null,
   "text": "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.\n\nIn the first commit:\n```\ndiff --git a/src/node/txorphanage.cpp b/src/node/txorphanage.cpp\nindex 58880139b3..3285cf51f5 100644\n--- a/src/node/txorphanage.cpp\n+++ b/src/node/txorphanage.cpp\n@@ -765,7 +765,7 @@ void TxOrphanageImpl::SanityCheck() const\n         TxOrphanage::Count{0}, [](TxOrphanage::Count sum, const auto pair) { return sum + pair.second.m_total_latency_score; });\n     assert(summed_peer_latency_score >= m_unique_rounded_input_scores + m_orphans.size());\n\n-    assert(!NeedsTrim());\n+    Assert(!NeedsTrim());\n }\n\n TxOrphanage::Count TxOrphanageImpl::MaxGlobalLatencyScore() const { return m_max_global_latency_score; }\ndiff --git a/src/test/orphanage_tests.cpp b/src/test/orphanage_tests.cpp\nindex 8a2c2b81ec..07fec603a8 100644\n--- a/src/test/orphanage_tests.cpp\n+++ b/src/test/orphanage_tests.cpp\n@@ -352,6 +352,8 @@ BOOST_AUTO_TEST_CASE(peer_dos_limits)\n\n         BOOST_CHECK_GT(orphanage->TotalLatencyScore(), global_limit); // TODO: Trim back to the global limit.\n         BOOST_CHECK_GT(orphanage->CountAnnouncements(), global_limit); // TODO: Trim back to the global limit.\n+\n+        BOOST_CHECK_EXCEPTION(orphanage->SanityCheck(), NonFatalCheckError, HasReason{\"!NeedsTrim()\"}); // TODO: should pass sanity checks\n     }\n\n     // Test eviction of multiple transactions at a time\n```\n\nThen you change it to `orphanage->SanityCheck();` in the second one."
  },
  {
   "t": "2026-09-14T22:19:50Z",
   "kind": "review_comment",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "path": "src/node/txorphanage.cpp",
   "commit": "57a903166501d02e3cc29aced563a8ceed7c3027",
   "in_reply_to": null,
   "text": "Might be worth it to clarify why it might be equal? Something like:\n\nNote 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."
  },
  {
   "t": "2026-09-15T11:00:35Z",
   "kind": "review_comment",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "path": "src/node/txorphanage.cpp",
   "commit": "57a903166501d02e3cc29aced563a8ceed7c3027",
   "in_reply_to": null,
   "text": "(comment placed in random location), in `src/node/txorphanage.h`, we say:\nhttps://github.com/bitcoin/bitcoin/blob/51ddab532cb38213e2258c24c492bc8a392ffc90/src/node/txorphanage.h#L35\n\n\"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\""
  },
  {
   "t": "2026-09-15T11:01:39Z",
   "kind": "review",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "5f4429c20c5cae0eb59baa867e131cc939e92d5d",
   "text": "light ACK 5f4429c20c5cae0eb59baa867e131cc939e92d5d\n\nI'm light ACKing because it's my first time looking into the orphanage. Code looks good, I left a couple of non-blocking ideas."
  },
  {
   "t": "2026-09-15T17:09:44Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/test/orphanage_tests.cpp",
   "commit": "fb3e7d549cb6941faa65697564bf6f4d995510ca",
   "in_reply_to": 4007181633,
   "text": "It's not extra, in the next commit it're replaced by a `!`, this way the diff is simpler :)"
  },
  {
   "t": "2026-09-15T17:34:30Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/node/txorphanage.cpp",
   "commit": "57a903166501d02e3cc29aced563a8ceed7c3027",
   "in_reply_to": 4006716933,
   "text": "Thanks, widened the simulation's limits and matched its trimming model to the minimum share of one, added you as coauthor."
  },
  {
   "t": "2026-09-15T17:36:02Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/test/orphanage_tests.cpp",
   "commit": "5f4429c20c5cae0eb59baa867e131cc939e92d5d",
   "in_reply_to": 4009993143,
   "text": "Thanks, added the sanity check after trimming back to the limit (was originally skipped since the characterization test couldn't include it yet)."
  },
  {
   "t": "2026-09-15T22:49:20Z",
   "kind": "force_push",
   "who": "l0rinc",
   "commit": "57a903166501d02e3cc29aced563a8ceed7c3027"
  },
  {
   "t": "2026-09-15T22:51:12Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/node/txorphanage.cpp",
   "commit": "57a903166501d02e3cc29aced563a8ceed7c3027",
   "in_reply_to": 4010115535,
   "text": "Added a whole new commit with doc adjustments, thanks!"
  },
  {
   "t": "2026-09-15T22:51:26Z",
   "kind": "review_comment",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "path": "src/node/txorphanage.cpp",
   "commit": "57a903166501d02e3cc29aced563a8ceed7c3027",
   "in_reply_to": 4014827728,
   "text": "Good point, fixed, thanks!"
  },
  {
   "t": "2026-09-15T22:53:25Z",
   "kind": "review",
   "who": "l0rinc",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "57a903166501d02e3cc29aced563a8ceed7c3027",
   "text": "Rebased, adjusted the commit structure, the PR description and pushed a version incorporating the review feedback:\n* The three-peer regression keeps the characterization and fix assertions aligned, and the fix runs `SanityCheck()` after trimming.\n* `txorphanage_sim` now exercises global latency scores below the peer count and models the minimum share and score-one eligibility.\n* The eviction comments now distinguish peers below their limits from peers exactly at a limit.\n* Both `GetDosScore()` resource preconditions now use `Assert()`.\n\nThanks @brunoerg and @danielabrozzoni for the suggestions."
  }
 ],
 "labels_log": [
  {
   "t": "2026-08-06T20:40:55Z",
   "action": "labeled",
   "label": "P2P",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-08-07T06:08:32Z",
   "kind": "closed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-07T06:08:35Z",
   "kind": "reopened",
   "who": "DrahtBot"
  }
 ],
 "text_chars": 9309,
 "text_tokens_estimate": 2327,
 "changed_paths": [
  "src/node/txorphanage.cpp",
  "src/node/txorphanage.h",
  "src/test/fuzz/txorphan.cpp",
  "src/test/orphanage_tests.cpp"
 ],
 "files": [
  {
   "path": "src/node/txorphanage.cpp",
   "add": 14,
   "del": 14
  },
  {
   "path": "src/node/txorphanage.h",
   "add": 2,
   "del": 2
  },
  {
   "path": "src/test/fuzz/txorphan.cpp",
   "add": 3,
   "del": 3
  },
  {
   "path": "src/test/orphanage_tests.cpp",
   "add": 17,
   "del": 0
  }
 ],
 "test_lines": 23,
 "git": {
  "head": "57a903166501d02e3cc29aced563a8ceed7c3027",
  "head_matches_backup": true,
  "base": "57721f8074c2e8ccfe4c6d10114d4683dca58118",
  "commits": [
   {
    "sha": "8272ff3c9a",
    "subject": "test: characterize orphanage zero-share behavior",
    "files": 2,
    "add": 20,
    "del": 2
   },
   {
    "sha": "c04d549331",
    "subject": "p2p: handle zero orphanage latency share",
    "files": 2,
    "add": 8,
    "del": 8
   },
   {
    "sha": "ba1c078275",
    "subject": "test: expand orphanage zero-share fuzz coverage",
    "files": 1,
    "add": 3,
    "del": 3
   },
   {
    "sha": "57a9031665",
    "subject": "doc: clarify orphanage eviction guarantees",
    "files": 2,
    "add": 10,
    "del": 11
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "9647e9b4ba1b3c10",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}