{
 "number": 36506,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36506",
 "title": "p2p: plug \"tier 1\" 1p1c liveness holes",
 "author": "instagibbs",
 "author_association": "MEMBER",
 "created_at": "2026-10-12T21:20:18Z",
 "updated_at": "2026-10-21T13:26:26Z",
 "description_hash": "667b00ce1c71440b",
 "age_days": 20,
 "draft": false,
 "labels": [
  "P2P"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "8d44ac89099393d632ba8e82ca081b6b6c5e7957",
 "head_ref": "2026-10-1p1c-liveness-fixes",
 "head_repo": "instagibbs/bitcoin",
 "head_history": [
  {
   "t": "2026-10-14T01:58:25Z",
   "sha": "9972cd92ddd7d2dab2a0ee2c5eb96dec74106caa"
  },
  {
   "t": "2026-10-14T02:05:45Z",
   "sha": "8746d5adcdc74d69babb9654f68e4e4029d8b0b8"
  },
  {
   "t": "2026-10-14T04:36:22Z",
   "sha": "c8abd9ee41129807b1a5a4ac4ead46c985d0f2b4"
  },
  {
   "t": "2026-10-15T02:33:20Z",
   "sha": "8d44ac89099393d632ba8e82ca081b6b6c5e7957"
  }
 ],
 "additions": 1103,
 "deletions": 22,
 "changed_files": 15,
 "commit_count": 5,
 "size_bucket": "XL",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "ack": [
     {
      "login": "fametrano",
      "url": "https://github.com/bitcoin/bitcoin/pull/36506#issuecomment-6183724291"
     }
    ]
   },
   "conflicts": []
  }
 },
 "acks_parsed": {
  "fametrano": {
   "kind": "ack",
   "hash": "8d44ac8909",
   "t": "2026-10-15T03:28:09Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 1,
  "stale_ack": 0,
  "concept_ack": 0,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "fametrano"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-10-15T04:04:24Z",
  "last_reviewer_activity": "2026-10-15T03:28:09Z",
  "last_reviewer": "fametrano",
  "author_silent_days": 17,
  "waiting_on_author_days": 0,
  "days_since_update": 11
 },
 "refs": {
  "mentioned": [
   36437
  ],
  "depends_on": [
   36437
  ],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 36437,
    "type": "pull",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "orphanage: Fix orphan reconsideration disconnect hole, add 1p1c invariant harness"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [
   36437
  ],
  "based_on": [
   36437
  ],
  "base_for": []
 },
 "review_paths": [],
 "body": "Follow-up to #36437 which resolves a few key weaknesses in 1p1c relay, focusing on issues that can be easily achieved on average cpfp behavior.\n\nResolves two fuzz harness \"holes\" and narrows a third.\n\nThis is a proposed stopping point on resolving the holes, as it's unclear if further work is worth the churn vs a sender-initiated package relay protocol update that would resolve this directly.\n\nKnown issues doc: https://gist.github.com/instagibbs/880a76535fd802fced9f9a5253cdbb40",
 "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."
  },
  {
   "sha": "152f7b74ce14020d03cfaad007eee1d91183bf9b",
   "date": "2026-10-15T02:12:12Z",
   "message": "p2p: don't add already-known transactions to the reject filter\n\nTX_CONFLICT means the transaction is already known, in the mempool or\nconfirmed, not that it is invalid. Its wtxid still went into the reject\nfilter. For a witnessless copy that is also the txid, so if the known\nparent then left the mempool before the next block, a child arriving\nafterwards was dropped as having a rejected parent. For the honest\nversion of a parent whose other-witness version is in the mempool, the\nhonest parent itself was ignored once that version left.\n\nDon't add TX_CONFLICT results to the reject filter. The entry only saved\nre-downloading and re-checking a transaction we already have. Do the\nsame for a DIFFERENT_WITNESS result in 1p1c package evaluation, which\nProcessPackageResult passed to ProcessInvalidTx with a valid state and\nso reached the filter by another path.\n\nThis also keeps a witnessless copy of a confirmed transaction out of\nthe filter, as it is rejected as already known when one of its outputs\nis in the coins cache, so children spending that transaction are no\nlonger dropped. A copy of a coinbase is rejected as TX_CONSENSUS\ninstead, before any input lookup, and still poisons the coinbase's\ntxid.\n\nUpdate the rejection-type table for TX_CONFLICT. In the liveness\ntarget, remove the replayed-parent hole and narrow the stripped\nconfirmed-parent hole to children funded by a coinbase."
  },
  {
   "sha": "feff499de9712af75bcf14495eb60bad39fcbfed",
   "date": "2026-10-15T02:12:24Z",
   "message": "p2p: keep orphan after a package feerate failure\n\nWhen a 1p1c package fails the package feerate check, the child gets a\nTX_RECONSIDERABLE result, and ProcessPackageResult passed it to\nProcessInvalidTx, which erased the child from the orphanage for all of\nits announcers.\n\nThe failure is a property of that pair of wtxids, which\nMempoolRejectedPackage already caches, not of the child. The child can\nstill succeed with another version of the parent, such as the one an\nhonest announcer holds, so it should stay available.\n\nSkip ProcessInvalidTx for package members with a TX_RECONSIDERABLE\nresult. A child that is too low feerate on its own is still rejected\nwhen its parent's work set is processed. Remove the hole from the\nliveness target."
  },
  {
   "sha": "8d44ac89099393d632ba8e82ca081b6b6c5e7957",
   "date": "2026-10-15T02:12:36Z",
   "message": "txrequest: forget announcements by type, keep orphan parent requests\n\nA reconsiderable rejection of a witnessless copy of a transaction forgot its\nwtxid in TxRequestTracker regardless of announcement type, and since that\nwtxid equals the txid, it also cancelled every peer's txid-typed request for\nthe transaction, including orphan resolution requests for a missing parent.\nNow only wtxid-typed announcements of that hash are forgotten, so witnessless\nparents are treated like witnessed ones, which were never affected since\ntheir wtxid differs from the txid."
  }
 ],
 "timeline": [
  {
   "t": "2026-10-14T01:58:25Z",
   "kind": "force_push",
   "who": "instagibbs",
   "commit": "9972cd92ddd7d2dab2a0ee2c5eb96dec74106caa",
   "id": "push:9972cd92ddd7d2dab2a0ee2c5eb96dec74106caa"
  },
  {
   "t": "2026-10-14T02:05:45Z",
   "kind": "force_push",
   "who": "instagibbs",
   "commit": "8746d5adcdc74d69babb9654f68e4e4029d8b0b8",
   "id": "push:8746d5adcdc74d69babb9654f68e4e4029d8b0b8"
  },
  {
   "t": "2026-10-14T02:07:31Z",
   "kind": "comment",
   "who": "instagibbs",
   "assoc": "MEMBER",
   "text": "@marcofleon @mzumsande this will be the ultimate target PR to fuzz",
   "id": "c:6159291810",
   "url": "https://github.com/bitcoin/bitcoin/pull/36506#issuecomment-6159291810",
   "hash": "43623d8a941601fb",
   "edited": null
  },
  {
   "t": "2026-10-14T04:36:22Z",
   "kind": "force_push",
   "who": "instagibbs",
   "commit": "c8abd9ee41129807b1a5a4ac4ead46c985d0f2b4",
   "id": "push:c8abd9ee41129807b1a5a4ac4ead46c985d0f2b4"
  },
  {
   "t": "2026-10-14T14:43:10Z",
   "kind": "comment",
   "who": "fametrano",
   "assoc": "CONTRIBUTOR",
   "text": "Tested c8abd9ee41: each of the four fixes is caught by a test this PR adds or extends. I reverted each fix on its own, keeping the tests, and each revert made `txdownload_tests`, `orphanage_tests` or `p2p_opportunistic_1p1c.py` fail, while the unchanged branch passes them all. I did not review the C++ changes. Details on request.",
   "id": "c:6171016228",
   "url": "https://github.com/bitcoin/bitcoin/pull/36506#issuecomment-6171016228",
   "hash": "05f3260b0ee034b1",
   "edited": null
  },
  {
   "t": "2026-10-14T15:31:55Z",
   "kind": "comment",
   "who": "fametrano",
   "assoc": "CONTRIBUTOR",
   "text": "ACK c8abd9ee41. I have now reviewed the four fixes commit by commit and found nothing blocking. One gap remains for a follow-up: in the 1p1c package path, a `DIFFERENT_WITNESS` result still goes through `ProcessInvalidTx`, so a witness-stripped copy of a mempool parent puts the parent's txid in the reject filter, as `TX_CONFLICT` did before 3a2b5539a1. A functional test shows it end to end: an attacker peer re-sends the stripped copy with an orphan of its own, and after the parent leaves the mempool, an honest child is dropped as having a rejected parent. Test on request.",
   "id": "c:6172002083",
   "url": "https://github.com/bitcoin/bitcoin/pull/36506#issuecomment-6172002083",
   "hash": "e59b01ae9edc26ee",
   "edited": null
  },
  {
   "t": "2026-10-15T02:33:20Z",
   "kind": "force_push",
   "who": "instagibbs",
   "commit": "8d44ac89099393d632ba8e82ca081b6b6c5e7957",
   "id": "push:8d44ac89099393d632ba8e82ca081b6b6c5e7957"
  },
  {
   "t": "2026-10-15T03:28:09Z",
   "kind": "comment",
   "who": "fametrano",
   "assoc": "CONTRIBUTOR",
   "text": "reACK 8d44ac8909. The new `DIFFERENT_WITNESS` case closes the 1p1c gap I noted at c8abd9ee41, and the only other code change is its test, `known_parent_in_package_does_not_poison_after_eviction`. I built `test_bitcoin` and ran `txdownload_tests`, `orphanage_tests` and `txpackage_tests`: the new test passes, and fails with that case moved back next to `INVALID`.",
   "id": "c:6183724291",
   "url": "https://github.com/bitcoin/bitcoin/pull/36506#issuecomment-6183724291",
   "hash": "3aaf45a83fad07d9",
   "edited": null
  },
  {
   "t": "2026-10-15T04:04:24Z",
   "kind": "comment",
   "who": "instagibbs",
   "assoc": "MEMBER",
   "text": "@fametrano fuzzer hit it, which is a good sign",
   "id": "c:6184133714",
   "url": "https://github.com/bitcoin/bitcoin/pull/36506#issuecomment-6184133714",
   "hash": "0342619755c670cc",
   "edited": null
  }
 ],
 "labels_log": [
  {
   "t": "2026-10-12T21:20:23Z",
   "action": "labeled",
   "label": "P2P",
   "who": "DrahtBot"
  },
  {
   "t": "2026-10-14T02:07:14Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-10-14T06:03:57Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-10-14T01:59:24Z",
   "kind": "ready_for_review",
   "who": "instagibbs"
  },
  {
   "t": "2026-10-14T02:02:08Z",
   "kind": "renamed",
   "who": "instagibbs",
   "from": "p2p: plug various 1p1c liveness holes",
   "to": "p2p: plug \"tier 1\" 1p1c liveness holes"
  }
 ],
 "text_chars": 6077,
 "text_tokens_estimate": 1519,
 "changed_paths": [
  "src/net_processing.cpp",
  "src/node/txdownloadman_impl.cpp",
  "src/node/txdownloadman_impl.h",
  "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/fuzz/txrequest.cpp",
  "src/test/orphanage_tests.cpp",
  "src/test/txdownload_tests.cpp",
  "src/test/txrequest_tests.cpp",
  "src/txrequest.cpp",
  "src/txrequest.h",
  "test/functional/p2p_opportunistic_1p1c.py"
 ],
 "files": [
  {
   "path": "src/net_processing.cpp",
   "add": 21,
   "del": 3
  },
  {
   "path": "src/node/txdownloadman_impl.cpp",
   "add": 10,
   "del": 2
  },
  {
   "path": "src/node/txdownloadman_impl.h",
   "add": 4,
   "del": 3
  },
  {
   "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": 513,
   "del": 0
  },
  {
   "path": "src/test/fuzz/txorphan.cpp",
   "add": 22,
   "del": 2
  },
  {
   "path": "src/test/fuzz/txrequest.cpp",
   "add": 38,
   "del": 1
  },
  {
   "path": "src/test/orphanage_tests.cpp",
   "add": 28,
   "del": 2
  },
  {
   "path": "src/test/txdownload_tests.cpp",
   "add": 191,
   "del": 1
  },
  {
   "path": "src/test/txrequest_tests.cpp",
   "add": 82,
   "del": 0
  },
  {
   "path": "src/txrequest.cpp",
   "add": 39,
   "del": 0
  },
  {
   "path": "src/txrequest.h",
   "add": 15,
   "del": 0
  },
  {
   "path": "test/functional/p2p_opportunistic_1p1c.py",
   "add": 118,
   "del": 2
  }
 ],
 "test_lines": 1001,
 "git": {
  "head": "8d44ac89099393d632ba8e82ca081b6b6c5e7957",
  "head_matches_backup": true,
  "base": "bfdcd9797cd1a1345bdaf7ee7ef48f94028262da",
  "patch_id": "c0c474c0161c2dad",
  "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
   },
   {
    "sha": "152f7b74ce",
    "subject": "p2p: don't add already-known transactions to the reject filter",
    "files": 4,
    "add": 165,
    "del": 21
   },
   {
    "sha": "feff499de9",
    "subject": "p2p: keep orphan after a package feerate failure",
    "files": 4,
    "add": 87,
    "del": 36
   },
   {
    "sha": "8d44ac8909",
    "subject": "txrequest: forget announcements by type, keep orphan parent requests",
    "files": 9,
    "add": 274,
    "del": 18
   }
  ],
  "patch_truncated": true
 },
 "patch_id": "c0c474c0161c2dad",
 "input_hash": "1e91f1cf3f5bf3f7",
 "extracted_at": "2026-11-02T01:09:49+00:00"
}