open-standards/wai/extensions/bearer-receipts.md
Transaction Science a4583d567c
All checks were successful
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 14s
wai-key-proxy / does this change touch it (push) Successful in 14s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 25s
wai-app and Create / wai-app · does this change touch it (push) Successful in 25s
wai-audit collector and player integration / does this change touch it (push) Successful in 32s
WAI Studio / does this change touch it (push) Successful in 19s
wai-quantum for JavaScript / does this change touch it (push) Successful in 1m42s
wai-quantum studio and desktop / does this change touch it (push) Successful in 1m51s
wai-sink-swift native sink package / does this change touch it (push) Successful in 2m14s
wai-fleet service / does this change touch it (push) Successful in 2m53s
wai-ledger service / does this change touch it (push) Successful in 3m11s
wai-sink-android native sink library / does this change touch it (push) Successful in 3m7s
WAI operation catalogue / does this change touch it (push) Successful in 3m24s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 3m26s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 3m38s
joule-code conformance gate / joule-moq-relay, every target (push) Successful in 5m27s
joule-code conformance gate / joule-moq-ingest, every target (push) Successful in 7m6s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 7m20s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 7m8s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 4m34s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 12s
wai-quantum energy-landscape jobs / does this change touch it (push) Successful in 11s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 37s
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Successful in 2m41s
wai-key-proxy / proxy · linux (push) Successful in 1m49s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m22s
wai-audit collector and player integration / collector · linux (push) Successful in 2m18s
wai-quantum for JavaScript / javascript · linux (push) Successful in 5m2s
wai-quantum studio and desktop / studio · browser (push) Successful in 5m54s
wai-ledger service / service · linux (push) Successful in 2m11s
wai-fleet service / service · linux (push) Successful in 4m2s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 9m50s
wai-quantum energy-landscape jobs / the same bits · linux-arm64 (push) Successful in 12m24s
WAI Studio / Studio in WebKit · linux (push) Successful in 13m19s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 11m24s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 1m32s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 6m4s
wai-quantum energy-landscape jobs / native against WebAssembly, over many seeds (push) Successful in 4m51s
wai-quantum energy-landscape jobs / reader, kernels, answers and the stated result · linux, release (push) Successful in 15m29s
wai-quantum energy-landscape jobs / the module under wasm32-wasip2 (wasmtime) (push) Successful in 19m19s
wai release binaries build on Windows / the seven release binaries · windows (push) Successful in 17m45s
wai-quantum studio and desktop / desktop app · windows (push) Successful in 10m5s
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Successful in 56s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 1m6s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 2m14s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 6m29s
wai-fleet service / service · macos (push) Successful in 39s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 4m0s
wai-ledger service / service · macos (push) Successful in 1m3s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 10m25s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 5m34s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 15m12s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 15m51s
wai-quantum energy-landscape jobs / the same bits · macos (push) Successful in 6m17s
Four MoQ drafts mapped onto what WAI has: playout timing, the FEC catalog binding, MoQ files and V3C tracks
Each is a mapping with a reference module, a corpus and a second implementation written
from its text. None adds a record or changes a byte of an envelope, a receipt or a claim,
and none is wired into a relay, the room service or a page: each module is what a publisher
and a receiver call. Every draft was read at the latest revision in the IETF archive on 10
November 2026.

- Playout timing (draft-nandakumar-moq-tempo-00; co-watch §15, new). joule-moq-room's tempo
  module reads and writes the draft's CaptureTimestamp, PlayoutDelay and HopTime, and holds
  the instant they state to the room: it is the millisecond in which co-watch's controlled
  position enters the object, and a delay above the track's stated bound is refused whatever
  instant it gives. A change of delay the room decides is a pause and a play, or a seek, sent
  as any control. The catalog member wai_tempo (moq-streaming-format §4.4) carries the bound
  and the two catalog extensions the draft leaves to be defined. The draft's property types
  are unassigned; the module uses placeholders in MOQT's application range.
