Skip to main content

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.md in the open-standards repository (https://git.transaction.science/Transaction-Science/open-standards). The verifier of the reference implementation is in https://git.transaction.science/Transaction-Science/open-standards/src/branch/main/wai/wai-rs: claim_receipt, jwp_delivery, and for §7 signer_id and key_log (feature key_discovery). A post-quantum signature beside each record’s Ed25519 one — a hybrid group, claim, key entry or key-log root — is pq-signatures’s (feature pq_signatures); a record without one is read and verified as this text says.

Keywords MUST, MUST NOT, SHOULD, MAY, REQUIRED, OPTIONAL are RFC 2119 / RFC 8174.


1. Scope and model

This extension defines a JWP-shaped receipt chain that attests per-object delivery of WAI envelope objects through a chain of relays. A receipt names the bytes a sink received, the publisher that produced them, the energy spent producing them, and the chain of relays the bytes traversed — all under a single Ed25519 signature per group.

In scope:

Out of scope:

Relationship to WAI SPEC.md:


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:

fieldtypemeaning
kindstringMUST equal "group".
track_aliasu64Carrier-scoped track identifier; 0 if unused.
group_idu64Monotonic group counter under (publisher_key, track_alias).
root_hash32 B hex§2.2 Merkle root over object content hashes.
leaf_countu64Number of objects covered by root_hash.
joules_totalu64Sum 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_startstringRFC 3339 timestamp of the first object.
group_endstringRFC 3339 timestamp of the last object.
signer_pubkey32 B hexEd25519 verifying key per RFC 8032.
signer_idstringBound 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.
sig64 B hexEd25519 signature over §2.3 payload.
parent_receipt_hash32 B hex or nullThe 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):

fieldtypemeaning
sign_versioninteger2. 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_refas energy-measurement §2.7The total’s class and its evidence; all absent for the unmetered marker beside a zero total.
energy_coveragestringWhole 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:

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:

fieldtypemeaning
kindstringMUST equal "object".
track_aliasu64Mirrors the parent group receipt.
group_idu64Mirrors the parent group receipt.
object_idu64Monotonic from 0 within the group.
content_hash32 B hex§2.1 BLAKE3 over the WAI envelope bytes.
joules_microu64Microjoules 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).
originstringDatacenter / origin label.
merkle_proofobjectInclusion 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.

fieldtypemeaning
kindstringMUST equal "claim".
classstringA class of §2.4.4 other than delivery.
subject_typestring"object" or "receipt"; which a class admits is fixed by §2.4.4.
subject32 B hexFor 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).
seqintegerStrictly increasing across the claims of one signer_pubkey.
issued_atstringA date-time (below) at which the signer issued the claim. Self-asserted, like group_end.
bodyobjectClass-scoped; its members are fixed by class and by nothing else (§2.4.5–§2.4.18, §2.6).
signer_pubkey32 B hexEd25519 verifying key per RFC 8032.
signer_idstringBound identity string, as for a group receipt (§2.4.1, §7.1).
parent_receipt_hash32 B hex or nulllink_hash of this signer’s previous claim; null for its first.
sig64 B hexEd25519 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

classcarried bysubject_typebodysigned by
deliverygroup, object (implied, not carried)—§2.4.1, §2.4.2the publisher
energyclaimobject§2.4.5whoever acquired the figure
lawful-basisclaimobject or receipt§2.4.6the service operator (a declaration); the party recording it (a decision)
log-inclusionclaimreceipt§2.4.7whoever attaches the log’s evidence
soft-bindingclaimobject§2.6.2the object’s publisher
soft-binding-spanclaimobject (the span’s anchor)§2.6.4the track’s publisher
binding-recoveryclaimobject (the ancestor)§2.6.3the party that performed the recovery
measurementclaimobject or receipt, as its figure profile fixes§2.4.8the key that signed the figure: the group’s publisher for moq-jwp:object, the receipt’s signer for every other profile
operating-pointclaimobject or receipt, as its figure profile fixes§2.4.9the party that records the request, the commitment and the delivery
stageclaimobject (the stage set’s base)§2.4.10the party that observed the stage set
delivery-gapclaimobject (the object not forwarded)§2.4.11the relay that did not forward it
reconstruct-sessionclaimobject (the session’s entry object)§2.4.12the sink that reconstructed: the key its decoder host holds
consumptionclaimobject (the first object it names)§2.4.13the agent that consumed the objects: its key, named by its did:key
transmissionclaimreceipt (the group transmitted)§2.4.14the group’s publisher: the key that signed the group
reconciliationclaimreceipt (the group reconciled)§2.4.15the sink that received the objects, never the publisher
scheduleclaimreceipt (the group scheduled)§2.4.16the group’s publisher: the key that signed the group; or a node, of an instance of its own (cache-authority §5.6)
timelinessclaimreceipt (the group whose objects it times)§2.4.17the group’s publisher: the key that signed the group
cache-transmissionclaimreceipt (the group sent again)§2.4.18a node that sent the objects from what it held, never the group’s publisher

Rules:

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.

membertypemeaning
joules_microintegerThe 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_provenancestringAn acquisition class of energy-measurement §2, verbatim.
energy_uncertaintyobject or nullThe declared uncertainty (energy-measurement §2.3): { "relative_ppm": integer, "window_us": integer }, exactly those two members.
energy_counter_filteringstring or nullAn OnChipCounter’s filtering state (energy-measurement §2.4).
energy_calibration_refstring or nullA CalibratedInstrument’s calibration reference (energy-measurement §2.5).
coveragestringWhole or Partial, per energy-binding §4.
gradestringAn evidential grade of oer-receipts §2, verbatim.
methodabsolute URIThe published protocol the figure was obtained under.
work_unitsintegerThe recomputable work the figure is expressed against (energy-measurement §5). A zero is refused by rule 6, at step 4.
work_unittermWhat one work unit is (e.g. subpixel).
oer_receiptstring or nullThe 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:

  1. joules_micro MUST NOT be zero. A zero is a failed measurement, and an unmetered object gets no energy claim at all (energy-measurement §2).
  2. The acquisition class, in this order:
    • energy_provenance MUST name a class of energy-measurement §2 (energy_unknown_acquisition);
    • it MUST NOT be HwShunt (energy_legacy_acquisition). energy-measurement §2.6 forbids the legacy label in a new receipt, and no claim predates the refinement, so a verifier can hold every claim to it — as energy-measurement §2.8 holds the optional labels;
    • the evidence MUST be the class’s, whole (energy_acquisition_evidence). CalibratedInstrument and OnChipCounter carry a declared uncertainty whose two parts are non-zero and whose relative_ppm is at most 2³² − 1, the width of its signed encoding (energy-measurement §2.7), and which reaches the resolution floor, joules_micro × relative_ppm ≥ 288 676 (energy-measurement §2.3). OnChipCounter carries a filtering state of On, Off or Unknown and no calibration reference; a filtering state of null is not Unknown here, so that one class has one spelling. CalibratedInstrument carries a calibration reference that is not empty once Unicode White_Space is trimmed from both ends, and no filtering state. ModelBased and Estimator carry none of the three.
  3. coverage MUST be Whole or Partial.
  4. grade MUST name a grade of oer-receipts §2.
  5. grade MUST NOT rank above the grade the class supports. The correspondence is oer-receipts §2’s — CalibratedInstrument and OnChipCounter support up to measured, ModelBased up to derived, Estimator up to projected — with grades ranked as oer-receipts lists them (projected < simulated < derived < measured). Grading a figure below what its class supports is permitted. The grade does not say which hardware class is behind a measured figure; the class and its declared uncertainty do.
  6. work_units MUST NOT be zero.

A relying party holding an energy claim with coverage: "Partial" for a subject MUST NOT report that subject’s joules_micro as the whole cost of producing it.

Against the delivery receipt (§3.1 step 6): the claim’s joules_micro MUST equal the object receipt’s for the same subject (energy_disagrees_with_delivery). Where that receipt carries a class label (§2.4.2), the claim’s class and its evidence MUST also equal the label’s (energy_class_disagrees_with_delivery). Each is read as energy-measurement §2.7 reads it, so an OnChipCounter label with no filtering state reads as Unknown.

Against the OER receipt it names (§3.1 step 6): a verifier holding that receipt checks, in this order. The receipt is read as oer-receipts §4 reads a document: a text it refuses whole is no receipt, so a verifier holding only such a text holds none.

  1. that it is the one named — its receipt_id equals oer_receipt (context_mismatch);
  2. that it verifies under oer-receipts with no reason (energy_oer_invalid);
  3. that it is about the claim’s subject — its subject.output equals the claim’s subject exactly as the claim writes it, 64 lowercase hex digits (context_mismatch). oer-receipts §3 Output fixes what a job receipt’s subject.output holds: the commitment to what it delivered, which for one object is that object’s §2.1 content hash in this form; an OER receipt an energy claim 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;
  4. that its provenance.how equals grade, and that its energy.joules_total, rounded to the nearest microjoule, equals joules_micro (a figure exactly half-way between two microjoules admits either) (energy_disagrees_with_oer). Check 2 has established that the figure is a quantity oer-receipts §4 reads — a decimal string of at most 34 significand digits, or an integer within I-JSON’s range — since a receipt whose figure is anything else, or whose body holds a fraction, -0 or an integer beyond that range anywhere, does not verify. The figure is compared exactly, as oer-receipts §6 compares figures: 0.0120005 is half-way and admits 12,000 and 12,001 µJ, and 0.0120004 followed by twenty-six nines is below half-way, although rounding it to 28 significant digits would make it a tie.

This checks that two signed statements of one figure about one object agree. It does not check that the OER receipt’s energy.boundary is the scope §2.4.2 attributes to joules_micro; that remains the claim signer’s assertion.

