open-standards/wai/extensions/jwp-receipts.md
Transaction Science 9751a06676
Some checks failed
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 15s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 18s
wai-app and Create / wai-app · does this change touch it (push) Successful in 15s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 32s
wai cross-architecture byte-equality / does this change touch it (push) Successful in 11s
wai-fleet service / does this change touch it (push) Successful in 11s
wai-audit collector and player integration / does this change touch it (push) Successful in 15s
WAI operation catalogue / does this change touch it (push) Successful in 15s
wai-key-proxy / does this change touch it (push) Successful in 14s
wai-ops integer operators / does this change touch it (push) Successful in 14s
wai-ledger service / does this change touch it (push) Successful in 18s
wai-quantum studio and desktop / does this change touch it (push) Successful in 14s
wai-quantum for JavaScript / does this change touch it (push) Successful in 16s
wai-sink-android native sink library / does this change touch it (push) Successful in 16s
wai-sink-swift native sink package / does this change touch it (push) Successful in 11s
WAI Studio / does this change touch it (push) Successful in 12s
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Has been cancelled
joule-code conformance gate / joule-moq-relay, every target (push) Has been cancelled
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Has been cancelled
joule-code conformance gate / joule-moq-ingest, every target (push) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (push) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Has been cancelled
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Has been cancelled
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Has been cancelled
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Has been cancelled
wai-audit collector and player integration / collector · linux (push) Has been cancelled
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Has been cancelled
WAI operation catalogue / the catalogue and every door · linux (push) Has been cancelled
wai-fleet service / service · linux (push) Has been cancelled
wai-fleet service / service · macos (push) Has been cancelled
wai-ledger service / service · linux (push) Has been cancelled
wai-ledger service / service · macos (push) Has been cancelled
wai-quantum for JavaScript / javascript · linux (push) Has been cancelled
wai-quantum studio and desktop / studio · browser (push) Has been cancelled
wai-quantum studio and desktop / desktop app · macos (push) Has been cancelled
wai-quantum studio and desktop / desktop app · linux (push) Has been cancelled
wai-quantum studio and desktop / desktop app · windows (push) Has been cancelled
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Has been cancelled
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Has been cancelled
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Has been cancelled
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Has been cancelled
WAI Studio / Studio in Chromium and Firefox · macos (push) Has been cancelled
WAI Studio / Studio in WebKit · linux (push) Has been cancelled
wai cross-architecture byte-equality / windows-x86_64 (push) Successful in 1m7s
wai cross-architecture byte-equality / macos-arm64 (reference) (push) Successful in 24s
wai-ops integer operators / macos-arm64 (push) Successful in 57s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 7s
wai-quantum energy-landscape jobs / does this change touch it (push) Successful in 7s
wai cross-architecture byte-equality / linux-x86_64 (push) Successful in 1m2s
wai-ops integer operators / wasm32 under Node (push) Successful in 31s
wai-ops integer operators / independent checker (Python) (push) Successful in 19s
wai-ops integer operators / linux-x86_64 (push) Successful in 1m12s
wai-ops integer operators / wasm32-wasip2 (wasmtime) (push) Successful in 1m7s
wai-key-proxy / proxy · linux (push) Successful in 1m34s
wai-quantum energy-landscape jobs / reader, kernels, answers and the stated result · linux, release (push) Has been cancelled
wai-quantum energy-landscape jobs / the same bits · linux-arm64 (push) Has been cancelled
wai-quantum energy-landscape jobs / the module under wasm32-wasip2 (wasmtime) (push) Has been cancelled
wai-quantum energy-landscape jobs / native against WebAssembly, over many seeds (push) Has been cancelled
wai-quantum energy-landscape jobs / the same bits · macos (push) Has been cancelled
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 3m0s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 5m21s
wai cross-architecture byte-equality / linux-aarch64 (push) Successful in 1m18s
wai-ops integer operators / linux-aarch64 (push) Successful in 1m58s
wai: a refinement conditioned on side information both ends hold, wai.image.int_cond_refine
Registers wai.image.int_cond_refine, refinement-only and StandardDefined, with the
staged form conditioned residual and the payload WCP1 (staged-delivery §6.2,
Appendix B). A layer's next stage is the base prediction (the stage below, or its
up2 at the output's size), plus a correction an integer network carried in the
payload computes from it and the side planes refines.side names, clamped, plus a
WIR1 residual decoded over that prediction. The network is one WHG1 graph lowered
by wcp1/1 under acc64, so every sink holding the same planes computes the same
bytes. Over rendered content the side planes are G-buffers carried as exact
objects of their own, or the frame the sink renders itself from a
wai.splat.int_codec object at a pinned pose with wai-splat-render:i1; with no side
plane over a lower-resolution base the layer is a dual stream's detail.

staged-delivery §3.1 reads refines.side (content hashes, or render entries with a
splat-exact §2 pose); §4.1 step 4 refuses a plane the sink does not hold
(side-absent) or that is not exact (side-not-exact), two reasons that join §4.2's
closed list and the stage claim's. A WAI2 refinement component carries no side
(refinement-side); a packer refuses a WAI1 layer that carries one. SPEC §5's
Refinement capabilities and Base admission rows, §7.1, integer-operators §11 and
jwp-receipts' session rule follow.

wai-rs: staged::wcp1 (parse, cost, lowering, graph decode, the loop-by-loop
arithmetic the encoder holds the graph to, encoder), staged::side, StageSink
add_side, add_render and set_macs_limit, the WCP1 framing check,
RoleError::RefinementSide, AppliedForm::Conditioned. staged-conformance/cond/
holds its cases (exact planes, the dual stream, both in one layer, a rendered
plane, planes absent and not exact, geometry, admission, side and WCP1 faults,
cost limits) and cond/wai2/; wai_staged_vectors (now with splat_exact) writes and
replays them, and staged_verify.py replays them with its own WCP1 parser, network
loop, sink, PNG decode and splat renderer. The two Rust API breaks are
acknowledged in the change's entry.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 17:50:44 -05:00

283 KiB
Raw Permalink Blame History

WAI Extension: JWP Receipt Chain

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:

  • A per-object content commitment (BLAKE3-256) over WAI envelope bytes.
  • A per-group Merkle root binding every object in a group under one signature.
  • A per-group Ed25519 signature over canonical group metadata.
  • A typed receipt envelope (JSON or equivalent CBOR) with explicit parent-receipt linkage for cross-hop preservation: one chain for each publisher key and track (§3 step 3, §6.1); and a binary form of the group and object receipts and of a group's receipts together (§2.7), whose group is its canonical bytes.
  • Verifier behaviour, relay preservation requirements, and conformance test vectors.
  • Typed receipt classes (§2.4): every receipt belongs to exactly one class. group and object receipts are the delivery class; a claim receipt (§2.4.3) carries one of the others — energy, lawful-basis, log-inclusion, measurement, operating-point, stage, delivery-gap, reconstruct-session, consumption, transmission, reconciliation, schedule, timeliness, cache-transmission (§2.4.5–§2.4.18) — on one envelope, one canonical form and one signature rule, so a statement other than delivery does not need a field set of its own.
  • Objects not delivered (§3, §4.4): an object a subscriber did not receive is accounted for by its receipt, checked without the object's bytes and reported as inclusion-verified, never as delivered; a relay forwards the receipts of the objects it does not forward, and may say why in a delivery-gap claim (§2.4.11).
  • What a sink reconstructed (§2.4.12): a reconstruct-session claim records one session of a sink — its decoders and host, its paths and switches, its late presentations, a commitment to every output it presented, the bytes it counted and received, the classical rendition it did not need, the stage claims it signed about the stage sets it observed, and its decode energy where metered — checkable against the outputs, the objects, the envelopes and those stage claims.
  • An optional acquisition-class label on the object receipt, bound with the object's figure and origin into its Merkle leaf (§2.2, §2.4.2); an optional class for the group total, signed under a labelled group payload with its coverage and the parent link (§2.3, §2.4.1); and the group total read from its objects (§3).
  • A second binding class (§2.6): a soft binding, declared by the publisher with a registry-named algorithm and a resolution endpoint, for objects whose bytes changed in normal distribution, over one object or over a span of a track (§2.6.4) — and the ancestor rule (§3.2): a receipt reached through it attests the object, or the span, the one in hand was derived from, never the object in hand.
  • Key discovery and signing authority (§7): a form of signer_id that names the web origin a signer publishes its keys under; a signed, chained key log the origin publishes at a well-known location, listing each key with what it may sign and whether it is active, retired or revoked; and a confirmation a verifier runs after the signature, against an origin it has anchored, reported beside the receipt's own verdict. It adds no member and changes no signed byte.
  • Module authorisation (§7.9): who may authorise the decoder and reconstruct modules a sink runs, and the parameter sets an envelope pins; their revocation by digest; the rule that a sink which cannot establish a module's authorisation falls back rather than run it; and the longest a revoked module keeps running.

Out of scope:

  • A transport. This extension does not define how receipts move on the wire; receipts ride alongside WAI envelopes in the carrier protocol (e.g. a MoQT object header field, an HTTP trailer, a sidecar file). http-delivery §8 defines the HTTP carriage: receipt files named by link hash, a per-object index of WAI_RECEIPT properties, playlist receipts, and the locations of key logs, statuses and grants.
  • A codec. The receipt chain is opaque to the WAI capability dispatch in WAI SPEC.md §4. A receipt attests bytes; it does not interpret them.
  • An envelope. WAI SPEC.md §2 defines the envelope; this extension sits above it. A receipt's content hash is computed over the full WAI envelope (WAI1 magic + manifest + payload), not over the payload alone.
  • A trust store. §7 says what a verifier checks against an origin it has anchored and how it records the anchor; which origins a relying party anchors, and how it provisions a pinned anchor, is its own.
  • A soft-binding algorithm, a resolution service, or a transparency log. This extension names them by identifier and locator (§2.4.7, §2.6) and defines none of them.

Relationship to WAI SPEC.md:

  • WAI SPEC.md §2 (container) — receipts hash the container bytes end-to-end; a sink that mutates the envelope (re-orders manifest keys, re-frames the payload) breaks the content hash and MUST be rejected by §3 step 4 below.
  • WAI SPEC.md §4 (capability dispatch) — receipts attest what was delivered to the dispatcher, not what the dispatcher produced. A sink that successfully verifies a receipt then dispatches the envelope per §4 unchanged.

2. Wire format

2.1. Per-object content hash

For each WAI envelope object the publisher emits, the publisher MUST compute a 32-byte BLAKE3-256 content hash:

content_hash = BLAKE3(domain || envelope_bytes)
domain       = "moq-jwp:object\x01"

envelope_bytes is the full WAI envelope per WAI SPEC.md §2 (the WAI1 magic, the manifest-length prefix, the manifest, the payload- length prefix, the payload — concatenated, with no insertion or reframing). The domain string is appended as a domain separator to prevent cross-protocol hash collisions.

2.2. Per-group Merkle root

A group is an ordered list of one or more objects under a single publisher key. The publisher MUST build a BLAKE3 binary Merkle tree over the object content hashes in their group order. Leaves and internal nodes use distinct domain separators:

leaf(content_hash)        = BLAKE3("moq-jwp:merkle-leaf\x01" || content_hash)
leaf(content_hash, joules_micro, class, origin)
                          = BLAKE3("moq-jwp:merkle-leaf\x02" || content_hash
                                   || u64_be(joules_micro) || class
                                   || u32_be(len(origin)) || origin_utf8)
node(left, right)         = BLAKE3("moq-jwp:merkle-node\x01" || left || right)

An object's leaf takes the first form when its receipt carries no class label (§2.4.2) and the second when it does. class is the label's signed encoding: the tag byte and the evidence that follows it (energy-measurement §2.7), tag 0 marking an object explicitly unmetered. The encoding is self-delimiting and origin is length-prefixed, so no two inputs share a leaf. The version byte keeps the two forms disjoint, so a label, or the origin, can be neither added, stripped nor altered on a delivered object without changing the root the group signature covers. A group may mix the forms. A verifier that predates the labelled form computes the first form for every object: it refuses a labelled object at §3 step 5 and verifies an unlabelled one as before.

When the leaf count at any level is odd, the publisher MUST duplicate the final leaf to obtain an even pair. The root is the single hash at the top of the tree. The empty-group root is the 32-byte all-zero value 0x00..00.

2.3. Per-group Ed25519 signature

The publisher MUST sign the canonical signing payload with an Ed25519 key per RFC 8032 (Pure Ed25519, no prehash). The signing payload is the big-endian concatenation:

signing_payload =
    "moq-jwp:group-sign\x01"          // domain
 || u64_be(track_alias)                // optional carrier-scoped id (0 if unused)
 || u64_be(group_id)                   // monotonic per (publisher_key, track_alias)
 || merkle_root[32]                    // §2.2
 || u64_be(leaf_count)                 // number of objects in the group
 || u64_be(total_joules_micro)         // sum of microjoules across the group
 || u32_be(len(group_start)) || group_start_utf8   // RFC 3339 start timestamp
 || u32_be(len(group_end))   || group_end_utf8     // RFC 3339 end timestamp
 || publisher_key[32]                  // Ed25519 verifying key (RFC 8032)
 || u32_be(len(publisher_id)) || publisher_id_utf8 // bound identity string

The signature is exactly 64 bytes. Implementations MUST reject any group receipt whose signature length is not 64.

A group whose total carries a class of its own (§2.4.1) is signed over the labelled payload instead: the fields above, in that order, under the domain "moq-jwp:group-sign\x02", followed by

 || class                              // energy-measurement §2.7: tag, evidence
 || u8(coverage)                       // 0 Whole, 1 Partial
 || u8(has_parent) || parent[32]       // 1 and parent_receipt_hash, or 0 and 32 zero bytes

class is tag 0 exactly when total_joules_micro is zero, and then coverage is 0. The labelled payload signs parent_receipt_hash, which the legacy payload does not (§6.1). The two domains keep the payloads disjoint. A verifier or relay that predates the labelled payload computes the legacy one and refuses a labelled group at §3 step 2; every group signed over the legacy payload verifies as before.

A group signed by a hybrid key is signed over the labelled payload's third version, "moq-jwp:group-sign\x03" (pq-signatures §6.2): the labelled payload's fields after its domain, then the publisher's ML-DSA-65 key, signed by both halves of the key, and its canonical bytes (§3 step 3) end with both signatures. A verifier that predates it refuses it as malformed: in the JSON form for the members it does not define, in the binary form for the signing domain (§2.7.4).

2.4. Receipt envelope (typed)

Three receipt kinds are defined: group (one per group), object (one per object) and claim (§2.4.3). Every receipt belongs to exactly one class (§2.4.4). group and object are the delivery class; for them the class is implied by the kind and is not carried, so their wire form is exactly as below. A claim names its class in a class member. All are typed JSON; equivalent CBOR is permitted when the carrier mandates it, provided the field set and semantics are identical (for a claim, see §2.4.3 on which bytes are signed). group and object receipts also have the binary form of §2.7.

2.4.1. kind: "group"

Required fields:

field type meaning
kind string MUST equal "group".
track_alias u64 Carrier-scoped track identifier; 0 if unused.
group_id u64 Monotonic group counter under (publisher_key, track_alias).
root_hash 32 B hex §2.2 Merkle root over object content hashes.
leaf_count u64 Number of objects covered by root_hash.
joules_total u64 Sum of microjoules across the group's objects; 0 is the unmetered marker (energy-measurement §2). Its class, where it carries one, is in the labelled members below; a verifier holding every object checks it, or, for a group without one, reads the total from the objects' labels (§3, The group total).
group_start string RFC 3339 timestamp of the first object.
group_end string RFC 3339 timestamp of the last object.
signer_pubkey 32 B hex Ed25519 verifying key per RFC 8032.
signer_id string Bound identity string (DID, URN, or display). In §7.1's origin-bound form it names the web origin whose key log lists signer_pubkey; an Ed25519 did:key names one key; any other form is a label, and a verifier reads nothing from it.
sig 64 B hex Ed25519 signature over §2.3 payload.
parent_receipt_hash 32 B hex or null The link hash (§3 step 3: BLAKE3, domain-separated, over the canonical bytes) of the previous group receipt under this publisher key and track_alias; null for the first group of that track.

The total's class (labelled form). A group whose total carries a class of its own adds these members and is signed over the labelled payload (§2.3):

field type meaning
sign_version integer 2. Absent (or null), the group is in the legacy form; any other value is refused (group_label_invalid), but 3 beside the hybrid members of pq-signatures §6.2 (pq_key, pq_sig), which a verifier that implements that extension reads.
energy_provenance, energy_uncertainty, energy_counter_filtering, energy_calibration_ref as energy-measurement §2.7 The total's class and its evidence; all absent for the unmetered marker beside a zero total.
energy_coverage string Whole or Partial; absent with the unmetered marker.

A legacy group carrying any of these members, a sign_version other than 2, or a labelled group whose class cannot be read, is refused (group_label_invalid), never read as the legacy form. The class MUST be the one the group's objects compose to, by this rule over the objects whose figure F_i is not zero, in integers:

  • if any such object is unlabelled, the group MUST NOT carry a class (it is signed in the legacy form);
  • if there is none, the class is the unmetered marker and the coverage Whole;
  • otherwise the class is the weakest of the objects' classes, in energy-measurement §2's order, and the coverage is Partial exactly when some object's figure is zero;
  • for a hardware class, relative_ppm is at least ⌈Σ F_i·p_i / Σ F_i⌉ and window_us is the longest window_us_i. The bound is linear in the parts, so it assumes their errors fully correlated, and it is conservative;
  • an OnChipCounter total's filtering state is the objects' common one, or Unknown; a CalibratedInstrument total needs one calibration reference shared by every object, or the group carries no class.

The resolution floor then holds for the total. With the group alone a verifier checks that the class reads, is not HwShunt, is the unmetered marker exactly beside a zero total, and reaches the resolution floor (§3 step 2); with every object it checks the composition (§3, The group total).

2.4.2. kind: "object"

Required fields:

field type meaning
kind string MUST equal "object".
track_alias u64 Mirrors the parent group receipt.
group_id u64 Mirrors the parent group receipt.
object_id u64 Monotonic from 0 within the group.
content_hash 32 B hex §2.1 BLAKE3 over the WAI envelope bytes.
joules_micro u64 Microjoules consumed producing this object; 0 is the unmetered marker. This field fixes the unit and the scope of attribution, not how the number is obtained — see energy-measurement, which requires an acquisition class beside it and forbids publishing a figure from a measurement that failed. Its class rides as an optional label, below, bound into the object's leaf with the figure and the origin (§2.2).
origin string Datacenter / origin label.
merkle_proof object Inclusion proof: { "siblings": [32 B hex, …], "direction_bits": u64, "leaf_index": u64 }. direction_bits bit i is 1 iff the current node at level i is the right child.

