WAI Extension: JWP Receipt Chain
Mirrored from the canonical text at commit 260cbd09 ().
Status: Draft. Sub-spec of WAI. Canonical text:
wai/extensions/jwp-receipts.mdin 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 §7signer_idandkey_log(featurekey_discovery). A post-quantum signature beside each record’s Ed25519 one — a hybrid group, claim, key entry or key-log root — is pq-signatures’s (featurepq_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.
groupandobjectreceipts are the delivery class; aclaimreceipt (§2.4.3) carries one of the others —energy,lawful-basis,log-inclusion,measurement,operating-point,stage,delivery-gap,reconstruct-session,consumption,transmission,reconciliation,schedule,timeliness,cache-transmission(§2.4.5–§2.4.18) — 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 adelivery-gapclaim (§2.4.11). - What a sink reconstructed (§2.4.12): a
reconstruct-sessionclaim 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, thestageclaims it signed about the stage sets it observed, and its decode energy where metered — checkable against the outputs, the objects, the envelopes and thosestageclaims. - 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_idthat 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 §8 defines the HTTP carriage:
receipt files named by link hash, a per-object index of
WAI_RECEIPTproperties, 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 (
WAI1magic + 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 §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 §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 §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 §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
Partialexactly when some object’s figure is zero; - for a hardware class,
relative_ppmis at least⌈Σ F_i·p_i / Σ F_i⌉andwindow_usis the longestwindow_us_i. The bound is linear in the parts, so it assumes their errors fully correlated, and it is conservative; - an
OnChipCountertotal’s filtering state is the objects’ common one, orUnknown; aCalibratedInstrumenttotal 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, 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 §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 §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 §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.18, §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 |
schedule | claim | receipt (the group scheduled) | §2.4.16 | the group’s publisher: the key that signed the group; or a node, of an instance of its own (cache-authority §5.6) |
timeliness | claim | receipt (the group whose objects it times) | §2.4.17 | the group’s publisher: the key that signed the group |
cache-transmission | claim | receipt (the group sent again) | §2.4.18 | a node that sent the objects from what it held, never the group’s publisher |
Rules:
- A verifier MUST reject a
claimwhoseclassit 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. deliveryis reserved togroupandobject. Aclaimnaming it MUST be rejected; delivery is stated only in the §2.3 signed payload.- The class is read from
classand 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
claimcarries the authority of its signer and no more.classsays 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
claimthat repeats a figure a delivery receipt carries for the same subject MUST agree with it; theenergyclass 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 —
measurementoroperating-point— names it by a figure profile (energy-measurement §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 §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 §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 §4. |
grade | string | An evidential grade of oer-receipts §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 §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 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:
joules_microMUST NOT be zero. A zero is a failed measurement, and an unmetered object gets noenergyclaim at all (energy-measurement §2).- The acquisition class, in this order:
energy_provenanceMUST 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).CalibratedInstrumentandOnChipCountercarry a declared uncertainty whose two parts are non-zero and whoserelative_ppmis 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).OnChipCountercarries a filtering state ofOn,OfforUnknownand no calibration reference; a filtering state ofnullis notUnknownhere, so that one class has one spelling.CalibratedInstrumentcarries a calibration reference that is not empty once Unicode White_Space is trimmed from both ends, and no filtering state.ModelBasedandEstimatorcarry none of the three.
coverageMUST beWholeorPartial.gradeMUST name a grade of oer-receipts §2.gradeMUST NOT rank above the grade the class supports. The correspondence is oer-receipts §2’s —CalibratedInstrumentandOnChipCountersupport up tomeasured,ModelBasedup toderived,Estimatorup toprojected— 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 ameasuredfigure; the class and its declared uncertainty do.work_unitsMUST 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.
- that it is the one named — its
receipt_idequalsoer_receipt(context_mismatch); - that it verifies under oer-receipts with no
reason (
energy_oer_invalid); - that it is about the claim’s subject — its
subject.outputequals the claim’ssubjectexactly as the claim writes it, 64 lowercase hex digits (context_mismatch). oer-receipts §3 Output fixes what ajobreceipt’ssubject.outputholds: the commitment to what it delivered, which for one object is that object’s §2.1 content hash in this form; an OER receipt anenergyclaim can name is about one object and holds its content hash there. 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; - that its
provenance.howequalsgrade, and that itsenergy.joules_total, rounded to the nearest microjoule, equalsjoules_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,-0or an integer beyond that range anywhere, does not verify. The figure is compared exactly, as oer-receipts §6 compares figures:0.0120005is half-way and admits 12,000 and 12,001 µJ, and0.0120004followed 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 §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
§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 §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 §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 §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 §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:
joules_microMUST NOT be zero (energy_zero_figure): a zero is the unmetered marker, or a failed measurement, and gets no report.figure_profileMUST name a profile of energy-measurement §6.1 whose subject type is the claim’ssubject_type(figure_unknown_profile);figure_indexMUST be an integer exactly when that profile carries several figures (figure_index_mismatch); andgroupMUST be a hash exactly when the profile ismoq-jwp:object(figure_group_mismatch).- The acquisition class, as §2.4.5’s rule 2 (
energy_unknown_acquisition,energy_legacy_acquisition,energy_acquisition_evidence). - Under
OnChipCounterorCalibratedInstrument,power_readerMUST NOT benull(measurement_no_power_reader): a hardware figure is a reading, and its report names what read it, whether or not it carries a bracket. - Where
bracketis notnull, in this order, withMits busy mean less its idle mean:- each
nMUST be at least 2 (measurement_too_few_samples); - under
OnChipCounterorCalibratedInstrument, each bracket’s rail MUST have been live (energy-measurement §3.1):sd_uwnot zero (a bracket whose samples are all equal advanced once at most),longest_live_runat least 2,live_intervalsat least 5 and at least four fifths ofn(live_intervals · 5 ≥ n · 4), andlongest_live_run ≤ live_intervals ≤ n(measurement_rail_not_live); window_us,completionsandin_flightMUST NOT be zero (measurement_empty_burst);MMUST be positive (measurement_no_marginal);- under
OnChipCounterorCalibratedInstrument,MMUST be at least the idlesd_uw. A ratio below 1 is a failure to measure (energy-measurement §4) (measurement_below_attribution_floor); joules_microMUST be the figureFthe bracket gives (energy-measurement §6.2) (measurement_figure_mismatch);- under
OnChipCounterorCalibratedInstrument, the declaredwindow_usMUST be the bracket’s (measurement_window_mismatch), and the declaredrelative_ppmMUST be at least the leastpthe bracket supports (energy-measurement §6.2) (measurement_declaration_below_bracket).
- each
- Where
fabricsis present, in this order:- every fabric is
cpu,gpu,mediaornpu, and every attributiontaskorrail(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.
- every fabric is
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:
- the receipt verifies (
context_invalid); it is offigure_profile, its identity under that profile issubject, it is within the groupgroupnames, and it carries a figure atfigure_index(context_mismatch); - that figure is
joules_micro(figure_disagrees); - 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; - that class and its evidence are the claim’s, each read as energy-measurement
§2.7 reads it (
figure_class_disagrees); - an Ed25519 key signs the figure (
figure_signer_unknown) — every profile butwai:energy-binding-idhas one — and it issigner_pubkey(figure_signer_mismatch). Formoq-jwp:objectit is the group receipt’ssigner_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 §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’s urn:wai:staged-delivery:1 (energy-binding §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:
- §2.4.8’s rules 1–3. A claim restating
HwShuntis refused there, so no operating point is recorded against a figure labelled with the legacy class. - A point that is present MUST NOT be empty (
operating_point_empty_point). op_deliveredMUST NOT be present withoutop_committed(operating_point_delivered_alone).op_requestedandop_committedMUST NOT be present withoutop_delivered(operating_point_incomplete).- A claim MUST carry
op_deliveredorprediction(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_deliveredequalsop_committed, and there was none whenop_committedisnull; - the residual,
joules_micro − predicted_joules_micro, a signed integer, wheneverpredictionis 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) 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 §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:
vocabularyMUST beurn:wai:staged-delivery:1, and every point present,presentedamong them, MUST be exactly the memberwai-stage:layers, from 0 to 255 (stage_point_malformed).op_requestedandop_committedMUST NOT be present withoutop_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.stage_hashMUST be present exactly whentierisdecode-equivalence(stage_hash_without_tier).units_by_stageMUST have from 1 to 256 elements summing to at least 1, its last element not 0;presentedMUST name the lowest stage with a unit; and neitherconflictsnordeadline_missesmay exceed the number of units (stage_units_malformed).- Each object’s
layerMUST be from 1 to 255 and itsunits.countat least 1 (at least 2 for apartialobject, which applied to some units of its span and not to others), and its units MUST lie within the set’s unless it isrefusedbefore itsrefineswas checked against the set (the table above); itsreasonMUST benullexactly where the table says so and otherwise a reason of staged-delivery §4.2. Whereop_deliveredis notnull— the base’s receipt was verified first — anappliedorpartialobject’sevidenceMUST bedelivered-verified, since over such a base a layer applies only on a receipt verified over its bytes (staged-delivery §10.1); where it isnull, a layer applied whatever was verified of its receipt, and any evidence is stated. Adroppedobject’sevidenceMUST NOT bedelivered-verified. An object that arrived —applied,partial,lateorrefused— MUST havebytesabove 0, and adroppedone 0. The objects MUST be in strictly increasing order of (layer,units.from,object); theirbytesMUST sum to less thanbytes_envelopes, which counts the base’s bytes too; and no unit MAY be covered by twoappliedobjects of one layer, since the second would conflict with the first or find the unit past its stage (stage_objects_malformed). - 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
appliedobject of layer s MUST lie within the spans of theappliedandpartialobjects of layer s − 1 (for layer 1, within the set), and apartialone MUST meet them; - the units presented at stage s or above MUST NOT outnumber the units layer
s can have reached: the units its
appliedobjects cover, and, of the units inside the spans of layer s − 1 that those do not cover, those itspartialobjects cover, counting no more than its span less one for eachpartialobject, 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.
- an
- Where
op_deliveredis notnull, it MUST be the stage the objects give (stage_delivered_disagrees). Where it isnull, this rule does not apply. A statedop_deliveredis then never belowpresented: by rules 4 and 5, at every layer up to the stage presented, the spans of theappliedandpartialobjects, each verified over its bytes, hold every unit, though apartialobject 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 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 §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 §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 | The decode path within the decoder that ran, where the capability has more than one: exact for the path that computes the reconstruction the capability registers (for wai.image.jpegai, its exact decoder), or float for a path that runs a stage in floating point the registration does not fix (for wai.image.jpegai, its float decode) (determinism-tiers §5). null for a capability with one path. A NeuralIntegerExact capability’s integer path may also be named by the lowering its registration names (integer-operators §11, its ops member’s lowering), as a present entry of opus-dred §7.2 names dred-int/1 (synthesis §10.3). The entry states an exact path when its variant is null, exact, or, for a capability of the replicate class neural-int-exact, that lowering; any other term, float among them, names a path that is not exact. A sink states the path that ran, as the reference dispatch reports it, not the tier its capability is registered at. |
pin | 32 B hex or null | The parameter set the decode read, by the digest a pin over it names (SPEC §3.1, Pin by default): the sha256 of the pin the sink verified; for a decode that verified no pin, the digest a pin over the bytes it read would name, which is the SHA-256 of the one file of a capability registered with a one-file set, and otherwise the SHA-256 of the listing (wai.prior.set) of the files it read. null exactly when the decode read no parameter set. |
tier | string or null | That pin’s declared tier as SPEC §3.1 reads it: decode-equivalence, entropy-consistency or none; none for a decode that verified no pin, which nothing declared. 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. |
A decoder entry sealed by a sink of an earlier text of this extension, under which pin
named only a pin the sink verified, may carry pin and tier null for an unpinned
decode that read a parameter set. A verifier cannot tell such an entry from one whose decode
read no set.
Module members. A decoder entry whose decoder is a module: decoder that ran under the
WAISM™ profile (waism §10) carries the
five members below, and every other entry carries none of them. A verifier that implements that
profile reads them: §3.1 step 2 does not report their absence (missing_required_field); where
they are present it reads them after leaves, and checks their types; and rules 15 to 17 check
their values. A verifier that does not implement it refuses an entry that carries any of them as
unknown_member, as one that does not implement pq-signatures refuses the hybrid members
(§2.4.3), and applies none of rules 15 to 17. An entry sealed by a sink of an earlier text of
this extension has none of them. Where such an entry is a module: entry whose tier is
decode-equivalence, it was valid under that text, and a verifier that implements the profile
refuses it under rule 16 (session_module_profile_missing): an earlier text let a sink state
that tier for a module’s output with nothing of the module checked, and this one does not. Every
other entry sealed under an earlier text has the outcome it had.
| member | type | meaning |
|---|---|---|
module_profile | term | The identifier of the level the module was held at (waism §2). |
engine | string | The engine that ran it, written as waism §7 states. |
engine_settings | array of terms | The settings of waism §7 the run was under. |
module_probe | string | match where waism §6’s check 10 ran its probe, or its vector, in the sink’s process and the output matched; none where none ran. |
conform_result | 32 B hex | The SHA-256 of the RFC 8785 form, sig included, of the conform-result/1 record that attested the module (conformance-results §2; waism §6, check 9c). |
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 §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 §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:
primary_unitsandfallback_unitsMUST sum tounits(session_path_sum).- The entries’
leavesMUST sum toleaf_count;leaf_countMUST NOT be belowunits; andunitsis 0 exactly whenleaf_countis (session_leaf_sum). output_rootMUST benullexactly whenleaf_countis 0 (session_root_presence).threadsMUST be a power of two (session_host_malformed).- Each decoder entry MUST be as its table says:
decoderof its grammar;renditionat most 65535;tiernullexactly whenpinis, and otherwise one of its three values;module_statusnullexactly for abuiltin:decoder, and otherwisepinnedorfetched;leavesat least 1. No two entries MAY be equal in every member butleaves(session_decoder_malformed). - A
tierthat is notnullMUST NOT exceed the class tier of its entry’scapability(SPEC §3.1 Tier) (session_tier_exceeds_class). - Each switch’s
fromMUST differ from itsto, and itsreasonMUST be a term of delivery-telemetry §5. The switches MUST be in non-decreasing order ofunit, and as many asswitches_total, or 1024 where it is more (session_switch_invalid). missedMUST NOT exceedleaf_count, and MUST be 0 whendeadline_usisnull.missed_unitsMUST hold as many entries asmissed, or 256 where it is more, in strictly increasing order of (unit,layers), each withlayersfrom 1 to 65535 andlate_usat least 1 (session_missed_invalid).stage_setsMUST be in strictly increasing order of (first_unit,claim) (session_stage_sets_invalid).counted_discardedMUST NOT exceed the sum ofcounted_primaryandcounted_fallback(session_counted_invalid).classical_sourceMUST benullexactly whenclassical_declaredis, and otherwisewai2-table(session_classical_invalid).- Where
energyis notnull, itsjoules_microMUST 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). - A decoder entry whose
capabilityhas the replicate classneural-int-exact(SPEC §7) MUST state an exact path:variantnull,exactor the lowering its registration names (session_variant_not_exact). A decode of such a capability by any other path is not a decode of it (determinism-tiers §3, No tier between the two, rule 1). - A decoder entry that does not state an exact path MUST NOT have
tierdecode-equivalence(session_tier_exceeds_variant). Frames a path that is not exact decoded are not decode-equivalent, whatever the pin declares (determinism-tiers §3, rule 2). Rules 6 and 13 already refuse such an entry for every capability the registry classifies; this rule reaches the others, such as a capability a module decodes. - A decoder entry’s module members MUST be all five or none, and none on a
builtin:entry;module_profileMUST be one of the identifiers of waism §2,engineof the grammar of waism §7,engine_settingsterms of that section’s table in ascending byte order with none twice, andmodule_probematchornone(session_decoder_malformed). - A
module:decoder entry whosetierisdecode-equivalenceMUST carry the module members, withmodule_profilethe integer level’s identifier (session_module_profile_missing): a module’s output holds that tier only at the integer level (waism §2). - A decoder entry whose
module_profileis the float level’s identifier, and whosetieris neithernullnornone, MUST havenan-canonicalamong itsengine_settingsandmodule_probematch(session_module_engine_not_deterministic): a float-level module’s output holds a tier only on an engine that passed the probe (waism §6, check 10).
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:
- The list has
leaf_countentries and no (unit,layers) twice, and the root of its output tree isoutput_root(session_output_mismatch). - It agrees with the members it gives (
session_output_inconsistent): everydecodernames an entry and everylayersis at least 1; its distinct units numberunits; each entry’sleavesis the number of presentations naming it; the entries are in first-use order;primary_unitsandfallback_unitscount the units whose presentation at their largestlayersnames an entry on that path; and everymissed_unitsentry names a presentation of the list. - A verifier that holds the objects MAY decode a presentation again where its
entry’s
capabilityhas the replicate classneural-int-exactorstandard-defined(SPEC §7) — the entry’s object’s unitunit − first_unit, atlayers— and then MUST find itsout_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:
- No
content_hashis listed twice with onepath(session_object_duplicate). - The session’s
subject, its entry object, is the content hash of an object received (session_entry_not_received). - So is each decoder entry’s
object(session_source_not_received). counted_primaryandcounted_fallbackare the sums ofcountedover the objects of each path,counted_discardedits sum over the objects not presented,uncountedthe number of objects whosecountedisnull, andtransferredthe sum oftransferred(session_counted_mismatch).- An object is
presentedexactly when a decoder entry’sobjectis 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
§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:
- Where the verifier holds the objects as well, each envelope is among the objects
received, by its content hash (
session_source_not_received). - Where it holds the objects as well, each object listed with an envelope’s content hash
states as
countedthat envelope’s counted bytes (staged-measurement §3), andnullexactly when it is not countable (session_counted_mismatch). - 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-formedsha256(rule 4), the units a base holds, a layer’sWIR1payload and the span read against its base’s units (rules 5 and 8) — which are the signer’s statement. - Each decoder entry whose
objectis a held envelope names what that envelope gives, as a sink dispatches it: of aWAI2envelope, arendition, 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 statedsha256(SPEC §7.1, Selection algorithm, Component identity), and of an encrypted envelope whosesha256, where stated, is well formed: 64 lowercase hex digits, the only form a payload can match, so a rendition whosesha256is not (uppercase, another length, not hex) is one no sink decodes, whatever its payload; of aWAI1envelope that is not a layer, norenditionand the manifest’scapabilityor itsfallback, a capability a sink dispatches, in a manifest whosecapabilityis not companion-only (SPEC §4); of aWAI1layer, norenditionand itscapability. And each decoder entry whoserenditionis notnullhas itsobjectamong the held envelopes, which hold everyWAI2envelope 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. - Each decoder entry whose
objectis 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
WAI1manifest, itsfallbackisnull(capability-not-refinement), and itsrefinesis present and reads as staged-delivery §3 defines it (refines-malformed); - a
WAI2rendition is offered only from an envelope every component of which conforms (SPEC §7.1, Refinement components; the referenceWaiMulti::check_component_roles): a refinement component that does not, or a companion, anidor 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
WAI2rendition — 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
WIR1for 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).
- in a
- Each decoder entry whose
objectis a held layer that is applied over an entry has thefirst_unitof 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. - 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 + 1for a layer (session_layers_mismatch). - 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
unitis at leastfirst_unit + aand belowfirst_unit + a + n, where for a base, or a rendition that is not a layer,ais 0 andnthe units its payload holds — aWIV1clip’s frames, and one for aWIH1picture, aWIA1clip and aWIS1tensor — read in the payload’s own format: of the formats SPEC §5 pins for aWAI1manifest’scapabilityand itsfallback, 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,aandnare itsspan’sfromandcount, 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. - The lengths of the held
WAI2envelopes’ classical renditions whose capability no decoder entry decoded — encrypted or not, since the rendition table declares them in the clear — sum toclassical_declared, which isnullexactly when there is none (session_classical_mismatch). In this revision a held envelope is one held in full: of aWAI2envelope 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:
- each passes §3.1 steps 1–4 (
context_invalid); - each is a
stageclaim, signed by this claim’ssigner_pubkey, and itslink_hashis theclaimof an entry ofstage_sets(context_mismatch); - 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,lateorrefused, with bytes — are among the objects received (session_stage_disagrees); - each decoder entry whose
objectis thesubjectof one of them, or a layer object one lists, has as itsfirst_unitthefirst_unitof an entry ofstage_setsnaming 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; - each of them that states a
stage_hash, a clip digest over its set’s units (staged-delivery §5), has every decoder entry whoseobjectis itssubjectatdecode-equivalenceby that section’s base step (a capability of the replicate classstandard-defined, or ofneural-int-exactwithtierdecode-equivalence), and every decoder entry whoseobjectis itssubjector a layer object it lists states an exact path (session_stage_hash_not_exact). A sequence any frame of which was decoded at a weaker tier, or by a path that is not exact, has no clip digest (determinism-tiers §3, No tier between the two, rule 3). The check is per object: an entry that names the base or such a layer counts whichever units it presented, so the outcome is the same whether or not the verifier holds the outputs; - where the verifier holds the outputs as well, for each claim and each entry of
stage_setsnaming it, withfthat entry’sfirst_unitandnthe units the claim counts (session_stage_disagrees):- every unit from
ftof + n − 1is presented; - from below: for every stage
sfrom 1 up, the units among them presented at more thanslayers are no fewer than the units the claim counts at stagesor 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 astageclaim counts and never less;
- every unit from
- where it holds the outputs, from above: every unit of an entry’s units presented at
more than
slayers lies in the span of a layer-sobject that a claim named by an entry holding the unit lists asappliedorpartial(session_stage_disagrees). A unit reaches a stage only through a layer the sink applied: alatelayer missed the deadline of every unit it covers and arefusedone 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 §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 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 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 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.4.16. class: "schedule"
A publisher’s plan of when each object of a group passes on a bearer instance with no return
path, and with which forward error correction: for each object, its code (RaptorQ, Raptor or
Compact No-Code) and that code’s object transmission information, the repair symbols each pass adds,
and the times and count of its passes. It attests a plan, not a transmission or a delivery, and
it sits beside the group receipt and the instance’s transmission claim, whose bytes it leaves
unchanged. carousel defines it: its body’s members are §4.1 there
(bearer_id, objects and attests), read at §3.1 step 2; its class rules are §4.2 there, with
§5’s bounds on each code’s parameters, 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, and the object receipts
and envelopes at the positions it names. A node that declares an instance of its own states its
plan for it in the same class, signed by its own key: cache-authority §5.6
replaces the check that the group’s key signs it, in the verdict that holds that instance.
2.4.17. class: "timeliness"
A publisher’s statement of when each object of a group is of use: the instant it is useful from,
the deadline by which a receiver must hold it for its full value, how that value falls after the
deadline, and the renditions a receiver presents in its place when it does not hold it in time, a
ladder in order of falling value. It attests a value over time, not a transmission or a delivery, and sits beside the group receipt,
whose bytes it leaves unchanged, one statement for every bearer. timeliness
defines it: its body’s members are §4.1 there (objects 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, the object receipts and
envelopes at the positions it names, and each rung’s group receipt, object receipt and
envelope.
2.4.18. class: "cache-transmission"
A node’s declaration that it sent, from what it held, objects of a group another key signed, over
a bearer instance of its own with no path back on it: 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 attests transmission, never delivery, and it is never in the publisher’s chain.
cache-authority defines it: its body’s members are §5.1 there (bearer,
bearer_id, leaf_count, sent, sent_root, energy 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):
{
"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:
{
"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:
{
"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:
{
"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 §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:
- the declaration passes §3.1 steps 1–4 (
context_invalid). Its members,signer_pubkeyamong 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; - the declaration is a
soft-bindingor asoft-binding-spanclaim (context_mismatch); declarationis itslink_hash, andsubject,algandbinding_typeequal the declaration’s (recovery_declaration_mismatch);- the declaration’s
signer_pubkeyis P (recovery_signer_mismatch) — §3.2 step 3’s check, without which a record could carry a recovery that §3.2 itself refuses; - for a
soft-binding-spandeclaration: R is within its span (§2.6.4,recovery_outside_span), andobservedis not R’scontent_hash(recovery_of_hard_bound_object); valuematches the declaration’svalueunder 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 §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. A reconciliation (§2.4.15) has a compact form for return paths whose messages hold few bytes, bearer-receipts §13, which a receiver expands to the claim’s canonical form before it verifies it.
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, 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 §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
OnChipCounterfiltering byte above2, a calibration reference that is not UTF-8. - A coverage byte is not
0or1; a parent flag is not0or1, or is0and the 32 bytes after it are not zero;leaf_versionis not1or2.
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).
-
Parse and shape-check. Read the group receipt. If
kind != "group", reject (wrong_kind). Ifsigis 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, asign_versionother than2, or a class that cannot be read, reject (group_label_invalid). Ifsigner_pubkeydoes not decode under RFC 8032 §5.1.3 or is of small order — §3.1 step 3’s key rule — reject (bad_signer_key). -
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. Verifysigagainstsigner_pubkeyunder §3.1 step 3’s rule —S< L,Rnot 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’sroot_hashfield, now bound tosigner_pubkeyunder signature, and for a labelled group the total’s class. A hybrid group (pq-signatures §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. -
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), neverchain_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
nullparent starts the chain, and any other parent isparent_not_held; - with the chain’s head held, a parent that is the head’s link hash
links the group; a
nullparent ischain_break(a second chain head), except for a group whosegroup_idlies inside the gap a join’s anchor left below it (Joining, History): there anullparent is the chain’s true start, and the group isparent_not_helduntil the group above it names its link hash; a parent that is the link hash of another group of the chain the verifier holds ischain_break(a fork); any other parent names a group the verifier does not hold, and isparent_not_held.
parent_not_heldis 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 §8.1). A verifier that keeps only the head cannot tell a fork from a gap and reports bothparent_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_heldand 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 whosegroup_idlies 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 lowestgroup_idbelow 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_idexceeds 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
nullparent 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_idlies 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_breakstays for a group naming a parent that already has a child on the chain, or for a second chain head. A group with anullparent 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 agroup_idabove 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’sgroup_idand, after a re-anchor, thegroup_idof 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 signgroup_id. A verifier therefore MUST refuse a legacy group whosegroup_iddoes not exceed its chain head’s (group_id_not_increasing), whatever it does for labelled groups:group_idis 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_iddoes not exceed its chain head’s (group_id_not_increasing), and a group whosegroup_endis outside its freshness window (group_not_fresh), unless its signedgroup_idis 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-bytesig, andBLAKE3("moq-jwp:receipt-link\x01" || canonical_bytes(G))is G’s link hash: the identity aclaimnames G by (§2.4.3), and aWAI_RECEIPTproperty’sgroup_receipt(moq-streaming-format §5). For a group in the legacy form it does not coverparent_receipt_hash, which that payload does not sign either; the labelled payload signs it. -
Verify the object’s content hash. For each accompanying
kind: "object"receipt: recomputecontent_hashper §2.1 over the received WAI envelope bytes and compare to the receipt’scontent_hashfield. 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, ismalformed. 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 §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 §7.1), is checked over that envelope, which the verifier builds from the verified head and the component’s bytes (http-delivery §8.3). -
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_indexis below the group’sleaf_count;merkle_proof.siblingsholds exactly ⌈log₂leaf_count⌉ hashes (none for one leaf);direction_bitsequalsleaf_index, so it holds the index’s bits and no higher ones;object_idequalsleaf_index; andtrack_aliasandgroup_idare 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.siblingslist from level 0 upward; at each leveli, combine the current value with the sibling using thedirection_bitsbiti(1 = current is right child, 0 = current is left child) and themoq-jwp:merkle-node\x01domain. After walking all siblings, compare the resulting hash to the verified root from step 2. If they differ, reject (inclusion_proof_failed).
- 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
(
-
Accept. The object’s bytes are bound to
signer_pubkeyat(track_alias, group_id, object_id). Where the object’s leaf is labelled, itsjoules_micro, its class, the class’s evidence and itsoriginare bound with them. Where it is not,joules_microandoriginare 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 keysigner_pubkeyis, is §7’s to confirm; a group receipt needs the authoritydelivery, 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 §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 arelative_ppmat 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.
- 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 ismalformeda value holding a\uescape 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 nevermalformed: every JSON number is JSON, whatever its size, and one a member may not hold (1e400,-0, an integer of four hundred digits) isfield_typebelow.kindabsent ismissing_required_field;kindother than"claim"iswrong_kind. Any §2.4.3 member absent ismissing_required_field; any other member present isunknown_member.classthat is not a string isfield_type; equal todeliveryit isclass_reserved, and a string naming no class of §2.4.4 isunknown_class. Then, in table order withclassdone andbodyleft to step 2, a member of the wrong type or encoding —subject_typeoutside its enumeration, a hash that is not 64 lowercase hex digits,seqnot an integer of §2.4.3,issued_atnot a date-time of §2.4.3,signot 128 lowercase hex digits — isfield_type. Asubject_typethe class does not admit issubject_type_not_admitted. - Body. A
bodythat is not an object isfield_type. Otherwise it is checked against the class’s member table (for a lawful-basis body, the table of the record thatrecord, read first, names; see §2.4.6): a member absent ismissing_required_field(but ameasurementbody’sfabrics, which may be absent, §2.4), an additional member isunknown_member, and then, in the table’s order, a member that is not of the type the table gives it isfield_type; afabricsthat isnullisfield_type, and within it, as in a bracket, a member absent ismissing_required_fieldand an additional oneunknown_member(stepsbeforeshares, 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’sopening, a build’sprofile, a stage’stier, a stage object’sstatusandevidence, a consumption claim’s usagevocabulary, a transmission, reconciliation or cache-transmission claim’sbearer, a schedule entry’sfecandesi, a gap’sreason, a session’sframing, its host’sprofile, and the paths of its decoder entries and switches); a date-time that is not one of §2.4.3 (a gap’sobserved_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 malformedoer_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, anenergy_uncertainty, a span, a timeliness entry, its decay points and its fallback — 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 neverfield_typehere: labels and evidence from vocabularies another extension defines (acquisition class and its evidence, coverage, grade); ajoules_microorwork_unitsof zero; an emptypurposes; an emptybasis, which is why its type isterm or ""; an emptylog_receiptorvalue; analgthat is a term but not in registry form; aresolution_endpointthat is an absolute URI but not anhttpsURI with a host and no userinfo; how a span’s members pair and order; anattestsother 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 astageclaim, 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 areconstruct-sessionclaim, 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 atransmissionorreconciliationclaim, itsattests, a transmission’s energy figure and a reconciliation’s bitmap; in ascheduleclaim, itsattests, the order of its entries, and each entry’s code parameters, symbols and passes; in atimelinessclaim, itsattests, the order of its entries, and each entry’s window, decay, fallback and ladder; in acache-transmissionclaim, itsattests, itsbearer, its bitmap and its energy figure; and, in aconsumptionclaim, the order of its objects, usage entries and seen digests, its subject, its terms and itssigner_id. - 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,Rnot of small order, and the cofactorless equation withRcompared byte for byte.signer_pubkeyisbad_signer_keyif 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 — andsigissignature_verify_failedif 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 §4.2 (hybrid_requiredunder 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). - Class rules, per §2.4.5–§2.4.18 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.
- 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-nullparent_receipt_hashwith no previous claim held isparent_not_held— recoverable by fetching that claim, if it verifies, and MUST NOT be accepted silently. A parent that is not the held claim’slink_hash, aseqthat does not increase, or anullparent while a previous claim is held, ischain_break. By the previous claim alone a verifier cannot tell a fork from a gap, a parent it has not yet obtained, and reports bothchain_break. A gap is recoverable asparent_not_heldis: the verifier MAY obtain the missing claims and read the chain again in its order, and MUST NOT accept the claim silently meanwhile. A fork stayschain_break, since no claim it obtains is the parent. - Context: checks against other receipts the verifier holds,
each verified first — an
energyclaim against theobjectreceipt 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); ameasurementoroperating-pointclaim against the receipt that carries its figure, verified under that receipt’s own profile (formoq-jwp:object, the object receipt accepted under §3 against the group receiptgroupnames) (§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; adelivery-gapclaim against the object receipt at its coordinates, verified by inclusion at least (§2.4.11); areconstruct-sessionclaim against its outputs, the objects it received, theWAI2envelopes it received and thestageclaims it names, in that order, each that the verifier holds, thestageclaims verified under steps 1–4 (§2.4.12); aconsumptionclaim against the objects it names, with their receipts, the usage statements that govern them and its room, as ai-usage §6.3 orders them (§2.4.13); atransmissionclaim against the group receipt its subject names, and areconciliationclaim against that group receipt and the object receipts at the positions it names, each verified by inclusion at least, as bearer-receipts §4.3 and §5.3 order them (§2.4.14, §2.4.15); ascheduleclaim against the group receipt its subject names and the object receipts and envelopes at the positions it names, as carousel §4.3 orders them (§2.4.16); and atimelinessclaim against the group receipt its subject names, the object receipts and envelopes at the positions it names, and each rung’s group receipt, object receipt and envelope that the verifier holds, as timeliness §4.3 orders them (§2.4.17); and acache-transmissionclaim against the group receipt its subject names and the object receipts at the positions it names, each verified by inclusion at least, as cache-authority §5.3 orders them (§2.4.18). A group receipt is verified here under §3 steps 1 and 2, and one that fails them iscontext_invalid. A declaration is verified here under steps 1–4, and one that fails them — or is not JSON — iscontext_invalid: its members,signer_pubkeyamong 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-bindingorsoft-binding-span(context_mismatch), then the class’s own checks. A context receipt of another class iscontext_mismatch. For anenergyclaim, anobjectreceipt about another subject, and an OER receipt other than the one named or about another subject, arecontext_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_mismatchorrecovery_declaration_mismatch; for a recovery record, a declaration signed by anyone but that publisher is thenrecovery_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; or an entry’s module members are not all five or none, are on a builtin: entry, or do not take the values rule 15 gives them |
| 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 | session_variant_not_exact | a decoder entry names a capability of the replicate class neural-int-exact and does not state an exact path: a variant other than null, exact or the lowering its registration names |
| 4 | session_tier_exceeds_variant | a decoder entry does not state an exact path, and its tier is decode-equivalence |
| 4 | session_module_profile_missing | a module: decoder entry whose tier is decode-equivalence does not name the integer level of the WAISM profile |
| 4 | session_module_engine_not_deterministic | a decoder entry at the float level of the WAISM profile, with a tier that is neither null nor none, lacks nan-canonical or a matching probe |
| 4 | consumption_objects_unordered | a consumption claim’s objects are not in strictly ascending order (ai-usage §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 §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 |
| 4 | schedule_not_scheduled | a schedule claim’s attests is not scheduled (carousel §4.2) |
| 4 | schedule_objects_malformed | it has no entry, or its entries’ leaf_index values do not strictly increase |
| 4 | schedule_oti_invalid | an entry’s code parameters are not ones its code admits (carousel §5.1, §5.2, §5.5) |
| 4 | schedule_symbols_invalid | an entry’s symbols are not ones its code can name (carousel §5.4) |
| 4 | schedule_passes_invalid | an entry’s passes are not well formed: none, of no duration, overlapping, ending past 9999, or sending more than 253 − 1 octets |
| 4 | timeliness_not_valued | a timeliness claim’s attests is not valued (timeliness §4.2) |
| 4 | timeliness_objects_malformed | it has no entry, or its entries’ leaf_index values do not strictly increase |
| 4 | timeliness_window_invalid | an entry’s from_ms is after its by_ms, or its by_ms is not below the last instant, 9999-12-31T23:59:59.999Z |
| 4 | timeliness_decay_invalid | an entry’s decay is not well formed: not 1 to 16 points; instants not strictly increasing from after by_ms to at most the last instant; a value at or above 1,000,000, above the one before it, a last one other than 0 or another one of 0; or a point but the last on the line through its neighbours |
| 4 | timeliness_fallback_invalid | a rung of an entry’s fallback is valued at 0 or above 1,000,000, has the entry’s content hash, or names the entry’s own position |
| 4 | timeliness_ladder_invalid | an entry’s fallback is an array of other than 2 to 8 rungs, whose values do not strictly decrease, or two of whose rungs have one content hash or one position |
| 4 | cache_transmission_not_transmitted | a cache-transmission claim’s attests is not transmitted (cache-authority §5.2) |
| 4 | cache_transmission_on_bidirectional | its bearer is bidirectional |
| 4 | cache_transmission_bitmap_malformed | its leaf_count is 0, its sent 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 §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 §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 | schedule_subject_mismatch | a schedule claim’s subject is not the link hash of the group receipt it is checked against (carousel §4.3) |
| 6 | schedule_signer_mismatch | it is not signed by the group’s key |
| 6 | schedule_leaf_out_of_range | an entry’s leaf_index is not less than the group’s leaf_count |
| 6 | schedule_before_group | an entry’s first pass starts before the group’s group_start |
| 6 | schedule_content_mismatch | an entry’s content_hash is not that of the object receipt the verifier holds at its position |
| 6 | schedule_length_mismatch | an entry’s transfer length is not the length of the object’s envelope the verifier holds |
| 6 | timeliness_subject_mismatch | a timeliness claim’s subject is not the link hash of the group receipt it is checked against (timeliness §4.3) |
| 6 | timeliness_signer_mismatch | it is not signed by the group’s key |
| 6 | timeliness_leaf_out_of_range | an entry’s leaf_index is not less than the group’s leaf_count |
| 6 | timeliness_before_group | an entry’s from_ms is before the group’s group_start, whole seconds compared |
| 6 | timeliness_content_mismatch | an entry’s content_hash is not that of the object receipt the verifier holds at its position |
| 6 | timeliness_on_layer | an envelope the verifier holds whose content hash is an entry’s or a rung’s is a WAI1 of a refinement-only capability |
| 6 | timeliness_fallback_mismatch | a rung’s leaf_index is not less than the leaf count of its group, where the verifier holds that group receipt, or its content_hash is not that of the object receipt the verifier holds at its position |
| 6 | timeliness_fallback_not_derived | an envelope the verifier holds whose content hash is a rung’s is not a WAI1 whose derived_from names the entry’s content_hash as its motif |
| 6 | cache_transmission_subject_mismatch | a cache-transmission claim’s subject is not the link hash of the group receipt it is checked against (cache-authority §5.3) |
| 6 | cache_transmission_by_publisher | it is signed by the group’s key |
| 6 | cache_transmission_leaf_count_mismatch | its leaf_count is not the group’s |
| 6 | cache_transmission_objects_not_held | it names a position whose object receipt the verifier does not hold: recoverable, never accepted |
| 6 | cache_transmission_root_mismatch | its sent_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 |
| 6 | session_stage_hash_not_exact | a stage claim the session names states a stage_hash, and a decoder entry names its base at a step weaker than decode-equivalence, or names its base or a layer it lists without stating an exact path, whichever units it presented |
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.
- Verify D under §3.1 steps 1–4. A rejection of D is the recovery’s rejection, reported unchanged.
- D MUST be a
soft-bindingclaim (context_mismatch). - D’s
signer_pubkeyMUST 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. - 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. - Evaluate V against D’s
valueunder the algorithm’s own rule. A verifier with no matcher for the algorithm MUST NOT report a recovery (recovery_no_matcher); no match isrecovery_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, 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 §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 §5 |
| carried over MoQ | moqt_mismatch | as moq-streaming-format §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 §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 §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_hashfield 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 (
WAI1magic, manifest bytes, payload bytes) end-to-end. Any rewriting at the envelope layer invalidates §2.1 content hashes. - Forward
claimreceipts (§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-verifiedis 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_totalacross 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 renumberobject_id. - Sign a
delivery-gapclaim (§2.4.11) for an object it did not forward, under its own key, as the parallel receipt §4.3 permits. - Sign a
cache-transmissionclaim (§2.4.18) for objects it sends again, from what it holds, over a bearer instance of its own, as the parallel receipt §4.3 permits. A node that is not a relay (§4.5) may sign one too. - Derive new objects from those it forwards and publish them as a publisher of its own (deriving-edge): 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_hashfield, even when its value isnull. 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 §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 §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 §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/, 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 §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/, 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
where §7.9’s checks 1 to 8 pass, or fallback or inert at one of
those checks 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 bytes its module vectors give are not
WebAssembly modules, and module runs neither of checks 9 and 10, which
are the WAISM profile’s: waism §9 defines
their cases. 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
of §7.9’s checks 1 to 8, 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 1 to 14 and of its
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. No session in it carries the module members
of §2.4.12, and its two verifiers do not implement the WAISM profile: each
refuses those members as unknown_member and applies none of rules 15 to 17, as §2.4.12
states for a verifier that does not implement that profile. The decode path an entry states has a
refusal for each of rules 13 and 14 and for a stage hash over a base or layer decoded at
a weaker tier or by a path that is not exact, beside accepted entries for each of
wai.image.jpegai’s two paths and for an integer path named by its registered lowering. 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 samesigner_pubkeyandtrack_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_idis monotonic per (signer_pubkey,track_alias), and both payloads sign it. A verifier MUST refuse a legacy group whosegroup_iddoes 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 highestgroup_idseen 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_idabove 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_aliasat0has 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). 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 whosegroup_endis 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 highestgroup_idof 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 whosegroup_idis 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 signgroup_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’sgroup_id. The reference verifier (jwp_delivery::ReceiptChains) holds every legacy group to thegroup_idguard 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 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
measurementclaim (§2.4.8) also carries the measuring machine’s power levels, thread count, build and power reader in cleartext. - Pricing semantics for joules.
joules_microandjoules_totalare 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 §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-bindingdeclaration 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_idnames: a valid log of that origin, or thesigner_iditself (§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_idas adid:webDID 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 theoriginlabel of anobjectreceipt (§2.4.2). Its key log is at the origin followed by/.well-known/wai-keys, and nowhere else. The suffixwai-keysis 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 charactersdid:key:zfollowed by the base58btc encoding (the Bitcoin alphabet, each leading zero byte written as1) of the bytes0xed,0x01and 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:keyanddid:webstrings 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 §5), and of the consent records withdrawn in its rooms (capture-binding §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 §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:
- 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). - Its
originis the origin being checked (origin_mismatch). - Its
sigverifies under itsrootby §3.1 step 3’s rule, which also refuses arootthat does not decode or is of small order (key_log_signature_invalid). - Its
seqis one more than its parent’s, and itsissued_atis not earlier than its parent’s (key_log_chain_break). Itsprevis its parent’srevision_hashby the definition of a parent; between consecutive revisions of a log being anchored (§7.3), aprevthat is not the previous revision’srevision_hashis alsokey_log_chain_break. - Either its
rootandnext_rootare its parent’s, or itsrootdiffers from its parent’s and has its parent’snext_rootas 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. - It erases and rewrites nothing: its
keysbegin with its parent’s entries, in the same order, each with the sameid,keyandmay_sign; astatusstays, or moves fromactivetoretiredorrevoked, or fromretiredtorevoked; a time, once set, does not change; and itsrevoked_modulesbegin 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. - 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 itsnext_rootcommits 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 iskey_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 §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 therevision_hashof 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):
signer_idis origin-bound (§7.1). Adid:keynaming a key other thansigner_pubkeyis refused (signer_id_names_another_key); any other form is unconfirmed (signer_id_not_origin_bound).- The verifier has anchored the origin and holds the anchor
(
origin_not_anchored). - 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). - The head lists an entry whose
idis thesigner_id’s key-id (key_not_listed) and whosekeyissigner_pubkey(key_mismatch). - 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. Aretiredentry fails (key_retired) when the item’s signed time is later thanuntil, 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 everyuntil. - The entry’s
may_signholds 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 §3: a trust anchor delivered over the bearer it authorises is not one).
- 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 (
- 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 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_idnames. - 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 §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_hashin 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, reconciliation, schedule, timeliness and cache-transmission. 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 |
the energy figure of a cache-transmission claim whose energy is not null | cache-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 §3, §4), or a room usage record (ai-usage §4.2) | live/room |
| an upload receipt of a take (capture-binding §6) | capture/store |
| an impression task file (impression-aggregates §5.4) | aggregate/task |
| a per-hop session record (live-metering §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 §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 §5) | live/watermark |
| a derivation record (deriving-edge §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 holdings record’s holder (holdings §4), and the upstream of a settlement acknowledgement or declaration (settlement §3, §4), placed as a serving node (cache-authority §4.4) | cache/serve |
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 — aggregate/task,
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 §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 givesdelivery. Beside a confirmed item that needs a hardware-class authority, a verifier reports whether its key also holdsdelivery(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, soalso_deliversis true there by construction, and is reported as such. Anenergyclaim 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/declarationis 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
measurementclaim needsmeasurementand the energy authority of the class it restates; anoperating-pointclaim needsoperating-pointand that energy authority too. A report of a hardware-class figure is therefore confirmed only under a pinned anchor, and is reported withalso_delivers. §2.4.8’s check 5 already requires ameasurementclaim’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 giveoperating-pointto the party that records requests and commitments without letting it state figures of its own. - Stage and delivery gap.
stagelets a key sign what its holder observed of a stage set (§2.4.10), anddelivery-gapwhat a relay did not forward (§2.4.11). Neither is pinned-only, and neither isdelivery: an origin givesstageto the sinks it speaks for anddelivery-gapto its relays, and a key that holdsdeliveryalone signs neither. - Reconstruct session.
reconstruct-sessionlets a key sign what a sink reconstructed (§2.4.12). It is not pinned-only and it is notdelivery: 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 needsreconstruct-sessionand the energy authority of its class, every one of them, and is reported withalso_deliverswhere 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 holdsreconstruct-sessionand 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.
transmissionlets a key declare the bearer a group it signed was sent over (§2.4.14), andreconciliationlets a key sign what a sink received of a group (§2.4.15). Neither is pinned-only and neither isdelivery: an origin givestransmissionto its publishing keys andreconciliationto the sinks it speaks for, and a key that holdsdeliveryalone signs neither. A transmission’s energy figure is a second item, as a session’s is: it needstransmissionand 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 §8).schedulelets a key state the passes of a group it signed over an instance (§2.4.16), or of a group another key signed over an instance it declares as its own (cache-authority §5.6); an origin gives it to its publishing keys, as it givestransmission, a node’s operator to the nodes that schedule their instances, and it is neither pinned-only nordelivery.timelinesslets a key state when the objects of a group it signed are of use (§2.4.17); an origin gives it to its publishing keys, as it givesschedule, and it is neither pinned-only nordelivery. - 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 withsoft-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/decoderlets a key grant a decoder module, or a parameter set pinned by an envelope, for the capabilities the grant names;module/reconstructlets it grant a reconstruct module (§7.9). Neither is pinned-only; §7.8 says what a module authorised under a fetched anchor is. A key that holdsmodule/decoderalso signs theconform-result/1record that attests a decoder module to the WAISM profile (waism §6, check 9c). Such a record carries nosigner_id, so it is no item §7.4 confirms: a sink finds its key in the head that the module’s grant was confirmed under (§7.9, check 3). - Rooms.
live/roomlets a key act as a room authority: sign room descriptors and the grants under them (live-rooms §5). It is not pinned-only, and it is notdelivery. - Stores.
capture/storelets a key act as a store a room’s takes are uploaded to: sign the upload receipts of a take (capture-binding §6.4). It is not pinned-only, and it is notdelivery. - Aggregation tasks.
aggregate/tasklets a key sign an impression task file: name a DAP task, its two aggregators and its collector, which receivers on a bearer with no return path report to (impression-aggregates §5.4). It is pinned-only, because a task decides which two aggregators receive every receiver’s shares and so gives up part of unidirectional-delivery §2’s “the source learns nothing”, and a fetched anchor is authority taken on first use. It is notdelivery. - Watermark edges.
live/watermarklets 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 §5.6). It is not pinned-only, and it is notlive/relayordelivery. - Deriving edges.
edge/derivelets a key act as an edge that derives objects from an origin’s content and publishes them under its own key (deriving-edge §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 notdeliveryorlive/relay. - Caches and peers.
cache/servelets a key act as a node that serves, from what it holds, objects of groups another key signed (cache-authority §4): sign its holdings records, sign its declarations, and be theupstreamof the acknowledgements its receivers sign.cache-transmissionlets a key declare a bearer instance of its own over which it sent such objects (§2.4.18), asdelivery-gaplets a relay sign what it did not forward. The origin that gives them is the node’s operator’s, not the publisher’s: a cache changes no byte, so the publisher’s receipts verify whatever it serves. Neither is pinned-only, and neither isdelivery,transmission,live/relayoredge/derive: a cache publishes nothing, declares none of the publisher’s transmissions, signs no room’s hop record and derives nothing. Acache-transmissionclaim’s subject is the publisher’s group, so a verifier expects an origin for one only where its relying party names one, as for a session. - Pre-emption (unidirectional-delivery
§3), and relay attestation (§4.3) other than a
delivery-gapclaim, acache-transmissionclaim 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
idbefore the successor signs, and retiring the old key with anuntilno 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 §5); a consent record is withdrawn by adding its digest there (capture-binding §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 §3), so that a fork is visible to any Relying Party that checks the log. Registration anchors nothing (transparency §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 §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); itswai-keystool 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) — 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 §4). A sink that holds a capability’s pinned sets to this section runs the checks below as SPEC §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
§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 §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:
- The grant has the form above, its
signer_idis origin-bound and names the grant’sorigin, and itssigverifies under itssigner_pubkeyby §3.1 step 3’s rule (module_grant_invalid). - The bytes’ digest is the grant’s
module(module_digest_mismatch). - The grant passes §7.4 as an item needing
module/<form>, with itsnot_beforeas 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. - The sink holds a status for that origin naming the head §7.4 step 3
reached; of those, the latest is fresh: its
issued_atis not later thannowand at most that head’sstatus_max_age_sseconds 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 withmodule_status_not_descended. - The grant’s
moduleis not in therevoked_modulesof 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). not_before≤now(module_grant_not_yet_valid) andnow≤not_after(module_grant_expired).not_after−not_beforeis at most the head’sgrant_max_s(module_grant_too_long).- The capability being decoded is one of the grant’s
capabilities(module_capability_not_granted). - Where the module is a decoder module the sink would run on a WebAssembly engine, it passes
the WAISM profile’s check 9 at one of its levels: its rules R1 to R12, and a
record that attests it, signed by a key that the head this grant was confirmed under lists
with
module/decoder(waism §6) (module_profile_violation,module_unattested). - It passes that profile’s check 10: where it passed check 9 at the float level and the
object’s need (waism §3) is above
none, the sink’s engine is set to the deterministic profile’s NaN rule and its probe matched; and, at either level, a probe the sink ran for it matched (module_engine_not_deterministic,module_probe_mismatch).
Checks 9 and 10 pass for a pinned parameter set, a reconstruct module and a decoder module the sink runs on no WebAssembly engine, which are none of the modules they name. A decoder module the sink would run on a WebAssembly engine is in their scope whatever its bytes: a component, or bytes that do not decode as a WebAssembly module, fail check 9 (waism §4.1, R2). A module that passes every check is instantiated and run as waism §6’s checks 11 to 13 state.
now is the sink’s trusted clock, as a log date-time. Checks 1 to 8 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 §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 checks 1 to 8. 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 §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 |
| 9 | module_profile_violation | the module breaks a rule of the WAISM profile at every level (waism §4) |
| 9 | module_unattested | the sink holds no record that attests the module for the capability, signed by a key the grant’s head lists with module/decoder (waism §6, check 9c) |
| 10 | module_engine_not_deterministic | the module is at the float level, the object needs a tier, and the engine is not set to the deterministic profile’s NaN rule |
| 10 | module_probe_mismatch | a probe the sink ran gave other bytes than waism §6, check 10, expects |