> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cowboy.inc/llms.txt
> Use this file to discover all available pages before exploring further.

# CIP-39: Cowboy Queue System

> A chain-anchored, off-chain durable messaging service for cross-runner coordination, with standard and fast delivery classes, scoped proof-of-possession grants, encrypted payloads, signed receipts, and consumer groups.

<Note>
  **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)
</Note>

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

| Term                         | Definition                                                                                                                                                                                                    |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Stream**                   | An ordered append-only sequence of records with bounded retention.                                                                                                                                            |
| **Lane**                     | An opaque broker-side filtered view inside a stream. Lanes inherit the parent stream's provider, rent, retention, encryption generation, and confidentiality membership.                                      |
| **Main stream**              | An application-data stream.                                                                                                                                                                                   |
| **Key stream**               | The linked `standard` stream that carries signed per-member key envelopes for an encrypted main stream.                                                                                                       |
| **Provider**                 | A registered operator of `cbqsd`.                                                                                                                                                                             |
| **Pricing basis**            | The provider-signed quote or the versioned genesis base schedule whose derived rate is snapshotted into a stream.                                                                                             |
| **Base-rate reservation**    | Chain-accounted provider capacity consumed by live governance-base-priced streams, including suspended streams.                                                                                               |
| **Provider epoch**           | A reserved provider-lineage generation that is fixed at zero in v1.                                                                                                                                           |
| **Broker epoch**             | A provider-controlled generation used by a `fast` stream across broker restarts.                                                                                                                              |
| **Chain instance**           | The exact Cowboy chain history identified by the domain-separated genesis-configuration fingerprint. It distinguishes re-geneses that reuse the same numeric chain id.                                        |
| **Finalized view**           | One immutable `(chain_instance_id, height, block_hash)` snapshot used for every chain-state decision made while processing one broker request.                                                                |
| **Authorization generation** | A chain-controlled generation embedded in every StreamGrant. A bump invalidates all older grants.                                                                                                             |
| **Encryption generation**    | The active data-key generation for an encrypted main stream.                                                                                                                                                  |
| **Consumer group**           | Broker-side delivery state that lets one or more consumers compete for each record.                                                                                                                           |
| **Cursor**                   | A replay position. `fast` subscriptions use cursors and do not use delivery leases.                                                                                                                           |
| **Durable checkpoint**       | A provider-signed commitment to `fast` records that crossed the durable-commit boundary.                                                                                                                      |
| **Broker epoch transition**  | The provider-signed `Clean` or `Void` record required whenever a `fast` stream advances its broker epoch. A `Void` transition invalidates prior-epoch provisional records beyond the last durable checkpoint. |

### 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:

```text theme={null}
SYS_CBQS = 218
```

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:

```text theme={null}
SigningKeyAlgorithm {
  Ed25519 = 1,  // 32-byte public key, 64-byte signature
}

HpkeSuite {
  X25519_HKDF_SHA256_CHACHA20POLY1305 = 1
}
```

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:

```text theme={null}
chain_instance_id =
  keccak256(
    "cowboy-chain-identity-v1:" ||
    genesis_source_bytes
  )
```

`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:

```text theme={null}
FinalizedViewV1 {
  chain_instance_id: Bytes32,
  height:            u64,
  block_hash:        Bytes32,
}

CbqsSigningContextV1 {
  version:           u8,      // 1
  chain_id:          u64,
  chain_instance_id: Bytes32,
}
```

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

```text theme={null}
PricingBasisV1 =
  ProviderQuote {              // tag 1
    quote_digest: Bytes32
  } |
  GovernanceBase {             // tag 2
    schedule_version: u64,
    schedule_digest:  Bytes32
  }

StreamRecordV1 {
  version:                   u8,      // 1
  chain_instance_id:         Bytes32,
  stream_id:                 Bytes32,
  purpose:                   Main | Key,
  owner:                     Address,
  owner_nonce:               u64,
  admin_key:                 SigningPublicKey,
  rotation_key:              Option<SigningPublicKey>,
  delivery_class:            Standard | Fast,
  encrypted:                 bool,
  linked_stream_id:          Option<Bytes32>,
  provider:                  Address,
  provider_epoch:            u64,
  provider_signing_key:      SigningPublicKey,
  authorization_generation: u64,
  encryption_generation:    u64,
  pricing_generation:       u64,
  status:                    Active | Suspended | Closed | DataExpired,
  config:                    StreamConfigV1,
  pricing_basis:             PricingBasisV1,
  rate_per_block:            u128,
  escrow_balance:            u128,
  last_settled_block:        u64,
  suspended_at_block:        Option<u64>,
  suspension_grace_deadline_block: Option<u64>,
  created_at_block:          u64,
}
```

The main stream id is:

```text theme={null}
keccak256(
  "cbqs/stream-id/v1" ||
  chain_id_u64_be ||
  chain_instance_id ||
  owner_address_20 ||
  owner_nonce_u64_be
)
```

`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:

```text theme={null}
key_stream_id =
  keccak256("cbqs/key-stream-id/v1" || main_stream_id)
```

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

```text theme={null}
StreamConfigV1 {
  retention_ms:              u64,
  retained_bytes_limit:      u64,
  max_message_bytes:         u32,
  max_append_bytes_per_sec:  u64,
  max_delivered_bytes_per_sec: u64,
  max_lanes:                 u32,
  idempotency_horizon_ms:    u64,  // Standard only
  default_visibility_ms:     u64,  // Standard only
  max_visibility_ms:         u64,  // Standard only
  default_max_attempts:      u16,  // Standard only
  fast_batch_max_ms:         u32,  // Fast only
  fast_batch_max_records:    u32,  // Fast only
}
```

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

| Field                         | Validated against                                                | Source    |
| ----------------------------- | ---------------------------------------------------------------- | --------- |
| `max_message_bytes`           | `cbqs.max_inline_message_bytes`                                  | LIVE      |
| `max_lanes`                   | `cbqs.max_lanes_per_stream`                                      | LIVE      |
| `max_delivered_bytes_per_sec` | `>= max_append_bytes_per_sec`                                    | —         |
| `retention_ms`                | `cbqs.standard.retention_ms` / `cbqs.fast.retention_ms` by class | IMMUTABLE |
| `idempotency_horizon_ms`      | `cbqs.standard.idempotency_horizon_ms`                           | IMMUTABLE |
| `max_visibility_ms`           | `cbqs.standard.visibility_ms`                                    | IMMUTABLE |
| `default_visibility_ms`       | `max_visibility_ms` of the same record                           | —         |
| `default_max_attempts`        | `cbqs.standard.max_attempts`                                     | IMMUTABLE |
| `fast_batch_max_ms`           | `cbqs.fast.batch_max_ms`                                         | IMMUTABLE |
| `fast_batch_max_records`      | `cbqs.fast.batch_max_records`                                    | IMMUTABLE |

`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

```text theme={null}
BaseRateAdmissionV1 {
  accepts_new:                   bool,
  max_active_streams:            u64,
  max_total_retained_bytes:      u128,
  max_total_append_bytes_per_sec:u128,
  max_total_delivered_bytes_per_sec:u128,
}

BaseRateReservationTotalsV1 {
  reserved_streams:              u64,
  retained_bytes:               u128,
  append_bytes_per_sec:         u128,
  delivered_bytes_per_sec:      u128,
}

ProviderEndpointTransportV1 {
  WebSocketTls = 1,
  Https        = 2,
}

ProviderEndpointV1 {
  transport: ProviderEndpointTransportV1,
  uri:       String, // canonical UTF-8, at most 2,048 bytes
}

