Skip to main content
Status: Draft Type: Standards Track Category: Core Created: 2026-07-27 Requires: CIP-2 (off-chain compute), CIP-3 (fee handling), CIP-9 (CBFS), CIP-10 (runner workloads), CIP-12 (governance), CIP-24 (CBSS), CIP-31 (CBFS rent schedule, at the 2,905 nano-CBY rate), CIP-36 (§6.6 administered CBY rate) Related: CIP-7 (public streams), CIP-15 (workload ingress), CIP-29 (actor events), CIP-38 (persistent runners)

Abstract

This proposal introduces the Cowboy Queue System (CBQS), a durable off-chain messaging service for workloads running on different Cowboy Runners. CBQS provides one ordered stream primitive from which applications construct work queues, publish/subscribe, chat rooms, request/reply, and replayable event logs. Broker-side lanes let many related rooms or documents share one chain-created stream without weakening its single total order. The Cowboy chain is the control plane. It records stream ownership, provider assignment, authorization and encryption generations, configuration, and prepaid rent. A new off-chain service, cbqsd, is the data plane. It stores records, manages consumer groups and delivery leases, pushes records to connected clients, and issues signed receipts. No chain transaction is required to append, deliver, acknowledge, or replay a message. CBQS encrypts message contents end-to-end by default. The chain, CBSS, and the broker still expose communication topology and operational metadata. This CIP does not claim communication-graph secrecy.

Background

Cowboy actors and actor-to-actor messages are consensus execution. Their inputs, state transitions, and outputs are part of the public replicated state machine. They are the correct path for finality-bearing application state, but not for private, high-frequency coordination between off-chain workloads. Cowboy Runners execute jobs and persistent workloads away from consensus, but there is no shared messaging backplane between them. A coordinator that talks to several specialist agents must currently implement persistence, retry, fan-out, cursors, leases, reconnection, authorization, and crash recovery as application infrastructure. Chat and collaborative applications repeat the same work. CBFS solves durable object storage and CBSS solves authorized secret release. Neither supplies queue delivery semantics. CBQS is the corresponding messaging service.

Goal

CBQS MUST provide:
  1. cross-runner message exchange with no consensus write on the data path;
  2. ordered replay and at-least-once work delivery;
  3. push delivery with bounded, explicit backpressure;
  4. encrypted payloads whose keys are unavailable to the broker;
  5. owner recovery and revocation through chain authority;
  6. receipts that make provider equivocation and acknowledged-data loss detectable;
  7. a lower-overhead class for replayable application data that can tolerate and repair a bounded crash-loss window;
  8. cheap logical room or document creation inside one shared access-and-encryption domain.
Exactly-once application effects, communication-graph secrecy, consensus publication, semantic routing, distributed transactions, and generic Redis-style key/value or lock operations are not goals. Provider reassignment, lane metadata updates, shared durable cursors, scheduled delivery, additional CBQS-object signature algorithms, and alternate persistent transport bindings are outside v1 and require follow-up CIPs.

Specification

The key words MUST, MUST NOT, REQUIRED, SHALL, SHOULD, SHOULD NOT, and MAY in this document are to be interpreted as described in RFC 2119.

1. Terms

2. Trust model

CBQS has a chain control plane and an off-chain data plane. The chain is authoritative for stream ownership, provider assignment, provider epoch, authorization generation, encryption generation, stream status, configuration, pricing basis, provider base-rate reservations, and escrow. cbqsd is authoritative for the records and consumer state it has accepted, subject to the signed-receipt rules in this CIP. For an encrypted stream:
  • clients encrypt and decrypt payloads;
  • the broker stores ciphertext and MUST NOT receive a data key;
  • CBSS escrows the stream root for an owner-authorized rotation workload; and
  • members receive per-generation data keys in HPKE-wrapped envelopes.
Privacy is limited to content confidentiality. The following are visible:
  • chain: stream existence, owner, configuration, provider, billing, pricing basis, provider base-rate caps and reservation totals, authorization-generation changes, encryption-generation changes, and linked key-stream existence;
  • CBSS: secret policy and release activity for the rotation workload; and
  • provider: connecting workloads, stream and group identifiers, message sizes, timing, traffic volume, delivery state, and key-envelope recipients; and
  • member: a member requesting lane proofs learns, for its own lanes, whether they carried records and what those records were. About other lanes it learns only an occupancy pattern: each set bitmap bit says that some sibling prefix subtree along its own query path is non-empty, and carries that subtree’s opaque hash. popcount(bitmap) is therefore a lower bound on the number of other active lanes, and the set bits indicate which regions of the lane_id space are occupied. No other lane’s identifier, exact count, payload, or activity window is disclosed.
Self-hosting hides provider-layer metadata from the hosted operator. It does not hide chain or CBSS activity. A provider can censor, delay, or destroy data. Receipts detect some misbehavior; they do not provide availability. Provider staking and slashing are outside v1.

3. Architecture

The chain MUST NOT receive message payloads, group operations, delivery leases, acknowledgements, or cursor advances. The Stream Registry system actor is allocated at 0x17. CBQS uses one top-level system-instruction opcode:
The payload is a bounded, versioned CbqsInstruction. This preserves the remaining one-byte top-level opcode space. cbqsd exposes the data-plane protocol, verifies finalized Stream Registry state, stores records and delivery state, enforces quotas, and signs receipts. It MUST bind its durable store to one chain instance and refuse startup when the connected node reports a different chain instance. Reusing broker data after a re-genesis requires an explicit export-and-import operation that creates and verifies target-chain streams, imports application payloads under their new stream ids and receipt lineages, and writes a new chain-instance binding. Old-chain signatures and anchors remain evidence for the source history but are never accepted as target-chain objects. Changing only configuration or an environment variable is insufficient. Clients connect directly to a provider endpoint. The Cowboy Gateway is not on the message data path.

4. Chain objects

All CBQS signing public keys carry an explicit algorithm tag. v1 accepts Ed25519 for CBQS object keys. HPKE keys carry their suite tag:
The signing-algorithm tag space is reserved for future algorithms. A v1 verifier MUST reject every other tag. This restriction does not change chain account signatures. Every provider_epoch field in a v1 chain object, request, receipt, frame, or signed payload MUST equal zero. Encoders MUST emit zero and verifiers MUST reject a nonzero value. The chain instance id is the chain’s domain-separated genesis-configuration fingerprint:
genesis_source_bytes are the exact raw config bytes used to initialize production state. A node started without a genesis file uses the canonical serialization of its default genesis config. Nodes MUST derive this value from the same captured bytes they parse for initialization; a second file read is invalid because it creates a state/identity time-of-check-to-time-of-use race. Genesis initialization MUST persist the derived value in the Stream Registry’s consensus singleton. Every CBQS execution path MUST read the chain instance from that singleton rather than node-local configuration. A missing or malformed singleton, or a record whose embedded chain instance does not match it, is invalid consensus state and MUST fail closed; execution MUST NOT derive or substitute a default at read time. The node MUST expose the finalized chain instance, finalized height, and finalized block hash through one atomic RPC read:
The view identifier alone is not a snapshot API. The node MUST either return the StreamRecord, ProviderRecord, active schedule, protocol bounds, and effective_rent_height needed for one broker decision in the same atomically constructed response as FinalizedViewV1, or support exact block-hash-pinned reads for every one of those values. The atomic unit is the broker-state response, of which FinalizedViewV1 is one field: effective_rent_height is deliberately not inside the view — the view is a consensus identifier that must mean the same thing to every reader, while the effective rent height is a derived registry quantity — but it is carried in the same response and inherits the same atomicity. A node MUST reject a pinned read it can no longer serve. Returning a view and then answering follow-up reads from whatever state is current at that later instant is invalid. Every signed CBQS object carries context: CbqsSigningContextV1. A v1 verifier MUST reject any other version, numeric chain id, or chain instance before performing authorization or mutating state.

4.1 StreamRecord

The main stream id is:
chain_instance_id is the genesis-configuration fingerprint defined in §3. Genesis initialization MUST seed it into a Stream Registry singleton in consensus state before CBQS instructions can execute. Handlers MUST read that singleton rather than node-local configuration, MUST reject an absent or malformed singleton, and MUST reject any stored provider or stream record whose carried chain_instance_id differs from it. A re-genesis therefore seeds a different value even when chain_id is reused. The Stream Registry MUST reject a CreateStream whose (owner, owner_nonce) has already been used, including after the earlier stream closes or expires. An owner nonce is never reusable. An encrypted stream is created atomically with a key stream:
The key stream MUST be Standard, unencrypted at the stream layer, owned by the same account, assigned to the same provider, and linked bidirectionally to the main stream. Its records are HPKE ciphertext, not plaintext keys. The encrypted main stream’s pricing basis and escrow cover both records. The key stream mirrors the main stream’s pricing_basis, has rate_per_block = 0, cannot be funded, suspended, or closed independently, and mirrors the main stream’s lifecycle atomically. delivery_class, purpose, encrypted, the stream-id linkage, and provider assignment are immutable for the stream’s lifetime. Immutable assignment keeps chain state, receipt lineage, and provider admission accounting single-owner. Moving an application to another provider is an application migration: create a new stream, replay or copy through a verified durable anchor, regrant and rekey, redirect clients, and close the old stream. Section 16.1 applies the same flow to confidentiality-domain migration. The owner MAY change bounded retention, quota, group defaults, admin key, and other mutable configuration. An admin-key change MUST increment authorization_generation. pricing_generation is 1 at creation on both pricing bases and increments exactly once for every accepted change to a priced capacity field: a ProviderQuote stream stores the accepted quote’s bound generation (§4.4), and a GovernanceBase stream increments alongside its §5 schedule re-derivation. It never decrements and never skips a value.

4.2 StreamConfigV1

StreamConfigV1 is validated at creation and at every update, field by field, against the source named below. The source is not uniform across the record, so it is enumerated rather than stated as a rule: two fields are admission bounds checked against the LIVE governance scalar, and the rest are configuration ceilings checked against the IMMUTABLE §18 Maximum. This table is what later sections mean when they call a snapshotted stream configuration already validated. max_message_bytes, max_lanes, retention_ms, default_max_attempts and both fast batch fields MUST be nonzero. Standard-only fields are validated only for Standard streams and fast-only fields only for Fast streams. One cross-field rule joins the table’s per-field checks: for a Fast stream, max_message_bytes × fast_batch_max_records MUST NOT exceed 268,435,456 (256 MiB), checked without overflow at creation and at every update. The bound is an IMMUTABLE protocol constant, not a governance parameter, and it is what makes the §13 replay admission bound finite: a batch’s maximum legal materialization is this product, so the §18 per-field maxima — whose product would be 64 GiB — are deliberately not jointly reachable. A configuration at both per-field maxima is rejected by this rule, not admitted by the table. retained_bytes_limit, max_append_bytes_per_sec, and max_delivered_bytes_per_sec carry no §18 row and are bounded instead by the pricing path — a signed PriceQuoteV1 or the governance-base admission counters of §4.3 — so they are deliberately absent from this table. That bound is concrete, not aspirational: every priced capacity field feeds the §4.5 rate derivation with checked arithmetic, and CreateStream must pay the §6 creation charge and any escrow against the derived rate in the same transition, so a capacity large enough to distort the rate produces a charge no balance can pay and the creation — with its record and reservations — fails atomically. An absurd cap cannot create a frozen reservation. The two LIVE rows are admission bounds in §18’s sense: they gate what may be created and the accepted record never consults them again, so lowering one stops new work without re-examining anything already accepted. At every configuration change of either linked stream, key-stream retention MUST cover every encryption generation that remains replayable under the main stream’s retention policy. The Stream Registry MUST reject any update that would violate this invariant.

4.3 ProviderRecordV1

The URI scheme MUST match the transport tag: WebSocketTls uses wss:// and Https uses https://. The URI MUST be nonempty and MUST NOT contain ASCII control or whitespace bytes. A decoder MUST reject an unknown transport tag, invalid UTF-8, a mismatched scheme, or an overlong URI before allocating or using the endpoint. Decoding alone does not admit an endpoint. RegisterProvider and UpdateProvider MUST additionally validate every carried endpoint at consensus admission with the following algorithm, rejecting the instruction on the first failure. The algorithm is consensus-critical: two conforming implementations MUST accept and reject the identical URI set.
  1. The endpoint list is nonempty and at most cbqs.max_provider_endpoints long, and each URI is at most cbqs.max_provider_endpoint_uri_bytes bytes.
  2. The URI begins with the transport’s scheme prefix — wss:// for WebSocketTls, https:// for Https — compared ASCII-case-insensitively.
  3. The URI parses under the WHATWG URL Standard’s basic URL parser; a parse failure rejects. The parsed scheme MUST equal the transport’s scheme exactly, the URL MUST NOT be cannot-be-a-base, and the parsed username MUST be empty and password absent.
  4. The raw authority — the URI bytes between the scheme prefix and the first /, ?, or #, or the end of the URI — MUST NOT contain @. This is checked on the raw bytes because a parser cannot distinguish absent userinfo from an explicitly empty @host form it normalizes away.
  5. The host MUST be present, and:
    • a domain host MUST NOT be localhost and MUST NOT have localhost as its final label, compared ASCII-case-insensitively after stripping at most one trailing dot — the special-use namespace that deterministically resolves locally;
    • an IPv4 literal — including a host the parser canonicalizes to one, such as a bare decimal like 2130706433 — MUST NOT fall in 0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.88.99.0/24, 192.168.0.0/16, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, or 224.0.0.0/3;
    • an IPv6 literal MUST fall inside currently allocated global unicast 2000::/3 and MUST NOT fall in 2001::/23, 2001:db8::/32, 2002::/16, 3ffe::/16, or 3fff::/20. Every address outside 2000::/3 is rejected as a blanket rule — loopback, unspecified, IPv4-mapped (::ffff:0:0/96), unique-local, link-local, and multicast alike — so no per-range list outside global unicast exists to drift.
Admission stores the URI as registered; it does not normalize it. What admission cannot establish is DNS: a hostname admitted here may later resolve to a private or loopback address. A dialing client MUST revalidate every resolved address against the literal-address policy above after DNS resolution and MUST revalidate the destination after every redirect before connecting. The following vectors are normative: a conforming implementation MUST admit every accept URI and reject every reject URI under the transport shown.
Provider registration is permissionless and creates no slashable stake in v1. Endpoints and keys are public. A provider in Draining cannot accept new streams. A provider cannot enter Deregistered while any live stream record still assigns it. The registry maintains live_assigned_streams across both pricing bases, incrementing once for each created main stream and decrementing exactly once on its first terminal transition. A linked key stream does not increment the counter separately because its lifecycle is atomic with the main stream. base_rate_admission is the provider’s standing consent to accept new governance-base-priced streams without a per-stream quote. The provider sets its four caps through its signed registration or UpdateProvider instruction. The Stream Registry, not the provider, maintains base_rate_reserved. An update MAY set accepts_new = false at any time, but MUST NOT lower a cap below its corresponding current reservation. Re-enabling acceptance consents to the genesis base-rate schedule, which v1 never changes, until the provider opts out again. Changing a provider signing key is prospective. Every StreamRecord snapshots the key that signs its receipts. Historical receipts remain verifiable against that snapshotted key; an UpdateProvider MUST NOT reinterpret an existing stream under a new key. The owner chooses an active provider at stream creation. Automatic provider selection is outside v1.

4.4 PriceQuoteV1

The provider signs:
The quote_digest stored in PricingBasisV1::ProviderQuote is exactly that 32-byte signing digest:
The Stream Registry MUST verify the quote against the provider’s finalized quote_key, exact owner, bounds, expiry, and the requested stream configuration. In particular, retained_bytes, append_bytes_sec, and delivered_bytes_sec MUST equal the requested configuration’s retained_bytes_limit, max_append_bytes_per_sec, and max_delivered_bytes_per_sec, respectively. The quoted rate_per_block MUST be at least CBQS_MIN_RATE_PER_BLOCK (§6), and is snapshotted into the stream record. CreateStream carrying a quote records PricingBasisV1::ProviderQuote { quote_digest }. A quote is single-use because it binds the exact stream and pricing generation it may admit, not because consensus remembers digests: stream_id MUST equal the id of the stream the accepting instruction creates or names, and pricing_generation MUST equal 1 at CreateStream and exactly the record’s pricing_generation + 1 at UpdateStream, which stores the accepted quote’s generation inside the same atomic transition, together with all admission accounting. A replayed quote fails the generation equality, a quote for one stream cannot admit another, and no consumed-quote marker exists to store. An explicit create/update action tag would be redundant: a record’s pricing_generation is never below 1, so a creation quote can never satisfy an update, and an owner nonce is never reusable (§4.1), so the stream_id a creation quote binds — derivable by both parties before creation from the §4.1 formula — can be created at most once, ever. The validity height bounds when that one admission may occur; it does not make the quote reusable. A request without a quote is valid only through the governance base-rate path below.

4.5 Base-rate schedule

