{
 "number": 36608,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/36608",
 "title": "Reimplement private broadcast as node state-less jobs + package relay support",
 "author": "instagibbs",
 "author_association": "MEMBER",
 "created_at": "2026-10-22T14:47:27Z",
 "updated_at": "2026-10-28T16:41:10Z",
 "description_hash": "c7090180cd6e859a",
 "age_days": 10,
 "draft": true,
 "labels": [
  "Needs rebase",
  "Private Broadcast"
 ],
 "milestone": null,
 "base": "master",
 "head_sha": "eec78ccf7d59f5b39d4525980e8e3a7fe6c60242",
 "head_ref": "privbcast-4-package",
 "head_repo": "instagibbs/bitcoin",
 "head_history": [
  {
   "t": "2026-10-22T15:15:40Z",
   "sha": "463f9859bb70cfc4d64aff327e4d4f7582b77edf"
  },
  {
   "t": "2026-10-22T17:40:35Z",
   "sha": "eec78ccf7d59f5b39d4525980e8e3a7fe6c60242"
  }
 ],
 "additions": 17211,
 "deletions": 4442,
 "changed_files": 121,
 "commit_count": 35,
 "size_bucket": "XL",
 "mergeable_state": "dirty",
 "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": []
 },
 "signals": {
  "needs_rebase": true,
  "ci_failed": false,
  "mergeable_state": "dirty",
  "last_author_activity": "2026-10-23T16:46:02Z",
  "last_reviewer_activity": null,
  "last_reviewer": null,
  "author_silent_days": 9,
  "waiting_on_author_days": 0,
  "days_since_update": 4
 },
 "refs": {
  "mentioned": [
   35032,
   35286,
   35319,
   35410,
   36277,
   36312
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 35319,
    "type": "pull",
    "state": "closed",
    "merged": false,
    "merged_at": null,
    "title": "net: use the proxy if overriden when doing v2->v1 reconnections"
   },
   {
    "number": 35410,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-06-04",
    "title": "net: use the proxy if overriden when doing v2->v1 reconnections"
   },
   {
    "number": 36312,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-09-25",
    "title": "net: don't discourage private broadcast peers"
   },
   {
    "number": 36277,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-10-06",
    "title": "net: always complete all initial private broadcast connections"
   },
   {
    "number": 35032,
    "type": "pull",
    "state": "closed",
    "merged": true,
    "merged_at": "2026-04-09",
    "title": "net_processing: don't modify addrman for private broadcast connections"
   },
   {
    "number": 35286,
    "type": "pull",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "rpc: add testsubmitpackage for 1p1c test submissions"
   }
  ],
  "conflicts": []
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [],
 "body": "## Goals\n\nGoal 1: Remove reliance on, and probes about, internal node state from private broadcast\n\nGoal 2: Specify observable behavior to aid reimplementation and reduce profile fingerprints across versions\n\n### Layout\n\nFour parts, each starting with an empty marker commit:\n\n1. support: moves shared code into the common library; no behavior change\n2. bitcoin-privbcast: the specification and the standalone tool\n3. bitcoind -privatebroadcast as jobs: the node runs the tool's job; the old relay and connections are removed\n4. package mode: one parent and its child. Its validation commits (saturating fee sum, test-accept as submission, self-paying parent) serve only this part.\n\nFor 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.\n\nThe 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.\n\n## Node-less state\n\nThe 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:\n\n1. The v2->v1 retry connected directly, without the proxy: #35319 (merged in #35410)\n2. Discouraging a misbehaving peer gave a clearnet oracle: #36312\n3. Eager job completion via mempool inspection: #36277\n4. Private broadcast connections wrote to addrman: #35032\n\nThere 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:\n\n- A job reveals neither the sender's IP address, onion address nor node identity to any party.\n- A hostile recipient, DNS seed or Tor exit learns no more than an honest one. It can only affect delivery.\n\n### Jobs, slots and opportunities\n\n- **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.\n- **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\u20132 open when delivery starts; slot 3 opens 35\u2013180 s later; slots 4\u20135 open 185\u2013240 s later, at least 5 s apart. Each of these times is drawn at random within its range.\n- **Opportunity**: one of a slot's 4 scheduled connection times: a first attempt and up to 3 fallbacks, each 50\u201360 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.\n\nThe 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\u2013C4). A job makes at most 24 connections, at most 6 of them at once.\n\n### Differences at a glance\n\n| | master | this PR |\n|---|---|---|\n| 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 |\n| 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) |\n| 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 |\n| 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 |\n| Transport | v2 when addrman advertises it, otherwise v1, with a v2\u2192v1 retry | BIP324 v2 only, with one constant VERSION per release (B4, E1) |\n| Node settings | Shape which networks and peers are used | Peer-selection settings don't apply to jobs (U2) |\n| Packages | \u2014 | `submitpackage` with one parent and one child: announce the child, serve the parent on request |\n\n### No addrman\n\nUnder 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.\n\nIts 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.\n\nIf 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.\n\nThe fingerprint potential also precludes fixed seeds, which change greatly each release, and which degrade in connectivity over time regardless.\n\n### Minimal release fingerprint\n\nOnce 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.\n\nAcross releases, the reimplemented p2p message state machine allows better control of unnecessary profile breaks going forward.\n\n### Specification\n\n`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\u2013U, 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.\n\nThis implementation is actually the second implementation, initially implemented solely via the spec and black box tests, which were handed to an otherwise blind agent.\n\n### Tradeoffs\n\n- 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.\n- DNS seeds and Tor exits learn that a job ran, in addition to end recipients.\n- No retries beyond the scheduled fallbacks. Retrying would act on whether the transaction comes back, which the adversary controls; the user resubmits instead.\n- `getprivatebroadcastinfo` now reports jobs, and the connections no longer show in `getpeerinfo`.",
 "commits": [
  {
   "sha": "bcd21b632c93473afdd3539ec969637101f0fb6b",
   "date": "2026-10-22T13:09:32Z",
   "message": "--- part 1/4: support ---\n\nEmpty commit marking where the parts of this pull request begin. Part 1 moves code that bitcoin-privbcast shares with the node into the common library and exports what it needs from net and netbase. No behaviour change."
  },
  {
   "sha": "e003fd21436e5862f32f6b2a8c6412ead5a359c5",
   "date": "2026-10-22T13:09:32Z",
   "message": "build: move bip324 to the common library\n\nbitcoin-privbcast (added later in this series) links the BIP324 cipher\nwithout the node library. The cipher depends only on crypto, keys,\nrandomness and chain parameters, which the common library already\nprovides."
  },
  {
   "sha": "0582ee800b433b28671a241a926a64607913f577",
   "date": "2026-10-22T13:09:32Z",
   "message": "refactor: move the transports out of net.h\n\nThe transports and the messages they carry are needed by a tool that\nmust not link the node library (bitcoin-privbcast, added later in this\nseries). Move them, with NodeId and MAX_PROTOCOL_MESSAGE_LENGTH, to\nnet_transport.{h,cpp} in the common library. net.h includes the new\nheader and drops the includes that only the moved code used; files that\nname NodeId include the new header themselves.\n\nnet.h includes the new header first, where bip324.h was: Qt's moc reads\nnet.h through the GUI's headers and cannot parse libstdc++'s <concepts>\nunless an earlier header has defined _GLIBCXX_VISIBILITY.\n\nMove-only: no code changes."
  },
  {
   "sha": "ec74e9c2f41cd64752478189dd1b4ef980b76377",
   "date": "2026-10-22T13:09:32Z",
   "message": "netbase: export Socks5ErrorString"
  },
  {
   "sha": "e596fc3cc2f552d736206579a2cf2f5c76c06c04",
   "date": "2026-10-22T13:09:32Z",
   "message": "build: move package policy to the common library"
  },
  {
   "sha": "6a0027f99bfa87ff9f5b40bd136f3786458ae924",
   "date": "2026-10-22T13:09:32Z",
   "message": "--- part 2/4: bitcoin-privbcast ---\n\nEmpty commit marking the start of part 2: the specification and the standalone tool. Nothing in the node changes."
  },
  {
   "sha": "a083390b73e85302e0962f1bed4a959d6c6097fb",
   "date": "2026-10-22T14:58:12Z",
   "message": "doc: add the private broadcast specification\n\nThis is the design the following commits implement: what a private\nbroadcast job guarantees, the values peers and observers see, and the\ninterfaces of the tool and the node. Later commits cite its items by\nnumber."
  },
  {
   "sha": "c1d3eb83e0fc91709d7a895f2315f041843b7978",
   "date": "2026-10-22T14:58:12Z",
   "message": "privbcast: add the parameters and the schedule\n\nStart bitcoin_privbcast, the library that will run private broadcast\njobs for bitcoin-privbcast and the node. params.h holds the values of\nthe specification. DrawPlan draws a job's plan from the job's own\nrandomness: the schedule, the order of the DNS seeds and the seed of\nthe order in which each DNS seed hands out its candidates.\nMIN_DNS_SEEDS is the floor R3 sets for the mainnet seed list, and a\nunit test holds the list to it, so a release that drops seeds below\nwhat fills a job fails the test rather than leaving opportunities\nempty.\n\nImplements C1 and R3; the values of Parameters."
  },
  {
   "sha": "d638a035d07b306dc5450742fc5e787c875da08a",
   "date": "2026-10-22T14:58:12Z",
   "message": "privbcast: add candidate assignment\n\nAssign fixes every opportunity's candidate once discovery ends, from\nthe plan and the discovery result alone. An endpoint that several\nseeds returned is credited to the seed that comes first in the plan's\nseed order, through the lowest query set that returned it. Each seed\nhands out its candidates in an order drawn at random by a generator of\nits own, seeded from the plan and the seed's place in that order. Slot\nj's first attempt comes from query set j mod 4 whenever a seed it may\ndraw from has one, so a single exit supplies at most two first\nattempts. Neither which seed answers first nor the order of a seed's\nanswers changes the assignment.\n\nImplements C2, R2, R4, R5a, R5b and R5c."
  },
  {
   "sha": "19844ed5af2443693bbbc7e3cacd2c94ac0ef1bc",
   "date": "2026-10-22T14:58:12Z",
   "message": "privbcast: add a byte-driven SOCKS5 client\n\nA job reaches recipients and resolves DNS seed names through the proxy,\non streams its event loop drives without blocking. Socks5Client holds\nno socket: it offers the bytes to write, consumes the proxy's replies,\nand leaves bytes past the final reply to the P2P transport. It offers\nonly username and password authentication, with the credentials it is\ngiven or fresh ones, and sends a numeric address as IPV4 or IPV6\nwithout resolving anything.\n\nImplements B2 and the Proxy interface section."
  },
  {
   "sha": "655553d8dc292520bbe764bda4a09f56fa8f213d",
   "date": "2026-10-22T14:58:12Z",
   "message": "privbcast: add the attempt state machine\n\nAn attempt is one connection to a recipient once the proxy connected\nit: a BIP324 session as initiator, the handshake with the job's\nVERSION, one INV, the transaction served at most once, and the PING.\nIt holds no socket and reads no clock: the job's loop passes the bytes\nread and the time; each call records what happened (a completed write,\nthe connection, bytes received) and then enforces the attempt's\ndeadlines, so that nothing is acted on at or after one.\n\nThe attempt draws its VERSION and PING nonces from the random context\nit is given, and takes its BIP324 key, ellswift entropy and garbage\nfrom a key source. Without one, as in production, V2Transport draws\nthem from the strong RNG. The tests and the fuzz target inject a\nsource that draws from their seeded context, so that an attempt's\nbytes are a function of that context and of what the peer sent.\n\nImplements E1-E8, A2, B4, D3 and the attempt part of D2."
  },
  {
   "sha": "7da6cfd2cc6a054acc6456cddf4761bdca65ca56",
   "date": "2026-10-22T14:58:12Z",
   "message": "privbcast: add proxy streams and discovery\n\nA job reaches the network only through streams to the SOCKS5 proxy,\nwhich its event loop drives without blocking. ProxyStream connects to\nthe proxy by address or unix socket path, resolving nothing, and runs\nthe exchange of its Socks5Client; each stage, the connect and each\nstep of the exchange, times out on the steady clock. Once the proxy\nhas connected the stream, its socket carries the destination's bytes.\n\nDiscovery launches every RESOLVE query at job start, each on a stream\nof its own, in four query sets: each set has credentials of its own\nand one query of every seed, so Tor builds four circuits rather than\none per query, and each seed is still asked through four exits.\nDiscovery ends when every query has ended or when the discovery window\ndoes, whatever is still pending. It keeps the answers that are public\nroutable IPv4 or IPv6 addresses, with the chain's default port, and\ncounts the others; its result gives each seed's answers by the query\nset they came through, whatever order they arrived in. An exception\nthrown while it acts on one query ends only that query, as a failed\nstream would.\n\nThe tests run discovery against a scripted proxy behind fake sockets\nthat connect as non-blocking sockets do. The fake socket and the\nproxy's side of the SOCKS5 exchange go into the test utilities, as the\njob's tests use them too.\n\nImplements B1, B2, C3, D1, R1, R2 and C7 for the queries."
  },
  {
   "sha": "28926fa818ec04da1697801f5fba818a521d366e",
   "date": "2026-10-22T14:58:12Z",
   "message": "privbcast: add the job\n\nA job is one event loop on its caller's thread. Step() runs the\ndeadlines and the dials that are due, waits on every open stream until\nthe next deadline, and does the I/O; Run() steps to the end. The plan\nis drawn when the job is made, before any socket exists, and discovery\nstarts with the first step. When discovery ends the assignment is\nfixed, and each slot dials its opportunities at their scheduled times,\none attempt at a time, until one reaches the announcement point or\nnone is left. The slots share only the loop, so a stalled stream holds\nup no other slot, and the cancel flag ends everything at the next step.\n\nAn exception thrown while the loop acts on one attempt ends only that\nattempt, as a failed stream would. One thrown anywhere else in a step\nfails the job: it stops as a cancelled job does, and its report gives\nthe error.\n\nThe report mirrors Interface/Report, every time an offset from job\nstart on the plan clock. Snapshot() gives the progress the node is to\nshow, from any thread. ExitStatus() gives the tool's exit status for a\nreport.\n\nImplements A1, A3, A4, C1-C8, D1, D2, H1, H3 and the report interface."
  },
  {
   "sha": "65da100adb631e3020000ca776f26d0003180dbf",
   "date": "2026-10-22T14:58:12Z",
   "message": "test: extend the harness for bitcoin-privbcast\n\nThe SOCKS5 server gains Tor's RESOLVE command, reply delays for a slow\nor stalled proxy, a mode that offers only unauthenticated access and a\nmode that behaves as an ordinary non-Tor proxy. A client that hangs up\nbefore its greeting, as a cancelled job's streams can, and a connection\nthat Windows reports as aborted count as closed, not as errors. The P2P\nnetwork thread judges a failed protocol by its own reconnect flag, so\nthat several listeners can share one loop. The framework learns where\nbitcoin-privbcast is and how to skip a test without it.\n\nPrepares the functional test that the next commit adds with the binary."
  },
  {
   "sha": "9f8105a33caa24f271a8bb1a522631e15c9c1301",
   "date": "2026-10-22T14:58:13Z",
   "message": "privbcast: add bitcoin-privbcast\n\nThe standalone tool: it reads one transaction on stdin, runs one job\nthrough the Tor proxy named by -tor and prints the report on stdout,\nwith progress lines on stderr. It takes no configuration file and only\na loopback proxy, refuses the regtest overrides on other chains and\nchecks its arguments and input before any network activity. The\ndiscover command runs discovery alone.\n\nBuild and packaging: the BUILD_PRIVBCAST option (on with BUILD_TESTS),\nthe privbcast subcommand of the bitcoin wrapper, the man page, the\nWindows resource file, the manpage generator and Windows CI lists, and\nthe test configuration that tells the functional framework the binary\nexists.\n\nThe functional tests tool_privbcast.py, tool_privbcast_proxy.py and\ntool_privbcast_peers.py, with their harness in\ntest_framework/privbcast.py, are the specification's black-box\nacceptance suite for the tool: scripted recipients and a scripted proxy\ncheck every wire-visible parameter, the schedule, the assignment, the\nexit statuses and cancellation.\n\nImplements B3, H2, U1 and the Interface section for bitcoin-privbcast."
  },
  {
   "sha": "ca786cc7b90b04965b90bd18653da4d2a0dc1f68",
   "date": "2026-10-22T14:58:13Z",
   "message": "doc: describe bitcoin-privbcast\n\nAdd the user guide to private broadcast with its bitcoin-privbcast\npart: the Tor proxy the tool needs, how to send a transaction and read\nthe exit status and the report, discover, and the limits of what a job\nprotects."
  },
  {
   "sha": "33fe47c1f7041dc1821a3ea3f8f2ce30de0fae25",
   "date": "2026-10-22T14:58:13Z",
   "message": "doc: release notes for bitcoin-privbcast"
  },
  {
   "sha": "64f1f264543e10d961301c516ae08daa1b663898",
   "date": "2026-10-22T14:58:13Z",
   "message": "--- part 3/4: bitcoind -privatebroadcast as jobs ---\n\nEmpty commit marking the start of part 3: the node runs each private broadcast as the tool's job, and the old relay and connections are removed."
  },
  {
   "sha": "71ea14ea32005875e53e172ef00fe22ed2a249ea",
   "date": "2026-10-22T14:58:13Z",
   "message": "node: add the private broadcast job queue\n\nThe node runs each private broadcast as a privbcast::Job, the tool's\njob, on a thread of its own. PrivbcastQueue queues the submitted\ntransactions, starts and cancels the jobs from one scheduler thread,\nand keeps in memory the records that getprivatebroadcastinfo and\nabortprivatebroadcast are to show. It reaches the node only through\ncallbacks for networking, the onion proxy and the mempool, and takes\nthe job runner as a parameter, so that the tests drive it with stub\njobs and mocked clocks. A job that failed, or whose runner threw, ends\naborted with the error in its record.\n\nThe scheduler's start rule is a pure function, StartPolicy::Decide(),\nwhich each pass of the scheduler applies: a fuzz target checks the\nrule on its own, and another the queue through its interface.\n\nbitcoin_node now links bitcoin_privbcast.\n\nImplements N3-N7, N9, N10, the node's D2 and D3, U3. Not yet wired:\nthe next commit switches -privatebroadcast to it."
  },
  {
   "sha": "8bd029b720f97c27e1f4e4270cbd41ce9ddcf2b0",
   "date": "2026-10-22T14:58:13Z",
   "message": "node: run private broadcast as jobs\n\nWith -privatebroadcast, sendrawtransaction now queues a job on the\nnode's PrivbcastQueue instead of handing the transaction to the\nconnection manager, getprivatebroadcastinfo lists the jobs with their\nprogress and reports, and abortprivatebroadcast aborts them. Startup\nrequires onion to be able to become reachable, refuses the regtest\nseed overrides on other chains, -signetseednode, -signetchallenge, and\noff regtest a non-zero -mocktime, and refuses to start rather than trim\nconnections when the jobs' file descriptors are not available. -connect\nand -proxyrandomize no longer matter.\n\nThe old private broadcast connections and relay stay in the tree, no\nlonger fed, until the commits that remove them.\n\nImplements N1, N2, N8, N10, N11, U1, U2 and Interface/Node; replaces\nthe old implementation's tests with the specification's."
  },
  {
   "sha": "e851f4d978601505d6c5b6e7d8f203a4e6ddebe0",
   "date": "2026-10-22T14:58:13Z",
   "message": "net: remove the old private broadcast relay\n\nSince -privatebroadcast runs jobs, nothing hands PeerManager a\ntransaction for private broadcast any more. Remove its queue\n(src/private_broadcast.{h,cpp}) with the methods, the retry task and\nthe private_broadcast option that served it, and its handling of\nprivate broadcast connections: the fixed VERSION, the INV after\nVERACK, the GETDATA and PONG handling, the incoming message filter,\nthe lifetime limit and the exemption from discouragement. The unit and\nfuzz tests of that code go with it, including the net_tests case for\nthe addrman exception and the denialofservice_tests cases for the\ndiscouragement exemption.\n\nDelete-only: besides whole lines, conditions and arguments are cut out\nof lines, and the lines of a removed branch's alternative move out one\nindentation level. git show -w --word-diff-regex='[A-Za-z0-9_]+|[^[:space:]]'\nshows deletions only, apart from the size of the node array in\ndenialofservice_tests, which goes back from 5 to 3."
  },
  {
   "sha": "c39973a4df47f2516c843e84b757d9f18a19627c",
   "date": "2026-10-22T14:58:13Z",
   "message": "net: remove the old private broadcast connections\n\nWith the relay gone, nothing asks CConnman for private broadcast\nconnections. Remove the connection type, the thread that opened such\nconnections with its counters and choice of network, and the places\nthat treated them specially: the incoming nonce check, the fresh I2P\nsession, the guard against direct connections, the outbound message\nfilter, the fixed protocol version, the exception in disconnection by\nsubnet and their term in the orphanage's static_assert. The type's\nnames go from getpeerinfo's help, bitcoin-cli -netinfo and the GUI. The\nproxy override of ConnectNode() and OpenNetworkConnection(), which only\nthat thread set, goes too, with its copy in CNode and in the\nreconnection record and the fuzz input for it.\n\nDelete-only: besides whole lines, conditions, arguments and list\nseparators are cut out of lines, what followed a cut argument joins\nits line, and one line moves out an indentation level with the if\naround it. git show -w --word-diff-regex='[A-Za-z0-9_]+|[^[:space:]]'\nshows deletions only."
  },
  {
   "sha": "fded8bbf39de5fd25540977338d80d0bded456dd",
   "date": "2026-10-22T14:58:13Z",
   "message": "refactor: drop the trivial AdvertisedVersion()\n\nWithout private broadcast connections, a node advertises\nPROTOCOL_VERSION on every connection, and CNode::AdvertisedVersion()\nonly returns it. Use the constant again where the method was called."
  },
  {
   "sha": "c5ac672d8db015bd909cd105628c724e2cce2153",
   "date": "2026-10-22T14:58:13Z",
   "message": "doc: describe bitcoind -privatebroadcast\n\nAdd the node part of the private broadcast guide: which Tor proxy the\njobs use, how sendrawtransaction queues them and when they start, what\nis not queued again, how to follow and abort jobs, what disabling\nnetworking and shutdown do, which node settings do not apply, what\nthe default log shows, the file descriptors the jobs need, and the\nlink between a job's connections and the node's peers at shutdown."
  },
  {
   "sha": "26e3a21e1c3b360b2e484dde775192ff43b9976a",
   "date": "2026-10-22T14:58:13Z",
   "message": "doc: release notes for private broadcast jobs"
  },
  {
   "sha": "a80a9d414ade037764901e6a4b5f80e29c135030",
   "date": "2026-10-22T14:58:13Z",
   "message": "--- part 4/4: package mode ---\n\nEmpty commit marking the start of part 4: one parent and its child. The validation commits here serve only this part."
  },
  {
   "sha": "d9f4c35cc358f632d824cd5737ffe4dbad942b26",
   "date": "2026-10-22T14:58:13Z",
   "message": "privbcast: serve a parent in package mode\n\nA child can pay for a parent too cheap to enter mempools alone when the\nrecipient takes the two together. A job may now carry the parent: each\nattempt announces only the child, serves the parent to a peer that asks\nfor it, and holds the PING for that request. A job without a parent\nruns and reports as before.\n\nImplements F1-F4 and the package amendments of E6-E8."
  },
  {
   "sha": "be1433a1bd61463f1badbfef45890bffb37760a8",
   "date": "2026-10-22T15:15:19Z",
   "message": "privbcast: accept a parent and child in the tool\n\nbitcoin-privbcast send also reads two transactions, an unconfirmed\nparent and its child in either order, checks that they are such a pair\nand runs the job in package mode. The acceptance suite gains its\npackage sections in tool_privbcast_package.py.\n\nImplements the Extension's Interface for bitcoin-privbcast."
  },
  {
   "sha": "ee5719d0c628f877889f2e5af4c1d9df38159c04",
   "date": "2026-10-22T15:15:19Z",
   "message": "validation: sum a package's modified fees saturating\n\nTwo prioritisetransaction deltas near INT64_MIN made the sum overflow."
  },
  {
   "sha": "76797040c201c3c18d5323ce1f616f44ebaadff6",
   "date": "2026-10-22T15:15:19Z",
   "message": "validation: test-accept a package as submission evaluates it\n\nProcessNewPackage's test mode checks each transaction without package\nfeerates or replacements, as testmempoolaccept does. With\ntest_as_submission it runs submission's evaluation instead and stops\nshort of the mempool, so a caller can learn whether a child pays for its\nparent before relaying them. A transaction that passed before another\nfailed is reported by its own result, as submission reports it. Results\ndo not list replaced transactions, and a parent that passes alone gives\nno answer for its child, which it does not stage.\n\nFrom #35286, without its RPC."
  },
  {
   "sha": "d27d450877a768969263bed5a477b8f8a7fcb8b6",
   "date": "2026-10-22T15:15:19Z",
   "message": "validation: test a package whose parent passes alone\n\nA test does not stage a transaction that passes alone, so the child\nthat spends it was found missing its inputs. Test the new transactions\ntogether instead, each paying for itself. Their replacements get\npackage RBF's checks of fees, feerate diagram and cluster limits, but\nnot its rules that the package have no mempool ancestors and pay more\nper vbyte than the parent: submission would take the two in turn, each\nreplacing what it may. A replacement of an ancestor of the package is\nrefused instead, as it would take away the package's inputs. For a\nparent and child this accepts at least what submission accepts."
  },
  {
   "sha": "079099f07972745a464fbc97715314fe12ed3177",
   "date": "2026-10-22T17:40:02Z",
   "message": "rpc: queue submitpackage for private broadcast\n\nWith -privatebroadcast, submitpackage now queues a private broadcast\njob instead of adding the transactions to the mempool. It takes one\ntransaction, or a parent and its child, and queues one job, the\nchild's, which carries the parent. A transaction whose txid is in the\nmempool counts as accepted; the rest is test-accepted, one transaction\nalone as by sendrawtransaction, two as a package, after the package\nchecks on the whole submission, two as submitpackage evaluates them.\nmaxfeerate follows each path's own convention. Unit tests drive the RPC through its table.\n\nThe queue takes the parent with the transaction: a submission with a\nparent is covered only by a job with the same parent, one without by a\njob with any parent. getprivatebroadcastinfo lists the parent's txid\nand documents the report's package fields; abortprivatebroadcast\nmatches the child's ids only. Both submission RPCs ask the queue\nwhether an onion proxy is configured, which lets unit tests drive\nthem without the process-wide proxy registry. The acceptance suite gains its package\nsections, run as p2p_private_broadcast.py --package.\n\nImplements the Extension's N5 and N11 amendments and its Interface for\nsubmitpackage."
  },
  {
   "sha": "30d5ea00b5c74bc4f4a41d67819274d9bb629f2e",
   "date": "2026-10-22T17:40:02Z",
   "message": "doc: specify package mode\n\nAdd the specification's section \"Extension: one parent, one child\",\nwhich the preceding commits implement: a job that carries a child and\nits unconfirmed parent announces only the child and serves the parent\non request (F1-F4), and the tool and the node take such a pair."
  },
  {
   "sha": "7f4e53befc01cd5da17be4d6713a61b5351d7916",
   "date": "2026-10-22T17:40:02Z",
   "message": "doc: describe package mode\n\nAdd the guide's part on a parent and its child: when it helps, which\nrecipients accept such a package, what the tool and the node check and\nwhat peers can tell; and how submitpackage queues a job under\n-privatebroadcast."
  },
  {
   "sha": "eec78ccf7d59f5b39d4525980e8e3a7fe6c60242",
   "date": "2026-10-22T17:40:02Z",
   "message": "doc: release notes for package mode"
  }
 ],
 "timeline": [
  {
   "t": "2026-10-22T15:15:40Z",
   "kind": "force_push",
   "who": "instagibbs",
   "commit": "463f9859bb70cfc4d64aff327e4d4f7582b77edf",
   "id": "push:463f9859bb70cfc4d64aff327e4d4f7582b77edf"
  },
  {
   "t": "2026-10-22T17:40:35Z",
   "kind": "force_push",
   "who": "instagibbs",
   "commit": "eec78ccf7d59f5b39d4525980e8e3a7fe6c60242",
   "id": "push:eec78ccf7d59f5b39d4525980e8e3a7fe6c60242"
  },
  {
   "t": "2026-10-23T16:46:02Z",
   "kind": "comment",
   "who": "instagibbs",
   "assoc": "MEMBER",
   "text": "in draft, https://github.com/bitcoin/bitcoin/pull/36628 is the slimmed down branch to review",
   "id": "c:6369998981",
   "url": "https://github.com/bitcoin/bitcoin/pull/36608#issuecomment-6369998981",
   "hash": "f3f9a5798c064f40",
   "edited": null
  }
 ],
 "labels_log": [
  {
   "t": "2026-10-22T15:17:48Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-10-22T16:23:41Z",
   "action": "labeled",
   "label": "Private Broadcast",
   "who": "fanquake"
  },
  {
   "t": "2026-10-22T19:41:30Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-10-28T16:41:09Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  }
 ],
 "state_log": [
  {
   "t": "2026-10-23T16:37:39Z",
   "kind": "convert_to_draft",
   "who": "instagibbs"
  }
 ],
 "text_chars": 25682,
 "text_tokens_estimate": 6420,
 "changed_paths": [
  ".github/ci-windows.py",
  "CMakeLists.txt",
  "cmake/module/Maintenance.cmake",
  "contrib/devtools/gen-manpages.py",
  "doc/README.md",
  "doc/design/private-broadcast-tool.md",
  "doc/man/bitcoin-privbcast.1",
  "doc/private-broadcast.md",
  "doc/release-notes-privbcast-node.md",
  "doc/release-notes-privbcast-package.md",
  "doc/release-notes-privbcast-tool.md",
  "src/CMakeLists.txt",
  "src/bench/txorphanage.cpp",
  "src/bitcoin-cli.cpp",
  "src/bitcoin-privbcast-res.rc",
  "src/bitcoin-privbcast.cpp",
  "src/bitcoin.cpp",
  "src/common/messages.cpp",
  "src/init.cpp",
  "src/interfaces/node.h",
  "src/kernel/chainparams.cpp",
  "src/net.cpp",
  "src/net.h",
  "src/net_processing.cpp",
  "src/net_processing.h",
  "src/net_transport.cpp",
  "src/net_transport.h",
  "src/netbase.cpp",
  "src/netbase.h",
  "src/netmessagemaker.h",
  "src/node/connection_types.cpp",
  "src/node/connection_types.h",
  "src/node/context.cpp",
  "src/node/context.h",
  "src/node/interfaces.cpp",
  "src/node/peerman_args.cpp",
  "src/node/privbcast.cpp",
  "src/node/privbcast.h",
  "src/node/transaction.cpp",
  "src/node/transaction.h",
  "src/node/txorphanage.h",
  "src/node/types.h",
  "src/primitives/transaction.h",
  "src/private_broadcast.cpp",
  "src/private_broadcast.h",
  "src/privbcast/CMakeLists.txt",
  "src/privbcast/assign.cpp",
  "src/privbcast/assign.h",
  "src/privbcast/attempt.cpp",
  "src/privbcast/attempt.h",
  "src/privbcast/discovery.cpp",
  "src/privbcast/discovery.h",
  "src/privbcast/input.cpp",
  "src/privbcast/input.h",
  "src/privbcast/job.cpp",
  "src/privbcast/job.h",
  "src/privbcast/params.h",
  "src/privbcast/plan.cpp",
  "src/privbcast/plan.h",
  "src/privbcast/report.cpp",
  "src/privbcast/report.h",
  "src/privbcast/socks5.cpp",
  "src/privbcast/socks5.h",
  "src/privbcast/stream.cpp",
  "src/privbcast/stream.h",
  "src/qt/guiutil.cpp",
  "src/qt/rpcconsole.cpp",
  "src/rpc/blockchain.cpp",
  "src/rpc/mempool.cpp",
  "src/rpc/net.cpp",
  "src/rpc/util.cpp",
  "src/test/CMakeLists.txt",
  "src/test/denialofservice_tests.cpp",
  "src/test/fuzz/CMakeLists.txt",
  "src/test/fuzz/connman.cpp",
  "src/test/fuzz/p2p_private_broadcast.cpp",
  "src/test/fuzz/p2p_transport_serialization.cpp",
  "src/test/fuzz/package_eval.cpp",
  "src/test/fuzz/private_broadcast.cpp",
  "src/test/fuzz/privbcast_attempt.cpp",
  "src/test/fuzz/privbcast_discovery.cpp",
  "src/test/fuzz/privbcast_input.cpp",
  "src/test/fuzz/privbcast_queue.cpp",
  "src/test/fuzz/privbcast_socks5.cpp",
  "src/test/net_tests.cpp",
  "src/test/private_broadcast_tests.cpp",
  "src/test/privbcast_assign_tests.cpp",
  "src/test/privbcast_attempt_tests.cpp",
  "src/test/privbcast_discovery_tests.cpp",
  "src/test/privbcast_input_tests.cpp",
  "src/test/privbcast_job_tests.cpp",
  "src/test/privbcast_plan_tests.cpp",
  "src/test/privbcast_queue_tests.cpp",
  "src/test/privbcast_rpc_tests.cpp",
  "src/test/privbcast_socks5_tests.cpp",
  "src/test/txpackage_tests.cpp",
  "src/test/util/net.cpp",
  "src/test/util/net.h",
  "src/test/util/privbcast.h",
  "src/test/util/privbcast_queue.h",
  "src/validation.cpp",
  "src/validation.h",
  "test/CMakeLists.txt",
  "test/config.ini.in",
  "test/functional/feature_config_args.py",
  "test/functional/feature_init.py",
  "test/functional/feature_signet.py",
  "test/functional/p2p_private_broadcast.py",
  "test/functional/p2p_private_broadcast_cap.py",
  "test/functional/p2p_private_broadcast_completion.py",
  "test/functional/p2p_private_broadcast_retry_v1.py",
  "test/functional/test_framework/p2p.py",
  "test/functional/test_framework/privbcast.py",
  "test/functional/test_framework/socks5.py",
  "test/functional/test_framework/test_framework.py",
  "test/functional/test_framework/util.py",
  "test/functional/test_runner.py",
  "test/functional/tool_privbcast.py",
  "test/functional/tool_privbcast_package.py",
  "test/functional/tool_privbcast_peers.py",
  "test/functional/tool_privbcast_proxy.py"
 ],
 "files": [
  {
   "path": ".github/ci-windows.py",
   "add": 1,
   "del": 0
  },
  {
   "path": "CMakeLists.txt",
   "add": 3,
   "del": 0
  },
  {
   "path": "cmake/module/Maintenance.cmake",
   "add": 2,
   "del": 1
  },
  {
   "path": "contrib/devtools/gen-manpages.py",
   "add": 1,
   "del": 0
  },
  {
   "path": "doc/README.md",
   "add": 1,
   "del": 0
  },
  {
   "path": "doc/design/private-broadcast-tool.md",
   "add": 589,
   "del": 0
  },
  {
   "path": "doc/man/bitcoin-privbcast.1",
   "add": 5,
   "del": 0
  },
  {
   "path": "doc/private-broadcast.md",
   "add": 137,
   "del": 0
  },
  {
   "path": "doc/release-notes-privbcast-node.md",
   "add": 58,
   "del": 0
  },
  {
   "path": "doc/release-notes-privbcast-package.md",
   "add": 36,
   "del": 0
  },
  {
   "path": "doc/release-notes-privbcast-tool.md",
   "add": 22,
   "del": 0
  },
  {
   "path": "src/CMakeLists.txt",
   "add": 24,
   "del": 3
  },
  {
   "path": "src/bench/txorphanage.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/bitcoin-cli.cpp",
   "add": 0,
   "del": 2
  },
  {
   "path": "src/bitcoin-privbcast-res.rc",
   "add": 33,
   "del": 0
  },
  {
   "path": "src/bitcoin-privbcast.cpp",
   "add": 353,
   "del": 0
  },
  {
   "path": "src/bitcoin.cpp",
   "add": 3,
   "del": 0
  },
  {
   "path": "src/common/messages.cpp",
   "add": 4,
   "del": 0
  },
  {
   "path": "src/init.cpp",
   "add": 55,
   "del": 30
  },
  {
   "path": "src/interfaces/node.h",
   "add": 1,
   "del": 0
  },
  {
   "path": "src/kernel/chainparams.cpp",
   "add": 2,
   "del": 0
  },
  {
   "path": "src/net.cpp",
   "add": 16,
   "del": 1113
  },
  {
   "path": "src/net.h",
   "add": 7,
   "del": 567
  },
  {
   "path": "src/net_processing.cpp",
   "add": 15,
   "del": 251
  },
  {
   "path": "src/net_processing.h",
   "add": 0,
   "del": 29
  },
  {
   "path": "src/net_transport.cpp",
   "add": 917,
   "del": 0
  },
  {
   "path": "src/net_transport.h",
   "add": 495,
   "del": 0
  },
  {
   "path": "src/netbase.cpp",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/netbase.h",
   "add": 3,
   "del": 0
  },
  {
   "path": "src/netmessagemaker.h",
   "add": 2,
   "del": 1
  },
  {
   "path": "src/node/connection_types.cpp",
   "add": 0,
   "del": 2
  },
  {
   "path": "src/node/connection_types.h",
   "add": 0,
   "del": 7
  },
  {
   "path": "src/node/context.cpp",
   "add": 1,
   "del": 0
  },
  {
   "path": "src/node/context.h",
   "add": 3,
   "del": 0
  },
  {
   "path": "src/node/interfaces.cpp",
   "add": 1,
   "del": 0
  },
  {
   "path": "src/node/peerman_args.cpp",
   "add": 0,
   "del": 2
  },
  {
   "path": "src/node/privbcast.cpp",
   "add": 464,
   "del": 0
  },
  {
   "path": "src/node/privbcast.h",
   "add": 310,
   "del": 0
  },
  {
   "path": "src/node/transaction.cpp",
   "add": 21,
   "del": 1
  },
  {
   "path": "src/node/transaction.h",
   "add": 13,
   "del": 0
  },
  {
   "path": "src/node/txorphanage.h",
   "add": 1,
   "del": 1
  },
  {
   "path": "src/node/types.h",
   "add": 4,
   "del": 2
  },
  {
   "path": "src/primitives/transaction.h",
   "add": 1,
   "del": 2
  },
  {
   "path": "src/private_broadcast.cpp",
   "add": 0,
   "del": 209
  },
  {
   "path": "src/private_broadcast.h",
   "add": 0,
   "del": 271
  },
  {
   "path": "src/privbcast/CMakeLists.txt",
   "add": 24,
   "del": 0
  },
  {
   "path": "src/privbcast/assign.cpp",
   "add": 106,
   "del": 0
  },
  {
   "path": "src/privbcast/assign.h",
   "add": 104,
   "del": 0
  },
  {
   "path": "src/privbcast/attempt.cpp",
   "add": 400,
   "del": 0
  },
  {
   "path": "src/privbcast/attempt.h",
   "add": 232,
   "del": 0
  },
  {
   "path": "src/privbcast/discovery.cpp",
   "add": 171,
   "del": 0
  },
  {
   "path": "src/privbcast/discovery.h",
   "add": 101,
   "del": 0
  },
  {
   "path": "src/privbcast/input.cpp",
   "add": 198,
   "del": 0
  },
  {
   "path": "src/privbcast/input.h",
   "add": 94,
   "del": 0
  },
  {
   "path": "src/privbcast/job.cpp",
   "add": 504,
   "del": 0
  },
  {
   "path": "src/privbcast/job.h",
   "add": 215,
   "del": 0
  },
  {
   "path": "src/privbcast/params.h",
   "add": 153,
   "del": 0
  },
  {
   "path": "src/privbcast/plan.cpp",
   "add": 75,
   "del": 0
  },
  {
   "path": "src/privbcast/plan.h",
   "add": 53,
   "del": 0
  },
  {
   "path": "src/privbcast/report.cpp",
   "add": 171,
   "del": 0
  },
  {
   "path": "src/privbcast/report.h",
   "add": 118,
   "del": 0
  },
  {
   "path": "src/privbcast/socks5.cpp",
   "add": 204,
   "del": 0
  },
  {
   "path": "src/privbcast/socks5.h",
   "add": 115,
   "del": 0
  },
  {
   "path": "src/privbcast/stream.cpp",
   "add": 234,
   "del": 0
  },
  {
   "path": "src/privbcast/stream.h",
   "add": 111,
   "del": 0
  },
  {
   "path": "src/qt/guiutil.cpp",
   "add": 0,
   "del": 2
  },
  {
   "path": "src/qt/rpcconsole.cpp",
   "add": 1,
   "del": 4
  },
  {
   "path": "src/rpc/blockchain.cpp",
   "add": 1,
   "del": 0
  },
  {
   "path": "src/rpc/mempool.cpp",
   "add": 320,
   "del": 83
  },
  {
   "path": "src/rpc/net.cpp",
   "add": 3,
   "del": 4
  },
  {
   "path": "src/rpc/util.cpp",
   "add": 2,
   "del": 0
  },
  {
   "path": "src/test/CMakeLists.txt",
   "add": 10,
   "del": 1
  },
  {
   "path": "src/test/denialofservice_tests.cpp",
   "add": 1,
   "del": 34
  },
  {
   "path": "src/test/fuzz/CMakeLists.txt",
   "add": 6,
   "del": 2
  },
  {
   "path": "src/test/fuzz/connman.cpp",
   "add": 1,
   "del": 7
  },
  {
   "path": "src/test/fuzz/p2p_private_broadcast.cpp",
   "add": 0,
   "del": 258
  },
  {
   "path": "src/test/fuzz/p2p_transport_serialization.cpp",
   "add": 2,
   "del": 1
  },
  {
   "path": "src/test/fuzz/package_eval.cpp",
   "add": 44,
   "del": 0
  },
  {
   "path": "src/test/fuzz/private_broadcast.cpp",
   "add": 0,
   "del": 250
  },
  {
   "path": "src/test/fuzz/privbcast_attempt.cpp",
   "add": 513,
   "del": 0
  },
  {
   "path": "src/test/fuzz/privbcast_discovery.cpp",
   "add": 462,
   "del": 0
  },
  {
   "path": "src/test/fuzz/privbcast_input.cpp",
   "add": 97,
   "del": 0
  },
  {
   "path": "src/test/fuzz/privbcast_queue.cpp",
   "add": 712,
   "del": 0
  },
  {
   "path": "src/test/fuzz/privbcast_socks5.cpp",
   "add": 223,
   "del": 0
  },
  {
   "path": "src/test/net_tests.cpp",
   "add": 0,
   "del": 30
  },
  {
   "path": "src/test/private_broadcast_tests.cpp",
   "add": 0,
   "del": 334
  },
  {
   "path": "src/test/privbcast_assign_tests.cpp",
   "add": 233,
   "del": 0
  },
  {
   "path": "src/test/privbcast_attempt_tests.cpp",
   "add": 824,
   "del": 0
  },
  {
   "path": "src/test/privbcast_discovery_tests.cpp",
   "add": 316,
   "del": 0
  },
  {
   "path": "src/test/privbcast_input_tests.cpp",
   "add": 511,
   "del": 0
  },
  {
   "path": "src/test/privbcast_job_tests.cpp",
   "add": 1413,
   "del": 0
  },
  {
   "path": "src/test/privbcast_plan_tests.cpp",
   "add": 185,
   "del": 0
  },
  {
   "path": "src/test/privbcast_queue_tests.cpp",
   "add": 698,
   "del": 0
  },
  {
   "path": "src/test/privbcast_rpc_tests.cpp",
   "add": 361,
   "del": 0
  },
  {
   "path": "src/test/privbcast_socks5_tests.cpp",
   "add": 178,
   "del": 0
  },
  {
   "path": "src/test/txpackage_tests.cpp",
   "add": 109,
   "del": 0
  },
  {
   "path": "src/test/util/net.cpp",
   "add": 209,
   "del": 4
  },
  {
   "path": "src/test/util/net.h",
   "add": 103,
   "del": 1
  },
  {
   "path": "src/test/util/privbcast.h",
   "add": 75,
   "del": 0
  },
  {
   "path": "src/test/util/privbcast_queue.h",
   "add": 29,
   "del": 0
  },
  {
   "path": "src/validation.cpp",
   "add": 83,
   "del": 20
  },
  {
   "path": "src/validation.h",
   "add": 6,
   "del": 1
  },
  {
   "path": "test/CMakeLists.txt",
   "add": 1,
   "del": 0
  },
  {
   "path": "test/config.ini.in",
   "add": 1,
   "del": 0
  },
  {
   "path": "test/functional/feature_config_args.py",
   "add": 17,
   "del": 16
  },
  {
   "path": "test/functional/feature_init.py",
   "add": 6,
   "del": 0
  },
  {
   "path": "test/functional/feature_signet.py",
   "add": 9,
   "del": 0
  },
  {
   "path": "test/functional/p2p_private_broadcast.py",
   "add": 761,
   "del": 388
  },
  {
   "path": "test/functional/p2p_private_broadcast_cap.py",
   "add": 43,
   "del": 72
  },
  {
   "path": "test/functional/p2p_private_broadcast_completion.py",
   "add": 0,
   "del": 99
  },
  {
   "path": "test/functional/p2p_private_broadcast_retry_v1.py",
   "add": 0,
   "del": 199
  },
  {
   "path": "test/functional/test_framework/p2p.py",
   "add": 3,
   "del": 1
  },
  {
   "path": "test/functional/test_framework/privbcast.py",
   "add": 335,
   "del": 0
  },
  {
   "path": "test/functional/test_framework/socks5.py",
   "add": 101,
   "del": 13
  },
  {
   "path": "test/functional/test_framework/test_framework.py",
   "add": 9,
   "del": 120
  },
  {
   "path": "test/functional/test_framework/util.py",
   "add": 5,
   "del": 0
  },
  {
   "path": "test/functional/test_runner.py",
   "add": 5,
   "del": 2
  },
  {
   "path": "test/functional/tool_privbcast.py",
   "add": 505,
   "del": 0
  },
  {
   "path": "test/functional/tool_privbcast_package.py",
   "add": 266,
   "del": 0
  },
  {
   "path": "test/functional/tool_privbcast_peers.py",
   "add": 256,
   "del": 0
  },
  {
   "path": "test/functional/tool_privbcast_proxy.py",
   "add": 168,
   "del": 0
  }
 ],
 "test_lines": 11641,
 "git": {
  "head": "eec78ccf7d59f5b39d4525980e8e3a7fe6c60242",
  "head_matches_backup": true,
  "base": "7efbdb6437a840217127bd7dea0b963c67613860",
  "patch_id": "ca7da6499b861907",
  "commits": [
   {
    "sha": "bcd21b632c",
    "subject": "--- part 1/4: support ---",
    "files": 0,
    "add": 0,
    "del": 0
   },
   {
    "sha": "e003fd2143",
    "subject": "build: move bip324 to the common library",
    "files": 1,
    "add": 1,
    "del": 1
   },
   {
    "sha": "0582ee800b",
    "subject": "refactor: move the transports out of net.h",
    "files": 12,
    "add": 1426,
    "del": 1368
   },
   {
    "sha": "ec74e9c2f4",
    "subject": "netbase: export Socks5ErrorString",
    "files": 2,
    "add": 4,
    "del": 1
   },
   {
    "sha": "e596fc3cc2",
    "subject": "build: move package policy to the common library",
    "files": 1,
    "add": 1,
    "del": 1
   },
   {
    "sha": "6a0027f99b",
    "subject": "--- part 2/4: bitcoin-privbcast ---",
    "files": 0,
    "add": 0,
    "del": 0
   },
   {
    "sha": "a083390b73",
    "subject": "doc: add the private broadcast specification",
    "files": 1,
    "add": 489,
    "del": 0
   },
   {
    "sha": "c1d3eb83e0",
    "subject": "privbcast: add the parameters and the schedule",
    "files": 8,
    "add": 482,
    "del": 0
   },
   {
    "sha": "d638a035d0",
    "subject": "privbcast: add candidate assignment",
    "files": 5,
    "add": 445,
    "del": 0
   },
   {
    "sha": "19844ed5af",
    "subject": "privbcast: add a byte-driven SOCKS5 client",
    "files": 7,
    "add": 724,
    "del": 0
   },
   {
    "sha": "655553d8dc",
    "subject": "privbcast: add the attempt state machine",
    "files": 8,
    "add": 1613,
    "del": 0
   },
   {
    "sha": "7da6cfd2cc",
    "subject": "privbcast: add proxy streams and discovery",
    "files": 11,
    "add": 1709,
    "del": 0
   },
   {
    "sha": "28926fa818",
    "subject": "privbcast: add the job",
    "files": 7,
    "add": 2317,
    "del": 0
   },
   {
    "sha": "65da100adb",
    "subject": "test: extend the harness for bitcoin-privbcast",
    "files": 4,
    "add": 118,
    "del": 14
   },
   {
    "sha": "9f8105a33c",
    "subject": "privbcast: add bitcoin-privbcast",
    "files": 23,
    "add": 2329,
    "del": 1
   },
   {
    "sha": "ca786cc7b9",
    "subject": "doc: describe bitcoin-privbcast",
    "files": 2,
    "add": 74,
    "del": 0
   },
   {
    "sha": "33fe47c1f7",
    "subject": "doc: release notes for bitcoin-privbcast",
    "files": 1,
    "add": 22,
    "del": 0
   },
   {
    "sha": "64f1f26454",
    "subject": "--- part 3/4: bitcoind -privatebroadcast as jobs ---",
    "files": 0,
    "add": 0,
    "del": 0
   },
   {
    "sha": "71ea14ea32",
    "subject": "node: add the private broadcast job queue",
    "files": 8,
    "add": 2048,
    "del": 0
   },
   {
    "sha": "8bd029b720",
    "subject": "node: run private broadcast as jobs",
    "files": 20,
    "add": 977,
    "del": 1002
   },
   {
    "sha": "e851f4d978",
    "subject": "net: remove the old private broadcast relay",
    "files": 14,
    "add": 14,
    "del": 1664
   },
   {
    "sha": "c39973a4df",
    "subject": "net: remove the old private broadcast connections",
    "files": 12,
    "add": 27,
    "del": 344
   },
   {
    "sha": "fded8bbf39",
    "subject": "refactor: drop the trivial AdvertisedVersion()",
    "files": 3,
    "add": 11,
    "del": 24
   },
   {
    "sha": "c5ac672d8d",
    "subject": "doc: describe bitcoind -privatebroadcast",
    "files": 1,
    "add": 51,
    "del": 8
   },
   {
    "sha": "26e3a21e1c",
    "subject": "doc: release notes for private broadcast jobs",
    "files": 1,
    "add": 58,
    "del": 0
   },
   {
    "sha": "a80a9d414a",
    "subject": "--- part 4/4: package mode ---",
    "files": 0,
    "add": 0,
    "del": 0
   },
   {
    "sha": "d9f4c35cc3",
    "subject": "privbcast: serve a parent in package mode",
    "files": 12,
    "add": 614,
    "del": 67
   },
   {
    "sha": "be1433a1bd",
    "subject": "privbcast: accept a parent and child in the tool",
    "files": 7,
    "add": 551,
    "del": 23
   },
   {
    "sha": "ee5719d0c6",
    "subject": "validation: sum a package's modified fees saturating",
    "files": 2,
    "add": 14,
    "del": 1
   },
   {
    "sha": "76797040c2",
    "subject": "validation: test-accept a package as submission evaluates it",
    "files": 4,
    "add": 121,
    "del": 12
   },
   {
    "sha": "d27d450877",
    "subject": "validation: test a package whose parent passes alone",
    "files": 4,
    "add": 115,
    "del": 16
   },
   {
    "sha": "079099f079",
    "subject": "rpc: queue submitpackage for private broadcast",
    "files": 12,
    "add": 863,
    "del": 59
   },
   {
    "sha": "30d5ea00b5",
    "subject": "doc: specify package mode",
    "files": 1,
    "add": 101,
    "del": 1
   },
   {
    "sha": "7f4e53befc",
    "subject": "doc: describe package mode",
    "files": 1,
    "add": 29,
    "del": 8
   },
   {
    "sha": "eec78ccf7d",
    "subject": "doc: release notes for package mode",
    "files": 1,
    "add": 36,
    "del": 0
   }
  ],
  "patch_truncated": true
 },
 "patch_id": "ca7da6499b861907",
 "input_hash": "aae774b9b1a9548e",
 "extracted_at": "2026-11-02T01:09:49+00:00"
}