ProviderRecordV1 {
  version:                u8,      // 1
  chain_instance_id:      Bytes32,
  provider:               Address,
  signing_key:            SigningPublicKey,
  endpoints:              Vec<ProviderEndpointV1>,
  quote_key:              SigningPublicKey,
  base_rate_admission:    BaseRateAdmissionV1,
  base_rate_reserved:     BaseRateReservationTotalsV1,
  live_assigned_streams:  u64,
  status:                 Active = 1 | Draining = 2 | Deregistered = 3,
  metadata_hash:          Bytes32,
  registered_block:       u64,
}
```

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.

```text theme={null}
ACCEPT
  WebSocketTls  wss://cbqs.example/stream
  WebSocketTls  wss://[2606:4700:4700::1111]/stream
  Https         https://provider.example
  Https         https://8.8.8.8/health

REJECT - transport mismatch (rule 2)
  WebSocketTls  https://cbqs.example/stream
  Https         wss://cbqs.example/stream

REJECT - userinfo (rule 3; the third via the raw-authority rule 4)
  Https         https://user:secret@cbqs.example/stream
  Https         https://user@cbqs.example/stream
  Https         https://@cbqs.example/stream

REJECT - localhost namespace (rule 5)
  Https         https://localhost/stream
  Https         https://localhost./stream
  Https         https://Api.LocalHost./stream

REJECT - IPv4 policy (rule 5)
  Https         https://0.0.0.0/stream              0.0.0.0/8
  Https         https://127.0.0.1/stream            127.0.0.0/8
  Https         https://2130706433/stream           127.0.0.0/8, canonicalized
  Https         https://10.0.0.1/stream             10.0.0.0/8
  Https         https://172.16.0.1/stream           172.16.0.0/12
  Https         https://192.168.0.1/stream          192.168.0.0/16
  Https         https://100.64.0.1/stream           100.64.0.0/10
  Https         https://169.254.169.254/metadata    169.254.0.0/16
  Https         https://192.0.2.1/stream            192.0.2.0/24
  Https         https://198.18.0.1/stream           198.18.0.0/15
  Https         https://224.0.0.1/stream            224.0.0.0/3
  Https         https://240.0.0.1/stream            224.0.0.0/3

REJECT - IPv6 policy (rule 5)
  Https         https://[::]/stream                 outside 2000::/3
  Https         https://[::1]/stream                outside 2000::/3
  Https         https://[::ffff:127.0.0.1]/stream   outside 2000::/3
  Https         https://[fd00::1]/stream            outside 2000::/3
  Https         https://[fe80::1]/stream            outside 2000::/3
  Https         https://[ff02::1]/stream            outside 2000::/3
  Https         https://[2001:db8::1]/stream        2001:db8::/32
```

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

```text theme={null}
PriceQuoteV1 {
  context:           CbqsSigningContextV1,
  provider:          Address,
  owner:             Address,
  stream_id:         Bytes32,
  pricing_generation: u64,
  delivery_class:    Standard | Fast,
  retained_bytes:    u64,
  append_bytes_sec:  u64,
  delivered_bytes_sec: u64,
  rate_per_block:    u128,
  valid_until_block: u64,
  signing_key_id:    Bytes32,
  signature:         Signature,
}
```

The provider signs:

```text theme={null}
keccak256(price_quote_signing_bytes_v1(quote))
```

The `quote_digest` stored in `PricingBasisV1::ProviderQuote` is exactly that
32-byte signing digest:

```text theme={null}
quote_digest = keccak256(price_quote_signing_bytes_v1(quote))
```

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:

```text theme={null}
BaseRateClassV1 {
  retained_unit_bytes:                  u64,
  retained_unit_rate_per_block:         u128,
  throughput_unit_bytes_per_sec:        u64,
  throughput_unit_rate_per_block:       u128,
  delivered_unit_bytes_per_sec:         u64,
  delivered_unit_rate_per_block:        u128,
}

BaseRateScheduleV1 {
  format_version:     u8,      // 1
  schedule_version:   u64,     // 1 at genesis
  activates_at_block: u64,     // 0 at genesis
  standard:           BaseRateClassV1,
  fast:               BaseRateClassV1,
}
```

The schedule digest is:

```text theme={null}
schedule_digest =
  keccak256(
    "cbqs/base-rate-schedule/v1" ||
    canonical(BaseRateScheduleV1)
  )
```

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:

```text theme={null}
retained_units =
  ceil_div(config.retained_bytes_limit, retained_unit_bytes)

throughput_units =
  ceil_div(config.max_append_bytes_per_sec, throughput_unit_bytes_per_sec)

delivered_units =
  ceil_div(config.max_delivered_bytes_per_sec, delivered_unit_bytes_per_sec)

rate_per_block =
  checked_u128(retained_units  * retained_unit_rate_per_block)  +
  checked_u128(throughput_units * throughput_unit_rate_per_block) +
  checked_u128(delivered_units * delivered_unit_rate_per_block)
```

`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:

```text theme={null}
rate_per_block = max(derived_rate, CBQS_MIN_RATE_PER_BLOCK)
```

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:

| Tag | Instruction             | Authorization    | Effect                                                                                                                                                           |
| --: | ----------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|   1 | `RegisterProvider`      | caller account   | Create `ProviderRecordV1`.                                                                                                                                       |
|   2 | `UpdateProvider`        | provider account | Change endpoints, metadata, keys, or base-rate admission; key rotation and admission changes are prospective.                                                    |
|   3 | `SetProviderStatus`     | provider account | Enter `Active`, `Draining`, or, when unassigned, `Deregistered`.                                                                                                 |
|   4 | `CreateStream`          | owner account    | Create a main stream using either a verified provider quote or the active governance base schedule; atomically create its zero-rate key stream when encrypted.   |
|   5 | `UpdateStream`          | owner account    | Set mutable config/admin fields, settle old rent, update pricing and base-rate reservations when capacity changes, and optionally bump authorization generation. |
|   6 | `TopUpStream`           | paying account   | Transfer CBY into stream escrow. Permissionless while the assigned provider is `Active`; otherwise owner-only.                                                   |
|   7 | `SettleStreamRent`      | permissionless   | Settle elapsed fixed rent and materialize suspension when underfunded.                                                                                           |
|   8 | `CloseStream`           | owner account    | Settle rent, stop new appends, transition to `Closed`, and refund unearned escrow.                                                                               |
|   9 | `ActivateKeyGeneration` | owner account    | Verify a durable key-batch receipt, then advance encryption generation; MAY atomically bump authorization generation.                                            |

The canonical v1 payload schemas are:

```text theme={null}
RegisterProviderArgsV1 {
  signing_key:         SigningPublicKey,
  endpoints:           Vec<ProviderEndpointV1>,
  quote_key:           SigningPublicKey,
  base_rate_admission: BaseRateAdmissionV1,
  metadata_hash:       Bytes32,
}

UpdateProviderArgsV1 {
  endpoints:           Option<Vec<ProviderEndpointV1>>,
  signing_key:         Option<SigningPublicKey>,
  quote_key:           Option<SigningPublicKey>,
  base_rate_admission: Option<BaseRateAdmissionV1>,
  metadata_hash:       Option<Bytes32>,
}

SetProviderStatusArgsV1 {
  status: ProviderStatus,
}

CreateStreamArgsV1 {
  owner_nonce:    u64,
  initial_escrow: u128,
  admin_key:      SigningPublicKey,
  rotation_key:   Option<SigningPublicKey>,
  delivery_class: DeliveryClass,
  encrypted:      bool,
  provider:       Address,
  config:         StreamConfigV1,
  price_quote:    Option<PriceQuoteV1>,
}

RotationKeyUpdateV1 = Keep = 0 | Set<SigningPublicKey> = 1 | Clear = 2

UpdateStreamArgsV1 {
  stream_id:                       Bytes32,
  config:                          Option<StreamConfigV1>,
  admin_key:                       Option<SigningPublicKey>,
  rotation_key:                    RotationKeyUpdateV1,
  price_quote:                     Option<PriceQuoteV1>,
  bump_authorization_generation:   bool,
}

TopUpStreamArgsV1 {
  stream_id: Bytes32,
  amount:    u128,
}

SettleStreamRentArgsV1 { stream_id: Bytes32 }
CloseStreamArgsV1      { stream_id: Bytes32 }

ActivateKeyGenerationArgsV1 {
  receipt:                          KeyBatchReceiptV1,
  bump_authorization_generation:   bool,
}
```