Optional class label. An object receipt MAY carry its figure's acquisition class as an optional label (energy-measurement §2.8): "leaf_version": 2, and the class in energy-measurement §2.7's JSON fields — energy_provenance, and for a refined hardware class energy_uncertainty with energy_counter_filtering or energy_calibration_ref. With "leaf_version": 2 and no class the object is explicitly unmetered (tag 0), which is valid only beside a zero figure. Absent, the receipt is exactly as above and its leaf takes the legacy form of §2.2. Present, its leaf takes the labelled form, and the group signature covers the figure, the class, the class's evidence and the origin through the root. A label MUST NOT be the legacy HwShunt and MUST NOT sit beside a zero figure; a refined class MUST carry its evidence and reach the resolution floor (energy-measurement §2.3); class members without "leaf_version": 2, evidence without a class, or a class that cannot be read, are refused rather than dropped (energy-measurement §2.7). A verifier refuses an object that breaks any of these at §3 step 5 (object_label_invalid). An energy claim (§2.4.5) remains the way for a party other than the publisher to sign a class for the figure.

2.4.3. kind: "claim"

group and object answer one question — what was delivered, by whom, at what energy cost. Other statements are made about the same objects: how an energy figure was acquired and how much weight it bears, on what basis what a sink observes may be processed, that a receipt was entered in a log, that an object carries a binding that survives re-encoding. None is a delivery statement, and several are signed by someone other than the publisher. A claim receipt carries one such statement — a class-scoped body under a class discriminator — on one envelope.

Required members. A claim has no optional members but two. The three hybrid members of pq-signatures §3.1 (pq_suite, pq_key, pq_sig) are carried all or none; a verifier that does not implement that extension refuses them as unknown_member. A measurement claim's fabrics (§2.4.8) is absent when the claim does not divide its figure by fabric and is never null, so a claim sealed before the member existed keeps its bytes and its link hash. Every other nullable member is present and null, so one claim has one canonical form.

field type meaning
kind string MUST equal "claim".
class string A class of §2.4.4 other than delivery.
subject_type string "object" or "receipt"; which a class admits is fixed by §2.4.4.
subject 32 B hex For object: the §2.1 content hash of the object the claim is about. For receipt: the link hash of a receipt — §3 step 3's for a group receipt, link_hash below for a claim — or, for a class that names a figure profile (§2.4.8–§2.4.9), the identity that profile defines (energy-measurement §6.1).
seq integer Strictly increasing across the claims of one signer_pubkey.
issued_at string A date-time (below) at which the signer issued the claim. Self-asserted, like group_end.
body object Class-scoped; its members are fixed by class and by nothing else (§2.4.5–§2.4.18, §2.6).
signer_pubkey 32 B hex Ed25519 verifying key per RFC 8032.
signer_id string Bound identity string, as for a group receipt (§2.4.1, §7.1).
parent_receipt_hash 32 B hex or null link_hash of this signer's previous claim; null for its first.
sig 64 B hex Ed25519 signature, below.

Encodings. Byte strings are lowercase hex — one byte string, one signed encoding. Integers are unsigned and at most 2^53 − 1, written in plain decimal: a number written with a sign, a fraction or an exponent — -0, 1.0, 1e0 — is not an integer here, whatever its value. A claim carries no other number, and no boolean. A term is a compact IRI (dpv:Consent) or an IRI: a non-empty string of at most 2048 bytes in UTF-8 holding no character with the Unicode White_Space property and no control character (general category Cc). An absolute URI is a term with a scheme (RFC 3986 §3.1), a colon, and a non-empty remainder. A date-time is RFC 3339 §5.6's date-time with ASCII digits, T and Z in either case: a month of 1–12 and a day its month has in its year (§5.7, in the proleptic Gregorian calendar of RFC 3339 Appendix C — 2026-02-29 is not a date, 2028-02-29 is); an hour of at most 23, a minute of at most 59 and a second of at most 60; any number of fractional-second digits, at least one if the point is written; and an offset of Z or ±hh:mm with hh at most 23 and mm at most 59. A second of 60 is accepted at any time of day: whether a leap second was inserted then is a fact about the calendar, not the syntax, and issued_at is self-asserted.

Canonical form, signature, identity. A claim's canonical form is its JSON Canonicalization Scheme serialisation (JCS, RFC 8785). On the values a claim may carry — strings, unsigned integers no larger than 2^53 − 1, null, arrays and objects — JCS reduces to: member names sorted by their UTF-16 code units; no insignificant whitespace; strings escaped with \" \\ \b \f \n \r \t and \u00xx (lowercase hex) for the remaining characters below U+0020, every other character written as itself in UTF-8; integers in plain decimal. No number formatting is involved.

sig       = Ed25519(signer_key, "moq-jwp:claim\x01" || JCS(claim without "sig"))
link_hash = BLAKE3("moq-jwp:claim-link\x01" || JCS(claim))

A verifier canonicalises before it verifies: whitespace and member order on the wire are not signed. Duplicate member names are rejected, since RFC 8785 is defined over I-JSON (RFC 7493), which forbids them, and two parsers resolving a duplicate differently would canonicalise one claim two ways. The signed bytes are this JSON form whatever the carriage: a carrier that moves claims as CBOR MUST reproduce it to verify. A claim SHOULD be sent in canonical form, and relays forward it byte-for-byte (§4.1).

Chain. A signer's claims form one chain through parent_receipt_hash and seq, separate from the group chain: a claim's parent is never a group receipt and a group receipt's parent is never a claim, so every existing group-chain verifier is unaffected by claims. A publisher that issues both keeps two chains under one key.

2.4.4. Receipt classes

