{
 "number": 36607,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36607",
 "title": "private broadcast: Extra guard against unintended non-private broadcast of private transactions",
 "author": "optout21",
 "author_association": "CONTRIBUTOR",
 "created_at": "2026-10-22T14:03:00Z",
 "updated_at": "2026-10-29T16:45:33Z",
 "description_hash": "a1cae5ee5bfcb629",
 "age_days": 10,
 "draft": true,
 "labels": [
  "Needs rebase",
  "Private Broadcast"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "350a7f37832cba1fc4e453806efd2f4726553968",
 "head_ref": "2610-privbcast-guard",
 "head_repo": "optout21/bitcoin",
 "head_history": [
  {
   "t": "2026-10-28T05:37:55Z",
   "sha": "7b2aae41d79624abc87198932d699ae77ec36323"
  },
  {
   "t": "2026-10-28T08:16:40Z",
   "sha": "0cd792580f01796d0ed16d9cf415f7b8a6ac5f20"
  },
  {
   "t": "2026-10-28T09:40:28Z",
   "sha": "350a7f37832cba1fc4e453806efd2f4726553968"
  }
 ],
 "additions": 502,
 "deletions": 78,
 "changed_files": 13,
 "commit_count": 4,
 "size_bucket": "L",
 "mergeable_state": "dirty",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {},
   "conflicts": []
  }
 },
 "acks_parsed": {},
 "acks_tally": {
  "ack": 0,
  "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": [
   "maflcko"
  ]
 },
 "signals": {
  "needs_rebase": true,
  "ci_failed": false,
  "mergeable_state": "dirty",
  "last_author_activity": "2026-10-28T09:40:29Z",
  "last_reviewer_activity": "2026-10-28T08:37:13Z",
  "last_reviewer": "maflcko",
  "author_silent_days": 4,
  "waiting_on_author_days": 0,
  "days_since_update": 3
 },
 "refs": {
  "mentioned": [
   35032,
   35319,
   35410,
   36277,
   36309,
   36312,
   36330,
   36477
  ],
  "depends_on": [
   36477
  ],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 36477,
    "type": "pull",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "private broadcast: show finished transactions in getprivatebroadcastinfo"
   },
   {
    "number": 35032,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-04-09",
    "title": "net_processing: don't modify addrman for private broadcast connections"
   },
   {
    "number": 35319,
    "type": "pull",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "net: use the proxy if overriden when doing v2->v1 reconnections"
   },
   {
    "number": 35410,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-06-04",
    "title": "net: use the proxy if overriden when doing v2->v1 reconnections"
   },
   {
    "number": 36309,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-09-23",
    "title": "private broadcast: clarify claims, mark as experimental"
   },
   {
    "number": 36312,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-09-25",
    "title": "net: don't discourage private broadcast peers"
   },
   {
    "number": 36277,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-10-06",
    "title": "net: always complete all initial private broadcast connections"
   },
   {
    "number": 36330,
    "type": "pull",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "wallet: don't resubmit transactions under -privatebroadcast"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [
   36477
  ],
  "based_on": [
   36477
  ],
  "base_for": []
 },
 "review_paths": [],
 "body": "Motivation: Reduce the risk of accidental privacy leak happening when broadcasting non-privately a requested transaction that had been previously broadcast privately.\n\nBackground: During the initial phase of private broadcast implementation there were unforeseen instances when a private transaction could get broadcast non-privately (see #35032 #35319 #35410 #36309 #36312 #36277 #36330).\n\nCurrently known pathways:\n\n- Re-submission through `submitpackage`\n- Wallet-rebroadcast (when a private transaction is \"picked up\" by the wallet, and rebrodcast after 1-2 days)\n\nNote that these are being addressed, but other, currently non-identified pathways cannot be excluded.\n\nChange summary: Add a general, extra safeguard: whenever there is a transaction broadcast request, check if the given transaction has been previously broadcast privately, and if so, refuse to broadcast non-privately.\n\nTransaction relay is not affected, as it should not be affected by private broadcast.  When we receive a previously private-broadcast transaction for the first time (it's not yet in our mempool), we relay it as any other relayed transaction. Behavior is unchanged in this regard.\n\nNotes:\n\n- Prerequisite: This is built on top of #36477, as keeping finished private broadcasts in the queue is a prerequisite for this change to work.\n- This proposed change is not an alternative to the use-case specific safeguards, but a complementary one.\n- Testing currently uses the two pathways listed above. When they are closed, testing will be more difficult (i.e., it is hard to establish whether non-private broadcast is prevented by the use-case specific or the present generic guard).\n- The private broadcast queue is not persisted, it doesn't survive node restarts\n- Entries deleted via `abortprivatebroadcast` will be forgotten.\n\nKeeping as Draft, because the prerequisite #36477 is pending.",
 "commits": [
  {
   "sha": "295c961c1f156bdbbddfa0f961751d5aa371a62b",
   "date": "2026-10-23T14:58:45Z",
   "message": "private broadcast: show finished transactions in getprivatebroadcastinfo\n\nFor finished transactions, this allows the inspection of when and to\nwhom the transaction was sent as well as when/if it was echoed from\nthe network.\n\nCo-authored-by: Vasil Dimov <vd@FreeBSD.org>"
  },
  {
   "sha": "7f31ea576ce3fd007f4b6e43f0357cd0e24521aa",
   "date": "2026-10-28T05:26:37Z",
   "message": "test: Add new private-then-public broadcast test via submitpackage"
  },
  {
   "sha": "ca6fdbe4a8e7ae4de06638ffff013dabc37de341",
   "date": "2026-10-28T09:37:12Z",
   "message": "test: Add private-then-public broadcast test via wallet resend\n\nExtend p2p_private_broadcast_then_public.py with a second case (if the\nwallet is compiled): a wallet transaction is sent via private broadcast,\ncomes back from the network into the mempool and the wallet of the\noriginator, and is then announced to all peers by the periodic resend\nof the wallet."
  },
  {
   "sha": "350a7f37832cba1fc4e453806efd2f4726553968",
   "date": "2026-10-28T09:37:15Z",
   "message": "net_processing: don't announce privately broadcast txs on request\n\nWith -privatebroadcast, sendrawtransaction sends the transaction only\nvia short-lived private broadcast connections and does not add it to\nthe mempool. However, other code paths can later add the same\ntransaction to the mempool and announce it to all peers, which links\nthe transaction to this node:\n- the submitpackage RPC\n- the wallet, on commit and on its periodic resend (e.g. after the\n  transaction came back from the network and was picked up by the\n  wallet)\n- the GUI PSBT broadcast\n- ReattemptInitialBroadcast(), for transactions in the unbroadcast set\n\nAll of these end up in PeerManager::InitiateTxBroadcastToAll(), which\nis also used to relay transactions received from peers. Add a reason\nparameter to it (RELAY or REQUESTED). If the reason is not RELAY and\nthe transaction is known to the private broadcast storage, then do not\nannounce it, remove it from the mempool's unbroadcast set, so that it\nis not retried, and log that.\n\nRelaying a privately broadcast transaction after it was received back\nfrom the network is not affected: at that point the node behaves like\nany other node that received the transaction.\n\nExtend p2p_private_broadcast_then_public.py to check that re-sending\na privately broadcast transaction via submitpackage or via the periodic\nresend of the wallet does not reach a public peer, while another\ntransaction sent the same way does."
  }
 ],
 "timeline": [
  {
   "t": "2026-10-28T05:37:55Z",
   "kind": "force_push",
   "who": "optout21",
   "commit": "7b2aae41d79624abc87198932d699ae77ec36323",
   "id": "push:7b2aae41d79624abc87198932d699ae77ec36323"
  },
  {
   "t": "2026-10-28T07:42:39Z",
   "kind": "comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "text": "CI failure: `TSan` fails. It may be related to the test pushing the mock time by 36 hours, which may affect some rate limiting. Investigating.",
   "id": "c:6463509830",
   "url": "https://github.com/bitcoin/bitcoin/pull/36607#issuecomment-6463509830",
   "hash": "2b26d179fbf00dc1",
   "edited": null
  },
  {
   "t": "2026-10-28T08:16:40Z",
   "kind": "comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "text": "Attempted fix for \"TSan\" CI timeout.\n\nLikely root cause: In the wallet resend part, node0's mocktime jumps 36 hours and then stays frozen. If node0's message handler processes the nodes[2] connection between the jump and the wallet resubmit, it resets that peer's inv trickle timer to the frozen time plus a random delay, so the timer never expires and the inv for the resent transaction is never sent.\n\nAttempted fix: start node0 with `-whitelist=noban,out@127.0.0.1`, which gives its manual connections `noban` permission so they skip the trickle timer, as `wallet_resendwallettransactions.py` does via `noban_tx_relay`.\n\n`git range-diff 7b2aae41d79624abc87198932d699ae77ec36323...0cd792580f01796d0ed16d9cf415f7b8a6ac5f20`",
   "id": "c:6464089083",
   "url": "https://github.com/bitcoin/bitcoin/pull/36607#issuecomment-6464089083",
   "hash": "5e18818c22bb809f",
   "edited": null
  },
  {
   "t": "2026-10-28T08:16:40Z",
   "kind": "force_push",
   "who": "optout21",
   "commit": "0cd792580f01796d0ed16d9cf415f7b8a6ac5f20",
   "id": "push:0cd792580f01796d0ed16d9cf415f7b8a6ac5f20"
  },
  {
   "t": "2026-10-28T08:37:13Z",
   "kind": "comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nHaven't looked closely, but I wonder if this is the correct fix here. `wallet_resendwallettransactions.py` only cares about the day-long resend-delay, not the second-long trickle-delay, so it seems correct for that test. However, for the test here, either the test here cares about trickle-delay, in which case it would be cleaner to not use `noban` permission and instead make the test deterministic with syncs or so? Or, the test doesn't care about trickle delay, and can just set `noban_tx_relay` globally? Just wondering, up to you.",
   "id": "c:6464447216",
   "url": "https://github.com/bitcoin/bitcoin/pull/36607#issuecomment-6464447216",
   "hash": "adc8027672c9525e",
   "edited": null
  },
  {
   "t": "2026-10-28T09:40:28Z",
   "kind": "force_push",
   "who": "optout21",
   "commit": "350a7f37832cba1fc4e453806efd2f4726553968",
   "id": "push:350a7f37832cba1fc4e453806efd2f4726553968"
  },
  {
   "t": "2026-10-28T09:40:29Z",
   "kind": "comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "text": "Thanks for chiming in! Indeed, the global `noban_tx_relay` seems to be a cleaner way.\nI used LLM assistance for finding the failure reason, I've reviewed that it looks plausible, and reproduced locally (both with and without).\nThis test doesn't care about trickle delay, so disabling it globally is a simpler solution. Changed now.\n\n`git range-diff 0cd792580f01796d0ed16d9cf415f7b8a6ac5f20...350a7f37832cba1fc4e453806efd2f4726553968`\n`git range-diff 7b2aae41d79624abc87198932d699ae77ec36323...350a7f37832cba1fc4e453806efd2f4726553968`",
   "id": "c:6465630280",
   "url": "https://github.com/bitcoin/bitcoin/pull/36607#issuecomment-6465630280",
   "hash": "4d49e0c39aef4551",
   "edited": null
  }
 ],
 "labels_log": [
  {
   "t": "2026-10-22T14:03:05Z",
   "action": "labeled",
   "label": "Private Broadcast",
   "who": "DrahtBot"
  },
  {
   "t": "2026-10-22T15:06:57Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-10-28T09:16:13Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-10-29T16:45:32Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 6074,
 "text_tokens_estimate": 1518,
 "changed_paths": [
  "doc/release-notes-36477.md",
  "src/net_processing.cpp",
  "src/net_processing.h",
  "src/node/transaction.cpp",
  "src/node/types.h",
  "src/private_broadcast.cpp",
  "src/private_broadcast.h",
  "src/rpc/mempool.cpp",
  "src/test/fuzz/private_broadcast.cpp",
  "src/test/private_broadcast_tests.cpp",
  "test/functional/p2p_private_broadcast.py",
  "test/functional/p2p_private_broadcast_then_public.py",
  "test/functional/test_runner.py"
 ],
 "files": [
  {
   "path": "doc/release-notes-36477.md",
   "add": 11,
   "del": 0
  },
  {
   "path": "src/net_processing.cpp",
   "add": 16,
   "del": 6
  },
  {
   "path": "src/net_processing.h",
   "add": 4,
   "del": 1
  },
  {
   "path": "src/node/transaction.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/node/types.h",
   "add": 8,
   "del": 0
  },
  {
   "path": "src/private_broadcast.cpp",
   "add": 57,
   "del": 18
  },
  {
   "path": "src/private_broadcast.h",
   "add": 38,
   "del": 17
  },
  {
   "path": "src/rpc/mempool.cpp",
   "add": 14,
   "del": 5
  },
  {
   "path": "src/test/fuzz/private_broadcast.cpp",
   "add": 68,
   "del": 20
  },
  {
   "path": "src/test/private_broadcast_tests.cpp",
   "add": 15,
   "del": 7
  },
  {
   "path": "test/functional/p2p_private_broadcast.py",
   "add": 7,
   "del": 3
  },
  {
   "path": "test/functional/p2p_private_broadcast_then_public.py",
   "add": 262,
   "del": 0
  },
  {
   "path": "test/functional/test_runner.py",
   "add": 1,
   "del": 0
  }
 ],
 "test_lines": 383,
 "git": {
  "head": "350a7f37832cba1fc4e453806efd2f4726553968",
  "head_matches_backup": true,
  "base": "f655a8df755330911c6c109c8b1d04ef5dd4ea09",
  "patch_id": "79dd99ac9f41cc4a",
  "commits": [
   {
    "sha": "295c961c1f",
    "subject": "private broadcast: show finished transactions in getprivatebroadcastinfo",
    "files": 8,
    "add": 197,
    "del": 71
   },
   {
    "sha": "7f31ea576c",
    "subject": "test: Add new private-then-public broadcast test via submitpackage",
    "files": 2,
    "add": 165,
    "del": 0
   },
   {
    "sha": "ca6fdbe4a8",
    "subject": "test: Add private-then-public broadcast test via wallet resend",
    "files": 1,
    "add": 89,
    "del": 16
   },
   {
    "sha": "350a7f3783",
    "subject": "net_processing: don't announce privately broadcast txs on request",
    "files": 7,
    "add": 82,
    "del": 22
   }
  ],
  "patch_truncated": true
 },
 "patch_id": "79dd99ac9f41cc4a",
 "input_hash": "302f112bb25bdede",
 "extracted_at": "2026-11-02T01:09:49+00:00"
}