# 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); and a binary form of the group and object receipts and of a group's receipts together (§2.7), whose group is its canonical bytes. - 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). `group` and `object` receipts also have the binary form of §2.7. #### 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:` or `blake3:`, 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:@` — `` 1 to 64 of `a`–`z`, `0`–`9`, `_` and `-`, `` 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. ### 2.7. Binary form Group and object receipts have, beside their JSON form, a **binary form**: the same members, as bytes, with no member names and no hexadecimal. A carrier uses it where the receipts' size matters, as the receipts track of [moq-streaming-format](moq-streaming-format.md) §4.1 does. Integers are big-endian, as in §2.2 and §2.3, and a *string* is `u32_be(len) ‖ UTF-8`, as there. Claims (§2.4.3) have no binary form. The binary form is a second spelling of the JSON members, not a second receipt: reading it yields the members of §2.4.1 and §2.4.2, each with the value the JSON form gives it, and a verifier then reads and checks those members by §3 unchanged. It is canonical: a receipt has one binary form, with no optional encoding, so a writer reproduces the bytes it read. A receipt whose JSON members a reader of §3 step 1 or step 4 refuses has no binary form. #### 2.7.1. A group receipt ``` group_bin = canonical_bytes(G) // the labelled form | canonical_bytes(G) || u8(has_parent) || parent[32] // the legacy form ``` `canonical_bytes(G)` is §3 step 3's: the group's §2.3 signing payload, in the form it was signed in, then its 64-byte `sig`. The labelled payload signs the parent link and carries it; the legacy one does not, so a legacy group adds it: `1` and `parent_receipt_hash`, or `0` and 32 zero bytes. The first 19 bytes, the signing domain, give the form: `moq-jwp:group-sign\x01` the legacy form, `moq-jwp:group-sign\x02` the labelled form (`"sign_version": 2`). Read in that order, each field's length is known: the strings are length-prefixed, and a class is a tag byte and the evidence that tag carries ([energy-measurement](energy-measurement.md) §2.7). The fields are the members `track_alias`, `group_id`, `root_hash`, `leaf_count`, `joules_total`, `group_start`, `group_end`, `signer_pubkey`, `signer_id` and `sig`; in the labelled form the class is `energy_provenance` and its evidence members (tag `0`: none, the unmetered marker), the coverage byte is `energy_coverage` (`0` `Whole`, written as no member beside tag `0`; `1` `Partial`), and the parent link is `parent_receipt_hash` (`null` beside `0`). A group's link hash (§3 step 3) is computed over the bytes received, the first `len(canonical_bytes(G))` of `group_bin`, with nothing re-serialised. #### 2.7.2. An object receipt ``` object_body = u64_be(object_id) || content_hash[32] || u64_be(joules_micro) || u32_be(len(origin)) || origin_utf8 || u8(n) || siblings[32 × n] // merkle_proof.siblings, in order || u64_be(direction_bits) || u64_be(leaf_index) || u8(leaf_version) // 1 the legacy leaf, 2 the labelled leaf || class // leaf_version 2 only: tag and evidence object_bin = u64_be(track_alias) || u64_be(group_id) || object_body ``` `leaf_version` `1` is the legacy leaf: no `leaf_version` member and no class members. `2` is the labelled leaf, `"leaf_version": 2`, and the class that follows is its label: tag `0` explicitly unmetered (no class members), any other the class and its evidence (§2.4.2). An object receipt standing alone is `object_bin`; inside a group's receipts (§2.7.3) it is `object_body`, its `track_alias` and `group_id` the group's. #### 2.7.3. A group's receipts ``` receipts_bin = "WJR1" // magic, 4 bytes || group_bin || u32_be(count) || object_body × count ``` One record holds a group receipt and object receipts of that group, each with the group's `track_alias` and `group_id`, in the order the carrier lists them. #### 2.7.4. Reading A reader reads exactly the bytes it is given, in the order above, and refuses them as `malformed` at the first of these that holds: at §3 step 1 for the magic, the group, the count and the record's end; at §3 step 4 for an object. - The magic is not `WJR1`, or the signing domain is neither of §2.3's. - The bytes end before a field the layout requires, or bytes follow the last field (the last object, or a group or object read on its own). - A string is not UTF-8. - A class's tag is not one energy-measurement §2.7 defines, or its evidence does not read under that tag: an `OnChipCounter` filtering byte above `2`, a calibration reference that is not UTF-8. - A coverage byte is not `0` or `1`; a parent flag is not `0` or `1`, or is `0` and the 32 bytes after it are not zero; `leaf_version` is not `1` or `2`. Whatever reads is the JSON form's members, and every rule of §2.4.1, §2.4.2 and §3 applies to them as it does to the JSON form: a class that reads but breaks §2.4.1 is `group_label_invalid`, a coverage beside the unmetered marker is `group_label_invalid`, a label that breaks §2.4.2 is `object_label_invalid`, an object at another position is `proof_position_mismatch`. --- ## 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), except for a group whose `group_id` lies inside the gap a join's anchor left below it (*Joining*, *History*): there a `null` parent is the chain's true start, and the group is `parent_not_held` until the group above it names its link hash; 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. Over HTTP the missing receipt is fetched by the link hash the group names ([http-delivery](http-delivery.md) §8.1). 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. A group with a `null` parent inside a join's gap is not a second chain head: the gap has no lower end, the group may be the chain's first, and it is held as history until the group above it links back to it, as a fetch from the chain's first group delivers it, in ascending order. 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; a joiner's fetch from the chain's first group, whose `null` parent is its true start and is held as history (`chain_join_fetch_from_group_0`), and, once that gap is closed, a second group with a `null` parent and a second child of the first, each `chain_break` (`chain_join_second_start_after_the_fill`); 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). ### 5.12. The binary form (`jwp-conformance/`) Procedure `binary`: bytes given as a group's receipts, a group or an object standing alone (§2.7). A vector that reads states the members of each receipt in the JSON form and, for a group, its link hash over the bytes as received; a verifier reads the bytes by §2.7.4, reads the members by §3 step 1 and step 4, and writes them again, which must give the bytes it was given (`binary_receipts_live_one_object`, the shape a live track's receipts take; `binary_receipts_mixed_legacy`, the legacy parent flag, five objects with their proofs and every label tag; `binary_receipts_labelled_partial`; `binary_receipts_no_object`; `binary_group_legacy_with_parent`; `binary_object_alone`). The others are refused `malformed` at their step: another magic, an unknown signing domain, a record that ends inside its group, a byte after the last object, a count past the objects, a parent flag of 2 and a flag of 0 beside a parent, a coverage byte of 2, a `signer_id` that is not UTF-8, a `leaf_version` of 3, a class tag and a filtering byte energy-measurement §2.7 does not define, and a legacy group without its parent; and one reads and is refused by §3 step 1, a coverage of `Partial` beside the unmetered marker (`group_label_invalid`). `wai_jwp_vectors` and `jwp_verify.py` give every vector its outcome, and `joule-moq` reads and writes every one. --- ## 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:[%3A]#`, 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), of the room grants and viewers it has revoked ([live-rooms](live-rooms.md) §5), and of the consent records withdrawn in its rooms ([capture-binding](capture-binding.md) §8.3), 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/` 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/` for each class its figures carry a label for | | a module grant (§7.9) | `module/decoder` or `module/reconstruct`, by its `form` | | a room descriptor or room grant ([live-rooms](live-rooms.md) §3, §4) | `live/room` | | an upload receipt of a take ([capture-binding](capture-binding.md) §6) | `capture/store` | | a per-hop session record ([live-metering](live-metering.md) §3) | `live/relay`, `live/compositor` or `live/ingest`, by its role; and `energy/` for its figure's class where it is metered | | a room-session receipt (live-metering §4) signed by a key other than the room authority | `live/meter`; and, from any signer, `energy/` for each class it totals | | a steering policy or steering decision ([energy-steering](energy-steering.md) §3.5) signed by a key other than the room authority | `live/steer`; and, from any signer of a decision, `energy/` for each class among its figures | | a watermark session record ([forensic-watermark](forensic-watermark.md) §5) | `live/watermark` | 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/` 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. - **Rooms.** `live/room` lets a key act as a room authority: sign room descriptors and the grants under them ([live-rooms](live-rooms.md) §5). It is not pinned-only, and it is not `delivery`. - **Stores.** `capture/store` lets a key act as a store a room's takes are uploaded to: sign the upload receipts of a take ([capture-binding](capture-binding.md) §6.4). It is not pinned-only, and it is not `delivery`. - **Watermark edges.** `live/watermark` lets a key act as an edge that chooses forensic A/B variants for a room's viewers: sign the session records of what it embedded ([forensic-watermark](forensic-watermark.md) §5.6). It is not pinned-only, and it is not `live/relay` or `delivery`. - **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. - **A room grant or a viewer** is revoked by adding the grant's digest, or the viewer's tag, to `revoked_modules` ([live-rooms](live-rooms.md) §5); **a consent record** is withdrawn by adding its digest there ([capture-binding](capture-binding.md) §8.3). The list holds digests of each kind under one rule: append-only, none twice. - **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/
`, 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/` 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 |