class carried by subject_type body signed by
delivery group, object (implied, not carried) — §2.4.1, §2.4.2 the publisher
energy claim object §2.4.5 whoever acquired the figure
lawful-basis claim object or receipt §2.4.6 the service operator (a declaration); the party recording it (a decision)
log-inclusion claim receipt §2.4.7 whoever attaches the log's evidence
soft-binding claim object §2.6.2 the object's publisher
soft-binding-span claim object (the span's anchor) §2.6.4 the track's publisher
binding-recovery claim object (the ancestor) §2.6.3 the party that performed the recovery
measurement claim object or receipt, as its figure profile fixes §2.4.8 the key that signed the figure: the group's publisher for moq-jwp:object, the receipt's signer for every other profile
operating-point claim object or receipt, as its figure profile fixes §2.4.9 the party that records the request, the commitment and the delivery
stage claim object (the stage set's base) §2.4.10 the party that observed the stage set
delivery-gap claim object (the object not forwarded) §2.4.11 the relay that did not forward it
reconstruct-session claim object (the session's entry object) §2.4.12 the sink that reconstructed: the key its decoder host holds
consumption claim object (the first object it names) §2.4.13 the agent that consumed the objects: its key, named by its did:key
transmission claim receipt (the group transmitted) §2.4.14 the group's publisher: the key that signed the group
reconciliation claim receipt (the group reconciled) §2.4.15 the sink that received the objects, never the publisher
schedule claim receipt (the group scheduled) §2.4.16 the group's publisher: the key that signed the group; or a node, of an instance of its own (cache-authority §5.6)
timeliness claim receipt (the group whose objects it times) §2.4.17 the group's publisher: the key that signed the group
cache-transmission claim receipt (the group sent again) §2.4.18 a node that sent the objects from what it held, never the group's publisher

Rules:

  • A verifier MUST reject a claim whose class it does not know, and MUST report the rejection rather than skip the claim. Silently discarding a class a signer thought load-bearing is the failure this rule exists to prevent. The rejection is of that claim only: a delivery receipt for the same subject verifies or fails on its own.
  • delivery is reserved to group and object. A claim naming it MUST be rejected; delivery is stated only in the §2.3 signed payload.
  • The class is read from class and MUST NOT be inferred from the body's shape. A body MUST carry exactly its class's members; a missing or additional member is a rejection, not an extension point. A new class, or a new member, is a new registration here.
  • A claim carries the authority of its signer and no more. class says what is asserted, never that it is true. A claim signature is not evidence about delivery, and a delivery signature is not evidence about a claim. What that authority is — which web origin stands behind the key, and whether it gave the key this class — is §7's to establish. The signed by column above names the party each class expects; §7.5 is how a verifier checks it.
  • A claim that repeats a figure a delivery receipt carries for the same subject MUST agree with it; the energy class does this deliberately, to bind its labels to the figure. Where the delivery receipt labels its figure (§2.4.2), the claim MUST also agree with that label's class and evidence. Where they disagree a verifier MUST reject rather than choose.
  • A class that is about one figure — measurement or operating-point — names it by a figure profile (energy-measurement §6.1) and the identity that profile defines, restates it with its class, and is checked against the receipt that carries it at §3.1 step 6. It never changes that receipt, and never supplies a class that receipt does not carry.

2.4.5. class: "energy"

§2.4.2's joules_micro fixes the unit and the scope of attribution; energy-measurement §2 requires an acquisition class beside any non-zero figure, with the class's evidence inside the signature, and the object receipt has no member for either. An energy claim supplies them without changing the signed object format: it restates the figure and binds to it, under the claim's signature, the labels other extensions define. It defines none of those vocabularies. The class and its evidence are written as energy-measurement §2.7 writes them in a JSON receipt, under the same member names; a class that carries no evidence has those members present and null.

member type meaning
joules_micro integer The figure. Equal to the object receipt's joules_micro for the same subject. A zero is an integer here and is refused by rule 1, at step 4.
energy_provenance string An acquisition class of energy-measurement §2, verbatim.
energy_uncertainty object or null The declared uncertainty (energy-measurement §2.3): { "relative_ppm": integer, "window_us": integer }, exactly those two members.
energy_counter_filtering string or null An OnChipCounter's filtering state (energy-measurement §2.4).
energy_calibration_ref string or null A CalibratedInstrument's calibration reference (energy-measurement §2.5).
coverage string Whole or Partial, per energy-binding §4.
grade string An evidential grade of oer-receipts §2, verbatim.
method absolute URI The published protocol the figure was obtained under.
work_units integer The recomputable work the figure is expressed against (energy-measurement §5). A zero is refused by rule 6, at step 4.
work_unit term What one work unit is (e.g. subpixel).
oer_receipt string or null The receipt_id (sha256:<hex> or blake3:<hex>, lowercase) of an OER receipt recording the same figure, or null.

§3.1 step 2 reads these members in the table's order, and energy_uncertainty's two members, where it is an object, as it is reached; it checks their types and encodings, and nothing more. Which class, evidence and figure they may hold together, and whether the figure or the work count is zero, is step 4's.

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

  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:

member type meaning
record string "declaration".
vocabulary absolute URI The vocabulary the terms are drawn from (for example the W3C Data Privacy Vocabulary).
fragment 32 B hex or null §2.1 content hash of the envelope carrying the declaration fragment beside the manifest, when one is carried.
purposes array One entry per purpose: { "purpose": term, "basis": term or "", "data": [term, …] }, data possibly empty. The array may be empty and a basis may be "" as far as step 2 is concerned: both are refused by the rules below, at step 4, each with its own reason.

A decision (record: "decision"), signed by the party recording it:

member type meaning
record string "decision".
vocabulary absolute URI As the declaration's.
declaration 32 B hex link_hash of the declaration claim the decision is expressed against.
purpose term The purpose decided on.
outcome term The decision, as a vocabulary term.

Rules (§3.1 step 4): a declaration MUST list at least one purpose; purposes are then checked in order, and each MUST carry its own non-empty basis — a basis is per purpose, never per service — and MUST NOT repeat an earlier purpose. Against the declaration it names (§3.1 step 6): the declaration MUST itself pass §3.1 steps 1–4 (context_invalid) and be a lawful-basis declaration (context_mismatch); a decision MUST then name it by link_hash, share its subject, subject_type and vocabulary, and decide on a purpose the declaration lists. That check does not establish that the declaration was the service operator's. A verified delivery receipt supplies a publisher's key (§3.2); an operator's is confirmed under the operator's web origin by §7, with the authority lawful-basis/declaration, and only under a pinned anchor (§7.5). Without that confirmation the check establishes only that the decision matches a declaration, signed by whoever signed it; with it, that the declaration was made under that origin, and, where the verifier expects the service's origin (§7.5), under the service's.

What this class does not do. It records what was declared and what was decided. It is not a consent mechanism: a declaration is agnostic across bases, and each purpose carries its own. It makes no statement that a basis is sufficient for any purpose under any regime, and a verifier here does not check terms against the vocabulary. The shape — purposes, each with its own basis and data categories, from a declared vocabulary — is the one ABNT NBR 25608:2025 uses for its broadcast privacy declaration, as described in that standard's public operational guidelines (see the 2030 landscape survey §7.2), whose records follow ISO/IEC TS 27560. A lawful-basis claim is signed, like every claim.

2.4.7. class: "log-inclusion"

A statement that a receipt was entered in an append-only log, carrying the log's own evidence. Its subject is always a receipt: a log registers receipts, never objects, and per transparency §3 per-object receipts are not registered individually — the subject is a group receipt's link hash, or a claim's.

member type meaning
log absolute URI The log.
profile absolute URI The registration profile the log's evidence is checked under (for example urn:ietf:rfc:9942, COSE Receipts).
entry term The log's identifier for the entry.
log_receipt hex The log's own receipt for the entry, verbatim.

Rule (§3.1 step 4): log_receipt MUST NOT be empty; an inclusion claim without the log's evidence is only its signer's say-so.

Accepting a log-inclusion claim establishes that its signer asserted inclusion and carried the log's evidence. It does not establish inclusion. Inclusion is established only by checking log_receipt under profile against the log, which this extension does not specify; a verifier that has not done so MUST NOT report the subject as logged. This claim's signature never stands in for the log's, and transparency §6 applies: registration is a witness, not a verification.

Whose key attached the log's evidence can be confirmed under §7, with the authority log-inclusion. That confirmation is not a check of log_receipt.

2.4.8. class: "measurement"

energy-measurement §6 requires a published figure to be accompanied by how it was measured: for a figure obtained under that extension's §3, the values its bracket gave, and for any figure, the build profile. A measurement claim carries them for one figure. It restates the figure and its acquisition class, so that it can be checked on its own, and leaves the receipt that seals the figure unchanged. It names that receipt by a figure profile (energy-measurement §6.1) and the receipt's identity under it. Its subject_type and subject are the ones the profile gives: object and the object's §2.1 content hash for moq-jwp:object, and receipt and the receipt's identity for every other profile.

member type meaning
figure_profile term A figure profile of energy-measurement §6.1.
figure_index integer or null Which figure, from 0, in a profile that carries several — for moq-jwp:object, the object's leaf index; null in one that carries one.
group 32 B hex or null For moq-jwp:object, the link hash of the group receipt the object was delivered under (§3 step 3); null for every other profile.
joules_micro integer The figure.
energy_provenance string Its acquisition class, as §2.4.5 writes it.
energy_uncertainty object or null As §2.4.5.
energy_counter_filtering string or null As §2.4.5.
energy_calibration_ref string or null As §2.4.5.
bracket object or null For a figure obtained under energy-measurement §3: { "idle": summary, "busy": summary, "window_us": integer, "completions": integer, "in_flight": integer, "opening": "from-rest" or "running" }, a summary being { "n": integer, "mean_uw": integer, "sd_uw": integer, "live_intervals": integer, "longest_live_run": integer } as energy-measurement §6.2 defines them. null otherwise.
build object { "profile": "release" or "debug", "toolchain": string or null, "target": term or null }: the build of the code measured (energy-measurement §5, §8). release is an optimised build (for a Rust build, opt-level 2, 3, s or z, whatever its profile is named); any other build is debug. A finer vocabulary is a new registration.
power_reader string or null The reader of the power interface (energy-measurement §8). Required under a hardware class (rule 4).
fabrics object, or absent Where the work the figure covers ran, and the figure divided by fabric (energy-measurement §6.3): { "steps": [step, ...], "shares": [share, ...] }. A step is { "step": term, "fabric": term, "backend": string }: a step of the work, in the vocabulary of the document that defines the figure's receipt (for wai:video-package-receipt-id, the steps of packaging §11), a fabric that ran it, and the implementation and device that did, a non-empty string. A share is { "fabric": term, "joules_micro": integer, "attribution": term }. Absent, the claim does not divide its figure; never null (§2.4).

A report carries none of what follows from its values: neither the §4 ratio nor its verdict, which every reader derives (energy-measurement §6.2). A bracket carrying either has a member its table does not define (unknown_member).

A null bracket says the figure was not obtained under energy-measurement §3 — under a hardware class, for example, a counter read through the span of the work. Such a report carries no values to check, so the figure's liveness (energy-measurement §3.1) rests on its producer's statement alone, and no reader derives a §4 verdict for it.

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

  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.

member type meaning
figure_profile, figure_index, group, joules_micro, energy_provenance, energy_uncertainty, energy_counter_filtering, energy_calibration_ref As §2.4.8.
vocabulary absolute URI The vocabulary the operating points' parameters, and the prediction's descriptor, are drawn from. WAI defines none except staged-delivery's urn:wai:staged-delivery:1 (energy-binding §7.2), whose points are a stage claim's (§2.4.10).
op_requested point or null What the requesting party asked for; null when nothing was requested.
op_committed point or null What the acting party said it would use.
op_delivered point or null What was used.
prediction object or null { "predictor": absolute URI, "descriptor_hash": 32 B hex, "predicted_joules_micro": integer }: the model that turned the declared descriptor into energy, BLAKE3("wai:energy-descriptor\x01" ‖ descriptor) over the descriptor as vocabulary serialises it, and the energy it implies for the work the figure covers.

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

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

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

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

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

  • whether the commitment was met: it was when op_delivered equals op_committed, and there was none when op_committed is null;
  • the residual, joules_micro − predicted_joules_micro, a signed integer, whenever prediction is present.

A reader MUST NOT report the commitment as met unless it derives that it was. An accepted claim establishes what its signer recorded about the request, the commitment, the delivery and the prediction. An op_requested in a claim the acting party signed is that party's account of the request.

2.4.10. class: "stage"

A stage set (staged-delivery) is a base and the layers that refine it, each its own object with its own delivery receipt. A stage claim records what one party observed of a stage set: what it asked for and was promised, which layer objects arrived or did not and what it verified of their receipts, what it presented on time, and how the presented output is reproduced. Its subject is the base's §2.1 content hash. It is signed by the party that observed the set: a sink, or another party that applies the set as a sink does (staged-delivery §10.2).

member type meaning
vocabulary absolute URI urn:wai:staged-delivery:1 (energy-binding §7.2), whose points have one parameter, wai-stage:layers: the number of layers a prefix of the set holds.
op_requested point or null The stage asked for.
op_committed point or null The stage the serving party said it would deliver.
op_delivered point or null The stage the delivered objects reach: the highest k such that, at every layer from 1 to k, the objects listed with delivered-verified evidence cover every unit of the set, whatever became of them, an object refused before its refines was checked against the set excepted. null when the observer did not verify the base's delivery receipt over its bytes before it offered the base's first layer.
presented point The lowest stage at which a unit was presented on time.
units_by_stage array of integers Element i: the number of units presented at stage i. The set's units, numbered from 0, are the units it counts.
tier string decode-equivalence, entropy-consistency or none: the weakest tier of the presented units (staged-delivery §5).
stage_hash 32 B hex or null The stage hash of the presented output over every unit (staged-delivery §5).
objects array Every layer object observed, each an object of the table below, ordered by layer, then units.from, then object.
conflicts integer Units frozen by a conflict (staged-delivery §4.1 step 7).
deadline_misses integer Units presented below the stage their layers would have reached on time (staged-delivery §8).
bytes_envelopes integer Envelope bytes received for the set, the base's included (staged-delivery §11).
member type meaning
layer integer The object's refines.layer.
object 32 B hex Its §2.1 content hash.
units object { "from": integer, "count": integer }: the units it covers, its refines.span or every unit of the set. For an object refused before its refines was checked against the set — a reason of staged-delivery §4.1 steps 1 and 2, refines-malformed, capability-not-refinement or base-absent — the units its refines states, which need not lie in the set.
bytes integer Its envelope bytes as received: more than 0 for an object that arrived, 0 for one that did not.
status string applied (to every unit it covers), partial (to some of them), late (it arrived, and missed the deadline of every unit it covers), dropped (it never arrived: not forwarded, or abandoned at a deadline) or refused (it arrived, and applied to no unit for another reason).
evidence string What the observer verified of its delivery receipt: delivered-verified (§3 step 6, over its envelope bytes), inclusion-verified (§3, Verified by inclusion) or none.
reason string or null For partial, the staged-delivery §4.2 reason of the lowest unit it was not applied to; for refused, the envelope's reason, or else that of its lowest unit. null for applied, late and dropped. A unit the object is still held for when the claim is made counts as not applied, for the reason it is held for.

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

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

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

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

2.4.11. class: "delivery-gap"

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

member type meaning
track_alias integer The object's track_alias.
group_id integer Its group_id.
object_id integer Its object_id.
reason string delivery-timeout (its delivery timeout passed at this relay), discretion (the relay chose not to forward it) or not-received (the relay never received it).
observed_at string A date-time (§2.4.3): when the relay observed the gap.

The coordinates are integers of §2.4.3, so an object at a track_alias, group_id or object_id above 2^53 − 1 has no gap claim.

Against the object receipt (§3.1 step 6): the object receipt at the claim's coordinates, verified by inclusion at least. In order: it is at the claim's track_alias, group_id and object_id (context_mismatch), and its content_hash is the claim's subject (gap_subject_mismatch). A claim checked without that receipt stops at step 5: it establishes that the relay says it did not forward an object with this content hash, not that the publisher sent one. An accepted claim says nothing about whether the object reached anyone by another path.

2.4.12. class: "reconstruct-session"

A sink that reconstructed a title MAY record what it did in a reconstruct-session claim: the decoders it used, against which parameter sets and on which host; which path each unit took and why it switched; the presentations it made after their deadline; a commitment to every output it presented; the bytes it counted and received; the declared length of the classical rendition it did not need; the stage claims it signed about the stage sets it observed; and, where its decode was metered, the decode's energy. Its subject is the §2.1 content hash of the session's entry object: the first envelope the session dispatched. It is signed by the sink, with the key its decoder host holds. It asserts what that key reports and nothing more, and it is not evidence that a person watched.

A session claim and a stage claim (§2.4.10) are about different things, and neither repeats the other. A stage claim is about one stage set: each layer object with its status and the evidence of its receipt, the units presented at each stage on time, the conflicts and deadline misses, the delivered stage and the set's envelope bytes. A session claim is about a title, which can hold several stage sets and envelopes that are no part of one. It names the stage claims its sink signed about the title's sets (stage_sets), and commits to every output it presented across the title, late ones included (output_root). A unit's highest stage is read from the outputs, and a set's layer objects from its stage claim. Step 6 checks the two against each other.

member type meaning
session_nonce 32 B hex Fresh random bytes per session, derived from nothing that identifies a person, a device or an account.
media string The entry object's manifest media (SPEC §3), as written: 1 to 32 of a–z, 0–9 and -.
host object The decoder host (below).
decoders array The decoder entries (below), in first-use order: taking the presentations in order of (layers, unit), each entry is first named after the entry before it.
units integer The units presented at least once.
leaf_count integer The presentations: one per (unit, layers) presented.
output_root 32 B hex or null The root of the output tree over the presentations (staged-measurement Appendix C), each leaf's decoder an index into decoders. null exactly when leaf_count is 0.
primary_units integer The units whose presentation at their largest layers came through a decoder entry whose path is primary.
fallback_units integer The same, for fallback.
switches array At most 1024 path switches, each { "unit": integer, "from": string, "to": string, "reason": string }, in order of unit: from and to are primary or fallback, and reason is a term of delivery-telemetry §5.
switches_total integer Every switch, including any not written.
deadline_us integer or null The time, in microseconds, within which the sink's schedule had each unit presented; null where it ran under none.
missed integer The presentations made after their unit's deadline.
missed_units array At most 256 of them, each { "unit": integer, "layers": integer, "late_us": integer }: which presentation, and how many microseconds late; in order of (unit, layers).
stage_sets array For each stage claim the sink signed about a stage set, { "first_unit": integer, "claim": 32 B hex }: the title's unit that is the set's unit 0, and that claim's link_hash; in order of (first_unit, claim). A sink signs one for each epoch of a set — before it forgets the set's base (staged-delivery §4.4, §14), and at the session's end — and names them all.
framing string The framing label of the counted members (staged-measurement §3, its §3.3): registered.
counted_primary integer The counted bytes (staged-measurement §3) of the countable objects received for the primary path.
counted_fallback integer The same, for fallback.
counted_discarded integer The counted bytes of the countable objects received and never presented. They are in the two members above as well.
uncounted integer The objects received that are not countable (staged-measurement §3, Only a checked format is counted). Their bytes are in no counted member.
transferred integer Bytes of WAI objects as received, whole or as byte ranges, without transport or receipts.
receipt_bytes integer Bytes of receipts and claims received (staged-measurement §3.5).
classical_declared integer or null The sum, over the WAI2 envelopes the session received whose classical rendition (below) is of a capability no decoder entry decoded, of that rendition's length as the envelope's rendition table declares it (SPEC §7.1). null where there is none. Never estimated. This revision derives it from the WAI2 envelopes the sink holds in full, the ones rule 9 reads: a sink that received a WAI2 envelope only as byte ranges holds its head but not the envelope, and cannot yet state that envelope's classical rendition. Recording such a session is HTTP session work still to come.
classical_source term or null Where classical_declared comes from: wai2-table, or a term a later extension registers here.
energy object or null null when the decode was unmetered. Otherwise { "joules_micro", "energy_provenance", "energy_uncertainty", "energy_counter_filtering", "energy_calibration_ref" }, each written as §2.4.5 writes it (below).

The host is { "profile": string, "target": term or null, "build": object, "threads": integer }. profile is native-ffi (a native process), browser-baseline (a browser decoding on its processor), browser-gpu (a browser decoding on its graphics processor) or edge-wasi (a WebAssembly host outside a browser), until an extension of sink profiles registers the set; target the platform the host ran on; build the build of its decoders, as §2.4.8 writes a build; and threads its decode workers, written as the largest power of two not above their number, so that it does not single out a machine. Where energy is not null, those are the workers of the bracket behind the figure, which ran on a power of two of them: its in_flight is threads (staged-measurement §8.7), so the measurement claim on the figure names no more of the machine (§6.7).

A decoder entry:

member type meaning
capability term The capability decoded.
rendition integer or null Its WAI2 rendition index (SPEC §7.1): capability is that rendition's capability. null for a WAI1 envelope.
object 32 B hex The content hash (§2.1) of the object it decoded from: the WAI2 envelope whose rendition it is, or the WAI1 envelope — a base, or a layer that refines one. An entry decodes one object, so a sink that decodes several through one decoder names an entry for each.
first_unit integer The title's unit that is unit 0 of the object's base: of the object itself for a base or a rendition with no refines, and of the base it refines for a layer. A presentation through the entry of the title's unit u is of the object's unit u − first_unit. Where a verifier holds a layer's envelope and an entry names its base, the layer's entry has that entry's first_unit; where it holds a stage claim of a set, an entry naming the set's base or a layer the claim lists has the unit stage_sets names that claim at (step 6).
path string primary or fallback.
decoder string builtin:<crate>@<version> — <crate> 1 to 64 of a–z, 0–9, _ and -, <version> 1 to 64 of ASCII letters, digits, ., + and -, beginning with a digit — or module:sha256: followed by a module's digest (§7.9), 64 lowercase hexadecimal digits.
variant term or null The decode path within the decoder that ran, where the capability has more than one: exact for the path that computes the reconstruction the capability registers (for wai.image.jpegai, its exact decoder), or float for a path that runs a stage in floating point the registration does not fix (for wai.image.jpegai, its float decode) (determinism-tiers §5). null for a capability with one path. A NeuralIntegerExact capability's integer path may also be named by the lowering its registration names (integer-operators §11, its ops member's lowering), as a present entry of opus-dred §7.2 names dred-int/1 (synthesis §10.3). The entry states an exact path when its variant is null, exact, or, for a capability of the replicate class neural-int-exact, that lowering; any other term, float among them, names a path that is not exact. A sink states the path that ran, as the reference dispatch reports it, not the tier its capability is registered at.
pin 32 B hex or null The parameter set the decode read, by the digest a pin over it names (SPEC §3.1, Pin by default): the sha256 of the pin the sink verified; for a decode that verified no pin, the digest a pin over the bytes it read would name, which is the SHA-256 of the one file of a capability registered with a one-file set, and otherwise the SHA-256 of the listing (wai.prior.set) of the files it read. null exactly when the decode read no parameter set.
tier string or null That pin's declared tier as SPEC §3.1 reads it: decode-equivalence, entropy-consistency or none; none for a decode that verified no pin, which nothing declared. null exactly when pin is.
module_status string or null For a module: decoder, the anchor its grant was confirmed under (§7.9): pinned or fetched. null for a builtin: decoder.
leaves integer The presentations decoded through the entry.

A decoder entry sealed by a sink of an earlier text of this extension, under which pin named only a pin the sink verified, may carry pin and tier null for an unpinned decode that read a parameter set. A verifier cannot tell such an entry from one whose decode read no set.

Module members. A decoder entry whose decoder is a module: decoder that ran under the WAISM™ profile (waism §10) carries the five members below, and every other entry carries none of them. A verifier that implements that profile reads them: §3.1 step 2 does not report their absence (missing_required_field); where they are present it reads them after leaves, and checks their types; and rules 15 to 17 check their values. A verifier that does not implement it refuses an entry that carries any of them as unknown_member, as one that does not implement pq-signatures refuses the hybrid members (§2.4.3), and applies none of rules 15 to 17. An entry sealed by a sink of an earlier text of this extension has none of them. Where such an entry is a module: entry whose tier is decode-equivalence, it was valid under that text, and a verifier that implements the profile refuses it under rule 16 (session_module_profile_missing): an earlier text let a sink state that tier for a module's output with nothing of the module checked, and this one does not. Every other entry sealed under an earlier text has the outcome it had.

member type meaning
module_profile term The identifier of the level the module was held at (waism §2).
engine string The engine that ran it, written as waism §7 states.
engine_settings array of terms The settings of waism §7 the run was under.
module_probe string match where waism §6's check 10 ran its probe, or its vector, in the sink's process and the output matched; none where none ran.
conform_result 32 B hex The SHA-256 of the RFC 8785 form, sig included, of the conform-result/1 record that attested the module (conformance-results §2; waism §6, check 9c).

energy is the decode energy of one completion of the presentations the claim commits to — the decodes behind its leaf_count leaves, the work unit staged-measurement §8 names (its §8.1) — on the host's processor rails (its §8.4), with its acquisition class. It is the figure of profile moq-jwp:claim-link (energy-measurement §6.1), named by this claim's link_hash, and its producer publishes its report in a measurement claim (§2.4.8) signed with the same key (energy-measurement §6). A decode that was not metered has energy null, never a figure of 0 (energy-measurement §2).

§3.1 step 2 reads the members in the table's order, and the members of the host, of each decoder entry, switch, missed_units entry and stage_sets entry, and of energy and its energy_uncertainty, as it reaches them; it checks their types, and host.profile, host.build.profile, path, from, to and framing against the values the tables give them.

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

  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 in its capability's staged form, WIR1 for its unit kind (staged-delivery Appendix A) or WCP1 for the conditioned form (Appendix B), 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):

member type meaning
alg term The algorithm identifier, verbatim from the registry registry names, in that registry's reverse-DNS form (a rule below, at step 4).
registry absolute URI The public algorithm registry alg is listed in.
binding_type string "watermark" or "fingerprint", as the registry types the algorithm.
value hex The value the algorithm produced for the object.
resolution_endpoint absolute URI Where this declaration — and through it the publisher's receipts for the object — may be recovered from value. That it is an https URI with a host and no userinfo is a rule below, at step 4.

Rules (§3.1 step 4): alg MUST be in reverse-DNS registry form (two or more dot-separated labels of ASCII letters, digits, _, + and -) and so MUST NOT be a bare name; value MUST NOT be empty; resolution_endpoint MUST be an https URI with a host and no userinfo. That is: it begins with the characters https://, and its authority — what follows, up to the first /, ? or # — holds no @ and is a host, optionally followed by : and a port of ASCII digits (RFC 3986 §3.2.2–§3.2.3; the port may be empty). The host is the text before the first :, or, for an IP literal, the text between [ and ], and it MUST NOT be empty. Nothing further about the host is checked.

The fields align with the C2PA soft-binding algorithm list — a public, schema-validated registry whose entries carry an algorithm identifier, its type (watermark | fingerprint) and optional resolution APIs. A declaration naming that list in registry names its algorithm by the list's identifier in alg. WAI defines no soft-binding algorithm, no resolution service and no transport for one: resolution_endpoint is a locator, referenced the way the carrier protocol is, and a verifier here checks that alg has registry form, not that it is listed. A soft binding of this class covers the whole object, and §2.6.4 leaves it unchanged; a binding over a span of a track is §2.6.4's class. A binding scoped to a region of a picture is not expressible.

2.6.3. Recovery record (class: "binding-recovery")

A recovery (§3.2) MAY be recorded as a claim of class binding-recovery, signed by the party that performed it. Its subject is the ancestor — the declaration's subject — never the object in hand.

member type meaning
alg term As the declaration's.
binding_type string As the declaration's.
value hex The value computed from the object in hand.
declaration 32 B hex link_hash of the soft-binding claim the value matched.
resolution_endpoint absolute URI The endpoint the recovery was made through; an https URI with a host and no userinfo, as §2.6.2's rule requires at step 4.
observed 32 B hex or null §2.1 content hash of the object in hand, when it is still a WAI envelope.
attests string MUST be "ancestor" — the ancestor rule, stated in-band. Any string passes step 2; another value is refused at step 4 (recovery_not_ancestor).

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

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

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

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

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

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

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

member type meaning
alg, registry, binding_type, value, resolution_endpoint as §2.6.2 As §2.6.2's, value being what the algorithm produced over the span.
span object The span, below.

span is an object of exactly these members:

member type meaning
track_alias integer The track, as its group receipts name it (§2.4.1), under the declaration's signer_pubkey.
start_ms integer The span's first instant of media time, in milliseconds from the track's media time 0.
end_ms integer Its last, in the same units; both ends are in the span.
first_group integer or null The first group that carries the span, or null where the declaration states no groups.
last_group integer or null The last such group, or null with first_group.
first_object integer or null With groups, the span's first object, an object_id in first_group; null where the span is stated by whole groups.
last_object integer or null Its last object, an object_id in last_group; null with first_object.

Rules (§3.1 step 4), after §2.6.2's rules on alg, value and resolution_endpoint: first_group and last_group are both null or both integers, first_object and last_object likewise, and the objects are integers only beside integer groups (span_malformed); start_ms ≤ end_ms, first_group ≤ last_group, and (first_group, first_object) ≤ (last_group, last_object), compared by group and then by object (span_inverted).

An object receipt is within the span when its track_alias is the span's and its signer is the declaration's, and: with objects stated, its (group_id, object_id) is from (first_group, first_object) to (last_group, last_object), compared as above; with groups alone, its group_id is from first_group to last_group; with media time alone, its content_hash is the anchor. Delivery receipts carry no media time, so a span stated by media time alone places no object but the anchor; groups or objects are how a publisher lets a recovery reach the rest of the span's receipts.

Whole-object bindings are unchanged. This class adds no member to soft-binding: a soft-binding claim that carries span is unknown_member (§3.1 step 2), and every soft-binding claim, and every recovery through one, verifies byte for byte as before. A span is a class of its own rather than an optional member of soft-binding because a claim has no optional members (§2.4.3): one claim, one canonical form.

The C2PA mapping. A declaration of either class is one block of a C2PA soft-binding assertion (C2PA 2.4 §18.10), {"scope": S, "value": V} with V its value, the assertion's alg its alg. For soft-binding, S is {}: no timespan and no region, the whole asset. For soft-binding-span, S is {"timespan": {"start": start_ms, "end": end_ms}}. C2PA's start and end are unsigned integers, the time range in milliseconds from the start of the media over which the value was computed (C2PA 2.4 §18.10.2); C2PA states no reading of the range's end, and its example's adjacent blocks end at 133016 and start at 133017, which is the reading of both ends included that this class states. The groups and the objects have no C2PA field: they are the coordinates a recovery checks receipts against, and stay on the WAI side. C2PA matches a soft binding by alg and value (C2PA 2.4 §18.10.5.1), and the scope says what the value was computed over without taking part in the match; a recovery here matches the same way and uses the span to bound what the match reaches.

Why a span. A live track has no single object a soft binding can be computed over that survives a cut: an object holds a fraction of a second, a clip taken from the track spans many objects, and a watermark whose payload repeats in every frame is read from any of them. Declaring the value once per span, with the coordinates that carry it, lets one declaration cover the stream, and keeps what a recovery attests no wider than what the publisher declared.

2.7. Binary form

Group and object receipts have, beside their JSON form, a binary form: the same members, as bytes, with no member names and no hexadecimal. A carrier uses it where the receipts' size matters, as the receipts track of moq-streaming-format §4.1 does. Integers are big-endian, as in §2.2 and §2.3, and a string is u32_be(len) ‖ UTF-8, as there. Claims (§2.4.3) have no binary form.

The binary form is a second spelling of the JSON members, not a second receipt: reading it yields the members of §2.4.1 and §2.4.2, each with the value the JSON form gives it, and a verifier then reads and checks those members by §3 unchanged. It is canonical: a receipt has one binary form, with no optional encoding, so a writer reproduces the bytes it read. A receipt whose JSON members a reader of §3 step 1 or step 4 refuses has no binary form. A reconciliation (§2.4.15) has a compact form for return paths whose messages hold few bytes, bearer-receipts §13, which a receiver expands to the claim's canonical form before it verifies it.

2.7.1. A group receipt

group_bin = canonical_bytes(G)                               // the labelled form
          | canonical_bytes(G) || u8(has_parent) || parent[32]   // the legacy form

canonical_bytes(G) is §3 step 3's: the group's §2.3 signing payload, in the form it was signed in, then its 64-byte sig. The labelled payload signs the parent link and carries it; the legacy one does not, so a legacy group adds it: 1 and parent_receipt_hash, or 0 and 32 zero bytes. The first 19 bytes, the signing domain, give the form: moq-jwp:group-sign\x01 the legacy form, moq-jwp:group-sign\x02 the labelled form ("sign_version": 2), and, for a verifier that implements pq-signatures, moq-jwp:group-sign\x03 the hybrid form (§6.2 there: the labelled form's fields, the 1952-byte key, sig, the 3309-byte pq_sig).

Read in that order, each field's length is known: the strings are length-prefixed, and a class is a tag byte and the evidence that tag carries (energy-measurement §2.7). The fields are the members track_alias, group_id, root_hash, leaf_count, joules_total, group_start, group_end, signer_pubkey, signer_id and sig; in the labelled form the class is energy_provenance and its evidence members (tag 0: none, the unmetered marker), the coverage byte is energy_coverage (0 Whole, written as no member beside tag 0; 1 Partial), and the parent link is parent_receipt_hash (null beside 0). A group's link hash (§3 step 3) is computed over the bytes received, the first len(canonical_bytes(G)) of group_bin, with nothing re-serialised.

2.7.2. An object receipt

object_body = u64_be(object_id)
           || content_hash[32]
           || u64_be(joules_micro)
           || u32_be(len(origin)) || origin_utf8
           || u8(n) || siblings[32 × n]     // merkle_proof.siblings, in order
           || u64_be(direction_bits)
           || u64_be(leaf_index)
           || u8(leaf_version)              // 1 the legacy leaf, 2 the labelled leaf
           || class                         // leaf_version 2 only: tag and evidence

object_bin  = u64_be(track_alias) || u64_be(group_id) || object_body

leaf_version 1 is the legacy leaf: no leaf_version member and no class members. 2 is the labelled leaf, "leaf_version": 2, and the class that follows is its label: tag 0 explicitly unmetered (no class members), any other the class and its evidence (§2.4.2). An object receipt standing alone is object_bin; inside a group's receipts (§2.7.3) it is object_body, its track_alias and group_id the group's.

2.7.3. A group's receipts

receipts_bin = "WJR1"                  // magic, 4 bytes
            || group_bin
            || u32_be(count)
            || object_body × count

One record holds a group receipt and object receipts of that group, each with the group's track_alias and group_id, in the order the carrier lists them.

2.7.4. Reading

A reader reads exactly the bytes it is given, in the order above, and refuses them as malformed at the first of these that holds: at §3 step 1 for the magic, the group, the count and the record's end; at §3 step 4 for an object.

  • The magic is not WJR1, or the signing domain is neither of §2.3's.
  • The bytes end before a field the layout requires, or bytes follow the last field (the last object, or a group or object read on its own).
  • A string is not UTF-8.
  • A class's tag is not one energy-measurement §2.7 defines, or its evidence does not read under that tag: an OnChipCounter filtering byte above 2, a calibration reference that is not UTF-8.
  • A coverage byte is not 0 or 1; a parent flag is not 0 or 1, or is 0 and the 32 bytes after it are not zero; leaf_version is not 1 or 2.

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


3. Verifier algorithm

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

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

  2. Verify the Ed25519 group signature. Construct the signing payload per §2.3 from the receipt's fields: the labelled payload for a group with "sign_version": 2, the legacy payload otherwise. Verify sig against signer_pubkey under §3.1 step 3's rule — S < L, R not of small order, the cofactorless equation — the same rule every claim signature is held to, so that every conforming verifier accepts the same group signatures. If verification fails, reject (signature_verify_failed). For a labelled group, check its class with the group alone (§2.4.1) and reject one that breaks it (group_label_invalid). After this step, the verifier holds a verified root: the receipt's root_hash field, now bound to signer_pubkey under signature, and for a labelled group the total's class. A hybrid group (pq-signatures §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:

  • for a group in the legacy form, it reads the total from the objects' labels as energy-measurement §2.8 reads a total: unmetered, unlabelled, or the weakest class as a class name only;
  • for a labelled group, it checks that the group's class is the one the objects compose to (§2.4.1) — the class and its evidence other than the declared uncertainty (group_class_disagrees), the coverage (group_coverage_disagrees), and a relative_ppm at least the composed one over the composed window (group_uncertainty_below_parts) — and reads the total at the group's signed class.

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

3.1. Claim receipts

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

  1. Envelope. A value that is not JSON (RFC 8259), is not an object, or has a duplicate member name is malformed. A text that begins with a byte-order mark is not JSON here: RFC 8259 §8.1 lets a parser ignore one, and this step does not. So is malformed a value holding a \u escape that leaves a surrogate unpaired — RFC 8259's grammar admits it, but it denotes no Unicode text, and parsers differ on what they make of it (RFC 8259 §8.2) — or arrays and objects nested more than 32 deep, the claim itself being the first level. A number is never malformed: every JSON number is JSON, whatever its size, and one a member may not hold (1e400, -0, an integer of four hundred digits) is field_type below. kind absent is missing_required_field; kind other than "claim" is wrong_kind. Any §2.4.3 member absent is missing_required_field; any other member present is unknown_member. class that is not a string is field_type; equal to delivery it is class_reserved, and a string naming no class of §2.4.4 is unknown_class. Then, in table order with class done and body left to step 2, a member of the wrong type or encoding — subject_type outside its enumeration, a hash that is not 64 lowercase hex digits, seq not an integer of §2.4.3, issued_at not a date-time of §2.4.3, sig not 128 lowercase hex digits — is field_type. A subject_type the class does not admit is subject_type_not_admitted.
  2. Body. A body that is not an object is field_type. Otherwise it is checked against the class's member table (for a lawful-basis body, the table of the record that record, read first, names; see §2.4.6): a member absent is missing_required_field (but a measurement body's fabrics, which may be absent, §2.4), an additional member is unknown_member, and then, in the table's order, a member that is not of the type the table gives it is field_type; a fabrics that is null is field_type, and within it, as in a bracket, a member absent is missing_required_field and an additional one unknown_member (steps before shares, each array's rows read for their members before its rows' values). That is a wrong JSON type; an integer that is not one of §2.4.3; a byte string that is not lowercase hex, two digits a byte, or a hash that is not 64 of them; a value outside an enumeration this extension defines here (record, binding_type, a bracket's opening, a build's profile, a stage's tier, a stage object's status and evidence, a consumption claim's usage vocabulary, a transmission, 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.

step reason raised when
4 energy_zero_figure joules_micro is 0 in an energy, measurement or operating-point claim, or in a reconstruct-session claim's energy
4 energy_unknown_acquisition energy_provenance names no class of energy-measurement §2
4 energy_legacy_acquisition energy_provenance is the legacy HwShunt
4 energy_acquisition_evidence the class's evidence is missing, carried by a class that has none, or below the resolution floor (§2.4.5 rule 2)
4 energy_unknown_coverage coverage is not Whole or Partial
4 energy_unknown_grade grade names no grade of oer-receipts §2
4 energy_grade_exceeds_acquisition grade ranks above what the acquisition class supports
4 energy_zero_work work_units is 0
4 lawful_basis_no_purposes a declaration lists no purpose
4 lawful_basis_purpose_without_basis a purpose has an empty basis
4 lawful_basis_duplicate_purpose a purpose repeats an earlier one
4 log_inclusion_no_evidence log_receipt is empty
4 soft_binding_alg_not_registry_form alg is not in registry form
4 soft_binding_empty_value value is empty
4 soft_binding_endpoint_not_https resolution_endpoint is not an https URI with a host and no userinfo
4 span_malformed a span's groups, or its objects, are one null and one integer, or its objects are integers beside null groups (§2.6.4)
4 span_inverted a span's start_ms is after its end_ms, its first group after its last, or its first object after its last (§2.6.4)
4 recovery_not_ancestor attests is not "ancestor"
4 recovery_of_hard_bound_object observed equals subject
4 figure_unknown_profile figure_profile names no profile of energy-measurement §6.1 for the claim's subject_type
4 figure_index_mismatch figure_index is an integer under a profile that carries one figure, or null under one that carries several
4 figure_group_mismatch group is a hash under a profile other than moq-jwp:object, or null under it
4 measurement_no_power_reader under a hardware class, power_reader is null
4 measurement_too_few_samples a bracket's n is below 2
4 measurement_rail_not_live under a hardware class, a bracket's liveness facts do not show a live rail, or its samples are all equal
4 measurement_empty_burst a bracket's window_us, completions or in_flight is 0
4 measurement_no_marginal a bracket's busy mean is not above its idle mean
4 measurement_below_attribution_floor under a hardware class, the marginal draw is below the idle standard deviation
4 measurement_figure_mismatch joules_micro is not the figure the bracket gives
4 measurement_window_mismatch under a hardware class, the declared window is not the bracket's
4 measurement_declaration_below_bracket under a hardware class, relative_ppm is below the least the bracket supports
4 measurement_fabric_unknown a fabric record names a fabric other than cpu, gpu, media and npu, or an attribution other than task and rail
4 measurement_fabric_order a fabric record with no step or no share, its steps not in the order of their step and fabric, or its shares not in the order of their fabric, or a pair or a fabric twice
4 measurement_fabric_unmetered a fabric record's step runs on a fabric the record gives no share, or a share of 0
4 measurement_fabric_unused a fabric record's share is on a fabric no step of it runs on
4 measurement_fabric_sum a fabric record's shares do not sum to joules_micro
4 operating_point_empty_point a point is present with no member
4 operating_point_delivered_alone op_delivered is present without op_committed
4 operating_point_incomplete op_requested or op_committed is present without op_delivered
4 operating_point_empty neither op_delivered nor prediction is present
4 stage_point_malformed a stage claim's vocabulary is not urn:wai:staged-delivery:1, or one of its points is not exactly wai-stage:layers from 0 to 255
4 stage_point_incomplete op_requested or op_committed is present in a stage claim without op_delivered
4 stage_hash_without_tier stage_hash is present without decode-equivalence, or absent with it
4 stage_units_malformed no unit, more than 256 stages, a last stage with no unit, a presented other than the lowest stage with a unit, or more conflicts or deadline misses than units
4 stage_objects_malformed a reported object out of order, at layer 0 or above 255, over no unit (a partial one over fewer than two), outside the units when its span was checked, with a reason where none belongs or none where one does; applied or partial with evidence other than delivered-verified beside a stated op_delivered; dropped with delivered-verified evidence; arrived with no bytes or dropped with some; objects counting no fewer bytes than the set; or two applied objects of one layer over one unit
4 stage_units_unsupported an applied object of layer s outside the spans of layer s − 1's applied and partial objects, or a partial one that does not meet them; or more units at stage s or above than layer s can have reached, a partial object counting no more than its span less one
4 stage_delivered_disagrees op_delivered is not the stage the reported objects give
4 session_path_sum a session's primary_units and fallback_units do not sum to units
4 session_leaf_sum the decoder entries' leaves do not sum to leaf_count, leaf_count is below units, or one of units and leaf_count is 0 and the other is not
4 session_root_presence output_root is null with presentations, or a hash with none
4 session_host_malformed the host's threads is not a power of two
4 session_decoder_malformed a decoder entry's decoder is not of its grammar, its rendition is above 65535, its tier is present without pin or absent with it or is not a tier, its module_status does not fit its decoder, or its leaves is 0; or two entries differ only in leaves; or an entry's module members are not all five or none, are on a builtin: entry, or do not take the values rule 15 gives them
4 session_tier_exceeds_class a decoder entry's tier exceeds its capability's class tier
4 session_switch_invalid a switch between equal paths or for a reason delivery-telemetry §5 does not list, switches out of order, or other than switches_total written, 1024 where it is more
4 session_missed_invalid missed above leaf_count, or not 0 with no deadline; missed_units holding other than missed entries, 256 where it is more, out of order, or an entry with layers 0 or above 65535 or late_us 0
4 session_stage_sets_invalid stage_sets out of order, or an entry twice
4 session_counted_invalid counted_discarded above counted_primary and counted_fallback together
4 session_classical_invalid classical_source present without classical_declared or absent with it, or not wai2-table
4 session_variant_not_exact a decoder entry names a capability of the replicate class neural-int-exact and does not state an exact path: a variant other than null, exact or the lowering its registration names
4 session_tier_exceeds_variant a decoder entry does not state an exact path, and its tier is decode-equivalence
4 session_module_profile_missing a module: decoder entry whose tier is decode-equivalence does not name the integer level of the WAISM profile
4 session_module_engine_not_deterministic a decoder entry at the float level of the WAISM profile, with a tier that is neither null nor none, lacks nan-canonical or a matching probe
4 consumption_objects_unordered a consumption claim's objects are not in strictly ascending order (ai-usage §6.2)
4 consumption_subject_mismatch its subject is not the first of its objects
4 consumption_usage_unordered its usage entries are not in strictly ascending order of vocabulary
4 consumption_term_unknown a term is not a term of its vocabulary
4 consumption_seen_unordered its seen digests are not in strictly ascending order
4 consumption_signer_not_principal its signer_id is not the did:key of its signer_pubkey
4 transmission_not_transmitted a transmission claim's attests is not transmitted (bearer-receipts §4.2)
4 reconciliation_not_received a reconciliation claim's attests is not received (bearer-receipts §5.2)
4 reconciliation_bitmap_malformed its leaf_count is 0, its received is not ⌈leaf_count / 8⌉ bytes, sets a bit at or past leaf_count, or sets none
4 schedule_not_scheduled a schedule claim's attests is not scheduled (carousel §4.2)
4 schedule_objects_malformed it has no entry, or its entries' leaf_index values do not strictly increase
4 schedule_oti_invalid an entry's code parameters are not ones its code admits (carousel §5.1, §5.2, §5.5)
4 schedule_symbols_invalid an entry's symbols are not ones its code can name (carousel §5.4)
4 schedule_passes_invalid an entry's passes are not well formed: none, of no duration, overlapping, ending past 9999, or sending more than 253 − 1 octets
4 timeliness_not_valued a timeliness claim's attests is not valued (timeliness §4.2)
4 timeliness_objects_malformed it has no entry, or its entries' leaf_index values do not strictly increase
4 timeliness_window_invalid an entry's from_ms is after its by_ms, or its by_ms is not below the last instant, 9999-12-31T23:59:59.999Z
4 timeliness_decay_invalid an entry's decay is not well formed: not 1 to 16 points; instants not strictly increasing from after by_ms to at most the last instant; a value at or above 1,000,000, above the one before it, a last one other than 0 or another one of 0; or a point but the last on the line through its neighbours
4 timeliness_fallback_invalid a rung of an entry's fallback is valued at 0 or above 1,000,000, has the entry's content hash, or names the entry's own position
4 timeliness_ladder_invalid an entry's fallback is an array of other than 2 to 8 rungs, whose values do not strictly decrease, or two of whose rungs have one content hash or one position
4 cache_transmission_not_transmitted a cache-transmission claim's attests is not transmitted (cache-authority §5.2)
4 cache_transmission_on_bidirectional its bearer is bidirectional
4 cache_transmission_bitmap_malformed its leaf_count is 0, its sent is not ⌈leaf_count / 8⌉ bytes, sets a bit at or past leaf_count, or sets none
6 energy_disagrees_with_delivery the figure differs from the object receipt's
6 energy_class_disagrees_with_delivery the object's leaf is labelled, and the energy claim's class or evidence differs from the label
6 figure_disagrees the receipt a measurement or operating-point claim names carries another figure at figure_index
6 figure_unclassified that figure carries no acquisition class
6 figure_class_disagrees its class or evidence differs from the claim's
6 figure_signer_unknown a measurement claim names a figure no Ed25519 key signs
6 figure_signer_mismatch a measurement claim's figure is signed by an Ed25519 key other than signer_pubkey
6 figure_not_held a measurement claim is checked without the receipt that carries its figure (§2.4.8)
6 energy_oer_invalid the named OER receipt reports any reason under oer-receipts
6 energy_disagrees_with_oer the named OER receipt states another grade or figure
6 context_invalid a receipt a claim is checked against — a declaration a decision or a recovery record names, the receipt that carries a figure, or a stage claim a session names — does not verify
6 decision_declaration_mismatch a decision does not match the declaration it names
6 decision_purpose_not_declared a decision is about a purpose its declaration does not list
6 recovery_declaration_mismatch a recovery record does not match the declaration it names
6 recovery_signer_mismatch the declaration a recovery record names (§2.6.3), or a recovery runs through (§3.2), is not signed by the publisher of a verified delivery receipt for its subject, or, for a span declaration, for an object of its span
6 recovery_value_mismatch the value does not match under the algorithm's rule
6 recovery_no_matcher the verifier has no matcher for the algorithm
6 recovery_outside_span the delivery receipt a recovery through a span declaration, or a recovery record naming one, is checked with is not within the span (§2.6.4): another track or signer, a group or an object outside its range, or, for a span stated by media time alone, an object other than the anchor
6 gap_subject_mismatch the object receipt at a delivery-gap claim's coordinates names another object
6 session_output_mismatch a session's presentations are not leaf_count, repeat a (unit, layers), or give another output_root
6 session_output_inconsistent the presentations name an entry that does not exist or a layers of 0, or give another units, another entry's leaves, another first-use order, other primary_units or fallback_units, or no presentation for a missed_units entry
6 session_output_irreproducible a presentation decoded again, through a decoder its replicate class makes byte-exact, gives another out_digest
6 session_object_duplicate an object received is listed twice with one path
6 session_entry_not_received a session's entry object is not among the objects it received
6 session_source_not_received a decoder entry's object, or an envelope held as the session's, is not among the objects it received
6 session_counted_mismatch the objects received give other counted, uncounted or transferred bytes, or an object whose envelope is held states other counted bytes than that envelope gives
6 session_presented_mismatch an object is listed as presented and no decoder entry names it, or as not presented and one does
6 session_envelope_unreadable an envelope a decoder entry names is neither a WAI1 or WAI2 envelope whose payloads are plaintext nor a well-formed encrypted one
6 session_source_mismatch a decoder entry names other than its object's envelope gives — another rendition, a capability that rendition or that WAI1 manifest does not have, or a rendition of a WAI1 envelope — or names a rendition and its envelope is not held
6 session_layer_not_applicable a decoder entry names a held layer a sink cannot apply: not a refinement, its refines malformed or absent, a nonconforming refinement component, its base not a base its capability admits or decoded only through a fallback, its span outside a held base's units, or its payload not WIR1 with a unit for each it covers
6 session_first_unit_mismatch a decoder entry's first_unit is not its base's: a held layer's, against the entries naming its base; or an entry naming a stage claim's base or layer, against the unit stage_sets names the claim at
6 session_layers_mismatch a presentation through a decoder entry whose envelope is held is at another layer count than that envelope gives
6 session_unit_outside_object a presentation through a decoder entry whose envelope is held is of a unit that envelope does not cover, from its first_unit
6 session_classical_mismatch the envelopes' classical renditions the session did not decode give another classical_declared
6 consumption_object_unknown a consumption claim names an object the verifier does not hold (ai-usage §6.3)
6 grant_mismatch the grant it names is not the signer's, not good at an object's time, or names objects that are not the room's
6 preference_mismatch its seen is not the digests of the statements that govern its objects
6 usage_unanswered its usage does not answer a vocabulary a governing statement carries
6 usage_disallowed a use it names is disallowed for one of its objects
6 transmission_subject_mismatch a transmission claim's subject is not the link hash of the group receipt it is checked against (bearer-receipts §4.3)
6 transmission_signer_mismatch it is not signed by the group's key
6 reconciliation_subject_mismatch a reconciliation claim's subject is not the link hash of the group receipt it is checked against (bearer-receipts §5.3)
6 reconciliation_by_publisher it is signed by the group's key
6 reconciliation_leaf_count_mismatch its leaf_count is not the group's
6 reconciliation_objects_not_held it names a position whose object receipt the verifier does not hold: recoverable, never accepted
6 reconciliation_root_mismatch its received_root is not the root over the leaves at the positions it names
6 schedule_subject_mismatch a schedule claim's subject is not the link hash of the group receipt it is checked against (carousel §4.3)
6 schedule_signer_mismatch it is not signed by the group's key
6 schedule_leaf_out_of_range an entry's leaf_index is not less than the group's leaf_count
6 schedule_before_group an entry's first pass starts before the group's group_start
6 schedule_content_mismatch an entry's content_hash is not that of the object receipt the verifier holds at its position
6 schedule_length_mismatch an entry's transfer length is not the length of the object's envelope the verifier holds
6 timeliness_subject_mismatch a timeliness claim's subject is not the link hash of the group receipt it is checked against (timeliness §4.3)
6 timeliness_signer_mismatch it is not signed by the group's key
6 timeliness_leaf_out_of_range an entry's leaf_index is not less than the group's leaf_count
6 timeliness_before_group an entry's from_ms is before the group's group_start, whole seconds compared
6 timeliness_content_mismatch an entry's content_hash is not that of the object receipt the verifier holds at its position
6 timeliness_on_layer an envelope the verifier holds whose content hash is an entry's or a rung's is a WAI1 of a refinement-only capability
6 timeliness_fallback_mismatch a rung's leaf_index is not less than the leaf count of its group, where the verifier holds that group receipt, or its content_hash is not that of the object receipt the verifier holds at its position
6 timeliness_fallback_not_derived an envelope the verifier holds whose content hash is a rung's is not a WAI1 whose derived_from names the entry's content_hash as its motif
6 cache_transmission_subject_mismatch a cache-transmission claim's subject is not the link hash of the group receipt it is checked against (cache-authority §5.3)
6 cache_transmission_by_publisher it is signed by the group's key
6 cache_transmission_leaf_count_mismatch its leaf_count is not the group's
6 cache_transmission_objects_not_held it names a position whose object receipt the verifier does not hold: recoverable, never accepted
6 cache_transmission_root_mismatch its sent_root is not the root over the leaves at the positions it names
6 session_stage_disagrees a stage claim the session names is about a base, or lists an arrived layer object, the session did not receive; counts a unit the session never presented, or more units at a stage than the session presented at it or above; or no claim whose set holds a unit lists an applied layer that reaches a stage the session presented it at
6 session_stage_hash_not_exact a stage claim the session names states a stage_hash, and a decoder entry names its base at a step weaker than decode-equivalence, or names its base or a layer it lists without stating an exact path, whichever units it presented

3.2. Recovery through a soft binding, and the ancestor rule

For an object in hand that fails §3 step 4 — or is no longer a WAI envelope at all — a verifier MAY attempt recovery. It needs: a soft-binding claim D, typically returned by a resolution endpoint; the publisher key P of a delivery receipt for D's subject that it has verified under §3; the value V the registered algorithm computes from the object in hand; the object in hand's §2.1 content hash H, if it is still a WAI envelope; and a matcher for D's algorithm.

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

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

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

What a recovery through a span does not attest:

  • which objects of the span the object in hand was derived from, or how much of the span it holds. A value computed over the span matches content cut from any part of it: a watermark that repeats its payload in every frame is read from one frame;
  • anything outside the span. An object of the track before or after it, or of another track, is not reached through D: a receipt outside the span is refused (recovery_outside_span), never reported as the ancestor's, and the same publisher's binding over another span is another declaration, recovered on its own;
  • for a span stated by media time alone, any object but the anchor: delivery receipts carry no media time, so none places an object inside the interval;
  • the energy of the object in hand: the figures of the span's objects cover producing them, and the ancestor rule below applies to the span as a whole.

A verifier MUST report a recovery through a span with its span, and MUST NOT report one object of the span, the anchor or R, as what the object in hand was derived from.

The ancestor rule (REQUIRED). A receipt reached through a soft binding attests the ancestor the object in hand was derived from, not the object in hand. A verifier MUST report a recovery as such, naming the ancestor, and MUST NOT report the object in hand as verified, as bound, or as the subject of the ancestor's receipts. The energy consequence is part of the rule: the ancestor's figures cover producing the ancestor; the transformation from the ancestor to the object in hand is unaccounted, which is not the same as zero, and a verifier MUST NOT report the ancestor's figures as the energy of the object in hand. An implementation that cannot express this distinction MUST NOT implement recovery: a recovery presented as a hard binding is worse than no recovery, because it launders a transformation nobody attested.

What a match is worth. A soft-binding match is evidence at the algorithm's own error rates, which a declaration does not state. A watermark can be copied onto other content, and unrelated content can share a fingerprint. A recovery says the object in hand matched the publisher's declared binding under the named algorithm — nothing about how it was derived, by whom, or with what changes. An algorithm's error rates under named transformations are measured, reproducibly, by the robustness records of robustness-evidence, and what it claims to survive holds only where such a record shows it (robustness-evidence §8). A verifier MAY report beside a recovery the accepted claims and records it holds for the declaration's algorithm; they change no step here.

A recovery MAY be recorded as a binding-recovery claim (§2.6.3). The reference implementation (wai-rs, feature receipt_claims, claim_receipt::recover, and claim_receipt::recover_span through a span) returns a value whose only attestation is the ancestor, or the span; there is no path from it to a statement about the object in hand.

3.3. Delivery reasons

check reason raised when
§3 step 1 wrong_kind kind is not the receipt's kind
§3 step 1, 4 malformed a group receipt (step 1) or object receipt (step 4) lacks a member, carries one its kind does not define, or holds one of the wrong type or encoding
§3 step 1 bad_signer_key signer_pubkey does not decode under RFC 8032 §5.1.3, or is of small order
§3 step 1, 2 group_label_invalid a group's labelled members appear without "sign_version": 2, its sign_version is not 2, or its labelled members cannot be read (step 1), or its class breaks §2.4.1 with the group alone (step 2)
§3 step 2 signature_verify_failed the group signature fails §3.1 step 3's rule over the payload of the group's form
§3 step 1, 2 pq_malformed, pq_suite_unknown, pq_signature_invalid, hybrid_required as pq-signatures §8, for a hybrid group or under the policy that requires one
§3 step 3 chain_break within the chain of the group's (signer_pubkey, track_alias), whose head the verifier holds: a null parent (a second chain head), or a parent that is another group of the chain the verifier holds (a fork)
§3 step 3 parent_not_held a non-null parent that names no group of the chain the verifier holds: requested, never accepted silently
§3 step 3 parent_not_recovered a group held for its parent past the verifier's stated bound: held live, refused when its chain re-anchors; held as history, refused alone
§3 step 3 group_id_not_increasing a group_id no greater than its chain head's: for a legacy group always, for a labelled one under §6.1's opt-in guard
§3 step 3 group_not_fresh under §6.1's opt-in freshness bound, a group_end older than the window, further ahead of the verifier's clock than the skew it allows, or not a date-time, in a group whose group_id is not below the chain's live edge
§3 step 4 content_hash_mismatch the envelope does not hash to content_hash
§3 step 5 object_label_invalid an object receipt's class label cannot be read or breaks §2.4.2: legacy HwShunt, a class beside a zero figure, the unmetered marker beside a non-zero one, a refined class without its evidence or below the resolution floor, or evidence without a class
§3 step 5 proof_position_mismatch the object is not at the position its proof walks: leaf_index outside the group, a proof of the wrong length, direction bits other than the index's, an object_id other than the index, or another group's coordinates
§3 step 5 inclusion_proof_failed the object's leaf, walked up its proof, does not reach the verified root — among other causes, because the leaf was computed in the other form of §2.2
the group total group_total_mismatch the accepted object receipts, one for every leaf, do not sum to joules_total
the group total group_class_disagrees a labelled group's class, or its evidence other than the declared uncertainty, is not the one its objects compose to, or its objects compose to none
the group total group_coverage_disagrees a labelled group's coverage is not the one its objects give
the group total group_uncertainty_below_parts a labelled group declares less than the composed relative_ppm, or another window
WAI_RECEIPT property_invalid, property_disagrees as moq-streaming-format §5
carried over MoQ moqt_mismatch as moq-streaming-format §4.1: a receipt whose key or track_alias is not its receipts track's, or whose group_id or object_id is not the MOQT Group ID or Object ID of the object it is for; refused before its signature is checked, and not held

4. Relay preservation requirements

A conforming relay forwards WAI envelopes from one or more publishers to one or more subscribers. To preserve the receipt chain across hops, a conforming relay:

4.1. MUST

Receipts carried as payload. Where a carrier moves receipts as the opaque payload of its own objects — the receipts tracks of moq-streaming-format §4.1, whose relays read no payload (MOQT §7.8) — a relay of that carrier forwards them without reading them. Such a relay is not a relay of this section: it cannot verify a group before fanning it out, and does not; the subscriber verifies every receipt itself, by §3, and the carrier's forwarding of the receipts' own objects is what forwards them. The subscriber ties each receipt to the carrier's object it came on: a receipt whose key, track or position is not that object's is refused before anything of it is held (moqt_mismatch for MoQ). The duties below bind a relay that reads the receipts it forwards.

  • Forward every group receipt and every object receipt unaltered to every subscriber that requested receipts for the corresponding track. Byte-for-byte equality of receipt bytes between ingress and egress is REQUIRED.
  • Verify the group receipt's Ed25519 signature per §3 step 2 before the first object of the group is fanned out. A relay that cannot verify the signature MUST drop the group and emit a structured error to upstream operators. A relay that predates the labelled group payload (§2.3) cannot verify a labelled group and drops it, so a publisher labels a group only once the relays on its path verify both payloads.
  • Hold to these duties, at its own door, the objects and receipts another relay forwards it, as it holds a publisher's: where relays carry a track from one to another (a room across relays, live-rooms §10.7), each verifies every group receipt and every object it fans out, whatever the relay before it verified, and refuses an object that does not verify though the relay before it forwarded it. A relay's own check is no statement to the next relay: none is carried between them, and none is trusted.
  • Forward the parent_receipt_hash field as received. A relay that has previously forwarded the chain MAY additionally check the link per §3 step 3 and refuse to fan out a broken chain.
  • Preserve the byte-exact WAI envelope (WAI1 magic, manifest bytes, payload bytes) end-to-end. Any rewriting at the envelope layer invalidates §2.1 content hashes.
  • Forward claim receipts (§2.4.3) the same way — unaltered, whatever their class. A relay MUST NOT drop a claim because it does not know its class: rejecting an unknown class is a verifier's rule (§2.4.4), and a relay that filters claims decides for every downstream verifier what it may see.
  • Forward, unaltered, every object receipt it receives for an object of a requested track that it does not forward, whatever the reason — a delivery timeout that passed at this hop, its own discretion, or never having received the object itself — with the object's group receipt. A subscriber can then check the receipt by inclusion (§3, Verified by inclusion): the publisher signed an object with this content hash at this position, and it was not delivered. inclusion-verified is never §3 step 6 acceptance, and a verifier MUST NOT treat it as delivery.

4.2. MAY

  • Batch multiple kind: "object" receipts into a carrier-level frame to amortize per-receipt overhead, provided every receipt's bytes are preserved (no field elision, no canonicalization rewrite).
  • Emit relay-scoped telemetry alongside the forwarded receipts: cumulative joules_total across observed groups, per-publisher delivery counts, fan-out fan-in ratios. Such telemetry is local to the relay and MUST NOT be conflated with publisher-issued receipts.
  • Reorder objects within a group on egress when the carrier protocol permits — Merkle inclusion proofs are position-bound by leaf_index, so reordering does not invalidate verification at the subscriber. The relay MUST NOT renumber object_id.
  • Sign a delivery-gap claim (§2.4.11) for an object it did not forward, under its own key, as the parallel receipt §4.3 permits.
  • Sign a cache-transmission claim (§2.4.18) for objects it sends again, from what it holds, over a bearer instance of its own, as the parallel receipt §4.3 permits. A node that is not a relay (§4.5) may sign one too.
  • Derive new objects from those it forwards and publish them as a publisher of its own (deriving-edge): each a new object with its own content hash, in a group it signs on a track of its own, with a derivation record (deriving-edge §5) as the parallel receipt §4.3 permits. What it received it forwards as this section requires, unaltered.

4.3. MUST NOT

  • Modify the WAI envelope payload bytes in any way (no transcoding, no manifest rewrite, no key reordering, no whitespace insertion). Envelope mutation is detected at §3 step 4.
  • Re-issue the Merkle root or re-compute a new tree over a subset of objects. The publisher is the sole authority for the group root.
  • Re-sign the group receipt under a relay-owned key. There is one signer per group, and it is the publisher named in signer_pubkey. Relay attestation, when desired, MUST be carried as a separate, parallel receipt and MUST NOT replace the publisher signature.
  • Strip the parent_receipt_hash field, even when its value is null. The field is required by §2.4.1 and its absence is a shape-check failure at §3 step 1.
  • Truncate the receipt to fewer fields than §2.4 requires, even when the carrier framing has size pressure.

4.4. Objects not delivered

A relay MAY drop an object whose delivery timeout has passed, and an HTTP client MAY abandon one (http-delivery §10.2). An object a subscriber did not receive is not delivered to it: it neither presents the object nor reports its delivery, and the group receipt stays valid for every object it did receive, each by its own inclusion proof (§3 step 5). A relay that drops an object MUST still forward the group receipt and the object's receipt (§4.1), and the group can still head its chain (§3 step 3).

A subscriber that holds the receipt of an object it did not receive MAY check it by inclusion (§3, Verified by inclusion). The object is then inclusion-verified: accounted for as signed and not delivered. A stage claim lists it as dropped with that evidence (§2.4.10), and staged delivery never applies a layer on it (staged-delivery §10.1). A receipt that rides on the object itself, such as WAI_RECEIPT (moq-streaming-format §5), is lost with the object, so a stream that relies on this accounting also carries its receipts where a dropped object does not take them with it (a sidecar, or a track of its own: on MoQ, the receipts track of moq-streaming-format §4.1, on HTTP the receipt index of http-delivery §8.2).

4.5. HTTP caches

An HTTP cache that stores and serves objects and receipt files byte for byte under http-delivery §9 is not a relay in the sense of §4.1 and §4.2: it fans out nothing, verifies nothing, and forwards what was requested. It is bound by §4.3. A node that verifies, filters or selects receipts is a relay.


5. Test vectors

A conforming verifier MUST give, for each case §5.1–§5.5 defines, the outcome that case states: accept, or reject at the stated step with the stated reason. The cases are normative as written here. Vectors that instantiate them are generated outside this tree, by a companion implementation, and are not yet published in this repository; once a revision publishes them in the tree, a conforming verifier MUST also verify against them. (The corpora of §5.6–§5.10 — claim receipts, labelled JWP receipts, key discovery, the receipt chain per key and track, and the stage and delivery-gap classes — are published in this tree.) Each vector consists of: the WAI envelope bytes for every object in the group, the canonical group-receipt JSON, the canonical object-receipt JSON for every object, and a result.json declaring the expected verifier outcome (accept or one of the named rejection reasons).

5.1. Single-object group (vector v1_single_object)

One WAI envelope (wai.text.zstd, payload "hello" zstd-encoded). Group receipt with leaf_count = 1, parent_receipt_hash = null. Object receipt with object_id = 0, merkle_proof.siblings = [], direction_bits = 0, leaf_index = 0. Expected outcome: accept.

5.2. Multi-object group with proof verification (vector v2_multi_object)

Five WAI envelopes (wai.audio.opus, 20 ms frames). Group receipt with leaf_count = 5 (the publisher duplicates the trailing leaf internally when building the tree). Five object receipts, each with a non-empty merkle_proof. Expected outcome: accept for each object receipt.

5.3. Cross-hop preservation through 2 relays (vector v3_cross_hop)

Three publisher-issued group receipts in chain order G0, G1, G2 with parent_receipt_hash chaining each to its predecessor. Two relay hops are simulated: ingress bytes → relay A egress bytes → relay B egress bytes are emitted as three byte-equal copies. The verifier walks the chain in order; expected outcome: accept at every hop, and the chain head after G2 equals the BLAKE3 of G2's canonical bytes per §3 step 3.

5.4. Invalid signature rejection (vector v4_bad_sig)

A well-formed group receipt with a single bit flipped in the sig field. Expected outcome: reject at §3 step 2 with reason signature_verify_failed.

5.5. Missing parent_receipt_hash rejection (vector v5_missing_parent)

Two group receipts G0, G1. G1 omits the parent_receipt_hash field entirely (rather than setting it to null or a hash). Expected outcome: reject at §3 step 1 with reason missing_required_field.

The generator writes the vectors as one directory per case. A conforming verifier MUST accept the accept cases bit-exactly and MUST reject the named cases at the declared step with the declared reason. The companion implementation's verifier exercises all five vectors in its own test suite.

5.6. Claim receipts and recovery (claim-conformance/)

The typed classes of §2.4.3–§2.4.9, the binding classes of §2.6 and the procedures of §3.1–§3.2 are checked by the corpus in claim-conformance/, generated and verified by wai_claim_vectors (wai-rs, feature receipt_claims) and run by the WAI conformance workflow on every platform of its matrix. Each vector declares one outcome: accept — with the claim's link_hash, which pins the canonical form, or for a recovery the ancestor it attests — or reject at a named step with a named reason. A verifier that rejects for another reason, or at another step, does not conform. The corpus exercises every reason of §3.1; the ancestor rule; each acquisition class with its evidence, each way the evidence can be missing, misplaced or below the resolution floor, and the legacy HwShunt; OER receipts sealed with and checked by the OER module, including figures at the 34 digits a decimal string may hold and figures and numbers oer-receipts §4 does not admit in a receipt body; signatures that satisfy the verification equation and that step 3 still refuses, by RFC 8032's own decoding and range rules (non-canonical encodings of a key, S ≥ L) or by the choices step 3 fixes (keys of small order with signatures made without a private key, an R of small order); a non-canonical encoding of a key of large order, which pins the reason step 3 gives; a recovery record naming a declaration that the ancestor's publisher did not sign, and one naming a declaration whose signer_pubkey was rewritten to the publisher's after another party signed it; a previous claim that does not itself verify; JSON that parsers read differently — a byte-order mark, an unpaired surrogate escape, deep nesting, numbers out of every range — and dates that do not exist; the boundary between a member's type (step 2) and what a class's rules add to it (step 4), from both sides, and the order step 2 reads a body in, where a body has two defects; and a claim whose canonical form spans three BLAKE3 chunks. The corpus also covers the measurement and operating-point classes: every reason they add, the arithmetic of energy-measurement §6.2 at its boundaries — a declaration at the least value a bracket supports and one below it, a figure at a half microjoule, the ratio thresholds, a flat idle bracket, and brackets whose products exceed 128 bits — the liveness rule of energy-measurement §3.1 (a frozen rail refreshed once, a refresh published in two steps, facts that describe no sequence), and the declaration a sealing path computes from unrounded samples, which the rule refuses. That arithmetic is pinned against values claim_verify.py computes with Python's integers. It covers an energy claim checked against an object whose leaf is labelled. It covers span bindings (§2.6.4): declarations stated by media time, by groups and by objects, each rule of a span from both sides, a soft-binding claim carrying a span, recoveries through a span with a delivery receipt at each edge of its range and one past it, of another track or signer, and of an object other than the anchor under a span of media time alone, and recovery records naming span declarations.

The generator and the verifier in wai-rs are one implementation, so two checks are made against code that shares nothing with it. A canonical fixture — a claim given with its members out of order at three levels and escapes in the input text — has its canonical bytes, signature and link_hash computed by claim-conformance/claim_verify.py (Python's JSON serialiser with UTF-16 member order, an Ed25519 implementation other than the one wai-rs uses, and BLAKE3 written from its specification), and pinned as literals that wai_claim_vectors checks first. And that script, a second verifier written from this section, reports every vector's declared outcome; it reads and verifies OER receipts with the OER reference verifier, as wai-rs does with its own OER module. The corpus pins one outcome per vector, which shows the two verifiers agree on those vectors and nothing more, so claim-conformance/differential.py also generates inputs the corpus does not hold — JSON at the edges of what parsers accept, calendar dates, URI authorities, acquisition classes with every combination of evidence, figures near a half-microjoule and at the edges of oer-receipts §4 and §6, OER receipts holding numbers §4 forbids in a body, files §4 does not read at all, context receipts that do not verify, bodies of every class with one defect and with two at different positions, measurement brackets and operating points around every rule — runs both verifiers' verdicts mode on them, and reports every input on which their verdicts differ. Agreement shows that the two read alike, not that either reads §3 as written; only the vectors' declared outcomes, set from the text, speak to that, and only for the inputs they hold. The fixture and the bracket arithmetic run in CI on every platform of the matrix; the script and the differential, which need Python with an Ed25519 library, run in CI on the linux runner with a fixed seed.

5.7. Labelled leaves, labelled groups and the group total (jwp-conformance/)

wai_jwp_vectors gen in wai-rs writes jwp-conformance/vectors.json, and verify requires a byte-identical regeneration and every declared outcome. Every vector is synthetic: keys come from published fixed seeds and attest nothing, and envelopes, figures and origins are invented. The corpus holds leaf vectors in both forms of §2.2; a group in the legacy payload whose objects mix the forms, and a group in the labelled payload chained to it, each with its signing payload, signature and link hash; each object's outcome (accept, or a rejection at its §3 step with its §3.3 reason), including objects presented at another object's position; group signatures the strict rule refuses; the group total's reading or its reason; and WAI_RECEIPT properties with the reading moq-streaming-format §5 gives them. jwp-conformance/jwp_verify.py, written from this text, reports the same outcomes; CI runs it on the linux runner, and runs wai_jwp_vectors verify on every platform of the conformance matrix. joule-code's joule-moq pins leaves, roots, signatures and link hashes copied from the corpus.

5.8. Key discovery (signer-conformance/)

§7 is checked by the corpus in signer-conformance/, generated and verified by wai_signer_vectors (wai-rs, feature key_discovery) and run by the WAI conformance workflow on every platform of its matrix. Three procedures appear. anchor gives a key log, how it was obtained and any anchor already held, and declares anchored with the anchor's revision_hash and path, not_anchored with a reason of §7.7, or replaced. confirm gives an item — a claim's wire form, or the signer, the acquisition classes and the signed time of a group receipt or another family's receipt — with the revisions and anchors a verifier holds and any expected origin, and declares confirmed with the origin, key-id, head and anchor path and what §7.4 reports beside them, or unconfirmed or refused at a step of §7.4 with a reason of §7.7. A claim that fails §3.1 is declared rejected at its own step with its own reason, and is never confirmed. module gives a module's bytes, a grant, the sink's module origins, the revisions, anchors and statuses a sink holds, the time, the capability being decoded and whether the envelope leaves a fallback, and declares run with the kill latency where §7.9's checks 1 to 8 pass, or fallback or inert at one of those checks with its reason; for a pinned parameter set it may give no grant, and it adds the refusal SPEC §3.1's check 5 reports. The bytes its module vectors give are not WebAssembly modules, and module runs neither of checks 9 and 10, which are the WAISM profile's: waism §9 defines their cases. The corpus covers each rule of §7.2, the head walk with revisions that are not held, unattached, injected, forked and superseded, each move of an anchor, each step of §7.4 including the pinned-only authorities, the authorities a measurement, operating-point, stage or delivery-gap claim needs, a family receipt whose figures carry two classes, instants compared across offsets, fractions and a leap second, and each of §7.9's checks 1 to 8, with grants from an origin that is not one of the sink's module origins, a grant whose origin is not the one its signer_id names, a digest one module origin revokes and another grants, a status signed under an injected revision, and a pinned parameter set whose digest is revoked while its grant fails an earlier check or no grant is held.

A canonical fixture — a revision given with its members out of order at two levels and escapes in its text — has its canonical bytes, signature and revision_hash computed by signer-conformance/signer_verify.py, which takes from claim-conformance/claim_verify.py only what §7 reuses of §3.1 and shares no code with wai-rs, and pinned as literals that wai_signer_vectors checks first. That script also reports every vector's verdict from this section. CI runs it on the linux runner, and requires its verdicts output to equal wai_signer_vectors verdicts line for line. §7 changes no vector of the claim corpus (§5.6): every signer it names writes a label (§7.1), and every vector keeps its declared outcome, those of the measurement and operating-point classes included.

5.9. Receipt chains per key and track (vector v6_per_track_chain, jwp-conformance/)

One key publishes a base on track 7 and a layer on track 8, each with three chained groups signed over the labelled payload, which signs the link. A verifier given only track 7's receipts accepts all three (v6_per_track_chain_base); given both tracks' receipts interleaved, it accepts all six, which a verifier keeping one chain per key would not (v6_per_track_chain_interleaved); given track 8's second group without its first, it reports parent_not_held and does not accept it (v6_per_track_chain_layer_without_its_first); and a track 8 group whose parent is a track 7 group names nothing the layer's chain holds, and is parent_not_held (v6_per_track_chain_cross_track_link). Beside them the corpus of §5.7 holds a track's first group offered again after its second, which changes nothing, a fork from a held group and a gap beside the head, a head offered again, another key on the same track, §6.1's guard off, on, and compared within one track; the guard holding a legacy group with it off, a legacy history replayed as new after one rewritten link and its loop, and a labelled replay whose rewritten link fails its signature; receipts chained per key across tracks; its freshness bound at the clock, with an offset, at the window's edge, stale, ahead of the clock, stale with no bound, a stale group behind the live edge, which fills a gap, a gap of seventy groups walked back and forward, and a stale group whose rewritten legacy link earns it nothing; a group whose signature fails and so heads nothing; a previous group of another track or key, which is not held; and object receipts checked without their envelopes, which are inclusion-verified or refused at step 2 or 5. chain_differential.py signs chains around these rules and requires both verifiers to agree on each. The corpus also holds recovery (§3 step 3): children held until their parent is fetched; a receipt lost, and the chain re-anchoring past it once a count bound is passed, its held groups refused (parent_not_recovered) and regained as history behind the labelled anchor; a re-anchor refused behind its head; a verifier that joins mid-stream, at the chain's first group and on a legacy chain; a stale group that may not anchor; a gap fetched in ascending order after a re-anchor, and a joiner's fill in ascending order, held as history outside the live bound, within its history bound and past it; a joiner's fetch from the chain's first group, whose null parent is its true start and is held as history (chain_join_fetch_from_group_0), and, once that gap is closed, a second group with a null parent and a second child of the first, each chain_break (chain_join_second_start_after_the_fill); and the bound across chains. chain_gap_differential.py lets both verifiers recover on chains it signs, with ranges fetched again in ascending order. wai_jwp_vectors and jwp_verify.py give every vector's outcome (§5.7).

5.10. Stage and delivery-gap claims (claim-conformance/)

The corpus of §5.6 holds the classes of §2.4.10 and §2.4.11: a set delivered and presented at its committed stage (stage_accept); its second layer dropped and verified by inclusion, so the commitment was not met (stage_not_met_accept); a base whose receipt was not verified first, which states no delivered stage, so rule 6 does not apply (stage_null_delivered_states_no_delivery), and a layer over such a base applied on inclusion evidence; a layer delivered and late for one unit; a conflict; a base presented alone; layers all refused, delivered as committed with stage 0 presented; a span past the set refused before it was checked, which stands and delivers nothing; every reason of §2.4.10's rules, among them stage_hash_without_tier, stage_objects_malformed and stage_units_unsupported: units at a stage no object of that layer reaches, a partial layer counted as though applied to its whole span, a layer applied over units the layer below never reached, and a partial object over one unit, with an honest partial layer accepted beside them; objects with no bytes or with all the set's, one layer applied twice to a unit, a layer applied with no receipt beside a stated delivered stage, and a delivered stage over objects that leave a unit uncovered; step 2's enumerations and order; a gap accepted against the object receipt at its coordinates and without it (gap_accept), and refused against a receipt naming another object (gap_subject_mismatch) or at other coordinates. The corpus of §5.8 checks the authorities both classes need (§7.5). claim_verify.py gives every outcome, and differential.py generates stage bodies around every rule and gaps against receipts at and away from their coordinates.

5.11. Session claims (claim-conformance/)

The corpus of §5.6 holds the class of §2.4.12: an unmetered session (energy null) and a session metered under OnChipCounter with its evidence, each accepted against its outputs, the objects it received, its base's WAI1 envelope and the WAI2 envelope it received, and the stage claim it names; output trees from 1 to 33 leaves; a presentation whose re-decode, as the vector states it, gives another digest through a byte-exact decoder (session_output_irreproducible) and through a decoder checked for membership only (accepted); a decoder entry relabelled to escape that check by each route — the base's WAI1 entry, a WAI2 entry with its rendition kept or nulled, a capability another envelope has — refused against the envelope of the object it names (session_source_mismatch), and an envelope held that the session did not receive and an entry's object it did not receive (session_source_not_received); the classical rendition read from envelopes whose second rendition is of each kind a rule excludes and of two it admits; an encrypted envelope, a WAI1 envelope with no manifest members and bytes of neither magic, each named by an entry (session_envelope_unreadable), and accepted where no entry names them; an object listed twice (session_object_duplicate) and one fetched twice and listed once (accepted); figures a sink stated rather than derived — counted bytes above what the held envelope gives (session_counted_mismatch), and an object marked presented that no entry names, or not presented that one does (session_presented_mismatch); a session holding its base's and a layer's envelopes, accepted, and a presentation at more layers than its envelope gives (session_layers_mismatch), of a unit past the base's frames, outside the layer's span, or outside a WAI2 rendition's frame, through an entry whose first_unit is moved, past the frames of a base decoded through its manifest's fallback, the same where the fallback's format and not the capability's is the payload's, and past a WAI2 base component a WAI1 layer refines by its exploded envelope (session_unit_outside_object); a held inert envelope listed with counted bytes 0 (session_counted_mismatch); an encrypted envelope an entry names, accepted with the classical length its table declares, and refused for that length unstated, a rendition its table does not hold, another capability, no rendition, a base at two layers, a companion-only capability, a refinement before a selectable rendition, and a rendition whose sha256 is malformed (uppercase, 63 digits, not hex), named by its rendition and by its exploded envelope's content hash, all read from the manifest in the clear, and accepted with a well-formed sha256 its payload does not match; an encrypted layer over an encrypted base, accepted; entries naming a WAI2 rendition by its exploded envelope's content hash, held as the WAI2 alone — accepted, relabelled, past its frames, a refinement off its base's first unit and one whose envelope does not conform — and a refinement's exploded envelope received on its own, accepted; bases whose manifest or rendition carries a stray refines, accepted, and a layer over one; a refinement of an envelope that does not conform — a refinement before a selectable rendition, a base rendition with no sha256, a layer before its producer, an unbound companion, a record component with no role — and two that do, beside a companion; entries a sink cannot have decoded through — a WAI2 companion, a rendition or a refinement whose payload does not match its sha256, a WAI1 envelope whose capability is companion-only, and a fallback that is (session_source_mismatch); held layers a sink cannot apply — layer 0, a span past or empty over a held base, runs that do not sum to the base's units, no refines on a refinement-only capability, refines on one that is not, a fallback on a layer, a base that is itself a layer, a base decoded only through its fallback, a base the layer does not admit, a payload that is not WIR1 or holds too few units, and in a WAI2 envelope a base that is no rendition, no component id, another refinement component or a companion, a base it does not admit, a refinement with no sha256, a producer the envelope does not hold, a selectable rendition after the refinement, and a span past the base (session_layer_not_applicable); a layer's entry at another first_unit than its base's, in a WAI1 layer and a WAI2 refinement component, and across the two forms of one base — a WAI1 layer over a WAI2 component, with its exploded envelope held and not, and a refinement component whose base an entry names by its exploded envelope — and the base's and a layer's entries at another than the unit stage_sets names their stage claim at (session_first_unit_mismatch); WAI2 refinement components presented within and past the layers and units they give; a stage claim that verifies, one that does not, one by another key, one the session does not name, one about a base the session never received, one whose arrived layers the session never received, and sessions presenting stages its applied layers cannot have given — through layers it lists as late or refused among them — and fewer than it counts; a base forgotten and decoded again (staged-conformance/'s record_budget), accepted naming each epoch's claim and refused naming only the last; every reason of §2.4.12's rules 1 to 14 and of its checks, the caps of the switches and missed presentations written at and past them; a metered session's figure of zero (energy_zero_figure) and every acquisition-class reason; and step 2's enumerations and order. No session in it carries the module members of §2.4.12, and its two verifiers do not implement the WAISM profile: each refuses those members as unknown_member and applies none of rules 15 to 17, as §2.4.12 states for a verifier that does not implement that profile. The decode path an entry states has a refusal for each of rules 13 and 14 and for a stage hash over a base or layer decoded at a weaker tier or by a path that is not exact, beside accepted entries for each of wai.image.jpegai's two paths and for an integer path named by its registered lowering. claim_verify.py gives every outcome. differential.py generates sessions around every rule — units, leaves and paths off by one, switches and missed presentations at and past their caps, a null deadline with misses, presences that disagree, decoder indices out of range, leaves moved between entries, entries out of first-use order, stage claims against presentations that reach, miss and pass their stages and objects that hold or lack their layers, every acquisition class — with their outputs and objects; envelopes whose manifests every reader may read otherwise; and every registered capability as an envelope's second rendition. It requires the two verifiers to agree. The corpus of §5.8 checks the authority a session needs, and the authorities its energy figure needs (§7.5).

5.12. The binary form (jwp-conformance/)

Procedure binary: bytes given as a group's receipts, a group or an object standing alone (§2.7). A vector that reads states the members of each receipt in the JSON form and, for a group, its link hash over the bytes as received; a verifier reads the bytes by §2.7.4, reads the members by §3 step 1 and step 4, and writes them again, which must give the bytes it was given (binary_receipts_live_one_object, the shape a live track's receipts take; binary_receipts_mixed_legacy, the legacy parent flag, five objects with their proofs and every label tag; binary_receipts_labelled_partial; binary_receipts_no_object; binary_group_legacy_with_parent; binary_object_alone). The others are refused malformed at their step: another magic, an unknown signing domain, a record that ends inside its group, a byte after the last object, a count past the objects, a parent flag of 2 and a flag of 0 beside a parent, a coverage byte of 2, a signer_id that is not UTF-8, a leaf_version of 3, a class tag and a filtering byte energy-measurement §2.7 does not define, and a legacy group without its parent; and one reads and is refused by §3 step 1, a coverage of Partial beside the unmetered marker (group_label_invalid). wai_jwp_vectors and jwp_verify.py give every vector its outcome, and joule-moq reads and writes every one.


6. Security considerations

6.1. Replay

A receipt without sequencing information is replayable: an attacker who captures group receipt G can re-present it to a downstream verifier and bind unrelated bytes to the publisher's identity at a later time. This extension mitigates replay with two mechanisms:

  • parent_receipt_hash (§2.4.1) links each group receipt to its predecessor under the same signer_pubkey and track_alias. The labelled payload (§2.3) signs it, and a labelled group's link hash covers it, so a replay of a labelled chain requires forging every intermediate hash. The legacy payload signs neither, so in a legacy group the link is an unsigned statement anyone on the path can rewrite: rewriting one link, the first group's, to name the head would make the whole history link again.
  • group_id is monotonic per (signer_pubkey, track_alias), and both payloads sign it. A verifier MUST refuse a legacy group whose group_id does not exceed its chain head's (group_id_not_increasing, §3 step 3): it is the only signed order a legacy chain has, and it stops the replay above. For a labelled group the check is OPTIONAL: verifiers SHOULD track the highest group_id seen per (signer_pubkey, track_alias) and reject a non-increasing one. This binds the chain to forward progress. In a legacy chain a relay can still drop a group and rewrite the next one's link to hide the gap; only the labelled payload rules that out.
  • Anchors (§3 step 3) resume verification without the groups before them, and prove nothing about those groups. A relay can withhold groups and let a re-anchor carry the chain past the gap, and can offer a joining verifier an old group, validly signed, as its anchor. A verifier therefore reports every anchor and the gap behind it, applies the freshness bound to an anchor wherever it applies it to the live edge, and accepts a re-anchor only at a group_id above its chain head's, so that a re-anchored chain still moves forward. History that later fills a gap (§3 step 3) is admitted only by signed links to the groups on either side, never on arrival order; and what a verifier holds while it waits is bounded across all its chains, so a sender of groups under fresh keys or of groups that never link fills nothing past the bound it states.
  • Both are per track. A publisher that leaves track_alias at 0 has one chain per key, as before. A publisher that chained its groups per key across non-zero tracks, as an earlier reading of §2.4.1 allowed, made receipts that now fail at step 3: each group whose parent is on another track names nothing its own chain holds (parent_not_held). Separate chains per track let a subscriber verify the tracks it subscribes to without the receipts of tracks it does not — a stage set carries each layer on a track of its own (staged-delivery). They also let a relay withhold a whole track without breaking another track's chain; which tracks exist is the carrier's catalog's to say, and a subscriber that requested a track notices its absence.
  • group_end (§2.3) carries a signed RFC 3339 timestamp. A verifier MAY additionally bound it against its own clock for the group it treats as the current tip — rejecting a receipt whose group_end is older than a deployment freshness window (a relay holding content back) or implausibly far in the future (skew). This wall-clock check is OPTIONAL and MUST NOT be applied to historical receipts replayed for late-joiner resync, whose timestamps are legitimately old — apply it only to the live edge (group_not_fresh). The live edge of a chain is the highest group_id of a group of it that verified (§3 steps 1–2) and was inside the window when it arrived, whether or not it linked. A group whose group_id is below the live edge is history, and the bound does not apply to it: so a group a relay delayed past the window, refused once as stale, can be fetched again and fill the gap behind a later group that arrived fresh, in any order and however long the gap, and the chain does not wedge behind it. This is safe because both payloads sign group_id: only a group the publisher signed, and that arrived fresh, moves the edge, and no rewrite of a link — signed in the labelled payload, unsigned in the legacy one — moves it or earns a group the exemption. A stale group at or past the edge is refused. Below the edge a stale group still has to link (§3 step 3) and, if legacy, to exceed the head's group_id. The reference verifier (jwp_delivery::ReceiptChains) holds every legacy group to the group_id guard and exposes it for labelled groups, per (signer_pubkey, track_alias), and this freshness bound, read to the second with its offset applied, as opt-in checks, disabled by default so durable re-verification of an old receipt stays a feature. It keeps the link hashes of the groups it accepted on each chain, so it tells a fork from a gap, and each chain's live edge.

6.2. Forgery

The signing payload (§2.3) is bound under Ed25519 (RFC 8032). At classical security levels, Ed25519 provides ~128-bit unforgeability under chosen-message attack (EUF-CMA). Forging a group receipt is equivalent to recovering the publisher's private key or breaking Ed25519. Implementations MUST source signing keys from a CSPRNG and MUST NOT reuse keys across distinct publisher identities.

6.3. Cross-group confusion

The signing payload binds track_alias, group_id, and signer_pubkey under the same signature. An attacker who attempts to re-present a group receipt under a different (track_alias, group_id) tuple cannot do so without re-signing — the receipt's fields are signature-covered, and any field mutation breaks verification at §3 step 2. The Merkle proof is similarly bound: the verifier checks that the proof walks the position leaf_index names (§3 step 5), so an object's proof cannot be transplanted to a different position within the same group, and the duplicated last leaf of an odd level cannot be presented at the position past the group's end.

6.4. Envelope mutation in transit

Any mutation of the WAI envelope bytes (manifest rewrite, payload re-frame, whitespace insertion) changes the §2.1 content hash and is detected at §3 step 4. Relays that re-canonicalize the manifest SHOULD be treated as broken: they break content-hash equality even when the semantic meaning is unchanged. WAI SPEC.md §2 already requires that the manifest be serialized with no insignificant whitespace and that encoders preserve key insertion order; receipt verification depends on relays honouring that requirement.

6.5. Quorum and multi-signer receipts

v1.0 of this extension is single-signer: every group receipt is signed by exactly one Ed25519 key. Multi-signer / quorum receipts (threshold Ed25519, BLS aggregate signatures) are explicitly out of scope for v1.0. A future minor revision MAY extend §2.3 with a multi-signer container; the v1.0 wire format remains a strict subset.

6.6. Out of scope

  • Post-quantum group-signing ciphersuites. Migration to hash-based signatures (SLH-DSA), lattice-based signatures (ML-DSA), or hybrid Ed25519 + post-quantum receipts is deferred to a future major revision of this extension. Implementers building for long-lived archives SHOULD plan key-rotation policies that allow ciphersuite migration when the upgrade lands. §7.6 gives the rotation and revocation of a signing key.
  • Confidentiality of receipts. Receipts carry identity, joules, and origin labels in cleartext. Carriers that require receipt confidentiality (e.g. trade-secret origin labels) MUST layer their own encryption above this extension; v1.0 receipts are integrity-protected, not confidentiality-protected. Note this is distinct from payload confidentiality: the Confidentiality extension seals the object bytes a receipt attests, and this chain deliberately commits over that ciphertext so a relay forwards the sealed bytes and their receipts unaltered, signs nothing in the publisher's name (§4.3), and never reads the content. A measurement claim (§2.4.8) also carries the measuring machine's power levels, thread count, build and power reader in cleartext.
  • Pricing semantics for joules. joules_micro and joules_total are unit-of-energy fields, not unit-of-account fields. Conversion to currency, carbon, or grid-mix CO₂ is performed by downstream aggregators and is not normative here.

6.7. Claims

A claim is a signed statement, and §6.1–§6.3 apply to it as to a group receipt: seq and parent_receipt_hash bound replay per signer, and every member — class, subject, the whole body — is inside the signature, so a label cannot be promoted in transit (an energy claim whose energy_provenance is rewritten from ModelBased to OnChipCounter, or whose declared uncertainty is rewritten smaller, fails at §3.1 step 3). Step 3 also refuses keys of small order, under which a signature can be made without any private key, and restates RFC 8032's rule that a key has only its canonical encoding, so that verifiers following it accept the same signatures. What a signature does not settle is whether its signer was entitled to say it. §7 settles as much of that as a web origin's say-so can: that an origin the verifier has anchored lists the key with the claim's authority. A confirmed claim is worth exactly as much as that confirmation; an unconfirmed one, as much as the verifier's own reason to trust the key that signed it.

A reconstruct-session claim says what a sink played. It carries no identifier of a person, a device or an account: its session_nonce is fresh for each session, and its host states a profile, a platform, a build and a rounded worker count; a measurement claim on its figure states its bracket's workers as that same count. A sink key used for many sessions links them, so a sink that does not mean its sessions to be linked signs each with a key of its own, under a did:key (§7.1). Where its claim goes is the sink's choice; delivery telemetry carries only its link_hash (delivery-telemetry §2, §8).

6.8. Soft binding

  • A copied watermark. A watermark decoded from an object shows that the object carries the payload, not that it descends from the ancestor: a payload can be embedded into unrelated content. The ancestor rule (§3.2) bounds the damage — a recovery attests the ancestor's receipts, never the object in hand — and a relying party MUST NOT read a recovery as the provenance of the object in hand.
  • Fingerprint collisions. Unrelated objects can share a fingerprint value. How often is a property of the algorithm: WAI states no rate, a soft-binding declaration carries none, and a verifier here does not assume one. A relying party that needs a rate takes it from the algorithm's own specification, where one is published; a match is evidence at that rate, never proof of derivation.
  • A hostile resolution endpoint. An endpoint can return any declaration it likes. What it returns counts only if it passes two checks, made wherever a declaration is relied on — §3.2 steps 1 and 3 when a recovery is performed, and §2.6.3's checks 1 and 4 when a recorded one is verified: the declaration verifies under §3.1 steps 1–4, and its signer is the publisher of a delivery receipt for its subject that the verifier has already verified. Neither suffices alone. Without the first, the signer check compares a key the declaration's author wrote in, over a signature that may be anyone's; without the second, a valid declaration binds the object in hand to whoever made it. A recovery record accepted without that context establishes nothing about who made the declaration it names (§2.6.3). What passing both establishes is bounded by the ancestor rule and by the algorithm's own error rates (§3.2).
  • Downgrade. An object whose hard binding holds is never recovered (§3.2 step 4; for a recovery record, observed ≠ subject), so a soft binding cannot stand in for a hard one that still verifies.
  • A span wider than its value. A span is its publisher's statement of what its value was computed over (§2.6.4). A publisher that declares a span wider than the content that carries the value makes a recovery reach receipts of objects the content in hand may not come from; the declaration carries its signer's authority and no more, as every claim does. A resolution endpoint cannot widen a span, which is signed, and a receipt outside it is refused rather than reported (recovery_outside_span).

7. Key discovery and signing authority

Every signature in this extension is checked against the signer_pubkey the receipt itself carries (§3 step 2, §3.1 step 3). That settles that the holder of that key signed. It does not settle who holds it: a forger supplies a key as easily as a signature. This section lets a signer say where its keys are published, and lets a verifier confirm it. The signer names a web origin in signer_id; the origin publishes a signed, chained key log listing its keys and what each may sign; a verifier that has anchored the origin checks an item it has accepted against that log. The same log says which keys may authorise the decoder and reconstruct modules a sink runs, revokes modules by digest, and bounds how long a revoked module keeps running (§7.9).

Nothing here adds a member to a receipt or claim, or changes a signed byte. signer_id is inside every signature already (§2.3, §2.4.3), so no relay can change the origin it names. A receipt verifies exactly as before, and one whose signer does not use this section is reported unconfirmed, as every signer was before it.

Beside an item's own verdict, a verifier that runs this section reports one of three levels, and MUST NOT report one as another:

  • confirmed — an origin the verifier has anchored lists the item's key, in force, with every authority the item needs (§7.4, §7.5);
  • refused — what the verifier holds contradicts the item's claim to speak for the identity its signer_id names: a valid log of that origin, or the signer_id itself (§7.1);
  • unconfirmed — the verifier holds nothing it can confirm against, or nothing that meets the anchor an authority requires (§7.5). It establishes nothing about who signed.

Confirmation never changes an item's verdict under §3 or §3.1. A refused claim is still a statement its signer made; a confirmed one still asserts only what §2.4.4 says its class asserts.

7.1. What a signer_id names

A signer_id — publisher_id in §2.3 — has one of three forms.

  • Origin-bound. A signer that publishes its keys under a web origin writes signer_id as a did:web DID URL with a host, no path and a fragment:

    signer_id = "did:web:" host [ "%3A" port ] "#" key-id
    host      = label 1*( "." label )   ; the last label is not all digits
    label     = 1-63 of a-z 0-9 "-", not beginning or ending with "-"
    port      = 1-65535 in decimal, no leading zero, not 443
    key-id    = 1-64 of A-Z a-z 0-9 "-" "_" "."
    

    A host is at most 253 characters. Its origin is https:// followed by the host and, where a port is written, : and the port — the ASCII serialisation of a web origin (RFC 6454 §6.2), unrelated to the origin label of an object receipt (§2.4.2). Its key log is at the origin followed by /.well-known/wai-keys, and nowhere else. The suffix wai-keys is not yet registered in the IANA Well-Known URIs registry; registration under RFC 8615 is to be requested. Each origin and each key-id has one spelling: a capital letter in the host, an escape other than %3A, a path, a missing fragment, the default port written out and an IP address are not this form. A signer writes this form only for a key its origin's log lists.

  • Ed25519 did:key. The characters did:key:z followed by the base58btc encoding (the Bitcoin alphabet, each leading zero byte written as 1) of the bytes 0xed, 0x01 and a 32-byte key, where decoding the text and encoding the result again gives the same text. It names that key and nothing else: not who holds it, and not what it may sign.

  • Label. Any other string, including did:key and did:web strings of neither form above. A verifier reads nothing from a label.

A verifier MUST NOT report a signer_id as the identity of an item's signer unless it has confirmed the item (§7.4); otherwise it reports the signer_id as the signer's own label.

A did:web resolver reads a DID document at /.well-known/did.json on the same origin. An origin that publishes one SHOULD list in it exactly the active keys of its latest key-log revision, each as an Ed25519 publicKeyMultibase under the id did:web:<host>[%3A<port>]#<key-id>, the key's origin-bound signer_id. A verifier here MUST NOT take a key from it: the key log is what §7.2 signs and chains, and it governs. The wai-keys tool of the reference implementation emits such a document from a log and reports every difference between a document and a log.

7.2. The key log

A log date-time is an RFC 3339 date-time in UTC, written YYYY-MM-DDTHH:MM:SSZ with T and Z in capitals, no fractional second and a second of at most 59, so that two of them compare as strings. An authority is one or more segments of A-Z, a-z, 0-9, -, _ and ., joined by /, at most 128 characters in all (§7.5).

A key log is a JSON object with exactly two members: kind, which is "wai-key-log", and revisions, a non-empty array of revisions whose first has seq 0 and each of which is the parent of the next. A revision is a JSON object with exactly these members, under §2.4.3's encodings:

member type meaning
kind string "wai-key-set".
origin string The origin the log is published under, as §7.1 derives it.
seq integer 0 for the first revision; otherwise one more than its parent's.
prev 32 B hex or null Its parent's revision_hash; null exactly when seq is 0.
issued_at log date-time When the origin issued it. Self-asserted; not earlier than its parent's.
root 32 B hex The Ed25519 key that signs this revision.
next_root 32 B hex The commitment (below) to the key that may sign as root after this one; never the commitment to root.
keys array The key entries, below.
revoked_modules array of 32 B hex The SHA-256 digests of the modules and pinned parameter sets the origin has revoked (§7.9), of the room grants and viewers it has revoked (live-rooms §5), and of the consent records withdrawn in its rooms (capture-binding §8.3), none twice.
grant_max_s integer The longest module grant, in seconds, the origin issues: at least 1 and at most §7.9's ceiling.
status_max_age_s integer How long, in seconds, a key status stays fresh: at least 1 and at most §7.9's ceiling.
sig 64 B hex root's signature, below.

A key entry has exactly these members (a hybrid entry adds pq_suite and pq_key, and a revision with a hybrid root adds pq_suite, root_pq_key and pq_sig, each with what pq-signatures §6.1 adds to rules 1, 3, 5 and 6 and to the commitment):

member type meaning
id string A key-id of §7.1.
key 32 B hex An Ed25519 verifying key that §3.1 step 3 accepts.
may_sign array of authorities The authorities (§7.5) the origin gives this key: at least one, none twice.
status string "active", "retired" or "revoked".
until log date-time or null For a retired key, the time after which the origin signs nothing with it; null for a key never retired.
compromised_since log date-time or null For a revoked key, the time from which the origin attests that another party may have held it; null otherwise.

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

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

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

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

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

7.3. Anchors

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

  • Pinned — the log is provisioned out of band by the relying party, as a sink holds a trust anchor (unidirectional-delivery §3).
  • Fetched — the log is retrieved from the origin's key-log location over HTTPS, under a certificate the verifier's TLS trust store accepts for the host (RFC 9110 §4.3.4), without following a redirect, and the verifier records when it retrieved it (acquired_at). The fetch establishes only that the origin served that log then, and only to the verifier that fetched it: whoever relies on a confirmation built on it relies on that verifier's record.

Anchoring checks the log in this order, reports the first failure, and anchors nothing on a failure: the document has exactly the members kind and revisions, with the values above (key_log_malformed); each revision, in order, passes rules 1–3 (that rule's reason); the first revision has seq 0 (key_log_chain_break); and each later revision passes rules 4–6 against the one before it (that rule's reason).

The anchor, and every revision it descends from that the verifier holds, is trusted. Later revisions may come from anywhere — the origin, a cache, a mirror, a transparency log — because each is checked against its parent; a revision obtained other than by pinning or from the origin's own location is never an anchor. An anchor moves in these ways only:

  • Forward, when the verifier anchors the origin again from its location and the log it retrieves contains the anchor: the log's latest revision is then the anchor. The record keeps how the origin was first anchored — a pinned record stays pinned, since the new anchor descends from the pinned one along the chain its root signed — and a fetched record takes the new acquired_at. A pinned record also keeps the revision_hash of the revision that was pinned (its pinned revision), which no move changes: the hardware-class and lawful-basis authorities are confirmed under the moved record, and a pre-emption authority only from the pinned revision itself (§7.4 step 6a).
  • To a rotation that supersedes (rule 7) the anchor or a trusted revision it descends from, where the trusted revision's own child is not a rotation. A confirmation reports such a move (§7.4 step 3). A retrieved log whose revisions after the last one it shares with the trusted line begin with such a rotation moves the anchor to its latest revision, and the record is then fetched: the rotation reached the verifier other than by pinning.
  • Anew, by pinning, whatever the verifier held.

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

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

7.4. Confirmation

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

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

  • A verifier MUST NOT present an item it has not confirmed as signed by the party its signer_id names.
  • A verifier MUST NOT report a refusal as unconfirmed.
  • A confirmation establishes that the origin, as anchored, lists this key with these authorities. It does not establish that the statement is true (§2.4.4), that the key was not misused within its authority, or that an energy figure is what its class says (energy-measurement §2.2). It is the origin's say-so, made checkable.
  • A confirmation describes the log as the verifier holds it. A revocation the verifier has not retrieved is not in it, and the head's revision_hash in the report shows how current the log was.

7.5. Authorities: who may sign which class

The authorities an item needs are fixed by the item, never by its signer. A confirmation needs every one of them:

item authorities
a group receipt, and the object receipts under it delivery; and energy/<class> for each acquisition class an object receipt under it labels, where its leaf binds a label (§2.2), and for the class a labelled group payload signs for the group's total (§2.3)
a claim its class's authority: energy/ followed by its energy_provenance for energy, for example energy/OnChipCounter; lawful-basis/declaration or lawful-basis/decision by its record; its class name for every other class — log-inclusion, soft-binding, soft-binding-span, binding-recovery, measurement, operating-point, stage, delivery-gap, reconstruct-session, consumption, transmission, reconciliation, schedule, timeliness and cache-transmission. And energy/ followed by its energy_provenance whenever its body carries that member, whatever its class
the energy figure of a reconstruct-session claim whose energy is not null reconstruct-session, and energy/ followed by energy.energy_provenance
the energy figure of a transmission claim whose energy is not null transmission, and energy/ followed by energy.energy_provenance
the energy figure of a cache-transmission claim whose energy is not null cache-transmission, and energy/ followed by energy.energy_provenance
a receipt of another WAI family that signs its own signer_pubkey and signer_id receipt; and energy/<class> for each class its figures carry a label for
a module grant (§7.9) module/decoder or module/reconstruct, by its form
a room descriptor or room grant (live-rooms §3, §4), or a room usage record (ai-usage §4.2) live/room
an upload receipt of a take (capture-binding §6) capture/store
an impression task file (impression-aggregates §5.4) aggregate/task
a per-hop session record (live-metering §3) live/relay, live/compositor or live/ingest, by its role; and energy/<class> for its figure's class where it is metered
a room-session receipt (live-metering §4) signed by a key other than the room authority live/meter; and, from any signer, energy/<class> for each class it totals
a steering policy or steering decision (energy-steering §3.5) signed by a key other than the room authority live/steer; and, from any signer of a decision, energy/<class> for each class among its figures
a watermark session record (forensic-watermark §5) live/watermark
a derivation record (deriving-edge §5) edge/derive, from the origin of each source object it derives from (deriving-edge §8); and energy/<class> for its figure's class where it is metered
a holdings record's holder (holdings §4), and the upstream of a settlement acknowledgement or declaration (settlement §3, §4), placed as a serving node (cache-authority §4.4) cache/serve

A class registered in §2.4.4 registers its authority here with it, and a family that needs a narrower authority than receipt registers one here. An authority a verifier does not know matches nothing. Because an energy authority is read from the energy_provenance member wherever it appears, a class that restates a figure with its acquisition class needs that class's authority without this table naming the class.

Pinned-only authorities. The hardware-class energy authorities — energy/CalibratedInstrument, energy/OnChipCounter and the legacy energy/HwShunt — the lawful-basis authorities — lawful-basis/declaration and lawful-basis/decision — aggregate/task, and any authority registered for pre-emption are confirmed only under a pinned anchor that has not moved (§7.4 step 6a). Every other authority is confirmed under a pinned or a fetched anchor, and every confirmation reports which.

Expected origin. A verifier MAY expect an origin for an item: one its relying party names — the service a sink used, the publisher it subscribed to — or the subject's origin: the origin under which it has confirmed, with the authority delivery, the group receipt that carries a claim's subject (for a claim whose subject_type is object), or under which it has confirmed the receipt a claim's subject names (subject_type receipt). It reports which. §7.9 expects one of the sink's module origins. A reconstruct-session claim is a sink's statement about an object a publisher delivered, so its subject's origin is not the sink's: a verifier expects an origin for a session only where its relying party names one. A session signed under a did:key is unconfirmed (§7.4 step 1), which is the normal case for a sink's ephemeral key.

§2.4.4's signed by column names the party each class expects. Which of its keys is that party is the origin's decision, and its log records it:

  • Delivery. A group receipt confirmed under an origin was signed with a key that origin gives delivery.
  • Energy. energy/<class> lets a key state figures of that class and no other, so an origin can confine what a metering key may claim. No authority raises a figure's class or grade, or adds to what a class establishes (energy-measurement §2, §2.2). An origin SHOULD give the hardware-class authorities only to a key held by the component that signs the reading, and not to a key it also gives delivery. Beside a confirmed item that needs a hardware-class authority, a verifier reports whether its key also holds delivery (also_delivers). A group receipt whose objects' leaves bind a hardware-class label, or whose labelled total carries one, is signed by one key for delivery and for the figures, so also_delivers is true there by construction, and is reported as such. An energy claim confirmed under the subject's origin was made under the publisher's authority, by its own key or by a metering key its log lists.
  • Lawful basis. A declaration confirmed with lawful-basis/declaration is its origin's; §2.4.6's check of a decision against it then establishes that the decision matches a declaration made under that origin. A verifier MUST NOT report a decision as matching a service operator's declaration unless it has confirmed the declaration, and reports the declaration's level beside the decision's.
  • Measurement and operating point. A measurement claim needs measurement and the energy authority of the class it restates; an operating-point claim needs operating-point and that energy authority too. A report of a hardware-class figure is therefore confirmed only under a pinned anchor, and is reported with also_delivers. §2.4.8's check 5 already requires a measurement claim's key to be the key that signed the figure; confirmation adds that the figure's origin gave that key the authority to report on it. An origin can give operating-point to the party that records requests and commitments without letting it state figures of its own.
  • Stage and delivery gap. stage lets a key sign what its holder observed of a stage set (§2.4.10), and delivery-gap what a relay did not forward (§2.4.11). Neither is pinned-only, and neither is delivery: an origin gives stage to the sinks it speaks for and delivery-gap to its relays, and a key that holds delivery alone signs neither.
  • Reconstruct session. reconstruct-session lets a key sign what a sink reconstructed (§2.4.12). It is not pinned-only and it is not delivery: an origin gives it to the sinks it speaks for. A session's energy figure is a second item, since the session's other members stand without it: it needs reconstruct-session and the energy authority of its class, every one of them, and is reported with also_delivers where that class is a hardware class. A verifier reports the session's confirmation and its figure's side by side, and MUST NOT report the figure as its origin's unless it confirmed the figure. A key that holds reconstruct-session and not the figure's energy authority confirms a session whose figure is refused (authority_not_granted); under a fetched anchor, a hardware-class figure is unconfirmed (anchor_not_pinned) beside a confirmed session. The origin then stands behind the session, and not behind its figure.
  • Transmission and reconciliation. transmission lets a key declare the bearer a group it signed was sent over (§2.4.14), and reconciliation lets a key sign what a sink received of a group (§2.4.15). Neither is pinned-only and neither is delivery: an origin gives transmission to its publishing keys and reconciliation to the sinks it speaks for, and a key that holds delivery alone signs neither. A transmission's energy figure is a second item, as a session's is: it needs transmission and the energy authority of its class, and the declaration stands without it. A sink with no return path confirms under anchors provisioned before it was cut off, and a key log that reaches it over the bearer anchors nothing (bearer-receipts §8). schedule lets a key state the passes of a group it signed over an instance (§2.4.16), or of a group another key signed over an instance it declares as its own (cache-authority §5.6); an origin gives it to its publishing keys, as it gives transmission, a node's operator to the nodes that schedule their instances, and it is neither pinned-only nor delivery. timeliness lets a key state when the objects of a group it signed are of use (§2.4.17); an origin gives it to its publishing keys, as it gives schedule, and it is neither pinned-only nor delivery.
  • Log inclusion. A confirmation says under whose authority the log's evidence was attached. It does not check log_receipt: §2.4.7 is unchanged.
  • Soft binding. A declaration confirmed with soft-binding, or a span declaration with soft-binding-span, is its origin's; confirmed under the subject's origin, it is the publisher's. §3.2 step 3 and §2.6.3 check 4 are unchanged: a recovery still needs the declaration's key to be the key of a verified delivery receipt.
  • Modules. module/decoder lets a key grant a decoder module, or a parameter set pinned by an envelope, for the capabilities the grant names; module/reconstruct lets it grant a reconstruct module (§7.9). Neither is pinned-only; §7.8 says what a module authorised under a fetched anchor is. A key that holds module/decoder also signs the conform-result/1 record that attests a decoder module to the WAISM profile (waism §6, check 9c). Such a record carries no signer_id, so it is no item §7.4 confirms: a sink finds its key in the head that the module's grant was confirmed under (§7.9, check 3).
  • Rooms. live/room lets a key act as a room authority: sign room descriptors and the grants under them (live-rooms §5). It is not pinned-only, and it is not delivery.
  • Stores. capture/store lets a key act as a store a room's takes are uploaded to: sign the upload receipts of a take (capture-binding §6.4). It is not pinned-only, and it is not delivery.
  • Aggregation tasks. aggregate/task lets a key sign an impression task file: name a DAP task, its two aggregators and its collector, which receivers on a bearer with no return path report to (impression-aggregates §5.4). It is pinned-only, because a task decides which two aggregators receive every receiver's shares and so gives up part of unidirectional-delivery §2's "the source learns nothing", and a fetched anchor is authority taken on first use. It is not delivery.
  • Watermark edges. live/watermark lets a key act as an edge that chooses forensic A/B variants for a room's viewers: sign the session records of what it embedded (forensic-watermark §5.6). It is not pinned-only, and it is not live/relay or delivery.
  • Deriving edges. edge/derive lets a key act as an edge that derives objects from an origin's content and publishes them under its own key (deriving-edge §8). The origin that gives it is the source's, not the edge's: an origin lists the edges it trusts with its content, and a hop is confirmed under each origin its chain reaches. It is not pinned-only, and it is not delivery or live/relay.
  • Caches and peers. cache/serve lets a key act as a node that serves, from what it holds, objects of groups another key signed (cache-authority §4): sign its holdings records, sign its declarations, and be the upstream of the acknowledgements its receivers sign. cache-transmission lets a key declare a bearer instance of its own over which it sent such objects (§2.4.18), as delivery-gap lets a relay sign what it did not forward. The origin that gives them is the node's operator's, not the publisher's: a cache changes no byte, so the publisher's receipts verify whatever it serves. Neither is pinned-only, and neither is delivery, transmission, live/relay or edge/derive: a cache publishes nothing, declares none of the publisher's transmissions, signs no room's hop record and derives nothing. A cache-transmission claim's subject is the publisher's group, so a verifier expects an origin for one only where its relying party names one, as for a session.
  • Pre-emption (unidirectional-delivery §3), and relay attestation (§4.3) other than a delivery-gap claim, a cache-transmission claim and a derivation record, have no class and so no authority. When one is registered for pre-emption it is pinned-only, and the pinned revision itself must list the key with it (§7.4 step 6a): a fetched anchor is authority taken on first use, which that section forbids for an instruction that displaces what a person chose.

7.6. Rotation and revocation

  • A signing key is rotated by listing its successor under a new id before the successor signs, and retiring the old key with an until no earlier than the last item it signed. Items the old key signed stay confirmable. A claim chain (§2.4.3) is per key: the successor starts its own, and §3.1 step 5 is unchanged; the log, not the chain, connects the two keys. An entry's authorities never change (rule 6): an origin that gives a key another authority lists the key's successor with it.
  • A compromised key is revoked with compromised_since, and every item under it is refused from then on (§7.4 step 5): every receipt, claim and module grant it signed, whatever time the item carries. An item's own time cannot restore it, and this section defines no independent time that could.
  • A module or a pinned parameter set is revoked by adding its digest to revoked_modules (§7.9). A revocation, like a key's, stays in every later revision.
  • A room grant or a viewer is revoked by adding the grant's digest, or the viewer's tag, to revoked_modules (live-rooms §5); a consent record is withdrawn by adding its digest there (capture-binding §8.3). The list holds digests of each kind under one rule: append-only, none twice.
  • The root is pre-committed: each revision commits to the next root, and only a rotation changes the commitment. Whoever steals the root key in use can sign revisions that add keys, but can neither rotate nor change the commitment; the owner's rotation, signed with the committed key, supersedes what the thief signed from the point where the two histories fork (§7.2 rule 7). The committed key is the recovery key and SHOULD be held apart from the root in use. An origin that loses control of the committed key loses its log: verifiers anchored on it confirm nothing new, and anchoring again means pinning.
  • Registration. An origin SHOULD register each revision with a Transparency Service, as it registers group receipts (transparency §3), so that a fork is visible to any Relying Party that checks the log. Registration anchors nothing (transparency §6).

7.7. Reasons

step level reason raised when
1 unconfirmed signer_id_not_origin_bound signer_id is not of §7.1's origin-bound form
1 refused signer_id_names_another_key signer_id is a did:key of a key other than signer_pubkey
2 unconfirmed origin_not_anchored the verifier has no anchor for the origin, or does not hold it
3 unconfirmed key_log_chain_break a child's seq or issued_at does not follow its parent's
3 unconfirmed key_log_root_not_committed a child signed by its parent's root has another next_root
3 unconfirmed key_log_rewrites_history a child drops or reorders an entry, changes its key or authorities, moves a status back, changes a time once set, or drops a revoked module
3 unconfirmed key_log_fork a parent has several authorised children and not exactly one rotation among them, or a trusted revision has several superseding rotations
4 refused key_not_listed the head lists no entry with the key-id
4 refused key_mismatch the entry's key is not signer_pubkey
4a refused hybrid_downgrade the entry is hybrid and the item carries a classical signature only (pq-signatures §6.1)
4a refused pq_key_mismatch the item's ML-DSA key is not the entry's, or the entry is classical (pq-signatures §6.1)
4a unconfirmed hybrid_required the anchor requires hybrid and the item carries a classical signature only (pq-signatures §6.1)
5 refused key_revoked the entry is revoked
5 refused key_retired the entry is retired and the item's signed time is later than until
6 refused authority_not_granted the entry's may_sign does not hold every authority the item needs
6a unconfirmed anchor_not_pinned the item needs a pinned-only authority, and the anchor is fetched, has moved, or has no pinned revision, or, for pre-emption, the pinned revision does not list the key with it
7 refused origin_not_expected the origin is not the one the verifier expects, or not one of the set it expects
§7.3 — key_log_malformed a log or a revision is not of §7.2's form
§7.3 — origin_mismatch a revision names another origin
§7.3 — key_log_signature_invalid a revision's signature does not verify under its root
§7.3 — key_log_chain_break the first revision's seq is not 0, or a revision does not follow the one before it
§7.3 — key_log_root_not_committed a revision's root is neither the previous one's nor committed to by it, or its next_root changes without a rotation
§7.3 — key_log_rewrites_history as at step 3, between consecutive revisions of the log being anchored
§7.3 — key_log_rolled_back a log retrieved from an anchored origin ends at a trusted revision older than the anchor; the anchor is kept
§7.3 — key_log_replaced a log retrieved from an anchored origin neither contains the anchor nor supersedes a trusted revision by a rotation; the anchor is kept

The reasons of §7.9's checks are listed there.

7.8. Security considerations

  • First use. A fetched anchor is as good as the first retrieval: the domain name system, the certificate accepted, and the origin's server at that moment. Whoever controls any of them then can serve a log of its own to a verifier anchoring for the first time. Once anchored, a verifier is not exposed to that again, since every later revision must descend from its anchor (§7.3). A relationship that cannot bear the risk pins its anchor; the authorities §7.5 makes pinned-only are those for which WAI does not leave that choice open.
  • Modules on first use. A module authorised under a fetched anchor is code accepted on first use: whoever served the first retrieval chose which keys may grant it. A sink that cannot bear that pins the anchors of its module origins. Every module outcome reports the anchor's path (§7.9).
  • Which origin grants a module. Anchoring an origin to confirm its receipts does not make it a source of modules: a grant is held only from one of the sink's module origins (§7.9), so an origin anchored on first use for some other publisher's receipts authorises nothing a sink runs on this envelope.
  • Revocation from any anchored origin. A digest revoked in the head of any origin a sink has anchored is not held, whichever origin grants it (§7.9 check 5), so a publisher's revocation cannot be undone by another origin's grant. Any anchored origin can therefore make a module fall back; it cannot make one run.
  • Withheld revisions. An origin, or anyone between it and a verifier, can withhold its latest revisions and with them a revocation. How often a verifier retrieves the log is its own policy; the head named in a report shows what it rested on, and registration (§7.6) makes a withheld revision's existence checkable. For modules, §7.9's status bounds how long a withheld revocation can go unnoticed.
  • A rolled-back server. A retrieval that returns an older log is reported (key_log_rolled_back), never taken as the anchor; a status that names a revision older than the head the sink holds is not current (§7.9).
  • A stolen root. Until the owner's rotation reaches a verifier, a verifier holding only the thief's revisions confirms what the thief adds. When the rotation arrives it supersedes them, and every key they added is no longer listed.
  • Injected revisions. A revision that fails rules 1–3, is unattached, or is signed by a key its parent does not authorise is no child of anything and cannot stop a confirmation; only the origin's own root and committed key can. A status names a revision on the head's line or is not held (§7.9), so one signed under an injected revision changes neither a module outcome nor its reason.
  • Retired keys and untimed items. Many receipt families carry no signed time. An item without one, under a retired key, stays confirmable for as long as the key is not revoked, and is reported as untimed with the key's until. An origin that cannot vouch for a retired key's past use revokes it.
  • Clocks. §7.9's freshness and grant windows are measured on the sink's clock. A sink whose clock can be set back runs a module for as long as the clock is set back; a sink with no clock it trusts treats every status as stale and runs no module. The kill latency of §7.9 is a bound on a trusted clock and on nothing else.
  • Published private keys. A key whose private half has been published attests nothing, and MUST NOT be listed in a key log. Every key WAI's own corpora, pins and reference meters sign with has a published seed, and every such seed is one byte repeated 32 times. The reference implementation exposes the public keys of all such seeds (key_log::published_test_keys); its wai-keys tool refuses to put one in a log unless told the log is a test log, and reports one it finds in a log it verifies. Every vector of §5.8 is a test log, and every anchored verdict there carries that report.
  • A name with a new owner. Whoever controls a domain next can publish a new log under its origin. It does not descend from an existing anchor, so an anchored verifier keeps its anchor and reports key_log_replaced; a verifier anchoring for the first time sees only the new log, in which items signed under the old one are not listed. A verifier that must keep confirming them keeps the revisions it confirmed them against (§7.3).
  • Retrieval reveals interest. Retrieving a log tells the origin that someone is checking its signatures. A verifier retrieves the log, not a key per item, and may take later revisions from any source.
  • Separation. A root signs revisions and statuses and nothing else: no entry may be a root or the committed key (§7.2), and a revision's, a status's and a grant's signing domains are distinct from every receipt's and claim's.

7.9. Decoder and reconstruct modules

A module is code a sink obtains apart from an envelope and runs to decode a capability (a decoder module) or to rebuild a reconstruction from what a decoder produced (a reconstruct module) — for example a WebAssembly module. A parameter set an envelope pins by its digest — the prior a neural capability decodes against (prior-carriage) — determines a decode the way code does. It is revoked the same way, by its digest, at every sink that holds a key log, and authorised the same way, as a decoder module, by a sink that holds pinned parameter sets to this section. Running a module requires this section; holding pinned sets to it is the relying party's choice per capability (SPEC §4). A sink that holds a capability's pinned sets to this section runs the checks below as SPEC §3.1's check 5, before it looks for the set among what it holds; a sink that holds a key log but not the capability's sets reads only check 5's revocation lists (Pinned sets and several grants, below).

Identity. A module's digest is the SHA-256 of its bytes exactly as the sink instantiates them, and a pinned parameter set's is its pin's sha256, the digest of the set in the pin's form (SPEC §3.1); both are written as 64 lowercase hexadecimal digits. For a pinned set the pin's digest stands for the bytes' at check 2, and SPEC §3.1's checks 6–8 then require the bytes the decode reads to have it. A sink that instantiates other bytes than it hashed runs code nothing authorised.

Grant. An origin authorises a module by a signed grant, a JSON object with exactly these members, under §2.4.3's encodings:

member type meaning
kind string "wai-module-grant".
origin string The origin under whose key log the grant is checked: the one its signer_id names (§7.1).
module 32 B hex The module's digest.
form string "decoder" or "reconstruct".
capabilities array of terms The capabilities the module may decode or reconstruct: at least one, none twice.
not_before log date-time When the grant takes effect.
not_after log date-time When it lapses; not earlier than not_before.
signer_pubkey 32 B hex The granting key.
signer_id string The granting key's signer_id (§7.1).
sig 64 B hex Ed25519(signer, "wai:module-grant\x01" || JCS(grant without "sig")).

Status. A key status is a cacheable signed pointer to an origin's head, re-issued on each heartbeat, so the key log does not grow with every heartbeat. It is a JSON object with exactly the members kind ("wai-key-status"), origin, head (32 B hex, a revision_hash), seq (that revision's seq), issued_at (a log date-time) and sig, Ed25519(root, "wai:key-status\x01" || JCS(status without "sig")), where root is the root of the revision head names. A sink holds a status only if it has that form, names the grant's origin, and names a revision on the head's line — the head §7.4 step 3 reached, or a revision the sink holds that the head descends from by prev — with its seq, under whose root the signature verifies. A status that is not held is ignored, as an injected revision is (§7.2).

Over HTTP, a status and an origin's grants are served where http-delivery §8.4 states.

Module origins. A sink holds a module only from an origin it accepts module grants from for the envelope and capability at hand — its module origins, each recorded with where it came from:

  • relying-party — an origin the sink's relying party names as a source of modules for that capability;
  • subject — the origin under which the sink confirmed, with the authority delivery, the group receipt that carries the envelope: its publisher.

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

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

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

  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.

check reason raised when
1 module_grant_invalid the grant is not of the form above, its signer_id does not name its origin, or its signature does not verify
2 module_digest_mismatch the bytes' SHA-256 is not the grant's module
3 §7.4's reason the granting key is not confirmed with module/<form> under one of the sink's module origins (origin_not_expected where its origin is not one)
4 module_status_stale no status is held, or the latest naming the head is older than status_max_age_s or later than now, or there is no trusted clock
4 module_status_not_descended statuses are held, and none names the head the sink reached
5 module_revoked the head's revoked_modules, or another anchored origin's, holds the digest
6 module_grant_not_yet_valid now is earlier than not_before
6 module_grant_expired now is later than not_after
7 module_grant_too_long the grant's window exceeds the head's grant_max_s
8 module_capability_not_granted the capability is not one the grant names
9 module_profile_violation the module breaks a rule of the WAISM profile at every level (waism §4)
9 module_unattested the sink holds no record that attests the module for the capability, signed by a key the grant's head lists with module/decoder (waism §6, check 9c)
10 module_engine_not_deterministic the module is at the float level, the object needs a tier, and the engine is not set to the deterministic profile's NaN rule
10 module_probe_mismatch a probe the sink ran gave other bytes than waism §6, check 10, expects