open-standards/wai/extensions/jwp-receipts.md
Transaction Science fbde9b2131
Some checks failed
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 3s
no-vanity-metrics / WAI public text names no comparison (pull_request) Successful in 3s
no-vanity-metrics / WAI public text names no company or product (pull_request) Successful in 3s
standards workspaces build + test / eoc-rs (pull_request) Successful in 1m49s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m23s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m16s
standards workspaces build + test / aac-rs (pull_request) Failing after 29s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m10s
standards workspaces build + test / joule-code-rs (pull_request) Failing after 24s
standards workspaces build + test / joulecontract-rs (pull_request) Failing after 6s
standards workspaces build + test / map-rs (pull_request) Failing after 23s
standards workspaces build + test / mesh-rs (pull_request) Successful in 1m36s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 3m38s
wai cross-architecture byte-equality / linux-x86_64 (pull_request) Successful in 23s
standards workspaces build + test / openpay (pull_request) Failing after 3m10s
wai cross-architecture byte-equality / linux-aarch64 (pull_request) Successful in 51s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 3m55s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 38s
standards workspaces build + test / jouleclaw-rs (pull_request) Failing after 15m48s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 2m9s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 24s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 45s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 5m59s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 30s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 1m4s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 1m8s
wai formal verification (integer core) / kani (pull_request) Successful in 4m22s
WAI JPEG-AI whole decode (pinned inputs, required) / fetch pinned JPEG-AI inputs → whole decode + pixel hash, required (pull_request) Successful in 57s
WAI neural decoders end-to-end / synthetic fixtures → Rust decode → reference (±4) (pull_request) Successful in 1m12s
WAI neural sink gates / model-free refusals of the neural sink (pull_request) Successful in 1m0s
WAI section references / cited sections exist (pull_request) Successful in 11s
WAI mbt2018_mean integer decode (regenerated vectors) / regenerate mbt2018_mean integer vectors → Rust decode, required (pull_request) Successful in 3m53s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 1m43s
standards workspaces build + test / arl-rs (pull_request) Successful in 2m33s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 2m59s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 3m1s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 2m42s
standards workspaces build + test / sense-rs (pull_request) Successful in 2m42s
standards workspaces build + test / macOS energy backend (jcp-energy) (pull_request) Successful in 3m23s
wai cross-architecture byte-equality / macos-arm64 (reference) (pull_request) Successful in 30s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 6m9s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 31s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 1m35s
wai: an unpinned decode reads its set once and is checked by every identity
Outside the integer path, a sink whose key log only revokes ran check 5
over the registered file's digest and any listing found beside it, then
decoded from the path. With no listing beside them, a revoked several-
file set decoded, and a file changed after the check was what the decode
read.