The claim does not make the figure verifiable — re-execution reproduces the work, never the joules (energy-measurement §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:

membertypemeaning
recordstring"declaration".
vocabularyabsolute URIThe vocabulary the terms are drawn from (for example the W3C Data Privacy Vocabulary).
fragment32 B hex or null§2.1 content hash of the envelope carrying the declaration fragment beside the manifest, when one is carried.
purposesarrayOne 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:

membertypemeaning
recordstring"decision".
vocabularyabsolute URIAs the declaration’s.
declaration32 B hexlink_hash of the declaration claim the decision is expressed against.
purposetermThe purpose decided on.
outcometermThe 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.

membertypemeaning
logabsolute URIThe log.
profileabsolute URIThe registration profile the log’s evidence is checked under (for example urn:ietf:rfc:9942, COSE Receipts).
entrytermThe log’s identifier for the entry.
log_receipthexThe 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.

membertypemeaning
figure_profiletermA figure profile of energy-measurement §6.1.
figure_indexinteger or nullWhich figure, from 0, in a profile that carries several — for moq-jwp:object, the object’s leaf index; null in one that carries one.
group32 B hex or nullFor moq-jwp:object, the link hash of the group receipt the object was delivered under (§3 step 3); null for every other profile.
joules_microintegerThe figure.
energy_provenancestringIts acquisition class, as §2.4.5 writes it.
energy_uncertaintyobject or nullAs §2.4.5.
energy_counter_filteringstring or nullAs §2.4.5.
energy_calibration_refstring or nullAs §2.4.5.
bracketobject or nullFor 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.
buildobject{ "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_readerstring or nullThe reader of the power interface (energy-measurement §8). Required under a hardware class (rule 4).
fabricsobject, or absentWhere 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:

  1. joules_micro MUST NOT be zero (energy_zero_figure): a zero is the unmetered marker, or a failed measurement, and gets no report.
  2. figure_profile MUST name a profile of energy-measurement §6.1 whose subject type is the claim’s subject_type (figure_unknown_profile); figure_index MUST be an integer exactly when that profile carries several figures (figure_index_mismatch); and group MUST be a hash exactly when the profile is moq-jwp:object (figure_group_mismatch).
  3. The acquisition class, as §2.4.5’s rule 2 (energy_unknown_acquisition, energy_legacy_acquisition, energy_acquisition_evidence).
  4. Under OnChipCounter or CalibratedInstrument, power_reader MUST NOT be null (measurement_no_power_reader): a hardware figure is a reading, and its report names what read it, whether or not it carries a bracket.
  5. Where bracket is not null, in this order, with M its busy mean less its idle mean:
    • each n MUST be at least 2 (measurement_too_few_samples);
    • under OnChipCounter or CalibratedInstrument, each bracket’s rail MUST have been live (energy-measurement §3.1): sd_uw not zero (a bracket whose samples are all equal advanced once at most), longest_live_run at least 2, live_intervals at least 5 and at least four fifths of n (live_intervals · 5 ≥ n · 4), and longest_live_run ≤ live_intervals ≤ n (measurement_rail_not_live);
    • window_us, completions and in_flight MUST NOT be zero (measurement_empty_burst);
    • M MUST be positive (measurement_no_marginal);
    • under OnChipCounter or CalibratedInstrument, M MUST be at least the idle sd_uw. A ratio below 1 is a failure to measure (energy-measurement §4) (measurement_below_attribution_floor);
    • joules_micro MUST be the figure F the bracket gives (energy-measurement §6.2) (measurement_figure_mismatch);
    • under OnChipCounter or CalibratedInstrument, the declared window_us MUST be the bracket’s (measurement_window_mismatch), and the declared relative_ppm MUST be at least the least p the bracket supports (energy-measurement §6.2) (measurement_declaration_below_bracket).
  6. Where fabrics is present, in this order:
    • every fabric is cpu, gpu, media or npu, and every attribution task or rail (energy-measurement §6.3) (measurement_fabric_unknown);
    • there is at least one step and one share; the steps are in the order of their step, in byte order, and then of their fabric, in the order just given, each pair once; and the shares are in the order of their fabric, each fabric once (measurement_fabric_order);
    • every fabric a step names has a share of more than 0 (measurement_fabric_unmetered): a fabric that ran work is metered, and 0 J is no reading of it;
    • every share’s fabric runs a step (measurement_fabric_unused);
    • the shares sum to joules_micro (measurement_fabric_sum): the record divides the figure, and the figure’s class covers every share.

Against the figure (§3.1 step 6). The context is the receipt the claim names, verified under its own profile’s rules; for moq-jwp:object, that is the object receipt accepted under §3 against the group receipt group names. In this order:

  1. the receipt verifies (context_invalid); it is of figure_profile, its identity under that profile is subject, it is within the group group names, and it carries a figure at figure_index (context_mismatch);
  2. that figure is joules_micro (figure_disagrees);
  3. that figure carries an acquisition class (figure_unclassified). An unlabelled figure has none, and neither has the unmetered marker; a claim does not supply one;
  4. that class and its evidence are the claim’s, each read as energy-measurement §2.7 reads it (figure_class_disagrees);
  5. an Ed25519 key signs the figure (figure_signer_unknown) — every profile but wai:energy-binding-id has one — and it is signer_pubkey (figure_signer_mismatch). For moq-jwp:object it is the group receipt’s signer_pubkey. Only the key that signed a figure signs its report.

Only check 5 shows that a measurement claim’s signer is the figure’s, so a measurement claim is accepted only once step 6 has run against the receipt it names. A verifier that does not hold that receipt reports figure_not_held at step 6 — recoverable by fetching the receipt — and MUST NOT present the claim as the figure signer’s report, nor derive a §4 verdict for the figure from its values: without the context, any key can sign any values about any figure.

An accepted measurement claim establishes that the figure’s signer reported these values for this figure, and that the figure and its declared uncertainty are what the values give. It does not establish that the values were measured: they cannot be re-measured (energy-measurement §5). A reader derives the §4 verdict from an accepted claim’s bracket (energy-measurement §6.2).

2.4.9. class: "operating-point"

energy-binding §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.

membertypemeaning
figure_profile, figure_index, group, joules_micro, energy_provenance, energy_uncertainty, energy_counter_filtering, energy_calibration_refAs §2.4.8.
vocabularyabsolute URIThe 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_requestedpoint or nullWhat the requesting party asked for; null when nothing was requested.
op_committedpoint or nullWhat the acting party said it would use.
op_deliveredpoint or nullWhat was used.
predictionobject or null{ "predictor": absolute URI, "descriptor_hash": 32 B hex, "predicted_joules_micro": integer }: the model that turned the declared descriptor into energy, BLAKE3("wai:energy-descriptor\x01" ‖ descriptor) over the descriptor as vocabulary serialises it, and the energy it implies for the work the figure covers.

A point is an object whose member names are terms and whose values are integers of §2.4.3. JCS orders its members and §3.1 step 1 refuses a repeated one, so two points are equal exactly when their canonical forms are.

Rules, checked at §3.1 step 4 in this order:

  1. §2.4.8’s rules 1–3. A claim restating HwShunt is refused there, so no operating point is recorded against a figure labelled with the legacy class.
  2. A point that is present MUST NOT be empty (operating_point_empty_point).
  3. op_delivered MUST NOT be present without op_committed (operating_point_delivered_alone).
  4. op_requested and op_committed MUST NOT be present without op_delivered (operating_point_incomplete).
  5. A claim MUST carry op_delivered or prediction (operating_point_empty).

Against the figure (§3.1 step 6): §2.4.8’s checks 1–4. The claim may be signed by a party other than the figure’s signer, so check 5 does not apply, and it may name a figure no Ed25519 key signs. An operating point recorded against a figure that carries the unmetered marker or no class is refused here (figure_disagrees, figure_unclassified).

From an accepted claim a reader derives, and no claim states:

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).

membertypemeaning
vocabularyabsolute URIurn: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_requestedpoint or nullThe stage asked for.
op_committedpoint or nullThe stage the serving party said it would deliver.
op_deliveredpoint or nullThe 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.
presentedpointThe lowest stage at which a unit was presented on time.
units_by_stagearray of integersElement i: the number of units presented at stage i. The set’s units, numbered from 0, are the units it counts.
tierstringdecode-equivalence, entropy-consistency or none: the weakest tier of the presented units (staged-delivery §5).
stage_hash32 B hex or nullThe stage hash of the presented output over every unit (staged-delivery §5).
objectsarrayEvery layer object observed, each an object of the table below, ordered by layer, then units.from, then object.
conflictsintegerUnits frozen by a conflict (staged-delivery §4.1 step 7).
deadline_missesintegerUnits presented below the stage their layers would have reached on time (staged-delivery §8).
bytes_envelopesintegerEnvelope bytes received for the set, the base’s included (staged-delivery §11).
membertypemeaning
layerintegerThe object’s refines.layer.
object32 B hexIts §2.1 content hash.
unitsobject{ "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.
bytesintegerIts envelope bytes as received: more than 0 for an object that arrived, 0 for one that did not.
statusstringapplied (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).
evidencestringWhat the observer verified of its delivery receipt: delivered-verified (§3 step 6, over its envelope bytes), inclusion-verified (§3, Verified by inclusion) or none.
reasonstring or nullFor partial, the staged-delivery §4.2 reason of the lowest unit it was not applied to; for refused, the envelope’s reason, or else that of its lowest unit. null for applied, late and dropped. A unit the object is still held for when the claim is made counts as not applied, for the reason it is held for.

Rules, checked at §3.1 step 4 in this order:

  1. vocabulary MUST be urn:wai:staged-delivery:1, and every point present, presented among them, MUST be exactly the member wai-stage:layers, from 0 to 255 (stage_point_malformed). op_requested and op_committed MUST NOT be present without op_delivered (stage_point_incomplete, as §2.4.9 rule 4). §2.4.9 rule 3 does not apply: an observer states what was delivered whether or not anyone committed to it.
  2. stage_hash MUST be present exactly when tier is decode-equivalence (stage_hash_without_tier).
  3. units_by_stage MUST have from 1 to 256 elements summing to at least 1, its last element not 0; presented MUST name the lowest stage with a unit; and neither conflicts nor deadline_misses may exceed the number of units (stage_units_malformed).
  4. Each object’s layer MUST be from 1 to 255 and its units.count at least 1 (at least 2 for a partial object, which applied to some units of its span and not to others), and its units MUST lie within the set’s unless it is refused before its refines was checked against the set (the table above); its reason MUST be null exactly where the table says so and otherwise a reason of staged-delivery §4.2. Where op_delivered is not null — the base’s receipt was verified first — an applied or partial object’s evidence MUST be delivered-verified, since over such a base a layer applies only on a receipt verified over its bytes (staged-delivery §10.1); where it is null, a layer applied whatever was verified of its receipt, and any evidence is stated. A dropped object’s evidence MUST NOT be delivered-verified. An object that arrived — applied, partial, late or refused — MUST have bytes above 0, and a dropped one 0. The objects MUST be in strictly increasing order of (layer, units.from, object); their bytes MUST sum to less than bytes_envelopes, which counts the base’s bytes too; and no unit MAY be covered by two applied objects of one layer, since the second would conflict with the first or find the unit past its stage (stage_objects_malformed).
  5. A unit reaches stage s only through an object of each layer up to s applied to it, and an object of layer s applies only to a unit an object of layer s − 1 applied to. So, for every layer s from 1 up (stage_units_unsupported):
    • an applied object of layer s MUST lie within the spans of the applied and partial objects of layer s − 1 (for layer 1, within the set), and a partial one MUST meet them;
    • the units presented at stage s or above MUST NOT outnumber the units layer s can have reached: the units its applied objects cover, and, of the units inside the spans of layer s − 1 that those do not cover, those its partial objects cover, counting no more than its span less one for each partial object, which was not applied to every unit of its span. Two objects of one layer never both apply to a unit (rule 4), so their applied units do not overlap.
  6. Where op_delivered is not null, it MUST be the stage the objects give (stage_delivered_disagrees). Where it is null, this rule does not apply. A stated op_delivered is then never below presented: by rules 4 and 5, at every layer up to the stage presented, the spans of the applied and partial objects, each verified over its bytes, hold every unit, though a partial object was not applied to all of its own.

op_delivered and presented are separate measures. op_delivered counts what arrived under a receipt verified over its bytes, whether or not the sink applied it; presented and units_by_stage count what the sink presented on time. A set whose layers all arrived and were all refused is delivered at their stage, and so meets a commitment to it, with stage 0 presented; a reader that asks what was presented reads presented, never op_delivered.

A reader derives whether the commitment was met as §2.4.9 derives it: it was when op_delivered equals op_committed, and there was none when op_committed is null. An accepted claim establishes what its signer observed, not that content was delivered: only group and object receipts state delivery, and an object’s evidence is the signer’s account of what it verified, which a reader holding the receipt checks under §3 itself.

2.4.11. class: "delivery-gap"

A relay that does not forward an object still forwards the object’s receipts (§4.1). A delivery-gap claim adds the relay’s own statement that it did not forward it, and why. Its subject is the object’s §2.1 content hash, and the relay signs it under its own key, as the parallel receipt §4.3 permits.

membertypemeaning
track_aliasintegerThe object’s track_alias.
group_idintegerIts group_id.
object_idintegerIts object_id.
reasonstringdelivery-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_atstringA 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.

membertypemeaning
session_nonce32 B hexFresh random bytes per session, derived from nothing that identifies a person, a device or an account.
mediastringThe entry object’s manifest media (SPEC §3), as written: 1 to 32 of a–z, 0–9 and -.
hostobjectThe decoder host (below).
decodersarrayThe 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.
unitsintegerThe units presented at least once.
leaf_countintegerThe presentations: one per (unit, layers) presented.
output_root32 B hex or nullThe 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_unitsintegerThe units whose presentation at their largest layers came through a decoder entry whose path is primary.
fallback_unitsintegerThe same, for fallback.
switchesarrayAt 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_totalintegerEvery switch, including any not written.
deadline_usinteger or nullThe time, in microseconds, within which the sink’s schedule had each unit presented; null where it ran under none.
missedintegerThe presentations made after their unit’s deadline.
missed_unitsarrayAt 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_setsarrayFor 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.
framingstringThe framing label of the counted members (staged-measurement §3, its §3.3): registered.
counted_primaryintegerThe counted bytes (staged-measurement §3) of the countable objects received for the primary path.
counted_fallbackintegerThe same, for fallback.
counted_discardedintegerThe counted bytes of the countable objects received and never presented. They are in the two members above as well.
uncountedintegerThe objects received that are not countable (staged-measurement §3, Only a checked format is counted). Their bytes are in no counted member.
transferredintegerBytes of WAI objects as received, whole or as byte ranges, without transport or receipts.
receipt_bytesintegerBytes of receipts and claims received (staged-measurement §3.5).
classical_declaredinteger or nullThe 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_sourceterm or nullWhere classical_declared comes from: wai2-table, or a term a later extension registers here.
energyobject or nullnull 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:

membertypemeaning
capabilitytermThe capability decoded.
renditioninteger or nullIts WAI2 rendition index (SPEC §7.1): capability is that rendition’s capability. null for a WAI1 envelope.
object32 B hexThe 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_unitintegerThe 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).
pathstringprimary or fallback.
decoderstringbuiltin:<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.
variantterm or nullThe 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.
pin32 B hex or nullThe 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.
tierstring or nullThat 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_statusstring or nullFor a module: decoder, the anchor its grant was confirmed under (§7.9): pinned or fetched. null for a builtin: decoder.
leavesintegerThe 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.

membertypemeaning
module_profiletermThe identifier of the level the module was held at (waism §2).
enginestringThe engine that ran it, written as waism §7 states.
engine_settingsarray of termsThe settings of waism §7 the run was under.
module_probestringmatch 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_result32 B hexThe 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:

  1. primary_units and fallback_units MUST sum to units (session_path_sum).
  2. The entries’ leaves MUST sum to leaf_count; leaf_count MUST NOT be below units; and units is 0 exactly when leaf_count is (session_leaf_sum).
  3. output_root MUST be null exactly when leaf_count is 0 (session_root_presence).
  4. threads MUST be a power of two (session_host_malformed).
  5. Each decoder entry MUST be as its table says: decoder of its grammar; rendition at most 65535; tier null exactly when pin is, and otherwise one of its three values; module_status null exactly for a builtin: decoder, and otherwise pinned or fetched; leaves at least 1. No two entries MAY be equal in every member but leaves (session_decoder_malformed).
  6. A tier that is not null MUST NOT exceed the class tier of its entry’s capability (SPEC §3.1 Tier) (session_tier_exceeds_class).
  7. Each switch’s from MUST differ from its to, and its reason MUST be a term of delivery-telemetry §5. The switches MUST be in non-decreasing order of unit, and as many as switches_total, or 1024 where it is more (session_switch_invalid).
  8. missed MUST NOT exceed leaf_count, and MUST be 0 when deadline_us is null. missed_units MUST hold as many entries as missed, or 256 where it is more, in strictly increasing order of (unit, layers), each with layers from 1 to 65535 and late_us at least 1 (session_missed_invalid).
  9. stage_sets MUST be in strictly increasing order of (first_unit, claim) (session_stage_sets_invalid).
  10. counted_discarded MUST NOT exceed the sum of counted_primary and counted_fallback (session_counted_invalid).
  11. classical_source MUST be null exactly when classical_declared is, and otherwise wai2-table (session_classical_invalid).
  12. Where energy is not null, its joules_micro MUST NOT be 0 (energy_zero_figure), and its class is admitted as §2.4.5’s rule 2 admits one (energy_unknown_acquisition, energy_legacy_acquisition, energy_acquisition_evidence).
  13. A decoder entry whose capability has the replicate class neural-int-exact (SPEC §7) MUST state an exact path: variant null, exact or 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).
  14. A decoder entry that does not state an exact path MUST NOT have tier decode-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.
  15. A decoder entry’s module members MUST be all five or none, and none on a builtin: entry; module_profile MUST be one of the identifiers of waism §2, engine of the grammar of waism §7, engine_settings terms of that section’s table in ascending byte order with none twice, and module_probe match or none (session_decoder_malformed).
  16. A module: decoder entry whose tier is decode-equivalence MUST carry the module members, with module_profile the integer level’s identifier (session_module_profile_missing): a module’s output holds that tier only at the integer level (waism §2).
  17. A decoder entry whose module_profile is the float level’s identifier, and whose tier is neither null nor none, MUST have nan-canonical among its engine_settings and module_probe match (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:

  1. The list has leaf_count entries and no (unit, layers) twice, and the root of its output tree is output_root (session_output_mismatch).
  2. It agrees with the members it gives (session_output_inconsistent): every decoder names an entry and every layers is at least 1; its distinct units number units; each entry’s leaves is the number of presentations naming it; the entries are in first-use order; primary_units and fallback_units count the units whose presentation at their largest layers names an entry on that path; and every missed_units entry names a presentation of the list.
  3. A verifier that holds the objects MAY decode a presentation again where its entry’s capability has the replicate class neural-int-exact or standard-defined (SPEC §7) — the entry’s object’s unit unit − first_unit, at layers — and then MUST find its out_digest (session_output_irreproducible). Every other presentation is checked for membership only.

Against the objects (§3.1 step 6). The context lists each object received: { "content_hash", "path", "counted", "transferred", "presented" }, with counted the object’s counted bytes, or null for one that is not countable; transferred every byte received for it; and presented whether a decoder entry names it. An object received more than once on a path — fetched again, or in byte ranges — is listed once, its transferred the bytes of every receipt. These rules need the objects and the claim alone. In order:

  1. No content_hash is listed twice with one path (session_object_duplicate).
  2. The session’s subject, its entry object, is the content hash of an object received (session_entry_not_received).
  3. So is each decoder entry’s object (session_source_not_received).
  4. counted_primary and counted_fallback are the sums of counted over the objects of each path, counted_discarded its sum over the objects not presented, uncounted the number of objects whose counted is null, and transferred the sum of transferred (session_counted_mismatch).
  5. An object is presented exactly when a decoder entry’s object is its content hash (session_presented_mismatch). The sums above can agree with figures a sink stated falsely; this cannot.

The classical rendition of a WAI2 envelope is the first of its renditions, in manifest order, that is selectable — not a companion or a refinement, and a capability a sink may dispatch (SPEC §7.1) — and whose capability is a classical one of the envelope’s media: one the codec glossary (glossary/codecs.json) links from an entry of era classical, whose registry media is the manifest’s media, and which is not text. WAI’s own formats, the neural codecs, records, parameter sets and text capabilities are never classical. An envelope has one classical rendition or none, and a verifier reads it from the envelope; nothing names it.

Against the envelopes (§3.1 step 6). The context is the bytes of envelopes the session received: every WAI2 envelope among them, and any WAI1 envelope — a base or a layer — the verifier holds. An envelope is readable when it reads as a WAI1 or WAI2 envelope (SPEC §2, §7.1) whose payloads are plaintext, and encrypted when it reads as one whose security is a security object naming an encryption (SPEC §6.1): a sink holding the key decrypts and dispatches it (§6.4 step 3), and its manifest and rendition table are in the clear. Both are held: the rules below read every held envelope’s manifest and table, and read an encrypted one’s payloads not at all. Bytes that are neither — one whose security member is not a security object, or bytes that are not an envelope at all — are inert. An inert or encrypted envelope no decoder entry names is an object received, neither counted (its counted is null, staged-measurement §3, Only a checked format is counted) nor presented; a sink lists it like any other, so the session accounts for every byte it transferred. Each rendition of a held WAI2 envelope is also its exploded envelope (SPEC §7.1, Exploding a component), the same object (staged-delivery §9): a decoder entry names it either by that WAI2 envelope and its rendition, or by the exploded envelope’s content hash. An entry naming it by that content hash, with no rendition, is read as naming the rendition, by every rule below — unless the verifier holds the exploded envelope’s own bytes, a separate delivery (staged-delivery §9, http-delivery §10.4) that the rules read as the WAI1 envelope it is. A held layer is a held WAI1 envelope of a refinement-only capability (SPEC §5), or a rendition of a held WAI2 envelope of one: dispatch and selection go by capability, so an envelope or a rendition of any other capability is a base, a refines it carries ignored (SPEC §4, staged-delivery §3). Its base is named by refines.base: in a WAI1 layer by a content hash, of a WAI1 envelope or of a WAI2 rendition’s exploded envelope; in a WAI2 rendition by the id of a rendition of the same envelope. The base is held when the verifier holds a held envelope that is it, in either form; its own capability is the WAI1 manifest’s capability, or the rendition’s; and an entry names it when it names it in either form. A layer is applied over the entries naming its base whose capability the layer’s admits (SPEC §5, Base admission) and, where the base is held, is the base’s own. Rules 1 and 2 need the objects as well, rules 7 and 8 the outputs; the others need the envelopes and the claim. In order:

  1. Where the verifier holds the objects as well, each envelope is among the objects received, by its content hash (session_source_not_received).
  2. Where it holds the objects as well, each object listed with an envelope’s content hash states as counted that envelope’s counted bytes (staged-measurement §3), and null exactly when it is not countable (session_counted_mismatch).
  3. Each envelope a decoder entry names is held, readable or encrypted (session_envelope_unreadable). The verifier holds no key: of an encrypted envelope, the rules below check everything its manifest and rendition table give, and nothing that needs a payload — whether a payload matches a well-formed sha256 (rule 4), the units a base holds, a layer’s WIR1 payload and the span read against its base’s units (rules 5 and 8) — which are the signer’s statement.
  4. Each decoder entry whose object is a held envelope names what that envelope gives, as a sink dispatches it: of a WAI2 envelope, a rendition, the capability that rendition has, a rendition selectable or a layer — never a companion, which is applied with the rendition a sink picks — whose payload matches its stated sha256 (SPEC §7.1, Selection algorithm, Component identity), and of an encrypted envelope whose sha256, where stated, is well formed: 64 lowercase hex digits, the only form a payload can match, so a rendition whose sha256 is not (uppercase, another length, not hex) is one no sink decodes, whatever its payload; of a WAI1 envelope that is not a layer, no rendition and the manifest’s capability or its fallback, a capability a sink dispatches, in a manifest whose capability is not companion-only (SPEC §4); of a WAI1 layer, no rendition and its capability. And each decoder entry whose rendition is not null has its object among the held envelopes, which hold every WAI2 envelope the session received (session_source_mismatch). A signer cannot relabel a decoder entry whose envelope the verifier holds, and so cannot move a presentation through it out of the reach of the re-decode check above.
  5. Each decoder entry whose object is a held layer names one a sink can apply (session_layer_not_applicable); a sink refuses any other (staged-delivery §4.1, SPEC §7.1), so no presentation through it is possible:
    • in a WAI1 manifest, its fallback is null (capability-not-refinement), and its refines is present and reads as staged-delivery §3 defines it (refines-malformed);
    • a WAI2 rendition is offered only from an envelope every component of which conforms (SPEC §7.1, Refinement components; the reference WaiMulti::check_component_roles): a refinement component that does not, or a companion, an id or another component that breaks a rule of §7.1 or §5, leaves none of the envelope’s refinements offered;
    • its base, where held, is a base — not a layer, and a selectable rendition where it is a WAI2 rendition — whose own capability the layer’s admits (base-not-admitted, step 4);
    • where entries name its base, it is applied over one of them: a sink applies a layer only over a base decoded through a capability the layer admits, and through the base’s own capability, not its fallback (base-via-fallback, step 3);
    • where its base’s units are read (rule 8), its span lies within them and its runs sum to the units it covers (refines-malformed, step 2);
    • its payload parses as WIR1 for its capability’s unit kind (staged-delivery Appendix A) and holds a unit for each unit it covers, where that count is read (payload-malformed, step 6).
  6. Each decoder entry whose object is a held layer that is applied over an entry has the first_unit of one of the entries it is applied over (session_first_unit_mismatch). A layer’s units are its base’s, so its entry states its base’s first unit, whichever form names the base.
  7. Where it holds the outputs as well, each presentation through a decoder entry whose envelope is held is at the layer count that envelope gives: 1 for a base, or a rendition that is not a layer, and refines.layer + 1 for a layer (session_layers_mismatch).
  8. Where it holds the outputs as well, each presentation through a decoder entry whose envelope is held is of a unit that envelope covers: its unit is at least first_unit + a and below first_unit + a + n, where for a base, or a rendition that is not a layer, a is 0 and n the units its payload holds — a WIV1 clip’s frames, and one for a WIH1 picture, a WIA1 clip and a WIS1 tensor — read in the payload’s own format: of the formats SPEC §5 pins for a WAI1 manifest’s capability and its fallback, which decodes the same payload (SPEC §4 step 5), the one whose magic the payload starts with, whichever capability the entry names, and a rendition’s capability’s; for a layer, a and n are its span’s from and count, or, with no span, 0 and the units of its base, read where the base is held, in either form (session_unit_outside_object). Where WAI reads no unit count — a payload in no format SPEC §5 pins, one that does not parse as its own, or a base not held — the presentation is not checked.
  9. The lengths of the held WAI2 envelopes’ classical renditions whose capability no decoder entry decoded — encrypted or not, since the rendition table declares them in the clear — sum to classical_declared, which is null exactly when there is none (session_classical_mismatch). In this revision a held envelope is one held in full: of a WAI2 envelope a sink received only as byte ranges it holds the head and not the envelope, so the session cannot yet state that envelope’s classical rendition, nor this rule read it. Recording such a session is HTTP session work still to come.

Against the stage claims (§3.1 step 6). The context is the wire form of each stage claim the verifier holds of those stage_sets names. A set has a claim for each of its epochs, and bases that cover the same units of the title each have their own. In order:

  1. each passes §3.1 steps 1–4 (context_invalid);
  2. each is a stage claim, signed by this claim’s signer_pubkey, and its link_hash is the claim of an entry of stage_sets (context_mismatch);
  3. where the verifier holds the objects as well, each one’s subject, the set’s base, and every layer object it lists as arrived — applied, partial, late or refused, with bytes — are among the objects received (session_stage_disagrees);
  4. each decoder entry whose object is the subject of one of them, or a layer object one lists, has as its first_unit the first_unit of an entry of stage_sets naming a claim of that object (session_first_unit_mismatch): a set’s base and its layers are at the units the session names the set at;
  5. each of them that states a stage_hash, a clip digest over its set’s units (staged-delivery §5), has every decoder entry whose object is its subject at decode-equivalence by that section’s base step (a capability of the replicate class standard-defined, or of neural-int-exact with tier decode-equivalence), and every decoder entry whose object is its subject or 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;
  6. where the verifier holds the outputs as well, for each claim and each entry of stage_sets naming it, with f that entry’s first_unit and n the units the claim counts (session_stage_disagrees):
    • every unit from f to f + n − 1 is presented;
    • from below: for every stage s from 1 up, the units among them presented at more than s layers are no fewer than the units the claim counts at stage s or above. A unit presented on time at a stage was presented at every lower stage before it, and late presentations are leaves of the tree as well, so the tree can hold more than a stage claim counts and never less;
  7. where it holds the outputs, from above: every unit of an entry’s units presented at more than s layers lies in the span of a layer-s object that a claim named by an entry holding the unit lists as applied or partial (session_stage_disagrees). A unit reaches a stage only through a layer the sink applied: a late layer missed the deadline of every unit it covers and a refused one applied to none (§2.4.10). The claims of every epoch count together, so a unit refined before the sink forgot its base is bounded by the claim of that epoch, and the tree never holds a stage the claims’ applied layers cannot have given.

An accepted claim establishes that its signer reported this session. Its outputs are committed to by the tree. A presentation through a decoder entry whose capability is byte-exact is reproducible, and a verifier that decodes it again checks it. An entry’s capability is the signer’s statement, except where the verifier holds the envelope of the object it names, which binds it; that the entry’s outputs were decoded from that object, and not from another the session received, is the signer’s statement. Its byte totals are checkable against the objects, its classical length against the envelopes, and its stage sets against their stage claims, both ways. Its timings, missed deadlines, switches and energy are its signer’s statements (energy-measurement §5). So, deliberately, is each refusal a sink makes that the rules above do not check, since no context of this claim holds what decides it: that a layer’s base was decoded before the layer was applied, where no decoder entry names the base (staged-delivery §4.1 step 2, base-absent), since a sink may present a base’s units only once a layer has refined them; a layer’s delivery receipt (step 5, refinement-unattested), which no context carries; the decode of a layer’s streams to their last symbol, its declared cost and the chain budget (step 6, payload-malformed for a stream, cost-over-limit, chain-over-limit), which a verifier that decodes a byte-exact presentation again checks, and which otherwise turn on the sink’s limits; each unit’s producer, conflict, geometry and deadline (steps 7–10) and the hold bound and batches (§4.3–§4.4), which turn on the order and the times of arrival the sink alone saw, while the stage claims bound the stages a unit reached and a re-decode its output; a pin’s resolution at the sink, its module authorisation and its key log (SPEC §3.1, §4), which a decoder entry’s pin and tier state; what an encrypted envelope’s payloads hold — whether each matches its well-formed sha256, the units a base holds and a layer’s WIR1 payload (SPEC §6.4); whether a refinement’s exploded envelope that the verifier holds as bytes of their own was received on its own, a separate delivery that rule 5 reads as a WAI1 layer and not through its envelope’s component rules (staged-delivery §9); and a companion’s conformance (SPEC §7.1), which does not decide whether the rendition it binds to is presented. Nothing binds a stage claim to a session beyond its base and its key: a stage claim the same key signed about the same base in an earlier session of the title can be named, and the checks above hold it as they hold one of this session. The claim an end observation carries (delivery-telemetry §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):

membertypemeaning
algtermThe algorithm identifier, verbatim from the registry registry names, in that registry’s reverse-DNS form (a rule below, at step 4).
registryabsolute URIThe public algorithm registry alg is listed in.
binding_typestring"watermark" or "fingerprint", as the registry types the algorithm.
valuehexThe value the algorithm produced for the object.
resolution_endpointabsolute URIWhere 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.

membertypemeaning
algtermAs the declaration’s.
binding_typestringAs the declaration’s.
valuehexThe value computed from the object in hand.
declaration32 B hexlink_hash of the soft-binding claim the value matched.
resolution_endpointabsolute URIThe 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.
observed32 B hex or null§2.1 content hash of the object in hand, when it is still a WAI envelope.
attestsstringMUST be "ancestor" — the ancestor rule, stated in-band. Any string passes step 2; another value is refused at step 4 (recovery_not_ancestor).

Rules (§3.1 step 4): §2.6.2’s rules on alg, value and resolution_endpoint; attests MUST be "ancestor"; observed MUST NOT equal subject — an object whose hard binding holds is verified by §3, not recovered, and recording it as a recovery would downgrade a hard binding to a soft one.

Against the declaration it names (§3.1 step 6). The context is the declaration together with the publisher key P of a delivery receipt for the declaration’s subject that the verifier has verified under §3 — P taken from that receipt, never from the declaration or from the record, as in §3.2 step 3. For a declaration of class soft-binding-span (§2.6.4) that receipt is R, a delivery receipt for an object of the declaration’s span, verified under §3, and P is R’s signer. A verifier that holds the declaration but no such receipt holds no context for the record, and MUST NOT report the record as checked against its declaration. The checks, in order:

  1. the declaration passes §3.1 steps 1–4 (context_invalid). Its members, signer_pubkey among them, are its signer’s only once its signature is checked: a declaration that was only parsed can carry the publisher’s key over anyone’s signature;
  2. the declaration is a soft-binding or a soft-binding-span claim (context_mismatch);
  3. declaration is its link_hash, and subject, alg and binding_type equal the declaration’s (recovery_declaration_mismatch);
  4. the declaration’s signer_pubkey is P (recovery_signer_mismatch) — §3.2 step 3’s check, without which a record could carry a recovery that §3.2 itself refuses;
  5. for a soft-binding-span declaration: R is within its span (§2.6.4, recovery_outside_span), and observed is not R’s content_hash (recovery_of_hard_bound_object);
  6. value matches the declaration’s value under the algorithm’s own rule (recovery_no_matcher, recovery_value_mismatch).

What an accepted record establishes. Checked against that context: that its signer recovered through a declaration made by the ancestor’s publisher, with a value that matches it under the algorithm — and no more, since §3.2’s ancestor rule and its bound on what a match is worth apply unchanged. Accepted without that context (steps 1–5 only): that its signer reports a recovery, and nothing about who made the declaration it names or whether the value matches it.

2.6.4. Span binding (class: "soft-binding-span")

A soft binding whose value was computed over a span of one track rather than over one object: a stretch of the track’s media timeline, and, where the publisher states them, the groups or the objects that carry it. A watermark embedded in every frame of a minute of a live track, or a fingerprint computed over that minute, binds the minute; a clip cut from it and re-encoded carries the value, and a recovery through the declaration reaches the publisher’s receipts for that minute and for nothing outside it (§3.2).

The track’s publisher declares it with a claim of class soft-binding-span. Its subject is the anchor: the §2.1 content hash of the span’s first object, in group and object order. Its body is §2.6.2’s members and span:

membertypemeaning
alg, registry, binding_type, value, resolution_endpointas §2.6.2As §2.6.2’s, value being what the algorithm produced over the span.
spanobjectThe span, below.

span is an object of exactly these members:

membertypemeaning
track_aliasintegerThe track, as its group receipts name it (§2.4.1), under the declaration’s signer_pubkey.
start_msintegerThe span’s first instant of media time, in milliseconds from the track’s media time 0.
end_msintegerIts last, in the same units; both ends are in the span.
first_groupinteger or nullThe first group that carries the span, or null where the declaration states no groups.
last_groupinteger or nullThe last such group, or null with first_group.
first_objectinteger or nullWith groups, the span’s first object, an object_id in first_group; null where the span is stated by whole groups.
last_objectinteger or nullIts 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.

Whatever reads is the JSON form’s members, and every rule of §2.4.1, §2.4.2 and §3 applies to them as it does to the JSON form: a class that reads but breaks §2.4.1 is group_label_invalid, a coverage beside the unmetered marker is group_label_invalid, a label that breaks §2.4.2 is object_label_invalid, an object at another position is proof_position_mismatch.


3. Verifier algorithm

A conforming verifier MUST execute the following six steps in order for every group receipt and every object receipt it accepts. Steps match the algorithm style of WAI SPEC.md §4 (numbered, terminating).

  1. Parse and shape-check. Read the group receipt. If kind != "group", reject (wrong_kind). If sig is not 64 bytes, if any required field from §2.4.1 is absent, or if a member §2.4.1 does not define is present, reject (malformed). Labelled members on a group without "sign_version": 2, a sign_version other than 2, or a class that cannot be read, reject (group_label_invalid). If signer_pubkey does not decode under RFC 8032 §5.1.3 or is of small order — §3.1 step 3’s key rule — reject (bad_signer_key).

  2. Verify the Ed25519 group signature. Construct the signing payload per §2.3 from the receipt’s fields: the labelled payload for a group with "sign_version": 2, the legacy payload otherwise. Verify sig against signer_pubkey under §3.1 step 3’s rule — S < L, R not of small order, the cofactorless equation — the same rule every claim signature is held to, so that every conforming verifier accepts the same group signatures. If verification fails, reject (signature_verify_failed). For a labelled group, check its class with the group alone (§2.4.1) and reject one that breaks it (group_label_invalid). After this step, the verifier holds a verified root: the receipt’s root_hash field, now bound to signer_pubkey under signature, and for a labelled group the total’s class. A hybrid group (pq-signatures §6.2) is verified at this step by both its halves, in the order of pq-signatures §4.2, and under a policy that requires hybrid (§5 there) a group with a classical signature only is refused here (hybrid_required) before its signature is examined.

  3. Check parent linkage. Receipt chains are kept per (signer_pubkey, track_alias): one chain for each publisher key and track, so a group on another track, or under another key, is never a group’s predecessor, and is not held as one. Against the chain of the group’s key and track, with the link hash of a group defined below:

    • the chain’s head offered again passes: another of its objects is being verified;
    • a group the verifier admitted to the chain before, offered again with the same bytes — a legacy group’s unsigned parent among them — passes and changes nothing (accept again), never chain_break: a carrier can deliver one object twice, as MOQT does by a subscription and a fill (MOQT §3.4), and the group’s objects are verified against it as against the head. A legacy group whose parent differs from the one admitted is not that group, and is checked as below;
    • with no group of the chain held, a null parent starts the chain, and any other parent is parent_not_held;
    • with the chain’s head held, a parent that is the head’s link hash links the group; a null parent is chain_break (a second chain head), except for a group whose group_id lies inside the gap a join’s anchor left below it (Joining, History): there a null parent is the chain’s true start, and the group is parent_not_held until the group above it names its link hash; a parent that is the link hash of another group of the chain the verifier holds is chain_break (a fork); any other parent names a group the verifier does not hold, and is parent_not_held.

    parent_not_held is recoverable: the verifier MAY request the missing receipt and offer it before the group again; it MUST NOT silently accept. Over HTTP the missing receipt is fetched by the link hash the group names (http-delivery §8.1). A verifier that keeps only the head cannot tell a fork from a gap and reports both parent_not_held; one that keeps the link hashes of earlier groups of the chain tells them apart, and refuses both either way.

    Holding. A verifier MAY hold a group that is parent_not_held and offer it again when its chain admits a group it links to — the group its parent names, or, inside a gap (History, below), the group that names it as parent; a held group that then links is accepted. A verifier that holds MUST state a bound on what it holds: for each chain, a count of groups held live, a time from each one’s arrival, or both; for each chain, a count of groups held as history, those whose group_id lies inside a gap the chain’s anchors left, outside the live count; and a count across all its chains, of both kinds, since any key makes a chain. When holding a group live would pass its chain’s live bound or the count across chains, or a group held live passes the time, the verifier refuses every group it holds live for that chain (parent_not_recovered) and re-anchors the chain. When holding a group as history would pass its chain’s history count or the count across chains, it refuses the group held as history with the lowest group_id below it, or, if there is none, the group itself (parent_not_recovered); the chain does not re-anchor. A verifier verifies each group’s signature once: a group it has admitted or holds is the same bytes, by its link hash.

    Re-anchoring. The next group of a re-anchored chain that passes steps 1 and 2, the freshness bound of §6.1 where the verifier applies it, and whose group_id exceeds the chain head’s, becomes the chain’s head whatever its parent: its anchor. Where the bound is a count, the group whose arrival passes it is that next group.

    Joining. A verifier that begins to verify a track after its first group MAY declare that it joins. It then anchors each chain it holds no group of at the first group of the chain that passes steps 1 and 2 and the freshness bound where it applies it, whatever its parent (a null parent is the chain’s true start, and no anchor).

    History. Each anchor leaves a gap: below it, the last group admitted before the anchor (none for a join); above it, the earliest group admitted at or after the anchor. A group whose group_id lies between the two and that links to either end is accepted as history: forward, its parent is the link hash of the group below; backward, its link hash is the parent the group above names under its signature. Either way it becomes that end, link by link, so a gap fills in any order — ascending, as a fetch delivers it, descending, or from both ends — and once one group links to both ends the gap is closed. A group that links into a gap is history, never a fork: chain_break stays for a group naming a parent that already has a child on the chain, or for a second chain head. A group with a null parent inside a join’s gap is not a second chain head: the gap has no lower end, the group may be the chain’s first, and it is held as history until the group above it links back to it, as a fetch from the chain’s first group delivers it, in ascending order. So a receipt fetched after its chain re-anchored, or before the group a joiner anchored at, is admitted. A legacy group’s parent is unsigned, so nothing is accepted backward from a legacy anchor or from a legacy group of the history; forward, a legacy group links into a gap as it links to a head, with a group_id above the group below. A verifier MAY keep a bounded number of gaps open for each chain and close the oldest past it.

    An anchor is not a link. A join or a re-anchor establishes what the anchor’s signature does — that its key signed this group, at this group_id — and nothing about the groups it skips: those before the anchor of a join, or between the last group admitted and the anchor of a re-anchor, may have been published and withheld, or never published. A verifier MUST report a chain it anchored as anchored, with the anchor’s group_id and, after a re-anchor, the group_id of the last group it admitted before it, and MUST NOT report the groups skipped as verified. The objects of every group it admits are checked by steps 4–6 as any others (§6.1).

    A legacy link authenticates nothing. The legacy payload (§2.3) does not sign parent_receipt_hash, and a legacy group’s link hash does not cover it, so a relay can rewrite it at will: a legacy group linked to the head shows only that someone wrote the head’s link hash beside it. The legacy payload does sign group_id. A verifier therefore MUST refuse a legacy group whose group_id does not exceed its chain head’s (group_id_not_increasing), whatever it does for labelled groups: group_id is the only order a legacy chain signs, and without this rule a rewritten link replays the chain’s history as new. The labelled payload signs the link, and its link hash covers it.

    A verifier that applies §6.1’s opt-in checks also refuses here a labelled group whose group_id does not exceed its chain head’s (group_id_not_increasing), and a group whose group_end is outside its freshness window (group_not_fresh), unless its signed group_id is below the chain’s live edge (§6.1). A group that passes steps 1–3 MAY become the head of its chain whether or not any of its objects arrives (§4.4), so a dropped object does not break the link of its track’s next group. canonical_bytes(G) is G’s §2.3 signing payload, in the form G was signed in, followed by its 64-byte sig, and BLAKE3("moq-jwp:receipt-link\x01" || canonical_bytes(G)) is G’s link hash: the identity a claim names G by (§2.4.3), and a WAI_RECEIPT property’s group_receipt (moq-streaming-format §5). For a group in the legacy form it does not cover parent_receipt_hash, which that payload does not sign either; the labelled payload signs it.

  4. Verify the object’s content hash. For each accompanying kind: "object" receipt: recompute content_hash per §2.1 over the received WAI envelope bytes and compare to the receipt’s content_hash field. If they differ, reject the object (content_hash_mismatch: the envelope was mutated in transit). An object receipt missing a member of §2.4.2, or carrying one neither §2.4.2 nor its label defines, is malformed. A verifier that received only part of an object over HTTP takes as the recomputed content hash the cid that a verified head proof establishes (http-delivery §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).

  5. Verify the Merkle inclusion proof. In this order:

    • If the object receipt carries a class label (§2.4.2), check it as §2.4.2 requires and reject the object if it fails (object_label_invalid).
    • Check the object’s position: leaf_index is below the group’s leaf_count; merkle_proof.siblings holds exactly ⌈log₂ leaf_count⌉ hashes (none for one leaf); direction_bits equals leaf_index, so it holds the index’s bits and no higher ones; object_id equals leaf_index; and track_alias and group_id are the group’s. Otherwise reject (proof_position_mismatch). Without this, the last leaf of an odd level, duplicated, walks two positions, and an object can be presented at another object’s index.
    • Compute the object’s leaf in the labelled form of §2.2 where it carries a label, and in the legacy form otherwise. Walk the merkle_proof.siblings list from level 0 upward; at each level i, combine the current value with the sibling using the direction_bits bit i (1 = current is right child, 0 = current is left child) and the moq-jwp:merkle-node\x01 domain. After walking all siblings, compare the resulting hash to the verified root from step 2. If they differ, reject (inclusion_proof_failed).
  6. Accept. The object’s bytes are bound to signer_pubkey at (track_alias, group_id, object_id). Where the object’s leaf is labelled, its joules_micro, its class, the class’s evidence and its origin are bound with them. Where it is not, joules_micro and origin are the publisher’s unsigned statements. The verifier MAY now persist the group receipt as the new chain head for (signer_pubkey, track_alias) and dispatch the WAI envelope per WAI SPEC.md §4. Whose key signer_pubkey is, is §7’s to confirm; a group receipt needs the authority delivery, and the energy authority of each class its labelled leaves and its labelled total carry (§7.5).

A verifier MUST treat any rejection in steps 1–5 as a hard failure for the object in question. A verifier MUST NOT accept a partial verification (e.g. valid Merkle proof against an unverified root): a reading of an object’s figure, a group total and a WAI_RECEIPT property are taken only from objects accepted under steps 1–6.

Verified by inclusion. An object receipt checked without the object’s envelope bytes — steps 1, 2, 3 and 5, step 4 not run because there are no bytes to hash — is inclusion-verified: the publisher signed an object with this content hash at this position under a root the group signature binds. It is not step 6 acceptance. A verifier MUST report it as inclusion-verified, MUST NOT report it, or anything read from it, as delivered, and MUST NOT dispatch an envelope on it. It is how a subscriber accounts for an object it did not receive (§4.4), and how a stage claim names the evidence for such an object (§2.4.10). An object receipt accepted under steps 1–6 over the object’s bytes is delivered-verified.

The group total. A verifier that holds, for every leaf_index below the group’s leaf_count, exactly one object receipt it has accepted under steps 1–6 MUST check that their joules_micro sum to joules_total; if they do not, it reports group_total_mismatch, and the objects’ own verdicts stand. If they do:

A verifier that does not hold exactly one accepted object for every index reads a legacy group’s total as unlabelled, and a labelled group’s total at its own signed class, which it has not checked against the parts.

3.1. Claim receipts

A verifier checks a claim in six steps and reports the first check that fails, as a step and a reason. Within a step, checks run in the order listed.

  1. Envelope. A value that is not JSON (RFC 8259), is not an object, or has a duplicate member name is malformed. A text that begins with a byte-order mark is not JSON here: RFC 8259 §8.1 lets a parser ignore one, and this step does not. So is malformed a value holding a \u escape that leaves a surrogate unpaired — RFC 8259’s grammar admits it, but it denotes no Unicode text, and parsers differ on what they make of it (RFC 8259 §8.2) — or arrays and objects nested more than 32 deep, the claim itself being the first level. A number is never malformed: every JSON number is JSON, whatever its size, and one a member may not hold (1e400, -0, an integer of four hundred digits) is field_type below. kind absent is missing_required_field; kind other than "claim" is wrong_kind. Any §2.4.3 member absent is missing_required_field; any other member present is unknown_member. class that is not a string is field_type; equal to delivery it is class_reserved, and a string naming no class of §2.4.4 is unknown_class. Then, in table order with class done and body left to step 2, a member of the wrong type or encoding — subject_type outside its enumeration, a hash that is not 64 lowercase hex digits, seq not an integer of §2.4.3, issued_at not a date-time of §2.4.3, sig not 128 lowercase hex digits — is field_type. A subject_type the class does not admit is subject_type_not_admitted.
  2. Body. A body that is not an object is field_type. Otherwise it is checked against the class’s member table (for a lawful-basis body, the table of the record that record, read first, names; see §2.4.6): a member absent is missing_required_field (but a measurement body’s fabrics, which may be absent, §2.4), an additional member is unknown_member, and then, in the table’s order, a member that is not of the type the table gives it is field_type; a fabrics that is null is field_type, and within it, as in a bracket, a member absent is missing_required_field and an additional one unknown_member (steps before shares, each array’s rows read for their members before its rows’ values). That is a wrong JSON type; an integer that is not one of §2.4.3; a byte string that is not lowercase hex, two digits a byte, or a hash that is not 64 of them; a value outside an enumeration this extension defines here (record, binding_type, a bracket’s opening, a build’s profile, a stage’s tier, a stage object’s status and evidence, a consumption claim’s usage vocabulary, a transmission, reconciliation or cache-transmission claim’s bearer, a schedule entry’s fec and esi, a gap’s reason, a session’s framing, its host’s profile, and the paths of its decoder entries and switches); a date-time that is not one of §2.4.3 (a gap’s observed_at); a URI that is not absolute; a term that is not one of §2.4.3 (empty, longer than 2048 bytes, or holding a White_Space or control character); or a malformed oer_receipt; or an operating point whose member names are not terms or whose values are not integers of §2.4.3. A member’s type and encoding are checked together, before the next member is read. An object a class fixes the members of — a lawful-basis purpose, an energy_uncertainty, a span, 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 never field_type here: labels and evidence from vocabularies another extension defines (acquisition class and its evidence, coverage, grade); a joules_micro or work_units of zero; an empty purposes; an empty basis, which is why its type is term or ""; an empty log_receipt or value; an alg that is a term but not in registry form; a resolution_endpoint that is an absolute URI but not an https URI with a host and no userinfo; how a span’s members pair and order; an attests other than "ancestor"; a figure profile, an index or a group that does not fit the profile; a hardware figure’s report that names no power reader; a bracket with fewer than two samples, liveness facts that do not show a live rail, a zero window, completion or thread count, or a busy mean not above its idle mean; the figure and least uncertainty a bracket gives; an empty point; and which points and predictions are present together; in a stage claim, the vocabulary, a point’s parameter and range, a reason of staged-delivery’s, and how the units, the objects and the delivered stage agree; and, in a reconstruct-session claim, how its counts agree, a decoder entry’s grammar and tier, the switches’ reasons and order, the missed presentations, the order of its stage sets, its counted bytes and its classical rendition; in a transmission or reconciliation claim, its attests, a transmission’s energy figure and a reconciliation’s bitmap; in a schedule claim, its attests, the order of its entries, and each entry’s code parameters, symbols and passes; in a timeliness claim, its attests, the order of its entries, and each entry’s window, decay, fallback and ladder; in a cache-transmission claim, its attests, its bearer, its bitmap and its energy figure; and, in a consumption claim, the order of its objects, usage entries and seen digests, its subject, its terms and its signer_id.
  3. Signature. Ed25519 over §2.4.3’s signing input, verified by the rule every WAI signature is held to (SPEC §7 Signatures): a canonical key not of small order, S < L, R not of small order, and the cofactorless equation with R compared byte for byte. signer_pubkey is bad_signer_key if it fails that rule’s steps 1 or 2 — it is not the one encoding of its point, or its point is of small order — and sig is signature_verify_failed if it fails steps 3 to 5. The key checks run first, so a key under which a signature needs no private key is refused before its signature is examined. A hybrid claim’s two halves are verified in the order of pq-signatures §4.2 (hybrid_required under its policy, pq_signature_invalid), and its hybrid members are read at step 1, after the subject type is admitted (pq_malformed, pq_suite_unknown).
  4. Class rules, per §2.4.5–§2.4.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.
  5. Chain. Against the previous claim the verifier holds for signer_pubkey. A claim is held only if it passes steps 1–4 itself. One that fails steps 1–3 is not known to be its signer’s, and one that fails step 4 breaks its class’s rules; neither is held, so an altered copy can neither satisfy a link nor break one in the signer’s name. A non-null parent_receipt_hash with no previous claim held is parent_not_held — recoverable by fetching that claim, if it verifies, and MUST NOT be accepted silently. A parent that is not the held claim’s link_hash, a seq that does not increase, or a null parent while a previous claim is held, is chain_break. By the previous claim alone a verifier cannot tell a fork from a gap, a parent it has not yet obtained, and reports both chain_break. A gap is recoverable as parent_not_held is: 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 stays chain_break, since no claim it obtains is the parent.
  6. Context: checks against other receipts the verifier holds, each verified first — an energy claim against the object receipt for its subject, verified under §3, including the class label its leaf carries (§2.4.5), and against the OER receipt it names, verified under oer-receipts as §2.4.5’s check 2 (energy_oer_invalid); a measurement or operating-point claim against the receipt that carries its figure, verified under that receipt’s own profile (for moq-jwp:object, the object receipt accepted under §3 against the group receipt group names) (§2.4.8); a decision against its declaration (§2.4.6); a recovery record against its declaration together with the publisher key of a verified delivery receipt for the declaration’s subject, or for a span declaration a verified delivery receipt for an object of its span (§2.6.3), and never against the declaration alone; a delivery-gap claim against the object receipt at its coordinates, verified by inclusion at least (§2.4.11); a reconstruct-session claim against its outputs, the objects it received, the WAI2 envelopes it received and the stage claims it names, in that order, each that the verifier holds, the stage claims verified under steps 1–4 (§2.4.12); a consumption claim against the objects it names, with their receipts, the usage statements that govern them and its room, as ai-usage §6.3 orders them (§2.4.13); a transmission claim against the group receipt its subject names, and a reconciliation claim against that group receipt and the object receipts at the positions it names, each verified by inclusion at least, as bearer-receipts §4.3 and §5.3 order them (§2.4.14, §2.4.15); a schedule claim 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 a timeliness claim 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 a cache-transmission claim 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 is context_invalid. A declaration is verified here under steps 1–4, and one that fails them — or is not JSON — is context_invalid: its members, signer_pubkey among them, are its signer’s only once its signature is checked. For a decision or a recovery record the order is: the claim is of a class that names a declaration (context_mismatch), the declaration passes steps 1–4 (context_invalid), it is of a class the claim names — for a recovery record, soft-binding or soft-binding-span (context_mismatch), then the class’s own checks. A context receipt of another class is context_mismatch. For an energy claim, an object receipt about another subject, and an OER receipt other than the one named or about another subject, are context_mismatch, in §2.4.5’s order. For a decision or a recovery record, a declaration other than the one named or about another subject is the class’s own reason, decision_declaration_mismatch or recovery_declaration_mismatch; for a recovery record, a declaration signed by anyone but that publisher is then recovery_signer_mismatch, in §2.6.3’s order.

A claim that passes every step it was checked at is accepted: its signer made this statement about this subject. A measurement claim is checked at step 6 always: without the receipt carrying its figure it is figure_not_held, not accepted (§2.4.8). §2.4.4’s rules bound what that means, and §2.4.7 bounds it further for log-inclusion. Who the signer is, and whether a web origin gave its key this claim’s authority, is §7’s to confirm. A verifier reports it beside acceptance and never folds it in: confirmation changes the outcome of no step here.

stepreasonraised when
4energy_zero_figurejoules_micro is 0 in an energy, measurement or operating-point claim, or in a reconstruct-session claim’s energy
4energy_unknown_acquisitionenergy_provenance names no class of energy-measurement §2
4energy_legacy_acquisitionenergy_provenance is the legacy HwShunt
4energy_acquisition_evidencethe class’s evidence is missing, carried by a class that has none, or below the resolution floor (§2.4.5 rule 2)
4energy_unknown_coveragecoverage is not Whole or Partial
4energy_unknown_gradegrade names no grade of oer-receipts §2
4energy_grade_exceeds_acquisitiongrade ranks above what the acquisition class supports
4energy_zero_workwork_units is 0
4lawful_basis_no_purposesa declaration lists no purpose
4lawful_basis_purpose_without_basisa purpose has an empty basis
4lawful_basis_duplicate_purposea purpose repeats an earlier one
4log_inclusion_no_evidencelog_receipt is empty
4soft_binding_alg_not_registry_formalg is not in registry form
4soft_binding_empty_valuevalue is empty
4soft_binding_endpoint_not_httpsresolution_endpoint is not an https URI with a host and no userinfo
4span_malformeda span’s groups, or its objects, are one null and one integer, or its objects are integers beside null groups (§2.6.4)
4span_inverteda 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)
4recovery_not_ancestorattests is not "ancestor"
4recovery_of_hard_bound_objectobserved equals subject
4figure_unknown_profilefigure_profile names no profile of energy-measurement §6.1 for the claim’s subject_type
4figure_index_mismatchfigure_index is an integer under a profile that carries one figure, or null under one that carries several
4figure_group_mismatchgroup is a hash under a profile other than moq-jwp:object, or null under it
4measurement_no_power_readerunder a hardware class, power_reader is null
4measurement_too_few_samplesa bracket’s n is below 2
4measurement_rail_not_liveunder a hardware class, a bracket’s liveness facts do not show a live rail, or its samples are all equal
4measurement_empty_bursta bracket’s window_us, completions or in_flight is 0
4measurement_no_marginala bracket’s busy mean is not above its idle mean
4measurement_below_attribution_floorunder a hardware class, the marginal draw is below the idle standard deviation
4measurement_figure_mismatchjoules_micro is not the figure the bracket gives
4measurement_window_mismatchunder a hardware class, the declared window is not the bracket’s
4measurement_declaration_below_bracketunder a hardware class, relative_ppm is below the least the bracket supports
4measurement_fabric_unknowna fabric record names a fabric other than cpu, gpu, media and npu, or an attribution other than task and rail
4measurement_fabric_ordera 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
4measurement_fabric_unmetereda fabric record’s step runs on a fabric the record gives no share, or a share of 0
4measurement_fabric_unuseda fabric record’s share is on a fabric no step of it runs on
4measurement_fabric_suma fabric record’s shares do not sum to joules_micro
4operating_point_empty_pointa point is present with no member
4operating_point_delivered_aloneop_delivered is present without op_committed
4operating_point_incompleteop_requested or op_committed is present without op_delivered
4operating_point_emptyneither op_delivered nor prediction is present
4stage_point_malformeda 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
4stage_point_incompleteop_requested or op_committed is present in a stage claim without op_delivered
4stage_hash_without_tierstage_hash is present without decode-equivalence, or absent with it
4stage_units_malformedno 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
4stage_objects_malformeda 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
4stage_units_unsupportedan 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
4stage_delivered_disagreesop_delivered is not the stage the reported objects give
4session_path_suma session’s primary_units and fallback_units do not sum to units
4session_leaf_sumthe 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
4session_root_presenceoutput_root is null with presentations, or a hash with none
4session_host_malformedthe host’s threads is not a power of two
4session_decoder_malformeda 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
4session_tier_exceeds_classa decoder entry’s tier exceeds its capability’s class tier
4session_switch_invalida 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
4session_missed_invalidmissed 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
4session_stage_sets_invalidstage_sets out of order, or an entry twice
4session_counted_invalidcounted_discarded above counted_primary and counted_fallback together
4session_classical_invalidclassical_source present without classical_declared or absent with it, or not wai2-table
4session_variant_not_exacta 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
4session_tier_exceeds_varianta decoder entry does not state an exact path, and its tier is decode-equivalence
4session_module_profile_missinga module: decoder entry whose tier is decode-equivalence does not name the integer level of the WAISM profile
4session_module_engine_not_deterministica 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
4consumption_objects_unordereda consumption claim’s objects are not in strictly ascending order (ai-usage §6.2)
4consumption_subject_mismatchits subject is not the first of its objects
4consumption_usage_unorderedits usage entries are not in strictly ascending order of vocabulary
4consumption_term_unknowna term is not a term of its vocabulary
4consumption_seen_unorderedits seen digests are not in strictly ascending order
4consumption_signer_not_principalits signer_id is not the did:key of its signer_pubkey
4transmission_not_transmitteda transmission claim’s attests is not transmitted (bearer-receipts §4.2)
4reconciliation_not_receiveda reconciliation claim’s attests is not received (bearer-receipts §5.2)
4reconciliation_bitmap_malformedits leaf_count is 0, its received is not ⌈leaf_count / 8⌉ bytes, sets a bit at or past leaf_count, or sets none
4schedule_not_scheduleda schedule claim’s attests is not scheduled (carousel §4.2)
4schedule_objects_malformedit has no entry, or its entries’ leaf_index values do not strictly increase
4schedule_oti_invalidan entry’s code parameters are not ones its code admits (carousel §5.1, §5.2, §5.5)
4schedule_symbols_invalidan entry’s symbols are not ones its code can name (carousel §5.4)
4schedule_passes_invalidan entry’s passes are not well formed: none, of no duration, overlapping, ending past 9999, or sending more than 253 − 1 octets
4timeliness_not_valueda timeliness claim’s attests is not valued (timeliness §4.2)
4timeliness_objects_malformedit has no entry, or its entries’ leaf_index values do not strictly increase
4timeliness_window_invalidan 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
4timeliness_decay_invalidan 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
4timeliness_fallback_invalida 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
4timeliness_ladder_invalidan 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
4cache_transmission_not_transmitteda cache-transmission claim’s attests is not transmitted (cache-authority §5.2)
4cache_transmission_on_bidirectionalits bearer is bidirectional
4cache_transmission_bitmap_malformedits leaf_count is 0, its sent is not ⌈leaf_count / 8⌉ bytes, sets a bit at or past leaf_count, or sets none
6energy_disagrees_with_deliverythe figure differs from the object receipt’s
6energy_class_disagrees_with_deliverythe object’s leaf is labelled, and the energy claim’s class or evidence differs from the label
6figure_disagreesthe receipt a measurement or operating-point claim names carries another figure at figure_index
6figure_unclassifiedthat figure carries no acquisition class
6figure_class_disagreesits class or evidence differs from the claim’s
6figure_signer_unknowna measurement claim names a figure no Ed25519 key signs
6figure_signer_mismatcha measurement claim’s figure is signed by an Ed25519 key other than signer_pubkey
6figure_not_helda measurement claim is checked without the receipt that carries its figure (§2.4.8)
6energy_oer_invalidthe named OER receipt reports any reason under oer-receipts
6energy_disagrees_with_oerthe named OER receipt states another grade or figure
6context_invalida 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
6decision_declaration_mismatcha decision does not match the declaration it names
6decision_purpose_not_declareda decision is about a purpose its declaration does not list
6recovery_declaration_mismatcha recovery record does not match the declaration it names
6recovery_signer_mismatchthe 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
6recovery_value_mismatchthe value does not match under the algorithm’s rule
6recovery_no_matcherthe verifier has no matcher for the algorithm
6recovery_outside_spanthe 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
6gap_subject_mismatchthe object receipt at a delivery-gap claim’s coordinates names another object
6session_output_mismatcha session’s presentations are not leaf_count, repeat a (unit, layers), or give another output_root
6session_output_inconsistentthe 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
6session_output_irreproduciblea presentation decoded again, through a decoder its replicate class makes byte-exact, gives another out_digest
6session_object_duplicatean object received is listed twice with one path
6session_entry_not_receiveda session’s entry object is not among the objects it received
6session_source_not_receiveda decoder entry’s object, or an envelope held as the session’s, is not among the objects it received
6session_counted_mismatchthe objects received give other counted, uncounted or transferred bytes, or an object whose envelope is held states other counted bytes than that envelope gives
6session_presented_mismatchan object is listed as presented and no decoder entry names it, or as not presented and one does
6session_envelope_unreadablean envelope a decoder entry names is neither a WAI1 or WAI2 envelope whose payloads are plaintext nor a well-formed encrypted one
6session_source_mismatcha 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
6session_layer_not_applicablea 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
6session_first_unit_mismatcha 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
6session_layers_mismatcha presentation through a decoder entry whose envelope is held is at another layer count than that envelope gives
6session_unit_outside_objecta presentation through a decoder entry whose envelope is held is of a unit that envelope does not cover, from its first_unit
6session_classical_mismatchthe envelopes’ classical renditions the session did not decode give another classical_declared
6consumption_object_unknowna consumption claim names an object the verifier does not hold (ai-usage §6.3)
6grant_mismatchthe grant it names is not the signer’s, not good at an object’s time, or names objects that are not the room’s
6preference_mismatchits seen is not the digests of the statements that govern its objects
6usage_unansweredits usage does not answer a vocabulary a governing statement carries
6usage_disalloweda use it names is disallowed for one of its objects
6transmission_subject_mismatcha transmission claim’s subject is not the link hash of the group receipt it is checked against (bearer-receipts §4.3)
6transmission_signer_mismatchit is not signed by the group’s key
6reconciliation_subject_mismatcha reconciliation claim’s subject is not the link hash of the group receipt it is checked against (bearer-receipts §5.3)
6reconciliation_by_publisherit is signed by the group’s key
6reconciliation_leaf_count_mismatchits leaf_count is not the group’s
6reconciliation_objects_not_heldit names a position whose object receipt the verifier does not hold: recoverable, never accepted
6reconciliation_root_mismatchits received_root is not the root over the leaves at the positions it names
6schedule_subject_mismatcha schedule claim’s subject is not the link hash of the group receipt it is checked against (carousel §4.3)
6schedule_signer_mismatchit is not signed by the group’s key
6schedule_leaf_out_of_rangean entry’s leaf_index is not less than the group’s leaf_count
6schedule_before_groupan entry’s first pass starts before the group’s group_start
6schedule_content_mismatchan entry’s content_hash is not that of the object receipt the verifier holds at its position
6schedule_length_mismatchan entry’s transfer length is not the length of the object’s envelope the verifier holds
6timeliness_subject_mismatcha timeliness claim’s subject is not the link hash of the group receipt it is checked against (timeliness §4.3)
6timeliness_signer_mismatchit is not signed by the group’s key
6timeliness_leaf_out_of_rangean entry’s leaf_index is not less than the group’s leaf_count
6timeliness_before_groupan entry’s from_ms is before the group’s group_start, whole seconds compared
6timeliness_content_mismatchan entry’s content_hash is not that of the object receipt the verifier holds at its position
6timeliness_on_layeran envelope the verifier holds whose content hash is an entry’s or a rung’s is a WAI1 of a refinement-only capability
6timeliness_fallback_mismatcha 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
6timeliness_fallback_not_derivedan 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
6cache_transmission_subject_mismatcha cache-transmission claim’s subject is not the link hash of the group receipt it is checked against (cache-authority §5.3)
6cache_transmission_by_publisherit is signed by the group’s key
6cache_transmission_leaf_count_mismatchits leaf_count is not the group’s
6cache_transmission_objects_not_heldit names a position whose object receipt the verifier does not hold: recoverable, never accepted
6cache_transmission_root_mismatchits sent_root is not the root over the leaves at the positions it names
6session_stage_disagreesa 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
6session_stage_hash_not_exacta 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.

  1. Verify D under §3.1 steps 1–4. A rejection of D is the recovery’s rejection, reported unchanged.
  2. D MUST be a soft-binding claim (context_mismatch).
  3. D’s signer_pubkey MUST equal P (recovery_signer_mismatch). P comes from the verified delivery receipt, never from D: a soft binding declared by anyone but the publisher links the object in hand to that party’s statement, not to the publisher’s receipts. The resolution endpoint is a locator, not an authority; what it returns counts only if it passes this step.
  4. If H equals D’s subject the hard binding holds and there is nothing to recover (recovery_of_hard_bound_object): verify under §3 instead.
  5. Evaluate V against D’s value under the algorithm’s own rule. A verifier with no matcher for the algorithm MUST NOT report a recovery (recovery_no_matcher); no match is recovery_value_mismatch.

