net: reimplement private broadcast as jobs isolated from the node #36628

pull instagibbs wants to merge 16 commits into bitcoin:master from instagibbs:2026-10-pb_reimpl changing 78 files +6341 −3046
  1. instagibbs commented at 4:45 PM on October 23, 2026: member

    Split out of #36608, the overall private broadcast rework to support ease of review for like-for-like feature.

    Differences from #36608 :

    1. No tool support or prep refactors for the tool
    2. No package relay
    3. Slimmed down test profile focusing on protecting aginst IP address linkage and not crashing the node on adversarial inputs (which is a leak in itself)

    With -privatebroadcast, sendrawtransaction queues a job instead of handing the transaction to the connection manager. Each job runs on its own thread, shares no state with the node, and reaches the network only through the Tor proxy: it resolves the chain's DNS seeds, then announces over BIP324 to recipients on a schedule drawn up front, each connection with fresh isolation credentials. Nothing a recipient does, and nothing the node sees of the transaction coming back, changes the plan, and the transaction enters the node's mempool only when it returns from the network. The old private broadcast connections and relay are removed.

    The invariants are specified in doc/design/private-broadcast.md.

    The rest of the tests are in a follow-up to be considered later: https://github.com/instagibbs/bitcoin/tree/2026-10-pb_reimpl_followup

  2. DrahtBot added the label P2P on Oct 23, 2026
  3. DrahtBot commented at 4:45 PM on October 23, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    External sites

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    Concept ACK darosior

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #36608 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36608.svg"></sub> (Reimplement private broadcast as node state-less jobs + package relay support by instagibbs)
    • #36607 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36607.svg"></sub> (private broadcast: Extra guard against unintended non-private broadcast of private transactions by optout21)
    • #36597 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36597.svg"></sub> (wallet: return broadcast errors from CommitTransaction by pseudoramdom)
    • #36566 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36566.svg"></sub> (private broadcast: wait synchronously for mempool acceptance by andrewtoth)
    • #36521 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36521.svg"></sub> (Keep witness stacks in serialized form in CScriptWitness by sipa)
    • #36477 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36477.svg"></sub> (private broadcast: show finished transactions in getprivatebroadcastinfo by vasild)
    • #36429 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36429.svg"></sub> ([RFC] Delayed transaction broadcast, minimal by optout21)
    • #36170 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36170.svg"></sub> (net: require a dedicated bind for automatic Tor by l0rinc)
    • #36074 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36074.svg"></sub> (scripted-diff: [test] Add util/check.h includes for assertions by maflcko)
    • #36002 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36002.svg"></sub> (txindex: allow running in pruned mode by andrewtoth)
    • #35888 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/35888.svg"></sub> (net: reject oversized outbound messages by l0rinc)
    • #35858 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/35858.svg"></sub> (doc: Add privacy.md consolidating network privacy recommendations by pablomartin4btc)
    • #35713 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/35713.svg"></sub> (Remove boost as a unit test runner by rustaceanrob)
    • #35027 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/35027.svg"></sub> (net: add -outboundbind option for outgoing source address by 8144225309)
    • #33922 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/33922.svg"></sub> (mining: add getMemoryLoad() and track template non-mempool memory footprint by Sjors)
    • #30951 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/30951.svg"></sub> (net: option to disallow v1 connection on ipv4 and ipv6 peers by stratospher)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

    LLM Linter (✨ experimental)

    Possible internal implementation names in log messages:

    • src/privbcast/discovery.cpp: LogDebug(BCLog::PRIVBROADCAST, "Discovery: %d queries of %d DNS seeds started\n", m_queries.size(), m_seeds.size()) → Replace “Discovery” with a plain-language event description; it exposes the internal Discovery class name.
    • src/privbcast/discovery.cpp: LogDebug(BCLog::PRIVBROADCAST, "Discovery ended at %d ms\n", at.count()) → Replace “Discovery” with a plain-language event description; it exposes the internal Discovery class name.
    • src/privbcast/job.cpp: LogDebug(BCLog::PRIVBROADCAST, "Discovery found %d candidates\n", m_assignment->exit_path_candidates) → Replace “Discovery” with a plain-language event description; it exposes the internal Discovery class name.

    <sup>2026-10-28 16:16:48</sup>

  4. instagibbs force-pushed on Oct 23, 2026
  5. DrahtBot added the label CI failed on Oct 23, 2026
  6. instagibbs force-pushed on Oct 23, 2026
  7. DrahtBot removed the label CI failed on Oct 23, 2026
  8. instagibbs force-pushed on Oct 23, 2026
  9. instagibbs force-pushed on Oct 23, 2026
  10. instagibbs force-pushed on Oct 24, 2026
  11. fametrano commented at 11:42 AM on October 25, 2026: none

    This PR's tests do not check that each attempt gets its own VERSION nonce, PING nonce and BIP324 key (A4 in doc/design/private-broadcast.md), so a regression there would pass.

    On 19f9cd7c4d I made each of these values a constant for the whole process, one line in src/privbcast/attempt.cpp at a time. All four p2p_private_broadcast*.py tests passed on each of the three variants. Constant proxy credentials, by contrast, make p2p_private_broadcast.py fail.

    Your follow-up branch already checks all three, in p2p_private_broadcast_job.py; I ran it on the PING-nonce variant and it fails there. A reused nonce or key lets recipients link a node's broadcasts to each other, the same kind of leak as the IP linkage the kept tests guard against. Could that check move into this PR?

  12. instagibbs commented at 12:04 AM on October 26, 2026: member

    @fametrano willing to be convinced to move particular items to this PR, but I'm really focusing on IP address level leaks for the first PR. Preventing cross-attempt or cross-tx fingerprinting is a secondary goal, and hard to achieve arbitrary level of assurances, hence its deferral.

    edit: changed my mind and brought it forward

  13. instagibbs force-pushed on Oct 26, 2026
  14. vasild commented at 9:50 AM on October 26, 2026: contributor

    I find the following not acceptable:

    Relying on DNS seeds

    Currently DNS seeds are used only to bootstrap a new node. That happens once in its lifetime and there is a fallback mechanism if the DNS seeds do not work. We never rely solely only on DNS seeds for something. They are centralized services.

    doc/dnsseed-policy.md mentions:

    Bitcoin Core attempts to minimize the level of trust in DNS seeds

    I do not think that it is reasonable to start relying on a few servers permanently (not just once in a lifetime, but every time a node wants to privately broadcast a transaction). This means that if a few operators decide to switch off (or misbehave) their services then everybody on the network is obstructed from privately broadcasting their transactions. That resembles more a client-server architecture than a decentralized P2P network.

    Not broadcasting to Tor or I2P peers

    Only broadcasting to IP peers via the Tor network would make the feature too fragile - it would rely on the Tor network and the Tor exit nodes working properly. The Tor network has been down in the past. I think it is important to have a fallback mechanism which currently is the I2P network. Also, right now we do not rely on the Tor exit nodes. We could broadcast a transaction within the Tor network to an .onion peer even if the exit nodes are off or misbehaving.

    Even moderate adoption of private broadcast in its current form improves the privacy of everybody because everybody could be a recipient. Privacy is improved even for nodes that broadcast-to-all. That would be spoiled if we only send to IP peers. Note that a private broadcast recipient would further broadcast-to-all the newly received transaction in a way that is indistinguishable from them being the transaction originator. So an observer can no longer tell whether a node is the original sender or was a recipient of a private broadcast. For this to work everybody must be a potential private broadcast recipient.

  15. instagibbs force-pushed on Oct 26, 2026
  16. instagibbs force-pushed on Oct 26, 2026
  17. DrahtBot added the label CI failed on Oct 26, 2026
  18. instagibbs commented at 6:50 PM on October 26, 2026: member

    I do not think that it is reasonable to start relying on a few servers permanently (not just once in a lifetime, but every time a node wants to privately broadcast a transaction). This means that if a few operators decide to switch off (or misbehave) their services then everybody on the network is obstructed from privately broadcasting their transactions. That resembles more a client-server architecture than a decentralized P2P network.

    If the seeds go offline, private broadcast stops; it doesn't leak. If they misbehave, the most they can do is pick recipients that drop the transaction. They can't learn who is sending.

    The alternative of picking Tor/i2p peers from addrman as master does, is not an answer anyone should be happy with.

    Today we give attackers significant influence over choices being made by victim. Peers can be influenced, by design, into dialing to peers that are unique fingerprints, with the attacker writing to their addrman.

    Tor isn't decentralized either, but we accept utility in terms of privacy.

    I think it is important to have a fallback mechanism which currently is the I2P network.

    Currently in master, it makes sybiling victims much easier, which results in an IP address leak, which is an unfortunate side-effect of the current architecture. Assuming we adopted something like this PR, then we would have to introduce a "just in time" gathering of addresses, which would still be going through seeder nodes, or again, reintroducing fundamental leaks via local addrman usage. No free lunch here.

    Even moderate adoption of private broadcast in its current form improves the privacy of everybody because everybody could be a recipient. Privacy is improved even for nodes that broadcast-to-all.

    The attacks it resists are quite limited today, and therefore it's sort of in purgatory where we can't make any promises about it. That's quite sad, and completely avoidable.

  19. instagibbs force-pushed on Oct 26, 2026
  20. DrahtBot removed the label CI failed on Oct 27, 2026
  21. in src/privbcast/params.h:96 in f547e5eb57 outdated
      91 | + * The job's VERSION (E1): this version and these services, time 0, addr_recv null with services
      92 | + * 0, addr_from null with these services, a random nonce, this user agent, start height 0 and
      93 | + * relay false.
      94 | + */
      95 | +inline constexpr int PROFILE_VERSION{70017};
      96 | +static_assert(PROFILE_VERSION == PROTOCOL_VERSION, "the profile's version tracks the release");
    


    ajtowns commented at 4:02 AM on October 27, 2026:

    70016 is what current private broadcast versions track, and avoiding varying these things across releases is exactly why we have a special profile.


    instagibbs commented at 6:07 PM on October 27, 2026:

    yeah this is a bad comment and probably not needed, it should not update as a profile break which is what the static assert is for.... will change. Tests would have failed on change.


    spacexm406-web commented at 8:30 AM on October 29, 2026:

    Breakdown of the Code and Discussion

    1. The Code Change (src/privbcast/params.h):
    • inline constexpr int PROFILE_VERSION{70017};: Defines the current profile version as 70017.
    • static_assert(PROFILE_VERSION == PROTOCOL_VERSION, ...);: A compile-time assertion checking that the profile version matches the core protocol version, with the comment explaining: "the profile's version tracks the release".
    1. The Feedback (ajtowns - Anthony Towns):
    • Points out that 70016 is what current private broadcast versions track. He notes that avoiding varying these things across standard releases is precisely why a special profile exists, meaning the profile version shouldn't necessarily be tied directly to changing protocol or release versions haphazardly.

    • Agrees that the comment/assertion logic is flawed ("yeah this is a bad comment and probably not needed").

    • Clarifies that changes to this should not act as a profile break (which is what the static_assert enforces), and notes that he will change it.

  22. ajtowns commented at 4:05 AM on October 27, 2026: contributor

    The design doc looks AI written; is it meant for human or agentic consumption? Shouldn't it be a BIP, or at least posted to the mailing list for broader discussion on exactly the sort of tradeoffs vasild raises above?

    +6k lines (+3.5k -1k under src excluding test) seems like a lot. Presumably also AI written with light human review?

    If the idea is to separate this from the node, it seems like having a separate binary interacting lightly with the node might be a better structure. Something like: a "bitcoin-privbcast" binary, connects only over i2p/tor, keeps it own addrman independent from the node, initially populated from dns seeds, perhaps updated in some way (again, independent of the node), bitcoind wallet sends private broadcast txs via bitcoin-privbcast's stdin, and then just monitors for them to appear in the mempool (and sends a cancel message to privbcast's stdin when that occurs), and bitcoin-privbcast rate limits, retries, expires them itself to avoid send bursts correlating with the node restarting.

    That approach would allow people with higher or different tx privacy goals to replace the privbcast binary with something custom if desired (eg to make different tradeoffs about addr handling, or using tor exit nodes, etc), without needing to modify bitcoind source.

  23. instagibbs commented at 12:47 PM on October 27, 2026: member

    The design doc looks AI written; is it meant for human or agentic consumption?

    The PR text is AI written, curated a number of times under my direction as the primary document. I think there's great value for both humans and AI. It doesn't have to be in the merged branch.

    Shouldn't it be a BIP, or at least posted to the mailing list for broader discussion on exactly the sort of tradeoffs vasild raises #36628 (comment)?

    I think there's definitely value increasing anonymity set if nothing else. By the same token, the current implementation should have gone through a BIP process, for exactly the same reasons.

    That approach

    Originally I designed it as a separate tool, but I was hoping for a like-for-like replacement. However the architecture lends itself just fine to it, as the only coupled code in this PR to the node is the queue portion. An "addrman but not node's addrman" is another design quest, because resisting cross-tx correlation, while a secondary goal, is worth trying to preserve under adversaries.

    appear in the mempool (and sends a cancel message to privbcast's stdin when that occurs),

    This is a leak. I prefer bounded tries. We could have it do retries based on block non-inclusion or some other criteria that isn't a clear fingerprint, e.g., not enough peers asking for the tx itself.

  24. darosior commented at 3:05 PM on October 27, 2026: member

    Concept ACK.

  25. andrewtoth commented at 3:27 PM on October 27, 2026: contributor

    Peers can be influenced, by design, into dialing to peers that are unique fingerprints, with the attacker writing to their addrman.

    Can this design be expanded on about how they can be influenced, and why that is by design? Would this mean the problem is with the design that allows peers to be influenced into dialing to peers that are unique fingerprints, and not the actual private broadcast? Perhaps this can be solved by a better upstream design in addrman?

    Currently in master, it makes sybiling victims much easier, which results in an IP address leak

    Would that be a problem with i2p in general, and not private broadcast? I'm not sure I understand how a sybil attack in i2p could leak a clearnet IP address.

    How would sybiling the node affect sending private broadcast transactions through ephemeral i2p tunnels? These connections should not be connected in any way to the broadcasting node's long-lived i2p address (if it has one) or their tor onion address or clearnet addresses.

    The attacks it resists are quite limited today

    Since we no longer make any promises, and this is marked experimental, can we elaborate on the attacks it cannot resist? I find it difficult to reason about the tradeoffs presented here with respect to attacks that I do not understand or know about. Similarly with the above claims about peer influence and i2p sybiling. Wasn't part of the benefit of marking experimental to allow these attacks and weaknesses to be discussed openly?

  26. instagibbs force-pushed on Oct 27, 2026
  27. instagibbs force-pushed on Oct 27, 2026
  28. DrahtBot added the label CI failed on Oct 27, 2026
  29. instagibbs commented at 6:02 PM on October 27, 2026: member

    Can this design be expanded on about how they can be influenced, and why that is by design? Would this mean the problem is with the design that allows peers to be influenced into dialing to peers that are unique fingerprints, and not the actual private broadcast? Perhaps this can be solved by a better upstream design in addrman?

    Ok I was a bit sloppy in my wording: the design permits influencing, as an accepted residual.

    Addrman is built for partition resistance with the goal of finding a single honest peer that will give us the heaviest chain. As long as the attackers can't take all slots, the node is protected.

    In the private broadcast scenario, our goals are different: the choice of recipient must not say anything about who made it. Each node's table is different, and anyone can write to it through addr gossip, directly as a peer or relayed through one. Addrman needs one honest peer to stay safe; private broadcast loses privacy to one dishonest peer that can "read" these differences.

    A "fixed" addrman would be one that doesn't allow peers to write to it, which would remove its whole purpose. A separate table (addrman lite?) would have to be filled with entries, and that source needs to be as similar to everyone else as possible. In Tor, that's the centralized directory to avoid fingerprinting. For this PR, the answer is "DNS seeds". Extending it to "seeds + gossip with seed provenance per job" would also work. If reused across jobs, then the per-tx tracking can continue via similar fingerprinting vectors.

    Would that be a problem with i2p in general, and not private broadcast? I'm not sure I understand how a sybil attack in i2p could leak a clearnet IP address.

    How would sybiling the node affect sending private broadcast transactions through ephemeral i2p tunnels? These connections should not be connected in any way to the broadcasting node's long-lived i2p address (if it has one) or their tor onion address or clearnet addresses.

    It's specific to the private broadcast implementation, not the i2p protocol itself. The population of i2p is small, thus making your i2p-supporting victim node more predisposed to connect to an attacker.

    Once the attacker is a recipient, it can act on a suspect over clearnet before, during or after its private connection. The crux is whether it still gets an INV from the victim, and whether that INV depends on what it did. In master it can (#36277 fixed one case). In this PR the job announces on a schedule drawn at job start, and nothing the job receives or sees of the transaction changes that, so the attacker gets the same INV whatever it does.

    This entire line of attack collapses by construction and we can be a little less "careful" on which endpoint we connect to, falling back to reasoning about liveness rather than privacy.

    Since we no longer make any promises, and this is marked experimental, can we elaborate on the attacks it cannot resist? I find it difficult to reason about the tradeoffs presented here with respect to attacks that I do not understand or know about. Similarly with the above claims about peer influence and i2p sybiling. Wasn't part of the benefit of marking experimental to allow these attacks and weaknesses to be discussed openly?

    To answer directly: I don't think experimental makes publishing working attacks ok. I think talking about the serial issues the feature has had publicly patched and extrapolating beyond that is more than sufficient to draw lessons.

    The fixes so far: #35032 (addrman updated from private connections), #35410 (v1 retry went over clearnet), #36277 (what recipients saw depended on receipt timing) and #36312 (discouragement of a private peer was observable). Each is a separate place where private broadcast shares state or code with the node, and each was found after merge. Private broadcast connections still run through the node's connection manager, message processing and peer state. The useful question is how we'd know we've found the last one. This PR removes the sharing.

    I think marking it as experimental has two considerations:

    1. API breaks. They happen even in the case where something is technically perfect. Should have been the case from the get-go.
    2. Failure to achieve stated aims. If our stated aims were to mildly frustrate known adversaries like a strawman-passive LinkingLion (connect and stay quiet), I would say it's unnecessary to mark it experimental. (We also had a bug that broke even this, #35410, fixed in v31.1. Assume for the sake of argument there are no others like it.)

    The issue is that this is not true. It currently fails to achieve what I believe the stated aim is, at a fundamental level. I think this is close: it should resist an attacker that "runs many bitcoin nodes and then is able to correlate incoming (decrypted) traffic to originating IP address" (https://github.com/bitcoin/bitcoin/pull/29415#issuecomment-2295439986). That description covers the active version too: an attacker that holds recipient slots and probes suspects over clearnet. That's where master falls short. Finding and building attacks like this is cheap now, so we should defend against the stated model unless that's impossible. The aim of the specification (and thus implementation, if correct) is to directly address this security model.

    The spec in this PR states its goals and invariants against that adversary. The tests here check the IP-linkage invariants, with more on the follow-up branch. I think anyone doubting that a fresh attempt at the problem is needed should be looking at what is in master, and figuring out which invariants it holds, and how they are validated.

  30. DrahtBot removed the label CI failed on Oct 27, 2026
  31. instagibbs force-pushed on Oct 27, 2026
  32. instagibbs force-pushed on Oct 27, 2026
  33. DrahtBot added the label CI failed on Oct 27, 2026
  34. ajtowns commented at 2:43 AM on October 28, 2026: contributor

    The design doc looks AI written; is it meant for human or agentic consumption?

    The PR text is AI written, curated a number of times under my direction as the primary document. I think there's great value for both humans and AI. It doesn't have to be in the merged branch.

    FWIW, it feels overly long, and poorly focused to me; same as most AI spec work. It doesn't really give you a TLDR high-level summary, mixes high level and low level stuff in the same sentence, and makes up its own unnecessary jargon (eg: "Attempt: an opportunity that is dialled: one proxy stream, one BIP324 connection, one session.") Which is fine for an implementation artifact from while you're exploring a feature, and also fine for AI's who like that sort of thing anyway, just seems more negative than positive as a published design doc.

    Shouldn't it be a BIP, or at least posted to the mailing list for broader discussion on exactly the sort of tradeoffs vasild raises #36628 (comment)?

    I think there's definitely value increasing anonymity set if nothing else. By the same token, the current implementation should have gone through a BIP process, for exactly the same reasons.

    Better late than never? shrug

    That approach

    Originally I designed it as a separate tool, but I was hoping for a like-for-like replacement.

    I mostly think this PR is way too much work for the value it gets. I seem to be thinking that about a lot of things though.

    Looking through my node's logs (long lived ipv4 ip and tor address) over the past few months, I saw 0 private broadcast connections in July, 9 in August (4 didn't send an INV at all, 3 accepted, 1 rejected for insufficient fee, 1 already have), 12 in September (3 didn't send an INV, 2 accepted, 1 rejected for fee, 6 already have), 9 in October so far (4 no INV, 1 accepted, 4 already have). Multiply 3 accepted by 18k listening nodes gives 55k txs/month vs ~20M txs in October so far, so perhaps 0.28% of transaction volume. That usage level doesn't feel like it warrants huge PRs to me.

    Could also plausibly just add the hook, and let the implementation live entirely out of tree, with any privacy issues dealt with externally. Then re-merge it, and add wallet support, only once the feature has proven itself robust.

    Since we no longer make any promises, and this is marked experimental, can we elaborate on the attacks it cannot resist? I find it difficult to reason about the tradeoffs presented here with respect to attacks that I do not understand or know about. Similarly with the above claims about peer influence and i2p sybiling. Wasn't part of the benefit of marking experimental to allow these attacks and weaknesses to be discussed openly?

    To answer directly: I don't think experimental makes publishing working attacks ok. I think talking about the serial issues the feature has had publicly patched and extrapolating beyond that is more than sufficient to draw lessons.

    I think there's three ways you could consider a feature:

    • purely research -- nobody's implemented or using it, people are exploring the idea to see if it works, no real reason not to discuss attacks openly
    • best effort -- this approach gives an improvement on alternatives (just broadcast on clearnet) in the average case, but doesn't really provide any guarantees against the worst case
    • fully supported -- this is intended to provide guarantees, many people are relying and would be harmed if attacks become more widely known

    Personally, I don't think private broadcast should be considered fully supported, and I think the amount of re-thinking it seems to need along with fairly low adoption suggests it could even be more "purely research" than "best effort". I think not talking about the potential attacks probably makes it harder to move towards fully supported rather than easier, as well.

    The issue is that this is not true. It currently fails to achieve what I believe the stated aim is, at a fundamental level.

    I don't think enabling privatebroadcast makes things worse for any of these privacy concerns compared to running the same node in the same adversarial environment with privatebroadcast disabled, and in many cases makes them better. I'd call that meeting the aims ("improves the privacy of the transaction originator"), even if it's far from perfect or was overhyped ("If the originator sends two otherwise unrelated transactions, they will not be linkable").

  35. in doc/design/private-broadcast.md:144 in 0053752738 outdated
     139 | +  VERACK. After such a VERSION the job sends WTXIDRELAY, then VERACK; after any other it sends
     140 | +  nothing more. A VERSION that lacks a field up to the relay flag, has bytes after it, or whose
     141 | +  user agent is longer than 256 bytes, is another. A peer that fails any of
     142 | +  these is left before the announcement.
     143 | +- **E3.** The job sends exactly one INV, with one entry: MSG_WTX for the announced transaction's
     144 | +  wtxid.
    


    ajtowns commented at 5:20 AM on October 28, 2026:

    Using MSG_TX and not sending wtxidrelay (matching the current code) seems likely to have a larger anonymity set, without losing any meaningful performance afaics.


    instagibbs commented at 1:02 PM on October 28, 2026:
    1. There's massive profile breaks everywhere in this PR. If we're going to break it, now is the time, especially if usage is low.
    2. Aside from simply being widely supported, WTXID relay allows for slightly more robust opportunistic package relay, since moredern Core nodes will only count wtxid relay peers as additional announcers of orphans. There avoids a potential future profile break (or logic that switches flags depending on what is being sent).
  36. instagibbs commented at 1:15 PM on October 28, 2026: member

    That usage level doesn't feel like it warrants huge PRs to me.

    That's sort of an argument for ripping it out entirely. Which granted you sort of follow up with immediately afte.

    My argument for keeping it, in replaced form: Yes it's a lot of code, relatively speaking, but it's far easier to reason about than what is in master. There's no interference with consensus critical code, was built for testing in mind, and contains future profile breaks to intentional design decisions. That's not even broaching the subject of the IP address leaks.

    If that's "too much work", we should remove it entirely, replaced with maybe a hook like you say.

    I don't think enabling privatebroadcast makes things worse for any of these privacy concerns compared to running the same node in the same adversarial environment with privatebroadcast disabled, and in many cases makes them better. I'd call that meeting the aims ("improves the privacy of the transaction originator"), even if it's far from perfect or was overhyped ("If the originator sends two otherwise unrelated transactions, they will not be linkable").

    I think there is very little we can say about today's implementation to the user, and incremental whackamole improvements doesn't look like it will change that. "best effort with no described privacy model forever" <-- is this something we should be supporting

  37. doc: add the private broadcast specification
    This is the design the following commits implement: what a private
    broadcast job guarantees, the values peers and observers see, and the
    node's interface. Later commits cite its items by number.
    88f960fd83
  38. privbcast: add the parameters and the schedule
    Start src/privbcast, the code that will run the node's private
    broadcast jobs, built into bitcoin_node. params.h holds the values of
    the specification. DrawPlan draws a job's plan from the job's own
    randomness: the schedule, the order of the DNS seeds and the seed of
    the order in which each DNS seed hands out its candidates.
    MIN_DNS_SEEDS is the floor R3 sets for the mainnet seed list, and a
    unit test holds the list to it, so a release that drops seeds below
    what fills a job fails the test rather than leaving opportunities
    empty.
    
    Implements C1 and R3; the values of Parameters.
    5905856aba
  39. privbcast: add candidate assignment
    Assign fixes every opportunity's candidate once discovery ends, from
    the plan and the discovery result alone. An endpoint that several
    seeds returned is credited to the seed that comes first in the plan's
    seed order, through the lowest query set that returned it. Each seed
    hands out its candidates in an order drawn at random by a generator of
    its own, seeded from the plan and the seed's place in that order. Slot
    j's first attempt comes from query set j mod 4 whenever a seed it may
    draw from has one, so a single exit supplies at most two first
    attempts. Neither which seed answers first nor the order of a seed's
    answers changes the assignment.
    
    Implements C2, R2, R4, R5a, R5b and R5c.
    f2dc2e2997
  40. privbcast: add a byte-driven SOCKS5 client
    A job reaches recipients and resolves DNS seed names through the proxy,
    on streams its event loop drives without blocking. Socks5Client holds
    no socket: it offers the bytes to write, consumes the proxy's replies,
    and leaves bytes past the final reply to the P2P transport. It offers
    only username and password authentication, with the credentials it is
    given or fresh ones, and fails before sending anything if they are
    outside RFC 1929's lengths or the username would select Tor's extended
    parameters. It sends every destination as a name, numeric addresses
    included, and resolves nothing: the node's own connections do the
    same, and Tor's SafeSocks refuses a request that carries only an
    address. Its errors name the proxy's reply with netbase's
    Socks5ErrorString, now exported.
    
    Implements B2 and the Proxy interface section.
    7b03d4229a
  41. privbcast: add the attempt state machine
    An attempt is one connection to a recipient once the proxy connected
    it: a BIP324 session as initiator, the handshake with the job's
    VERSION, one INV, the transaction served at most once, and the PING.
    It holds no socket and reads no clock: the job's loop passes the bytes
    read and the time; each call records what happened (a completed write,
    the connection, bytes received) and then enforces the attempt's
    deadlines, so that nothing is acted on at or after one.
    
    The attempt draws its VERSION and PING nonces from the random context
    it is given, and takes its BIP324 key, ellswift entropy and garbage
    from a key source. Without one, as in production, V2Transport draws
    them from the strong RNG. The fuzz target injects a source
    that draws from its seeded context, so that an attempt's bytes are a
    function of that context and of what the peer sent.
    
    Implements E1-E8, A2, B4, D3 and the attempt part of D2.
    5d5707f05f
  42. privbcast: add proxy streams and discovery
    A job reaches the network only through streams to the SOCKS5 proxy,
    which its event loop drives without blocking. ProxyStream connects to
    the proxy by address or unix socket path, resolving nothing, and runs
    the exchange of its Socks5Client; each stage, the connect and each
    step of the exchange, times out on the steady clock. Once the proxy
    has connected the stream, its socket carries the destination's bytes.
    
    Discovery launches every RESOLVE query at job start, each on a stream
    of its own, in four query sets: each set has credentials of its own
    and one query of every seed, so Tor builds four circuits rather than
    one per query, and each seed is still asked through four exits.
    Discovery ends when every query has ended or when the discovery window
    does, whatever is still pending. It keeps the answers that are public
    routable IPv4 or IPv6 addresses, with the chain's default port, and
    counts the others; its result gives each seed's answers by the query
    set they came through, whatever order they arrived in. An exception
    thrown while it acts on one query ends only that query, as a failed
    stream would.
    
    Implements B1, B2, C3, D1, R1, R2 and C7 for the queries.
    cac5ffe15f
  43. privbcast: add the job
    A job is one event loop on its caller's thread. Step() runs the
    deadlines and the dials that are due, waits on every open stream until
    the next deadline, and does the I/O; Run() steps to the end. The plan
    is drawn when the job is made, before any socket exists, and discovery
    starts with the first step. When discovery ends the assignment is
    fixed, and each slot dials its opportunities at their scheduled times,
    one attempt at a time, until one reaches the announcement point or
    none is left; a job whose discovery found no candidate ends there,
    failed with "no candidates". The slots share only the loop, so a stalled stream holds
    up no other slot, and the cancel flag ends everything at the next step.
    
    An exception thrown while the loop acts on one attempt ends only that
    attempt, as a failed stream would. One thrown anywhere else in a step
    fails the job: it stops as a cancelled job does, and its report gives
    the error.
    
    The report mirrors Interface/Report, every time an offset from job
    start on the plan clock. Snapshot() gives the progress the node is to
    show, from any thread.
    
    Implements A1, A3, A4, C1-C8, D1, D2, H1-H3 and the report interface.
    03435a45ef
  44. test: extend the harness for private broadcast jobs
    The SOCKS5 server gains Tor's RESOLVE command, reply delays for a slow
    or stalled proxy, a mode that offers only unauthenticated access and a
    mode that behaves as an ordinary non-Tor proxy. A client that hangs up
    before its greeting, as a cancelled job's streams can, and a connection
    that Windows reports as aborted count as closed, not as errors. The P2P
    network thread judges a failed protocol by its own reconnect flag, so
    that several listeners can share one loop. test_framework/privbcast.py
    holds the release's parameters, a scripted recipient, and the checks of
    a job's wire profile, of fresh keys and nonces in each attempt, and of
    its schedule, report summary and proxy credentials.
    
    Prepares the functional tests of the node's jobs.
    91144bdd5a
  45. node: add the private broadcast job queue
    The node runs each private broadcast as a privbcast::Job on a thread of
    its own. PrivbcastQueue queues the submitted transactions, starts and
    cancels the jobs from one scheduler thread, and keeps in memory the
    records that getprivatebroadcastinfo and abortprivatebroadcast are to
    show. It reaches the node only through callbacks for networking, the
    onion proxy and the mempool, and takes the job runner as a parameter,
    so that tests can drive it with stub jobs and mocked clocks. A job that
    failed, or whose runner threw, ends aborted with the error in its
    record. Whether a cancel cut a job short is read from its report, so
    one that lands after the job ended leaves it done. At shutdown the
    default log gives the number of jobs stopped, and nothing more.
    
    The scheduler's start rule is a pure function, StartPolicy::Decide(),
    which each pass of the scheduler applies.
    
    Implements N3-N7, N9, N10, the node's D2 and D3, U3. Not yet wired:
    the next commit switches -privatebroadcast to it.
    4f78532ddf
  46. node: run private broadcast as jobs
    With -privatebroadcast, sendrawtransaction now queues a job on the
    node's PrivbcastQueue instead of handing the transaction to the
    connection manager, getprivatebroadcastinfo lists the jobs with their
    progress and reports, and abortprivatebroadcast aborts them. Startup
    requires onion to be able to become reachable, refuses the regtest
    seed and time divisor overrides on other chains, on signet
    -signetseednode and -signetchallenge, and off regtest a non-zero
    -mocktime, requires -privatebroadcastseed on regtest, which has no DNS
    seeds, and refuses to start rather than trim connections when the
    jobs' file descriptors are not available. -connect and -proxyrandomize
    no longer matter.
    
    -privatebroadcasttimedivisor, regtest only, divides every duration of
    a job's plan, so that functional tests can run jobs in real time.
    
    The old private broadcast connections and relay stay in the tree, no
    longer fed, until the commits that remove them.
    
    Implements N1, N2, N8, N10, N11, U1, U2 and Interface/Node; replaces
    the old implementation's tests with the specification's.
    0742c8a882
  47. test: run private broadcast jobs end to end through the node
    p2p_private_broadcast.py drives jobs on a mocked clock, which moves a
    job's schedule but not the steady clock that times its proxy exchanges
    and connections. These tests run jobs in real time instead, shortened
    by -privatebroadcasttimedivisor: a recipient that floods a job is cut
    off at the receive cap, and a proxy that refuses authentication or is
    not Tor gets nothing to carry.
    
    test_framework/privbcast.py gains the harness: it restarts the node
    behind each scenario's SOCKS5 server with its seeds and time divisor,
    and reads the job's report from getprivatebroadcastinfo.
    82cc75b326
  48. net: remove the old private broadcast relay
    Since -privatebroadcast runs jobs, nothing hands PeerManager a
    transaction for private broadcast any more. Remove its queue
    (src/private_broadcast.{h,cpp}) with the methods, the retry task and
    the private_broadcast option that served it, and its handling of
    private broadcast connections: the fixed VERSION, the INV after
    VERACK, the GETDATA and PONG handling, the incoming message filter,
    the lifetime limit and the exemption from discouragement. The unit and
    fuzz tests of that code go with it, including the net_tests case for
    the addrman exception and the denialofservice_tests cases for the
    discouragement exemption.
    
    Delete-only: besides whole lines, conditions and arguments are cut out
    of lines, and the lines of a removed branch's alternative move out one
    indentation level. git show -w --word-diff-regex='[A-Za-z0-9_]+|[^[:space:]]'
    shows deletions only, apart from the size of the node array in
    denialofservice_tests, which goes back from 5 to 3.
    e452ebc06e
  49. net: remove the old private broadcast connections
    With the relay gone, nothing asks CConnman for private broadcast
    connections. Remove the connection type, the thread that opened such
    connections with its counters and choice of network, and the places
    that treated them specially: the incoming nonce check, the fresh I2P
    session, the guard against direct connections, the outbound message
    filter, the fixed protocol version, the exception in disconnection by
    subnet and their term in the orphanage's static_assert. The type's
    names go from getpeerinfo's help, bitcoin-cli -netinfo and the GUI. The
    proxy override of ConnectNode() and OpenNetworkConnection(), which only
    that thread set, goes too, with its copy in CNode and in the
    reconnection record and the fuzz input for it.
    
    Delete-only: besides whole lines, conditions, arguments and list
    separators are cut out of lines, what followed a cut argument joins
    its line, and one line moves out an indentation level with the if
    around it. git show -w --word-diff-regex='[A-Za-z0-9_]+|[^[:space:]]'
    shows deletions only.
    4acf24a802
  50. refactor: drop the trivial AdvertisedVersion()
    Without private broadcast connections, a node advertises
    PROTOCOL_VERSION on every connection, and CNode::AdvertisedVersion()
    only returns it. Use the constant again where the method was called.
    f8c5e64add
  51. doc: describe bitcoind -privatebroadcast
    Add the user guide to private broadcast: which Tor proxy the jobs use,
    how sendrawtransaction queues them and when they start, what is not
    queued again, how to follow and abort jobs, what disabling networking
    and shutdown do, which node settings do not apply, what the default
    log shows, the file descriptors the jobs need, and the limits of what
    a job protects.
    4bd50b805e
  52. doc: release notes for private broadcast jobs 8e34f76058
  53. instagibbs force-pushed on Oct 28, 2026
  54. darosior commented at 4:24 PM on October 28, 2026: member

    Since we no longer make any promises, and this is marked experimental, can we elaborate on the attacks it cannot resist?

    That we mark it experimental in new releases does not automatically notify users that may depend on it because of the misleading claims we made in previous releases.

    It's also unnecessary to put these users at risk, as we can discuss the architectural defects without the details of the node state leaks that allow to exploit those.

    find it difficult to reason about the tradeoffs presented here with respect to attacks that I do not understand or know about.

    The general pattern is pretty simple. The victim node connects to the attacker both on clearnet and through the private broadcast connection. The attacker correlates the node state on both connections, giving away the privatebroadcasting node's IP address.

    Having a non-trivial chance of getting selected for outbound connections costs very little to an attacker. Preventing that a single entity does not occupy all our outbound slots is a design goal of Addrman, but preventing that we don't make a single connection to an attacker with a lot of reachable nodes is not one, nor something we can achieve. Stated differently: it is trivial for today's spies to have the vast majority of Bitcoin nodes connect to them, and then have a non-trivial chance of getting connected to for a private broadcast connection.

    (I'll pass on the terrible incentive for spies to Sybil the network created by the current implementation of this feature.)

    Therefore the only place where we can give meaningful guarantees, is in preventing a private broadcast connection from being correlated with the victim node's connection. This is not something we can guarantee for 2 connections established by the same node, due to the design of our p2p stack, and as i hope the numerous historical correlation vectors and private broadcast defects make clear.

    I want to emphasize guaranteeing the absence of leaks here. In these discussions i've seen advocates of the current private broadcast architecture challenge critics to lay out the details of specific leaks, so they can be (dismissed or) patched. This is backwards. The onus is on advocates of a feature claiming to offer privacy or security, to convince others that it does actually achieve the stated goals under an acceptable threat model. Accepting the feature as secure by default and asking skeptics to keep coming up with end to end exploits to prove otherwise until they get tired of it, is not a serious way to approach development of a privacy feature.

    In this instance, this means giving reasonable assurance that state can't be leaked between a regular connection and a private broadcast connection. I'm always happy to consider arguments to the contrary, but as far as i know the only way to get any such assurance currently, is to isolate the private broadcast connections' from the node's state.

    FWIW, it feels overly long, and poorly focused to me; same as most AI spec work.

    I think the design document is useful and correct as is, but i agree it's fair to ask the author of a +6000,-3000 PR that directly follows from that document to put in the effort of organically writing such document.

    I mostly think this PR is way too much work for the value it gets.

    I think this applies to the private broadcast version currently in master. It provides categorically weaker privacy than existing solutions, and can't provide any guarantee in adversarial conditions (which are the norm: there are entities attempting to deanonymize Bitcoin users today).

    The stated goals are valuable, and I think would make for a useful feature to have in Core. But the current implementation does not achieve them. So we are stuck in this (now familiar) position of having taken on all the technical debt without achieving anything meaningful. The default position in this situation should be to correct the mistake by reverting the merge, but i like Greg's more positive approach to try and fix it instead.

    It's a big PR, but once the specs are pinned down, the implementation is mostly straightforward. It is also not complicated by having to reason about the interaction with existing (and future) P2P machinery, and implements a valuable feature.

    I don't think enabling privatebroadcast makes things worse for any of these privacy concerns compared to running the same node in the same adversarial environment with privatebroadcast disabled

    I strongly disagree. @andrewtoth @vasild as far as i understand you share this opinion? I think this view underpins much of our disagreement on how to approach private broadcast, so it's important to get some common ground here to be able to make progress.

    The status quo before opting into the private broadcast option was to opt into using one of the available tools making a one shot Tor connection to a public broadcast API to obfuscate source+links, or opt into -onlynet=tor to obfuscate the source only.

    Today, you can opt into the private broadcast option, which sounds like it may protect your source IP, but actually may not if you also run your node on clearnet. And despite marking it as experimental, some users may consider it relatively more reliable than an external tool maintained by a single person, because it was released by Bitcoin Core :tm:. Others may switch to using it with some expectation of privacy ("surely the Core developers wouldn't release a privacy feature which can be defeated that easily, they are just being overly conservative in marking it as experimental").

    In both cases, this gives users a false sense of security, which i think ranks pretty high in the list of things Bitcoin Core should be very careful not to do.

    Now you may argue there exists some users with a desire for privacy that would neither use the one-shot Tor broadcast tools, nor the onlynet=tor option, but would use -privatebroadcast AND would not change their behaviour due to thinking they are now private (which falls under the footgun discussed just above). And you may say these users were previously susceptible to existing spies on clearnet, so -privatebroadcast is an improvement for them.

    That may be true before the spies adapt (which is a tautology in itself: users are secure if nobody cares to attack them), but actually makes things even worse once they do. Now the spy can still deanonymize transactions, but they also Sybil the network and we give them for free a "transactions made by users with a particular desire for privacy" filter!

    Therefore, i think we should stop describe the current implementation of this feature a "best effort". It’s an innocuous footgun at best, until people start using it and spies adapt, at which point it becomes actively harmful. I understand this would not be true if we fixed all possible leaks, but i think this PR is the only way to get reasonable assurance about this.

  55. DrahtBot removed the label CI failed on Oct 28, 2026
  56. instagibbs commented at 5:42 PM on October 28, 2026: member

    I think the design document is useful and correct as is, but i agree it's fair to ask the author of a +6000,-3000 PR that directly follows from that document to put in the effort of organically writing such document.

    Definitely worth putting in a hand-written design doc that's not targeting reimplementation and I had one before (that suffered rot due to churn). But I think the crux of the current arguments currently don't rest on that document,

  57. andrewtoth commented at 6:06 PM on October 28, 2026: contributor

    To be perfectly clear I believe separating the node state as much as possible from private broadcast is the correct goal. However, I shared some concerns about this approach in an earlier draft here https://github.com/instagibbs/bitcoin/pull/7#issuecomment-6201251786, and the same concerns were raised independently by vasild here #36628 (comment). I don't think using the built-in DNS seeds to source addresses for private broadcast is the right approach, and I don't think we should drop i2p or direct tor onion connections.

    The latter concern doesn't seem to continue to make sense if we successfully execute separating the rest of the state.

    Regarding the former concern: as of today that would require us to continue using addrman. Since this is an experimental feature, I don't think we need to rush to a solution. I don't think this needs to be done by v33, and we can brainstorm for other ways to better source addresses. IMO supplying addresses is also a better interface for a tool, since other consumers could source addresses how they wish (say just give the tool mempool.space's node IPs) without dependence on the built-in seeds.

    Are these two tradeoffs acceptable, while taking the rest of the patch? I'm not sure. I understand the security team needs to use discretion when discussing possible vulnerabilities, but the status quo here is quite frustrating I'm sure for both sides. How can we know if the issue with only addrman or tor onions or i2p are realistically exploitable?

    Preventing that a single entity does not occupy all our outbound slots is a design goal of Addrman

    From the attacks I've seen, the only ones that have not been patched require an attacker to eclipse the node and swallow all addr messages to be effective. If that's the case, this would be very expensive and the user would have bigger problems. Again, is this tradeoff acceptable, or are there now other attacks that have been discovered?

    The onus is on advocates of a feature claiming to offer privacy or security, to convince others that it does actually achieve the stated goals under an acceptable threat model. Accepting the feature as secure by default and asking skeptics to keep coming up with end to end exploits to prove otherwise until they get tired of it, is not a serious way to approach development of a privacy feature.

    I don't think this is an acceptable stance if we want to collaborate on improving this feature. AI will continue to insist there are bugs and leaks that are not realistically possible to exploit.

    I think this view underpins much of our disagreement on how to approach private broadcast, so it's important to get some common ground here to be able to make progress.

    The status quo before opting into the private broadcast option was to opt into using one of the available tools making a one shot Tor connection to a public broadcast API to obfuscate source+links, or opt into -onlynet=tor to obfuscate the source only.

    Now you may argue there exists some users with a desire for privacy that would neither use the one-shot Tor broadcast tools

    I think the status quo for most users is just using default broadcast, and everyone desires privacy (but they don't know they desire it until someone reveals to them they've violated it). I think making even the current version default behavior if we detect access to a privacy network (and warning if we can't) would be a vast improvement over the status quo. So I think improving the UX of private broadcast (by adding it to wallet, package relay, synchronous, etc.) on the way to making it default behavior is the correct path forward. That way a sybil attacker will spend a lot more resources to find out transactions that are just the default.

    Of course we should continue to harden the privacy against these attacks as well, but I don't think the status quo makes this feature less valuable or somehow dangerous to users.

    Looking through my node's logs

    Indeed but this is an optional feature that has only been available since last release. I wouldn't expect it to have lots of usage until node-in-a-box implementations start turning it on by default at least.

  58. instagibbs referenced this in commit 9951f1ea15 on Oct 28, 2026
  59. instagibbs referenced this in commit 66de69042f on Oct 28, 2026
  60. ajtowns commented at 5:17 AM on October 29, 2026: contributor

    The status quo before opting into the private broadcast option was to opt into using one of the available tools making a one shot Tor connection to a public broadcast API to obfuscate source+links, or opt into -onlynet=tor to obfuscate the source only.

    I think what we have is better than nothing, but I believe the argument you're making is that the relevant alternative isn't nothing, but rather people using an independent tool like https://github.com/alfred-hodler/pushtx and that doing so is better than what we have now (because, being independent, it doesn't risk leaks due to node state contamination).

    A quick AI analysis suggests that pushtx has its own failings: if tor isn't available it will just drop to clearnet, it gets the peers to connect to via clearnet DNS, and it allows its connections to be correlated to each other by listening for and reacting to the INV for its own transaction. The alternative approach of using tor to connect to a block explorer's push-tx api endpoint works for privacy, but is obviously pretty terrible for censorship resistance. So I think I'm not really convinced that what we have, even with its flaws, isn't better than the actual alternatives. (I think I am convinced that improving those alternatives is a better path than trying to have an in-node solution that's not contaminated by in-node state though)

    Today, you can opt into the private broadcast option, which sounds like it may protect your source IP, but actually may not if you also run your node on clearnet. And despite marking it as experimental, some users may consider it relatively more reliable than an external tool maintained by a single person, because it was released by Bitcoin Core ™️. Others may switch to using it with some expectation of privacy ("surely the Core developers wouldn't release a privacy feature which can be defeated that easily, they are just being overly conservative in marking it as experimental").

    I think it's a mistake in both theory and practice for people to assume Core developers are infallible with regard to crypto or privacy concerns. Getting things right is a matter of correcting mistakes when they occur, not never making mistakes. Trying to satisfy people who expect us never to make mistakes isn't going to work -- the best we can possibly do is satisfy people who expect that we'll tell them when we make mistakes and that we'll do our best to fix those mistakes.

    I think you might want to review #35858 as the recommendations there seem stronger than is justified ("The transaction reaches the wider network only once a recipient relays it, which prevents linking the submission to the node's IP address.")

    In any event, it sounds to me like a better approach to this feature would be:

    • abandon the approach of in-node support for private broadcast -- there's too much risk of state contamination that defeats the entire purpose of the feature for realistic adversarial scenarios
    • deprecate the existing feature in point updates to existing releases -- what we have isn't something we can really recommend that people use (change it to -privatebroadcast=allow-deprecated to continue enabling it perhaps)
    • provide a BIP/spec for private broadcast behaviour, with discussion of the tradeoffs and risks. if different people strongly prefer different approaches, have multiple BIPs as appropriate
    • provide external tools implementing the approach documented in the spec(s)
    • add a hook so that sendrawtransaction always uses a specified external tool
    • add a hook so that wallet broadcast (and wallet rebroadcast) always uses a specified external tool rather than adding to the mempool or relaying directly (goal here would be to reduce linkage between txs in different wallets, I think)
    • include an in-repo implementation of a spec whose tradeoffs are reasonable/broadly acceptable and bundle it with bitcoind

    I don't have a strong preference as to order; but deprecating the feature shouldn't be delayed pending other steps if our feature either is not something we can recommend as fit for purpose and/or is worse than using alternative third party tools.

    (Even in this scenario, there's a potential linkage between node and tx: the wallet will likely only rebroadcast once the tx in question exits your mempool)

    Apart from "include an in-repo implementation of a spec" I think all of those should be relatively small or negative-LOC PRs as far as the bitcoin core repo is concerned, so relatively easy to review.

    (To clarify maybe: this is my opinion on an approach would most easily meet everyone's goals here; it's not meant to be an ultimatum, or a "your ideas are bad and should be thrown away" against anyone)

  61. ajtowns commented at 6:30 AM on October 29, 2026: contributor

    Preventing that a single entity does not occupy all our outbound slots is a design goal of Addrman

    From the attacks I've seen, the only ones that have not been patched require an attacker to eclipse the node and swallow all addr messages to be effective.

    I think an extreme attack scenario worth thinking about here is:

    • about 50% of the reachable addresses in your addrman that also function as a working node (ie, will give you a good headers chain, respond to pings etc) are honest nodes. this is sufficient for addrman's goal of "preventing a single entity from occupying all outbound slots" with 99% chance
    • the remaining 50% of reachable addresses in your addrman (or more, given the additional constraints above) are attacker controlled
    • not only that, most of them (say 42% of addrman entries) are roughly unique to your node -- they were given to only you by the attacker, and you didn't successfully relay them to an honest outbound, because addr relay is probabilistic not flood fill).
    • when you make three outbound connections to do private broadcast, you have ~80% chance of one of those connections being to an attacker controlled address that has not been shared with anyone else, linking that transaction with your node (because the attacker only gave that address to your node), and hence with your other transactions

    Hopefully that exact attack is unrealistic, given the expectation that the attacker has to provide perhaps ~650M unique addresses in total (18k addresses for each of 18k public nodes to be 50% of reachable addresses, but unique for each node?) all with a reachable bitcoin node answering them, ready to spy on relayed transactions. It's meant to be a thought-experiment, not an attack recipe, after all.

    In that scenario, addrman, block relay and transaction relay all continue to work as specced and the node is not eclipsed, because you do have at least one honest outbound connection, but a pervasive group attempting to do tx linking still immediately succeeds in identifying 4-out-of-5 private broadcast txs.

    Since this is an experimental feature, I don't think we need to rush to a solution.

    I think making even the current version default behavior

    Indeed but this is an optional feature that has only been available since last release. I wouldn't expect it to have lots of usage until node-in-a-box implementations start turning it on by default at least.

    I don't think you can reasonably say "this is an experimental feature" and "it should be default and enabled by node-in-a-box implementations" at the same time.

  62. spacexm406-web commented at 8:36 AM on October 29, 2026: none

    Improve

  63. DrahtBot added the label Needs rebase on Oct 29, 2026
  64. DrahtBot commented at 12:14 PM on October 29, 2026: contributor

    <!--cf906140f33d8803c4a75a2196329ecb-->

    🐙 This pull request conflicts with the target branch and needs rebase.

  65. andrewtoth commented at 3:18 PM on October 29, 2026: contributor
    • not only that, most of them (say 42% of addrman entries) are roughly unique to your node -- they were given to only you by the attacker, and you didn't successfully relay them to an honest outbound
    • when you make three outbound connections to do private broadcast, you have ~80% chance of one of those connections being to an attacker controlled address that has not been shared with anyone else

    But I think it's important to note that the attacker does not know which addresses were not successfully relayed to an honest outbound. The attacker cannot know that the address has not been shared with anyone else, so the attack falls apart since the connections could come from anyone.

    I don't think you can reasonably say "this is an experimental feature" and "it should be default and enabled by node-in-a-box implementations" at the same time.

    I am not saying it "should be default and enabled" today while it is still experimental. I was describing the vision I have for this feature, which I hope we can get to at some point in the future.

  66. vasild commented at 4:06 PM on October 29, 2026: contributor

    Thank you, @ajtowns for actually trying to analyze an attack scenario in details.

    I think all this talking that private broadcast is broken, flawed, foot gun, a failure, worse than send-to-all or harmful, without explaining why in details have not been constructive.

    Now about this:

    I think an extreme attack scenario worth thinking about here is:

    note that the attacker does not know which addresses were not successfully relayed...

    Right, further note that whenever we receive a new inbound connection we would send 1000 addresses from our addrman to the peer. So those 42% unique addresses in our addrman will soon stop to be unique and the attacker cannot make any reliable conclusions. There is unpredictable spill out from our addrman into the addrmans of other nodes.

    Hopefully that exact attack is unrealistic

    I agree, for more than one reason.

  67. darosior commented at 5:27 PM on October 29, 2026: member

    @ajtowns

    I think what we have is better than nothing

    Could you explain why? I disagree, and i tried to explain why in my previous message.

    but I believe the argument you're making is that the relevant alternative isn't nothing

    Yes but that's only part of my argument. Shipping something that is worse (privacy wise, which i think is the relevant dimension here) than existing alternatives, certainly isn't better. But also, shipping a privacy branded feature with known architectural defects which permit breaching the promised privacy under a very reasonable threat model, is worse than shipping nothing.

    but rather people using an independent tool like https://github.com/alfred-hodler/pushtx and that doing so is better than what we have now

    However for that part, no, this is not the argument i was making. What i had in mind was simply broadcasting the transaction through Tor to one of the many available broadcast API out there. A simple curl using your Tor proxy is sufficient here, and i know of multiple people who have been doing just that in the past. Some popular explorers like Mempool's or Blockstream's offer hidden services and even a webpage for transaction submission (1, 2), so less technical users can use the Tor browser instead of the command line.

    It is indeed less reliable as it's limited to a few single points of failures, and if we can offer a more decentralized option i'm all in, but i don't think this should compromise on privacy, which is the desired property in the first place.

    That said i am skeptical we could realistically achieve this. I think that would merely shift the single point of failures from explorer APIs towards DNS seeds, and while there may be reasons why the latter may be reasonable, that does not seem like a particularly interesting goal.

    Instead, the value i see in private broadcast is to make this limited tool, which allows one to trade-off reliability for substantially improved privacy, available to more people. If we can implement it properly, i think it could have some pretty interesting properties at scale: some turnkey node solutions could start defaulting to using it by default for broadcasting the transactions from wallet commonly associated with these boxes, and if a few % of transactions on the network start being broadcast this way it might create enough false positives to defeat spies' transaction origin analysis.

    This perspective is why i'm supporting this attempt to salvage the feature rather than stripping it off entirely.

    So I think I'm not really convinced that what we have, even with its flaws, isn't better than the actual alternatives. (I think I am convinced that improving those alternatives is a better path than trying to have an in-node solution that's not contaminated by in-node state though)

    Fair enough for those alternatives, but i don't think this should be our baseline for comparison.

    In any event, it sounds to me like a better approach to this feature would be:

    Ok, i can agree with that list. I would rather we don't deprecate the feature and instead fix it with this PR, but i may be too optimistic. It also seems to me that the threat model / design document for that feature can be discussed internally to Bitcoin Core, and does not need to go through the BIP process, but that's of secondary importance. @andrewtoth

    I understand the security team needs to use discretion when discussing possible vulnerabilities

    To be clear: this is unrelated to the security team. As far as i'm aware, no embargo on any of the remaining leaks was requested. Not discussing those in details publicly while users may be affected is just a matter of judgement on the part of the discoverer.

    How can we know if the issue with only addrman or tor onions or i2p are realistically exploitable?

    Again, this is backward. How can we be reasonably convinced this feature is robust enough that it is reasonable to ship?

    The onus is on advocates of a feature claiming to offer privacy or security, to convince others that it does actually achieve the stated goals under an acceptable threat model.

    I don't think this is an acceptable stance if we want to collaborate on improving this feature. AI will continue to insist there are bugs and leaks that are not realistically possible to exploit.

    I'm sorry, but this is a deeply unserious approach to developing security-critical software. By that standard, should reviewers identifying a memory safety vulnerability be expected to demonstrate a working RCE before the issue is taken seriously? And even then, should we merely address the specific exploit demonstrated, which has been the approach for private broadcast leaks, rather than the underlying vulnerability? This would turn security review into an endless game of whack-a-mole, with the burden falling on reviewers to keep finding new ways to exploit the same fundamental flaws.

    The bit about AI is also concerning. It reads as if you suggested AI tools are hallucinating unreasonable privacy breaches against an otherwise sound implementation of the feature. Yet you've not given a single threat model by which we could judge whether a breach can be considered reasonable, the implementation has repeatedly been shown to be unsound, and AI tools have been pretty effective at poking holes in it.

  68. instagibbs commented at 6:21 PM on October 29, 2026: member

    But I think it's important to note that the attacker does not know which addresses were not successfully relayed to an honest outbound. The attacker cannot know that the address has not been shared with anyone else, so the attack falls apart since the connections could come from anyone.

    Again, onus is backwards here. Where's this invariant promised in the code or docs? How did you verify this? Is there existing test for this supposed invariant?

    Blind assertions forcing me to do more work is not appropriate.


    To actually circle back to the PR at hand: I'm currently putting DNS seeds as a liveness requirement for a privacy tool. This is something I have to defend, but given that, neatly slots into what I consider a reasonable security model, from which flows invariants that can be stated clearly, tested, and audited as heavily as we'd like.

    The alternative is to continue arguing about what is what is "unrealistic" under no defined model at all.

  69. ajtowns commented at 1:03 AM on October 30, 2026: contributor

    But I think it's important to note that the attacker does not know which addresses were not successfully relayed to an honest outbound.

    That is captured in the "80% success rate" -- they have up to a 20% false-positive rate in this scenario, but that's hardly meeting the "private" goal of this feature.

    Right, further note that whenever we receive a new inbound connection we would send 1000 addresses from our addrman to the peer.

    The addresses we send are cached in order to avoid an attacker being able to poll all the addresses in our addrman.

    Hopefully that exact attack is unrealistic

    I agree, for more than one reason.

    And yes, I realise that (hopefully) wasn't a sufficient recipe to form a practical attack -- if it were, I wouldn't have posted it publicly. You should be able to conceive of the possibility of a theoretical attack being made both more more impactful and more practical and work to defend against that possibility, not dismiss it out of hand.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-10-30 09:40 UTC