At a registry with a key log, the neural sink now reads once what the
decode can read: the registered file of a one-file set; otherwise every
file below the directory the decoder reads from (the registered
directory, or the registered file's), as pin::listing_of_dir lists it
(pin::read_dir_set, which listing_of_dir now uses). Check 5 runs over
every digest a pin over it could name: the file's, or the directory's
derived listing and each member's, and each listing beside them that
matches what was read. The decode then reads only those bytes
(neural::ParamSource::Checked, which replaces Authorised and records the
registered file within a set), never the path; where the key log holds
the capability's sets to jwp-receipts §7.9, only the part §7.9
authorised. The decoders outside the integer path read files by name
from that directory, some chosen by the payload, so the set is the
directory rather than a fixed list. With no key log the decode reads the
path, as before.

SPEC §4 (Module authorisation) and jwp-receipts §7.9 state the reference
behaviour.

Test: neural_native_decode::a_revoke_only_key_log_checks_and_reads_the_set_once
(a revoked set beside its listing; with no listing, the decoder and the
directory registered; a member's own digest revoked; the tables swapped
on disk after the check; no key log).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-01 23:08:03 -04:00

151 KiB
Raw Permalink Blame History

WAI Extension: JWP Receipt Chain

Status: Draft. Sub-spec of WAI v1.0. Canonical text: wai/extensions/jwp-receipts.md in the open-standards repository (https://git.transaction.science/Transaction-Science/open-standards). The verifier of the reference implementation is in https://git.transaction.science/Transaction-Science/open-standards/src/branch/main/wai/wai-rs: claim_receipt, jwp_delivery, and for §7 signer_id and key_log (feature key_discovery).

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


1. Scope and model

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

In scope:

  • A per-object content commitment (BLAKE3-256) over WAI envelope bytes.
  • A per-group Merkle root binding every object in a group under one signature.
  • A per-group Ed25519 signature over canonical group metadata.
  • A typed receipt envelope (JSON or equivalent CBOR) with explicit parent-receipt linkage for cross-hop preservation.
  • 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 (§2.4.5–§2.4.9) — 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.
  • An optional acquisition-class label on the object receipt, bound with the object's figure and origin into its Merkle leaf (§2.2, §2.4.2); an optional class for the group total, signed under a labelled group payload with its coverage and the parent link (§2.3, §2.4.1); and the group total read from its objects (§3).
  • A second binding class (§2.6): a soft binding, declared by the publisher with a registry-named algorithm and a resolution endpoint, for objects whose bytes changed in normal distribution — and the ancestor rule (§3.2): a receipt reached through it attests the object the one in hand was derived from, never the object in hand.
  • Key discovery and signing authority (§7): a form of signer_id that names the web origin a signer publishes its keys under; a signed, chained key log the origin publishes at a well-known location, listing each key with what it may sign and whether it is active, retired or revoked; and a confirmation a verifier runs after the signature, against an origin it has anchored, reported beside the receipt's own verdict. It adds no member and changes no signed byte.
  • Module authorisation (§7.9): who may authorise the decoder and reconstruct modules a sink runs, and the parameter sets an envelope pins; their revocation by digest; the rule that a sink which cannot establish a module's authorisation falls back rather than run it; and the longest a revoked module keeps running.

Out of scope:

  • A transport. This extension does not define how receipts move on the wire; receipts ride alongside WAI envelopes in the carrier protocol (e.g. a MoQT object header field, an HTTP trailer, a sidecar file).
  • 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-track group counter
 || merkle_root[32]                    // §2.2
 || u64_be(leaf_count)                 // number of objects in the group
 || u64_be(total_joules_micro)         // sum of microjoules across the group
 || u32_be(len(group_start)) || group_start_utf8   // RFC 3339 start timestamp
 || u32_be(len(group_end))   || group_end_utf8     // RFC 3339 end timestamp
 || publisher_key[32]                  // Ed25519 verifying key (RFC 8032)
 || u32_be(len(publisher_id)) || publisher_id_utf8 // bound identity string

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

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

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

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

2.4. Receipt envelope (typed)

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

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.
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 BLAKE3 of the previous group receipt's canonical bytes under this publisher key; null for the first group.

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

field type meaning
sign_version integer 2. Absent (or null), the group is in the legacy form; any other value is refused (group_label_invalid).
energy_provenance, energy_uncertainty, energy_counter_filtering, energy_calibration_ref as energy-measurement §2.7 The total's class and its evidence; all absent for the unmetered marker beside a zero total.
energy_coverage string Whole or Partial; absent with the unmetered marker.

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

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

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

2.4.2. kind: "object"

Required fields:

field type meaning
kind string MUST equal "object".
track_alias u64 Mirrors the parent group receipt.
group_id u64 Mirrors the parent group receipt.
object_id u64 Monotonic from 0 within the group.
content_hash 32 B hex §2.1 BLAKE3 over the WAI envelope bytes.
joules_micro u64 Microjoules consumed producing this object; 0 is the unmetered marker. This field fixes the unit and the scope of attribution, not how the number is obtained — see energy-measurement, 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: a nullable member is present and null, so one claim has one canonical form.

field type meaning
kind string MUST equal "claim".
class string A class of §2.4.4 other than delivery.
subject_type string "object" or "receipt"; which a class admits is fixed by §2.4.4.
subject 32 B hex For object: the §2.1 content hash of the object the claim is about. For receipt: the link hash of a receipt — §3 step 3's for a group receipt, link_hash below for a claim — or, for a class that names a figure profile (§2.4.8–§2.4.9), the identity that profile defines (energy-measurement §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.9, §2.6).
signer_pubkey 32 B hex Ed25519 verifying key per RFC 8032.
signer_id string Bound identity string, as for a group receipt (§2.4.1, §7.1).
parent_receipt_hash 32 B hex or null link_hash of this signer's previous claim; null for its first.
sig 64 B hex Ed25519 signature, below.

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

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

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

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

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

2.4.4. Receipt classes

class carried by subject_type body signed by
delivery group, object (implied, not carried) — §2.4.1, §2.4.2 the publisher
energy claim object §2.4.5 whoever acquired the figure
lawful-basis claim object or receipt §2.4.6 the service operator (a declaration); the party recording it (a decision)
log-inclusion claim receipt §2.4.7 whoever attaches the log's evidence
soft-binding claim object §2.6.2 the object's publisher
binding-recovery claim object (the ancestor) §2.6.3 the party that performed the recovery
measurement claim object or receipt, as its figure profile fixes §2.4.8 the key that signed the figure: the group's publisher for moq-jwp:object, the receipt's signer for every other profile
operating-point claim object or receipt, as its figure profile fixes §2.4.9 the party that records the request, the commitment and the delivery

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 does not fix what subject.output holds; an OER receipt an energy claim can name holds there the object's §2.1 content hash in this form. Without this check a claim about one object could name a valid OER receipt about another that happens to carry the same figure and grade;
  4. that its provenance.how equals grade, and that its energy.joules_total, rounded to the nearest microjoule, equals joules_micro (a figure exactly half-way between two microjoules admits either) (energy_disagrees_with_oer). Check 2 has established that the figure is a quantity oer-receipts §4 reads — a decimal string of at most 34 significand digits, or an integer within I-JSON's range — since a receipt whose figure is anything else, or whose body holds a fraction, -0 or an integer beyond that range anywhere, does not verify. The figure is compared exactly, as oer-receipts §6 compares figures: 0.0120005 is half-way and admits 12,000 and 12,001 µJ, and 0.0120004 followed by twenty-six nines is below half-way, although rounding it to 28 significant digits would make it a tie.

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

The claim does not make the figure verifiable — re-execution reproduces the work, never the joules (energy-measurement §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 a build optimised as shipped; any other build is debug. A finer vocabulary is a new registration.
power_reader string or null The reader of the power interface (energy-measurement §8). Required under a hardware class (rule 4).

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

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

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

  1. joules_micro MUST NOT be zero (energy_zero_figure): a zero is the unmetered marker, or a failed measurement, and gets no report.
  2. figure_profile MUST name a profile of energy-measurement §6.1 whose subject type is the claim's subject_type (figure_unknown_profile); figure_index MUST be an integer exactly when that profile carries several figures (figure_index_mismatch); and group MUST be a hash exactly when the profile is moq-jwp:object (figure_group_mismatch).
  3. The acquisition class, as §2.4.5's rule 2 (energy_unknown_acquisition, energy_legacy_acquisition, energy_acquisition_evidence).
  4. Under OnChipCounter or CalibratedInstrument, power_reader MUST NOT be null (measurement_no_power_reader): a hardware figure is a reading, and its report names what read it, whether or not it carries a bracket.
  5. Where bracket is not null, in this order, with M its busy mean less its idle mean:
    • each n MUST be at least 2 (measurement_too_few_samples);
    • under OnChipCounter or CalibratedInstrument, each bracket's rail MUST have been live (energy-measurement §3.1): sd_uw not zero (a bracket whose samples are all equal advanced once at most), longest_live_run at least 2, live_intervals at least 5 and at least four fifths of n (live_intervals · 5 ≥ n · 4), and longest_live_run ≤ live_intervals ≤ n (measurement_rail_not_live);
    • window_us, completions and in_flight MUST NOT be zero (measurement_empty_burst);
    • M MUST be positive (measurement_no_marginal);
    • under OnChipCounter or CalibratedInstrument, M MUST be at least the idle sd_uw. A ratio below 1 is a failure to measure (energy-measurement §4) (measurement_below_attribution_floor);
    • joules_micro MUST be the figure F the bracket gives (energy-measurement §6.2) (measurement_figure_mismatch);
    • under OnChipCounter or CalibratedInstrument, the declared window_us MUST be the bracket's (measurement_window_mismatch), and the declared relative_ppm MUST be at least the least p the bracket supports (energy-measurement §6.2) (measurement_declaration_below_bracket).

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

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

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

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

2.4.9. class: "operating-point"

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

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 here covers the whole object; a binding scoped to a region or a time range is not expressible.

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

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

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

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

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

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

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


3. Verifier algorithm

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

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

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

  3. Check parent linkage. If the verifier maintains receipt-chain state for signer_pubkey, compute BLAKE3("moq-jwp:receipt-link\x01" || canonical_bytes(previous_group_receipt)) and compare to parent_receipt_hash. If they differ, reject (chain_break). If no previous receipt is known and parent_receipt_hash is null, accept as the chain head. A non-null parent_receipt_hash without a corresponding previously-verified receipt is a recoverable condition (parent_not_held): the verifier MAY request the missing receipt before accepting; it MUST NOT silently accept. canonical_bytes(G) is G's §2.3 signing payload, in the form G was signed in, followed by its 64-byte sig. The resulting hash is G's link hash: the identity a claim names G by (§2.4.3), and a WAI_RECEIPT property's group_receipt (moq-streaming-format §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.

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

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, an additional member is unknown_member, and then, in the table's order, a member that is not of the type the table gives it is field_type. That is a wrong JSON type; an integer that is not one of §2.4.3; a byte string that is not lowercase hex, two digits a byte, or a hash that is not 64 of them; a value outside an enumeration this extension defines here (record, binding_type, a bracket's opening, a build's profile); a URI that is not absolute; a term that is not one of §2.4.3 (empty, longer than 2048 bytes, or holding a White_Space or control character); or a malformed oer_receipt; or an operating point whose member names are not terms or whose values are not integers of §2.4.3. A member's type and encoding are checked together, before the next member is read. An object a class fixes the members of — a lawful-basis purpose, an energy_uncertainty — is checked the same way, its members and then their types, where the table reaches it, and the entries of an array in the array's order. Of a member's value, this step checks the type column and nothing else. What a class's rules require beyond it is step 4's, each with its own reason, and is never field_type here: labels and evidence from vocabularies another extension defines (acquisition class and its evidence, coverage, grade); a joules_micro or work_units of zero; an empty purposes; an empty basis, which is why its type is term or ""; an empty log_receipt or value; an alg that is a term but not in registry form; a resolution_endpoint that is an absolute URI but not an https URI with a host and no userinfo; an attests other than "ancestor"; a figure profile, an index or a group that does not fit the profile; a hardware figure's report that names no power reader; a bracket with fewer than two samples, liveness facts that do not show a live rail, a zero window, completion or thread count, or a busy mean not above its idle mean; the figure and least uncertainty a bracket gives; an empty point; and which points and predictions are present together.
  3. Signature. Ed25519 (RFC 8032). This step restates RFC 8032's own decoding and range rules, and fixes the two choices on which RFC 8032 verifiers differ — whether a key or R of small order is refused (RFC 8032's procedure does not refuse them; this step does), and which verification equation is checked — so that every conforming verifier accepts the same signatures. signer_pubkey is bad_signer_key if it does not decode under RFC 8032 §5.1.3 — which refuses a y coordinate ≥ p, and x = 0 with the sign bit set, so every point has exactly one encoding — or if it decodes to a point of small order (one whose order divides 8). sig is R ‖ S, and it is signature_verify_failed unless S < L (the order of the base point B, as RFC 8032 §5.1.7 requires), R is not the encoding of a point of small order, and the encoding of [S]B − [k]A equals R byte for byte, where A is the key's point and k = SHA-512(R ‖ signer_pubkey ‖ §2.4.3's signing input) mod L. That is the cofactorless equation. RFC 8032 §5.1.7 states the cofactored one and allows the cofactorless one; they disagree on some signatures whose points have a small-order component, and this step uses the cofactorless one only. The key checks run first. Under a key of small order the equation can hold for a signature made without any private key — under the identity, R = [s]B with S = s verifies for every message — so such a key is refused before its signature is examined.
  4. Class rules, per §2.4.5–§2.4.9 and §2.6, each with its own reason (below). They run after the signature because a rule about what a signer said only means something once it is established who said it.
  5. Chain. Against the previous claim the verifier holds for signer_pubkey. A claim is held only if it passes steps 1–4 itself. One that fails steps 1–3 is not known to be its signer's, and one that fails step 4 breaks its class's rules; neither is held, so an altered copy can neither satisfy a link nor break one in the signer's name. A non-null parent_receipt_hash with no previous claim held is parent_not_held — recoverable by fetching that claim, if it verifies, and MUST NOT be accepted silently. A parent that is not the held claim's link_hash, a seq that does not increase, or a null parent while a previous claim is held, is chain_break.
  6. Context: checks against other receipts the verifier holds, each verified first — an energy claim against the object receipt for its subject, verified under §3, including the class label its leaf carries (§2.4.5), and against the OER receipt it names, verified under oer-receipts as §2.4.5's check 2 (energy_oer_invalid); a measurement or operating-point claim against the receipt that carries its figure, verified under that receipt's own profile (for moq-jwp:object, the object receipt accepted under §3 against the group receipt group names) (§2.4.8); a decision against its declaration (§2.4.6); a recovery record against its declaration together with the publisher key of a verified delivery receipt for the declaration's subject (§2.6.3), and never against the declaration alone. A declaration is verified here under steps 1–4, and one that fails them — or is not JSON — is context_invalid: its members, signer_pubkey among them, are its signer's only once its signature is checked. For a decision or a recovery record the order is: the claim is of a class that names a declaration (context_mismatch), the declaration passes steps 1–4 (context_invalid), it is of the class the claim names (context_mismatch), then the class's own checks. A context receipt of another class is context_mismatch. For an energy claim, an object receipt about another subject, and an OER receipt other than the one named or about another subject, are context_mismatch, in §2.4.5's order. For a decision or a recovery record, a declaration other than the one named or about another subject is the class's own reason, decision_declaration_mismatch or recovery_declaration_mismatch; for a recovery record, a declaration signed by anyone but that publisher is then recovery_signer_mismatch, in §2.6.3's order.

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

step reason raised when
4 energy_zero_figure joules_micro is 0 in an energy, measurement or operating-point claim
4 energy_unknown_acquisition energy_provenance names no class of energy-measurement §2
4 energy_legacy_acquisition energy_provenance is the legacy HwShunt
4 energy_acquisition_evidence the class's evidence is missing, carried by a class that has none, or below the resolution floor (§2.4.5 rule 2)
4 energy_unknown_coverage coverage is not Whole or Partial
4 energy_unknown_grade grade names no grade of oer-receipts §2
4 energy_grade_exceeds_acquisition grade ranks above what the acquisition class supports
4 energy_zero_work work_units is 0
4 lawful_basis_no_purposes a declaration lists no purpose
4 lawful_basis_purpose_without_basis a purpose has an empty basis
4 lawful_basis_duplicate_purpose a purpose repeats an earlier one
4 log_inclusion_no_evidence log_receipt is empty
4 soft_binding_alg_not_registry_form alg is not in registry form
4 soft_binding_empty_value value is empty
4 soft_binding_endpoint_not_https resolution_endpoint is not an https URI with a host and no userinfo
4 recovery_not_ancestor attests is not "ancestor"
4 recovery_of_hard_bound_object observed equals subject
4 figure_unknown_profile figure_profile names no profile of energy-measurement §6.1 for the claim's subject_type
4 figure_index_mismatch figure_index is an integer under a profile that carries one figure, or null under one that carries several
4 figure_group_mismatch group is a hash under a profile other than moq-jwp:object, or null under it
4 measurement_no_power_reader under a hardware class, power_reader is null
4 measurement_too_few_samples a bracket's n is below 2
4 measurement_rail_not_live under a hardware class, a bracket's liveness facts do not show a live rail, or its samples are all equal
4 measurement_empty_burst a bracket's window_us, completions or in_flight is 0
4 measurement_no_marginal a bracket's busy mean is not above its idle mean
4 measurement_below_attribution_floor under a hardware class, the marginal draw is below the idle standard deviation
4 measurement_figure_mismatch joules_micro is not the figure the bracket gives
4 measurement_window_mismatch under a hardware class, the declared window is not the bracket's
4 measurement_declaration_below_bracket under a hardware class, relative_ppm is below the least the bracket supports
4 operating_point_empty_point a point is present with no member
4 operating_point_delivered_alone op_delivered is present without op_committed
4 operating_point_incomplete op_requested or op_committed is present without op_delivered
4 operating_point_empty neither op_delivered nor prediction is present
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, or the receipt that carries a figure — does not verify
6 decision_declaration_mismatch a decision does not match the declaration it names
6 decision_purpose_not_declared a decision is about a purpose its declaration does not list
6 recovery_declaration_mismatch a recovery record does not match the declaration it names
6 recovery_signer_mismatch the declaration a recovery record names (§2.6.3), or a recovery runs through (§3.2), is not signed by the publisher of a verified delivery receipt for its subject
6 recovery_value_mismatch the value does not match under the algorithm's rule
6 recovery_no_matcher the verifier has no matcher for the algorithm

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

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

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

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

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

What a match is worth. A soft-binding match is evidence at the algorithm's own error rates, which WAI does not state. A watermark can be copied onto other content, and unrelated content can share a fingerprint. A recovery says the object in hand matched the publisher's declared binding under the named algorithm — nothing about how it was derived, by whom, or with what changes.

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

3.3. Delivery reasons

check reason raised when
§3 step 1 wrong_kind kind is not the receipt's kind
§3 step 1, 4 malformed a group receipt (step 1) or object receipt (step 4) lacks a member, carries one its kind does not define, or holds one of the wrong type or encoding
§3 step 1 bad_signer_key signer_pubkey does not decode under RFC 8032 §5.1.3, or is of small order
§3 step 1, 2 group_label_invalid a group's labelled members appear without "sign_version": 2, its sign_version is not 2, or its labelled members cannot be read (step 1), or its class breaks §2.4.1 with the group alone (step 2)
§3 step 2 signature_verify_failed the group signature fails §3.1 step 3's rule over the payload of the group's form
§3 step 3 chain_break, parent_not_held as §3.1 step 5, for groups
§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

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

  • Forward every group receipt and every object receipt unaltered to every subscriber that requested receipts for the corresponding track. Byte-for-byte equality of receipt bytes between ingress and egress is REQUIRED.
  • Verify the group receipt's Ed25519 signature per §3 step 2 before the first object of the group is fanned out. A relay that cannot verify the signature MUST drop the group and emit a structured error to upstream operators. A relay that predates the labelled group payload (§2.3) cannot verify a labelled group and drops it, so a publisher labels a group only once the relays on its path verify both payloads.
  • Forward the parent_receipt_hash field as received. A relay that has previously forwarded the chain MAY additionally check the link per §3 step 3 and refuse to fan out a broken chain.
  • Preserve the byte-exact WAI envelope (WAI1 magic, manifest bytes, payload bytes) end-to-end. Any rewriting at the envelope layer invalidates §2.1 content hashes.
  • Forward claim receipts (§2.4.3) the same way — unaltered, whatever their class. A relay MUST NOT drop a claim because it does not know its class: rejecting an unknown class is a verifier's rule (§2.4.4), and a relay that filters claims decides for every downstream verifier what it may see.

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.

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.

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.7 and §5.8 — claim receipts, labelled JWP receipts and key discovery — 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.

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, or fallback or inert at a check of §7.9 with its reason; for a pinned parameter set it may give no grant, and it adds the refusal SPEC §3.1's check 5 reports. The corpus covers each rule of §7.2, the head walk with revisions that are not held, unattached, injected, forked and superseded, each move of an anchor, each step of §7.4 including the pinned-only authorities, the authorities a measurement or operating-point claim needs, a family receipt whose figures carry two classes, instants compared across offsets, fractions and a leap second, and each check of §7.9, with grants from an origin that is not one of the sink's module origins, a grant whose origin is not the one its signer_id names, a digest one module origin revokes and another grants, a status signed under an injected revision, and a pinned parameter set whose digest is revoked while its grant fails an earlier check or no grant is held.

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


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, so a chain replay requires forging every intermediate hash. The labelled payload (§2.3) signs it; the legacy payload does not, so in a legacy group the link is the publisher's unsigned statement and a relay can rewrite it.
  • group_id is monotonic per publisher key. Verifiers SHOULD track the highest group_id seen per signer_pubkey and reject any receipt with a non-increasing group_id. This binds the chain to forward progress.
  • 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. The reference verifier exposes both the monotonic group_id guard (per track) and this freshness bound as opt-in checks, disabled by default so durable re-verification of an old receipt stays a feature.

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.

6.8. Soft binding

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

7. Key discovery and signing authority

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

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

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

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

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

7.1. What a signer_id names

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

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

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

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

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

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

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

A did:web resolver reads a DID document at /.well-known/did.json on the same origin. An origin that publishes one SHOULD list in it exactly the active keys of its latest key-log revision, each as an Ed25519 publicKeyMultibase under the id did:web:<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), none twice.
grant_max_s integer The longest module grant, in seconds, the origin issues: at least 1 and at most §7.9's ceiling.
status_max_age_s integer How long, in seconds, a key status stays fresh: at least 1 and at most §7.9's ceiling.
sig 64 B hex root's signature, below.