Steps 2–5 report at step 6. On success the result is a recovery, and it attests D’s subject: the ancestor.

Through a span. D may instead be a soft-binding-span claim (§2.6.4). The verifier then needs, in place of a delivery receipt for D’s subject, R: a delivery receipt for an object of D’s span that it has verified under §3, P being R’s signer. Step 2 admits the class; after step 3, R MUST be within D’s span (§2.6.4, recovery_outside_span); and step 4 also refuses an H equal to R’s content_hash. The steps are otherwise those above. The recovery attests the span: the track (P, track_alias), its media time, its groups or objects where D states them, and the anchor. Of the publisher’s receipts, the ancestor’s are those within the span: R, and any other object receipt of the track the verifier holds whose coordinates are within it.

What a recovery through a span does not attest:

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

checkreasonraised when
§3 step 1wrong_kindkind is not the receipt’s kind
§3 step 1, 4malformeda 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 1bad_signer_keysigner_pubkey does not decode under RFC 8032 §5.1.3, or is of small order
§3 step 1, 2group_label_invalida 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 2signature_verify_failedthe group signature fails §3.1 step 3’s rule over the payload of the group’s form
§3 step 1, 2pq_malformed, pq_suite_unknown, pq_signature_invalid, hybrid_requiredas pq-signatures §8, for a hybrid group or under the policy that requires one
§3 step 3chain_breakwithin 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 3parent_not_helda non-null parent that names no group of the chain the verifier holds: requested, never accepted silently
§3 step 3parent_not_recovereda 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 3group_id_not_increasinga 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 3group_not_freshunder §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 4content_hash_mismatchthe envelope does not hash to content_hash
§3 step 5object_label_invalidan 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 5proof_position_mismatchthe 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 5inclusion_proof_failedthe 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 totalgroup_total_mismatchthe accepted object receipts, one for every leaf, do not sum to joules_total
the group totalgroup_class_disagreesa 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 totalgroup_coverage_disagreesa labelled group’s coverage is not the one its objects give
the group totalgroup_uncertainty_below_partsa labelled group declares less than the composed relative_ppm, or another window
WAI_RECEIPTproperty_invalid, property_disagreesas moq-streaming-format §5
carried over MoQmoqt_mismatchas 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.