`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:

```text theme={null}
elapsed = H - last_settled_block
due     = elapsed * r                    // checked u128
```

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:

```text theme={null}
CBQS_MIN_RATE_PER_BLOCK    = 8_000    // wei per block
CBQS_CREATION_CHARGE_BLOCKS = 604_800 // blocks
```

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`:

```text theme={null}
settle(record, H):
  require status in {Active, Suspended}          // else fail closed
  require H >= last_settled_block                // else fail closed
  due = (H - last_settled_block) * r             // checked u128

  if r == 0 or escrow_balance >= due:
      escrow_balance  -= due
      last_settled_block = H
      status = Active
      clear suspended_at_block
      clear suspension_grace_deadline_block
      paid = due
  else:
      paid_blocks = escrow_balance / r
      paid        = paid_blocks * r
      escrow_balance  -= paid
      last_settled_block += paid_blocks
      if status == Active:                       // first materialization only
          suspended_at_block = last_settled_block
          suspension_grace_deadline_block =
              last_settled_block + cbqs.suspend_grace_blocks
      // when already Suspended, BOTH pinned fields are preserved unchanged
      status = Suspended
  return paid
```

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:

| Instruction             | Discipline                                                                                                              |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `TopUpStream`           | Settle at pre-deposit escrow, check expiry, credit, settle again                                                        |
| `SettleStreamRent`      | Check expiry on an already-materialized `Suspended` record first, materializing `DataExpired` if past; otherwise settle |
| `CloseStream`           | Settle, then close unconditionally — a close is valid past the deadline and is never also an expiry                     |
| `UpdateStream`          | Require materialized `Active` with both pinned fields absent; settle ONLY when priced capacity changes                  |
| `ActivateKeyGeneration` | Require materialized `Active` with both pinned fields absent; never settle                                              |

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:

```text theme={null}
cbqs.rent.provider_bps = 9000
cbqs.rent.platform_bps = 1000
```

`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:

```text theme={null}
provider_share = floor(paid_amount * provider_bps / 10000)
platform_share = paid_amount - provider_share
```

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.

```text theme={null}
StreamGrantV1 {
  context:                  CbqsSigningContextV1,
  stream_id:                Bytes32,
  authorization_generation:u64,
  grant_nonce:              Bytes32,
  holder_signing_key:       SigningPublicKey,
  holder_hpke_key:          Option<HpkePublicKey>,
  verbs:                    u16,
  lane_scope:               Any | Exact<Bytes32> | Set<Vec<Bytes32>>,
  group_scope:              Any | Exact<Bytes32>,
  not_before_ms:            u64,
  expires_at_ms:            u64,
  max_message_bytes:        u32,
  max_append_bytes_per_sec: u64,
  max_in_flight:            u32,
  max_lane_creates_per_min: u32,
  signature:                Signature,
}
```

After verifying the admin signature, every implementation derives:

```text theme={null}
grant_id = keccak256(stream_grant_signing_bytes_v1(grant))
```

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:

| Bit | Verb          |
| --: | ------------- |
|   0 | `APPEND`      |
|   1 | `CONSUME`     |
|   2 | `ACKNOWLEDGE` |
|   3 | `REPLAY`      |
|   4 | `GROUP_ADMIN` |
|   5 | `REDRIVE`     |
|   6 | `LANE_ADMIN`  |

Unknown bits MUST be rejected. A key stream does not accept ordinary `APPEND`;
it accepts only `KeyRotationBatchV1` under §9.

The admin signature is over:

```text theme={null}
keccak256(stream_grant_signing_bytes_v1(grant))
```

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:

```text theme={null}
SessionProofV1 {
  context:        CbqsSigningContextV1,
  stream_id:      Bytes32,
  provider_epoch: u64,
  grant_id:       Bytes32,
  session_id:     Bytes32,
  challenge:      Bytes32,
  signature:      Signature,
}
```

`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:

```text theme={null}
keccak256(
  "cbqs/session/v1" ||
  canonical(context) ||
  stream_id ||
  provider_epoch_u64_be ||
  grant_id ||
  session_id ||
  challenge
)
```

Every post-session request, including bounded reads, carries:

```text theme={null}
RequestProofV1 {
  context:     CbqsSigningContextV1,
  session_id:  Bytes32,
  counter:     u64,
  method:      CbqsRequestMethodV1,
  body_hash:   Bytes32,
  signature:   Signature,
}
```

`CbqsRequestMethodV1` is a canonical `u16` registry:

| Tag | Method              |
| --: | ------------------- |
|   1 | `Append`            |
|   2 | `BeginKeyBatch`     |
|   3 | `PutKeyBatchChunk`  |
|   4 | `CommitKeyBatch`    |
|   5 | `CreateLane`        |
|   6 | `CloseLane`         |
|   7 | `ListLanes`         |
|   8 | `CreateGroup`       |
|   9 | `UpdateGroup`       |
|  10 | `DeleteGroup`       |
|  11 | `OpenSubscription`  |
|  12 | `CloseSubscription` |
|  13 | `Credit`            |
|  14 | `Ack`               |
|  15 | `Nack`              |
|  16 | `Extend`            |
|  17 | `Reject`            |
|  18 | `Redrive`           |

Unknown method tags are invalid. Every proof-bound method defines one canonical
request-body type that excludes `RequestProofV1`. Its `body_hash` is:

```text theme={null}
keccak256(
  "cbqs/request-body/v1" ||
  method_u16_be ||
  canonical(method_specific_body)
)
```

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:

```text theme={null}
lane_scope: Any | Exact<Bytes32> | Set<Vec<Bytes32>>
```