Rates are denominated in nano-CBY. Escrow is a native-balance position. USD figures quoted here are the administered CBY rate defined in CIP-36 §6.6 applied to these constants, not properties of the constants; §6.6 is that rate’s only definition and this section does not restate it. At the current rate the schedule below prices retained bytes at ~1.95x the CIP-31 storage rate this document requires (2,905 nano-CBY/MiB/epoch), and one append-throughput unit alone at roughly half the monthly price of a Kinesis shard; a symmetric 1 MiB/s in-and-out configuration, including the delivered dimension, is ~2.8x a shard. Genesis seeds schedule version 1, and v1 fixes it for the life of the chain; the frozen semantics and their rationale are stated at the end of this section:
The schedule digest is:
Both the stored PricingBasisV1::GovernanceBase.schedule_digest and every atomically returned active-schedule view MUST use this derivation. For the requested delivery class, the Stream Registry derives:
max_delivered_bytes_per_sec prices egress, which the first two dimensions do not reach. max_append_bytes_per_sec bounds ingress only, while every consumer group reads the whole stream and cbqs.max_groups_per_stream defaults to 4,096 — so without this term one stream can draw up to 4,096x its provisioned bytes in egress at no additional rent, against a provider whose dominant cost is precisely that egress. CIP-31 §2 already meters the same quantity for CBFS as TRANSFER_FEE_PER_MIB; this is the queue-side equivalent. It is provisioned rather than measured, so no trusted per-byte meter is introduced. The same three configuration fields are charged by this formula and reserved against the provider’s base-rate admission counters, which carry a matching max_total_delivered_bytes_per_sec bound and a delivered_bytes_per_sec reservation total. Pricing a dimension the provider cannot bound would let it be committed past capacity, which is what the admission counters exist to prevent. Unit sizes MUST be nonzero and every multiplication and addition MUST be checked. Every governance-base rate derivation — at CreateStream and at every UpdateStream repricing alike — MUST reject a configuration whose three priced capacity fields are all zero, and MUST otherwise clamp the derived rate upward to the §6 floor:
An all-zero-capacity configuration buys nothing and would otherwise mint an immortal stream — rent never accrues, suspension and DataExpired are unreachable, only the owner can ever close it, and each one consumes a reserved_streams unit the provider can neither shed nor shrink beneath. The clamp handles every other cheap shape: a retained-only configuration whose derived rate would be 67 nano-CBY per block pays the 8,000 floor instead. A stream with accepted rate r stays Active for escrow_balance / r blocks; escrow is attacker-chosen, so that duration is attacker-chosen, but the burn rate is floor-bounded. Two economics bound that funded phase. First, the floor: the cheapest possible stream pays 8,000 nano-CBY per block, about 20.7 CBY/month at one-second blocks, and the cheapest append-capable configuration — which must reserve at least matching delivered throughput — pays 80,000 nano-CBY per block for Standard or 74,700 for Fast, about 207.36 or 193.62 CBY/month. Second, the §6 creation charge: every creation burns rate_per_block × CBQS_CREATION_CHARGE_BLOCKS (at least ~4.84 CBY), so a record’s existence costs unrecoverable capital before its first block of rent. The pinned grace deadline governs Suspended streams only; throughout a funded window the record is Active with both suspension fields absent, and what bounds that window is the funded escrow itself, burning at or above the floor into the rent split. Provider base-rate admission caps are a count bound on top of those economics, not a substitute for them — §4.3 forbids a provider lowering a cap below its current reservation, so caps are consent given before exposure, not a lever during it. There is no free-stream path: a quote below the floor is invalid (§4.4), and the zero-escrow creation §6 permits still pays the creation charge. Genesis seeds the schedule, and in v1 it is frozen. The twelve cbqs.base_rate.* rows in §18 are genesis-validation values, not live governance parameters: no §5 instruction and no governance parameter write re-derives the stored schedule, they sit outside §18’s governance-bounds read, and a parameter-store write to those paths would change nothing a CBQS handler reads. CBQS’s retained-byte cost basis tracks the CBFS rate (cip31.cbfs.storage_fee_per_mib_per_epoch, which is live Tier-0 under CIP-31) in practice, and nothing reconciles the two automatically: repricing CBQS to follow a CBFS change is a coordinated-release protocol change specified by a later CIP, exactly like the surcharge constants, and that CIP is where a mutable-schedule mechanism with notice semantics belongs. Until then a CBFS rate increase narrows CBQS provider margins for the life of the schedule — a disclosed, accepted v1 exposure. The 0x03 record stores the canonical encoding of BaseRateSchedulesV1 { active: BaseRateScheduleV1, pending: Option<BaseRateScheduleV1> }, the container the protocol codec defines. The genesis schedule MUST carry schedule_version = 1 and activates_at_block = 0 with pending absent, and genesis validation MUST reject any other values, so the stored record is exactly 162 canonical bytes — 161 for active plus one absent-option byte — and the record, the genesis state root, and every GovernanceBase schedule_digest derived from it are identical on every conforming node. The schedule_digest preimage covers the 161-byte BaseRateScheduleV1 alone, never the container, so the pinned 187-byte digest preimage and the 162-byte stored record are different byte strings by construction. The active schedule at height H is the container’s active_at(H) selection: pending when present and H >= pending.activates_at_block, else active. v1 has no writer of pending, so the switch is unreachable — but an implementation MUST select through active_at rather than read active unconditionally, so the later CIP that populates pending does not fork implementations that hard-coded the v1 reachability argument. Streams keep the schedule_digest they snapshotted at creation until an UpdateStream priced-capacity change replaces it per §5. CreateStream snapshots the active schedule_version, digest, and derived rate in PricingBasisV1::GovernanceBase, so the schedule a stream was admitted under stays derivable from finalized state.

5. Chain instructions

CbqsInstructionV1 contains the following variants: The canonical v1 payload schemas are:
CbqsInstructionV1 encodes the v1 version byte, then the one-byte action tag from the table, then that action’s fields in declaration order. The account authorized by the containing Cowboy transaction is the provider for provider actions, the owner for owner actions, and the payer for top-up; those identities MUST NOT be repeated in the CBQS payload. Chain instance, block height, initial record status, registration height, reservation totals, and created/settled heights are derived from consensus execution context. Every height STORED in a stream record — created_at_block, last_settled_block, suspended_at_block, suspension_grace_deadline_block — is an effective rent height, never a physical block height, so the two are never subtracted from each other and a pause can never make an elapsed-block difference negative. CreateStream atomically debits initial_escrow from the owner account and credits the new stream escrow; the transition MUST fail without creating the stream when that debit cannot be completed. Stream escrow is real custody, not a bookkeeping field: CreateStream and TopUpStream move native CBY from the payer into the Stream Registry account, and settlement, close, and expiry move it out of that account to the provider, the Platform Fee Account, and the owner. The escrow_balance in each stream record is the per-stream liability against that pooled balance, and every debit MUST assert the registry account holds the amount before any recipient or record write, failing closed on absent or underfunded custody. A defect in a credit path therefore trips the custody assertion rather than minting native CBY, and the sum of live escrow_balance fields is always backed by an equal or greater registry balance. Wire amounts are u128 wei while the account layer stores u64 wei balances. Every debit MUST checked-convert and reject an amount exceeding u64 as insufficient balance, never truncate. Every credit — provider rent share, platform rent share, creation-charge burn, close refund, expiry residual — MUST use checked u64 addition; because total supply fits u64, a legitimate credit cannot overflow, so an overflow is state corruption and MUST fail the instruction rather than truncate or strand value silently. A zero initial_escrow is valid, and later funding remains available through TopUpStream; creation is nevertheless never free, because CreateStream also debits the §6 creation charge. A SettleStreamRent MUST be rejected as a no-op exactly when its transition would leave the stream record byte-identical. Keying the rejection on accrued due instead would miss the case that matters: a materialized Suspended record has last_settled_block = suspended_at_block, a past block, so due is positive at every height while the recomputed paid-block count stays zero and the write is identical — unboundedly repeatable inside one block for the whole grace window. A transition that advances last_settled_block is real work and remains valid even when it pays nothing. The rejection is scoped to the standalone instruction: settlement performed inside TopUpStream, UpdateStream, and CloseStream is a no-op-tolerant subroutine that skips its write rather than failing its caller, so a griefer cannot land one settlement per block to block every top-up on a stream inside its grace window. An UpdateProvider with every field absent MUST be rejected. UpdateStream MUST be rejected as a no-op exactly when config, admin_key, and price_quote are absent, rotation_key = Keep, and bump_authorization_generation = false. A revocation-only update with the bump flag set is therefore valid. RotationKeyUpdateV1 is three-way so Keep and Clear cannot share one optional encoding. Changing admin_key always bumps authorization generation regardless of the explicit bump flag. A replacement quote is accepted only where §5 requires repricing. A PriceQuoteV1 is single-use through its §4.4 generation binding: UpdateStream MUST reject a quote whose stream_id is not the named stream or whose pricing_generation is not exactly the record’s pricing_generation + 1, and MUST store the accepted quote’s generation inside the same atomic transition, so a superseded quote can never be replayed to reprice a stream. RegisterProvider and UpdateProvider use the canonical Cowboy transaction account signature already verified before system-instruction dispatch. They do not define a second provider-operator signing layout. CreateStream performs at most one CBQS Ed25519 verification for an optional PriceQuoteV1; UpdateStream performs at most one for a replacement quote; and ActivateKeyGeneration performs exactly one for KeyBatchReceiptV1. Every instruction MUST use compute-then-commit semantics. Failure MUST leave all registry, balance, and linked-stream state unchanged. Closed and DataExpired are absorbing: every instruction that names a stream MUST reject one whose status is terminal, so a permissionless settlement can never re-suspend a closed record, resurrect it through top-up, or decrement admission and reservation counters that its one terminal transition already released. Creating, modifying, suspending, reactivating, and closing an encrypted stream and its linked key stream are atomic operations. A partially created or partially transitioned stream pair is invalid. For a governance-base-priced creation, CreateStream MUST verify accepts_new, add the requested stream, retained-byte, append-throughput, and delivered-throughput reservations with checked arithmetic, reject any result above a provider cap, and commit the counters atomically with the stream. A quoted stream does not consume base-rate reservation counters because its quote is explicit per-stream admission. An encrypted main/key pair consumes one stream reservation using the main stream’s three priced configuration fields. A base-rate reservation is acquired exactly once by CreateStream, retained while its stream is Active or Suspended, and adjusted only by the checked delta in UpdateStream. The first terminal transition, either CloseStream or the transition to DataExpired, releases the reservation exactly once. When UpdateStream changes retained_bytes_limit, max_append_bytes_per_sec, or max_delivered_bytes_per_sec:
  • a GovernanceBase stream MUST settle the old rate through the update block, derive a new rate from the then-active schedule, replace its snapshotted schedule version and digest, and atomically apply the reservation delta;
  • a positive reservation delta requires accepts_new = true and MUST fit all provider caps, while a negative delta releases capacity; and
  • a ProviderQuote stream MUST carry a fresh quote authorizing the new capacity and rate.
An update that does not change any priced capacity field does not reprice the stream. Schedule activation by itself never changes an existing rate.

6. Rent and suspension

CBQS charges fixed reserved-capacity rent rather than provider-reported per-message usage. This keeps message activity off chain and avoids trusting a provider usage meter. For a stream with rate r, a settlement at block H computes:
If escrow_balance >= due, the chain deducts due and sets last_settled_block = H. Two protocol constants price the existence of a stream, as distinct from its service:
A main stream’s rate_per_block MUST be at least CBQS_MIN_RATE_PER_BLOCK on either pricing basis. A governance-base derivation clamps upward to the floor (§4.5); a provider quote MUST carry a rate at or above it as signed — a quote is a stated price, never clamped — which rejects the zero-or-token rate that would otherwise price registry state at nothing. A linked key stream keeps rate_per_block = 0; its lifecycle, escrow, and creation charge are the main stream’s (§4.1). CreateStream MUST debit the owner account a nonrefundable creation charge of rate_per_block * CBQS_CREATION_CHARGE_BLOCKS (checked u128), in the same atomic transition that debits initial_escrow, and MUST fail without creating the stream when either debit cannot be completed. The charge is burned — credited to the zero address like slashed relay stake — not split as rent: it is an economic-security bond against permanent-state growth, not payment for service, and routing any share of it through the rent split would let a self-providing owner recover that share of it. It is deliberately a charge and not an escrow floor: escrow returns to the owner at close, so any floor expressed through escrow is recoverable and prices permanent state at zero. The charge is the capital a registry record costs no matter how quickly its stream closes. It is sized to the grace window a record may occupy while unfunded — CBQS_CREATION_CHARGE_BLOCKS equals the genesis cbqs.suspend_grace_blocks default — so at the rate floor the cheapest possible permanent record costs 8,000 × 604,800 wei ≈ 4.84 CBY. An encrypted pair pays once, on the main stream’s rate. Both constants are compile-time protocol constants with no live governance parameter — the charge does not read cbqs.suspend_grace_blocks, so a governance rewrite of the grace window neither cheapens nor breaks creation — and retuning them is a coordinated-release protocol change, per the same rule as the handler surcharge constants. Creation always initializes the stream as Active, including when initial_escrow = 0. Suspension is materialized only by a settlement that finds accrued rent underfunded; a zero initial deposit is not itself a suspension predicate. The two rules compose without contradiction: a nonzero-rate stream created with zero escrow is chain-Active but already economically suspended at the broker one block later, serves no mutations until a top-up covers accrued rent, and enters the same grace window as any other underfunded stream. Zero-escrow creation therefore buys — at the price of the creation charge — a registry record and an id, not service; funding it before the grace deadline is what starts service. The broker MUST nevertheless derive effective availability from the same finalized view used for authorization. For a record whose materialized status is Active, it computes the checked accrued amount through the effective_rent_height the broker-state response reports beside that view — never served_at.height, which is physical and would over-accrue by the whole pause coverage, rejecting service on streams the chain considers funded — and MUST reject mutations when escrow_balance < accrued. When the record carries a pinned suspension_grace_deadline_block, the broker MUST also reject mutations at or past it. A broker MUST NOT compute a grace deadline of its own from live governance parameters: the pinned field is the only deadline that exists, and a broker that invented one would disagree with the chain the moment cbqs.suspend_grace_blocks was rewritten. A finalized height below last_settled_block or any arithmetic overflow fails closed. The materialized status flag alone is not sufficient: permissionless settlement may lag behind economic suspension, so a stream reading Active may already owe more than it holds, and the accrued check above — not the flag — is what withholds service.

6.1 The settlement transition

Settlement is defined exactly once, here. Every other section — §7’s authorization rules, §19’s event ordering and gating, and each instruction in §5 — refers to this function rather than restating it, and where an earlier draft restated it those restatements were the source of the divergence. settle(record, H), with H an effective rent height, is the only transition that moves a stream between Active and Suspended:
The status == Active guard on the pinning branch is load-bearing. A partial payment on an already-suspended stream advances last_settled_block to the newly paid-through point and leaves both pinned fields alone, so repeated small top-ups cannot walk a grace window forward; without the guard the same transaction would produce different record bytes on two implementations and fork state_root. Grace expiry is exactly status == Suspended and H >= suspension_grace_deadline_block. It is only ever evaluated against a materialized record, and each instruction reaches that guarantee one of two ways. There is no universal settle-first rule: requiring every instruction to settle would make an administrative update mutate status, last_settled_block and the emitted events on an economically underfunded record, which is neither what this protocol intends nor what a conforming node does. Each instruction MUST follow exactly the discipline named for it: The last two carry an Active-only gate, and that gate is what makes them safe: they refuse to operate on any record that is Suspended or carries either pinned field, so neither ever consults a deadline and neither needs one. ActivateKeyGeneration never settles at all. UpdateStream settles only on its priced-capacity path, and that path may take settle’s underfunded branch and leave the stream Suspended with both fields freshly pinned — which is consistent, because the Active-only gate is an ENTRY condition. It establishes that no deadline exists to consult when the instruction begins; it does not claim the instruction cannot create one. An administrative UpdateStream that changes no priced capacity performs no settlement and touches neither escrow nor status. Both may run against a stream that is economically underfunded but not yet materialized. That is deliberate and forgives no rent: the next settlement still accrues from the unchanged last_settled_block, so the gap costs the provider nothing. SettleStreamRent is the one instruction that evaluates expiry before its own settlement, because materializing DataExpired is precisely the case where settling first would be wrong. An economically suspended stream can sit unmaterialized indefinitely, since SettleStreamRent is permissionless and nobody is obliged to submit it. Two things follow, and neither is that such a stream is safe. The provider’s data-deletion right is not evidenced until materialization — the chain shows no deadline for anyone to check against — so a provider that wants to exercise it submits SettleStreamRent itself, and if the stream is already past grace, a further call to materialize DataExpired. This is not “starting a clock”: settle pins suspended_at_block to the historical paid-through height, so the deadline it writes may already be in the past at the moment it is first written. Nor does the unmaterialized window make such a stream revivable. TopUpStream settles before it credits, computing that same historical deadline and rejecting against it. An overdue stream is refused a top-up whether or not anyone had settled it first — which is the property the ordering exists to produce. A rejected TopUpStream persists NOTHING. Its pre-deposit settlement is evaluated to determine the deadline, not to commit one: on rejection the record, the escrow balance, the custody account, the provider and platform shares, and the payer’s balance are all exactly as they were, and no event is emitted. A rejected instruction has no effect, which is the only rule under which two implementations agree — one that persisted the settlement and then returned an error would produce different StreamRecordV1 bytes and different balances in the same block. The consequence is deliberate: the provider is not paid, by this instruction, for blocks it served before the escrow ran out. It collects that rent by submitting SettleStreamRent, which is the instruction whose whole purpose is to commit a settlement, and which is why a provider wanting to act on an overdue stream submits one itself rather than waiting for a top-up to do it. Settlement distributes each paid amount using governance parameters:
provider_bps + platform_bps = 10000 MUST hold after any parameter write — the same sum guard CIP-31 §4 states for the CBFS fee split, enforced on the proposal, so neither share is ever a silent complement of a stale partner. The platform_bps share is credited to the Platform Fee Account, system actor 0x18, rather than burned. Burning is irreversible and crediting is not: a credited share can still be burned later from that account, while a burned one cannot be recovered, and the credit preserves an on-chain record of accrued platform revenue that a burn destroys. The address is fixed, so no governance write can redirect the share and settlement needs no state read to find it. Withdrawal authority is genesis-defined for the account under COW-2915 — 0x18 sits in the keyless reserved system range, so it needs an explicit genesis-configured owner or multisig rather than a recoverable key, and this document defines accrual here while COW-2915’s resolution defines the exit path before mainnet genesis. This is the same account and the same rule as CIP-31 §4. All elapsed-block quantities in this section — accrued rent, paid-through positions, suspension points, and grace deadlines — are computed on the effective rent height: the physical block height minus the registry’s cumulative CIP-12 pause coverage, exposed to brokers as the broker-state effective_rent_height and never greater than the finalized height. While the registry has never been paused the two are equal and this section reads as plain block arithmetic. For each paid amount, execution uses checked u128 arithmetic:
The subtraction assigns any indivisible remainder to the platform share, so the two shares always conserve the exact paid amount; with the sum guard above, platform_share is exactly the platform_bps complement plus that remainder. The two split parameters are read live at each settlement and apply prospectively from their parameter-write block: already-settled rent never moves, but future rent on existing streams follows the new split, which is why both carry §18 rows and the same governance change path as every live scalar rather than silent prose defaults. The provider share credits the assigned provider account. The platform share credits the Platform Fee Account (0x18), per the rule that opens this section. A self-host quote MUST still carry at least CBQS_MIN_RATE_PER_BLOCK: no main stream accrues zero rent, so every stream either stays funded or walks the §6.1 suspension path to a terminal state, and the open-ended zero-rate exit pin earlier drafts disclosed cannot be constructed. A provider still accepts the one-sided exposure §Security Considerations discloses for funded streams by signing any quote. While suspended, the broker MUST reject appends, new groups, redrives, and administrative mutations. It SHOULD permit read, replay, ack, and key recovery during the grace window. suspended_at_block and suspension_grace_deadline_block are written only by §6.1’s settle, which pins them together on the Active -> Suspended materialization using the grace parameter in force at that block and preserves them unchanged on every later partial payment. There is no second way to obtain a deadline and no formula that reproduces one from a live parameter, so a governance change can neither shorten nor extend a window that is already pinned. That is the guarantee, and it is narrower than “governance cannot affect grace at all”. settle anchors the deadline on the historical paid-through height but reads cbqs.suspend_grace_blocks live at the settling block, and §6.1 allows a stream to sit unmaterialized indefinitely. Two streams that exhausted escrow at the same height can therefore receive different deadlines, depending on which side of a governance write each was settled. This is deterministic — the settling block fixes the parameter, so every node computes the same deadline and there is no fork — but it is a race: after a reduction a provider gains by settling promptly, and before an increase an owner gains by delaying. Once pinned the value is fixed for that stream forever. Removing the race entirely would require snapshotting the grace parameter into the record at creation, which v1 does not do. inverted means exactly suspension_grace_deadline_block < suspended_at_block; equality is valid, since a zero-length grace parameter is permitted, and expiry applies at H >= suspension_grace_deadline_block. Reactivation and every terminal transition clear both fields together; an encrypted stream mirrors both to its linked key stream and rejects divergence, a missing or inverted pair fails closed with no mutation, and an active record carrying stale suspension fields is rejected. Grace is available only while the stream is Suspended and H < suspension_grace_deadline_block, evaluated per §6.1 under the discipline that section assigns to the evaluating instruction. At that height or beyond, the provider MAY delete data and a subsequent SettleStreamRent MUST materialize DataExpired. TopUpStream is the only instruction that can reactivate a stream, and §6.1 requires it to settle before it credits, so it applies this check against a deadline it has just computed: an economically suspended stream has its deadline derived by the very instruction attempting to revive it, and is then rejected on that deadline. The rejection persists nothing, per §6.1 — the settlement is evaluated to find the deadline, not committed. Top-up revives a suspended stream only before that deadline. TopUpStream is permissionless only while the stream’s assigned provider is Active; when the provider status is not Active, the payer MUST be the stream owner. Third-party sponsorship therefore works in normal operation but cannot keep an abandoned stream funded against a provider whose deregistration requires every assigned stream to reach a terminal state. TopUpStream MUST reject amount = 0 and MUST apply §6.1 in this order: settle at the pre-deposit escrow through the effective rent height, reject if the resulting record is Suspended at or past its now-pinned deadline, then atomically credit the deposit, then settle again through the same height. Settling before the credit is what makes the deadline check meaningful — a deposit applied first can cover the accrued window and erase the very suspension the check exists to catch, admitting a top-up on a stream whose data the provider was already entitled to delete. The second settlement restores Active only when the credited escrow covers the full accrued window; a partial payment advances the paid-through block, preserves both pinned fields per §6.1, and leaves the stream Suspended. Materializing DataExpired refunds the residual escrow_balance to the owner and sets the record balance to zero. The residual is less than one rent block and is not earned by the provider. The same atomic transition mirrors DataExpired to a linked key stream, releases any governance-base reservation, decrements the owner’s active-stream count, and decrements the provider’s live_assigned_streams exactly once. Changing pricing basis or priced capacity settles the old rate through the activation block before installing the new rate.

7. StreamGrant authorization

CBQS does not reuse CBFS OwnerCapTokenV1. The implemented CBFS token is an owner-issued credential with access-mode and path restrictions, but it does not bind a grantee holder key or express CBQS verbs and group limits. The stream’s current admin key issues a directly signed StreamGrantV1. The owner controls that key through UpdateStream; an owner account signature is not itself a valid StreamGrant signature.
After verifying the admin signature, every implementation derives:
The digest commits to the complete canonical fields the admin signed, including grant_nonce, but not to a transport serialization or a locally invented identifier. The verb bits are: Unknown bits MUST be rejected. A key stream does not accept ordinary APPEND; it accepts only KeyRotationBatchV1 under §9. The admin signature is over:
The broker MUST verify:
  1. the signature under the stream record’s admin key from the request’s FinalizedViewV1;
  2. exact signing context and stream_id;
  3. exact current authorization_generation;
  4. the time window, allowing at most CBQS_MAX_CLOCK_SKEW_MS;
  5. the requested verb, lane scope, and group scope; and
  6. the intersection of stream, grant, provider, and account quota limits.
The grant’s max_lane_creates_per_min MUST NOT exceed the finalized cbqs.max_lane_creates_per_min protocol bound. The broker MUST fail closed when its finalized chain-state view is older than CBQS_MAX_CHAIN_STALENESS_BLOCKS. It MUST acquire one FinalizedViewV1 before processing a request and evaluate the stream record, provider record, status, effective escrow availability under §6, keys, generations, schedule, and applicable bounds from that same immutable snapshot. A request MUST NOT combine values read at different heights. The broker’s observed finalized cursor MUST advance monotonically. After it observes a finalized status, provider-key, provider-epoch, authorization- generation, or encryption-generation change, it MUST NOT admit a later mutation under an older view. Existing sessions MUST be revalidated against the newest observed finalized view before their next mutation and terminated when their grant no longer matches. The activation boundary is the finalized block containing the change.

7.1 Proof of possession

On connection, the broker sends a random 32-byte challenge and a unique session_id. The client proves possession of the grant’s holder key by signing:
signature is carried in the canonical proof object and is excluded from the signing preimage. It MUST NOT be supplied through a transport-only side field. The holder signs:
Every post-session request, including bounded reads, carries:
CbqsRequestMethodV1 is a canonical u16 registry: Unknown method tags are invalid. Every proof-bound method defines one canonical request-body type that excludes RequestProofV1. Its body_hash is:
The method tag inside RequestProofV1 MUST equal the tag used in the body-hash preimage. Proof bytes, signatures, request-correlation ids, WebSocket framing, and HTTP framing never enter body_hash. Implementations MUST use the named cowboy-protocol-codec::cbqs body-hash function for the method and MUST NOT select fields locally. The holder signs keccak256(request_proof_signing_bytes_v1(request_proof)). Counters start at one and MUST increase strictly within a session. The broker MUST reject duplicate, skipped-backward, or cross-session proofs. A captured grant without the holder private key is therefore insufficient. authorization_generation is coarse revocation. Owners SHOULD also issue short grant expiries. v1 has no delegation chain or third-party attenuation.

7.2 Lane scope

Every main-stream grant carries:
Any permits its verbs on every lane, Exact permits one lane, and Set permits the listed, sorted, unique lane ids. A set is bounded by cbqs.max_lane_ids_per_grant. One lane-set grant therefore covers a user’s workspace sidebar without minting one grant per page. A group scope never widens lane scope. The broker MUST evaluate both. Lane authorization is a transport boundary, not a cryptographic boundary. All lanes in an encrypted stream share its data-key generation. Applications MUST therefore place only lanes in the same cryptographic confidentiality domain in one parent stream. Within that domain, narrower lane grants are broker-enforced routing isolation, not protection from another holder of the parent data key. A page or room that needs a genuinely different confidentiality boundary MUST use a separate parent stream or independent application-layer encryption outside this CIP.

8. Lanes

The all-zero lane_id is the default lane and exists for every main stream. Additional lanes are broker-side state:
A client with LANE_ADMIN creates a lane by choosing a random lane_nonce:
Lane metadata is set at creation and is immutable in v1. Lane create and close require no chain transaction and charge no separate rent. The parent stream’s max_lanes, retained-byte limit, and throughput quota bound the aggregate. The broker MUST also enforce the grant’s lane-creation rate caveat. A closed lane rejects new appends but remains replayable through the parent retention floor. Closed lane ids MUST NOT be reused. max_lane_creates_per_min is enforced per (stream_id, grant_id) across all sessions using a rolling 60-second window. At time T, a broker admits a lane creation only when fewer than the caveat value have been admitted in (T - 60_000 ms, T]; a value of zero denies all lane creation. Requests rejected before mutation admission, including an already-used lane_id or an exhausted max_lanes limit, do not consume the caveat. A broker clock regression MUST fail closed for lane-creation admission. max_lanes counts the implicit all-zero default lane. A stream configured with max_lanes = 1 therefore permits no additional lanes. Every lane create and close atomically appends a provider-originated LaneEventV1 to the default lane, carrying the affected lane_id, new status, and metadata hash. The operation succeeds only after the event and its ordinary AppendReceiptV1 are durable, including on a fast parent. That append receipt is the authoritative lane-mutation receipt. On a fast parent it carries a FastRecordIdV1, including the broker epoch; a receipt MUST NOT identify that record using only the broker-epoch-local sequence.
The event uses the provider-event framing in §10, participates in both the parent’s global receipt chain and the default lane’s receipt chain, and lets stream-wide consumers discover topology changes without polling. The broker returns the affected nonzero LaneRecordV1, but the mutation’s AppendReceiptV1.lane_id MUST be the all-zero default lane where the event was appended. The broker also exposes bounded, paginated lane enumeration filtered by the caller’s lane scope and capped by cbqs.max_lane_list_page, so a consumer can recover after the default-lane event falls behind retention. ListLanes requires at least one of CONSUME, REPLAY, or LANE_ADMIN; an unrelated verb such as APPEND alone does not authorize topology discovery. after_lane_id is an exclusive cursor. The broker applies the grant’s lane scope before the page limit, and returns next_after_lane_id only when another authorized lane remains. The cleartext lane_id is carried in each record header. The provider MUST maintain an index that serves exact-lane subscriptions without transmitting unrelated records. This is opaque routing-metadata filtering, not payload-semantic filtering. A stream retains one global append order and one global receipt chain. It also maintains a receipt predecessor per lane so a lane-only consumer can verify that the provider did not rewrite or omit a record within that lane. Lanes are not partitions: they have no independent provider, rent, retention, authorization generation, encryption generation, or ordering authority. There are no per-lane quota carve-outs in v1; a hot lane consumes the parent’s shared quota and may backpressure its siblings.

9. Encryption and key rotation

9.1 Algorithms

Main-stream payload encryption uses XChaCha20-Poly1305 with a random 24-byte nonce. The associated data is the canonical record header through encryption_generation, including stream_id and lane_id but excluding payload bytes and provider-assigned sequence. A ciphertext therefore cannot be moved between lanes without authentication failure. Member envelopes use HPKE base mode with suite X25519-HKDF-SHA256/ChaCha20Poly1305 (RFC 9180). Each grant for an encrypted main stream MUST bind the holder signing key and the member’s HPKE public key. Signing keys and HPKE keys are distinct. Every Ed25519 verification defined by this CIP uses pure Ed25519 from RFC 8032 with an exactly 32-byte public key and exactly 64-byte signature. Verification MUST use strict semantics equivalent to ed25519-dalek 2.2 VerifyingKey::verify_strict: canonical point and scalar encodings are required, weak or small-order public keys and R values are rejected, and a noncanonical S is rejected. Consensus code MUST verify each signature individually and MUST NOT use a batch-verification result. ZIP-215 or a permissive Verifier::verify result is not valid for a CBQS object. SigningPublicKey canonically encodes the one-byte numeric algorithm tag followed by the fixed-width public-key bytes. v1 defines only Ed25519 = 1. Every signing key id in this CIP is:
PriceQuoteV1.signing_key_id MUST equal signing_key_id(ProviderRecordV1.quote_key) for the finalized provider record used to verify that quote. holder_signing_key_id means signing_key_id(StreamGrantV1.holder_signing_key) everywhere it appears, including deduplication and delivery leases.

9.2 Root escrow

The owner creates a random 32-byte stream_root, stores it as a CBSS secret, and records the derived rotation public key in both linked stream records. The v1 rotation actor is the sole actor in that secret’s CBSS read policy. The rotation signing seed and each data key are derived:
The owner derives and records the rotation public key while provisioning the CBSS secret, then erases any local plaintext root after successful provisioning. A normal CIP-24-authorized job submitted by the rotation actor obtains the root on its assigned Runner. CBSS is not on the per-message path and does not distribute keys to members. If CBSS is unavailable, existing generations continue operating, but the owner cannot add recoverable members, remove a member, or rotate keys. The broker MUST NOT invent a fallback key or advance a generation without a verified durable key-batch receipt. The current CIP-24 release path remains job- and assigned-runner-bound. CBQS does not require a persistent-workload CBSS release extension.

9.3 KeyRotationBatchV1

For encryption generation g, the rotation workload publishes:
sorted_envelope_digests is sorted by keccak256(canonical(recipient_hpke_key)). The rotation key signs:
The batch_digest referenced by envelopes and receipts is exactly the 32-byte signing digest above:
Each entry of sorted_envelope_digests is the canonical envelope digest:
membership_set_digest commits the generation’s complete membership — the recipient HPKE keys in strictly ascending order of keccak256(canonical(recipient_hpke_key)), duplicates forbidden:
The digest preimage is the domain string, the explicit big-endian u32 member count, then each recipient key’s §14 struct encoding concatenated in order with no per-element or vector length prefix; the explicit count is the only cardinality in the preimage. The envelope-root preimage in §9.3 uses the same convention for its ordered digest list. The batch chain has an explicit per-key-stream anchor and tail, distinct from the append-receipt chain. With no committed batch, the tail is:
previous_batch_digest MUST equal the current batch tail — the genesis anchor for the first batch, the prior committed batch’s batch_digest afterwards, including supplemental batches — and each committed batch atomically advances the tail to its own batch_digest inside the §14.3 key-batch commit set. Sealing MUST NOT depend on digests of sealed envelopes, so associated data binds the pre-envelope batch identity rather than batch_digest:
The derivation order is therefore acyclic: fix membership and previous_batch_digest, compute membership_set_digest and batch_context_digest, seal every envelope, compute envelope digests, then sign the batch, whose batch_digest exists only after sealing. Each envelope is HPKE single-shot seal for X25519HkdfSha256ChaCha20Poly1305 with info = "cbqs/key-envelope-hpke/v1". The associated data is the bare concatenation, with no length prefixes or vector headers, of: the 32-byte stream_id, the 8-byte big-endian encryption_generation, the 32-byte recipient order key, and the 32-byte batch_context_digest. The recipient order key is exactly the §9.3 sort key keccak256(canonical(recipient_hpke_key)), where canonical is the recipient key’s §14 struct encoding (suite tag then key bytes). The plaintext is exactly the generation’s 32-byte symmetric data key. hpke_ciphertext is the 32-byte X25519 encapsulated key followed by the AEAD output; anything shorter than one sealed data key is invalid. The broker MUST reject a batch whose generation is lower than or more than one greater than the chain-active generation, whose predecessor does not match the current batch tail defined above, or whose signature is invalid. Committing a staged batch materializes it as exactly 1 + envelope_count contiguous standard records on the key stream’s default lane in one atomic §14.3 commit. The first record, at first_sequence, has kind = KeyBatchHeader, payload = the canonical signed KeyRotationBatchV1 bytes, and client_message_id = batch_digest. It is followed by one kind = KeyEnvelope record per envelope in sorted_envelope_digests order, each with payload = the canonical KeyEnvelopeV1 bytes and client_message_id = that envelope’s envelope_digest, so last_sequence = first_sequence + envelope_count. All of these records use origin = ProviderEvent, zero visible_after_ms and expires_at_ms, a zero nonce, and encryption_generation = the batch generation; their payloads are HPKE ciphertexts and signed objects, not stream-encrypted content, and ordinary append receipts chain across them. final_append_receipt is the Keccak-256 digest of the last materialized record’s append-receipt signing bytes. The receipt’s envelope root is:
binding the root to the batch identity so it cannot be replayed for a different batch, stream, or generation. These formulas and the record layout above are the only valid v1 materialization; a provider MUST NOT choose an alternative chunking. After every envelope ciphertext and the batch header are durable, it returns:
A structurally impossible receipt is a decode error, not a semantic one: envelope_count MUST be nonzero and within the batch bound, envelope_root and batch_digest MUST be nonzero, and last_sequence MUST equal first_sequence + envelope_count exactly (the header record plus the envelopes). ActivateKeyGeneration verifies the receipt signature and these structural invariants only; recomputing the envelope root or membership digest requires the key-stream records and is client and auditor work, by design. The provider MUST reserve enough key-stream capacity for the stream-configuration membership maximum. Ordinary main-stream backpressure MUST NOT consume that reserve. ActivateKeyGeneration carries the KeyBatchReceiptV1; the Stream Registry verifies its signature against the StreamRecord’s snapshotted provider key and epoch before advancing the main stream. It also verifies that the receipt context matches the executing chain, that served_at.chain_instance_id matches that context, and that the served_at.height is lower than the executing block height. Before submitting the instruction, the owner or its client MUST verify through the finalized-view RPC that the signed served_at height and block hash identify one canonical finalized view. The consensus handler MUST NOT query a validator’s local block archive or marshal state: served_at.block_hash remains provider-signed evidence, while chain-instance replay protection and receipt authorization are enforced by the signed context, generation, stream record, and provider key.

9.4 Membership changes

Adding a member MAY append a supplemental batch for the current generation. The supplemental batch MUST extend the prior batch digest and MUST NOT remove an existing envelope. A valid batch has at least one envelope: a rotation that would leave zero members is invalid, so envelope_count is never zero and removing the final reader of a stream is expressed by CloseStream, not an empty rotation. Membership entries are HPKE keys and the admin signing key is not itself a member; an owner who wants continued read access registers an HPKE key like any other member. Removing a member requires:
  1. produce generation g + 1 envelopes for the retained membership;
  2. durably append the batch to the key stream;
  3. submit ActivateKeyGeneration with bump_authorization_generation = true; and
  4. issue new StreamGrants only to retained members.
Step 3 atomically advances both generations. The broker MUST then reject every old grant and every append declaring a key generation other than g + 1. A failure before step 3 leaves generation g active and g + 1 inert. A failure after step 3 cannot strand retained members because the chain verified the durable key-stream receipt first. Removed members may retain plaintext and keys they previously received. CBQS provides forward exclusion, not retroactive erasure.

10. Record model

The client submits an AppendRequestBodyV1; the transport carries its RequestProofV1 as a sibling field rather than inside the hashed body:
Its body_hash MUST equal append_request_body_hash_v1(AppendRequestBodyV1), using method tag Append = 1 and the §7.1 request-body domain. The provider assigns order and emits a broker-visible envelope:
A client append MUST use origin = Client, kind = Application, and copy client_created_at_ms to created_at_ms. Clients MUST set visible_after_ms = 0; the broker MUST reject any other v1 value. A provider event MUST use origin = ProviderEvent, a lane-event kind, the all-zero default lane_id, and keccak256(canonical(LaneEventV1)) as its synthetic client_message_id. Its created_at_ms is provider time, it is visible immediately (visible_after_ms = 0), it has no application deadline (expires_at_ms = 0), its payload is the canonical LaneEventV1, encryption_generation = 0, and nonce is zero. Provider events are cleartext delivery mechanics even on an encrypted stream; a consumer MUST inspect origin and kind before deciding whether to decrypt the payload. Provider events do not enter the client deduplication table and require no holder request proof. They receive ordinary AppendReceiptV1 receipts, consume the parent’s append, retained-byte, and retention quotas, and are evicted only under the same retention rules as application records. A committed standard record is:
sequence is monotone within a stream. A fast record id is:
Application semantics, reply routing, CBFS references, and application deduplication identifiers belong inside the payload. The broker-visible header contains only delivery mechanics. Payloads above max_message_bytes or the protocol maximum MUST be rejected. The SDK SHOULD store larger encrypted objects in CBFS and place the CBFS reference inside the encrypted payload.

11. Standard delivery

A standard append is successful only after the record and its append-receipt predecessor are durably committed. The provider MUST NOT acknowledge it from memory or an operating-system page cache without its configured durable-commit boundary.

11.1 Idempotent append

The deduplication key is:
holder_signing_key_id is derived by §9.1; no raw-key-only, JSON, or transport-specific alternative is valid. Deduplication is stream-scoped even when a request names a lane. On first acceptance, the broker records its own received_at_ms. It MUST reject a new key when client_created_at_ms is later than received_at_ms + CBQS_MAX_CLOCK_SKEW_MS or older than received_at_ms - (idempotency_horizon_ms + CBQS_MAX_CLOCK_SKEW_MS). The broker retains the dedupe entry for at least idempotency_horizon_ms after first acceptance; the client clock does not shorten that retention. Within the retained horizon:
  • an identical retry returns the original append receipt;
  • reuse with different canonical request bytes, including a different lane_id, returns IdempotencyConflict; and
  • a retry after the horizon returns IdempotencyExpired.
New logical messages MUST use new message ids. This contract bounds broker deduplication state without permitting silent second appends.

11.2 Consumer groups

Groups are broker-side objects and are not chain state.
Broker state keys a group by (stream_id, lane_id, group_id). Creating, updating, or deleting a group requires GROUP_ADMIN scoped to both the group and its lane. A group created at Head sees only later records in that lane. A group created at an explicit retained id begins with the first lane record at or after that id. A stale start returns CursorTooOld with the latest signed lane retention-floor anchor. cbqs.max_groups_per_stream counts active groups across the parent stream. Deleting a group decrements that active count but leaves a durable deletion tombstone and receipt-chain tail. The exact (stream_id, lane_id, group_id) tuple MUST NOT be reused; a new logical group uses a new random group_id. This prevents a stale consumer from confusing a replacement group with the deleted group’s start cursor or receipt history. Each record is delivered at least once to each applicable group while the provider is honest and available, until one terminal state occurs:
Delivery creates a lease containing group id, record id, delivery-cycle id, attempt number, consumer holder-key id, and lease expiry. ACK, NACK, EXTEND, and REJECT MUST name that lease. A stale lease cannot mutate a later delivery cycle.
  • ACK transitions the cycle to ACKED.
  • NACK releases it for retry after a requested bounded delay, unless the completed attempt exhausts max_attempts, in which case it transitions directly to DEAD_LETTERED.
  • EXTEND advances expiry but never beyond max_visibility_ms.
  • REJECT transitions directly to DEAD_LETTERED.
  • exhaustion of max_attempts transitions to DEAD_LETTERED.
  • passage of record_deadline_ms transitions to EXPIRED.
Terminality is monotone. Redrive creates a new delivery cycle at the group tail and returns a signed receipt; it does not erase the earlier terminal state or rewind the append log. Each group assigns a one-indexed, strictly increasing group_queue_sequence when a delivery cycle first enters its queue. Initial cycles follow the group’s applicable lane-record order. A redriven successor receives the next sequence at the then-current tail. Sequences are immutable and never reused, including after terminality or group deletion. StrictFifo exposes only the lowest nonterminal record. A terminal state fills the cursor hole. Concurrent may lease later records up to max_in_flight. Two in-flight limits apply together, and a lease requires room under both. GroupConfigV1.max_in_flight bounds the group’s total outstanding leases across every holder, counted per group. StreamGrantV1.max_in_flight bounds the leases held concurrently by ONE holder, counted per holder_signing_key_id across every group, subscription, and connection on that stream, so a holder cannot multiply its signed allowance by opening more subscriptions or spreading across groups. The two counters therefore have different scopes on purpose — one per group, one per holder per stream — and a lease is admitted only when both have room. A grant whose limit exceeds the group’s is capped by the group, so a holder’s effective ceiling in any one group is min(grant.max_in_flight, group.max_in_flight); a group whose limit exceeds a grant’s still admits other holders up to the group total. When several grants name the same holder_signing_key_id, each request is evaluated under the grant it presents, against the one stream-wide count for that holder. The holder’s reachable ceiling is thus the maximum over its own valid grants, which is intended: the owner signed each of those allowances, and taking the minimum instead would let a later narrow grant silently revoke a broader one still inside its validity window. Enforcing only the group limit would make the grant’s field decorative and let a holder issued one concurrent lease take the whole group’s capacity. The per-holder count MUST be durable and MUST be released in the same atomic write as the lease it accompanies; a broker that reconstructs it MUST do so by scanning live leases, never by trusting a client. It MUST be checked after lease preparation rather than before, so that leases expiring during the operation are released first — a pre-check against a stale count wedges a capped holder permanently once its leases have expired. Undecryptable or malformed application payloads SHOULD be nacked or rejected by the consumer; the broker cannot inspect ciphertext. Live records MUST NOT be evicted under capacity pressure. The broker instead returns Backpressure. A stalled group pins retention only until its configured pin deadline and within its stream byte quota. When the deadline expires, affected records become EXPIRED for that group and retention may advance. Deleting a group returns a signed group-state receipt and releases its retention pins.

12. Fast delivery

fast is for repairable application logs such as CRDT operations, presence, typing state, telemetry, and cursor broadcast. It does not support consumer groups, leases, dead-letter lanes, redrive, or an at-least-once work-queue claim. The provider may acknowledge an append provisionally before durable commit. It MUST commit a batch when either fast_batch_max_ms or fast_batch_max_records is reached. The provider publishes a checkpoint receipt after every durable batch. Two watermarks exist:
  • provisional watermark: highest record emitted in the current broker epoch; and
  • durable watermark: highest record covered by a valid checkpoint.
Clients persist their own durable replay positions. They MAY retain a later provisional position locally, but MUST NOT persist it as durable state until a checkpoint covers it.

12.1 Broker epochs

Before accepting records after startup, the provider MUST durably increment the stream’s broker_epoch. Sequences begin at zero in the new epoch and MUST NOT be reused within it. Every increment MUST produce one signed transition:
Clean is valid only when the broker durably recorded, before shutdown, that the provisional and durable watermarks were equal; last_provisional_sequence and last_durable_sequence MUST both be present and equal, or both absent for an empty genesis lineage. Void means every record in the prior lineage above last_durable_sequence is void, and its last_provisional_sequence MAY be absent because a crashed process may not know its exact emitted upper bound. previous_lineage identifies the preceding same-provider (provider_epoch, broker_epoch). It is absent only for a newly created stream. The stream starts at broker epoch zero with a Clean transition anchored to the checkpoint-genesis digest in §14.1. The broker MUST durably publish the transition before accepting new appends. The first CheckpointReceiptV1 of the replacement broker epoch MUST set previous_checkpoint to the transition digest. Because the transition itself names last_durable_checkpoint, it is a link in the checkpoint chain rather than an unaudited side record. Conflicting transitions for the same predecessor and replacement lineage are provider-equivocation evidence. A client that observes a broker-epoch change without a valid transition MUST treat it as an unclean restart and MUST NOT advance beyond its last verified durable checkpoint. Clients compare their locally observed watermark with the transition, discard void records, and re-send application operations from local state. Applications using fast MUST either carry stable application operation ids or use an idempotent merge model with a defined resynchronization procedure. For a CRDT such as Yjs, the SDK recovery path MUST compare watermarks, discard the void suffix, materialize state through the durable checkpoint, compute a state-vector diff from the client’s local state, and re-append that diff. The SDK MUST surface clean and void transitions and the current durable watermark; it MUST NOT silently reconnect past either an unexplained epoch change or a void. When either fast-batch threshold is reached, the provider MUST stop issuing additional provisional acknowledgements for that stream until the batch commit completes. Under honest stable storage, the provisional loss window is therefore bounded by both configured thresholds rather than merely targeted by them.

12.2 CheckpointReceiptV1

