From 8f1326fe2617511e2187e38aeef50068478869ce Mon Sep 17 00:00:00 2001 From: zelig Date: Wed, 5 Aug 2026 09:56:11 +0200 Subject: [PATCH] =?UTF-8?q?add=20SWIP-61:=20BPS=20multihop=20=E2=80=94=20F?= =?UTF-8?q?CFS=20multicast=20tree?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- SWIPs/assets/swip-61/bps.proto | 173 +++++++++++++++ SWIPs/swip-61.md | 373 +++++++++++++++++++++++++++++++++ 2 files changed, 546 insertions(+) create mode 100644 SWIPs/assets/swip-61/bps.proto create mode 100644 SWIPs/swip-61.md diff --git a/SWIPs/assets/swip-61/bps.proto b/SWIPs/assets/swip-61/bps.proto new file mode 100644 index 00000000..a91a28f6 --- /dev/null +++ b/SWIPs/assets/swip-61/bps.proto @@ -0,0 +1,173 @@ +// Broadcast Pub/Sub (BPS) — protocol messages and types. +// Spec: swip-bps-singlehop.md (SWIP-60, base protocol) and +// swip-bps-multihop.md (SWIP-61, multihop control plane). +// +// 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. +// +// SWIP-61 (2026-08-05) fills the reserved multihop control plane: Ack gains +// attachment candidates, Broadcast gains Reparent / Probe / Candidates. +// CohortSpec is untouched. +// +// 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 groundwork: bee PR #5435 (hand-rolled byte framing with the +// same semantics). + +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 SWIP-60: binding semantics). +enum TopicBinding { + 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. +enum PublisherRegime { + 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) +} + +// 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_* + 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, 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. +// --------------------------------------------------------------------------- + +// Opener -> broker: the one peer that fixes the cohort. +message Open { + CohortSpec cohort = 1; + PublisherAuth auth = 2; // present iff the opener publishes (explicit regimes) +} + +// 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 { + bytes owner = 1; // 20-byte eth address of the SOC owner key + 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 + repeated Candidate candidates = 3; // SWIP-61: set iff FULL at a relaying + // node — the two shallowest attachment + // points its probe found +} + +enum Status { + STATUS_UNSPECIFIED = 0; // invalid on the wire (see header note) + OK = 1; + FULL = 2; // at per-topic capacity; a singlehop (SWIP-60) + // broker refuses — nothing else; a multihop + // (SWIP-61) relay attaches candidates + 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. 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) + bytes signature = 3; // 65 bytes + bytes span = 4; // 8 bytes LE + bytes payload = 5; // wrapped-CAC data, <= 4096 bytes +} + +// Publisher -> broker. +message Publish { + Soc soc = 1; +} + +// The frame envelope. Control frames (SWIP-61) ride it in both directions on +// a (peer, topic) stream; frame type and direction disambiguate. +message Broadcast { + oneof frame { + Soc soc = 1; + Reparent reparent = 2; // SWIP-61, parent -> child + Probe probe = 3; // SWIP-61, parent -> child: find free slots below + Candidates found = 4; // SWIP-61, child -> parent: probe reply, min-2 filtered + // 5–15 remain reserved + } +} + +// --------------------------------------------------------------------------- +// Multihop control plane (SWIP-61) +// --------------------------------------------------------------------------- + +// Parent -> child: re-point, make-before-break — the child Subscribes to the +// new parent and drops the old stream only once the new one is live. +message Reparent { + bytes to = 1; // overlay address of the new parent +} + +// Parent -> child: find free slots below. depth is the depth of the receiver, +// incremented at each forwarding hop — no node stores its own position. +message Probe { + uint32 depth = 1; +} + +// Child -> parent: probe reply — at most two, smallest depth first. +message Candidates { + repeated Candidate candidates = 1; +} + +message Candidate { + bytes addr = 1; // overlay address of a node with a free slot + uint32 depth = 2; // its depth at probe time +} + +// Keepalive / RTT: none at the BPS level. Liveness is the transport's job +// (libp2p). diff --git a/SWIPs/swip-61.md b/SWIPs/swip-61.md new file mode 100644 index 00000000..f8bf10c0 --- /dev/null +++ b/SWIPs/swip-61.md @@ -0,0 +1,373 @@ +--- +SWIP: 61 +title: BPS multihop — FCFS multicast tree +author: Viktor Trón (@zelig), Viktor Tóth (@nugaon) +discussions-to: https://discord.gg/Q6BvSkCv +status: Draft +type: Standards Track (Networking) +created: 2026-08-04 +requires: 60 +--- + +- **Business line**: audiences beyond one node's capacity — live streaming and event + fan-out scaled by recruiting subscribers as relays, so a cohort's capacity grows with + its audience instead of being fixed by the broker's stream limit — and streams that + survive relay churn without interruption. +- **Dev line**: fill three reserved `Broadcast` control-plane fields of + [bps.proto](assets/swip-61/bps.proto) (`Reparent`, `Probe`, `Candidates`), extend `Ack` with + attachment candidates, and implement three behaviours — subtree probing, dual-parent + maintenance, make-before-break reparenting; done when a cohort scales past a single + node's capacity, masks a single relay failure with no delivery gap, and unmodified + SWIP-60 clients attach as leaves. +- DISC: the first time proper **forwarding by consumers** (subscribers as + relays) is implemented; a wire-protocol change (reserved frames become normative), + no storage or Bee-API change. +- Bandwidth-incentive integration is a separate SWIP (bps-bw-incentives). + +## Simple Summary + +SWIP-60 bounds a topic's audience by one broker's per-topic capacity. This SWIP removes +the ceiling: a full node answering a `Subscribe` **probes its own subtree over the +streams it already has** and returns the two shallowest free attachment points — and +the joiner attaches to **both**. Every node thus keeps two parents (typically sister +nodes): both feed it, duplicates are absorbed by SWIP-60's dedup rule, and the loss of +any single relay or link is masked, not suffered — the subtree below never notices, +while the affected node self-heals by re-running the join. A subscriber that accepts a +child thereby becomes a **relay**; the tree grows first-come-first-served, with no +control traffic while nobody is joining. End-to-end authentication is unchanged: a +relay can withhold, never forge — and with two feeds to compare, withholding shows. + +## Motivation + +One principle carries over from SWIP-60 and does all the work here: **forwarders are +subscribers**. A forwarder is simply a subscriber that fans out messages to subscribers +of its own; singlehop is the depth = 1 variant of the same structure, and the broker +remains the root in every configuration. Scaling to large audiences is then not a new +protocol but the removal of one restriction — the singlehop rule "at capacity, refuse +with `FULL` and nothing else" is replaced by referral to where capacity is. Placement +quality is explicitly **not** the join protocol's problem: FCFS means the first free +slot found wins. What multihop must not compromise is the contract itself: messages +arrive at **all subscribers** — a live stream that drops messages on relay churn is +broken, so churn tolerance is built into the topology, not patched on. + +## Specification + +### The seam from SWIP-60 + +Everything in SWIP-60 stands — cohort genesis (`CohortSpec` untouched), bindings and +dedup rules, publisher legitimacy, the `Open`/`Subscribe`/`Ack` establishment, +self-contained SOC frames, end-to-end verification — with exactly one amendment: + +**The capacity rule.** A relaying node at capacity answering a `Subscribe` MUST NOT +bare-refuse; it probes its subtree and answers `Ack(FULL)` **with the two shallowest +attachment points found** (new `candidates` field). A SWIP-60-only client ignores the +unknown field and reads a plain refusal. + +There is **no depth bound**: the tree grows as deep as joins take it. Depth is +self-limiting — per-hop pricing (bps-bw-incentives) charges upstream by depth, so +joiners are economically steered toward the root; a protocol bound would only duplicate +the price signal. And the one configuration that genuinely needs depth = 1 — the closed +cohort, where all and only publishers subscribe — is **structurally** singlehop already: +publishers connect directly to the root. + +Invariant: **publishers (and the opener) still connect directly to the root.** Multihop +restructures the audience side only. + +### Roles + +- **Broker** — as in SWIP-60: root of the tree, target of `Open`, sole entry point for + publishers, default entry point for joins. +- **Relay** — a subscriber with children: re-broadcasts every SOC frame down its own + direct streams, answers `Subscribe` like any parent, and forwards and aggregates + probes. There is no promotion step: **accepting your first child is what becoming a + relay means.** Relay capability is latent in every subscriber — the `Ack`-echoed + `CohortSpec` from its own join is exactly what it echoes to children of its own. Any + full-node subscriber is relay-eligible; NAT'd and light-client relays are reachable + if the transport can dial them (see NAT'd relays below), not required — a node unable + or unwilling to relay simply has capacity 0 and refuses. +- **Leaf** — a subscriber with no children; the SWIP-60 subscriber role verbatim. A + SWIP-60 client is a conformant leaf (single-parented — it forgoes the resilience, + not the stream). + +### Join: probe and attach (FCFS) + +A joiner sends `Subscribe(topic)` to the broker (or any cohort node it already knows). +A node with a free slot accepts on the spot: `Ack(OK, CohortSpec)`. A full relaying +node **searches its own subtree before answering** — over the streams it already has: + +1. **Probe down.** The full node sends `Probe{depth}` to its children, `depth` being + the receiver's depth (root's children = 1). A full child forwards the probe to its + own children, incrementing `depth`; a child with a free slot replies instead of + forwarding; a capacity-0 child replies empty (or not at all — see below). +2. **Filter up.** A node with a free slot answers `Candidates` naming itself + `{addr, depth}`. A forwarding node collects its children's replies within a bounded + wait (parameter), keeps the **two smallest-depth** candidates, and passes + them up. +3. **Answer.** The originating node answers the joiner `Ack(FULL, candidates)` — the + two shallowest attachment points of its subtree. No candidates ⇒ bare `Ack(FULL)`: + the probe came back empty, the subtree genuinely cannot host the joiner. +4. **Attach — to both.** The joiner `Subscribe`s at **both candidates**: the two + resulting streams are its two parents (see Resilience). A candidate lost to a race + is replaced by re-`Subscribe`ing at the origin. **A join is ideally a two-step + process** — one round to learn where, one to attach — regardless of tree size. + +The probe wave stops at the first non-full node on every path: nothing below a free +slot is ever searched. Depth travels **in the probe itself**, incremented hop by hop — +no node knows or stores its own position, so there is nothing to go stale when the tree +is reorganised. And there is no standing state anywhere: an idle cohort is perfectly +silent, state is gathered on demand and discarded. A node MAY reuse candidates from a +just-completed probe for immediately subsequent joins instead of re-probing. + +Preferring shallow slots is deliberate: min-depth filtering fills the tree +breadth-first, keeping delivery paths short — and it agrees with the incentive gradient +(upstream pricing is depth-scaled), so the protocol default and the economics point the +same way. It is still FCFS, not optimality: candidates race, replies are best-effort +within the wait bound, and the first slot won wins. + +### Resilience: two parents + +Churn-related misses are not tolerated: the contract says messages arrive at all +subscribers, and a live stream cannot pause for tree repair. So redundancy is +structural: + +- **Every node maintains two parents** — the probe's two candidates; by min-depth + filtering these sit at (near-)equal depth, typically **sister nodes**. The two + parents MUST be distinct; while the cohort is too small to offer two (e.g. the + broker's first children), a node stays single-parented and upgrades when it can. +- **Both parents feed.** Each parent treats the node as an ordinary child and streams + every frame; frames are self-contained SOCs and the binding's dedup rule (SWIP-60) + suppresses the duplicate — this is the make-before-break overlap made permanent. +- **Single failure masks.** If a relay or a link fails, every child of the failed node + keeps receiving through its other parent, so **the subtree below notices nothing**; + masking is recursive — each level's redundancy protects the level below. +- **Self-healing is the join protocol re-run.** The half-orphaned node acquires a + replacement second parent by `Subscribe`ing at its surviving parent (accepted + directly, or answered with fresh candidates via probe). No dedicated repair + machinery. +- **Withholding becomes visible.** With two independent feeds, a parent whose stream + persistently lags its sister's is caught by comparison; the node drops it and + replaces it the same way — demotion, not detection heroics. + +The price is stated, not hidden: each subscription consumes two tree slots and every +message crosses every level twice, roughly doubling bandwidth. Resilience is bought, +and per-hop pricing (bps-bw-incentives) is what will price it; an active/standby +variant (second parent connected but mute until asked) trades the gap-free guarantee +for half the traffic and is deliberately **not** specified here. + +### Reparenting (`Reparent`) — make-before-break + +`Reparent{to}` is the parent→child instruction to move: the child `Subscribe`s to the +new parent, waits for `Ack(OK)` confirming the stream is live, and **only then** drops +the old stream. During the overlap the dedup rule absorbs doubles, as everywhere. + +Two cases: + +- **graceful leave** — a departing relay `Reparent`s each child toward a replacement + (its own parents, or spread over its children **(?)**) before closing; the child's + flow continues on its other parent throughout the move; +- **churn repair** — is the *absence* case: BPS has no keepalive, so a parent's death + surfaces as a transport-level stream reset or connection loss; no `Reparent` arrives, + the surviving parent carries the stream, and the node re-runs the join for a + replacement. + +Only a *simultaneous* loss of both parents opens a delivery gap; that is then a +liveness fault like any other — repaired by rejoin from the broker, with no recovery of +the gap itself (a resumed subscriber is in the same position as a fresh one). + +### NAT'd relays + +Reaching a NAT'd relay is the transport's business, not this protocol's: libp2p circuit +relay + DCUtR hole punching (the dcutr work item, bee +[#5355](https://github.com/ethersphere/bee/issues/5355)). BPS carries no signalling for +it — the only tree-relevant observation is that a NAT'd child's **parent is its natural +circuit relay**, so a candidate a probe returns can be handed out under an address the +transport knows how to dial. + +### Information flow (probe and attach) + +```mermaid +sequenceDiagram + autonumber + participant J as joiner + participant B as broker (root, full) + participant C as child C (full) + participant X as grandchild X (free slot) + participant Y as grandchild Y (free slot) + + J->>B: Subscribe(topic) + Note over B: at capacity — probe the subtree
over existing streams + B->>C: Probe{depth: 1} + C->>X: Probe{depth: 2} + C->>Y: Probe{depth: 2} + X-->>C: Candidates[{X, 2}] + Y-->>C: Candidates[{Y, 2}] + C-->>B: Candidates[{X, 2}, {Y, 2}] (min-2 filtered) + B-->>J: Ack(FULL, candidates: [{X, 2}, {Y, 2}]) + par attach to both + J->>X: Subscribe(topic) + X-->>J: Ack(OK, CohortSpec) + and + J->>Y: Subscribe(topic) + Y-->>J: Ack(OK, CohortSpec) + end + Note over J,Y: two parents, sister nodes —
either may fail unnoticed +``` + +### Process: a subscriber's lifecycle + +```mermaid +flowchart TD + S["Subscribe at broker or
any known cohort node"] --> Q{"free slot?"} + Q -- "yes" --> A["Ack(OK, CohortSpec)
attached"] + Q -- "no — relaying node" --> P["probe subtree
over existing streams"] + Q -- "no — capacity 0" --> F["bare FULL"] + P --> C{"candidates
found?"} + C -- "two" --> D["attach to both:
two parents, sisters"] + C -- "none" --> F + A --> L + D --> L["LIVE: both parents feed,
dedup absorbs doubles"] + L -- "one parent fails
or withholds" --> M["survivor carries the stream —
subtree below notices nothing"] + M --> R["re-run join at survivor:
replacement second parent"] + R --> L + L -- "both parents fail" --> G["liveness fault"] + G -- "rejoin from broker" --> S +``` + +### Wire protocol + +Amendments to [bps.proto](assets/swip-61/bps.proto) — three reserved `Broadcast` control-plane fields +become normative, the rest stay reserved; control frames ride the `Broadcast` envelope +in **both directions** on a (peer, topic) stream (`Candidates` flows child→parent); +frame type and direction disambiguate. On submission the amended file goes to +`assets/swip-61/`. `CohortSpec` is untouched: + +```proto +message Ack { + Status status = 1; + CohortSpec cohort = 2; // set iff status == OK + repeated Candidate candidates = 3; // set iff FULL at a relaying node: the two + // shallowest attachment points the probe found +} + +message Broadcast { + oneof frame { + Soc soc = 1; // as SWIP-60 + Reparent reparent = 2; // parent -> child + Probe probe = 3; // parent -> child: find free slots below + Candidates found = 4; // child -> parent: probe reply, min-2 filtered + // 5–15 remain reserved + } +} + +// parent -> child: re-point, make-before-break +message Reparent { + bytes to = 1; // overlay address of the new parent +} + +// depth of the receiver, incremented at each forwarding hop — +// no node stores its own position +message Probe { + uint32 depth = 1; +} + +// at most two, smallest depth first +message Candidates { + repeated Candidate candidates = 1; +} + +message Candidate { + bytes addr = 1; // overlay address of a node with a free slot + uint32 depth = 2; // its depth at probe time +} +``` + +## Rationale + +**Why no standing capacity state — beacons, slot counts, subtree summaries?** Because +any standing summary must be kept fresh on every edge forever and is stale by +construction. The probe gathers exactly the state one join needs, at the moment of +needing it, and discards it; an idle cohort is perfectly silent — the same philosophy +that keeps keepalives and capacity advertisement out of BPS altogether (SWIP-60). + +**Why does the tree search, and not the joiner?** Cost asymmetry. A probe hop rides a +stream that already exists — one frame each way; a joiner's probe is a fresh dial to a +stranger — connection setup, handshake, possibly NAT traversal. Letting the tree run +the search over its own edges and handing the joiner two ready candidates turns a chain +of expensive dials into a wave of cheap frames plus two dials. + +**Why two live parents rather than a standby?** Because the failure this design must +mask is precisely the one a standby cannot: messages sent during the window between a +parent dying and anyone noticing. There is no keepalive to notice with — liveness is +the transport's job, and transport-level detection has latency. Two always-on feeds +close that window structurally, cost 2×, and come with a side effect no standby offers: +withholding is caught by comparing the feeds. + +**Why no depth bound?** Because depth already has a price. Per-hop publish pricing +(bps-bw-incentives) is depth-scaled upstream, so joiners are economically steered +toward the root and deep chains cost their occupants; a protocol-level `max_depth` +would duplicate that signal with a blunter instrument — and give the tree a notion of +"full" it doesn't otherwise need. The closed cohort, the one shape that must stay +depth 1, is structurally singlehop: publishers connect directly to the root. + +**Why do relays get nothing here?** They do — later. Metered per-hop pricing is +bps-bw-incentives' business; this SWIP keeps the edges it will price. Until then, +relaying is the same volunteer economics as singlehop brokering. + +**Why is withholding still tolerable with more intermediaries?** Unchanged from +SWIP-60 in principle — authentication is structural (SOC signature against the topic +binding, verified against the `Ack`-echoed `CohortSpec` at any depth), so adding relays +adds withholding points but no forging points — and strengthened in practice: a +withholding parent is exposed by its sister's feed and replaced by the ordinary +self-healing path. + +## Out of scope (deliberately) + +Paying for forwarded messages (bps-bw-incentives); message history — delivery of +messages from before the subscription (bps-history); broker discovery (SWIP-59 MEX); +NAT traversal — circuit relay and hole punching are transport business (dcutr item); +dynamic publisher-list changes (out of scope in SWIP-60, unaffected here). + +## Conformance (definition of done) + +An implementation is conformant when: + +1. a relaying node at capacity answers `Subscribe` with `Ack(FULL, candidates)` — the + two smallest-depth attachment points its probe returned; bare `FULL` only on an + empty probe, or from a node that cannot or will not relay; +2. a probe wave terminates: nodes with a free slot reply and do not forward, and + forwarding nodes answer within the wait bound with the min-2-by-depth filter of what + arrived; +3. an accept carries the echoed `CohortSpec`; +4. absent races, a join completes in two steps: one probe round, one (dual) attach; +5. a node offered two distinct parents maintains both; duplicate deliveries never + surface past the binding's dedup rule; +6. the failure of any single relay or link causes no delivery gap anywhere below it; + the half-orphaned children re-acquire a second parent via the ordinary join; +7. every reparent is make-before-break; +8. an unmodified SWIP-60 client attaches as a leaf anywhere in the tree and cannot + distinguish its parent from a singlehop broker; +9. a subscriber at any depth re-verifies every message end-to-end; loss of both + parents is a liveness fault repaired by rejoin. + +## Backwards compatibility + +Same stream name `pubsub/1.0.0`, no version bump: `Reparent`, `Probe` and `Candidates` +fill `Broadcast` oneof fields reserved by SWIP-60, and the single new `Ack` field +is invisible to old implementations — an old client ignores `Ack.candidates` and reads +a plain `FULL`. A SWIP-60 leaf that receives a `Probe` ignores the unknown frame and +never replies — exactly the empty answer the bounded wait absorbs, which is why old +leaves are compatible by construction (single-parented, without the resilience). +`CohortSpec` is untouched. SWIP-60's conformance item 4 ("`FULL` — and nothing else") +is superseded for nodes implementing this SWIP. + +## References + +[SWIP-60](https://github.com/ethersphere/SWIPs/pull/104) / PR +[#104](https://github.com/ethersphere/SWIPs/pull/104) · wire: [bps.proto](assets/swip-61/bps.proto) · +origin: [PR #93](https://github.com/ethersphere/SWIPs/pull/93) "Add: pubsub" · +implementation groundwork: bee [#5435](https://github.com/ethersphere/bee/pull/5435) · +dCUTr: [bee#5355](https://github.com/ethersphere/bee/issues/5355) + +## Copyright + +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/).