Skip to content

SWIP-061: BPS multihop — FCFS multicast tree - #105

Open
zelig wants to merge 4 commits into
masterfrom
swip-61-bps-multihop
Open

SWIP-061: BPS multihop — FCFS multicast tree#105
zelig wants to merge 4 commits into
masterfrom
swip-61-bps-multihop

Conversation

@zelig

@zelig zelig commented Aug 5, 2026

Copy link
Copy Markdown
Member

Extends SWIP-60 (#104): removes the singlehop audience ceiling. A full node at capacity answers Subscribe by probing its own subtree over existing streams and returning the two shallowest attachment points — the joiner attaches to both, so every node keeps two parents (typically sisters): churn is masked, not suffered, self-healing is re-running the join, and withholding shows by comparing feeds. Join is ideally a two-step process regardless of tree size.

Key properties: forwarders are subscribers (accepting your first child = becoming a relay); no standing control state — idle cohorts are silent, probes carry depth in the frame itself; no depth bound (per-hop pricing steers joins rootward; closed cohorts are structurally singlehop); CohortSpec untouched; three control frames (Reparent, Probe, Candidates) fill the reserved Broadcast range; single new Ack field. Unmodified SWIP-60 clients attach as conformant (single-parented) leaves.

Deliberately out of scope (own SWIPs): bandwidth incentives, message history (pre-subscription delivery), broker discovery (SWIP-59, #103), NAT traversal (transport: circuit relay + DCUtR).

Wire: assets/swip-61/bps.proto — the SWIP-60 proto with the multihop control plane filled in. Implementation groundwork: bee ethersphere/bee#5435.

🤖 Generated with Claude Code

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@zelig zelig self-assigned this Aug 6, 2026
@zelig zelig changed the title add SWIP-61: BPS multihop — FCFS multicast tree SWIP-061: BPS multihop — FCFS multicast tree Aug 7, 2026
…PO_MIN

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per SWIP-65 (PR #106): a delivery gap on a self-indexed cohort is
detectable and recoverable from storage, so the contract is satisfied
by deliver-or-recover. The second parent becomes a per-subscriber
latency/cost choice - single-parented nodes were already well-formed,
and a single-parented relay endangers only itself. Conformance items
5-6 bind only nodes that maintain a second parent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@zelig

zelig commented Aug 9, 2026

Copy link
Copy Markdown
Member Author

Amendment pushed (1ca57ae), companion to SWIP-65 (#106):

  • Dual parenting is not required on self-indexed cohorts: a gap there is detectable at the next frame and recoverable from storage, so the contract is satisfied by deliver-or-recover. The second parent becomes a per-subscriber latency/cost choice — single-parented nodes were already well-formed (SWIP-60 leaves), and a single-parented relay endangers only itself. Conformance items 5–6 bind only nodes that maintain one. Dual parenting remains the costlier-but-faster product for the gap-intolerant live edge.

🤖 Generated with Claude Code

@zelig
zelig marked this pull request as ready for review August 11, 2026 07:55
Copilot AI lite review requested due to automatic review settings August 11, 2026 07:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces SWIP-61, extending the BPS (Broadcast Pub/Sub) family beyond the SWIP-60 single-hop audience ceiling by specifying a multihop, FCFS multicast tree join/control plane with subtree probing and dual-parent attachment for churn masking.

Changes:

  • Adds the SWIP-61 specification document describing probe-based joining, two-parent resilience, and make-before-break reparenting.
  • Adds the SWIP-61 wire specification (bps.proto) defining Reparent, Probe, Candidates, and Ack.candidates.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
SWIPs/swip-61.md New SWIP-61 spec describing multihop join/probe behavior, dual-parent resilience, and reparenting semantics.
SWIPs/assets/swip-61/bps.proto Proto definitions for SWIP-61 control frames and the Ack extension carrying attachment candidates.
Suppressed comments (3)

SWIPs/swip-61.md:381

  • Inconsistent acronym casing: dCUTr here should match the DCUtR spelling used earlier in the document.
dCUTr: [bee#5355](https://github.com/ethersphere/bee/issues/5355)

SWIPs/assets/swip-61/bps.proto:84

  • The Subscribe comment is broker-specific, but in multihop a joiner can Subscribe to any known cohort node (broker or relay). Keeping the comment broker-only is misleading for implementers.
// 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).

SWIPs/assets/swip-61/bps.proto:96

  • The Ack comment is broker-specific, but in multihop relays also answer Subscribe with Ack. Adjust wording to cover any cohort node responding on the stream.
// Broker -> peer, answering Open or Subscribe. The echoed CohortSpec lets a
// subscriber verify every message end-to-end against the topic binding.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread SWIPs/swip-61.md
Comment thread SWIPs/swip-61.md
Comment on lines +71 to +74
// 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.
// ---------------------------------------------------------------------------
Comment thread SWIPs/assets/swip-61/bps.proto Outdated
Comment on lines +2 to +3
// Spec: swip-bps-singlehop.md (SWIP-60, base protocol) and
// swip-bps-multihop.md (SWIP-61, multihop control plane).
Comment thread SWIPs/swip-61.md Outdated
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
…pth 1

Realigns SWIP-61 with SWIP-60 rev 4 and lifts the invariant that confined
publishers to the root.

## Two amendments to SWIP-60, not one

1. The capacity rule (unchanged in substance): a relaying node at capacity
   answers Ack(FULL, candidates).
2. NEW — Publish travels rootward, forwarded hop by hop to the broker, which
   validates, dedups and broadcasts it back down. SWIP-60's direct attachment
   of publishers was a consequence of depth = 1, not an invariant. Multihop now
   restructures both directions, which is what an everyone-publishes cohort
   (SWIP-60's ALL, and any GRANTED cohort larger than one node's capacity)
   needs in order to exist at all.

Open still goes to the broker — opening a cohort IS contacting the root — but
its admin is thereafter an ordinary publisher and may sit at any depth.

New section "Publishing from depth" covers: validation authoritative at the
root (dedup is a statement about what the cohort has seen, and only the root
sees everything) while relays MAY and SHOULD run the stateless checks early;
publishing up BOTH parents, so a single relay cannot silently swallow a publish
— the upstream twin of what two feeds buy downstream; and depth priced rather
than bounded.

Revocation across a tree: the roster update is an ordinary broadcast, so
enforcement lands at the revoked publisher's parents, and the revokee learns of
its own revocation from the same broadcast at the same time — which is what
makes SWIP-60's second phase fair at any depth.

## The depth-bound argument, re-anchored

It previously rested on the closed cohort being "structurally singlehop". That
anchor is gone twice over: `closed` no longer exists in SWIP-60, and publishers
no longer attach to the root. NO configuration requires depth 1. The argument
now stands on the price alone — and is stronger, since depth-scaled upstream
pricing bills a publisher for its depth, the case where the incentive bites
hardest.

## What a relay owes a joiner

SWIP-60 rev 4's Ack carries the echoed CohortSpec, the admin-signed genesis SOC
and the latest service SOC with its index. A relay MUST pass on all three
unchanged: depth must cost a subscriber nothing in what it can verify, so a
leaf ten hops out checks cohort and roster against the admin's key exactly as
the broker's own child does. Extra intermediaries add withholding points and no
forging points; a relay serving a stale service SOC is withholding, detected as
an index gap. `spectators: false` cohorts work at depth for the same reason —
any node answering a Subscribe holds the roster.

## Wire

bps.proto regenerated from SWIP-60 rev 4 rather than patched, so the two files
no longer drift. Ack.candidates takes field 6, NOT 3 — fields 3-5 are SWIP-60's
service SOCs, which the old draft would have collided with.

## Review items

- Copilot: front matter split into `type: Standards Track` + `category:
  Networking`; proto header pointed at swip-60.md / swip-61.md instead of the
  nonexistent swip-bps-*.md; establishment comments corrected to say any cohort
  node answers Subscribe, not only the broker; DCUtR casing normalised.
- The `(?)` on graceful leave is resolved: a departing relay reparents children
  to its own parents — known-live, known-compatible, one level shallower.
  Handing children to one another would deepen the tree and has no answer for
  the first child.
- Both mermaid diagrams verified to parse (mermaid 11), after the semicolon
  defect found in SWIP-60's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@zelig

zelig commented Aug 30, 2026

Copy link
Copy Markdown
Member Author

Rev 2 pushed (00aa789), realigning this SWIP with SWIP-60 rev 4 and lifting the invariant that confined publishers to the root.

Two amendments to SWIP-60 now, not one

The capacity rule is unchanged. The new one: Publish travels rootward, forwarded hop by hop to the broker, which validates, dedups and broadcasts it back down.

SWIP-60's direct attachment of publishers was a consequence of depth = 1, not an invariant — so multihop now restructures both directions rather than the audience side only. That is what an everyone-publishes cohort needs in order to exist at all: SWIP-60's ALL (group chat), and any GRANTED cohort larger than one node's capacity, were previously capped by the broker's stream limit on the publisher side no matter how far the tree grew.

Open still goes to the broker — opening a cohort is contacting the root — but its admin is thereafter an ordinary publisher and may sit at any depth.

A new section, Publishing from depth, settles three things:

  • Validation is authoritative at the root, because dedup is a statement about what the cohort has seen, and only the root sees everything: a relay applying the rule locally would suppress a message some sibling subtree never received. Relays MAY and SHOULD run the stateless checks early — signature, binding, authorship — since carrying a doomed frame up d hops wastes every one of them. Stateless checks anywhere, stateful ones where the state is. A relay that forwards everything is still conformant; one that drops a valid frame is withholding.
  • Publish up both parents. The upstream twin of the two downstream feeds: a single relay cannot silently swallow a publish, and the dedup rule absorbs the double at the sisters' shared parent.
  • Depth is priced, not bounded — see below.

Revocation across a tree: the roster update is an ordinary broadcast, so enforcement lands at the revoked publisher's parents — they are the nodes accepting its Publish frames — and the revokee learns of its own revocation from the same broadcast at the same time. That is what makes SWIP-60's two-phase rule fair at any depth.

The "why no depth bound" argument, re-anchored

It used to rest on the closed cohort being structurally singlehop. That anchor is gone twice over: closed no longer exists in SWIP-60, and publishers no longer attach to the root. No configuration requires depth 1.

The argument now stands on the price alone, and is stronger for it — with Publish travelling rootward, depth-scaled upstream pricing bills a publisher for its depth, which is the case where the incentive bites hardest.

What a relay owes a joiner

SWIP-60 rev 4's Ack carries the echoed CohortSpec, the admin-signed genesis SOC and the latest service SOC with its index. A relay MUST pass on all three unchanged — it holds them from its own join, and keeps the service SOC current from broadcasts it already receives.

Depth must cost a subscriber nothing in what it can verify. A leaf ten hops out checks the cohort and the roster against the admin's key exactly as the broker's own child does, so extra intermediaries add withholding points and no forging points. A relay serving a stale service SOC is withholding, which its subscriber detects as an index gap. spectators: false cohorts work at depth for the same reason: any node answering a Subscribe holds the roster.

Wire — one collision caught

Ack.candidates is now field 6, not 3. SWIP-60 rev 4 uses fields 3, 4 and 5 for genesis, service and index, so the number this draft previously claimed would have collided with them.

assets/swip-61/bps.proto is now regenerated from the SWIP-60 rev 4 file rather than patched, so the two stop drifting.

Review items cleared

Thanks @copilot — all taken:

  • front matter split into type: Standards Track + category: Networking, matching swip-19/20
  • proto header pointed at swip-60.md / swip-61.md instead of the nonexistent swip-bps-singlehop.md / swip-bps-multihop.md
  • establishment comments corrected: any cohort node answers Subscribe, not only the broker
  • DCUtR casing normalised throughout

The **(?)** on graceful leave is resolved rather than removed: a departing relay reparents each child to its own parents — known-live, known-compatible, and one level shallower, so the subtree gets shallower rather than deeper. Handing children to one another would deepen the tree and has no answer for the first child.

Both mermaid diagrams are verified to parse under mermaid 11. Worth noting for anyone editing SWIP diagrams: a semicolon in note text silently breaks mermaid — it is a statement separator, so Note over A,B: one; two is a parse error and GitHub falls back to showing the raw source. That defect had been in SWIP-60's diagram since it was written; it is fixed there now.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants