Goals
Goal 1: Remove reliance on, and probes about, internal node state from private broadcast
Goal 2: Specify observable behavior to aid reimplementation and reduce profile fingerprints across versions
Layout
Four parts, each starting with an empty marker commit:
- support: moves shared code into the common library; no behavior change
- bitcoin-privbcast: the specification and the standalone tool
- bitcoind -privatebroadcast as jobs: the node runs the tool's job; the old relay and connections are removed
- package mode: one parent and its child. Its validation commits (saturating fee sum, test-accept as submission, self-paying parent) serve only this part.
For the v33 release, I think 1 through 3 should be the goal, but the roadmap could be trimmed down to remove tool-only code if the conceptual support for the standalone tool is not there.
The tool itself introduces about 600 loc of logic, and also necessitates some of the prep move-only commits putting things in common. The vast majority is the shared Job machinery.
Node-less state
The primary failure mode of the in-master private broadcast is that it relies on node state for operation, and this has led to a number of "careful coding" fixes:
- The v2->v1 retry connected directly, without the proxy: #35319 (merged in #35410)
- Discouraging a misbehaving peer gave a clearnet oracle: #36312
- Eager job completion via mempool inspection: #36277
- Private broadcast connections wrote to addrman: #35032
There are many other sources of fingerprints as well, some harder than others to exploit, and to fix. The solution: remove reliance on node state entirely to drive attempts. The spec states the goals as:
- A job reveals neither the sender's IP address, onion address nor node identity to any party.
- A hostile recipient, DNS seed or Tor exit learns no more than an honest one. It can only affect delivery.
Jobs, slots and opportunities
- Job: one broadcast of one transaction, from submission to its report. A job spends its first 12 s finding candidates (IPv4/IPv6 addresses from the DNS seeds, resolved through Tor exits), then delivers. It runs for at most 10 min.
- Slot: one of a job's 6 independent delivery targets. Each slot tries to announce the transaction to one peer and serve it when asked. Slots 0–2 open when delivery starts; slot 3 opens 35–180 s later; slots 4–5 open 185–240 s later, at least 5 s apart. Each of these times is drawn at random within its range.
- Opportunity: one of a slot's 4 scheduled connection times: a first attempt and up to 3 fallbacks, each 50–60 s after the previous. A fallback runs only if the slot's previous attempt failed before announcing; once a slot announces, its remaining opportunities are dropped.
The times are drawn at job start, and each opportunity's candidate is fixed when discovery ends, before any peer is contacted. Nothing a peer does moves them (C1–C4). A job makes at most 24 connections, at most 6 of them at once.
Differences at a glance
| master | this PR | |
|---|---|---|
| Where it runs | Inside bitcoind only |
bitcoind -privatebroadcast, or the standalone bitcoin-privbcast with no node; the same job either way (U3), with a JSON report of every connection |
| Recipients | addrman.Select() over onion, I2P, and IPv4/IPv6 through the proxy; networks follow -onlynet |
DNS seed names resolved through Tor exits (RESOLVE), IPv4/IPv6 only; no node state used (R1, R6) |
| Connections | CConnman peers of type PRIVATE_BROADCAST, visible in getpeerinfo |
The job's own proxy streams, each with fresh SOCKS credentials, so each gets its own circuit; not peers of the node |
| Plan | 3 connections, then more whenever the tx is stale (5 min, then 1 min) until it comes back from the network | 6 slots of 4 opportunities, all scheduled at job start; no retries; at most 10 min |
| Transport | v2 when addrman advertises it, otherwise v1, with a v2→v1 retry | BIP324 v2 only, with one constant VERSION per release (B4, E1) |
| Node settings | Shape which networks and peers are used | Peer-selection settings don't apply to jobs (U2) |
| Packages | — | submitpackage with one parent and one child: announce the child, serve the parent on request |
No addrman
Under master, the node's private broadcast destinations are drawn from the node's addrman. Other parties write to that table through addr gossip, which makes it a unique fingerprint vector, and inherently attacker influenced. This rules out drawing recipients from anything the node or its peers can write to.
Its replacement is querying DNS seeds in a predetermined way under a static profile. This is inherently less robust than the p2p network in terms of liveness, but leaves no fingerprint within a release. The marginal fingerprint is the set of DNS seeds per release, which affects who is queried and exactly how. Each RESOLVE returns one address and exits cache answers, so each seed is asked through several exits: 4 Tor circuits, each querying all 7 seeds, for up to 28 results, which are assigned to the 6x4 opportunities before any dialing happens. A circuit per query, 28 in all, kept Tor busy enough that overlapping discoveries lost answers.
If the list shrinks, grows, or is modified, the release is revealed to the Tor exits carrying the queries, which see every seed name queried. A DNS seed learns only that it is on the list, and only when the exit has no cached answer.
The fingerprint potential also precludes fixed seeds, which change greatly each release, and which degrade in connectivity over time regardless.
Minimal release fingerprint
Once a release is cut, behavior profiles are intended to be identical per user. This precludes user knobs and per-instance parameters, and requires a constant user agent and VERSION.
Across releases, the reimplemented p2p message state machine allows better control of unnecessary profile breaks going forward.
Specification
doc/design/private-broadcast-tool.md comes before any code in part 2. It has goals, a threat model per party (recipient, exit, seed, network observer, guard), 54 numbered invariants (A–U, N), parameters, interface, assumptions, non-goals and a rationale. Code comments and tests cite the invariant IDs, so review can check code against a stated property. Concept review of the goals, threat model and non-goals is most useful first, then review of the implementation against the invariants.
This implementation is actually the second implementation, initially implemented solely via the spec and black box tests, which were handed to an otherwise blind agent.
Tradeoffs
- Needs Tor, including Tor's RESOLVE extension. I2P is gone, and so are onion recipients. To re-add these, we need a connect->GETADDR step, and then have to filter out most of the addresses that do not answer when dialed.
- DNS seeds and Tor exits learn that a job ran, in addition to end recipients.
- No retries beyond the scheduled fallbacks. Retrying would act on whether the transaction comes back, which the adversary controls; the user resubmits instead.
getprivatebroadcastinfonow reports jobs, and the connections no longer show ingetpeerinfo.