Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added SWIPs/.Rhistory
Empty file.
200 changes: 200 additions & 0 deletions SWIPs/assets/swip-60/bps.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
// Broadcast Pub/Sub (BPS) — protocol messages and types.
// Spec: SWIP-60 (../../swip-60.md), extending the base wire of SWIP-74 (BPS-lite).
//
// Revision 10 (2026-09-27), per Viktor — the claim carried as a chunk; Broadcast.
//
// SWIP-74 fixes the base: four frames (Join, Ack, Auth, Broadcast) and the one type
// they carry (CohortSpec), for a single publisher over a feed at one broker, one hop,
// with the publisher role claimed by signing a broker-derived challenge — as a
// single-owner chunk, verified by the ordinary SOC code. This
// file adds what the full singlehop protocol needs and changes nothing SWIP-74
// defines:
// - CohortSpec gains `publishers` (one value, ALL), `history` and `closed`;
// - the admin's service feed (ROSTER, END_OF_STREAM) travels as Broadcast frames.
// Field numbers follow SWIP-74's; the added fields come after. There is no envelope:
// what a frame is follows from the stream's direction and role. Multihop (SWIP-61)
// adds its control frames as messages of its own.
//
// 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.
//
// Implementation: bee PR #5626.

syntax = "proto3";
package bps;

option go_package = "github.com/ethersphere/bee/v2/pkg/bps/pb";

// ---------------------------------------------------------------------------
// Cohort genesis — immutable policy, and the cohort's identity: cohorts are keyed
// by the spec's canonical serialisation (fields in number order, unset fields not
// emitted). The roster is NOT here (see ServiceKind).
// ---------------------------------------------------------------------------

// What the topic binds to (see SWIP-60: binding semantics). SWIP-74 defines
// FEED_TOPIC alone; the numbers are shared.
enum TopicBinding {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 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?

@acud acud Aug 5, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I quoted your words which indeed were unnecessarily agressive.
As for your original intention, what was it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

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 = keccak256(owner); any id, same PO constraint (MIC)
FEED_TOPIC = 4; // id = keccak256(topic || index); feed-update streams
MNEMONIC = 5; // the topic names the cohort and constrains nothing: any SOC
// from any owner qualifies (dedup on chunk address). What
// PublisherRegime.ALL needs -- authorship unrestricted, but
// never unattributable, since every message is SOC-signed.
}

// One value. Set: anyone attached may publish (group chat) -- no claim; a stream
// declares the address it publishes as, and every message it sends is validated
// against it. Unset, with an admin: the admin publishes, and whoever its roster
// ever names -- a cohort is multi-publisher iff a ROSTER is ever published, and
// nobody needs to know in advance. With no admin the cohort is implicit:
// authorship follows the binding's SOC shape and this does not apply.
enum PublisherRegime {
PUBLISHER_REGIME_UNSPECIFIED = 0; // invalid on the wire (see header note)
ALL = 1; // anyone attached; needs MNEMONIC binding
}

// Fixed by whoever joins first; immutable; keyed as a whole -- two specs that
// differ in any field are two cohorts, even on one topic.
// NOTE: broker capacity is NOT a cohort parameter -- a cohort cannot dictate a
// remote node's connection count. Each broker enforces its own bounds and
// answers FULL when one is exhausted.
// NOTE: the proximity constraint for implicit bindings is a protocol constant,
// PO_MIN = 16 -- not a cohort parameter (a proto3 unset uint32 is
// indistinguishable from 0, which would silently disable the constraint; and
// no use case varies it).
message CohortSpec {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the comment is misleading and partially incorrect. the seq diagram in the markdown file defines that actually both publisher and subscriber use the same type of message to connect to a broker. i'm not sure what are my feelings around this. it seems to be too elaborate for both sides to use symmetrically - why does a subscriber need to provide the whole CohortSpec when connecting? i.e. why is it necessary to mention the admin, history, publishers, etc? seems like irrelevant information. i can't see why a subscriber should provide anything more than a topic and an identity, and that's it.

so it begs the question, why shouldn't a state channel opening become its own specific message? and then equally Connect turns into a Subscribe

bytes topic = 1; // 32 bytes, meaning per binding
TopicBinding binding = 2;
bytes admin = 3; // 20-byte eth address: the cohort's authority
// and always a member of its publisher set.
// Absent (length 0) => implicit authorship,
// and `publishers`, `closed` do not apply.
PublisherRegime publishers = 4; // ALL, or unset (see the enum)
bool history = 5; // deliver matching chunks from the local store
bool closed = 6; // no audience: every stream is admitted silent,
// receiving nothing, and is disconnected unless
// a claim recovering to the admin or a rostered
// address arrives within the claim deadline.
// Unset = open, so that a SWIP-74 spec, which
// never sets it, reads as an open cohort.
}

// ---------------------------------------------------------------------------
// Stream establishment, stream name "pubsub/1.0.0" — one stream per
// (peer, cohort, identity). The first and only handshake frame on a fresh stream
// is Join; the broker answers with Ack. The first frame settles the cohort; the
// claim settles the role.
// ---------------------------------------------------------------------------

