{
 "number": 36437,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36437",
 "title": "orphanage: Fix orphan reconsideration disconnect hole, add 1p1c invariant harness",
 "author": "instagibbs",
 "author_association": "MEMBER",
 "created_at": "2026-10-05T12:58:32Z",
 "updated_at": "2026-10-30T11:51:50Z",
 "description_hash": "aedafaccdb473029",
 "age_days": 27,
 "draft": false,
 "labels": [],
 "milestone": "33.0",
 "base": "master",
 "head_sha": "65b48ba1f1f4c6c04a0d1bea9f7774091ec4e130",
 "head_ref": "2026-10-orphan-reconsider-handoff",
 "head_repo": "instagibbs/bitcoin",
 "head_history": [
  {
   "t": "2026-10-05T14:22:11Z",
   "sha": "d4bb72b8ce9ce6b9c9bb2c0a35c71c4c70385118"
  },
  {
   "t": "2026-10-05T14:26:19Z",
   "sha": "be57df6a2e1a598135f4d948be5def37ec076905"
  },
  {
   "t": "2026-10-05T15:56:00Z",
   "sha": "e974d21b4e44ff637345b00e6f4dd74b45100fdb"
  },
  {
   "t": "2026-10-11T18:38:31Z",
   "sha": "ef0f9762c6f51e6c85af290db8ffb6197170859f"
  },
  {
   "t": "2026-10-12T21:14:22Z",
   "sha": "65b48ba1f1f4c6c04a0d1bea9f7774091ec4e130"
  }
 ],
 "additions": 640,
 "deletions": 10,
 "changed_files": 6,
 "commit_count": 2,
 "size_bucket": "L",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "ack": [
     {
      "login": "fametrano",
      "url": "https://github.com/bitcoin/bitcoin/pull/36437#issuecomment-6395056559"
     }
    ],
    "concept_ack": [
     {
      "login": "w0xlt",
      "url": "https://github.com/bitcoin/bitcoin/pull/36437#issuecomment-6050349567"
     }
    ]
   },
   "conflicts": []
  }
 },
 "acks_parsed": {
  "w0xlt": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-10-08T01:34:09Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 1,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "fametrano",
   "w0xlt"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-10-12T21:24:25Z",
  "last_reviewer_activity": "2026-10-25T07:48:54Z",
  "last_reviewer": "fametrano",
  "author_silent_days": 20,
  "waiting_on_author_days": 7,
  "days_since_update": 2
 },
 "refs": {
  "mentioned": [
   31666,
   36578,
   36594
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 31666,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2025-02-04",
    "title": "multi-peer orphan resolution followups"
   },
   {
    "number": 36578,
    "type": "pull",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "p2p: clean up a 1p1c package child that is already in the mempool"
   },
   {
    "number": 36594,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-10-30",
    "title": "net: make orphan reconsideration more granular"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [
   36506
  ],
  "based_on": [],
  "base_for": [
   36506
  ]
 },
 "review_paths": [],
 "body": "The first commit adds `p2p_1p1c_liveness`, an end-to-end fuzz target for parent and child relay. It\n  drives a real PeerManager with real messages and validation, and checks that if an honest peer announces\n  the child of an acceptable parent and child, the child reaches the mempool within a bounded time,\n  whatever other peers do. Known liveness holes are marked in the target as flags that turn the check off\n  once opened, including this one. With that flag turned off, the target finds this bug on master from an\n  empty corpus in under an hour on one core; an earlier version found it first\n  (https://github.com/bitcoin/bitcoin/pull/36371#issuecomment-5937814761).\n\n  The second commit hands reconsideration over to a remaining announcer when the marked announcement is\n  erased, and removes the flag.\n\nFollow-up PR to fill the holes is here: https://github.com/bitcoin/bitcoin/pull/36506",
 "commits": [
  {
   "sha": "47776a909e29f40d86517fe7737bac161337843b",
   "date": "2026-10-12T21:05:32Z",
   "message": "fuzz: add end-to-end liveness target for parent and child relay\n\nCheck that if an honest peer announces the child of an acceptable\nparent and child, the child ends up in the mempool within a bounded\ntime, whatever other peers do. The target drives a real PeerManager\nwith real messages and validation, which the existing targets cannot,\nas they model validation results.\n\nAfter an adversarial prefix the honest peer only answers requests. With\nan outbound honest peer the adversaries keep acting; with an inbound\none they go quiet. Adversary connections are limited, and adversaries\ndo not refill outbound slots.\n\nKnown liveness holes are marked in the target and turn the check off\nonce opened, including the lost orphan reconsideration fixed in the\nnext commit."
  },
  {
   "sha": "65b48ba1f1f4c6c04a0d1bea9f7774091ec4e130",
   "date": "2026-10-12T21:05:32Z",
   "message": "txorphanage: hand reconsideration over when its announcement is erased\n\nA child that arrives before its parent is kept as an orphan. Once the\nparent is accepted on its own, the orphan is put in one randomly chosen\nannouncer's work set to be retried. If that announcer disconnects or is\nevicted first, the orphan is never retried, and re-announcements by the\nother announcers don't fetch it again since it is already held. This\nkeeps e.g. a CPFP child of a parent that pays for itself out of the\nmempool until the orphan is evicted, and an announcer can cause it on\npurpose.\n\nHand reconsideration over to the remaining announcer with the lowest\nNodeId instead.\n\nRemove its hole from the liveness target."
  }
 ],
 "timeline": [
  {
   "t": "2026-10-05T13:12:37Z",
   "kind": "comment",
   "who": "instagibbs",
   "assoc": "MEMBER",
   "text": "Prior harness found this PR's issue that is fixed, and this harness development found \"R11\", so I think it's already paid for itself\n\n  Concept review notes\n\n  **Diff.** Two commits.\n  - `294d6ffcd0` changes `TxOrphanageImpl::Erase` in about 20 lines. When the erased announcement was the\n  one marked for reconsideration, the mark moves to the remaining announcer with the lowest NodeId. The\n  comment in `AddChildrenToWorkSet` and the doc comment for `EraseForPeer` are updated, and\n  `txorphanage_sim` and `orphanage_tests` are updated to match.\n  - `345497515a` adds the `p2p_1p1c_liveness` fuzz target (547 lines, test-only).\n\n  The description matches the diff.\n\n  **Problem.** The node is sent a parent and a child (for example a CPFP), and the child does not reach the\n  mempool until the orphan expires or is evicted. The trigger is one announcer leaving at the wrong\n  moment, and an adversary can try to cause it. The evidence is a fuzzer reproduction on master. There is\n  no reported incident. The goal one level up is that 1p1c relay gets a child accepted in bounded time\n  whatever adversarial peers do.\n\n  **Baseline.** The child is delayed until orphan expiry or eviction. The attacker needs to be the randomly\n  chosen announcer, which is likelier with many sybil connections, and it must disconnect before its next\n  `ProcessMessages` turn. In that turn `ProcessOrphanTx` runs first, and the turn is skipped once\n  `fDisconnect` is set. If the attacker also supplies the parent and then closes the socket, the race is\n  plausible. The cost is delay, not loss.\n\n  **Payoff.** The fix closes the hole for both the disconnect and the eviction path. The new unit and sim\n  tests demonstrate it, and the liveness harness checks it end to end. The payoff is modest but real.\n\n  **Risks.**\n  - The invariant: \"an orphan whose inputs became available keeps a reconsiderable announcement while any\n  announcement exists.\" It is enforced by construction, because every removal of an announcement goes\n  through `Erase<Tag>`.\n  - The change stays inside txorphanage. net_processing and txdownloadman are untouched.\n  - Blast radius is relay liveness and, at worst, extra orphan-validation work. No consensus or fund\n  exposure, and no new Asserts.\n  - The change is internal and easy to reverse.\n  - Review cost is mostly the harness. It drives a real PeerManager, so it is a possible source of\n  maintenance work and flakiness.\n\n  **Prior discussion.** This came up in #31666, which introduced the single random announcer:\n  - sipa asked \"How is this prevented?\"\n  (https://github.com/bitcoin/bitcoin/pull/31666#discussion_r1934078679), and instagibbs laid out the\n  disconnect attack.\n  - mzumsande judged it \"really hard to pull off timing-wise\" and proposed passing outstanding workset\n  entries \"on to another random announcer\" at disconnect\n  (https://github.com/bitcoin/bitcoin/pull/31666#discussion_r1934886294).\n\n  That proposal was deferred, not rejected. This PR implements it, and the fuzzer reproduction is new\n  evidence against the impracticality point. No closed PRs attempt this fix.\n\n  **Alternatives.**\n  - Kept, both trades rather than improvements:\n    - Wtxid-keyed reconsider queue that any announcer drains. Removes per-peer ownership structurally, but\n  changes the TxOrphanage interface and moves the cost of other peers' work into each peer's processing\n  turn.\n    - Detect-and-heal sweep for orphans with available inputs and no reconsider mark. Catches causes not\n  yet known, but adds a second mechanism and CPU cost.\n  - Rejected:\n    - Mark every announcer reconsiderable. It undoes `ed24e01696` (at most one reconsiderable announcement\n  per wtxid) and wastes work. This is glozow's argument from #31666.\n    - Process one workset item at disconnect. An attacker defeats it by putting two items in the workset.\n  Also glozow's argument from #31666.\n    - Process children synchronously after the parent is accepted. It changes net_processing's per-peer\n  processing budget, a larger change in a costlier place.\n    - Docs-only or tests-only. They leave the hole open.\n    - Re-request the child via txrequest. Adds a new bandwidth path an attacker can trigger.\n\n  The PR takes the shallowest placement of the candidates.\n\n  **Dominance:** none.\n\n  **Verdict:** Concept ACK. The fix delivers the payoff without the harness. The harness is test-only and\n  has already found bugs, so splitting it out would not change the verdict.",
   "id": "c:5995163189",
   "url": "https://github.com/bitcoin/bitcoin/pull/36437#issuecomment-5995163189",
   "hash": "d6c69930b3623358",
   "edited": "2026-10-05T13:18:05Z"
  },
  {
   "t": "2026-10-05T14:22:11Z",
   "kind": "force_push",
   "who": "instagibbs",
   "commit": "d4bb72b8ce9ce6b9c9bb2c0a35c71c4c70385118",
   "id": "push:d4bb72b8ce9ce6b9c9bb2c0a35c71c4c70385118"
  },
  {
   "t": "2026-10-05T14:25:31Z",
   "kind": "comment",
   "who": "instagibbs",
   "assoc": "MEMBER",
   "text": "pushed an update to address ralph review: https://git.fish.foo/bitcoin/bitcoin/pulls/36437#author-ralph",
   "id": "c:5996474279",
   "url": "https://github.com/bitcoin/bitcoin/pull/36437#issuecomment-5996474279",
   "hash": "83c7d99c25079f92",
   "edited": null
  },
  {
   "t": "2026-10-05T14:26:19Z",
   "kind": "force_push",
   "who": "instagibbs",
   "commit": "be57df6a2e1a598135f4d948be5def37ec076905",
   "id": "push:be57df6a2e1a598135f4d948be5def37ec076905"
  },
  {
   "t": "2026-10-05T15:56:00Z",
   "kind": "force_push",
   "who": "instagibbs",
   "commit": "e974d21b4e44ff637345b00e6f4dd74b45100fdb",
   "id": "push:e974d21b4e44ff637345b00e6f4dd74b45100fdb"
  },
  {
   "t": "2026-10-08T01:34:09Z",
   "kind": "comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "text": "Concept ACK",
   "id": "c:6050349567",
   "url": "https://github.com/bitcoin/bitcoin/pull/36437#issuecomment-6050349567",
   "hash": "e8334cc43879b73c",
   "edited": null
  },
  {
   "t": "2026-10-11T18:38:31Z",
   "kind": "force_push",
   "who": "instagibbs",
   "commit": "ef0f9762c6f51e6c85af290db8ffb6197170859f",
   "id": "push:ef0f9762c6f51e6c85af290db8ffb6197170859f"
  },
  {
   "t": "2026-10-12T21:14:22Z",
   "kind": "force_push",
   "who": "instagibbs",
   "commit": "65b48ba1f1f4c6c04a0d1bea9f7774091ec4e130",
   "id": "push:65b48ba1f1f4c6c04a0d1bea9f7774091ec4e130"
  },
  {
   "t": "2026-10-12T21:24:25Z",
   "kind": "comment",
   "who": "instagibbs",
   "assoc": "MEMBER",
   "text": "added coinbase-based censorship cast stuff to the PR. Does not add any new holes per se, but covers more ground.\n\n@FlashWayne this PR and the follow-up in the OP should cover known holes. If we can get this in first, then subtracting holes just becomes a matter of implementation debate, and which are worth filling. `parent_stripped` does seem worth filling, for sure.",
   "id": "c:6132879008",
   "url": "https://github.com/bitcoin/bitcoin/pull/36437#issuecomment-6132879008",
   "hash": "5a22ed81af2d71c1",
   "edited": null
  },
  {
   "t": "2026-10-25T07:48:54Z",
   "kind": "comment",
   "who": "fametrano",
   "assoc": "NONE",
   "text": "tACK 65b48ba1f1f4c6c04a0d1bea9f7774091ec4e130\n\nI reproduced the hole end to end with a Boost case of my own. Peer A sends twelve children and peer B announces each of them by wtxid, so each orphan has two announcers. A then sends the parent and disconnects before its next turn, and B runs until it has no more work. On master 6 of the 12 children reach the mempool, and the other 6 stay in the orphanage with nobody to reconsider them. With this PR all 12 are accepted in 13 turns of B, and the orphanage is empty. The updated `peer_worksets` case fails on master at its new hand-over checks and passes here.\n\nThe hand-over keeps the callers' iterators valid. `EraseForPeer`, `EraseTxInternal` and `LimitOrphans` advance their iterator before each erase. In `Erase()`, `lower_bound` can land on the announcement being erased, and the `++` skips it. `modify()` only changes the other announcement's `m_reconsider`, which moves it within `ByPeer` alone, and that key stays unique through `m_entry_sequence`.\n\nI tested the head merged into master a773e1e33b, in a Debug build, with the orphanage, denialofservice and txdownload unit tests, p2p_orphan_handling and p2p_opportunistic_1p1c. Under libFuzzer, p2p_1p1c_liveness ran for 10 minutes and txorphanage_sim for 3, with no crash. I repeated all of it with #36594 and #36578 merged on top, with the liveness target for 5 minutes and txorphanage_sim for 1. I can share the test case as a gist.",
   "id": "c:6395056559",
   "url": "https://github.com/bitcoin/bitcoin/pull/36437#issuecomment-6395056559",
   "hash": "532c2c30cbce66b5",
   "edited": null
  }
 ],
 "labels_log": [
  {
   "t": "2026-10-05T14:22:56Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-10-05T17:27:04Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-10-05T12:58:55Z",
   "kind": "ready_for_review",
   "who": "instagibbs"
  },
  {
   "t": "2026-10-05T12:59:33Z",
   "kind": "renamed",
   "who": "instagibbs",
   "from": "p2p/fuzz: Fix orphan reconsideration disconnect hole, add 1p1c invariant harness",
   "to": "orphanage: Fix orphan reconsideration disconnect hole, add 1p1c invariant harness"
  },
  {
   "t": "2026-10-30T11:51:50Z",
   "kind": "milestoned",
   "who": "fanquake"
  }
 ],
 "text_chars": 8849,
 "text_tokens_estimate": 2212,
 "changed_paths": [
  "src/node/txorphanage.cpp",
  "src/node/txorphanage.h",
  "src/test/fuzz/CMakeLists.txt",
  "src/test/fuzz/p2p_1p1c_liveness.cpp",
  "src/test/fuzz/txorphan.cpp",
  "src/test/orphanage_tests.cpp"
 ],
 "files": [
  {
   "path": "src/node/txorphanage.cpp",
   "add": 19,
   "del": 5
  },
  {
   "path": "src/node/txorphanage.h",
   "add": 2,
   "del": 1
  },
  {
   "path": "src/test/fuzz/CMakeLists.txt",
   "add": 1,
   "del": 0
  },
  {
   "path": "src/test/fuzz/p2p_1p1c_liveness.cpp",
   "add": 568,
   "del": 0
  },
  {
   "path": "src/test/fuzz/txorphan.cpp",
   "add": 22,
   "del": 2
  },
  {
   "path": "src/test/orphanage_tests.cpp",
   "add": 28,
   "del": 2
  }
 ],
 "test_lines": 623,
 "git": {
  "head": "65b48ba1f1f4c6c04a0d1bea9f7774091ec4e130",
  "head_matches_backup": true,
  "base": "bfdcd9797cd1a1345bdaf7ee7ef48f94028262da",
  "patch_id": "8a4f362309028f11",
  "commits": [
   {
    "sha": "47776a909e",
    "subject": "fuzz: add end-to-end liveness target for parent and child relay",
    "files": 2,
    "add": 586,
    "del": 0
   },
   {
    "sha": "65b48ba1f1",
    "subject": "txorphanage: hand reconsideration over when its announcement is erased",
    "files": 5,
    "add": 72,
    "del": 28
   }
  ],
  "patch_truncated": true
 },
 "patch_id": "8a4f362309028f11",
 "input_hash": "2beeb575e6b3846b",
 "extracted_at": "2026-11-02T01:09:49+00:00"
}