A key entry has exactly these members:

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

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

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

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

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

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

7.3. Anchors

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

  • Pinned — the log is provisioned out of band by the relying party, as a sink holds a trust anchor (unidirectional-delivery §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; 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.

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, binding-recovery, measurement and operating-point. And energy/ followed by its energy_provenance whenever its body carries that member, whatever its class
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 class registered in §2.4.4 registers its authority here with it, and a family that needs a narrower authority than receipt registers one here. An authority a verifier does not know matches nothing. Because an energy authority is read from the energy_provenance member wherever it appears, a class that restates a figure with its acquisition class needs that class's authority without this table naming the class.

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

Expected origin. A verifier MAY expect an origin for an item: one its relying party names — the service a sink used, the publisher it subscribed to — or the subject's origin: the origin under which it has confirmed, with the authority delivery, the group receipt that carries a claim's subject (for a claim whose subject_type is object), or under which it has confirmed the receipt a claim's subject names (subject_type receipt). It reports which. §7.9 expects one of the sink's module origins.

§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.
  • Log inclusion. A confirmation says under whose authority the log's evidence was attached. It does not check log_receipt: §2.4.7 is unchanged.
  • Soft binding. A declaration confirmed with soft-binding is its origin's; confirmed under the subject's origin, it is the publisher's. §3.2 step 3 and §2.6.3 check 4 are unchanged: a recovery still needs the declaration's key to be the key of a verified delivery receipt.
  • Modules. module/decoder lets a key grant a decoder module, or a parameter set pinned by an envelope, for the capabilities the grant names; module/reconstruct lets it grant a reconstruct module (§7.9). Neither is pinned-only; §7.8 says what a module authorised under a fetched anchor is.
  • Pre-emption (unidirectional-delivery §3), relay attestation (§4.3) and reconciliation have no class and so no authority. When one is registered for pre-emption it is pinned-only, and the pinned revision itself must list the key with it (§7.4 step 6a): a fetched anchor is authority taken on first use, which that section forbids for an instruction that displaces what a person chose.

7.6. Rotation and revocation

  • A signing key is rotated by listing its successor under a new id before the successor signs, and retiring the old key with an until no earlier than the last item it signed. Items the old key signed stay confirmable. A claim chain (§2.4.3) is per key: the successor starts its own, and §3.1 step 5 is unchanged; the log, not the chain, connects the two keys. An entry's authorities never change (rule 6): an origin that gives a key another authority lists the key's successor with it.
  • A compromised key is revoked with compromised_since, and every item under it is refused from then on (§7.4 step 5): every receipt, claim and module grant it signed, whatever time the item carries. An item's own time cannot restore it, and this section defines no independent time that could.
  • A module or a pinned parameter set is revoked by adding its digest to revoked_modules (§7.9). A revocation, like a key's, stays in every later revision.
  • 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
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).

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

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

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

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

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

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