`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:

```text theme={null}
LaneRecordV1 {
  stream_id:     Bytes32,
  lane_id:       Bytes32,
  status:        Active | Closed,
  created_at_ms: u64,
  metadata_hash: Bytes32,
}
```

A client with `LANE_ADMIN` creates a lane by choosing a random `lane_nonce`:

```text theme={null}
lane_id =
  keccak256("cbqs/lane-id/v1" || stream_id || 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.

```text theme={null}
LaneEventV1 {
  stream_id:      Bytes32,
  lane_id:        Bytes32,
  status:         Active | Closed,
  metadata_hash:  Bytes32,
  occurred_at_ms: u64,
}
```

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:

```text theme={null}
signing_key_id(key) =
  keccak256("cbqs/signing-key-id/v1" || canonical(key))
```

`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:

```text theme={null}
rotation_seed =
  HKDF-SHA256(stream_root, "cbqs/rotation-signing/v1" || stream_id)

data_key[g] =
  HKDF-SHA256(stream_root, "cbqs/data-key/v1" || stream_id || g_u64_be)
```

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:

```text theme={null}
KeyEnvelopeV1 {
  stream_id:             Bytes32,
  encryption_generation:u64,
  recipient_hpke_key:    HpkePublicKey,
  hpke_ciphertext:       Bytes,
}

KeyRotationBatchV1 {
  context:                   CbqsSigningContextV1,
  stream_id:                 Bytes32,
  encryption_generation:    u64,
  previous_batch_digest:     Bytes32,
  sorted_envelope_digests:   Vec<Bytes32>,
  membership_set_digest:     Bytes32,
  created_at_ms:             u64,
  signature:                 Signature,
}
```

`sorted_envelope_digests` is sorted by
`keccak256(canonical(recipient_hpke_key))`. The rotation key signs:

```text theme={null}
keccak256(key_rotation_batch_signing_bytes_v1(batch))
```

The `batch_digest` referenced by envelopes and receipts is exactly the
32-byte signing digest above:

```text theme={null}
batch_digest = keccak256(key_rotation_batch_signing_bytes_v1(batch))
```

Each entry of `sorted_envelope_digests` is the canonical envelope digest:

```text theme={null}
envelope_digest = keccak256("cbqs/key-envelope/v1" || canonical(KeyEnvelopeV1))
```

`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:

```text theme={null}
membership_set_digest = keccak256(
  "cbqs/membership-set/v1" || member_count_u32 ||
  ordered canonical recipient_hpke_key bytes
)
```

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:

```text theme={null}
key_batch_genesis = keccak256(
  "cbqs/key-batch-genesis/v1" || canonical(context) || key_stream_id
)
```

`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`:

```text theme={null}
batch_context_digest = keccak256(
  "cbqs/key-batch-context/v1" || canonical(context) || stream_id ||
  encryption_generation || previous_batch_digest || membership_set_digest ||
  created_at_ms
)
```

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:

```text theme={null}
envelope_root = keccak256(
  "cbqs/envelope-root/v1" ||
  canonical(context) ||
  key_stream_id ||
  encryption_generation ||
  batch_digest ||
  envelope_count_u32 ||
  ordered envelope digests
)
```

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:

```text theme={null}
KeyBatchReceiptV1 {
  context:                  CbqsSigningContextV1,
  served_at:                FinalizedViewV1,
  key_stream_id:            Bytes32,
  main_stream_id:           Bytes32,
  provider_epoch:           u64,
  encryption_generation:   u64,
  batch_digest:             Bytes32,
  envelope_count:           u32,
  envelope_root:            Bytes32,
  first_sequence:           u64,
  last_sequence:            u64,
  previous_global_receipt:  Bytes32,
  final_append_receipt:     Bytes32,
  signature:                Signature,
}
```

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:

```text theme={null}
AppendRequestBodyV1 {
  stream_id:              Bytes32,
  lane_id:                Bytes32,
  client_message_id:      Bytes32,
  client_created_at_ms:   u64,
  visible_after_ms:       u64,
  expires_at_ms:          u64,
  encryption_generation: u64,
  nonce:                  Bytes24,   // zero for unencrypted streams
  payload:                Bytes,
}
```

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:

```text theme={null}
RecordOriginV1 = Client | ProviderEvent

RecordKindV1 =
  Application |
  LaneCreated |
  LaneClosed |
  KeyBatchHeader |
  KeyEnvelope

RecordHeaderV1 {
  stream_id:              Bytes32,
  lane_id:                Bytes32,
  origin:                 RecordOriginV1,
  kind:                   RecordKindV1,
  client_message_id:      Bytes32,
  created_at_ms:          u64,
  visible_after_ms:       u64,
  expires_at_ms:          u64,
  encryption_generation: u64,
  nonce:                  Bytes24,
}
```

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:

```text theme={null}
StandardRecordId {
  provider_epoch: u64,
  sequence:       u64,
}
```

`sequence` is monotone within a stream. A `fast` record id is:

```text theme={null}
FastRecordId {
  provider_epoch: u64,
  broker_epoch:   u64,
  sequence:       u64,
}
```

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:

```text theme={null}
(stream_id, holder_signing_key_id, client_message_id)
```

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

```text theme={null}
GroupConfigV1 {
  group_id:               Bytes32,
  lane_id:                Bytes32,
  mode:                   StrictFifo | Concurrent,
  start:                  Head | At<StandardRecordId>,
  visibility_timeout_ms:  u64,
  max_visibility_ms:      u64,
  max_in_flight:          u32,
  max_attempts:           u16,
  record_deadline_ms:     u64,
  dead_letter_ttl_ms:     u64,
  retention_pin_until_ms: u64,
}
```

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:

```text theme={null}
ACKED | DEAD_LETTERED | EXPIRED
```

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:

```text theme={null}
FastLineageV1 {
  provider_epoch: u64,
  broker_epoch:   u64,
}

BrokerEpochTransitionV1 {
  context:                      CbqsSigningContextV1,
  served_at:                    FinalizedViewV1,
  stream_id:                    Bytes32,
  provider_epoch:               u64,
  previous_lineage:             Option<FastLineageV1>,
  replacement_broker_epoch:     u64,
  kind:                         Clean | Void,
  last_durable_checkpoint:      Bytes32,
  last_durable_sequence:        Option<u64>,
  last_provisional_sequence:    Option<u64>,
  created_at_ms:                u64,
  signature:                    Signature,
}
```

`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

```text theme={null}
CheckpointReceiptV1 {
  context:                  CbqsSigningContextV1,
  served_at:                FinalizedViewV1,
  stream_id:                Bytes32,
  provider_epoch:           u64,
  broker_epoch:             u64,
  first_sequence:           u64,
  last_sequence:            u64,
  ordered_range_root:       Bytes32,
  previous_checkpoint:      Bytes32,
  durable_high_water:       u64,
  committed_at_ms:          u64,
  signature:                Signature,
}
```

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:

```text theme={null}
fast_record_digest =
  keccak256(
    "cbqs/fast-record/v1" ||
    canonical(context) ||
    canonical(FastRecordIdV1) ||
    canonical(RecordHeaderV1) ||
    keccak256(payload)
  )
```

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:

```text theme={null}
EMPTY[256] = keccak256("cbqs/sparse-empty/v1")
EMPTY[d]   = keccak256("cbqs/merkle-node/v1" || EMPTY[d+1] || EMPTY[d+1])
```

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:

```text theme={null}
lane_leaf_digest = keccak256("cbqs/fast-lane-leaf/v1" || record_digest)

lane_sub_root(L) =
  merkle_root_v1(lane_leaf_digest for L's records, ascending global sequence)

lane_payload_digest(L) =
  keccak256(
    "cbqs/fast-lane-payload/v1" ||
    lane_id ||
    lane_sub_root ||
    lane_record_count_u32_be ||
    lane_first_sequence_u64_be ||
    lane_last_sequence_u64_be
  )

lane_map_root =
  sparse_root_v1({ lane_id -> lane_payload_digest } for each lane in the batch)
```

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:

```text theme={null}
ordered_range_root =
  keccak256(
    "cbqs/fast-ordered-range/v1" ||
    canonical(context) ||
    stream_id ||
    provider_epoch_u64_be ||
    broker_epoch_u64_be ||
    first_sequence_u64_be ||
    last_sequence_u64_be ||
    record_count_u32_be ||
    lane_map_root
  )
```

`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:

```text theme={null}
AuthenticatedRequestV1 {
  request_id: Bytes32,
  proof:      RequestProofV1,
  body:       CbqsRequestBodyV1,
}
```

`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:

```text theme={null}
BeginKeyBatchRequestBodyV1 {
  key_stream_id: Bytes32,
  batch:         KeyRotationBatchV1,
  total_chunks:  u32,
}

PutKeyBatchChunkRequestBodyV1 {
  key_stream_id: Bytes32,
  batch_digest:  Bytes32,
  chunk_index:   u32,
  envelopes:     Vec<KeyEnvelopeV1>,
}

CommitKeyBatchRequestBodyV1 {
  key_stream_id: Bytes32,
  batch_digest:  Bytes32,
}

CreateLaneRequestBodyV1 {
  stream_id:     Bytes32,
  lane_nonce:    Bytes32,
  metadata_hash: Bytes32,
}

CloseLaneRequestBodyV1 {
  stream_id: Bytes32,
  lane_id:   Bytes32,
}

ListLanesRequestBodyV1 {
  stream_id:    Bytes32,
  after_lane_id:Option<Bytes32>,
  limit:        u32,
}

CreateGroupRequestBodyV1 {
  stream_id: Bytes32,
  config:    GroupConfigV1,
}

GroupMutableConfigV1 {
  mode:                   StrictFifo | Concurrent,
  visibility_timeout_ms:  u64,
  max_visibility_ms:      u64,
  max_in_flight:          u32,
  max_attempts:           u16,
  record_deadline_ms:     u64,
  dead_letter_ttl_ms:     u64,
  retention_pin_until_ms: u64,
}

UpdateGroupRequestBodyV1 {
  stream_id: Bytes32,
  lane_id:   Bytes32,
  group_id:  Bytes32,
  config:    GroupMutableConfigV1,
}

DeleteGroupRequestBodyV1 {
  stream_id: Bytes32,
  lane_id:   Bytes32,
  group_id:  Bytes32,
}
```

A group configuration MUST satisfy, at create and at every update:

```text theme={null}
0 < visibility_timeout_ms <= max_visibility_ms <= stream.config.max_visibility_ms
0 < max_attempts          <= the §18 maximum for cbqs.standard.max_attempts
0 < max_in_flight
0 < record_deadline_ms
    dead_letter_ttl_ms     <= stream.config.retention_ms
    retention_pin_until_ms <= now_ms + stream.config.retention_ms   // saturating add
```

`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:

```text theme={null}
SubscriptionSourceV1 =
  StandardGroup {
    lane_id:  Bytes32,
    group_id: Bytes32,
  } |
  StandardReplay {
    scope: SubscriptionScopeV1,
    after: Option<StandardRecordIdV1>,
  } |
  Fast {
    scope: SubscriptionScopeV1,
    after: Option<FastRecordIdV1>,
  } |
  DeadLetters {
    lane_id:  Bytes32,
    group_id: Bytes32,
    after:    Option<StandardRecordIdV1>,
  }

OpenSubscriptionRequestBodyV1 {
  stream_id:       Bytes32,
  subscription_id: Bytes32,
  source:          SubscriptionSourceV1,
  after_group_receipt: Option<Bytes32>,
  verification:    VerificationModeV1,
}

VerificationModeV1 =
  1 None |
  2 ScopedProofs |
  3 FullStream

CloseSubscriptionRequestBodyV1 {
  stream_id:       Bytes32,
  subscription_id: Bytes32,
}

CreditRequestBodyV1 {
  stream_id: Bytes32,
  credit:    CreditV1,
}
```

`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:

```text theme={null}
DeliveryLeaseRefV1 {
  stream_id:         Bytes32,
  lane_id:           Bytes32,
  group_id:          Bytes32,
  record_id:         StandardRecordIdV1,
  delivery_cycle_id: Bytes32,
  lease_id:          Bytes32,
}

AckRequestBodyV1    { lease: DeliveryLeaseRefV1 }
NackRequestBodyV1   { lease: DeliveryLeaseRefV1, retry_after_ms: u64 }
ExtendRequestBodyV1 { lease: DeliveryLeaseRefV1, extend_by_ms: u64 }
RejectRequestBodyV1 { lease: DeliveryLeaseRefV1, reason_hash: Bytes32 }

RedriveRequestBodyV1 {
  stream_id:        Bytes32,
  lane_id:          Bytes32,
  group_id:         Bytes32,
  record_id:        StandardRecordIdV1,
  terminal_cycle_id:Bytes32,
}
```

`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:

```text theme={null}
initial_delivery_cycle_id =
  keccak256(
    "cbqs/delivery-cycle-initial/v1" ||
    canonical(context) ||
    stream_id ||
    lane_id ||
    group_id ||
    canonical(record_id)
  )
```

A successful redrive creates exactly one successor cycle:

```text theme={null}
redrive_delivery_cycle_id =
  keccak256(
    "cbqs/delivery-cycle-redrive/v1" ||
    canonical(context) ||
    stream_id ||
    lane_id ||
    group_id ||
    canonical(record_id) ||
    terminal_cycle_id ||
    previous_group_receipt
  )
```

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:

```text theme={null}
lease_id =
  keccak256(
    "cbqs/delivery-lease/v1" ||
    canonical(context) ||
    stream_id ||
    lane_id ||
    group_id ||
    canonical(record_id) ||
    delivery_cycle_id ||
    attempt_u16_be ||
    holder_signing_key_id
  )
```

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:

```text theme={null}
CbqsClientFrameV1 =
  1 OpenSession(OpenSessionRequestV1) |
  2 CompleteSession(CompleteSessionRequestV1) |
  3 Request(AuthenticatedRequestV1)

OpenSessionRequestV1 {
  request_id: Bytes32,
  stream_id:  Bytes32,
  grant:      StreamGrantV1,
}

CompleteSessionRequestV1 {
  request_id: Bytes32,
  proof:      SessionProofV1,
}
```

`OpenSession.stream_id` MUST equal `grant.stream_id`. The server frame tags are:

```text theme={null}
CbqsServerFrameV1 =
  1 SessionChallenge(SessionChallengeV1) |
  2 SessionOpened(SessionOpenedV1) |
  3 Response(CbqsResponseV1) |
  4 Delivery(RecordDeliveryV1) |
  5 Checkpoint(CheckpointFrameV1) |
  6 BrokerEpochTransition(BrokerEpochTransitionV1) |
  7 Error(CbqsErrorV1) |
  8 DeliveryState(DeliveryStateUpdateV1) |
  9 GroupState(GroupStateUpdateV1)
```

The session result bodies are:

```text theme={null}
SessionChallengeV1 {
  request_id:    Bytes32,
  context:       CbqsSigningContextV1,
  served_at:     FinalizedViewV1,
  stream_id:     Bytes32,
  grant_id:      Bytes32,
  provider_epoch:u64,
  session_id:    Bytes32,
  challenge:     Bytes32,
  expires_at_ms: u64,
}

SessionOpenedV1 {
  request_id:    Bytes32,
  context:       CbqsSigningContextV1,
  served_at:     FinalizedViewV1,
  stream_id:     Bytes32,
  grant_id:      Bytes32,
  provider_epoch:u64,
  session_id:    Bytes32,
  expires_at_ms: u64,
}
```

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:

```text theme={null}
CbqsResponseV1 {
  request_id: Bytes32,
  body:       CbqsResponseBodyV1,
}

CbqsResponseBodyV1 =                              // 18 variants
    Append(AppendResultV1)
  | BeginKeyBatch(KeyBatchStageResultV1)
  | PutKeyBatchChunk(KeyBatchStageResultV1)
  | CommitKeyBatch(KeyBatchReceiptV1)
  | CreateLane(LaneRecordV1, AppendReceiptV1)
  | CloseLane(LaneRecordV1, AppendReceiptV1)
  | ListLanes(LanePageV1)
  | CreateGroup(GroupStateUpdateV1)
  | UpdateGroup(GroupStateUpdateV1)
  | DeleteGroup(GroupStateUpdateV1)
  | OpenSubscription(SubscriptionStateV1)
  | CloseSubscription(SubscriptionStateV1)
  | Credit(CreditV1)
  | Ack(DeliveryStateUpdateV1)
  | Nack(DeliveryStateUpdateV1)
  | Extend(DeliveryStateUpdateV1)
  | Reject(DeliveryStateUpdateV1)
  | Redrive(DeliveryStateUpdateV1)

AppendResultV1 {
  record_id:        RecordIdV1,
  append_receipt:   Option<AppendReceiptV1>,
  durable_watermark:Option<FastDurableWatermarkV1>,
}

FastDurableWatermarkV1 {
  stream_id:             Bytes32,
  provider_epoch:        u64,
  broker_epoch:          u64,
  last_durable_sequence: Option<u64>,
  checkpoint_anchor:     Bytes32,
}

KeyBatchStageResultV1 {
  key_stream_id:   Bytes32,
  batch_digest:    Bytes32,
  total_chunks:    u32,
  accepted_chunks: u32,
}

LanePageV1 {
  lanes:              Vec<LaneRecordV1>,
  next_after_lane_id: Option<Bytes32>,
}

SubscriptionStateV1 {
  stream_id:       Bytes32,
  subscription_id: Bytes32,
}
```

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:

```text theme={null}
DeliveryLeaseV1 {
  reference:             DeliveryLeaseRefV1,
  attempt:               u16,
  holder_signing_key_id: Bytes32,
  group_queue_sequence:  u64,
  issued_at_ms:          u64,
  expires_at_ms:         u64,
}

RecordDeliveryV1 {
  subscription_id:  Bytes32,
  record_id:         RecordIdV1,
  header:            RecordHeaderV1,
  payload:           Bytes,
  append_receipt:    Option<AppendReceiptV1>,
  lease:             Option<DeliveryLeaseV1>,
  delivery_state_receipt: Option<DeliveryStateReceiptV1>,
  durable_watermark: Option<FastDurableWatermarkV1>,
}
```

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

```text theme={null}
LaneProofV1 {
  lane_id:              Bytes32,
  payload:             Option<LanePayloadV1>,
  bitmap:              Bytes32,
  siblings:            Vec<Bytes32>,
}

LanePayloadV1 {
  lane_sub_root:        Bytes32,
  lane_record_count:    u32,
  lane_first_sequence:  u64,
  lane_last_sequence:   u64,
}

CheckpointFrameV1 {
  subscription_id: Bytes32,
  receipt:         CheckpointReceiptV1,
  lanes:           Vec<LaneProofV1>,
}
```

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:

```text theme={null}
CbqsErrorV1 {
  request_id:      Option<Bytes32>,
  subscription_id: Option<Bytes32>,
  code:            u16,
  message:         Utf8Bytes,
  details:         Bytes,
}
```

`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:

```text theme={null}
SubscriptionScopeV1 = Any | Exact<Bytes32> | Set<Vec<Bytes32>>
```

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:

```text theme={null}
CreditV1 {
  subscription_id: Bytes32,
  records:         u32,
  bytes:           u64,
}
```

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:

```text theme={null}
CbqsServerFrameBatchV1 {
  frames: Vec<CbqsServerFrameV1>,
}
```

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:

| Enum                   | v1 tags                                                                                                                                                                                                      |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Signing algorithm      | `Ed25519 = 1`                                                                                                                                                                                                |
| Delivery class         | `Standard = 1`, `Fast = 2`                                                                                                                                                                                   |
| Lane scope             | `Any = 1`, `Exact = 2`, `Set = 3`                                                                                                                                                                            |
| Group scope            | `Any = 1`, `Exact = 2`                                                                                                                                                                                       |
| Record origin          | `Client = 1`, `ProviderEvent = 2`                                                                                                                                                                            |
| Record kind            | `Application = 1`, `LaneCreated = 2`, `LaneClosed = 3`, `KeyBatchHeader = 4`, `KeyEnvelope = 5`                                                                                                              |
| Record id              | `Standard = 1`, `Fast = 2`                                                                                                                                                                                   |
| Broker transition kind | `Clean = 1`, `Void = 2`                                                                                                                                                                                      |
| Delivery-state action  | `Ack = 1`, `Nack = 2`, `Extend = 3`, `Reject = 4`, `Redrive = 5`, `GroupCreate = 6`, `GroupUpdate = 7`, `GroupDelete = 8`, `LeaseIssue = 9`, `LeaseExpire = 10`, `AttemptsExhaust = 11`, `RecordExpire = 12` |
| Snapshot scope         | `Stream = 1`, `Lane = 2`                                                                                                                                                                                     |
| Snapshot coverage      | `StandardRecordId = 1`, `CheckpointReceiptDigest = 2`                                                                                                                                                        |

For every signed type, the named codec function returns:

```text theme={null}
domain_separator ||
canonical(context) ||
canonical(listed fields after context and before signature)
```

The signer signs the Keccak-256 digest of those bytes:

| Signed object               | Domain separator                    | Sole signing-bytes function                  |
| --------------------------- | ----------------------------------- | -------------------------------------------- |
| `PriceQuoteV1`              | `cbqs/price-quote/v1`               | `price_quote_signing_bytes_v1`               |
| `StreamGrantV1`             | `cbqs/stream-grant/v1`              | `stream_grant_signing_bytes_v1`              |
| Session proof of possession | `cbqs/session/v1`                   | `session_proof_signing_bytes_v1`             |
| `RequestProofV1`            | `cbqs/request/v1`                   | `request_proof_signing_bytes_v1`             |
| `KeyRotationBatchV1`        | `cbqs/key-rotation-batch/v1`        | `key_rotation_batch_signing_bytes_v1`        |
| `KeyBatchReceiptV1`         | `cbqs/key-batch-receipt/v1`         | `key_batch_receipt_signing_bytes_v1`         |
| `BrokerEpochTransitionV1`   | `cbqs/broker-epoch-transition/v1`   | `broker_epoch_transition_signing_bytes_v1`   |
| `CheckpointReceiptV1`       | `cbqs/checkpoint-receipt/v1`        | `checkpoint_receipt_signing_bytes_v1`        |
| `AppendReceiptV1`           | `cbqs/append-receipt/v1`            | `append_receipt_signing_bytes_v1`            |
| `DeliveryStateReceiptV1`    | `cbqs/delivery-state-receipt/v1`    | `delivery_state_receipt_signing_bytes_v1`    |
| `CbqsSnapshotManifestV1`    | `cbqs/snapshot-manifest/v1`         | `snapshot_manifest_signing_bytes_v1`         |
| `StandardRetentionAnchorV1` | `cbqs/standard-retention-anchor/v1` | `standard_retention_anchor_signing_bytes_v1` |
| `GroupRetentionAnchorV1`    | `cbqs/group-retention-anchor/v1`    | `group_retention_anchor_signing_bytes_v1`    |
| `FastRetentionAnchorV1`     | `cbqs/fast-retention-anchor/v1`     | `fast_retention_anchor_signing_bytes_v1`     |

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:

```text theme={null}
stream_receipt_genesis =
  keccak256(
    "cbqs/stream-genesis/v1" ||
    canonical(context) ||
    stream_id ||
    provider_epoch_u64_be
  )

lane_receipt_genesis =
  keccak256(
    "cbqs/lane-genesis/v1" ||
    canonical(context) ||
    stream_id ||
    lane_id ||
    provider_epoch_u64_be
  )

checkpoint_genesis =
  keccak256(
    "cbqs/checkpoint-genesis/v1" ||
    canonical(context) ||
    stream_id ||
    provider_epoch_u64_be
  )
```

```text theme={null}
AppendReceiptV1 {
  context:                CbqsSigningContextV1,
  served_at:              FinalizedViewV1,
  stream_id:              Bytes32,
  lane_id:                Bytes32,
  origin:                 RecordOriginV1,
  kind:                   RecordKindV1,
  record_id:              StandardRecordIdV1 | FastRecordIdV1,
  client_message_id:      Bytes32,
  header_hash:            Bytes32,
  payload_hash:           Bytes32,
  visible_after_ms:       u64,
  expires_at_ms:          u64,
  encryption_generation: u64,
  previous_global_receipt:Bytes32,
  previous_lane_receipt:  Bytes32,
  committed_at_ms:        u64,
  signature:              Signature,
}
```

`header_hash` is:

```text theme={null}
keccak256(
  "cbqs/record-header/v1" ||
  canonical(RecordHeaderV1)
)
```

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:

```text theme={null}
keccak256(append_receipt_signing_bytes_v1(receipt))
```

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.

```text theme={null}
DeliveryStateActionV1 {
  Ack         = 1,
  Nack        = 2,
  Extend      = 3,
  Reject      = 4,
  Redrive     = 5,
  GroupCreate = 6,
  GroupUpdate = 7,
  GroupDelete = 8,
  LeaseIssue  = 9,
  LeaseExpire = 10,
  AttemptsExhaust = 11,
  RecordExpire    = 12,
}

DeliveryStateReceiptV1 {
  context:                CbqsSigningContextV1,
  served_at:              FinalizedViewV1,
  stream_id:              Bytes32,
  lane_id:                Bytes32,
  group_id:               Bytes32,
  provider_epoch:         u64,
  action:                 DeliveryStateActionV1,
  record_id:              Option<StandardRecordId>,
  lease_id:               Option<Bytes32>,
  resulting_state_digest: Bytes32,
  previous_group_receipt: Bytes32,
  committed_at_ms:        u64,
  signature:              Signature,
}
```

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:

```text theme={null}
GroupCreate | GroupUpdate:
  keccak256(
    "cbqs/group-config-state/v1" ||
    canonical(context) ||
    stream_id ||
    provider_epoch_u64_be ||
    canonical(resulting GroupConfigV1)
  )

GroupDelete:
  keccak256(
    "cbqs/group-deleted-state/v1" ||
    canonical(context) ||
    stream_id ||
    lane_id ||
    group_id ||
    provider_epoch_u64_be
  )
```

Group mutation replay carries the committed state preimage:

```text theme={null}
GroupStateUpdateV1 {
  config:  Option<GroupConfigV1>,
  receipt: DeliveryStateReceiptV1,
}
```

`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:

```text theme={null}
DeliveryResultStateV1 {
  action:                  DeliveryStateActionV1,
  stream_id:               Bytes32,
  lane_id:                 Bytes32,
  group_id:                Bytes32,
  record_id:               StandardRecordIdV1,
  delivery_cycle_id:       Bytes32,
  lease_id:                Option<Bytes32>,
  attempt:                 u16,
  holder_signing_key_id:   Option<Bytes32>,
  available_at_ms:         Option<u64>,
  issued_at_ms:            Option<u64>,
  lease_expires_at_ms:     Option<u64>,
  terminal_at_ms:          Option<u64>,
  terminal_expires_at_ms:  Option<u64>,
  reason_hash:             Option<Bytes32>,
  successor_cycle_id:      Option<Bytes32>,
  group_queue_sequence:    u64,
}

resulting_state_digest =
  keccak256(
    "cbqs/delivery-result-state/v1" ||
    canonical(context) ||
    canonical(DeliveryResultStateV1)
  )
```

```text theme={null}
DeliveryStateUpdateV1 {
  state:   DeliveryResultStateV1,
  receipt: DeliveryStateReceiptV1,
}
```

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.

| Action            | Required result fields                                                                                                                                                       |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Ack`             | `lease_id`, nonzero `attempt`, `terminal_at_ms`                                                                                                                              |
| `Nack`            | `lease_id`, nonzero `attempt`, and either `available_at_ms` for a retry or `terminal_at_ms` plus `terminal_expires_at_ms` when the completed attempt exhausts `max_attempts` |
| `Extend`          | `lease_id`, nonzero `attempt`, `holder_signing_key_id`, `issued_at_ms`, `lease_expires_at_ms`                                                                                |
| `Reject`          | `lease_id`, nonzero `attempt`, `terminal_at_ms`, `terminal_expires_at_ms`, present `reason_hash`                                                                             |
| `Redrive`         | zero `attempt`, `available_at_ms`, `successor_cycle_id`; `delivery_cycle_id` names the terminal predecessor                                                                  |
| `LeaseIssue`      | `lease_id`, nonzero `attempt`, `holder_signing_key_id`, `issued_at_ms`, `lease_expires_at_ms`                                                                                |
| `LeaseExpire`     | `lease_id`, nonzero `attempt`, `lease_expires_at_ms`, `available_at_ms`                                                                                                      |
| `AttemptsExhaust` | `lease_id`, nonzero `attempt`, `lease_expires_at_ms`, `terminal_at_ms`, `terminal_expires_at_ms`                                                                             |
| `RecordExpire`    | `terminal_at_ms`; `lease_id` is optional and `attempt` is nonzero exactly when it is present                                                                                 |

`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:

| Operation                  | Atomic durable commit set                                                                                                                                                                                                                                                                                                                           |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `standard` append          | Record bytes, global and lane receipt predecessors and new tails, append receipt, dedupe request digest, and the exact original response returned by a retry.                                                                                                                                                                                       |
| Lane mutation              | `LaneRecordV1`, provider-originated `LaneEventV1`, its append receipt, and both receipt-chain tails.                                                                                                                                                                                                                                                |
| Group or delivery mutation | Group config or record delivery state, lease state, resulting-state digest, self-verifying state update, delivery-state receipt, replay successor index, and group receipt-chain tail.                                                                                                                                                              |
| `fast` durable batch       | Batch records, ordered-range root, durable watermark, checkpoint receipt, and checkpoint-chain tail.                                                                                                                                                                                                                                                |
| Broker restart             | Incremented broker epoch, clean or void transition, and new checkpoint-chain tail before any replacement-lineage append.                                                                                                                                                                                                                            |
| Retention floor            | Advanced floor, its signed retention anchor, and the pruned records, so a served anchor always attests completed deletion.                                                                                                                                                                                                                          |
| Key rotation batch         | Each staged chunk during upload; then at commit, atomically: the materialized `KeyBatchHeader` and `KeyEnvelope` records with their append receipts, the key-stream receipt-chain tail, the batch-chain tail, and the signed `KeyBatchReceiptV1`. The §9.3 layout is the only valid materialization; chunking exists in the staging namespace only. |

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:

```text theme={null}
StandardRetentionAnchorV1 {
  context:                  CbqsSigningContextV1,
  served_at:                FinalizedViewV1,
  stream_id:                Bytes32,
  provider_epoch:           u64,
  lane_id:                  Option<Bytes32>,
  first_retained_sequence:  u64,
  previous_global_receipt:  Bytes32,
  previous_lane_receipt:    Option<Bytes32>,
  committed_at_ms:          u64,
  signature:                Signature,
}

GroupRetentionAnchorV1 {
  context:                  CbqsSigningContextV1,
  served_at:                FinalizedViewV1,
  stream_id:                Bytes32,
  provider_epoch:           u64,
  lane_id:                  Bytes32,
  group_id:                 Bytes32,
  latest_retained_receipt:  Bytes32,
  group_queue_sequence:     u64,
  committed_at_ms:          u64,
  signature:                Signature,
}

FastRetentionAnchorV1 {
  context:                  CbqsSigningContextV1,
  served_at:                FinalizedViewV1,
  stream_id:                Bytes32,
  provider_epoch:           u64,
  broker_epoch:             u64,
  first_retained_sequence:  u64,
  previous_checkpoint:      Bytes32,
  committed_at_ms:          u64,
  signature:                Signature,
}
```

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:

```text theme={null}
CbqsSnapshotManifestV1 {
  context:               CbqsSigningContextV1,
  stream_id:             Bytes32,
  scope:                 Stream | Lane<Bytes32>,
  snapshot_hash:         Bytes32,
  cbfs_reference:        Bytes,
  covered_through:       StandardRecordId | CheckpointReceiptDigest,
  encryption_generation:u64,
  provider_anchor:       Bytes32,
  application_signature:Signature,
}
```

`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:

| Code | Name                           |
| ---: | ------------------------------ |
| 3900 | `StreamNotFound`               |
| 3901 | `StreamNotActive`              |
| 3902 | `StreamSuspended`              |
| 3903 | `ProviderEpochStale`           |
| 3904 | `AuthorizationGenerationStale` |
| 3905 | `EncryptionGenerationStale`    |
| 3906 | `InvalidGrant`                 |
| 3907 | `GrantExpired`                 |
| 3908 | `VerbDenied`                   |
| 3909 | `GroupScopeDenied`             |
| 3910 | `InvalidProofOfPossession`     |
| 3911 | `RequestReplay`                |
| 3912 | `MessageTooLarge`              |
| 3913 | `RateLimited`                  |
| 3914 | `Backpressure`                 |
| 3915 | `IdempotencyConflict`          |
| 3916 | `IdempotencyExpired`           |
| 3917 | `CursorTooOld`                 |
| 3918 | `GroupNotFound`                |
| 3919 | `LeaseStale`                   |
| 3920 | `DeliveryTerminal`             |
| 3921 | `FastModeHasNoLeases`          |
| 3923 | `InvalidReceiptChain`          |
| 3924 | `InvalidKeyBatch`              |
| 3925 | `ChainStateStale`              |
| 3926 | `LaneNotFound`                 |
| 3927 | `LaneClosed`                   |
| 3928 | `LaneScopeDenied`              |
| 3929 | `LaneLimitExceeded`            |
| 3930 | `BrokerEpochTransitionMissing` |
| 3932 | `SubscriptionLimitExceeded`    |
| 3933 | `BaseRateNotAccepted`          |
| 3934 | `BaseRateCapacityExceeded`     |
| 3935 | `BaseRateScheduleUnavailable`  |
| 3936 | `PricingBasisInvalid`          |
| 3937 | `UnsupportedVersion`           |
| 3938 | `ChainInstanceMismatch`        |
| 3939 | `GroupConfigInvalid`           |
| 3940 | `HolderInFlightLimitExceeded`  |
| 3941 | `ChainUnreachable`             |
| 3942 | `ChainViewInconsistent`        |
| 3943 | `VerificationModeIncompatible` |
| 3944 | `NotCheckpointBoundary`        |
| 3945 | `ProofScopeTooWide`            |
| 3946 | `LongPollBusy`                 |
| 3947 | `ControlBacklogExceeded`       |

`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:

| Parameter                                                | Default       | Maximum           |
| -------------------------------------------------------- | ------------- | ----------------- |
| `cbqs.max_streams_per_owner`                             | 1,024         | 16,384            |
| `cbqs.max_provider_endpoints`                            | 4             | 8                 |
| `cbqs.max_provider_endpoint_uri_bytes`                   | 2,048         | 2,048             |
| `cbqs.max_inline_message_bytes`                          | 262,144       | 1,048,576         |
| `cbqs.max_groups_per_stream`                             | 4,096         | 65,536            |
| `cbqs.max_lanes_per_stream`                              | 16,384        | 65,536            |
| `cbqs.max_lane_ids_per_grant`                            | 4,096         | 16,384            |
| `cbqs.max_proof_lanes_per_subscription`                  | 512           | 512               |
| `cbqs.max_key_envelopes_per_batch`                       | 65,536        | 65,536            |
| `cbqs.max_key_envelopes_per_chunk`                       | 1,024         | 1,024             |
| `cbqs.max_key_batch_chunk_bytes`                         | 4,194,304     | 4,194,304         |
| `cbqs.max_wire_frame_bytes`                              | 5,242,880     | 5,242,880         |
| `cbqs.max_server_frames_per_batch`                       | 256           | 256               |
| `cbqs.max_error_message_bytes`                           | 2,048         | 2,048             |
| `cbqs.max_error_details_bytes`                           | 16,384        | 16,384            |
| `cbqs.max_lane_creates_per_min`                          | 256           | 4,096             |
| `cbqs.max_lane_list_page`                                | 256           | 1,024             |
| `cbqs.max_subscriptions_per_connection`                  | 256           | 4,096             |
| `cbqs.max_grant_ttl_ms`                                  | 86,400,000    | 604,800,000       |
| `cbqs.max_clock_skew_ms`                                 | 60,000        | 300,000           |
| `cbqs.max_chain_staleness_blocks`                        | 30            | 300               |
| `cbqs.base_rate.standard.retained_unit_bytes`            | 1,073,741,824 | 1,099,511,627,776 |
| `cbqs.base_rate.standard.retained_unit_rate_per_block`   | 67            | 1,000,000,000     |
| `cbqs.base_rate.standard.throughput_unit_bytes_per_sec`  | 1,048,576     | 1,073,741,824     |
| `cbqs.base_rate.standard.throughput_unit_rate_per_block` | 13,300        | 1,000,000,000     |
| `cbqs.base_rate.standard.delivered_unit_bytes_per_sec`   | 1,048,576     | 1,073,741,824     |
| `cbqs.base_rate.standard.delivered_unit_rate_per_block`  | 66,700        | 1,000,000,000     |
| `cbqs.base_rate.fast.retained_unit_bytes`                | 1,073,741,824 | 1,099,511,627,776 |
| `cbqs.base_rate.fast.retained_unit_rate_per_block`       | 67            | 1,000,000,000     |
| `cbqs.base_rate.fast.throughput_unit_bytes_per_sec`      | 1,048,576     | 1,073,741,824     |
| `cbqs.base_rate.fast.throughput_unit_rate_per_block`     | 8,000         | 1,000,000,000     |
| `cbqs.base_rate.fast.delivered_unit_bytes_per_sec`       | 1,048,576     | 1,073,741,824     |
| `cbqs.base_rate.fast.delivered_unit_rate_per_block`      | 66,700        | 1,000,000,000     |
| `cbqs.standard.retention_ms`                             | 604,800,000   | 2,592,000,000     |
| `cbqs.standard.idempotency_horizon_ms`                   | 86,400,000    | 604,800,000       |
| `cbqs.standard.visibility_ms`                            | 30,000        | 900,000           |
| `cbqs.standard.max_attempts`                             | 10            | 100               |
| `cbqs.fast.retention_ms`                                 | 3,600,000     | 86,400,000        |
| `cbqs.fast.batch_max_ms`                                 | 100           | 1,000             |
| `cbqs.fast.batch_max_records`                            | 1,024         | 65,536            |
| `cbqs.slow_consumer_timeout_ms`                          | 60,000        | 900,000           |
| `cbqs.rent.provider_bps`                                 | 9,000         | 10,000            |
| `cbqs.rent.platform_bps`                                 | 1,000         | 10,000            |
| `cbqs.suspend_grace_blocks`                              | 604,800       | 2,592,000         |

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.

| Instruction             | Reserved reads | Reserved writes |
| ----------------------- | -------------: | --------------: |
| `RegisterProvider`      |             28 |               2 |
| `UpdateProvider`        |             28 |               2 |
| `SetProviderStatus`     |              3 |               2 |
| `CreateStream`          |             34 |              11 |
| `UpdateStream`          |             35 |               8 |
| `TopUpStream`           |             34 |               7 |
| `SettleStreamRent`      |             33 |               7 |
| `CloseStream`           |             34 |               9 |
| `ActivateKeyGeneration` |              4 |               2 |

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.

| Topic                                  | Payload                  |
| -------------------------------------- | ------------------------ |
| `cbqs.provider.registered`             | 20-byte provider address |
| `cbqs.provider.updated`                | 20-byte provider address |
| `cbqs.provider.status_changed`         | 20-byte provider address |
| `cbqs.stream.created`                  | 32-byte main `stream_id` |
| `cbqs.stream.updated`                  | 32-byte `stream_id`      |
| `cbqs.stream.topped_up`                | 32-byte `stream_id`      |
| `cbqs.stream.rent_settled`             | 32-byte `stream_id`      |
| `cbqs.stream.suspended`                | 32-byte `stream_id`      |
| `cbqs.stream.reactivated`              | 32-byte `stream_id`      |
| `cbqs.stream.key_generation_activated` | 32-byte main `stream_id` |
| `cbqs.stream.closed`                   | 32-byte `stream_id`      |
| `cbqs.stream.data_expired`             | 32-byte `stream_id`      |

`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_id`s 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:

```text theme={null}
coverage(H) = completed_pause_blocks +
              active.map(|a| min(H, a.expires_at_block) − a.paused_at_block)
                    .unwrap_or(0)
effective_rent_height(H) = H − coverage(H)
```

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:

| Pattern           | Construction                                                                                |
| ----------------- | ------------------------------------------------------------------------------------------- |
| Work queue        | One `standard` stream, one group, many competing consumers                                  |
| Fan-out           | One stream, one group per subscriber class                                                  |
| Chat              | One lane per room inside a shared confidentiality domain; groups or cursors per participant |
| Request/reply     | Request stream plus a reply-stream grant in the payload                                     |
| Event or CRDT log | One lane per logical log, retained replay with independent cursors; `fast` when repairable  |

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.
