{
 "number": 34707,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/34707",
 "title": "net: keep finished private broadcast txs in memory",
 "author": "andrewtoth",
 "author_association": "MEMBER",
 "created_at": "2026-03-02T01:46:35Z",
 "updated_at": "2026-09-15T15:51:58Z",
 "age_days": 199,
 "draft": false,
 "labels": [
  "P2P",
  "Private Broadcast"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "9217011129558f3bda626bce0cb343f2861a2967",
 "head_ref": "private_broadcast_store",
 "head_repo": "andrewtoth/bitcoin",
 "head_history": [
  {
   "t": "2026-03-02T02:03:24Z",
   "sha": "afca84489c26f920d842f34d00f8418349255a65"
  },
  {
   "t": "2026-03-11T01:26:17Z",
   "sha": "2c3c80de063ac2d12cf17c037cba4d5b08d39fb3"
  },
  {
   "t": "2026-03-11T01:40:28Z",
   "sha": "9f58d9a1691b10341784e13ae32549bb0244bbda"
  },
  {
   "t": "2026-03-11T13:39:59Z",
   "sha": "c9b19d76dd336b2342d6d766b408410e1b93107b"
  },
  {
   "t": "2026-03-11T15:12:48Z",
   "sha": "6ecbf1899e0ae79611a03221b73e52d75be6dbc4"
  },
  {
   "t": "2026-03-11T20:58:27Z",
   "sha": "911d4ebe235e4d50761aa79a339dc3100c54bbd5"
  },
  {
   "t": "2026-03-12T00:12:06Z",
   "sha": "bd31f7391dd45b2582092762466e40ad2594b178"
  },
  {
   "t": "2026-03-20T19:29:46Z",
   "sha": "420abcb42742a8baa768c1978eb17bd019478ccc"
  },
  {
   "t": "2026-03-20T19:39:05Z",
   "sha": "673124c53013a91d0ca7a70de9c54d521bb3c308"
  },
  {
   "t": "2026-03-23T22:33:32Z",
   "sha": "d5d7c28e1b4b76daddad95a23cbf56209ca0b86e"
  },
  {
   "t": "2026-04-04T15:27:12Z",
   "sha": "f60a433037b925e1e23c9ca91151511734634d36"
  },
  {
   "t": "2026-04-13T02:02:59Z",
   "sha": "a6db9ea77b245519186f1341b1bf89ace130a4fa"
  },
  {
   "t": "2026-04-24T20:36:18Z",
   "sha": "0b1f5f4aece438887b16368f6ecf3f69b22133ef"
  },
  {
   "t": "2026-09-02T01:17:53Z",
   "sha": "a0508a315935eec89e92b29d8cb99ba073e22f61"
  },
  {
   "t": "2026-09-03T01:20:55Z",
   "sha": "9217011129558f3bda626bce0cb343f2861a2967"
  }
 ],
 "additions": 275,
 "deletions": 68,
 "changed_files": 8,
 "commit_count": 4,
 "size_bucket": "M",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "ack": [
     {
      "login": "vasild",
      "url": "https://github.com/bitcoin/bitcoin/pull/34707#pullrequestreview-5152033442"
     },
     {
      "login": "optout21",
      "url": "https://github.com/bitcoin/bitcoin/pull/34707#pullrequestreview-5212089847"
     }
    ],
    "concept_nack": [
     {
      "login": "instagibbs",
      "url": "https://github.com/bitcoin/bitcoin/pull/34707#pullrequestreview-5093703360"
     }
    ],
    "stale_ack": [
     {
      "login": "danielabrozzoni",
      "url": "https://github.com/bitcoin/bitcoin/pull/34707#pullrequestreview-4122116224"
     }
    ]
   },
   "conflicts": []
  }
 },
 "acks_parsed": {
  "optout21": {
   "kind": "ack",
   "hash": "9217011129558f3bda626bce0cb343f2861a2967",
   "t": "2026-09-15T15:51:53Z",
   "stale": false
  },
  "vasild": {
   "kind": "ack",
   "hash": "9217011129558f3bda626bce0cb343f2861a2967",
   "t": "2026-09-09T09:22:38Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 2,
  "stale_ack": 0,
  "concept_ack": 0,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 5,
  "changes_requested": 0,
  "distinct_reviewers": [
   "danielabrozzoni",
   "instagibbs",
   "kenji-yamam0to",
   "optout21",
   "rkrux",
   "vasild"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-09-15T13:24:03Z",
  "last_reviewer_activity": "2026-09-15T15:51:53Z",
  "last_reviewer": "optout21",
  "author_silent_days": 2,
  "waiting_on_author_days": 2,
  "days_since_update": 2
 },
 "refs": {
  "mentioned": [
   30471,
   34322,
   34329,
   34873,
   35090,
   35406,
   35680
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 34329,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-02-19",
    "title": "rpc,net: Add private broadcast RPCs"
   },
   {
    "number": 30471,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "Feature Request: Broadcast Pool"
   },
   {
    "number": 34322,
    "type": "pull",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "node: Persist private broadcast transactions over node restarts"
   },
   {
    "number": 34873,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-04-04",
    "title": "net: fix premature stale flagging of unpicked private broadcast txs"
   },
   {
    "number": 35090,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-07-20",
    "title": "fuzz: add p2p_private_broadcast harness"
   },
   {
    "number": 35406,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-07-07",
    "title": "private broadcast: limit outstanding txs to count of 10,000"
   },
   {
    "number": 35680,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-08-18",
    "title": "private broadcast: bound rebroadcast attempts to 1,000"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "doc/release-notes-34707.md",
  "src/net_processing.cpp",
  "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"
 ],
 "body": "Follow up from #34329.\n\nPrivate broadcast transaction data is removed from memory when either 1) the tx is received back from another peer and inserted into our mempool or 2) the tx is no longer acceptable to our mempool.\n\nWe want to persist this data to be able to verify that transactions have been received back, and to use that information for statistical analysis. Also see https://github.com/bitcoin/bitcoin/issues/30471.\n\nThis PR changes the behavior of the private broadcast queue to keep received txs in memory. The peer's address we received the tx from and the time is now also returned via `getprivatebroadcastinfo` RPC.\n\nMempool rejected txs are still removed from the queue as before.",
 "commits": [
  {
   "sha": "f6753e7f430df7ca0bafe60a56f30724bf3cca73",
   "date": "2026-09-03T01:20:46Z",
   "message": "net: track received peer in private broadcast\n\nIntroduce MarkReceived method in PrivateBroadcast to record which peer\nrelayed the transaction back to us, and at what time. The transaction stays in the\nqueue instead of being removed, preserving broadcast metadata.\nHowever, the transaction will not be selected for broadcasting again.\n\nThe received metadata is cleared when a transaction is added to the queue again,\nallowing the transaction to be broadcast again."
  },
  {
   "sha": "495b03bf68972e8191d6f838d672a2e670f4e043",
   "date": "2026-09-03T01:20:46Z",
   "message": "test: extract find_tx_info to static helper\n\nNon-functional refactor."
  },
  {
   "sha": "b40c8f18fd61a8a7b009e2764479338c8d8adcd0",
   "date": "2026-09-03T01:20:46Z",
   "message": "test: add unit test covering MarkReceived"
  },
  {
   "sha": "9217011129558f3bda626bce0cb343f2861a2967",
   "date": "2026-09-03T01:20:47Z",
   "message": "doc: release notes for 34707"
  }
 ],
 "timeline": [
  {
   "t": "2026-03-02T02:03:24Z",
   "kind": "force_push",
   "who": "andrewtoth",
   "commit": "afca84489c26f920d842f34d00f8418349255a65"
  },
  {
   "t": "2026-03-06T10:37:46Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/net_processing.cpp",
   "commit": "fa857a980c2fd6a1f88cea8d0cff984824352609",
   "in_reply_to": null,
   "text": "nit: indenting is off"
  },
  {
   "t": "2026-03-06T10:40:58Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/private_broadcast.cpp",
   "commit": "fa857a980c2fd6a1f88cea8d0cff984824352609",
   "in_reply_to": null,
   "text": "What's the reason for using \"missing-inputs\" here?"
  },
  {
   "t": "2026-03-06T10:44:47Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/private_broadcast.cpp",
   "commit": "fa857a980c2fd6a1f88cea8d0cff984824352609",
   "in_reply_to": null,
   "text": "I find the dependency between `rejection_reason` and `received_from` confusing here. If the TX can be accepted in the mempool, the received from info is cleared. Could you elaborate on the why?\n\nUpdate: I found it described in the header comment, but I still don't understand fully (may be my problem)."
  },
  {
   "t": "2026-03-06T10:55:25Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/private_broadcast.h",
   "commit": "fa857a980c2fd6a1f88cea8d0cff984824352609",
   "in_reply_to": null,
   "text": "nit: Ordering according to the chronology of setting the fields would make more sense (i.e., `sent_to` first)."
  },
  {
   "t": "2026-03-06T11:14:42Z",
   "kind": "review",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "afca84489c26f920d842f34d00f8418349255a65",
   "text": "ACK 1df06de4ed97fd95758f4cdcd0d3fe80a3a6dda0\n\nRe-checked following previous rounds of changes, re-ack'd.\n\nPrevious:\nACK d5d7c28e1b4b76daddad95a23cbf56209ca0b86e\nConceptACK bd31f7391dd45b2582092762466e40ad2594b178\nAnother round of review, no concerns (but keeping the status for now).\nPrev:\nPre-review, left some minor comments.\nA general conceptual point is raised in separate comment (main-thread)."
  },
  {
   "t": "2026-03-06T11:20:10Z",
   "kind": "comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "text": "A more important conceptual point I'd like to raise:\nthis work is the precursor of features that are related to __locally-originated__ transactions, and not __private broadcast__. I think that it is important to clarify this and use the right nomenclature at this early stage.\n\nAs I see:\n- there where wishes/proposals related to local-originated transaction handling, private broadcast being one of them\n- private broadcast for locally-originating txs was implemented (\ud83d\udd25), and it introduced a short-lived queue needed for private broadcast\n- other features are starting to be implemented on top of the private broadcast queue, but logically they are not strictly related to the private nature of the broadcast, but the locally-generated property of it.\n- a good test is to answer the question: would this feature make sense even if the broadcast was not private, but 'regular' broadcast? If the answer is yes or likely yes, than the feaure is not related to the private broadcast, and its naming should not be based on it.\n- it should be even possible to decouple private broadcast from other local-originating features (though it makes sense to use private bc always).\n- With the current direction of things I even propose that the new RPC's names (`getprivatebroadcastinfo`) should not be based on the 'private' feature, but 'local tx' . Also, if the mentioned features are being considered, the name of the queue could be reconsidered at this point.\n\nIn short, for the newly considered features I consider the \"locally-originated TX\" to be more relevant than the \"private\" nature of the broadcast, and suggest using it in namings.\n\n(fyi @vasild)"
  },
  {
   "t": "2026-03-09T11:45:37Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/rpc/mempool.cpp",
   "commit": "9217011129558f3bda626bce0cb343f2861a2967",
   "in_reply_to": null,
   "text": "In the commit message of fa857a980c2fd6a1f88cea8d0cff984824352609 `net: track received peer and mempool status in private broadcast`:\n\n\"... or not accepted to mempool in stale check\"\n\n`s/accepted/acceptable/`"
  },
  {
   "t": "2026-03-09T14:15:46Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "afca84489c26f920d842f34d00f8418349255a65",
   "in_reply_to": null,
   "text": "In commit fa857a980c2fd6a1f88cea8d0cff984824352609 `net: track received peer and mempool status in private broadcast` the return is unnecessary changed from\n\n```cpp\nreturn tx;\n```\nto\n```cpp\nreturn it->first;\n```\nI find the former more readable."
  },
  {
   "t": "2026-03-09T14:30:21Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "d5d7c28e1b4b76daddad95a23cbf56209ca0b86e",
   "in_reply_to": null,
   "text": "Unnecessary change."
  },
  {
   "t": "2026-03-10T09:34:01Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "afca84489c26f920d842f34d00f8418349255a65",
   "in_reply_to": null,
   "text": "\"Giving up\" brings the sense of finality. With this change we may start sending the tx again in the future. Maybe change to \"Stopping\". Also, I think it would be more clear to put \"not acceptable in our mempool: %s\" in there.\n\n```suggestion\n                LogDebug(BCLog::PRIVBROADCAST, \"Stopping broadcast attempts for txid=%s wtxid=%s: not acceptable in our mempool: %s\",\n```"
  },
  {
   "t": "2026-03-10T10:01:41Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "afca84489c26f920d842f34d00f8418349255a65",
   "in_reply_to": null,
   "text": "There is this notion of \"if acceptable in our mempool - send it, otherwise don't send it\".\n\nHere in this PR we reset the state from `Add()` for an already existent transaction to \"acceptable in our mempool\". However, this will not result in \"send it\". That is because `PeerManagerImpl::ReattemptPrivateBroadcast()` will reevaluate and reset mempool acceptance state before deciding whether a transaction is eligible for rebroadcast. So the effect of resetting the state to \"acceptable\" in `Add()` will be short lived and would have no effect.\n\nMaybe keep this PR to just retaining the transactions after broadcast is concluded, without changing the functionality of `Add()`?"
  },
  {
   "t": "2026-03-10T10:06:20Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "1df06de4ed97fd95758f4cdcd0d3fe80a3a6dda0",
   "in_reply_to": null,
   "text": "`GetStale()` needs to be changed to omit the concluded (finished) transactions."
  },
  {
   "t": "2026-03-10T10:08:39Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "afca84489c26f920d842f34d00f8418349255a65",
   "in_reply_to": null,
   "text": "This is repeated also in `PrivateBroadcast::HavePendingTransactions()`. Better introduce a new method `PrivateBroadcast::GetPendingTransactions()` and use it here and in `HavePendingTransactions()`."
  },
  {
   "t": "2026-03-10T10:14:48Z",
   "kind": "review",
   "who": "vasild",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "afca84489c26f920d842f34d00f8418349255a65",
   "text": "Reviewed up to and including fa857a980c `net: track received peer and mempool status in private broadcast`.\n\n@optout21, good points. Consider also https://github.com/bitcoin/bitcoin/pull/34533 and https://github.com/bitcoin/bitcoin/issues/3828. In the light of that, a sensitive transaction may not have originated locally. So \"local\" may not be the best term to use for these."
  },
  {
   "t": "2026-03-10T15:38:11Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "afca84489c26f920d842f34d00f8418349255a65",
   "in_reply_to": 2910632010,
   "text": "[quoted text omitted]\n\nIt *may* reevaluate and reset mempool acceptance, but it may also be sent first from one of the connections that are created.\nI think we can solve this issue by checking if a tx exists in the mempool and continuing before updating mempool state in `ReattemptPrivateBroadcast`."
  },
  {
   "t": "2026-03-10T15:45:01Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "1df06de4ed97fd95758f4cdcd0d3fe80a3a6dda0",
   "in_reply_to": 2910659271,
   "text": "I was thinking more about this and I don't see a way to have \"finished\" transactions.\n\nIf a tx is acceptable to our mempool, it is not yet finished because it has not been received.\nIf a tx exists in our mempool, it is not yet finished because it has not been mined and may be evicted.\nIf a tx is not acceptable to our mempool, it or a conflicting tx *may* have been mined. However, it may also be the case that its parent tx has been evicted from our mempool as well and not yet mined. Either way, checking the confirmed utxo set will also show this tx as unacceptable, when it will be acceptable if the parent enters the mempool again. This tx is not yet finished since it may be acceptable to our mempool again if the parent returns.\n\nSo, is there a heuristic we can use to determine if a tx is concluded (finished)? I don't see one. We need to keep txs in the queue indefinitely.\nOne way is to have an expiry on txs which you suggested in #34322, but I believe that is out of scope for this PR."
  },
  {
   "t": "2026-03-11T01:26:17Z",
   "kind": "force_push",
   "who": "andrewtoth",
   "commit": "2c3c80de063ac2d12cf17c037cba4d5b08d39fb3"
  },
  {
   "t": "2026-03-11T01:27:39Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "fa857a980c2fd6a1f88cea8d0cff984824352609",
   "in_reply_to": 2895073551,
   "text": "Removed this line."
  },
  {
   "t": "2026-03-11T01:31:29Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "fa857a980c2fd6a1f88cea8d0cff984824352609",
   "in_reply_to": 2895089313,
   "text": "I updated this logic and added a clarifying comment.\nIt only clears the received from info now if there is a transition from mempool rejected -> mempool acceptable. This means the tx has been evicted from our mempool for some reason that does not cause a conflict with it. In this case, it is like we never received the tx from a peer. It would be confusing to have a state where we received it but it is not in our mempool and not conflicted or confirmed. So, we clear received state and try the broadcasting flow again.\n\nNow we also pick txs if they have are acceptable to our mempool and have no received from state. So, it is necessary to clear this data to ever have this tx picked for private broadcast again."
  },
  {
   "t": "2026-03-11T01:33:34Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "d5d7c28e1b4b76daddad95a23cbf56209ca0b86e",
   "in_reply_to": 2905806446,
   "text": "It's necessary in a later commit. I moved it to the one that requires this change."
  },
  {
   "t": "2026-03-11T01:33:49Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/rpc/mempool.cpp",
   "commit": "9217011129558f3bda626bce0cb343f2861a2967",
   "in_reply_to": 2904920457,
   "text": "Oops forgot to update the commit message. Will do when I push again."
  },
  {
   "t": "2026-03-11T01:37:04Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "afca84489c26f920d842f34d00f8418349255a65",
   "in_reply_to": 2910632010,
   "text": "I removed this change to `Add` logic in this PR. But, it required also removing `\"Sending a transaction that is already in the mempool\"` from the functional test. Since resending a tx already in the queue now will not trigger a rebroadcast."
  },
  {
   "t": "2026-03-11T01:40:28Z",
   "kind": "force_push",
   "who": "andrewtoth",
   "commit": "9f58d9a1691b10341784e13ae32549bb0244bbda"
  },
  {
   "t": "2026-03-11T01:50:16Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "text": "Thank you for your reviews @optout21 and @vasild.\n- `Add` no longer clears received state, so it is a simpler change. However, it does not rebroadcast txs if they are already received and sent again with `sendrawtransaction`. Removed unit test and functional test that covered this.\n- Only clear received state if transitioning from mempool rejected -> mempool acceptable. This happens if the tx is evicted but is not conflicted. In this case it is no longer \"received\" and we want to start the broadcast flow again.\n- Check if a tx already exists in mempool in `ReattemptPrivateBroadcast` before checking mempool acceptance.\n\nAlso addressed several other refactor and style changes, and split up and reordered commits for easier review.\n\n`git range-diff 9cad97f6cdf1bd660732cd10e844a6a7e0771ea0..afca84489c26f920d842f34d00f8418349255a65 eed316189376d72fc959e8d2e45cafef279f77da..9f58d9a1691b10341784e13ae32549bb0244bbda`\n\n[quoted text omitted]\n@optout21 that's an interesting point. I do think it is out of scope for this PR though. Could be raised independently."
  },
  {
   "t": "2026-03-11T13:39:59Z",
   "kind": "force_push",
   "who": "andrewtoth",
   "commit": "c9b19d76dd336b2342d6d766b408410e1b93107b"
  },
  {
   "t": "2026-03-11T13:45:40Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "fa857a980c2fd6a1f88cea8d0cff984824352609",
   "in_reply_to": 2895089313,
   "text": "I thought about this some more, and it seems it was as confusing as it was because I was trying to use a single piece of state to represent two different possible states. If `mempool_rejection_reason` was not `nullopt` it could mean either the tx is not in the mempool and is not acceptable, or is currently in the mempool.\n\nI split this out into an enum with 3 states:\n`IN_MEMPOOL`,\n`ACCEPTABLE`,\n`REJECTED`.\n\nNow only if we are `REJECTED` will we store a reason string.\n\nThis simplifies the logic since only `ACCEPTABLE` txs will be picked for broadcast and returned as pending."
  },
  {
   "t": "2026-03-11T13:47:38Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "text": "Updated test to fix a bad rebase. Also, addressed feedback to make rebroadcast logic more clear https://github.com/bitcoin/bitcoin/pull/34707#discussion_r2918476073.\n\n`git diff 9f58d9a1691b10341784e13ae32549bb0244bbda..c9b19d76dd336b2342d6d766b408410e1b93107b`"
  },
  {
   "t": "2026-03-11T15:12:48Z",
   "kind": "force_push",
   "who": "andrewtoth",
   "commit": "6ecbf1899e0ae79611a03221b73e52d75be6dbc4"
  },
  {
   "t": "2026-03-11T20:58:27Z",
   "kind": "force_push",
   "who": "andrewtoth",
   "commit": "911d4ebe235e4d50761aa79a339dc3100c54bbd5"
  },
  {
   "t": "2026-03-12T00:12:06Z",
   "kind": "force_push",
   "who": "andrewtoth",
   "commit": "bd31f7391dd45b2582092762466e40ad2594b178"
  },
  {
   "t": "2026-03-12T10:17:13Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/private_broadcast.cpp",
   "commit": "afca84489c26f920d842f34d00f8418349255a65",
   "in_reply_to": 2910671587,
   "text": "I second to the proposal for a separate `HavePendingTransactions()`. For simplicity, it can just call `GetPendingTransactions()` and test for emptiness, or do a similar search with early exit. I don't think performance is relevant here (queues will be small generally), but it's nice to express intent precisely."
  },
  {
   "t": "2026-03-12T10:25:15Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/private_broadcast.h",
   "commit": "420abcb42742a8baa768c1978eb17bd019478ccc",
   "in_reply_to": null,
   "text": "Nit: \"was sent and confirmed by the recipient\" -> \"was sent and confirmed by a recipient\"\nReading \"the recipient\" made me think that it's always the same recipient."
  },
  {
   "t": "2026-03-12T12:09:41Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "afca84489c26f920d842f34d00f8418349255a65",
   "in_reply_to": 2910671587,
   "text": "I took this suggestion. There is now both `HavePendingTransactions` and `GetPendingTransactions`. I just left a thumbs up and resolved this comment though and didn't leave a message."
  },
  {
   "t": "2026-03-12T13:30:12Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/private_broadcast.cpp",
   "commit": "afca84489c26f920d842f34d00f8418349255a65",
   "in_reply_to": 2910671587,
   "text": "Sorry, my mistake (I pattern-matched `!GetPendingTransactions().empty()` and missed that it's exactly in `HavePendingTransactions` :D)"
  },
  {
   "t": "2026-03-12T14:32:18Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "text": "Changed the approach to use an enum for the mempool states we care about:\n`IN_MEMPOOL`,\n`ACCEPTABLE`,\n`REJECTED`.\n\nThis should make it easier to reason about what is happening in this PR.\nI also split up the commits so they are easier to review."
  },
  {
   "t": "2026-03-17T17:13:21Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "1df06de4ed97fd95758f4cdcd0d3fe80a3a6dda0",
   "in_reply_to": 2910659271,
   "text": "A transaction is certainly finished if it is mined or if a conflicting transaction is mined, no?\n\nCurrently (on `master` without this PR) we consider a transaction finished once we receive it back from the network without even checking whether it will make it to our mempool. Then we call `Remove()` and forget about the transaction.\n\nThis is deliberate to avoid infinite broadcasting of unacceptable transactions. That leaves retries of transactions that made it back but after a while need to be broadcast again at the hands of the users. I think that is worth reviewing and better be done in a focused PR that does just that. That would slim the current PR to just keeping the finished transactions in memory, without changing the meaning of \"finished\"."
  },
  {
   "t": "2026-03-20T19:29:46Z",
   "kind": "force_push",
   "who": "andrewtoth",
   "commit": "420abcb42742a8baa768c1978eb17bd019478ccc"
  },
  {
   "t": "2026-03-20T19:33:30Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "text": "I updated this PR to only track transactions that are in the happy-path of received back from the network. Tracking txs that are removed due to mempool rejection and rebroadcasting is left for follow-ups.\n\nThis should make this much simpler to review.\n\nI also aligned naming to what is done in https://github.com/bitcoin/bitcoin/pull/34873."
  },
  {
   "t": "2026-03-20T19:35:38Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "1df06de4ed97fd95758f4cdcd0d3fe80a3a6dda0",
   "in_reply_to": 2910659271,
   "text": "[quoted text omitted]\n\nHow to determine this is tricky. There are a lot of ways to potentially solve this problem with different trade-offs. Let's leave this to a follow-up."
  },
  {
   "t": "2026-03-20T19:39:05Z",
   "kind": "force_push",
   "who": "andrewtoth",
   "commit": "673124c53013a91d0ca7a70de9c54d521bb3c308"
  },
  {
   "t": "2026-03-20T19:39:40Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.h",
   "commit": "420abcb42742a8baa768c1978eb17bd019478ccc",
   "in_reply_to": 2923644791,
   "text": "Done, and updated the docstring for `Removed` as well."
  },
  {
   "t": "2026-03-23T10:24:19Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/private_broadcast.cpp",
   "commit": "1df06de4ed97fd95758f4cdcd0d3fe80a3a6dda0",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nIn `PrivateBroadcast::Add`, when the TX was already present, but has not been received back yet, it returns `true`. However, the documentation says for the return value: \"The transaction was already present.\". I feel an inconsistency here: either the code should return `false` in this case, or the documentation be more precise."
  },
  {
   "t": "2026-03-23T10:31:17Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/net_processing.cpp",
   "commit": "d5d7c28e1b4b76daddad95a23cbf56209ca0b86e",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nThis change has became unnecessary, it is no longer needed by later commit (was discussed previously in Resolved thread)."
  },
  {
   "t": "2026-03-23T10:32:39Z",
   "kind": "review",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "673124c53013a91d0ca7a70de9c54d521bb3c308",
   "text": "Minor comments provided."
  },
  {
   "t": "2026-03-23T13:38:57Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "1df06de4ed97fd95758f4cdcd0d3fe80a3a6dda0",
   "in_reply_to": 2974135205,
   "text": "I think the documentation should be updated.\nWe need to return true in this case to maintain the same behavior. Previously, a received tx is removed, so calling `Add` again would return `true` and add the tx with a fresh state."
  },
  {
   "t": "2026-03-23T13:42:23Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "d5d7c28e1b4b76daddad95a23cbf56209ca0b86e",
   "in_reply_to": 2974171982,
   "text": "Removing this change causes a compilation error. It is needed when we add new fields to `TxBroadcastInfo`. The later commit that was discussed previously has been rolled into this one commit. In the previous version, updates to `TxBroadcastInfo` happened later and only `TxState` was updated, which is why the change wasn't needed earlier.\n\n```\n../src/net_processing.cpp: In member function \u2018virtual std::vector<std::shared_ptr<const CTransaction> > {anonymous}::PeerManagerImpl::AbortPrivateBroadcast(const uint256&)\u2019:\n../src/net_processing.cpp:1871:22: error: only 2 names provided for structured binding\n 1871 |     for (const auto& [tx, _] : snapshot) {\n      |                      ^~~~~~~\n../src/net_processing.cpp:1871:22: note: while \u2018const PrivateBroadcast::TxBroadcastInfo\u2019 decomposes into 4 elements\n```"
  },
  {
   "t": "2026-03-23T14:28:49Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/net_processing.cpp",
   "commit": "d5d7c28e1b4b76daddad95a23cbf56209ca0b86e",
   "in_reply_to": 2974171982,
   "text": "Got it. Sg. like `(const auto& [tx, _a, _b, _c, _d] : snapshot)` would be needed, so the changed version is better."
  },
  {
   "t": "2026-03-23T22:33:32Z",
   "kind": "force_push",
   "who": "andrewtoth",
   "commit": "d5d7c28e1b4b76daddad95a23cbf56209ca0b86e"
  },
  {
   "t": "2026-03-23T22:36:25Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "text": "Thanks for your review @optout21. Took your suggestion https://github.com/bitcoin/bitcoin/pull/34707#discussion_r2974135205. Only diff is changing docstring of `PrivateBroadcast::Add()`.\n\n`git diff 673124c53013a91d0ca7a70de9c54d521bb3c308..d5d7c28e1b4b76daddad95a23cbf56209ca0b86e`"
  },
  {
   "t": "2026-03-26T11:15:04Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "d5d7c28e1b4b76daddad95a23cbf56209ca0b86e",
   "in_reply_to": null,
   "text": "Just curious - why change the type of the `DerivePriority()`'s parameter? Do you think that maybe in the future it might be needed to use also other fields from `TxSendStatus` to derive the priority?"
  },
  {
   "t": "2026-03-26T11:25:35Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/test/private_broadcast_tests.cpp",
   "commit": "d5d7c28e1b4b76daddad95a23cbf56209ca0b86e",
   "in_reply_to": null,
   "text": "Correct me if I am wrong, anonymous namespace and `static` (on global functions) achieve the same purpose - the symbols will stay within that file when compiled, so they cannot collide with symbols from other files. If that is the case, then better use just one - either `static` or a namespace."
  },
  {
   "t": "2026-03-26T13:28:21Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.h",
   "commit": "d5d7c28e1b4b76daddad95a23cbf56209ca0b86e",
   "in_reply_to": null,
   "text": "`received_from` and `received_time` must be either both set or both not set. To enforce this we can have one `optional` holding a struct with two members: `from` and `when`.\n\n```cpp\n    struct TxSendStatus {\n        std::vector<SendStatus> send_statuses;\n        struct Received {\n            CService from;\n            NodeClock::time_point when;\n        }\n        std::optional<Received> received;\n    };\n```"
  },
  {
   "t": "2026-03-26T16:39:22Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "9217011129558f3bda626bce0cb343f2861a2967",
   "in_reply_to": null,
   "text": "Inbound Tor connections would probably look like `127.0.0.1` (the address of the Tor router). I am not sure if we should bother trying to make this more user friendly. If yes, then something like:\n\n```\nif pfrom.m_inbound_onion is true\n    CService s;\n    s.SetSpecial(\"inboundconnectionfromtoraaaaaaaaaaaaaaaaaaaaaaaaaaan5wyd.onion\");\n    use s\nelse\n    use pfrom.addr\n```"
  },
  {
   "t": "2026-03-26T16:48:47Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_private_broadcast.py",
   "commit": "9217011129558f3bda626bce0cb343f2861a2967",
   "in_reply_to": null,
   "text": "This line is now not needed, can drop it."
  },
  {
   "t": "2026-03-26T16:49:14Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "test/functional/p2p_private_broadcast.py",
   "commit": "9217011129558f3bda626bce0cb343f2861a2967",
   "in_reply_to": null,
   "text": "Why remove this test? It is passing, so maybe restore it?\n\n```diff\n--- i/test/functional/p2p_private_broadcast.py\n+++ w/test/functional/p2p_private_broadcast.py\n@@ -444,12 +444,17 @@ class P2PPrivateBroadcast(BitcoinTestFramework):\n         info = [t for t in pbinfo[\"transactions\"] if t[\"wtxid\"] == txs[0][\"wtxid\"]]\n         assert_equal(len(info), 1)\n         assert \"received_from\" in info[0]\n         assert \"received_time\" in info[0]\n         skip_destinations = len(self.destinations)\n\n+        self.log.info(\"Sending a transaction that is already in the mempool\")\n+        skip_destinations = len(self.destinations)\n+        tx_originator.sendrawtransaction(hexstring=txs[0][\"hex\"], maxfeerate=0)\n+        self.check_broadcasts(\"Broadcast of mempool transaction\", txs[0], NUM_PRIVATE_BROADCAST_PER_TX, skip_destinations)\n+\n         self.log.info(\"Sending a transaction with a dependency in the mempool\")\n         skip_destinations = len(self.destinations)\n         tx_originator.sendrawtransaction(hexstring=txs[1][\"hex\"], maxfeerate=0.1)\n         self.check_broadcasts(\"Dependency in mempool\", txs[1], NUM_PRIVATE_BROADCAST_PER_TX, skip_destinations)\n\n         self.log.info(\"Sending a transaction with a dependency not in the mempool (should be rejected)\")\n```"
  },
  {
   "t": "2026-03-26T17:02:41Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/test/private_broadcast_tests.cpp",
   "commit": "d5d7c28e1b4b76daddad95a23cbf56209ca0b86e",
   "in_reply_to": null,
   "text": "The naming of this variable is usingCamelCase, should_be_snake."
  },
  {
   "t": "2026-03-26T18:03:56Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "d5d7c28e1b4b76daddad95a23cbf56209ca0b86e",
   "in_reply_to": null,
   "text": "I have been thinking that the public structs `TxBroadcastInfo` and `PeerSendInfo` are the same as the private ones `TxSendStatus` and `SendStatus`. Before it wasn't so striking, but now it becomes more obvious as more fields are added and they have to be added to both and copied here in `GetBroadcastInfo()`\n\nWe can have just one set of structs and return `m_transactions` from `GetBroadcastInfo()`. The callers of `GetBroadcastInfo()` don't care whether it is a `vector` or `unordered_map` as long as they can iterate over it.\n\nHere is a change that does that, on top of this PR. If you do not want to bloat this PR with it, I will submit as a followup:\n\n[patch] Simplify GetBroadcastInfo(); 71 insertions(+), 109 deletions(-)\n\n```diff\ndiff --git i/src/net_processing.cpp w/src/net_processing.cpp\nindex ed3e5ebf98..db8a007a04 100644\n--- i/src/net_processing.cpp\n+++ w/src/net_processing.cpp\n@@ -539,13 +539,13 @@ public:\n     void CheckForStaleTipAndEvictPeers() override;\n     util::Expected<void, std::string> FetchBlock(NodeId peer_id, const CBlockIndex& block_index) override\n         EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);\n     bool GetNodeStateStats(NodeId nodeid, CNodeStateStats& stats) const override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);\n     std::vector<node::TxOrphanage::OrphanInfo> GetOrphanTransactions() override EXCLUSIVE_LOCKS_REQUIRED(!m_tx_download_mutex);\n     PeerManagerInfo GetInfo() const override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);\n-    std::vector<PrivateBroadcast::TxBroadcastInfo> GetPrivateBroadcastInfo() const override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);\n+    PrivateBroadcast::Transactions GetPrivateBroadcastInfo() const override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);\n     std::vector<CTransactionRef> AbortPrivateBroadcast(const uint256& id) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);\n     void SendPings() override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);\n     void InitiateTxBroadcastToAll(const Txid& txid, const Wtxid& wtxid) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);\n     void InitiateTxBroadcastPrivate(const CTransactionRef& tx) override EXCLUSIVE_LOCKS_REQUIRED(!m_peer_mutex);\n     void SetBestBlock(int height, std::chrono::seconds time) override\n     {\n@@ -1854,25 +1854,24 @@ PeerManagerInfo PeerManagerImpl::GetInfo() const\n     return PeerManagerInfo{\n         .median_outbound_time_offset = m_outbound_time_offsets.Median(),\n         .ignores_incoming_txs = m_opts.ignore_incoming_txs,\n     };\n }\n\n-std::vector<PrivateBroadcast::TxBroadcastInfo> PeerManagerImpl::GetPrivateBroadcastInfo() const\n+PrivateBroadcast::Transactions PeerManagerImpl::GetPrivateBroadcastInfo() const\n {\n     return m_tx_for_private_broadcast.GetBroadcastInfo();\n }\n\n std::vector<CTransactionRef> PeerManagerImpl::AbortPrivateBroadcast(const uint256& id)\n {\n     const auto snapshot{m_tx_for_private_broadcast.GetBroadcastInfo()};\n     std::vector<CTransactionRef> removed_txs;\n\n     size_t connections_cancelled{0};\n-    for (const auto& tx_info : snapshot) {\n-        const auto& tx{tx_info.tx};\n+    for (const auto& [tx, status] : snapshot) {\n         if (tx->GetHash().ToUint256() != id && tx->GetWitnessHash().ToUint256() != id) continue;\n         if (const auto peer_acks{m_tx_for_private_broadcast.Remove(tx)}) {\n             removed_txs.push_back(tx);\n             if (NUM_PRIVATE_BROADCAST_PER_TX > *peer_acks) {\n                 connections_cancelled += (NUM_PRIVATE_BROADCAST_PER_TX - *peer_acks);\n             }\ndiff --git i/src/net_processing.h w/src/net_processing.h\nindex 36ae021f67..9472aa7735 100644\n--- i/src/net_processing.h\n+++ w/src/net_processing.h\n@@ -118,13 +118,13 @@ public:\n     virtual std::vector<node::TxOrphanage::OrphanInfo> GetOrphanTransactions() = 0;\n\n     /** Get peer manager info. */\n     virtual PeerManagerInfo GetInfo() const = 0;\n\n     /** Get info about transactions currently being privately broadcast. */\n-    virtual std::vector<PrivateBroadcast::TxBroadcastInfo> GetPrivateBroadcastInfo() const = 0;\n+    virtual PrivateBroadcast::Transactions GetPrivateBroadcastInfo() const = 0;\n\n     /**\n      * Abort private broadcast attempts for transactions currently being privately broadcast.\n      *\n      * @param[in] id A transaction identifier. It will be matched against both txid and wtxid for\n      *               all transactions in the private broadcast queue.\ndiff --git i/src/private_broadcast.cpp w/src/private_broadcast.cpp\nindex 0c70fff7d8..e0349032be 100644\n--- i/src/private_broadcast.cpp\n+++ w/src/private_broadcast.cpp\n@@ -124,37 +124,20 @@ std::vector<CTransactionRef> PrivateBroadcast::GetStale() const\n             stale.push_back(tx);\n         }\n     }\n     return stale;\n }\n\n-std::vector<PrivateBroadcast::TxBroadcastInfo> PrivateBroadcast::GetBroadcastInfo() const\n+PrivateBroadcast::Transactions PrivateBroadcast::GetBroadcastInfo() const\n     EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)\n {\n     LOCK(m_mutex);\n-    std::vector<TxBroadcastInfo> entries;\n-    entries.reserve(m_transactions.size());\n-\n-    for (const auto& [tx, state] : m_transactions) {\n-        std::vector<PeerSendInfo> peers;\n-        peers.reserve(state.send_statuses.size());\n-        for (const auto& status : state.send_statuses) {\n-            peers.emplace_back(PeerSendInfo{.address = status.address, .sent = status.picked, .received = status.confirmed});\n-        }\n-        entries.emplace_back(TxBroadcastInfo{\n-            .tx = tx,\n-            .peers = std::move(peers),\n-            .received_from = state.received_from,\n-            .received_time = state.received_time,\n-        });\n-    }\n-\n-    return entries;\n+    return m_transactions;\n }\n\n-PrivateBroadcast::Priority PrivateBroadcast::DerivePriority(const TxSendStatus& tx_status)\n+PrivateBroadcast::Priority PrivateBroadcast::DerivePriority(const Status& tx_status)\n {\n     Priority p;\n     p.num_picked = tx_status.send_statuses.size();\n     for (const auto& send_status : tx_status.send_statuses) {\n         p.last_picked = std::max(p.last_picked, send_status.picked);\n         if (send_status.confirmed.has_value()) {\n@@ -176,17 +159,17 @@ std::optional<PrivateBroadcast::TxAndSendStatusForNode> PrivateBroadcast::GetSen\n             }\n         }\n     }\n     return std::nullopt;\n }\n\n-std::vector<PrivateBroadcast::TransactionMap::iterator> PrivateBroadcast::GetPendingTransactions()\n+std::vector<PrivateBroadcast::Transactions::iterator> PrivateBroadcast::GetPendingTransactions()\n     EXCLUSIVE_LOCKS_REQUIRED(m_mutex)\n {\n     AssertLockHeld(m_mutex);\n-    std::vector<TransactionMap::iterator> result;\n+    std::vector<Transactions::iterator> result;\n     for (auto it{m_transactions.begin()}; it != m_transactions.end(); ++it) {\n         if (!it->second.received_from.has_value()) {\n             result.push_back(it);\n         }\n     }\n     return result;\ndiff --git i/src/private_broadcast.h w/src/private_broadcast.h\nindex 5c70754df2..3c39ad298a 100644\n--- i/src/private_broadcast.h\n+++ w/src/private_broadcast.h\n@@ -28,25 +28,47 @@\n  * - Query whether a given recipient node has confirmed reception\n  * - Query whether any transactions that need sending are currently on the list\n  */\n class PrivateBroadcast\n {\n public:\n-    struct PeerSendInfo {\n-        CService address;\n-        NodeClock::time_point sent;\n-        std::optional<NodeClock::time_point> received;\n+    /// Status of a transaction sent to a given node.\n+    struct SendStatus {\n+        const NodeId nodeid; /// Node to which the transaction will be sent (or was sent).\n+        const CService address; /// Address of the node.\n+        const NodeClock::time_point picked; ///< When was the transaction picked for sending to the node.\n+        std::optional<NodeClock::time_point> confirmed; ///< When was the transaction reception confirmed by the node (by PONG).\n+\n+        SendStatus(const NodeId& nodeid, const CService& address, const NodeClock::time_point& picked) : nodeid{nodeid}, address{address}, picked{picked} {}\n     };\n\n-    struct TxBroadcastInfo {\n-        CTransactionRef tx;\n-        std::vector<PeerSendInfo> peers;\n-        std::optional<CService> received_from;\n-        std::optional<NodeClock::time_point> received_time;\n+    /// Status of a transaction, including all send attempts and a possible reception info.\n+    struct Status {\n+        std::vector<SendStatus> send_statuses; /// All send attempts.\n+        std::optional<CService> received_from; /// When we receive back the transaction from the network, this is the peer we got it from.\n+        std::optional<NodeClock::time_point> received_time; /// Time of receiving back the transaction.\n     };\n\n+    // No need for salted hasher because we are going to store just a bunch of locally originating transactions.\n+\n+    struct CTransactionRefHash {\n+        size_t operator()(const CTransactionRef& tx) const\n+        {\n+            return static_cast<size_t>(tx->GetWitnessHash().ToUint256().GetUint64(0));\n+        }\n+    };\n+\n+    struct CTransactionRefComp {\n+        bool operator()(const CTransactionRef& a, const CTransactionRef& b) const\n+        {\n+            return a->GetWitnessHash() == b->GetWitnessHash(); // If wtxid equals, then txid also equals.\n+        }\n+    };\n+\n+    using Transactions = std::unordered_map<CTransactionRef, Status, CTransactionRefHash, CTransactionRefComp>;\n+\n     /**\n      * Add a transaction to the storage, or reset a transaction's state if the\n      * transaction has been marked received.\n      * @param[in] tx The transaction to add.\n      * @retval true The transaction was added or reset.\n      * @retval false The transaction was already present and not marked received.\n@@ -124,32 +146,16 @@ public:\n     std::vector<CTransactionRef> GetStale() const\n         EXCLUSIVE_LOCKS_REQUIRED(!m_mutex);\n\n     /**\n      * Get stats about all transactions currently being privately broadcast.\n      */\n-    std::vector<TxBroadcastInfo> GetBroadcastInfo() const\n+    Transactions GetBroadcastInfo() const\n         EXCLUSIVE_LOCKS_REQUIRED(!m_mutex);\n\n private:\n-    /// Status of a transaction sent to a given node.\n-    struct SendStatus {\n-        const NodeId nodeid; /// Node to which the transaction will be sent (or was sent).\n-        const CService address; /// Address of the node.\n-        const NodeClock::time_point picked; ///< When was the transaction picked for sending to the node.\n-        std::optional<NodeClock::time_point> confirmed; ///< When was the transaction reception confirmed by the node (by PONG).\n-\n-        SendStatus(const NodeId& nodeid, const CService& address, const NodeClock::time_point& picked) : nodeid{nodeid}, address{address}, picked{picked} {}\n-    };\n-\n-    struct TxSendStatus {\n-        std::vector<SendStatus> send_statuses;\n-        std::optional<CService> received_from;\n-        std::optional<NodeClock::time_point> received_time;\n-    };\n-\n     /// Cumulative stats from all the send attempts for a transaction. Used to prioritize transactions.\n     struct Priority {\n         size_t num_picked{0}; ///< Number of times the transaction was picked for sending.\n         NodeClock::time_point last_picked{}; ///< The most recent time when the transaction was picked for sending.\n         size_t num_confirmed{0}; ///< Number of nodes that have confirmed reception of a transaction (by PONG).\n         NodeClock::time_point last_confirmed{}; ///< The most recent time when the transaction was confirmed.\n@@ -167,46 +173,28 @@ private:\n     /// A pair of a transaction and a sent status for a given node. Convenience return type of GetSendStatusByNode().\n     struct TxAndSendStatusForNode {\n         const CTransactionRef& tx;\n         SendStatus& send_status;\n     };\n\n-    // No need for salted hasher because we are going to store just a bunch of locally originating transactions.\n-\n-    struct CTransactionRefHash {\n-        size_t operator()(const CTransactionRef& tx) const\n-        {\n-            return static_cast<size_t>(tx->GetWitnessHash().ToUint256().GetUint64(0));\n-        }\n-    };\n-\n-    struct CTransactionRefComp {\n-        bool operator()(const CTransactionRef& a, const CTransactionRef& b) const\n-        {\n-            return a->GetWitnessHash() == b->GetWitnessHash(); // If wtxid equals, then txid also equals.\n-        }\n-    };\n-\n-    using TransactionMap = std::unordered_map<CTransactionRef, TxSendStatus, CTransactionRefHash, CTransactionRefComp>;\n-\n     /**\n      * Derive the sending priority of a transaction.\n      * @param[in] status The send status of the transaction.\n      */\n-    static Priority DerivePriority(const TxSendStatus& status);\n+    static Priority DerivePriority(const Status& status);\n\n     /**\n      * Find which transaction we sent to a given node (marked by PickTxForSend()).\n      * @return That transaction together with the send status or nullopt if we did not\n      * send any transaction to the given node.\n      */\n     std::optional<TxAndSendStatusForNode> GetSendStatusByNode(const NodeId& nodeid)\n         EXCLUSIVE_LOCKS_REQUIRED(m_mutex);\n\n-    std::vector<TransactionMap::iterator> GetPendingTransactions()\n+    std::vector<Transactions::iterator> GetPendingTransactions()\n         EXCLUSIVE_LOCKS_REQUIRED(m_mutex);\n\n     mutable Mutex m_mutex;\n-    TransactionMap m_transactions GUARDED_BY(m_mutex);\n+    Transactions m_transactions GUARDED_BY(m_mutex);\n };\n\n #endif // BITCOIN_PRIVATE_BROADCAST_H\ndiff --git i/src/rpc/mempool.cpp w/src/rpc/mempool.cpp\nindex caa0c9e636..bdc61a3e8c 100644\n--- i/src/rpc/mempool.cpp\n+++ w/src/rpc/mempool.cpp\n@@ -174,34 +174,36 @@ static RPCHelpMan getprivatebroadcastinfo()\n             + HelpExampleRpc(\"getprivatebroadcastinfo\", \"\")\n         },\n         [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue\n         {\n             const NodeContext& node{EnsureAnyNodeContext(request.context)};\n             const PeerManager& peerman{EnsurePeerman(node)};\n-            const auto txs{peerman.GetPrivateBroadcastInfo()};\n+            const auto pbinfo{peerman.GetPrivateBroadcastInfo()};\n\n             UniValue transactions(UniValue::VARR);\n-            for (const auto& tx_info : txs) {\n+            for (const auto& [tx, status] : pbinfo) {\n                 UniValue o(UniValue::VOBJ);\n-                o.pushKV(\"txid\", tx_info.tx->GetHash().ToString());\n-                o.pushKV(\"wtxid\", tx_info.tx->GetWitnessHash().ToString());\n-                o.pushKV(\"hex\", EncodeHexTx(*tx_info.tx));\n+                o.pushKV(\"txid\", tx->GetHash().ToString());\n+                o.pushKV(\"wtxid\", tx->GetWitnessHash().ToString());\n+                o.pushKV(\"hex\", EncodeHexTx(*tx));\n                 UniValue peers(UniValue::VARR);\n-                for (const auto& peer : tx_info.peers) {\n+                for (const auto& peer : status.send_statuses) {\n                     UniValue p(UniValue::VOBJ);\n                     p.pushKV(\"address\", peer.address.ToStringAddrPort());\n-                    p.pushKV(\"sent\", TicksSinceEpoch<std::chrono::seconds>(peer.sent));\n-                    if (peer.received.has_value()) {\n-                        p.pushKV(\"received\", TicksSinceEpoch<std::chrono::seconds>(*peer.received));\n+                    p.pushKV(\"sent\", TicksSinceEpoch<std::chrono::seconds>(peer.picked));\n+                    if (peer.confirmed.has_value()) {\n+                        p.pushKV(\"received\", TicksSinceEpoch<std::chrono::seconds>(*peer.confirmed));\n                     }\n                     peers.push_back(std::move(p));\n                 }\n                 o.pushKV(\"peers\", std::move(peers));\n-                if (tx_info.received_from.has_value()) {\n-                    o.pushKV(\"received_from\", tx_info.received_from->ToStringAddrPort());\n-                    o.pushKV(\"received_time\", TicksSinceEpoch<std::chrono::seconds>(*tx_info.received_time));\n+                if (status.received_from.has_value()) {\n+                    o.pushKV(\"received_from\", status.received_from->ToStringAddrPort());\n+                }\n+                if (status.received_time.has_value()) {\n+                    o.pushKV(\"received_time\", TicksSinceEpoch<std::chrono::seconds>(status.received_time.value()));\n                 }\n                 transactions.push_back(std::move(o));\n             }\n\n             UniValue ret(UniValue::VOBJ);\n             ret.pushKV(\"transactions\", std::move(transactions));\ndiff --git i/src/test/private_broadcast_tests.cpp w/src/test/private_broadcast_tests.cpp\nindex 715cafb190..0cf58e5d0e 100644\n--- i/src/test/private_broadcast_tests.cpp\n+++ w/src/test/private_broadcast_tests.cpp\n@@ -21,19 +21,12 @@ static CTransactionRef MakeDummyTx(uint32_t id, size_t num_witness)\n         mtx.vin[0].scriptWitness = CScriptWitness{};\n         mtx.vin[0].scriptWitness.stack.resize(num_witness);\n     }\n     return MakeTransactionRef(mtx);\n }\n\n-static auto FindTxInfo(const std::vector<PrivateBroadcast::TxBroadcastInfo>& infos, const CTransactionRef& tx)\n-{\n-    const auto it{std::ranges::find(infos, tx->GetWitnessHash(), [](const auto& info) { return info.tx->GetWitnessHash(); })};\n-    BOOST_REQUIRE(it != infos.end());\n-    return it;\n-}\n-\n } // namespace\n\n BOOST_FIXTURE_TEST_SUITE(private_broadcast_tests, BasicTestingSetup)\n\n BOOST_AUTO_TEST_CASE(basic)\n {\n@@ -63,14 +56,14 @@ BOOST_AUTO_TEST_CASE(basic)\n     BOOST_REQUIRE(tx1->GetWitnessHash() != tx2->GetWitnessHash());\n\n     BOOST_CHECK(pb.Add(tx2));\n     const auto check_peer_counts{[&](size_t tx1_peer_count, size_t tx2_peer_count) {\n         const auto infos{pb.GetBroadcastInfo()};\n         BOOST_CHECK_EQUAL(infos.size(), 2);\n-        BOOST_CHECK_EQUAL(FindTxInfo(infos, tx1)->peers.size(), tx1_peer_count);\n-        BOOST_CHECK_EQUAL(FindTxInfo(infos, tx2)->peers.size(), tx2_peer_count);\n+        BOOST_CHECK_EQUAL(infos.at(tx1).send_statuses.size(), tx1_peer_count);\n+        BOOST_CHECK_EQUAL(infos.at(tx2).send_statuses.size(), tx2_peer_count);\n     }};\n\n     check_peer_counts(/*tx1_peer_count=*/0, /*tx2_peer_count=*/0);\n\n     const auto tx_for_recipient1{pb.PickTxForSend(/*will_send_to_nodeid=*/recipient1, /*will_send_to_address=*/addr1).value()};\n     BOOST_CHECK(tx_for_recipient1 == tx1 || tx_for_recipient1 == tx2);\n@@ -105,24 +98,22 @@ BOOST_AUTO_TEST_CASE(basic)\n     BOOST_CHECK(pb.DidNodeConfirmReception(recipient1));\n     BOOST_CHECK(!pb.DidNodeConfirmReception(recipient2));\n\n     const auto infos{pb.GetBroadcastInfo()};\n     BOOST_CHECK_EQUAL(infos.size(), 2);\n     {\n-        const auto info_it{FindTxInfo(infos, tx_for_recipient1)};\n-        const auto& peers{info_it->peers};\n+        const auto& peers{infos.at(tx_for_recipient1).send_statuses};\n         BOOST_CHECK_EQUAL(peers.size(), 1);\n         BOOST_CHECK_EQUAL(peers[0].address.ToStringAddrPort(), addr1.ToStringAddrPort());\n-        BOOST_CHECK(peers[0].received.has_value());\n+        BOOST_CHECK(peers[0].confirmed.has_value());\n     }\n     {\n-        const auto info_it{FindTxInfo(infos, tx_for_recipient2)};\n-        const auto& peers{info_it->peers};\n+        const auto& peers{infos.at(tx_for_recipient2).send_statuses};\n         BOOST_CHECK_EQUAL(peers.size(), 1);\n         BOOST_CHECK_EQUAL(peers[0].address.ToStringAddrPort(), addr2.ToStringAddrPort());\n-        BOOST_CHECK(!peers[0].received.has_value());\n+        BOOST_CHECK(!peers[0].confirmed.has_value());\n     }\n\n     BOOST_CHECK_EQUAL(pb.GetStale().size(), 1);\n     BOOST_CHECK_EQUAL(pb.GetStale()[0], tx_for_recipient2);\n\n     SetMockTime(Now<NodeSeconds>() + 10h);\n@@ -164,36 +155,35 @@ BOOST_AUTO_TEST_CASE(mark_received)\n\n     // MarkReceived succeeds and returns the number of confirmed sends.\n     BOOST_CHECK_EQUAL(pb.MarkReceived(tx, received_from).value(), 1);\n\n     {\n         const auto infos{pb.GetBroadcastInfo()};\n-        const auto info{FindTxInfo(infos, tx)};\n-        BOOST_CHECK_EQUAL(info->received_from->ToStringAddrPort(), received_from.ToStringAddrPort());\n-        BOOST_CHECK(info->received_time.has_value());\n+        const auto& info{infos.at(tx)};\n+        BOOST_CHECK_EQUAL(info.received_from->ToStringAddrPort(), received_from.ToStringAddrPort());\n+        BOOST_CHECK(info.received_time.has_value());\n     }\n     BOOST_CHECK(!pb.HavePendingTransactions());\n     BOOST_CHECK(!pb.PickTxForSend(/*will_send_to_nodeid=*/recipient2, /*will_send_to_address=*/addr2).has_value());\n\n     // Subsequent MarkReceived returns nullopt and does not overwrite.\n     in_addr ipv4Addr2;\n     ipv4Addr2.s_addr = 0xa0b0c099;\n     const CService received_from2{ipv4Addr2, 4444};\n     BOOST_CHECK(!pb.MarkReceived(tx, received_from2).has_value());\n\n     {\n         const auto infos{pb.GetBroadcastInfo()};\n-        const auto info{FindTxInfo(infos, tx)};\n-        BOOST_CHECK_EQUAL(info->received_from->ToStringAddrPort(), received_from.ToStringAddrPort());\n+        BOOST_CHECK_EQUAL(infos.at(tx).received_from->ToStringAddrPort(), received_from.ToStringAddrPort());\n         BOOST_CHECK(!pb.HavePendingTransactions());\n     }\n\n     // Re-adding after received clears the received state and makes it pending again.\n     BOOST_CHECK(pb.Add(tx));\n     BOOST_CHECK(pb.HavePendingTransactions());\n     const auto infos{pb.GetBroadcastInfo()};\n-    const auto info{FindTxInfo(infos, tx)};\n-    BOOST_CHECK(!info->received_from.has_value());\n-    BOOST_CHECK(!info->received_time.has_value());\n+    const auto& info{infos.at(tx)};\n+    BOOST_CHECK(!info.received_from.has_value());\n+    BOOST_CHECK(!info.received_time.has_value());\n }\n\n BOOST_AUTO_TEST_SUITE_END()\n```"
  },
  {
   "t": "2026-03-26T18:04:48Z",
   "kind": "review",
   "who": "vasild",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "d5d7c28e1b4b76daddad95a23cbf56209ca0b86e",
   "text": "Approach ACK d5d7c28e1b4b76daddad95a23cbf56209ca0b86e"
  },
  {
   "t": "2026-04-04T15:27:12Z",
   "kind": "force_push",
   "who": "andrewtoth",
   "commit": "f60a433037b925e1e23c9ca91151511734634d36"
  },
  {
   "t": "2026-04-04T16:48:00Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "text": "Thank you for the detailed review @vasild. I've taken many of your suggestions.\n\nRebased due to #34873, which meant dropping the first commit.\n\n`git range-diff 902708e..d5d7c28 master..HEAD`"
  },
  {
   "t": "2026-04-04T16:50:19Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "d5d7c28e1b4b76daddad95a23cbf56209ca0b86e",
   "in_reply_to": 2994096769,
   "text": "This commit was dropped, so no longer relevant.\n\n[quoted text omitted]\nYes. If we want to periodically rebroadcast txs that have been received but are still acceptable to our utxo set (not mined or conflicted), we would need more information here. But this needs more discussion and we can update this parameter when we decide to do something like that."
  },
  {
   "t": "2026-04-04T16:50:35Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/test/private_broadcast_tests.cpp",
   "commit": "d5d7c28e1b4b76daddad95a23cbf56209ca0b86e",
   "in_reply_to": 2994171471,
   "text": "Removed `static`."
  },
  {
   "t": "2026-04-04T16:50:54Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.h",
   "commit": "d5d7c28e1b4b76daddad95a23cbf56209ca0b86e",
   "in_reply_to": 2994899718,
   "text": "Done."
  },
  {
   "t": "2026-04-04T17:00:29Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "9217011129558f3bda626bce0cb343f2861a2967",
   "in_reply_to": 2996266154,
   "text": "I decided not to take this. I'm not sure this is the best way to signal to a user that this is an inbound tor connection. They might mistake it for the actual address and be confused when it doesn't exist. I think a cleaner approach might be to use a variant of `{CService, std::string}` and write `\"inbound tor\"` for the string? But I don't think the code complexity would be worth it. I'm not entirely convinced using `127.0.0.1` will cause any issues."
  },
  {
   "t": "2026-04-04T17:01:17Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "d5d7c28e1b4b76daddad95a23cbf56209ca0b86e",
   "in_reply_to": 2996756760,
   "text": "I had similar thoughts, but didn't want to bloat the PR. I will leave this for your follow-up."
  },
  {
   "t": "2026-04-13T00:06:37Z",
   "kind": "review_comment",
   "who": "kenji-yamam0to",
   "assoc": "CONTRIBUTOR",
   "path": "src/private_broadcast.h",
   "commit": "f60a433037b925e1e23c9ca91151511734634d36",
   "in_reply_to": null,
   "text": "Nit: the `MarkReceived()` docstring still says\n\"if the transaction existed and was removed\",\nbut after this change the transaction is kept in storage.\nShould that wording be updated?"
  },
  {
   "t": "2026-04-13T02:02:59Z",
   "kind": "force_push",
   "who": "andrewtoth",
   "commit": "a6db9ea77b245519186f1341b1bf89ace130a4fa"
  },
  {
   "t": "2026-04-13T02:15:23Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "text": "Addressed nit (https://github.com/bitcoin/bitcoin/pull/34707#discussion_r3070373291 thanks @takeshikurosawaa).\n\n`git diff f60a433037b925e1e23c9ca91151511734634d36..a6db9ea77b245519186f1341b1bf89ace130a4fa`"
  },
  {
   "t": "2026-04-14T13:50:20Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "a6db9ea77b245519186f1341b1bf89ace130a4fa",
   "in_reply_to": null,
   "text": "C++ nit: this creates a temporary and then moves it into the optional. Can be simpler and avoid the temporary and the moving, constructing the object inplace into the optional storage:\n\n```suggestion\n    it->second.received.emplace(received_from, NodeClock::now());\n```"
  },
  {
   "t": "2026-04-14T14:08:49Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "9217011129558f3bda626bce0cb343f2861a2967",
   "in_reply_to": null,
   "text": "There is no cap on the number of transactions kept into memory. I think this is fine, just mentioning. This is unlikely to create problems. A cap can be added in a followup if deemed necessary. If the node is restarted the list will be truncated to 0 because it is kept in memory only."
  },
  {
   "t": "2026-04-14T14:09:35Z",
   "kind": "review",
   "who": "vasild",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "a6db9ea77b245519186f1341b1bf89ace130a4fa",
   "text": "ACK a6db9ea77b245519186f1341b1bf89ace130a4fa\n\nShow Signature\n\n```\n-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA256\n\nACK a6db9ea77b245519186f1341b1bf89ace130a4fa\n-----BEGIN PGP SIGNATURE-----\n\niQQzBAEBCAAdFiEE5k2NRWFNsHVF2czBVN8G9ktVy78FAmneSo0ACgkQVN8G9ktV\ny79XxR//VLYZqFCLl0RxyVk2nSd5QZVVdCmOA0auZXXsXlDg4QaJE6BfgohTS3sw\n0lLxmMAfR+UiWVH1ABq1HQ5RIugI2HHZC2xAljhDK3iRIWUNKFqXskgzQRrcDB6i\nVuLNyFdTs0DTrc5xvGmOXy7AQwDaltrG1gUdB82wlOQeJnzmCSB/Vs4wo5APSP2V\nMHqlfJ2XIzp4QJI0/8ODV4D4f2wPc7S9+JW7H5/UY1jFW0m852kUSu1fXjrySl+y\ncPXHaigcKmwC7MRRK0fkdO/3GUD1reSYzrIN720LvR3Ww+bASVwfQlh2GcunJC7o\n/msYp2aGicrA6alNiEg+YBhZY2JZ+GC9V84VSF+jUprRTF6VGEgxv9XVk7gnrZqL\n+VQsG1cmyabzVow16Ko7KWegI13Kp058/xKkU2Ygot1eyVwgzzWiFr1jMEg7z+Wp\nzwLmtuMXsS2n+VMnwKTXUIaYdt1In/ldxMJaerm0jwEvl2nDuZaL/wQ/h0Lk2wVG\nW2psvURfUW/1f15YqlsoPkBEw4rjhiyhaxZw02cVUiK+09OfRePMJkPSwr2w1yzU\nw5H0jH2TVa5btFEr9biuFv1Rc0/lc/i8tByTFdutJ5j8FOogJxOe1G8hBICc+I+L\nTSqy0bkcy5xv35Blp2un0mFp4icgYZhrWabXLUI+EGRCVU8erp8Jxkc1nvNbkUkx\nXKbwn2oyKDVO03lOmkFocBS019gxihblPTaXd55XC3zvpv5YymbStSILt17okoC8\nRVRnacuiCsfntnoEAzqRaBK02T9RiqlnyzbBVl1YeZRTnlfVWPptXb/8kZSyRrFO\nkNjYu+3C83bt0XK9oPpsNtDpHzK9uB24lsiLwjDNHyXH12VY0vrU6cblvcb51BtT\n9SJRmTim2d3IbiDh/e3Yo9NPIBrmOZxpHHGa/d4W5FxHybcf5qzFtdNlZTfPr9jZ\nyrL3Yt/z4w/48wzvEt0BrX0u/H0ASaVGYvL1G3sX+aSWlO7sk+1q35UKhzB6/3Ex\nBqsE/CTLt36vX+9fhFx4mj1l7/99Tj/n9xAzkJhvkNgTXfIFTpCBpz5xRxwJWtJR\nvORZ2nfKMHX5oGYT47nA2Hf+70cSVdbR2exEfVZDupzkEPIAMOY7tXoeapNSO/5E\n4OVIzSli1WbUyNwu5yD4hAMTmwFNFozrNiFLJo0FzzHQXI/ZXJzHwMdBapj98cGp\nHB/jbb0xAjEv6PnT/WBqgVitiggDAQhH2JjGoihu5D8t+fUaLimX1+RDAh/Jkvbg\n2Vgqvr0+NIyaiz/s9o07zuxB8Y8D5+OWzrbBpu8Uv4ISakXopRMxvSbpjzv5yUPV\nHvNM8FvyR6IFXSbuHXmIlGUPI9/XiQ==\n=Bbwh\n-----END PGP SIGNATURE-----\n```\n\nvasild's public key is [on openpgp.org](https://keys.openpgp.org/vks/v1/by-fingerprint/850E138BA9C0923881DFD55FCE712013582E2230)"
  },
  {
   "t": "2026-04-16T11:29:39Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "d5d7c28e1b4b76daddad95a23cbf56209ca0b86e",
   "in_reply_to": 2996756760,
   "text": "A followup has been opened in https://github.com/bitcoin/bitcoin/pull/35016"
  },
  {
   "t": "2026-04-16T15:23:13Z",
   "kind": "review_comment",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "1df06de4ed97fd95758f4cdcd0d3fe80a3a6dda0",
   "in_reply_to": null,
   "text": "I think we should also set `time_added` to `now()` here, otherwise you're not resetting the entire state of the transaction. However, this would mean making `TxSendStatus::time_added` non-const.\n\nThe downside of leaving as-is is quite minimal: it means that if you call `Add` on an already present tx to reset its state, and the tx was added more than `INITIAL_STALE_DURATION` (5 min) ago, the tx would immediately become stale."
  },
  {
   "t": "2026-04-16T15:48:06Z",
   "kind": "review",
   "who": "danielabrozzoni",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "a6db9ea77b245519186f1341b1bf89ace130a4fa",
   "text": "tACK a6db9ea77b245519186f1341b1bf89ace130a4fa\n\nOne note: since we don't persist mempool invalid transactions for now, if we broadcast a transaction and it gets included in a block before we receive a TX message for it, I think we would then reattempt broadcasting it in ReattemptBroadcast, see that it's not mempool valid anymore, and call `Remove` on it. I don't think it's a big problem, just a edge case I noticed."
  },
  {
   "t": "2026-04-21T10:56:14Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "path": "src/rpc/mempool.cpp",
   "commit": "0e5ef5da3664970587a1b66a54bae4f29af93cb9",
   "in_reply_to": null,
   "text": "In 0e5ef5da3664970587a1b66a54bae4f29af93cb9 net: track received peer in private broadcast - Doesn't this warrant a release note because the API is being updated?"
  },
  {
   "t": "2026-04-21T10:57:30Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "path": "src/test/private_broadcast_tests.cpp",
   "commit": "9217011129558f3bda626bce0cb343f2861a2967",
   "in_reply_to": null,
   "text": "In 3c73bd73e5d180f7196a6b3e5d01bea18c80f0b7 test: extract find_tx_info to static helper - It reads a bit oddly that this is the first commit and the extracted out helper is used in the third commit. Reorder to make this the second commit instead so that it's immediately followed by its usage?"
  },
  {
   "t": "2026-04-21T10:58:25Z",
   "kind": "review",
   "who": "rkrux",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "a6db9ea77b245519186f1341b1bf89ace130a4fa",
   "text": "Started reviewing at a6db9ea77b245519186f1341b1bf89ace130a4fa, left quick comments."
  },
  {
   "t": "2026-04-21T14:24:39Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.h",
   "commit": "0e5ef5da3664970587a1b66a54bae4f29af93cb9",
   "in_reply_to": null,
   "text": "In 0e5ef5da3664970587a1b66a54bae4f29af93cb9 \"net: track received peer in private broadcast\"\n\nWith the addition of received, this struct doesn't feel like a send status anymore and instead more like a communication status because bidirectional information is being stored. My preference is for `s/TxSendStatus/TxCommunicationStatus` but since the term broadcast is already ubiquitous here, so `s/TxSendStatus/TxBroadcastStatus`?"
  },
  {
   "t": "2026-04-21T14:31:45Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.h",
   "commit": "0e5ef5da3664970587a1b66a54bae4f29af93cb9",
   "in_reply_to": null,
   "text": "In 0e5ef5d \"net: track received peer in private broadcast\"\n\nSimilarly, `s/TransactionMap/TransactionBroadcastMap`?"
  },
  {
   "t": "2026-04-21T14:34:49Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.h",
   "commit": "0e5ef5da3664970587a1b66a54bae4f29af93cb9",
   "in_reply_to": null,
   "text": "In 0e5ef5da3664970587a1b66a54bae4f29af93cb9 \"net: track received peer in private broadcast\"\n\nThough bidirectional information is being stored in this struct, I feel the term broadcast falls short now. But I understand that renaming it would increase the diff quite a lot, so not needed in this PR.\n\nHowever, I do notice that `received` is present both in TxBroadcastInfo and in PeerSendInfo as well. So to disambiguate, maybe call this `received` as `received_by_us`?"
  },
  {
   "t": "2026-04-21T14:37:12Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "0e5ef5da3664970587a1b66a54bae4f29af93cb9",
   "in_reply_to": null,
   "text": "In 0e5ef5d \"net: track received peer in private broadcast\"\n\nThis seems like a costly(-ier) way to figure out if there is any pending transaction. We can short-circuit early in the iteration.\n\nThis diff passes unit tests.\n```diff\ndiff --git a/src/private_broadcast.cpp b/src/private_broadcast.cpp\nindex f43ebd7eca..42c163bc4a 100644\n--- a/src/private_broadcast.cpp\n+++ b/src/private_broadcast.cpp\n@@ -6,6 +6,7 @@\n #include <util/check.h>\n\n #include <algorithm>\n+#include <ranges>\n\n bool PrivateBroadcast::Add(const CTransactionRef& tx)\n@@ -103,7 +104,9 @@ bool PrivateBroadcast::HavePendingTransactions()\n     EXCLUSIVE_LOCKS_REQUIRED(!m_mutex)\n {\n     LOCK(m_mutex);\n-    return !GetPendingTransactions().empty();\n+    return std::ranges::any_of(m_transactions | std::views::values, [](const TxSendStatus& send_status) {\n+        return !send_status.received.has_value();\n+    });\n }\n\n std::vector<CTransactionRef> PrivateBroadcast::GetStale() const\n```"
  },
  {
   "t": "2026-04-21T14:42:23Z",
   "kind": "review",
   "who": "rkrux",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "a6db9ea77b245519186f1341b1bf89ace130a4fa",
   "text": "Partial review at a6db9ea77b245519186f1341b1bf89ace130a4fa"
  },
  {
   "t": "2026-04-24T20:36:18Z",
   "kind": "force_push",
   "who": "andrewtoth",
   "commit": "0b1f5f4aece438887b16368f6ecf3f69b22133ef"
  },
  {
   "t": "2026-04-24T20:45:02Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "text": "Thank you for your reviews @vasild, @danielabrozzoni, @rkrux.\n\n- Reset `time_added` in `Add()`. Cover this in unit test.\n- Renamed `TxSendStatus` -> `TxBroadcastStatus`, `received` -> `received_by_us`.\n- Removed `GetPendingTransactions()` and replaced with `std::ranges::any_of` in `HavePendingTransactions()` and `std::views::filter` in `PickTxForSend()`.\n- Added release notes\n- Addressed other nits\n\n`git diff a6db9ea77b245519186f1341b1bf89ace130a4fa..1df06de4ed97fd95758f4cdcd0d3fe80a3a6dda0`"
  },
  {
   "t": "2026-04-24T20:45:16Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "a6db9ea77b245519186f1341b1bf89ace130a4fa",
   "in_reply_to": 3079934199,
   "text": "Done."
  },
  {
   "t": "2026-04-24T20:46:47Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/net_processing.cpp",
   "commit": "9217011129558f3bda626bce0cb343f2861a2967",
   "in_reply_to": 3080055404,
   "text": "I agree it is unlikely to create problems.\nYou can use `abortprivatebroadcast` to remove transactions from memory.\nAn expiry can be added in a follow-up, which can be used to remove transaction from the queue after some time."
  },
  {
   "t": "2026-04-24T20:46:57Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "1df06de4ed97fd95758f4cdcd0d3fe80a3a6dda0",
   "in_reply_to": 3094334197,
   "text": "Done."
  },
  {
   "t": "2026-04-24T20:47:09Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/rpc/mempool.cpp",
   "commit": "0e5ef5da3664970587a1b66a54bae4f29af93cb9",
   "in_reply_to": 3116912945,
   "text": "Added release notes."
  },
  {
   "t": "2026-04-24T20:47:23Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/test/private_broadcast_tests.cpp",
   "commit": "9217011129558f3bda626bce0cb343f2861a2967",
   "in_reply_to": 3116919434,
   "text": "Reordered the commits."
  },
  {
   "t": "2026-04-24T20:47:38Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.h",
   "commit": "0e5ef5da3664970587a1b66a54bae4f29af93cb9",
   "in_reply_to": 3118136100,
   "text": "Renamed."
  },
  {
   "t": "2026-04-24T20:47:54Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.h",
   "commit": "0e5ef5da3664970587a1b66a54bae4f29af93cb9",
   "in_reply_to": 3118182127,
   "text": "Removed the alias along with `GetPendingTransactions()`."
  },
  {
   "t": "2026-04-24T20:48:04Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.h",
   "commit": "0e5ef5da3664970587a1b66a54bae4f29af93cb9",
   "in_reply_to": 3118202850,
   "text": "Renamed."
  },
  {
   "t": "2026-04-24T20:48:21Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "0e5ef5da3664970587a1b66a54bae4f29af93cb9",
   "in_reply_to": 3118218838,
   "text": "Done."
  },
  {
   "t": "2026-04-27T15:39:32Z",
   "kind": "review",
   "who": "vasild",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "1df06de4ed97fd95758f4cdcd0d3fe80a3a6dda0",
   "text": "ACK 1df06de4ed97fd95758f4cdcd0d3fe80a3a6dda0\n\nShow Signature\n\n```\n-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA256\n\nACK 1df06de4ed97fd95758f4cdcd0d3fe80a3a6dda0\n-----BEGIN PGP SIGNATURE-----\n\niQQzBAEBCAAdFiEE5k2NRWFNsHVF2czBVN8G9ktVy78FAmnvgxMACgkQVN8G9ktV\ny78DDx/+I6r/ELxFl2o0qTH797wMwquButcIQvdx0mFk6bNlc/WWSNWGvAQJJgD/\n1GkLOsDIzznZqs1ePl6CkxWugucdSZqVfH0Oz65mEk+MyzGsSZqhYAhue6MFBi7f\nGtKXAkp7LCw9fyatGdrGmNhBK9cMYekr1uDPdVndS5hxYes1j+MsqQraM5By6Jvk\ntd0HdW2BU92AittiryBswAQW8SHH/FneQl9iWxo8sfsvlHrES/+LUfNAtmXECCLh\nZRT1C/Tz2ZWSoSrLP7hZHzrzm4QJYIJyEEdouEL84nYNhrU9FEWkcABFqPKb7HvB\n4o4+4ZNc+F8Z5+D6Je49xwlRTNj4JCtMpgHeboRL2YO7YJNBqXzPrLJZXowEpYbj\n5M0b9v3R3hEkFiethfdBfQ6cYlUZ9Gsa6CiKzeZKhPzLGQEpMlpoiR1dG96KNZxB\nnz6RIKFynIxXE2qiWuIk0cSHJvzMfMiOeoGUivb4ypSBHyGb1HhEN7tGd3ao+Kt8\nrbKSlG8bIP6kTv59a8f91KJPGTCCiV2J0qi1EodvfG8injxqxCIGyG9VT2tIBpuZ\nl02PPxXmCIGbF84pxMTgXTqn9Ui2bsn0fpaY211hh3wYDkvcdYL9twfwONalciOn\n8C2Hc3iY6YVqld+IxYqBZVVTefbUW/svLANBiMtvnBvDBWg2JCJBa7FYlLgop+wv\nKMYB9dI9TZcVc9QDsx2tOHqjBSDkDZaztgXDymXbntL+aQwTtVpe/LDY25Mcm9Ob\nptzS0KjNd9xGSqmL6GK00GhjzqKzIGZF7GmYQSw9aLGc+r6sYzN5+h5aYB/X3b13\nZai8/SWIV6/arwdjlFyqk7LtP1T/ORmIx3mXZXjOBQXAGSToBjn9P9HgwfDv1oG+\nD4owDx/JTaMHv9GfmCoS/156Cwemu51wymjwdlzZfgRVjq9lD1G/29shQd4EqsJO\ngEA4SkBEYhRPjNpBSEo3ttpeRGJRdLS2CAETPINkRmKqHo4yVytVWoBNCoSs3d8Q\n9zyZz6cZSFCMcaBpytuoE+d4tMOOaEtnvenxf3t4+TsFHMi3Ee/QazDNyfjxW0dw\nRZL0KrxrOiGnoyomPZuKf/PPeCXYFjn6zXm70yN+c7f0qOvnOkS8dGVz1ywGaens\n0e98CzB7SofqjFqJeGc0hZTQsnt6kutYHkbWWQiCMGplOSp317ogNojAuoOFYwTA\n4LzLoxgoeLVItlN9Wi23do7DTVeiJtlyffTZUo9SOX1bHduZDU3T7wkOj7RwW4hu\n5JuKTet+w2Frh/vY9V/g1oSSgQ7/RSyB7mC8dEyZpn05+tyGepN5tRMdPJOy+pv1\nk2uXsaJ2wX4q/ECdZGGyZjhXAI1eFA==\n=1o2j\n-----END PGP SIGNATURE-----\n```\n\nvasild's public key is [on openpgp.org](https://keys.openpgp.org/vks/v1/by-fingerprint/850E138BA9C0923881DFD55FCE712013582E2230)"
  },
  {
   "t": "2026-05-11T15:45:18Z",
   "kind": "comment",
   "who": "instagibbs",
   "assoc": "MEMBER",
   "text": "I think it's worth pausing and making sure that the overarching list of efforts have the appropriate buy-in to continue with this split off piece.\n\nFrom talking in person, I think the major justification was making it easier to introspect/debug your private tx list. That seems reasoanable.\n\nThe current model in master will probably tend to flush entries out pretty quickly except in the cases where propagation is poor., or if their internet connection drops. With this PR, unless the caller is careful to `abortprivatebroadcast`, it seems you could get to an OOM situation, and if combined with disk persistence, potentially crash loop kind of situation?\n\nGiven private tx relay is basically a \"fake\" mempool driven by RPC usage, regardless of this PR, we probably want to treat it as such and do some basic anti-DoS measures. Cap to X entries in a buffer, evict oldest once budget is going to be exceeded, onus is on caller to rebroadcast later if they accidentally exceed this.\n\n---\n\nOnto the supporting motivations beyond diagnostics:\n\n[quoted text omitted]\nimo, it's not clear to me the module should be doing this driving. Up until now the caller (Wallet or sendraw caller) is burdened with this logic, and I am not sure we should be merging this as a prep for changes this, again, until there's larger buy-in for shifting this burden.  By and large I agree with tankyleo in the other PR about similar expectations: \"Overall, I expect the wallet to take care of persisting any signed transactions that are in the \"broadcast queue\", and make any broadcast retries as necessary following node shutdowns.\"\n\nIn the future, I'd presume the wallet would be giving a list of wtxids or similar to rebroadcast, the private tx module would figure out what it needs to effectuate the gossip, and do the thing, including supporting fetches of missing ancestor transactions.\n\nThis is the most interesting piece of the existing missing featureset, I suspect.\n\n[quoted text omitted]\nThis one seems reasonable on its own, but I'd have to think if it's totally necessary to include this logic here. Maybe this is better accomplished by \"just don't clear it until time T\" works?\n\n[quoted text omitted]\nUnclear to me how these are related intrinsically. Seems like an orthogonal feature? In regular operation we're not going to hold onto below-minfee transactions, so I don't think we're behooved to in this other PR which is separately of interest.\n\n[quoted text omitted]\nNot sure how this is related to this PR"
  },
  {
   "t": "2026-05-25T16:16:28Z",
   "kind": "comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nAssuming 1KB per transaction, if a node sends 100 transactions every day, then after 1 year (without restart) it will occupy 36MB of RAM.\n\nI do not mind if a cap is added here or in a followup but in my opinion, its absence is not a blocker for this PR.\n\n[quoted text omitted]\nThis PR does not include \"disk persistence\".\n\n[quoted text omitted]\nRPC is not supposed to be exposed to malicious actors. If it is by mistake then there are already easier ways to brick the node than driving it out-of-memory by sending a lot of transactions to the `sendrawtransaction` RPC.\n\n[quoted text omitted]\nI agree. To me it is clear that the private broadcast module _should not_ be driving possible retries once the transaction is received back from the network. That is best done by the caller (wallet or the user of the `sendrawtransaction` RPC)\n\n[quoted text omitted]\nI guess the OP needs to be updated."
  },
  {
   "t": "2026-05-26T15:33:27Z",
   "kind": "comment",
   "who": "instagibbs",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nThis is an error prone and I believe false assumption to make on behalf of the users who may switch this on. One example I always bring up is that Core Lightning, partly as a defense against https://bitcoinops.org/en/topics/replacement-cycling/ replacement cycling, will make multiple transactions a minute, many/most of which are unable to enter their local mempool due to RBF rules rejecting them. They are tweaking their wtxid, and sending again, Just In Case it helps get a winning tx to miners to be mined.\n\nDepending on `sendrawtransaction` strategies employed, this could result in at least an order of magnitude more than your estimate (1 per minute implies over 1k a day, pretty sure that's what CLN does), for a single package attempt. Multiply that by whatever number of packages they are trying to do. Regardless of whether this happens in a single tx setting,\n\nIn master, once *a* version of the package gets in, all the RBF-failing related transactions (including future ones?) will fail and be cleared from the private tx queue, iiuc, so there's less risk of unbounded accrual.\n\nHowever there is another risk if the sending node is doing things that are considered non-standard, as no \"winning\" transaction will necessarily enter their local mempool, leading to an ever-growing queue.\n\nThis PR makes it easier to misuse. Luckily it probably could be made safe very simply by some naive accounting and evicting the oldest stuff when some large limit is breached (think it might be a copy/paste from how Knots does their \"extra txn\" queue?). I think it's reasonable to assume high volume `sendrawtransaction` spammers will rebroadcast over time and recover if they got older entries evicted."
  },
  {
   "t": "2026-05-26T16:41:21Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nThis PR does not change this behavior. When a version of the package is received back from a peer it will remain in the queue, but all other versions that are no longer acceptable to the mempool will be removed from the queue on the next stale check."
  },
  {
   "t": "2026-05-26T16:44:25Z",
   "kind": "comment",
   "who": "instagibbs",
   "assoc": "MEMBER",
   "text": "@andrewtoth ah! was not clear to me, probably worth making the lifecycle clear. I'll think more about this, thanks\n\nSo only successful attempts at broadcasting are persisted, not failed, in other words"
  },
  {
   "t": "2026-05-28T02:06:09Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "text": "@instagibbs thanks for the review.\n\nI updated the PR description to remove the follow-up ideas. Those don't belong in the PR description and are better suited to discussion in issues.\n\nre: a limit to the number of transactions in the queue - I agree with @vasild and don't think it's a blocker for this change. But, if you insist I can add a commit to limit to 1000 transactions."
  },
  {
   "t": "2026-05-28T15:05:36Z",
   "kind": "comment",
   "who": "instagibbs",
   "assoc": "MEMBER",
   "text": "@andrewtoth opened a separate pr to discuss there to not distract any more from the at hand topic https://github.com/bitcoin/bitcoin/pull/35406"
  },
  {
   "t": "2026-05-28T15:06:30Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "text": "@instagibbs thanks - that looks like it will conflict with this though. Which should we review first?"
  },
  {
   "t": "2026-05-28T15:11:26Z",
   "kind": "comment",
   "who": "instagibbs",
   "assoc": "MEMBER",
   "text": "Implicitly voting for very simple limits first, again to make it \"obvious\" what happens. Will let others weigh in."
  },
  {
   "t": "2026-08-01T21:21:15Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "text": "Drafting until https://github.com/bitcoin/bitcoin/pull/35680 is merged."
  },
  {
   "t": "2026-09-01T16:32:15Z",
   "kind": "comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nMerged.\n\nI have been using this PR for myself. Would be nice to get it moving."
  },
  {
   "t": "2026-09-02T01:17:53Z",
   "kind": "force_push",
   "who": "andrewtoth",
   "commit": "a0508a315935eec89e92b29d8cb99ba073e22f61"
  },
  {
   "t": "2026-09-02T03:17:24Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "text": "@vasild sorry for the delay! Meant to get to this sooner.\n\n- Rebased due to #35680.\n- Pops the earliest added no-longer-pending tx if there are 10k entries, due to the limit from #35406. Adds a unit test for this as well, and updates the fuzz harness from #35090.\n\n`git range-diff a7c30da1f6fc2eebd7514dd032fbb54940d467de..1df06de4ed97fd95758f4cdcd0d3fe80a3a6dda0 58a7869f860f022e6163dc3fae3106a0599830c5..a0508a315935eec89e92b29d8cb99ba073e22f61`"
  },
  {
   "t": "2026-09-02T13:02:42Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/test/fuzz/private_broadcast.cpp",
   "commit": "a0508a315935eec89e92b29d8cb99ba073e22f61",
   "in_reply_to": null,
   "text": "\"modelled\"? I guess this reads just as fine: \"the only one no longer in...\""
  },
  {
   "t": "2026-09-02T13:04:14Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/test/fuzz/private_broadcast.cpp",
   "commit": "a0508a315935eec89e92b29d8cb99ba073e22f61",
   "in_reply_to": null,
   "text": "This is `O(N^2)`. Is that ok? With 10k transactions that will do 10M comparisons."
  },
  {
   "t": "2026-09-02T14:50:59Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/test/private_broadcast_tests.cpp",
   "commit": "9217011129558f3bda626bce0cb343f2861a2967",
   "in_reply_to": null,
   "text": "This mimics a scenario where the transaction was received back by us without us sending it to anybody. Currently that is fine with the private broadcast module - it does not care and does not check such a nonsensical caller. Just mentioning, consider making this more realistic or leave it as it is, to be adjusted in the future if needed."
  },
  {
   "t": "2026-09-02T14:58:35Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/test/private_broadcast_tests.cpp",
   "commit": "9217011129558f3bda626bce0cb343f2861a2967",
   "in_reply_to": null,
   "text": "Is a valid and shorter way to write that:\n\n```suggestion\n    const CService address{in_addr{.s_addr = 0xa0b0c001}, 1111};\n```\n\nLast time I tried that it did not compile on some Windovz environment, but that was a few years go, maybe now we don't use that old compiler anymore."
  },
  {
   "t": "2026-09-02T15:05:00Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "doc/release-notes-34707.md",
   "commit": "a0508a315935eec89e92b29d8cb99ba073e22f61",
   "in_reply_to": null,
   "text": "To avoid somebody confusing \"memory\" with the mempool:\n\n```suggestion\n- Transactions submitted via private broadcast are now retained in the private broadcast queue after\n  they are received back from the network, rather than being removed. They can\n  still be removed manually using the `abortprivatebroadcast` RPC, and are\n```"
  },
  {
   "t": "2026-09-02T15:27:54Z",
   "kind": "review",
   "who": "vasild",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "a0508a315935eec89e92b29d8cb99ba073e22f61",
   "text": "ACK a0508a315935eec89e92b29d8cb99ba073e22f61\n\nShow Signature\n\n```\n-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA256\n\nACK a0508a315935eec89e92b29d8cb99ba073e22f61\n-----BEGIN PGP SIGNATURE-----\n\niQRPBAEBCAA5FiEE5k2NRWFNsHVF2czBVN8G9ktVy78FAmqYQFUbFIAAAAAABAAO\nbWFudTIsMi41KzEuMTIsMiwzAAoJEFTfBvZLVcu/ghIf/ib9EdCLB/4pfiM0hPn/\neCR/HvGTEir1QNxUE0LBReE0xQwyUaTf5Lvuj3qrBgexoEwGcHTUsY0lJxoz85YH\n3RSNPP/58W33GyWfP9x08BXxkX2polxPCX+mMg0Upn2UI4B7D7+cLRmxVMujJU3o\nS58aMef7zQvHgHwo3xT6LGsRjb08fVTo9QyYheNDSzwAYd7ETbpVhA9vlukJKLPB\n2Hk+v2kIIseBOnWJiJXgfsDyEJnqKXm8bsfSEsNLib5Pv2kw7W8NQTFb+HdJQDvK\n6QMi5EbQ8Zl2FRn5ykY2pm2EzOaQLBoDIZ2tiB8esZ73VBgh0sutGxzE64BVGDXk\ng6Wzws72f4QF1DNYk+jRg5N0ASw4dMJgW9PDFctujNbnObnZXuP8mwyUNH7Dt3+S\neNsLOeZu80xYgdUHzqk156AlobanCyf+Is1JpURycZugA7OpIOsDlTK7KERkRNPM\nCyI9jxGs8NofcbDdoHhct/dxa/OSXU5+5AfLwEhhH7hdxZp+FTJsrhvyXU9JshoS\nCj4FrPUsRih5ggsIeVjjxX+TBBVi7JhgSzIST5GU8j7qmMMPUhpDPk0ccEDFQ2yl\ncXT71hZU1EsPa0KfubztqIAIZlc8FrReGOGm4V10Yw2r1YWt0v9g5ju99BV/lvOD\nxXqhlQ9dHl25XR9BRr9Erfsn7j2km10G/K9/bYui5UzvIG00OgipPGxwaECjwAy3\nvtwNATwTK2HHxHhzW23qC6zzwLQgrFAW5nmjx5jBO1RXLzqP+A6k5KaDcyh9fzf+\nPjCvQT1g2p8XHzl5kHQnSj0Oh4eypXqkfvBEm832SOZFw2jP0x1artX7II0q9vEd\nRJyUzEZOFrYW/yItVy4tQZXpKIbt2E890F4jpWPTlb79l9GYbQLtpl8540cD4+1/\nn26wfF9r550zv5VVwQ5FcTL2ATdjtErQB/UF0jKOrpTAJbJhVZ9ETm7YhKXWJNE2\n7gxosp2cZJlS3aeIAl3FTX/GF291CztT3vaSHpi0OrUTGOju04fa7LxKY+vl9Rn5\noJOCUAvvMpd3L0sWwUiM3V6KlVhflvDFxhCv0tQJEfoKPTn8Aq3akWxv70NjrrH2\n7vKJXlk4KT7n0gt2nzIe2jBCKaoN9Nq4Un/BI59wXiq5G6aRGe+2+sOl70Zr2d6l\nCs7w7JRJcvgMxEZACh3XM26cHpXbrxEClLQzb/Q8iQUiliedyE84p92djZyqDxoO\ni7YXtAeMMJD1JC6XPhwbUXPthxC6DiK9Cy8zJfSgBQc4L+4h1mnBhz9Q5RjYA18d\nI8UfCdimN9eUNfzdVtxraeWOdEoxVhKpxbXq41VqB8nloJvjO0jXctcdeXgyHx7D\n/8o=\n=YbDU\n-----END PGP SIGNATURE-----\n```\n\nvasild's public key is [on openpgp.org](https://keys.openpgp.org/vks/v1/by-fingerprint/850E138BA9C0923881DFD55FCE712013582E2230)"
  },
  {
   "t": "2026-09-02T18:43:44Z",
   "kind": "review_comment",
   "who": "instagibbs",
   "assoc": "MEMBER",
   "path": "src/rpc/mempool.cpp",
   "commit": "a0508a315935eec89e92b29d8cb99ba073e22f61",
   "in_reply_to": null,
   "text": "nit: `attempts_remaining` might need to report zero once it's reported received and not being retried later"
  },
  {
   "t": "2026-09-02T18:50:04Z",
   "kind": "review",
   "who": "instagibbs",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "a0508a315935eec89e92b29d8cb99ba073e22f61",
   "text": "One correctness issue identified, a double-decrement against already-confirmed tx: https://github.com/instagibbs/bitcoin/commit/ea4bba7c4e02d49bc09c2f954f289c986a72bf2b\n\nconcept NACK for now\n\nI'm not persuaded this change is net positive. An average user simply will not need this feature, so it's being aimed at, certain devs I guess, with the tradeoff being things not promptly leaving during the happy path.\n\nWhy not just set `-debug=privbroadcast` and inspect mempool directly? Can we just do better logging if we're missing something?\n\nI'd like to see where currently tooling in master falls flat and see if there's a better way to satisfy the requirements."
  },
  {
   "t": "2026-09-02T18:59:47Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nOne thing an average user will want is rebroadcasting txs in the private broadcast queue until they confirm. See https://github.com/bitcoin/bitcoin/issues/30471. Users want a way to have a tx continually rebroadcast, and right now there's no way to do that without using the wallet. If we keep these txs around after they are received back, we can periodically check if they have not yet confirmed or no longer in mempool (they were evicted due to size limits or expired) and rebroadcast them. This would be a first step to that, next would be https://github.com/bitcoin/bitcoin/pull/34322."
  },
  {
   "t": "2026-09-02T19:08:18Z",
   "kind": "comment",
   "who": "instagibbs",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\ngetrawtransaction?\n\n[quoted text omitted]\nOk, directionally I disagree with this. I do not think private broadcast should become some omni broadcast logic, and rather we push that complexity to the edges."
  },
  {
   "t": "2026-09-02T19:11:49Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "text": "`getrawtransaction` is an RPC your wallet can call when you open it. But, our node can be always on and wallets can rely on it to continually rebroadcast until it is confirmed. This is a pain point for many wallets and services. See the list in the OP of #30471."
  },
  {
   "t": "2026-09-02T21:16:59Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/rpc/mempool.cpp",
   "commit": "a0508a315935eec89e92b29d8cb99ba073e22f61",
   "in_reply_to": 3917483165,
   "text": "[quoted text omitted]\n\nThis doesn't seem like something we should worry about to me. I'm not sure it's worth the code complexity. A user aborts their own tx after it is already received (why would they do that?), and they have another tx that is sending in flight. Worst case the tx in flight might have to wait another minute or so if its first or second connection doesn't result in the tx getting back to the node."
  },
  {
   "t": "2026-09-03T01:20:55Z",
   "kind": "force_push",
   "who": "andrewtoth",
   "commit": "9217011129558f3bda626bce0cb343f2861a2967"
  },
  {
   "t": "2026-09-03T01:23:37Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/test/fuzz/private_broadcast.cpp",
   "commit": "a0508a315935eec89e92b29d8cb99ba073e22f61",
   "in_reply_to": 3914355517,
   "text": "Oops, some claude-ish snuck in there."
  },
  {
   "t": "2026-09-03T01:24:24Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/test/fuzz/private_broadcast.cpp",
   "commit": "a0508a315935eec89e92b29d8cb99ba073e22f61",
   "in_reply_to": 3914371420,
   "text": "Updated to be in linear time."
  },
  {
   "t": "2026-09-03T01:25:46Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/test/private_broadcast_tests.cpp",
   "commit": "9217011129558f3bda626bce0cb343f2861a2967",
   "in_reply_to": 3915407736,
   "text": "Left as is for now."
  },
  {
   "t": "2026-09-03T01:26:31Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/rpc/mempool.cpp",
   "commit": "a0508a315935eec89e92b29d8cb99ba073e22f61",
   "in_reply_to": 3917483165,
   "text": "Set `attempts_remaining` to `0` when already received back."
  },
  {
   "t": "2026-09-03T01:27:13Z",
   "kind": "review_comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "path": "src/test/private_broadcast_tests.cpp",
   "commit": "9217011129558f3bda626bce0cb343f2861a2967",
   "in_reply_to": 3915484288,
   "text": "I don't have a windows machine to test this on..."
  },
  {
   "t": "2026-09-03T13:30:23Z",
   "kind": "review_comment",
   "who": "instagibbs",
   "assoc": "MEMBER",
   "path": "src/rpc/mempool.cpp",
   "commit": "a0508a315935eec89e92b29d8cb99ba073e22f61",
   "in_reply_to": 3917483165,
   "text": "https://github.com/instagibbs/bitcoin/commit/a17048bcedfaab0c55acc44d9163fd640693a421\n\naccepts a tiny chance of a race if a TX is accepted in the middle, fixes the issue the other five 9's of the time, and adds coverage for abandoning an already accepted tx, which I think is thin right now"
  },
  {
   "t": "2026-09-03T13:37:17Z",
   "kind": "comment",
   "who": "instagibbs",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nIf bitcoind is open, a wallet can be open. I don't think increasing scope of Bitcoin Core is justified.\n\n(I don't want to drown this PR in meta-commentary in case this PR moves forward on its own merits, maybe continue on the issue https://github.com/bitcoin/bitcoin/issues/30471)"
  },
  {
   "t": "2026-09-09T08:55:34Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/private_broadcast.cpp",
   "commit": "9217011129558f3bda626bce0cb343f2861a2967",
   "in_reply_to": null,
   "text": "This implies that if `IsPending()` returns true then `m_max_send_attempts >= state.send_statuses.size()`. I can see how this ends with assigning a negative value to `size_t` after a future change in `IsPending()`.\n\nIt is ok to rely on that if `IsPending()` is documented to behave in that way. But it is not documented at all. So either document it like \"when this function returns then `m_max_send_attempts >= state.send_statuses.size()` will be true\". Or, I would prefer add the condition here, then it is more obvious and the reader does not have to jump to other places in the code to check for the correctness:\n\n```cpp\nconst size_t attempts_remaining{IsPending(state) && m_max_send_attempts >= state.send_statuses.size()\n    ? m_max_send_attempts - state.send_statuses.size()\n    : 0};\n```\n\nBtw currently when `IsPending()` returns `m_max_send_attempts > state.send_statuses.size()` is true (off-by-one in the \"good\" direction)."
  },
  {
   "t": "2026-09-09T09:22:38Z",
   "kind": "review",
   "who": "vasild",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "9217011129558f3bda626bce0cb343f2861a2967",
   "text": "ACK 9217011129558f3bda626bce0cb343f2861a2967\n\n@instagibbs,\n\n[quoted text omitted]\nI think it is useful for users and node operators to see what happened with their transaction. Like, how long ago was it broadcast. The network roundtrip time is also useful in general, IMO.\n\n[quoted text omitted]\nWhy would that be a tradeoff? In my view the stats disappearing from the RPC output as soon as the transaction is received back is a downside. And there is zero downside with the stats remaining in the output. The user can ignore the data if not interested in it.\n\n[quoted text omitted]\nBecause parsing the log requires local access to the filesystem and is error prone. A successful broadcast is not the same as mempool presence. The transaction might enter the mempool by other means without being broadcast to anybody or the other way around - it might be broadcast successfully, come back to us and not enter our mempool.\n\nThe private broadcast feature ideally works without the mempool.\n\n[quoted text omitted]\nIMO it \"falls flat\" because the stats disappear once the transaction comes back and the best way to improve that is to not remove them from the RPC output like this PR does. The stats contain information like to whom we sent the transaction and at what time and when did we receive it back and from whom. This info can't be derived by looking at the mempool.\n\nFurther, imagine writing a program (e.g. a wallet) around the RPC interface. Currently, without this PR, that program cannot distinguish between the following states: \"not yet in the queue\", \"broadcast concluded successfully\", \"node was restarted, unsure if broadcast was successful or not\".\n\nShow Signature\n\n```\n-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA256\n\nACK 9217011129558f3bda626bce0cb343f2861a2967\n\n@instagibbs,\n\n[quoted text omitted]\nI think it is useful for users and node operators to see what happened with their transaction. Like, how long ago was it broadcast. The network roundtrip time is also useful in general, IMO.\n\n[quoted text omitted]\nWhy would that be a tradeoff? In my view the stats disappearing from the RPC output as soon as the transaction is received back is a downside. And there is zero downside with the stats remaining in the output. The user can ignore the data if not interested in it.\n\n[quoted text omitted]\nBecause parsing the log requires local access to the filesystem and is error prone. A successful broadcast is not the same as mempool presence. The transaction might enter the mempool by other means without being broadcast to anybody or the other way around - it might be broadcast successfully, come back to us and not enter our mempool.\n\nThe private broadcast feature ideally works without the mempool.\n\n[quoted text omitted]\nIMO it \"falls flat\" because the stats disappear once the transaction comes back and the best way to improve that is to not remove them from the RPC output like this PR does. The stats contain information like to whom we sent the transaction and at what time and when did we receive it back and from whom. This info can't be derived by looking at the mempool.\n\nFurther, imagine writing a program (e.g. a wallet) around the RPC interface. Currently, without this PR, that program cannot distinguish between the following states: \"not yet in the queue\", \"broadcast concluded successfully\", \"node was restarted, unsure if broadcast was successful or not\".\n-----BEGIN PGP SIGNATURE-----\n\niQRPBAEBCAA5FiEE5k2NRWFNsHVF2czBVN8G9ktVy78FAmqhJRUbFIAAAAAABAAO\nbWFudTIsMi41KzEuMTIsMiwzAAoJEFTfBvZLVcu/zH4f/0FAz/CmqvBfveKT/Yhe\nc27NEsGkYggcO4KAXlqiCW58SV/mZYetfFAEw543PoLfTzn3UutbYh+fWEh8/nFL\nxMkseR5gbam1oLa3cIdF/NT1opC4VePzlfLkPUyQfDBm5UyWyWge6tYp5bRzGcHY\n9dMCdjBseWCbuSCk3fufjKWbBWaBkkxJRxDOJxOp/KzvnJGP6seZcFpqImafVypT\nIKFGXL0gIQIF1Zspr9/12QTeZQcjKqmlkz7spM/Er/dzRdMxAzAzIbOIVa90iDER\n8EqQxV0dE/H/moH73Gpt8Ccbgh+uDDLIO2yFygyIwrSaowRlNGPS+CzVOUaCABhR\n5eSBq5pL4EFMwkHVmiHAw41TgNytkofY9CNrUaLjWdpL53TTr6F/0KV/FdKUjVmI\n16iBJ6eDQY6hi1CpsaGi9MNrFuPX6OzTc21MtlN8KGZWtIfIGT1u76Dlkiczk9o0\nYaSJky1JXs9DblatSuRvipO88OETxF6+2yG3CDS34gQMPcdOM939zPHLGuN7F4XU\n1Xi8ghPWFLK4j4Anc5RTGXCWJBoTDdpEz6l/2FTHNlqRYQtOaBuUUbz/vIVHI9Mt\nbQ/RJO+fgy6ydWF2CJFHx9bgp2Y1sfSg9ebpglDuLXymqJjGC5EzH5Rznt5V3daT\nhXDPVwnpBywQcOFDGwDhpReemptetEk/Xx6iGQCIjOn2F02/PSIFxR+3VVDoIXC1\nU0ceT6F4D7n3NpqbO9KPRwVGBC0xJl9z4XKep7iHx+R8liLQM0VwU6vUURGgQMzR\niS1txc5s7+DXP7Gl7VfbayLkw6hkcnpvvZKLe5B7Q/MXySUaK4ReMLrzAQrIG0Pn\nBtxLArBrvxM93wiB0zfmAx8GNADmYsHIkRdbBRRlE6HulJmkGve68zg+JBWz0YPr\npob89lF5lzc8IpAgkxWuR6Da4OvlAT4HLe5kJcenfltOvHSoyNxJzxmlxkaeYJ9K\n5vLY7GxzncEoqbDxXG34RWgpYGfHa18qs8BC6kZfjSml6puhNtdi1neEccpPXD6A\nUN9zi+dSM6TpTckIqNCjkm8cVUHa05C7pj3ejPRKmxQ/nYh90a53nVTvO/QaN+R4\nRZSU9s3QDXxYPMSz3D2xbDRxXqyiOyfplCgpZG4erZU8kPOU2+MWaGetq8ltwYTK\nHnoV9yjoOTP2BWwEMBapFmWqJZOdaG6pNBpA1qNYqPpeAu7qi030IPRNCzO0jrdh\n5V3y9smxVtbVXIkMq+B30JmW2JvYrbfSh+VNtFRoBzLPgcKj9v4dD8SB8z5PGteD\nIwEt7YhMT/1bRYmkU3A9nAdyHo9uF/H+I9IM9ofyO/75qpeM6435j2xVEtaxXgM9\nElY=\n=w3c+\n-----END PGP SIGNATURE-----\n```\n\nvasild's public key is [on openpgp.org](https://keys.openpgp.org/vks/v1/by-fingerprint/850E138BA9C0923881DFD55FCE712013582E2230)"
  },
  {
   "t": "2026-09-09T13:39:26Z",
   "kind": "comment",
   "who": "instagibbs",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nNot saying it's not useful to a couple people, but I don't think it's worth it, given there are costs and limited realistic users.\n\n[quoted text omitted]\nSure, we don't need tons of variable sized state for that. A segregated buffer of timings with their according wtxid may be handy and less heavy weight. But this goes against the desire to keep tx around \"just in case\", which I disagree with as motivated enough, at a minimum.\n\nWhile it's not longer ever-increasing with explicit caps, I don't think holding around transactions forever for a tiny minority use case is \"no tradeoff\". If you just meant the \"stats\", there's less downside, I agree as I mention above, and we should think of what those stats should be. Could start with the obvious accumulator-y kinda things, number of failure\n\nGave my two cents, I'll let others weigh in now."
  },
  {
   "t": "2026-09-09T14:09:33Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "text": "As discussed with @instagibbs offline, this will also close the theoretical timing attack discussed in https://github.com/bitcoin/bitcoin/pull/29415#discussion_r2035781049.\n\nEssentially it works with a sybil attacker Alice receiving all 3 txs from Bob, and on the third connection:\n1. Alice receives the inv but does not yet send getdata.\n2. Alice opens another connection to Bob (or already has a long-lived connection to him), and sends the tx to him.\n3. Alice now sends the getdata to Bob over the private broadcast connection. If Bob closes the connection instead of responding with the tx message, Bob is confirmed to Alice to be the tx initiator.\n\nWith the tx held in memory after being received back, this attack is thwarted since Bob will reply with the tx message in step 3 whether or not he was given the tx via a different connection by Alice."
  },
  {
   "t": "2026-09-09T14:26:53Z",
   "kind": "comment",
   "who": "instagibbs",
   "assoc": "MEMBER",
   "text": "This PR is neither necessary nor sufficient for plugging known privacy leaks"
  },
  {
   "t": "2026-09-09T15:56:09Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nI may be missing some context on other known privacy leaks. I did not make any claims about anything other than the timing attack outlined in https://github.com/bitcoin/bitcoin/pull/34707#issuecomment-5603256702.\nCan you elaborate on why it is insufficient at closing that attack? I'm not convinced it can be exploited in the first place, but my understanding is that retaining the transaction would remove the observable difference described there."
  },
  {
   "t": "2026-09-15T13:24:03Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "MEMBER",
   "text": "@optout21 @danielabrozzoni would either of you care to re-review this?"
  },
  {
   "t": "2026-09-15T15:27:18Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/private_broadcast.h",
   "commit": "f6753e7f430df7ca0bafe60a56f30724bf3cca73",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nNit: Line-breaking, the parenthesis part could be in the second line entirely."
  },
  {
   "t": "2026-09-15T15:35:43Z",
   "kind": "review_comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "path": "src/private_broadcast.cpp",
   "commit": "f6753e7f430df7ca0bafe60a56f30724bf3cca73",
   "in_reply_to": null,
   "text": "[quoted text omitted]\n\nThe `if` body removes at most one element, but the condition checks for larger. So there is no guarantee, that after the if `m_transactions.size() < m_max_transactions`.\nThe `if` could be made into a `while` to achieve this. After the if there could be an assert for `m_transactions.size() < m_max_transactions` (or after the `try_emplace`, with `<=`)."
  },
  {
   "t": "2026-09-15T15:51:53Z",
   "kind": "review",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "9217011129558f3bda626bce0cb343f2861a2967",
   "text": "ACK 9217011129558f3bda626bce0cb343f2861a2967\n\nI'd like to state some cons first:\n\n- It adds some extra complexity (pending vs. non-pending, removal if needed, extra corner cases, etc.).\n- It's not evident that the extra complexity is worth the benefits.\n\nPros:\n\n- The extra complexity is encapsulated rather well into the private broadcast implementation.\n- The received info can be useful for some clients.\n- Received info can be especially useful in case of a future delayed broadcast feature.\n\nOverall, I'm for it.\nThe implementation LGTM. Left a 1-2 comments.\nRe-reviewed (as previous was some time ago).\nBuilt and executed tests locally."
  }
 ],
 "labels_log": [
  {
   "t": "2026-03-02T01:46:39Z",
   "action": "labeled",
   "label": "P2P",
   "who": "DrahtBot"
  },
  {
   "t": "2026-03-02T02:04:12Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-03-02T03:24:48Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-03-11T01:42:06Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-03-11T16:33:03Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-03-20T19:39:33Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-03-20T20:21:27Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-04T01:37:38Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-04T16:37:52Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-21T13:55:13Z",
   "action": "labeled",
   "label": "Private Broadcast",
   "who": "fanquake"
  },
  {
   "t": "2026-04-24T20:38:01Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-04-24T21:26:01Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-06-30T12:57:53Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-09-02T03:11:25Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-03-20T19:30:12Z",
   "kind": "renamed",
   "who": "andrewtoth",
   "from": "net: keep finished private broadcast txs in memory, rebroadcast if evicted from mempool",
   "to": "net: keep finished private broadcast txs in memory"
  },
  {
   "t": "2026-08-01T21:21:22Z",
   "kind": "convert_to_draft",
   "who": "andrewtoth"
  },
  {
   "t": "2026-09-02T01:32:49Z",
   "kind": "ready_for_review",
   "who": "andrewtoth"
  }
 ],
 "text_chars": 71521,
 "text_tokens_estimate": 17880,
 "changed_paths": [
  "doc/release-notes-34707.md",
  "src/net_processing.cpp",
  "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"
 ],
 "files": [
  {
   "path": "doc/release-notes-34707.md",
   "add": 10,
   "del": 0
  },
  {
   "path": "src/net_processing.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/private_broadcast.cpp",
   "add": 31,
   "del": 5
  },
  {
   "path": "src/private_broadcast.h",
   "add": 43,
   "del": 15
  },
  {
   "path": "src/rpc/mempool.cpp",
   "add": 15,
   "del": 3
  },
  {
   "path": "src/test/fuzz/private_broadcast.cpp",
   "add": 37,
   "del": 23
  },
  {
   "path": "src/test/private_broadcast_tests.cpp",
   "add": 132,
   "del": 18
  },
  {
   "path": "test/functional/p2p_private_broadcast.py",
   "add": 6,
   "del": 3
  }
 ],
 "test_lines": 219,
 "git": {
  "head": "9217011129558f3bda626bce0cb343f2861a2967",
  "head_matches_backup": true,
  "base": "58a7869f860f022e6163dc3fae3106a0599830c5",
  "commits": [
   {
    "sha": "f6753e7f43",
    "subject": "net: track received peer in private broadcast",
    "files": 6,
    "add": 133,
    "del": 50
   },
   {
    "sha": "495b03bf68",
    "subject": "test: extract find_tx_info to static helper",
    "files": 1,
    "add": 25,
    "del": 18
   },
   {
    "sha": "b40c8f18fd",
    "subject": "test: add unit test covering MarkReceived",
    "files": 1,
    "add": 107,
    "del": 0
   },
   {
    "sha": "9217011129",
    "subject": "doc: release notes for 34707",
    "files": 1,
    "add": 10,
    "del": 0
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "2bf19eff57cf20f6",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}