open-standards/wai/extensions/jwp-receipts.md
Transaction Science 0077d74c6c
Some checks failed
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 1m54s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 3s
no-vanity-metrics / WAI public text names no comparison (pull_request) Successful in 3s
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 2m7s
no-vanity-metrics / WAI public text names no company or product (pull_request) Successful in 3s
standards workspaces build + test / arl-rs (pull_request) Successful in 1m9s
standards workspaces build + test / aac-rs (pull_request) Successful in 1m18s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 3m36s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 48s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 32s
standards workspaces build + test / eoc-rs (pull_request) Successful in 1m55s
standards workspaces build + test / map-rs (pull_request) Successful in 28s
standards workspaces build + test / mesh-rs (pull_request) Successful in 23s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m33s
standards workspaces build + test / openpay (pull_request) Successful in 1m29s
wai cross-architecture byte-equality / linux-aarch64 (pull_request) Successful in 22s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 1m8s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 1m10s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 5m38s
wai cross-architecture byte-equality / macos-arm64 (reference) (pull_request) Successful in 29s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m16s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m17s
standards workspaces build + test / sense-rs (pull_request) Successful in 1m6s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 8m7s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 35s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 3m39s
wai cross-architecture byte-equality / linux-x86_64 (pull_request) Successful in 27s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 2m41s
wai-pack / wai-pack · linux-arm64 (pull_request) Failing after 59s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 6m36s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 13s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 12m59s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 8s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 24s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 45s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 1m40s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 31m50s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 2m3s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 10m46s
wai-pack / wai-pack · macos (pull_request) Failing after 16s
WAI HTTP delivery / HTTP corpus · falsification and differential (pull_request) Successful in 1h19m18s
WAI HTTP delivery / cache rules · controls and labelled caches (pull_request) Successful in 1m4s
WAI JPEG-AI whole decode (pinned inputs, required) / fetch pinned JPEG-AI inputs → whole decode + pixel hash, required (pull_request) Successful in 1m5s
WAI neural sink gates / model-free refusals of the neural sink (pull_request) Successful in 1m10s
wai-pack / wai-pack · linux (pull_request) Failing after 15s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Failing after 55s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 20s
wai-pack / wai-pack · dependency licences (pull_request) Failing after 1m8s
WAI section references / cited sections exist (pull_request) Successful in 10s
WAI mbt2018_mean integer decode (regenerated vectors) / regenerate mbt2018_mean integer vectors → Rust decode, required (pull_request) Successful in 4m22s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Failing after 2m24s
workflow hygiene / every workflow supersedes its own queue (pull_request) Successful in 3s
wai: state the ranged-WAI2 classical length as a limitation of this revision
jwp-receipts §2.4.12 (classical_declared and rule 9), the CHANGELOG and the
claim-conformance README (the envelopes context and the sweep table's SPEC §6.4 row,
the one that cites rule 9): this revision derives the classical length from the WAI2
envelopes held in full; a sink that received a WAI2 only as byte ranges holds its head,
not the envelope, and cannot yet state its classical rendition. Recording such a
session is HTTP session work still to come.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 05:23:56 -04:00