4.2. MAY

4.3. MUST NOT

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:

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

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

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:

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.

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:

membertypemeaning
kindstring"wai-key-set".
originstringThe origin the log is published under, as §7.1 derives it.
seqinteger0 for the first revision; otherwise one more than its parent’s.
prev32 B hex or nullIts parent’s revision_hash; null exactly when seq is 0.
issued_atlog date-timeWhen the origin issued it. Self-asserted; not earlier than its parent’s.
root32 B hexThe Ed25519 key that signs this revision.
next_root32 B hexThe commitment (below) to the key that may sign as root after this one; never the commitment to root.
keysarrayThe key entries, below.
revoked_modulesarray of 32 B hexThe 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_sintegerThe longest module grant, in seconds, the origin issues: at least 1 and at most §7.9’s ceiling.
status_max_age_sintegerHow long, in seconds, a key status stays fresh: at least 1 and at most §7.9’s ceiling.
sig64 B hexroot’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):

membertypemeaning
idstringA key-id of §7.1.
key32 B hexAn Ed25519 verifying key that §3.1 step 3 accepts.
may_signarray of authoritiesThe authorities (§7.5) the origin gives this key: at least one, none twice.
statusstring"active", "retired" or "revoked".
untillog date-time or nullFor a retired key, the time after which the origin signs nothing with it; null for a key never retired.
compromised_sincelog date-time or nullFor a revoked key, the time from which the origin attests that another party may have held it; null otherwise.

