{
 "number": 36486,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36486",
 "title": "p2p: keep orphan resolution requests for non-witness 1p1c parents",
 "author": "FlashWayne",
 "author_association": "CONTRIBUTOR",
 "created_at": "2026-10-09T08:05:28Z",
 "updated_at": "2026-10-13T08:18:46Z",
 "description_hash": "2f8bd21e233fd6f4",
 "age_days": 23,
 "draft": false,
 "labels": [
  "P2P"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "8859ada450bcdf4d8e1b600076585515d83a584e",
 "head_ref": "fix-1p1c-nonsegwit-parent-requests",
 "head_repo": "FlashWayne/bitcoin",
 "head_history": [],
 "additions": 44,
 "deletions": 1,
 "changed_files": 2,
 "commit_count": 1,
 "size_bucket": "S",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "ack": [
     {
      "login": "Yudis-bit",
      "url": "https://github.com/bitcoin/bitcoin/pull/36486#issuecomment-6081515374"
     }
    ]
   },
   "conflicts": []
  }
 },
 "acks_parsed": {
  "Yudis-bit": {
   "kind": "ack",
   "hash": "8859ada450",
   "t": "2026-10-09T13:08:15Z",
   "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": [
   "Yudis-bit",
   "instagibbs"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-10-13T08:18:46Z",
  "last_reviewer_activity": "2026-10-12T21:25:04Z",
  "last_reviewer": "instagibbs",
  "author_silent_days": 19,
  "waiting_on_author_days": 0,
  "days_since_update": 19
 },
 "refs": {
  "mentioned": [
   8279,
   36369,
   36437
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 8279,
    "type": "issue",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "Mempool DoS risk in segwit due to malleated transactions"
   },
   {
    "number": 36369,
    "type": "pull",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "p2p: don't let one peer's same-txid variant cancel another peer's 1p1c or orphan resolution"
   },
   {
    "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": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [],
 "body": "When a low-feerate parent is rejected as `TX_RECONSIDERABLE`, `MempoolRejectedTx` calls `m_txrequest.ForgetTxHash(ptx->GetWitnessHash())` (txdownloadman_impl.cpp:473). For a transaction without witness, wtxid == txid, so this also deletes the txid requests that `MaybeAddOrphanResolutionCandidate` queued for every peer that announced the orphan child.\n\nIf the parent then comes from a peer that did not announce the child:\n\n- `Find1P1CPackage` only pairs the parent with children from the same peer, so no package is formed\n- the child's announcers are never asked for the parent\n- the child stays in the orphanage until some new peer announces it\n\nWith a segwit parent the txid differs from the wtxid, so the txid requests survive and the package gets accepted. A legacy (P2PK) parent is affected.\n\nThis skips `ForgetTxHash` when the result is `TX_RECONSIDERABLE` and the tx has no witness. New wtxid announcements of that parent are still ignored through `AlreadyHaveTx(include_reconsiderable=true)`. The cost is that announcements already queued may download the parent once more.\n\nThe new `test_parent_from_other_peer` in `p2p_opportunistic_1p1c.py` runs for a segwit and a legacy parent: peer A sends the child (orphan), peer B sends the low-fee parent, then it checks that A is asked for the parent by txid and the package is accepted. On master the legacy case times out waiting for that request; with the change it passes. `p2p_orphan_handling.py` and `p2p_tx_download.py` still pass.",
 "commits": [
  {
   "sha": "8859ada450bcdf4d8e1b600076585515d83a584e",
   "date": "2026-10-09T08:05:16Z",
   "message": "p2p: keep orphan resolution requests for non-witness 1p1c parents\n\nWhen a low-feerate parent is rejected as TX_RECONSIDERABLE, the tx\ndownload manager calls m_txrequest.ForgetTxHash(wtxid). For a tx without\nwitness, wtxid == txid, so this also deletes the txid requests that were\nqueued to resolve an orphan child from the peers that announced it.\n\nIf the parent arrives from a peer that didn't announce the child,\nFind1P1CPackage doesn't pair them (it only considers children from the\nsame peer), and the child's announcers are never asked for the parent. The\nchild stays in the orphanage until some new peer announces it. With a\nsegwit parent the txid differs from the wtxid, so the requests survive.\n\nSkip ForgetTxHash for a reconsiderable tx that has no witness. New wtxid\nannouncements are still ignored through AlreadyHaveTx."
  }
 ],
 "timeline": [
  {
   "t": "2026-10-09T13:08:15Z",
   "kind": "comment",
   "who": "Yudis-bit",
   "assoc": "NONE",
   "text": "Tested ACK 8859ada450 (Windows x86_64, Clang 22).\n\nLine 461 `RecentRejectsReconsiderableFilter` only caches `wtxid`, so line 382 `contains(parent_txid)` misses segwit reconsiderable parents. Inserting `ptx->GetHash()` if witness addresses the mirror case (here or follow-up).",
   "id": "c:6081515374",
   "url": "https://github.com/bitcoin/bitcoin/pull/36486#issuecomment-6081515374",
   "hash": "76044e742296d59a",
   "edited": null
  },
  {
   "t": "2026-10-11T18:33:51Z",
   "kind": "comment",
   "who": "instagibbs",
   "assoc": "MEMBER",
   "text": "https://github.com/bitcoin/bitcoin/pull/36437 introduces this case as `parent_stripped`, where a maliciously stripped variant causes the cancellation. IIUC this is the same code paths in the end.\n\nI'd rather catalog all the cases we know and care about, put them under test, and then attack them as a group to ensure we're not doing whackamole forever.\n\n(added an explanatory note for the fuzz case)",
   "id": "c:6112305457",
   "url": "https://github.com/bitcoin/bitcoin/pull/36486#issuecomment-6112305457",
   "hash": "ccca2bd72467f583",
   "edited": "2026-10-11T18:50:39Z"
  },
  {
   "t": "2026-10-12T09:28:43Z",
   "kind": "comment",
   "who": "FlashWayne",
   "assoc": "CONTRIBUTOR",
   "text": "Thanks, agreed it's the same path: the stripped parent in `parent_stripped` and an honest non-segwit parent both reach `ForgetTxHash` with wtxid == txid. The non-segwit case doesn't need an adversary though, a parent relayed by a peer that didn't announce the child is enough.\n\nIn case it helps with the catalog: I applied this commit on top of #36437 and stopped setting `holes.parent_stripped`. Without the commit, `p2p_1p1c_liveness` hits the liveness assert within seconds from an empty corpus (964 crashing inputs in 10 minutes); with it, all of those inputs pass, and 5 workers ran for an hour (~13M executions) without a failure. So it looks like it closes that hole. Happy to rebase onto #36437 and drop the flag here, or for you to fold it into #36369/#36437 if you'd rather land them as a group.\n\n@Yudis-bit thanks for testing. I'd keep the txid of a segwit tx out of the reconsiderable filter: a low-feerate copy with a mutated witness also fails as `TX_RECONSIDERABLE`, so it would put the honest parent's txid there, and `AlreadyHaveTx(..., include_reconsiderable=true)` would then ignore txid announcements of the honest parent (the #8279 concern in the comment above that branch). At the line you point to it only affects the early give-up for a child with two reconsiderable parents.",
   "id": "c:6121499796",
   "url": "https://github.com/bitcoin/bitcoin/pull/36486#issuecomment-6121499796",
   "hash": "cfeda4bfb55526a5",
   "edited": null
  },
  {
   "t": "2026-10-12T21:25:04Z",
   "kind": "comment",
   "who": "instagibbs",
   "assoc": "MEMBER",
   "text": "tagged you on #36437 I think review should move there for now",
   "id": "c:6132889495",
   "url": "https://github.com/bitcoin/bitcoin/pull/36486#issuecomment-6132889495",
   "hash": "7a05cdef16122636",
   "edited": null
  },
  {
   "t": "2026-10-13T08:18:46Z",
   "kind": "comment",
   "who": "FlashWayne",
   "assoc": "CONTRIBUTOR",
   "text": "Thanks, makes sense to move review to #36437. I re-checked against its current head (65b48ba1f1): with this commit applied and `holes.parent_stripped` no longer set, `p2p_1p1c_liveness` ran 10 minutes with 5 workers from an empty corpus (~2.4M execs) with no failures. So this closes the `parent_stripped` hole. Happy to rebase on top of #36437 once it lands and drop the flag in this PR, or to have this folded into #36437/#36369 if you'd rather land the holes as a group.",
   "id": "c:6141355065",
   "url": "https://github.com/bitcoin/bitcoin/pull/36486#issuecomment-6141355065",
   "hash": "65a866b48d7a82f9",
   "edited": null
  }
 ],
 "labels_log": [
  {
   "t": "2026-10-09T08:05:32Z",
   "action": "labeled",
   "label": "P2P",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 4902,
 "text_tokens_estimate": 1225,
 "changed_paths": [
  "src/node/txdownloadman_impl.cpp",
  "test/functional/p2p_opportunistic_1p1c.py"
 ],
 "files": [
  {
   "path": "src/node/txdownloadman_impl.cpp",
   "add": 5,
   "del": 1
  },
  {
   "path": "test/functional/p2p_opportunistic_1p1c.py",
   "add": 39,
   "del": 0
  }
 ],
 "test_lines": 39,
 "git": {
  "head": "8859ada450bcdf4d8e1b600076585515d83a584e",
  "head_matches_backup": true,
  "base": "4bacf21a13c2ed25ef9362ca38f26bf0a67d22c9",
  "patch_id": "b5bdf6598af6886e",
  "commits": [
   {
    "sha": "8859ada450",
    "subject": "p2p: keep orphan resolution requests for non-witness 1p1c parents",
    "files": 2,
    "add": 44,
    "del": 1
   }
  ],
  "patch_truncated": false
 },
 "patch_id": "b5bdf6598af6886e",
 "input_hash": "e08147d03059e43b",
 "extracted_at": "2026-11-02T01:09:49+00:00"
}