now is the sink's trusted clock, as a log date-time. The checks are a function of the module's bytes, the grant, the sink's module origins, the revisions, anchors and statuses the sink holds, and now, and read no clock and no network of their own. A sink runs them before it instantiates a module, and again before it keeps using an instance once the status or the grant it relied on is no longer current; an instance that fails is discarded.

Fail closed. Every failure — no network, no status, a stale status, a clock the sink does not trust, an expired grant — means the module is not held. A sink with no trusted clock has no now: no status is fresh, and every grant is treated as expired. A grant that lapses without renewal is not held from the moment it lapses.

Fallback, never inert. A module or pinned parameter set that is not held makes its capability unsupported for that envelope at that sink, and dispatch continues to the declared fallback or a sibling rendition (SPEC §4). A sink MUST NOT report a file inert because of module authorisation while a fallback or sibling it can decode without that module remains. An encoder emitting a capability whose decode needs a module MUST declare such a fallback. A pinned parameter set this section does not hold is refused at SPEC §3.1's check 5: prior_revoked where its digest is revoked (below), and prior_not_authorised for every other failure.

Pinned sets and several grants. For a pinned set, a sink runs the checks with each grant of form decoder it holds; a grant of form reconstruct names no set. The set is held when one grant passes every check. When none does, the sink reports module_revoked if any grant reached check 5 and failed it, or, failing that, if the set's digest is in the revoked_modules of the head the sink reaches from any anchor it holds — or, where none can be reached, of that anchor revision — with that anchor's origin. A revocation is of the digest, whichever grant names it, whichever check a grant fails first, and whether or not the sink holds a grant at all. Otherwise the sink reports the check and reason of the grant that passed the most checks; a sink that holds no such grant reports that.