- The MoQ FEC catalog binding (draft-ramadan-moq-fec-catalog-00, -fec-core-00,
  -fec-block-codes-00; carousel §8.3, new). wai-rs's carousel::moq_fec binds a schedule's
  raptorq entries to the FEC instances a catalog declares under moq-object-v1, and back:
  an object a block, a pass a repair layer, symbols as symbol-track and repair objects. An
  entry binds only where everything carousel §6 derives from it holds of the block sent (the
  same symbols a pass, the same K'); a catalog is read failing closed; a rebuilt object is
  delivered only at its entry's Location, length and content hash. tests/carousel_moq_fec.rs
  writes the corpus and decodes every object with an independent implementation of RFC 6330,
  whose symbols for the block-codes draft's Appendix B are that appendix's.
- Multicast endpoints (draft-ramadan-moq-multicast-02; bearer-receipts §9.7, which read -00).
  At -02 an endpoint lists only mmtp tracks and their repair tracks, so no WAI track rides
  one of the draft's endpoints, and §9.7 now says so. What is implemented is the reading of a
  catalog's multicast member, each endpoint standing or ignored with a label; it joins nothing.
- MoQ files (draft-jennings-moq-file-05; live-recording §8.7, new). joule-moq's file module
  writes a track's objects as the draft's .moq and .dat files and reads them back byte for
  byte, properties and all, so a track and its receipts track verify from the files alone. A
  reader opens nothing outside its directory and refuses a file cut short.
- V3C tracks (draft-yuyou-moq-v3c-00; moq-streaming-format §4.5, new). joule-moq's v3c
  module splits a wai.pc.vpcc payload into the draft's component tracks and joins it back,
  with the catalog member wai_v3c. ISO/IEC 23090-5 was not read: the unit header is RFC 10034
  §4.3.1's copy, unit types 7 and above are refused, and the corpus holds opaque test bytes,
  no bitstream an encoder wrote.

Planted faults, each caught by the reference and, but where noted, by the second
implementation: a symbol of another object (offered as itself, in place of a block's own
symbol, and, in the test alone, as a repair symbol under a rewritten header); a truncated data
file and a truncated .moq file; a timing claim outside its bound (a delay past the track's
bound, and an instant one millisecond off the controlled position's). Writing the tests
found two holes, both closed: a rebuilt object was not held to its entry's Location, and a
Group missing its last object on a track joins to another payload, which the text now states.

Corpora: wai/moqt-conformance/{moq-file,tempo,v3c}/ and
wai/live-conformance/carousel-moq-fec/, with moq_file_verify.py, tempo_verify.py,
v3c_verify.py and carousel_moq_fec_verify.py. The registry lists wai_tempo and wai_v3c
(export regenerated). wai/ietf/catalog_check.py holds them, fec and the packaging values
fec-repair, fec-source and v3c in three more writers' catalogs, with 31 more planted faults;
draft-charlot-moq-wai-packaging-01 is unchanged. joule-code-conformance.yml and
joule-moq-room.yml run the second implementations and trigger on the corpora and texts their
tests read; wai conform --independent runs four more verifiers.

Not done: no relay rewrites HopTime or reads the Sync Support parameter; nothing sends
stamped objects, FEC tracks or V3C tracks on a session; the drafts' MMTP packaging, FEC on
multicast paths and Reed-Solomon are not mapped.

Co-Authored-By: Claude <[email protected]>
2026-11-10 18:34:23 -05:00

940 lines
60 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# WAI Extension: Bearer Receipts
> Status: Draft. Registers the record `wai.bearer.receipts`: one receipt on every bearer. A
> publisher's group and object receipts ([jwp-receipts](jwp-receipts.md) §2.4.1, §2.4.2) are
> the same bytes whether a group travels over a two-way relay chain, IP multicast or a
> broadcast bearer with no return path. What a bearer changes is what can honestly be said
> about delivery, and this extension says it beside the receipt, in two claim classes of
> jwp-receipts: a publisher's **transmission** claim (§4, jwp-receipts §2.4.14), which declares
> the bearer a group was sent over and attests transmission, never delivery; and a sink's
> **reconciliation** claim (§5, jwp-receipts §2.4.15), signed later and out of band, which
> attests the objects the sink received by a Merkle root under the group's own tree. A
> verifier derives from them what a relying party may state about a group (§6), an upper
> bound on each receiver's share of a shared transmission's energy (§7), and confirms keys
> on a bearer with no return path under anchors provisioned in advance (§8). §9 maps the
> carriage onto the bearers. WAI defines no transport, here as elsewhere
> ([moq-streaming-format](moq-streaming-format.md)). The reference implementation is the `bearer_receipts` module of `wai-rs` (feature
> `bearer_receipts`), with the two classes' bodies in `claim_receipt::bearer`; the corpus is
> [`live-conformance/bearers/`](../live-conformance/bearers/), and its second implementation
> is `live-conformance/bearer_verify.py`.
> §13 writes a reconciliation and a holdings record compactly for return paths whose messages
> hold few bytes; its reference is the `compact_return` module (features `bearer_receipts` and
> `holdings`), its corpus [`live-conformance/compact-return/`](../live-conformance/compact-return/)
> and its second implementation `live-conformance/compact_return_verify.py`.
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
>
> Texts read for this revision, each at the edition named: ETSI TS 103 769 V1.2.1 (2024-11);
> ETSI TS 103 770 V1.2.1 (2024-09); ATSC A/331:2026-04, A/333:2026-04 and A/360:2026-08; 3GPP
> TS 26.346 V19.3.0, TS 26.517 V19.3.0 and TS 26.502 V19.6.0; RFC 6726;
> `draft-ramadan-moq-multicast-02` (§1 to §6; the latest revision in the IETF archive on 10
> November 2026, with -00 and -01 beside it) and `draft-ietf-moq-transport-22`, Internet-Drafts;
> ETSI TS 102 796 V1.8.1 (2026-09) and ETSI TS 102 809 V1.3.1. A clause number of one of
> them in this text is that edition's.
## 1. Scope
In scope: the bearer classes (§2); why the bearer is declared beside the receipt and not in
it (§3); the transmission claim (§4) and the reconciliation claim (§5), their bodies, rules
and context checks; the verdict a verifier derives for one group from its receipts and those
claims (§6); the upper bound on a receiver's share of a shared transmission's energy (§7);
verification with no return path (§8); the mappings onto bearers (§9); and a compact form of
reconciliations and holdings records for return paths whose messages hold few bytes, carried in
fragments (§13).
When each object passes on a bearer with no return path, and with which forward error
correction, is [carousel](carousel.md)'s: a publisher's `schedule` claim beside the transmission
claim here, from which a verifier derives when a receiver within a loss bound holds each object.
A node that sends again, over a bearer instance of its own, objects it holds of a group another key
signed, declares that instance in a `cache-transmission` claim, and reconciliations over it are
counted beside the publisher's instances: [cache-authority](cache-authority.md) §5, §7.
Out of scope: a transport or a bearer; a reception-reporting protocol — a reconciliation
travels on whatever path a sink later has, as a receipt file (http-delivery §8.1) or inside a
bearer's own reporting (§9); a pre-emption authority ([unidirectional-delivery](unidirectional-delivery.md)
§3 registers none, and nor does this extension); and any statement of rendering. No receipt
over a bearer with no return path can prove rendering, and nothing here attempts it.
Relationship to the core spec: `wai.bearer.receipts` is a **record** ([SPEC](../SPEC.md)
§5): the transmission and reconciliation claims are signed records a WAI tool emits and
verifies. It names no payload format and is not dispatchable.
## 2. Terms
- **Bearer**: what carries a group's objects from a publisher to sinks. Its **class** is one of:
- `bidirectional`: each sink is reached over a path that can carry its statements back,
such as a relay chain or unicast HTTP;
- `multicast`: one transmission reaches every sink that joined it, with no per-sink path on
the bearer itself; a sink may have another path;
- `unidirectional`: one transmission, and no path back from any sink.
- **Bearer instance**: one transmission a publisher made, or a node that sends again what it holds
([cache-authority](cache-authority.md) §5), named by an absolute URI, its
`bearer_id`: a multicast session, a broadcast service, a relay chain's origin. WAI reads
nothing from it and compares it byte for byte.
- **Transmission**: putting a group's objects and their receipts onto a bearer instance.
**Delivery**: a sink receiving them. A transmission is not a delivery, on any bearer.
- **Sink**: a receiver that verifies what it received under [jwp-receipts](jwp-receipts.md)
§3. **Relying party**: anyone else who reads a group's receipts and claims.
- **Group**, **link hash**, **leaf**, **root**: as jwp-receipts §2.2, §2.4.1 and §3 step 3.
`delivered-verified` and `inclusion-verified`: as jwp-receipts §3.
## 3. One receipt on every bearer
The group receipt signs its objects' root, its coordinates, its times and its publisher
(jwp-receipts §2.3). It names no bearer, and this extension adds no member to it. The bearer
is declared beside the receipt, in a transmission claim signed by the group's own key and
naming the group by its link hash (§4). Three facts fix that form:
- **A group is not tied to one bearer.** A group may be sent over a relay chain and a
broadcast at once, and carried again later by another. A bearer inside the signed payload
would make one group per bearer, each with its own link hash, and the chain of a track
(jwp-receipts §3 step 3) could link through only one of them.
- **The bearer is a fact about a transmission, not about the bytes.** The receipt attests the
bytes; a declaration attests what the publisher did with them, and is a statement of its own
with its own signature.
- **Every receipt already issued keeps verifying.** A group carried before this extension, or
carried by a party that does not use it, verifies exactly as before; what is missing is a
declaration, which is reported as missing (§6.5), never inferred.
So the "MUST declare the bearer" of [unidirectional-delivery](unidirectional-delivery.md) §4
is met by a signed declaration that changes no byte of the receipt it declares. A sink that
holds a group's objects and receipts verifies them under jwp-receipts §3 the same way on
every bearer and reaches the same verdicts (`delivered-verified` for each object whose bytes
it holds): the corpus carries one group, byte for byte, on all three classes (§11). What
differs between bearers is what anyone other than that sink may state about delivery (§6),
and how a receiver's share of the transmission's energy is bounded (§7).
## 4. The transmission claim (`class: "transmission"`)
A publisher that sends a group over a bearer **MAY** declare the transmission in a
`transmission` claim, one per bearer instance, and **MUST** where
[unidirectional-delivery](unidirectional-delivery.md) §4 requires it. Its `subject_type` is
`receipt` and its `subject` the group receipt's link hash (jwp-receipts §3 step 3). It is
signed by the key that signed the group. It attests that the publisher transmitted the group
over the bearer instance it names, and nothing about what any sink received.
### 4.1 Body
| member | type | meaning |
|---|---|---|
| `bearer` | string | The bearer's class: `bidirectional`, `multicast` or `unidirectional` (§2). |
| `bearer_id` | absolute URI | The bearer instance (§2). |
| `energy` | object or `null` | The energy the bearer spent transmitting the group on this instance, every repetition included, written as jwp-receipts §2.4.12 writes a session's `energy`: `{ "joules_micro", "energy_provenance", "energy_uncertainty", "energy_counter_filtering", "energy_calibration_ref" }`. `null` when the transmission was not metered, never a figure of 0 ([energy-measurement](energy-measurement.md) §2). It is not the group's `joules_total`, which is the energy of producing the objects. |
| `attests` | string | **MUST** be `"transmitted"`. Any string passes step 2; another value is refused at step 4. |
`bearer` is read at jwp-receipts §3.1 step 2 against its enumeration (`field_type`), and
`bearer_id` as an absolute URI.
### 4.2 Rules
Checked at jwp-receipts §3.1 step 4, in this order:
1. `attests` **MUST** be `"transmitted"` (`transmission_not_transmitted`): a declaration that
says it attests anything more is refused, never read as a weaker one.
2. Where `energy` is not `null`, its `joules_micro` **MUST NOT** be 0
(`energy_zero_figure`), and its class is admitted as jwp-receipts §2.4.5 rule 2 admits one
(`energy_unknown_acquisition`, `energy_legacy_acquisition`,
`energy_acquisition_evidence`).
### 4.3 Against the group
Checked at jwp-receipts §3.1 step 6 against the group receipt the verifier holds, which has
passed jwp-receipts §3 steps 1 and 2 (`context_invalid` otherwise; another receipt is
`context_mismatch`). In order:
1. the group's link hash is the claim's `subject` (`transmission_subject_mismatch`): a
declaration over another group declares nothing about this one;
2. the claim's `signer_pubkey` is the group's (`transmission_signer_mismatch`): only the
publisher declares its own transmission.
A transmission claim checked without the group stops at step 5 and is not counted by §6.
### 4.4 Authority
Confirmed under jwp-receipts §7, a transmission claim needs the authority `transmission`,
which is not pinned-only and is not `delivery`. Its energy figure, where `energy` is not
`null`, is a second item, as a session's is (jwp-receipts §7.5): it needs `transmission` and
the energy authority of its class, so a hardware-class figure is confirmed only under a pinned
anchor, and a verifier reports the declaration's confirmation and its figure's side by side.
## 5. The reconciliation claim (`class: "reconciliation"`)
A sink that received objects of a group **MAY** later sign which ones, in a `reconciliation`
claim. Its `subject_type` is `receipt` and its `subject` the group receipt's link hash. It is
signed by the sink, never by the publisher. It is not a hop: it is never a group's parent,
never in the publisher's chain, and nothing in the publisher's chain changes when it arrives.
### 5.1 Body
| member | type | meaning |
|---|---|---|
| `bearer` | string | The class of the bearer the sink received over (§2). |
| `bearer_id` | absolute URI | The bearer instance it received over: the `bearer_id` of the publisher's transmission claim, or of a node's cache-transmission claim, for that instance. |
| `leaf_count` | integer | The group's `leaf_count`. |
| `received` | hex | A bitmap of ⌈`leaf_count` / 8⌉ bytes. Object *i* is bit `0x80 >> (i mod 8)` of byte ⌊*i* / 8⌋, so the hexadecimal reads left to right in object order; the bit is set exactly when the sink accepted object *i* under jwp-receipts §3 steps 1–6 over its bytes. |
| `received_root` | 32 B hex | The root jwp-receipts §2.2 computes over the leaves of the received objects, in increasing `leaf_index`, each leaf in the form its object receipt takes (legacy or labelled). With every bit set it is the group's `root_hash`. |
| `attests` | string | **MUST** be `"received"`. Any string passes step 2; another value is refused at step 4. |
The root is a commitment under the group's own tree, so it is checked against the
publisher's root without the objects' bytes (§5.3).
### 5.2 Rules
Checked at jwp-receipts §3.1 step 4, in this order:
1. `attests` **MUST** be `"received"` (`reconciliation_not_received`).
2. `leaf_count` is at least 1, `received` holds exactly ⌈`leaf_count` / 8⌉ bytes, no bit at
or past `leaf_count` is set, and at least one bit is set (`reconciliation_bitmap_malformed`).
A sink that received nothing has nothing to reconcile.
### 5.3 Against the group and its objects
Checked at jwp-receipts §3.1 step 6 against the group receipt the verifier holds, which has
passed jwp-receipts §3 steps 1 and 2 (`context_invalid`, `context_mismatch` as §4.3), and the
object receipts it holds under that group, each of which has passed jwp-receipts §3 step 5 —
verified by inclusion at least. In order:
1. the group's link hash is the claim's `subject` (`reconciliation_subject_mismatch`);
2. the claim's `signer_pubkey` is not the group's (`reconciliation_by_publisher`): a
publisher's statement that it received its own objects is not a reconciliation;
3. `leaf_count` is the group's (`reconciliation_leaf_count_mismatch`);
4. with every bit set, `received_root` is the group's `root_hash`; otherwise the verifier
holds an object receipt at every set bit (`reconciliation_objects_not_held`, recoverable
by obtaining them, never accepted silently) and `received_root` is the root over their
leaves (`reconciliation_root_mismatch`). A root over a leaf that is not the group's at its
position — a hash not in the group — is refused here.
A reconciliation checked without the group stops at step 5 and is not counted by §6.
### 5.4 What it attests
An accepted reconciliation establishes that its signer says it received the objects its
bitmap names, with the bytes their receipts commit to. It does not establish that the signer
held those bytes: content hashes travel with the receipts, so a party that saw only the
receipts can sign one. It never attests presentation, rendering or timeliness. Like a
`binding-recovery` record (jwp-receipts §2.6.3), it attests less than a hop would, and says so
in-band (`attests`).
### 5.5 Authority
Confirmed under jwp-receipts §7, a reconciliation needs the authority `reconciliation`, which
is not pinned-only and is not `delivery`. A sink signs under an origin's key where its origin
speaks for it, and under a `did:key` otherwise, which is unconfirmed (jwp-receipts §7.4
step 1), as a sink's `reconstruct-session` claim is.
## 6. The bearer verdict
What a verifier may state about one group, from its receipts and the claims about it.
### 6.1 Inputs
The group receipt; the object receipts the verifier holds under it, each with the object's
envelope bytes where it holds them; the transmission and reconciliation claims it holds about
the group, each signer's in the order of its chain; optionally, for a verifier that is itself
a sink, the bearer class and instance it received the group over (§6.5); optionally, a
statement offered about the group (§6.4); and optionally, for a relying party that counts
receivers only by keys it has confirmed, the set of those keys (§7).
### 6.2 Steps
1. **The group.** jwp-receipts §3 steps 1 and 2. A group that fails is refused with that
step's reason, and nothing below is derived from it. (Its chain is jwp-receipts §3 step
3's, and unchanged.)
2. **The objects.** Each object receipt is checked against the group: with the envelope, by
jwp-receipts §3 steps 4–6 (`delivered-verified`); without it, by step 5
(`inclusion-verified`); or refused with its step's reason. These verdicts do not depend on
the bearer, the declarations or the reconciliations.
3. **The claims.** Each claim, in order, under jwp-receipts §3.1 steps 1–5, each signer's
previous claim being the last of its claims held before it here; then step 6, a
transmission claim by §4.3 and a reconciliation by §5.3 against the object receipts that
passed step 2. A claim of another class is `context_mismatch` at step 6. A claim refused at
any step takes no further part.
4. **The declarations.** The accepted transmission claims, by `bearer_id`. Two with one
`bearer_id` and different bodies set aside every declaration of that instance
(`bearer_contradicted`): a publisher that contradicts itself about one transmission has
declared neither. Two with the same body are one declaration.
5. **The reconciliations**, each accepted one in order. It is set aside, and counted nowhere,
at the first of these that holds: step 4 set aside the declarations of its `bearer_id`
(`bearer_contradicted`); no declaration has its `bearer_id` (`bearer_not_declared`); the
declaration names another `bearer` (`bearer_contradicted`); the verifier counts only
confirmed keys and its key is not among them (`sink_not_confirmed`). Otherwise it is
**counted** for its bearer instance. A second counted reconciliation of one key for one
instance adds no sink (`reconciliation_duplicate_sink`): its bits are joined to the first's.
6. **The counts.** `sinks`: the distinct keys with a counted reconciliation; `whole`: those
whose joined bits, over every instance, cover every object of the group; for each object,
the number of distinct keys whose joined bits hold it; and for each declaration, the
distinct keys counted for its instance.
### 6.3 Status
- **`published`**: no declaration stands. The group verifies; nothing is stated about where
it went.
- **`transmitted`**: at least one declaration stands, and no reconciliation is counted. The
publisher sent it over the bearers declared; nothing is stated about any sink.
- **`reconciled`**: at least one reconciliation is counted. `sinks` keys say they received
objects of the group, out of band and on their own authority, and `whole` of them say they
received every object.
A verifier **MUST NOT** report a group, or anything read from it, as delivered to anyone but
itself on the strength of the publisher's receipts and declarations: the receipts attest
publication, the declarations transmission. `delivered-verified` stays the verdict of a sink
over bytes it holds (jwp-receipts §3).
### 6.4 Statements offered about a group
A verifier **MAY** be asked to check a statement made about the group:
- **transmitted over a bearer class**: holds when a declaration of that class stands, and is
otherwise refused (`bearer_not_declared`);
- **delivered to *k* sinks** (*k* at least 1): holds when `whole` is at least *k*. With `whole`
0 it is refused (`delivery_not_reconciled`), whatever was declared: a transmitted-only group
presented as delivered is refused here. With `whole` from 1 to *k* − 1 it is refused
(`delivery_exceeds_reconciled`).
### 6.5 A sink's own bearer
A sink knows the bearer instance it received over. Beside its object verdicts, and never
changing them, it reports the group's declaration for that instance: `declared` when one
stands with the sink's class; `bearer_contradicted` when one stands with another class, or
step 4 set the instance's declarations aside; and `bearer_not_declared` when none names the
instance. A group received over a unidirectional bearer with no declaration is
[unidirectional-delivery](unidirectional-delivery.md) §4's missing declaration.
## 7. Energy per receiver on a shared transmission
A transmission's energy is spent once, whoever receives it. On a multicast or broadcast
bearer the number of receivers is not known, and **unknown is not zero**: no figure divided by
an assumed audience is a measurement of anyone's share. What a verifier states instead is an
upper bound, for each declaration of §6.2 step 4 whose `energy` is not `null`:
```
per_receiver_upper_bound = ⌈ joules_micro / max(1, n) ⌉
```
where `n` is the number of distinct keys counted for that instance (§6.2 step 6).
- **It bounds an allocation, not a reading.** Dividing one transmission's figure among its
receivers is an allocation of that figure (as energy-steering §3.3 says of a hop shared by
pathways); no receiver's own energy was read. A verifier **MUST** report it as a bound, named
so, and **MUST NOT** report it as any receiver's measured energy.
- **Why it is an upper bound.** Every receiver of the transmission shares the one figure. If
the counted keys are distinct receivers, at least `n` receivers shared it, and an equal share
is at most `joules_micro / n`; with no reconciliation, a receiver's share is at most the whole
figure. Rounding up keeps it a bound in integer microjoules. Each reconciliation counted for
the instance tightens it, and none raises it.
- **Its class is the figure's.** The bound is stated with the transmission figure's
acquisition class and evidence (energy-measurement §2); the division adds no measurement and
changes no relative uncertainty. Bounds of two instances, or of two classes, are never added.
- **No figure, no bound.** Where `energy` is `null` the verifier states no bound at all, never
a bound of 0 (energy-measurement §2).
- **Keys are not receivers.** A receiver that reconciles under several keys is counted once
for each and lowers the bound below its share. The count is a count of keys, and a verifier
reports it as one. A relying party that needs a bound per receiver counts only keys it has
confirmed (jwp-receipts §7.4) with the authority `reconciliation` under an origin it relies
on to give that authority to one key per receiver, and reports that it did (§6.2 step 5,
`sink_not_confirmed`).
- It is not the group's production energy: `joules_total` (jwp-receipts §2.4.1) and its
objects' figures are read as jwp-receipts reads them, and never added to a transmission's.
## 8. Verification without a return path
### 8.1 Anchors
A sink on a bearer with no return path cannot retrieve a key log from an origin's location
(jwp-receipts §7.3, http-delivery §8.4): first-use retrieval is unavailable to it. It confirms
under anchors it already holds — logs provisioned in advance by its relying party (pinned), or
logs it retrieved earlier over another path (fetched, with the time it retrieved them). The
pinned-only authorities, and any authority registered for pre-emption, are confirmed only
under a pinned anchor that has not moved (jwp-receipts §7.4 step 6a), on this bearer as on any
other.
### 8.2 Logs that arrive on the bearer
A key log that reaches a sink over the bearer — in a file carousel, a delivery session, a
multicast track — is neither pinned nor fetched, whatever it holds. The sink:
- reads it as jwp-receipts §7.3 reads a log, and holds its revisions, each checked against its
parent when §7.4 step 3 walks them, so a revocation or a rotation its origin sends over the
bearer reaches a sink that has the origin anchored;
- **MUST NOT** anchor an origin from it (`anchor_on_bearer`): an item under an origin the sink
has not otherwise anchored stays unconfirmed (`origin_not_anchored`). A trust anchor
delivered over the bearer it authorises is not a trust anchor
([unidirectional-delivery](unidirectional-delivery.md) §3), for receipts as for pre-emption.
A revision on the bearer that its anchored line does not authorise — another root, a log
written by whoever controls the bearer — is no child of the trusted line (jwp-receipts §7.2
rule 7) and confirms nothing.
### 8.3 The procedure
Given the item to confirm (a group receipt with its object receipts, or a claim), the logs the
sink was provisioned with and the logs that arrived on the bearer:
1. verify the item — the group under jwp-receipts §3 steps 1 and 2 and each object under step
5; a claim under §3.1 steps 1–4 — and stop with its reason if it fails;
2. anchor each provisioned log as pinned (jwp-receipts §7.3), holding its revisions; a log
that does not read anchors nothing, with its reason;
3. read each log that arrived on the bearer as jwp-receipts §7.3 reads one and hold its
revisions; report it `held` where its origin is anchored by step 2, and `anchor_on_bearer`
otherwise. A log that does not read is held in no part, with its reason;
4. confirm the item under jwp-receipts §7.4 against every revision held and the anchors of
step 2, and, for a transmission claim with an energy figure, its figure as a second item
(§4.4).
The procedure touches no network and no clock; sinks holding the same bytes reach the same
verdicts, offline, on every platform. A confirmation describes the log as the sink holds it:
a revocation that has not reached it is not in it, and the head's `revision_hash` it reports
says how current that was.
## 9. Bearer mappings
### 9.1 What WAI fixes on every bearer (normative)
Where a bearer carries files or objects, WAI controls these bytes:
- **Envelopes.** Each object is one WAI envelope, carried whole as one file, object or segment
of the bearer; its bytes are the ones jwp-receipts §2.1 hashes. The bearer's own framing —
packets, forward error correction, an object's encoding for transport — is outside them, and
a sink hashes the envelope it reconstructs.
- **Receipts and claims.** A group receipt and every claim — transmission and reconciliation
claims among them — travel as the receipt files of [http-delivery](http-delivery.md) §8.1,
under the same name relative to the objects' location (`rcpt/<link hash>.json`), with the
same bytes and the same limits, and a sink reads each as §8.1 reads one. An object's receipts
travel in its receipt index (http-delivery §8.2). On a bearer that carries tracks rather than
files, the group and object receipts ride the receipts track
([moq-streaming-format](moq-streaming-format.md) §4.1); this revision defines no carriage of
claims on a track, so claims travel as receipt files on whatever file path the sink has.
- **The declaration rides with the group.** A publisher that declares a transmission **SHOULD**
send the transmission claim on the same bearer instance and in the same session as the
group's receipts, so that a sink receives the declaration that covers what it received.
- **Key logs.** A key log a bearer carries is the document of jwp-receipts §7.2, at the path
`/.well-known/wai-keys` relative to the origin it names, and anchors nothing (§8.2).
- **Reconciliation.** A sink sends its reconciliations, when it has a path, as receipt files;
where the bearer's own reporting carries files or opaque payloads, it **MAY** carry them
there. A reconciliation is the same claim whatever carried it.
The mappings below are **informative**: each says where these ride in the bearer's own
structures and what path back it defines.
### 9.2 FLUTE (RFC 6726)
Each envelope, receipt file and receipt index is one FLUTE file, named in the File Delivery
Table by its `Content-Location`, with `Content-Type` and, where the sender gives it,
`Content-MD5` (§3.4.2). The table admits further attributes (§3.4.2), and WAI defines none:
everything a sink checks is in the files. FLUTE is a unidirectional delivery service with no
feedback (§7.4), so a transmission over it is declared `unidirectional` unless another standard
adds a path back, and reconciliation goes over another path or none.
### 9.3 DVB multicast ABR (ETSI TS 103 769)
The multicast server carries each object as a transport object of FLUTE (Annex F) or ROUTE
(Annex H), and the gateway serves it to players over HTTP. Envelopes ride as the media
objects; receipt files, receipt indexes and key logs ride as the "other resources" a
`ResourceLocator` carousels in the same session (clause 10.2.3.14.3; ancillary objects, clause
8.3.4.4). Unicast repair (clause 9) and service reporting (clause 11: a JSON report posted to
`/dvb/mabr/reportingInformationInstance`) are paths back where the deployment has them; a
report's object-delivery event carries a status, not the object (Table 11.1.2.2-1), so a
reconciliation, which names objects, travels beside it as a receipt file. A deployment with
both paths declares `multicast`; one without, `unidirectional`. The integrity and authenticity
the specification adds (clause 12: content digests and HTTP message signatures) are the
gateway's checks of transport objects and change no WAI byte.
### 9.4 DVB-I (ETSI TS 103 770)
A service list signals the instances of a service — among them multicast transport streams
and DASH (clause 5.5.18) — and an identifier-based instance whose locator is an opaque URI
(clause 5.5.18.8). It carries no objects of its own and defines no path back. Where the service list gives an
instance a URI — an identifier-based locator, or a DASH presentation's address — a publisher's
`bearer_id` for that instance **SHOULD** be that URI, so a sink reading the list finds the
declaration by it.
### 9.5 ATSC 3.0 (A/331)
ROUTE carries envelopes as file-mode objects described by the Extended FDT, or in entity mode
(Annex A.3.3.2, A.3.3.3); receipt files, receipt indexes and key logs ride as locally cached
non-real-time files in the same ROUTE session, with relative `Content-Location` (clause
8.1.1.2), or inside a signed package (Annex A.3.3.5). A/331 requires its signalling tables to
be signed (clause 5.9; A/360:2026-08 clause 5.2.2); that signs the signalling, not the media
files, and is the broadcaster's statement, not the publisher's. Service usage reporting
(A/333:2026-04) reports consumption by service and interval, not by file or object, and is
unsigned; byte-range repair over broadband (A/331 clause 8.3) is a path back where the receiver
has broadband. A receiver with broadband sends reconciliations as receipt files; the broadcast
transmission is declared `unidirectional`, and the broadband path, where a group is also served
over it, is a `bidirectional` instance of its own.
### 9.6 3GPP MBMS and 5G multicast-broadcast (TS 26.346, TS 26.517)
MBMS download delivery carries each file over FLUTE with its own `Content-Location` (TS 26.346
clause 7.2); 5G multicast-broadcast user services carry objects by their object distribution
method, on the same download profile, in single, collection, carousel or streaming mode (TS
26.517 clause 6.2), and envelopes, receipt files, indexes and key logs ride as objects of the
session. File repair (TS 26.346 clause 9.3; TS 26.517 clause 6.2.4) is a path back. MBMS
reception reporting (TS 26.346 clause 9.4) lists each file received by its `fileURI`, with
`Content-MD5` where the table carried it, and is unsigned: it names files, not objects of a
group, and a reconciliation travels beside it. TS 26.517 defines no reception report, and
TS 26.502 leaves one for further study (clause 4.2.5). A session whose receivers report
declares `multicast`; one without a path back, `unidirectional`. A receiver of MBMS download
supports the Compact No-Code and Raptor schemes (TS 26.346 clause 7.2.2), which a schedule of such a
session states as `no-code` and `raptor` ([carousel](carousel.md) §4.1).
### 9.7 MoQ over multicast (`draft-ramadan-moq-multicast-02`)
The draft adds a root member `multicast` to a MoQ catalog, listing the multicast endpoints
its tracks are sent to (§4.1), with a MoQ unicast session first and unicast failover and
repair (§1.1, §6). A sink that holds the unicast session is reached `multicast` with a path
back, and one that only joined a multicast group is reached `unidirectional`.
**What changed since -00, and what it leaves a WAI track.** At -02 every multicast datagram
carries one MMTP packet, and an endpoint lists only tracks of packaging `mmtp` and the
`fec-repair` tracks that repair them; a track of another packaging reaches multicast through
an `mmtp` rendition of the same media, a separate track in its `altGroup` (§4.1, §5). A WAI
track is not of packaging `mmtp`, and WAI defines no MMTP rendition of an envelope
(`draft-ramadan-moq-mmt-02`, which defines that packaging, was not read), so at this revision
**no WAI track rides one of the draft's endpoints**, and the receipts track does not either.
What §9.1 fixes still holds of whatever carries the envelopes: a publisher that sends WAI
objects over a multicast group by other means declares that instance by its own `bearer_id`
(§4), and schedules it with a carousel schedule, whose RaptorQ entries are the FEC instances
of a MoQ catalog ([carousel](carousel.md) §8.3). The draft's own signed manifest records
(§7.2) are the draft's, beside the receipts; WAI reads nothing from them. No MoQ
working-group draft (`draft-ietf-moq-transport-23` among them) defines multicast at this
revision.
**Reading the endpoints.** What a sink of WAI does read is the list, to know which endpoints
a catalog offers and to refuse the ones the draft has a receiver ignore (§4.1). `multicast`
is an object whose `endpoints` is an array of at least one object, or the catalog offers none
(`endpoints_malformed`). Each endpoint is read in this order and **ignored**, with the first
that fails, the others standing:
| label | when |
|---|---|
| `endpoint_malformed` | The endpoint is not an object; `groupAddress` is not a string or `tracks` not an array of at least one object; `sourceAddress` or `protocol`, where present, is not a string; or `bandwidth`, where present, is not an integer from 0 to 2^53 − 1. |
| `endpoint_address` | `groupAddress`, or `sourceAddress` where present, is not an IP address; the two are of different families; or the group is not a multicast address of its mode: with a `sourceAddress` (source-specific), inside 232.0.0.0/8 or ff3*x*::/32; without one, inside 224.0.0.0/4 or ff00::/8 and outside those ranges. |
| `endpoint_mode` | `protocol` is neither `"ssm"` nor `"asm"`, or disagrees with whether `sourceAddress` is present. |
| `endpoint_port` | `port` is not an integer from 1 to 65,535. |
| `endpoint_packet_id` | A listed track's `name` is not a string, or its `packetId` is not an integer from 1 to 65,535 (0 is the sending entity's signalling flow). |
| `endpoint_track` | A listed track is not a track of the catalog, declares a namespace, or is neither of packaging `mmtp` nor a `fec-repair` track whose `source` is a track of packaging `mmtp`. |
| `endpoint_auth` | The endpoint carries `auth` and `multicast` does not, or the reverse; or it carries `auth` and no `bandwidth`. |
An endpoint that stands is a candidate, and no more: the draft has a receiver validate a
relay or a tunnel at the protocol layer before it uses one, with bounded attempts (§3.1), and
that is the receiver's own. The reference reads the list and joins nothing: it opens no
socket, resolves no name and reads no `networkSource`. A namespace a listed track declares
is refused outright, where the draft refuses one that differs from the catalog track's; the
reader is given a catalog, not the track it came on.
The reference reader is `carousel::moq_fec::read_endpoints` of `wai-rs`; its cases are in
[`live-conformance/carousel-moq-fec/`](../live-conformance/carousel-moq-fec/), and
`live-conformance/carousel_moq_fec_verify.py` reads them independently.
### 9.8 HbbTV (ETSI TS 102 796)
An HbbTV application reads files of a broadcast DSM-CC object carousel (ETSI TS 102 809
clause 7.1, ETSI TS 102 796 clause 7.2.2) and fetches over broadband (clause 7.3.2). V1.8.1
(HbbTV 2.0.5) adds WebAssembly for applications (clause A.3.28, Annex R), so a sink built for
WebAssembly runs inside an application. Envelopes, receipt files and key logs ride as carousel
files; the carousel is `unidirectional`, and the application's broadband path is the path back
for reconciliations. A key log carried in the carousel is not an anchor (§8.2): the anchor is
provisioned with the application, by its relying party. The carousel's own signing (ETSI TS
102 809 clause 9) is optional in HbbTV and signs the carousel, not the publisher's objects.
## 10. Reasons
The codes this extension adds, each with where it is raised:
| reason | raised at |
|---|---|
| `transmission_not_transmitted` | §4.2 rule 1 |
| `transmission_subject_mismatch` | §4.3 step 1 |
| `transmission_signer_mismatch` | §4.3 step 2 |
| `reconciliation_not_received` | §5.2 rule 1 |
| `reconciliation_bitmap_malformed` | §5.2 rule 2 |
| `reconciliation_subject_mismatch` | §5.3 step 1 |
| `reconciliation_by_publisher` | §5.3 step 2 |
| `reconciliation_leaf_count_mismatch` | §5.3 step 3 |
| `reconciliation_objects_not_held` | §5.3 step 4 |
| `reconciliation_root_mismatch` | §5.3 step 4 |
| `bearer_contradicted` | §6.2 steps 4 and 5, §6.5 |
| `bearer_not_declared` | §6.2 step 5, §6.4, §6.5 |
| `sink_not_confirmed` | §6.2 step 5 |
| `reconciliation_duplicate_sink` | §6.2 step 5 |
| `delivery_not_reconciled` | §6.4 |
| `delivery_exceeds_reconciled` | §6.4 |
| `anchor_on_bearer` | §8.2, §8.3 step 3 |
A group or object also raises the codes of jwp-receipts §3.3, a claim every code of jwp-receipts
§3.1 steps 1 to 5 and those of step 6 named in §4.3 and §5.3, and a confirmation the codes of
jwp-receipts §7.7.
## 11. Conformance
[`live-conformance/bearers/`](../live-conformance/bearers/) holds the corpus, written from fixed
seeds by `wai_bearer_vectors gen` as `vectors.json`. It has:
- **One receipt on every bearer**: one group and its object receipts, byte for byte, with a
declaration of each class, verified by a sink holding the bytes; the object verdicts are the
same in every case, and the group's bytes are checked equal across them.
- **Verdicts**: groups `published`, `transmitted` and `reconciled`; a group simulcast on two
instances, each with its own count and bound; partial and whole reconciliations, a labelled
group among them; a duplicate sink, joined; a relying party counting confirmed keys only.
- **Energy**: bounds before and after reconciliations arrive, an unmetered transmission with
no bound, and a hardware-class figure.
- **Negative controls**, one for each reason of §10 that a verdict or a claim raises: a
transmitted-only group presented as delivered; a partial reconciliation presented as delivered
to a sink; a reconciliation over a leaf that is not the group's; one signed by the publisher's
key; one whose objects are not held; a declaration over another group, one signed by another
key, and two that contradict each other; a reconciliation over an undeclared instance; a
sink whose bearer was not declared; and malformed bodies.
- **Offline**: confirmation under a pinned anchor with no network; a revocation that arrives on
the bearer; a log forged on the bearer for a pinned origin; a log that arrives on the bearer
for an origin not pinned (`anchor_on_bearer`); and a hardware-class transmission figure
confirmed beside its declaration.
A conforming verifier gives every case the outcome, the reason and everything else the corpus
states. `wai_bearer_vectors verify` and the test `tests/bearer_receipts.rs` check that the file
regenerates byte for byte, that `wai-rs` gives every case its outcome, that every reason of §10
appears, and that §10 lists exactly the reference's codes. `live-conformance/bearer_verify.py`
is the second implementation, written from this text: it shares no code with `wai-rs`; it
verifies groups and objects with the JWP corpus's second implementation
(`jwp-conformance/jwp_verify.py`), claims through steps 1–5 with the claim corpus's
(`claim-conformance/claim_verify.py`), and confirmations with the key-discovery corpus's
(`signer-conformance/signer_verify.py`), and writes §4.3, §5.3, §6, §7 and §8 itself; and it
mutates copies of the corpus and requires every copy to fail.
## 12. Security and privacy considerations
- **A declaration is the publisher's word.** Nothing checks that a bearer instance exists or
that its class is the one declared. A false `bidirectional` gains nothing here: no class of
bearer makes a group delivered (§6.3).
- **A reconciliation is the sink's word** (§5.4). Keys are cheap; a party that wants a group to
look widely received signs many reconciliations. That raises `sinks` and `whole` and lowers
the bound, and both are reported as counts of keys (§7). A relying party that needs more
counts confirmed keys only.
- **Replay.** A reconciliation names the group by its link hash, so it cannot be moved to
another group; within one group a second one of a key adds nothing (§6.2 step 5).
- **What a sink discloses.** A sink on a broadcast bearer is unobserved:
[unidirectional-delivery](unidirectional-delivery.md) §2's property is that the source learns
nothing. A reconciliation gives that up for the groups it names, to the distributor that
receives it. Reconciling is part of the delivery service a distributor operates over WAI, and
this extension sets no consent rule of its own for it. Where a regime asks the distributor
for a lawful basis, the distributor records it as a `lawful-basis` claim
([jwp-receipts](jwp-receipts.md) §2.4.6). A sink **MAY** sign each reconciliation with a
fresh `did:key`, which unlinks its reconciliations and leaves each group's count unchanged.
- **A bearer that lies about keys.** A log on the bearer anchors nothing (§8.2), so whoever
controls the bearer can withhold revisions — a revocation among them — but cannot add a key
to an anchored origin. A sink that has had no path for long holds a stale log, and its
confirmation reports the head it used.
## 13. The compact return profile
A path back from a sink or a node may carry only a few bytes in each message: a radio frame, a
text message, a satellite uplink. A reconciliation (§5) is a JSON claim that writes its group's
link hash, its root, its signer's key and its signature in hexadecimal, beside the instance's URI;
a holdings record ([holdings](holdings.md) §4) names its holder and each group by 32 bytes. This
profile writes both records in a binary form that names by short references what the receiver
already holds — the group, the bearer instance, the signer — and carries a message too long for
one frame in fragments, with a check over the whole message.
The compact form is an encoding, never a statement of its own. A receiver expands a compact
record, against what it holds, into the record's own bytes and verifies that record by its own
text; the signature a compact record carries is the record's. Nothing here changes what a
reconciliation or a holdings record states, how either is verified, or what a verifier derives
from it (§6, holdings §5).
### 13.1 Terms
- **Path**: what carries messages from a **sender** (a sink, or a node) to a **receiver** (the
distributor that reconciles, or a node that gathers for it). Its **frame** is the most bytes
one of its messages carries.
- **Context**: what the receiver holds that a compact record names. Nothing a compact record
carries adds to it.
- **Groups**: group receipts that pass jwp-receipts §3 steps 1 and 2, each with the object
receipts held under it that pass §3 step 5 against it, and the `bearer_id` of each instance
of the group the receiver holds a declaration of (§4).
- **Signers**: keys, each with a `signer_id`: keys the receiver confirmed under jwp-receipts
§7, or keys its senders enrolled with it.
- **Compact record**: a reconciliation (§13.3) or a holdings record (§13.4) in this profile's
form, at most 65,531 bytes. **Message**: a compact record followed by its **check**, the first 4
bytes of SHA-256 over the compact record. **Fragment**: one frame's part of a message (§13.6).
A fixed-size integer is unsigned and big-endian.
### 13.2 Uints, instants and references
- A **uint** is an unsigned integer in LEB128: seven bits a byte, least significant first, the
high bit set in every byte but the last. It is written in the fewest bytes, so a last byte of
0x00 follows no other byte, and its value is at most 2^53 − 1, so it has at most 8 bytes.
- An **instant** is a uint of seconds since 1970-01-01T00:00:00Z, leap seconds not counted, at
most 253,402,300,799 (9999-12-31T23:59:59Z). It stands for the RFC 3339 date-time of that
second written `YYYY-MM-DDTHH:MM:SSZ`, with `T` and `Z` in capitals, no fraction and a second
below 60. A date-time written in any other way has no instant.
- The **reference** of a value is the first *n* bytes of SHA-256 over it: a group's is 8 bytes
over its link hash (32 bytes); a bearer instance's, 4 bytes over its `bearer_id` in UTF-8; a
signer's, 8 bytes over its key (32 bytes).
- A reference **resolves** to the one value of its kind in the context that has it: a group
among the groups; a bearer instance among the instances of the group the record names; a
signer among the signers — a key with its `signer_id` for a reconciliation, a key for a
holdings record, which names no `signer_id`. Values that are equal are one value. A
reference that no value has is refused `compact_reference_unknown`, and one that more than
one value has, `compact_reference_ambiguous`: neither is resolved by choosing.
### 13.3 A compact reconciliation
| size | field | meaning |
|---|---|---|
| 1 | `kind` | 0x01 |
| 1 | `form` | below |
| 8 | `group` | the reference of the group: the claim's `subject` |
| 4 | `bearer` | the reference of the bearer instance: the body's `bearer_id` |
| uint | `seq` | the claim's `seq` |
| uint | `issued_at` | the claim's `issued_at`, as an instant (§13.2) |
| 32, 32 + uint + *n*, or 8 | `signer` | by the signer form, below |
| 32 or 0 | `parent` | the claim's `parent_receipt_hash`, where `form` says the claim has one |
| ⌈`leaf_count` / 8⌉ or 0 | `received` | the bitmap of §5.1, where `form` says the reconciliation is partial; `leaf_count` is the group's |
| 64 | `sig` | the claim's `sig` |
`form` is read by its bits:
| bits | meaning |
|---|---|
| `form & 0x03` | the body's `bearer`: 0 `bidirectional`, 1 `multicast`, 2 `unidirectional` |
| `form & 0x04` | set: **whole**, every object of the group received, and no `received` follows; clear: **partial** |
| `(form >> 3) & 0x03` | the signer form: 0, the key (32 bytes), whose Ed25519 `did:key` (jwp-receipts §7.1) is the `signer_id`; 1, the key, then a uint *n* and the `signer_id` in *n* bytes of UTF-8; 2, the reference of the signer (8 bytes) |
| `form & 0x20` | set: a `parent` follows; clear: `parent_receipt_hash` is `null` |
| `form & 0xc0` | 0 |
A reader reads exactly the bytes it is given, field by field in the order of the table, and
refuses them `compact_malformed` when they are more than 65,531 bytes, end before a field or go
on after `sig`, or when:
1. `form` sets a bit of 0xc0, or names the bearer 3 or the signer form 3;
2. a uint breaks §13.2, or `issued_at` is past its bound;
3. in the signer form 1, the `signer_id` is not UTF-8, or is the key's own `did:key`, which is
the signer form 0;
4. a partial `received` sets no bit, sets a bit at or past `leaf_count`, or sets every bit below
it, which is the whole form.
It resolves `group` as it reads it, since `received`'s length is the group's; then `bearer` among
that group's instances; and, in the signer form 2, `signer` (§13.2).
Its **expansion** is the reconciliation claim in its canonical form (jwp-receipts §2.4.3), with:
- `kind` `claim`, `class` `reconciliation`, `subject_type` `receipt`, and `subject` the link hash
of the group resolved;
- `seq`, `issued_at` (the instant's date-time), `signer_pubkey`, `signer_id`,
`parent_receipt_hash` and `sig` from the fields;
- a body of `bearer` from `form`, `bearer_id` the instance resolved, `leaf_count` the group's,
`attests` `received`, and:
- whole: `received` the bitmap that sets every bit below `leaf_count`, and `received_root` the
group's `root_hash`;
- partial: `received` as carried, and `received_root` the root jwp-receipts §2.2 computes over
the leaves of the object receipts the context holds at its set bits, in increasing order, each
leaf in its object receipt's form. A set bit at which the context holds no object receipt is
refused `compact_objects_not_held`.
The expansion is then verified as every reconciliation is: jwp-receipts §3.1, and §5 here. A
compact record whose bytes were changed, where it still reads, expands to a claim its signer did
not sign.
### 13.4 A compact holdings record
| size | field | meaning |
|---|---|---|
| 1 | `kind` | 0x02 |
| 1 | `form` | 0: the holder's key follows (32 bytes); 1: the reference of the holder follows (8 bytes) |
| 32 or 8 | `holder` | the holder |
| uint | `as_of_ms` | the record's `as_of_ms` |
| uint | `window_ms` | its `expires_ms` less its `as_of_ms` |
| uint | `n` | its entries' count, at most 2^32 − 1 |
| … | `entries` | `n` entries, below, in the record's order |
| 64 | `sig` | the record's `sig` |
An entry:
| size | field | meaning |
|---|---|---|
| 1 | `form` | `form & 0x01` set: every object of the group (holdings §4.2's form 1), and no `held` follows; `form & 0x02` set: the group by reference; every other bit 0 |
| 8, or 32 + uint | `group` | by reference: the reference of the group, whose `leaf_count` is the group's; else the group's link hash, then its `leaf_count` as a uint |
| ⌈`leaf_count` / 8⌉ or 0 | `held` | the bitmap of holdings §4.2, in its form 0 |
A reader reads exactly the bytes it is given, in the order of the tables, resolving each
reference as it reads it, and refuses them `compact_malformed` when they are more than 65,531
bytes, end before a field or go on after `sig`, or when a `form` is not one the tables give, a
uint breaks §13.2, or `n` is past its bound. Its **expansion** is the holdings record of holdings
§4.1: `WHD1`, version 1, the holder's key, `as_of_ms`, `as_of_ms` + `window_ms`, `n`, each
entry's link hash, `leaf_count`, form and `held`, and `sig`. The expansion is then read as every
holdings record is, by holdings §4.4, and holdings §5 applies to it.
### 13.5 Writing a record compactly
The compact form of a record **against a context** — what its sender knows its receiver holds —
is the one the rules below give, and its expansion against that context is the record, byte for
byte. A reader takes every form: a value written inline expands as its reference would.
- **A reconciliation** has a compact form when it reads under jwp-receipts §3.1 steps 1 and 2 as
a claim of class `reconciliation` without the hybrid members of
[pq-signatures](pq-signatures.md) §3.1, its text is its canonical form, its `issued_at` has an
instant, and its expansion against the context is that record: the context holds its group
and, for that group, its instance; its `leaf_count` is the group's; and its `received` and
`received_root` are ones §13.3 reads and derives. Its signer is written by
reference where the context holds its key with its `signer_id` and the reference resolves to
them, else in the form 0 where its `signer_id` is its key's `did:key`, else in the form 1.
- **A holdings record** has a compact form when it reads under holdings §4.4, its signature
verifying. Its holder is written by reference where the context holds its key and the reference
resolves to it, and each entry's group by reference where the context holds that group with the
entry's `leaf_count` and the reference resolves to it; each is written inline otherwise.
A record that has no compact form is refused `compact_not_encodable`, and travels in its own
form. A sink that sends its reconciliations over a path whose frame is small **SHOULD** sign them
with an `issued_at` that has an instant: to the second, in UTC.
### 13.6 Fragments
| size | field | meaning |
|---|---|---|
| 1 | `message` | the sender's number for the message, the same in each of its fragments |
| 1 | `index` | the fragment's position in the message, from 0 |
| 1 | `count` | the message's fragments, 1 to 255 |
| … | `data` | at least one byte of the message |
A message is at most 65,535 bytes. A sender cuts it, in order, into fragments of the path's frame,
the last holding what remains: the fragments' `data`, in increasing `index`, are the message. It
numbers its messages on one path modulo 256, and does not use a number again while a fragment
under it may still arrive. A path whose frame is 3 bytes or fewer carries no fragment, and a
message that would need more than 255 fragments of a path's frame is not sent over that path.
### 13.7 Reassembly
The steps below are defined over a set of fragments taken as one message's. A receiver forms that
set from the fragments it receives, by their sender, as the path addresses it, and by `message`;
how it forms the set is not a step, and refuses nothing. A set formed by `message` holds no
fragment under another number, and step 2 refuses one in a set formed otherwise. Fragments that
are the same bytes are one fragment of the set, so a fragment the path delivers twice is held
once. Given a message's fragments, in any order, a receiver checks, in this order:
1. each fragment has at least 4 bytes, a `count` of at least 1 and an `index` below its `count`
(`fragment_malformed`);
2. each has the first's `message` and `count` (`fragment_foreign`);
3. no two that differ have one `index` (`fragment_repeated`);
4. there are `count` of them (`fragment_missing`); a message of which no fragment is held is
missing;
5. their `data`, in increasing `index`, the message, is at most 65,535 bytes
(`fragment_malformed`);
6. the message has more than 4 bytes, and its last 4 are the check over the bytes before them
(`fragment_check_failed`).
The bytes before the check are the compact record, read by its `kind`: §13.3 for 0x01, §13.4 for
0x02, and `compact_malformed` for any other.
Fragments arrive in any order and are placed by their `index`. A fragment placed at a position
other than the one it was cut from — its `index` changed, or the data of two fragments exchanged —
is a **reordered** fragment. Where the headers still show a whole message, the check refuses the
message it is in (step 6), as it refuses a fragment of another message that carries this
message's `message` and `count`: steps 2 to 4 find what the headers show, and step 6 what they do
not.
### 13.8 Aggregated measurement on a small frame
A report of [telemetry-aggregation](telemetry-aggregation.md), a DAP report, is not a record of
this profile, and this profile never cuts one into fragments. A sink uploads each report whole to
the leader (telemetry-aggregation §5), and a report has no shorter form: its input shares are
encrypted to the aggregators. A task's **floor** is the length of its longest report. A sink
sends a task's reports only over a path that carries one whole, so a path whose frame is below a
task's floor carries none of its reports. Every report of a task of measures 1 to 4 of
telemetry-aggregation §3 has one length, its floor:
| measure | floor, bytes |
|---|---|
| 1 `outcomes` | 552 |
| 2 `stalls` | 824 |
| 3 `durations` | 2,664 |
| 4 `start_time` | 648 |
A sink whose every path is below a task's floor holds its reports until it has a path that
carries them, or gives them, over a short link, to a **node** that has one — a receiver's cache,
or a box that holds a carousel for the receivers near it. The node uploads each report as it
received it. It learns of each report its id, its time, its length and its task, as each
aggregator does (telemetry-aggregation §10), and which neighbour gave it, and nothing of its
measurement. A node **MAY** carry its neighbours' messages of this profile, or the records they
expand to, over its own path in the same way: a record a node carries is its sender's, unchanged,
and the node signs nothing of it.
### 13.9 Reasons
The codes this profile adds, each with where it is raised:
| reason | raised at |
|---|---|
| `compact_malformed` | §13.3, §13.4, §13.7 |
| `compact_reference_unknown` | §13.2 |
| `compact_reference_ambiguous` | §13.2 |
| `compact_objects_not_held` | §13.3 |
| `compact_not_encodable` | §13.5 |
| `fragment_malformed` | §13.7 steps 1 and 5 |
| `fragment_foreign` | §13.7 step 2 |
| `fragment_repeated` | §13.7 step 3 |
| `fragment_missing` | §13.7 step 4 |
| `fragment_check_failed` | §13.7 step 6 |
An expansion also raises the codes of its record's own verification: those of jwp-receipts §3.1
and §10 here for a reconciliation, and holdings §7's for a holdings record.
### 13.10 Conformance
[`live-conformance/compact-return/`](../live-conformance/compact-return/) holds the corpus,
written from fixed seeds by `wai_compact_return_vectors gen` as `vectors.json`. It has:
- **Round trips**: reconciliations whole and partial, over each bearer class, in each signer form,
with and without a parent, a group of labelled leaves and a bitmap whose last byte is part used
among them; and holdings records with their holder and groups by reference and inline. Each
case carries the record, the context and the compact record, written from the record and
expanded back to it, byte for byte, and the record's verdict under its own text.
- **Expansions**: every form a reader takes, a value the receiver holds written inline among
them; a compact record with a byte changed, which expands and is refused by its record's own
signature; one negative control for each rule of §13.3 and §13.4 but the bound of 65,531
bytes, which both implementations check themselves; references unknown and ambiguous, two
bearer instances that share one reference among them; a bearer instance the receiver holds for
other groups and not for the record's, which is unknown; a holder's key held under two
`signer_id`s, which resolves, since a holdings record names no `signer_id`; and a partial
reconciliation against a receiver that holds no object receipt.
- **Encodings**: records with no compact form, each refused, a reconciliation whose `leaf_count` is
2^53 − 1 beside a bitmap of one byte among them.
- **Messages and reassemblies**: compact records cut into frames of several sizes, one fragment
among them, and reassembled in order and in other orders, and with a fragment delivered twice,
byte for byte, which is held once; and a missing fragment, a repeated one (other data at its
`index`, and a fragment relabelled to another's `index`), a reordered one (data exchanged,
indices exchanged), a foreign one (another number, another count, and the same header from
another message), a changed byte, and malformed fragments.
A conforming verifier gives every case the outcome, the reason and everything else the corpus
states. `wai_compact_return_vectors verify` and the test `tests/compact_return.rs` check that the
file regenerates byte for byte, that `wai-rs` gives every case its outcome, that every reason of
§13.9 appears, that §13.9 lists exactly the reference's codes, and that §13.8's floors are the
lengths of the reports in telemetry-aggregation's corpus, read with the reference's DAP reader.
`live-conformance/compact_return_verify.py` is the second implementation, written from this text:
it shares no code with `wai-rs`; it reads groups, objects and leaves with the JWP corpus's second
implementation (`jwp-conformance/jwp_verify.py`), claims with the claim corpus's
(`claim-conformance/claim_verify.py`), §5.3 with `bearer_verify.py`, holdings records with
`holdings_verify.py` and DAP reports with `telemetry_aggregate_verify.py`, and writes §13.2 to
§13.8 itself; it checks the floors against telemetry-aggregation's own table and corpus; and it
mutates copies of the corpus and requires every copy to fail, and drops each of its own checks in
turn and requires the corpus to catch the weakened verifier.
### 13.11 Security and privacy considerations
- **The compact form carries no trust of its own.** It carries its record's signature, and the
receiver verifies the expansion by the record's own text. A reference that resolved to another
value would expand to another record, which that signature does not verify.
- **The check is not a signature.** Its four bytes of SHA-256 find what a fragment's header does
not show — a fragment misplaced, or mixed in from another message under the same header, and a
byte changed on the path — and authenticate nothing. Anyone on a path can make a message whose
check holds: what it expands to is refused unless its signer signed it.
- **What a reference discloses.** A reference names what the receiver holds, and says no less
than the record it stands for: anyone who holds the group receipts resolves a group's. A signer
by reference links a sender's messages as its key does. A sink that signs each reconciliation
with a fresh `did:key` (§12) sends its key inline, in the signer form 0.
- **Bounded work.** A message is at most 65,535 bytes in at most 255 fragments, a compact record is
read once, in order, and resolving a reference is a lookup. A receiver **SHOULD** bound how long
it holds the fragments of a message that has not completed; this profile sets no bound. A writer
sizes nothing by a reconciliation's own `leaf_count` before it has found it to be the group's
(§13.5): a record a node carries for a neighbour (§13.8) can state a `leaf_count` of 2^53 − 1
beside a bitmap of one byte and still read under jwp-receipts §3.1 steps 1 and 2.