Checkpoints are parent-stream physical durability commitments. The ordered range root commits to every record digest in the batch, partitioned by lane, so that a lane-filtered subscriber can verify the records of its own lanes without receiving any other lane’s records. Lane cursors and lane snapshots name global sequence positions filtered by their lane. The first checkpoint in a broker epoch links to its BrokerEpochTransitionV1; later checkpoints link to the prior checkpoint. A snapshot manifest may bind only to a stream checkpoint receipt, never to a provisional watermark. The partition is required rather than optional because §7.2 directs applications to subscribe with Exact or Set lane scope, and lane authorization is a transport boundary rather than a cryptographic one. A flat commitment over the whole batch can only be checked by a subscriber holding every record in it, so it would force every verifying subscriber to Any — which, since all lanes in an encrypted stream share one data-key generation, would hand every verifying subscriber the plaintext of every lane it is not authorized for. The partition is what lets record-level verification and lane-scoped delivery coexist. The canonical fast-record commitment is:
For one non-empty contiguous durable batch, let record_digest[i] be the fast-record commitment for sequence first_sequence + i, in increasing global sequence order. Two commitment shapes are used, and the difference is deliberate. Ordered lists use merkle_root_v1: for a non-empty leaf list it pairs adjacent nodes left to right at each level as keccak256("cbqs/merkle-node/v1" || left || right); a trailing odd node is promoted unchanged to the next level and MUST NOT be duplicated. A single-leaf list roots to that leaf. An ordered root is not by itself a commitment to its leaf list, and MUST NOT be used as one. Promotion makes a single-leaf list root to that leaf, so the two-leaf list [a, b] and the one-leaf list [node(a, b)] share a root by construction — the ambiguity duplication would have introduced is removed, but length ambiguity remains. Every structure consuming an ordered root therefore commits the leaf count alongside it, which is what makes the pair unambiguous. lane_sub_root is only ever consumed through lane_payload_digest, which commits lane_record_count with it. Lane membership uses sparse_root_v1, a fixed-depth-256 tree keyed by lane_id rather than a sorted list, so that a lane’s absence can be proven without naming any other lane. The leaf for lane_id sits at the path given by its bits, most significant first. Empty subtrees are canonical constants:
A proof is compact: a 256-bit big-endian bitmap whose bit d is set when the sibling at depth d differs from EMPTY[d+1], followed by exactly those non-default siblings in increasing depth order. A verifier reconstructs the remaining siblings from the constants. Descent is canonical. At depth d, the key bit is lane_id[d / 8] bit 7 - (d % 8) — most significant bit of the most significant byte first. A zero bit descends left and the sibling is the right child; a one bit descends right and the sibling is the left child. bitmap bit d refers to that same depth, and siblings holds the set-bit siblings in increasing d. Typical proofs carry about log2(active lanes) hashes, but the protocol bound is 256, not the typical case: a valid map built from sufficiently distinct lanes can place a non-empty sibling at every depth along one query path. Decode limits and the scope bound in §12.2.1 are sized against that worst case, never against an expected one. Any sibling supplied that equals its EMPTY[d+1] constant MUST be rejected, so a proof has exactly one canonical encoding. A present leaf whose fast_lane_payload_digest_v1 equals EMPTY[256] MUST also be rejected, so presence and absence cannot collapse into each other except by a hash collision. Each lane present in the batch commits its own records:
The map is keyed by lane_id and holds lane_payload_digest directly, so a proof about one lane carries no information about any other: absence is proven against the empty-leaf constant at the lane’s own key, not against neighbours. lane_id appears in the leaf preimage as well as the key, and the redundancy is load-bearing: it makes a payload digest valid at its own key and nowhere else. Position alone cannot provide that. A leaf whose preimage omitted its key would verify at any key, and a provider could then move a lane’s entry to a key that is no lane’s lane_id — the batch-global sums would still balance, and the lane’s own subscriber would receive a perfectly valid absence proof at its key while its records sat re-keyed elsewhere in the same signed map. With lane_id in the preimage, an absence proof at L’s key means L’s payload is committed nowhere in the map, because it cannot verify anywhere else. A verifier recomputing fast_lane_payload_digest_v1 MUST use the lane id of the key being proven, never a lane id carried in the payload material. The binding closes leaf relabelling, not record mis-partitioning — a leaf naming the wrong key over another lane’s records remains a FullStream-detectable falsehood, not a scoped-detectable one; §Security Considerations states the exact division. Absence proofs carry no leaf, so their key binding rests on a different mechanism: verification by sparse_proof_root_v1 is driven by the key being queried, so a proof accepted at key K is a statement about K alone. Proof bytes are not themselves key-bound — two absent keys whose paths meet only default (empty) siblings share identical proof bytes — and a proof lifted from another key can only verify at K when K is genuinely absent. Verifiers MUST NOT reject an absence proof because its bytes were previously seen for a different key. The canonical ordered range root is:
record_count MUST be at least 1, MUST NOT exceed the protocol maximum of 65,536, and MUST equal last_sequence - first_sequence + 1 without integer overflow. A provider MUST NOT commit a batch whose record_count exceeds the fast_batch_max_records snapshotted for that batch under the batch-boundary rule below; that is a commit-time obligation on the signer, not a verification condition — a verifier checks only the immutable bound, per the historical-verification rule below. Each lane appears at most once in the map. The sum of every lane_record_count MUST equal record_count, and the union of all lanes’ sequences MUST be exactly the contiguous range first_sequence..=last_sequence. durable_high_water MUST equal last_sequence. ordered_range_root and previous_checkpoint MUST be nonzero valid commitments. Verification MUST reject a missing, duplicated, out-of-order, wrong-lineage, or non-contiguous record before accepting the root. The sum and union rules are dischargeable only by a verifier holding every lane. They bind the provider at commit and a FullStream verifier at replay, and the codec’s full-stream verification enforces them. A ScopedProofs subscriber holds nothing about lanes outside its scope, so it can evaluate neither; to it the receipt’s record_count and range contiguity are signed claims, authenticated as to signer but not checked against the map. What a scoped subscriber does verify is stated in §12.2.1: its own lanes’ counts, sequences and sub-roots, and — because lane_id is in the leaf preimage — that each of its lanes is present or absent at its own key and nowhere else. A binding MUST NOT report the batch-global properties as verified for a ScopedProofs subscription. A configuration change takes effect at a batch boundary, never inside one. StreamConfigV1 fields that govern batching — fast_batch_max_records, fast_batch_max_ms and max_message_bytes — are snapshotted by a stream and may be changed by UpdateStream while a broker holds an open buffer. A provider MUST flush any buffered batch under the configuration and finalized view it was opened with, then install the new configuration for subsequent records; if that flush fails it MUST stop accepting new records rather than proceed. Adopting a change mid-batch would sign a batch against a configuration it was not built under, and caching the configuration at first use — never re-reading it — would sign every later batch against a configuration the stream no longer has. Verifying a historical checkpoint MUST NOT consult mutable present state. record_count is bounded by the immutable protocol maximum of 65,536, never by the stream’s currently configured fast_batch_max_records. A stream’s configuration may be updated, so checking an old receipt against today’s value would let a configuration write retroactively invalidate checkpoints that were correct when signed — bricking replay and retention for history that has not changed. This is the same retroactivity §18’s class partition exists to prevent, and the same failure the pinned suspension-grace deadline avoids: a signed historical artifact must be verifiable from itself and immutable constants alone. A provider MAY record the configured value in force at commit time for its own audit, but verification MUST NOT depend on it, because a remote verifier does not have it. Contiguity is also required across checkpoints: within one lineage a receipt’s first_sequence MUST equal the prior receipt’s last_sequence + 1, and the first receipt of a broker epoch MUST start at zero. Within-batch union contiguity alone would let a provider reorder records across a batch boundary while each batch stayed internally well-formed; the cross-checkpoint rule plus previous_checkpoint, which digests the entire prior receipt, closes that. The codec helpers fast_record_digest_v1, merkle_root_v1, sparse_root_v1, sparse_proof_root_v1, fast_lane_sub_root_v1, fast_lane_payload_digest_v1 and fast_ordered_range_root_v1 are the sole canonical implementations of these bytes. The record digest keeps its v1 name because its preimage is unchanged; only the range root’s shape moved.
12.2.1 Lane verification and omission detection
A verifying subscription is served durable-only, one batch at a time. A lane sub-root does not exist until its batch closes, so no delivery carries proof material and none is defined. Per batch, the provider MUST emit every in-scope durable record in ascending global sequence under ordinary delivery credit, then that subscription’s CheckpointFrameV1, and only then begin the next batch. The checkpoint is therefore an unambiguous batch-completion signal — which is what lets a subscriber reject an incomplete batch rather than wait indefinitely, and what bounds the proof metadata it must hold to one batch. When the cursor names a checkpoint in the immediate predecessor lineage, the provider MUST first serve the predecessor’s owed batches in this same per-batch order, then the adopted BrokerEpochTransitionV1, then the adopted lineage’s batches. Serving the transition first unconditionally would strand a client whose durable anchor predates a broker restart: a CBFS snapshot names the checkpoint that was current when it was taken, and a restart moves the lineage under it. Resuming at the current boundary instead is not a substitute, because records already committed in the replacement epoch below that boundary would never be served. A binding MUST NOT report record-set verification for records above the last verified checkpoint; their assurance is AEAD authenticity alone until then. Should a future revision push provisional records to proof subscriptions, it MUST specify a separate retroactive buffer rather than reusing this ordering: provisional push and batch-completion ordering are not compatible guarantees. A verifying subscriber holds every record of its own lanes, so it does not need per-record inclusion paths: it recomputes lane_sub_root from the records it received for that lane, in ascending sequence, and compares against the committed value. Per-record paths would only serve a subscriber holding a subset of a lane, which §12.2.2 forbids for proof subscriptions. For every lane L in scope S the provider MUST supply a proof at L’s own key in lane_map_root: a presence proof carrying L’s payload fields, or an absence proof resolving to the empty-leaf constant. Both are sparse_root_v1 proofs at the same position and differ only in the leaf value, so a proof about L names no other lane: no neighbouring identifier, no ordering rank, and no count of how many lanes the batch touched. It does carry the prefix-occupancy pattern inherent to a sparse path, which §Security Considerations states. The signed receipt is the anchor, and every proof MUST fold into it. A verifier MUST reconstruct one candidate lane_map_root from all the frame’s LaneProofV1 entries — every proof in a frame MUST fold to the same candidate, and a frame whose proofs disagree MUST be rejected — then recompute fast_ordered_range_root_v1 over that candidate together with the receipt’s range fields, and compare against the signed ordered_range_root. Verifying individual proofs against a root taken from the frame would authenticate the proofs against themselves; only the provider’s signature over the receipt makes the check meaningful. A None subscription receives no checkpoint frame and therefore no signed checkpoint chain at all. Given a presence proof the subscriber MUST verify that it received exactly lane_record_count records for L, that their global sequences are strictly increasing, that the first and last equal lane_first_sequence and lane_last_sequence, and that its recomputed lane_sub_root matches. A lane’s sequences are not contiguous: lanes interleave in the global sequence space, so one lane may validly hold 1 and 3 while another holds 2. Gaps inside a lane are expected; the union rule in §12.2 is what accounts for them globally, and a per-lane contiguity requirement would reject well-formed batches. Because the count and the sub-root are both committed inside the signed root, a provider can neither withhold a record from an authorized lane nor inject an extra one: either makes the recomputed root and the committed count disagree. This is what makes silent per-lane omission detectable, which a flat batch commitment cannot offer a filtered subscriber at all. Proofs are requested per subscription. A subscription MAY decline them, and SHOULD do so for lanes whose content is disposable — presence and cursor state, where a lost record is superseded by the next one and repair is meaningless. Choosing None is a delivery-cost and latency choice. It preserves AEAD authenticity, which travels with every record regardless of mode, but it forgoes the signed checkpoint chain entirely — a None subscription receives no checkpoint frame, so there is no chain to verify — and with it any omission detection, because nothing pins the lane’s record set. A FullStream subscription detects omission without lane proofs by recomputing lane_map_root from complete batches. Bindings MUST expose the distinction in the delivery assurance they report, and MUST NOT report record-set verification for a subscription that requested None.
12.2.2 Verifying subscriptions start at a checkpoint boundary
Both verifying modes require an after position at a checkpoint boundary. A provider MUST reject any other combination with NotCheckpointBoundary. The boundary is provider-validated, never client-asserted. In a verifying mode, after = Some(id) MUST name a last_sequence of an actual checkpoint in the adopted or immediate-predecessor lineage, and the provider MUST reject an id that does not — a client claiming to have verified a position is not evidence that the position is a boundary. A cursor older than the immediate predecessor is CursorTooOld; the client re-enters from a snapshot. after = None in a verifying mode means the current durable checkpoint boundary, which is checkpoint-genesis only on an empty lineage; this differs from None under None verification, which tails the current durable head wherever it happens to fall. The reason for the restriction is arithmetic: a subscriber entering mid-batch legitimately receives only a suffix of a lane, so it would fail a committed count that describes the whole batch — a correct provider would look like a withholding one. Requiring a boundary makes every covered batch complete for the subscriber, so the count means what it says. ScopedProofs on Any scope is rejected rather than ignored, because silently accepting it would report an assurance the wire never carried. FullStream is the mode an Any subscriber asks for instead: it needs no lane proofs, since it holds every record and recomputes lane_map_root directly, but it inherits the same boundary condition — an Any subscriber that tails from mid-batch holds a partial first batch and can verify nothing about it, exactly as a filtered one cannot. This costs applications nothing that matters. Homestead’s ops lanes resume from the checkpoint position bound to a CBFS snapshot, which is already a boundary; its presence lanes tail from mid-batch and verify nothing anyway.

13. Transport and flow control

The canonical persistent transport is a bidirectional TLS WebSocket using binary frames. It carries the versioned protocol objects and error codes defined by this CIP. Canonical signing bytes live in cowboy-protocol-codec::cbqs; the WebSocket binding MUST NOT invent its own signing layout.

13.1 Proof-bound request bodies

After session establishment, every client operation is:
request_id is an opaque correlation id echoed by the server and is not an idempotency key. CbqsRequestBodyV1 starts with the method’s canonical u16 tag from §7.1. Decode MUST reject the request before dispatch when proof.method differs from the body tag or proof.body_hash differs from cbqs_request_body_hash_v1(body). The non-append request bodies are:
A group configuration MUST satisfy, at create and at every update:
retention_pin_until_ms = 0 is the ordinary “no pin” value and MUST be accepted. Writing the bound as an addition on the right rather than a subtraction on the left is what makes that automatic: 0 satisfies the inequality trivially, whereas the subtractive form underflows u64 into a value exceeding every ceiling. The addition MUST saturate. A checked addition that rejects the configuration on overflow would refuse retention_pin_until_ms = 0 for a sufficiently distant now_ms, contradicting the MUST above — the horizon is a ceiling, and a ceiling that cannot be represented is u64::MAX, not an error. With saturation no implementation needs a special case for zero, and one that adds a == 0 disjunct has written dead code. Group configuration is validated by the broker, not the chain — CreateGroup and UpdateGroup are §7 grant verbs, not §5 chain instructions, and no consensus state records a group. now_ms is therefore the broker’s own clock. A stream has exactly one assigned provider serving it, so a single broker evaluates this inequality for a given group and there is no second party to disagree with; the clock needs no consensus treatment and none is implied by its appearance here. The ceilings are deliberately not the live governance scalars. cbqs.standard.visibility_ms and cbqs.standard.max_attempts are DEFAULTS a client MAY adopt when it does not choose its own, and binding a group to them would let a parameter write retroactively invalidate running groups — the same hazard the pinned grace deadline exists to prevent. The per-stream max_visibility_ms is snapshotted at creation and validated there against the immutable §18 maxima per §4.2, and the attempts ceiling is the immutable §18 maximum, so both bounds are stable for a group’s lifetime. Each inequality closes a configuration that would otherwise be accepted and then be unsatisfiable or unbounded: max_attempts = 0, which no delivery can ever satisfy; visibility values at u64::MAX, which pin a record’s in-flight slot for the life of the stream; and dead_letter_ttl_ms or retention_pin_until_ms at u64::MAX, which pin storage the owner is billed for indefinitely and defeat §15 retention. A group is a consumer of the stream’s retention budget and may never enlarge it: every group-held pin expires no later than the stream’s own retention horizon. Three of these ceilings are per-stream fields rather than protocol constants, and UpdateStream can lower retention_ms or max_visibility_ms beneath a value an existing group already holds. Existing groups are NOT re-validated and continue to operate on their snapshotted configuration — the same principle that keeps a parameter write from redefining a running group applies to an owner’s own write. The narrowed ceiling binds the next UpdateGroup, which MUST bring the group within the current stream configuration to be accepted. An owner that shrinks a stream beneath its groups has therefore frozen those groups’ configurations rather than invalidated them, and the remedy is in the owner’s own hands. Group update is full replacement of the mutable fields above. stream_id, lane_id, group_id, and the original start are immutable. Changing mode is permitted only when the group has no live leases. All other updates apply to new delivery cycles; they do not rewrite a lease already emitted. Subscription sources are:
VerificationModeV1 selects what the provider owes and is rejected rather than silently downgraded whenever the request cannot supply it. None is the ordinary subscription and may start anywhere. ScopedProofs requires a Fast source with Exact or Set scope; FullStream requires a Fast source with Any scope. Both verifying modes require an after at a checkpoint boundary (§12.2.2) and both receive the one-batch completion ordering of §12.2.1; only ScopedProofs carries lane proof material, because a FullStream subscriber holds every record and recomputes lane_map_root directly. A provider MUST reject either verifying mode on a standard source or on the wrong scope with VerificationModeIncompatible, and at a mid-batch position with NotCheckpointBoundary (§17). The mode is explicit rather than a boolean because the two verifying modes differ in scope, in what travels on the wire, and in what a binding may claim — and because a subscriber that wants full-stream verification has no way to ask for it otherwise. StandardGroup is exact-lane group delivery and requires CONSUME. StandardReplay requires REPLAY. Fast requires CONSUME, and resuming before the live head additionally requires REPLAY. DeadLetters requires CONSUME; moving one of its records back to delivery additionally requires REDRIVE. The requested source scope MUST remain inside the grant’s lane and group scopes. after_group_receipt is valid only for StandardGroup and DeadLetters. It names the Keccak-256 digest of delivery_state_receipt_signing_bytes_v1(receipt). When absent, the provider replays self-verifying group-state and delivery-state updates from group genesis before live frames. When present, it is an exclusive cursor and replay starts with its successor. An unknown or no-longer-retained cursor returns CursorTooOld with the latest retained signed group anchor. Other subscription sources MUST set it absent. Lease mutation bodies are:
reason_hash is application-defined audit metadata; the all-zero value means unspecified. The broker does not interpret it. Delivery-cycle and lease identifiers are canonical. The first cycle for one record in one group is:
A successful redrive creates exactly one successor cycle:
Here previous_group_receipt is the group receipt-chain tail immediately before the redrive transaction. Replaying the same committed redrive returns the original response and identifier; it does not create another cycle. Lease attempts are one-indexed. The first lease in a cycle has attempt = 1; each lease issued after expiry or NACK increments the attempt exactly once. EXTEND changes the active lease expiry without changing its attempt or identifier. A redriven successor is a new cycle, so its first lease restarts at attempt one. The identifier for an attempt is:
The provider MUST reject attempt zero, a non-v1 provider epoch, a lease whose identity fields disagree with its delivery, or reuse of an attempt for a different holder key. These formulas are the only valid v1 derivations; JSON, UUID, random, and transport-specific alternatives are invalid. Key batches use staged upload because the v1 membership maximum cannot fit in one normal append frame. BeginKeyBatch creates a staging namespace keyed by (key_stream_id, batch_digest) and requires total_chunks > 0. PutKeyBatchChunk accepts indices in [0, total_chunks), at most cbqs.max_key_envelopes_per_chunk envelopes, and at most cbqs.max_key_batch_chunk_bytes canonical envelope bytes. An identical chunk retry is idempotent; reusing an index with different bytes is InvalidKeyBatch. CommitKeyBatch succeeds only when every index is present, the concatenated envelope ordering and digests exactly match the signed batch, and the complete §14.3 key-batch commit set crosses one durable transaction. Staged chunks are not records, are never delivered, and produce no receipt. An incomplete staging namespace may be garbage-collected after the provider’s documented staging timeout.

13.2 Canonical transport frames and results