// A publisher's claim on the stream it is sent on, carried as a single-owner chunk
// so that the ordinary SOC validation verifies it in one call:
// id = keccak256("bps-claim:v1" || topic) never a feed id
// owner = addr address = keccak256(id || addr)
// payload = S || O_B || index 32 + 32 + 8 bytes
// signed as any SOC is, with the key of `addr`, where
// S_C = a secret drawn once at broker boot, never persisted
// S_s = keccak256(Marshal(spec)) the cohort's key
// S_c = keccak256(S_C || S_s) the cohort's secret
// S = keccak256(S_C || S_c || addr) the challenge for addr on this cohort
// O_B is the overlay of the broker the claiming node is connected to and `index`
// (eight bytes big-endian) the publisher's cursor: its next message has a feed
// index >= index. The receiver derives the id from the topic and the expected
// address from `addr`, validates the chunk against it, and checks the payload
// against its own S and overlay. The broker stores nothing; S is the same for an
// address on a cohort for as long as the broker runs, from any node. Sent inside
// Join by a peer that already holds S, or as the next frame after Ack by one that
// has just received it -- or has just seen itself named in a roster. No reply: the
// outcome is whether the stream survives the publication that follows. Under ALL
// and under implicit authorship there is no claim.
message Auth {
bytes soc = 1; // chunk data: id (32) || signature (65) || span (8, LE) || payload (72)
}

// Peer -> broker: the first frame on a fresh stream. Creates the cohort if no live
// cohort has this spec, attaches to it otherwise.
message Join {
CohortSpec cohort = 1;
bytes addr = 2; // 20 bytes: the address this stream will publish as --
// under explicit authorship the one it will claim; under
// ALL and under implicit authorship the one every
// publication is validated against, no claim; absent: a
// spectator, and no challenge is issued
Auth auth = 3; // a returning publisher's claim, verified before any bound
}

enum Status {
STATUS_UNSPECIFIED = 0; // invalid on the wire (see header note)
OK = 1;
FULL = 2; // a capacity bound (per cohort, per broker, per peer
// connection); a singlehop broker refuses -- nothing
// else
REJECTED = 3; // the SPEC is unacceptable: a value outside this SWIP
}

// Broker -> peer, answering Join. A non-OK Ack ends the stream. The roster
// reaches a newly attached stream as its first Broadcast (see ServiceKind) --
// except the admin's own, and except under `closed`, where nothing is delivered
// before the claim.
message Ack {
Status status = 1;
bytes challenge = 2; // S, iff status == OK and addr was declared
}

// ---------------------------------------------------------------------------
// Broadcast — SOC-only is a protocol feature
// ---------------------------------------------------------------------------

// Both directions after the handshake: publisher -> broker is a publication,
// broker -> peer a delivery of the same bytes. The single-owner chunk travels
// whole, address and data, opaque to the protocol and validated by the ordinary
// SOC code once the id slot has been rewritten as SWIP-74's Frames section says:
// data = id (32) || signature (65) || span (8, LE) || payload (<= 4096)
// Under FEED_TOPIC a feed update's id slot carries the bare index (SWIP-74,
// SWIP-65); a service SOC carries its full id (see ServiceKind).
message Broadcast {
bytes address = 1; // 32 bytes: the SOC address, keccak256(id || owner)
bytes data = 2;
}

// ---------------------------------------------------------------------------
// The service feed — the admin's control plane.
//
// Service messages are ordinary SOCs on the ordinary path, owned by the admin:
//
// owner = admin id = keccak256("bps-service:v1" || topic || index)
//
// travelling as Broadcast frames with their full 32-byte id, so a broker relays
// them and cannot author them, and a subscriber checks them with the same code
// as any broadcast. The payload carries its own index, so the id is verifiable
// without an out-of-band hint. Sequential indices (SWIP-65 self-indexed feeds)
// make gaps visible: a single constant-id slot overwritten in place would make
// a stale roster undetectable, reintroducing forging-by-omission at the one
// point that decides who may write. The feed starts at index 0 with the first
// ROSTER or END_OF_STREAM; a cohort whose admin has published nothing has an
// empty service feed, and the admin alone may write.
// ---------------------------------------------------------------------------

enum ServiceKind {
SERVICE_KIND_UNSPECIFIED = 0; // invalid on the wire (see header note)
ROSTER = 1; // the full publisher set as of this index (not a delta)
END_OF_STREAM = 2; // the admin closes the cohort, attributably
}

// The payload of a service SOC.
message ServiceMessage {
ServiceKind kind = 1;
uint64 index = 2; // this update's index on the service feed
repeated bytes publishers = 3; // set iff ROSTER: 20-byte eth addresses, the
// complete set excl. admin (who is always a
// publisher). Full state, not a delta, so a
// reader needs only the latest it can verify.
}

// 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.
Loading