An active entry has both times null; a retired entry has until and no compromised_since; a revoked entry has compromised_since, and until if it was retired first. No two entries share an id or a key, no entry’s key is root, and none has next_root as its commitment.

sig           = Ed25519(root, "wai:key-set\x01" || JCS(revision without "sig"))
revision_hash = BLAKE3("wai:key-set-link\x01" || JCS(revision))
commitment(K) = BLAKE3("wai:key-set-next\x01" || K)

JCS is §2.4.3’s canonical form. Of the revisions a verifier holds for an origin, a revision’s parent is the one whose revision_hash is its prev. A revision whose prev names no revision the verifier holds is unattached: it is ignored until its parent is held. Each rule below has a reason (§7.7). Rules 1–3 concern a revision alone, rules 4–6 compare it with its parent, and rule 7 compares revisions that share a parent:

  1. It is a JSON text that §3.1 step 1 would read, with exactly the members above, each of the type and encoding given, and it meets the constraints stated with the tables (key_log_malformed).
  2. Its origin is the origin being checked (origin_mismatch).
  3. Its sig verifies under its root by §3.1 step 3’s rule, which also refuses a root that does not decode or is of small order (key_log_signature_invalid).
  4. Its seq is one more than its parent’s, and its issued_at is not earlier than its parent’s (key_log_chain_break). Its prev is its parent’s revision_hash by the definition of a parent; between consecutive revisions of a log being anchored (§7.3), a prev that is not the previous revision’s revision_hash is also key_log_chain_break.
  5. Either its root and next_root are its parent’s, or its root differs from its parent’s and has its parent’s next_root as its commitment (key_log_root_not_committed). A revision of the second kind is a rotation: signing it proves possession of the committed key. Only a rotation changes the commitment.
  6. It erases and rewrites nothing: its keys begin with its parent’s entries, in the same order, each with the same id, key and may_sign; a status stays, or moves from active to retired or revoked, or from retired to revoked; a time, once set, does not change; and its revoked_modules begin with its parent’s (key_log_rewrites_history). A revocation therefore stays in every later revision, and the latest revision lists every key and every revoked module the origin has ever listed.
  7. It has one history. Of the revisions a verifier holds that share a parent and are signed by a key that parent authorises — its root, or the key its next_root commits to — exactly one is taken: the only one, or, where there are several, the only rotation among them, which supersedes the others and every revision descended from them. Any other case is key_log_fork.