One WebSocket binary message contains exactly one canonical frame. A frame starts with version: u8 = 1, then a one-byte frame tag, then the canonical variant body. The lane-partitioned checkpoint commitment changes bytes that earlier drafts of this specification signed, and does so in place rather than behind a version bump. CBQS is reset-only and pre-launch: activation wipes chain state, and every deployment is replaced at deploy time, so no peer or store predating this document survives to be misread. A provider MUST reject a store or a peer it cannot decode rather than attempt a partial decode. That reset scope explicitly includes §15’s CBFS-resident snapshot bundles. A pre-amendment bundle would be the one artifact this argument otherwise misses: CheckpointReceiptV1’s byte layout is unchanged, so an old bundled receipt passes every structural check while its ordered_range_root commits the earlier flat shape. The environments this in-place change ships to wipe owner CBFS volumes at the same deploy boundary that wipes chain state, so no such bundle exists to hydrate from. An operator of any environment that preserves CBFS content across this change MUST delete or regenerate pre-amendment snapshot generations; a reader has no in-band way to distinguish the two meanings. The client tags are:
OpenSession.stream_id MUST equal grant.stream_id. The server frame tags are:
The session result bodies are:
Both objects MUST use the finalized view against which the grant and current stream generations were checked. Their served_at.chain_instance_id MUST equal context.chain_instance_id. CbqsResponseV1 echoes the request correlation id and contains a response-body enum whose u16 tag is the same method tag as the corresponding request:
Group mutation responses use the same self-verifying state preimage as replay; the response method and receipt.action MUST match. A staged key-batch result MUST have total_chunks > 0 and accepted_chunks <= total_chunks. Lane pages are strictly sorted by lane_id, contain no duplicate lane id, and are bounded by cbqs.max_lane_list_page. When next_after_lane_id is present it MUST equal the final returned lane id; it is absent on a terminal or empty page. Delivery-state receipt actions MUST match their response method (GroupCreate, GroupUpdate, GroupDelete, Ack, Nack, Extend, Reject, or Redrive) or decode fails. A standard append result MUST contain the matching standard append receipt and no fast watermark. A fast append result MUST contain no append receipt and MUST contain the watermark for the same provider/broker lineage. Create/close lane receipts are standard provider-generated lane-event records. Their stream, lane, status, origin, and record kind MUST agree with the lane result. checkpoint_anchor is the digest of the latest CheckpointReceiptV1, or the stream’s checkpoint-genesis digest before the first checkpoint. It is never an unconstrained zero digest. last_durable_sequence is absent exactly while the anchor is checkpoint genesis. Delivery frames contain:
A fast delivery carries no lane proof material. It precedes the batch that would commit it, so no lane sub-root yet exists to authenticate it against; proofs arrive with the checkpoint that closes the batch (§12.2.1).
A LaneProofV1 proves one lane at its own key. payload present means the lane carried records in this batch and the leaf is fast_lane_payload_digest_v1(payload); payload absent means the leaf is EMPTY[256]. Both cases verify through the identical sparse path, so presence and absence are indistinguishable in shape and neither discloses anything about another lane. A CheckpointFrameV1 for a ScopedProofs subscription MUST carry proofs for exactly the lanes in that subscription’s scope — every in-scope lane exactly once, no lane outside the scope, sorted ascending by lane_id. A FullStream subscription carries an empty lanes, and a None subscription receives no checkpoint frame at all. A subscriber MUST reject a frame whose scope coverage is incomplete, duplicated, misordered, or extraneous. A missing lane is indistinguishable from a withheld one, which is precisely the condition the proofs exist to remove; an extra lane, even one inserted in correct ascending position, discloses an out-of-scope lane’s identifier, count, and activity window — exactly what §2 promises a member never learns about another lane — so accepting it would convert the proof channel into the disclosure channel the sparse construction exists to avoid. Decode bounds are hard: lanes is bounded by cbqs.max_proof_lanes_per_subscription, and siblings by 256. The siblings length MUST equal the number of set bits in bitmap, and no supplied sibling may equal the EMPTY constant for its depth, so a proof of any other shape fails before verification rather than during it. A verifying subscription’s scope is bounded separately from a grant’s, at cbqs.max_proof_lanes_per_subscription, and a provider MUST reject a ScopedProofs request whose scope exceeds it with ProofScopeTooWide. The bound is sized against the worst case a valid map can produce, not the typical one: a single LaneProofV1 may legally carry 256 non-default siblings, so it reaches 8,313 bytes (about 8.12 KiB), and 512 of them come to 4,256,256 bytes (about 4.06 MiB) — inside a cbqs.max_wire_frame_bytes of 5,242,880 with room for the receipt and envelope. A larger scope can produce a legal frame the transport cannot carry, which is why the bound is a hard decode limit rather than guidance. Canonical multiproofs would share sibling paths across a frame and are the way to raise it later if an application needs a wider verified scope in one subscription. Until then a client needing more opens additional subscriptions, which costs nothing it was not already paying — subscriptions are per-connection and cheap, and the limit binds open pages rather than granted pages. A standard delivery MUST carry its matching append receipt, MAY carry a lease only for consumer-group delivery, and MUST NOT carry a fast watermark. A consumer-group delivery with a lease MUST carry the matching LeaseIssue delivery-state receipt; standard replay and dead-letter delivery carry neither. A fast delivery MUST carry no append receipt, lease, or delivery-state receipt and MUST carry the watermark for the same stream and provider/broker lineage. Receipt, header, record, lease, and delivery-state identities MUST agree exactly or decode fails. The decoder reconstructs the LeaseIssue DeliveryResultStateV1 from the lease fields and requires its canonical digest to equal the receipt’s resulting_state_digest. Errors preserve unknown future codes:
request_id is absent only when the error cannot be attributed to one client request. An error that terminates a subscription MUST carry its subscription_id, whether or not it answers a request. A provider may close a subscription unprompted — retention overtaking a partially delivered verified batch yields CursorTooOld with no request to attribute it to — and a client that cannot tell which subscription died has no way to release that subscription’s verification state, pending records, or reserved bytes. It would treat a terminal condition as a generic error and diverge from the provider silently, holding state for a subscription the provider has already discarded. A provider that cannot name the affected subscription MUST close the session instead, because an unattributable terminal error leaves the client no correct action. Closing the session is a last resort, not an alternative. An attributed terminal frame MUST take precedence over further delivery when output is bounded: naming the dead subscription lets a client release that subscription’s state precisely, whereas closing the session discards every subscription’s state and explains nothing. A provider MUST therefore prefer deferring an already-encoded attributed terminal frame to the next response over dropping it, and MAY invalidate the session only when no path can carry it. This matters most on long-poll, where a client cannot learn of a closure until its next request and would otherwise treat a dead subscription as live in the meantime. Message and details are bounded before allocation. Unknown frame versions/tags, invalid UTF-8 messages, oversized frames/fields, semantic identity mismatches, and trailing bytes are decode errors. Append, subscription, credit, delivery, ack, and administrative frames may share one connection. Every lane-bearing frame carries the exact lane_id. Each stream session performs the proof-of-possession handshake in §7.1. Every subscription names:
The requested subscription scope MUST be a subset of the StreamGrant’s lane scope. Any is therefore permitted only by lane_scope = Any; Exact may be covered by an identical exact scope, a containing set, or Any; and Set requires a containing set or Any. Any and Set deliver matching records in the parent’s global sequence order and every delivery frame carries its record’s lane_id. Each subscription uses one cursor and one credit window, so snapshotters, archivers, search workloads, and auditors do not need one subscription per lane. Standard consumer-group subscriptions remain exact-lane; multi-lane forms are for replay and fast cursor delivery, not a cross-lane work queue. Subscription sets MUST be sorted, unique, and bounded by cbqs.max_lane_ids_per_grant; the provider MUST reject a noncanonical or oversized set before allocation. Every subscription begins with zero transport credit. The consumer grants both record and byte credit:
The broker MUST NOT send a record unless both counters cover it. Sending decrements both counters. For standard, a delivery lease starts only when the delivery frame is emitted and counts against both transport credit and max_in_flight. The broker MUST enforce cbqs.max_subscriptions_per_connection; opening a subscription beyond the cap returns SubscriptionLimitExceeded. The provider MUST schedule active subscriptions with deficit round robin or an equivalent starvation-bounded algorithm. It MUST NOT allow one stream or group to consume all connection send capacity. A subscription with no credit for CBQS_SLOW_CONSUMER_TIMEOUT_MS MAY be closed. Closing a subscription does not ack records; outstanding standard leases expire normally. Reconnect resumes from durable group state (standard) or from a supplied durable checkpoint plus local provisional watermark (fast). Push is an optimization over those states, not a second consistency model. Every fast append response and delivery frame MUST surface the current durable watermark so an SDK can distinguish “sent” from “durably saved.” A provider MUST NOT imply that a provisional acknowledgement is durable. HTTPS long-poll is a required fallback. It MUST use the same authorization, leases, cursors, receipts, and credit-equivalent response limits. Consumers connect outbound; CBQS requires no inbound workload route. An HTTPS request body is exactly one canonical CbqsClientFrameV1. Its response body is:
The batch encoding is u32 frame_count, followed for each frame by u32 frame_byte_length || canonical_server_frame. The count and total response bytes are bounded before allocation. An empty batch is a successful long-poll timeout. The WebSocket transport sends individual server frames and does not wrap them in this batch. A long-poll virtual session admits at most one in-flight request. A second concurrent request MUST be rejected with LongPollBusy without advancing any cursor, consuming credit, or consuming a proof counter. Long-poll otherwise has no frame ordering of its own — order is the sequence of served requests — so concurrent requests against one session would interleave batches and break the §12.2.1 batch-completion ordering that proof subscriptions depend on. Rejecting is what keeps the two transports semantically equivalent, as §13 requires. A verified replay MUST bound the bytes it materializes, not only the record count. A batch may legally hold fast_batch_max_records records of max_message_bytes each; §4.2’s cross-field rule caps that product at 268,435,456 bytes, so a count-only bound still admits a materialization roughly 51× cbqs.max_wire_frame_bytes (5,242,880) — and a provider holding a global lock across it converts a legal replay into a denial of service against every other session on the broker. A provider MUST apply a total-byte admission bound of 268,435,456 bytes per batch replay — the immutable §4.2 product constant, deliberately not the stream’s current configuration: no legal batch exceeds the constant, and reading today’s configuration against a historical batch is exactly the retroactive-invalidation §12.2 forbids, since a later tightening would oblige the provider to refuse a legal, verified, in-retention batch it signed itself. The provider MUST NOT hold a global storage lock for the duration of one. A long-poll provider MUST bound its output at admission, never by truncating afterwards. A single request can legally require more frames than a batch carries: OpenSubscription returns its response plus up to cbqs.max_groups_per_stream replay follow-ons, against a cbqs.max_server_frames_per_batch of 256. A provider MUST therefore decide how much work to schedule before mutating any state, and MUST leave whatever does not fit deliverable on a subsequent poll with its cursors unadvanced. Truncating a batch after the state behind it has already moved would silently destroy frames — the client would see a well-formed short batch and no indication that anything was dropped, which is precisely the failure this transport’s equivalence requirement exists to prevent. The WebSocket transport sends individual frames and is not subject to the batch frame cap, so this bound is long-poll-specific. That asymmetry is permitted because it is invisible to semantics: both transports deliver the same frames in the same order, and only the packaging differs. Undelivered output MUST also be bounded across requests, not only within one. Admission-time bounding alone still permits unbounded growth: each OpenSubscription may queue up to cbqs.max_groups_per_stream follow-on controls while a single poll drains at most cbqs.max_server_frames_per_batch, so a caller that repeatedly opens subscriptions outruns its own draining and the provider’s pending queue grows without limit. A provider MUST therefore cap the undelivered controls a virtual session may hold — retaining at most one open request’s follow-ons is sufficient — and MUST reject a further OpenSubscription with ControlBacklogExceeded before authenticating it, consuming any counter, or mutating state, until earlier controls drain. Pending controls MUST NOT be observable on an unauthenticated path. The distinction matters because the two rules fail differently. Overflowing one batch loses frames; accumulating across batches exhausts the provider. An implementation can satisfy the first completely and still be trivially memory-exhausted by an authenticated caller doing nothing but opening subscriptions.

14. Receipts

The provider signing key snapshotted in the StreamRecord from the receipt’s served_at finalized view signs every provider receipt. Every receipt binds its signing context, stream id, provider epoch, served finalized view, receipt kind, and domain separator. All CBQS canonical bytes are emitted only by cowboy-protocol-codec::cbqs. Integers use unsigned big-endian encoding. Fixed byte strings and addresses are their raw fixed-width bytes. A variable byte string is u32 length || bytes. An option is tag 0 for absent or tag 1 || value for present. A vector is u32 element_count || canonical(element_0) || .... Struct fields appear in the order listed in this CIP. Enum tags are the numeric tags listed by the corresponding definition; unknown tags are invalid. The wire and state enums tag themselves at their definitions — ProviderEndpointTransportV1 (§4.3), ProviderStatus (§4.3), PricingBasisV1 (§4.1), and RotationKeyUpdateV1 (§5). RotationKeyUpdateV1 is deliberately 0-based, Keep = 0, matching the reference codec: Keep is the default-shaped no-op and shares the option convention’s absent tag. The 1-based table below covers signed objects only and is not a convention the other enums inherit. Maps and serialization-framework field names never enter canonical bytes. The signed-object enum tags are: For every signed type, the named codec function returns:
The signer signs the Keccak-256 digest of those bytes: Implementations MUST NOT reconstruct a signing preimage from JSON, CBOR, WebSocket frames, or locally repeated field concatenation. The table above is the complete v1 signing registry. For every provider receipt, served_at.chain_instance_id MUST equal context.chain_instance_id; served_at MUST be the immutable finalized view used for all authorization reads for that operation. After the provider observes a finalized status, provider-key, provider-epoch, authorization- generation, or encryption-generation change for a stream, it MUST NOT sign a later receipt for that stream from a view older than the observed change. escrow_balance and last_settled_block are deliberately NOT in that list: permissionless settlement can change them every block, so change-conditioning on them would let anyone serialize a provider’s every operation behind the newest view for the price of a settlement transaction. Economic availability instead binds to the per-operation snapshot that served_at identifies, under the cbqs.max_chain_staleness_blocks bound, so enforcement of economic suspension lags finalized state by at most that bound and no further. It binds to that snapshot, not to served_at.height: the rent and grace arithmetic inside it uses the effective_rent_height carried in the same broker-state response, per §6, so a paused registry does not make the broker and a receipt verifier disagree about whether the provider was entitled to serve. Unrelated finalized-height advancement does not serialize otherwise independent operations or lanes; the change-conditioned monotonicity rule in §7 is authoritative.

14.1 Append receipts

Receipt chains use the following empty-history anchors:
header_hash is:
The sole codec helpers are record_header_bytes_v1 and record_header_hash_v1. The hash binds the complete delivered header, including created_at_ms and nonce; payload_hash independently binds the delivered payload. A decoder MUST reject a standard delivery when either hash does not match or when any duplicated receipt/header identity field differs. The provider signs:
The global receipt chain makes two histories at the same sequence provable. The per-lane predecessor lets a lane-only consumer verify continuity without downloading unrelated lanes. Consumers MUST receive append receipts with records and SHOULD retain the latest stream and lane anchors. record_id uses the tagged RecordIdV1 union. A standard record MUST carry StandardRecordIdV1 { provider_epoch, sequence }; a receipt for a durable provider event on a fast stream MUST carry FastRecordIdV1 { provider_epoch, broker_epoch, sequence }. Normal provisional fast application appends are checkpoint-covered and do not receive an AppendReceiptV1. The first global receipt of a newly created stream uses stream_receipt_genesis. The first receipt in a newly created lane uses lane_receipt_genesis. An implementation MUST NOT encode absence as an unconstrained zero digest.

14.2 Delivery-state receipts

Every ACK, NACK, EXTEND, REJECT, redrive, group mutation, and group deletion returns a signed receipt containing the prior group-state receipt digest. The provider maintains one receipt chain per group.
The first receipt for a group uses keccak256("cbqs/group-genesis/v1" || canonical(context) || stream_id || lane_id || group_id || provider_epoch_u64_be) as previous_group_receipt. For group mutations, resulting_state_digest is canonical:
Group mutation replay carries the committed state preimage:
GroupCreate and GroupUpdate require config present with matching lane and group identities. GroupDelete requires it absent. Recomputing the applicable group state digest above MUST reproduce receipt.resulting_state_digest. For record delivery mutations, resulting_state_digest commits the complete absolute post-mutation state:
The update is invalid unless the receipt action and all stream, lane, group, record, lease, and provider-epoch identities equal the state; recomputing the canonical state digest MUST reproduce receipt.resulting_state_digest. Every action requires a nonzero group_queue_sequence; for every record-state action except Redrive it is the current cycle’s immutable position, while for Redrive it is the successor cycle’s newly allocated tail position. Every option not listed for an action below MUST be absent. Relative request values such as retry_after_ms and extend_by_ms are not result state; the provider commits their checked absolute outputs. issued_at_ms <= lease_expires_at_ms. For LeaseExpire, available_at_ms >= lease_expires_at_ms. For dead-letter terminal states, terminal_at_ms <= terminal_expires_at_ms; exhaustion cannot precede the last lease expiry. stream_id, group_id, delivery_cycle_id, present lease/holder ids, and successor cycle ids MUST be nonzero; lane_id MAY be the all-zero default lane. record_id.provider_epoch MUST be the v1 epoch. The codec helper delivery_result_state_digest_v1 is the sole canonical encoder and rejects every other option combination. GroupCreate, GroupUpdate, and GroupDelete receipts MUST have absent record_id and lease_id. Ack, Nack, Extend, and Reject receipts MUST have both fields present and matching the mutated lease. Redrive MUST have the redriven record_id present and lease_id absent. LeaseIssue, LeaseExpire, and AttemptsExhaust MUST have both fields present and matching the issued or completed lease. RecordExpire MUST have its record_id present; lease_id is present only when expiry also terminates an active lease. Provider-driven delivery-state changes are receipt-chain mutations, not hidden local bookkeeping. Lease issuance, lease expiry, attempt exhaustion, and record-deadline expiry each append the corresponding signed receipt in the same atomic transaction as the state change. A consumer-group subscription receives the LeaseIssue receipt with the leased delivery. Authorized group and dead-letter subscribers receive group mutations as GroupStateUpdateV1 server frames and subsequent provider-driven record mutations as DeliveryStateUpdateV1 server frames. A provider MUST retain and replay those frames from the subscription cursor so that a disconnected verifier can restore receipt-chain continuity. A receipt proves what a provider signed. It does not prove that the provider served every authorized consumer or retained data it refused to serve.

14.3 Atomic commit sets

An acknowledgement is valid only after the complete operation-specific commit set below crosses one atomic durable transaction boundary: The broker MUST NOT persist a dedupe success before its record, acknowledge a record before its dedupe result and receipt tails, or expose a new durable watermark before the checkpoint receipt commits. Recovery MUST replay or roll back an incomplete transaction as a unit. ActivateKeyGeneration remains a separate chain transaction and can succeed only after the complete key-rotation commit set is durably readable and its receipt verifies.

15. Retention and CBFS

The provider stores the bounded hot window. Retention applies only after every group has reached a terminal state or its pin deadline. fast retention is cursor-independent. Behind the retention floor, the broker returns CursorTooOld whose error details are exactly the canonical bytes of the governing signed anchor:
A fast cursor behind the floor is governed by FastRetentionAnchorV1, not by a checkpoint receipt. A receipt attests that a batch was committed and what it contained; it says nothing about what has since been deleted, so it cannot state a floor. Without a signed floor a client receiving CursorTooOld cannot distinguish a provider that genuinely pruned from one that is simply refusing to serve — the withholding problem §12.2.1 exists to remove, reappearing at the retention boundary. broker_epoch is carried because fast sequences restart per epoch (§12.1), so a floor is meaningless without the lineage it belongs to. A standard anchor states the scope’s floor: everything before first_retained_sequence is pruned, and receipt-chain verification restarts from the embedded predecessor digests (lane_id and previous_lane_receipt are present together for an exact-lane anchor, absent together for the stream-global floor). A group anchor states the latest retained group receipt; a stale after_group_receipt replays from it. Anchors are signed by the provider signing key over the §14 registry layouts, carry the provider epoch every receipt binds (zero in v1) so evidence is distinguishable across provider lineages, and floors are monotone within one lineage and one anchor scope — two anchors compare only when their full comparison keys are equal:
  • standard: (stream_id, provider_epoch, lane_id?), where lane_id = None is the stream-global floor and each exact-lane floor is its own scope;
  • group: (stream_id, provider_epoch, lane_id, group_id);
  • fast: (stream_id, provider_epoch, broker_epoch).
