Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9068012
feat(bps): initial commit
acud Sep 23, 2026
6955d99
chore: strip protobuf
acud Sep 23, 2026
e2eb9cd
chore: improve structure
acud Sep 24, 2026
995dfb1
chore: general outline
acud Sep 24, 2026
1e5151d
chore: small things
acud Sep 24, 2026
f0749b4
chore: simplify further
acud Sep 24, 2026
12e7832
chore: cleanups
acud Sep 24, 2026
91f755f
refactor(bps): protocol registry
acud Sep 25, 2026
1f1cfb3
chore: condense, simplify
acud Sep 25, 2026
b7de350
chore: tests, improvements
acud Sep 25, 2026
b348979
chore: bps api
acud Sep 29, 2026
e17dfea
feat(bps): cohort spec with binding and principal, identity per strea…
acud Oct 3, 2026
7681119
feat(bps): align with SWIP-74 rev 8 (BPS-lite) (#5644)
acud Oct 9, 2026
a11cca6
chore(bps): rename and update the options
zelig Oct 10, 2026
3298698
chore(bps): rename cohort.lastActivity to inactiveSince
zelig Oct 10, 2026
4260c10
fix(bps): refuse a frame at index 2^64-1
zelig Oct 10, 2026
28ec4a6
feat(bps): the AUTH is the only way to authenticate a stream
zelig Oct 10, 2026
e6803b7
fix(bps): the bridge checks a frame in the broker's order, and refuse…
zelig Oct 10, 2026
04f1f37
docs(bps): say what the soc in a Broadcast is
zelig Oct 13, 2026
00abb9f
test(bps): fuzz the Broadcast verify path
Copilot Oct 13, 2026
a6562a6
refactor(api): one string for the bps upgrade failure
zelig Oct 13, 2026
84a6f56
refactor(bps): refuse derives the Ack status from its cause
zelig Oct 13, 2026
6033682
docs(bps): explain the fan-out and why the resets run after the lock
zelig Oct 13, 2026
e20f567
feat(bps): make the broker role opt-in with bps-broker
acud Oct 19, 2026
6af7649
feat(bps): explicit multi-publisher mode (SWIP-87) (#5661)
zelig Oct 20, 2026
733dc47
feat(bps): a forging broker is blocklisted for ten days, a named cons…
zelig Oct 22, 2026
cca78d1
feat(api): a subscriber passes back its roster and a cursor per grantee
zelig Oct 22, 2026
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
2 changes: 2 additions & 0 deletions cmd/bee/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const (
optionNameChequebookVerification = "chequebook-verification"
optionNameChequebookMinBalance = "chequebook-min-balance"
optionNameFullNode = "full-node"
optionNameBpsBroker = "bps-broker"
optionNameLightNodeLimit = "light-node-limit"
optionNamePostageContractAddress = "postage-stamp-address"
optionNamePostageContractStartBlock = "postage-stamp-start-block"
Expand Down Expand Up @@ -364,6 +365,7 @@ func (c *command) setAllFlags(cmd *cobra.Command) {
cmd.Flags().Bool(optionNameChequebookVerification, false, "reject full-node hive/handshake records that carry no chequebook address")
cmd.Flags().String(optionNameChequebookMinBalance, "110000000000000000", "minimum chequebook token balance required for verification, in token small units (default 11 BZZ)")
cmd.Flags().Bool(optionNameFullNode, false, "cause the node to start in full mode")
cmd.Flags().Bool(optionNameBpsBroker, false, "enable the bps broker (requires full node)")
cmd.Flags().Int(optionNameLightNodeLimit, 100, "light node limit")
cmd.Flags().String(optionNamePostageContractAddress, "", "postage stamp contract address")
cmd.Flags().Uint64(optionNamePostageContractStartBlock, 0, "postage stamp contract start block number")
Expand Down
1 change: 1 addition & 0 deletions cmd/bee/cmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ func buildBeeNode(ctx context.Context, c *command, cmd *cobra.Command, logger lo
AutoTLSDomain: c.config.GetString(optionAutoTLSDomain),
AutoTLSRegistrationEndpoint: c.config.GetString(optionAutoTLSRegistrationEndpoint),
FullNodeMode: fullNode,
BpsBroker: c.config.GetBool(optionNameBpsBroker),
LightNodeLimit: c.config.GetInt(optionNameLightNodeLimit),
Logger: logger,
MinimumGasTipCap: c.config.GetUint64(optionNameMinimumGasTipCap),
Expand Down
157 changes: 157 additions & 0 deletions openapi/Swarm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,163 @@ paths:
default:
description: Default response

"/bps/subscribe/{owner}/{topic}":
get:
summary: Subscribe to a BPS cohort
description: |
WebSocket. The node joins the BPS cohort `{topic, FEED_TOPIC, owner}` (SWIP-74, with
the multi-publisher mode of SWIP-87) at `broker` as `identity` and sends every
verified delivery as a binary frame
`kind (1) | owner (20) | challenge (32) | index (8, big-endian) | soc`, where `soc` is
the full single-owner chunk (`id | signature | span | payload`) signed by `owner` with
`id = keccak256(sessionTopic | index)`. `kind` is 1 (DATA): an update, by the cohort
owner or by an address on its latest roster; 4 (ROSTER): the owner's publisher set,
payload a `Roster` protobuf of 20-byte addresses; or 3 (EOS): the owner ended the
channel, after which the socket closes with 4004. `sessionTopic` is
`keccak256(topic | challenge)` for DATA and `keccak256("bps-service:v1" | kind | topic | challenge)`
otherwise. Deliveries that do not verify, by an owner the roster does not admit, or
below the subscriber's cursor are dropped.
The endpoint is read-only: any client frame closes the socket with 4002.
Close codes: 4002 invalid message, 4003 broker stream closed, 4004 end of stream.
tags:
- BPS
parameters:
- in: path
name: owner
schema:
$ref: "SwarmCommon.yaml#/components/schemas/EthereumAddress"
required: true
description: Ethereum address of the cohort owner
- in: path
name: topic
schema:
$ref: "SwarmCommon.yaml#/components/schemas/HexString"
required: true
description: 32-byte cohort topic
- in: query
name: broker
schema:
$ref: "SwarmCommon.yaml#/components/schemas/SwarmAddress"
required: true
description: Overlay address of the broker node. Must not be this node.
- in: query
name: identity
schema:
$ref: "SwarmCommon.yaml#/components/schemas/EthereumAddress"
required: true
description: Ethereum address identifying the joiner. Must not be the owner, whose stream would be a pending publisher stream; publish instead.
- in: query
name: cursor
schema:
type: integer
required: false
description: Lowest index of the owner's updates to deliver. Defaults to 0.
- in: query
name: roster
schema:
type: string
required: false
description: |
The roster the subscriber last received, with a cursor per grantee:
comma-separated `address:index` pairs, each the lowest index of that grantee's
updates to deliver (at most 186; not the owner). A dApp keeps it from the
deliveries it received and passes it back when it reconnects; it is the set
the node admits updates by until the broker delivers a later roster.
- in: query
name: rosterCursor
schema:
type: integer
required: false
description: Lowest roster index to accept — one past the index of the roster passed in `roster`. Defaults to 0.
responses:
"200":
description: Establishes a WebSocket subscription for incoming broadcasts
"400":
$ref: "SwarmCommon.yaml#/components/responses/400"
"500":
$ref: "SwarmCommon.yaml#/components/responses/500"
"503":
description: BPS service is not configured
default:
description: Default response

"/bps/publish/{owner}/{topic}":
get:
summary: Publish to a BPS cohort as its owner or as a rostered seat
description: |
WebSocket. The node joins the BPS cohort `{topic, FEED_TOPIC, owner}` (SWIP-74, with
the multi-publisher mode of SWIP-87) at `broker` declaring `identity` — the owner by
default, or an address the owner's roster names or will name (a seat). A seat's
stream receives from the start, like a subscriber's; once a roster naming the seat
has reached it, the server sends `{"type":"granted"}` and the seat may send its
AUTH, which upgrades the stream in place. A seat that sends anything before the
grant is closed with 4002. A seat whose address a later roster drops finds the
socket closed after that roster (4003).

1. Server sends `{"type":"challenge","challenge":"<hex>","broker":"<hex>","dataTopic":"<hex>","authTopic":"<hex>"}`,
and to the owner also `"rosterTopic"` and `"eosTopic"`.
`dataTopic` is `keccak256(topic | challenge)`, the session feed for updates; the
others are `keccak256("bps-service:v1" | kind | topic | challenge)` with kind 0x02
(AUTH), 0x04 (ROSTER), 0x03 (EOS).
2. Client sends binary frames `kind (1) | index (8, big-endian) | soc`, where `soc` is
a single-owner chunk with `id = keccak256(sessionTopic | index)`, signed by
`identity` for kind 1 (DATA) and 2 (AUTH) and by `owner` for kind 4 (ROSTER) and 3
(EOS), which only the owner may send. The first frame must be the AUTH — the empty
chunk at index 0 of `authTopic`, which authenticates the stream — and must arrive
within 25 seconds — of the connection for the owner, of the grant for a seat;
sent again later it is a heartbeat. DATA frames are updates of
`dataTopic`. A ROSTER carries a `Roster` protobuf naming every address that may
publish besides the owner, full state at each index; an EOS — the empty chunk at
index 0 of `eosTopic` — ends the channel. An owner that has a roster MUST publish
it again, at its next roster index, right after its AUTH: the broker may hold
none, after an inactivity reclaim or a restart. Indexes must not be reused across
sessions; frames below the broker's cursor are dropped silently.
3. Server sends what the other publishers publish, and the owner's rosters — to
the owner too — as binary frames in the subscribe endpoint's format.

Close codes: 4001 invalid soc (an AUTH or EOS at another index or with a payload
included), 4002 invalid message (wrong frame type, any frame before the AUTH, unknown
kind, a ROSTER or EOS from a seat, a roster that does not decode, a frame from a seat
before its grant, auth timeout), 4003 broker stream closed, 4004 end of stream.
tags:
- BPS
parameters:
- in: path
name: owner
schema:
$ref: "SwarmCommon.yaml#/components/schemas/EthereumAddress"
required: true
description: Ethereum address of the cohort owner
- in: path
name: topic
schema:
$ref: "SwarmCommon.yaml#/components/schemas/HexString"
required: true
description: 32-byte cohort topic
- in: query
name: broker
schema:
$ref: "SwarmCommon.yaml#/components/schemas/SwarmAddress"
required: true
description: Overlay address of the broker node. Must not be this node.
- in: query
name: identity
schema:
$ref: "SwarmCommon.yaml#/components/schemas/EthereumAddress"
required: false
description: The address the stream publishes as. Defaults to the owner; any other may publish once the owner's roster names it.
responses:
"200":
description: Establishes a WebSocket subscription for publishing broadcasts
"400":
$ref: "SwarmCommon.yaml#/components/responses/400"
"500":
$ref: "SwarmCommon.yaml#/components/responses/500"
"503":
description: BPS service is not configured
default:
description: Default response

"/soc/{owner}/{id}":
post:
summary: Upload a Single Owner Chunk
Expand Down
2 changes: 2 additions & 0 deletions packaging/bee.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
# bootnode: ["/dnsaddr/mainnet.ethswarm.org"]
## cause the node to always accept incoming connections
# bootnode-mode: false
## enable the bps broker (requires full node)
# bps-broker: false
## bzz token contract address
# bzz-token-address: ""
## cache capacity in chunks, multiply by 4096 to get approximate capacity in bytes
Expand Down
2 changes: 2 additions & 0 deletions packaging/docker/env
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
# BEE_BOOTNODE=[/dnsaddr/mainnet.ethswarm.org]
## cause the node to always accept incoming connections (default false)
# BEE_BOOTNODE_MODE=false
## enable the bps broker (requires full node)
# BEE_BPS_BROKER=false
## bzz token contract address
# BEE_BZZ_TOKEN_ADDRESS=
## cache capacity in chunks, multiply by 4096 to get approximate capacity in bytes (default 1000000)
Expand Down
2 changes: 2 additions & 0 deletions packaging/homebrew-amd64/bee.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
# bootnode: ["/dnsaddr/mainnet.ethswarm.org"]
## cause the node to always accept incoming connections
# bootnode-mode: false
## enable the bps broker (requires full node)
# bps-broker: false
## bzz token contract address
# bzz-token-address: ""
## cache capacity in chunks, multiply by 4096 to get approximate capacity in bytes
Expand Down
2 changes: 2 additions & 0 deletions packaging/homebrew-arm64/bee.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
# bootnode: ["/dnsaddr/mainnet.ethswarm.org"]
## cause the node to always accept incoming connections
# bootnode-mode: false
## enable the bps broker (requires full node)
# bps-broker: false
## bzz token contract address
# bzz-token-address: ""
## cache capacity in chunks, multiply by 4096 to get approximate capacity in bytes
Expand Down
2 changes: 2 additions & 0 deletions packaging/scoop/bee.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
# bootnode: ["/dnsaddr/mainnet.ethswarm.org"]
## cause the node to always accept incoming connections
# bootnode-mode: false
## enable the bps broker (requires full node)
# bps-broker: false
## bzz token contract address
# bzz-token-address: ""
## cache capacity in chunks, multiply by 4096 to get approximate capacity in bytes
Expand Down
4 changes: 4 additions & 0 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethersphere/bee/v2/pkg/accesscontrol"
"github.com/ethersphere/bee/v2/pkg/accounting"
"github.com/ethersphere/bee/v2/pkg/bps"
"github.com/ethersphere/bee/v2/pkg/crypto"
"github.com/ethersphere/bee/v2/pkg/feeds"
"github.com/ethersphere/bee/v2/pkg/file/pipeline"
Expand Down Expand Up @@ -160,6 +161,7 @@ type Service struct {
resolver resolver.Interface
pss pss.Interface
gsoc gsoc.Listener
bps bps.Interface
steward steward.Interface
logger log.Logger
loggerV1 log.Logger
Expand Down Expand Up @@ -277,6 +279,7 @@ type ExtraOptions struct {
Resolver resolver.Interface
Pss pss.Interface
Gsoc gsoc.Listener
Bps bps.Interface
FeedFactory feeds.Factory
Post postage.Service
AccessControl accesscontrol.Controller
Expand Down Expand Up @@ -360,6 +363,7 @@ func (s *Service) Configure(signer crypto.Signer, tracer *tracing.Tracer, o Opti
s.resolver = e.Resolver
s.pss = e.Pss
s.gsoc = e.Gsoc
s.bps = e.Bps
s.feedFactory = e.FeedFactory
s.post = e.Post
s.accesscontrol = e.AccessControl
Expand Down
3 changes: 3 additions & 0 deletions pkg/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
mockac "github.com/ethersphere/bee/v2/pkg/accesscontrol/mock"
accountingmock "github.com/ethersphere/bee/v2/pkg/accounting/mock"
"github.com/ethersphere/bee/v2/pkg/api"
"github.com/ethersphere/bee/v2/pkg/bps"
"github.com/ethersphere/bee/v2/pkg/crypto"
"github.com/ethersphere/bee/v2/pkg/feeds"
"github.com/ethersphere/bee/v2/pkg/file/pipeline"
Expand Down Expand Up @@ -96,6 +97,7 @@ type testServerOptions struct {
Resolver resolver.Interface
Pss pss.Interface
Gsoc gsoc.Listener
Bps bps.Interface
WsPath string
WsPingPeriod time.Duration
Logger log.Logger
Expand Down Expand Up @@ -210,6 +212,7 @@ func newTestServer(t *testing.T, o testServerOptions) (*http.Client, *websocket.
Resolver: o.Resolver,
Pss: o.Pss,
Gsoc: o.Gsoc,
Bps: o.Bps,
FeedFactory: o.Feeds,
Post: o.Post,
AccessControl: o.AccessControl,
Expand Down
Loading
Loading