SWIP-060: BPS singlehop — brokered broadcast pub/sub, base protocol - #104
SWIP-060: BPS singlehop — brokered broadcast pub/sub, base protocol#104zelig wants to merge 9 commits into
Conversation
Base SWIP of the Broadcast Pub/Sub (BPS) family — the decomposition of the monolithic PubSub SWIP (PR #93) into work-package-sized SWIPs. Companion wire spec: assets/swip-60/bps.proto (singlehop concrete, multihop control frames reserved). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
||
| // What the topic binds to (see epic: "What does the topic bind to?"). | ||
| enum TopicBinding { | ||
| TOPIC_BINDING_UNSPECIFIED = 0; |
There was a problem hiding this comment.
what does this semantically mean? why is this a legitimate value that can be used?
|
|
||
| // Who may author (see epic: genesis dimensions). | ||
| enum PublisherRegime { | ||
| PUBLISHER_REGIME_UNSPECIFIED = 0; |
There was a problem hiding this comment.
what does this semantically mean? why is this a legitimate value that can be used?
| TopicBinding binding = 2; | ||
| PublisherRegime publishers = 3; | ||
| bool history = 4; // deliver matching chunks from the local store | ||
| bytes admin = 5; // 20-byte eth address; set iff EXPLICIT_* |
There was a problem hiding this comment.
if EXPLICIT_LIST is this then a concatenated list of ethereum keys?
| EXPLICIT_SINGLE = 1; // opener is admin and sole publisher (live streaming) | ||
| EXPLICIT_LIST = 2; // admin dictates who the other publishers are | ||
| IMPLICIT = 3; // authorship implied by the topic binding (PO constraint) | ||
| ALL = 4; // every peer publishes (gossipsub-equivalent cohort) |
There was a problem hiding this comment.
either ALL or EXPLICIT list needed. HOnestly I do not find it very natural that you can edit a file ith 3 other random people :) you want to restrict, explicitly list those that do .
| bool history = 4; // deliver matching chunks from the local store | ||
| bytes admin = 5; // 20-byte eth address; set iff EXPLICIT_* | ||
| uint32 po_min = 6; // proximity order for implicit bindings (default 16) | ||
| uint32 cap = 7; // max direct streams the broker accepts for this topic (0 = broker default) |
There was a problem hiding this comment.
why should a 3rd party be able to control the number of connections on a remote peer? what if the number is too large for the remote node to accept?
There was a problem hiding this comment.
fair enough i dont think it should
| message Broadcast { | ||
| oneof frame { | ||
| Soc handshake = 1; // first frame on a stream: full SOC identity | ||
| DataFrame data = 2; // subsequent frames: signature ‖ span ‖ payload only |
There was a problem hiding this comment.
why split the same chunk to multiple messages? you're also overloading the protocol code to do the message sequencing/buffering/etc... seems really unnecessary. also if you assume just one stream per topic then essentially you're coercing the applications to manage multiple streams between the same two peers continuously - why not multiplex everything over the same stream?
There was a problem hiding this comment.
while the single/multiple stream part is debatable, i'm not sure we need to skimp out on these few bytes that the chunk carries - it really doesn't save much, and then if we want to do single/multi stream management, we don't have to break the message format
| oneof frame { | ||
| Soc handshake = 1; // first frame on a stream: full SOC identity | ||
| DataFrame data = 2; // subsequent frames: signature ‖ span ‖ payload only | ||
| Ping ping = 3; // keepalive; parent measures RTT off the echo |
There was a problem hiding this comment.
who needs this information? we already measure rtt using other means. not sure why this message is needed
|
|
||
| ## Out of scope (deliberately) | ||
|
|
||
| Multihop relaying and referral (bps-multihop), reorganisation policies (SWATCH, SPORE — |
There was a problem hiding this comment.
i would also add to this: remove multi-publisher setup from this iteration. it can be added later and just adds more review surface to deal with at this moment.
There was a problem hiding this comment.
funnily i think a single one only is more complex to implement since you need to authenticate.
| enum PublisherRegime { | ||
| PUBLISHER_REGIME_UNSPECIFIED = 0; | ||
| EXPLICIT_SINGLE = 1; // opener is admin and sole publisher (live streaming) | ||
| EXPLICIT_LIST = 2; // admin dictates who the other publishers are |
There was a problem hiding this comment.
i would get rid of this for a first iteration
There was a problem hiding this comment.
Alright , but then you cannot get rid of ALL, otherwise you cannot have collab eediting..see below
| // --------------------------------------------------------------------------- | ||
|
|
||
| // What the topic binds to (see epic: "What does the topic bind to?"). | ||
| enum TopicBinding { |
There was a problem hiding this comment.
nit: i find this whole thing really confusing and not very approachable and i wonder if this even makes sense to do in a first iteration. "pubsub" is very dumb in this sense - it usually does not give you different topic semantics. here, a topic could have different semantics and input validation according to its "type" which makes for a much more complex API surfaces for users later on...
There was a problem hiding this comment.
- confusing, not very approachable, does not make sense, very dumb, no topic semantics, hmmm, thats a lot of negative things to asspciated to something that could have different semantics according to its type which makes for a... complex API surfaces? hhwhhat?
There was a problem hiding this comment.
i meant the concept of pubsub usually does not offer different semantics over the concept of a topic. i would appreciate you not hijacking my words and initial intention as this is really counter productive and aggressive. thanks
There was a problem hiding this comment.
I quoted your words which indeed were unnecessarily agressive.
As for your original intention, what was it?
There was a problem hiding this comment.
Not sure the semantics of topic or pubsub changes here, I thinkk the various bindings merely link the updates on a topic differently to each other as well as allow for multiple sources
- Connect split into Open (opener fixes CohortSpec) / Subscribe (topic only, no cohort metadata); broker Ack echoes the spec to subscribers for end-to-end verification; Role enum gone - broker capacity removed from CohortSpec: broker-side policy, not a cohort parameter; jam-cohort seat bound now = genesis publisher list - EXPLICIT_LIST mechanics specified: repeated publisher_list fixed at genesis; dynamic grants/revocations deferred (out of scope) - every frame carries the full SOC: handshake/data split dropped; stream-model rationale added (per-topic streams, mux-migration safe) - Ping dropped: liveness/RTT are transport concerns - *_UNSPECIFIED enum zero values documented as invalid on the wire Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Revision 2 pushed (25f6f08), addressing the review: Taken:
Specified (was a gap): Kept, per the discussion above: 🤖 Generated with Claude Code |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds SWIP-60 as the base specification for the Broadcast Pub/Sub (BPS) “singlehop” protocol, including its cohort-genesis parameters, roles/capacity semantics, framing model, and a companion protobuf wire definition to enable interoperable implementations.
Changes:
- Introduces the SWIP-60 markdown specification describing singlehop brokered broadcast pub/sub semantics and conformance criteria.
- Adds
bps.protodefining the protocol messages/types forpubsub/1.0.0(Open/Subscribe/Ack + SOC-only Publish/Broadcast).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| SWIPs/swip-60.md | New SWIP-60 spec text describing cohort parameters, singlehop flow, and conformance expectations. |
| SWIPs/assets/swip-60/bps.proto | New protobuf schema for the SWIP-60 wire messages and cohort specification. |
Suppressed comments (1)
SWIPs/assets/swip-60/bps.proto:131
- The comment says "2–15 reserved", which can be read as protobuf
reserved(meaning the numbers must never be used) even though the intent appears to be "kept for future multihop fields". Rewording avoids confusion for readers and implementers generating code from the schema.
// 2–15 reserved: multihop control plane (Beacon, Reparent, Expect,
// DcutrSignal, SwapProposal) — named to fix intent, not final.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| | `publishers` | `EXPLICIT_SINGLE` / `EXPLICIT_LIST` / `IMPLICIT` / `ALL` | who may author | | ||
| | `admin` + `publisher_list` | eth addresses | set iff explicit publishers; with `EXPLICIT_LIST` the full publisher set is **fixed at genesis** (dynamic grants/revocations are deferred to a later revision) | | ||
| | `history` | bool | deliver matching chunks already in the local store (mechanism in bps-history; a singlehop broker MAY refuse) | | ||
| | `po_min` | uint (default 16) | proximity constraint for implicit bindings: `PO(socAddr, anchor) ≥ po_min` | |
There was a problem hiding this comment.
po_min should be a constant not. a param
| bytes admin = 5; // 20-byte eth address; set iff EXPLICIT_* | ||
| repeated bytes publisher_list = 6; // 20-byte eth addresses, excl. admin; | ||
| // set iff EXPLICIT_LIST | ||
| uint32 po_min = 7; // proximity order for implicit bindings (default 16) |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… constant PO_MIN Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Revision 3 pushed (22e8325):
🤖 Generated with Claude Code |
…ained under explicit regimes (SWIP-65 pointer); worked API calls Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wrapped-CAC dedup under ANCHOR guards against unsolicited republication of old SOCs, and is sound only if the application guarantees distinct payloads - i.e. includes some index in the payload (per the SWIP-65 discussion: without self-indexing the sequence requirement moves above the protocol, unspecified). The two 'SWIP-65 (forthcoming)' anchors now link PR #106. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Amendment pushed (98e8918), companion to SWIP-65 (#106):
🤖 Generated with Claude Code |
|
comments courtesy of claude regarding the part of the implementation that is already on my branch |
…oved Auth Revision after implementation feedback from the bee prototype (acud, PR #104 comment of 2026-08-17) and a restructuring pass. ## Cohort spec carries immutable policy; the roster does not Five configurations, distinguished by three fields: jam admin GRANTED spectators:false spectator-jam admin GRANTED spectators:true live-stream admin ADMIN_ONLY spectators:true group-chat admin ALL spectators:true implicit no admin SOC shape decides - New binding `MNEMONIC`: the topic names the cohort and constrains nothing — any SOC from any owner. This is what ALL needs; authorship there is unrestricted but never unattributable, since every message is still SOC-signed, so a group chat knows who said what without an authorised set to check against. - `publishers` = ADMIN_ONLY / GRANTED / ALL. ADMIN_ONLY is an immutable promise ("this stream will never have a second author"), not a roster that happens to be empty. - `spectators` replaces the previous `closed` and is enforceable, because Auth is recovered rather than asserted. It is the only refusal for identity in the protocol; openers MUST set it true under ALL and implicit, where every attached peer is already a potential author. - The admin is always in the publisher set. A non-publishing moderator is just an admin that never sends — being a publisher obliges nobody to publish. - `publisher_list`, `po_min` and `closed` are reserved in CohortSpec. ## The service feed — the admin's control plane owner = admin id = keccak256("bps-service:v1" || topic || index) index 0 GENESIS the CohortSpec, signed by the admin index n ROSTER the full publisher set at version n last END_OF_STREAM the admin closes the cohort, attributably The roster is dynamic and the spec is immutable, so the roster cannot live in it; grantee identities are also not public the way an admin's is. A feed rather than one constant-id slot: overwriting in place would make a stale roster undetectable, reintroducing forging-by-omission at the one point that decides who may write. Sequential indices make gaps visible, so withholding stays a liveness fault (SWIP-65 carries the construction). Ack now delivers the echoed CohortSpec, the admin-signed genesis SOC and the latest service SOC with its index, so a joiner verifies the cohort and its roster against the admin rather than the broker. END_OF_STREAM separates "over" from "the broker stopped relaying". Revocation is two-phase, and the boundary is the moment the reduced roster reaches subscribers. Before it the revoked peer cannot know, so its frames are dropped and TOLERATED — no penalty, no teardown, because it is not misbehaving. After it the peer has been told on the same feed as everyone else, so publishing is a protocol violation and the connection is broken. The announcement is what converts an unknowing publisher into a violating one: disconnecting first would punish a peer for a rule it had not been given, and never publishing the roster leaves the violation unable to begin at all, which is an ordinary visible withholding fault. It also makes the revocation legible to the rest of the cohort, which learns why a publisher fell silent from an admin-signed message rather than from an unattributable disconnection. ## Wire - `Open` and `Subscribe` wrapped in a `Hello` envelope. As bare frames they are byte-indistinguishable (length-delimited field 1 + optional Auth in field 2) and proto3's permissive unmarshalling makes a wrong guess succeed silently, misread the frame, and answer with a Status describing the wrong problem. (acud, finding 1.) - `Auth` carries a signature and no address: owner = ecrecover over H("bps-join:v1" || topic || admin), so identity and proof arrive in one operation and the handshake stays one frame each way. No libp2p peer id in the preimage — binding to the node would weld the publishing identity to the node holding the stream and leak an eth-identity/peer-id link on every join. The preimage is therefore static and replayable, which costs nothing: a replayed role is worthless without the signing key. The "bps-join:v1" separator keeps the join-signature space disjoint from the SOC-signature space the same keys serve. (acud, finding 2.) - Dedup horizon: implementation-defined but MUST be bounded; replay of an evicted message by a legitimate publisher is the accepted consequence. - Cohort lifetime: broker-side, not tied to the opener, reclaimable when unattached — except by END_OF_STREAM, which is attributable. - A conformant broker bounds how many cohorts it will create; `Open` is otherwise an unbounded allocation primitive. (acud, finding 3.) ## Prose New "Security considerations": the admin and the cohort are authenticated by the genesis message; the publisher role is proved, not asserted; defence in depth is the real guarantee, so no challenge round trip; audience control exists only as `spectators` and is not confidentiality — BPS offers none at any layer, and a bounded audience is payload encryption, application-side. "Why not gossipsub" gains both halves of the trade: rootward-then-leafward carries each edge exactly once, so a single-parented tree needs no duplicate suppression at all and beats a mesh on closely knit topologies — and the concession that a genuinely gossip-shaped use case should just use libp2p gossipsub. Publishers' direct attachment to the broker is now stated as a consequence of depth = 1 rather than a protocol invariant: bps-multihop forwards Publish rootward from the leaves, which is what lets an everyone-publishes cohort outgrow one broker. (SWIP-61 needs the matching change.) API: `publishers`/`spectators` query parameters, no publisher list, `auth` replacing `owner`, and POST /pubsub/{topic}/service for the admin's grants, revocations and close. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Both wire findings taken, and the second one pushed the design further than the fix — 1.
|
|
@acud one implementation note that came out of revising SWIP-61 alongside this — relevant if
The other thing worth knowing before you touch the multihop side: SWIP-61 no longer keeps publishers at the root. 🤖 Generated with Claude Code |
|
@acud — separate ask: would you open a draft PR for the I went through it properly and it deserves a review surface. Right now it exists only as a branch, so there is nowhere to comment and nobody but me can see that the spec has a working implementation behind it — which is the strongest argument this SWIP has. Things I wanted to say inline and couldn't:
Two things a review would actually be about:
And it is the natural place to settle the scoping question from my previous comment — which configurations land first. 🤖 Generated with Claude Code |
SWIP-0 specifies `type: Standards Track` with the subcategory in a separate `category:` header (one of Core / Networking / Interface), as swip-19 and swip-20 do. This file carried the category inside `type:`, which is the only form in the repo and may break tooling that parses the front matter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Base SWIP of the Broadcast Pub/Sub (BPS) family — the decomposition of the monolithic PubSub SWIP (#93) into work-package-sized SWIPs.
What it specifies: the smallest complete BPS protocol — one broker per topic, direct long-lived p2p streams, an explicit per-topic connection cap, SOC-only messages verified end-to-end. A cohort is fully described by a
CohortSpecof genesis parameters; modes are parameter combinations, not an enum. Companion wire spec:assets/swip-60/bps.proto(singlehop concrete; multihop control frames reserved).Deliberately out of scope (own SWIPs): multihop relaying/referral, reorganisation policies (SWATCH, SPORE), bandwidth incentives, broker discovery (SWIP-59 MEX, #103), history delivery, implicit-publisher event sourcing.
Relation to #93: this SWIP absorbs its Milestone 1 plus the mode system (reframed as genesis parameters); Milestone 3 was already extracted as SWIP-59 (#103). Implementation groundwork: bee #5435, bee-js #1151.
🤖 Generated with Claude Code