A sink that holds a key log but does not hold the capability's pinned sets to this section reads only those revocation lists: it refuses a set whose digest they hold (module_revoked, and SPEC §3.1's prior_revoked), and runs none of checks 1–8 otherwise. Both apply as well to the set a sink decodes an envelope that pins none against (SPEC §4), identified by each digest a pin over it could name: it is refused when any of them is revoked, and, for a capability held to this section, unless one of them is held.

Kill latency. The longest a module whose grant is no longer renewed, or whose digest or granting key is revoked, keeps running at a sink that follows this section is

K = grant_max_s + status_max_age_s

with both values from the head the sink holds, measured on the sink's clock and bounded by the ceilings. K is a normative parameter of the origin's log: an origin states it by the two members it declares, and a sink reports it with every module it runs. K bounds what the granting origin does; a revocation in another anchored origin's head (check 5) takes effect once the sink holds that head.

Reporting. A module that runs is reported with the granting key's origin and key-id, the head's revision_hash, the anchor record's path, where the module origin came from, and K. One that does not is reported with the check and the reason — for a digest another origin revokes, that origin — and with what dispatch did instead: fell back, or left the file inert because no fallback was declared.

key_log::module_check in wai-rs implements these checks. The reference dispatcher runs SPEC §3.1's check 5 when the sink is configured with a key log (key_log::SinkKeyLog, given to pin::verify_with, to int_payload::resolve, which runs it for a pinned decode and over the files an unpinned integer decode reads, or to neural::ModelRegistry::with_key_log, which also runs it before an unpinned decode, over the bytes that decode then reads, read once, by every digest a pin over them could name): the revocation lists for every capability a pin applies to, and these checks for the capabilities a sink holds to this section. A sink configured with no key log runs neither. The reference dispatcher runs no module obtained apart from an envelope, and wai-web and the C ABI take no key log (SPEC §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