{
 "number": 34322,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/34322",
 "title": "node: Persist private broadcast transactions over node restarts",
 "author": "andrewtoth",
 "author_association": "CONTRIBUTOR",
 "created_at": "2026-01-16T19:37:05Z",
 "updated_at": "2026-08-26T00:34:52Z",
 "age_days": 243,
 "draft": true,
 "labels": [
  "Needs rebase",
  "Private Broadcast"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "29c8277d740b1422fda3887f09e970834bbf5420",
 "head_ref": "andrew/persist-private-broadcast",
 "head_repo": "andrewtoth/bitcoin",
 "head_history": [
  {
   "t": "2026-01-18T01:36:22Z",
   "sha": "29c8277d740b1422fda3887f09e970834bbf5420"
  }
 ],
 "additions": 377,
 "deletions": 0,
 "changed_files": 14,
 "commit_count": 5,
 "size_bucket": "M",
 "mergeable_state": "dirty",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "vasild",
      "url": "https://github.com/bitcoin/bitcoin/pull/34322#issuecomment-3799974990"
     },
     {
      "login": "craigraw",
      "url": "https://github.com/bitcoin/bitcoin/pull/34322#issuecomment-3809245705"
     }
    ]
   },
   "conflicts": [
    {
     "number": 34329,
     "title": "rpc,net: Add private broadcast RPCs",
     "author": "andrewtoth"
    },
    {
     "number": 33854,
     "title": "fix assumevalid is ignored during reindex",
     "author": "Eunovo"
    }
   ]
  }
 },
 "acks_parsed": {
  "vasild": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-01-26T14:47:30Z",
   "stale": false
  },
  "craigraw": {
   "kind": "concept_ack",
   "hash": null,
   "t": "2026-01-28T06:27:15Z",
   "stale": false
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 2,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "craigraw",
   "tankyleo",
   "vasild"
  ]
 },
 "signals": {
  "needs_rebase": true,
  "ci_failed": false,
  "mergeable_state": "dirty",
  "last_author_activity": "2026-03-02T02:43:42Z",
  "last_reviewer_activity": "2026-01-28T06:27:15Z",
  "last_reviewer": "craigraw",
  "author_silent_days": 199,
  "waiting_on_author_days": 0,
  "days_since_update": 22
 },
 "refs": {
  "mentioned": [
   29415,
   30471,
   34118,
   34267,
   34329
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 29415,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-01-12",
    "title": "Broadcast own transactions only via short-lived Tor or I2P connections"
   },
   {
    "number": 30471,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "Feature Request: Broadcast Pool"
   },
   {
    "number": 34118,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "[feature] Scheduled Transaction Broadcast"
   },
   {
    "number": 34267,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-01-27",
    "title": "net: avoid unconditional `privatebroadcast` logging (+ warn for debug logs)"
   },
   {
    "number": 34329,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-02-19",
    "title": "rpc,net: Add private broadcast RPCs"
   }
  ],
  "conflicts": [
   34329,
   33854
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [],
 "body": "Follow-up from #29415\n\nCurrently private broadcast transactions are stored in peer manager and do not persist over restarts. A submitted transaction can be lost if the node restarts before it is privately broadcast.\n\nThis change dumps the set of private broadcast transactions to a `privatebroadcast.dat` file on shutdown, and adds the transactions back to the private broadcast data structure on restart.",
 "commits": [
  {
   "sha": "c9889ec13146621003d9ae816f535ad2184c8292",
   "date": "2026-01-18T01:35:59Z",
   "message": "net: Add PrivateBroadcast::GetBroadcastInfo"
  },
  {
   "sha": "ca92f601e84caf7bf27a0619c91f7d65ffa7bc4a",
   "date": "2026-01-18T01:35:59Z",
   "message": "node: Persist private broadcast transactions to privatebroadcast.dat\n\nIntroduce helpers to serialize and deserialize in-flight private broadcast\ntransactions to a new on-disk file, privatebroadcast.dat."
  },
  {
   "sha": "4936339e4f5eabb463159fd3789b4693acf14757",
   "date": "2026-01-18T01:35:59Z",
   "message": "fuzz: Add private_broadcast_persist target"
  },
  {
   "sha": "361c2395f52a3368db156169c3b055b7ba14af7d",
   "date": "2026-01-18T01:35:59Z",
   "message": "init: Load and dump privatebroadcast.dat with -privatebroadcast\n\nOn startup, load transactions from privatebroadcast.dat into PrivateBroadcast\nwhen -privatebroadcast is enabled, and remove the file afterward.\n\nOn shutdown, dump any remaining private broadcast transactions to disk."
  },
  {
   "sha": "29c8277d740b1422fda3887f09e970834bbf5420",
   "date": "2026-01-18T01:35:59Z",
   "message": "test: Cover privatebroadcast.dat persistence"
  }
 ],
 "timeline": [
  {
   "t": "2026-01-18T01:36:22Z",
   "kind": "force_push",
   "who": "andrewtoth",
   "commit": "29c8277d740b1422fda3887f09e970834bbf5420"
  },
  {
   "t": "2026-01-18T01:47:18Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "CONTRIBUTOR",
   "text": "The first commit introduces `PrivateBroadcast::GetBroadcastInfo()` which has more information than we need for this change, but it is the same first commit in #34329."
  },
  {
   "t": "2026-01-18T02:24:18Z",
   "kind": "comment",
   "who": "tankyleo",
   "assoc": "NONE",
   "text": "Casual observer here, very excited by the progress on this feature.\n\n[quoted text omitted]\nI have reservations about dumping sensitive data to disk on node shutdown. Previously a non-wallet, broadcaster-only node would not persist any sensitive data to disk, but with this PR it now does. This changes the risk profile. How do you assess this tradeoff ? I am aware I am missing most of the context here, thank you very much for your time.\n\n[quoted text omitted]\nOverall, 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."
  },
  {
   "t": "2026-01-18T02:58:28Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "CONTRIBUTOR",
   "text": "[quoted text omitted]\n\nThis PR only changes behavior of nodes configured with `privatebroadcast=1`, so a previous node would not have any behavior change unless it is intentionally reconfigured.\n\nThe usecase I envision is for quickly restarting a node shortly after a transaction is sent, in which case the file is deleted as soon as the node starts up again. It wouldn't make sense to me to shut down your node right after you call `sendrawtransaction` and then leave it off before your transaction was successfully broadcast. This would cause the file to stay on your disk, but why would you shut it down before your send completes? Your intention was to broadcast the transaction. Do you see any other scenario where we would leave the file intact on disk for an extended period?\n\n[quoted text omitted]\nCurrently `privatebroadcast` only works with the `sendrawtransaction` RPC and not any of the wallet RPCs that broadcast transactions. If an external application calls `sendrawtransaction` on the node, it would expect that the transaction would be broadcast to the network. It should not have to be concerned with whether the node is restarted shortly after sending."
  },
  {
   "t": "2026-01-19T06:07:08Z",
   "kind": "comment",
   "who": "tankyleo",
   "assoc": "NONE",
   "text": "[quoted text omitted]\n\nI agree that there is no change by default. My concern is that turning on `privatebroadcast=1` expands the attack surface I have to worry about on a non-wallet, broadcaster only node, to include the disk. The disk now *could potentially* hold a list of some transactions I (or the users of my service / wallet / app) originated. This piece of data is much more sensitive than any other data persisted to the disk on this kind of node (non-wallet, broadcaster only).\n\n[quoted text omitted]\nI have in mind accidental / non-intentional / maliciously triggered shutdowns, or even intentional shutdowns followed by some unexpected failure to start the node again.\n\nIn general, I would expect wallets to call `sendrawtransaction` again if the transaction does not appear in the mempool after some timeout, which would handle the quick restart you describe.\n\n[quoted text omitted]\nRight earlier I was referring more generally to my conviction that wallets *alone* should persist transactions from the originator, not the node (including but not limited to Bitcoin Core's wallet).\n\n[quoted text omitted]\nI agree that the wallet should not be aware of whether the node restarted. Nonetheless the wallet *should* handle the case: \"hey the node crashed / shutdown / encountered a temporary failure during broadcast, and the transaction got lost. Try broadcasting again please.\""
  },
  {
   "t": "2026-01-20T15:28:54Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "CONTRIBUTOR",
   "text": "@tankyleo Thank you for your thoughts.\n\n[quoted text omitted]\nNote that this node must also be configured with `disablewallet=1`, otherwise RPC clients could use the disk for wallets even if not intended by the node runner.\n\nAs of right now, all this information is already stored in the debug.log (see #34267). This change could potentially store even less information to disk but also obfuscated. I don't agree that this is a major concern, but I suppose it should be documented.\n\nAlso, decoy transactions (also discussed in #29415) would mitigate this risk. It would create plausible deniability about the origin of the transactions stored in this file.\n\n[quoted text omitted]\nThis scenario feels far fetched to me. A private broadcast is in the middle of being sent, and an accident or adversary causes the node to shutdown gracefully. Then, the node does not get started up again before this file's contents is copied somewhere else.\n\nRight now this PR removes the privatebroadcast.dat if it exists after the mempool is loaded, even if it fails to parse it somehow. I suppose this could be moved further up the loading path, so it gets removed as early as possible. This should mitigate any \"failure to start the node again\".\n\n[quoted text omitted]\nI agree that wallets *should* do all these things, but I don't think we should *expect* that behavior. We should aim to make our features as robust as possible, which this change does. Users should expect that Core will \"just work\" when they call `sendtorawtransaction`."
  },
  {
   "t": "2026-01-26T14:47:30Z",
   "kind": "comment",
   "who": "vasild",
   "assoc": "CONTRIBUTOR",
   "text": "Concept ACK\n\n[quoted text omitted]\nTrue.\n\nI think that the benefits of not losing the transaction if the node is restarted before a successful completion out-weight the concerns about storing the transaction on disk. Make sure the file gets permissions `0600`. If an attacker has an access to the filesystem with the same user `bitcoind` is running as then they can debug or trace the running process to extract the information even if it is not stored on disk. Still there is a scenario where the machine is shutdown, stolen and inspected at a later time.\n\nWe can probably have the best from both worlds if a new option is introduced to instruct `bitcoind` how long to keep records, both in-memory and on-disk. With a value of `0` meaning \"don't keep records\"."
  },
  {
   "t": "2026-01-28T06:27:15Z",
   "kind": "comment",
   "who": "craigraw",
   "assoc": "NONE",
   "text": "Concept ACK. However, I don't think this change goes far enough (or at least, should be considered a first step). Losing broadcasted transactions due to a node restart is probably quite rare. A much more common UX issue is broadcasted transactions being lost from the user's local mempool due to fee rate environment changes. This is disconcerting to the user as the transaction mysteriously disappears from their client wallet and they no longer have access to fee bump it. This is a poor user experience, and leads users to consider out-of-band solutions, or to increase the size of their local mempool which is resource inefficient and counterproductive to the network as a whole. It is reasonable to assume that a user's broadcasted transactions carry much more economic value to them than most other transactions.\n\nA further common need is scheduling a transaction broadcast so that it is stored locally and submitted to the network at some point in the future. Avoiding timing analysis is a common need, for example when migrating a wallet or regularly rotating UTXOs in a decaying multisig. Most bitcoin nodes (as opposed to client wallet software) are run as continuously online servers, and therefore are ideal candidates to handle this otherwise onerous task.\n\nI won't belabour these points, as they are described in detail in the feature request in #30471, which was partially (and independently) repeated in the feature request #34118. I believe there is substantial interest in addressing these common pain points.\n\n[quoted text omitted]\nI agree, although the idea of decoy transactions seems like a good addition."
  },
  {
   "t": "2026-01-28T18:44:13Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "CONTRIBUTOR",
   "text": "Thanks for your thoughts @craigraw. Indeed, we could make the private broadcast storage a broadcast pool. However, I think the approach I have taken here is not suitable for that. This change dumps unfinished txs to disk and reloads them. If a node is not shut down gracefully, then these records are lost.\n\nFor a broadcast pool, we would want to keep a persistent SQLite file on disk and keep storing these txs until they expire. That would be a better first step here. Read a sqlite file on startup (create if doesn't exist and privatebroadcast=1) and clear all entries past expiry time. Load entries that are not yet expired and try broadcasting again if privatebroadcast=1.\n\nFuture enhancements like storing a time at which a tx should be broadcast and decoy txs could be built on top of something like this. I think #34329 should be merged first, which persists finished txs at least in memory."
  },
  {
   "t": "2026-02-03T14:46:00Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "CONTRIBUTOR",
   "text": "Marking as draft until https://github.com/bitcoin/bitcoin/pull/34329 is merged."
  },
  {
   "t": "2026-03-02T02:43:42Z",
   "kind": "comment",
   "who": "andrewtoth",
   "assoc": "CONTRIBUTOR",
   "text": "I opened https://github.com/bitcoin/bitcoin/pull/34707 as a first step towards this."
  }
 ],
 "labels_log": [
  {
   "t": "2026-02-19T23:08:14Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-05-29T19:54:02Z",
   "action": "labeled",
   "label": "Private Broadcast",
   "who": "fanquake"
  }
 ],
 "state_log": [
  {
   "t": "2026-02-03T14:46:24Z",
   "kind": "convert_to_draft",
   "who": "andrewtoth"
  }
 ],
 "text_chars": 9442,
 "text_tokens_estimate": 2360,
 "changed_paths": [],
 "files": [],
 "test_lines": null,
 "git": null,
 "input_hash": "84488304cf146764",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}