A verifier holds a revision for an origin only if the revision passes rules 1–3 for that origin. A revision it does not hold, an unattached revision, and a revision signed by a key its parent does not authorise are no child of anything, and cannot stop a confirmation. Only a key the log itself authorises can: a root that signs two histories, or a history that breaks rules 4–6.

7.3. Anchors

A verifier confirms against an origin only once it has anchored it: it holds a key log of that origin that passed the checks below, whose latest revision is its anchor, and a record of how it came to hold it.

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:

Otherwise an anchor never moves. A verifier that has anchored an origin and retrieves from it a log whose latest revision is a trusted revision other than the anchor keeps its anchor and reports key_log_rolled_back: the log is an older copy, or the origin’s server was rolled back. One that retrieves a log that does none of the above keeps its anchor, reports key_log_replaced, and anchors that origin anew only by pinning. A name that has changed owner and a server held by an intruder look the same from here.

Retrieval is the only step that touches the network, and it comes before confirmation, never within it. Confirmation (§7.4) and the module checks (§7.9) are functions of what they are given — the item, the revisions the verifier holds, its anchor records, and for §7.9 the statuses it holds and the time — so verifiers holding the same bytes reach the same verdict, offline, on every platform. A verifier that must re-check a confirmation later keeps those revisions and its anchor record.

