{
 "number": 36429,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36429",
 "title": "[RFC] Delayed transaction broadcast, minimal",
 "author": "optout21",
 "author_association": "CONTRIBUTOR",
 "created_at": "2026-10-03T18:01:58Z",
 "updated_at": "2026-10-31T13:15:59Z",
 "description_hash": "3b43b0c5b5ff33ab",
 "age_days": 29,
 "draft": true,
 "labels": [],
 "milestone": null,
 "base": "master",
 "head_sha": "f692a4860b992005dc505e7c5f730dbbd143c9b1",
 "head_ref": "2610-delayed-broadcast1",
 "head_repo": "optout21/bitcoin",
 "head_history": [
  {
   "t": "2026-10-05T14:19:42Z",
   "sha": "89d22467207070ad4a2a2fa5d051e7e522c0c73c"
  },
  {
   "t": "2026-10-06T11:23:56Z",
   "sha": "882e58ad5807337778ff395ca21884930ac5f370"
  },
  {
   "t": "2026-10-06T21:44:38Z",
   "sha": "f692a4860b992005dc505e7c5f730dbbd143c9b1"
  }
 ],
 "additions": 488,
 "deletions": 28,
 "changed_files": 12,
 "commit_count": 6,
 "size_bucket": "L",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {},
   "conflicts": []
  }
 },
 "acks_parsed": {},
 "acks_tally": {
  "ack": 0,
  "stale_ack": 0,
  "concept_ack": 0,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 0,
  "changes_requested": 0,
  "distinct_reviewers": [
   "ismaelsadeeq",
   "vasild"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-10-21T12:09:53Z",
  "last_reviewer_activity": "2026-10-21T04:59:09Z",
  "last_reviewer": "vasild",
  "author_silent_days": 11,
  "waiting_on_author_days": 0,
  "days_since_update": 1
 },
 "refs": {
  "mentioned": [
   34118,
   36277
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 34118,
    "type": "issue",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "[feature] Scheduled Transaction Broadcast"
   },
   {
    "number": 36277,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-10-06",
    "title": "net: always complete all initial private broadcast connections"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [],
 "body": "Note: planned to be reworked, see https://github.com/bitcoin/bitcoin/pull/36429#issuecomment-6315408546\n\nMinimal implementation for \"Delayed Transaction Broadcast\" new feature.\n\nGoal: Allow a user to submit a transaction for delayed broadcasting, that will be broadcast only later, at a randomized time. The aim is increased privacy, by making time-correlation-based tracking more difficult.\nThis fits together with private broadcast: private connection broadcast tries to hide the originator node; randomized delay tries to hide the creation time.\n\nGoals in this PR: Extend the `sendrawtransaction` RPC with an optional `delay_for` parameter, and if set, postpone the initial broadcast to that later time, with some extra jitter.\n\nUse case Details, with the limitations of the current changeset:\n- What is a typical delay time? 30 - 90 minutes. (Too long times can get problematic: risk fee increase, node restart.)\n- Delayed broadcast is only supported with private broadcast.\n- Delay option and time is specified per submission (in `sendrawtransaction`). Alternative: with node level config.\n- For now pending transactions are NOT persisted, so pending transactions will be lost during a node stop/restart/crash.\n- Randomized delay is expressed as a percentage of the requested delay. A hard-coded value of 50% is used.\n- Delay times are capped: requested delay between 0min to 12 hours, randomization range to minimum 5 minutes.\n- Validation happens both at submission and broadcast time.\n- No special handling for sudden fee increase situations.\n- Pending (submitted but not yet broadcast) transactions can be inspected via existing `getprivatebroadcastinfo` RPC.\n- The (pre-existing) `abortprivatebroadcast` RPC can be used to cancel a pending transaction submission.\n- No special `nLockTime` handling, to be refined later.\n- No special support from the wallet subsystem. (A submitted but not broadcast transactions is not visible in the wallet.)\n\nSolution Details:\n- The solution is based on the Private Broadcast implementation.\n- Transactions are stored in the already-existing `m_transactions` collection.\n- A scheduled periodic check is added to check for transactions that have became due. The implementation is very similar to the existing retry logic (`ReattemptPrivateBroadcast`).\n- Queue is not persisted currently, to be added later.\n\nRelated issue: #34118",
 "commits": [
  {
   "sha": "3b837c116c198e99995a5815dedaed7ba8eb11c2",
   "date": "2026-10-06T09:38:52Z",
   "message": "net: Support delay in private broadcast queue and logic\n\nCore support for delayed private broadcast:\n- Add target release time to submitted transactions;\n  `release_time` field added to `TxBroadcastInfo` and `TxSendStatus`\n- Adapt logic to handle delayed submission (Add, Remove, etc.):\n  a transaction is not sent before it is released, has no planned\n  sends to cancel on Remove(), and is forgotten by MarkResolved()"
  },
  {
   "sha": "c29b738405cdf699e1cfa42a462b6d42925c109b",
   "date": "2026-10-06T09:39:03Z",
   "message": "net: Support delayed broadcasts in PeerManager\n\nOnly send delayed transactions after they are due."
  },
  {
   "sha": "dd4e115ccf50686b116447f8f86b639d15171b58",
   "date": "2026-10-06T09:39:17Z",
   "message": "net: Add randomization of private broadcast delay\n\nAdd constants and the RandomizeDelay() helper to randomize the release\ntime of a delayed private broadcast, and use it in PeerManager.\nThe randomized delay is never shorter than requested, and the requested\ndelay is capped at MAX_DELAY."
  },
  {
   "sha": "92ccc14c31aa8d20d2a8add32ffd334aac05462e",
   "date": "2026-10-06T09:39:46Z",
   "message": "rpc: Add delay_for parameter to sendrawtransaction RPC\n\nIf set, and private broadcast is enabled (prerequisite), the submitted\ntransaction will be delayed."
  },
  {
   "sha": "3d1d85c1077c74fc4ffc3ac6aed8d5ea8d6cecb4",
   "date": "2026-10-06T21:29:14Z",
   "message": "test: Add delayed broadcast functional test"
  },
  {
   "sha": "f692a4860b992005dc505e7c5f730dbbd143c9b1",
   "date": "2026-10-06T21:29:15Z",
   "message": "doc: Add release note for sendrawtransaction delay_for"
  }
 ],
 "timeline": [
  {
   "t": "2026-10-05T14:19:42Z",
   "kind": "force_push",
   "who": "optout21",
   "commit": "89d22467207070ad4a2a2fa5d051e7e522c0c73c",
   "id": "push:89d22467207070ad4a2a2fa5d051e7e522c0c73c"
  },
  {
   "t": "2026-10-06T11:23:56Z",
   "kind": "force_push",
   "who": "optout21",
   "commit": "882e58ad5807337778ff395ca21884930ac5f370",
   "id": "push:882e58ad5807337778ff395ca21884930ac5f370"
  },
  {
   "t": "2026-10-06T11:23:59Z",
   "kind": "comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "text": "Rebased. #36277 got merged, which caused the first commit to be substantially changed, but behavior is the same.",
   "id": "c:6015213813",
   "url": "https://github.com/bitcoin/bitcoin/pull/36429#issuecomment-6015213813",
   "hash": "87ba30c0b2f3be17",
   "edited": "2026-10-06T11:24:53Z"
  },
  {
   "t": "2026-10-06T13:27:52Z",
   "kind": "comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "text": "Putting the current block height in `nLockTime` when the transaction is created defeats delaying its broadcast:\n\nhttps://github.com/bitcoin/bitcoin/blob/acaf322fc438e6bd180b23189a5661c8f02d88fb/src/wallet/spend.cpp#L1020-L1030\n\nSo an artificial delay must be combined with a non-default `nLockTime` in order to be effective. External wallets that use the RPC and use other `nLockTime`  might benefit more from this.\n\n---\n\nDelaying in order to make linking transactions (that are otherwise unrelated) by creation time harder makes sense if private broadcast is used. If send-to-all broadcast is used, then the otherwise unrelated transactions can be linked as coming from the same node.\n\n---\n\nThe delay does not play nice with node restart (unbroadcast transactions would be lost).\n\n---\n\n[quoted text omitted]\nHmm?\n\n---\n\nNit, from the OP:\n\n[quoted text omitted]\nprobably you meant \"submitted but not yet broadcast\".",
   "id": "c:6017308339",
   "url": "https://github.com/bitcoin/bitcoin/pull/36429#issuecomment-6017308339",
   "hash": "adb92d8f3d098d66",
   "edited": null
  },
  {
   "t": "2026-10-06T14:21:08Z",
   "kind": "comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "text": "@vasild, thanks for the general remarks!\n\n- nLockTime: In the current PR nLockTime is not considered (\"No special nLockTime handling\").\n\nThe Core wallet could be enhanced, to use matching nLockTime if using delyed broadcast (in a followup). Also, delayed broadcast could check and warn on nLockTime (closer to submission time than broadcast time, or too much in the future). Even better, nLockTime could be used to set/extend the requested broadcast time. (The time-based option should be there nonetheless, as it makes the feature more accessible.)\n\n[quoted text omitted]\nTrue, but delayed broadcast is available only in conjunction with private broadcast.\n\n- Persisting the queue is an important feature, should be added (but I thing a followup is fine).\n\n- Undo/Cancel support: that's a mistake in the description, removed (originally was not planned, but it came 'free' with existing private broadcast infrastructure).\n\n- Typo, fixed.",
   "id": "c:6018323325",
   "url": "https://github.com/bitcoin/bitcoin/pull/36429#issuecomment-6018323325",
   "hash": "61f4c6f803b689ad",
   "edited": null
  },
  {
   "t": "2026-10-06T21:42:52Z",
   "kind": "comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "text": "Applied a fix to the new functional test. One [CI run failed](https://github.com/bitcoin/bitcoin/actions/runs/37456136376/job/112244101233?pr=36429), and it turned out to be a timing issue in the test. Note that the failing run was a no wallet run, but the sporadic failure is not specific to the no-wallet config. Pre-#36277 the test was correct, but now it had to be adapted.",
   "id": "c:6025995269",
   "url": "https://github.com/bitcoin/bitcoin/pull/36429#issuecomment-6025995269",
   "hash": "d724b9e8b727cb1b",
   "edited": null
  },
  {
   "t": "2026-10-06T21:44:38Z",
   "kind": "force_push",
   "who": "optout21",
   "commit": "f692a4860b992005dc505e7c5f730dbbd143c9b1",
   "id": "push:f692a4860b992005dc505e7c5f730dbbd143c9b1"
  },
  {
   "t": "2026-10-07T06:23:52Z",
   "kind": "comment",
   "who": "ismaelsadeeq",
   "assoc": "MEMBER",
   "text": "Hmm, I am tending towards concept ~0, As I mentioned here https://github.com/bitcoin/bitcoin/issues/34118, the delay itself seems like something a client can already do: sleep for a random time, then call sendrawtransaction on a node running with -privatebroadcast. I had Claude write a ~30 line shell script that reproduces the algorithm here.\n\nBut the shell script has downsides, the client/shell process has to stay running for up to 12 hours, whereas with this PR the node holds it instead, which is better. Info calls like visibility in `getprivatebroadcastinfo` and aborting via `abortprivatebroadcast` aren't available to a script either. But as the PR is, pending txs aren't persisted and are lost on restart, so the node has to stay up too (so having the client up vs. the node up is the same tradeoff).\n\nIt would be a bit more ideal if this were implemented for our wallet users, since it'd be awkward for them to create a tx and then have to use a script to delay broadcasting it.\n\nAlso see the recent discussion in https://github.com/bitcoin/bitcoin/issues/36403, which attempts to revive a lot of old ideas like a node rebroadcast module, improving wallet rebroadcast, e.t.c. it would involve some machinery to distinguish between delayed-rebroadcast txs, mempool txs, private-rebroadcast txs, and wallet txs, since they'd all need to be taken into account, but in practice can be done correctly with good SOC.\n\nFwiw, I am not completely dismissing this, I'd like to understand the privacy goal better. \"Making time-correlation-based tracking more difficult\" is a bit vague can you be more explicit, e.g. with scenarios and data? Because I think I'm missing something:\n\n1. What is the concrete privacy gain?\n2. What's the actual harm in an attacker knowing the creation time? Given that private broadcast already hides which node sent it, what does also hiding when it was made protect against in practice?",
   "id": "c:6032279563",
   "url": "https://github.com/bitcoin/bitcoin/pull/36429#issuecomment-6032279563",
   "hash": "19e1429b382016a6",
   "edited": null
  },
  {
   "t": "2026-10-09T16:29:55Z",
   "kind": "comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "text": "Hey @ismaelsadeeq , thanks for sharing your concerns!\n\n[quoted text omitted]\nThat's true, most client could do the delay themselves (this is not true for private broadcast, for which the core node must be involved). However, many clients can not do this easily, as wallets are typically not used in an always-on mode. If someone uses a third-party wallet software (e.g. Sparrow, Nunchuk, etc.), connected to his own core node (directly or indirectly via an electrum server), ultimately the core node is the always-on component interfacing with the bitcoin network, and it's a good fit to offer scheduled/delayed broadcast support. I don't think shell script is a practical solution for an average user, even a power user.\n\nFor more concrete examples, here are some use cases:\n- A user manages TXs through a non-core wallet, connected to a core node. He wants to initiate a few transfers for some other persons, to be done the next day, without a need for a future user interaction, or keeping the wallet running, or leaving an early trace in the mempool/blockchain. Since the code node is used to interface with the network, and it is an always-on component, it makes sense to delegate. Note that this is a simple scheduled broadcast example.\n\n- A user manages TXs through a non-core wallet, connected to a core node. He wants to initiate a few transfers for some other persons, initiated at once, but with different public timing fingerprint (care is taken that the TXs are not related through UTXOs). The user does not want a future user interaction, or keeping the wallet running.\n\n- A service backend sometimes creates bitcoin transactions (through a core node), in response to some events, e.g. a trading/betting system. The triggering events are publicly observable, but not on the bitcoin network. Transactions related to specific events could be probabilistically matched based on timing analysis. To avoid that, the service wants to create the transactions with a random delay. While not the only solution, but it's preferred that the core node can handle delayed broadcast.\n\n[quoted text omitted]\nI think private broadcast, delayed broadcast, rebroadcast all fit in a larger picture: __locally-originated transactions__. Transactions originated in/through the node are special in many ways compared to relayed transactions originated elsewhere.\n\nSources:\n- The bitcoin core wallet\n- External clients -- wallets, services, etc, using core node (via RPC; directly or indirectly)\n\nFunctionalities:\n- Rebroadcast\n- Masking of origin -- private broadcast\n- Scheduled/delayed broadcast\n- RBF\n- Undo (if possible)\n\nI would argue that an abstraction for local-originated transactions and a pool for such transactions would make a lot of sense.\n\n(Note: added '[RFC]' prefix to title)",
   "id": "c:6084992932",
   "url": "https://github.com/bitcoin/bitcoin/pull/36429#issuecomment-6084992932",
   "hash": "10970cc0e196ba8a",
   "edited": "2026-10-09T16:31:32Z"
  },
  {
   "t": "2026-10-21T04:59:09Z",
   "kind": "comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\nYes, I like this idea - use `nLockTime` for deciding when to send the transaction. Because:\n\n* If a transaction is sent before the `nLockTime` height is reached, then it cannot confirm, so it does not make sense to send it earlier.\n* If a transaction is sent after the `nLockTime` height is reached, then it trivially reveals when the transaction was created (`nLockTime`) and a delayed send will not hide it. So it does not make sense from privacy perspective.\n\n[quoted text omitted]\nIf the point is to hide the transaction creation time and the wallet puts current height for `nLockTime` (like the Bitcoin Core wallet mostly does) then sending later does not make sense. So such an option could mislead users that they can hide the transaction creation time by using it?",
   "id": "c:6308429903",
   "url": "https://github.com/bitcoin/bitcoin/pull/36429#issuecomment-6308429903",
   "hash": "7e95fba3fcf089b9",
   "edited": null
  },
  {
   "t": "2026-10-21T12:02:16Z",
   "kind": "comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "text": "Following some recent discussions, I plan to do some changes to the feature:\n\n- Acknowledge the two separate sub use cases of scheduling (for convenience) and random delay (for time obfuscation).\n\n- Use the 'nLockTime' of the transaction as a basis for scheduling time, instead of an extra parameter. This way it can be ensured that the scheduling time and lock time are in sync (or if they are not, that's intended).\n\n- Drop the random delay computation, because (1) it's not compatible with the lock time usage of the previous point, and (2) it is easy to be done on the client side\n\nKeeping in Draft status until the above changes are applied.",
   "id": "c:6315408546",
   "url": "https://github.com/bitcoin/bitcoin/pull/36429#issuecomment-6315408546",
   "hash": "29e692fa7d5bab9b",
   "edited": "2026-10-21T12:10:59Z"
  },
  {
   "t": "2026-10-21T12:09:53Z",
   "kind": "comment",
   "who": "optout21",
   "assoc": "CONTRIBUTOR",
   "text": "[quoted text omitted]\n\n@vasild , thanks for this and previous feedback! I also arrived at the same conclusion, see the previous comment.\n\nI am thinking to also support the option for using a past lock time for obfuscation (wallet fingerprinting). One idea I came up with:\n\n- In `sendrawtransaction` RPC introduce a new parameter `delay_after_lock_time`\n- If not set, there is no delay\n- If set (usually to 0), the delay is the sum of `nLockTime` and the parameter value\n\nExamples (assume current height is 1000:\n-  The wish is to broadcast only at height 1005: set lock time to 1005, and `delay_after_lock_time=0`\n- The wish is to broadcast only at height 1005, but lock time set to 985: set lock time to 985, and `delay_after_lock_time=20`. Broadcast will be delayed until height 1005 (985+20).\n\nScheme to be refined. It should also support time-based lock time.",
   "id": "c:6315555722",
   "url": "https://github.com/bitcoin/bitcoin/pull/36429#issuecomment-6315555722",
   "hash": "cec3bdbe92f720db",
   "edited": null
  }
 ],
 "labels_log": [
  {
   "t": "2026-10-06T12:50:55Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-10-06T22:49:41Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-10-09T16:30:44Z",
   "kind": "renamed",
   "who": "optout21",
   "from": "Delayed transaction broadcast, minimal",
   "to": "[RFC] Delayed transaction broadcast, minimal"
  }
 ],
 "text_chars": 12838,
 "text_tokens_estimate": 3209,
 "changed_paths": [
  "doc/release-notes-delayed-private-broadcast.md",
  "src/net_processing.cpp",
  "src/net_processing.h",
  "src/node/transaction.cpp",
  "src/node/transaction.h",
  "src/private_broadcast.cpp",
  "src/private_broadcast.h",
  "src/rpc/client.cpp",
  "src/rpc/mempool.cpp",
  "src/test/fuzz/p2p_private_broadcast.cpp",
  "src/test/private_broadcast_tests.cpp",
  "test/functional/p2p_private_broadcast.py"
 ],
 "files": [
  {
   "path": "doc/release-notes-delayed-private-broadcast.md",
   "add": 14,
   "del": 0
  },
  {
   "path": "src/net_processing.cpp",
   "add": 56,
   "del": 3
  },
  {
   "path": "src/net_processing.h",
   "add": 4,
   "del": 1
  },
  {
   "path": "src/node/transaction.cpp",
   "add": 5,
   "del": 2
  },
  {
   "path": "src/node/transaction.h",
   "add": 6,
   "del": 1
  },
  {
   "path": "src/private_broadcast.cpp",
   "add": 52,
   "del": 8
  },
  {
   "path": "src/private_broadcast.h",
   "add": 62,
   "del": 10
  },
  {
   "path": "src/rpc/client.cpp",
   "add": 1,
   "del": 0
  },
  {
   "path": "src/rpc/mempool.cpp",
   "add": 29,
   "del": 1
  },
  {
   "path": "src/test/fuzz/p2p_private_broadcast.cpp",
   "add": 3,
   "del": 2
  },
  {
   "path": "src/test/private_broadcast_tests.cpp",
   "add": 145,
   "del": 0
  },
  {
   "path": "test/functional/p2p_private_broadcast.py",
   "add": 111,
   "del": 0
  }
 ],
 "test_lines": 261,
 "git": {
  "head": "f692a4860b992005dc505e7c5f730dbbd143c9b1",
  "head_matches_backup": true,
  "base": "acaf322fc438e6bd180b23189a5661c8f02d88fb",
  "patch_id": "42e2e9b9961f6084",
  "commits": [
   {
    "sha": "3b837c116c",
    "subject": "net: Support delay in private broadcast queue and logic",
    "files": 3,
    "add": 193,
    "del": 18
   },
   {
    "sha": "c29b738405",
    "subject": "net: Support delayed broadcasts in PeerManager",
    "files": 4,
    "add": 60,
    "del": 7
   },
   {
    "sha": "dd4e115ccf",
    "subject": "net: Add randomization of private broadcast delay",
    "files": 5,
    "add": 73,
    "del": 3
   },
   {
    "sha": "92ccc14c31",
    "subject": "rpc: Add delay_for parameter to sendrawtransaction RPC",
    "files": 4,
    "add": 41,
    "del": 4
   },
   {
    "sha": "3d1d85c107",
    "subject": "test: Add delayed broadcast functional test",
    "files": 1,
    "add": 111,
    "del": 0
   },
   {
    "sha": "f692a4860b",
    "subject": "doc: Add release note for sendrawtransaction delay_for",
    "files": 1,
    "add": 14,
    "del": 0
   }
  ],
  "patch_truncated": true
 },
 "patch_id": "42e2e9b9961f6084",
 "input_hash": "eaa08d05e92ed07a",
 "extracted_at": "2026-11-02T01:09:49+00:00"
}