3563 lines
227 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: JWP Receipt Chain
> Status: Draft. Sub-spec of WAI v1.0. Canonical text: `wai/extensions/jwp-receipts.md`
> in the open-standards repository (https://git.transaction.science/Transaction-Science/open-standards).
> The verifier of the reference implementation is in
> https://git.transaction.science/Transaction-Science/open-standards/src/branch/main/wai/wai-rs:
> `claim_receipt`, `jwp_delivery`, and for §7 `signer_id` and `key_log` (feature
> `key_discovery`).
Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY**, **REQUIRED**,
**OPTIONAL** are RFC 2119 / RFC 8174.
---
## 1. Scope and model
This extension defines a JWP-shaped receipt chain that attests
**per-object delivery** of WAI envelope objects through a chain of
relays. A receipt names the bytes a sink received, the publisher that
produced them, the energy spent producing them, and the chain of
relays the bytes traversed — all under a single Ed25519 signature per
group.
In scope:
- A per-object content commitment (BLAKE3-256) over WAI envelope bytes.
- A per-group Merkle root binding every object in a group under one
signature.
- A per-group Ed25519 signature over canonical group metadata.
- A typed receipt envelope (JSON or equivalent CBOR) with explicit
parent-receipt linkage for cross-hop preservation: one chain for each
publisher key and track (§3 step 3, §6.1).
- Verifier behaviour, relay preservation requirements, and conformance
test vectors.
- **Typed receipt classes** (§2.4): every receipt belongs to exactly one
class. `group` and `object` receipts are the **delivery** class; a
`claim` receipt (§2.4.3) carries one of the others — `energy`,
`lawful-basis`, `log-inclusion`, `measurement`, `operating-point`,
`stage`, `delivery-gap`, `reconstruct-session` (§2.4.5–§2.4.12) — on one
envelope, one canonical form and one signature rule, so a statement other
than delivery does not need a field set of its own.
- **Objects not delivered** (§3, §4.4): an object a subscriber did not
receive is accounted for by its receipt, checked without the object's bytes
and reported as `inclusion-verified`, never as delivered; a relay forwards
the receipts of the objects it does not forward, and may say why in a
`delivery-gap` claim (§2.4.11).
- **What a sink reconstructed** (§2.4.12): a `reconstruct-session` claim
records one session of a sink — its decoders and host, its paths and
switches, its late presentations, a commitment to every output it
presented, the bytes it counted and received, the classical rendition it
did not need, the `stage` claims it signed about the stage sets it
observed, and its decode energy where metered — checkable against the
outputs, the objects, the envelopes and those `stage` claims.
- An optional acquisition-class label on the object receipt, bound with the
object's figure and origin into its Merkle leaf (§2.2, §2.4.2); an
optional class for the group total, signed under a labelled group payload
with its coverage and the parent link (§2.3, §2.4.1); and the group total
read from its objects (§3).
- **A second binding class** (§2.6): a soft binding, declared by the
publisher with a registry-named algorithm and a resolution endpoint,
for objects whose bytes changed in normal distribution — and the
**ancestor rule** (§3.2): a receipt reached through it attests the
object the one in hand was derived from, never the object in hand.
- **Key discovery and signing authority** (§7): a form of `signer_id`
that names the web origin a signer publishes its keys under; a signed,
chained **key log** the origin publishes at a well-known location,
listing each key with what it may sign and whether it is active,
retired or revoked; and a confirmation a verifier runs after the
signature, against an origin it has anchored, reported beside the
receipt's own verdict. It adds no member and changes no signed byte.
- **Module authorisation** (§7.9): who may authorise the decoder and
reconstruct modules a sink runs, and the parameter sets an envelope
pins; their revocation by digest; the rule that a sink which cannot
establish a module's authorisation falls back rather than run it; and
the longest a revoked module keeps running.
Out of scope:
- A transport. This extension does **not** define how receipts move on
the wire; receipts ride alongside WAI envelopes in the carrier
protocol (e.g. a MoQT object header field, an HTTP trailer, a
sidecar file).
[http-delivery](http-delivery.md) §8 defines the HTTP carriage:
receipt files named by link hash, a per-object index of `WAI_RECEIPT`
properties, playlist receipts, and the locations of key logs,
statuses and grants.
- A codec. The receipt chain is opaque to the WAI capability dispatch
in WAI SPEC.md §4. A receipt attests bytes; it does not interpret
them.
- An envelope. WAI SPEC.md §2 defines the envelope; this extension
sits **above** it. A receipt's content hash is computed over the
full WAI envelope (`WAI1` magic + manifest + payload), not over the
payload alone.
- A trust store. §7 says what a verifier checks against an origin it
has anchored and how it records the anchor; which origins a relying
party anchors, and how it provisions a pinned anchor, is its own.
- A soft-binding algorithm, a resolution service, or a transparency
log. This extension names them by identifier and locator (§2.4.7,
§2.6) and defines none of them.
Relationship to WAI SPEC.md:
- WAI SPEC.md §2 (container) — receipts hash the container bytes
end-to-end; a sink that mutates the envelope (re-orders manifest
keys, re-frames the payload) breaks the content hash and MUST be
rejected by §3 step 4 below.
- WAI SPEC.md §4 (capability dispatch) — receipts attest **what was
delivered to the dispatcher**, not what the dispatcher produced. A
sink that successfully verifies a receipt then dispatches the
envelope per §4 unchanged.
---
## 2. Wire format
### 2.1. Per-object content hash
For each WAI envelope object the publisher emits, the publisher MUST
compute a 32-byte BLAKE3-256 content hash:
```
content_hash = BLAKE3(domain || envelope_bytes)
domain = "moq-jwp:object\x01"
```
`envelope_bytes` is the full WAI envelope per WAI SPEC.md §2 (the
`WAI1` magic, the manifest-length prefix, the manifest, the payload-
length prefix, the payload — concatenated, with no insertion or
reframing). The domain string is appended as a domain separator to
prevent cross-protocol hash collisions.
### 2.2. Per-group Merkle root
A **group** is an ordered list of one or more objects under a single
publisher key. The publisher MUST build a BLAKE3 binary Merkle tree
over the object content hashes in their group order. Leaves and
internal nodes use distinct domain separators:
```
leaf(content_hash) = BLAKE3("moq-jwp:merkle-leaf\x01" || content_hash)
leaf(content_hash, joules_micro, class, origin)
= BLAKE3("moq-jwp:merkle-leaf\x02" || content_hash
|| u64_be(joules_micro) || class
|| u32_be(len(origin)) || origin_utf8)
node(left, right) = BLAKE3("moq-jwp:merkle-node\x01" || left || right)
```
An object's leaf takes the first form when its receipt carries no class
label (§2.4.2) and the second when it does. `class` is the label's signed
encoding: the tag byte and the evidence that follows it
([energy-measurement](energy-measurement.md) §2.7), tag `0` marking an
object explicitly unmetered. The encoding is self-delimiting and `origin` is
length-prefixed, so no two inputs share a leaf. The version byte keeps the two
forms disjoint, so a label, or the origin, can be neither added, stripped nor
altered on a delivered object without changing the root the group signature
covers. A group may mix the forms. A verifier that predates the labelled form
computes the first form for every object: it refuses a labelled object at §3
step 5 and verifies an unlabelled one as before.
When the leaf count at any level is odd, the publisher MUST duplicate
the final leaf to obtain an even pair. The root is the single hash at
the top of the tree. The empty-group root is the 32-byte all-zero
value `0x00..00`.
### 2.3. Per-group Ed25519 signature
The publisher MUST sign the **canonical signing payload** with an
Ed25519 key per RFC 8032 (Pure Ed25519, no prehash). The signing
payload is the big-endian concatenation:
```
signing_payload =
"moq-jwp:group-sign\x01" // domain
|| u64_be(track_alias) // optional carrier-scoped id (0 if unused)
|| u64_be(group_id) // monotonic per (publisher_key, track_alias)
|| merkle_root[32] // §2.2
|| u64_be(leaf_count) // number of objects in the group
|| u64_be(total_joules_micro) // sum of microjoules across the group
|| u32_be(len(group_start)) || group_start_utf8 // RFC 3339 start timestamp
|| u32_be(len(group_end)) || group_end_utf8 // RFC 3339 end timestamp
|| publisher_key[32] // Ed25519 verifying key (RFC 8032)
|| u32_be(len(publisher_id)) || publisher_id_utf8 // bound identity string
```
The signature is exactly 64 bytes. Implementations MUST reject any
group receipt whose `signature` length is not 64.
A group whose total carries a class of its own (§2.4.1) is signed over the
**labelled** payload instead: the fields above, in that order, under the
domain `"moq-jwp:group-sign\x02"`, followed by
```
|| class // energy-measurement §2.7: tag, evidence
|| u8(coverage) // 0 Whole, 1 Partial
|| u8(has_parent) || parent[32] // 1 and parent_receipt_hash, or 0 and 32 zero bytes
```
`class` is tag `0` exactly when `total_joules_micro` is zero, and then
`coverage` is `0`. The labelled payload signs `parent_receipt_hash`, which the
legacy payload does not (§6.1). The two domains keep the payloads disjoint. A
verifier or relay that predates the labelled payload computes the legacy one
and refuses a labelled group at §3 step 2; every group signed over the legacy
payload verifies as before.
### 2.4. Receipt envelope (typed)
Three receipt kinds are defined: `group` (one per group), `object`
(one per object) and `claim` (§2.4.3). Every receipt belongs to
exactly one **class** (§2.4.4). `group` and `object` are the
`delivery` class; for them the class is implied by the kind and is
not carried, so their wire form is exactly as below. A `claim` names
its class in a `class` member. All are typed JSON; equivalent CBOR is
permitted when the carrier mandates it, provided the field set and
semantics are identical (for a `claim`, see §2.4.3 on which bytes are
signed).
#### 2.4.1. `kind: "group"`
Required fields:
| field | type | meaning |
|---|---|---|
| `kind` | string | MUST equal `"group"`. |
| `track_alias` | u64 | Carrier-scoped track identifier; `0` if unused. |
| `group_id` | u64 | Monotonic group counter under (`publisher_key`, `track_alias`). |
| `root_hash` | 32 B hex | §2.2 Merkle root over object content hashes. |
| `leaf_count` | u64 | Number of objects covered by `root_hash`. |
| `joules_total` | u64 | Sum of microjoules across the group's objects; `0` is the unmetered marker ([energy-measurement](energy-measurement.md) §2). Its class, where it carries one, is in the labelled members below; a verifier holding every object checks it, or, for a group without one, reads the total from the objects' labels (§3, *The group total*). |
| `group_start` | string | RFC 3339 timestamp of the first object. |
| `group_end` | string | RFC 3339 timestamp of the last object. |
| `signer_pubkey` | 32 B hex | Ed25519 verifying key per RFC 8032. |
| `signer_id` | string | Bound identity string (DID, URN, or display). In §7.1's origin-bound form it names the web origin whose key log lists `signer_pubkey`; an Ed25519 `did:key` names one key; any other form is a label, and a verifier reads nothing from it. |
| `sig` | 64 B hex | Ed25519 signature over §2.3 payload. |
| `parent_receipt_hash` | 32 B hex or `null` | BLAKE3 of the previous group receipt's canonical bytes under this publisher key and `track_alias` (its link hash, §3 step 3); `null` for the first group of that track. |
**The total's class (labelled form).** A group whose total carries a class of
its own adds these members and is signed over the labelled payload (§2.3):
| field | type | meaning |
|---|---|---|
| `sign_version` | integer | `2`. Absent (or `null`), the group is in the legacy form; any other value is refused (`group_label_invalid`). |
| `energy_provenance`, `energy_uncertainty`, `energy_counter_filtering`, `energy_calibration_ref` | as energy-measurement §2.7 | The total's class and its evidence; all absent for the unmetered marker beside a zero total. |
| `energy_coverage` | string | `Whole` or `Partial`; absent with the unmetered marker. |
A legacy group carrying any of these members, a `sign_version` other than `2`,
or a labelled group whose class cannot be read, is refused
(`group_label_invalid`), never read as the legacy form. The class **MUST** be the one the group's objects compose to, by this
rule over the objects whose figure `F_i` is not zero, in integers:
- if any such object is unlabelled, the group **MUST NOT** carry a class (it is
signed in the legacy form);
- if there is none, the class is the unmetered marker and the coverage `Whole`;
- otherwise the class is the weakest of the objects' classes, in
energy-measurement §2's order, and the coverage is `Partial` exactly when some
object's figure is zero;
- for a hardware class, `relative_ppm` is at least `⌈Σ F_i·p_i / Σ F_i⌉` and
`window_us` is the longest `window_us_i`. The bound is linear in the parts, so
it assumes their errors fully correlated, and it is conservative;
- an `OnChipCounter` total's filtering state is the objects' common one, or
`Unknown`; a `CalibratedInstrument` total needs one calibration reference
shared by every object, or the group carries no class.
The resolution floor then holds for the total. With the group alone a verifier
checks that the class reads, is not `HwShunt`, is the unmetered marker exactly
beside a zero total, and reaches the resolution floor (§3 step 2); with every
object it checks the composition (§3, *The group total*).
#### 2.4.2. `kind: "object"`
Required fields:
| field | type | meaning |
|---|---|---|
| `kind` | string | MUST equal `"object"`. |
| `track_alias` | u64 | Mirrors the parent group receipt. |
| `group_id` | u64 | Mirrors the parent group receipt. |
| `object_id` | u64 | Monotonic from 0 within the group. |
| `content_hash` | 32 B hex | §2.1 BLAKE3 over the WAI envelope bytes. |
| `joules_micro` | u64 | Microjoules consumed producing this object; `0` is the unmetered marker. This field fixes the unit and the scope of attribution, not how the number is obtained — see [energy-measurement](energy-measurement.md), which requires an acquisition class beside it and forbids publishing a figure from a measurement that failed. Its class rides as an optional label, below, bound into the object's leaf with the figure and the origin (§2.2). |
| `origin` | string | Datacenter / origin label. |
| `merkle_proof` | object | Inclusion proof: `{ "siblings": [32 B hex, …], "direction_bits": u64, "leaf_index": u64 }`. `direction_bits` bit `i` is `1` iff the current node at level `i` is the right child. |
**Optional class label.** An object receipt **MAY** carry its figure's
acquisition class as an optional label
([energy-measurement](energy-measurement.md) §2.8): `"leaf_version": 2`, and
the class in energy-measurement §2.7's JSON fields — `energy_provenance`, and
for a refined hardware class `energy_uncertainty` with
`energy_counter_filtering` or `energy_calibration_ref`. With
`"leaf_version": 2` and no class the object is explicitly unmetered (tag `0`),
which is valid only beside a zero figure. Absent, the receipt is exactly as
above and its leaf takes the legacy form of §2.2. Present, its leaf takes the
labelled form, and the group signature covers the figure, the class, the
class's evidence and the origin through the root. A label **MUST NOT** be the
legacy `HwShunt` and **MUST NOT** sit beside a zero figure; a refined class
**MUST** carry its evidence and reach the resolution floor (energy-measurement
§2.3); class members without `"leaf_version": 2`, evidence without a class, or
a class that cannot be read, are refused rather than dropped
(energy-measurement §2.7). A verifier refuses an object that breaks any of
these at §3 step 5 (`object_label_invalid`). An `energy` claim (§2.4.5) remains
the way for a party other than the publisher to sign a class for the figure.
#### 2.4.3. `kind: "claim"`
`group` and `object` answer one question — *what was delivered, by
whom, at what energy cost*. Other statements are made about the same
objects: how an energy figure was acquired and how much weight it
bears, on what basis what a sink observes may be processed, that a
receipt was entered in a log, that an object carries a binding that
survives re-encoding. None is a delivery statement, and several are
signed by someone other than the publisher. A `claim` receipt carries
one such statement — a class-scoped `body` under a `class`
discriminator — on one envelope.
Required members. A claim has no optional members: a nullable member
is present and `null`, so one claim has one canonical form.
| field | type | meaning |
|---|---|---|
| `kind` | string | MUST equal `"claim"`. |
| `class` | string | A class of §2.4.4 other than `delivery`. |
| `subject_type` | string | `"object"` or `"receipt"`; which a class admits is fixed by §2.4.4. |
| `subject` | 32 B hex | For `object`: the §2.1 content hash of the object the claim is about. For `receipt`: the link hash of a receipt — §3 step 3's for a group receipt, `link_hash` below for a claim — or, for a class that names a figure profile (§2.4.8–§2.4.9), the identity that profile defines ([energy-measurement](energy-measurement.md) §6.1). |
| `seq` | integer | Strictly increasing across the claims of one `signer_pubkey`. |
| `issued_at` | string | A date-time (below) at which the signer issued the claim. Self-asserted, like `group_end`. |
| `body` | object | Class-scoped; its members are fixed by `class` and by nothing else (§2.4.5–§2.4.12, §2.6). |
| `signer_pubkey` | 32 B hex | Ed25519 verifying key per RFC 8032. |
| `signer_id` | string | Bound identity string, as for a group receipt (§2.4.1, §7.1). |
| `parent_receipt_hash` | 32 B hex or `null` | `link_hash` of this signer's previous claim; `null` for its first. |
| `sig` | 64 B hex | Ed25519 signature, below. |
**Encodings.** Byte strings are lowercase hex — one byte string, one
signed encoding. Integers are unsigned and at most 2^53 − 1, written in
plain decimal: a number written with a sign, a fraction or an exponent
— `-0`, `1.0`, `1e0` — is not an integer here, whatever its value. A
claim carries no other number, and no boolean. A **term** is a compact
IRI (`dpv:Consent`) or an IRI: a non-empty string of at most 2048 bytes
in UTF-8 holding no character with the Unicode White_Space property and
no control character (general category Cc). An **absolute URI** is a
term with a scheme (RFC 3986 §3.1), a colon, and a non-empty
remainder. A **date-time** is RFC 3339 §5.6's `date-time` with ASCII
digits, `T` and `Z` in either case: a month of 1–12 and a day its month
has in its year (§5.7, in the proleptic Gregorian calendar of RFC 3339
Appendix C — `2026-02-29` is not a date, `2028-02-29` is); an hour of at
most 23, a minute of at most 59 and a second of at most 60; any number
of fractional-second digits, at least one if the point is written; and
an offset of `Z` or `±hh:mm` with `hh` at most 23 and `mm` at most 59.
A second of 60 is accepted at any time of day: whether a leap second
was inserted then is a fact about the calendar, not the syntax, and
`issued_at` is self-asserted.
**Canonical form, signature, identity.** A claim's canonical form is
its JSON Canonicalization Scheme serialisation (JCS, RFC 8785). On the
values a claim may carry — strings, unsigned integers no larger than
2^53 − 1, `null`, arrays and objects — JCS reduces to: member names
sorted by their UTF-16 code units; no insignificant whitespace;
strings escaped with `\"` `\\` `\b` `\f` `\n` `\r` `\t` and `\u00xx`
(lowercase hex) for the remaining characters below U+0020, every other
character written as itself in UTF-8; integers in plain decimal. No
number formatting is involved.
```
sig = Ed25519(signer_key, "moq-jwp:claim\x01" || JCS(claim without "sig"))
link_hash = BLAKE3("moq-jwp:claim-link\x01" || JCS(claim))
```
A verifier canonicalises before it verifies: whitespace and member
order on the wire are not signed. Duplicate member names are rejected,
since RFC 8785 is defined over I-JSON (RFC 7493), which forbids them,
and two parsers resolving a duplicate differently would canonicalise
one claim two ways. The signed bytes are this JSON form whatever the
carriage: a carrier that moves claims as CBOR MUST reproduce it to
verify. A claim SHOULD be sent in canonical form, and relays forward
it byte-for-byte (§4.1).
**Chain.** A signer's claims form one chain through
`parent_receipt_hash` and `seq`, **separate from the group chain**: a
claim's parent is never a group receipt and a group receipt's parent
is never a claim, so every existing group-chain verifier is unaffected
by claims. A publisher that issues both keeps two chains under one
key.
#### 2.4.4. Receipt classes
| `class` | carried by | `subject_type` | body | signed by |
|---|---|---|---|---|
| `delivery` | `group`, `object` (implied, not carried) | — | §2.4.1, §2.4.2 | the publisher |
| `energy` | `claim` | `object` | §2.4.5 | whoever acquired the figure |
| `lawful-basis` | `claim` | `object` or `receipt` | §2.4.6 | the service operator (a declaration); the party recording it (a decision) |
| `log-inclusion` | `claim` | `receipt` | §2.4.7 | whoever attaches the log's evidence |
| `soft-binding` | `claim` | `object` | §2.6.2 | the object's publisher |
| `binding-recovery` | `claim` | `object` (the ancestor) | §2.6.3 | the party that performed the recovery |
| `measurement` | `claim` | `object` or `receipt`, as its figure profile fixes | §2.4.8 | the key that signed the figure: the group's publisher for `moq-jwp:object`, the receipt's signer for every other profile |
| `operating-point` | `claim` | `object` or `receipt`, as its figure profile fixes | §2.4.9 | the party that records the request, the commitment and the delivery |
| `stage` | `claim` | `object` (the stage set's base) | §2.4.10 | the party that observed the stage set |
| `delivery-gap` | `claim` | `object` (the object not forwarded) | §2.4.11 | the relay that did not forward it |
| `reconstruct-session` | `claim` | `object` (the session's entry object) | §2.4.12 | the sink that reconstructed: the key its decoder host holds |
Rules:
- A verifier **MUST** reject a `claim` whose `class` it does not know,
and **MUST** report the rejection rather than skip the claim.
Silently discarding a class a signer thought load-bearing is the
failure this rule exists to prevent. The rejection is of that claim
only: a delivery receipt for the same subject verifies or fails on
its own.
- `delivery` is reserved to `group` and `object`. A `claim` naming it
**MUST** be rejected; delivery is stated only in the §2.3 signed
payload.
- The class is read from `class` and **MUST NOT** be inferred from the
body's shape. A body **MUST** carry exactly its class's members; a
missing or additional member is a rejection, not an extension point.
A new class, or a new member, is a new registration here.
- A `claim` carries the authority of its signer and no more. `class`
says what is asserted, never that it is true. A claim signature is
not evidence about delivery, and a delivery signature is not
evidence about a claim. What that authority is — which web origin
stands behind the key, and whether it gave the key this class — is
§7's to establish. The *signed by* column above names the party each
class expects; §7.5 is how a verifier checks it.
- A `claim` that repeats a figure a delivery receipt carries for the
same subject **MUST** agree with it; the `energy` class does this
deliberately, to bind its labels to the figure. Where the delivery
receipt labels its figure (§2.4.2), the claim **MUST** also agree with
that label's class and evidence. Where they disagree a verifier **MUST**
reject rather than choose.
- A class that is about one figure — `measurement` or `operating-point` —
names it by a figure profile ([energy-measurement](energy-measurement.md)
§6.1) and the identity that profile defines, restates it with its class,
and is checked against the receipt that carries it at §3.1 step 6. It never
changes that receipt, and never supplies a class that receipt does not
carry.
#### 2.4.5. `class: "energy"`
§2.4.2's `joules_micro` fixes the unit and the scope of attribution;
[energy-measurement](energy-measurement.md) §2 requires an acquisition
class beside any non-zero figure, with the class's evidence inside the
signature, and the `object` receipt has no member for either. An
`energy` claim supplies them without changing the signed `object`
format: it restates the figure and binds to it, under the claim's
signature, the labels other extensions define. It defines none of those
vocabularies. The class and its evidence are written as
energy-measurement §2.7 writes them in a JSON receipt, under the same
member names; a class that carries no evidence has those members
present and `null`.
| member | type | meaning |
|---|---|---|
| `joules_micro` | integer | The figure. Equal to the `object` receipt's `joules_micro` for the same subject. A zero is an integer here and is refused by rule 1, at step 4. |
| `energy_provenance` | string | An acquisition class of [energy-measurement](energy-measurement.md) §2, verbatim. |
| `energy_uncertainty` | object or `null` | The declared uncertainty (energy-measurement §2.3): `{ "relative_ppm": integer, "window_us": integer }`, exactly those two members. |
| `energy_counter_filtering` | string or `null` | An `OnChipCounter`'s filtering state (energy-measurement §2.4). |
| `energy_calibration_ref` | string or `null` | A `CalibratedInstrument`'s calibration reference (energy-measurement §2.5). |
| `coverage` | string | `Whole` or `Partial`, per [energy-binding](energy-binding.md) §4. |
| `grade` | string | An evidential grade of [oer-receipts](oer-receipts.md) §2, verbatim. |
| `method` | absolute URI | The published protocol the figure was obtained under. |
| `work_units` | integer | The recomputable work the figure is expressed against ([energy-measurement](energy-measurement.md) §5). A zero is refused by rule 6, at step 4. |
| `work_unit` | term | What one work unit is (e.g. `subpixel`). |
| `oer_receipt` | string or `null` | The `receipt_id` (`sha256:<hex>` or `blake3:<hex>`, lowercase) of an [OER receipt](oer-receipts.md) recording the same figure, or `null`. |
§3.1 step 2 reads these members in the table's order, and
`energy_uncertainty`'s two members, where it is an object, as it is
reached; it checks their types and encodings, and nothing more. Which
class, evidence and figure they may hold together, and whether the
figure or the work count is zero, is step 4's.
Rules, checked at §3.1 step 4 in this order:
1. `joules_micro` **MUST NOT** be zero. A zero is a failed measurement,
and an unmetered object gets no `energy` claim at all
([energy-measurement](energy-measurement.md) §2).
2. The acquisition class, in this order:
- `energy_provenance` **MUST** name a class of energy-measurement §2
(`energy_unknown_acquisition`);
- it **MUST NOT** be `HwShunt` (`energy_legacy_acquisition`).
energy-measurement §2.6 forbids the legacy label in a new receipt,
and no claim predates the refinement, so a verifier can hold every
claim to it — as energy-measurement §2.8 holds the optional labels;
- the evidence **MUST** be the class's, whole
(`energy_acquisition_evidence`). `CalibratedInstrument` and
`OnChipCounter` carry a declared uncertainty whose two parts are
non-zero and whose `relative_ppm` is at most 2³² − 1, the width of
its signed encoding (energy-measurement §2.7), and which reaches
the resolution floor, `joules_micro × relative_ppm ≥ 288 676`
(energy-measurement §2.3). `OnChipCounter` carries a filtering state
of `On`, `Off` or `Unknown` and no calibration reference; a filtering
state of `null` is not `Unknown` here, so that one class has one
spelling. `CalibratedInstrument` carries a calibration reference
that is not empty once Unicode White_Space is trimmed from both
ends, and no filtering state. `ModelBased` and `Estimator` carry
none of the three.
3. `coverage` **MUST** be `Whole` or `Partial`.
4. `grade` **MUST** name a grade of oer-receipts §2.
5. `grade` **MUST NOT** rank above the grade the class supports. The
correspondence is oer-receipts §2's — `CalibratedInstrument` and
`OnChipCounter` support up to `measured`, `ModelBased` up to
`derived`, `Estimator` up to `projected` — with grades ranked as
oer-receipts lists them (`projected` < `simulated` < `derived` <
`measured`). Grading a figure *below* what its class supports is
permitted. The grade does not say which hardware class is behind a
`measured` figure; the class and its declared uncertainty do.
6. `work_units` **MUST NOT** be zero.
A relying party holding an `energy` claim with `coverage: "Partial"`
for a subject **MUST NOT** report that subject's `joules_micro` as the
whole cost of producing it.
**Against the delivery receipt** (§3.1 step 6): the claim's
`joules_micro` **MUST** equal the `object` receipt's for the same subject
(`energy_disagrees_with_delivery`). Where that receipt carries a class label
(§2.4.2), the claim's class and its evidence **MUST** also equal the label's
(`energy_class_disagrees_with_delivery`). Each is read as energy-measurement
§2.7 reads it, so an `OnChipCounter` label with no filtering state reads as
`Unknown`.
**Against the OER receipt it names** (§3.1 step 6): a verifier holding
that receipt checks, in this order. The receipt is read as oer-receipts
§4 reads a document: a text it refuses whole is no receipt, so a
verifier holding only such a text holds none.
1. that it is the one named — its `receipt_id` equals `oer_receipt`
(`context_mismatch`);
2. that it verifies under [oer-receipts](oer-receipts.md) with no
reason (`energy_oer_invalid`);
3. that it is about the claim's subject — its `subject.output` equals
the claim's `subject` exactly as the claim writes it, 64 lowercase
hex digits (`context_mismatch`). oer-receipts does not fix what
`subject.output` holds; an OER receipt an `energy` claim can name
holds there the object's §2.1 content hash in this form. Without
this check a claim about one object could name a valid OER receipt
about another that happens to carry the same figure and grade;
4. that its `provenance.how` equals `grade`, and that its
`energy.joules_total`, rounded to the nearest microjoule, equals
`joules_micro` (a figure exactly half-way between two microjoules
admits either) (`energy_disagrees_with_oer`). Check 2 has
established that the figure is a quantity oer-receipts §4 reads — a
decimal string of at most 34 significand digits, or an integer
within I-JSON's range — since a receipt whose figure is anything
else, or whose body holds a fraction, `-0` or an integer beyond that
range anywhere, does not verify. The figure is compared exactly, as
oer-receipts §6 compares figures: `0.0120005` is half-way and admits
12,000 and 12,001 µJ, and `0.0120004` followed by twenty-six nines is
below half-way, although rounding it to 28 significant digits would
make it a tie.
This checks that two signed statements of one figure about one object
agree. It does not check that the OER receipt's `energy.boundary` is
the scope §2.4.2 attributes to `joules_micro`; that remains the claim
signer's assertion.
The claim does not make the figure verifiable — re-execution
reproduces the work, never the joules
([energy-measurement](energy-measurement.md) §5). It makes the
figure's labels signed, and a contradiction between them detectable.
#### 2.4.6. `class: "lawful-basis"`
The afferent dual of a delivery statement: on what basis what a sink
observes may be processed, and the record that a decision was taken
against that basis. A body is one of two records, discriminated by
`record`, which §3.1 step 2 reads before anything else in the body:
`record` absent is `missing_required_field`, and a `record` other than
`"declaration"` or `"decision"` is `field_type`. The body is then
checked against the table of the record it names.
A **declaration** (`record: "declaration"`), signed by the service
operator:
| member | type | meaning |
|---|---|---|
| `record` | string | `"declaration"`. |
| `vocabulary` | absolute URI | The vocabulary the terms are drawn from (for example the W3C Data Privacy Vocabulary). |
| `fragment` | 32 B hex or `null` | §2.1 content hash of the envelope carrying the declaration fragment beside the manifest, when one is carried. |
| `purposes` | array | One entry per purpose: `{ "purpose": term, "basis": term or "", "data": [term, …] }`, `data` possibly empty. The array may be empty and a `basis` may be `""` as far as step 2 is concerned: both are refused by the rules below, at step 4, each with its own reason. |
A **decision** (`record: "decision"`), signed by the party recording
it:
| member | type | meaning |
|---|---|---|
| `record` | string | `"decision"`. |
| `vocabulary` | absolute URI | As the declaration's. |
| `declaration` | 32 B hex | `link_hash` of the declaration claim the decision is expressed against. |
| `purpose` | term | The purpose decided on. |
| `outcome` | term | The decision, as a vocabulary term. |
Rules (§3.1 step 4): a declaration **MUST** list at least one purpose;
purposes are then checked in order, and each **MUST** carry its own
non-empty basis — a basis is per purpose, never per service — and
**MUST NOT** repeat an earlier purpose. Against the declaration it
names (§3.1 step 6): the declaration **MUST** itself pass §3.1 steps
1–4 (`context_invalid`) and be a lawful-basis declaration
(`context_mismatch`); a decision **MUST** then name it by `link_hash`,
share its `subject`, `subject_type` and `vocabulary`, and decide on a
purpose the declaration lists. That check does not establish that the
declaration was the service operator's. A verified delivery receipt
supplies a publisher's key (§3.2); an operator's is confirmed under the
operator's web origin by §7, with the authority
`lawful-basis/declaration`, and only under a pinned anchor (§7.5).
Without that confirmation the check establishes only that the decision
matches a declaration, signed by whoever signed it; with it, that the
declaration was made under that origin, and, where the verifier expects
the service's origin (§7.5), under the service's.
What this class does not do. It records what was declared and what
was decided. It is not a consent mechanism: a declaration is agnostic
across bases, and each purpose carries its own. It makes no statement
that a basis is sufficient for any purpose under any regime, and a
verifier here does not check terms against the vocabulary. The shape
— purposes, each with its own basis and data categories, from a
declared vocabulary — is the one ABNT NBR 25608:2025 uses for its
broadcast privacy declaration, as described in that standard's public
operational guidelines (see the [2030 landscape survey](../research/2030-transport-landscape.md)
§7.2), whose records follow ISO/IEC TS 27560. A `lawful-basis` claim
is signed, like every claim.
#### 2.4.7. `class: "log-inclusion"`
A statement that a receipt was entered in an append-only log,
carrying the log's own evidence. Its subject is always a receipt: a
log registers receipts, never objects, and per
[transparency](transparency.md) §3 per-object receipts are not
registered individually — the subject is a group receipt's link hash,
or a claim's.
| member | type | meaning |
|---|---|---|
| `log` | absolute URI | The log. |
| `profile` | absolute URI | The registration profile the log's evidence is checked under (for example `urn:ietf:rfc:9942`, COSE Receipts). |
| `entry` | term | The log's identifier for the entry. |
| `log_receipt` | hex | The log's own receipt for the entry, verbatim. |
Rule (§3.1 step 4): `log_receipt` **MUST NOT** be empty; an inclusion
claim without the log's evidence is only its signer's say-so.
**Accepting a `log-inclusion` claim establishes that its signer
asserted inclusion and carried the log's evidence. It does not
establish inclusion.** Inclusion is established only by checking
`log_receipt` under `profile` against the log, which this extension
does not specify; a verifier that has not done so **MUST NOT** report
the subject as logged. This claim's signature never stands in for the
log's, and [transparency](transparency.md) §6 applies: registration is
a witness, not a verification.
Whose key attached the log's evidence can be confirmed under §7, with
the authority `log-inclusion`. That confirmation is not a check of
`log_receipt`.
#### 2.4.8. `class: "measurement"`
[energy-measurement](energy-measurement.md) §6 requires a published figure
to be accompanied by how it was measured: for a figure obtained under that
extension's §3, the values its bracket gave, and for any figure, the build
profile. A `measurement` claim carries them for one figure. It restates the
figure and its acquisition class, so that it can be checked on its own, and
leaves the receipt that seals the figure unchanged. It names that receipt by a
figure profile (energy-measurement §6.1) and the receipt's identity under it.
Its `subject_type` and `subject` are the ones the profile gives: `object` and
the object's §2.1 content hash for `moq-jwp:object`, and `receipt` and the
receipt's identity for every other profile.
| member | type | meaning |
|---|---|---|
| `figure_profile` | term | A figure profile of energy-measurement §6.1. |
| `figure_index` | integer or `null` | Which figure, from 0, in a profile that carries several — for `moq-jwp:object`, the object's leaf index; `null` in one that carries one. |
| `group` | 32 B hex or `null` | For `moq-jwp:object`, the link hash of the group receipt the object was delivered under (§3 step 3); `null` for every other profile. |
| `joules_micro` | integer | The figure. |
| `energy_provenance` | string | Its acquisition class, as §2.4.5 writes it. |
| `energy_uncertainty` | object or `null` | As §2.4.5. |
| `energy_counter_filtering` | string or `null` | As §2.4.5. |
| `energy_calibration_ref` | string or `null` | As §2.4.5. |
| `bracket` | object or `null` | For a figure obtained under energy-measurement §3: `{ "idle": summary, "busy": summary, "window_us": integer, "completions": integer, "in_flight": integer, "opening": "from-rest" or "running" }`, a summary being `{ "n": integer, "mean_uw": integer, "sd_uw": integer, "live_intervals": integer, "longest_live_run": integer }` as energy-measurement §6.2 defines them. `null` otherwise. |
| `build` | object | `{ "profile": "release" or "debug", "toolchain": string or null, "target": term or null }`: the build of the code measured (energy-measurement §5, §8). `release` is an optimised build (for a Rust build, `opt-level` 2, 3, `s` or `z`, whatever its profile is named); any other build is `debug`. A finer vocabulary is a new registration. |
| `power_reader` | string or `null` | The reader of the power interface (energy-measurement §8). Required under a hardware class (rule 4). |
A report carries none of what follows from its values: neither the §4 ratio nor
its verdict, which every reader derives (energy-measurement §6.2). A bracket
carrying either has a member its table does not define (`unknown_member`).
A `null` bracket says the figure was not obtained under energy-measurement §3 —
under a hardware class, for example, a counter read through the span of the
work. Such a report carries no values to check, so the figure's liveness
(energy-measurement §3.1) rests on its producer's statement alone, and no reader
derives a §4 verdict for it.
Rules, checked at §3.1 step 4 in this order:
1. `joules_micro` **MUST NOT** be zero (`energy_zero_figure`): a zero is the
unmetered marker, or a failed measurement, and gets no report.
2. `figure_profile` **MUST** name a profile of energy-measurement §6.1 whose
subject type is the claim's `subject_type` (`figure_unknown_profile`);
`figure_index` **MUST** be an integer exactly when that profile carries
several figures (`figure_index_mismatch`); and `group` **MUST** be a hash
exactly when the profile is `moq-jwp:object` (`figure_group_mismatch`).
3. The acquisition class, as §2.4.5's rule 2 (`energy_unknown_acquisition`,
`energy_legacy_acquisition`, `energy_acquisition_evidence`).
4. Under `OnChipCounter` or `CalibratedInstrument`, `power_reader` **MUST NOT**
be `null` (`measurement_no_power_reader`): a hardware figure is a reading, and
its report names what read it, whether or not it carries a bracket.
5. Where `bracket` is not `null`, in this order, with `M` its busy mean less its
idle mean:
- each `n` **MUST** be at least 2 (`measurement_too_few_samples`);
- under `OnChipCounter` or `CalibratedInstrument`, each bracket's rail
**MUST** have been live (energy-measurement §3.1): `sd_uw` not zero (a
bracket whose samples are all equal advanced once at most),
`longest_live_run` at least 2, `live_intervals` at least 5 and at least
four fifths of `n` (`live_intervals · 5 ≥ n · 4`), and
`longest_live_run ≤ live_intervals ≤ n` (`measurement_rail_not_live`);
- `window_us`, `completions` and `in_flight` **MUST NOT** be zero
(`measurement_empty_burst`);
- `M` **MUST** be positive (`measurement_no_marginal`);
- under `OnChipCounter` or `CalibratedInstrument`, `M` **MUST** be at least
the idle `sd_uw`. A ratio below 1 is a failure to measure
(energy-measurement §4) (`measurement_below_attribution_floor`);
- `joules_micro` **MUST** be the figure `F` the bracket gives
(energy-measurement §6.2) (`measurement_figure_mismatch`);
- under `OnChipCounter` or `CalibratedInstrument`, the declared `window_us`
**MUST** be the bracket's (`measurement_window_mismatch`), and the declared
`relative_ppm` **MUST** be at least the least `p` the bracket supports
(energy-measurement §6.2) (`measurement_declaration_below_bracket`).
**Against the figure** (§3.1 step 6). The context is the receipt the claim
names, verified under its own profile's rules; for `moq-jwp:object`, that is
the object receipt accepted under §3 against the group receipt `group` names.
In this order:
1. the receipt verifies (`context_invalid`); it is of `figure_profile`, its
identity under that profile is `subject`, it is within the group `group`
names, and it carries a figure at `figure_index` (`context_mismatch`);
2. that figure is `joules_micro` (`figure_disagrees`);
3. that figure carries an acquisition class (`figure_unclassified`). An
unlabelled figure has none, and neither has the unmetered marker; a claim does
not supply one;
4. that class and its evidence are the claim's, each read as energy-measurement
§2.7 reads it (`figure_class_disagrees`);
5. an Ed25519 key signs the figure (`figure_signer_unknown`) — every profile but
`wai:energy-binding-id` has one — and it is `signer_pubkey`
(`figure_signer_mismatch`). For `moq-jwp:object` it is the group receipt's
`signer_pubkey`. Only the key that signed a figure signs its report.
Only check 5 shows that a `measurement` claim's signer is the figure's, so a
`measurement` claim is accepted only once step 6 has run against the receipt
it names. A verifier that does not hold that receipt reports
`figure_not_held` at step 6 — recoverable by fetching the receipt — and
**MUST NOT** present the claim as the figure signer's report, nor derive a §4
verdict for the figure from its values: without the context, any key can sign
any values about any figure.
An accepted `measurement` claim establishes that the figure's signer reported
these values for this figure, and that the figure and its declared uncertainty
are what the values give. It does not establish that the values were measured:
they cannot be re-measured (energy-measurement §5). A reader derives the §4
verdict from an accepted claim's bracket (energy-measurement §6.2).
#### 2.4.9. `class: "operating-point"`
[energy-binding](energy-binding.md) §7 makes the operating point a figure was
spent at part of the claim about it: what was requested, what was committed,
what was delivered, and what a declared descriptor predicted. An
`operating-point` claim records these for one figure, named and restated as
§2.4.8 names and restates it, without changing the receipt that seals the
figure. It is signed by the party that records them.
| member | type | meaning |
|---|---|---|
| `figure_profile`, `figure_index`, `group`, `joules_micro`, `energy_provenance`, `energy_uncertainty`, `energy_counter_filtering`, `energy_calibration_ref` | | As §2.4.8. |
| `vocabulary` | absolute URI | The vocabulary the operating points' parameters, and the prediction's descriptor, are drawn from. WAI defines none except [staged-delivery](staged-delivery.md)'s `urn:wai:staged-delivery:1` ([energy-binding](energy-binding.md) §7.2), whose points are a `stage` claim's (§2.4.10). |
| `op_requested` | point or `null` | What the requesting party asked for; `null` when nothing was requested. |
| `op_committed` | point or `null` | What the acting party said it would use. |
| `op_delivered` | point or `null` | What was used. |
| `prediction` | object or `null` | `{ "predictor": absolute URI, "descriptor_hash": 32 B hex, "predicted_joules_micro": integer }`: the model that turned the declared descriptor into energy, `BLAKE3("wai:energy-descriptor\x01" ‖ descriptor)` over the descriptor as `vocabulary` serialises it, and the energy it implies for the work the figure covers. |
A **point** is an object whose member names are terms and whose values are
integers of §2.4.3. JCS orders its members and §3.1 step 1 refuses a repeated
one, so two points are equal exactly when their canonical forms are.
Rules, checked at §3.1 step 4 in this order:
1. §2.4.8's rules 1–3. A claim restating `HwShunt` is refused there, so no
operating point is recorded against a figure labelled with the legacy class.
2. A point that is present **MUST NOT** be empty (`operating_point_empty_point`).
3. `op_delivered` **MUST NOT** be present without `op_committed`
(`operating_point_delivered_alone`).
4. `op_requested` and `op_committed` **MUST NOT** be present without
`op_delivered` (`operating_point_incomplete`).
5. A claim **MUST** carry `op_delivered` or `prediction` (`operating_point_empty`).
**Against the figure** (§3.1 step 6): §2.4.8's checks 1–4. The claim may be
signed by a party other than the figure's signer, so check 5 does not apply,
and it may name a figure no Ed25519 key signs. An operating point recorded
against a figure that carries the unmetered marker or no class is refused here
(`figure_disagrees`, `figure_unclassified`).
From an accepted claim a reader derives, and no claim states:
- whether the commitment was met: it was when `op_delivered` equals
`op_committed`, and there was none when `op_committed` is `null`;
- the **residual**, `joules_micro − predicted_joules_micro`, a signed integer,
whenever `prediction` is present.
A reader **MUST NOT** report the commitment as met unless it derives that it
was. An accepted claim establishes what its signer recorded about the request,
the commitment, the delivery and the prediction. An `op_requested` in a claim
the acting party signed is that party's account of the request.
#### 2.4.10. `class: "stage"`
A stage set ([staged-delivery](staged-delivery.md)) is a base and the layers that
refine it, each its own object with its own delivery receipt. A `stage` claim
records what one party observed of a stage set: what it asked for and was promised,
which layer objects arrived or did not and what it verified of their receipts, what
it presented on time, and how the presented output is reproduced. Its subject is the
base's §2.1 content hash. It is signed by the party that observed the set: a sink, or
another party that applies the set as a sink does (staged-delivery §10.2).
| member | type | meaning |
|---|---|---|
| `vocabulary` | absolute URI | `urn:wai:staged-delivery:1` ([energy-binding](energy-binding.md) §7.2), whose points have one parameter, `wai-stage:layers`: the number of layers a prefix of the set holds. |
| `op_requested` | point or `null` | The stage asked for. |
| `op_committed` | point or `null` | The stage the serving party said it would deliver. |
| `op_delivered` | point or `null` | The stage the delivered objects reach: the highest *k* such that, at every layer from 1 to *k*, the objects listed with `delivered-verified` evidence cover every unit of the set, whatever became of them, an object refused before its `refines` was checked against the set excepted. `null` when the observer did not verify the base's delivery receipt over its bytes before it offered the base's first layer. |
| `presented` | point | The lowest stage at which a unit was presented on time. |
| `units_by_stage` | array of integers | Element *i*: the number of units presented at stage *i*. The set's units, numbered from 0, are the units it counts. |
| `tier` | string | `decode-equivalence`, `entropy-consistency` or `none`: the weakest tier of the presented units (staged-delivery §5). |
| `stage_hash` | 32 B hex or `null` | The stage hash of the presented output over every unit (staged-delivery §5). |
| `objects` | array | Every layer object observed, each an object of the table below, ordered by `layer`, then `units.from`, then `object`. |
| `conflicts` | integer | Units frozen by a conflict (staged-delivery §4.1 step 7). |
| `deadline_misses` | integer | Units presented below the stage their layers would have reached on time (staged-delivery §8). |
| `bytes_envelopes` | integer | Envelope bytes received for the set, the base's included (staged-delivery §11). |
| member | type | meaning |
|---|---|---|
| `layer` | integer | The object's `refines.layer`. |
| `object` | 32 B hex | Its §2.1 content hash. |
| `units` | object | `{ "from": integer, "count": integer }`: the units it covers, its `refines.span` or every unit of the set. For an object refused before its `refines` was checked against the set — a reason of staged-delivery §4.1 steps 1 and 2, `refines-malformed`, `capability-not-refinement` or `base-absent` — the units its `refines` states, which need not lie in the set. |
| `bytes` | integer | Its envelope bytes as received: more than `0` for an object that arrived, `0` for one that did not. |
| `status` | string | `applied` (to every unit it covers), `partial` (to some of them), `late` (it arrived, and missed the deadline of every unit it covers), `dropped` (it never arrived: not forwarded, or abandoned at a deadline) or `refused` (it arrived, and applied to no unit for another reason). |
| `evidence` | string | What the observer verified of its delivery receipt: `delivered-verified` (§3 step 6, over its envelope bytes), `inclusion-verified` (§3, *Verified by inclusion*) or `none`. |
| `reason` | string or `null` | For `partial`, the staged-delivery §4.2 reason of the lowest unit it was not applied to; for `refused`, the envelope's reason, or else that of its lowest unit. `null` for `applied`, `late` and `dropped`. A unit the object is still held for when the claim is made counts as not applied, for the reason it is held for. |
Rules, checked at §3.1 step 4 in this order:
1. `vocabulary` **MUST** be `urn:wai:staged-delivery:1`, and every point present,
`presented` among them, **MUST** be exactly the member `wai-stage:layers`, from 0
to 255 (`stage_point_malformed`). `op_requested` and `op_committed` **MUST NOT**
be present without `op_delivered` (`stage_point_incomplete`, as §2.4.9 rule 4).
§2.4.9 rule 3 does not apply: an observer states what was delivered whether or
not anyone committed to it.
2. `stage_hash` **MUST** be present exactly when `tier` is `decode-equivalence`
(`stage_hash_without_tier`).
3. `units_by_stage` **MUST** have from 1 to 256 elements summing to at least 1, its
last element not 0; `presented` **MUST** name the lowest stage with a unit; and
neither `conflicts` nor `deadline_misses` may exceed the number of units
(`stage_units_malformed`).
4. Each object's `layer` **MUST** be from 1 to 255 and its `units.count` at least 1
(at least 2 for a `partial` object, which applied to some units of its span and
not to others), and its units **MUST** lie within the set's unless it is `refused` before its
`refines` was checked against the set (the table above); its `reason` **MUST** be
`null` exactly where the table says so and otherwise a reason of staged-delivery
§4.2. Where `op_delivered` is not `null` — the base's receipt was verified first —
an `applied` or `partial` object's `evidence` **MUST** be `delivered-verified`,
since over such a base a layer applies only on a receipt verified over its bytes
(staged-delivery §10.1); where it is `null`, a layer applied whatever was verified
of its receipt, and any evidence is stated. A `dropped` object's `evidence` **MUST
NOT** be `delivered-verified`. An object that arrived — `applied`, `partial`,
`late` or `refused` — **MUST** have `bytes` above 0, and a `dropped` one 0. The
objects **MUST** be in strictly increasing order of (`layer`, `units.from`,
`object`); their `bytes` **MUST** sum to less than `bytes_envelopes`, which counts
the base's bytes too; and no unit **MAY** be covered by two `applied` objects of
one layer, since the second would conflict with the first or find the unit past
its stage (`stage_objects_malformed`).
5. A unit reaches stage *s* only through an object of each layer up to *s* applied to
it, and an object of layer *s* applies only to a unit an object of layer *s* − 1
applied to. So, for every layer *s* from 1 up (`stage_units_unsupported`):
- an `applied` object of layer *s* **MUST** lie within the spans of the `applied`
and `partial` objects of layer *s* − 1 (for layer 1, within the set), and a
`partial` one **MUST** meet them;
- the units presented at stage *s* or above **MUST NOT** outnumber the units layer
*s* can have reached: the units its `applied` objects cover, and, of the units
inside the spans of layer *s* − 1 that those do not cover, those its `partial`
objects cover, counting no more than its span less one for each `partial` object,
which was not applied to every unit of its span. Two objects of one layer never
both apply to a unit (rule 4), so their applied units do not overlap.
6. Where `op_delivered` is not `null`, it **MUST** be the stage the objects give
(`stage_delivered_disagrees`). Where it is `null`, this rule does not apply. A
stated `op_delivered` is then never below `presented`: by rules 4 and 5, at every
layer up to the stage presented, the spans of the `applied` and `partial` objects,
each verified over its bytes, hold every unit, though a `partial` object was not
applied to all of its own.
`op_delivered` and `presented` are separate measures. `op_delivered` counts what
arrived under a receipt verified over its bytes, whether or not the sink applied it;
`presented` and `units_by_stage` count what the sink presented on time. A set whose
layers all arrived and were all refused is delivered at their stage, and so meets a
commitment to it, with stage 0 presented; a reader that asks what was presented reads
`presented`, never `op_delivered`.
A reader derives whether the commitment was met as §2.4.9 derives it: it was when
`op_delivered` equals `op_committed`, and there was none when `op_committed` is
`null`. An accepted claim establishes what its signer observed, not that content was
delivered: only `group` and `object` receipts state delivery, and an object's
`evidence` is the signer's account of what it verified, which a reader holding the
receipt checks under §3 itself.
#### 2.4.11. `class: "delivery-gap"`
A relay that does not forward an object still forwards the object's receipts (§4.1).
A `delivery-gap` claim adds the relay's own statement that it did not forward it, and
why. Its subject is the object's §2.1 content hash, and the relay signs it under its
own key, as the parallel receipt §4.3 permits.
| member | type | meaning |
|---|---|---|
| `track_alias` | integer | The object's `track_alias`. |
| `group_id` | integer | Its `group_id`. |
| `object_id` | integer | Its `object_id`. |
| `reason` | string | `delivery-timeout` (its delivery timeout passed at this relay), `discretion` (the relay chose not to forward it) or `not-received` (the relay never received it). |
| `observed_at` | string | A date-time (§2.4.3): when the relay observed the gap. |
The coordinates are integers of §2.4.3, so an object at a `track_alias`, `group_id` or
`object_id` above 2^53 − 1 has no gap claim.
**Against the object receipt** (§3.1 step 6): the object receipt at the claim's
coordinates, verified by inclusion at least. In order: it is at the claim's
`track_alias`, `group_id` and `object_id` (`context_mismatch`), and its `content_hash`
is the claim's `subject` (`gap_subject_mismatch`). A claim checked without that receipt
stops at step 5: it establishes that the relay says it did not forward an object with
this content hash, not that the publisher sent one. An accepted claim says nothing
about whether the object reached anyone by another path.
#### 2.4.12. `class: "reconstruct-session"`
A sink that reconstructed a title **MAY** record what it did in a `reconstruct-session`
claim: the decoders it used, against which parameter sets and on which host; which path
each unit took and why it switched; the presentations it made after their deadline; a
commitment to every output it presented; the bytes it counted and received; the declared
length of the classical rendition it did not need; the `stage` claims it signed about the
stage sets it observed; and, where its decode was metered, the decode's energy. Its
subject is the §2.1 content hash of the session's **entry object**: the first envelope
the session dispatched. It is signed by the sink, with the key its decoder host holds. It
asserts what that key reports and nothing more, and it is not evidence that a person
watched.
A session claim and a `stage` claim (§2.4.10) are about different things, and neither
repeats the other. A `stage` claim is about one stage set: each layer object with its
status and the evidence of its receipt, the units presented at each stage on time, the
conflicts and deadline misses, the delivered stage and the set's envelope bytes. A session
claim is about a title, which can hold several stage sets and envelopes that are no part of
one. It names the `stage` claims its sink signed about the title's sets (`stage_sets`), and
commits to every output it presented across the title, late ones included
(`output_root`). A unit's highest stage is read from the outputs, and a set's layer objects
from its `stage` claim. Step 6 checks the two against each other.
| member | type | meaning |
|---|---|---|
| `session_nonce` | 32 B hex | Fresh random bytes per session, derived from nothing that identifies a person, a device or an account. |
| `media` | string | The entry object's manifest `media` (SPEC §3), as written: 1 to 32 of `a`–`z`, `0`–`9` and `-`. |
| `host` | object | The decoder host (below). |
| `decoders` | array | The decoder entries (below), in **first-use order**: taking the presentations in order of (`layers`, `unit`), each entry is first named after the entry before it. |
| `units` | integer | The units presented at least once. |
| `leaf_count` | integer | The presentations: one per (unit, layers) presented. |
| `output_root` | 32 B hex or `null` | The root of the output tree over the presentations ([staged-measurement](staged-measurement.md) Appendix C), each leaf's `decoder` an index into `decoders`. `null` exactly when `leaf_count` is 0. |
| `primary_units` | integer | The units whose presentation at their largest `layers` came through a decoder entry whose `path` is `primary`. |
| `fallback_units` | integer | The same, for `fallback`. |
| `switches` | array | At most 1024 path switches, each `{ "unit": integer, "from": string, "to": string, "reason": string }`, in order of `unit`: `from` and `to` are `primary` or `fallback`, and `reason` is a term of [delivery-telemetry](delivery-telemetry.md) §5. |
| `switches_total` | integer | Every switch, including any not written. |
| `deadline_us` | integer or `null` | The time, in microseconds, within which the sink's schedule had each unit presented; `null` where it ran under none. |
| `missed` | integer | The presentations made after their unit's deadline. |
| `missed_units` | array | At most 256 of them, each `{ "unit": integer, "layers": integer, "late_us": integer }`: which presentation, and how many microseconds late; in order of (`unit`, `layers`). |
| `stage_sets` | array | For each `stage` claim the sink signed about a stage set, `{ "first_unit": integer, "claim": 32 B hex }`: the title's unit that is the set's unit 0, and that claim's `link_hash`; in order of (`first_unit`, `claim`). A sink signs one for each epoch of a set — before it forgets the set's base (staged-delivery §4.4, §14), and at the session's end — and names them all. |
| `framing` | string | The framing label of the counted members ([staged-measurement](staged-measurement.md) §3, its §3.3): `registered`. |
| `counted_primary` | integer | The counted bytes (staged-measurement §3) of the countable objects received for the `primary` path. |
| `counted_fallback` | integer | The same, for `fallback`. |
| `counted_discarded` | integer | The counted bytes of the countable objects received and never presented. They are in the two members above as well. |
| `uncounted` | integer | The objects received that are not countable (staged-measurement §3, *Only a checked format is counted*). Their bytes are in no counted member. |
| `transferred` | integer | Bytes of WAI objects as received, whole or as byte ranges, without transport or receipts. |
| `receipt_bytes` | integer | Bytes of receipts and claims received (staged-measurement §3.5). |
| `classical_declared` | integer or `null` | The sum, over the `WAI2` envelopes the session received whose classical rendition (below) is of a capability no decoder entry decoded, of that rendition's length as the envelope's rendition table declares it (SPEC §7.1). `null` where there is none. Never estimated. This revision derives it from the `WAI2` envelopes the sink holds in full, the ones rule 9 reads: a sink that received a `WAI2` envelope only as byte ranges holds its head but not the envelope, and cannot yet state that envelope's classical rendition. Recording such a session is HTTP session work still to come. |
| `classical_source` | term or `null` | Where `classical_declared` comes from: `wai2-table`, or a term a later extension registers here. |
| `energy` | object or `null` | `null` when the decode was unmetered. Otherwise `{ "joules_micro", "energy_provenance", "energy_uncertainty", "energy_counter_filtering", "energy_calibration_ref" }`, each written as §2.4.5 writes it (below). |
The **host** is `{ "profile": string, "target": term or null, "build": object, "threads":
integer }`. `profile` is `native-ffi` (a native process), `browser-baseline` (a browser
decoding on its processor), `browser-gpu` (a browser decoding on its graphics processor)
or `edge-wasi` (a WebAssembly host outside a browser), until an extension of sink
profiles registers the set; `target` the platform the host ran on; `build` the build of
its decoders, as §2.4.8 writes a build; and `threads` its decode workers, written as the
largest power of two not above their number, so that it does not single out a machine.
Where `energy` is not `null`, those are the workers of the bracket behind the figure,
which ran on a power of two of them: its `in_flight` is `threads` (staged-measurement
§8.7), so the `measurement` claim on the figure names no more of the machine (§6.7).
A **decoder entry**:
| member | type | meaning |
|---|---|---|
| `capability` | term | The capability decoded. |
| `rendition` | integer or `null` | Its `WAI2` rendition index (SPEC §7.1): `capability` is that rendition's capability. `null` for a `WAI1` envelope. |
| `object` | 32 B hex | The content hash (§2.1) of the object it decoded from: the `WAI2` envelope whose rendition it is, or the `WAI1` envelope — a base, or a layer that refines one. An entry decodes one object, so a sink that decodes several through one decoder names an entry for each. |
| `first_unit` | integer | The title's unit that is unit 0 of the object's base: of the object itself for a base or a rendition with no `refines`, and of the base it refines for a layer. A presentation through the entry of the title's unit `u` is of the object's unit `u − first_unit`. Where a verifier holds a layer's envelope and an entry names its base, the layer's entry has that entry's `first_unit`; where it holds a `stage` claim of a set, an entry naming the set's base or a layer the claim lists has the unit `stage_sets` names that claim at (step 6). |
| `path` | string | `primary` or `fallback`. |
| `decoder` | string | `builtin:<crate>@<version>` — `<crate>` 1 to 64 of `a`–`z`, `0`–`9`, `_` and `-`, `<version>` 1 to 64 of ASCII letters, digits, `.`, `+` and `-`, beginning with a digit — or `module:sha256:` followed by a module's digest (§7.9), 64 lowercase hexadecimal digits. |
| `variant` | term or `null` | A decode path within the decoder, where it has more than one, such as an exact and a floating-point path. |
| `pin` | 32 B hex or `null` | The `sha256` of the parameter-set pin the sink verified (SPEC §3.1). |
| `tier` | string or `null` | That pin's declared tier as SPEC §3.1 reads it: `decode-equivalence`, `entropy-consistency` or `none`. `null` exactly when `pin` is. |
| `module_status` | string or `null` | For a `module:` decoder, the anchor its grant was confirmed under (§7.9): `pinned` or `fetched`. `null` for a `builtin:` decoder. |
| `leaves` | integer | The presentations decoded through the entry. |
`energy` is the decode energy of one completion of the presentations the claim commits
to — the decodes behind its `leaf_count` leaves, the work unit
[staged-measurement](staged-measurement.md) §8 names (its §8.1) — on the host's
processor rails (its §8.4), with its acquisition class. It is the figure of profile
`moq-jwp:claim-link` ([energy-measurement](energy-measurement.md) §6.1), named by this
claim's `link_hash`, and its producer publishes its report in a `measurement` claim
(§2.4.8) signed with the same key (energy-measurement §6). A decode that was not metered
has `energy` `null`, never a figure of 0 (energy-measurement §2).
§3.1 step 2 reads the members in the table's order, and the members of the host, of
each decoder entry, switch, `missed_units` entry and `stage_sets` entry, and of `energy`
and its `energy_uncertainty`, as it reaches them; it checks their types, and
`host.profile`, `host.build.profile`, `path`, `from`, `to` and `framing` against the
values the tables give them.
Rules, checked at §3.1 step 4 in this order:
1. `primary_units` and `fallback_units` **MUST** sum to `units` (`session_path_sum`).
2. The entries' `leaves` **MUST** sum to `leaf_count`; `leaf_count` **MUST NOT** be below
`units`; and `units` is 0 exactly when `leaf_count` is (`session_leaf_sum`).
3. `output_root` **MUST** be `null` exactly when `leaf_count` is 0
(`session_root_presence`).
4. `threads` **MUST** be a power of two (`session_host_malformed`).
5. Each decoder entry **MUST** be as its table says: `decoder` of its grammar;
`rendition` at most 65535; `tier` `null` exactly when `pin` is, and otherwise one of
its three values; `module_status` `null` exactly for a `builtin:` decoder, and
otherwise `pinned` or `fetched`; `leaves` at least 1. No two entries **MAY** be equal
in every member but `leaves` (`session_decoder_malformed`).
6. A `tier` that is not `null` **MUST NOT** exceed the class tier of its entry's
`capability` (SPEC §3.1 *Tier*) (`session_tier_exceeds_class`).
7. Each switch's `from` **MUST** differ from its `to`, and its `reason` **MUST** be a
term of delivery-telemetry §5. The switches **MUST** be in non-decreasing order of
`unit`, and as many as `switches_total`, or 1024 where it is more
(`session_switch_invalid`).
8. `missed` **MUST NOT** exceed `leaf_count`, and **MUST** be 0 when `deadline_us` is
`null`. `missed_units` **MUST** hold as many entries as `missed`, or 256 where it is
more, in strictly increasing order of (`unit`, `layers`), each with `layers` from 1 to
65535 and `late_us` at least 1 (`session_missed_invalid`).
9. `stage_sets` **MUST** be in strictly increasing order of (`first_unit`, `claim`)
(`session_stage_sets_invalid`).
10. `counted_discarded` **MUST NOT** exceed the sum of `counted_primary` and
`counted_fallback` (`session_counted_invalid`).
11. `classical_source` **MUST** be `null` exactly when `classical_declared` is, and
otherwise `wai2-table` (`session_classical_invalid`).
12. Where `energy` is not `null`, its `joules_micro` **MUST NOT** be 0
(`energy_zero_figure`), and its class is admitted as §2.4.5's rule 2 admits one
(`energy_unknown_acquisition`, `energy_legacy_acquisition`,
`energy_acquisition_evidence`).
**Against the outputs** (§3.1 step 6). The context is the list of presentations, each
`{ "unit", "layers", "decoder", "out_digest" }`: a unit, the layers it was presented
at, the index of the decoder entry that produced it, and the SHA-256 of its output
(staged-measurement Appendix C). In order:
1. The list has `leaf_count` entries and no (`unit`, `layers`) twice, and the root of its
output tree is `output_root` (`session_output_mismatch`).
2. It agrees with the members it gives (`session_output_inconsistent`): every `decoder`
names an entry and every `layers` is at least 1; its distinct units number `units`;
each entry's `leaves` is the number of presentations naming it; the entries are in
first-use order; `primary_units` and `fallback_units` count the units whose
presentation at their largest `layers` names an entry on that path; and every
`missed_units` entry names a presentation of the list.
3. A verifier that holds the objects **MAY** decode a presentation again where its
entry's `capability` has the replicate class `neural-int-exact` or `standard-defined`
(SPEC §7) — the entry's object's unit `unit − first_unit`, at `layers` — and then
**MUST** find its `out_digest` (`session_output_irreproducible`). Every other
presentation is checked for membership only.
**Against the objects** (§3.1 step 6). The context lists each object received:
`{ "content_hash", "path", "counted", "transferred", "presented" }`, with `counted` the
object's counted bytes, or `null` for one that is not countable; `transferred` every byte
received for it; and `presented` whether a decoder entry names it. An object received more
than once on a path — fetched again, or in byte ranges — is listed once, its `transferred`
the bytes of every receipt. These rules need the objects and the claim alone. In order:
1. No `content_hash` is listed twice with one `path` (`session_object_duplicate`).
2. The session's `subject`, its entry object, is the content hash of an object received
(`session_entry_not_received`).
3. So is each decoder entry's `object` (`session_source_not_received`).
4. `counted_primary` and `counted_fallback` are the sums of `counted` over the objects of
each path, `counted_discarded` its sum over the objects not presented, `uncounted` the
number of objects whose `counted` is `null`, and `transferred` the sum of
`transferred` (`session_counted_mismatch`).
5. An object is `presented` exactly when a decoder entry's `object` is its content hash
(`session_presented_mismatch`). The sums above can agree with figures a sink stated
falsely; this cannot.
**The classical rendition** of a `WAI2` envelope is the first of its renditions, in
manifest order, that is selectable — not a companion or a refinement, and a capability a
sink may dispatch (SPEC §7.1) — and whose capability is a classical one of the envelope's
media: one the codec glossary (`glossary/codecs.json`) links from an entry of era
`classical`, whose registry media is the manifest's `media`, and which is not text. WAI's
own formats, the neural codecs, records, parameter sets and text capabilities are never
classical. An envelope has one classical rendition or none, and a verifier reads it from
the envelope; nothing names it.
**Against the envelopes** (§3.1 step 6). The context is the bytes of envelopes the
session received: every `WAI2` envelope among them, and any `WAI1` envelope — a base or
a layer — the verifier holds. An envelope is **readable** when it reads as a `WAI1` or
`WAI2` envelope (SPEC §2, §7.1) whose payloads are plaintext, and **encrypted** when it
reads as one whose `security` is a security object naming an encryption (SPEC §6.1): a
sink holding the key decrypts and dispatches it (§6.4 step 3), and its manifest and
rendition table are in the clear. Both are **held**: the rules below read every held
envelope's manifest and table, and read an encrypted one's payloads not at all. Bytes
that are neither — one whose `security` member is not a security object, or bytes that
are not an envelope at all — are **inert**. An inert or encrypted envelope no decoder
entry names is an object received, neither counted (its `counted` is `null`,
staged-measurement §3, *Only a checked format is counted*) nor presented; a sink lists
it like any other, so the session accounts for every byte it transferred. Each
rendition of a held `WAI2` envelope is also its exploded envelope (SPEC §7.1, *Exploding a
component*), the same object (staged-delivery §9): a decoder entry names it either by
that `WAI2` envelope and its `rendition`, or by the exploded envelope's content hash. An
entry naming it by that content hash, with no `rendition`, is read as naming the
rendition, by every rule below — unless the verifier holds the exploded envelope's own
bytes, a separate delivery (staged-delivery §9, [http-delivery](http-delivery.md)
§10.4) that the rules read as the `WAI1` envelope it is. A **held layer** is a held
`WAI1` envelope of a refinement-only capability (SPEC §5), or a rendition of a held
`WAI2` envelope of one: dispatch and selection go by capability, so an envelope or a
rendition of any other capability is a base, a `refines` it carries ignored (SPEC §4,
staged-delivery §3). Its
**base** is named by `refines.base`: in a `WAI1` layer by a content hash, of a `WAI1`
envelope or of a `WAI2` rendition's exploded envelope; in a `WAI2` rendition by the
`id` of a rendition of the same envelope. The base is **held** when the verifier holds a
held envelope that is it, in either form; its own capability is the `WAI1`
manifest's `capability`, or the rendition's; and an entry **names** it when it names it
in either form. A layer is **applied over** the entries naming its base whose
capability the layer's admits (SPEC §5, *Base admission*) and, where the base is held,
is the base's own. Rules 1 and 2 need the objects as well, rules 7 and 8 the outputs;
the others need the envelopes and the claim. In order:
1. Where the verifier holds the objects as well, each envelope is among the objects
received, by its content hash (`session_source_not_received`).
2. Where it holds the objects as well, each object listed with an envelope's content hash
states as `counted` that envelope's counted bytes (staged-measurement §3), and `null`
exactly when it is not countable (`session_counted_mismatch`).
3. Each envelope a decoder entry names is held, readable or encrypted
(`session_envelope_unreadable`). The verifier holds no key: of an encrypted envelope,
the rules below check everything its manifest and rendition table give, and nothing
that needs a payload — whether a payload matches a well-formed `sha256` (rule 4), the
units a base holds, a layer's `WIR1` payload and the span read against its base's units
(rules 5 and 8) — which are the signer's statement.
4. Each decoder entry whose `object` is a held envelope names what that envelope
gives, as a sink dispatches it: of a `WAI2` envelope, a `rendition`, the capability
that rendition has, a rendition selectable or a layer — never a companion, which is
applied with the rendition a sink picks — whose payload matches its stated `sha256`
(SPEC §7.1, *Selection algorithm*, *Component identity*), and of an encrypted envelope
whose `sha256`, where stated, is well formed: 64 lowercase hex digits, the only form a
payload can match, so a rendition whose `sha256` is not (uppercase, another length,
not hex) is one no sink decodes, whatever its payload; of a `WAI1` envelope that
is not a layer, no `rendition` and the manifest's `capability` or its `fallback`, a
capability a sink dispatches, in a manifest whose `capability` is not companion-only
(SPEC §4); of a `WAI1` layer, no `rendition` and its `capability`. And each decoder
entry whose `rendition` is not `null` has its `object` among the held
envelopes, which hold every `WAI2` envelope the session received
(`session_source_mismatch`). A
signer cannot relabel a decoder entry whose envelope the verifier holds, and so cannot
move a presentation through it out of the reach of the re-decode check above.
5. Each decoder entry whose `object` is a held layer names one a sink can apply
(`session_layer_not_applicable`); a sink refuses any other (staged-delivery §4.1, SPEC
§7.1), so no presentation through it is possible:
- in a `WAI1` manifest, its `fallback` is `null` (`capability-not-refinement`), and
its `refines` is present and reads as staged-delivery §3 defines it
(`refines-malformed`);
- a `WAI2` rendition is offered only from an envelope every component of which
conforms (SPEC §7.1, *Refinement components*; the reference
`WaiMulti::check_component_roles`): a refinement component that does not, or a
companion, an `id` or another component that breaks a rule of §7.1 or §5, leaves
none of the envelope's refinements offered;
- its base, where held, is a base — not a layer, and a selectable rendition where it
is a `WAI2` rendition — whose own capability the layer's admits
(`base-not-admitted`, step 4);
- where entries name its base, it is applied over one of them: a sink applies a layer
only over a base decoded through a capability the layer admits, and through the
base's own capability, not its fallback (`base-via-fallback`, step 3);
- where its base's units are read (rule 8), its span lies within them and its runs
sum to the units it covers (`refines-malformed`, step 2);
- its payload parses as `WIR1` for its capability's unit kind (staged-delivery
Appendix A) and holds a unit for each unit it covers, where that count is read
(`payload-malformed`, step 6).
6. Each decoder entry whose `object` is a held layer that is applied over an entry has
the `first_unit` of one of the entries it is applied over
(`session_first_unit_mismatch`). A layer's units are its base's, so its entry states
its base's first unit, whichever form names the base.
7. Where it holds the outputs as well, each presentation through a decoder entry whose
envelope is held is at the layer count that envelope gives: 1 for a base, or a
rendition that is not a layer, and `refines.layer + 1` for a layer
(`session_layers_mismatch`).
8. Where it holds the outputs as well, each presentation through a decoder entry whose
envelope is held is of a unit that envelope covers: its `unit` is at least
`first_unit + a` and below `first_unit + a + n`, where for a base, or a rendition that
is not a layer, `a` is 0 and `n` the units its payload holds — a `WIV1` clip's frames,
and one for a `WIH1` picture, a `WIA1` clip and a `WIS1` tensor — read in the
payload's own format: of the formats SPEC §5 pins for a `WAI1` manifest's
`capability` and its `fallback`, which decodes the same payload (SPEC §4 step 5), the
one whose magic the payload starts with, whichever capability the entry names, and a
rendition's capability's; for a layer, `a` and `n` are its `span`'s `from` and
`count`, or, with no span, 0 and the units of its base, read where the base is held,
in either form (`session_unit_outside_object`). Where WAI reads no unit count — a
payload in no format SPEC §5 pins, one that does not parse as its own, or a base not
held — the presentation is not checked.
9. The lengths of the held `WAI2` envelopes' classical renditions whose capability no
decoder entry decoded — encrypted or not, since the rendition table declares them in
the clear — sum to `classical_declared`, which is `null` exactly when there is none
(`session_classical_mismatch`). In this revision a held envelope is one held in full:
of a `WAI2` envelope a sink received only as byte ranges it holds the head and not the
envelope, so the session cannot yet state that envelope's classical rendition, nor this
rule read it. Recording such a session is HTTP session work still to come.
**Against the stage claims** (§3.1 step 6). The context is the wire form of each `stage`
claim the verifier holds of those `stage_sets` names. A set has a claim for each of its
epochs, and bases that cover the same units of the title each have their own. In order:
1. each passes §3.1 steps 1–4 (`context_invalid`);
2. each is a `stage` claim, signed by this claim's `signer_pubkey`, and its `link_hash` is
the `claim` of an entry of `stage_sets` (`context_mismatch`);
3. where the verifier holds the objects as well, each one's `subject`, the set's base, and
every layer object it lists as arrived — `applied`, `partial`, `late` or `refused`,
with bytes — are among the objects received (`session_stage_disagrees`);
4. each decoder entry whose `object` is the `subject` of one of them, or a layer object
one lists, has as its `first_unit` the `first_unit` of an entry of `stage_sets` naming
a claim of that object (`session_first_unit_mismatch`): a set's base and its layers
are at the units the session names the set at;
5. where the verifier holds the outputs as well, for each claim and each entry of
`stage_sets` naming it, with `f` that entry's `first_unit` and `n` the units the claim
counts (`session_stage_disagrees`):
- every unit from `f` to `f + n − 1` is presented;
- from below: for every stage `s` from 1 up, the units among them presented at more
than `s` layers are no fewer than the units the claim counts at stage `s` or above. A
unit presented on time at a stage was presented at every lower stage before it, and
late presentations are leaves of the tree as well, so the tree can hold more than a
`stage` claim counts and never less;
6. where it holds the outputs, from above: every unit of an entry's units presented at
more than `s` layers lies in the span of a layer-`s` object that a claim named by an
entry holding the unit lists as `applied` or `partial` (`session_stage_disagrees`). A
unit reaches a stage only through a layer the sink applied: a `late` layer missed the
deadline of every unit it covers and a `refused` one applied to none (§2.4.10). The
claims of every epoch count together, so a unit
refined before the sink forgot its base is bounded by the claim of that epoch, and
the tree never holds a stage the claims' applied layers cannot have given.
An accepted claim establishes that its signer reported this session. Its outputs are
committed to by the tree. A presentation through a decoder entry whose capability is
byte-exact is reproducible, and a verifier that decodes it again checks it. An entry's
capability is the signer's statement, except where the verifier holds the envelope of the
object it names, which binds it; that the entry's outputs were decoded from that object,
and not from another the session received, is the signer's statement. Its byte totals are checkable against the
objects, its classical length against the envelopes, and its stage sets against their
`stage` claims, both ways. Its timings, missed deadlines, switches and energy are its
signer's statements (energy-measurement §5). So, deliberately, is each refusal a sink
makes that the rules above do not check, since no context of this claim holds what
decides it: that a layer's base was decoded before the layer was applied, where no
decoder entry names the base (staged-delivery §4.1 step 2, `base-absent`), since a sink
may present a base's units only once a layer has refined them; a layer's delivery
receipt (step 5, `refinement-unattested`), which no context carries; the decode of a
layer's streams to their last symbol, its declared cost and the chain budget (step 6,
`payload-malformed` for a stream, `cost-over-limit`, `chain-over-limit`), which a
verifier that decodes a byte-exact presentation again checks, and which otherwise turn
on the sink's limits; each unit's producer, conflict, geometry and deadline (steps
7–10) and the hold bound and batches (§4.3–§4.4), which turn on the order and the times
of arrival the sink alone saw, while the `stage` claims bound the stages a unit reached
and a re-decode its output; a pin's resolution at the sink, its module authorisation and
its key log (SPEC §3.1, §4), which a decoder entry's `pin` and `tier` state; what an
encrypted envelope's payloads hold — whether each matches its well-formed `sha256`, the
units a base holds and a layer's `WIR1` payload (SPEC §6.4); whether a refinement's exploded envelope that the verifier
holds as bytes of their own was received on its own, a separate delivery that rule 5
reads as a `WAI1` layer and not through its envelope's component rules (staged-delivery
§9); and a
companion's conformance (SPEC §7.1), which does not decide whether the rendition it binds
to is presented. Nothing binds a `stage` claim to a session
beyond its base and its key: a `stage` claim the same key signed about the same base in
an earlier session of the title can be named, and the checks above hold it as they hold
one of this session. The `claim` an
`end` observation carries ([delivery-telemetry](delivery-telemetry.md) §2, its §2.1)
is this claim's `link_hash`.
### 2.5. Worked example
Manifest JSON (group receipt, formatted for readability — wire form
serializes with no insignificant whitespace and preserves key
insertion order):
```json
{
"kind": "group",
"track_alias": 7,
"group_id": 3,
"root_hash": "5d2c7b1f9a04e2a16c83b3d774a09f12c44b65a3e8d11f88c2901a47b50d2c61",
"leaf_count": 2,
"joules_total": 250,
"group_start": "2026-04-25T12:00:00Z",
"group_end": "2026-04-25T12:00:01Z",
"signer_pubkey": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
"signer_id": "did:joule:publisher-1",
"sig": "9f3a4d6b1c0e8742f1b6b94e7d1c2a85f5103e4c8b2d61a93f0e4b8f2c70a1d61f6a2b71d4eecbb0d7a2e90c4b1bf2c8a0e4d63b9101fa0e2c4d83b1f70e9a103",
"parent_receipt_hash": null
}
```
Object receipt for the second object in that group:
```json
{
"kind": "object",
"track_alias": 7,
"group_id": 3,
"object_id": 1,
"content_hash": "1c3f4a2e88b0d77192aabd0c66f12345e89c7b41ab2cd35f7e80b912ad6c9012",
"joules_micro": 150,
"origin": "joule-cloud-us-west-2",
"merkle_proof": {
"siblings": [
"a07b4d2c91f5e3110687f4b210ff8e1133acdb0741e3920c19acf8b3110d57e2"
],
"direction_bits": 1,
"leaf_index": 1
}
}
```
The signature hex shown is illustrative; conformance is checked
against the test vectors in §5.
An `energy` claim (corpus vector `energy_accept`, §5.6), formatted for
readability — its wire form is its JCS serialisation. This signature
is real, under the corpus's published test seed, which attests
nothing; the figure is invented:
```json
{
"body": {
"coverage": "Whole",
"energy_calibration_ref": null,
"energy_counter_filtering": null,
"energy_provenance": "ModelBased",
"energy_uncertainty": null,
"grade": "derived",
"joules_micro": 12000,
"method": "https://transaction.science/wai/extensions/energy-measurement",
"oer_receipt": "sha256:970afc417e8a310929f3cdcd65ac989a477f240b5a7f27435139261da73288ea",
"work_unit": "subpixel",
"work_units": 49152
},
"class": "energy",
"issued_at": "2026-09-27T12:00:00Z",
"kind": "claim",
"parent_receipt_hash": null,
"seq": 0,
"sig": "58ab60212898afda4315a8f864769aa3378f8b6afd3ddaa77ea4d0061e64a447f298f935aaa26e878e94a0a50fa4f5e76b83cb48376e081c4f761596d94c4f0a",
"signer_id": "did:example:meter",
"signer_pubkey": "a09aa5f47a6759802ff955f8dc2d2a14a5c99d23be97f864127ff9383455a4f0",
"subject": "5bb20a10b980be8c6d95915394f6e89cd49c16d0f5e94cd87c0ec6ce37bcfc5e",
"subject_type": "object"
}
```
Its `link_hash` is
`797a6d60712bb66270be3683546ed9d92c7134d3a3cfa34f304e6fdad886046f`.
A `measurement` claim (corpus vector `measurement_accept`, §5.6) reporting
on an `energy` claim's figure, by the key that signed it: ten idle and
fourteen busy samples, every one live, ten workers released from rest and
sixty completions over 3 s. Its figure and its declared uncertainty are the
least its bracket's integers give (energy-measurement §6.2). Formatted for
readability; the signature is real, under the corpus's published test seed,
and the values are invented:
```json
{
"body": {
"bracket": {
"busy": {
"live_intervals": 14,
"longest_live_run": 14,
"mean_uw": 30000000,
"n": 14,
"sd_uw": 115471
},
"completions": 60,
"idle": {
"live_intervals": 10,
"longest_live_run": 10,
"mean_uw": 20000000,
"n": 10,
"sd_uw": 115471
},
"in_flight": 10,
"opening": "from-rest",
"window_us": 3000000
},
"build": {
"profile": "release",
"target": "x86_64-unknown-linux-gnu",
"toolchain": "rustc 1.98.0 (example)"
},
"energy_calibration_ref": null,
"energy_counter_filtering": "Unknown",
"energy_provenance": "OnChipCounter",
"energy_uncertainty": {
"relative_ppm": 44669,
"window_us": 3000000
},
"figure_index": null,
"figure_profile": "moq-jwp:claim-link",
"group": null,
"joules_micro": 461538,
"power_reader": "reader:example"
},
"class": "measurement",
"issued_at": "2026-09-27T12:00:00Z",
"kind": "claim",
"parent_receipt_hash": null,
"seq": 1,
"sig": "1e3f79ad3571073e64b4ff97733c472c030d241377367a511e2a7be4f5975ea0fb34f2612fc78271941281fa185c3ba1252e8b8c96e3b3c61ee7854580e56e0f",
"signer_id": "did:example:meter",
"signer_pubkey": "a09aa5f47a6759802ff955f8dc2d2a14a5c99d23be97f864127ff9383455a4f0",
"subject": "c857bdf859edf4041541d2507611c4205435be54f47ec38fc6ab4350feaf6b4c",
"subject_type": "receipt"
}
```
Its `link_hash` is
`31cfac4ec8c104a7db59c4156cd7968f539c690c7b5df0c69e34cf2c48529db9`.
### 2.6. Binding classes
A receipt binds to an object in one of two ways.
#### 2.6.1. Hard binding
§2.1: BLAKE3 over the envelope bytes. It holds or it fails, with no
tolerance, and it is destroyed by any change to those bytes — a
transcode, a remux, a platform re-encode. Those are not attacks; they
are what normal distribution does. Reconstruct-class objects have no
transcode hop by construction, and a transcoder that participates is a
lineage step ([provenance](provenance.md) §2). What the hard binding
cannot survive is a re-encode by a party that does not participate:
the receipt stays intact, the object no longer matches it, and with
one binding class nothing connects them.
#### 2.6.2. Soft binding (`class: "soft-binding"`)
A second, deliberately weaker class. A soft binding is a value
computed from the samples rather than the bytes — a watermark decoded
back out of them, or a fingerprint computed over them — by a
registered algorithm, and it survives re-encodes the hard binding does
not. The publisher declares it with a `claim` of class `soft-binding`
whose subject is the object (its §2.1 content hash):
| member | type | meaning |
|---|---|---|
| `alg` | term | The algorithm identifier, verbatim from the registry `registry` names, in that registry's reverse-DNS form (a rule below, at step 4). |
| `registry` | absolute URI | The public algorithm registry `alg` is listed in. |
| `binding_type` | string | `"watermark"` or `"fingerprint"`, as the registry types the algorithm. |
| `value` | hex | The value the algorithm produced for the object. |
| `resolution_endpoint` | absolute URI | Where this declaration — and through it the publisher's receipts for the object — may be recovered from `value`. That it is an `https` URI with a host and no userinfo is a rule below, at step 4. |
Rules (§3.1 step 4): `alg` **MUST** be in reverse-DNS registry form
(two or more dot-separated labels of ASCII letters, digits, `_`, `+`
and `-`) and so **MUST NOT** be a bare name; `value` **MUST NOT** be
empty; `resolution_endpoint` **MUST** be an `https` URI with a host
and no userinfo. That is: it begins with the characters `https://`,
and its authority — what follows, up to the first `/`, `?` or `#` —
holds no `@` and is a host, optionally followed by `:` and a port of
ASCII digits (RFC 3986 §3.2.2–§3.2.3; the port may be empty). The host
is the text before the first `:`, or, for an IP literal, the text
between `[` and `]`, and it **MUST NOT** be empty. Nothing further
about the host is checked.
The fields align with the C2PA soft-binding algorithm list — a public,
schema-validated registry whose entries carry an algorithm identifier,
its type (`watermark` | `fingerprint`) and optional resolution APIs. A
declaration naming that list in `registry` names its algorithm by the
list's identifier in `alg`. WAI defines no soft-binding algorithm, no
resolution service and no transport for one: `resolution_endpoint` is
a locator, referenced the way the carrier protocol is, and a verifier
here checks that `alg` has registry form, not that it is listed. A
soft binding here covers the whole object; a binding scoped to a
region or a time range is not expressible.
#### 2.6.3. Recovery record (`class: "binding-recovery"`)
A recovery (§3.2) **MAY** be recorded as a `claim` of class
`binding-recovery`, signed by the party that performed it. Its subject
is **the ancestor** — the declaration's subject — never the object in
hand.
| member | type | meaning |
|---|---|---|
| `alg` | term | As the declaration's. |
| `binding_type` | string | As the declaration's. |
| `value` | hex | The value computed from the object in hand. |
| `declaration` | 32 B hex | `link_hash` of the `soft-binding` claim the value matched. |
| `resolution_endpoint` | absolute URI | The endpoint the recovery was made through; an `https` URI with a host and no userinfo, as §2.6.2's rule requires at step 4. |
| `observed` | 32 B hex or `null` | §2.1 content hash of the object in hand, when it is still a WAI envelope. |
| `attests` | string | **MUST** be `"ancestor"` — the ancestor rule, stated in-band. Any string passes step 2; another value is refused at step 4 (`recovery_not_ancestor`). |
Rules (§3.1 step 4): §2.6.2's rules on `alg`, `value` and
`resolution_endpoint`; `attests` **MUST** be `"ancestor"`; `observed`
**MUST NOT** equal `subject` — an object whose hard binding holds is
verified by §3, not recovered, and recording it as a recovery would
downgrade a hard binding to a soft one.
**Against the declaration it names** (§3.1 step 6). The context is the
declaration **together with** the publisher key *P* of a delivery
receipt for the declaration's subject that the verifier has verified
under §3 — *P* taken from that receipt, never from the declaration or
from the record, as in §3.2 step 3. A verifier that holds the
declaration but no such receipt holds no context for the record, and
**MUST NOT** report the record as checked against its declaration.
The checks, in order:
1. the declaration passes §3.1 steps 1–4 (`context_invalid`). Its
members, `signer_pubkey` among them, are its signer's only once its
signature is checked: a declaration that was only parsed can carry
the publisher's key over anyone's signature;
2. the declaration is a `soft-binding` claim (`context_mismatch`);
3. `declaration` is its `link_hash`, and `subject`, `alg` and
`binding_type` equal the declaration's
(`recovery_declaration_mismatch`);
4. the declaration's `signer_pubkey` is *P*
(`recovery_signer_mismatch`) — §3.2 step 3's check, without which a
record could carry a recovery that §3.2 itself refuses;
5. `value` matches the declaration's `value` under the algorithm's own
rule (`recovery_no_matcher`, `recovery_value_mismatch`).
**What an accepted record establishes.** Checked against that context:
that its signer recovered through a declaration made by the
ancestor's publisher, with a value that matches it under the
algorithm — and no more, since §3.2's ancestor rule and its bound on
what a match is worth apply unchanged. Accepted without that context
(steps 1–5 only): that its signer reports a recovery, and nothing
about who made the declaration it names or whether the value matches
it.
---
## 3. Verifier algorithm
A conforming verifier MUST execute the following six steps in order
for every group receipt and every object receipt it accepts. Steps
match the algorithm style of WAI SPEC.md §4 (numbered, terminating).
1. **Parse and shape-check.** Read the group receipt. If `kind !=
"group"`, reject (`wrong_kind`). If `sig` is not 64 bytes, if any
required field from §2.4.1 is absent, or if a member §2.4.1 does not
define is present, reject (`malformed`). Labelled members on a group
without `"sign_version": 2`, a `sign_version` other than `2`, or a class
that cannot be read, reject (`group_label_invalid`). If `signer_pubkey` does not decode under
RFC 8032 §5.1.3 or is of small order — §3.1 step 3's key rule — reject
(`bad_signer_key`).
2. **Verify the Ed25519 group signature.** Construct the signing payload
per §2.3 from the receipt's fields: the labelled payload for a group
with `"sign_version": 2`, the legacy payload otherwise. Verify `sig`
against `signer_pubkey` under §3.1 step 3's rule — `S` < L, `R` not of
small order, the cofactorless equation — the same rule every claim
signature is held to, so that every conforming verifier accepts the same
group signatures. If verification fails, reject
(`signature_verify_failed`). For a labelled group, check its class with
the group alone (§2.4.1) and reject one that breaks it
(`group_label_invalid`). After this step, the verifier holds a
**verified root**: the receipt's `root_hash` field, now bound to
`signer_pubkey` under signature, and for a labelled group the total's
class.
3. **Check parent linkage.** Receipt chains are kept per
(`signer_pubkey`, `track_alias`): one chain for each publisher key and
track, so a group on another track, or under another key, is never a
group's predecessor, and is not held as one. Against the chain of the
group's key and track, with the **link hash** of a group defined below:
- the chain's head offered again passes: another of its objects is
being verified;
- a group the verifier admitted to the chain before, offered again with
the same bytes — a legacy group's unsigned parent among them — passes
and changes nothing (`accept again`), never `chain_break`: a carrier
can deliver one object twice, as MOQT does by a subscription and a fill
(MOQT §3.4), and the group's objects are verified against it as against
the head. A legacy group whose parent differs from the one admitted is
not that group, and is checked as below;
- with no group of the chain held, a `null` parent starts the chain, and
any other parent is `parent_not_held`;
- with the chain's head held, a parent that is the head's link hash
links the group; a `null` parent is `chain_break` (a second chain
head); a parent that is the link hash of another group of the chain
the verifier holds is `chain_break` (a fork); any other parent names a
group the verifier does not hold, and is `parent_not_held`.
`parent_not_held` is recoverable: the verifier MAY request the missing
receipt and offer it before the group again; it MUST NOT silently accept.
A verifier that keeps only the head cannot tell a fork from a gap and
reports both `parent_not_held`; one that keeps the link hashes of
earlier groups of the chain tells them apart, and refuses both either
way.
**Holding.** A verifier MAY hold a group that is `parent_not_held` and
offer it again when its chain admits a group it links to — the group its
parent names, or, inside a gap (*History*, below), the group that names it
as parent; a held group that then links is accepted. A verifier that holds
**MUST** state a bound on what it holds: for each chain, a count of groups
held live, a time from each one's arrival, or both; for each chain, a count
of groups held as history, those whose `group_id` lies inside a gap the
chain's anchors left, outside the live count; and a count across all its
chains, of both kinds, since any key makes a chain. When holding a group
live would pass its chain's live bound or the count across chains, or a
group held live passes the time, the verifier refuses every group it
holds live for that chain (`parent_not_recovered`) and re-anchors the
chain. When holding a group as history would pass its chain's history
count or the count across chains, it refuses the group held as history
with the lowest `group_id` below it, or, if there is none, the group
itself (`parent_not_recovered`); the chain does not re-anchor. A verifier
verifies each group's signature once: a group it has admitted or holds is
the same bytes, by its link hash.
**Re-anchoring.** The next group of a re-anchored chain that passes steps
1 and 2, the freshness bound of §6.1 where the verifier applies it, and
whose `group_id` exceeds the chain head's, becomes the chain's head
whatever its parent: its **anchor**. Where the bound is a count, the group
whose arrival passes it is that next group.
**Joining.** A verifier that begins to verify a track after its first
group **MAY** declare that it joins. It then anchors each chain it holds
no group of at the first group of the chain that passes steps 1 and 2 and
the freshness bound where it applies it, whatever its parent (a `null`
parent is the chain's true start, and no anchor).
**History.** Each anchor leaves a **gap**: below it, the last group
admitted before the anchor (none for a join); above it, the earliest group
admitted at or after the anchor. A group whose `group_id` lies between the
two and that links to either end is accepted as **history**: forward, its
parent is the link hash of the group below; backward, its link hash is the
parent the group above names under its signature. Either way it becomes
that end, link by link, so a gap fills in any order — ascending, as a
fetch delivers it, descending, or from both ends — and once one group
links to both ends the gap is closed. A group that links into a gap is
history, never a fork: `chain_break` stays for a group naming a parent
that already has a child on the chain, or for a second chain head. So a
receipt fetched after its chain re-anchored, or before the group a joiner
anchored at, is admitted. A legacy group's parent is unsigned, so nothing
is accepted backward from a legacy anchor or from a legacy group of the
history; forward, a legacy group links into a gap as it links to a head,
with a `group_id` above the group below. A verifier **MAY** keep a bounded
number of gaps open for each chain and close the oldest past it.
An anchor is not a link. A join or a re-anchor establishes what the
anchor's signature does — that its key signed this group, at this
`group_id` — and nothing about the groups it skips: those before the anchor
of a join, or between the last group admitted and the anchor of a
re-anchor, may have been published and withheld, or never published. A
verifier **MUST** report a chain it anchored as anchored, with the
anchor's `group_id` and, after a re-anchor, the `group_id` of the last
group it admitted before it, and **MUST NOT** report the groups skipped as
verified. The objects of every group it admits are checked by steps 4–6 as
any others (§6.1).
**A legacy link authenticates nothing.** The legacy payload (§2.3) does
not sign `parent_receipt_hash`, and a legacy group's link hash does not
cover it, so a relay can rewrite it at will: a legacy group linked to the
head shows only that someone wrote the head's link hash beside it. The
legacy payload does sign `group_id`. A verifier therefore **MUST** refuse
a legacy group whose `group_id` does not exceed its chain head's
(`group_id_not_increasing`), whatever it does for labelled groups:
`group_id` is the only order a legacy chain signs, and without this rule
a rewritten link replays the chain's history as new. The labelled
payload signs the link, and its link hash covers it.
A verifier that applies §6.1's opt-in checks also refuses here a
labelled group whose `group_id` does not exceed its chain head's
(`group_id_not_increasing`), and a group whose `group_end` is outside
its freshness window (`group_not_fresh`), unless its signed `group_id` is
below the chain's live edge (§6.1). A group that passes steps 1–3 MAY become
the head of its chain whether or not any of its objects arrives (§4.4),
so a dropped object does not break the link of its track's next group.
`canonical_bytes(G)` is G's §2.3 signing payload, in the form G was
signed in, followed by its 64-byte `sig`. The resulting hash is G's
**link hash**: the identity a `claim` names G by (§2.4.3), and a
`WAI_RECEIPT` property's `group_receipt`
([moq-streaming-format](moq-streaming-format.md) §5). For a group in
the legacy form it does not cover `parent_receipt_hash`, which that
payload does not sign either; the labelled payload signs it.
4. **Verify the object's content hash.** For each accompanying
`kind: "object"` receipt: recompute `content_hash` per §2.1 over
the received WAI envelope bytes and compare to the receipt's
`content_hash` field. If they differ, reject the object
(`content_hash_mismatch`: the envelope was mutated in transit). An
object receipt missing a member of §2.4.2, or carrying one neither
§2.4.2 nor its label defines, is `malformed`.
A verifier that received only part of an object over HTTP takes as
the recomputed content hash the cid that a verified head proof
establishes ([http-delivery](http-delivery.md) §6), provided every
component it uses was checked against its entry's digest
(http-delivery §7). The object receipt then attests the object that
cid names. A receipt about a component's own object, its exploded
envelope ([SPEC](../SPEC.md) §7.1), is checked over that envelope,
which the verifier builds from the verified head and the component's
bytes (http-delivery §8.3).
5. **Verify the Merkle inclusion proof.** In this order:
- If the object receipt carries a class label (§2.4.2), check it as
§2.4.2 requires and reject the object if it fails
(`object_label_invalid`).
- Check the object's position: `leaf_index` is below the group's
`leaf_count`; `merkle_proof.siblings` holds exactly
⌈log₂ `leaf_count`⌉ hashes (none for one leaf); `direction_bits`
equals `leaf_index`, so it holds the index's bits and no higher
ones; `object_id` equals `leaf_index`; and `track_alias` and
`group_id` are the group's. Otherwise reject
(`proof_position_mismatch`). Without this, the last leaf of an odd
level, duplicated, walks two positions, and an object can be
presented at another object's index.
- Compute the object's leaf in the labelled form of §2.2 where it
carries a label, and in the legacy form otherwise. Walk the
`merkle_proof.siblings` list from level 0 upward; at each level `i`,
combine the current value with the sibling using the
`direction_bits` bit `i` (1 = current is right child, 0 = current is
left child) and the `moq-jwp:merkle-node\x01` domain. After walking
all siblings, compare the resulting hash to the verified root from
step 2. If they differ, reject (`inclusion_proof_failed`).
6. **Accept.** The object's bytes are bound to `signer_pubkey` at
`(track_alias, group_id, object_id)`. Where the object's leaf is
labelled, its `joules_micro`, its class, the class's evidence and its
`origin` are bound with them. Where it is not, `joules_micro` and
`origin` are the publisher's unsigned statements. The verifier MAY now
persist the group receipt as the new chain head for (`signer_pubkey`,
`track_alias`) and dispatch the WAI envelope per WAI SPEC.md §4. Whose key
`signer_pubkey` is, is §7's to confirm; a group receipt needs the
authority `delivery`, and the energy authority of each class its
labelled leaves and its labelled total carry (§7.5).
A verifier MUST treat any rejection in steps 1–5 as a hard failure
for the object in question. A verifier MUST NOT accept a partial
verification (e.g. valid Merkle proof against an unverified root): a
reading of an object's figure, a group total and a `WAI_RECEIPT`
property are taken only from objects accepted under steps 1–6.
**Verified by inclusion.** An object receipt checked without the object's
envelope bytes — steps 1, 2, 3 and 5, step 4 not run because there are no
bytes to hash — is **`inclusion-verified`**: the publisher signed an object
with this content hash at this position under a root the group signature
binds. It is not step 6 acceptance. A verifier **MUST** report it as
`inclusion-verified`, **MUST NOT** report it, or anything read from it, as
delivered, and **MUST NOT** dispatch an envelope on it. It is how a subscriber
accounts for an object it did not receive (§4.4), and how a `stage` claim names
the evidence for such an object (§2.4.10). An object receipt accepted under
steps 1–6 over the object's bytes is **`delivered-verified`**.
**The group total.** A verifier that holds, for every `leaf_index` below
the group's `leaf_count`, exactly one object receipt it has accepted under
steps 1–6 **MUST** check that their `joules_micro` sum to `joules_total`;
if they do not, it reports `group_total_mismatch`, and the objects' own
verdicts stand. If they do:
- for a group in the legacy form, it reads the total from the objects'
labels as [energy-measurement](energy-measurement.md) §2.8 reads a
total: unmetered, unlabelled, or the weakest class as a class name only;
- for a labelled group, it checks that the group's class is the one the
objects compose to (§2.4.1) — the class and its evidence other than the
declared uncertainty (`group_class_disagrees`), the coverage
(`group_coverage_disagrees`), and a `relative_ppm` at least the
composed one over the composed window (`group_uncertainty_below_parts`)
— and reads the total at the group's signed class.
A verifier that does not hold exactly one accepted object for every index
reads a legacy group's total as unlabelled, and a labelled group's total
at its own signed class, which it has not checked against the parts.
### 3.1. Claim receipts
A verifier checks a `claim` in six steps and reports the first check
that fails, as a step and a reason. Within a step, checks run in the
order listed.
1. **Envelope.** A value that is not JSON (RFC 8259), is not an
object, or has a duplicate member name is `malformed`. A text that
begins with a byte-order mark is not JSON here: RFC 8259 §8.1 lets a
parser ignore one, and this step does not. So is `malformed` a value
holding a `\u` escape that leaves a surrogate unpaired — RFC 8259's
grammar admits it, but it denotes no Unicode text, and parsers
differ on what they make of it (RFC 8259 §8.2) — or arrays and
objects nested more than 32 deep, the claim itself being the first
level. A number is never `malformed`: every JSON number is JSON,
whatever its size, and one a member may not hold (`1e400`, `-0`, an
integer of four hundred digits) is `field_type` below. `kind`
absent is `missing_required_field`; `kind` other than `"claim"` is
`wrong_kind`. Any §2.4.3 member absent is `missing_required_field`;
any other member present is `unknown_member`. `class` that is not a
string is `field_type`; equal to `delivery` it is `class_reserved`,
and a string naming no class of §2.4.4 is `unknown_class`. Then, in
table order with `class` done and `body` left to step 2, a
member of the wrong type or encoding — `subject_type` outside its
enumeration, a hash that is not 64 lowercase hex digits, `seq` not
an integer of §2.4.3, `issued_at` not a date-time of §2.4.3, `sig`
not 128 lowercase hex digits — is `field_type`. A `subject_type` the
class does not admit is `subject_type_not_admitted`.
2. **Body.** A `body` that is not an object is `field_type`. Otherwise
it is checked against the class's member table (for a lawful-basis
body, the table of the record that `record`, read first, names; see
§2.4.6): a member absent is `missing_required_field`, an additional
member is `unknown_member`, and then, in the table's order, a member
that is not of the type the table gives it is `field_type`. That is
a wrong JSON type; an integer that is not one of §2.4.3; a byte
string that is not lowercase hex, two digits a byte, or a hash that
is not 64 of them; a value outside an enumeration this extension
defines here (`record`, `binding_type`, a bracket's `opening`, a build's
`profile`, a stage's `tier`, a stage object's `status` and `evidence`, a
gap's `reason`, a session's `framing`, its host's `profile`, and the paths
of its decoder entries and switches); a date-time that is not one of §2.4.3 (a gap's
`observed_at`); a URI that is not absolute;
a term that is not one of §2.4.3 (empty, longer than 2048 bytes, or
holding a White_Space or control character); or a malformed
`oer_receipt`; or an operating point whose member names are not terms
or whose values are not integers of §2.4.3. A member's type and encoding are checked together,
before the next member is read. An object a class fixes the members
of — a lawful-basis purpose, an `energy_uncertainty` — is checked
the same way, its members and then their types, where the table
reaches it, and the entries of an array in the array's order. Of a
member's value, this step checks the type column and nothing else.
What a class's rules require beyond it is step 4's, each with its
own reason, and is never `field_type` here: labels and evidence
from vocabularies another extension defines (acquisition class and
its evidence, coverage, grade); a `joules_micro` or `work_units` of
zero; an empty `purposes`; an empty `basis`, which is why its type
is `term or ""`; an empty `log_receipt` or `value`; an `alg` that is
a term but not in registry form; a `resolution_endpoint` that is an
absolute URI but not an `https` URI with a host and no userinfo; an
`attests` other than `"ancestor"`; a figure profile, an index or a
group that does not fit the profile; a hardware figure's report that
names no power reader; a bracket with fewer than two
samples, liveness facts that do not show a live rail, a zero window,
completion or thread count, or a busy mean not above its idle mean;
the figure and least uncertainty a bracket gives; an empty point; and
which points and predictions are present together; in a `stage`
claim, the vocabulary, a point's parameter and range, a reason of
staged-delivery's, and how the units, the objects and the delivered stage
agree; and, in a `reconstruct-session` claim, how its counts agree, a
decoder entry's grammar and tier, the switches' reasons and order, the
missed presentations, the order of its stage sets, its counted bytes and
its classical rendition.
3. **Signature.** Ed25519 over §2.4.3's signing input, verified by the
rule every WAI signature is held to (SPEC §7 *Signatures*): a canonical
key not of small order, `S` < L, `R` not of small order, and the
cofactorless equation with `R` compared byte for byte. `signer_pubkey`
is `bad_signer_key` if it fails that rule's steps 1 or 2 — it is not
the one encoding of its point, or its point is of small order — and
`sig` is `signature_verify_failed` if it fails steps 3 to 5. The key
checks run first, so a key under which a signature needs no private
key is refused before its signature is examined.
4. **Class rules**, per §2.4.5–§2.4.12 and §2.6, each with its own
reason (below). They run after the signature because a rule about
what a signer said only means something once it is established who
said it.
5. **Chain.** Against the previous claim the verifier holds for
`signer_pubkey`. A claim is held only if it passes steps 1–4 itself.
One that fails steps 1–3 is not known to be its signer's, and one
that fails step 4 breaks its class's rules; neither is held, so an
altered copy can neither satisfy a link nor break one in the
signer's name. A non-null `parent_receipt_hash` with no previous
claim held is `parent_not_held` — recoverable by fetching that claim,
if it verifies, and **MUST NOT** be accepted silently. A parent that is
not the held claim's `link_hash`, a `seq` that does not increase, or
a `null` parent while a previous claim is held, is `chain_break`.
6. **Context**: checks against other receipts the verifier holds,
each verified first — an `energy` claim against the `object` receipt
for its subject, verified under §3, including the class label its leaf
carries (§2.4.5), and against the OER receipt it names, verified under
oer-receipts as §2.4.5's check 2 (`energy_oer_invalid`); a
`measurement` or `operating-point` claim against the receipt that
carries its figure, verified under that receipt's own profile (for
`moq-jwp:object`, the object receipt accepted under §3 against the
group receipt `group` names) (§2.4.8); a decision against its
declaration (§2.4.6);
a recovery record against its declaration together with the
publisher key of a verified delivery receipt for the declaration's
subject (§2.6.3), and never against the declaration alone; a
`delivery-gap` claim against the object receipt at its coordinates,
verified by inclusion at least (§2.4.11); a `reconstruct-session` claim
against its outputs, the objects it received, the `WAI2` envelopes it received
and the `stage` claims it names, in that order, each that the verifier holds, the
`stage` claims verified under steps 1–4 (§2.4.12). A
declaration is verified here under steps 1–4, and one that fails
them — or is not JSON — is `context_invalid`: its members,
`signer_pubkey` among them, are its signer's only once its signature
is checked. For a decision or a recovery record the order is: the
claim is of a class that names a declaration (`context_mismatch`),
the declaration passes steps 1–4 (`context_invalid`), it is of the
class the claim names (`context_mismatch`), then the class's own
checks. A context receipt of another class is `context_mismatch`.
For an `energy` claim, an `object` receipt about another subject,
and an OER receipt other than the one named or about another
subject, are `context_mismatch`, in §2.4.5's order. For a decision
or a recovery record, a declaration other than the one named or
about another subject is the class's own reason,
`decision_declaration_mismatch` or `recovery_declaration_mismatch`;
for a recovery record, a declaration signed by anyone but that
publisher is then `recovery_signer_mismatch`, in §2.6.3's order.
A claim that passes every step it was checked at is **accepted**: its
signer made this statement about this subject. A `measurement` claim is
checked at step 6 always: without the receipt carrying its figure it is
`figure_not_held`, not accepted (§2.4.8). §2.4.4's rules bound
what that means, and §2.4.7 bounds it further for `log-inclusion`.
Who the signer is, and whether a web origin gave its key this claim's
authority, is §7's to confirm. A verifier reports it beside acceptance
and never folds it in: confirmation changes the outcome of no step here.
| step | reason | raised when |
|---|---|---|
| 4 | `energy_zero_figure` | `joules_micro` is 0 in an `energy`, `measurement` or `operating-point` claim, or in a `reconstruct-session` claim's `energy` |
| 4 | `energy_unknown_acquisition` | `energy_provenance` names no class of energy-measurement §2 |
| 4 | `energy_legacy_acquisition` | `energy_provenance` is the legacy `HwShunt` |
| 4 | `energy_acquisition_evidence` | the class's evidence is missing, carried by a class that has none, or below the resolution floor (§2.4.5 rule 2) |
| 4 | `energy_unknown_coverage` | `coverage` is not `Whole` or `Partial` |
| 4 | `energy_unknown_grade` | `grade` names no grade of oer-receipts §2 |
| 4 | `energy_grade_exceeds_acquisition` | `grade` ranks above what the acquisition class supports |
| 4 | `energy_zero_work` | `work_units` is 0 |
| 4 | `lawful_basis_no_purposes` | a declaration lists no purpose |
| 4 | `lawful_basis_purpose_without_basis` | a purpose has an empty basis |
| 4 | `lawful_basis_duplicate_purpose` | a purpose repeats an earlier one |
| 4 | `log_inclusion_no_evidence` | `log_receipt` is empty |
| 4 | `soft_binding_alg_not_registry_form` | `alg` is not in registry form |
| 4 | `soft_binding_empty_value` | `value` is empty |
| 4 | `soft_binding_endpoint_not_https` | `resolution_endpoint` is not an `https` URI with a host and no userinfo |
| 4 | `recovery_not_ancestor` | `attests` is not `"ancestor"` |
| 4 | `recovery_of_hard_bound_object` | `observed` equals `subject` |
| 4 | `figure_unknown_profile` | `figure_profile` names no profile of energy-measurement §6.1 for the claim's `subject_type` |
| 4 | `figure_index_mismatch` | `figure_index` is an integer under a profile that carries one figure, or `null` under one that carries several |
| 4 | `figure_group_mismatch` | `group` is a hash under a profile other than `moq-jwp:object`, or `null` under it |
| 4 | `measurement_no_power_reader` | under a hardware class, `power_reader` is `null` |
| 4 | `measurement_too_few_samples` | a bracket's `n` is below 2 |
| 4 | `measurement_rail_not_live` | under a hardware class, a bracket's liveness facts do not show a live rail, or its samples are all equal |
| 4 | `measurement_empty_burst` | a bracket's `window_us`, `completions` or `in_flight` is 0 |
| 4 | `measurement_no_marginal` | a bracket's busy mean is not above its idle mean |
| 4 | `measurement_below_attribution_floor` | under a hardware class, the marginal draw is below the idle standard deviation |
| 4 | `measurement_figure_mismatch` | `joules_micro` is not the figure the bracket gives |
| 4 | `measurement_window_mismatch` | under a hardware class, the declared window is not the bracket's |
| 4 | `measurement_declaration_below_bracket` | under a hardware class, `relative_ppm` is below the least the bracket supports |
| 4 | `operating_point_empty_point` | a point is present with no member |
| 4 | `operating_point_delivered_alone` | `op_delivered` is present without `op_committed` |
| 4 | `operating_point_incomplete` | `op_requested` or `op_committed` is present without `op_delivered` |
| 4 | `operating_point_empty` | neither `op_delivered` nor `prediction` is present |
| 4 | `stage_point_malformed` | a `stage` claim's vocabulary is not `urn:wai:staged-delivery:1`, or one of its points is not exactly `wai-stage:layers` from 0 to 255 |
| 4 | `stage_point_incomplete` | `op_requested` or `op_committed` is present in a `stage` claim without `op_delivered` |
| 4 | `stage_hash_without_tier` | `stage_hash` is present without `decode-equivalence`, or absent with it |
| 4 | `stage_units_malformed` | no unit, more than 256 stages, a last stage with no unit, a `presented` other than the lowest stage with a unit, or more conflicts or deadline misses than units |
| 4 | `stage_objects_malformed` | a reported object out of order, at layer 0 or above 255, over no unit (a `partial` one over fewer than two), outside the units when its span was checked, with a reason where none belongs or none where one does; applied or partial with evidence other than `delivered-verified` beside a stated `op_delivered`; dropped with `delivered-verified` evidence; arrived with no bytes or dropped with some; objects counting no fewer bytes than the set; or two `applied` objects of one layer over one unit |
| 4 | `stage_units_unsupported` | an `applied` object of layer *s* outside the spans of layer *s* − 1's `applied` and `partial` objects, or a `partial` one that does not meet them; or more units at stage *s* or above than layer *s* can have reached, a `partial` object counting no more than its span less one |
| 4 | `stage_delivered_disagrees` | `op_delivered` is not the stage the reported objects give |
| 4 | `session_path_sum` | a session's `primary_units` and `fallback_units` do not sum to `units` |
| 4 | `session_leaf_sum` | the decoder entries' `leaves` do not sum to `leaf_count`, `leaf_count` is below `units`, or one of `units` and `leaf_count` is 0 and the other is not |
| 4 | `session_root_presence` | `output_root` is `null` with presentations, or a hash with none |
| 4 | `session_host_malformed` | the host's `threads` is not a power of two |
| 4 | `session_decoder_malformed` | a decoder entry's `decoder` is not of its grammar, its `rendition` is above 65535, its `tier` is present without `pin` or absent with it or is not a tier, its `module_status` does not fit its decoder, or its `leaves` is 0; or two entries differ only in `leaves` |
| 4 | `session_tier_exceeds_class` | a decoder entry's `tier` exceeds its capability's class tier |
| 4 | `session_switch_invalid` | a switch between equal paths or for a reason delivery-telemetry §5 does not list, switches out of order, or other than `switches_total` written, 1024 where it is more |
| 4 | `session_missed_invalid` | `missed` above `leaf_count`, or not 0 with no deadline; `missed_units` holding other than `missed` entries, 256 where it is more, out of order, or an entry with `layers` 0 or above 65535 or `late_us` 0 |
| 4 | `session_stage_sets_invalid` | `stage_sets` out of order, or an entry twice |
| 4 | `session_counted_invalid` | `counted_discarded` above `counted_primary` and `counted_fallback` together |
| 4 | `session_classical_invalid` | `classical_source` present without `classical_declared` or absent with it, or not `wai2-table` |
| 6 | `energy_disagrees_with_delivery` | the figure differs from the `object` receipt's |
| 6 | `energy_class_disagrees_with_delivery` | the object's leaf is labelled, and the `energy` claim's class or evidence differs from the label |
| 6 | `figure_disagrees` | the receipt a `measurement` or `operating-point` claim names carries another figure at `figure_index` |
| 6 | `figure_unclassified` | that figure carries no acquisition class |
| 6 | `figure_class_disagrees` | its class or evidence differs from the claim's |
| 6 | `figure_signer_unknown` | a `measurement` claim names a figure no Ed25519 key signs |
| 6 | `figure_signer_mismatch` | a `measurement` claim's figure is signed by an Ed25519 key other than `signer_pubkey` |
| 6 | `figure_not_held` | a `measurement` claim is checked without the receipt that carries its figure (§2.4.8) |
| 6 | `energy_oer_invalid` | the named OER receipt reports any reason under oer-receipts |
| 6 | `energy_disagrees_with_oer` | the named OER receipt states another grade or figure |
| 6 | `context_invalid` | a receipt a claim is checked against — a declaration a decision or a recovery record names, the receipt that carries a figure, or a `stage` claim a session names — does not verify |
| 6 | `decision_declaration_mismatch` | a decision does not match the declaration it names |
| 6 | `decision_purpose_not_declared` | a decision is about a purpose its declaration does not list |
| 6 | `recovery_declaration_mismatch` | a recovery record does not match the declaration it names |
| 6 | `recovery_signer_mismatch` | the declaration a recovery record names (§2.6.3), or a recovery runs through (§3.2), is not signed by the publisher of a verified delivery receipt for its subject |
| 6 | `recovery_value_mismatch` | the value does not match under the algorithm's rule |
| 6 | `recovery_no_matcher` | the verifier has no matcher for the algorithm |
| 6 | `gap_subject_mismatch` | the object receipt at a `delivery-gap` claim's coordinates names another object |
| 6 | `session_output_mismatch` | a session's presentations are not `leaf_count`, repeat a (unit, layers), or give another `output_root` |
| 6 | `session_output_inconsistent` | the presentations name an entry that does not exist or a `layers` of 0, or give another `units`, another entry's `leaves`, another first-use order, other `primary_units` or `fallback_units`, or no presentation for a `missed_units` entry |
| 6 | `session_output_irreproducible` | a presentation decoded again, through a decoder its replicate class makes byte-exact, gives another `out_digest` |
| 6 | `session_object_duplicate` | an object received is listed twice with one path |
| 6 | `session_entry_not_received` | a session's entry object is not among the objects it received |
| 6 | `session_source_not_received` | a decoder entry's `object`, or an envelope held as the session's, is not among the objects it received |
| 6 | `session_counted_mismatch` | the objects received give other counted, uncounted or transferred bytes, or an object whose envelope is held states other counted bytes than that envelope gives |
| 6 | `session_presented_mismatch` | an object is listed as presented and no decoder entry names it, or as not presented and one does |
| 6 | `session_envelope_unreadable` | an envelope a decoder entry names is neither a `WAI1` or `WAI2` envelope whose payloads are plaintext nor a well-formed encrypted one |
| 6 | `session_source_mismatch` | a decoder entry names other than its object's envelope gives — another rendition, a capability that rendition or that `WAI1` manifest does not have, or a rendition of a `WAI1` envelope — or names a rendition and its envelope is not held |
| 6 | `session_layer_not_applicable` | a decoder entry names a held layer a sink cannot apply: not a refinement, its `refines` malformed or absent, a nonconforming refinement component, its base not a base its capability admits or decoded only through a fallback, its span outside a held base's units, or its payload not `WIR1` with a unit for each it covers |
| 6 | `session_first_unit_mismatch` | a decoder entry's `first_unit` is not its base's: a held layer's, against the entries naming its base; or an entry naming a `stage` claim's base or layer, against the unit `stage_sets` names the claim at |
| 6 | `session_layers_mismatch` | a presentation through a decoder entry whose envelope is held is at another layer count than that envelope gives |
| 6 | `session_unit_outside_object` | a presentation through a decoder entry whose envelope is held is of a unit that envelope does not cover, from its `first_unit` |
| 6 | `session_classical_mismatch` | the envelopes' classical renditions the session did not decode give another `classical_declared` |
| 6 | `session_stage_disagrees` | a `stage` claim the session names is about a base, or lists an arrived layer object, the session did not receive; counts a unit the session never presented, or more units at a stage than the session presented at it or above; or no claim whose set holds a unit lists an applied layer that reaches a stage the session presented it at |
### 3.2. Recovery through a soft binding, and the ancestor rule
For an object in hand that fails §3 step 4 — or is no longer a WAI
envelope at all — a verifier **MAY** attempt recovery. It needs: a
`soft-binding` claim *D*, typically returned by a resolution endpoint;
the publisher key *P* of a delivery receipt for *D*'s subject that it
has verified under §3; the value *V* the registered algorithm computes
from the object in hand; the object in hand's §2.1 content hash *H*,
if it is still a WAI envelope; and a matcher for *D*'s algorithm.
1. Verify *D* under §3.1 steps 1–4. A rejection of *D* is the
recovery's rejection, reported unchanged.
2. *D* **MUST** be a `soft-binding` claim (`context_mismatch`).
3. *D*'s `signer_pubkey` **MUST** equal *P*
(`recovery_signer_mismatch`). *P* comes from the verified delivery
receipt, never from *D*: a soft binding declared by anyone but the
publisher links the object in hand to that party's statement, not
to the publisher's receipts. The resolution endpoint is a locator,
not an authority; what it returns counts only if it passes this
step.
4. If *H* equals *D*'s subject the hard binding holds and there is
nothing to recover (`recovery_of_hard_bound_object`): verify under
§3 instead.
5. Evaluate *V* against *D*'s `value` under the algorithm's own rule.
A verifier with no matcher for the algorithm **MUST NOT** report a
recovery (`recovery_no_matcher`); no match is
`recovery_value_mismatch`.
Steps 2–5 report at step 6. On success the result is a **recovery**,
and it attests *D*'s subject: the ancestor.
**The ancestor rule (REQUIRED).** A receipt reached through a soft
binding attests the ancestor the object in hand was derived from, not
the object in hand. A verifier **MUST** report a recovery as such,
naming the ancestor, and **MUST NOT** report the object in hand as
verified, as bound, or as the subject of the ancestor's receipts. The
energy consequence is part of the rule: the ancestor's figures cover
producing the ancestor; the transformation from the ancestor to the
object in hand is **unaccounted, which is not the same as zero**, and
a verifier **MUST NOT** report the ancestor's figures as the energy of
the object in hand. An implementation that cannot express this
distinction **MUST NOT** implement recovery: a recovery presented as a
hard binding is worse than no recovery, because it launders a
transformation nobody attested.
**What a match is worth.** A soft-binding match is evidence at the
algorithm's own error rates, which WAI does not state. A watermark can
be copied onto other content, and unrelated content can share a
fingerprint. A recovery says the object in hand matched the
publisher's declared binding under the named algorithm — nothing about
how it was derived, by whom, or with what changes.
A recovery **MAY** be recorded as a `binding-recovery` claim (§2.6.3).
The reference implementation (`wai-rs`, feature `receipt_claims`,
`claim_receipt::recover`) returns a value whose only attestation is the
ancestor; there is no path from it to a statement about the object in
hand.
### 3.3. Delivery reasons
| check | reason | raised when |
|---|---|---|
| §3 step 1 | `wrong_kind` | `kind` is not the receipt's kind |
| §3 step 1, 4 | `malformed` | a group receipt (step 1) or object receipt (step 4) lacks a member, carries one its kind does not define, or holds one of the wrong type or encoding |
| §3 step 1 | `bad_signer_key` | `signer_pubkey` does not decode under RFC 8032 §5.1.3, or is of small order |
| §3 step 1, 2 | `group_label_invalid` | a group's labelled members appear without `"sign_version": 2`, its `sign_version` is not `2`, or its labelled members cannot be read (step 1), or its class breaks §2.4.1 with the group alone (step 2) |
| §3 step 2 | `signature_verify_failed` | the group signature fails §3.1 step 3's rule over the payload of the group's form |
| §3 step 3 | `chain_break` | within the chain of the group's (`signer_pubkey`, `track_alias`), whose head the verifier holds: a `null` parent (a second chain head), or a parent that is another group of the chain the verifier holds (a fork) |
| §3 step 3 | `parent_not_held` | a non-null parent that names no group of the chain the verifier holds: requested, never accepted silently |
| §3 step 3 | `parent_not_recovered` | a group held for its parent past the verifier's stated bound: held live, refused when its chain re-anchors; held as history, refused alone |
| §3 step 3 | `group_id_not_increasing` | a `group_id` no greater than its chain head's: for a legacy group always, for a labelled one under §6.1's opt-in guard |
| §3 step 3 | `group_not_fresh` | under §6.1's opt-in freshness bound, a `group_end` older than the window, further ahead of the verifier's clock than the skew it allows, or not a date-time, in a group whose `group_id` is not below the chain's live edge |
| §3 step 4 | `content_hash_mismatch` | the envelope does not hash to `content_hash` |
| §3 step 5 | `object_label_invalid` | an object receipt's class label cannot be read or breaks §2.4.2: legacy `HwShunt`, a class beside a zero figure, the unmetered marker beside a non-zero one, a refined class without its evidence or below the resolution floor, or evidence without a class |
| §3 step 5 | `proof_position_mismatch` | the object is not at the position its proof walks: `leaf_index` outside the group, a proof of the wrong length, direction bits other than the index's, an `object_id` other than the index, or another group's coordinates |
| §3 step 5 | `inclusion_proof_failed` | the object's leaf, walked up its proof, does not reach the verified root — among other causes, because the leaf was computed in the other form of §2.2 |
| the group total | `group_total_mismatch` | the accepted object receipts, one for every leaf, do not sum to `joules_total` |
| the group total | `group_class_disagrees` | a labelled group's class, or its evidence other than the declared uncertainty, is not the one its objects compose to, or its objects compose to none |
| the group total | `group_coverage_disagrees` | a labelled group's coverage is not the one its objects give |
| the group total | `group_uncertainty_below_parts` | a labelled group declares less than the composed `relative_ppm`, or another window |
| `WAI_RECEIPT` | `property_invalid`, `property_disagrees` | as [moq-streaming-format](moq-streaming-format.md) §5 |
| carried over MoQ | `moqt_mismatch` | as [moq-streaming-format](moq-streaming-format.md) §4.1: a receipt whose key or `track_alias` is not its receipts track's, or whose `group_id` or `object_id` is not the MOQT Group ID or Object ID of the object it is for; refused before its signature is checked, and not held |
---
## 4. Relay preservation requirements
A **conforming relay** forwards WAI envelopes from one or more
publishers to one or more subscribers. To preserve the receipt
chain across hops, a conforming relay:
### 4.1. MUST
**Receipts carried as payload.** Where a carrier moves receipts as the
opaque payload of its own objects — the receipts tracks of
[moq-streaming-format](moq-streaming-format.md) §4.1, whose relays read no
payload (MOQT §7.8) — a relay of that carrier forwards them without reading
them. Such a relay is not a relay of this section: it cannot verify a group
before fanning it out, and does not; the subscriber verifies every receipt
itself, by §3, and the carrier's forwarding of the receipts' own objects is
what forwards them. The subscriber ties each receipt to the carrier's object
it came on: a receipt whose key, track or position is not that object's is
refused before anything of it is held (`moqt_mismatch` for MoQ). The duties
below bind a relay that reads the receipts it forwards.
- Forward every group receipt and every object receipt **unaltered**
to every subscriber that requested receipts for the corresponding
track. Byte-for-byte equality of receipt bytes between ingress and
egress is REQUIRED.
- Verify the group receipt's Ed25519 signature per §3 step 2 before
the first object of the group is fanned out. A relay that cannot
verify the signature MUST drop the group and emit a structured
error to upstream operators. A relay that predates the labelled
group payload (§2.3) cannot verify a labelled group and drops it, so
a publisher labels a group only once the relays on its path verify
both payloads.
- Forward the `parent_receipt_hash` field as received. A relay that
has previously forwarded the chain MAY additionally check the link
per §3 step 3 and refuse to fan out a broken chain.
- Preserve the byte-exact WAI envelope (`WAI1` magic, manifest bytes,
payload bytes) end-to-end. Any rewriting at the envelope layer
invalidates §2.1 content hashes.
- Forward `claim` receipts (§2.4.3) the same way — unaltered, whatever
their class. A relay **MUST NOT** drop a claim because it does not
know its class: rejecting an unknown class is a verifier's rule
(§2.4.4), and a relay that filters claims decides for every
downstream verifier what it may see.
- Forward, unaltered, every object receipt it receives for an object of a
requested track that it does not forward, whatever the reason — a delivery
timeout that passed at this hop, its own discretion, or never having
received the object itself — with the object's group receipt. A subscriber can then check the
receipt by inclusion (§3, *Verified by inclusion*): the publisher signed an
object with this content hash at this position, and it was not delivered.
`inclusion-verified` is never §3 step 6 acceptance, and a verifier **MUST
NOT** treat it as delivery.
### 4.2. MAY
- Batch multiple `kind: "object"` receipts into a carrier-level frame
to amortize per-receipt overhead, provided every receipt's bytes
are preserved (no field elision, no canonicalization rewrite).
- Emit relay-scoped telemetry alongside the forwarded receipts:
cumulative `joules_total` across observed groups, per-publisher
delivery counts, fan-out fan-in ratios. Such telemetry is local to
the relay and MUST NOT be conflated with publisher-issued
receipts.
- Reorder objects within a group on egress when the carrier protocol
permits — Merkle inclusion proofs are position-bound by
`leaf_index`, so reordering does not invalidate verification at
the subscriber. The relay MUST NOT renumber `object_id`.
- Sign a `delivery-gap` claim (§2.4.11) for an object it did not forward,
under its own key, as the parallel receipt §4.3 permits.
### 4.3. MUST NOT
- Modify the WAI envelope payload bytes in any way (no transcoding,
no manifest rewrite, no key reordering, no whitespace insertion).
Envelope mutation is detected at §3 step 4.
- Re-issue the Merkle root or re-compute a new tree over a subset of
objects. The publisher is the sole authority for the group root.
- Re-sign the group receipt under a relay-owned key. There is one
signer per group, and it is the publisher named in
`signer_pubkey`. Relay attestation, when desired, MUST be carried
as a separate, parallel receipt and MUST NOT replace the publisher
signature.
- Strip the `parent_receipt_hash` field, even when its value is
`null`. The field is required by §2.4.1 and its absence is a
shape-check failure at §3 step 1.
- Truncate the receipt to fewer fields than §2.4 requires, even when
the carrier framing has size pressure.
### 4.4. Objects not delivered
A relay MAY drop an object whose delivery timeout has passed, and an
HTTP client MAY abandon one ([http-delivery](http-delivery.md) §10.2).
An object a subscriber did not receive is not delivered to it: it
neither presents the object nor reports its delivery, and the group
receipt stays valid for every object it did receive, each by its own
inclusion proof (§3 step 5). A relay that drops an object MUST still
forward the group receipt and the object's receipt (§4.1), and the group
can still head its chain (§3 step 3).
A subscriber that holds the receipt of an object it did not receive MAY
check it by inclusion (§3, *Verified by inclusion*). The object is then
`inclusion-verified`: accounted for as signed and not delivered. A `stage`
claim lists it as `dropped` with that evidence (§2.4.10), and staged
delivery never applies a layer on it (staged-delivery §10.1). A receipt
that rides on the object itself, such as `WAI_RECEIPT`
([moq-streaming-format](moq-streaming-format.md) §5), is lost with the
object, so a stream that relies on this accounting also carries its
receipts where a dropped object does not take them with it (a sidecar, or
a track of its own: on MoQ, the receipts track of moq-streaming-format
§4.1, on HTTP the receipt index of http-delivery §8.2).
### 4.5. HTTP caches
An HTTP cache that stores and serves objects and receipt files byte for
byte under [http-delivery](http-delivery.md) §9 is not a relay in the
sense of §4.1 and §4.2: it fans out nothing, verifies nothing, and
forwards what was requested. It is bound by §4.3. A node that verifies,
filters or selects receipts is a relay.
---
## 5. Test vectors
A conforming verifier MUST give, for each case §5.1–§5.5 defines, the outcome
that case states: accept, or reject at the stated step with the stated reason.
The cases are normative as written here. Vectors that instantiate them are
generated outside this tree, by a companion implementation, and are not yet
published in this repository; once a revision publishes them in the tree, a
conforming verifier MUST also verify against them. (The corpora of §5.6–§5.10 —
claim receipts, labelled JWP receipts, key discovery, the receipt chain per key
and track, and the `stage` and `delivery-gap` classes — are published in this
tree.) Each
vector consists of: the WAI envelope bytes for every object in the
group, the canonical group-receipt JSON, the canonical object-receipt
JSON for every object, and a `result.json` declaring the expected
verifier outcome (`accept` or one of the named rejection reasons).
### 5.1. Single-object group (vector `v1_single_object`)
One WAI envelope (`wai.text.zstd`, payload `"hello"` zstd-encoded).
Group receipt with `leaf_count = 1`, `parent_receipt_hash = null`.
Object receipt with `object_id = 0`, `merkle_proof.siblings = []`,
`direction_bits = 0`, `leaf_index = 0`. Expected outcome: `accept`.
### 5.2. Multi-object group with proof verification (vector `v2_multi_object`)
Five WAI envelopes (`wai.audio.opus`, 20 ms frames). Group receipt
with `leaf_count = 5` (the publisher duplicates the trailing leaf
internally when building the tree). Five object receipts, each with
a non-empty `merkle_proof`. Expected outcome: `accept` for each
object receipt.
### 5.3. Cross-hop preservation through 2 relays (vector `v3_cross_hop`)
Three publisher-issued group receipts in chain order G0, G1, G2 with
`parent_receipt_hash` chaining each to its predecessor. Two relay
hops are simulated: ingress bytes → relay A egress bytes → relay B
egress bytes are emitted as three byte-equal copies. The verifier
walks the chain in order; expected outcome: `accept` at every hop,
and the chain head after G2 equals the BLAKE3 of G2's canonical
bytes per §3 step 3.
### 5.4. Invalid signature rejection (vector `v4_bad_sig`)
A well-formed group receipt with a single bit flipped in the `sig`
field. Expected outcome: reject at §3 step 2 with reason
`signature_verify_failed`.
### 5.5. Missing parent_receipt_hash rejection (vector `v5_missing_parent`)
Two group receipts G0, G1. G1 omits the `parent_receipt_hash` field
entirely (rather than setting it to `null` or a hash). Expected
outcome: reject at §3 step 1 with reason `missing_required_field`.
The generator writes the vectors as one directory per case. A conforming
verifier MUST accept the
`accept` cases bit-exactly and MUST reject the named cases at the
declared step with the declared reason. The companion implementation's verifier
exercises all five vectors in its own test suite.
### 5.6. Claim receipts and recovery (`claim-conformance/`)
The typed classes of §2.4.3–§2.4.9, the binding classes of §2.6 and
the procedures of §3.1–§3.2 are checked by the corpus in
[`claim-conformance/`](../claim-conformance/), generated and verified
by `wai_claim_vectors` (`wai-rs`, feature `receipt_claims`) and run by
the WAI conformance workflow on every platform of its matrix. Each
vector declares one outcome: `accept` — with the claim's `link_hash`,
which pins the canonical form, or for a recovery the ancestor it
attests — or `reject` at a named step with a named reason. A verifier
that rejects for another reason, or at another step, does not
conform. The corpus exercises every reason of §3.1; the ancestor rule;
each acquisition class with its evidence, each way the evidence can be
missing, misplaced or below the resolution floor, and the legacy
`HwShunt`; OER receipts sealed with and checked by the OER module,
including figures at the 34 digits a decimal string may hold and
figures and numbers oer-receipts §4 does not admit in a receipt body;
signatures that satisfy the verification equation and that step 3
still refuses, by RFC 8032's own decoding and range rules
(non-canonical encodings of a key, `S` ≥ L) or by the choices step 3
fixes (keys of small order with signatures made without a private key,
an `R` of small order); a non-canonical encoding of a key of large
order, which pins the reason step 3 gives; a recovery record naming a
declaration that the ancestor's publisher did not sign, and one naming
a declaration whose `signer_pubkey` was rewritten to the publisher's
after another party signed it; a previous claim that does not itself
verify; JSON that parsers read differently — a byte-order mark, an
unpaired surrogate escape, deep nesting, numbers out of every range —
and dates that do not exist; the boundary between a member's type
(step 2) and what a class's rules add to it (step 4), from both sides,
and the order step 2 reads a body in, where a body has two defects;
and a claim whose canonical form spans three BLAKE3 chunks. The corpus
also covers the `measurement` and `operating-point` classes: every reason
they add, the arithmetic of energy-measurement §6.2 at its boundaries — a
declaration at the least value a bracket supports and one below it, a
figure at a half microjoule, the ratio thresholds, a flat idle bracket, and
brackets whose products exceed 128 bits — the liveness rule of
energy-measurement §3.1 (a frozen rail refreshed once, a refresh published
in two steps, facts that describe no sequence), and the declaration a
sealing path computes from unrounded samples, which the rule refuses. That
arithmetic is pinned against values `claim_verify.py` computes with
Python's integers. It covers an `energy` claim checked against an object
whose leaf is labelled.
The generator and the verifier in `wai-rs` are one implementation, so
two checks are made against code that shares nothing with it. A
**canonical fixture** — a claim given with its members out of order at
three levels and escapes in the input text — has its canonical bytes,
signature and `link_hash` computed by
`claim-conformance/claim_verify.py` (Python's JSON serialiser with
UTF-16 member order, an Ed25519 implementation other than the one
`wai-rs` uses, and BLAKE3 written from its specification), and pinned
as literals that `wai_claim_vectors` checks first. And that script, a
second verifier written from this section, reports every vector's
declared outcome; it reads and verifies OER receipts with the OER
reference verifier, as `wai-rs` does with its own OER module. The
corpus pins one outcome per vector, which shows the two verifiers agree
on those vectors and nothing more, so
`claim-conformance/differential.py` also generates inputs the corpus
does not hold — JSON at the edges of what parsers accept, calendar
dates, URI authorities, acquisition classes with every combination of
evidence, figures near a half-microjoule and at the edges of
oer-receipts §4 and §6, OER receipts holding numbers §4 forbids in a
body, files §4 does not read at all, context receipts that do not
verify, bodies of every class with one defect and with two at
different positions, measurement brackets and operating points around
every rule — runs both verifiers' `verdicts` mode on them, and reports
every input on which their verdicts differ. Agreement shows that the two
read alike, not that either reads §3 as written; only the vectors'
declared outcomes, set from the text, speak to that, and only for the
inputs they hold. The fixture and the bracket arithmetic run in CI on every
platform of the matrix; the script and the differential, which need Python
with an Ed25519 library, run in CI on the linux runner with a fixed seed.
### 5.7. Labelled leaves, labelled groups and the group total (`jwp-conformance/`)
`wai_jwp_vectors gen` in `wai-rs` writes `jwp-conformance/vectors.json`,
and `verify` requires a byte-identical regeneration and every declared
outcome. Every vector is synthetic: keys come from published fixed seeds
and attest nothing, and envelopes, figures and origins are invented. The
corpus holds leaf vectors in both forms of §2.2; a group in the legacy
payload whose objects mix the forms, and a group in the labelled payload
chained to it, each with its signing payload, signature and link hash;
each object's outcome (`accept`, or a rejection at its §3 step with its
§3.3 reason), including objects presented at another object's position;
group signatures the strict rule refuses; the group total's reading or its
reason; and `WAI_RECEIPT` properties with the reading
[moq-streaming-format](moq-streaming-format.md) §5 gives them.
`jwp-conformance/jwp_verify.py`, written from this text, reports the same
outcomes; CI runs it on the linux runner, and runs `wai_jwp_vectors
verify` on every platform of the conformance matrix. joule-code's
`joule-moq` pins leaves, roots, signatures and link hashes copied from the
corpus.
### 5.8. Key discovery (`signer-conformance/`)
§7 is checked by the corpus in
[`signer-conformance/`](../signer-conformance/), generated and verified
by `wai_signer_vectors` (`wai-rs`, feature `key_discovery`) and run by
the WAI conformance workflow on every platform of its matrix. Three
procedures appear. `anchor` gives a key log, how it was obtained and any
anchor already held, and declares `anchored` with the anchor's
`revision_hash` and path, `not_anchored` with a reason of §7.7, or
`replaced`. `confirm` gives an item — a claim's wire form, or the
signer, the acquisition classes and the signed time of a group receipt
or another family's receipt — with the revisions and anchors a verifier
holds and any expected origin, and declares `confirmed` with the origin,
key-id, head and anchor path and what §7.4 reports beside them, or
`unconfirmed` or `refused` at a step of §7.4 with a reason of §7.7. A
claim that fails §3.1 is declared rejected at its own step with its own
reason, and is never confirmed. `module` gives a module's bytes, a
grant, the sink's module origins, the revisions, anchors and statuses a
sink holds, the time, the capability being decoded and whether the
envelope leaves a fallback, and declares `run` with the kill latency,
or `fallback` or `inert` at a check of §7.9 with its reason; for a
pinned parameter set it may give no grant, and it adds the refusal SPEC
§3.1's check 5 reports. The corpus
covers each rule of §7.2, the head walk with revisions that are not
held, unattached, injected, forked and superseded, each move of an
anchor, each step of §7.4 including the pinned-only authorities, the
authorities a `measurement`, `operating-point`, `stage` or `delivery-gap`
claim needs, a family
receipt whose figures carry two classes,
instants compared across offsets, fractions and a leap second, and each
check of §7.9, with grants from an origin that is not one of the sink's
module origins, a grant whose `origin` is not the one its `signer_id`
names, a digest one module origin revokes and another grants, a status
signed under an injected revision, and a pinned parameter set whose
digest is revoked while its grant fails an earlier check or no grant is
held.
A **canonical fixture** — a revision given with its members out of order
at two levels and escapes in its text — has its canonical bytes,
signature and `revision_hash` computed by
`signer-conformance/signer_verify.py`, which takes from
`claim-conformance/claim_verify.py` only what §7 reuses of §3.1 and
shares no code with `wai-rs`, and pinned as literals that
`wai_signer_vectors` checks first. That script also reports every
vector's verdict from this section. CI runs it on the linux runner, and
requires its `verdicts` output to equal `wai_signer_vectors verdicts`
line for line. §7 changes no vector of the claim corpus (§5.6): every
signer it names writes a label (§7.1), and every vector keeps its
declared outcome, those of the `measurement` and `operating-point`
classes included.
### 5.9. Receipt chains per key and track (vector `v6_per_track_chain`, `jwp-conformance/`)
One key publishes a base on track 7 and a layer on track 8, each with three
chained groups signed over the labelled payload, which signs the link. A
verifier given only track 7's receipts accepts all three
(`v6_per_track_chain_base`); given both tracks' receipts interleaved, it accepts
all six, which a verifier keeping one chain per key would not
(`v6_per_track_chain_interleaved`); given track 8's second group without its
first, it reports `parent_not_held` and does not accept it
(`v6_per_track_chain_layer_without_its_first`); and a track 8 group whose parent
is a track 7 group names nothing the layer's chain holds, and is `parent_not_held`
(`v6_per_track_chain_cross_track_link`). Beside them the corpus of §5.7 holds a
track's first group offered again after its second, which changes nothing, a fork
from a held group and a gap beside the head, a head offered again, another key on the
same track, §6.1's guard
off, on, and compared within one track; the guard holding a legacy group with it
off, a legacy history replayed as new after one rewritten link and its loop, and a
labelled replay whose rewritten link fails its signature; receipts chained per key
across tracks; its freshness bound at the clock, with an offset, at the window's
edge, stale, ahead of the clock, stale with no bound, a stale group behind the live
edge, which fills a gap, a gap of seventy groups walked back and forward, and a stale
group whose rewritten legacy link earns it nothing; a group whose signature fails and so heads nothing; a
previous group of another track or key, which is not held; and object receipts
checked without their envelopes, which are `inclusion-verified` or refused at step 2
or 5. `chain_differential.py` signs chains around these rules and requires both
verifiers to agree on each.
The corpus also holds recovery (§3 step 3): children held until their parent is
fetched; a receipt lost, and the chain re-anchoring past it once a count bound is passed,
its held groups refused (`parent_not_recovered`) and regained as history behind the
labelled anchor; a re-anchor refused behind its head; a verifier that joins mid-stream,
at the chain's first group and on a legacy chain; a stale group that may not anchor; a
gap fetched in ascending order after a re-anchor, and a joiner's fill in ascending
order, held as history outside the live bound, within its history bound and past it;
and the bound across chains. `chain_gap_differential.py` lets both verifiers recover on
chains it signs, with ranges fetched again in ascending order.
`wai_jwp_vectors` and `jwp_verify.py` give every vector's outcome (§5.7).
### 5.10. Stage and delivery-gap claims (`claim-conformance/`)
The corpus of §5.6 holds the classes of §2.4.10 and §2.4.11: a set delivered and
presented at its committed stage (`stage_accept`); its second layer dropped and
verified by inclusion, so the commitment was not met (`stage_not_met_accept`); a
base whose receipt was not verified first, which states no delivered stage, so rule 6
does not apply (`stage_null_delivered_states_no_delivery`), and a layer over such a
base applied on inclusion evidence; a layer delivered and late for one unit; a
conflict; a base presented alone; layers all refused, delivered as committed with
stage 0 presented; a span past the set refused before it was checked, which stands
and delivers nothing; every reason of §2.4.10's rules, among them
`stage_hash_without_tier`, `stage_objects_malformed` and `stage_units_unsupported`:
units at a stage no object of that layer reaches, a partial layer counted as though
applied to its whole span, a layer applied over units the layer below never reached,
and a partial object over one unit, with an honest partial layer accepted beside
them; objects with no bytes or with all
the set's, one layer applied twice to a unit, a layer applied with no receipt beside
a stated delivered stage, and a delivered stage over objects that leave a unit
uncovered; step 2's enumerations and order; a gap accepted against the object
receipt at its coordinates and without it (`gap_accept`), and refused against a
receipt naming another object (`gap_subject_mismatch`) or at other coordinates.
The corpus of §5.8 checks the authorities both classes need (§7.5).
`claim_verify.py` gives every outcome, and `differential.py` generates `stage`
bodies around every rule and gaps against receipts at and away from their
coordinates.
### 5.11. Session claims (`claim-conformance/`)
The corpus of §5.6 holds the class of §2.4.12: an unmetered session (`energy` `null`)
and a session metered under `OnChipCounter` with its evidence, each accepted against
its outputs, the objects it received, its base's `WAI1` envelope and the `WAI2` envelope
it received, and the `stage` claim it names; output trees from 1 to 33 leaves; a
presentation whose re-decode, as the vector states it, gives another digest through a
byte-exact decoder (`session_output_irreproducible`) and through a decoder checked for
membership only (accepted); a decoder entry relabelled to escape that check by each
route — the base's `WAI1` entry, a `WAI2` entry with its rendition kept or nulled, a
capability another envelope has — refused against the envelope of the object it names
(`session_source_mismatch`), and an envelope held that the session did not receive and
an entry's object it did not receive (`session_source_not_received`); the classical
rendition read from envelopes whose second rendition is of each kind a rule excludes and
of two it admits; an encrypted envelope, a `WAI1` envelope with no manifest members and
bytes of neither magic, each named by an entry (`session_envelope_unreadable`), and
accepted where no entry names them; an object listed twice
(`session_object_duplicate`) and one fetched twice and listed once (accepted); figures a
sink stated rather than derived — counted bytes above what the held envelope gives
(`session_counted_mismatch`), and an object marked presented that no entry names, or
not presented that one does (`session_presented_mismatch`); a session holding its base's
and a layer's envelopes, accepted, and a presentation at more layers than its envelope
gives (`session_layers_mismatch`), of a unit past the base's frames, outside the layer's
span, or outside a `WAI2` rendition's frame, through an entry whose `first_unit` is
moved, past the frames of a base decoded through its manifest's `fallback`, the same
where the fallback's format and not the capability's is the payload's, and past a
`WAI2` base component a `WAI1` layer refines by its exploded envelope
(`session_unit_outside_object`); a held inert envelope listed with counted bytes 0
(`session_counted_mismatch`); an encrypted envelope an entry names, accepted with the classical length its table
declares, and refused for that length unstated, a rendition its table does not hold,
another capability, no rendition, a base at two layers, a companion-only capability, a
refinement before a selectable rendition, and a rendition whose `sha256` is malformed
(uppercase, 63 digits, not hex), named by its rendition and by its exploded envelope's
content hash, all read from the manifest in the clear, and accepted with a well-formed
`sha256` its payload does not match;
an encrypted layer over an encrypted base, accepted; entries naming a `WAI2` rendition by its exploded envelope's
content hash, held as the `WAI2` alone — accepted, relabelled, past its frames, a
refinement off its base's first unit and one whose envelope does not conform — and a
refinement's exploded envelope received on its own, accepted; bases whose manifest or
rendition carries a stray `refines`, accepted, and a layer over one; a refinement of an
envelope that does not conform — a refinement before a selectable rendition, a base
rendition with no `sha256`, a layer before its producer, an unbound companion, a record
component with no role — and two
that do, beside a companion; entries a sink cannot have decoded through — a `WAI2`
companion, a rendition or a refinement whose payload does not match its `sha256`, a
`WAI1` envelope whose capability is companion-only, and a fallback that is
(`session_source_mismatch`); held layers a sink cannot apply — layer 0, a span past or
empty over a held base, runs that do not sum to the base's units, no `refines` on a
refinement-only capability, `refines` on one that is not, a fallback on a layer, a base
that is itself a layer, a base decoded only through its fallback, a base the layer does
not admit, a payload that is not `WIR1` or holds too few units, and in a `WAI2` envelope
a base that is no rendition, no component id, another refinement component or a
companion, a base it does not admit, a refinement with no `sha256`, a producer the
envelope does not hold, a selectable rendition after the refinement, and a span past the
base (`session_layer_not_applicable`); a layer's entry at another `first_unit` than its
base's, in a `WAI1` layer and a `WAI2` refinement component, and across the two forms of
one base — a `WAI1` layer over a `WAI2` component, with its exploded envelope held and
not, and a refinement component whose base an entry names by its exploded envelope —
and the base's and a layer's entries at another than the unit `stage_sets` names their
`stage` claim at (`session_first_unit_mismatch`); `WAI2` refinement components presented
within and past the layers and units they give; a `stage` claim that verifies, one that does not, one by another key,
one the session does not name, one about a base the session never received, one whose
arrived layers the session never received, and sessions presenting stages its applied
layers cannot have given — through layers it lists as late or refused among them — and
fewer than it counts; a base forgotten and decoded again (`staged-conformance/`'s
`record_budget`), accepted naming each epoch's claim and refused naming only the last;
every reason of §2.4.12's rules and
checks, the caps of the switches and missed presentations written at and past them; a
metered session's figure of zero (`energy_zero_figure`) and every acquisition-class
reason; and step 2's enumerations and order. `claim_verify.py` gives every outcome.
`differential.py` generates sessions around every rule — units, leaves and paths off by
one, switches and missed presentations at and past their caps, a `null` deadline with
misses, presences that disagree, decoder indices out of range, leaves moved between
entries, entries out of first-use order, stage claims against presentations that reach,
miss and pass their stages and objects that hold or lack their layers, every acquisition
class — with their outputs and objects; envelopes whose manifests every reader may read
otherwise; and every registered capability as an envelope's second rendition. It requires
the two verifiers to agree. The corpus of §5.8 checks the authority a session needs, and
the authorities its energy figure needs (§7.5).
---
## 6. Security considerations
### 6.1. Replay
A receipt without sequencing information is replayable: an attacker
who captures group receipt G can re-present it to a downstream
verifier and bind unrelated bytes to the publisher's identity at a
later time. This extension mitigates replay with two mechanisms:
- `parent_receipt_hash` (§2.4.1) links each group receipt to its
predecessor under the same `signer_pubkey` and `track_alias`. The labelled
payload (§2.3) signs it, and a labelled group's link hash covers it, so a
replay of a labelled chain requires forging every intermediate hash. The
legacy payload signs neither, so in a legacy group the link is an unsigned
statement anyone on the path can rewrite: rewriting one link, the first
group's, to name the head would make the whole history link again.
- `group_id` is monotonic per (`signer_pubkey`, `track_alias`), and both
payloads sign it. A verifier **MUST** refuse a legacy group whose
`group_id` does not exceed its chain head's (`group_id_not_increasing`,
§3 step 3): it is the only signed order a legacy chain has, and it stops
the replay above. For a labelled group the check is OPTIONAL: verifiers
SHOULD track the highest `group_id` seen per (`signer_pubkey`,
`track_alias`) and reject a non-increasing one. This binds the chain to
forward progress. In a legacy chain a relay can still drop a group and
rewrite the next one's link to hide the gap; only the labelled payload
rules that out.
- Anchors (§3 step 3) resume verification without the groups before them,
and prove nothing about those groups. A relay can withhold groups and let
a re-anchor carry the chain past the gap, and can offer a joining verifier
an old group, validly signed, as its anchor. A verifier therefore reports
every anchor and the gap behind it, applies the freshness bound to an
anchor wherever it applies it to the live edge, and accepts a re-anchor
only at a `group_id` above its chain head's, so that a re-anchored chain
still moves forward. History that later fills a gap (§3 step 3) is
admitted only by signed links to the groups on either side, never on
arrival order; and what a verifier holds while it waits is bounded across
all its chains, so a sender of groups under fresh keys or of groups that
never link fills nothing past the bound it states.
- Both are per track. A publisher that leaves `track_alias` at `0` has one
chain per key, as before. A publisher that chained its groups per key
across non-zero tracks, as an earlier reading of §2.4.1 allowed, made
receipts that now fail at step 3: each group whose parent is on another
track names nothing its own chain holds (`parent_not_held`). Separate
chains per track let a subscriber verify the tracks it subscribes to
without the receipts of tracks it does not — a stage set carries each
layer on a track of its own ([staged-delivery](staged-delivery.md)). They
also let a relay withhold a whole track without breaking another track's
chain; which tracks exist is the carrier's catalog's to say, and a
subscriber that requested a track notices its absence.
- `group_end` (§2.3) carries a signed RFC 3339 timestamp. A verifier
MAY additionally bound it against its own clock for the group it
treats as the current tip — rejecting a receipt whose `group_end`
is older than a deployment freshness window (a relay holding
content back) or implausibly far in the future (skew). This
wall-clock check is OPTIONAL and MUST NOT be applied to historical
receipts replayed for late-joiner resync, whose timestamps are
legitimately old — apply it only to the live edge (`group_not_fresh`).
The **live edge** of a chain is the highest `group_id` of a group of it
that verified (§3 steps 1–2) and was inside the window when it arrived,
whether or not it linked. A group whose `group_id` is below the live edge
is history, and the bound does not apply to it: so a group a relay
delayed past the window, refused once as stale, can be fetched again and
fill the gap behind a later group that arrived fresh, in any order and
however long the gap, and the chain does not wedge behind it. This is
safe because both payloads sign `group_id`: only a group the publisher
signed, and that arrived fresh, moves the edge, and no rewrite of a link —
signed in the labelled payload, unsigned in the legacy one — moves it or
earns a group the exemption. A stale group at or past the edge is
refused. Below the edge a stale group still has to link (§3 step 3) and,
if legacy, to exceed the head's `group_id`. The reference verifier
(`jwp_delivery::ReceiptChains`) holds every legacy group to the
`group_id` guard and exposes it for labelled groups, per
(`signer_pubkey`, `track_alias`), and this freshness bound, read to the
second with its offset applied, as opt-in checks, disabled by default so
durable re-verification of an old receipt stays a feature. It keeps the
link hashes of the groups it accepted on each chain, so it tells a fork
from a gap, and each chain's live edge.
### 6.2. Forgery
The signing payload (§2.3) is bound under Ed25519 (RFC 8032). At
classical security levels, Ed25519 provides ~128-bit unforgeability
under chosen-message attack (EUF-CMA). Forging a group receipt is
equivalent to recovering the publisher's private key or breaking
Ed25519. Implementations MUST source signing keys from a CSPRNG and
MUST NOT reuse keys across distinct publisher identities.
### 6.3. Cross-group confusion
The signing payload binds `track_alias`, `group_id`, and
`signer_pubkey` under the same signature. An attacker who attempts
to re-present a group receipt under a different `(track_alias,
group_id)` tuple cannot do so without re-signing — the receipt's
fields are signature-covered, and any field mutation breaks
verification at §3 step 2. The Merkle proof is similarly bound:
the verifier checks that the proof walks the position `leaf_index`
names (§3 step 5), so an object's proof cannot be transplanted to a
different position within the same group, and the duplicated last
leaf of an odd level cannot be presented at the position past the
group's end.
### 6.4. Envelope mutation in transit
Any mutation of the WAI envelope bytes (manifest rewrite, payload
re-frame, whitespace insertion) changes the §2.1 content hash and is
detected at §3 step 4. Relays that re-canonicalize the manifest
SHOULD be treated as broken: they break content-hash equality even
when the semantic meaning is unchanged. WAI SPEC.md §2 already
requires that the manifest be serialized with no insignificant
whitespace and that encoders preserve key insertion order; receipt
verification depends on relays honouring that requirement.
### 6.5. Quorum and multi-signer receipts
v1.0 of this extension is single-signer: every group receipt is
signed by exactly one Ed25519 key. Multi-signer / quorum receipts
(threshold Ed25519, BLS aggregate signatures) are explicitly out of
scope for v1.0. A future minor revision MAY extend §2.3 with a
multi-signer container; the v1.0 wire format remains a strict subset.
### 6.6. Out of scope
- **Post-quantum group-signing ciphersuites.** Migration to
hash-based signatures (SLH-DSA), lattice-based signatures
(ML-DSA), or hybrid Ed25519 + post-quantum receipts is deferred
to a future major revision of this extension. Implementers
building for long-lived archives SHOULD plan key-rotation policies
that allow ciphersuite migration when the upgrade lands. §7.6 gives
the rotation and revocation of a signing key.
- **Confidentiality of receipts.** Receipts carry identity,
joules, and origin labels in cleartext. Carriers that require
receipt confidentiality (e.g. trade-secret origin labels) MUST
layer their own encryption above this extension; v1.0 receipts
are integrity-protected, not confidentiality-protected. Note this
is distinct from **payload** confidentiality: the
[Confidentiality extension](confidentiality.md) seals the object
bytes a receipt attests, and this chain deliberately commits over
that ciphertext so a relay forwards the sealed bytes and their
receipts unaltered, signs nothing in the publisher's name (§4.3), and
never reads the content. A `measurement` claim (§2.4.8) also carries the
measuring machine's power levels, thread count, build and power reader
in cleartext.
- **Pricing semantics for joules.** `joules_micro` and
`joules_total` are unit-of-energy fields, not unit-of-account
fields. Conversion to currency, carbon, or grid-mix CO₂ is
performed by downstream aggregators and is not normative here.
### 6.7. Claims
A `claim` is a signed statement, and §6.1–§6.3 apply to it as to a
group receipt: `seq` and `parent_receipt_hash` bound replay per
signer, and every member — `class`, `subject`, the whole body — is
inside the signature, so a label cannot be promoted in transit (an
`energy` claim whose `energy_provenance` is rewritten from `ModelBased`
to `OnChipCounter`, or whose declared uncertainty is rewritten smaller,
fails at §3.1 step 3). Step 3 also refuses keys of small
order, under which a signature can be made without any private key,
and restates RFC 8032's rule that a key has only its canonical
encoding, so that verifiers following it accept the same signatures.
What a signature does not settle is whether its signer was entitled
to say it. §7 settles as much of that as a web origin's say-so can:
that an origin the verifier has anchored lists the key with the claim's
authority. A confirmed claim is worth exactly as much as that
confirmation; an unconfirmed one, as much as the verifier's own reason
to trust the key that signed it.
A `reconstruct-session` claim says what a sink played. It carries no
identifier of a person, a device or an account: its `session_nonce` is fresh
for each session, and its host states a profile, a platform, a build and a
rounded worker count; a `measurement` claim on its figure states its bracket's workers
as that same count. A sink key used for many sessions links them, so a sink
that does not mean its sessions to be linked signs each with a key of its own,
under a `did:key` (§7.1). Where its claim goes is the sink's choice; delivery
telemetry carries only its `link_hash`
([delivery-telemetry](delivery-telemetry.md) §2, §8).
### 6.8. Soft binding
- **A copied watermark.** A watermark decoded from an object shows
that the object carries the payload, not that it descends from the
ancestor: a payload can be embedded into unrelated content. The
ancestor rule (§3.2) bounds the damage — a recovery attests the
ancestor's receipts, never the object in hand — and a relying party
**MUST NOT** read a recovery as the provenance of the object in hand.
- **Fingerprint collisions.** Unrelated objects can share a
fingerprint value. How often is a property of the algorithm: WAI
states no rate, a `soft-binding` declaration carries none, and a
verifier here does not assume one. A relying party that needs a rate
takes it from the algorithm's own specification, where one is
published; a match is evidence at that rate, never proof of
derivation.
- **A hostile resolution endpoint.** An endpoint can return any
declaration it likes. What it returns counts only if it passes two
checks, made wherever a declaration is relied on — §3.2 steps 1 and 3
when a recovery is performed, and §2.6.3's checks 1 and 4 when a
recorded one is verified: the declaration verifies under §3.1 steps
1–4, and its signer is the publisher of a delivery receipt for its
subject that the verifier has already verified. Neither suffices
alone. Without the first, the signer check compares a key the
declaration's author wrote in, over a signature that may be anyone's;
without the second, a valid declaration binds the object in hand to
whoever made it. A recovery record accepted without that context
establishes nothing about who made the declaration it names
(§2.6.3). What passing both establishes is bounded by the ancestor
rule and by the algorithm's own error rates (§3.2).
- **Downgrade.** An object whose hard binding holds is never
recovered (§3.2 step 4; for a recovery record, `observed` ≠
`subject`), so a soft binding cannot stand in for a hard one that
still verifies.
## 7. Key discovery and signing authority
Every signature in this extension is checked against the `signer_pubkey`
the receipt itself carries (§3 step 2, §3.1 step 3). That settles that
the holder of that key signed. It does not settle who holds it: a forger
supplies a key as easily as a signature. This section lets a signer say
where its keys are published, and lets a verifier confirm it. The signer
names a web origin in `signer_id`; the origin publishes a signed, chained
**key log** listing its keys and what each may sign; a verifier that has
**anchored** the origin checks an item it has accepted against that log.
The same log says which keys may authorise the decoder and reconstruct
modules a sink runs, revokes modules by digest, and bounds how long a
revoked module keeps running (§7.9).
Nothing here adds a member to a receipt or claim, or changes a signed
byte. `signer_id` is inside every signature already (§2.3, §2.4.3), so no
relay can change the origin it names. A receipt verifies exactly as
before, and one whose signer does not use this section is reported
unconfirmed, as every signer was before it.
Beside an item's own verdict, a verifier that runs this section reports
one of three levels, and **MUST NOT** report one as another:
- **confirmed** — an origin the verifier has anchored lists the item's
key, in force, with every authority the item needs (§7.4, §7.5);
- **refused** — what the verifier holds contradicts the item's claim to
speak for the identity its `signer_id` names: a valid log of that
origin, or the `signer_id` itself (§7.1);
- **unconfirmed** — the verifier holds nothing it can confirm against,
or nothing that meets the anchor an authority requires (§7.5). It
establishes nothing about who signed.
Confirmation never changes an item's verdict under §3 or §3.1. A refused
claim is still a statement its signer made; a confirmed one still asserts
only what §2.4.4 says its class asserts.
### 7.1. What a `signer_id` names
A `signer_id` — `publisher_id` in §2.3 — has one of three forms.
- **Origin-bound.** A signer that publishes its keys under a web origin
writes `signer_id` as a `did:web` DID URL with a host, no path and a
fragment:
```
signer_id = "did:web:" host [ "%3A" port ] "#" key-id
host = label 1*( "." label ) ; the last label is not all digits
label = 1-63 of a-z 0-9 "-", not beginning or ending with "-"
port = 1-65535 in decimal, no leading zero, not 443
key-id = 1-64 of A-Z a-z 0-9 "-" "_" "."
```
A host is at most 253 characters. Its **origin** is `https://`
followed by the host and, where a port is written, `:` and the port —
the ASCII serialisation of a web origin (RFC 6454 §6.2), unrelated to
the `origin` label of an `object` receipt (§2.4.2). Its **key log** is
at the origin followed by `/.well-known/wai-keys`, and nowhere else.
The suffix `wai-keys` is not yet registered in the IANA Well-Known URIs
registry; registration under RFC 8615 is to be requested. Each origin
and each key-id has one spelling: a capital letter in the host, an
escape other than `%3A`, a path, a missing fragment, the default port
written out and an IP address are not this form. A signer writes this
form only for a key its origin's log lists.
- **Ed25519 `did:key`.** The characters `did:key:z` followed by the
base58btc encoding (the Bitcoin alphabet, each leading zero byte
written as `1`) of the bytes `0xed`, `0x01` and a 32-byte key, where
decoding the text and encoding the result again gives the same text. It
names that key and nothing else: not who holds it, and not what it may
sign.
- **Label.** Any other string, including `did:key` and `did:web` strings
of neither form above. A verifier reads nothing from a label.
A verifier **MUST NOT** report a `signer_id` as the identity of an item's
signer unless it has confirmed the item (§7.4); otherwise it reports the
`signer_id` as the signer's own label.
A `did:web` resolver reads a DID document at `/.well-known/did.json` on
the same origin. An origin that publishes one **SHOULD** list in it
exactly the `active` keys of its latest key-log revision, each as an
Ed25519 `publicKeyMultibase` under the id
`did:web:<host>[%3A<port>]#<key-id>`, the key's origin-bound `signer_id`. A
verifier here **MUST NOT** take a key from it: the key log is what §7.2
signs and chains, and it governs. The `wai-keys` tool of the reference
implementation emits such a document from a log and reports every
difference between a document and a log.
### 7.2. The key log
A **log date-time** is an RFC 3339 date-time in UTC, written
`YYYY-MM-DDTHH:MM:SSZ` with `T` and `Z` in capitals, no fractional second
and a second of at most 59, so that two of them compare as strings. An
**authority** is one or more segments of `A-Z`, `a-z`, `0-9`, `-`, `_` and
`.`, joined by `/`, at most 128 characters in all (§7.5).
A key log is a JSON object with exactly two members: `kind`, which is
`"wai-key-log"`, and `revisions`, a non-empty array of revisions whose
first has `seq` 0 and each of which is the parent of the next. A revision
is a JSON object with exactly these members, under §2.4.3's encodings:
| member | type | meaning |
|---|---|---|
| `kind` | string | `"wai-key-set"`. |
| `origin` | string | The origin the log is published under, as §7.1 derives it. |
| `seq` | integer | `0` for the first revision; otherwise one more than its parent's. |
| `prev` | 32 B hex or `null` | Its parent's `revision_hash`; `null` exactly when `seq` is `0`. |
| `issued_at` | log date-time | When the origin issued it. Self-asserted; not earlier than its parent's. |
| `root` | 32 B hex | The Ed25519 key that signs this revision. |
| `next_root` | 32 B hex | The commitment (below) to the key that may sign as `root` after this one; never the commitment to `root`. |
| `keys` | array | The key entries, below. |
| `revoked_modules` | array of 32 B hex | The SHA-256 digests of the modules and pinned parameter sets the origin has revoked (§7.9), none twice. |
| `grant_max_s` | integer | The longest module grant, in seconds, the origin issues: at least 1 and at most §7.9's ceiling. |
| `status_max_age_s` | integer | How long, in seconds, a key status stays fresh: at least 1 and at most §7.9's ceiling. |
| `sig` | 64 B hex | `root`'s signature, below. |
A key entry has exactly these members:
| member | type | meaning |
|---|---|---|
| `id` | string | A key-id of §7.1. |
| `key` | 32 B hex | An Ed25519 verifying key that §3.1 step 3 accepts. |
| `may_sign` | array of authorities | The authorities (§7.5) the origin gives this key: at least one, none twice. |
| `status` | string | `"active"`, `"retired"` or `"revoked"`. |
| `until` | log date-time or `null` | For a retired key, the time after which the origin signs nothing with it; `null` for a key never retired. |
| `compromised_since` | log date-time or `null` | For a revoked key, the time from which the origin attests that another party may have held it; `null` otherwise. |
An `active` entry has both times `null`; a `retired` entry has `until` and
no `compromised_since`; a `revoked` entry has `compromised_since`, and
`until` if it was retired first. No two entries share an `id` or a `key`,
no entry's `key` is `root`, and none has `next_root` as its commitment.
```
sig = Ed25519(root, "wai:key-set\x01" || JCS(revision without "sig"))
revision_hash = BLAKE3("wai:key-set-link\x01" || JCS(revision))
commitment(K) = BLAKE3("wai:key-set-next\x01" || K)
```
JCS is §2.4.3's canonical form. Of the revisions a verifier holds for an
origin, a revision's **parent** is the one whose `revision_hash` is its
`prev`. A revision whose `prev` names no revision the verifier holds is
**unattached**: it is ignored until its parent is held. Each rule below
has a reason (§7.7). Rules 1–3 concern a revision alone, rules 4–6
compare it with its parent, and rule 7 compares revisions that share a
parent:
1. It is a JSON text that §3.1 step 1 would read, with exactly the
members above, each of the type and encoding given, and it meets the
constraints stated with the tables (`key_log_malformed`).
2. Its `origin` is the origin being checked (`origin_mismatch`).
3. Its `sig` verifies under its `root` by §3.1 step 3's rule, which also
refuses a `root` that does not decode or is of small order
(`key_log_signature_invalid`).
4. Its `seq` is one more than its parent's, and its `issued_at` is not
earlier than its parent's (`key_log_chain_break`). Its `prev` is its
parent's `revision_hash` by the definition of a parent; between
consecutive revisions of a log being anchored (§7.3), a `prev` that is
not the previous revision's `revision_hash` is also
`key_log_chain_break`.
5. Either its `root` and `next_root` are its parent's, or its `root`
differs from its parent's and has its parent's `next_root` as its
commitment (`key_log_root_not_committed`). A revision of the second
kind is a **rotation**: signing it proves possession of the committed
key. Only a rotation changes the commitment.
6. It erases and rewrites nothing: its `keys` begin with its parent's
entries, in the same order, each with the same `id`, `key` and
`may_sign`; a `status` stays, or moves from `active` to `retired` or
`revoked`, or from `retired` to `revoked`; a time, once set, does not
change; and its `revoked_modules` begin with its parent's
(`key_log_rewrites_history`). A revocation therefore stays in every
later revision, and the latest revision lists every key and every
revoked module the origin has ever listed.
7. It has one history. Of the revisions a verifier holds that share a
parent and are signed by a key that parent **authorises** — its
`root`, or the key its `next_root` commits to — exactly one is taken:
the only one, or, where there are several, the only rotation among
them, which **supersedes** the others and every revision descended
from them. Any other case is `key_log_fork`.
A verifier **holds** a revision for an origin only if the revision passes
rules 1–3 for that origin. A revision it does not hold, an unattached
revision, and a revision signed by a key its parent does not authorise
are no child of anything, and cannot stop a confirmation. Only a key the
log itself authorises can: a root that signs two histories, or a history
that breaks rules 4–6.
### 7.3. Anchors
A verifier confirms against an origin only once it has **anchored** it:
it holds a key log of that origin that passed the checks below, whose
latest revision is its **anchor**, and a record of how it came to hold
it.
- **Pinned** — the log is provisioned out of band by the relying party,
as a sink holds a trust anchor
([unidirectional-delivery](unidirectional-delivery.md) §3).
- **Fetched** — the log is retrieved from the origin's key-log location
over HTTPS, under a certificate the verifier's TLS trust store accepts
for the host (RFC 9110 §4.3.4), without following a redirect, and the
verifier records when it retrieved it (`acquired_at`). The fetch
establishes only that the origin served that log then, and only to the
verifier that fetched it: whoever relies on a confirmation built on it
relies on that verifier's record.
Anchoring checks the log in this order, reports the first failure, and
anchors nothing on a failure: the document has exactly the members
`kind` and `revisions`, with the values above (`key_log_malformed`);
each revision, in order, passes rules 1–3 (that rule's reason); the first
revision has `seq` 0 (`key_log_chain_break`); and each later revision
passes rules 4–6 against the one before it (that rule's reason).
The anchor, and every revision it descends from that the verifier holds,
is **trusted**. Later revisions may come from anywhere — the origin, a
cache, a mirror, a transparency log — because each is checked against its
parent; a revision obtained other than by pinning or from the origin's
own location is never an anchor. An anchor moves in these ways only:
- **Forward**, when the verifier anchors the origin again from its
location and the log it retrieves contains the anchor: the log's latest
revision is then the anchor. The record keeps how the origin was first
anchored — a pinned record stays pinned, since the new anchor descends
from the pinned one along the chain its root signed — and a fetched
record takes the new `acquired_at`. A pinned record also keeps the
`revision_hash` of the revision that was pinned (its **pinned
revision**), which no move changes: the hardware-class and
lawful-basis authorities are confirmed under the moved record, and a
pre-emption authority only from the pinned revision itself (§7.4
step 6a).
- **To a rotation** that supersedes (rule 7) the anchor or a trusted
revision it descends from, where the trusted revision's own child is
not a rotation. A confirmation reports such a move (§7.4 step 3). A
retrieved log whose revisions after the last one it shares with the
trusted line begin with such a rotation moves the anchor to its latest
revision, and the record is then fetched: the rotation reached the
verifier other than by pinning.
- **Anew**, by pinning, whatever the verifier held.
Otherwise an anchor never moves. A verifier that has anchored an origin
and retrieves from it a log whose latest revision is a trusted revision
other than the anchor keeps its anchor and reports `key_log_rolled_back`:
the log is an older copy, or the origin's server was rolled back. One
that retrieves a log that does none of the above keeps its anchor,
reports `key_log_replaced`, and anchors that origin anew only by pinning.
A name that has changed owner and a server held by an intruder look the
same from here.
Retrieval is the only step that touches the network, and it comes before
confirmation, never within it. Confirmation (§7.4) and the module checks
(§7.9) are functions of what they are given — the item, the revisions
the verifier holds, its anchor records, and for §7.9 the statuses it
holds and the time — so verifiers holding the same bytes reach the same
verdict, offline, on every platform. A verifier that must re-check a
confirmation later keeps those revisions and its anchor record.
### 7.4. Confirmation
A verifier confirms only an item it has accepted: a `group` receipt that
passed §3 steps 1–2, whose `object` receipts share its confirmation; a
`claim` accepted under §3.1, and the energy figure of an accepted
`reconstruct-session` claim, which is an item of its own (§7.5); a receipt of another WAI family whose signed
bytes include its `signer_pubkey` and `signer_id` and whose own
verification passed; or a module grant whose signature verified (§7.9).
Its inputs are the item's `signer_pubkey`, `signer_id`, the authorities
it needs (§7.5) and its signed time where it has one — a claim's
`issued_at`, a group receipt's `group_end`, a grant's `not_before` — the
revisions the verifier holds, its anchor records, and, where it has one,
an expected origin (§7.5). It runs these steps in order and reports the
first that fails, as a level and a reason (§7.7):
1. `signer_id` is origin-bound (§7.1). A `did:key` naming a key other
than `signer_pubkey` is refused (`signer_id_names_another_key`); any
other form is unconfirmed (`signer_id_not_origin_bound`).
2. The verifier has anchored the origin and holds the anchor
(`origin_not_anchored`).
3. The **head**. The verifier first looks along the trusted line, from
its oldest held revision to the anchor, for a trusted revision whose
trusted child is not a rotation and which has a held child that is a
rotation: the first such revision's rotation supersedes the rest of
the line, and the walk starts there, checked against its parent under
rules 4–6 (the anchor has **moved**). Otherwise it starts at the
anchor. From the start it takes, repeatedly, the child rule 7 selects,
checking it against its parent under rules 4–6, until it reaches a
revision with no child it holds. That revision is the head. The
result depends on which revisions are held, never on the order in
which they arrived. A failure is unconfirmed with the rule's reason
(`key_log_chain_break`, `key_log_root_not_committed`,
`key_log_rewrites_history`, `key_log_fork`).
4. The head lists an entry whose `id` is the `signer_id`'s key-id
(`key_not_listed`) and whose `key` is `signer_pubkey`
(`key_mismatch`).
5. The entry is not `revoked` (`key_revoked`), whatever time the item
carries: whoever holds a compromised key can write any time into what
it signs. A `retired` entry fails (`key_retired`) when the item's
signed time is later than `until`, and passes when it is not or the
item has none. A signed time that is not an RFC 3339 date-time is
later than every `until`.
6. The entry's `may_sign` holds every authority the item needs
(`authority_not_granted`).
- 6a. Where any of those authorities is **pinned-only** (§7.5), the
anchor record is pinned, with its pinned revision, and the anchor
has not moved in step 3 (`anchor_not_pinned`). For an authority
registered for pre-emption, the entry is also listed, with that
authority, in the record's pinned revision itself — the revision
the relying party provisioned, not a later one a forward move
brought ([unidirectional-delivery](unidirectional-delivery.md) §3:
a trust anchor delivered over the bearer it authorises is not
one).
7. Where the verifier expects an origin (§7.5), or one of a set of
origins (§7.9's module origins), the origin is that one, or one of
that set (`origin_not_expected`). An empty set expects no origin the
item can have.
Steps 1–3 and 6a fail as unconfirmed and steps 4–7 as refused, except
step 1's `signer_id_names_another_key`, which is refused. Step 6a is
unconfirmed, not refused: nothing contradicts the item; the verifier
lacks the anchor the authority requires.
Two times are compared as instants: each is brought to UTC by applying
its offset, carrying into the minute, hour and date as the calendar
requires and leaving the second as written, and the results are compared
as (date, hour, minute, second, fraction), the fraction as a decimal
fraction. A second of 60 thereby falls after second 59 of its minute and
before the next minute.
A confirmed item is reported with its origin, key-id and authorities; the
head's `revision_hash`; the anchor record — pinned or fetched, the
anchor's `revision_hash`, `acquired_at` if fetched, and the pinned
revision if pinned — and the rotation
it moved to, if it moved; where the entry is retired, `until`, and that
the item is untimed if it has no signed time; where an origin was
expected, where the expectation came from; and, where the item needs a
hardware-class authority (§7.5), whether its key also holds `delivery`
(`also_delivers`). A `key_revoked` refusal is reported with
`compromised_since`, and with the item's own time if it has one.
- A verifier **MUST NOT** present an item it has not confirmed as signed
by the party its `signer_id` names.
- A verifier **MUST NOT** report a refusal as unconfirmed.
- A confirmation establishes that the origin, as anchored, lists this key
with these authorities. It does not establish that the statement is
true (§2.4.4), that the key was not misused within its authority, or
that an energy figure is what its class says
([energy-measurement](energy-measurement.md) §2.2). It is the origin's
say-so, made checkable.
- A confirmation describes the log as the verifier holds it. A
revocation the verifier has not retrieved is not in it, and the head's
`revision_hash` in the report shows how current the log was.
### 7.5. Authorities: who may sign which class
The authorities an item needs are fixed by the item, never by its signer.
A confirmation needs every one of them:
| item | authorities |
|---|---|
| a `group` receipt, and the `object` receipts under it | `delivery`; and `energy/<class>` for each acquisition class an `object` receipt under it labels, where its leaf binds a label (§2.2), and for the class a labelled group payload signs for the group's total (§2.3) |
| a `claim` | its class's authority: `energy/` followed by its `energy_provenance` for `energy`, for example `energy/OnChipCounter`; `lawful-basis/declaration` or `lawful-basis/decision` by its `record`; its class name for every other class — `log-inclusion`, `soft-binding`, `binding-recovery`, `measurement`, `operating-point`, `stage`, `delivery-gap` and `reconstruct-session`. And `energy/` followed by its `energy_provenance` whenever its body carries that member, whatever its class |
| the energy figure of a `reconstruct-session` claim whose `energy` is not `null` | `reconstruct-session`, and `energy/` followed by `energy.energy_provenance` |
| a receipt of another WAI family that signs its own `signer_pubkey` and `signer_id` | `receipt`; and `energy/<class>` for each class its figures carry a label for |
| a module grant (§7.9) | `module/decoder` or `module/reconstruct`, by its `form` |
A class registered in §2.4.4 registers its authority here with it, and a
family that needs a narrower authority than `receipt` registers one here.
An authority a verifier does not know matches nothing. Because an energy
authority is read from the `energy_provenance` member wherever it
appears, a class that restates a figure with its acquisition class needs
that class's authority without this table naming the class.
**Pinned-only authorities.** The hardware-class energy authorities —
`energy/CalibratedInstrument`, `energy/OnChipCounter` and the legacy
`energy/HwShunt` — the lawful-basis authorities —
`lawful-basis/declaration` and `lawful-basis/decision` — and any
authority registered for pre-emption are confirmed only under a pinned
anchor that has not moved (§7.4 step 6a). Every other authority is
confirmed under a pinned or a fetched anchor, and every confirmation
reports which.
**Expected origin.** A verifier **MAY** expect an origin for an item: one
its relying party names — the service a sink used, the publisher it
subscribed to — or the **subject's origin**: the origin under which it
has confirmed, with the authority `delivery`, the group receipt that
carries a claim's subject (for a claim whose `subject_type` is `object`),
or under which it has confirmed the receipt a claim's subject names
(`subject_type` `receipt`). It reports which. §7.9 expects one of the
sink's module origins. A `reconstruct-session` claim is a sink's statement
about an object a publisher delivered, so its subject's origin is not the
sink's: a verifier expects an origin for a session only where its relying
party names one. A session signed under a `did:key` is unconfirmed (§7.4 step
1), which is the normal case for a sink's ephemeral key.
§2.4.4's *signed by* column names the party each class expects. Which of
its keys is that party is the origin's decision, and its log records it:
- **Delivery.** A group receipt confirmed under an origin was signed with
a key that origin gives `delivery`.
- **Energy.** `energy/<class>` lets a key state figures of that class and
no other, so an origin can confine what a metering key may claim. No
authority raises a figure's class or grade, or adds to what a class
establishes ([energy-measurement](energy-measurement.md) §2, §2.2). An
origin **SHOULD** give the hardware-class authorities only to a key
held by the component that signs the reading, and not to a key it also
gives `delivery`. Beside a confirmed item that needs a hardware-class
authority, a verifier reports whether its key also holds `delivery`
(`also_delivers`). A group receipt whose objects' leaves bind a
hardware-class label, or whose labelled total carries one, is signed by
one key for delivery and for the figures, so `also_delivers` is true
there by construction, and is reported as such. An `energy` claim
confirmed under the subject's origin was made under the publisher's
authority, by its own key or by a metering key its log lists.
- **Lawful basis.** A declaration confirmed with
`lawful-basis/declaration` is its origin's; §2.4.6's check of a
decision against it then establishes that the decision matches a
declaration made under that origin. A verifier **MUST NOT** report a
decision as matching a service operator's declaration unless it has
confirmed the declaration, and reports the declaration's level beside
the decision's.
- **Measurement and operating point.** A `measurement` claim needs
`measurement` and the energy authority of the class it restates; an
`operating-point` claim needs `operating-point` and that energy
authority too. A report of a hardware-class figure is therefore
confirmed only under a pinned anchor, and is reported with
`also_delivers`. §2.4.8's check 5 already requires a `measurement`
claim's key to be the key that signed the figure; confirmation adds
that the figure's origin gave that key the authority to report on it.
An origin can give `operating-point` to the party that records
requests and commitments without letting it state figures of its own.
- **Stage and delivery gap.** `stage` lets a key sign what its holder observed
of a stage set (§2.4.10), and `delivery-gap` what a relay did not forward
(§2.4.11). Neither is pinned-only, and neither is `delivery`: an origin gives
`stage` to the sinks it speaks for and `delivery-gap` to its relays, and a key
that holds `delivery` alone signs neither.
- **Reconstruct session.** `reconstruct-session` lets a key sign what a sink
reconstructed (§2.4.12). It is not pinned-only and it is not `delivery`: an
origin gives it to the sinks it speaks for. A session's energy figure is a
second item, since the session's other members stand without it: it needs
`reconstruct-session` and the energy authority of its class, every one of
them, and is reported with `also_delivers` where that class is a hardware
class. A verifier reports the session's confirmation and its figure's side by
side, and **MUST NOT** report the figure as its origin's unless it confirmed the
figure. A key that holds `reconstruct-session` and not the figure's energy
authority confirms a session whose figure is refused (`authority_not_granted`);
under a fetched anchor, a hardware-class figure is unconfirmed
(`anchor_not_pinned`) beside a confirmed session. The origin then stands behind
the session, and not behind its figure.
- **Log inclusion.** A confirmation says under whose authority the log's
evidence was attached. It does not check `log_receipt`: §2.4.7 is
unchanged.
- **Soft binding.** A declaration confirmed with `soft-binding` is its
origin's; confirmed under the subject's origin, it is the publisher's.
§3.2 step 3 and §2.6.3 check 4 are unchanged: a recovery still needs
the declaration's key to be the key of a verified delivery receipt.
- **Modules.** `module/decoder` lets a key grant a decoder module, or a
parameter set pinned by an envelope, for the capabilities the grant
names; `module/reconstruct` lets it grant a reconstruct module (§7.9).
Neither is pinned-only; §7.8 says what a module authorised under a
fetched anchor is.
- **Pre-emption** ([unidirectional-delivery](unidirectional-delivery.md)
§3), relay attestation (§4.3) other than a `delivery-gap` claim, and
reconciliation have no class and so no authority. When one is registered for pre-emption it is pinned-only,
and the pinned revision itself must list the key with it (§7.4
step 6a): a fetched anchor is authority taken on first use, which that
section forbids for an instruction that displaces what a person chose.
### 7.6. Rotation and revocation
- **A signing key** is rotated by listing its successor under a new `id`
before the successor signs, and retiring the old key with an `until`
no earlier than the last item it signed. Items the old key signed stay
confirmable. A claim chain (§2.4.3) is per key: the successor starts
its own, and §3.1 step 5 is unchanged; the log, not the chain, connects
the two keys. An entry's authorities never change (rule 6): an origin
that gives a key another authority lists the key's successor with it.
- **A compromised key** is revoked with `compromised_since`, and every
item under it is refused from then on (§7.4 step 5): every receipt,
claim and module grant it signed, whatever time the item carries. An
item's own time cannot restore it, and this section defines no
independent time that could.
- **A module or a pinned parameter set** is revoked by adding its digest
to `revoked_modules` (§7.9). A revocation, like a key's, stays in every
later revision.
- **The root** is pre-committed: each revision commits to the next root,
and only a rotation changes the commitment. Whoever steals the root key
in use can sign revisions that add keys, but can neither rotate nor
change the commitment; the owner's rotation, signed with the committed
key, supersedes what the thief signed from the point where the two
histories fork (§7.2 rule 7). The committed key is the recovery key and
**SHOULD** be held apart from the root in use. An origin that loses
control of the committed key loses its log: verifiers anchored on it
confirm nothing new, and anchoring again means pinning.
- **Registration.** An origin **SHOULD** register each revision with a
Transparency Service, as it registers group receipts
([transparency](transparency.md) §3), so that a fork is visible to any
Relying Party that checks the log. Registration anchors nothing
([transparency](transparency.md) §6).
### 7.7. Reasons
| step | level | reason | raised when |
|---|---|---|---|
| 1 | unconfirmed | `signer_id_not_origin_bound` | `signer_id` is not of §7.1's origin-bound form |
| 1 | refused | `signer_id_names_another_key` | `signer_id` is a `did:key` of a key other than `signer_pubkey` |
| 2 | unconfirmed | `origin_not_anchored` | the verifier has no anchor for the origin, or does not hold it |
| 3 | unconfirmed | `key_log_chain_break` | a child's `seq` or `issued_at` does not follow its parent's |
| 3 | unconfirmed | `key_log_root_not_committed` | a child signed by its parent's `root` has another `next_root` |
| 3 | unconfirmed | `key_log_rewrites_history` | a child drops or reorders an entry, changes its key or authorities, moves a status back, changes a time once set, or drops a revoked module |
| 3 | unconfirmed | `key_log_fork` | a parent has several authorised children and not exactly one rotation among them, or a trusted revision has several superseding rotations |
| 4 | refused | `key_not_listed` | the head lists no entry with the key-id |
| 4 | refused | `key_mismatch` | the entry's key is not `signer_pubkey` |
| 5 | refused | `key_revoked` | the entry is revoked |
| 5 | refused | `key_retired` | the entry is retired and the item's signed time is later than `until` |
| 6 | refused | `authority_not_granted` | the entry's `may_sign` does not hold every authority the item needs |
| 6a | unconfirmed | `anchor_not_pinned` | the item needs a pinned-only authority, and the anchor is fetched, has moved, or has no pinned revision, or, for pre-emption, the pinned revision does not list the key with it |
| 7 | refused | `origin_not_expected` | the origin is not the one the verifier expects, or not one of the set it expects |
| §7.3 | — | `key_log_malformed` | a log or a revision is not of §7.2's form |
| §7.3 | — | `origin_mismatch` | a revision names another origin |
| §7.3 | — | `key_log_signature_invalid` | a revision's signature does not verify under its `root` |
| §7.3 | — | `key_log_chain_break` | the first revision's `seq` is not 0, or a revision does not follow the one before it |
| §7.3 | — | `key_log_root_not_committed` | a revision's `root` is neither the previous one's nor committed to by it, or its `next_root` changes without a rotation |
| §7.3 | — | `key_log_rewrites_history` | as at step 3, between consecutive revisions of the log being anchored |
| §7.3 | — | `key_log_rolled_back` | a log retrieved from an anchored origin ends at a trusted revision older than the anchor; the anchor is kept |
| §7.3 | — | `key_log_replaced` | a log retrieved from an anchored origin neither contains the anchor nor supersedes a trusted revision by a rotation; the anchor is kept |
The reasons of §7.9's checks are listed there.
### 7.8. Security considerations
- **First use.** A fetched anchor is as good as the first retrieval: the
domain name system, the certificate accepted, and the origin's server
at that moment. Whoever controls any of them then can serve a log of
its own to a verifier anchoring for the first time. Once anchored, a
verifier is not exposed to that again, since every later revision must
descend from its anchor (§7.3). A relationship that cannot bear the
risk pins its anchor; the authorities §7.5 makes pinned-only are those
for which WAI does not leave that choice open.
- **Modules on first use.** A module authorised under a fetched anchor is
code accepted on first use: whoever served the first retrieval chose
which keys may grant it. A sink that cannot bear that pins the anchors
of its module origins. Every module outcome reports the anchor's path
(§7.9).
- **Which origin grants a module.** Anchoring an origin to confirm its
receipts does not make it a source of modules: a grant is held only
from one of the sink's module origins (§7.9), so an origin anchored on
first use for some other publisher's receipts authorises nothing a sink
runs on this envelope.
- **Revocation from any anchored origin.** A digest revoked in the head
of any origin a sink has anchored is not held, whichever origin grants
it (§7.9 check 5), so a publisher's revocation cannot be undone by
another origin's grant. Any anchored origin can therefore make a
module fall back; it cannot make one run.
- **Withheld revisions.** An origin, or anyone between it and a verifier,
can withhold its latest revisions and with them a revocation. How
often a verifier retrieves the log is its own policy; the head named in
a report shows what it rested on, and registration (§7.6) makes a
withheld revision's existence checkable. For modules, §7.9's status
bounds how long a withheld revocation can go unnoticed.
- **A rolled-back server.** A retrieval that returns an older log is
reported (`key_log_rolled_back`), never taken as the anchor; a status
that names a revision older than the head the sink holds is not
current (§7.9).
- **A stolen root.** Until the owner's rotation reaches a verifier, a
verifier holding only the thief's revisions confirms what the thief
adds. When the rotation arrives it supersedes them, and every key they
added is no longer listed.
- **Injected revisions.** A revision that fails rules 1–3, is unattached,
or is signed by a key its parent does not authorise is no child of
anything and cannot stop a confirmation; only the origin's own root and
committed key can. A status names a revision on the head's line or is
not held (§7.9), so one signed under an injected revision changes
neither a module outcome nor its reason.
- **Retired keys and untimed items.** Many receipt families carry no
signed time. An item without one, under a retired key, stays
confirmable for as long as the key is not revoked, and is reported as
untimed with the key's `until`. An origin that cannot vouch for a
retired key's past use revokes it.
- **Clocks.** §7.9's freshness and grant windows are measured on the
sink's clock. A sink whose clock can be set back runs a module for as
long as the clock is set back; a sink with no clock it trusts treats
every status as stale and runs no module. The kill latency of §7.9 is
a bound on a trusted clock and on nothing else.
- **Published private keys.** A key whose private half has been
published attests nothing, and **MUST NOT** be listed in a key log. Every
key WAI's own corpora, pins and reference meters sign with has a
published seed, and every such seed is one byte repeated 32 times. The
reference implementation exposes the public keys of all such seeds
(`key_log::published_test_keys`); its `wai-keys` tool refuses to put one
in a log unless told the log is a test log, and reports one it finds
in a log it verifies. Every vector of §5.8 is a test log, and every
anchored verdict there carries that report.
- **A name with a new owner.** Whoever controls a domain next can publish
a new log under its origin. It does not descend from an existing
anchor, so an anchored verifier keeps its anchor and reports
`key_log_replaced`; a verifier anchoring for the first time sees only
the new log, in which items signed under the old one are not listed. A
verifier that must keep confirming them keeps the revisions it
confirmed them against (§7.3).
- **Retrieval reveals interest.** Retrieving a log tells the origin that
someone is checking its signatures. A verifier retrieves the log, not a
key per item, and may take later revisions from any source.
- **Separation.** A root signs revisions and statuses and nothing else:
no entry may be a root or the committed key (§7.2), and a revision's,
a status's and a grant's signing domains are distinct from every
receipt's and claim's.
### 7.9. Decoder and reconstruct modules
A **module** is code a sink obtains apart from an envelope and runs to
decode a capability (a **decoder** module) or to rebuild a
reconstruction from what a decoder produced (a **reconstruct** module) —
for example a WebAssembly module. A parameter set an envelope pins by
its digest — the prior a neural capability decodes against
([prior-carriage](prior-carriage.md)) — determines a decode the way code
does. It is revoked the same way, by its digest, at every sink that holds
a key log, and authorised the same way, as a decoder module, by a sink
that holds pinned parameter sets to this section. Running a module
requires this section; holding pinned sets to it is the relying party's
choice per capability ([SPEC](../SPEC.md) §4). A sink that holds a
capability's pinned sets to this section runs the checks below as
[SPEC](../SPEC.md) §3.1's check 5, before it looks for the set among
what it holds; a sink that holds a key log but not the capability's sets
reads only check 5's revocation lists (*Pinned sets and several grants*,
below).
**Identity.** A module's **digest** is the SHA-256 of its bytes exactly as
the sink instantiates them, and a pinned parameter set's is its pin's
`sha256`, the digest of the set in the pin's `form` ([SPEC](../SPEC.md)
§3.1); both are written as 64 lowercase hexadecimal digits. For a pinned
set the pin's digest stands for the bytes' at check 2, and SPEC §3.1's
checks 6–8 then require the bytes the decode reads to have it. A sink that
instantiates other bytes than it hashed runs code nothing authorised.
**Grant.** An origin authorises a module by a signed **grant**, a JSON
object with exactly these members, under §2.4.3's encodings:
| member | type | meaning |
|---|---|---|
| `kind` | string | `"wai-module-grant"`. |
| `origin` | string | The origin under whose key log the grant is checked: the one its `signer_id` names (§7.1). |
| `module` | 32 B hex | The module's digest. |
| `form` | string | `"decoder"` or `"reconstruct"`. |
| `capabilities` | array of terms | The capabilities the module may decode or reconstruct: at least one, none twice. |
| `not_before` | log date-time | When the grant takes effect. |
| `not_after` | log date-time | When it lapses; not earlier than `not_before`. |
| `signer_pubkey` | 32 B hex | The granting key. |
| `signer_id` | string | The granting key's `signer_id` (§7.1). |
| `sig` | 64 B hex | `Ed25519(signer, "wai:module-grant\x01" \|\| JCS(grant without "sig"))`. |
**Status.** A key status is a cacheable signed pointer to an origin's
head, re-issued on each heartbeat, so the key log does not grow with
every heartbeat. It is a JSON object with exactly the members `kind`
(`"wai-key-status"`), `origin`, `head` (32 B hex, a `revision_hash`),
`seq` (that revision's `seq`), `issued_at` (a log date-time) and `sig`,
`Ed25519(root, "wai:key-status\x01" || JCS(status without "sig"))`,
where `root` is the `root` of the revision `head` names. A sink **holds**
a status only if it has that form, names the grant's origin, and names a
revision on the **head's line** — the head §7.4 step 3 reached, or a
revision the sink holds that the head descends from by `prev` — with its
`seq`, under whose `root` the signature verifies. A status that is not
held is ignored, as an injected revision is (§7.2).
Over HTTP, a status and an origin's grants are served where
[http-delivery](http-delivery.md) §8.4 states.
**Module origins.** A sink holds a module only from an origin it accepts
module grants from for the envelope and capability at hand — its
**module origins**, each recorded with where it came from:
- **relying-party** — an origin the sink's relying party names as a
source of modules for that capability;
- **subject** — the origin under which the sink confirmed, with the
authority `delivery`, the group receipt that carries the envelope: its
publisher.
A sink with no module origin for an envelope holds no module for it. An
origin the sink anchored for any other purpose — to confirm some other
publisher's receipts, say — is not thereby one of them.
**Ceilings.** WAI sets two normative ceilings: `grant_max_s` is at most
**86400** seconds and `status_max_age_s` at most **3600** seconds. A key
log declaring more is malformed (§7.2 rule 1).
**The checks.** A sink runs a module, or, where it holds pinned
parameter sets to this section, decodes against one, only if every
check below passes. It runs them in this
order, before it instantiates the module, and reports the first that
fails:
1. The grant has the form above, its `signer_id` is origin-bound and
names the grant's `origin`, and its `sig` verifies under its
`signer_pubkey` by §3.1 step 3's rule (`module_grant_invalid`).
2. The bytes' digest is the grant's `module` (`module_digest_mismatch`).
3. The grant passes §7.4 as an item needing `module/<form>`, with its
`not_before` as its signed time and the sink's module origins
expected: step 7 refuses an origin that is not one of them
(`origin_not_expected`). A failure is reported with §7.4's own
reason. A revoked granting key therefore fails every grant it signed,
whatever the grant's times.
4. The sink holds a status for that origin naming the head §7.4 step 3
reached; of those, the latest is **fresh**: its `issued_at` is not
later than `now` and at most that head's `status_max_age_s` seconds
earlier (`module_status_stale`). A sink that holds statuses but none
naming that head — each names an older revision of the head's line —
fails with `module_status_not_descended`.
5. The grant's `module` is not in the `revoked_modules` of that head,
nor of the head §7.4 step 3 reaches from any other anchor the sink
holds — or, where none can be reached, of that anchor revision
(`module_revoked`).
6. `not_before` ≤ `now` (`module_grant_not_yet_valid`) and `now` ≤
`not_after` (`module_grant_expired`).
7. `not_after` − `not_before` is at most the head's `grant_max_s`
(`module_grant_too_long`).
8. The capability being decoded is one of the grant's `capabilities`
(`module_capability_not_granted`).
`now` is the sink's trusted clock, as a log date-time. The checks are a
function of the module's bytes, the grant, the sink's module origins,
the revisions, anchors and statuses the sink holds, and `now`, and read
no clock and no network of their own. A sink runs them before it instantiates a module, and again
before it keeps using an instance once the status or the grant it relied
on is no longer current; an instance that fails is discarded.
**Fail closed.** Every failure — no network, no status, a stale status, a
clock the sink does not trust, an expired grant — means the module is
**not held**. A sink with no trusted clock has no `now`: no status is
fresh, and every grant is treated as expired. A grant that lapses without
renewal is not held from the moment it lapses.
**Fallback, never inert.** A module or pinned parameter set that is not
held makes its capability unsupported for that envelope at that sink, and
dispatch continues to the declared fallback or a sibling rendition
([SPEC](../SPEC.md) §4). A sink **MUST NOT** report a file inert because
of module authorisation while a fallback or sibling it can decode without
that module remains. An encoder emitting a capability whose decode needs
a module **MUST** declare such a fallback. A pinned parameter set this
section does not hold is refused at SPEC §3.1's check 5: `prior_revoked`
where its digest is revoked (below), and `prior_not_authorised` for every
other failure.
**Pinned sets and several grants.** For a pinned set, a sink runs the
checks with each grant of form `decoder` it holds; a grant of form
`reconstruct` names no set. The set is held when one grant passes every
check. When none does, the sink reports `module_revoked` if any grant
reached check 5 and failed it, or, failing that, if the set's digest is in
the `revoked_modules` of the head the sink reaches from any anchor it
holds — or, where none can be reached, of that anchor revision — with
that anchor's origin. A revocation is of the digest, whichever grant
names it, whichever check a grant fails first, and whether or not the
sink holds a grant at all. Otherwise the sink reports the check and
reason of the grant that passed the most checks; a sink that holds no
such grant reports that.
A sink that holds a key log but does not hold the capability's pinned
sets to this section reads only those revocation lists: it refuses a set
whose digest they hold (`module_revoked`, and SPEC §3.1's
`prior_revoked`), and runs none of checks 1–8 otherwise. Both apply as
well to the set a sink decodes an envelope that pins none against (SPEC
§4), identified by each digest a pin over it could name: it is refused
when any of them is revoked, and, for a capability held to this section,
unless one of them is held.
**Kill latency.** The longest a module whose grant is no longer renewed,
or whose digest or granting key is revoked, keeps running at a sink that
follows this section is
```
K = grant_max_s + status_max_age_s
```
with both values from the head the sink holds, measured on the sink's
clock and bounded by the ceilings. `K` is a normative parameter of the
origin's log: an origin states it by the two members it declares, and a
sink reports it with every module it runs. `K` bounds what the granting
origin does; a revocation in another anchored origin's head (check 5)
takes effect once the sink holds that head.
**Reporting.** A module that runs is reported with the granting key's
origin and key-id, the head's `revision_hash`, the anchor record's path,
where the module origin came from, and `K`. One that does not is
reported with the check and the reason — for a digest another origin
revokes, that origin — and with what dispatch did instead: fell back, or
left the file inert because no fallback was declared.
`key_log::module_check` in `wai-rs` implements these checks. The
reference dispatcher runs SPEC §3.1's check 5 when the sink is
configured with a key log (`key_log::SinkKeyLog`, given to
`pin::verify_with`, to `int_payload::resolve`, which runs it for a pinned
decode and over the files an unpinned integer decode reads, or to
`neural::ModelRegistry::with_key_log`, which also runs it before an
unpinned decode, over the bytes that decode then reads, read once, by
every digest a pin over them could name): the revocation
lists for every capability a pin applies to, and these checks for the
capabilities a sink holds to this section. A sink configured with no key
log runs neither. The reference dispatcher runs no module obtained
apart from an envelope, and `wai-web` and the C ABI take no key log
([SPEC](../SPEC.md) §4).
Sub-delegation, where one grant authorises another, is out of scope. A
module is revoked by its digest or through the key that granted it.
| check | reason | raised when |
|---|---|---|
| 1 | `module_grant_invalid` | the grant is not of the form above, its `signer_id` does not name its `origin`, or its signature does not verify |
| 2 | `module_digest_mismatch` | the bytes' SHA-256 is not the grant's `module` |
| 3 | §7.4's reason | the granting key is not confirmed with `module/<form>` under one of the sink's module origins (`origin_not_expected` where its origin is not one) |
| 4 | `module_status_stale` | no status is held, or the latest naming the head is older than `status_max_age_s` or later than `now`, or there is no trusted clock |
| 4 | `module_status_not_descended` | statuses are held, and none names the head the sink reached |
| 5 | `module_revoked` | the head's `revoked_modules`, or another anchored origin's, holds the digest |
| 6 | `module_grant_not_yet_valid` | `now` is earlier than `not_before` |
| 6 | `module_grant_expired` | `now` is later than `not_after` |
| 7 | `module_grant_too_long` | the grant's window exceeds the head's `grant_max_s` |
| 8 | `module_capability_not_granted` | the capability is not one the grant names |