7.4. Confirmation

A verifier confirms only an item it has accepted: a group receipt that passed §3 steps 1–2, whose object receipts share its confirmation; a claim accepted under §3.1, and the energy figure of an accepted reconstruct-session claim, which is an item of its own (§7.5); a receipt of another WAI family whose signed bytes include its signer_pubkey and signer_id and whose own verification passed; or a module grant whose signature verified (§7.9). Its inputs are the item’s signer_pubkey, signer_id, the authorities it needs (§7.5) and its signed time where it has one — a claim’s issued_at, a group receipt’s group_end, a grant’s not_before — the revisions the verifier holds, its anchor records, and, where it has one, an expected origin (§7.5). It runs these steps in order and reports the first that fails, as a level and a reason (§7.7):

  1. signer_id is origin-bound (§7.1). A did:key naming a key other than signer_pubkey is refused (signer_id_names_another_key); any other form is unconfirmed (signer_id_not_origin_bound).
  2. The verifier has anchored the origin and holds the anchor (origin_not_anchored).
  3. The head. The verifier first looks along the trusted line, from its oldest held revision to the anchor, for a trusted revision whose trusted child is not a rotation and which has a held child that is a rotation: the first such revision’s rotation supersedes the rest of the line, and the walk starts there, checked against its parent under rules 4–6 (the anchor has moved). Otherwise it starts at the anchor. From the start it takes, repeatedly, the child rule 7 selects, checking it against its parent under rules 4–6, until it reaches a revision with no child it holds. That revision is the head. The result depends on which revisions are held, never on the order in which they arrived. A failure is unconfirmed with the rule’s reason (key_log_chain_break, key_log_root_not_committed, key_log_rewrites_history, key_log_fork).
  4. The head lists an entry whose id is the signer_id’s key-id (key_not_listed) and whose key is signer_pubkey (key_mismatch).
  5. The entry is not revoked (key_revoked), whatever time the item carries: whoever holds a compromised key can write any time into what it signs. A retired entry fails (key_retired) when the item’s signed time is later than until, and passes when it is not or the item has none. A signed time that is not an RFC 3339 date-time is later than every until.
  6. The entry’s may_sign holds every authority the item needs (authority_not_granted).
    • 6a. Where any of those authorities is pinned-only (§7.5), the anchor record is pinned, with its pinned revision, and the anchor has not moved in step 3 (anchor_not_pinned). For an authority registered for pre-emption, the entry is also listed, with that authority, in the record’s pinned revision itself — the revision the relying party provisioned, not a later one a forward move brought (unidirectional-delivery §3: a trust anchor delivered over the bearer it authorises is not one).
  7. Where the verifier expects an origin (§7.5), or one of a set of origins (§7.9’s module origins), the origin is that one, or one of that set (origin_not_expected). An empty set expects no origin the item can have.

