From d0fd9524e62703b656eb9bcca571b077660b0caf Mon Sep 17 00:00:00 2001 From: zelig Date: Mon, 3 Aug 2026 12:42:42 +0200 Subject: [PATCH 1/4] =?UTF-8?q?add=20SWIP-60:=20BPS=20singlehop=20?= =?UTF-8?q?=E2=80=94=20brokered=20broadcast=20pub/sub,=20base=20protocol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- SWIPs/assets/swip-60/bps.proto | 113 +++++++++++++++ SWIPs/swip-60.md | 242 +++++++++++++++++++++++++++++++++ 2 files changed, 355 insertions(+) create mode 100644 SWIPs/assets/swip-60/bps.proto create mode 100644 SWIPs/swip-60.md diff --git a/SWIPs/assets/swip-60/bps.proto b/SWIPs/assets/swip-60/bps.proto new file mode 100644 index 00000000..36381999 --- /dev/null +++ b/SWIPs/assets/swip-60/bps.proto @@ -0,0 +1,113 @@ +// Broadcast Pub/Sub (BPS) — protocol messages and types. +// Spec: SWIP-60 (../../swip-60.md). +// +// Deliberately incomplete as of 2026-08-02: the singlehop (depth = 1) subset is +// concrete; multihop control-plane messages are named but reserved. The existing +// implementation (bee PR #5435) uses hand-rolled byte framing with the same +// semantics; this file is the normative description of the message structure, +// and — bee protocols being protobuf-over-libp2p elsewhere — the candidate +// replacement framing. + +syntax = "proto3"; +package bps; + +option go_package = "github.com/ethersphere/bee/v2/pkg/bps/pb"; + +// --------------------------------------------------------------------------- +// Cohort genesis — the primitive decisions whose combinations are the "modes" +// --------------------------------------------------------------------------- + +// What the topic binds to (see epic: "What does the topic bind to?"). +enum TopicBinding { + TOPIC_BINDING_UNSPECIFIED = 0; + ANCHOR = 1; // topic = full SOC/GSOC address; dedup on the wrapped CAC + SOC_ID = 2; // topic = SOC id; any owner with PO(addr, anchor) >= po_min + OWNER = 3; // topic = SOC owner; any id with PO(addr, anchor) >= po_min (MIC) + FEED_TOPIC = 4; // id = keccak256(topic ‖ index); graffiti MIC / feed streams +} + +// Who may author (see epic: genesis dimensions). +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 + IMPLICIT = 3; // authorship implied by the topic binding (PO constraint) + ALL = 4; // every peer publishes (gossipsub-equivalent cohort) +} + +// The (partial) decisions fixed the moment the first full node is contacted. +message CohortSpec { + bytes topic = 1; // 32 bytes, meaning per binding + 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_* + 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) + bool closed = 8; // no audience: subscribers restricted to the publisher list +} + +// --------------------------------------------------------------------------- +// Stream establishment (client -> broker), stream name "pubsub/1.0.0" +// --------------------------------------------------------------------------- + +enum Role { + ROLE_UNSPECIFIED = 0; + SUBSCRIBER = 1; + PUBLISHER = 2; // implies direct connection to the broker (necessary, not sufficient) +} + +message Connect { + CohortSpec cohort = 1; + Role role = 2; + PublisherAuth auth = 3; // present iff role == PUBLISHER +} + +message PublisherAuth { + bytes owner = 1; // 20-byte eth address of the SOC owner key + bytes id = 2; // 32-byte SOC id, when the binding fixes it +} + +// --------------------------------------------------------------------------- +// Messages — SOC-only is a protocol feature +// --------------------------------------------------------------------------- + +// A full single-owner chunk in transit. +message Soc { + bytes id = 1; // 32 bytes + bytes owner = 2; // 20 bytes (recoverable from signature; explicit for cheap filtering) + bytes signature = 3; // 65 bytes + bytes span = 4; // 8 bytes LE + bytes payload = 5; // wrapped-CAC data, <= 4096 bytes +} + +// Publisher -> broker. No type prefix needed: the stream's role was declared at Connect. +message Publish { + Soc soc = 1; +} + +// Broker -> subscriber: exactly one of the following per frame. +message Broadcast { + 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 + } +} + +message DataFrame { + bytes signature = 1; + bytes span = 2; + bytes payload = 3; +} + +message Ping {} + +// --------------------------------------------------------------------------- +// Multihop control plane — RESERVED, named to fix intent (not final for AFM) +// --------------------------------------------------------------------------- +// message Beacon {} // child -> parent capacity/score summary (0xFE) +// message Reparent {} // parent -> child: REPARENT{to, gateway?} (0xFD) +// message Expect {} // parent -> relay: EXPECT{children} (0xFC) +// message DcutrSignal {} // via circuit relay (0xFB) +// message SwapProposal {} // promotion swap propose/ack (0xFA) diff --git a/SWIPs/swip-60.md b/SWIPs/swip-60.md new file mode 100644 index 00000000..56243765 --- /dev/null +++ b/SWIPs/swip-60.md @@ -0,0 +1,242 @@ +--- +SWIP: 60 +title: BPS singlehop — brokered broadcast pub/sub, base protocol +author: Viktor Trón (@zelig), Viktor Tóth (@nugaon) +discussions-to: https://discord.gg/Q6BvSkCv +status: Draft +type: Standards Track (Networking) +created: 2026-08-03 +--- + + + +- **Business line**: real-time topic streams for dApps without storing chunks or polling — + enough on its own for small closed collaboration cohorts (collaborative remix editing, a + strudel livecoding session, multiparty games) and basic single-publisher limited-audience + live streaming. +- **Dev line**: implement one libp2p protocol (`pubsub/1.0.0`, messages in + [bps.proto](assets/swip-60/bps.proto)) plus a WebSocket bridge on the Bee API; done when + a broker, publishers and subscribers interoperate per the conformance section. Groundwork + exists in bee [#5435](https://github.com/ethersphere/bee/pull/5435). +- Bandwidth-incentive integration is a separate SWIP (bps-bw-incentives). +- Broker discovery integration is from a separate SWIP (bps-broker-discovery, building on + [SWIP-58 MEX](https://github.com/ethersphere/SWIPs/pull/103)). + +## Simple Summary + +A real-time messaging protocol: WebSocket clients publish and subscribe to topic streams +through Bee nodes. One full node per topic acts as **broker**, re-broadcasting each message +over direct, long-lived p2p streams to at most **cap** connected peers. Messages are +single-owner chunks, so every subscriber verifies authorship end-to-end; the broker can +withhold, never forge. + +## Motivation + +Swarm's event primitives (GSOC, PSS) require full-node operation; light clients can only +poll storage. BPS singlehop is the smallest protocol that fixes this: one broker, direct +streams, authenticated messages, an explicit connection cap. Everything larger — multihop +trees, adaptive reorganisation, incentives, discovery — is layered on top by later SWIPs +without changing the semantics defined here. + +## Specification + +### The contract + +Per topic-cohort: + +- messages come from **publishers, and publishers only**; +- they arrive at **all subscribers**. + +### Cohort genesis: the parameters + +A cohort is fully described by a `CohortSpec` ([bps.proto](assets/swip-60/bps.proto)), +fixed the moment the first peer contacts a BPS-speaking full node with a topic. There is +no mode enum; **modes are combinations of these parameters**. + +| parameter | values | meaning | +|---|---|---| +| `topic` | 32 bytes | interpreted per `binding` | +| `binding` | `ANCHOR` / `SOC_ID` / `OWNER` / `FEED_TOPIC` | what the topic binds to; fixes which SOCs qualify as messages and the dedup rule | +| `publishers` | `EXPLICIT_SINGLE` / `EXPLICIT_LIST` / `IMPLICIT` / `ALL` | who may author | +| `admin` | eth address | set iff explicit publishers; may extend the publisher list, nothing more | +| `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` | +| `cap` | uint | **max direct streams the broker accepts for this topic**; 0 = broker's default | +| `closed` | bool | no audience: subscribers are restricted to the publisher list (all and only publishers subscribe) | + +Binding semantics (dedup rule in parentheses): + +- **`ANCHOR`** — topic = full SOC/GSOC address; all messages share one address (dedup on + the wrapped CAC). +- **`SOC_ID`** — topic = SOC id; any owner with `PO(socAddr(id, owner), anchor) ≥ po_min` + qualifies (dedup on chunk address). +- **`OWNER`** — topic = SOC owner; any id under the same PO constraint — MIC semantics + (dedup on chunk address). +- **`FEED_TOPIC`** — id = `keccak256(topic ‖ index)`; feed-update streams, graffiti MIC + (dedup on chunk address). + +### Roles and the cap + +- **Broker**: the first full node contacted; root of the (here, depth = 1) multicast tree. + Accepts at most `cap` concurrent streams for the topic. **At cap it MUST answer a + `Connect` with a refusal** (`FULL`); referral to another attachment point is reserved + for bps-multihop — a singlehop-only broker simply refuses. +- **Publisher**: sends and receives. MUST be directly connected to the broker; direct + connection is necessary, not sufficient — with explicit publishers, the admin's list + decides. +- **Subscriber**: receives only. Does not exist in `closed` cohorts. + +### Information flow + +```mermaid +sequenceDiagram + autonumber + participant PD as publisher dApp + participant PN as publisher's bee node
(WS bridge) + participant B as broker
(root, full node) + participant SN as subscriber's bee node
(WS bridge + mux) + participant SD as subscriber dApp(s) + + Note over B: cohort open: topic set,
genesis parameters fixed + SN->>B: Connect(CohortSpec, SUBSCRIBER) + PN->>B: Connect(CohortSpec, PUBLISHER, auth) + Note over PN,B: publisher ⇒ direct connection to broker
(necessary, not sufficient — admin's list decides) + + loop keepalive (30 s) + B->>SN: Ping + SN-->>B: echo (RTT measured by parent) + end + + PD->>PN: WS: payload + PN->>B: Publish(SOC) + B->>B: validate: SOC sig ⊨ topic binding
(+ dedup per binding) + + par fan-out to every subscriber stream + B->>SN: Broadcast: handshake frame (full SOC identity, first) /
data frame (sig ‖ span ‖ payload, after) + SN->>SN: mux: one p2p stream → N WS sessions + SN->>SD: WS: payload + and publisher's own subscription (if subscriber too) + B->>PN: Broadcast + PN->>PD: WS: payload + end +``` + +The broadcast is **end-to-end authenticated**: every subscriber re-verifies the SOC +signature against the topic binding regardless of path. + +### Wire protocol + +Messages are defined in [bps.proto](assets/swip-60/bps.proto). Framing notes: + +- Transport: libp2p stream `pubsub/1.0.0`, one stream per (peer, topic); `Connect` as the + first message (protobuf-over-libp2p, as bee protocols elsewhere) — bee #5435 + currently uses stream headers. +- Frame-type byte split: service frames grow downward from `0xFF` (ping `0xFF`; multihop + control frames `0xFE`… reserved), data frames grow upward from `0x00` — no collision. +- Broker→subscriber: first frame per stream is the **handshake** frame carrying full SOC + identity (id, owner); subsequent **data** frames carry `sig ‖ span ‖ payload` only. +- Publisher→broker frames carry no type prefix: the stream's role was declared at + `Connect`. +- Broker validation on `Publish`: SOC signature verifies against the topic binding, PO + constraint holds where applicable, sender is a legitimate publisher, message is not a + duplicate per the binding's dedup rule. Invalid ⇒ drop; repeated invalid ⇒ disconnect + (blocklisting policy). + +### API (WebSocket bridge) + +WS clients see raw mode payloads only; all p2p framing is transparent. One p2p stream is +muxed to N local WS sessions per topic. Endpoint shape per bee +[#5435](https://github.com/ethersphere/bee/pull/5435). + +### Configurations (worked examples) + +Modes are rows over the parameters; two normative examples: + +**The 4-seat jam cohort** — collaborative remix editing, a strudel livecoding session, a +multiparty game. + +``` +binding: ANCHOR (topic = mnemonic anchor) publishers: EXPLICIT_LIST (admin + ≤3) +closed: true (all and only publishers subscribe) cap: 4 history: false +``` + +Every seat sends and receives; there is no audience; a fifth `Connect` gets `FULL`. + +**Basic live streaming** — single publisher, open audience: + +``` +binding: FEED_TOPIC (sequential index) publishers: EXPLICIT_SINGLE +closed: false cap: broker default history: false +``` + +### The modes — enumerated as combinations of dimension choices + +Known use cases attach here; each mode is nothing more than a row — a combination of +publisher/subscriber info, topic match type, and history. (`+/−` = both configurations +meaningful.) + +| # of pubs | pubs implicit? | subscribers | topic / anchor match | history | use case | +|---|---|---|---|---|---| +| 1 | — | all | feed topic, index sequential | — | live video streaming | +| any | — | all | feed topic, index sequential | — | live videoconference | +| — | + | all | feed topic | +/— | tags, adverts; private co-authoring | +| all | — | all | topic a mere mnemonic of the cohort | +/— | gossip cohort for multi-party / group chat | +| any | + | all | anchor (ephemeral GSOC) | +/— | anythread comments / troll-box | +| any | + | all | ID = `keccak256(topic ‖ index)` | +/— | following one or more feeds | +| — | + | all | feed special, mined index | +/— | following graffiti soc | + +The audience is bounded by the broker's cap; scaling past it is bps-multihop's business. + +Rows requiring implicit publishers or history are specified in bps-implicit-publisher and +bps-history respectively. + +## Rationale: why not gossipsub + +libp2p ships gossipsub, a battle-tested mesh multicast. BPS builds its own protocol +because gossipsub's core mechanisms — flooding to a random mesh, IHAVE/IWANT +pull-recovery — are exactly what an incentivised network rejects: **no node wants to pay +for a message it did not ask for.** That one economic fact dissolves gossipsub's +machinery: metered edges mean no redundant paths and no transport-level duplicates; a +cohort's `CohortSpec` scopes every session; authentication is structural (SOC-signed +against the topic binding), so brokers and relays forward without being trusted — an +intermediate can withhold, never forge; and withholding is a liveness fault recoverable +by re-pointing or relocating the topic. Multihop forwarding (bps-multihop) adds capacity +without reintroducing flooding: every edge still pays upstream, every node still receives +only its topic's stream. + +## Out of scope (deliberately) + +Multihop relaying and referral (bps-multihop), reorganisation policies (SWATCH, SPORE — +policy SWIPs over this protocol's events and actions, no new frames), bandwidth incentives +(bps-bw-incentives), broker discovery (SWIP-58 MEX; early deployments hardcode brokers), +history delivery mechanism (bps-history), implicit-publisher event sourcing +(bps-implicit-publisher). + +## Conformance (definition of done) + +An implementation is conformant when: + +1. a broker enforces cap, publisher legitimacy, per-binding validation and dedup; +2. a subscriber re-verifies every message end-to-end and detects (only) liveness faults; +3. the two worked configurations above interoperate across independent implementations + against the frames in [bps.proto](assets/swip-60/bps.proto); +4. a `FULL` refusal is issued at cap — and nothing else is (no referral). + +## Backwards compatibility + +New protocol; no existing behaviour changes. Frame-byte split reserves the service range +so bps-multihop extends without version bump. + +## References + +Wire: [bps.proto](assets/swip-60/bps.proto) · origin: +[PR #93](https://github.com/ethersphere/SWIPs/pull/93) "Add: pubsub" · broker discovery: +[SWIP-58 MEX, PR #103](https://github.com/ethersphere/SWIPs/pull/103) · implementation: +bee [#5435](https://github.com/ethersphere/bee/pull/5435), bee-js +[#1151](https://github.com/ethersphere/bee-js/pull/1151) + +## Copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). From 25f6f084e2cfe93151fe5dd9dbd903793c41fc2e Mon Sep 17 00:00:00 2001 From: zelig Date: Wed, 5 Aug 2026 01:11:26 +0200 Subject: [PATCH 2/4] swip-60: revision 2 after acud's review - 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 --- SWIPs/assets/swip-60/bps.proto | 128 +++++++++++++++++++-------------- SWIPs/swip-60.md | 94 +++++++++++++----------- 2 files changed, 130 insertions(+), 92 deletions(-) diff --git a/SWIPs/assets/swip-60/bps.proto b/SWIPs/assets/swip-60/bps.proto index 36381999..db495975 100644 --- a/SWIPs/assets/swip-60/bps.proto +++ b/SWIPs/assets/swip-60/bps.proto @@ -1,12 +1,21 @@ // Broadcast Pub/Sub (BPS) — protocol messages and types. // Spec: SWIP-60 (../../swip-60.md). // -// Deliberately incomplete as of 2026-08-02: the singlehop (depth = 1) subset is -// concrete; multihop control-plane messages are named but reserved. The existing -// implementation (bee PR #5435) uses hand-rolled byte framing with the same -// semantics; this file is the normative description of the message structure, -// and — bee protocols being protobuf-over-libp2p elsewhere — the candidate -// replacement framing. +// Revision 2 (2026-08-05), after review on PR #104: Connect split into +// Open/Subscribe (subscribers carry no cohort metadata), broker capacity +// removed from CohortSpec (it is broker-side policy, not a cohort parameter), +// Ping dropped (liveness/RTT are transport concerns), and every frame carries +// the full SOC (no handshake/data split). Field numbers renumbered — the +// draft has no deployed compatibility surface. +// +// Enum zero values (*_UNSPECIFIED): proto3 requires a zero value; it is +// deliberately NOT a legitimate wire value. It exists so that an unset field +// is detectable and no implementation can silently rely on a default. +// Receivers MUST reject messages carrying it. +// +// The singlehop (depth = 1) subset is concrete; multihop control-plane +// messages are reserved. Implementation groundwork: bee PR #5435 +// (hand-rolled byte framing with the same semantics). syntax = "proto3"; package bps; @@ -17,50 +26,59 @@ option go_package = "github.com/ethersphere/bee/v2/pkg/bps/pb"; // Cohort genesis — the primitive decisions whose combinations are the "modes" // --------------------------------------------------------------------------- -// What the topic binds to (see epic: "What does the topic bind to?"). +// What the topic binds to (see SWIP-60: binding semantics). enum TopicBinding { - TOPIC_BINDING_UNSPECIFIED = 0; + TOPIC_BINDING_UNSPECIFIED = 0; // invalid on the wire (see header note) ANCHOR = 1; // topic = full SOC/GSOC address; dedup on the wrapped CAC SOC_ID = 2; // topic = SOC id; any owner with PO(addr, anchor) >= po_min OWNER = 3; // topic = SOC owner; any id with PO(addr, anchor) >= po_min (MIC) FEED_TOPIC = 4; // id = keccak256(topic ‖ index); graffiti MIC / feed streams } -// Who may author (see epic: genesis dimensions). +// Who may author. 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 + PUBLISHER_REGIME_UNSPECIFIED = 0; // invalid on the wire (see header note) + EXPLICIT_SINGLE = 1; // opener is the sole publisher (live streaming) + EXPLICIT_LIST = 2; // set fixed at genesis: admin + publisher_list + // (dynamic grants/revocations: later revision) IMPLICIT = 3; // authorship implied by the topic binding (PO constraint) ALL = 4; // every peer publishes (gossipsub-equivalent cohort) } -// The (partial) decisions fixed the moment the first full node is contacted. +// Fixed by the cohort's opener; immutable for the cohort's lifetime. +// NOTE: broker capacity is NOT a cohort parameter — a cohort cannot dictate a +// remote node's connection count. Each broker enforces its own per-topic +// stream limit and answers FULL when it is exhausted. message CohortSpec { - bytes topic = 1; // 32 bytes, meaning per binding - 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_* - 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) - bool closed = 8; // no audience: subscribers restricted to the publisher list + bytes topic = 1; // 32 bytes, meaning per binding + 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_* + 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) + bool closed = 8; // no audience: subscribers restricted to the publishers } // --------------------------------------------------------------------------- -// Stream establishment (client -> broker), stream name "pubsub/1.0.0" +// Stream establishment, stream name "pubsub/1.0.0" — one stream per (peer, topic). +// The first message on a fresh stream is Open (fixes a new cohort) or +// Subscribe (joins an existing one); the broker answers with Ack. // --------------------------------------------------------------------------- -enum Role { - ROLE_UNSPECIFIED = 0; - SUBSCRIBER = 1; - PUBLISHER = 2; // implies direct connection to the broker (necessary, not sufficient) +// Opener -> broker: the one peer that fixes the cohort. +message Open { + CohortSpec cohort = 1; + PublisherAuth auth = 2; // present iff the opener publishes (explicit regimes) } -message Connect { - CohortSpec cohort = 1; - Role role = 2; - PublisherAuth auth = 3; // present iff role == PUBLISHER +// Joiner -> broker: names the topic — nothing more. Subscribers carry no +// cohort metadata; auth is present iff the joiner publishes (publishers +// connect directly to the broker). +message Subscribe { + bytes topic = 1; // 32 bytes + PublisherAuth auth = 2; // present iff publisher } message PublisherAuth { @@ -68,11 +86,30 @@ message PublisherAuth { bytes id = 2; // 32-byte SOC id, when the binding fixes it } +// Broker -> peer, answering Open or Subscribe. The echoed CohortSpec lets a +// subscriber verify every message end-to-end against the topic binding. +message Ack { + Status status = 1; + CohortSpec cohort = 2; // set iff status == OK +} + +enum Status { + STATUS_UNSPECIFIED = 0; // invalid on the wire (see header note) + OK = 1; + FULL = 2; // broker at its per-topic capacity; + // a singlehop broker refuses — nothing else + UNKNOWN_TOPIC = 3; // Subscribe for a topic the broker does not serve + REJECTED = 4; // e.g. publisher not on the list, invalid auth, + // non-publisher Subscribe on a closed cohort +} + // --------------------------------------------------------------------------- // Messages — SOC-only is a protocol feature // --------------------------------------------------------------------------- -// A full single-owner chunk in transit. +// A full single-owner chunk in transit. Every frame is self-contained: no +// per-stream handshake state, and no format change if the stream model +// evolves (e.g. topic-muxed streams later). message Soc { bytes id = 1; // 32 bytes bytes owner = 2; // 20 bytes (recoverable from signature; explicit for cheap filtering) @@ -81,33 +118,20 @@ message Soc { bytes payload = 5; // wrapped-CAC data, <= 4096 bytes } -// Publisher -> broker. No type prefix needed: the stream's role was declared at Connect. +// Publisher -> broker. message Publish { Soc soc = 1; } -// Broker -> subscriber: exactly one of the following per frame. +// Broker -> subscriber. message Broadcast { 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 + Soc soc = 1; + // 2–15 reserved: multihop control plane (Beacon, Reparent, Expect, + // DcutrSignal, SwapProposal) — named to fix intent, not final. } } -message DataFrame { - bytes signature = 1; - bytes span = 2; - bytes payload = 3; -} - -message Ping {} - -// --------------------------------------------------------------------------- -// Multihop control plane — RESERVED, named to fix intent (not final for AFM) -// --------------------------------------------------------------------------- -// message Beacon {} // child -> parent capacity/score summary (0xFE) -// message Reparent {} // parent -> child: REPARENT{to, gateway?} (0xFD) -// message Expect {} // parent -> relay: EXPECT{children} (0xFC) -// message DcutrSignal {} // via circuit relay (0xFB) -// message SwapProposal {} // promotion swap propose/ack (0xFA) +// Keepalive / RTT: none at the BPS level. Liveness is the transport's job +// (libp2p), and latency metrics for reorganisation policies (SWATCH) are +// sourced there as well. diff --git a/SWIPs/swip-60.md b/SWIPs/swip-60.md index 56243765..0b28bbca 100644 --- a/SWIPs/swip-60.md +++ b/SWIPs/swip-60.md @@ -60,11 +60,14 @@ no mode enum; **modes are combinations of these parameters**. | `topic` | 32 bytes | interpreted per `binding` | | `binding` | `ANCHOR` / `SOC_ID` / `OWNER` / `FEED_TOPIC` | what the topic binds to; fixes which SOCs qualify as messages and the dedup rule | | `publishers` | `EXPLICIT_SINGLE` / `EXPLICIT_LIST` / `IMPLICIT` / `ALL` | who may author | -| `admin` | eth address | set iff explicit publishers; may extend the publisher list, nothing more | +| `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` | -| `cap` | uint | **max direct streams the broker accepts for this topic**; 0 = broker's default | -| `closed` | bool | no audience: subscribers are restricted to the publisher list (all and only publishers subscribe) | +| `closed` | bool | no audience: subscribers are restricted to the publisher set (all and only publishers subscribe) | + +Broker **capacity is deliberately not a cohort parameter**: a cohort cannot dictate a +remote node's connection count. Each broker enforces its own per-topic stream limit and +answers `FULL` when it is exhausted. Binding semantics (dedup rule in parentheses): @@ -77,16 +80,20 @@ Binding semantics (dedup rule in parentheses): - **`FEED_TOPIC`** — id = `keccak256(topic ‖ index)`; feed-update streams, graffiti MIC (dedup on chunk address). -### Roles and the cap +### Roles and capacity - **Broker**: the first full node contacted; root of the (here, depth = 1) multicast tree. - Accepts at most `cap` concurrent streams for the topic. **At cap it MUST answer a - `Connect` with a refusal** (`FULL`); referral to another attachment point is reserved - for bps-multihop — a singlehop-only broker simply refuses. + Enforces its own per-topic capacity. **At capacity it MUST answer `Open`/`Subscribe` + with a refusal** (`FULL`); referral to another attachment point is reserved for + bps-multihop — a singlehop-only broker simply refuses. +- **Opener**: the one peer that fixes the `CohortSpec` (`Open`); with explicit publisher + regimes the opener publishes. - **Publisher**: sends and receives. MUST be directly connected to the broker; direct - connection is necessary, not sufficient — with explicit publishers, the admin's list + connection is necessary, not sufficient — with explicit publishers, the genesis list decides. -- **Subscriber**: receives only. Does not exist in `closed` cohorts. +- **Subscriber**: receives only; joins by naming the topic (`Subscribe`) and carries no + cohort metadata — the broker echoes the `CohortSpec` back so every message can be + verified end-to-end. Does not exist in `closed` cohorts. ### Information flow @@ -99,26 +106,23 @@ sequenceDiagram participant SN as subscriber's bee node
(WS bridge + mux) participant SD as subscriber dApp(s) - Note over B: cohort open: topic set,
genesis parameters fixed - SN->>B: Connect(CohortSpec, SUBSCRIBER) - PN->>B: Connect(CohortSpec, PUBLISHER, auth) - Note over PN,B: publisher ⇒ direct connection to broker
(necessary, not sufficient — admin's list decides) - - loop keepalive (30 s) - B->>SN: Ping - SN-->>B: echo (RTT measured by parent) - end + PN->>B: Open(CohortSpec, auth) + Note over PN,B: opener fixes the cohort; publisher ⇒
direct connection to broker + B-->>PN: Ack(OK) + SN->>B: Subscribe(topic) + B-->>SN: Ack(OK, CohortSpec) + Note over B,SN: echoed spec ⇒ subscriber verifies
every message end-to-end PD->>PN: WS: payload PN->>B: Publish(SOC) B->>B: validate: SOC sig ⊨ topic binding
(+ dedup per binding) par fan-out to every subscriber stream - B->>SN: Broadcast: handshake frame (full SOC identity, first) /
data frame (sig ‖ span ‖ payload, after) + B->>SN: Broadcast(SOC) — every frame self-contained SN->>SN: mux: one p2p stream → N WS sessions SN->>SD: WS: payload and publisher's own subscription (if subscriber too) - B->>PN: Broadcast + B->>PN: Broadcast(SOC) PN->>PD: WS: payload end ``` @@ -130,15 +134,18 @@ signature against the topic binding regardless of path. Messages are defined in [bps.proto](assets/swip-60/bps.proto). Framing notes: -- Transport: libp2p stream `pubsub/1.0.0`, one stream per (peer, topic); `Connect` as the - first message (protobuf-over-libp2p, as bee protocols elsewhere) — bee #5435 - currently uses stream headers. -- Frame-type byte split: service frames grow downward from `0xFF` (ping `0xFF`; multihop - control frames `0xFE`… reserved), data frames grow upward from `0x00` — no collision. -- Broker→subscriber: first frame per stream is the **handshake** frame carrying full SOC - identity (id, owner); subsequent **data** frames carry `sig ‖ span ‖ payload` only. -- Publisher→broker frames carry no type prefix: the stream's role was declared at - `Connect`. +- Transport: libp2p stream `pubsub/1.0.0`, one stream per (peer, topic), + protobuf-over-libp2p as bee protocols elsewhere. The first message on a fresh stream is + `Open` (fixes a new cohort) or `Subscribe` (joins one — topic only, no cohort + metadata); the broker answers with `Ack`, echoing the `CohortSpec` to subscribers. +- **Stream model rationale**: per-topic streams give per-cohort flow control, teardown + and role typing, and match bee's protocol idiom. Because every frame carries the full + SOC (self-contained, no per-stream handshake state), a later move to topic-muxed + streams requires no format change. +- Every `Broadcast` frame carries the **full SOC** (id, owner, signature, span, payload); + there is no handshake/data frame split. +- No BPS-level keepalive or RTT probing: liveness is the transport's job, and latency + metrics for reorganisation policies are sourced there too. - Broker validation on `Publish`: SOC signature verifies against the topic binding, PO constraint holds where applicable, sender is a legitimate publisher, message is not a duplicate per the binding's dedup rule. Invalid ⇒ drop; repeated invalid ⇒ disconnect @@ -158,17 +165,18 @@ Modes are rows over the parameters; two normative examples: multiparty game. ``` -binding: ANCHOR (topic = mnemonic anchor) publishers: EXPLICIT_LIST (admin + ≤3) -closed: true (all and only publishers subscribe) cap: 4 history: false +binding: ANCHOR (topic = mnemonic anchor) publishers: EXPLICIT_LIST (admin + 3) +closed: true (all and only publishers subscribe) history: false ``` -Every seat sends and receives; there is no audience; a fifth `Connect` gets `FULL`. +Every seat sends and receives; there is no audience; the genesis list **is** the seat +bound — a fifth peer's `Subscribe` gets `REJECTED`. **Basic live streaming** — single publisher, open audience: ``` binding: FEED_TOPIC (sequential index) publishers: EXPLICIT_SINGLE -closed: false cap: broker default history: false +closed: false history: false ``` ### The modes — enumerated as combinations of dimension choices @@ -187,7 +195,8 @@ meaningful.) | any | + | all | ID = `keccak256(topic ‖ index)` | +/— | following one or more feeds | | — | + | all | feed special, mined index | +/— | following graffiti soc | -The audience is bounded by the broker's cap; scaling past it is bps-multihop's business. +The audience is bounded by the broker's capacity; scaling past it is bps-multihop's +business. Rows requiring implicit publishers or history are specified in bps-implicit-publisher and bps-history respectively. @@ -212,22 +221,27 @@ Multihop relaying and referral (bps-multihop), reorganisation policies (SWATCH, policy SWIPs over this protocol's events and actions, no new frames), bandwidth incentives (bps-bw-incentives), broker discovery (SWIP-58 MEX; early deployments hardcode brokers), history delivery mechanism (bps-history), implicit-publisher event sourcing -(bps-implicit-publisher). +(bps-implicit-publisher), and **dynamic publisher-list changes** — grants/revocations +after genesis are deferred to a later revision; the `EXPLICIT_LIST` set is fixed at +`Open`. ## Conformance (definition of done) An implementation is conformant when: -1. a broker enforces cap, publisher legitimacy, per-binding validation and dedup; -2. a subscriber re-verifies every message end-to-end and detects (only) liveness faults; +1. a broker enforces its per-topic capacity, publisher legitimacy, per-binding validation + and dedup; +2. a subscriber re-verifies every message end-to-end (against the `Ack`-echoed + `CohortSpec`) and detects (only) liveness faults; 3. the two worked configurations above interoperate across independent implementations against the frames in [bps.proto](assets/swip-60/bps.proto); -4. a `FULL` refusal is issued at cap — and nothing else is (no referral). +4. a `FULL` refusal is issued at capacity — and nothing else is (no referral). ## Backwards compatibility -New protocol; no existing behaviour changes. Frame-byte split reserves the service range -so bps-multihop extends without version bump. +New protocol; no existing behaviour changes. Reserved `Broadcast` frame fields hold the +multihop control plane, so bps-multihop extends without a version bump; self-contained +frames mean a change of stream model needs no format change either. ## References From 77f60889cd84aac328141c6ab25c41201bf9547b Mon Sep 17 00:00:00 2001 From: zelig Date: Wed, 5 Aug 2026 01:19:47 +0200 Subject: [PATCH 3/4] swip-60: cap wording in summary/motivation follows capacity change Co-Authored-By: Claude Fable 5 --- SWIPs/swip-60.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SWIPs/swip-60.md b/SWIPs/swip-60.md index 0b28bbca..bb7956df 100644 --- a/SWIPs/swip-60.md +++ b/SWIPs/swip-60.md @@ -28,7 +28,7 @@ assets/swip-60/bps.proto. --> A real-time messaging protocol: WebSocket clients publish and subscribe to topic streams through Bee nodes. One full node per topic acts as **broker**, re-broadcasting each message -over direct, long-lived p2p streams to at most **cap** connected peers. Messages are +over direct, long-lived p2p streams to a capacity-bounded set of connected peers. Messages are single-owner chunks, so every subscriber verifies authorship end-to-end; the broker can withhold, never forge. @@ -36,7 +36,7 @@ withhold, never forge. Swarm's event primitives (GSOC, PSS) require full-node operation; light clients can only poll storage. BPS singlehop is the smallest protocol that fixes this: one broker, direct -streams, authenticated messages, an explicit connection cap. Everything larger — multihop +streams, authenticated messages, an explicit capacity bound. Everything larger — multihop trees, adaptive reorganisation, incentives, discovery — is layered on top by later SWIPs without changing the semantics defined here. From 74812864a31ab78d2dc370ee6659f8509e4e5296 Mon Sep 17 00:00:00 2001 From: zelig Date: Wed, 5 Aug 2026 05:19:44 +0200 Subject: [PATCH 4/4] swip-60: MEX renumbered SWIP-58 -> SWIP-59 Co-Authored-By: Claude Fable 5 --- SWIPs/swip-60.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SWIPs/swip-60.md b/SWIPs/swip-60.md index bb7956df..9a3fb3ba 100644 --- a/SWIPs/swip-60.md +++ b/SWIPs/swip-60.md @@ -22,7 +22,7 @@ assets/swip-60/bps.proto. --> exists in bee [#5435](https://github.com/ethersphere/bee/pull/5435). - Bandwidth-incentive integration is a separate SWIP (bps-bw-incentives). - Broker discovery integration is from a separate SWIP (bps-broker-discovery, building on - [SWIP-58 MEX](https://github.com/ethersphere/SWIPs/pull/103)). + [SWIP-59 MEX](https://github.com/ethersphere/SWIPs/pull/103)). ## Simple Summary @@ -219,7 +219,7 @@ only its topic's stream. Multihop relaying and referral (bps-multihop), reorganisation policies (SWATCH, SPORE — policy SWIPs over this protocol's events and actions, no new frames), bandwidth incentives -(bps-bw-incentives), broker discovery (SWIP-58 MEX; early deployments hardcode brokers), +(bps-bw-incentives), broker discovery (SWIP-59 MEX; early deployments hardcode brokers), history delivery mechanism (bps-history), implicit-publisher event sourcing (bps-implicit-publisher), and **dynamic publisher-list changes** — grants/revocations after genesis are deferred to a later revision; the `EXPLICIT_LIST` set is fixed at @@ -247,7 +247,7 @@ frames mean a change of stream model needs no format change either. Wire: [bps.proto](assets/swip-60/bps.proto) · origin: [PR #93](https://github.com/ethersphere/SWIPs/pull/93) "Add: pubsub" · broker discovery: -[SWIP-58 MEX, PR #103](https://github.com/ethersphere/SWIPs/pull/103) · implementation: +[SWIP-59 MEX, PR #103](https://github.com/ethersphere/SWIPs/pull/103) · implementation: bee [#5435](https://github.com/ethersphere/bee/pull/5435), bee-js [#1151](https://github.com/ethersphere/bee-js/pull/1151)