The scope components are as load-bearing as the epochs. Standard sequences are stream-global while retention_ms is one time window, so a uniform time floor maps to wildly different per-lane sequence floors — a 1,000/s lane and a 1/hr lane at the same boundary legitimately anchor near 900,000 and 10 — and comparing across lanes, or a lane floor against the stream-global floor, would convict an honest provider in the ordinary steady state of any multi-lane stream. The broker_epoch component exists because fast sequences restart at zero each broker epoch (§12.1): a floor of 10 in epoch N+1 after 900,000 in epoch N is correct behaviour, not regression. A provider MUST NOT sign an anchor whose floor regresses an earlier one with the same comparison key, so any same-key pair whose floors regress is portable equivocation evidence; a pair whose keys differ is evidence of nothing. Garbage collection is a §14.3 commit set: the new floor, its signed anchor, and the pruned records cross one durable transaction, so a served anchor is always attested state rather than an in-progress deletion. Applications that need older history archive to owner-controlled CBFS volumes. A CBFS snapshot manifest SHOULD contain:
application_signature uses the canonical signing layout in §14, but its verification key and authorization policy are application state, not the provider key or Stream Registry state. A broker does not validate or rely on that signature for retention. A fast snapshot MUST cover a durable checkpoint. Its content MUST be materialized exclusively from records at or below that checkpoint; a snapshotter MUST maintain or reconstruct a durable-only application replica and MUST NOT serialize its live view when that view includes provisional records. A lane snapshot includes only records for its lane at or below the parent-stream checkpoint. provider_anchor is the signing digest of the provider receipt the snapshot is bound to, and it MUST be nonzero for both stream classes. For a fast snapshot it is the signing digest of the bound CheckpointReceiptV1, and covered_through MUST be that same digest. For a standard snapshot it is the signing digest of the AppendReceiptV1 whose record id equals covered_through — the receipt the snapshotter already holds for the last record the snapshot covers, serving exactly the anchoring role the checkpoint receipt serves for fast. The canonical signed receipt MUST be published with the manifest — bundled or content-addressed together, so that retrieving one retrieves the other. That requirement exists because of when the manifest is used. A reader hydrates precisely when the hot window no longer holds its records, so the broker can no longer supply the receipt naming that digest. A manifest carrying only a digest would therefore be unverifiable at the one moment it matters — the reader could not tell a real checkpoint from an invented one. What the bundled receipt does and does not establish. It authenticates the anchor: the named checkpoint really existed, at that position, signed by the provider. It does not authenticate the snapshot bytes. The provider signs an ordered range of records; it never sees the application’s materialized state and holds no key with which to attest it. An application signer can therefore bind any bytes — including bytes derived from provisional records — to an otherwise valid receipt, and no reader can detect it cryptographically. Snapshot content fidelity is consequently an application trust property, not a protocol-verified one. Hydration trusts whoever holds the snapshotter’s signing key, and the protocol bounds only the anchor. Applications SHOULD treat that key with the custody its role implies, and SHOULD structurally prevent accidental contamination — materializing from a private replica mutated only by verified durable batches, so that serializing provisional state is impossible by construction rather than merely prohibited. Neither measure makes the fidelity claim verifiable by a reader; they reduce the chance the honest signer is wrong, not the power of a dishonest one. snapshot_hash is Keccak-256 over the exact stored snapshot bytes, matching every other digest in this CIP. encryption_generation names the data-key generation the snapshot content is encrypted under, so a reader knows which key to request and can refuse a snapshot from a generation it is no longer a member of; authorizing and rotating the key that protects snapshot content is application policy and outside this specification, but an application MUST state its rule rather than leave it implicit. Lane snapshots published together MUST share one parent checkpoint. A Set subscription carries one cursor over the stream-global sequence space (§13), so lanes resumed together must resume from a single position. Publishing a generation whose lane snapshots name different checkpoints would leave no cursor that resumes all of them without either replaying delivered records or skipping undelivered ones. An application SHOULD therefore publish a generation atomically: content-addressed snapshot blobs, one bundled receipt, sorted per-lane manifests, and a head promoted only once every part is committed. A reader MUST verify, before hydrating: the application signature and its authorization; that context, stream_id, scope, cbfs_reference and encryption_generation are the ones it expects; that the Keccak-256 hash of the snapshot bytes equals snapshot_hash; that the bundled receipt’s own context and stream_id equal the manifest’s — a digest match alone accepts a receipt the manifest signer took from any stream or lineage, so the cross-binding is a check of its own, not a corollary; the provider signature on the bundled receipt, against the provider signing key snapshotted in the StreamRecord at the receipt’s own served_at finalized view — the same key-resolution rule §14 states for every receipt. Resolving the key at the reader’s current view instead would make one bundle verify or fail depending on when it is read: after a key rotation, two readers at different heights would disagree about the same bytes at exactly the hydration moment the bundle exists for. And finally that the receipt’s own signing digest equals provider_anchor; and that covered_through is bound to that same receipt — for a fast snapshot it MUST equal the receipt’s signing digest (the same value as provider_anchor), and for a standard snapshot the bundled append receipt’s record_id MUST equal covered_through. The last check is not a corollary of the digest equality: for standard, provider_anchor and covered_through are two free variables, and a manifest bundling a genuine receipt for record N while claiming covered_through far beyond it would otherwise pass every listed check and make the reader resume past records the snapshot never contained — silently and unrecoverably, since hydration happens precisely when retention no longer holds them. The record_id equality also pins the standard snapshot’s lineage, since the epoch travels inside it. For a fast snapshot, the bundled checkpoint receipt’s provider_epoch and broker_epoch are then the lineage the reader resumes against under §12.2.2. A reader MUST refuse an anchor it cannot verify rather than hydrate from a plausible-looking one. An application supporting reconnect after the fast retention window SHOULD publish its latest verified manifest pointer in a designated standard discovery stream or in actor state. An actor-state pointer is public consensus metadata even when the referenced snapshot is encrypted. Snapshot cadence MUST be shorter than both the configured retention window and the application’s declared offline-recovery objective. The broker does not create or trust application snapshots and does not delete retained data solely because a client claims to have archived it.

16. Actor and application integration

CBQS does not directly invoke a consensus actor. An SDK actor-bridge helper MAY consume a stream, derive a caller-selected projection, and submit an ordinary actor transaction. The projection and its timing become public consensus data. The helper MUST document that boundary and MUST NOT imply that encrypted CBQS content remains private after projection. Finality-bearing actor communication remains on chain. Public, auditable event distribution remains the role of actor events and the public-stream design in CIP-7. CBQS is the private-content, off-chain complement.

16.1 Application migration

An application that moves data to another provider or confidentiality domain MUST create a new stream, copy or replay data through a verified durable source anchor, issue new grants and key envelopes, redirect clients, and close the old stream only after verifying the target’s durable anchor. This flow is also v1’s only remedy when a provider disappears or stops serving: there is no on-chain dispute, slashing, or forced-reassignment path, so recovery depends on whatever durable anchor the application maintained elsewhere (a CBFS snapshot, another stream, or client-side state). The exposure is disclosed in §Security Considerations; applications with availability requirements SHOULD maintain such an anchor continuously. The target stream has an independent stream id and receipt history. SDKs SHOULD expose this sequence as one migration flow.

16.2 Homestead application boundary

Homestead is Cowboy’s collaborative team-wiki application and is used here as an application-integration fixture, not as a protocol component. Its synthetic acceptance fixture contains 463 page lanes; 463 is a fixed scale-test input, not a normative product page count. Homestead SHOULD use one parent stream per uniform cryptographic membership, retention, provider, and billing domain, with one opaque lane per page. The fixture then requires one chain-created stream for the team wiki rather than 463 stream records, and a member’s lane-set grant can cover the pages in their sidebar. Lane grants within an encrypted parent are only broker-enforced. When a page first needs a materially different confidentiality boundary, the application MUST use the migration flow in §16.1 for a new parent stream and close the old lane only after the target anchor is verified. SDKs SHOULD provide this migrate-on-share flow. Ordinary wiki ACL churn MAY deliberately remain one workspace encryption domain; highly sensitive pages use separate streams; search-heavy deployments MAY choose the explicit unencrypted class. For encrypted deployments, search MUST run client-side or in an explicitly authorized search workload that is a member of the encryption domain and stores its index in a private CBFS volume. Authorizing such a workload reveals plaintext to that workload and expands the confidentiality boundary. The CBQS broker never performs semantic search over ciphertext. CBQS replaces relay transport state: WebSocket fan-out, room-log persistence, reconnect cursors, push backpressure, transport authorization, and crash-loss signaling. A Homestead application service remains responsible for Google identity to ACL mapping and grant minting, share links, metadata, seeding, snapshotting, search, and walletless-client attachment upload to CBFS. That service is a CBQS client; it is not a second queue. Yjs presence SHOULD use a fast lane with a short application expiry and awareness timeout. v1 has no broker-generated subscriber-departure event, so abrupt disconnects can leave ghost presence until that timeout. The SDK MUST document this behavior. Homestead’s document and comment lanes SHOULD use ScopedProofs: a withheld document operation is real data loss and the §12.2.1 count commitment is what makes it detectable. Its presence lanes SHOULD use None — a dropped cursor update is superseded by the next one, so verification would pay delivery cost and batch-ordering latency for a guarantee with nothing to protect. This split is the reason the mode is per-subscription rather than per-stream.

17. Typed errors

All bindings expose stable typed errors. v1 reserves: GroupConfigInvalid is returned when a CreateGroup or UpdateGroup violates any inequality in §13’s group-configuration block, with details naming the offending field. HolderInFlightLimitExceeded is returned when a lease is refused by the per-holder StreamGrantV1.max_in_flight of §11 rather than by the group total, which Backpressure continues to report; the two are distinguished because the first is a property of the caller’s own signed grant and will not clear as other holders drain, so a client that retries it indefinitely is wrong where the same retry on Backpressure is correct. The three chain-state codes are separated by REMEDY, not by internal error variant, because a client’s correct response differs across them and a single code forces every client to guess:
  • ChainStateStale (3925) means the broker holds a valid finalized view that is older than cbqs.max_chain_staleness_blocks. The condition is expected under normal lag and clears on its own; a client SHOULD retry with backoff.
  • ChainUnreachable (3941) means the broker cannot read finalized state at all — the node is unreachable, returned an error status, or returned a response exceeding the transport bound. Retrying will not help until an operator intervenes, so a client SHOULD surface the failure rather than spin, and a broker SHOULD fail readiness.
  • ChainViewInconsistent (3942) means the chain state the broker did read cannot be trusted: a finalized view that regressed in height, a height-identical view with a different block hash, a chain-instance change, or a response that failed canonical decode. A client MUST NOT retry the request as though it were transient; the node has contradicted itself or the broker’s own state is corrupt, and continuing risks acting on a fork.
Collapsing these into one code is a live hazard rather than an aesthetic one: an unreachable node reports as staleness, so an operator alerted on 3925 looks at chain sync while the actual fault is connectivity or a fork. The five verification-and-flow codes are likewise separated by remedy:
  • VerificationModeIncompatible (3943) rejects a verifying mode the request cannot carry — ScopedProofs or FullStream on a Standard source, or on the wrong lane scope (§13.1), with details naming which. The request is malformed for the source; retrying it unchanged is wrong.
  • NotCheckpointBoundary (3944) rejects a verifying-mode after that names no checkpoint boundary in the adopted or immediate-predecessor lineage (§12.2.2). The client re-enters at a boundary or from a snapshot. A cursor older than the immediate predecessor remains CursorTooOld.
  • ProofScopeTooWide (3945) rejects a ScopedProofs scope exceeding cbqs.max_proof_lanes_per_subscription. The remedy is to split the scope across additional subscriptions (§12.2.1), which no amount of waiting or backoff accomplishes.
  • LongPollBusy (3946) rejects a second concurrent request on a long-poll virtual session (§13.2). The caller has violated the one-in-flight protocol; the remedy is to await the outstanding request, and blind retry only collides again.
  • ControlBacklogExceeded (3947) rejects a further OpenSubscription while earlier follow-on controls remain undelivered (§13.2). The remedy is to poll and drain, after which the same request is accepted. §13.2 mandates this reject before per-request authentication; the specific code adds no information that branch does not already carry.
Backpressure continues to mean what §11 and §13 define — transient credit or capacity exhaustion that clears as the system drains, where retry with backoff is the correct response. None of the five conditions above clears that way, which is why each carries its own code rather than borrowing 3914. Unknown future errors MUST be preserved by clients as (code, message, details) rather than mapped to success or retry.

18. Parameters and bounds

All decode-time collection and byte lengths MUST be capped before allocation. The following are v1 defaults with their ceilings. For the governance-writable rows the Maximum is the ceiling a governance write may not exceed; for the twelve cbqs.base_rate.* rows, which are genesis values frozen for the life of the chain (§4.5), and for the seven codec-bound rows, which are protocol constants rather than governance parameters, it constrains genesis validation only. The classes below the table say which is which: The maxima are per-field, and one pair is bounded jointly below its per-field product: cbqs.max_inline_message_bytes and cbqs.fast.batch_max_records combine under §4.2’s cross-field rule, so a Fast configuration’s max_message_bytes × fast_batch_max_records never exceeds 268,435,456 even though the table rows alone would permit 64 GiB. The actual duration represented by a block count depends on network block time. Governance changes are prospective, and which bound an operation reads follows from what the value governs: The four classes below partition the governance-writable rows exactly: 15 + 7
  • 2 + 1 = 25, which is precisely §18.1’s set of 25 live governance scalars. The twelve base_rate.* rows and the seven codec bounds are not governance-writable at all; both are classified after the list. The assignment is given by name rather than by example, because an example list leaves the unlisted rows undetermined and implementations then differ on them.
  • Admission bounds are read LIVE and checked per operation. The test is not whether the operation creates something lasting but whether the accepted object thereafter behaves by the bound: cbqs.max_lanes_per_stream is an admission bound even though a LaneRecordV1 persists and its id is never reused, because the bound limits how many lanes may be created and the lane itself never consults it again. Lowering one stops new work immediately and re-examines nothing already accepted. Rows (15): the 14 rows from cbqs.max_streams_per_owner to cbqs.max_chain_staleness_blocks that §18.1 lists among its 25 live scalars, plus cbqs.slow_consumer_timeout_ms. The seven codec bounds in that span — max_key_envelopes_per_chunk, max_key_batch_chunk_bytes, max_wire_frame_bytes, max_server_frames_per_batch, max_error_message_bytes, max_error_details_bytes, max_proof_lanes_per_subscription — are NOT in this class and are not governance-writable at all; see below.
  • Configuration ceilings bound a value a record snapshots at creation and then behaves by. They are bounded by the IMMUTABLE table maxima, never by the live default; the record’s own validated snapshot is thereafter its ceiling. Reading these live would let a parameter write silently redefine how existing streams and groups behave. Rows (7): cbqs.standard.retention_ms, cbqs.standard.idempotency_horizon_ms, cbqs.standard.visibility_ms, cbqs.standard.max_attempts, cbqs.fast.retention_ms, cbqs.fast.batch_max_ms, cbqs.fast.batch_max_records. The two fast batch rows are ceilings, not admission bounds: StreamConfigV1 stores fast_batch_max_ms and fast_batch_max_records, and the stream behaves by its own snapshot thereafter. §4.2 is the authority on which source each config field takes.
  • Settlement parameters are read LIVE at each settlement and apply to future accrual only. Already-settled rent never moves; rent accruing after the parameter write follows the new value. This is prospective in the sense that matters — no completed accounting is revisited — even though existing streams are affected going forward, which is why these are their own class rather than an awkward fit in either of the two above. Rows (2): cbqs.rent.provider_bps, cbqs.rent.platform_bps.
  • Materialization parameters are read exactly once, at the transition that pins a field into a record, and never again by any later decision. Row (1): cbqs.suspend_grace_blocks, read by §6.1’s settle when it pins suspension_grace_deadline_block and never afterward — which is what makes a governance write unable to shorten a running grace window.
The twelve cbqs.base_rate.* rows sit outside the partition with the codec bounds: they are genesis-validation values, frozen for the life of the chain (§4.5). They are not governance-writable, are NOT part of the 25-scalar governance-bounds read, and cost no governance state read — a rate derivation reads the active schedule record, never a parameter store. Their Maximum column constrains genesis validation only. The seven codec bounds named above likewise sit outside the partition: they are protocol constants that decode paths enforce, are not governance-writable, and cost no governance state read. Their Maximum column constrains genesis validation only. cbqs.max_proof_lanes_per_subscription belongs here rather than among the governance scalars because it is a wire-safety bound derived from cbqs.max_wire_frame_bytes: it is enforced entirely by the broker’s decode and admission paths, never read by a chain handler, and adding it to the 25-scalar governance set would change the §18.1 state-I/O reservations for every handler that reads the bounds set. A stream snapshots its pricing basis and rate, and a governance reduction MUST NOT invalidate it: an already-accepted stream record is never re-examined against a later parameter write, and v1 has no transition-height mechanism because it needs none. Grants are the deliberate exception. A StreamGrantV1 is not a stored record but a credential presented per request, and §7 checks its max_lane_creates_per_min against the live cbqs.max_lane_creates_per_min on every presentation, so lowering that bound does narrow outstanding grants. That is intended — the bound exists to cap instantaneous load, which a credential signed before the write would otherwise escape — and it is safe in a way record re-validation is not, because the grant carries no accrued economic position that a narrowing could destroy. Base-rate unit sizes and unit rates MUST be at least one and no greater than the table maxima. Unit-rate values are denominated in the chain’s smallest CBY unit per block. These are genesis values, fixed for the life of the chain; repricing the base rate is a coordinated-release protocol change by a later CIP, as §4.5 states.

18.1 State-I/O reservation bounds