A verifier that implements pq-signatures 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.

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:

itemauthorities
a group receipt, and the object receipts under itdelivery; 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 claimits 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 nullreconstruct-session, and energy/ followed by energy.energy_provenance
the energy figure of a transmission claim whose energy is not nulltransmission, and energy/ followed by energy.energy_provenance
the energy figure of a cache-transmission claim whose energy is not nullcache-transmission, and energy/ followed by energy.energy_provenance
a receipt of another WAI family that signs its own signer_pubkey and signer_idreceipt; 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 authoritylive/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 authoritylive/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:

7.6. Rotation and revocation

7.7. Reasons

steplevelreasonraised when
1unconfirmedsigner_id_not_origin_boundsigner_id is not of §7.1’s origin-bound form
1refusedsigner_id_names_another_keysigner_id is a did:key of a key other than signer_pubkey
2unconfirmedorigin_not_anchoredthe verifier has no anchor for the origin, or does not hold it
3unconfirmedkey_log_chain_breaka child’s seq or issued_at does not follow its parent’s
3unconfirmedkey_log_root_not_committeda child signed by its parent’s root has another next_root
3unconfirmedkey_log_rewrites_historya 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
3unconfirmedkey_log_forka parent has several authorised children and not exactly one rotation among them, or a trusted revision has several superseding rotations
4refusedkey_not_listedthe head lists no entry with the key-id
4refusedkey_mismatchthe entry’s key is not signer_pubkey
4arefusedhybrid_downgradethe entry is hybrid and the item carries a classical signature only (pq-signatures §6.1)
4arefusedpq_key_mismatchthe item’s ML-DSA key is not the entry’s, or the entry is classical (pq-signatures §6.1)
4aunconfirmedhybrid_requiredthe anchor requires hybrid and the item carries a classical signature only (pq-signatures §6.1)
5refusedkey_revokedthe entry is revoked
5refusedkey_retiredthe entry is retired and the item’s signed time is later than until
6refusedauthority_not_grantedthe entry’s may_sign does not hold every authority the item needs
6aunconfirmedanchor_not_pinnedthe 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
7refusedorigin_not_expectedthe origin is not the one the verifier expects, or not one of the set it expects
§7.3—key_log_malformeda log or a revision is not of §7.2’s form
§7.3—origin_mismatcha revision names another origin
§7.3—key_log_signature_invalida revision’s signature does not verify under its root
§7.3—key_log_chain_breakthe first revision’s seq is not 0, or a revision does not follow the one before it
§7.3—key_log_root_not_committeda 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_historyas at step 3, between consecutive revisions of the log being anchored
§7.3—key_log_rolled_backa log retrieved from an anchored origin ends at a trusted revision older than the anchor; the anchor is kept
§7.3—key_log_replaceda 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

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:

membertypemeaning
kindstring"wai-module-grant".
originstringThe origin under whose key log the grant is checked: the one its signer_id names (§7.1).
module32 B hexThe module’s digest.
formstring"decoder" or "reconstruct".
capabilitiesarray of termsThe capabilities the module may decode or reconstruct: at least one, none twice.
not_beforelog date-timeWhen the grant takes effect.
not_afterlog date-timeWhen it lapses; not earlier than not_before.
signer_pubkey32 B hexThe granting key.
signer_idstringThe granting key’s signer_id (§7.1).
sig64 B hexEd25519(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:

A sink with no module origin for an envelope holds no module for it. An origin the sink anchored for any other purpose — to confirm some other publisher’s receipts, say — is not thereby one of them.

Ceilings. WAI sets two normative ceilings: grant_max_s is at most 86400 seconds and status_max_age_s at most 3600 seconds. A key log declaring more is malformed (§7.2 rule 1).

The checks. A sink runs a module, or, where it holds pinned parameter sets to this section, decodes against one, only if every check below passes. It runs them in this order, before it instantiates the module, and reports the first that fails:

  1. The grant has the form above, its signer_id is origin-bound and names the grant’s origin, and its sig verifies under its signer_pubkey by §3.1 step 3’s rule (module_grant_invalid).
  2. The bytes’ digest is the grant’s module (module_digest_mismatch).
  3. The grant passes §7.4 as an item needing module/<form>, with its not_before as its signed time and the sink’s module origins expected: step 7 refuses an origin that is not one of them (origin_not_expected). A failure is reported with §7.4’s own reason. A revoked granting key therefore fails every grant it signed, whatever the grant’s times.
  4. The sink holds a status for that origin naming the head §7.4 step 3 reached; of those, the latest is fresh: its issued_at is not later than now and at most that head’s status_max_age_s seconds earlier (module_status_stale). A sink that holds statuses but none naming that head — each names an older revision of the head’s line — fails with module_status_not_descended.
  5. The grant’s module is not in the revoked_modules of that head, nor of the head §7.4 step 3 reaches from any other anchor the sink holds — or, where none can be reached, of that anchor revision (module_revoked).
  6. not_before ≤ now (module_grant_not_yet_valid) and now ≤ not_after (module_grant_expired).
  7. not_after − not_before is at most the head’s grant_max_s (module_grant_too_long).
  8. The capability being decoded is one of the grant’s capabilities (module_capability_not_granted).
  9. 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).
  10. 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.

checkreasonraised when
1module_grant_invalidthe grant is not of the form above, its signer_id does not name its origin, or its signature does not verify
2module_digest_mismatchthe bytes’ SHA-256 is not the grant’s module
3§7.4’s reasonthe 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)
4module_status_staleno 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
4module_status_not_descendedstatuses are held, and none names the head the sink reached
5module_revokedthe head’s revoked_modules, or another anchored origin’s, holds the digest
6module_grant_not_yet_validnow is earlier than not_before
6module_grant_expirednow is later than not_after
7module_grant_too_longthe grant’s window exceeds the head’s grant_max_s
8module_capability_not_grantedthe capability is not one the grant names
9module_profile_violationthe module breaks a rule of the WAISM profile at every level (waism §4)
9module_unattestedthe 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)
10module_engine_not_deterministicthe module is at the float level, the object needs a tier, and the engine is not set to the deterministic profile’s NaN rule
10module_probe_mismatcha probe the sink ran gave other bytes than waism §6, check 10, expects