Conflicts, both sides kept: - joule-moq-relay metering.rs's module text and live-metering §8: #463's self-metered relay (its counter, OnChipCounter or the unmetered marker) and #475's hop links (counted in the track entries, neither subscriber sessions nor in the ledger), and both tests' account of the corpus record. - joule-code and wai CHANGELOGs: #475's entries on top. Co-Authored-By: Claude Opus 5.5 <[email protected]>
4039 lines
260 KiB
Markdown
4039 lines
260 KiB
Markdown
# 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`). A post-quantum signature beside each record's Ed25519 one — a hybrid
|
||
> group, claim, key entry or key-log root — is [pq-signatures](pq-signatures.md)'s (feature
|
||
> `pq_signatures`); a record without one is read and verified as this text says.
|
||
|
||
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`, `consumption`,
|
||
`transmission`, `reconciliation` (§2.4.5–§2.4.15) — 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, over one
|
||
object or over a span of a track (§2.6.4) — and the
|
||
**ancestor rule** (§3.2): a receipt reached through it attests the
|
||
object, or the span, 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.
|
||
|
||
A group signed by a hybrid key is signed over the labelled payload's third version,
|
||
`"moq-jwp:group-sign\x03"` ([pq-signatures](pq-signatures.md) §6.2): the labelled
|
||
payload's fields after its domain, then the publisher's ML-DSA-65 key, signed by both halves
|
||
of the key, and its canonical bytes (§3 step 3) end with both signatures. A verifier that
|
||
predates it refuses it as `malformed`: in the JSON form for the members it does not define,
|
||
in the binary form for the signing domain (§2.7.4).
|
||
|
||
### 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` | The link hash (§3 step 3: BLAKE3, domain-separated, over the canonical bytes) of the previous group receipt under this publisher key and `track_alias`; `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`), but `3` beside the hybrid members of [pq-signatures](pq-signatures.md) §6.2 (`pq_key`, `pq_sig`), which a verifier that implements that extension reads. |
|
||
| `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 but two. The three hybrid
|
||
members of [pq-signatures](pq-signatures.md) §3.1 (`pq_suite`, `pq_key`, `pq_sig`)
|
||
are carried all or none; a verifier that does not implement that extension refuses
|
||
them as `unknown_member`. A `measurement` claim's `fabrics` (§2.4.8) is absent when
|
||
the claim does not divide its figure by fabric and is never `null`, so a claim
|
||
sealed before the member existed keeps its bytes and its link hash. Every other
|
||
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 |
|
||
| `soft-binding-span` | `claim` | `object` (the span's anchor) | §2.6.4 | the track'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 |
|
||
| `consumption` | `claim` | `object` (the first object it names) | §2.4.13 | the agent that consumed the objects: its key, named by its `did:key` |
|
||
| `transmission` | `claim` | `receipt` (the group transmitted) | §2.4.14 | the group's publisher: the key that signed the group |
|
||
| `reconciliation` | `claim` | `receipt` (the group reconciled) | §2.4.15 | the sink that received the objects, never the publisher |
|
||
|
||
Rules:
|
||
|
||
- A verifier **MUST** reject a `claim` whose `class` it does not know,
|
||
and **MUST** report the rejection rather than skip the claim.
|
||
Silently discarding a class a signer thought load-bearing is the
|
||
failure this rule exists to prevent. The rejection is of that claim
|
||
only: a delivery receipt for the same subject verifies or fails on
|
||
its own.
|
||
- `delivery` is reserved to `group` and `object`. A `claim` naming it
|
||
**MUST** be rejected; delivery is stated only in the §2.3 signed
|
||
payload.
|
||
- The class is read from `class` and **MUST NOT** be inferred from the
|
||
body's shape. A body **MUST** carry exactly its class's members; a
|
||
missing or additional member is a rejection, not an extension point.
|
||
A new class, or a new member, is a new registration here.
|
||
- A `claim` carries the authority of its signer and no more. `class`
|
||
says what is asserted, never that it is true. A claim signature is
|
||
not evidence about delivery, and a delivery signature is not
|
||
evidence about a claim. What that authority is — which web origin
|
||
stands behind the key, and whether it gave the key this class — is
|
||
§7's to establish. The *signed by* column above names the party each
|
||
class expects; §7.5 is how a verifier checks it.
|
||
- A `claim` that repeats a figure a delivery receipt carries for the
|
||
same subject **MUST** agree with it; the `energy` class does this
|
||
deliberately, to bind its labels to the figure. Where the delivery
|
||
receipt labels its figure (§2.4.2), the claim **MUST** also agree with
|
||
that label's class and evidence. Where they disagree a verifier **MUST**
|
||
reject rather than choose.
|
||
- A class that is about one figure — `measurement` or `operating-point` —
|
||
names it by a figure profile ([energy-measurement](energy-measurement.md)
|
||
§6.1) and the identity that profile defines, restates it with its class,
|
||
and is checked against the receipt that carries it at §3.1 step 6. It never
|
||
changes that receipt, and never supplies a class that receipt does not
|
||
carry.
|
||
|
||
#### 2.4.5. `class: "energy"`
|
||
|
||
§2.4.2's `joules_micro` fixes the unit and the scope of attribution;
|
||
[energy-measurement](energy-measurement.md) §2 requires an acquisition
|
||
class beside any non-zero figure, with the class's evidence inside the
|
||
signature, and the `object` receipt has no member for either. An
|
||
`energy` claim supplies them without changing the signed `object`
|
||
format: it restates the figure and binds to it, under the claim's
|
||
signature, the labels other extensions define. It defines none of those
|
||
vocabularies. The class and its evidence are written as
|
||
energy-measurement §2.7 writes them in a JSON receipt, under the same
|
||
member names; a class that carries no evidence has those members
|
||
present and `null`.
|
||
|
||
| member | type | meaning |
|
||
|---|---|---|
|
||
| `joules_micro` | integer | The figure. Equal to the `object` receipt's `joules_micro` for the same subject. A zero is an integer here and is refused by rule 1, at step 4. |
|
||
| `energy_provenance` | string | An acquisition class of [energy-measurement](energy-measurement.md) §2, verbatim. |
|
||
| `energy_uncertainty` | object or `null` | The declared uncertainty (energy-measurement §2.3): `{ "relative_ppm": integer, "window_us": integer }`, exactly those two members. |
|
||
| `energy_counter_filtering` | string or `null` | An `OnChipCounter`'s filtering state (energy-measurement §2.4). |
|
||
| `energy_calibration_ref` | string or `null` | A `CalibratedInstrument`'s calibration reference (energy-measurement §2.5). |
|
||
| `coverage` | string | `Whole` or `Partial`, per [energy-binding](energy-binding.md) §4. |
|
||
| `grade` | string | An evidential grade of [oer-receipts](oer-receipts.md) §2, verbatim. |
|
||
| `method` | absolute URI | The published protocol the figure was obtained under. |
|
||
| `work_units` | integer | The recomputable work the figure is expressed against ([energy-measurement](energy-measurement.md) §5). A zero is refused by rule 6, at step 4. |
|
||
| `work_unit` | term | What one work unit is (e.g. `subpixel`). |
|
||
| `oer_receipt` | string or `null` | The `receipt_id` (`sha256:<hex>` or `blake3:<hex>`, lowercase) of an [OER receipt](oer-receipts.md) recording the same figure, or `null`. |
|
||
|
||
§3.1 step 2 reads these members in the table's order, and
|
||
`energy_uncertainty`'s two members, where it is an object, as it is
|
||
reached; it checks their types and encodings, and nothing more. Which
|
||
class, evidence and figure they may hold together, and whether the
|
||
figure or the work count is zero, is step 4's.
|
||
|
||
Rules, checked at §3.1 step 4 in this order:
|
||
|
||
1. `joules_micro` **MUST NOT** be zero. A zero is a failed measurement,
|
||
and an unmetered object gets no `energy` claim at all
|
||
([energy-measurement](energy-measurement.md) §2).
|
||
2. The acquisition class, in this order:
|
||
- `energy_provenance` **MUST** name a class of energy-measurement §2
|
||
(`energy_unknown_acquisition`);
|
||
- it **MUST NOT** be `HwShunt` (`energy_legacy_acquisition`).
|
||
energy-measurement §2.6 forbids the legacy label in a new receipt,
|
||
and no claim predates the refinement, so a verifier can hold every
|
||
claim to it — as energy-measurement §2.8 holds the optional labels;
|
||
- the evidence **MUST** be the class's, whole
|
||
(`energy_acquisition_evidence`). `CalibratedInstrument` and
|
||
`OnChipCounter` carry a declared uncertainty whose two parts are
|
||
non-zero and whose `relative_ppm` is at most 2³² − 1, the width of
|
||
its signed encoding (energy-measurement §2.7), and which reaches
|
||
the resolution floor, `joules_micro × relative_ppm ≥ 288 676`
|
||
(energy-measurement §2.3). `OnChipCounter` carries a filtering state
|
||
of `On`, `Off` or `Unknown` and no calibration reference; a filtering
|
||
state of `null` is not `Unknown` here, so that one class has one
|
||
spelling. `CalibratedInstrument` carries a calibration reference
|
||
that is not empty once Unicode White_Space is trimmed from both
|
||
ends, and no filtering state. `ModelBased` and `Estimator` carry
|
||
none of the three.
|
||
3. `coverage` **MUST** be `Whole` or `Partial`.
|
||
4. `grade` **MUST** name a grade of oer-receipts §2.
|
||
5. `grade` **MUST NOT** rank above the grade the class supports. The
|
||
correspondence is oer-receipts §2's — `CalibratedInstrument` and
|
||
`OnChipCounter` support up to `measured`, `ModelBased` up to
|
||
`derived`, `Estimator` up to `projected` — with grades ranked as
|
||
oer-receipts lists them (`projected` < `simulated` < `derived` <
|
||
`measured`). Grading a figure *below* what its class supports is
|
||
permitted. The grade does not say which hardware class is behind a
|
||
`measured` figure; the class and its declared uncertainty do.
|
||
6. `work_units` **MUST NOT** be zero.
|
||
|
||
A relying party holding an `energy` claim with `coverage: "Partial"`
|
||
for a subject **MUST NOT** report that subject's `joules_micro` as the
|
||
whole cost of producing it.
|
||
|
||
**Against the delivery receipt** (§3.1 step 6): the claim's
|
||
`joules_micro` **MUST** equal the `object` receipt's for the same subject
|
||
(`energy_disagrees_with_delivery`). Where that receipt carries a class label
|
||
(§2.4.2), the claim's class and its evidence **MUST** also equal the label's
|
||
(`energy_class_disagrees_with_delivery`). Each is read as energy-measurement
|
||
§2.7 reads it, so an `OnChipCounter` label with no filtering state reads as
|
||
`Unknown`.
|
||
|
||
**Against the OER receipt it names** (§3.1 step 6): a verifier holding
|
||
that receipt checks, in this order. The receipt is read as oer-receipts
|
||
§4 reads a document: a text it refuses whole is no receipt, so a
|
||
verifier holding only such a text holds none.
|
||
|
||
1. that it is the one named — its `receipt_id` equals `oer_receipt`
|
||
(`context_mismatch`);
|
||
2. that it verifies under [oer-receipts](oer-receipts.md) with no
|
||
reason (`energy_oer_invalid`);
|
||
3. that it is about the claim's subject — its `subject.output` equals
|
||
the claim's `subject` exactly as the claim writes it, 64 lowercase
|
||
hex digits (`context_mismatch`). oer-receipts does not fix what
|
||
`subject.output` holds; an OER receipt an `energy` claim can name
|
||
holds there the object's §2.1 content hash in this form. Without
|
||
this check a claim about one object could name a valid OER receipt
|
||
about another that happens to carry the same figure and grade;
|
||
4. that its `provenance.how` equals `grade`, and that its
|
||
`energy.joules_total`, rounded to the nearest microjoule, equals
|
||
`joules_micro` (a figure exactly half-way between two microjoules
|
||
admits either) (`energy_disagrees_with_oer`). Check 2 has
|
||
established that the figure is a quantity oer-receipts §4 reads — a
|
||
decimal string of at most 34 significand digits, or an integer
|
||
within I-JSON's range — since a receipt whose figure is anything
|
||
else, or whose body holds a fraction, `-0` or an integer beyond that
|
||
range anywhere, does not verify. The figure is compared exactly, as
|
||
oer-receipts §6 compares figures: `0.0120005` is half-way and admits
|
||
12,000 and 12,001 µJ, and `0.0120004` followed by twenty-six nines is
|
||
below half-way, although rounding it to 28 significant digits would
|
||
make it a tie.
|
||
|
||
This checks that two signed statements of one figure about one object
|
||
agree. It does not check that the OER receipt's `energy.boundary` is
|
||
the scope §2.4.2 attributes to `joules_micro`; that remains the claim
|
||
signer's assertion.
|
||
|
||
The claim does not make the figure verifiable — re-execution
|
||
reproduces the work, never the joules
|
||
([energy-measurement](energy-measurement.md) §5). It makes the
|
||
figure's labels signed, and a contradiction between them detectable.
|
||
|
||
#### 2.4.6. `class: "lawful-basis"`
|
||
|
||
The afferent dual of a delivery statement: on what basis what a sink
|
||
observes may be processed, and the record that a decision was taken
|
||
against that basis. A body is one of two records, discriminated by
|
||
`record`, which §3.1 step 2 reads before anything else in the body:
|
||
`record` absent is `missing_required_field`, and a `record` other than
|
||
`"declaration"` or `"decision"` is `field_type`. The body is then
|
||
checked against the table of the record it names.
|
||
|
||
A **declaration** (`record: "declaration"`), signed by the service
|
||
operator:
|
||
|
||
| member | type | meaning |
|
||
|---|---|---|
|
||
| `record` | string | `"declaration"`. |
|
||
| `vocabulary` | absolute URI | The vocabulary the terms are drawn from (for example the W3C Data Privacy Vocabulary). |
|
||
| `fragment` | 32 B hex or `null` | §2.1 content hash of the envelope carrying the declaration fragment beside the manifest, when one is carried. |
|
||
| `purposes` | array | One entry per purpose: `{ "purpose": term, "basis": term or "", "data": [term, …] }`, `data` possibly empty. The array may be empty and a `basis` may be `""` as far as step 2 is concerned: both are refused by the rules below, at step 4, each with its own reason. |
|
||
|
||
A **decision** (`record: "decision"`), signed by the party recording
|
||
it:
|
||
|
||
| member | type | meaning |
|
||
|---|---|---|
|
||
| `record` | string | `"decision"`. |
|
||
| `vocabulary` | absolute URI | As the declaration's. |
|
||
| `declaration` | 32 B hex | `link_hash` of the declaration claim the decision is expressed against. |
|
||
| `purpose` | term | The purpose decided on. |
|
||
| `outcome` | term | The decision, as a vocabulary term. |
|
||
|
||
Rules (§3.1 step 4): a declaration **MUST** list at least one purpose;
|
||
purposes are then checked in order, and each **MUST** carry its own
|
||
non-empty basis — a basis is per purpose, never per service — and
|
||
**MUST NOT** repeat an earlier purpose. Against the declaration it
|
||
names (§3.1 step 6): the declaration **MUST** itself pass §3.1 steps
|
||
1–4 (`context_invalid`) and be a lawful-basis declaration
|
||
(`context_mismatch`); a decision **MUST** then name it by `link_hash`,
|
||
share its `subject`, `subject_type` and `vocabulary`, and decide on a
|
||
purpose the declaration lists. That check does not establish that the
|
||
declaration was the service operator's. A verified delivery receipt
|
||
supplies a publisher's key (§3.2); an operator's is confirmed under the
|
||
operator's web origin by §7, with the authority
|
||
`lawful-basis/declaration`, and only under a pinned anchor (§7.5).
|
||
Without that confirmation the check establishes only that the decision
|
||
matches a declaration, signed by whoever signed it; with it, that the
|
||
declaration was made under that origin, and, where the verifier expects
|
||
the service's origin (§7.5), under the service's.
|
||
|
||
What this class does not do. It records what was declared and what
|
||
was decided. It is not a consent mechanism: a declaration is agnostic
|
||
across bases, and each purpose carries its own. It makes no statement
|
||
that a basis is sufficient for any purpose under any regime, and a
|
||
verifier here does not check terms against the vocabulary. The shape
|
||
— purposes, each with its own basis and data categories, from a
|
||
declared vocabulary — is the one ABNT NBR 25608:2025 uses for its
|
||
broadcast privacy declaration, as described in that standard's public
|
||
operational guidelines (see the [2030 landscape survey](../research/2030-transport-landscape.md)
|
||
§7.2), whose records follow ISO/IEC TS 27560. A `lawful-basis` claim
|
||
is signed, like every claim.
|
||
|
||
#### 2.4.7. `class: "log-inclusion"`
|
||
|
||
A statement that a receipt was entered in an append-only log,
|
||
carrying the log's own evidence. Its subject is always a receipt: a
|
||
log registers receipts, never objects, and per
|
||
[transparency](transparency.md) §3 per-object receipts are not
|
||
registered individually — the subject is a group receipt's link hash,
|
||
or a claim's.
|
||
|
||
| member | type | meaning |
|
||
|---|---|---|
|
||
| `log` | absolute URI | The log. |
|
||
| `profile` | absolute URI | The registration profile the log's evidence is checked under (for example `urn:ietf:rfc:9942`, COSE Receipts). |
|
||
| `entry` | term | The log's identifier for the entry. |
|
||
| `log_receipt` | hex | The log's own receipt for the entry, verbatim. |
|
||
|
||
Rule (§3.1 step 4): `log_receipt` **MUST NOT** be empty; an inclusion
|
||
claim without the log's evidence is only its signer's say-so.
|
||
|
||
**Accepting a `log-inclusion` claim establishes that its signer
|
||
asserted inclusion and carried the log's evidence. It does not
|
||
establish inclusion.** Inclusion is established only by checking
|
||
`log_receipt` under `profile` against the log, which this extension
|
||
does not specify; a verifier that has not done so **MUST NOT** report
|
||
the subject as logged. This claim's signature never stands in for the
|
||
log's, and [transparency](transparency.md) §6 applies: registration is
|
||
a witness, not a verification.
|
||
|
||
Whose key attached the log's evidence can be confirmed under §7, with
|
||
the authority `log-inclusion`. That confirmation is not a check of
|
||
`log_receipt`.
|
||
|
||
#### 2.4.8. `class: "measurement"`
|
||
|
||
[energy-measurement](energy-measurement.md) §6 requires a published figure
|
||
to be accompanied by how it was measured: for a figure obtained under that
|
||
extension's §3, the values its bracket gave, and for any figure, the build
|
||
profile. A `measurement` claim carries them for one figure. It restates the
|
||
figure and its acquisition class, so that it can be checked on its own, and
|
||
leaves the receipt that seals the figure unchanged. It names that receipt by a
|
||
figure profile (energy-measurement §6.1) and the receipt's identity under it.
|
||
Its `subject_type` and `subject` are the ones the profile gives: `object` and
|
||
the object's §2.1 content hash for `moq-jwp:object`, and `receipt` and the
|
||
receipt's identity for every other profile.
|
||
|
||
| member | type | meaning |
|
||
|---|---|---|
|
||
| `figure_profile` | term | A figure profile of energy-measurement §6.1. |
|
||
| `figure_index` | integer or `null` | Which figure, from 0, in a profile that carries several — for `moq-jwp:object`, the object's leaf index; `null` in one that carries one. |
|
||
| `group` | 32 B hex or `null` | For `moq-jwp:object`, the link hash of the group receipt the object was delivered under (§3 step 3); `null` for every other profile. |
|
||
| `joules_micro` | integer | The figure. |
|
||
| `energy_provenance` | string | Its acquisition class, as §2.4.5 writes it. |
|
||
| `energy_uncertainty` | object or `null` | As §2.4.5. |
|
||
| `energy_counter_filtering` | string or `null` | As §2.4.5. |
|
||
| `energy_calibration_ref` | string or `null` | As §2.4.5. |
|
||
| `bracket` | object or `null` | For a figure obtained under energy-measurement §3: `{ "idle": summary, "busy": summary, "window_us": integer, "completions": integer, "in_flight": integer, "opening": "from-rest" or "running" }`, a summary being `{ "n": integer, "mean_uw": integer, "sd_uw": integer, "live_intervals": integer, "longest_live_run": integer }` as energy-measurement §6.2 defines them. `null` otherwise. |
|
||
| `build` | object | `{ "profile": "release" or "debug", "toolchain": string or null, "target": term or null }`: the build of the code measured (energy-measurement §5, §8). `release` is an optimised build (for a Rust build, `opt-level` 2, 3, `s` or `z`, whatever its profile is named); any other build is `debug`. A finer vocabulary is a new registration. |
|
||
| `power_reader` | string or `null` | The reader of the power interface (energy-measurement §8). Required under a hardware class (rule 4). |
|
||
| `fabrics` | object, or absent | Where the work the figure covers ran, and the figure divided by fabric (energy-measurement §6.3): `{ "steps": [step, ...], "shares": [share, ...] }`. A step is `{ "step": term, "fabric": term, "backend": string }`: a step of the work, in the vocabulary of the document that defines the figure's receipt (for `wai:video-package-receipt-id`, the steps of [packaging](packaging.md) §11), a fabric that ran it, and the implementation and device that did, a non-empty string. A share is `{ "fabric": term, "joules_micro": integer, "attribution": term }`. Absent, the claim does not divide its figure; never `null` (§2.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`).
|
||
6. Where `fabrics` is present, in this order:
|
||
- every fabric is `cpu`, `gpu`, `media` or `npu`, and every attribution `task`
|
||
or `rail` (energy-measurement §6.3) (`measurement_fabric_unknown`);
|
||
- there is at least one step and one share; the steps are in the order of their
|
||
`step`, in byte order, and then of their fabric, in the order just given, each pair
|
||
once; and the shares are in the order of their fabric, each fabric once
|
||
(`measurement_fabric_order`);
|
||
- every fabric a step names has a share of more than 0
|
||
(`measurement_fabric_unmetered`): a fabric that ran work is metered, and 0 J is
|
||
no reading of it;
|
||
- every share's fabric runs a step (`measurement_fabric_unused`);
|
||
- the shares sum to `joules_micro` (`measurement_fabric_sum`): the record divides
|
||
the figure, and the figure's class covers every share.
|
||
|
||
**Against the figure** (§3.1 step 6). The context is the receipt the claim
|
||
names, verified under its own profile's rules; for `moq-jwp:object`, that is
|
||
the object receipt accepted under §3 against the group receipt `group` names.
|
||
In this order:
|
||
1. the receipt verifies (`context_invalid`); it is of `figure_profile`, its
|
||
identity under that profile is `subject`, it is within the group `group`
|
||
names, and it carries a figure at `figure_index` (`context_mismatch`);
|
||
2. that figure is `joules_micro` (`figure_disagrees`);
|
||
3. that figure carries an acquisition class (`figure_unclassified`). An
|
||
unlabelled figure has none, and neither has the unmetered marker; a claim does
|
||
not supply one;
|
||
4. that class and its evidence are the claim's, each read as energy-measurement
|
||
§2.7 reads it (`figure_class_disagrees`);
|
||
5. an Ed25519 key signs the figure (`figure_signer_unknown`) — every profile but
|
||
`wai:energy-binding-id` has one — and it is `signer_pubkey`
|
||
(`figure_signer_mismatch`). For `moq-jwp:object` it is the group receipt's
|
||
`signer_pubkey`. Only the key that signed a figure signs its report.
|
||
|
||
Only check 5 shows that a `measurement` claim's signer is the figure's, so a
|
||
`measurement` claim is accepted only once step 6 has run against the receipt
|
||
it names. A verifier that does not hold that receipt reports
|
||
`figure_not_held` at step 6 — recoverable by fetching the receipt — and
|
||
**MUST NOT** present the claim as the figure signer's report, nor derive a §4
|
||
verdict for the figure from its values: without the context, any key can sign
|
||
any values about any figure.
|
||
|
||
An accepted `measurement` claim establishes that the figure's signer reported
|
||
these values for this figure, and that the figure and its declared uncertainty
|
||
are what the values give. It does not establish that the values were measured:
|
||
they cannot be re-measured (energy-measurement §5). A reader derives the §4
|
||
verdict from an accepted claim's bracket (energy-measurement §6.2).
|
||
|
||
#### 2.4.9. `class: "operating-point"`
|
||
|
||
[energy-binding](energy-binding.md) §7 makes the operating point a figure was
|
||
spent at part of the claim about it: what was requested, what was committed,
|
||
what was delivered, and what a declared descriptor predicted. An
|
||
`operating-point` claim records these for one figure, named and restated as
|
||
§2.4.8 names and restates it, without changing the receipt that seals the
|
||
figure. It is signed by the party that records them.
|
||
|
||
| member | type | meaning |
|
||
|---|---|---|
|
||
| `figure_profile`, `figure_index`, `group`, `joules_micro`, `energy_provenance`, `energy_uncertainty`, `energy_counter_filtering`, `energy_calibration_ref` | | As §2.4.8. |
|
||
| `vocabulary` | absolute URI | The vocabulary the operating points' parameters, and the prediction's descriptor, are drawn from. WAI defines none except [staged-delivery](staged-delivery.md)'s `urn:wai:staged-delivery:1` ([energy-binding](energy-binding.md) §7.2), whose points are a `stage` claim's (§2.4.10). |
|
||
| `op_requested` | point or `null` | What the requesting party asked for; `null` when nothing was requested. |
|
||
| `op_committed` | point or `null` | What the acting party said it would use. |
|
||
| `op_delivered` | point or `null` | What was used. |
|
||
| `prediction` | object or `null` | `{ "predictor": absolute URI, "descriptor_hash": 32 B hex, "predicted_joules_micro": integer }`: the model that turned the declared descriptor into energy, `BLAKE3("wai:energy-descriptor\x01" ‖ descriptor)` over the descriptor as `vocabulary` serialises it, and the energy it implies for the work the figure covers. |
|
||
|
||
A **point** is an object whose member names are terms and whose values are
|
||
integers of §2.4.3. JCS orders its members and §3.1 step 1 refuses a repeated
|
||
one, so two points are equal exactly when their canonical forms are.
|
||
|
||
Rules, checked at §3.1 step 4 in this order:
|
||
1. §2.4.8's rules 1–3. A claim restating `HwShunt` is refused there, so no
|
||
operating point is recorded against a figure labelled with the legacy class.
|
||
2. A point that is present **MUST NOT** be empty (`operating_point_empty_point`).
|
||
3. `op_delivered` **MUST NOT** be present without `op_committed`
|
||
(`operating_point_delivered_alone`).
|
||
4. `op_requested` and `op_committed` **MUST NOT** be present without
|
||
`op_delivered` (`operating_point_incomplete`).
|
||
5. A claim **MUST** carry `op_delivered` or `prediction` (`operating_point_empty`).
|
||
|
||
**Against the figure** (§3.1 step 6): §2.4.8's checks 1–4. The claim may be
|
||
signed by a party other than the figure's signer, so check 5 does not apply,
|
||
and it may name a figure no Ed25519 key signs. An operating point recorded
|
||
against a figure that carries the unmetered marker or no class is refused here
|
||
(`figure_disagrees`, `figure_unclassified`).
|
||
|
||
From an accepted claim a reader derives, and no claim states:
|
||
- whether the commitment was met: it was when `op_delivered` equals
|
||
`op_committed`, and there was none when `op_committed` is `null`;
|
||
- the **residual**, `joules_micro − predicted_joules_micro`, a signed integer,
|
||
whenever `prediction` is present.
|
||
|
||
A reader **MUST NOT** report the commitment as met unless it derives that it
|
||
was. An accepted claim establishes what its signer recorded about the request,
|
||
the commitment, the delivery and the prediction. An `op_requested` in a claim
|
||
the acting party signed is that party's account of the request.
|
||
|
||
#### 2.4.10. `class: "stage"`
|
||
|
||
A stage set ([staged-delivery](staged-delivery.md)) is a base and the layers that
|
||
refine it, each its own object with its own delivery receipt. A `stage` claim
|
||
records what one party observed of a stage set: what it asked for and was promised,
|
||
which layer objects arrived or did not and what it verified of their receipts, what
|
||
it presented on time, and how the presented output is reproduced. Its subject is the
|
||
base's §2.1 content hash. It is signed by the party that observed the set: a sink, or
|
||
another party that applies the set as a sink does (staged-delivery §10.2).
|
||
|
||
| member | type | meaning |
|
||
|---|---|---|
|
||
| `vocabulary` | absolute URI | `urn:wai:staged-delivery:1` ([energy-binding](energy-binding.md) §7.2), whose points have one parameter, `wai-stage:layers`: the number of layers a prefix of the set holds. |
|
||
| `op_requested` | point or `null` | The stage asked for. |
|
||
| `op_committed` | point or `null` | The stage the serving party said it would deliver. |
|
||
| `op_delivered` | point or `null` | The stage the delivered objects reach: the highest *k* such that, at every layer from 1 to *k*, the objects listed with `delivered-verified` evidence cover every unit of the set, whatever became of them, an object refused before its `refines` was checked against the set excepted. `null` when the observer did not verify the base's delivery receipt over its bytes before it offered the base's first layer. |
|
||
| `presented` | point | The lowest stage at which a unit was presented on time. |
|
||
| `units_by_stage` | array of integers | Element *i*: the number of units presented at stage *i*. The set's units, numbered from 0, are the units it counts. |
|
||
| `tier` | string | `decode-equivalence`, `entropy-consistency` or `none`: the weakest tier of the presented units (staged-delivery §5). |
|
||
| `stage_hash` | 32 B hex or `null` | The stage hash of the presented output over every unit (staged-delivery §5). |
|
||
| `objects` | array | Every layer object observed, each an object of the table below, ordered by `layer`, then `units.from`, then `object`. |
|
||
| `conflicts` | integer | Units frozen by a conflict (staged-delivery §4.1 step 7). |
|
||
| `deadline_misses` | integer | Units presented below the stage their layers would have reached on time (staged-delivery §8). |
|
||
| `bytes_envelopes` | integer | Envelope bytes received for the set, the base's included (staged-delivery §11). |
|
||
|
||
| member | type | meaning |
|
||
|---|---|---|
|
||
| `layer` | integer | The object's `refines.layer`. |
|
||
| `object` | 32 B hex | Its §2.1 content hash. |
|
||
| `units` | object | `{ "from": integer, "count": integer }`: the units it covers, its `refines.span` or every unit of the set. For an object refused before its `refines` was checked against the set — a reason of staged-delivery §4.1 steps 1 and 2, `refines-malformed`, `capability-not-refinement` or `base-absent` — the units its `refines` states, which need not lie in the set. |
|
||
| `bytes` | integer | Its envelope bytes as received: more than `0` for an object that arrived, `0` for one that did not. |
|
||
| `status` | string | `applied` (to every unit it covers), `partial` (to some of them), `late` (it arrived, and missed the deadline of every unit it covers), `dropped` (it never arrived: not forwarded, or abandoned at a deadline) or `refused` (it arrived, and applied to no unit for another reason). |
|
||
| `evidence` | string | What the observer verified of its delivery receipt: `delivered-verified` (§3 step 6, over its envelope bytes), `inclusion-verified` (§3, *Verified by inclusion*) or `none`. |
|
||
| `reason` | string or `null` | For `partial`, the staged-delivery §4.2 reason of the lowest unit it was not applied to; for `refused`, the envelope's reason, or else that of its lowest unit. `null` for `applied`, `late` and `dropped`. A unit the object is still held for when the claim is made counts as not applied, for the reason it is held for. |
|
||
|
||
Rules, checked at §3.1 step 4 in this order:
|
||
1. `vocabulary` **MUST** be `urn:wai:staged-delivery:1`, and every point present,
|
||
`presented` among them, **MUST** be exactly the member `wai-stage:layers`, from 0
|
||
to 255 (`stage_point_malformed`). `op_requested` and `op_committed` **MUST NOT**
|
||
be present without `op_delivered` (`stage_point_incomplete`, as §2.4.9 rule 4).
|
||
§2.4.9 rule 3 does not apply: an observer states what was delivered whether or
|
||
not anyone committed to it.
|
||
2. `stage_hash` **MUST** be present exactly when `tier` is `decode-equivalence`
|
||
(`stage_hash_without_tier`).
|
||
3. `units_by_stage` **MUST** have from 1 to 256 elements summing to at least 1, its
|
||
last element not 0; `presented` **MUST** name the lowest stage with a unit; and
|
||
neither `conflicts` nor `deadline_misses` may exceed the number of units
|
||
(`stage_units_malformed`).
|
||
4. Each object's `layer` **MUST** be from 1 to 255 and its `units.count` at least 1
|
||
(at least 2 for a `partial` object, which applied to some units of its span and
|
||
not to others), and its units **MUST** lie within the set's unless it is `refused` before its
|
||
`refines` was checked against the set (the table above); its `reason` **MUST** be
|
||
`null` exactly where the table says so and otherwise a reason of staged-delivery
|
||
§4.2. Where `op_delivered` is not `null` — the base's receipt was verified first —
|
||
an `applied` or `partial` object's `evidence` **MUST** be `delivered-verified`,
|
||
since over such a base a layer applies only on a receipt verified over its bytes
|
||
(staged-delivery §10.1); where it is `null`, a layer applied whatever was verified
|
||
of its receipt, and any evidence is stated. A `dropped` object's `evidence` **MUST
|
||
NOT** be `delivered-verified`. An object that arrived — `applied`, `partial`,
|
||
`late` or `refused` — **MUST** have `bytes` above 0, and a `dropped` one 0. The
|
||
objects **MUST** be in strictly increasing order of (`layer`, `units.from`,
|
||
`object`); their `bytes` **MUST** sum to less than `bytes_envelopes`, which counts
|
||
the base's bytes too; and no unit **MAY** be covered by two `applied` objects of
|
||
one layer, since the second would conflict with the first or find the unit past
|
||
its stage (`stage_objects_malformed`).
|
||
5. A unit reaches stage *s* only through an object of each layer up to *s* applied to
|
||
it, and an object of layer *s* applies only to a unit an object of layer *s* − 1
|
||
applied to. So, for every layer *s* from 1 up (`stage_units_unsupported`):
|
||
- an `applied` object of layer *s* **MUST** lie within the spans of the `applied`
|
||
and `partial` objects of layer *s* − 1 (for layer 1, within the set), and a
|
||
`partial` one **MUST** meet them;
|
||
- the units presented at stage *s* or above **MUST NOT** outnumber the units layer
|
||
*s* can have reached: the units its `applied` objects cover, and, of the units
|
||
inside the spans of layer *s* − 1 that those do not cover, those its `partial`
|
||
objects cover, counting no more than its span less one for each `partial` object,
|
||
which was not applied to every unit of its span. Two objects of one layer never
|
||
both apply to a unit (rule 4), so their applied units do not overlap.
|
||
6. Where `op_delivered` is not `null`, it **MUST** be the stage the objects give
|
||
(`stage_delivered_disagrees`). Where it is `null`, this rule does not apply. A
|
||
stated `op_delivered` is then never below `presented`: by rules 4 and 5, at every
|
||
layer up to the stage presented, the spans of the `applied` and `partial` objects,
|
||
each verified over its bytes, hold every unit, though a `partial` object was not
|
||
applied to all of its own.
|
||
|
||
`op_delivered` and `presented` are separate measures. `op_delivered` counts what
|
||
arrived under a receipt verified over its bytes, whether or not the sink applied it;
|
||
`presented` and `units_by_stage` count what the sink presented on time. A set whose
|
||
layers all arrived and were all refused is delivered at their stage, and so meets a
|
||
commitment to it, with stage 0 presented; a reader that asks what was presented reads
|
||
`presented`, never `op_delivered`.
|
||
|
||
A reader derives whether the commitment was met as §2.4.9 derives it: it was when
|
||
`op_delivered` equals `op_committed`, and there was none when `op_committed` is
|
||
`null`. An accepted claim establishes what its signer observed, not that content was
|
||
delivered: only `group` and `object` receipts state delivery, and an object's
|
||
`evidence` is the signer's account of what it verified, which a reader holding the
|
||
receipt checks under §3 itself.
|
||
|
||
#### 2.4.11. `class: "delivery-gap"`
|
||
|
||
A relay that does not forward an object still forwards the object's receipts (§4.1).
|
||
A `delivery-gap` claim adds the relay's own statement that it did not forward it, and
|
||
why. Its subject is the object's §2.1 content hash, and the relay signs it under its
|
||
own key, as the parallel receipt §4.3 permits.
|
||
|
||
| member | type | meaning |
|
||
|---|---|---|
|
||
| `track_alias` | integer | The object's `track_alias`. |
|
||
| `group_id` | integer | Its `group_id`. |
|
||
| `object_id` | integer | Its `object_id`. |
|
||
| `reason` | string | `delivery-timeout` (its delivery timeout passed at this relay), `discretion` (the relay chose not to forward it) or `not-received` (the relay never received it). |
|
||
| `observed_at` | string | A date-time (§2.4.3): when the relay observed the gap. |
|
||
|
||
The coordinates are integers of §2.4.3, so an object at a `track_alias`, `group_id` or
|
||
`object_id` above 2^53 − 1 has no gap claim.
|
||
|
||
**Against the object receipt** (§3.1 step 6): the object receipt at the claim's
|
||
coordinates, verified by inclusion at least. In order: it is at the claim's
|
||
`track_alias`, `group_id` and `object_id` (`context_mismatch`), and its `content_hash`
|
||
is the claim's `subject` (`gap_subject_mismatch`). A claim checked without that receipt
|
||
stops at step 5: it establishes that the relay says it did not forward an object with
|
||
this content hash, not that the publisher sent one. An accepted claim says nothing
|
||
about whether the object reached anyone by another path.
|
||
|
||
#### 2.4.12. `class: "reconstruct-session"`
|
||
|
||
A sink that reconstructed a title **MAY** record what it did in a `reconstruct-session`
|
||
claim: the decoders it used, against which parameter sets and on which host; which path
|
||
each unit took and why it switched; the presentations it made after their deadline; a
|
||
commitment to every output it presented; the bytes it counted and received; the declared
|
||
length of the classical rendition it did not need; the `stage` claims it signed about the
|
||
stage sets it observed; and, where its decode was metered, the decode's energy. Its
|
||
subject is the §2.1 content hash of the session's **entry object**: the first envelope
|
||
the session dispatched. It is signed by the sink, with the key its decoder host holds. It
|
||
asserts what that key reports and nothing more, and it is not evidence that a person
|
||
watched.
|
||
|
||
A session claim and a `stage` claim (§2.4.10) are about different things, and neither
|
||
repeats the other. A `stage` claim is about one stage set: each layer object with its
|
||
status and the evidence of its receipt, the units presented at each stage on time, the
|
||
conflicts and deadline misses, the delivered stage and the set's envelope bytes. A session
|
||
claim is about a title, which can hold several stage sets and envelopes that are no part of
|
||
one. It names the `stage` claims its sink signed about the title's sets (`stage_sets`), and
|
||
commits to every output it presented across the title, late ones included
|
||
(`output_root`). A unit's highest stage is read from the outputs, and a set's layer objects
|
||
from its `stage` claim. Step 6 checks the two against each other.
|
||
|
||
| member | type | meaning |
|
||
|---|---|---|
|
||
| `session_nonce` | 32 B hex | Fresh random bytes per session, derived from nothing that identifies a person, a device or an account. |
|
||
| `media` | string | The entry object's manifest `media` (SPEC §3), as written: 1 to 32 of `a`–`z`, `0`–`9` and `-`. |
|
||
| `host` | object | The decoder host (below). |
|
||
| `decoders` | array | The decoder entries (below), in **first-use order**: taking the presentations in order of (`layers`, `unit`), each entry is first named after the entry before it. |
|
||
| `units` | integer | The units presented at least once. |
|
||
| `leaf_count` | integer | The presentations: one per (unit, layers) presented. |
|
||
| `output_root` | 32 B hex or `null` | The root of the output tree over the presentations ([staged-measurement](staged-measurement.md) Appendix C), each leaf's `decoder` an index into `decoders`. `null` exactly when `leaf_count` is 0. |
|
||
| `primary_units` | integer | The units whose presentation at their largest `layers` came through a decoder entry whose `path` is `primary`. |
|
||
| `fallback_units` | integer | The same, for `fallback`. |
|
||
| `switches` | array | At most 1024 path switches, each `{ "unit": integer, "from": string, "to": string, "reason": string }`, in order of `unit`: `from` and `to` are `primary` or `fallback`, and `reason` is a term of [delivery-telemetry](delivery-telemetry.md) §5. |
|
||
| `switches_total` | integer | Every switch, including any not written. |
|
||
| `deadline_us` | integer or `null` | The time, in microseconds, within which the sink's schedule had each unit presented; `null` where it ran under none. |
|
||
| `missed` | integer | The presentations made after their unit's deadline. |
|
||
| `missed_units` | array | At most 256 of them, each `{ "unit": integer, "layers": integer, "late_us": integer }`: which presentation, and how many microseconds late; in order of (`unit`, `layers`). |
|
||
| `stage_sets` | array | For each `stage` claim the sink signed about a stage set, `{ "first_unit": integer, "claim": 32 B hex }`: the title's unit that is the set's unit 0, and that claim's `link_hash`; in order of (`first_unit`, `claim`). A sink signs one for each epoch of a set — before it forgets the set's base (staged-delivery §4.4, §14), and at the session's end — and names them all. |
|
||
| `framing` | string | The framing label of the counted members ([staged-measurement](staged-measurement.md) §3, its §3.3): `registered`. |
|
||
| `counted_primary` | integer | The counted bytes (staged-measurement §3) of the countable objects received for the `primary` path. |
|
||
| `counted_fallback` | integer | The same, for `fallback`. |
|
||
| `counted_discarded` | integer | The counted bytes of the countable objects received and never presented. They are in the two members above as well. |
|
||
| `uncounted` | integer | The objects received that are not countable (staged-measurement §3, *Only a checked format is counted*). Their bytes are in no counted member. |
|
||
| `transferred` | integer | Bytes of WAI objects as received, whole or as byte ranges, without transport or receipts. |
|
||
| `receipt_bytes` | integer | Bytes of receipts and claims received (staged-measurement §3.5). |
|
||
| `classical_declared` | integer or `null` | The sum, over the `WAI2` envelopes the session received whose classical rendition (below) is of a capability no decoder entry decoded, of that rendition's length as the envelope's rendition table declares it (SPEC §7.1). `null` where there is none. Never estimated. This revision derives it from the `WAI2` envelopes the sink holds in full, the ones rule 9 reads: a sink that received a `WAI2` envelope only as byte ranges holds its head but not the envelope, and cannot yet state that envelope's classical rendition. Recording such a session is HTTP session work still to come. |
|
||
| `classical_source` | term or `null` | Where `classical_declared` comes from: `wai2-table`, or a term a later extension registers here. |
|
||
| `energy` | object or `null` | `null` when the decode was unmetered. Otherwise `{ "joules_micro", "energy_provenance", "energy_uncertainty", "energy_counter_filtering", "energy_calibration_ref" }`, each written as §2.4.5 writes it (below). |
|
||
|
||
The **host** is `{ "profile": string, "target": term or null, "build": object, "threads":
|
||
integer }`. `profile` is `native-ffi` (a native process), `browser-baseline` (a browser
|
||
decoding on its processor), `browser-gpu` (a browser decoding on its graphics processor)
|
||
or `edge-wasi` (a WebAssembly host outside a browser), until an extension of sink
|
||
profiles registers the set; `target` the platform the host ran on; `build` the build of
|
||
its decoders, as §2.4.8 writes a build; and `threads` its decode workers, written as the
|
||
largest power of two not above their number, so that it does not single out a machine.
|
||
Where `energy` is not `null`, those are the workers of the bracket behind the figure,
|
||
which ran on a power of two of them: its `in_flight` is `threads` (staged-measurement
|
||
§8.7), so the `measurement` claim on the figure names no more of the machine (§6.7).
|
||
|
||
A **decoder entry**:
|
||
|
||
| member | type | meaning |
|
||
|---|---|---|
|
||
| `capability` | term | The capability decoded. |
|
||
| `rendition` | integer or `null` | Its `WAI2` rendition index (SPEC §7.1): `capability` is that rendition's capability. `null` for a `WAI1` envelope. |
|
||
| `object` | 32 B hex | The content hash (§2.1) of the object it decoded from: the `WAI2` envelope whose rendition it is, or the `WAI1` envelope — a base, or a layer that refines one. An entry decodes one object, so a sink that decodes several through one decoder names an entry for each. |
|
||
| `first_unit` | integer | The title's unit that is unit 0 of the object's base: of the object itself for a base or a rendition with no `refines`, and of the base it refines for a layer. A presentation through the entry of the title's unit `u` is of the object's unit `u − first_unit`. Where a verifier holds a layer's envelope and an entry names its base, the layer's entry has that entry's `first_unit`; where it holds a `stage` claim of a set, an entry naming the set's base or a layer the claim lists has the unit `stage_sets` names that claim at (step 6). |
|
||
| `path` | string | `primary` or `fallback`. |
|
||
| `decoder` | string | `builtin:<crate>@<version>` — `<crate>` 1 to 64 of `a`–`z`, `0`–`9`, `_` and `-`, `<version>` 1 to 64 of ASCII letters, digits, `.`, `+` and `-`, beginning with a digit — or `module:sha256:` followed by a module's digest (§7.9), 64 lowercase hexadecimal digits. |
|
||
| `variant` | term or `null` | A decode path within the decoder, where it has more than one, such as an exact and a floating-point path. |
|
||
| `pin` | 32 B hex or `null` | The `sha256` of the parameter-set pin the sink verified (SPEC §3.1). |
|
||
| `tier` | string or `null` | That pin's declared tier as SPEC §3.1 reads it: `decode-equivalence`, `entropy-consistency` or `none`. `null` exactly when `pin` is. |
|
||
| `module_status` | string or `null` | For a `module:` decoder, the anchor its grant was confirmed under (§7.9): `pinned` or `fetched`. `null` for a `builtin:` decoder. |
|
||
| `leaves` | integer | The presentations decoded through the entry. |
|
||
|
||
`energy` is the decode energy of one completion of the presentations the claim commits
|
||
to — the decodes behind its `leaf_count` leaves, the work unit
|
||
[staged-measurement](staged-measurement.md) §8 names (its §8.1) — on the host's
|
||
processor rails (its §8.4), with its acquisition class. It is the figure of profile
|
||
`moq-jwp:claim-link` ([energy-measurement](energy-measurement.md) §6.1), named by this
|
||
claim's `link_hash`, and its producer publishes its report in a `measurement` claim
|
||
(§2.4.8) signed with the same key (energy-measurement §6). A decode that was not metered
|
||
has `energy` `null`, never a figure of 0 (energy-measurement §2).
|
||
|
||
§3.1 step 2 reads the members in the table's order, and the members of the host, of
|
||
each decoder entry, switch, `missed_units` entry and `stage_sets` entry, and of `energy`
|
||
and its `energy_uncertainty`, as it reaches them; it checks their types, and
|
||
`host.profile`, `host.build.profile`, `path`, `from`, `to` and `framing` against the
|
||
values the tables give them.
|
||
|
||
Rules, checked at §3.1 step 4 in this order:
|
||
1. `primary_units` and `fallback_units` **MUST** sum to `units` (`session_path_sum`).
|
||
2. The entries' `leaves` **MUST** sum to `leaf_count`; `leaf_count` **MUST NOT** be below
|
||
`units`; and `units` is 0 exactly when `leaf_count` is (`session_leaf_sum`).
|
||
3. `output_root` **MUST** be `null` exactly when `leaf_count` is 0
|
||
(`session_root_presence`).
|
||
4. `threads` **MUST** be a power of two (`session_host_malformed`).
|
||
5. Each decoder entry **MUST** be as its table says: `decoder` of its grammar;
|
||
`rendition` at most 65535; `tier` `null` exactly when `pin` is, and otherwise one of
|
||
its three values; `module_status` `null` exactly for a `builtin:` decoder, and
|
||
otherwise `pinned` or `fetched`; `leaves` at least 1. No two entries **MAY** be equal
|
||
in every member but `leaves` (`session_decoder_malformed`).
|
||
6. A `tier` that is not `null` **MUST NOT** exceed the class tier of its entry's
|
||
`capability` (SPEC §3.1 *Tier*) (`session_tier_exceeds_class`).
|
||
7. Each switch's `from` **MUST** differ from its `to`, and its `reason` **MUST** be a
|
||
term of delivery-telemetry §5. The switches **MUST** be in non-decreasing order of
|
||
`unit`, and as many as `switches_total`, or 1024 where it is more
|
||
(`session_switch_invalid`).
|
||
8. `missed` **MUST NOT** exceed `leaf_count`, and **MUST** be 0 when `deadline_us` is
|
||
`null`. `missed_units` **MUST** hold as many entries as `missed`, or 256 where it is
|
||
more, in strictly increasing order of (`unit`, `layers`), each with `layers` from 1 to
|
||
65535 and `late_us` at least 1 (`session_missed_invalid`).
|
||
9. `stage_sets` **MUST** be in strictly increasing order of (`first_unit`, `claim`)
|
||
(`session_stage_sets_invalid`).
|
||
10. `counted_discarded` **MUST NOT** exceed the sum of `counted_primary` and
|
||
`counted_fallback` (`session_counted_invalid`).
|
||
11. `classical_source` **MUST** be `null` exactly when `classical_declared` is, and
|
||
otherwise `wai2-table` (`session_classical_invalid`).
|
||
12. Where `energy` is not `null`, its `joules_micro` **MUST NOT** be 0
|
||
(`energy_zero_figure`), and its class is admitted as §2.4.5's rule 2 admits one
|
||
(`energy_unknown_acquisition`, `energy_legacy_acquisition`,
|
||
`energy_acquisition_evidence`).
|
||
|
||
**Against the outputs** (§3.1 step 6). The context is the list of presentations, each
|
||
`{ "unit", "layers", "decoder", "out_digest" }`: a unit, the layers it was presented
|
||
at, the index of the decoder entry that produced it, and the SHA-256 of its output
|
||
(staged-measurement Appendix C). In order:
|
||
1. The list has `leaf_count` entries and no (`unit`, `layers`) twice, and the root of its
|
||
output tree is `output_root` (`session_output_mismatch`).
|
||
2. It agrees with the members it gives (`session_output_inconsistent`): every `decoder`
|
||
names an entry and every `layers` is at least 1; its distinct units number `units`;
|
||
each entry's `leaves` is the number of presentations naming it; the entries are in
|
||
first-use order; `primary_units` and `fallback_units` count the units whose
|
||
presentation at their largest `layers` names an entry on that path; and every
|
||
`missed_units` entry names a presentation of the list.
|
||
3. A verifier that holds the objects **MAY** decode a presentation again where its
|
||
entry's `capability` has the replicate class `neural-int-exact` or `standard-defined`
|
||
(SPEC §7) — the entry's object's unit `unit − first_unit`, at `layers` — and then
|
||
**MUST** find its `out_digest` (`session_output_irreproducible`). Every other
|
||
presentation is checked for membership only.
|
||
|
||
**Against the objects** (§3.1 step 6). The context lists each object received:
|
||
`{ "content_hash", "path", "counted", "transferred", "presented" }`, with `counted` the
|
||
object's counted bytes, or `null` for one that is not countable; `transferred` every byte
|
||
received for it; and `presented` whether a decoder entry names it. An object received more
|
||
than once on a path — fetched again, or in byte ranges — is listed once, its `transferred`
|
||
the bytes of every receipt. These rules need the objects and the claim alone. In order:
|
||
1. No `content_hash` is listed twice with one `path` (`session_object_duplicate`).
|
||
2. The session's `subject`, its entry object, is the content hash of an object received
|
||
(`session_entry_not_received`).
|
||
3. So is each decoder entry's `object` (`session_source_not_received`).
|
||
4. `counted_primary` and `counted_fallback` are the sums of `counted` over the objects of
|
||
each path, `counted_discarded` its sum over the objects not presented, `uncounted` the
|
||
number of objects whose `counted` is `null`, and `transferred` the sum of
|
||
`transferred` (`session_counted_mismatch`).
|
||
5. An object is `presented` exactly when a decoder entry's `object` is its content hash
|
||
(`session_presented_mismatch`). The sums above can agree with figures a sink stated
|
||
falsely; this cannot.
|
||
|
||
**The classical rendition** of a `WAI2` envelope is the first of its renditions, in
|
||
manifest order, that is selectable — not a companion or a refinement, and a capability a
|
||
sink may dispatch (SPEC §7.1) — and whose capability is a classical one of the envelope's
|
||
media: one the codec glossary (`glossary/codecs.json`) links from an entry of era
|
||
`classical`, whose registry media is the manifest's `media`, and which is not text. WAI's
|
||
own formats, the neural codecs, records, parameter sets and text capabilities are never
|
||
classical. An envelope has one classical rendition or none, and a verifier reads it from
|
||
the envelope; nothing names it.
|
||
|
||
**Against the envelopes** (§3.1 step 6). The context is the bytes of envelopes the
|
||
session received: every `WAI2` envelope among them, and any `WAI1` envelope — a base or
|
||
a layer — the verifier holds. An envelope is **readable** when it reads as a `WAI1` or
|
||
`WAI2` envelope (SPEC §2, §7.1) whose payloads are plaintext, and **encrypted** when it
|
||
reads as one whose `security` is a security object naming an encryption (SPEC §6.1): a
|
||
sink holding the key decrypts and dispatches it (§6.4 step 3), and its manifest and
|
||
rendition table are in the clear. Both are **held**: the rules below read every held
|
||
envelope's manifest and table, and read an encrypted one's payloads not at all. Bytes
|
||
that are neither — one whose `security` member is not a security object, or bytes that
|
||
are not an envelope at all — are **inert**. An inert or encrypted envelope no decoder
|
||
entry names is an object received, neither counted (its `counted` is `null`,
|
||
staged-measurement §3, *Only a checked format is counted*) nor presented; a sink lists
|
||
it like any other, so the session accounts for every byte it transferred. Each
|
||
rendition of a held `WAI2` envelope is also its exploded envelope (SPEC §7.1, *Exploding a
|
||
component*), the same object (staged-delivery §9): a decoder entry names it either by
|
||
that `WAI2` envelope and its `rendition`, or by the exploded envelope's content hash. An
|
||
entry naming it by that content hash, with no `rendition`, is read as naming the
|
||
rendition, by every rule below — unless the verifier holds the exploded envelope's own
|
||
bytes, a separate delivery (staged-delivery §9, [http-delivery](http-delivery.md)
|
||
§10.4) that the rules read as the `WAI1` envelope it is. A **held layer** is a held
|
||
`WAI1` envelope of a refinement-only capability (SPEC §5), or a rendition of a held
|
||
`WAI2` envelope of one: dispatch and selection go by capability, so an envelope or a
|
||
rendition of any other capability is a base, a `refines` it carries ignored (SPEC §4,
|
||
staged-delivery §3). Its
|
||
**base** is named by `refines.base`: in a `WAI1` layer by a content hash, of a `WAI1`
|
||
envelope or of a `WAI2` rendition's exploded envelope; in a `WAI2` rendition by the
|
||
`id` of a rendition of the same envelope. The base is **held** when the verifier holds a
|
||
held envelope that is it, in either form; its own capability is the `WAI1`
|
||
manifest's `capability`, or the rendition's; and an entry **names** it when it names it
|
||
in either form. A layer is **applied over** the entries naming its base whose
|
||
capability the layer's admits (SPEC §5, *Base admission*) and, where the base is held,
|
||
is the base's own. Rules 1 and 2 need the objects as well, rules 7 and 8 the outputs;
|
||
the others need the envelopes and the claim. In order:
|
||
1. Where the verifier holds the objects as well, each envelope is among the objects
|
||
received, by its content hash (`session_source_not_received`).
|
||
2. Where it holds the objects as well, each object listed with an envelope's content hash
|
||
states as `counted` that envelope's counted bytes (staged-measurement §3), and `null`
|
||
exactly when it is not countable (`session_counted_mismatch`).
|
||
3. Each envelope a decoder entry names is held, readable or encrypted
|
||
(`session_envelope_unreadable`). The verifier holds no key: of an encrypted envelope,
|
||
the rules below check everything its manifest and rendition table give, and nothing
|
||
that needs a payload — whether a payload matches a well-formed `sha256` (rule 4), the
|
||
units a base holds, a layer's `WIR1` payload and the span read against its base's units
|
||
(rules 5 and 8) — which are the signer's statement.
|
||
4. Each decoder entry whose `object` is a held envelope names what that envelope
|
||
gives, as a sink dispatches it: of a `WAI2` envelope, a `rendition`, the capability
|
||
that rendition has, a rendition selectable or a layer — never a companion, which is
|
||
applied with the rendition a sink picks — whose payload matches its stated `sha256`
|
||
(SPEC §7.1, *Selection algorithm*, *Component identity*), and of an encrypted envelope
|
||
whose `sha256`, where stated, is well formed: 64 lowercase hex digits, the only form a
|
||
payload can match, so a rendition whose `sha256` is not (uppercase, another length,
|
||
not hex) is one no sink decodes, whatever its payload; of a `WAI1` envelope that
|
||
is not a layer, no `rendition` and the manifest's `capability` or its `fallback`, a
|
||
capability a sink dispatches, in a manifest whose `capability` is not companion-only
|
||
(SPEC §4); of a `WAI1` layer, no `rendition` and its `capability`. And each decoder
|
||
entry whose `rendition` is not `null` has its `object` among the held
|
||
envelopes, which hold every `WAI2` envelope the session received
|
||
(`session_source_mismatch`). A
|
||
signer cannot relabel a decoder entry whose envelope the verifier holds, and so cannot
|
||
move a presentation through it out of the reach of the re-decode check above.
|
||
5. Each decoder entry whose `object` is a held layer names one a sink can apply
|
||
(`session_layer_not_applicable`); a sink refuses any other (staged-delivery §4.1, SPEC
|
||
§7.1), so no presentation through it is possible:
|
||
- in a `WAI1` manifest, its `fallback` is `null` (`capability-not-refinement`), and
|
||
its `refines` is present and reads as staged-delivery §3 defines it
|
||
(`refines-malformed`);
|
||
- a `WAI2` rendition is offered only from an envelope every component of which
|
||
conforms (SPEC §7.1, *Refinement components*; the reference
|
||
`WaiMulti::check_component_roles`): a refinement component that does not, or a
|
||
companion, an `id` or another component that breaks a rule of §7.1 or §5, leaves
|
||
none of the envelope's refinements offered;
|
||
- its base, where held, is a base — not a layer, and a selectable rendition where it
|
||
is a `WAI2` rendition — whose own capability the layer's admits
|
||
(`base-not-admitted`, step 4);
|
||
- where entries name its base, it is applied over one of them: a sink applies a layer
|
||
only over a base decoded through a capability the layer admits, and through the
|
||
base's own capability, not its fallback (`base-via-fallback`, step 3);
|
||
- where its base's units are read (rule 8), its span lies within them and its runs
|
||
sum to the units it covers (`refines-malformed`, step 2);
|
||
- its payload parses as `WIR1` for its capability's unit kind (staged-delivery
|
||
Appendix A) and holds a unit for each unit it covers, where that count is read
|
||
(`payload-malformed`, step 6).
|
||
6. Each decoder entry whose `object` is a held layer that is applied over an entry has
|
||
the `first_unit` of one of the entries it is applied over
|
||
(`session_first_unit_mismatch`). A layer's units are its base's, so its entry states
|
||
its base's first unit, whichever form names the base.
|
||
7. Where it holds the outputs as well, each presentation through a decoder entry whose
|
||
envelope is held is at the layer count that envelope gives: 1 for a base, or a
|
||
rendition that is not a layer, and `refines.layer + 1` for a layer
|
||
(`session_layers_mismatch`).
|
||
8. Where it holds the outputs as well, each presentation through a decoder entry whose
|
||
envelope is held is of a unit that envelope covers: its `unit` is at least
|
||
`first_unit + a` and below `first_unit + a + n`, where for a base, or a rendition that
|
||
is not a layer, `a` is 0 and `n` the units its payload holds — a `WIV1` clip's frames,
|
||
and one for a `WIH1` picture, a `WIA1` clip and a `WIS1` tensor — read in the
|
||
payload's own format: of the formats SPEC §5 pins for a `WAI1` manifest's
|
||
`capability` and its `fallback`, which decodes the same payload (SPEC §4 step 5), the
|
||
one whose magic the payload starts with, whichever capability the entry names, and a
|
||
rendition's capability's; for a layer, `a` and `n` are its `span`'s `from` and
|
||
`count`, or, with no span, 0 and the units of its base, read where the base is held,
|
||
in either form (`session_unit_outside_object`). Where WAI reads no unit count — a
|
||
payload in no format SPEC §5 pins, one that does not parse as its own, or a base not
|
||
held — the presentation is not checked.
|
||
9. The lengths of the held `WAI2` envelopes' classical renditions whose capability no
|
||
decoder entry decoded — encrypted or not, since the rendition table declares them in
|
||
the clear — sum to `classical_declared`, which is `null` exactly when there is none
|
||
(`session_classical_mismatch`). In this revision a held envelope is one held in full:
|
||
of a `WAI2` envelope a sink received only as byte ranges it holds the head and not the
|
||
envelope, so the session cannot yet state that envelope's classical rendition, nor this
|
||
rule read it. Recording such a session is HTTP session work still to come.
|
||
|
||
**Against the stage claims** (§3.1 step 6). The context is the wire form of each `stage`
|
||
claim the verifier holds of those `stage_sets` names. A set has a claim for each of its
|
||
epochs, and bases that cover the same units of the title each have their own. In order:
|
||
1. each passes §3.1 steps 1–4 (`context_invalid`);
|
||
2. each is a `stage` claim, signed by this claim's `signer_pubkey`, and its `link_hash` is
|
||
the `claim` of an entry of `stage_sets` (`context_mismatch`);
|
||
3. where the verifier holds the objects as well, each one's `subject`, the set's base, and
|
||
every layer object it lists as arrived — `applied`, `partial`, `late` or `refused`,
|
||
with bytes — are among the objects received (`session_stage_disagrees`);
|
||
4. each decoder entry whose `object` is the `subject` of one of them, or a layer object
|
||
one lists, has as its `first_unit` the `first_unit` of an entry of `stage_sets` naming
|
||
a claim of that object (`session_first_unit_mismatch`): a set's base and its layers
|
||
are at the units the session names the set at;
|
||
5. where the verifier holds the outputs as well, for each claim and each entry of
|
||
`stage_sets` naming it, with `f` that entry's `first_unit` and `n` the units the claim
|
||
counts (`session_stage_disagrees`):
|
||
- every unit from `f` to `f + n − 1` is presented;
|
||
- from below: for every stage `s` from 1 up, the units among them presented at more
|
||
than `s` layers are no fewer than the units the claim counts at stage `s` or above. A
|
||
unit presented on time at a stage was presented at every lower stage before it, and
|
||
late presentations are leaves of the tree as well, so the tree can hold more than a
|
||
`stage` claim counts and never less;
|
||
6. where it holds the outputs, from above: every unit of an entry's units presented at
|
||
more than `s` layers lies in the span of a layer-`s` object that a claim named by an
|
||
entry holding the unit lists as `applied` or `partial` (`session_stage_disagrees`). A
|
||
unit reaches a stage only through a layer the sink applied: a `late` layer missed the
|
||
deadline of every unit it covers and a `refused` one applied to none (§2.4.10). The
|
||
claims of every epoch count together, so a unit
|
||
refined before the sink forgot its base is bounded by the claim of that epoch, and
|
||
the tree never holds a stage the claims' applied layers cannot have given.
|
||
|
||
An accepted claim establishes that its signer reported this session. Its outputs are
|
||
committed to by the tree. A presentation through a decoder entry whose capability is
|
||
byte-exact is reproducible, and a verifier that decodes it again checks it. An entry's
|
||
capability is the signer's statement, except where the verifier holds the envelope of the
|
||
object it names, which binds it; that the entry's outputs were decoded from that object,
|
||
and not from another the session received, is the signer's statement. Its byte totals are checkable against the
|
||
objects, its classical length against the envelopes, and its stage sets against their
|
||
`stage` claims, both ways. Its timings, missed deadlines, switches and energy are its
|
||
signer's statements (energy-measurement §5). So, deliberately, is each refusal a sink
|
||
makes that the rules above do not check, since no context of this claim holds what
|
||
decides it: that a layer's base was decoded before the layer was applied, where no
|
||
decoder entry names the base (staged-delivery §4.1 step 2, `base-absent`), since a sink
|
||
may present a base's units only once a layer has refined them; a layer's delivery
|
||
receipt (step 5, `refinement-unattested`), which no context carries; the decode of a
|
||
layer's streams to their last symbol, its declared cost and the chain budget (step 6,
|
||
`payload-malformed` for a stream, `cost-over-limit`, `chain-over-limit`), which a
|
||
verifier that decodes a byte-exact presentation again checks, and which otherwise turn
|
||
on the sink's limits; each unit's producer, conflict, geometry and deadline (steps
|
||
7–10) and the hold bound and batches (§4.3–§4.4), which turn on the order and the times
|
||
of arrival the sink alone saw, while the `stage` claims bound the stages a unit reached
|
||
and a re-decode its output; a pin's resolution at the sink, its module authorisation and
|
||
its key log (SPEC §3.1, §4), which a decoder entry's `pin` and `tier` state; what an
|
||
encrypted envelope's payloads hold — whether each matches its well-formed `sha256`, the
|
||
units a base holds and a layer's `WIR1` payload (SPEC §6.4); whether a refinement's exploded envelope that the verifier
|
||
holds as bytes of their own was received on its own, a separate delivery that rule 5
|
||
reads as a `WAI1` layer and not through its envelope's component rules (staged-delivery
|
||
§9); and a
|
||
companion's conformance (SPEC §7.1), which does not decide whether the rendition it binds
|
||
to is presented. Nothing binds a `stage` claim to a session
|
||
beyond its base and its key: a `stage` claim the same key signed about the same base in
|
||
an earlier session of the title can be named, and the checks above hold it as they hold
|
||
one of this session. The `claim` an
|
||
`end` observation carries ([delivery-telemetry](delivery-telemetry.md) §2, its §2.1)
|
||
is this claim's `link_hash`.
|
||
|
||
#### 2.4.13. `class: "consumption"`
|
||
|
||
An agent's statement that it consumed objects, for a use it names in the AI usage
|
||
vocabularies a usage statement carries, having seen the statements that governed them and
|
||
holding the room grant it names. [ai-usage](ai-usage.md) defines it: its body's members are
|
||
§6.1 there (`objects`, `usage`, `seen` and `grant`), read at §3.1 step 2; its class rules are
|
||
§6.2 there, at step 4; and its context is §6.3 there, at step 6. Its `subject` is the first of
|
||
`objects`, and its `signer_id` is the `did:key` of its `signer_pubkey`.
|
||
|
||
#### 2.4.14. `class: "transmission"`
|
||
|
||
A publisher's declaration that it transmitted a group over a bearer, and over which: a
|
||
two-way relay chain, a multicast transmission or a bearer with no return path. It attests
|
||
transmission, never delivery, and it sits beside the group receipt, whose bytes it leaves
|
||
unchanged, so one group carried on several bearers has one receipt and a declaration for each.
|
||
[bearer-receipts](bearer-receipts.md) defines it: its body's members are §4.1 there
|
||
(`bearer`, `bearer_id`, `energy` and `attests`), read at §3.1 step 2; its class rules are §4.2
|
||
there, at step 4; and its context is §4.3 there, at step 6: the group receipt, whose link hash
|
||
is its `subject` and whose key signs it.
|
||
|
||
#### 2.4.15. `class: "reconciliation"`
|
||
|
||
A sink's statement, made later and out of band, of the objects of a group it received: a
|
||
bitmap of their positions and a root over their leaves under the group's own tree (§2.2),
|
||
checked against the publisher's root without the objects. It is never a hop, never in the
|
||
publisher's chain, and attests receipt, not presentation.
|
||
[bearer-receipts](bearer-receipts.md) defines it: its body's members are §5.1 there
|
||
(`bearer`, `bearer_id`, `leaf_count`, `received`, `received_root` and `attests`), read at §3.1
|
||
step 2; its class rules are §5.2 there, at step 4; and its context is §5.3 there, at step 6:
|
||
the group receipt, whose link hash is its `subject` and whose key does not sign it, and the
|
||
object receipts at the positions it names.
|
||
|
||
### 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. The second, a soft
|
||
binding, is declared over one object (§2.6.2) or over a span of a track
|
||
(§2.6.4).
|
||
|
||
#### 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 of this class covers the whole object, and §2.6.4 leaves
|
||
it unchanged; a binding over a span of a track is §2.6.4's class. A
|
||
binding scoped to a region of a picture 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. For a declaration of class
|
||
`soft-binding-span` (§2.6.4) that receipt is *R*, a delivery receipt
|
||
for an object of the declaration's span, verified under §3, and *P* is
|
||
*R*'s signer. 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` or a `soft-binding-span` 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. for a `soft-binding-span` declaration: *R* is within its span
|
||
(§2.6.4, `recovery_outside_span`), and `observed` is not *R*'s
|
||
`content_hash` (`recovery_of_hard_bound_object`);
|
||
6. `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.6.4. Span binding (`class: "soft-binding-span"`)
|
||
|
||
A soft binding whose value was computed over a **span** of one track
|
||
rather than over one object: a stretch of the track's media timeline,
|
||
and, where the publisher states them, the groups or the objects that
|
||
carry it. A watermark embedded in every frame of a minute of a live
|
||
track, or a fingerprint computed over that minute, binds the minute; a
|
||
clip cut from it and re-encoded carries the value, and a recovery
|
||
through the declaration reaches the publisher's receipts for that
|
||
minute and for nothing outside it (§3.2).
|
||
|
||
The track's publisher declares it with a `claim` of class
|
||
`soft-binding-span`. Its `subject` is the **anchor**: the §2.1 content
|
||
hash of the span's first object, in group and object order. Its body
|
||
is §2.6.2's members and `span`:
|
||
|
||
| member | type | meaning |
|
||
|---|---|---|
|
||
| `alg`, `registry`, `binding_type`, `value`, `resolution_endpoint` | as §2.6.2 | As §2.6.2's, `value` being what the algorithm produced over the span. |
|
||
| `span` | object | The span, below. |
|
||
|
||
`span` is an object of exactly these members:
|
||
|
||
| member | type | meaning |
|
||
|---|---|---|
|
||
| `track_alias` | integer | The track, as its group receipts name it (§2.4.1), under the declaration's `signer_pubkey`. |
|
||
| `start_ms` | integer | The span's first instant of media time, in milliseconds from the track's media time 0. |
|
||
| `end_ms` | integer | Its last, in the same units; both ends are in the span. |
|
||
| `first_group` | integer or `null` | The first group that carries the span, or `null` where the declaration states no groups. |
|
||
| `last_group` | integer or `null` | The last such group, or `null` with `first_group`. |
|
||
| `first_object` | integer or `null` | With groups, the span's first object, an `object_id` in `first_group`; `null` where the span is stated by whole groups. |
|
||
| `last_object` | integer or `null` | Its last object, an `object_id` in `last_group`; `null` with `first_object`. |
|
||
|
||
Rules (§3.1 step 4), after §2.6.2's rules on `alg`, `value` and
|
||
`resolution_endpoint`: `first_group` and `last_group` are both `null`
|
||
or both integers, `first_object` and `last_object` likewise, and the
|
||
objects are integers only beside integer groups (`span_malformed`);
|
||
`start_ms` ≤ `end_ms`, `first_group` ≤ `last_group`, and
|
||
(`first_group`, `first_object`) ≤ (`last_group`, `last_object`),
|
||
compared by group and then by object (`span_inverted`).
|
||
|
||
An object receipt is **within** the span when its `track_alias` is the
|
||
span's and its signer is the declaration's, and: with objects stated,
|
||
its (`group_id`, `object_id`) is from (`first_group`, `first_object`)
|
||
to (`last_group`, `last_object`), compared as above; with groups alone,
|
||
its `group_id` is from `first_group` to `last_group`; with media time
|
||
alone, its `content_hash` is the anchor. Delivery receipts carry no
|
||
media time, so a span stated by media time alone places no object but
|
||
the anchor; groups or objects are how a publisher lets a recovery
|
||
reach the rest of the span's receipts.
|
||
|
||
**Whole-object bindings are unchanged.** This class adds no member to
|
||
`soft-binding`: a `soft-binding` claim that carries `span` is
|
||
`unknown_member` (§3.1 step 2), and every `soft-binding` claim, and
|
||
every recovery through one, verifies byte for byte as before. A span
|
||
is a class of its own rather than an optional member of
|
||
`soft-binding` because a claim has no optional members (§2.4.3): one
|
||
claim, one canonical form.
|
||
|
||
**The C2PA mapping.** A declaration of either class is one block of a
|
||
C2PA soft-binding assertion (C2PA 2.4 §18.10), `{"scope": S, "value":
|
||
V}` with `V` its `value`, the assertion's `alg` its `alg`. For
|
||
`soft-binding`, `S` is `{}`: no `timespan` and no `region`, the whole
|
||
asset. For `soft-binding-span`, `S` is `{"timespan": {"start":
|
||
start_ms, "end": end_ms}}`. C2PA's `start` and `end` are unsigned
|
||
integers, the time range in milliseconds from the start of the media
|
||
over which the value was computed (C2PA 2.4 §18.10.2); C2PA states no
|
||
reading of the range's end, and its example's adjacent blocks end at
|
||
133016 and start at 133017, which is the reading of both ends included
|
||
that this class states. The groups and the objects have no C2PA field:
|
||
they are the coordinates a recovery checks receipts against, and stay
|
||
on the WAI side. C2PA matches a soft binding by `alg` and `value`
|
||
(C2PA 2.4 §18.10.5.1), and the scope says what the value was computed
|
||
over without taking part in the match; a recovery here matches the
|
||
same way and uses the span to bound what the match reaches.
|
||
|
||
**Why a span.** A live track has no single object a soft binding can
|
||
be computed over that survives a cut: an object holds a fraction of a
|
||
second, a clip taken from the track spans many objects, and a
|
||
watermark whose payload repeats in every frame is read from any of
|
||
them. Declaring the value once per span, with the coordinates that
|
||
carry it, lets one declaration cover the stream, and keeps what a
|
||
recovery attests no wider than what the publisher declared.
|
||
|
||
### 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`), and, for a verifier that
|
||
implements [pq-signatures](pq-signatures.md), `moq-jwp:group-sign\x03` the hybrid form
|
||
(§6.2 there: the labelled form's fields, the 1952-byte key, `sig`, the 3309-byte `pq_sig`).
|
||
|
||
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. A hybrid group ([pq-signatures](pq-signatures.md) §6.2) is verified at this
|
||
step by both its halves, in the order of pq-signatures §4.2, and under a policy that
|
||
requires hybrid (§5 there) a group with a classical signature only is refused here
|
||
(`hybrid_required`) before its signature is examined.
|
||
|
||
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`, and
|
||
`BLAKE3("moq-jwp:receipt-link\x01" || canonical_bytes(G))` 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` (but a
|
||
`measurement` body's `fabrics`, which may be absent, §2.4), 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`; a `fabrics`
|
||
that is `null` is `field_type`, and within it, as in a bracket, a member
|
||
absent is `missing_required_field` and an additional one `unknown_member`
|
||
(`steps` before `shares`, each array's rows read for their members before
|
||
its rows' values). 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
|
||
consumption claim's usage `vocabulary`, a transmission or reconciliation
|
||
claim's `bearer`, 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`, a span — 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; how
|
||
a span's members pair and order; 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; in a `transmission` or `reconciliation` claim, its
|
||
`attests`, a transmission's energy figure and a reconciliation's bitmap;
|
||
and, in a `consumption` claim, the order of its
|
||
objects, usage entries and seen digests, its subject, its terms and its
|
||
`signer_id`.
|
||
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. A hybrid claim's two halves are
|
||
verified in the order of [pq-signatures](pq-signatures.md) §4.2 (`hybrid_required`
|
||
under its policy, `pq_signature_invalid`), and its hybrid members are read at step 1,
|
||
after the subject type is admitted (`pq_malformed`, `pq_suite_unknown`).
|
||
4. **Class rules**, per §2.4.5–§2.4.15 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, or for a span declaration a verified delivery receipt for an
|
||
object of its span (§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 `consumption` claim against
|
||
the objects it names, with their receipts, the usage statements that govern them
|
||
and its room, as [ai-usage](ai-usage.md) §6.3 orders them (§2.4.13); a
|
||
`transmission` claim against the group receipt its subject names, and a
|
||
`reconciliation` claim against that group receipt and the object receipts
|
||
at the positions it names, each verified by inclusion at least, as
|
||
[bearer-receipts](bearer-receipts.md) §4.3 and §5.3 order them (§2.4.14,
|
||
§2.4.15). A group receipt is verified here under §3 steps 1 and 2, and one
|
||
that fails them is `context_invalid`. 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 a
|
||
class the claim names — for a recovery record, `soft-binding` or
|
||
`soft-binding-span` (`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 | `span_malformed` | a span's groups, or its objects, are one `null` and one integer, or its objects are integers beside `null` groups (§2.6.4) |
|
||
| 4 | `span_inverted` | a span's `start_ms` is after its `end_ms`, its first group after its last, or its first object after its last (§2.6.4) |
|
||
| 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 | `measurement_fabric_unknown` | a fabric record names a fabric other than `cpu`, `gpu`, `media` and `npu`, or an attribution other than `task` and `rail` |
|
||
| 4 | `measurement_fabric_order` | a fabric record with no step or no share, its steps not in the order of their step and fabric, or its shares not in the order of their fabric, or a pair or a fabric twice |
|
||
| 4 | `measurement_fabric_unmetered` | a fabric record's step runs on a fabric the record gives no share, or a share of 0 |
|
||
| 4 | `measurement_fabric_unused` | a fabric record's share is on a fabric no step of it runs on |
|
||
| 4 | `measurement_fabric_sum` | a fabric record's shares do not sum to `joules_micro` |
|
||
| 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` |
|
||
| 4 | `consumption_objects_unordered` | a `consumption` claim's `objects` are not in strictly ascending order ([ai-usage](ai-usage.md) §6.2) |
|
||
| 4 | `consumption_subject_mismatch` | its `subject` is not the first of its `objects` |
|
||
| 4 | `consumption_usage_unordered` | its `usage` entries are not in strictly ascending order of `vocabulary` |
|
||
| 4 | `consumption_term_unknown` | a `term` is not a term of its vocabulary |
|
||
| 4 | `consumption_seen_unordered` | its `seen` digests are not in strictly ascending order |
|
||
| 4 | `consumption_signer_not_principal` | its `signer_id` is not the `did:key` of its `signer_pubkey` |
|
||
| 4 | `transmission_not_transmitted` | a `transmission` claim's `attests` is not `transmitted` ([bearer-receipts](bearer-receipts.md) §4.2) |
|
||
| 4 | `reconciliation_not_received` | a `reconciliation` claim's `attests` is not `received` (bearer-receipts §5.2) |
|
||
| 4 | `reconciliation_bitmap_malformed` | its `leaf_count` is 0, its `received` is not ⌈`leaf_count` / 8⌉ bytes, sets a bit at or past `leaf_count`, or sets none |
|
||
| 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, or, for a span declaration, for an object of its span |
|
||
| 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 | `recovery_outside_span` | the delivery receipt a recovery through a span declaration, or a recovery record naming one, is checked with is not within the span (§2.6.4): another track or signer, a group or an object outside its range, or, for a span stated by media time alone, an object other than the anchor |
|
||
| 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 | `consumption_object_unknown` | a `consumption` claim names an object the verifier does not hold ([ai-usage](ai-usage.md) §6.3) |
|
||
| 6 | `grant_mismatch` | the grant it names is not the signer's, not good at an object's time, or names objects that are not the room's |
|
||
| 6 | `preference_mismatch` | its `seen` is not the digests of the statements that govern its objects |
|
||
| 6 | `usage_unanswered` | its `usage` does not answer a vocabulary a governing statement carries |
|
||
| 6 | `usage_disallowed` | a use it names is disallowed for one of its objects |
|
||
| 6 | `transmission_subject_mismatch` | a `transmission` claim's `subject` is not the link hash of the group receipt it is checked against ([bearer-receipts](bearer-receipts.md) §4.3) |
|
||
| 6 | `transmission_signer_mismatch` | it is not signed by the group's key |
|
||
| 6 | `reconciliation_subject_mismatch` | a `reconciliation` claim's `subject` is not the link hash of the group receipt it is checked against (bearer-receipts §5.3) |
|
||
| 6 | `reconciliation_by_publisher` | it is signed by the group's key |
|
||
| 6 | `reconciliation_leaf_count_mismatch` | its `leaf_count` is not the group's |
|
||
| 6 | `reconciliation_objects_not_held` | it names a position whose object receipt the verifier does not hold: recoverable, never accepted |
|
||
| 6 | `reconciliation_root_mismatch` | its `received_root` is not the root over the leaves at the positions it names |
|
||
| 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.
|
||
|
||
**Through a span.** *D* may instead be a `soft-binding-span` claim
|
||
(§2.6.4). The verifier then needs, in place of a delivery receipt for
|
||
*D*'s subject, *R*: a delivery receipt for an object of *D*'s span that
|
||
it has verified under §3, *P* being *R*'s signer. Step 2 admits the
|
||
class; after step 3, *R* **MUST** be within *D*'s span (§2.6.4,
|
||
`recovery_outside_span`); and step 4 also refuses an *H* equal to
|
||
*R*'s `content_hash`. The steps are otherwise those above. The
|
||
recovery attests the **span**: the track (*P*, `track_alias`), its
|
||
media time, its groups or objects where *D* states them, and the
|
||
anchor. Of the publisher's receipts, the ancestor's are those within
|
||
the span: *R*, and any other object receipt of the track the verifier
|
||
holds whose coordinates are within it.
|
||
|
||
What a recovery through a span does not attest:
|
||
|
||
- which objects of the span the object in hand was derived from, or
|
||
how much of the span it holds. A value computed over the span matches
|
||
content cut from any part of it: a watermark that repeats its payload
|
||
in every frame is read from one frame;
|
||
- anything outside the span. An object of the track before or after
|
||
it, or of another track, is not reached through *D*: a receipt
|
||
outside the span is refused (`recovery_outside_span`), never reported
|
||
as the ancestor's, and the same publisher's binding over another span
|
||
is another declaration, recovered on its own;
|
||
- for a span stated by media time alone, any object but the anchor:
|
||
delivery receipts carry no media time, so none places an object
|
||
inside the interval;
|
||
- the energy of the object in hand: the figures of the span's objects
|
||
cover producing them, and the ancestor rule below applies to the span
|
||
as a whole.
|
||
|
||
A verifier **MUST** report a recovery through a span with its span,
|
||
and **MUST NOT** report one object of the span, the anchor or *R*, as
|
||
what the object in hand was derived from.
|
||
|
||
**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 a declaration 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. An algorithm's
|
||
error rates under named transformations are measured, reproducibly,
|
||
by the robustness records of
|
||
[robustness-evidence](robustness-evidence.md), and what it claims to
|
||
survive holds only where such a record shows it (robustness-evidence
|
||
§8). A verifier **MAY** report beside a recovery the accepted claims
|
||
and records it holds for the declaration's algorithm; they change no
|
||
step here.
|
||
|
||
A recovery **MAY** be recorded as a `binding-recovery` claim (§2.6.3).
|
||
The reference implementation (`wai-rs`, feature `receipt_claims`,
|
||
`claim_receipt::recover`, and `claim_receipt::recover_span` through a
|
||
span) returns a value whose only attestation is the ancestor, or the
|
||
span; 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 1, 2 | `pq_malformed`, `pq_suite_unknown`, `pq_signature_invalid`, `hybrid_required` | as [pq-signatures](pq-signatures.md) §8, for a hybrid group or under the policy that requires one |
|
||
| §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.
|
||
- Hold to these duties, at its own door, the objects and receipts another relay forwards
|
||
it, as it holds a publisher's: where relays carry a track from one to another (a room
|
||
across relays, [live-rooms](live-rooms.md) §10.7), each verifies every group receipt
|
||
and every object it fans out, whatever the relay before it verified, and refuses an
|
||
object that does not verify though the relay before it forwarded it. A relay's own check
|
||
is no statement to the next relay: none is carried between them, and none is trusted.
|
||
- 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.
|
||
- Derive new objects from those it forwards and publish them as a
|
||
publisher of its own ([deriving-edge](deriving-edge.md)): each a new
|
||
object with its own content hash, in a group it signs on a track of its
|
||
own, with a derivation record (deriving-edge §5) as the parallel receipt
|
||
§4.3 permits. What it received it forwards as this section requires,
|
||
unaltered.
|
||
|
||
### 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. It covers span bindings (§2.6.4): declarations
|
||
stated by media time, by groups and by objects, each rule of a span from
|
||
both sides, a `soft-binding` claim carrying a span, recoveries through a
|
||
span with a delivery receipt at each edge of its range and one past it,
|
||
of another track or signer, and of an object other than the anchor
|
||
under a span of media time alone, and recovery records naming span
|
||
declarations.
|
||
|
||
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.
|
||
- **A span wider than its value.** A span is its publisher's
|
||
statement of what its value was computed over (§2.6.4). A publisher
|
||
that declares a span wider than the content that carries the value
|
||
makes a recovery reach receipts of objects the content in hand may
|
||
not come from; the declaration carries its signer's authority and no
|
||
more, as every claim does. A resolution endpoint cannot widen a span,
|
||
which is signed, and a receipt outside it is refused rather than
|
||
reported (`recovery_outside_span`).
|
||
|
||
## 7. Key discovery and signing authority
|
||
|
||
Every signature in this extension is checked against the `signer_pubkey`
|
||
the receipt itself carries (§3 step 2, §3.1 step 3). That settles that
|
||
the holder of that key signed. It does not settle who holds it: a forger
|
||
supplies a key as easily as a signature. This section lets a signer say
|
||
where its keys are published, and lets a verifier confirm it. The signer
|
||
names a web origin in `signer_id`; the origin publishes a signed, chained
|
||
**key log** listing its keys and what each may sign; a verifier that has
|
||
**anchored** the origin checks an item it has accepted against that log.
|
||
The same log says which keys may authorise the decoder and reconstruct
|
||
modules a sink runs, revokes modules by digest, and bounds how long a
|
||
revoked module keeps running (§7.9).
|
||
|
||
Nothing here adds a member to a receipt or claim, or changes a signed
|
||
byte. `signer_id` is inside every signature already (§2.3, §2.4.3), so no
|
||
relay can change the origin it names. A receipt verifies exactly as
|
||
before, and one whose signer does not use this section is reported
|
||
unconfirmed, as every signer was before it.
|
||
|
||
Beside an item's own verdict, a verifier that runs this section reports
|
||
one of three levels, and **MUST NOT** report one as another:
|
||
|
||
- **confirmed** — an origin the verifier has anchored lists the item's
|
||
key, in force, with every authority the item needs (§7.4, §7.5);
|
||
- **refused** — what the verifier holds contradicts the item's claim to
|
||
speak for the identity its `signer_id` names: a valid log of that
|
||
origin, or the `signer_id` itself (§7.1);
|
||
- **unconfirmed** — the verifier holds nothing it can confirm against,
|
||
or nothing that meets the anchor an authority requires (§7.5). It
|
||
establishes nothing about who signed.
|
||
|
||
Confirmation never changes an item's verdict under §3 or §3.1. A refused
|
||
claim is still a statement its signer made; a confirmed one still asserts
|
||
only what §2.4.4 says its class asserts.
|
||
|
||
### 7.1. What a `signer_id` names
|
||
|
||
A `signer_id` — `publisher_id` in §2.3 — has one of three forms.
|
||
|
||
- **Origin-bound.** A signer that publishes its keys under a web origin
|
||
writes `signer_id` as a `did:web` DID URL with a host, no path and a
|
||
fragment:
|
||
|
||
```
|
||
signer_id = "did:web:" host [ "%3A" port ] "#" key-id
|
||
host = label 1*( "." label ) ; the last label is not all digits
|
||
label = 1-63 of a-z 0-9 "-", not beginning or ending with "-"
|
||
port = 1-65535 in decimal, no leading zero, not 443
|
||
key-id = 1-64 of A-Z a-z 0-9 "-" "_" "."
|
||
```
|
||
|
||
A host is at most 253 characters. Its **origin** is `https://`
|
||
followed by the host and, where a port is written, `:` and the port —
|
||
the ASCII serialisation of a web origin (RFC 6454 §6.2), unrelated to
|
||
the `origin` label of an `object` receipt (§2.4.2). Its **key log** is
|
||
at the origin followed by `/.well-known/wai-keys`, and nowhere else.
|
||
The suffix `wai-keys` is not yet registered in the IANA Well-Known URIs
|
||
registry; registration under RFC 8615 is to be requested. Each origin
|
||
and each key-id has one spelling: a capital letter in the host, an
|
||
escape other than `%3A`, a path, a missing fragment, the default port
|
||
written out and an IP address are not this form. A signer writes this
|
||
form only for a key its origin's log lists.
|
||
- **Ed25519 `did:key`.** The characters `did:key:z` followed by the
|
||
base58btc encoding (the Bitcoin alphabet, each leading zero byte
|
||
written as `1`) of the bytes `0xed`, `0x01` and a 32-byte key, where
|
||
decoding the text and encoding the result again gives the same text. It
|
||
names that key and nothing else: not who holds it, and not what it may
|
||
sign.
|
||
- **Label.** Any other string, including `did:key` and `did:web` strings
|
||
of neither form above. A verifier reads nothing from a label.
|
||
|
||
A verifier **MUST NOT** report a `signer_id` as the identity of an item's
|
||
signer unless it has confirmed the item (§7.4); otherwise it reports the
|
||
`signer_id` as the signer's own label.
|
||
|
||
A `did:web` resolver reads a DID document at `/.well-known/did.json` on
|
||
the same origin. An origin that publishes one **SHOULD** list in it
|
||
exactly the `active` keys of its latest key-log revision, each as an
|
||
Ed25519 `publicKeyMultibase` under the id
|
||
`did:web:<host>[%3A<port>]#<key-id>`, the key's origin-bound `signer_id`. A
|
||
verifier here **MUST NOT** take a key from it: the key log is what §7.2
|
||
signs and chains, and it governs. The `wai-keys` tool of the reference
|
||
implementation emits such a document from a log and reports every
|
||
difference between a document and a log.
|
||
|
||
### 7.2. The key log
|
||
|
||
A **log date-time** is an RFC 3339 date-time in UTC, written
|
||
`YYYY-MM-DDTHH:MM:SSZ` with `T` and `Z` in capitals, no fractional second
|
||
and a second of at most 59, so that two of them compare as strings. An
|
||
**authority** is one or more segments of `A-Z`, `a-z`, `0-9`, `-`, `_` and
|
||
`.`, joined by `/`, at most 128 characters in all (§7.5).
|
||
|
||
A key log is a JSON object with exactly two members: `kind`, which is
|
||
`"wai-key-log"`, and `revisions`, a non-empty array of revisions whose
|
||
first has `seq` 0 and each of which is the parent of the next. A revision
|
||
is a JSON object with exactly these members, under §2.4.3's encodings:
|
||
|
||
| member | type | meaning |
|
||
|---|---|---|
|
||
| `kind` | string | `"wai-key-set"`. |
|
||
| `origin` | string | The origin the log is published under, as §7.1 derives it. |
|
||
| `seq` | integer | `0` for the first revision; otherwise one more than its parent's. |
|
||
| `prev` | 32 B hex or `null` | Its parent's `revision_hash`; `null` exactly when `seq` is `0`. |
|
||
| `issued_at` | log date-time | When the origin issued it. Self-asserted; not earlier than its parent's. |
|
||
| `root` | 32 B hex | The Ed25519 key that signs this revision. |
|
||
| `next_root` | 32 B hex | The commitment (below) to the key that may sign as `root` after this one; never the commitment to `root`. |
|
||
| `keys` | array | The key entries, below. |
|
||
| `revoked_modules` | array of 32 B hex | The SHA-256 digests of the modules and pinned parameter sets the origin has revoked (§7.9), 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 (a hybrid entry adds `pq_suite` and `pq_key`, and a
|
||
revision with a hybrid root adds `pq_suite`, `root_pq_key` and `pq_sig`, each with what
|
||
[pq-signatures](pq-signatures.md) §6.1 adds to rules 1, 3, 5 and 6 and to the commitment):
|
||
|
||
| 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.
|
||
|
||
A verifier that implements [pq-signatures](pq-signatures.md) runs its step 4a after step 4:
|
||
a hybrid entry and an item with a classical signature only is refused (`hybrid_downgrade`),
|
||
an item whose ML-DSA key is not its hybrid entry's, or that is hybrid under a classical entry,
|
||
is refused (`pq_key_mismatch`), and under an anchor that requires hybrid an item with a
|
||
classical signature only is unconfirmed (`hybrid_required`); under such an anchor step 3
|
||
follows only hybrid-signed revisions.
|
||
|
||
Steps 1–3 and 6a fail as unconfirmed and steps 4–7 as refused, except
|
||
step 1's `signer_id_names_another_key`, which is refused. Step 6a is
|
||
unconfirmed, not refused: nothing contradicts the item; the verifier
|
||
lacks the anchor the authority requires.
|
||
|
||
Two times are compared as instants: each is brought to UTC by applying
|
||
its offset, carrying into the minute, hour and date as the calendar
|
||
requires and leaving the second as written, and the results are compared
|
||
as (date, hour, minute, second, fraction), the fraction as a decimal
|
||
fraction. A second of 60 thereby falls after second 59 of its minute and
|
||
before the next minute.
|
||
|
||
A confirmed item is reported with its origin, key-id and authorities; the
|
||
head's `revision_hash`; the anchor record — pinned or fetched, the
|
||
anchor's `revision_hash`, `acquired_at` if fetched, and the pinned
|
||
revision if pinned — and the rotation
|
||
it moved to, if it moved; where the entry is retired, `until`, and that
|
||
the item is untimed if it has no signed time; where an origin was
|
||
expected, where the expectation came from; and, where the item needs a
|
||
hardware-class authority (§7.5), whether its key also holds `delivery`
|
||
(`also_delivers`). A `key_revoked` refusal is reported with
|
||
`compromised_since`, and with the item's own time if it has one.
|
||
|
||
- A verifier **MUST NOT** present an item it has not confirmed as signed
|
||
by the party its `signer_id` names.
|
||
- A verifier **MUST NOT** report a refusal as unconfirmed.
|
||
- A confirmation establishes that the origin, as anchored, lists this key
|
||
with these authorities. It does not establish that the statement is
|
||
true (§2.4.4), that the key was not misused within its authority, or
|
||
that an energy figure is what its class says
|
||
([energy-measurement](energy-measurement.md) §2.2). It is the origin's
|
||
say-so, made checkable.
|
||
- A confirmation describes the log as the verifier holds it. A
|
||
revocation the verifier has not retrieved is not in it, and the head's
|
||
`revision_hash` in the report shows how current the log was.
|
||
|
||
### 7.5. Authorities: who may sign which class
|
||
|
||
The authorities an item needs are fixed by the item, never by its signer.
|
||
A confirmation needs every one of them:
|
||
|
||
| item | authorities |
|
||
|---|---|
|
||
| a `group` receipt, and the `object` receipts under it | `delivery`; and `energy/<class>` for each acquisition class an `object` receipt under it labels, where its leaf binds a label (§2.2), and for the class a labelled group payload signs for the group's total (§2.3) |
|
||
| a `claim` | its class's authority: `energy/` followed by its `energy_provenance` for `energy`, for example `energy/OnChipCounter`; `lawful-basis/declaration` or `lawful-basis/decision` by its `record`; its class name for every other class — `log-inclusion`, `soft-binding`, `soft-binding-span`, `binding-recovery`, `measurement`, `operating-point`, `stage`, `delivery-gap`, `reconstruct-session`, `consumption`, `transmission` and `reconciliation`. 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` |
|
||
| the energy figure of a `transmission` claim whose `energy` is not `null` | `transmission`, and `energy/` followed by `energy.energy_provenance` |
|
||
| a receipt of another WAI family that signs its own `signer_pubkey` and `signer_id` | `receipt`; and `energy/<class>` for each class its figures carry a label for |
|
||
| a module grant (§7.9) | `module/decoder` or `module/reconstruct`, by its `form` |
|
||
| a room descriptor or room grant ([live-rooms](live-rooms.md) §3, §4), or a room usage record ([ai-usage](ai-usage.md) §4.2) | `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/<class>` 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/<class>` 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/<class>` for each class among its figures |
|
||
| a watermark session record ([forensic-watermark](forensic-watermark.md) §5) | `live/watermark` |
|
||
| a derivation record ([deriving-edge](deriving-edge.md) §5) | `edge/derive`, from the origin of each source object it derives from (deriving-edge §8); and `energy/<class>` for its figure's class where it is metered |
|
||
|
||
A class registered in §2.4.4 registers its authority here with it, and a
|
||
family that needs a narrower authority than `receipt` registers one here.
|
||
An authority a verifier does not know matches nothing. Because an energy
|
||
authority is read from the `energy_provenance` member wherever it
|
||
appears, a class that restates a figure with its acquisition class needs
|
||
that class's authority without this table naming the class.
|
||
|
||
**Pinned-only authorities.** The hardware-class energy authorities —
|
||
`energy/CalibratedInstrument`, `energy/OnChipCounter` and the legacy
|
||
`energy/HwShunt` — the lawful-basis authorities —
|
||
`lawful-basis/declaration` and `lawful-basis/decision` — and any
|
||
authority registered for pre-emption are confirmed only under a pinned
|
||
anchor that has not moved (§7.4 step 6a). Every other authority is
|
||
confirmed under a pinned or a fetched anchor, and every confirmation
|
||
reports which.
|
||
|
||
**Expected origin.** A verifier **MAY** expect an origin for an item: one
|
||
its relying party names — the service a sink used, the publisher it
|
||
subscribed to — or the **subject's origin**: the origin under which it
|
||
has confirmed, with the authority `delivery`, the group receipt that
|
||
carries a claim's subject (for a claim whose `subject_type` is `object`),
|
||
or under which it has confirmed the receipt a claim's subject names
|
||
(`subject_type` `receipt`). It reports which. §7.9 expects one of the
|
||
sink's module origins. A `reconstruct-session` claim is a sink's statement
|
||
about an object a publisher delivered, so its subject's origin is not the
|
||
sink's: a verifier expects an origin for a session only where its relying
|
||
party names one. A session signed under a `did:key` is unconfirmed (§7.4 step
|
||
1), which is the normal case for a sink's ephemeral key.
|
||
|
||
§2.4.4's *signed by* column names the party each class expects. Which of
|
||
its keys is that party is the origin's decision, and its log records it:
|
||
|
||
- **Delivery.** A group receipt confirmed under an origin was signed with
|
||
a key that origin gives `delivery`.
|
||
- **Energy.** `energy/<class>` lets a key state figures of that class and
|
||
no other, so an origin can confine what a metering key may claim. No
|
||
authority raises a figure's class or grade, or adds to what a class
|
||
establishes ([energy-measurement](energy-measurement.md) §2, §2.2). An
|
||
origin **SHOULD** give the hardware-class authorities only to a key
|
||
held by the component that signs the reading, and not to a key it also
|
||
gives `delivery`. Beside a confirmed item that needs a hardware-class
|
||
authority, a verifier reports whether its key also holds `delivery`
|
||
(`also_delivers`). A group receipt whose objects' leaves bind a
|
||
hardware-class label, or whose labelled total carries one, is signed by
|
||
one key for delivery and for the figures, so `also_delivers` is true
|
||
there by construction, and is reported as such. An `energy` claim
|
||
confirmed under the subject's origin was made under the publisher's
|
||
authority, by its own key or by a metering key its log lists.
|
||
- **Lawful basis.** A declaration confirmed with
|
||
`lawful-basis/declaration` is its origin's; §2.4.6's check of a
|
||
decision against it then establishes that the decision matches a
|
||
declaration made under that origin. A verifier **MUST NOT** report a
|
||
decision as matching a service operator's declaration unless it has
|
||
confirmed the declaration, and reports the declaration's level beside
|
||
the decision's.
|
||
- **Measurement and operating point.** A `measurement` claim needs
|
||
`measurement` and the energy authority of the class it restates; an
|
||
`operating-point` claim needs `operating-point` and that energy
|
||
authority too. A report of a hardware-class figure is therefore
|
||
confirmed only under a pinned anchor, and is reported with
|
||
`also_delivers`. §2.4.8's check 5 already requires a `measurement`
|
||
claim's key to be the key that signed the figure; confirmation adds
|
||
that the figure's origin gave that key the authority to report on it.
|
||
An origin can give `operating-point` to the party that records
|
||
requests and commitments without letting it state figures of its own.
|
||
- **Stage and delivery gap.** `stage` lets a key sign what its holder observed
|
||
of a stage set (§2.4.10), and `delivery-gap` what a relay did not forward
|
||
(§2.4.11). Neither is pinned-only, and neither is `delivery`: an origin gives
|
||
`stage` to the sinks it speaks for and `delivery-gap` to its relays, and a key
|
||
that holds `delivery` alone signs neither.
|
||
- **Reconstruct session.** `reconstruct-session` lets a key sign what a sink
|
||
reconstructed (§2.4.12). It is not pinned-only and it is not `delivery`: an
|
||
origin gives it to the sinks it speaks for. A session's energy figure is a
|
||
second item, since the session's other members stand without it: it needs
|
||
`reconstruct-session` and the energy authority of its class, every one of
|
||
them, and is reported with `also_delivers` where that class is a hardware
|
||
class. A verifier reports the session's confirmation and its figure's side by
|
||
side, and **MUST NOT** report the figure as its origin's unless it confirmed the
|
||
figure. A key that holds `reconstruct-session` and not the figure's energy
|
||
authority confirms a session whose figure is refused (`authority_not_granted`);
|
||
under a fetched anchor, a hardware-class figure is unconfirmed
|
||
(`anchor_not_pinned`) beside a confirmed session. The origin then stands behind
|
||
the session, and not behind its figure.
|
||
- **Transmission and reconciliation.** `transmission` lets a key declare the bearer a group
|
||
it signed was sent over (§2.4.14), and `reconciliation` lets a key sign what a sink
|
||
received of a group (§2.4.15). Neither is pinned-only and neither is `delivery`: an origin
|
||
gives `transmission` to its publishing keys and `reconciliation` to the sinks it speaks
|
||
for, and a key that holds `delivery` alone signs neither. A transmission's energy figure is
|
||
a second item, as a session's is: it needs `transmission` and the energy authority of its
|
||
class, and the declaration stands without it. A sink with no return path confirms under
|
||
anchors provisioned before it was cut off, and a key log that reaches it over the bearer
|
||
anchors nothing ([bearer-receipts](bearer-receipts.md) §8).
|
||
- **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`, or a
|
||
span declaration with `soft-binding-span`, 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`.
|
||
- **Deriving edges.** `edge/derive` lets a key act as an edge that derives
|
||
objects from an origin's content and publishes them under its own key
|
||
([deriving-edge](deriving-edge.md) §8). The origin that gives it is the
|
||
source's, not the edge's: an origin lists the edges it trusts with its
|
||
content, and a hop is confirmed under each origin its chain reaches. It is
|
||
not pinned-only, and it is not `delivery` or `live/relay`.
|
||
- **Pre-emption** ([unidirectional-delivery](unidirectional-delivery.md)
|
||
§3), and relay attestation (§4.3) other than a `delivery-gap` claim and a
|
||
derivation record, 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` |
|
||
| 4a | refused | `hybrid_downgrade` | the entry is hybrid and the item carries a classical signature only ([pq-signatures](pq-signatures.md) §6.1) |
|
||
| 4a | refused | `pq_key_mismatch` | the item's ML-DSA key is not the entry's, or the entry is classical (pq-signatures §6.1) |
|
||
| 4a | unconfirmed | `hybrid_required` | the anchor requires hybrid and the item carries a classical signature only (pq-signatures §6.1) |
|
||
| 5 | refused | `key_revoked` | the entry is revoked |
|
||
| 5 | refused | `key_retired` | the entry is retired and the item's signed time is later than `until` |
|
||
| 6 | refused | `authority_not_granted` | the entry's `may_sign` does not hold every authority the item needs |
|
||
| 6a | unconfirmed | `anchor_not_pinned` | the item needs a pinned-only authority, and the anchor is fetched, has moved, or has no pinned revision, or, for pre-emption, the pinned revision does not list the key with it |
|
||
| 7 | refused | `origin_not_expected` | the origin is not the one the verifier expects, or not one of the set it expects |
|
||
| §7.3 | — | `key_log_malformed` | a log or a revision is not of §7.2's form |
|
||
| §7.3 | — | `origin_mismatch` | a revision names another origin |
|
||
| §7.3 | — | `key_log_signature_invalid` | a revision's signature does not verify under its `root` |
|
||
| §7.3 | — | `key_log_chain_break` | the first revision's `seq` is not 0, or a revision does not follow the one before it |
|
||
| §7.3 | — | `key_log_root_not_committed` | a revision's `root` is neither the previous one's nor committed to by it, or its `next_root` changes without a rotation |
|
||
| §7.3 | — | `key_log_rewrites_history` | as at step 3, between consecutive revisions of the log being anchored |
|
||
| §7.3 | — | `key_log_rolled_back` | a log retrieved from an anchored origin ends at a trusted revision older than the anchor; the anchor is kept |
|
||
| §7.3 | — | `key_log_replaced` | a log retrieved from an anchored origin neither contains the anchor nor supersedes a trusted revision by a rotation; the anchor is kept |
|
||
|
||
The reasons of §7.9's checks are listed there.
|
||
|
||
### 7.8. Security considerations
|
||
|
||
- **First use.** A fetched anchor is as good as the first retrieval: the
|
||
domain name system, the certificate accepted, and the origin's server
|
||
at that moment. Whoever controls any of them then can serve a log of
|
||
its own to a verifier anchoring for the first time. Once anchored, a
|
||
verifier is not exposed to that again, since every later revision must
|
||
descend from its anchor (§7.3). A relationship that cannot bear the
|
||
risk pins its anchor; the authorities §7.5 makes pinned-only are those
|
||
for which WAI does not leave that choice open.
|
||
- **Modules on first use.** A module authorised under a fetched anchor is
|
||
code accepted on first use: whoever served the first retrieval chose
|
||
which keys may grant it. A sink that cannot bear that pins the anchors
|
||
of its module origins. Every module outcome reports the anchor's path
|
||
(§7.9).
|
||
- **Which origin grants a module.** Anchoring an origin to confirm its
|
||
receipts does not make it a source of modules: a grant is held only
|
||
from one of the sink's module origins (§7.9), so an origin anchored on
|
||
first use for some other publisher's receipts authorises nothing a sink
|
||
runs on this envelope.
|
||
- **Revocation from any anchored origin.** A digest revoked in the head
|
||
of any origin a sink has anchored is not held, whichever origin grants
|
||
it (§7.9 check 5), so a publisher's revocation cannot be undone by
|
||
another origin's grant. Any anchored origin can therefore make a
|
||
module fall back; it cannot make one run.
|
||
- **Withheld revisions.** An origin, or anyone between it and a verifier,
|
||
can withhold its latest revisions and with them a revocation. How
|
||
often a verifier retrieves the log is its own policy; the head named in
|
||
a report shows what it rested on, and registration (§7.6) makes a
|
||
withheld revision's existence checkable. For modules, §7.9's status
|
||
bounds how long a withheld revocation can go unnoticed.
|
||
- **A rolled-back server.** A retrieval that returns an older log is
|
||
reported (`key_log_rolled_back`), never taken as the anchor; a status
|
||
that names a revision older than the head the sink holds is not
|
||
current (§7.9).
|
||
- **A stolen root.** Until the owner's rotation reaches a verifier, a
|
||
verifier holding only the thief's revisions confirms what the thief
|
||
adds. When the rotation arrives it supersedes them, and every key they
|
||
added is no longer listed.
|
||
- **Injected revisions.** A revision that fails rules 1–3, is unattached,
|
||
or is signed by a key its parent does not authorise is no child of
|
||
anything and cannot stop a confirmation; only the origin's own root and
|
||
committed key can. A status names a revision on the head's line or is
|
||
not held (§7.9), so one signed under an injected revision changes
|
||
neither a module outcome nor its reason.
|
||
- **Retired keys and untimed items.** Many receipt families carry no
|
||
signed time. An item without one, under a retired key, stays
|
||
confirmable for as long as the key is not revoked, and is reported as
|
||
untimed with the key's `until`. An origin that cannot vouch for a
|
||
retired key's past use revokes it.
|
||
- **Clocks.** §7.9's freshness and grant windows are measured on the
|
||
sink's clock. A sink whose clock can be set back runs a module for as
|
||
long as the clock is set back; a sink with no clock it trusts treats
|
||
every status as stale and runs no module. The kill latency of §7.9 is
|
||
a bound on a trusted clock and on nothing else.
|
||
- **Published private keys.** A key whose private half has been
|
||
published attests nothing, and **MUST NOT** be listed in a key log. Every
|
||
key WAI's own corpora, pins and reference meters sign with has a
|
||
published seed, and every such seed is one byte repeated 32 times. The
|
||
reference implementation exposes the public keys of all such seeds
|
||
(`key_log::published_test_keys`); its `wai-keys` tool refuses to put one
|
||
in a log unless told the log is a test log, and reports one it finds
|
||
in a log it verifies. Every vector of §5.8 is a test log, and every
|
||
anchored verdict there carries that report.
|
||
- **A name with a new owner.** Whoever controls a domain next can publish
|
||
a new log under its origin. It does not descend from an existing
|
||
anchor, so an anchored verifier keeps its anchor and reports
|
||
`key_log_replaced`; a verifier anchoring for the first time sees only
|
||
the new log, in which items signed under the old one are not listed. A
|
||
verifier that must keep confirming them keeps the revisions it
|
||
confirmed them against (§7.3).
|
||
- **Retrieval reveals interest.** Retrieving a log tells the origin that
|
||
someone is checking its signatures. A verifier retrieves the log, not a
|
||
key per item, and may take later revisions from any source.
|
||
- **Separation.** A root signs revisions and statuses and nothing else:
|
||
no entry may be a root or the committed key (§7.2), and a revision's,
|
||
a status's and a grant's signing domains are distinct from every
|
||
receipt's and claim's.
|
||
|
||
### 7.9. Decoder and reconstruct modules
|
||
|
||
A **module** is code a sink obtains apart from an envelope and runs to
|
||
decode a capability (a **decoder** module) or to rebuild a
|
||
reconstruction from what a decoder produced (a **reconstruct** module) —
|
||
for example a WebAssembly module. A parameter set an envelope pins by
|
||
its digest — the prior a neural capability decodes against
|
||
([prior-carriage](prior-carriage.md)) — determines a decode the way code
|
||
does. It is revoked the same way, by its digest, at every sink that holds
|
||
a key log, and authorised the same way, as a decoder module, by a sink
|
||
that holds pinned parameter sets to this section. Running a module
|
||
requires this section; holding pinned sets to it is the relying party's
|
||
choice per capability ([SPEC](../SPEC.md) §4). A sink that holds a
|
||
capability's pinned sets to this section runs the checks below as
|
||
[SPEC](../SPEC.md) §3.1's check 5, before it looks for the set among
|
||
what it holds; a sink that holds a key log but not the capability's sets
|
||
reads only check 5's revocation lists (*Pinned sets and several grants*,
|
||
below).
|
||
|
||
**Identity.** A module's **digest** is the SHA-256 of its bytes exactly as
|
||
the sink instantiates them, and a pinned parameter set's is its pin's
|
||
`sha256`, the digest of the set in the pin's `form` ([SPEC](../SPEC.md)
|
||
§3.1); both are written as 64 lowercase hexadecimal digits. For a pinned
|
||
set the pin's digest stands for the bytes' at check 2, and SPEC §3.1's
|
||
checks 6–8 then require the bytes the decode reads to have it. A sink that
|
||
instantiates other bytes than it hashed runs code nothing authorised.
|
||
|
||
**Grant.** An origin authorises a module by a signed **grant**, a JSON
|
||
object with exactly these members, under §2.4.3's encodings:
|
||
|
||
| member | type | meaning |
|
||
|---|---|---|
|
||
| `kind` | string | `"wai-module-grant"`. |
|
||
| `origin` | string | The origin under whose key log the grant is checked: the one its `signer_id` names (§7.1). |
|
||
| `module` | 32 B hex | The module's digest. |
|
||
| `form` | string | `"decoder"` or `"reconstruct"`. |
|
||
| `capabilities` | array of terms | The capabilities the module may decode or reconstruct: at least one, none twice. |
|
||
| `not_before` | log date-time | When the grant takes effect. |
|
||
| `not_after` | log date-time | When it lapses; not earlier than `not_before`. |
|
||
| `signer_pubkey` | 32 B hex | The granting key. |
|
||
| `signer_id` | string | The granting key's `signer_id` (§7.1). |
|
||
| `sig` | 64 B hex | `Ed25519(signer, "wai:module-grant\x01" \|\| JCS(grant without "sig"))`. |
|
||
|
||
**Status.** A key status is a cacheable signed pointer to an origin's
|
||
head, re-issued on each heartbeat, so the key log does not grow with
|
||
every heartbeat. It is a JSON object with exactly the members `kind`
|
||
(`"wai-key-status"`), `origin`, `head` (32 B hex, a `revision_hash`),
|
||
`seq` (that revision's `seq`), `issued_at` (a log date-time) and `sig`,
|
||
`Ed25519(root, "wai:key-status\x01" || JCS(status without "sig"))`,
|
||
where `root` is the `root` of the revision `head` names. A sink **holds**
|
||
a status only if it has that form, names the grant's origin, and names a
|
||
revision on the **head's line** — the head §7.4 step 3 reached, or a
|
||
revision the sink holds that the head descends from by `prev` — with its
|
||
`seq`, under whose `root` the signature verifies. A status that is not
|
||
held is ignored, as an injected revision is (§7.2).
|
||
|
||
Over HTTP, a status and an origin's grants are served where
|
||
[http-delivery](http-delivery.md) §8.4 states.
|
||
|
||
**Module origins.** A sink holds a module only from an origin it accepts
|
||
module grants from for the envelope and capability at hand — its
|
||
**module origins**, each recorded with where it came from:
|
||
|
||
- **relying-party** — an origin the sink's relying party names as a
|
||
source of modules for that capability;
|
||
- **subject** — the origin under which the sink confirmed, with the
|
||
authority `delivery`, the group receipt that carries the envelope: its
|
||
publisher.
|
||
|
||
A sink with no module origin for an envelope holds no module for it. An
|
||
origin the sink anchored for any other purpose — to confirm some other
|
||
publisher's receipts, say — is not thereby one of them.
|
||
|
||
**Ceilings.** WAI sets two normative ceilings: `grant_max_s` is at most
|
||
**86400** seconds and `status_max_age_s` at most **3600** seconds. A key
|
||
log declaring more is malformed (§7.2 rule 1).
|
||
|
||
**The checks.** A sink runs a module, or, where it holds pinned
|
||
parameter sets to this section, decodes against one, only if every
|
||
check below passes. It runs them in this
|
||
order, before it instantiates the module, and reports the first that
|
||
fails:
|
||
|
||
1. The grant has the form above, its `signer_id` is origin-bound and
|
||
names the grant's `origin`, and its `sig` verifies under its
|
||
`signer_pubkey` by §3.1 step 3's rule (`module_grant_invalid`).
|
||
2. The bytes' digest is the grant's `module` (`module_digest_mismatch`).
|
||
3. The grant passes §7.4 as an item needing `module/<form>`, with its
|
||
`not_before` as its signed time and the sink's module origins
|
||
expected: step 7 refuses an origin that is not one of them
|
||
(`origin_not_expected`). A failure is reported with §7.4's own
|
||
reason. A revoked granting key therefore fails every grant it signed,
|
||
whatever the grant's times.
|
||
4. The sink holds a status for that origin naming the head §7.4 step 3
|
||
reached; of those, the latest is **fresh**: its `issued_at` is not
|
||
later than `now` and at most that head's `status_max_age_s` seconds
|
||
earlier (`module_status_stale`). A sink that holds statuses but none
|
||
naming that head — each names an older revision of the head's line —
|
||
fails with `module_status_not_descended`.
|
||
5. The grant's `module` is not in the `revoked_modules` of that head,
|
||
nor of the head §7.4 step 3 reaches from any other anchor the sink
|
||
holds — or, where none can be reached, of that anchor revision
|
||
(`module_revoked`).
|
||
6. `not_before` ≤ `now` (`module_grant_not_yet_valid`) and `now` ≤
|
||
`not_after` (`module_grant_expired`).
|
||
7. `not_after` − `not_before` is at most the head's `grant_max_s`
|
||
(`module_grant_too_long`).
|
||
8. The capability being decoded is one of the grant's `capabilities`
|
||
(`module_capability_not_granted`).
|
||
|
||
`now` is the sink's trusted clock, as a log date-time. The checks are a
|
||
function of the module's bytes, the grant, the sink's module origins,
|
||
the revisions, anchors and statuses the sink holds, and `now`, and read
|
||
no clock and no network of their own. A sink runs them before it instantiates a module, and again
|
||
before it keeps using an instance once the status or the grant it relied
|
||
on is no longer current; an instance that fails is discarded.
|
||
|
||
**Fail closed.** Every failure — no network, no status, a stale status, a
|
||
clock the sink does not trust, an expired grant — means the module is
|
||
**not held**. A sink with no trusted clock has no `now`: no status is
|
||
fresh, and every grant is treated as expired. A grant that lapses without
|
||
renewal is not held from the moment it lapses.
|
||
|
||
**Fallback, never inert.** A module or pinned parameter set that is not
|
||
held makes its capability unsupported for that envelope at that sink, and
|
||
dispatch continues to the declared fallback or a sibling rendition
|
||
([SPEC](../SPEC.md) §4). A sink **MUST NOT** report a file inert because
|
||
of module authorisation while a fallback or sibling it can decode without
|
||
that module remains. An encoder emitting a capability whose decode needs
|
||
a module **MUST** declare such a fallback. A pinned parameter set this
|
||
section does not hold is refused at SPEC §3.1's check 5: `prior_revoked`
|
||
where its digest is revoked (below), and `prior_not_authorised` for every
|
||
other failure.
|
||
|
||
**Pinned sets and several grants.** For a pinned set, a sink runs the
|
||
checks with each grant of form `decoder` it holds; a grant of form
|
||
`reconstruct` names no set. The set is held when one grant passes every
|
||
check. When none does, the sink reports `module_revoked` if any grant
|
||
reached check 5 and failed it, or, failing that, if the set's digest is in
|
||
the `revoked_modules` of the head the sink reaches from any anchor it
|
||
holds — or, where none can be reached, of that anchor revision — with
|
||
that anchor's origin. A revocation is of the digest, whichever grant
|
||
names it, whichever check a grant fails first, and whether or not the
|
||
sink holds a grant at all. Otherwise the sink reports the check and
|
||
reason of the grant that passed the most checks; a sink that holds no
|
||
such grant reports that.
|
||
|
||
A sink that holds a key log but does not hold the capability's pinned
|
||
sets to this section reads only those revocation lists: it refuses a set
|
||
whose digest they hold (`module_revoked`, and SPEC §3.1's
|
||
`prior_revoked`), and runs none of checks 1–8 otherwise. Both apply as
|
||
well to the set a sink decodes an envelope that pins none against (SPEC
|
||
§4), identified by each digest a pin over it could name: it is refused
|
||
when any of them is revoked, and, for a capability held to this section,
|
||
unless one of them is held.
|
||
|
||
**Kill latency.** The longest a module whose grant is no longer renewed,
|
||
or whose digest or granting key is revoked, keeps running at a sink that
|
||
follows this section is
|
||
|
||
```
|
||
K = grant_max_s + status_max_age_s
|
||
```
|
||
|
||
with both values from the head the sink holds, measured on the sink's
|
||
clock and bounded by the ceilings. `K` is a normative parameter of the
|
||
origin's log: an origin states it by the two members it declares, and a
|
||
sink reports it with every module it runs. `K` bounds what the granting
|
||
origin does; a revocation in another anchored origin's head (check 5)
|
||
takes effect once the sink holds that head.
|
||
|
||
**Reporting.** A module that runs is reported with the granting key's
|
||
origin and key-id, the head's `revision_hash`, the anchor record's path,
|
||
where the module origin came from, and `K`. One that does not is
|
||
reported with the check and the reason — for a digest another origin
|
||
revokes, that origin — and with what dispatch did instead: fell back, or
|
||
left the file inert because no fallback was declared.
|
||
|
||
`key_log::module_check` in `wai-rs` implements these checks. The
|
||
reference dispatcher runs SPEC §3.1's check 5 when the sink is
|
||
configured with a key log (`key_log::SinkKeyLog`, given to
|
||
`pin::verify_with`, to `int_payload::resolve`, which runs it for a pinned
|
||
decode and over the files an unpinned integer decode reads, or to
|
||
`neural::ModelRegistry::with_key_log`, which also runs it before an
|
||
unpinned decode, over the bytes that decode then reads, read once, by
|
||
every digest a pin over them could name): the revocation
|
||
lists for every capability a pin applies to, and these checks for the
|
||
capabilities a sink holds to this section. A sink configured with no key
|
||
log runs neither. The reference dispatcher runs no module obtained
|
||
apart from an envelope, and `wai-web` and the C ABI take no key log
|
||
([SPEC](../SPEC.md) §4).
|
||
|
||
Sub-delegation, where one grant authorises another, is out of scope. A
|
||
module is revoked by its digest or through the key that granted it.
|
||
|
||
| check | reason | raised when |
|
||
|---|---|---|
|
||
| 1 | `module_grant_invalid` | the grant is not of the form above, its `signer_id` does not name its `origin`, or its signature does not verify |
|
||
| 2 | `module_digest_mismatch` | the bytes' SHA-256 is not the grant's `module` |
|
||
| 3 | §7.4's reason | the granting key is not confirmed with `module/<form>` under one of the sink's module origins (`origin_not_expected` where its origin is not one) |
|
||
| 4 | `module_status_stale` | no status is held, or the latest naming the head is older than `status_max_age_s` or later than `now`, or there is no trusted clock |
|
||
| 4 | `module_status_not_descended` | statuses are held, and none names the head the sink reached |
|
||
| 5 | `module_revoked` | the head's `revoked_modules`, or another anchored origin's, holds the digest |
|
||
| 6 | `module_grant_not_yet_valid` | `now` is earlier than `not_before` |
|
||
| 6 | `module_grant_expired` | `now` is later than `not_after` |
|
||
| 7 | `module_grant_too_long` | the grant's window exceeds the head's `grant_max_s` |
|
||
| 8 | `module_capability_not_granted` | the capability is not one the grant names |
|