Each instruction reserves exactly this many state reads and writes before its first state operation. The pairs are protocol constants: they do not vary with stream shape, encryption, pricing basis, or any other observed state, and changing one is a consensus change requiring a new document version. Reservations that read the full governance bounds set include its 25 parameter reads in the totals above. That set is exactly the 25 live governance scalars: cbqs.max_streams_per_owner, cbqs.max_provider_endpoints, cbqs.max_provider_endpoint_uri_bytes, cbqs.max_inline_message_bytes, cbqs.max_groups_per_stream, cbqs.max_lanes_per_stream, cbqs.max_lane_ids_per_grant, cbqs.max_key_envelopes_per_batch, cbqs.max_lane_creates_per_min, cbqs.max_lane_list_page, cbqs.max_subscriptions_per_connection, cbqs.max_grant_ttl_ms, cbqs.max_clock_skew_ms, cbqs.max_chain_staleness_blocks, cbqs.standard.retention_ms, cbqs.standard.idempotency_horizon_ms, cbqs.standard.visibility_ms, cbqs.standard.max_attempts, cbqs.fast.retention_ms, cbqs.fast.batch_max_ms, cbqs.fast.batch_max_records, cbqs.slow_consumer_timeout_ms, cbqs.suspend_grace_blocks, cbqs.rent.provider_bps, and cbqs.rent.platform_bps — one read each — plus one read of the registry’s pause-accounting record, which every admitted handler performs so rent and grace arithmetic can use the pause-adjusted height. Instructions that move escrow additionally read and write the Stream Registry custody account, which the per-instruction pairs above already include. CreateStream additionally reads and writes the zero address, which the nonrefundable creation charge (§6) credits; its pair above includes that read and write. The remaining §18 rows are protocol constants, codec bounds, or per-record schedule fields, which cost no governance read. After the transition commits, the handler refunds the unreserved remainder before transaction fee computation; the post-refund actual cycles and cells are what settle the fee and what feed block-usage demand for the basefee update, and the meter’s over-limit clamp survives only on rejected over-limit paths. The actual operation counts are those of the canonical plan, pinned per instruction and per path by the normative gas vectors published beside this document as cip-39-gas-vectors-v1.json (schema cowboy.cbqs.gas-vectors.v1): thirteen vectors covering all nine instructions plus the maximum-size variable-length payloads and the two additional TopUpStream paths §6.1 made normative — the rejection past a grace deadline and the partial payment that preserves the pinned pair. Each records payload bytes, reservation counts, actual read/write counts, charged read/write counts, hash preimage lengths, signature count, and exact cycles and cells. A conforming implementation MUST reproduce every vector exactly, measured against this artifact, whose SHA-256 is 8bfaf1d47b6f312980c828a0b37d6c4550b97e375d2b3320aa30a72d3f388da2. Any change to a metered cost changes that digest, which makes the hash the coordination point between this document and an implementation: a node whose fixture hashes differently is not running the schedule specified here. The vectors are CBQS-dispatch scoped. Each covers the decode surcharge this document defines plus its handler’s hash, signature, and state-I/O work, and excludes charges the platform levies on every transaction regardless of instruction family: the transaction base, intrinsic calldata cells, and the outer system-instruction dispatch. A conforming implementation reproduces the vector for the CBQS portion and adds its platform charges on top. Hash charging is max(1, ceil(len / 32)) words, so an empty preimage costs one word rather than zero — a distinction no vector’s magnitudes reveal, and therefore stated here rather than left inferable.

19. Chain events

Events are consensus data: emitter, topic, payload bytes, and order commit through logs_root into receipt_root, so every field below is canonical. The topic is the exact UTF-8 byte string shown; the payload is exactly the one fixed-length identifier listed — no encoding envelope, no additional fields. Registry details behind an event (generations, status, balances, epoch) are read from 0x17 state at the event’s block rather than duplicated into the log. Events MUST NOT contain record payloads, data keys, StreamGrant bytes, or member envelopes. suspended and reactivated are emitted on the NET status edge of an instruction — the difference between the status the record held when the instruction began and the status it holds when the instruction completes — and always AFTER that instruction’s primary event. At most one such event is emitted per instruction, and none is emitted when the two are equal. The net rule is normative because §6.1 gives TopUpStream two settlements around its deposit, and a reviving top-up passes through Suspended between them: settle #1 finds the pre-deposit escrow underfunded and suspends, then settle #2 finds the credited escrow sufficient and restores Active. Those interior transitions are not observable. Emitting one event per interior edge, or emitting reactivated alone, would make event count and order depend on which settlement an implementer instrumented, and both commit through logs_root into receipt_root. rent_settled is reported by exactly one instruction, the standalone SettleStreamRent. TopUpStream and CloseStream always settle internally per §6.1, and UpdateStream does so only when priced capacity changes, but that settlement is part of the operation rather than a separately reported event: TopUpStream emits topped_up then any status edge, UpdateStream emits updated then any status edge, and CloseStream emits closed alone. Exactly one reporting instruction is required because event count and per-event topic commit through logs_root into receipt_root: were more than one instruction to report an internal settlement, the emitted sequence would vary with which rule an implementer applied. SettleStreamRent emits rent_settled then any status edge, except that an expiry-materializing settlement emits data_expired alone; it is the only instruction that materializes DataExpired. TopUpStream applies §6.1’s ordering — settle, check the deadline, credit, settle — so at or past the deadline it is rejected before the deposit is debited and never materializes a terminal state. CloseStream settles and refunds directly to Closed even past the deadline and emits closed alone, so a close is never also an expiry. UpdateStream never materializes a terminal state. No instruction therefore emits two terminal topics or a terminal topic in an unenumerated position. RegisterProvider, UpdateProvider, and SetProviderStatus each emit exactly their own topic once — and a status write that does not change the status is a rejected no-op, so status_changed always marks an actual edge. CreateStream emits created alone, CloseStream emits closed alone — its internal settlement is part of the close, not a separate rent_settled — and ActivateKeyGeneration emits key_generation_activated alone. An encrypted main/key pair transitions atomically but emits exactly one event per transition, on the main stream_id; the linked key stream never emits separately. Event order within an instruction is consensus data like the events themselves. v1 base-rate schedules are fixed at genesis: no instruction writes them, so they emit no event, and changing them is a coordinated-release protocol change by a later CIP. Every GovernanceBase record carries its schedule_version, so the schedule such a stream was admitted under is always derivable from finalized state; a ProviderQuote record carries its quote digest instead.

Security Considerations

Content and metadata

End-to-end encryption protects payload contents only. Stream records, provider assignment, CBSS release activity, member-envelope recipient keys, lane and group identifiers, lane-scoped membership, sizes, timing, and connection metadata can reveal relationships. Applications requiring traffic-analysis resistance need a separate design. Provider-event record headers and payloads are intentionally cleartext operational metadata. They MUST NOT carry application payloads, member keys, or grants. The provider MUST redact payloads, grants, request proofs, envelopes, and derived keys from logs. Diagnostic logging SHOULD use hashes truncated to a documented length and MUST avoid linkable recipient-key material.

Authorization

A StreamGrant is powerful until expiry or generation bump. Holder keys SHOULD be workload-scoped and kept outside application logs. Brokers MUST use one immutable finalized view per request, fail closed on stale state or a chain- instance mismatch, reject old authorization generations, and bind each session to provider epoch zero. A lane-set grant restricts what an honest broker serves, but it does not create a cryptographic boundary among lanes sharing a data key. The cleartext lane id also exposes finer activity and membership topology to the provider. Coarse generation revocation invalidates every outstanding grant. Applications must be prepared to refresh all retained participants after a removal.

Key management

Compromise of stream_root compromises every deterministically derived generation. CBSS escrow, the rotation actor, and its job environment are high-value targets. A root-compromise recovery MUST create a new root and new stream; rotating only the generation is insufficient. Envelope signatures prevent broker substitution and forked membership batches. They do not hide the membership list from the broker. Removed members retain old keys and plaintext.

Provider equivocation

Receipt chains make conflicting signed histories provable. They do not force a provider to respond. Clients SHOULD retain their latest append, group, and checkpoint anchors outside the provider. A nonzero provider epoch is invalid in v1 and MUST be rejected rather than treated as a new lineage. Loss of the assigned provider requires the application migration flow in §16.1. CBQS receipts identify the verified durable boundary but do not make the old provider’s unavailable state recoverable.

Delivery

At-least-once delivery can repeat application effects. Consumers MUST make effects idempotent or transactionally deduplicate in their own state. A broker deduplication receipt does not make downstream effects exactly once. Strict FIFO allows a poison record to block later records until the consumer rejects it, attempts exhaust, or its deadline expires. Group deadlines and dead-letter inspection are required operational controls.

Fast mode

fast acknowledges data before durable commit. A crash may lose every provisional record after the last checkpoint. The bound is a provider operational promise under honest storage, not a cryptographic guarantee. Applications that cannot re-send or tolerate a gap MUST use standard. Within a checkpointed batch, the §12.2.1 lane commitment does make withholding detectable: a provider that drops a record from an authorized lane cannot produce a proof consistent with the signed lane_record_count, and — because lane_id is committed inside lane_payload_digest, not only as the map key — it cannot relocate the lane’s own payload to a different key and serve the lane’s subscriber a valid absence proof: a payload digest verifies at its own key and nowhere else, so absence at L’s key means no committed payload names L. What that binding does and does not establish must be stated exactly, because the two failure modes it leaves are different in kind. A provider can still commit a batch that mis-partitions records — a leaf naming some other key over records whose headers name L. Every FullStream verifier catches that by recomputing the partition from the records themselves; L’s scoped subscriber alone cannot see it, because its absence proof is genuine. Scoped omission detection is therefore honest-partition detection: it is inherited from the batch-global rules and the standing possibility of full-stream audit, not established by the absence proof itself — and the batch-global rules (lane_record_count sum, sequence-union contiguity) are dischargeable only by a FullStream verifier, as §12.2 states; to a ScopedProofs subscriber they are signed claims. Separately, a provider can decline to assign sequences to a lane’s records at all. The resulting batch satisfies every §12.2 rule, batch-global ones included, because a checkpoint claims only what it covers; no verifier of any mode detects it. That is refusal to serve rather than an inconsistency — the censorship residue below, whose remedy remains §16.1. Three limits are worth stating plainly. It bounds only what a checkpoint covers: verification completes when a batch closes, so records already delivered in an unclosed batch carry AEAD authenticity alone, and records lost while still provisional at a crash are governed by the void statement rather than by any proof. A None subscription keeps AEAD authenticity but receives no checkpoint frame at all, so it forgoes both the signed chain and omission detection — the correct trade only where a lost record is superseded rather than lost. And detection is not prevention: a provider may still withhold, and the remedy remains the §16.1 migration flow. Lane proofs disclose a prefix-occupancy pattern and nothing more. Absence is proven at the lane’s own key against the empty-leaf constant rather than against neighbouring entries, so no other lane’s identifier, ordering rank, exact count, payload, or activity window appears in a proof, and presence and absence proofs are identical in shape. What remains is structural and should not be described as nothing: a sparse path carries opaque sibling hashes and a bitmap marking which of them are non-empty. A member therefore learns that certain prefix subtrees of the lane_id space along its own query path are occupied, and popcount(bitmap) gives a lower bound on how many other lanes were active in that batch. Since lane_ids are opaque and the disclosure is a lower bound over prefixes rather than a set of identifiers, this is accepted for v1 — but it is a residual leak, not an absence of one, and a claim of zero cross-lane disclosure would be false. The sparse keying is load-bearing and MUST NOT be simplified to a sorted lane map with bracketing absence proofs. That construction is easier to implement and leaks: proving n absent lanes reveals up to 2n unauthorized lane identifiers per checkpoint — thousands at a legal scope — along with the batch’s active-lane cardinality and each proven lane’s relative rank. The leak scales with scope rather than being bounded by it, and the sparse form is also the smaller wire.

Denial of service

Message size, append rate, stream bytes, lane count, lane-list page size and creation rate, lane ids per grant, group count, subscriptions per connection, in-flight leases, retention pins, envelope count, transport credits, base-rate reservations, and decode allocations are bounded. Base-rate counter changes use checked arithmetic and fail atomically at provider caps. Capacity pressure rejects new work; it never silently evicts a live standard record. Permanent registry keys are additionally priced in capital, not only gas: every creation burns the §6 creation charge, and the §6 rate floor keeps a provider quote from zeroing it. Every consensus-side decode, hash, signature verification, state read, state write, and index update is charged by §18. The fixed one-signature-per-action limit and root-only key-batch verification prevent a caller from converting an apparently constant-cost instruction into unmetered linear cryptographic work. SetProviderStatus is free, notice-less, reversible, and fee-orderable within a block, and provider status also gates who may fund a stream: a provider can front-run a sponsor’s pending TopUpStream with a higher-fee flip to Draining, burn the sponsor’s gas, and flip back next block, invisibly to block-boundary snapshots. v1 discloses this rather than adding status-change notice or rate machinery. The bound is that the gate never binds the owner’s own funding or CloseStream, so sustained abuse only starves the provider of the rent it would have earned; sponsors of owner-absent streams remain exposed until notice machinery exists. All nine instructions MUST be classified under the CIP-12 emergency-pause circuit breaker the same way existing system-instruction families are: while the breaker covers 0x17, every CBQS instruction is rejected before state reads with no event, and read paths (/cbqs/broker-state) stay available so brokers keep serving already-authorized traffic against their last finalized view. Because the only remedies for an underfunded stream — settle, fund, close — are exactly the instructions a pause rejects while heights keep advancing, blocks during which the breaker covers 0x17 MUST NOT count toward rent accrual or grace deadlines. The mechanism is a cumulative pause-accounting record under the governance actor, keyed "system:gov:pause-accounting:" || actor address, holding completed_pause_blocks plus an optional active [paused_at_block, expires_at_block) interval: pausing folds any auto-expired prior interval and starts a new one, extension refreshes the expiry, and explicit unpause folds elapsed coverage capped at expiry instead of erasing it. Coverage is derived at READ time, not only at write time, because pause expiry is lazy — no transaction runs at the expiry block, so an expired interval can sit unfolded indefinitely:
with checked arithmetic that fails closed. The min with the expiry is mandatory in both directions: omitting the active interval entirely would lurch the clock forward by a whole pause at its expiry and mass-expire the streams the mechanism protects, while omitting the cap would freeze the rent clock permanently and pin every provider in unpaid service. Only the expression above is conforming. effective_rent_height is a derived quantity, not attested state: it is a pure function of the finalized height and the pause-accounting record, both consensus state, so any party recomputes it from a finalized view and no signing preimage changes. It is therefore NOT a field of FinalizedViewV1, which appears as served_at in nine §14 signing preimages: adding it would break every golden vector and, worse, would put inside a signature the very value two honest brokers can compute differently before a lazy pause expiry is folded. Brokers obtain it from the same atomic broker-state read as the registry records, per §4, so excluding it from the view costs no atomicity; a disagreement is resolved by recomputation rather than by comparing signatures. Reading the record is the one extra metered read in every §18.1 reservation. Every prior height-coupled system actor required exactly this kind of isolation before its pause classification. Adding the classification, the pause-interval record, and the exclusion to the deployed allowlist is a required node change. State-operation pricing intentionally differs from the flat per-instruction constants of existing system-instruction families: a TopUpStream balance movement meters cheaper on the cycles axis than a bare Transfer, while its cells charge — the axis that prices state growth — is higher. CBQS is the first dynamically metered family, and its congestion contribution is dominated by cells; re-basing the flat legacy constants is out of scope here.

Rationale

One stream primitive

One append-only stream plus groups covers the required patterns: Lanes are indexed views over one attested global order, not partitions. They reduce chain records and grants only when their rooms share a confidentiality, retention, provider, and billing domain. Partitions and exchange/binding algebra are omitted from v1. One total order is easier to specify, verify, and use for agent coordination.

Dedicated broker

Client-facing RabbitMQ, Redis, NATS, or Kafka cannot directly verify Cowboy StreamGrants, track finalized chain generations, issue CBQS receipt chains, or implement CBQS terminal delivery cycles. cbqsd is the protocol layer that supplies those semantics. Its storage engine is pluggable. An embedded segmented log or RocksDB-class engine is the v1 default. A provider MAY use a managed engine internally only when every externally observable CBQS rule is preserved. Clients never speak the internal engine protocol.

Two pricing paths

Provider quotes are the market path. They let a provider price and admit one stream explicitly, down to the §6 floor for a self-host discount. Governance base rates are the simple path: a provider publishes bounded standing capacity, owners consume that capacity at a deterministic active schedule, and the chain enforces admission counters. The base path does not remove provider consent. Consent moves from a per-stream quote to a signed provider admission record, bounded by streams, retained bytes, and throughput. A provider opts out by clearing accepts_new in that record, which refuses further governance-base admissions without disturbing anything already accepted: every existing stream keeps its snapshotted rate until its owner changes priced capacity. Opting out is the whole of the mechanism — v1 has no schedule transition to be given notice of, since the base schedule is seeded at genesis and repricing it is a coordinated-release protocol change rather than a governance write.

CBSS root escrow

CBSS releases are currently bound to a job, its assigned Runner, and an actor manifest, and secret policies cap actor ACLs. Making CBSS release one room key to every chat member would not match that contract. CBQS instead gives one rotation job access to one root and performs member fan-out with HPKE envelopes.

Rejected alternatives

  • Actor mailboxes: consensus-visible, gas-metered, and block-latency; retain them for finality-bearing state.
  • Per-runner sidecars: not a cross-runner backplane.
  • Validator data path: adds consensus cost without making off-chain agent chatter consensus-relevant.
  • Strict topology privacy: complex provisioning and blind billing still do not hide timing from a single broker.
  • Verbatim CBFS owner tokens: useful precedent, but no holder binding or CBQS verb/group model.
  • CBSS member fan-out: wrong release shape and scale.
  • Redis-style general service: expands into caches, locks, counters, and ambiguous durability outside this proposal.
  • Exactly-once claims: cannot include arbitrary consumer side effects.
  • In-place provider reassignment: adds mutable assignment and cross-provider lineage machinery; applications instead migrate through a new stream and a verified durable anchor.