wai: the signed session claim — bound to its stage claims, its objects and its decoders #173

Merged
dcharlot merged 28 commits from wai/w5-3-session-claim into main 2026-10-06 13:51:24 -04:00
39 changed files with 18412 additions and 117 deletions

View file

@ -6,6 +6,111 @@ wire format may change and such changes are recorded here.
## v0.4.0 — current
### The session claim: what a sink reconstructed
- **The `reconstruct-session` class** ([jwp-receipts](extensions/jwp-receipts.md)
§2.4.12, the next number after `stage` and `delivery-gap`): a sink's statement of one
session, about its entry object — the decoders it used, on which host and against
which parameter sets; the path each unit took and why it switched; the presentations
it made after their deadline; a commitment to every output it presented, by the root
and leaf count of [staged-measurement](extensions/staged-measurement.md)'s output tree;
the bytes it counted and received, and the objects it could not count; the classical
rendition it did not need, by its `WAI2` table length; the `stage` claims it signed
about the stage sets it observed; and, where metered, the decode figure of one
completion with its acquisition class (`null` when unmetered, never 0). Its rules at
§3.1 step 4, and step 6 against the presentations (the tree, first-use order of the
decoder entries, paths, units, missed presentations, and re-decodes through decoders
whose replicate class makes them byte-exact), the objects received (its entry object
and every decoder entry's object among them), the envelopes received and the `stage`
claims it names. Each rule
and check has its reason in §3.1's table. Its capped lists are exact: the switches and
the missed presentations are each listed in full up to the list's cap, and counted in
full.
- **It restates no `stage` claim.** A `stage` claim states one set's layer objects, its
units at each stage on time and its delivered stage; the session names it by link hash
with the title's unit that is the set's unit 0, and commits to every output across the
title, late ones included. A sink signs one for each epoch of a set — before it forgets
the set's base, and at the end — and names them all. Step 6 binds the two both ways:
each set's units were presented; the session presented, at each stage, no fewer units
than a `stage` claim counts; no unit at a stage beyond the layers that the claims whose
sets hold it — every epoch's, and every base's over the unit — list as `applied` or
`partial` (a `late` or `refused` layer applied to no unit); and each claim's base and
arrived objects are among the objects the session received. Nothing binds a `stage`
claim to a session beyond its base and key: one an earlier session of the title signed
can be named.
- **A decoder entry names the object it decoded**, and the envelope binds it. Its
`object` is the content hash of the `WAI2` envelope whose rendition it decoded, or of
the `WAI1` envelope; it is among the objects received (`session_source_not_received`).
The envelopes a verifier holds — every `WAI2` envelope received, and any `WAI1` one —
are among the objects received, read as plaintext envelopes
(`session_envelope_unreadable`), and an entry whose object is one of them names what it
gives: a rendition and that rendition's capability, or the `WAI1` manifest's capability
or fallback (`session_source_mismatch`). A signer cannot relabel an entry whose envelope
the verifier holds out of the re-decode check, by any route; which object an output came
from remains its statement. An entry decodes one object, so a title's output-tree
entries are (capability, pin, object) as well (staged-measurement Appendix C), and a
session over a title still commits to the title's tree.
- **The classical rendition.** An envelope's classical rendition is the first selectable
one whose capability the codec glossary links from an entry of era `classical` and
whose media is the envelope's; WAI's own formats, neural codecs, records, parameter sets
and text never qualify.
- **Authorities** (§7.5): a session needs `reconstruct-session`, not pinned-only. Its
energy figure is a second item for confirmation, needing `reconstruct-session` and the
energy authority of its class; a verifier reports the two side by side, so an origin
can stand behind a session and not behind its figure. A session's subject's origin is
not expected for it: a relying party names the origin it expects, if any.
- **Figure profiles** ([energy-measurement](extensions/energy-measurement.md) §6.1): the
`moq-jwp:claim-link` row widens to a session whose `energy` is not `null`, so a
`measurement` claim publishes the bracket behind a session's figure.
- **Delivery telemetry**'s `claim` member names a registered class now
([delivery-telemetry](extensions/delivery-telemetry.md) §2.1).
- **staged-measurement §8.6–§8.7**: a tool probes a power reader's rail at rest before it
brackets a decode — absent, stale or live by energy-measurement §3.1's rule — and reads
an energy counter as power by its successive differences across its wrap, ending the
samples at an interval that cannot be a reading — one implying more power than a
processor rail draws, one longer than ten sampling intervals, or a difference of more
than half the counter's range (a counter reset); a tool that signs a session claim
seals a figure only where its verdict is clean, beside the `measurement` claim on it,
its burst run on a power of two of workers, which the session's host states as its
`threads`, so the claim names no more of the machine than the session
(jwp-receipts §6.7).
- **Reference implementation.** `claim_receipt::session` (the class),
`session_recorder` (a sink's tally, each decoder named with the object it decodes; it
verifies each `stage` claim as it records it — one for each epoch of a set — requires
it signed by the key that finishes the session, and checks the sealed session against
what it recorded and against those claims before it is returned),
`key_log::Item::figure_of_claim`, `energy_report::rail_liveness`,
`energy_burst::counter_power_samples` and `rapl_power_reader`,
`energy_burst::{measure_on, power_of_two_workers}`, `stage_measure::run::completion`,
`stage_measure::decode::{steps, prepare}` and `stage_measure::session`, and
`wai_stage_measure bench`, which times each case of a title — one base's pinned decode
or one layer's offer, its checks and hashing after the clock — over rotating rounds,
compares every run's outputs with the evaluation's and aborts on a difference, records
each run and the operator's statement of whether the machine was idle, meters the
decode where a live rail allows, and with `--sign` seals the title's session and the
report on its figure. A lib test replays every `staged-conformance/` case as a sink and
requires the session it records, naming each epoch's `stage` claim, to seal. The
output tree builds under `receipt_claims`; the switch reasons live in the ungated
`delivery_terms`, which `telemetry` re-exports, so `receipt_claims` does not take
`telemetry`.
- **Corpora.** `claim-conformance/` gains the session vectors (§5.11) — every reason the
class adds, output trees of 1 to 33 leaves, every context, the classical rule across
the registry's kinds of capability, the caps at their edges, a base forgotten and
decoded again (`staged-conformance/`'s `record_budget`, its envelopes and each epoch's
`stage` claim) and every route by which an entry could be relabelled — and
`signer-conformance/` the session's authority and its figure's; `claim_verify.py`,
`signer_verify.py` and `differential.py` read the class. `claim_verify.py` reads
envelopes with `staged-conformance`'s strict readers, and `differential.py` compares the
two implementations across both stage bounds and many epochs, the caps, malformed and
encrypted `WAI1` and `WAI2` envelopes, entries naming their objects or not, and every
registered capability as a classical candidate. Every vector already in both corpora is
unchanged, and the claim corpus pins the vectors before the class by a digest computed
before it was added. `staged-measure-conformance/goldens.json` restates each title's
decoder entries with the envelope each decoded, and so its output root.
- **Wire.** No existing receipt, claim, pin or corpus vector changes bytes; a verifier of
an earlier revision refuses the new class as `unknown_class`. A title's output root
under staged-measurement Appendix C changes, its entries now naming their envelopes.
### Stage receipts: chains per key and track, objects verified by inclusion, `stage` and `delivery-gap` claims
- **One receipt chain per publisher key and track** ([jwp-receipts](extensions/jwp-receipts.md)

View file

@ -649,8 +649,8 @@ Merkle roots, Ed25519 group signatures, and cross-hop preservation; an
object's figure, acquisition class and origin bound into its Merkle leaf,
and a group total's class signed under a labelled group payload; the
typed receipt classes beside delivery (`energy`, `lawful-basis`,
`log-inclusion`, `measurement`, `operating-point`, `stage`, `delivery-gap`)
carried as `claim` receipts; one receipt chain per publisher key and track, and
`log-inclusion`, `measurement`, `operating-point`, `stage`, `delivery-gap`,
`reconstruct-session`) carried as `claim` receipts; one receipt chain per publisher key and track, and
an object not delivered accounted for by its receipt, verified by inclusion; and a second, weaker binding class (a registry-named soft
binding with a resolution endpoint) whose recovered receipts attest the
ancestor, never the object in hand.

View file

@ -1,10 +1,10 @@
# claim-conformance
Conformance for [typed receipt classes and the soft binding class](../extensions/jwp-receipts.md)
(§2.4.3–§2.4.11, §2.6, §3.1–§3.2): `claim` receipts of class `energy`,
(§2.4.3–§2.4.12, §2.6, §3.1–§3.2): `claim` receipts of class `energy`,
`lawful-basis`, `log-inclusion`, `soft-binding`, `binding-recovery`, `measurement`,
`operating-point`, `stage` and `delivery-gap`, and recovery through a soft binding under
the ancestor rule.
`operating-point`, `stage`, `delivery-gap` and `reconstruct-session`, and recovery
through a soft binding under the ancestor rule.
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
@ -30,7 +30,17 @@ a vector that gives a recovery record one without the other is malformed), and
corpus's matcher is byte equality), and `figure` (the receipt that carries the figure a
`measurement` or `operating-point` claim names), and `gap_object` (the object receipt at
a `delivery-gap` claim's coordinates, as the verifier holds it once verified, by
inclusion at least: `{track_alias, group_id, object_id, content_hash}`). An `object` context may also carry the
inclusion at least: `{track_alias, group_id, object_id, content_hash}`), and, for a
`reconstruct-session` claim, `outputs` (its presentations, `{unit, layers, decoder,
out_digest}`), `redecoded` (what a verifier that decoded presentations again found,
`{index, out_digest}`, the index into `outputs`), `objects` (each object received,
`{content_hash, path, counted, transferred, presented}`, `counted` `null` for an object
not countable), `envelopes` (each `WAI2` envelope the session received and any `WAI1`
envelope the verifier holds, `{envelope}`, its bytes in hex; a decoder entry naming one
as its `object` is checked against it, and the classical rendition is read from each
`WAI2` envelope by §2.4.12's rule) and
`stage_claims` (the wire forms of the `stage`
claims it names). An `object` context may also carry the
class its leaf is labelled with (jwp-receipts §2.2), in energy-measurement §2.7's JSON
members, read as that section reads them. A `figure` context is one of two forms:
`claim`, an `energy` claim's wire form, viewed under the `moq-jwp:claim-link` profile;
@ -44,7 +54,8 @@ object in hand's `observed_hash` (or `null`). Both procedures report
defect, in a recovery performed and in one recorded.
Everything is synthetic. Keys are Ed25519 from fixed seeds (0x11 publisher, 0x22 meter,
0x33 operator, 0x44 sink, 0x55 resolver, 0x66 relay) and attest nothing; figures, uncertainties and
0x33 operator, 0x44 sink, 0x55 resolver, 0x66 relay) and attest nothing; outputs,
parameter sets and envelope payloads are synthetic bytes; figures, uncertainties and
calibration references are invented; algorithm identifiers, registries, endpoints, logs
and calibration certificates use reserved example domains; vocabulary terms are
illustrative and no verifier here checks them against a vocabulary.
@ -165,6 +176,80 @@ The vectors cover:
given to a claim that is not a gap (`context_mismatch`), for a reason outside its
enumeration, a date that does not exist, or a subject that is a receipt.
## What a sink reconstructed: `reconstruct-session`
A `reconstruct-session` claim (§2.4.12) is signed by a sink (key 0x44) about the entry
object of one session. Most vectors edit one model session: six units. Four are a stage
set over a pinned base — the session's entry object, a `WAI1` envelope — refined to up to
three layers, with two late layer objects never presented; each decoder entry names the
object it decoded, so each layer applied has an entry of its own. One unit is decoded from
the first rendition of a `WAI2` envelope, built by `wai-rs`'s container module, whose
classical rendition the sink did not need. The sixth is decoded on the fallback path after
a switch and presented late.
The vectors cover:
- **Accepted:** the model unmetered and metered under `OnChipCounter`, each against every
context: its presentations, re-decodes of byte-exact presentations that match, the
objects received, its base's `WAI1` envelope and the `WAI2` envelope, and the `stage`
claim it names; the model with no
context, and with its `stage` claim but neither presentations nor objects; a session
with no classical length beside an envelope whose second rendition is WAI's own; a
classical rendition of two other video capabilities; a base forgotten under the record
budget and decoded again with no layer sent again (`staged-conformance/`'s
`record_budget`, its envelopes and the `stage` claim of each epoch as the sink's
observer states them), naming both epochs' claims and only the first's; output trees
of 1 to 33 leaves, half through a
decoder whose replicate class makes it byte-exact and half through one checked for
membership only, each re-decoded (the second with another digest, which is not
checked); the caps of switches and missed presentations, written in full and up to the
cap; and a `measurement` claim on a metered session's figure, under
`moq-jwp:claim-link`, by the session's key.
- **Rejected at step 4,** each rule with its reason: units, leaves and paths that do not
add up, a root without leaves or leaves without a root, three workers, a decoder
identifier of the wrong grammar, a tier without a pin or one SPEC §3.1 does not define,
module statuses that do not fit, a rendition past 65535, an entry twice or with no
leaves, a refinement entry pinned above its class's tier, switches between one path and
itself, for an unlisted reason, out of order, or written in another number than their
total (or 1024), missed presentations with no deadline, above the leaves, written in
another number than `missed` (or 256), zero microseconds late or out of order, a stage
set listed twice, more bytes discarded than counted, a classical length without its
source or with an unregistered one, and a figure of zero or of every acquisition-class
defect.
- **Rejected at steps 1–3:** a subject that is a receipt; a missing and an unknown member;
`media`, `framing`, a host profile and a decoder path outside their grammar or
enumeration; a count written as a string; the host read before the decoders; an energy
object missing a member; and a figure written into an unmetered session after signing.
- **Rejected at step 6:** a presentation whose output differs, a list one short, a
(unit, layers) twice (`session_output_mismatch`); leaves moved between entries, a
presentation naming an entry that does not exist, entries out of first-use order, paths
and units that the presentations do not give, a missed presentation never made
(`session_output_inconsistent`); a byte-exact re-decode that differs
(`session_output_irreproducible`), and a byte-exact entry relabelled to a class checked
for membership only by each route — the base's `WAI1` entry, a `WAI2` entry with its
rendition kept or nulled, a capability another received envelope has — refused against
the envelope of the object it names, as is an entry naming a rendition whose envelope is
not held (`session_source_mismatch`); an entry object never received
(`session_entry_not_received`); an entry's object, or an envelope held, the session did
not receive (`session_source_not_received`); objects whose counted, uncounted or
discarded bytes differ (`session_counted_mismatch`); an encrypted envelope, a `WAI1`
envelope with no manifest members and bytes of neither magic
(`session_envelope_unreadable`); a second rendition of a capability the session decoded,
of another media, WAI's own, a companion, a record, a parameter set, a neural codec, an
unregistered string, or a text capability in a text envelope — none a classical
rendition the session did not need — and a length one byte off
(`session_classical_mismatch`); a `stage`
claim whose signature is broken (`context_invalid`), signed by another key, or not named
(`context_mismatch`); a `stage` claim counting more units at stage 2 than the session
presented there, a set placed so that one of its units was never presented, units
presented at stages no applied layer reaches — among them units presented through a
first and a second layer the claim lists as late and a first layer it lists as
refused — a `stage` claim about a base the session never received, objects received
that hold none of the layers it says arrived, and the forgotten base's session naming
only its last epoch's claim, which lists no layer, or presenting a unit past both
epochs' layers (`session_stage_disagrees`); a session context given to a `stage` claim;
and a
`measurement` claim on an unmetered session, which carries no figure.
## Steps 1 and 2: types, and the order a body is read in
§3.1 step 2 checks exactly the type a member table gives, member by member in the
@ -275,7 +360,28 @@ that do not themselves verify, bodies of every class with one defect from a fixe
and with every pair of those at two different positions (a member, or a member of a
nested object), where the order §3.1 step 2 reads a body in decides the reason, and, with
`--random N`, a seeded random sample of dates, endpoints, figures, sums and class
combinations. Separately, each value that makes a whole file unreadable under
combinations, and of sessions: each built from a random presentation list as an honest
sink states it, then given a defect or two in the body or in a context (units, leaves and
paths off by one, switches and missed presentations past their caps, a `null` deadline
with misses, presences that disagree, decoder indices out of range, leaves moved between
entries, entries out of first-use order, outputs dropped or changed, re-decodes that match
and that do not, objects counted otherwise, every acquisition class), checked against its
presentations, objects, WAI2 envelopes and a `stage` claim. Further session families:
sessions that pass steps 1–5 against a `stage` claim whose first layer applied to some
units and arrived late for others, presented at one to three layers, with objects that
hold or lack the set's base and layers (both bounds of §2.4.12's check); sessions naming
one to three `stage` claims of one set, as a sink that forgot its base names one per
epoch, each listing first and second layers of every status, sometimes one named a unit
further on, presented within and past what the claims holding each unit applied; the missed
presentations and switches written at, around and past their caps; WAI2 envelopes whose
manifests readers may read otherwise (members of the wrong type, pins that are not pins,
`security` members, duplicate members, numbers and nesting past every range, byte-order
marks, lone surrogates) and whose framing is short or long, and the base's WAI1 envelope
likewise, with a capability and a fallback the base's entry does or does not name — each
with the session signed again so the entry names that envelope as its object; random
sessions whose entries name their envelopes, a layer of their own or another object, with
envelopes and objects that hold them or not; and every registered capability as an
envelope's second rendition, in the session's media and in its own. Separately, each value that makes a whole file unreadable under
oer-receipts §4 — a number beyond the double range, `NaN`, an unpaired surrogate escape,
a repeated member name, nesting past 64 levels — goes in a file of its own beside
readable twins, and the two verifiers must both refuse the file or give it the same

View file

@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""Second verifier for claim-conformance/ — typed claim receipts, the soft binding class,
and recovery under the ancestor rule (extensions/jwp-receipts.md §2.4.3-§2.4.11, §2.6,
and recovery under the ancestor rule (extensions/jwp-receipts.md §2.4.3-§2.4.12, §2.6,
§3.1-§3.2), and the arithmetic of a measurement report (energy-measurement.md §3.1, §6.2),
computed with Python's integers.
@ -30,12 +30,14 @@ has no verdicts, and `verdicts` exits 1 with no output. No integer in a claim is
from more than twenty digits, so no verdict depends on an interpreter's limit on integer
digits.
"""
import json, os, re, sys, unicodedata
import hashlib, json, os, re, struct, sys, unicodedata
from decimal import localcontext
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "oer-conformance"))
import oer2_verify as OER # noqa: E402 (the OER reference verifier: grades + receipt checks)
sys.path.insert(0, os.path.join(HERE, "..", "staged-conformance"))
import staged_verify as SV # noqa: E402 (the strict JSON and manifest reader http_verify.py uses too)
sys.path.insert(0, HERE)
from cryptography.exceptions import InvalidSignature # noqa: E402
@ -218,7 +220,7 @@ ENVELOPE = ["kind", "class", "subject_type", "subject", "seq", "issued_at", "bod
ADMITS = {"energy": {"object"}, "lawful-basis": {"object", "receipt"}, "log-inclusion": {"receipt"},
"soft-binding": {"object"}, "binding-recovery": {"object"},
"measurement": {"object", "receipt"}, "operating-point": {"object", "receipt"},
"stage": {"object"}, "delivery-gap": {"object"}}
"stage": {"object"}, "delivery-gap": {"object"}, "reconstruct-session": {"object"}}
FIGURE = ["figure_profile", "figure_index", "group", "joules_micro", "energy_provenance", "energy_uncertainty",
"energy_counter_filtering", "energy_calibration_ref"]
BODY = {
@ -232,6 +234,11 @@ BODY = {
"stage": ["vocabulary", "op_requested", "op_committed", "op_delivered", "presented", "units_by_stage", "tier",
"stage_hash", "objects", "conflicts", "deadline_misses", "bytes_envelopes"],
"delivery-gap": ["track_alias", "group_id", "object_id", "reason", "observed_at"],
"reconstruct-session": ["session_nonce", "media", "host", "decoders", "units", "leaf_count", "output_root",
"primary_units", "fallback_units", "switches", "switches_total", "deadline_us", "missed",
"missed_units", "stage_sets", "framing", "counted_primary", "counted_fallback",
"counted_discarded", "uncounted", "transferred", "receipt_bytes", "classical_declared",
"classical_source", "energy"],
}
# §2.4.10: one reported layer object, its units, the enumerations this extension defines
# for it, the tiers of staged-delivery §5, and the reasons of staged-delivery §4.2.
@ -280,6 +287,36 @@ WHITE_SPACE = ("\t\n\x0b\x0c\r \x85\xa0\u1680\u2000\u2001\u2002\u2003\u2004\u200
COVERAGE = {"Whole", "Partial"}
GRADES = OER.GRADES
# §2.4.12: a session's host, decoder entries, switches, missed presentations, stage sets
# and energy, the enumerations it defines, and delivery-telemetry.md §5's switch reasons.
SESSION_HOST = ["profile", "target", "build", "threads"]
SESSION_DECODER = ["capability", "rendition", "object", "path", "decoder", "variant", "pin", "tier", "module_status", "leaves"]
SESSION_SWITCH = ["unit", "from", "to", "reason"]
SESSION_MISSED = ["unit", "layers", "late_us"]
SESSION_STAGE_SET = ["first_unit", "claim"]
SESSION_ENERGY = ["joules_micro", "energy_provenance", "energy_uncertainty", "energy_counter_filtering",
"energy_calibration_ref"]
HOST_PROFILES = ("native-ffi", "browser-baseline", "browser-gpu", "edge-wasi")
PATHS = ("primary", "fallback")
FRAMINGS = ("registered",)
CLASSICAL_SOURCES = ("wai2-table",)
MODULE_STATUSES = ("pinned", "fetched")
TIER_RANK = {"none": 0, "entropy-consistency": 1, "decode-equivalence": 2} # SPEC §3.1
SWITCH_REASONS = ("pin-malformed", "form-not-registered", "form-unreadable", "form-required", "prior-revoked",
"prior-not-authorised", "prior-not-held", "digest-superseded", "set-malformed", "part-mismatch",
"set-incomplete", "missing-capability", "module-unverified", "module-revoked", "trap",
"fuel-exhausted", "memory-exhausted", "deadline-threshold", "unit-hash-mismatch",
"budget-exhausted", "recovered")
# The capability registry (../registry/capabilities.json): each capability's kind, status
# and replicate class, for a session's tiers, re-decodes and classical rendition.
with open(os.path.join(HERE, "..", "registry", "capabilities.json"), encoding="utf-8") as _f:
REGISTRY = {c["capability"]: c for c in json.load(_f)["capabilities"]}
# The codec glossary (../glossary/codecs.json): the capabilities an entry of era `classical`
# links, from which a session's classical rendition is read (§2.4.12).
with open(os.path.join(HERE, "..", "glossary", "codecs.json"), encoding="utf-8") as _f:
CLASSICAL = {link["capability"] for e in json.load(_f)["entries"] if e["era"] == "classical"
for link in (e.get("wai") or [])}
class Reject(Exception):
def __init__(self, step, reason):
@ -398,6 +435,9 @@ def check_body(cls, b):
raise Reject(2, "field_type")
return
members(b, BODY[cls], 2)
if cls == "reconstruct-session":
check_session_body(b)
return
if cls == "stage":
# In the member table's order: `vocabulary` before the objects, whose members are
# checked where the table reaches them.
@ -498,6 +538,443 @@ def check_body(cls, b):
need(is_token(b["alg"]) and is_uri(b["resolution_endpoint"]), 2)
def nested(v, required):
"""A nested object a table fixes: an object, then its members, missing then unknown."""
need(isinstance(v, dict), 2)
members(v, required, 2)
return v
def check_session_body(b):
"""§2.4.12, at §3.1 step 2: each member's type and encoding in the table's order, the
members of the host, of each decoder entry, switch, missed presentation and stage set,
and of `energy`, where the table reaches them."""
need(is_hex(b["session_nonce"], 32), 2)
need(isinstance(b["media"], str) and re.fullmatch(r"[a-z0-9-]{1,32}", b["media"]) is not None, 2)
h = nested(b["host"], SESSION_HOST)
need(h["profile"] in HOST_PROFILES, 2)
need(h["target"] is None or is_token(h["target"]), 2)
bd = nested(h["build"], BUILD)
need(bd["profile"] in ("release", "debug"), 2)
need(bd["toolchain"] is None or isinstance(bd["toolchain"], str), 2)
need(bd["target"] is None or is_token(bd["target"]), 2)
need(is_int(h["threads"]), 2)
need(isinstance(b["decoders"], list), 2)
for d in b["decoders"]:
nested(d, SESSION_DECODER)
need(is_token(d["capability"]), 2)
need(d["rendition"] is None or is_int(d["rendition"]), 2)
need(is_hex(d["object"], 32), 2)
need(d["path"] in PATHS, 2)
need(isinstance(d["decoder"], str), 2)
need(d["variant"] is None or is_token(d["variant"]), 2)
need(d["pin"] is None or is_hex(d["pin"], 32), 2)
need(d["tier"] is None or isinstance(d["tier"], str), 2)
need(d["module_status"] is None or isinstance(d["module_status"], str), 2)
need(is_int(d["leaves"]), 2)
need(all(is_int(b[k]) for k in ("units", "leaf_count")), 2)
need(b["output_root"] is None or is_hex(b["output_root"], 32), 2)
need(all(is_int(b[k]) for k in ("primary_units", "fallback_units")), 2)
need(isinstance(b["switches"], list), 2)
for w in b["switches"]:
nested(w, SESSION_SWITCH)
need(is_int(w["unit"]) and w["from"] in PATHS and w["to"] in PATHS and isinstance(w["reason"], str), 2)
need(is_int(b["switches_total"]), 2)
need(b["deadline_us"] is None or is_int(b["deadline_us"]), 2)
need(is_int(b["missed"]), 2)
need(isinstance(b["missed_units"], list), 2)
for x in b["missed_units"]:
nested(x, SESSION_MISSED)
need(all(is_int(x[k]) for k in SESSION_MISSED), 2)
need(isinstance(b["stage_sets"], list), 2)
for x in b["stage_sets"]:
nested(x, SESSION_STAGE_SET)
need(is_int(x["first_unit"]) and is_hex(x["claim"], 32), 2)
need(b["framing"] in FRAMINGS, 2)
need(all(is_int(b[k]) for k in ("counted_primary", "counted_fallback", "counted_discarded", "uncounted",
"transferred", "receipt_bytes")), 2)
need(b["classical_declared"] is None or is_int(b["classical_declared"]), 2)
need(b["classical_source"] is None or is_token(b["classical_source"]), 2)
e = b["energy"]
if e is not None:
nested(e, SESSION_ENERGY)
need(is_int(e["joules_micro"]), 2)
need(isinstance(e["energy_provenance"], str), 2)
u = e["energy_uncertainty"]
if u is not None:
nested(u, UNCERTAINTY)
need(is_int(u["relative_ppm"]) and is_int(u["window_us"]), 2)
need(e["energy_counter_filtering"] is None or isinstance(e["energy_counter_filtering"], str), 2)
need(e["energy_calibration_ref"] is None or isinstance(e["energy_calibration_ref"], str), 2)
def decoder_ok(s):
"""A decoder identifier of §2.4.12: builtin:<crate>@<version> or module:sha256:<hex>."""
if s.startswith("module:sha256:"):
return re.fullmatch(r"[0-9a-f]{64}", s[len("module:sha256:"):]) is not None
if not s.startswith("builtin:") or "@" not in s:
return False
krate, version = s[len("builtin:"):].split("@", 1)
return (re.fullmatch(r"[a-z0-9_-]{1,64}", krate) is not None
and re.fullmatch(r"[0-9][0-9A-Za-z.+-]{0,63}", version) is not None)
def replicate(cap):
"""A capability's replicate class as the registry states it, None for one it does not
register or classify."""
c = REGISTRY.get(cap)
return c["replicate"] if c else None
def class_tier(cap):
"""SPEC §3.1 *Tier*: the tier a capability's class gives — decode-equivalence for an
integer-exact neural decode, entropy-consistency for an entropy-exact one, none for
every other class — or None for a capability with no class."""
r = replicate(cap)
if r is None:
return None
return {"neural-int-exact": "decode-equivalence", "neural-entropy-exact": "entropy-consistency"}.get(r, "none")
def selectable(entry):
"""SPEC §7.1: not a companion or a refinement, and a capability a sink may dispatch —
one the registry does not list, or a payload that is not a candidate."""
if entry.get("role") in ("companion", "refinement"):
return False
c = REGISTRY.get(entry["capability"])
return c is None or (c["kind"] == "payload" and c["status"] != "candidate")
def session_rules(b):
"""§2.4.12's rules, at §3.1 step 4, in order."""
ds = b["decoders"]
need(b["primary_units"] + b["fallback_units"] == b["units"], 4, "session_path_sum")
need(sum(d["leaves"] for d in ds) == b["leaf_count"] and b["leaf_count"] >= b["units"]
and (b["units"] == 0) == (b["leaf_count"] == 0), 4, "session_leaf_sum")
need((b["output_root"] is not None) == (b["leaf_count"] > 0), 4, "session_root_presence")
t = b["host"]["threads"]
need(t > 0 and t & (t - 1) == 0, 4, "session_host_malformed")
for d in ds:
builtin = d["decoder"].startswith("builtin:")
tier_ok = (d["pin"] is None and d["tier"] is None) or (
d["pin"] is not None and d["tier"] is not None and d["tier"] in TIER_RANK)
status_ok = builtin if d["module_status"] is None else (not builtin and d["module_status"] in MODULE_STATUSES)
need(decoder_ok(d["decoder"]) and (d["rendition"] is None or d["rendition"] <= 65535) and tier_ok
and status_ok and d["leaves"] != 0, 4, "session_decoder_malformed")
keys = [tuple(d[k] for k in SESSION_DECODER if k != "leaves") for d in ds]
need(len(set(keys)) == len(keys), 4, "session_decoder_malformed")
for d in ds:
ct = class_tier(d["capability"])
need(d["tier"] is None or ct is None or TIER_RANK[d["tier"]] <= TIER_RANK[ct], 4, "session_tier_exceeds_class")
sw = b["switches"]
need(len(sw) == min(b["switches_total"], 1024)
and all(w["from"] != w["to"] and w["reason"] in SWITCH_REASONS for w in sw)
and all(sw[i]["unit"] <= sw[i + 1]["unit"] for i in range(len(sw) - 1)), 4, "session_switch_invalid")
mu = b["missed_units"]
need(b["missed"] <= b["leaf_count"] and (b["deadline_us"] is not None or b["missed"] == 0)
and len(mu) == min(b["missed"], 256)
and all(1 <= x["layers"] <= 65535 and x["late_us"] >= 1 for x in mu)
and all((mu[i]["unit"], mu[i]["layers"]) < (mu[i + 1]["unit"], mu[i + 1]["layers"]) for i in range(len(mu) - 1)),
4, "session_missed_invalid")
ss = b["stage_sets"]
need(all((ss[i]["first_unit"], ss[i]["claim"]) < (ss[i + 1]["first_unit"], ss[i + 1]["claim"])
for i in range(len(ss) - 1)), 4, "session_stage_sets_invalid")
need(b["counted_discarded"] <= b["counted_primary"] + b["counted_fallback"], 4, "session_counted_invalid")
cd, cs = b["classical_declared"], b["classical_source"]
need((cd is None and cs is None) or (cd is not None and cs in CLASSICAL_SOURCES), 4, "session_classical_invalid")
e = b["energy"]
if e is not None:
need(e["joules_micro"] != 0, 4, "energy_zero_figure")
acquisition(e)
# ------------------------------------------- §2.4.12 step 6: what a session is checked against
def out_leaf(p):
"""staged-measurement Appendix C: a presentation's leaf."""
return hashlib.sha256(b"wai:out-leaf\x01" + p["unit"].to_bytes(8, "big") + p["layers"].to_bytes(2, "big")
+ p["decoder"].to_bytes(2, "big") + bytes.fromhex(p["out_digest"])).digest()
def out_mth(leaves):
"""RFC 6962's Merkle tree hash, under the output tree's node domain."""
if len(leaves) == 1:
return leaves[0]
k = 1
while k * 2 < len(leaves):
k *= 2
return hashlib.sha256(b"wai:out-node\x01" + out_mth(leaves[:k]) + out_mth(leaves[k:])).digest()
def ctx_int(v, bits=64):
if not (isinstance(v, int) and not isinstance(v, bool) and 0 <= v < 2 ** bits):
raise ValueError("bad integer in a session context")
return v
def ctx_hash(v):
if not is_hex(v, 32):
raise ValueError("bad hash in a session context")
return v
def presentations(v):
if not isinstance(v, list):
raise ValueError("outputs must be an array")
return [{"unit": ctx_int(p["unit"]), "layers": ctx_int(p["layers"], 16), "decoder": ctx_int(p["decoder"], 16),
"out_digest": ctx_hash(p["out_digest"])} for p in v]
def finals(ps):
"""Each unit presented, with its presentation at its largest layer count."""
out = {}
for p in ps:
if p["unit"] not in out or p["layers"] > out[p["unit"]]["layers"]:
out[p["unit"]] = p
return out
def check_session_outputs(c, ps, redecoded):
b = c["body"]
need(c["class"] == "reconstruct-session", 6, "context_mismatch")
keys = [(p["unit"], p["layers"]) for p in ps]
need(len(ps) == b["leaf_count"] and len(set(keys)) == len(keys), 6, "session_output_mismatch")
if ps:
ordered = sorted(ps, key=lambda p: (p["unit"], p["layers"]))
need(out_mth([out_leaf(p) for p in ordered]).hex() == b["output_root"], 6, "session_output_mismatch")
else:
need(b["output_root"] is None, 6, "session_output_mismatch")
ds = b["decoders"]
bad = "session_output_inconsistent"
need(all(p["decoder"] < len(ds) and p["layers"] >= 1 for p in ps), 6, bad)
fin = finals(ps)
need(len(fin) == b["units"], 6, bad)
need(all(d["leaves"] == sum(1 for p in ps if p["decoder"] == i) for i, d in enumerate(ds)), 6, bad)
nxt = 0
for p in sorted(ps, key=lambda p: (p["layers"], p["unit"])):
need(p["decoder"] <= nxt, 6, bad)
if p["decoder"] == nxt:
nxt += 1
primary = sum(1 for p in fin.values() if ds[p["decoder"]]["path"] == "primary")
need(primary == b["primary_units"] and len(fin) - primary == b["fallback_units"], 6, bad)
need(all((x["unit"], x["layers"]) in set(keys) for x in b["missed_units"]), 6, bad)
for i, p in enumerate(ps):
if replicate(ds[p["decoder"]]["capability"]) in ("neural-int-exact", "standard-defined") and i in redecoded:
need(redecoded[i] == p["out_digest"], 6, "session_output_irreproducible")
def objects_seen(v):
if not isinstance(v, list):
raise ValueError("objects must be an array")
out = []
for o in v:
ctx_hash(o["content_hash"])
if o["path"] not in PATHS or not isinstance(o["presented"], bool):
raise ValueError("bad object")
out.append({"content_hash": o["content_hash"], "path": o["path"],
"counted": None if o["counted"] is None else ctx_int(o["counted"]),
"transferred": ctx_int(o["transferred"]), "presented": o["presented"]})
return out
def check_session_objects(c, objs):
b = c["body"]
need(c["class"] == "reconstruct-session", 6, "context_mismatch")
need(any(o["content_hash"] == c["subject"] for o in objs), 6, "session_entry_not_received")
received = {o["content_hash"] for o in objs}
need(all(d["object"] in received for d in b["decoders"]), 6, "session_source_not_received")
sums = {"primary": 0, "fallback": 0}
discarded = uncounted = transferred = 0
for o in objs:
n = o["counted"]
transferred += o["transferred"]
if n is None:
uncounted += 1
else:
sums[o["path"]] += n
if not o["presented"]:
discarded += n
need((sums["primary"], sums["fallback"], discarded, uncounted, transferred)
== (b["counted_primary"], b["counted_fallback"], b["counted_discarded"], b["uncounted"], b["transferred"]),
6, "session_counted_mismatch")
def wai2_read(data):
"""SPEC §7.1: a WAI2 envelope's manifest and each rendition's payload length, or None
for bytes that are not one. The manifest is read as staged_verify.py and
http_verify.py read one: strict JSON, then the members SPEC §7.1 requires with their
types."""
if len(data) < 8 or data[:4] != b"WAI2":
return None
ml = struct.unpack_from("<I", data, 4)[0]
if 8 + ml > len(data):
return None
try:
m = SV.strict_json(data[8:8 + ml])
SV.check_manifest_v2(m)
except (SV.NotJSON, SV.Schema):
return None
rs = m["renditions"]
o = 8 + ml
if o + 2 > len(data) or struct.unpack_from("<H", data, o)[0] != len(rs):
return None
o += 2
if o + 8 * len(rs) > len(data):
return None
lengths, expected = [], 0
for i in range(len(rs)):
off, ln = struct.unpack_from("<II", data, o + 8 * i)
if off != expected:
return None
lengths.append(ln)
expected += ln
if o + 8 * len(rs) + expected > len(data):
return None
return m, lengths
def plaintext_v2(m):
"""SPEC §6.1, §7.1: a WAI2 manifest's payloads are plaintext when its `security` is
absent or null, or a security object whose `encryption` is absent, null or "none". A
`security` that is not a security object (not an object, an `encryption` that is not a
string, a `kid` that is not an integer from 0 to 2^64 - 1) is not plaintext."""
sec = m.get("security")
if sec is None:
return True
if not isinstance(sec, dict):
return False
enc, kid = sec.get("encryption"), sec.get("kid")
if enc is not None and not isinstance(enc, str):
return False
if kid is not None and not (isinstance(kid, int) and not isinstance(kid, bool) and 0 <= kid < 2 ** 64):
return False
return enc is None or enc == "none"
def classical_rendition(m):
"""§2.4.12: the first rendition, in manifest order, that is selectable and of a
classical capability whose registry media is the manifest's `media` and is not text."""
for i, e in enumerate(m["renditions"]):
c = REGISTRY.get(e["capability"])
if (selectable(e) and e["capability"] in CLASSICAL and c is not None
and c["media"] == m["media"] and c["media"] != "text"):
return i
return None
def envelopes(v):
if not isinstance(v, list):
raise ValueError("envelopes must be an array")
out = []
for e in v:
if not is_hex(e["envelope"]):
raise ValueError("bad envelope")
out.append(bytes.fromhex(e["envelope"]))
return out
def check_session_envelopes(c, envs, objs):
"""§2.4.12 against the envelopes the session received: every WAI2 one, and any WAI1
one the verifier holds. Each among the objects received, where those are held; each a
WAI1 or WAI2 envelope whose payloads are plaintext; each decoder entry whose object is
one of them naming what it gives, and each naming a rendition having its object among
them; then the classical length."""
b = c["body"]
need(c["class"] == "reconstruct-session", 6, "context_mismatch")
if objs is not None:
received = {o["content_hash"] for o in objs}
need(all(SV.content_hash(e) in received for e in envs), 6, "session_source_not_received")
read = []
for data in envs:
if data[:4] == b"WAI2":
r = wai2_read(data)
need(r is not None and plaintext_v2(r[0]), 6, "session_envelope_unreadable")
read.append((SV.content_hash(data), "multi", r))
elif data[:4] == b"WAI1":
try:
m, _, _ = SV.read_wai1(data)
except SV.NotWai1:
raise Reject(6, "session_envelope_unreadable")
need(SV.plaintext(m), 6, "session_envelope_unreadable")
read.append((SV.content_hash(data), "one", m))
else:
raise Reject(6, "session_envelope_unreadable")
by_hash = {}
for h, kind, r in read:
by_hash.setdefault(h, (kind, r))
for d in b["decoders"]:
kind, r = by_hash.get(d["object"], (None, None))
if kind == "multi":
rs = r[0]["renditions"]
gives = d["rendition"] is not None and d["rendition"] < len(rs) and rs[d["rendition"]]["capability"] == d["capability"]
elif kind == "one":
mr = r["model_requirement"]
gives = d["rendition"] is None and d["capability"] in (mr["capability"], mr.get("fallback"))
else:
gives = d["rendition"] is None
need(gives, 6, "session_source_mismatch")
decoded = {d["capability"] for d in b["decoders"]}
total = None
for _, kind, r in read:
if kind != "multi":
continue
m, lengths = r
i = classical_rendition(m)
if i is not None and m["renditions"][i]["capability"] not in decoded:
total = (total or 0) + lengths[i]
need(total == b["classical_declared"], 6, "session_classical_mismatch")
def check_session_stage_claims(c, wires, ps, objs):
"""§2.4.12 against the stage claims. A sink names one for each epoch of a stage set,
and bases over the same units of the title each have their own: each claim's
identity and objects first, then with the outputs each claim's units and its count
from below, and each unit of a set from above, by the objects listed as applied or
partial by the claims whose sets hold it."""
if not isinstance(wires, list) or not all(isinstance(w, str) for w in wires):
raise ValueError("stage_claims must be an array of wire forms")
b = c["body"]
need(c["class"] == "reconstruct-session", 6, "context_mismatch")
received = {o["content_hash"] for o in objs} if objs is not None else None
claims = []
for w in wires:
try:
s = verify_claim(w)
except Reject:
raise Reject(6, "context_invalid")
link = link_hash(s)
named = [x["first_unit"] for x in b["stage_sets"] if x["claim"] == link]
need(s["class"] == "stage" and s["signer_pubkey"] == c["signer_pubkey"] and named, 6, "context_mismatch")
arrived = [o for o in s["body"]["objects"] if o["status"] != "dropped" and o["bytes"] > 0]
if received is not None:
need(s["subject"] in received and all(o["object"] in received for o in arrived), 6, "session_stage_disagrees")
claims.append((s, named))
if ps is None:
return
fin = finals(ps)
def in_set(f, n):
return [(u - f, p["layers"]) for u, p in fin.items() if f <= u < f + n]
sets = []
for s, named in claims:
ubs = s["body"]["units_by_stage"]
n = sum(ubs)
for f in named:
presented = in_set(f, n)
need(len(presented) == n, 6, "session_stage_disagrees")
for st in range(1, len(ubs)):
need(sum(1 for _, l in presented if l > st) >= sum(ubs[st:]), 6, "session_stage_disagrees")
sets.append((f, n, [o for o in s["body"]["objects"] if o["status"] in ("applied", "partial")]))
for u, p in fin.items():
holding = [(f, applied) for f, n, applied in sets if f <= u < f + n]
if not holding:
continue
for layer in range(1, p["layers"]):
need(any(o["layer"] == layer and o["units"]["from"] <= u - f < o["units"]["from"] + o["units"]["count"]
for f, applied in holding for o in applied), 6, "session_stage_disagrees")
def check_figure_header(b):
"""§2.4.8's header at step 2, in its table's order: `figure_profile` a term, as it is
read; the index an integer or null; the group a hash or null; then the figure and the
@ -655,6 +1132,8 @@ def class_rules(c):
need(dlv is not None or b["prediction"] is not None, 4, "operating_point_empty")
elif cls == "stage":
stage_rules(b)
elif cls == "reconstruct-session":
session_rules(b)
elif cls == "log-inclusion":
need(b["log_receipt"] != "", 4, "log_inclusion_no_evidence")
elif cls in ("soft-binding", "binding-recovery"):
@ -917,6 +1396,24 @@ def check_context(c, ctx):
need(c["class"] == "delivery-gap", 6, "context_mismatch")
need(all(g[k] == b[k] for k in ("track_alias", "group_id", "object_id")), 6, "context_mismatch")
need(g["content_hash"] == c["subject"], 6, "gap_subject_mismatch")
# §2.4.12: a session against its outputs, the objects it received, the WAI2 envelopes
# it received and the stage claims it names, in that order.
ps = presentations(ctx["outputs"]) if "outputs" in ctx else None
if ps is not None:
red = ctx.get("redecoded", [])
if not isinstance(red, list):
raise ValueError("redecoded must be an array")
found = {}
for x in red:
found[ctx_int(x["index"])] = ctx_hash(x["out_digest"])
check_session_outputs(c, ps, found)
objs = objects_seen(ctx["objects"]) if "objects" in ctx else None
if objs is not None:
check_session_objects(c, objs)
if "envelopes" in ctx:
check_session_envelopes(c, envelopes(ctx["envelopes"]), objs)
if "stage_claims" in ctx:
check_session_stage_claims(c, ctx["stage_claims"], ps, objs)
if "declaration" in ctx:
# In §3.1 step 6's order: the claim must be one that names a declaration; the
# declaration must itself pass steps 1-4 (context_invalid) — its signer_pubkey is
@ -993,6 +1490,9 @@ def figure_view(f):
figs = {}
if e["class"] == "energy":
figs[None] = (e["body"]["joules_micro"], class_of_body(e["body"]))
elif e["class"] == "reconstruct-session" and e["body"]["energy"] is not None:
# energy-measurement §6.1: a session's figure is its `energy`, with its class.
figs[None] = (e["body"]["energy"]["joules_micro"], class_of_json(e["body"]["energy"]))
return ("moq-jwp:claim-link", link_hash(e), None, figs, e["signer_pubkey"])
v = f["view"]
figs = {x["index"]: (x["joules_micro"], class_of_json(x)) for x in v["figures"]}

View file

@ -19,7 +19,13 @@ presence combination of their points and prediction; reports checked against fig
that agree and disagree in each member; energy claims against labelled objects; stage
claims around every rule of §2.4.10 — points, the stage hash, the units, the objects'
order, statuses, evidence, reasons and bytes, and the delivered stage they give — and
delivery-gap claims against object receipts at their coordinates and elsewhere —
delivery-gap claims against object receipts at their coordinates and elsewhere; and
reconstruct-session claims built from random presentation lists, around every rule of
§2.4.12 — units, leaves and paths off by one, switches and missed presentations at and past
their caps, a null deadline with misses, presences that disagree, decoder indices out of
range, leaves moved between entries, entries out of first-use order, every acquisition
class — with their outputs, re-decodes, objects, entry envelopes and stage claims that
reach their stages or miss them —
writes them in the corpus's format with no declared outcome, runs both verifiers'
`verdicts` mode, and reports every input on which they differ.
@ -48,6 +54,7 @@ from decimal import Decimal, localcontext
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import claim_verify as CV # noqa: E402
SV = CV.SV # the strict JSON, manifest and envelope readers claim_verify.py uses
from cryptography.hazmat.primitives import serialization # noqa: E402
from cryptography.hazmat.primitives.asymmetric import ed25519 # noqa: E402
@ -528,6 +535,606 @@ def stages(ins, rng, n):
{"gap_object": {"track_alias": 8, "group_id": 3, "object_id": 0, "content_hash": g["subject"]}})
SESSION_CAPS = [("wai.video.int_hyper", "primary", True), ("wai.video.av1", "fallback", False),
("wai.video.int_refine", "primary", False), ("wai.image.png", "fallback", False),
("wai.neural.int_hyper", "primary", True), ("wai.image.int_refine", "primary", False)]
SESSION_ENERGY = [
None,
{"joules_micro": 250000, "energy_provenance": "OnChipCounter",
"energy_uncertainty": {"relative_ppm": 20000, "window_us": 1000000}, "energy_counter_filtering": "Unknown",
"energy_calibration_ref": None},
{"joules_micro": 1, "energy_provenance": "OnChipCounter", "energy_uncertainty": {"relative_ppm": 288676, "window_us": 1},
"energy_counter_filtering": "Off", "energy_calibration_ref": None},
{"joules_micro": 1, "energy_provenance": "OnChipCounter", "energy_uncertainty": {"relative_ppm": 288675, "window_us": 1},
"energy_counter_filtering": "On", "energy_calibration_ref": None},
{"joules_micro": 250000, "energy_provenance": "OnChipCounter",
"energy_uncertainty": {"relative_ppm": 20000, "window_us": 1000000}, "energy_counter_filtering": None,
"energy_calibration_ref": None},
{"joules_micro": 9000, "energy_provenance": "CalibratedInstrument",
"energy_uncertainty": {"relative_ppm": 5000, "window_us": 1000000}, "energy_counter_filtering": None,
"energy_calibration_ref": "\u3000"},
{"joules_micro": 9000, "energy_provenance": "CalibratedInstrument",
"energy_uncertainty": {"relative_ppm": 5000, "window_us": 1000000}, "energy_counter_filtering": None,
"energy_calibration_ref": "urn:example:certificate"},
{"joules_micro": 9000, "energy_provenance": "ModelBased", "energy_uncertainty": None,
"energy_counter_filtering": None, "energy_calibration_ref": None},
{"joules_micro": 9000, "energy_provenance": "ModelBased", "energy_uncertainty": {"relative_ppm": 1, "window_us": 1},
"energy_counter_filtering": None, "energy_calibration_ref": None},
{"joules_micro": 9000, "energy_provenance": "Estimator", "energy_uncertainty": None,
"energy_counter_filtering": None, "energy_calibration_ref": None},
{"joules_micro": 9000, "energy_provenance": "HwShunt", "energy_uncertainty": None,
"energy_counter_filtering": None, "energy_calibration_ref": None},
{"joules_micro": 0, "energy_provenance": "ModelBased", "energy_uncertainty": None,
"energy_counter_filtering": None, "energy_calibration_ref": None},
{"joules_micro": 9000, "energy_provenance": "onchipcounter", "energy_uncertainty": None,
"energy_counter_filtering": None, "energy_calibration_ref": None},
]
def session_stage_claim(rng, n_set, at_stage_1, signer, subject, late=0):
"""A stage claim (§2.4.10) that passes its own rules: `n_set` units, `at_stage_1` of
them at stage 1 through one applied first layer, the rest at stage 0; and, with
`late`, a first layer over the next `late` units that arrived late."""
ubs = [n_set - at_stage_1, at_stage_1] if at_stage_1 else [n_set]
objects = [] if not at_stage_1 else [{
"layer": 1, "object": CV.blake3(b"differential: a stage layer").hex(), "units": {"from": 0, "count": at_stage_1},
"bytes": 700, "status": "applied", "evidence": "none", "reason": None}]
if late:
objects.append({"layer": 1, "object": CV.blake3(b"differential: a late stage layer").hex(),
"units": {"from": at_stage_1, "count": late}, "bytes": 650, "status": "late", "evidence": "none",
"reason": None})
body = {"vocabulary": CV.STAGE_VOCABULARY, "op_requested": None, "op_committed": None, "op_delivered": None,
"presented": {"wai-stage:layers": 0 if ubs[0] else 1}, "units_by_stage": ubs, "tier": "none",
"stage_hash": None, "objects": objects, "conflicts": 0, "deadline_misses": 0, "bytes_envelopes": 5700}
c = {"kind": "claim", "class": "stage", "subject_type": "object", "subject": subject, "seq": 0,
"issued_at": "2026-09-27T12:00:00Z", "body": body, "signer_id": "did:example:sink", "parent_receipt_hash": None}
return CV.jcs(sign(c, signer))
def sessions(ins, rng, n):
"""§2.4.12 around its rules: a random presentation list — units with gaps, one to three
layers each, through one to four decoder entries — and the session it gives, then a
defect or two in the body or the contexts, signed by the sink."""
base = ins.receipt("session_accept")
wai2 = model_envelope(ins, b"WAI2")
envelope, wai2_hash = wai2.hex(), SV.content_hash(wai2)
reasons = list(CV.SWITCH_REASONS)
for i in range(n):
# The entry object: a WAI1 envelope of one capability or another, so that an
# entry naming it has its capability or not.
base_cap = rng.choice(["wai.video.int_hyper", "wai.video.int_motion"])
base_env = SV.wai1(json.dumps({"wai": "1.0", "media": "video", "intent": "replicate",
"model_requirement": {"capability": base_cap}, "conditioning": {"kind": "none"}},
separators=(",", ":")).encode(), b"\x33" * 16)
entry = SV.content_hash(base_env)
pool = rng.sample(SESSION_CAPS, rng.randint(1, 4))
ps = []
for u in range(rng.randint(1, 12)):
if rng.random() < 0.1:
continue
for layers in range(1, rng.randint(1, 3) + 1):
if layers == 1 or rng.random() < 0.9:
ps.append({"unit": u, "layers": layers, "decoder": rng.randrange(len(pool)),
"out_digest": CV.blake3(b"differential: output %d %d %d" % (i, u, layers)).hex()})
# Entries in first-use order, as an honest sink lists them.
order = []
for p in sorted(ps, key=lambda p: (p["layers"], p["unit"])):
if p["decoder"] not in order:
order.append(p["decoder"])
for p in ps:
p["decoder"] = order.index(p["decoder"])
decoders = []
for j, old in enumerate(order):
cap, path, pinned = pool[old]
# The envelope's first rendition is wai.video.int_hyper: an entry naming rendition 0
# with that capability is bound by it, any other is not.
rendition = rng.choice([None, 0]) if cap == "wai.video.int_hyper" or rng.random() < 0.2 else None
# Its object: the WAI2 envelope for a rendition, else the entry object or a
# layer of its own, and now and then the other.
if rendition is not None:
source = wai2_hash if rng.random() < 0.9 else entry
else:
source = rng.choice([entry, entry, CV.blake3(b"differential: a source %d %d" % (i, j)).hex(), wai2_hash])
decoders.append({"capability": cap, "rendition": rendition, "object": source, "path": path,
"decoder": "builtin:[email protected]", "variant": None,
"pin": CV.blake3(b"differential: a pin").hex() if pinned else None,
"tier": "decode-equivalence" if pinned else None, "module_status": None,
"leaves": sum(1 for p in ps if p["decoder"] == j)})
fin = CV.finals(ps)
primary = sum(1 for p in fin.values() if decoders[p["decoder"]]["path"] == "primary")
ordered = sorted(ps, key=lambda p: (p["unit"], p["layers"]))
root = CV.out_mth([CV.out_leaf(p) for p in ordered]).hex() if ps else None
deadline = rng.choice([None, 40000])
late = rng.sample(ordered, min(len(ordered), rng.randint(0, 3))) if deadline else []
late = sorted(({"unit": p["unit"], "layers": p["layers"], "late_us": rng.randint(1, 5000)} for p in late),
key=lambda x: (x["unit"], x["layers"]))
switches = sorted(({"unit": rng.randint(0, 12), "from": "primary", "to": "fallback", "reason": rng.choice(reasons)}
for _ in range(rng.randint(0, 3))), key=lambda w: w["unit"])
def obj(h, path="primary", counted=None, presented=True):
return {"content_hash": h, "path": path, "counted": counted, "transferred": rng.choice([700, 1200, 3000]),
"presented": presented}
objects = [obj(CV.blake3(b"differential: object %d %d" % (i, k)).hex(), rng.choice(["primary", "primary", "fallback"]),
rng.choice([None, 700, 1200, 0]), rng.random() < 0.8) for k in range(rng.randint(0, 5))]
if rng.random() < 0.85:
objects.insert(rng.randint(0, len(objects)), obj(entry, counted=5000))
# The envelopes held, chosen here so the objects received can hold them or not.
envs = rng.choice([None, None, [], [envelope], [envelope] * 2, [base_env.hex(), envelope], [base_env.hex()]])
for h in sorted({d["object"] for d in decoders} | ({wai2_hash} if envs and envelope in envs else set())):
if h != entry and rng.random() < 0.9 and all(o["content_hash"] != h for o in objects):
objects.append(obj(h, counted=rng.choice([None, 700])))
stage_wires, stage_sets = [], []
if fin and rng.random() < 0.5:
# A stage claim over n_set units from a presented unit f: its first `at1` units
# at stage 1 through one applied first layer, so a unit of the set presented at
# two layers or more lies within that layer's span or not, and at three layers
# beyond any layer it lists (§2.4.12 step 6, both bounds).
units = sorted(fin)
f = rng.choice(units)
n_set = rng.randint(1, 3)
reached = sum(1 for u, p in fin.items() if f <= u < f + n_set and p["layers"] > 1)
at1 = min(n_set, max(0, reached + rng.choice([0, 0, 1, -1])))
signer = VIEWER if rng.random() < 0.9 else RELAY
base_hash = CV.blake3(b"differential: a base %d" % i).hex()
w = session_stage_claim(rng, n_set, at1, signer, base_hash)
if rng.random() < 0.05:
w = w.replace('"tier":"none"', '"tier":"nonе"')
stage_wires.append(w)
stage_sets.append({"first_unit": f, "claim": CV.link_hash(json.loads(w)) if rng.random() < 0.95 else "00" * 32})
# The set's base and its layer object among the objects received, or not.
if rng.random() < 0.85:
objects.append(obj(base_hash, counted=4000))
if at1 and rng.random() < 0.85:
objects.append(obj(CV.blake3(b"differential: a stage layer").hex(), counted=700))
counted = lambda path: sum(o["counted"] for o in objects if o["counted"] is not None and o["path"] == path)
body = dict(base["body"], decoders=decoders, units=len(fin), leaf_count=len(ps), output_root=root,
primary_units=primary, fallback_units=len(fin) - primary, switches=switches,
switches_total=len(switches) + rng.choice([0, 0, 0, 2]), deadline_us=deadline,
missed=len(late) + rng.choice([0, 0, 0, 1]) if deadline else 0, missed_units=late,
stage_sets=stage_sets, counted_primary=counted("primary"), counted_fallback=counted("fallback"),
counted_discarded=sum(o["counted"] for o in objects if o["counted"] is not None and not o["presented"]),
uncounted=sum(1 for o in objects if o["counted"] is None),
transferred=sum(o["transferred"] for o in objects),
energy=copy.deepcopy(rng.choice(SESSION_ENERGY)))
if rng.random() < 0.5:
body["classical_declared"], body["classical_source"] = None, None
ctx = {"outputs": copy.deepcopy(ps), "objects": objects}
# One or two defects, in the body or in a context.
for _ in range(rng.choice([0, 1, 1, 2])):
k = rng.randrange(26)
ds = body["decoders"]
if k == 0:
body["primary_units"] += rng.choice([1, -1]) if body["primary_units"] else 1
elif k == 1:
body["units"] += 1
elif k == 2 and len(ds) > 1 and ds[0]["leaves"] > 1:
ds[0]["leaves"] -= 1
ds[1]["leaves"] += 1
elif k == 3:
body["leaf_count"] += rng.choice([1, -1]) if body["leaf_count"] else 1
elif k == 4:
body["output_root"] = rng.choice([None, CV.blake3(b"differential: another root").hex()])
elif k == 5 and late:
late[0]["layers"] += 3
elif k == 6:
body["switches_total"] = max(0, len(switches) - 1)
elif k == 7:
body["switches"] = [{"unit": u, "from": "primary", "to": "fallback", "reason": "recovered"} for u in range(1025)]
body["switches_total"] = 1025
elif k == 8 and ps:
body["deadline_us"], body["missed"] = 40000, 257
body["missed_units"] = [{"unit": u, "layers": 1, "late_us": 1} for u in range(257)]
elif k == 9:
body["deadline_us"], body["missed"] = None, max(1, body["missed"])
elif k == 10:
body["classical_declared"], body["classical_source"] = rng.choice([(700, None), (None, "wai2-table"), (700, "referenced")])
elif k == 11 and ctx["outputs"]:
ctx["outputs"][rng.randrange(len(ctx["outputs"]))]["decoder"] = len(ds) + rng.choice([0, 1])
elif k == 12 and len(ds) > 1:
ds[0], ds[1] = ds[1], ds[0]
elif k == 13:
body["counted_discarded"] += 1
elif k == 14 and ctx["objects"]:
o = ctx["objects"][0]
o["counted"] = None if o["counted"] is not None else 5
elif k == 15:
body["host"] = dict(body["host"], threads=rng.choice([0, 3, 6, 2 ** 52]))
elif k == 16 and ds:
ds[-1]["decoder"] = rng.choice(["builtin:wai", "builtin:wai@x", "module:sha256:" + "a" * 64, "builtin:Wai@1"])
elif k == 17 and ds:
ds[-1]["pin"] = rng.choice([ds[-1]["pin"], CV.blake3(b"differential: a pin").hex()])
ds[-1]["tier"] = rng.choice(["none", "decode-equivalence", "entropy-consistency", "bit-exact", None])
elif k == 18 and ds:
ds.append(dict(ds[0]))
elif k == 19 and ctx["outputs"]:
ctx["outputs"].pop(rng.randrange(len(ctx["outputs"])))
elif k == 20 and ctx["outputs"]:
ctx["outputs"][0]["out_digest"] = CV.blake3(b"differential: not this output").hex()
elif k == 21 and stage_sets:
stage_sets.append(dict(stage_sets[0]))
elif k == 22:
body["framing"] = rng.choice(["registered", "wai-measure/0"])
elif k == 23 and ds:
ds[0]["module_status"] = rng.choice(["pinned", "fetched", "cached"])
elif k == 24:
body["counted_primary"] += rng.choice([1, -1]) if body["counted_primary"] else 1
elif k == 25 and switches:
body["switches"] = [dict(switches[0], to="primary")] + switches[1:]
if rng.random() < 0.5:
ctx["redecoded"] = [{"index": j, "out_digest": rng.choice([p["out_digest"], CV.blake3(b"differential: redecoded").hex()])}
for j, p in enumerate(ps) if rng.random() < 0.3]
if envs is not None:
ctx["envelopes"] = [{"envelope": e} for e in envs]
n2 = sum(1 for e in envs if e == envelope)
if n2:
body["classical_declared"], body["classical_source"] = rng.choice(
[(700, "wai2-table"), (700 * n2, "wai2-table"), (701, "wai2-table"), (None, None)])
if stage_wires:
ctx["stage_claims"] = stage_wires
if rng.random() < 0.1:
del ctx["outputs"]
if rng.random() < 0.1:
del ctx["objects"]
ins.add(f"session_random_{i}", CV.jcs(sign(dict(base, subject=entry, body=body), VIEWER)), ctx)
def session_stage_bounds(ins, rng, n):
"""§2.4.12 step 6 against a stage claim, both ways, on sessions that pass steps 1-5: a
set of one to six units whose first layer applied to some and arrived late for others,
and presentations of its units at one to three layers. A unit presented at two layers
lies within an arrived first layer or not, one at three layers never has an arrived
second layer, and the units presented at two layers or more reach the units the claim
counts at stage 1 or fall short. The objects received hold the set's base and its
layers, or lack either."""
base = ins.receipt("session_accept")
dec = [dict(base["body"]["decoders"][0], leaves=0), dict(base["body"]["decoders"][3], leaves=0)]
for i in range(n):
n_set = rng.randint(1, 6)
at1 = rng.randint(0, n_set)
late = rng.randint(0, n_set - at1)
ps = []
for u in range(n_set):
top = 1
if u < at1 + late and rng.random() < 0.8:
top = rng.choice([2, 2, 3])
elif rng.random() < 0.15:
top = 2
for layers in range(1, top + 1):
ps.append({"unit": u, "layers": layers, "decoder": 0 if layers == 1 else 1,
"out_digest": CV.blake3(b"differential: bounds %d %d %d" % (i, u, layers)).hex()})
base_hash = CV.blake3(b"differential: a bounded base %d" % i).hex()
decoders = [dict(dec[0], object=base_hash, leaves=sum(1 for p in ps if p["decoder"] == 0))]
if any(p["decoder"] == 1 for p in ps):
decoders.append(dict(dec[1], object=CV.blake3(b"differential: a stage layer").hex(), leaves=sum(1 for p in ps if p["decoder"] == 1)))
w = session_stage_claim(rng, n_set, rng.choice([at1, at1, max(0, at1 - 1), min(n_set, at1 + 1)]), VIEWER, base_hash,
late=late if late and at1 + late <= n_set else 0)
objects = []
for h, cnt in [(base_hash, 4000), (CV.blake3(b"differential: a stage layer").hex(), 700),
(CV.blake3(b"differential: a late stage layer").hex(), 650)]:
if rng.random() < 0.9:
objects.append({"content_hash": h, "path": "primary", "counted": cnt, "transferred": cnt, "presented": cnt != 650})
entry = objects[0]["content_hash"] if objects else base_hash
ordered = sorted(ps, key=lambda p: (p["unit"], p["layers"]))
body = dict(base["body"], decoders=decoders, units=n_set, leaf_count=len(ps), primary_units=n_set, fallback_units=0,
output_root=CV.out_mth([CV.out_leaf(p) for p in ordered]).hex(), switches=[], switches_total=0,
deadline_us=None, missed=0, missed_units=[], stage_sets=[{"first_unit": 0, "claim": CV.link_hash(json.loads(w))}],
counted_primary=sum(o["counted"] for o in objects), counted_fallback=0,
counted_discarded=sum(o["counted"] for o in objects if not o["presented"]), uncounted=0,
transferred=sum(o["transferred"] for o in objects), classical_declared=None, classical_source=None,
energy=None)
ctx = {"outputs": ps, "objects": objects, "stage_claims": [w]}
if rng.random() < 0.1:
del ctx["objects"]
ins.add(f"session_bounds_{i}", CV.jcs(sign(dict(base, subject=entry, body=body), VIEWER)), ctx)
def epoch_stage_claim(rng, i, k, n_set, subject):
"""A stage claim (§2.4.10) that passes its own rules, as the claim of one epoch of a
set of `n_set` units: a first layer of any status over a span, perhaps a second over
other units, and perhaps a second layer applied or not within an applied first; its
units at each stage as many as its applied layers can have reached, or fewer."""
def obj(layer, j, frm, cnt, status):
h = CV.blake3(b"differential: epoch layer %d %d %d %d" % (i, k, layer, j)).hex()
return {"layer": layer, "object": h, "units": {"from": frm, "count": cnt},
"bytes": 0 if status == "dropped" else 600 + 10 * j + layer, "status": status, "evidence": "none",
"reason": {"partial": "layer-gap", "refused": "geometry-mismatch"}.get(status)}
objects = []
cut = rng.randint(1, n_set)
st1 = rng.choice(["applied", "applied", "partial", "late", "refused", "dropped"])
if st1 == "partial" and cut < 2:
st1 = "applied"
objects.append(obj(1, 0, 0, cut, st1))
if cut < n_set and rng.random() < 0.6:
st = rng.choice(["applied", "late", "refused", "dropped"])
objects.append(obj(1, 1, cut, n_set - cut, st))
reach1 = sum(o["units"]["count"] if o["status"] == "applied" else o["units"]["count"] - 1 if o["status"] == "partial" else 0
for o in objects)
reach2 = 0
if st1 == "applied" and rng.random() < 0.6:
st = rng.choice(["applied", "applied", "late", "refused"])
objects.append(obj(2, 0, 0, 1, st))
reach2 = 1 if st == "applied" else 0
s1 = rng.randint(0, reach1)
s2 = rng.randint(0, min(reach2, s1))
ubs = [n_set - s1, s1 - s2, s2] if s2 else ([n_set - s1, s1] if s1 else [n_set])
objects.sort(key=lambda o: (o["layer"], o["units"]["from"], o["object"]))
body = {"vocabulary": CV.STAGE_VOCABULARY, "op_requested": None, "op_committed": None, "op_delivered": None,
"presented": {"wai-stage:layers": next(j for j, c in enumerate(ubs) if c)}, "units_by_stage": ubs, "tier": "none",
"stage_hash": None, "objects": objects, "conflicts": 0, "deadline_misses": 0,
"bytes_envelopes": 5000 + sum(o["bytes"] for o in objects)}
c = {"kind": "claim", "class": "stage", "subject_type": "object", "subject": subject, "seq": k,
"issued_at": "2026-09-27T12:00:00Z", "body": body, "signer_id": "did:example:sink", "parent_receipt_hash": None}
return CV.jcs(sign(c, VIEWER)), objects
def session_stage_epochs(ins, rng, n):
"""§2.4.12 step 6 from above across epochs: one to three stage claims of one set, as a
sink that forgot its base signs one per epoch, each listing first and second layers of
every status; sometimes one named one unit further on. The session presents each unit
at one layer and some at two or three, within what one of the claims holding the unit
applied or past it, and receives the arrived layers or misses one."""
base = ins.receipt("session_accept")
dec = [dict(base["body"]["decoders"][0], leaves=0), dict(base["body"]["decoders"][3], leaves=0)]
for i in range(n):
n_set = rng.randint(2, 6)
base_hash = CV.blake3(b"differential: an epoch base %d" % i).hex()
claims = [epoch_stage_claim(rng, i, k, n_set, base_hash) for k in range(rng.randint(1, 3))]
named = [(1 if k and rng.random() < 0.15 else 0, w) for k, (w, _) in enumerate(claims)]
ps = []
for u in range(n_set + 1):
top = rng.choice([1, 1, 2, 2, 2, 3])
for layers in range(1, top + 1):
ps.append({"unit": u, "layers": layers, "decoder": 0 if layers == 1 else 1,
"out_digest": CV.blake3(b"differential: epochs %d %d %d" % (i, u, layers)).hex()})
decoders = [dict(dec[0], object=base_hash, leaves=sum(1 for p in ps if p["decoder"] == 0))]
objects = [{"content_hash": base_hash, "path": "primary", "counted": 4000, "transferred": 4000, "presented": True}]
seen = set()
for _, objs in claims:
for o in objs:
if o["status"] != "dropped" and o["object"] not in seen and rng.random() < 0.95:
seen.add(o["object"])
objects.append({"content_hash": o["object"], "path": "primary", "counted": o["bytes"],
"transferred": o["bytes"], "presented": o["status"] in ("applied", "partial")})
if any(p["decoder"] == 1 for p in ps):
# The refinement's entry names a layer the session received, or its base.
layer = next((o["content_hash"] for o in objects[1:] if o["presented"]), base_hash)
decoders.append(dict(dec[1], object=layer, leaves=sum(1 for p in ps if p["decoder"] == 1)))
units = n_set + 1
ordered = sorted(ps, key=lambda p: (p["unit"], p["layers"]))
sets = sorted({(f, CV.link_hash(json.loads(w))) for f, w in named})
body = dict(base["body"], decoders=decoders, units=units, leaf_count=len(ps), primary_units=units, fallback_units=0,
output_root=CV.out_mth([CV.out_leaf(p) for p in ordered]).hex(), switches=[], switches_total=0,
deadline_us=None, missed=0, missed_units=[], stage_sets=[{"first_unit": f, "claim": h} for f, h in sets],
counted_primary=sum(o["counted"] for o in objects), counted_fallback=0,
counted_discarded=sum(o["counted"] for o in objects if not o["presented"]), uncounted=0,
transferred=sum(o["transferred"] for o in objects), classical_declared=None, classical_source=None,
energy=None)
ctx = {"outputs": ps, "objects": objects, "stage_claims": [w for w, _ in claims]}
ins.add(f"session_epochs_{i}", CV.jcs(sign(dict(base, subject=base_hash, body=body), VIEWER)), ctx)
def wai2_with(manifest_text, rest):
"""A WAI2 envelope of the manifest text (or bytes) given, with the rest of the session
corpus's envelope: its rendition count, table and payloads."""
t = manifest_text.encode() if isinstance(manifest_text, str) else manifest_text
return b"WAI2" + len(t).to_bytes(4, "little") + t + rest
def model_envelope(ins, magic):
"""The model session's envelope of this magic: its base's WAI1, or its WAI2."""
for e in ins.by_name["session_accept"]["context"]["envelopes"]:
b = bytes.fromhex(e["envelope"])
if b[:4] == magic:
return b
raise ValueError(f"the model session holds no {magic!r} envelope")
def sourced(ins, entry, env):
"""The model session with decoder entry `entry`'s object the envelope given, signed
again: the session that decoded from it."""
c = copy.deepcopy(ins.receipt("session_accept"))
c["body"]["decoders"][entry]["object"] = SV.content_hash(env)
return CV.jcs(sign(c, VIEWER))
def session_envelopes(ins):
"""§2.4.12 step 6, the envelopes: the model session against its WAI2 envelope with
manifests every reader may read otherwise — members of the wrong type, pins that are
not pins, a `security` that encrypts or is no security object, duplicate members,
numbers past every range, nesting at and past the depth a reader allows, byte-order
marks, lone surrogates — with the claim unchanged, so only the envelope decides."""
env = model_envelope(ins, b"WAI2")
ml = int.from_bytes(env[4:8], "little")
text, rest = env[8:8 + ml].decode(), env[8 + ml:]
idx = 1 # the classical rendition
def mutate(f):
m = json.loads(text)
f(m)
return json.dumps(m, separators=(",", ":"))
def setr(k, val, i=idx):
def f(m):
m["renditions"][i][k] = val
return f
deep = lambda n: "[" * n + "]" * n
cases = {
"control": text,
"id_int": mutate(setr("id", 5)),
"sha256_int": mutate(setr("sha256", 5)),
"companion_of_array": mutate(setr("companion_of", [])),
"companion_of_id_object": mutate(setr("companion_of_id", {})),
"prior_string": mutate(setr("prior", "x")),
"prior_empty_object": mutate(setr("prior", {})),
"prior_no_determinism": mutate(setr("prior", {"id": "a", "sha256": "b"})),
"prior_id_int": mutate(setr("prior", {"id": 1, "sha256": "b", "determinism": "c"})),
"prior_tier_int": mutate(setr("prior", {"id": "a", "sha256": "b", "determinism": "c", "tier": 5})),
"prior_supersedes_string": mutate(setr("prior", {"id": "a", "sha256": "b", "determinism": "c", "supersedes": "x"})),
"other_entry_id_int": mutate(setr("id", 7, 0)),
"other_entry_prior_string": mutate(setr("prior", "x", 0)),
"role_int": mutate(setr("role", 5)),
"security_string": mutate(lambda m: m.__setitem__("security", "none")),
"security_int": mutate(lambda m: m.__setitem__("security", 5)),
"security_encrypted": mutate(lambda m: m.__setitem__("security", {"encryption": "aes-128-gcm", "kid": 1})),
"security_none": mutate(lambda m: m.__setitem__("security", {"encryption": "none"})),
"security_kid_negative": mutate(lambda m: m.__setitem__("security", {"encryption": "none", "kid": -1})),
"duplicate_ids": mutate(lambda m: [r.__setitem__("id", "a") for r in m["renditions"]]),
"missing_intent": mutate(lambda m: m.pop("intent")),
"wai_number": mutate(lambda m: m.__setitem__("wai", 1.1)),
"target_absent": mutate(lambda m: m.pop("target")),
"target_1e400": text.replace('"target":null', '"target":1e400'),
"target_neg_zero": text.replace('"target":null', '"target":-0'),
"target_2pow64": text.replace('"target":null', '"target":18446744073709551616'),
"target_depth_70": text.replace('"target":null', '"target":' + deep(70)),
"target_depth_126": text.replace('"target":null', '"target":' + deep(126)),
"target_depth_130": text.replace('"target":null', '"target":' + deep(130)),
"duplicate_member_media": text.replace('"media":"video"', '"media":"video","media":"video"'),
"duplicate_member_in_entry": text.replace('"kind":"second"', '"kind":"second","kind":"second"'),
"trailing_garbage": text + " x",
"trailing_space": text + " ",
"bom": "\ufeff" + text,
"lone_surrogate_target": text.replace('"target":null', '"target":"\\ud800"'),
"manifest_array": "[]",
"kind_null": mutate(setr("kind", None)),
"capability_int": mutate(setr("capability", 5)),
"entry_not_object": mutate(lambda m: m["renditions"].__setitem__(0, "x")),
"renditions_object": mutate(lambda m: m.__setitem__("renditions", {})),
"envelope_manifest_int": mutate(setr("envelope_manifest", 5)),
"refines_int": mutate(setr("refines", 5)),
"id_null": mutate(setr("id", None)),
"prior_null": mutate(setr("prior", None)),
"role_companion": mutate(setr("role", "companion")),
"role_unknown": mutate(setr("role", "rendition")),
"media_audio": mutate(lambda m: m.__setitem__("media", "audio")),
}
for name, t in cases.items():
e = wai2_with(t, rest)
ins.add("session_envelope_" + name, sourced(ins, 1, e), {"envelopes": [{"envelope": e.hex()}]})
# The table and payload framing around a sound manifest.
t = text.encode()
framing = {
"count_short": b"WAI2" + len(t).to_bytes(4, "little") + t + (1).to_bytes(2, "little") + rest[2:],
"truncated": wai2_with(t, rest)[:-1],
"trailing_bytes": wai2_with(t, rest) + b"\x00",
"manifest_length_past_end": b"WAI2" + (len(t) + len(rest) + 1).to_bytes(4, "little") + t + rest,
}
for name, e in framing.items():
ins.add("session_envelope_" + name, sourced(ins, 1, e), {"envelopes": [{"envelope": e.hex()}]})
def session_wai1_envelopes(ins):
"""§2.4.12 step 6, the envelopes: the model session against its base's WAI1 envelope
with manifests every reader may read otherwise, a capability or fallback the base's
entry does or does not name, and framing short or long; the session signed again
with the base's entry naming that envelope, so only the envelope decides."""
env = model_envelope(ins, b"WAI1")
multi = {"envelope": model_envelope(ins, b"WAI2").hex()}
ml = int.from_bytes(env[4:8], "little")
text, payload = env[8:8 + ml].decode(), env[12 + ml:]
def mutate(f):
m = json.loads(text)
f(m)
return json.dumps(m, separators=(",", ":"))
def mr(k, val):
return mutate(lambda m: m["model_requirement"].__setitem__(k, val))
deep = lambda n: "[" * n + "]" * n
cases = {
"control": text,
"capability_other": mr("capability", "wai.video.av1"),
"fallback_is_entry": mutate(lambda m: m["model_requirement"].update(capability="wai.video.av1", fallback="wai.video.int_hyper")),
"fallback_other": mr("fallback", "wai.video.hevc"),
"capability_int": mr("capability", 5),
"fallback_int": mr("fallback", 5),
"min_sim_float": mr("min_sim", 0.5),
"min_sim_string": mr("min_sim", "0.5"),
"min_sim_huge": text.replace('"capability":"wai.video.int_hyper"', '"capability":"wai.video.int_hyper","min_sim":1e400'),
"sim_metric_int": mr("sim_metric", 5),
"model_requirement_missing": mutate(lambda m: m.pop("model_requirement")),
"model_requirement_array": mutate(lambda m: m.__setitem__("model_requirement", [])),
"conditioning_missing": mutate(lambda m: m.pop("conditioning")),
"conditioning_kind_int": mutate(lambda m: m["conditioning"].__setitem__("kind", 5)),
"prior_string": mutate(lambda m: m["conditioning"].__setitem__("prior", "x")),
"security_none": mutate(lambda m: m.__setitem__("security", {"encryption": "none"})),
"security_null": mutate(lambda m: m.__setitem__("security", None)),
"security_encrypted": mutate(lambda m: m.__setitem__("security", {"encryption": "aes-128-gcm", "kid": 1})),
"security_string": mutate(lambda m: m.__setitem__("security", "none")),
"security_kid_negative": mutate(lambda m: m.__setitem__("security", {"encryption": "none", "kid": -1})),
"security_kid_2pow64": mutate(lambda m: m.__setitem__("security", {"kid": 2 ** 64})),
"missing_intent": mutate(lambda m: m.pop("intent")),
"media_int": mutate(lambda m: m.__setitem__("media", 5)),
"wai_number": mutate(lambda m: m.__setitem__("wai", 1.0)),
"unknown_member": mutate(lambda m: m.__setitem__("x_unknown", 1)),
"target_object": mutate(lambda m: m.__setitem__("target", {"a": 1})),
"target_depth_130": mutate(lambda m: m.__setitem__("target", None)).replace('"target":null', '"target":' + deep(130)),
"refines_int": mutate(lambda m: m.__setitem__("refines", 5)),
"derived_from_string": mutate(lambda m: m.__setitem__("derived_from", "x")),
"duplicate_member": text.replace('"media":"video"', '"media":"video","media":"video"'),
"duplicate_in_requirement": text.replace('"capability":"wai.video.int_hyper"', '"capability":"wai.video.int_hyper","capability":"wai.video.int_hyper"'),
"bom": "\ufeff" + text,
"trailing_garbage": text + " x",
"lone_surrogate": mutate(lambda m: m.__setitem__("target", None)).replace('"target":null', '"target":"\\ud800"'),
"manifest_array": "[]",
}
for name, t in cases.items():
tb = t.encode()
e = SV.wai1(tb, payload)
ins.add("session_wai1_" + name, sourced(ins, 0, e), {"envelopes": [{"envelope": e.hex()}, multi]})
tb = text.encode()
framing = {
"payload_short": SV.wai1(tb, payload)[:-1],
"trailing_bytes": SV.wai1(tb, payload) + b"\x00",
"manifest_length_past_end": b"WAI1" + (len(tb) + 100).to_bytes(4, "little") + tb,
"no_payload_length": b"WAI1" + len(tb).to_bytes(4, "little") + tb,
"magic_other": b"WAI3" + SV.wai1(tb, payload)[4:],
"short": b"WAI",
}
for name, e in framing.items():
ins.add("session_wai1_" + name, sourced(ins, 0, e), {"envelopes": [{"envelope": e.hex()}, multi]})
def session_classical_sweep(ins):
"""§2.4.12's classical rendition: every registered capability, an unregistered string
and the model's own, as the second rendition of the model session's WAI2 envelope —
once in a manifest of the session's media and once in a manifest of the capability's
own media — so that every rule that admits or excludes one is reached."""
env = model_envelope(ins, b"WAI2")
ml = int.from_bytes(env[4:8], "little")
m0, rest = json.loads(env[8:8 + ml]), env[8 + ml:]
for cap in sorted(CV.REGISTRY) + ["org.example.codec", "wai.video.int_hyper"]:
medias = ["video"] + ([CV.REGISTRY[cap]["media"]] if cap in CV.REGISTRY and CV.REGISTRY[cap]["media"] not in (None, "video") else [])
for media in medias:
m = copy.deepcopy(m0)
m["media"] = media
m["renditions"][1]["capability"] = cap
e = wai2_with(json.dumps(m, separators=(",", ":")), rest)
ins.add(f"session_classical_{cap}_{media}", sourced(ins, 1, e), {"envelopes": [{"envelope": e.hex()}]})
def session_caps(ins):
"""§2.4.12 rules 7 and 8 at their caps, where nothing else decides: 300 units presented
once and late, with the missed presentations and the switches written as many as their
totals, as the cap, and one either side of each."""
base = ins.receipt("session_accept")
ps = [{"unit": u, "layers": 1, "decoder": 0, "out_digest": CV.blake3(b"differential: capped %d" % u).hex()}
for u in range(300)]
root = CV.out_mth([CV.out_leaf(p) for p in ps]).hex()
decoder = dict(base["body"]["decoders"][0], leaves=300)
for missed, written in [(300, 255), (300, 256), (300, 257), (255, 255), (256, 256), (257, 257), (256, 255), (255, 256)]:
for total, switched in [(0, 0), (1100, 1023), (1100, 1024), (1100, 1025), (1023, 1023), (1024, 1024), (1025, 1024),
(1023, 1024)]:
sw = [{"unit": u // 4, "from": "primary" if u % 2 == 0 else "fallback",
"to": "fallback" if u % 2 == 0 else "primary", "reason": "recovered"} for u in range(switched)]
body = dict(base["body"], decoders=[decoder], units=300, leaf_count=300, output_root=root,
primary_units=300, fallback_units=0, switches=sw, switches_total=total,
deadline_us=40000, missed=missed, stage_sets=[],
missed_units=[{"unit": u, "layers": 1, "late_us": 1 + u} for u in range(written)])
ins.add(f"session_caps_{missed}_{written}_{total}_{switched}", CV.jcs(sign(dict(base, body=body), VIEWER)),
{"outputs": copy.deepcopy(ps)})
def figures(ins, rng, n):
oer_t = ins.by_name["energy_matches_oer_accept"]["context"]["oer"]["receipt"]
claim_t = ins.receipt("energy_matches_oer_accept")
@ -1007,6 +1614,13 @@ def main(argv):
bodies(ins)
reports(ins, random.Random(f"reports {a.seed}"), a.random) # its own stream, as classes()
stages(ins, random.Random(f"stages {a.seed}"), a.random) # its own stream, as classes()
sessions(ins, random.Random(f"sessions {a.seed}"), a.random) # its own stream, as classes()
session_caps(ins)
session_stage_bounds(ins, random.Random(f"session bounds {a.seed}"), a.random) # its own stream, as classes()
session_stage_epochs(ins, random.Random(f"session epochs {a.seed}"), a.random) # its own stream, as classes()
session_envelopes(ins)
session_wai1_envelopes(ins)
session_classical_sweep(ins)
doc = text_of({"about": "differential inputs; no outcome is declared", "vectors": ins.out})
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as f:
f.write(doc)

File diff suppressed because one or more lines are too long

View file

@ -46,7 +46,7 @@ every observation can be reported. A string a report carries is printable ASCII.
| `capable` | object or `null` | for `capable`: `{ outcome, reason, classical_floor }` (§2.3) |
| `cohort` | string or `null` | `control` or `wai`: the arm the content's service assigned the session to |
| `metrics` | object or `null` | `{ msd_ms, bsa, bsda_ms, dfa }`: media start delay, buffer starvations and their total duration since the session began, and dropped frames since the session began |
| `claim` | 64 hexadecimal digits or `null` | on `end` only: the link hash of the session's `reconstruct-session` claim, where the sink sealed one |
| `claim` | 64 hexadecimal digits or `null` | on `end` only: the link hash of the session's `reconstruct-session` claim ([jwp-receipts](jwp-receipts.md) §2.4.12), where the sink sealed one |
§2.2 **The object** has exactly the members `ot`, `url`, `content_hash`, `capability`,
`rendition`, `layer`, `layers_total`, `layer_cap`, `counted`, `transferred`, `d_ms`,

View file

@ -939,7 +939,7 @@ where the figure's class is, and which key signed it.
| figure profile | receipt | `subject_type` and identity | figure; its class | index | signed by |
|---|---|---|---|---|---|
| `moq-jwp:object` | JWP object receipt (jwp-receipts §2.4.2), accepted under its group (§3) | `object`: its §2.1 content hash; the claim's `group` is the group receipt's link hash | `joules_micro`; the label in its leaf (jwp-receipts §2.2) | its leaf index | the group receipt's `signer_pubkey` |
| `moq-jwp:claim-link` | `energy` claim (jwp-receipts §2.4.5) | `receipt`: its `link_hash` | `joules_micro`; its class | `null` | its `signer_pubkey` |
| `moq-jwp:claim-link` | `energy` claim (jwp-receipts §2.4.5), or `reconstruct-session` claim whose `energy` is not `null` (§2.4.12) | `receipt`: its `link_hash` | `joules_micro`; its class (for a session, `energy.joules_micro` and the class `energy` writes) | `null` | its `signer_pubkey` |
| `wai:energy-binding-id` | energy binding ([energy-binding](energy-binding.md) §2) | `receipt`: `BLAKE3("wai:energy-binding-id\x01" ‖ JCS(body))` (energy-binding §4) | `microjoules`; `acquisition` | `null` | no Ed25519 key |
| `wai:video-receipt-id`, `wai:video-manifest-receipt-id`, `wai:video-ssai-receipt-id`, `wai:video-keys-receipt-id`, `wai:video-package-receipt-id`, `wai:video-channel-receipt-id` | the `wai.video.int_motion` receipt and the `wai.video.*` OTT receipts | `receipt`: its receipt hash | `joules_micro`; its class | `null` | its `signer_pubkey` |
| `wai:world-receipt-id`, `wai:film-receipt-id`, `wai:haptic-receipt-id`, `wai:score-receipt-id`, `wai:splat4d-receipt-id`, `wai:audio-receipt-id`, `wai:ternary-receipt-id` | the world-session, film and reel, haptic, score, 4D-splat, audio-scene and ternary-reconstruction receipts | `receipt`: its receipt hash, over the payload in the form it was signed (§2.8) | `joules_micro`; its optional label | `null` | its `signer_pubkey` |

View file

@ -36,14 +36,21 @@ In scope:
class. `group` and `object` receipts are the **delivery** class; a
`claim` receipt (§2.4.3) carries one of the others — `energy`,
`lawful-basis`, `log-inclusion`, `measurement`, `operating-point`,
`stage`, `delivery-gap` (§2.4.5–§2.4.11) — 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.
`stage`, `delivery-gap`, `reconstruct-session` (§2.4.5–§2.4.12) — on one
envelope, one canonical form and one signature rule, so a statement other
than delivery does not need a field set of its own.
- **Objects not delivered** (§3, §4.4): an object a subscriber did not
receive is accounted for by its receipt, checked without the object's bytes
and reported as `inclusion-verified`, never as delivered; a relay forwards
the receipts of the objects it does not forward, and may say why in a
`delivery-gap` claim (§2.4.11).
- **What a sink reconstructed** (§2.4.12): a `reconstruct-session` claim
records one session of a sink — its decoders and host, its paths and
switches, its late presentations, a commitment to every output it
presented, the bytes it counted and received, the classical rendition it
did not need, the `stage` claims it signed about the stage sets it
observed, and its decode energy where metered — checkable against the
outputs, the objects, the envelopes and those `stage` claims.
- An optional acquisition-class label on the object receipt, bound with the
object's figure and origin into its Merkle leaf (§2.2, §2.4.2); an
optional class for the group total, signed under a labelled group payload
@ -315,7 +322,7 @@ is present and `null`, so one claim has one canonical form.
| `subject` | 32 B hex | For `object`: the §2.1 content hash of the object the claim is about. For `receipt`: the link hash of a receipt — §3 step 3's for a group receipt, `link_hash` below for a claim — or, for a class that names a figure profile (§2.4.8–§2.4.9), the identity that profile defines ([energy-measurement](energy-measurement.md) §6.1). |
| `seq` | integer | Strictly increasing across the claims of one `signer_pubkey`. |
| `issued_at` | string | A date-time (below) at which the signer issued the claim. Self-asserted, like `group_end`. |
| `body` | object | Class-scoped; its members are fixed by `class` and by nothing else (§2.4.5–§2.4.9, §2.6). |
| `body` | object | Class-scoped; its members are fixed by `class` and by nothing else (§2.4.5–§2.4.12, §2.6). |
| `signer_pubkey` | 32 B hex | Ed25519 verifying key per RFC 8032. |
| `signer_id` | string | Bound identity string, as for a group receipt (§2.4.1, §7.1). |
| `parent_receipt_hash` | 32 B hex or `null` | `link_hash` of this signer's previous claim; `null` for its first. |
@ -386,6 +393,7 @@ key.
| `operating-point` | `claim` | `object` or `receipt`, as its figure profile fixes | §2.4.9 | the party that records the request, the commitment and the delivery |
| `stage` | `claim` | `object` (the stage set's base) | §2.4.10 | the party that observed the stage set |
| `delivery-gap` | `claim` | `object` (the object not forwarded) | §2.4.11 | the relay that did not forward it |
| `reconstruct-session` | `claim` | `object` (the session's entry object) | §2.4.12 | the sink that reconstructed: the key its decoder host holds |
Rules:
@ -915,6 +923,232 @@ stops at step 5: it establishes that the relay says it did not forward an object
this content hash, not that the publisher sent one. An accepted claim says nothing
about whether the object reached anyone by another path.
#### 2.4.12. `class: "reconstruct-session"`
A sink that reconstructed a title **MAY** record what it did in a `reconstruct-session`
claim: the decoders it used, against which parameter sets and on which host; which path
each unit took and why it switched; the presentations it made after their deadline; a
commitment to every output it presented; the bytes it counted and received; the declared
length of the classical rendition it did not need; the `stage` claims it signed about the
stage sets it observed; and, where its decode was metered, the decode's energy. Its
subject is the §2.1 content hash of the session's **entry object**: the first envelope
the session dispatched. It is signed by the sink, with the key its decoder host holds. It
asserts what that key reports and nothing more, and it is not evidence that a person
watched.
A session claim and a `stage` claim (§2.4.10) are about different things, and neither
repeats the other. A `stage` claim is about one stage set: each layer object with its
status and the evidence of its receipt, the units presented at each stage on time, the
conflicts and deadline misses, the delivered stage and the set's envelope bytes. A session
claim is about a title, which can hold several stage sets and envelopes that are no part of
one. It names the `stage` claims its sink signed about the title's sets (`stage_sets`), and
commits to every output it presented across the title, late ones included
(`output_root`). A unit's highest stage is read from the outputs, and a set's layer objects
from its `stage` claim. Step 6 checks the two against each other.
| member | type | meaning |
|---|---|---|
| `session_nonce` | 32 B hex | Fresh random bytes per session, derived from nothing that identifies a person, a device or an account. |
| `media` | string | The entry object's manifest `media` (SPEC §3), as written: 1 to 32 of `a`–`z`, `0`–`9` and `-`. |
| `host` | object | The decoder host (below). |
| `decoders` | array | The decoder entries (below), in **first-use order**: taking the presentations in order of (`layers`, `unit`), each entry is first named after the entry before it. |
| `units` | integer | The units presented at least once. |
| `leaf_count` | integer | The presentations: one per (unit, layers) presented. |
| `output_root` | 32 B hex or `null` | The root of the output tree over the presentations ([staged-measurement](staged-measurement.md) Appendix C), each leaf's `decoder` an index into `decoders`. `null` exactly when `leaf_count` is 0. |
| `primary_units` | integer | The units whose presentation at their largest `layers` came through a decoder entry whose `path` is `primary`. |
| `fallback_units` | integer | The same, for `fallback`. |
| `switches` | array | At most 1024 path switches, each `{ "unit": integer, "from": string, "to": string, "reason": string }`, in order of `unit`: `from` and `to` are `primary` or `fallback`, and `reason` is a term of [delivery-telemetry](delivery-telemetry.md) §5. |
| `switches_total` | integer | Every switch, including any not written. |
| `deadline_us` | integer or `null` | The time, in microseconds, within which the sink's schedule had each unit presented; `null` where it ran under none. |
| `missed` | integer | The presentations made after their unit's deadline. |
| `missed_units` | array | At most 256 of them, each `{ "unit": integer, "layers": integer, "late_us": integer }`: which presentation, and how many microseconds late; in order of (`unit`, `layers`). |
| `stage_sets` | array | For each `stage` claim the sink signed about a stage set, `{ "first_unit": integer, "claim": 32 B hex }`: the title's unit that is the set's unit 0, and that claim's `link_hash`; in order of (`first_unit`, `claim`). A sink signs one for each epoch of a set — before it forgets the set's base (staged-delivery §4.4, §14), and at the session's end — and names them all. |
| `framing` | string | The framing label of the counted members ([staged-measurement](staged-measurement.md) §3, its §3.3): `registered`. |
| `counted_primary` | integer | The counted bytes (staged-measurement §3) of the countable objects received for the `primary` path. |
| `counted_fallback` | integer | The same, for `fallback`. |
| `counted_discarded` | integer | The counted bytes of the countable objects received and never presented. They are in the two members above as well. |
| `uncounted` | integer | The objects received that are not countable (staged-measurement §3.2). Their bytes are in no counted member. |
| `transferred` | integer | Bytes of WAI objects as received, whole or as byte ranges, without transport or receipts. |
| `receipt_bytes` | integer | Bytes of receipts and claims received (staged-measurement §3.5). |
| `classical_declared` | integer or `null` | The sum, over the `WAI2` envelopes the session received whose classical rendition (below) is of a capability no decoder entry decoded, of that rendition's length as the envelope's rendition table declares it (SPEC §7.1). `null` where there is none. Never estimated. |
| `classical_source` | term or `null` | Where `classical_declared` comes from: `wai2-table`, or a term a later extension registers here. |
| `energy` | object or `null` | `null` when the decode was unmetered. Otherwise `{ "joules_micro", "energy_provenance", "energy_uncertainty", "energy_counter_filtering", "energy_calibration_ref" }`, each written as §2.4.5 writes it (below). |
The **host** is `{ "profile": string, "target": term or null, "build": object, "threads":
integer }`. `profile` is `native-ffi` (a native process), `browser-baseline` (a browser
decoding on its processor), `browser-gpu` (a browser decoding on its graphics processor)
or `edge-wasi` (a WebAssembly host outside a browser), until an extension of sink
profiles registers the set; `target` the platform the host ran on; `build` the build of
its decoders, as §2.4.8 writes a build; and `threads` its decode workers, written as the
largest power of two not above their number, so that it does not single out a machine.
Where `energy` is not `null`, those are the workers of the bracket behind the figure,
which ran on a power of two of them: its `in_flight` is `threads` (staged-measurement
§8.7), so the `measurement` claim on the figure names no more of the machine (§6.7).
A **decoder entry**:
| member | type | meaning |
|---|---|---|
| `capability` | term | The capability decoded. |
| `rendition` | integer or `null` | Its `WAI2` rendition index (SPEC §7.1): `capability` is that rendition's capability. `null` for a `WAI1` envelope. |
| `object` | 32 B hex | The content hash (§2.1) of the object it decoded from: the `WAI2` envelope whose rendition it is, or the `WAI1` envelope — a base, or a layer that refines one. An entry decodes one object, so a sink that decodes several through one decoder names an entry for each. |
| `path` | string | `primary` or `fallback`. |
| `decoder` | string | `builtin:<crate>@<version>` — `<crate>` 1 to 64 of `a`–`z`, `0`–`9`, `_` and `-`, `<version>` 1 to 64 of ASCII letters, digits, `.`, `+` and `-`, beginning with a digit — or `module:sha256:` followed by a module's digest (§7.9), 64 lowercase hexadecimal digits. |
| `variant` | term or `null` | A decode path within the decoder, where it has more than one, such as an exact and a floating-point path. |
| `pin` | 32 B hex or `null` | The `sha256` of the parameter-set pin the sink verified (SPEC §3.1). |
| `tier` | string or `null` | That pin's declared tier as SPEC §3.1 reads it: `decode-equivalence`, `entropy-consistency` or `none`. `null` exactly when `pin` is. |
| `module_status` | string or `null` | For a `module:` decoder, the anchor its grant was confirmed under (§7.9): `pinned` or `fetched`. `null` for a `builtin:` decoder. |
| `leaves` | integer | The presentations decoded through the entry. |
`energy` is the decode energy of one completion of the presentations the claim commits
to — the decodes behind its `leaf_count` leaves, the work unit
[staged-measurement](staged-measurement.md) §8 names (its §8.1) — on the host's
processor rails (its §8.4), with its acquisition class. It is the figure of profile
`moq-jwp:claim-link` ([energy-measurement](energy-measurement.md) §6.1), named by this
claim's `link_hash`, and its producer publishes its report in a `measurement` claim
(§2.4.8) signed with the same key (energy-measurement §6). A decode that was not metered
has `energy` `null`, never a figure of 0 (energy-measurement §2).
§3.1 step 2 reads the members in the table's order, and the members of the host, of
each decoder entry, switch, `missed_units` entry and `stage_sets` entry, and of `energy`
and its `energy_uncertainty`, as it reaches them; it checks their types, and
`host.profile`, `host.build.profile`, `path`, `from`, `to` and `framing` against the
values the tables give them.
Rules, checked at §3.1 step 4 in this order:
1. `primary_units` and `fallback_units` **MUST** sum to `units` (`session_path_sum`).
2. The entries' `leaves` **MUST** sum to `leaf_count`; `leaf_count` **MUST NOT** be below
`units`; and `units` is 0 exactly when `leaf_count` is (`session_leaf_sum`).
3. `output_root` **MUST** be `null` exactly when `leaf_count` is 0
(`session_root_presence`).
4. `threads` **MUST** be a power of two (`session_host_malformed`).
5. Each decoder entry **MUST** be as its table says: `decoder` of its grammar;
`rendition` at most 65535; `tier` `null` exactly when `pin` is, and otherwise one of
its three values; `module_status` `null` exactly for a `builtin:` decoder, and
otherwise `pinned` or `fetched`; `leaves` at least 1. No two entries **MAY** be equal
in every member but `leaves` (`session_decoder_malformed`).
6. A `tier` that is not `null` **MUST NOT** exceed the class tier of its entry's
`capability` (SPEC §3.1 *Tier*) (`session_tier_exceeds_class`).
7. Each switch's `from` **MUST** differ from its `to`, and its `reason` **MUST** be a
term of delivery-telemetry §5. The switches **MUST** be in non-decreasing order of
`unit`, and as many as `switches_total`, or 1024 where it is more
(`session_switch_invalid`).
8. `missed` **MUST NOT** exceed `leaf_count`, and **MUST** be 0 when `deadline_us` is
`null`. `missed_units` **MUST** hold as many entries as `missed`, or 256 where it is
more, in strictly increasing order of (`unit`, `layers`), each with `layers` from 1 to
65535 and `late_us` at least 1 (`session_missed_invalid`).
9. `stage_sets` **MUST** be in strictly increasing order of (`first_unit`, `claim`)
(`session_stage_sets_invalid`).
10. `counted_discarded` **MUST NOT** exceed the sum of `counted_primary` and
`counted_fallback` (`session_counted_invalid`).
11. `classical_source` **MUST** be `null` exactly when `classical_declared` is, and
otherwise `wai2-table` (`session_classical_invalid`).
12. Where `energy` is not `null`, its `joules_micro` **MUST NOT** be 0
(`energy_zero_figure`), and its class is admitted as §2.4.5's rule 2 admits one
(`energy_unknown_acquisition`, `energy_legacy_acquisition`,
`energy_acquisition_evidence`).
**Against the outputs** (§3.1 step 6). The context is the list of presentations, each
`{ "unit", "layers", "decoder", "out_digest" }`: a unit, the layers it was presented
at, the index of the decoder entry that produced it, and the SHA-256 of its output
(staged-measurement Appendix C). In order:
1. The list has `leaf_count` entries and no (`unit`, `layers`) twice, and the root of its
output tree is `output_root` (`session_output_mismatch`).
2. It agrees with the members it gives (`session_output_inconsistent`): every `decoder`
names an entry and every `layers` is at least 1; its distinct units number `units`;
each entry's `leaves` is the number of presentations naming it; the entries are in
first-use order; `primary_units` and `fallback_units` count the units whose
presentation at their largest `layers` names an entry on that path; and every
`missed_units` entry names a presentation of the list.
3. A verifier that holds the objects **MAY** decode a presentation again where its
entry's `capability` has the replicate class `neural-int-exact` or `standard-defined`
(SPEC §7), and then **MUST** find its `out_digest`
(`session_output_irreproducible`). Every other presentation is checked for
membership only.
**Against the objects** (§3.1 step 6). The context lists each object received:
`{ "content_hash", "path", "counted", "transferred", "presented" }`, with `counted` the
object's counted bytes, or `null` for one that is not countable. In order:
1. The session's `subject`, its entry object, is the content hash of an object received
(`session_entry_not_received`).
2. So is each decoder entry's `object` (`session_source_not_received`).
3. `counted_primary` and `counted_fallback` are the sums of `counted` over the objects of
each path, `counted_discarded` its sum over the objects not presented, `uncounted` the
number of objects whose `counted` is `null`, and `transferred` the sum of
`transferred` (`session_counted_mismatch`).
**The classical rendition** of a `WAI2` envelope is the first of its renditions, in
manifest order, that is selectable — not a companion or a refinement, and a capability a
sink may dispatch (SPEC §7.1) — and whose capability is a classical one of the envelope's
media: one the codec glossary (`glossary/codecs.json`) links from an entry of era
`classical`, whose registry media is the manifest's `media`, and which is not text. WAI's
own formats, the neural codecs, records, parameter sets and text capabilities are never
classical. An envelope has one classical rendition or none, and a verifier reads it from
the envelope; nothing names it.
**Against the envelopes** (§3.1 step 6). The context is the bytes of envelopes the
session received: every `WAI2` envelope among them, and any `WAI1` envelope — a base or
a layer — the verifier holds. In order:
1. Where the verifier holds the objects as well, each envelope is among the objects
received, by its content hash (`session_source_not_received`).
2. Each reads as a `WAI1` or `WAI2` envelope (SPEC §2, §7.1) whose payloads are
plaintext. An encrypted one is inert at a sink that cannot open it (SPEC §6.4), and a
`security` member that is not a security object leaves it inert too
(`session_envelope_unreadable`).
3. Each decoder entry whose `object` is one of the envelopes names what that envelope
gives: of a `WAI2` envelope, a `rendition` and the capability that rendition has; of
a `WAI1` envelope, no `rendition` and the manifest's `capability` or its `fallback`.
And each decoder entry whose `rendition` is not `null` has its `object` among the
envelopes, which hold every `WAI2` envelope the session received
(`session_source_mismatch`). A signer cannot relabel a decoder entry whose envelope
the verifier holds, and so cannot move a presentation through it out of the reach of
the re-decode check above.
4. The lengths of the `WAI2` envelopes' classical renditions whose capability no decoder
entry decoded sum to `classical_declared`, which is `null` exactly when there is none
(`session_classical_mismatch`).
**Against the stage claims** (§3.1 step 6). The context is the wire form of each `stage`
claim the verifier holds of those `stage_sets` names. A set has a claim for each of its
epochs, and bases that cover the same units of the title each have their own. In order:
1. each passes §3.1 steps 1–4 (`context_invalid`);
2. each is a `stage` claim, signed by this claim's `signer_pubkey`, and its `link_hash` is
the `claim` of an entry of `stage_sets` (`context_mismatch`);
3. where the verifier holds the objects as well, each one's `subject`, the set's base, and
every layer object it lists as arrived — `applied`, `partial`, `late` or `refused`,
with bytes — are among the objects received (`session_stage_disagrees`);
4. where the verifier holds the outputs as well, for each claim and each entry of
`stage_sets` naming it, with `f` that entry's `first_unit` and `n` the units the claim
counts (`session_stage_disagrees`):
- every unit from `f` to `f + n − 1` is presented;
- from below: for every stage `s` from 1 up, the units among them presented at more
than `s` layers are no fewer than the units the claim counts at stage `s` or above. A
unit presented on time at a stage was presented at every lower stage before it, and
late presentations are leaves of the tree as well, so the tree can hold more than a
`stage` claim counts and never less;
5. where it holds the outputs, from above: every unit of an entry's units presented at
more than `s` layers lies in the span of a layer-`s` object that a claim named by an
entry holding the unit lists as `applied` or `partial` (`session_stage_disagrees`). A
unit reaches a stage only through a layer the sink applied: a `late` layer missed the
deadline of every unit it covers and a `refused` one applied to none (§2.4.10). The
claims of every epoch count together, so a unit
refined before the sink forgot its base is bounded by the claim of that epoch, and
the tree never holds a stage the claims' applied layers cannot have given.
An accepted claim establishes that its signer reported this session. Its outputs are
committed to by the tree. A presentation through a decoder entry whose capability is
byte-exact is reproducible, and a verifier that decodes it again checks it. An entry's
capability is the signer's statement, except where the verifier holds the envelope of the
object it names, which binds it; that the entry's outputs were decoded from that object,
and not from another the session received, is the signer's statement. Its byte totals are checkable against the
objects, its classical length against the envelopes, and its stage sets against their
`stage` claims, both ways. Its timings, missed deadlines, switches and energy are its
signer's statements (energy-measurement §5). Nothing binds a `stage` claim to a session
beyond its base and its key: a `stage` claim the same key signed about the same base in
an earlier session of the title can be named, and the checks above hold it as they hold
one of this session. The `claim` an
`end` observation carries ([delivery-telemetry](delivery-telemetry.md) §2, its §2.1)
is this claim's `link_hash`.
### 2.5. Worked example
Manifest JSON (group receipt, formatted for readability — wire form
@ -1379,7 +1613,8 @@ order listed.
is not 64 of them; a value outside an enumeration this extension
defines here (`record`, `binding_type`, a bracket's `opening`, a build's
`profile`, a stage's `tier`, a stage object's `status` and `evidence`, a
gap's `reason`); a date-time that is not one of §2.4.3 (a gap's
gap's `reason`, a session's `framing`, its host's `profile`, and the paths
of its decoder entries and switches); a date-time that is not one of §2.4.3 (a gap's
`observed_at`); a URI that is not absolute;
a term that is not one of §2.4.3 (empty, longer than 2048 bytes, or
holding a White_Space or control character); or a malformed
@ -1404,10 +1639,13 @@ order listed.
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; and, in a `stage`
which points and predictions are present together; in a `stage`
claim, the vocabulary, a point's parameter and range, a reason of
staged-delivery's, and how the units, the objects and the delivered stage
agree.
agree; and, in a `reconstruct-session` claim, how its counts agree, a
decoder entry's grammar and tier, the switches' reasons and order, the
missed presentations, the order of its stage sets, its counted bytes and
its classical rendition.
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
@ -1431,7 +1669,7 @@ order listed.
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
4. **Class rules**, per §2.4.5–§2.4.12 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.
@ -1459,7 +1697,10 @@ order listed.
publisher key of a verified delivery receipt for the declaration's
subject (§2.6.3), and never against the declaration alone; a
`delivery-gap` claim against the object receipt at its coordinates,
verified by inclusion at least (§2.4.11). A
verified by inclusion at least (§2.4.11); a `reconstruct-session` claim
against its outputs, the objects it received, the `WAI2` envelopes it received
and the `stage` claims it names, in that order, each that the verifier holds, the
`stage` claims verified under steps 1–4 (§2.4.12). A
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
@ -1488,7 +1729,7 @@ 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_zero_figure` | `joules_micro` is 0 in an `energy`, `measurement` or `operating-point` claim, or in a `reconstruct-session` claim's `energy` |
| 4 | `energy_unknown_acquisition` | `energy_provenance` names no class of energy-measurement §2 |
| 4 | `energy_legacy_acquisition` | `energy_provenance` is the legacy `HwShunt` |
| 4 | `energy_acquisition_evidence` | the class's evidence is missing, carried by a class that has none, or below the resolution floor (§2.4.5 rule 2) |
@ -1528,6 +1769,17 @@ and never folds it in: confirmation changes the outcome of no step here.
| 4 | `stage_objects_malformed` | a reported object out of order, at layer 0 or above 255, over no unit (a `partial` one over fewer than two), outside the units when its span was checked, with a reason where none belongs or none where one does; applied or partial with evidence other than `delivered-verified` beside a stated `op_delivered`; dropped with `delivered-verified` evidence; arrived with no bytes or dropped with some; objects counting no fewer bytes than the set; or two `applied` objects of one layer over one unit |
| 4 | `stage_units_unsupported` | an `applied` object of layer *s* outside the spans of layer *s* − 1's `applied` and `partial` objects, or a `partial` one that does not meet them; or more units at stage *s* or above than layer *s* can have reached, a `partial` object counting no more than its span less one |
| 4 | `stage_delivered_disagrees` | `op_delivered` is not the stage the reported objects give |
| 4 | `session_path_sum` | a session's `primary_units` and `fallback_units` do not sum to `units` |
| 4 | `session_leaf_sum` | the decoder entries' `leaves` do not sum to `leaf_count`, `leaf_count` is below `units`, or one of `units` and `leaf_count` is 0 and the other is not |
| 4 | `session_root_presence` | `output_root` is `null` with presentations, or a hash with none |
| 4 | `session_host_malformed` | the host's `threads` is not a power of two |
| 4 | `session_decoder_malformed` | a decoder entry's `decoder` is not of its grammar, its `rendition` is above 65535, its `tier` is present without `pin` or absent with it or is not a tier, its `module_status` does not fit its decoder, or its `leaves` is 0; or two entries differ only in `leaves` |
| 4 | `session_tier_exceeds_class` | a decoder entry's `tier` exceeds its capability's class tier |
| 4 | `session_switch_invalid` | a switch between equal paths or for a reason delivery-telemetry §5 does not list, switches out of order, or other than `switches_total` written, 1024 where it is more |
| 4 | `session_missed_invalid` | `missed` above `leaf_count`, or not 0 with no deadline; `missed_units` holding other than `missed` entries, 256 where it is more, out of order, or an entry with `layers` 0 or above 65535 or `late_us` 0 |
| 4 | `session_stage_sets_invalid` | `stage_sets` out of order, or an entry twice |
| 4 | `session_counted_invalid` | `counted_discarded` above `counted_primary` and `counted_fallback` together |
| 4 | `session_classical_invalid` | `classical_source` present without `classical_declared` or absent with it, or not `wai2-table` |
| 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` |
@ -1538,7 +1790,7 @@ and never folds it in: confirmation changes the outcome of no step here.
| 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 | `context_invalid` | a receipt a claim is checked against — a declaration a decision or a recovery record names, the receipt that carries a figure, or a `stage` claim a session names — does not verify |
| 6 | `decision_declaration_mismatch` | a decision does not match the declaration it names |
| 6 | `decision_purpose_not_declared` | a decision is about a purpose its declaration does not list |
| 6 | `recovery_declaration_mismatch` | a recovery record does not match the declaration it names |
@ -1546,6 +1798,16 @@ and never folds it in: confirmation changes the outcome of no step here.
| 6 | `recovery_value_mismatch` | the value does not match under the algorithm's rule |
| 6 | `recovery_no_matcher` | the verifier has no matcher for the algorithm |
| 6 | `gap_subject_mismatch` | the object receipt at a `delivery-gap` claim's coordinates names another object |
| 6 | `session_output_mismatch` | a session's presentations are not `leaf_count`, repeat a (unit, layers), or give another `output_root` |
| 6 | `session_output_inconsistent` | the presentations name an entry that does not exist or a `layers` of 0, or give another `units`, another entry's `leaves`, another first-use order, other `primary_units` or `fallback_units`, or no presentation for a `missed_units` entry |
| 6 | `session_output_irreproducible` | a presentation decoded again, through a decoder its replicate class makes byte-exact, gives another `out_digest` |
| 6 | `session_entry_not_received` | a session's entry object is not among the objects it received |
| 6 | `session_source_not_received` | a decoder entry's `object`, or an envelope held as the session's, is not among the objects it received |
| 6 | `session_counted_mismatch` | the objects received give other counted, uncounted or transferred bytes |
| 6 | `session_envelope_unreadable` | an envelope held as the session's is not a `WAI1` or `WAI2` envelope whose payloads are plaintext |
| 6 | `session_source_mismatch` | a decoder entry names other than its object's envelope gives — another rendition, a capability that rendition or that `WAI1` manifest does not have, or a rendition of a `WAI1` envelope — or names a rendition and its envelope is not held |
| 6 | `session_classical_mismatch` | the envelopes' classical renditions the session did not decode give another `classical_declared` |
| 6 | `session_stage_disagrees` | a `stage` claim the session names is about a base, or lists an arrived layer object, the session did not receive; counts a unit the session never presented, or more units at a stage than the session presented at it or above; or no claim whose set holds a unit lists an applied layer that reaches a stage the session presented it at |
### 3.2. Recovery through a soft binding, and the ancestor rule
@ -1993,6 +2255,41 @@ The corpus of §5.8 checks the authorities both classes need (§7.5).
bodies around every rule and gaps against receipts at and away from their
coordinates.
### 5.11. Session claims (`claim-conformance/`)
The corpus of §5.6 holds the class of §2.4.12: an unmetered session (`energy` `null`)
and a session metered under `OnChipCounter` with its evidence, each accepted against
its outputs, the objects it received, its base's `WAI1` envelope and the `WAI2` envelope
it received, and the `stage` claim it names; output trees from 1 to 33 leaves; a
presentation whose re-decode, as the vector states it, gives another digest through a
byte-exact decoder (`session_output_irreproducible`) and through a decoder checked for
membership only (accepted); a decoder entry relabelled to escape that check by each
route — the base's `WAI1` entry, a `WAI2` entry with its rendition kept or nulled, a
capability another envelope has — refused against the envelope of the object it names
(`session_source_mismatch`), and an envelope held that the session did not receive and
an entry's object it did not receive (`session_source_not_received`); the classical
rendition read from envelopes whose second rendition is of each kind a rule excludes and
of two it admits; an encrypted envelope, a `WAI1` envelope with no manifest members and
bytes of neither magic (`session_envelope_unreadable`); a `stage` claim that verifies, one that does not, one by another key,
one the session does not name, one about a base the session never received, one whose
arrived layers the session never received, and sessions presenting stages its applied
layers cannot have given — through layers it lists as late or refused among them — and
fewer than it counts; a base forgotten and decoded again (`staged-conformance/`'s
`record_budget`), accepted naming each epoch's claim and refused naming only the last;
every reason of §2.4.12's rules and
checks, the caps of the switches and missed presentations written at and past them; a
metered session's figure of zero (`energy_zero_figure`) and every acquisition-class
reason; and step 2's enumerations and order. `claim_verify.py` gives every outcome.
`differential.py` generates sessions around every rule — units, leaves and paths off by
one, switches and missed presentations at and past their caps, a `null` deadline with
misses, presences that disagree, decoder indices out of range, leaves moved between
entries, entries out of first-use order, stage claims against presentations that reach,
miss and pass their stages and objects that hold or lack their layers, every acquisition
class — with their outputs and objects; envelopes whose manifests every reader may read
otherwise; and every registered capability as an envelope's second rendition. It requires
the two verifiers to agree. The corpus of §5.8 checks the authority a session needs, and
the authorities its energy figure needs (§7.5).
---
## 6. Security considerations
@ -2149,6 +2446,16 @@ authority. A confirmed claim is worth exactly as much as that
confirmation; an unconfirmed one, as much as the verifier's own reason
to trust the key that signed it.
A `reconstruct-session` claim says what a sink played. It carries no
identifier of a person, a device or an account: its `session_nonce` is fresh
for each session, and its host states a profile, a platform, a build and a
rounded worker count; a `measurement` claim on its figure states its bracket's workers
as that same count. A sink key used for many sessions links them, so a sink
that does not mean its sessions to be linked signs each with a key of its own,
under a `did:key` (§7.1). Where its claim goes is the sink's choice; delivery
telemetry carries only its `link_hash`
([delivery-telemetry](delivery-telemetry.md) §2, §8).
### 6.8. Soft binding
- **A copied watermark.** A watermark decoded from an object shows
@ -2439,7 +2746,8 @@ confirmation later keeps those revisions and its anchor record.
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
`claim` accepted under §3.1, and the energy figure of an accepted
`reconstruct-session` claim, which is an item of its own (§7.5); a receipt of another WAI family whose signed
bytes include its `signer_pubkey` and `signer_id` and whose own
verification passed; or a module grant whose signature verified (§7.9).
Its inputs are the item's `signer_pubkey`, `signer_id`, the authorities
@ -2536,7 +2844,8 @@ 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`, `operating-point`, `stage` and `delivery-gap`. And `energy/` followed by its `energy_provenance` whenever its body carries that member, whatever its class |
| 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`, `operating-point`, `stage`, `delivery-gap` and `reconstruct-session`. And `energy/` followed by its `energy_provenance` whenever its body carries that member, whatever its class |
| the energy figure of a `reconstruct-session` claim whose `energy` is not `null` | `reconstruct-session`, and `energy/` followed by `energy.energy_provenance` |
| 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` |
@ -2563,7 +2872,11 @@ 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.
sink's module origins. A `reconstruct-session` claim is a sink's statement
about an object a publisher delivered, so its subject's origin is not the
sink's: a verifier expects an origin for a session only where its relying
party names one. A session signed under a `did:key` is unconfirmed (§7.4 step
1), which is the normal case for a sink's ephemeral key.
§2.4.4's *signed by* column names the party each class expects. Which of
its keys is that party is the origin's decision, and its log records it:
@ -2606,6 +2919,19 @@ its keys is that party is the origin's decision, and its log records it:
(§2.4.11). Neither is pinned-only, and neither is `delivery`: an origin gives
`stage` to the sinks it speaks for and `delivery-gap` to its relays, and a key
that holds `delivery` alone signs neither.
- **Reconstruct session.** `reconstruct-session` lets a key sign what a sink
reconstructed (§2.4.12). It is not pinned-only and it is not `delivery`: an
origin gives it to the sinks it speaks for. A session's energy figure is a
second item, since the session's other members stand without it: it needs
`reconstruct-session` and the energy authority of its class, every one of
them, and is reported with `also_delivers` where that class is a hardware
class. A verifier reports the session's confirmation and its figure's side by
side, and **MUST NOT** report the figure as its origin's unless it confirmed the
figure. A key that holds `reconstruct-session` and not the figure's energy
authority confirms a session whose figure is refused (`authority_not_granted`);
under a fetched anchor, a hardware-class figure is unconfirmed
(`anchor_not_pinned`) beside a confirmed session. The origin then stands behind
the session, and not behind its figure.
- **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.

View file

@ -6,8 +6,10 @@
> layering overhead is stated, how decode time and energy are reported, how
> deduplication is computed, how delivery is simulated, and what a record carries.
> Reference impl: `wai-rs` — `measure_framing`, `quality`, `output_tree`,
> `content_dedup`, `bench_env`, `stage_measure::{title, decode, run, sim}`; the
> `wai_stage_measure` tool; the corpus
> `content_dedup`, `bench_env`, `stage_measure::{title, decode, run, sim, session}`,
> `session_recorder`, `energy_report::rail_liveness`,
> `energy_burst::counter_power_samples`; the `wai_stage_measure` tool, whose `bench`
> measures a title and seals its session claim (§8.7); the corpus
> [`staged-measure-conformance/`](../staged-measure-conformance/). Keywords **MUST**,
> **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
@ -233,6 +235,27 @@ reading from a whole-system rail that includes a display is not published.
§8.5 A rail that is not live in a bracket (energy-measurement §3.1) gives no figure; the
decode is unmetered.
§8.6 Before a tool brackets a decode, it reads the power reader at rest and checks that
the rail is live there under the same rule: a reader that gives no sample is absent, and
one whose samples do not show a live rail is stale. Either way the tool brackets nothing,
and the decode is unmetered with that reason. A reader of an energy counter gives power
as the counter's successive differences over the time between its reads, across the
counter's wrap. A counter reset to a low value reads as a wrap of nearly its whole range
in one interval, so an interval that cannot be a reading ends the samples there: one
implying more power than a processor rail draws; one longer than ten sampling intervals,
across which a reset implies less; and one whose difference is more than half the
counter's range. The reference reader's power bound is 5 kW.
§8.7 A sink records a session's decode figure in its session claim
([jwp-receipts](jwp-receipts.md) §2.4.12): the energy of one completion of the
presentations the claim commits to, under §8.1–§8.5, or `null` where the decode was
unmetered. A tool that signs a session claim seals a figure in it only where its verdict
is clean (§8.3), and publishes the bracket in a `measurement` claim about the session
claim, signed with the same key (energy-measurement §6). The bracket's burst runs on a
power of two of workers, and the session's host states them: its `threads` is the
bracket's `in_flight`, so the `measurement` claim names no more of the machine than the
session does (jwp-receipts §6.7).
## 9. Deduplication
§9.1 Over a set of objects:
@ -576,9 +599,11 @@ node = SHA-256("wai:out-node\x01" ‖ left ‖ right)
```
- `decoder` is the index of the decoder entry that produced the output: entries are
(capability, pin) pairs in first-use order, presentations taken by (layers, unit).
For a title, the base's capability with the pin it verified, then each refinement
capability.
(capability, pin, object) triples in first-use order, presentations taken by (layers,
unit), the object being the content hash ([jwp-receipts](jwp-receipts.md) §2.1) of
the envelope decoded. For a title, each base's capability with the pin it verified and
its envelope, then each layer's refinement capability and envelope. For a session
claim, the entries are its decoder entries (jwp-receipts §2.4.12), in the same order.
- Leaves are ordered by (unit, layers), and a (unit, layers) given twice is refused.
- The root over `n ≥ 1` leaves is RFC 6962's Merkle tree hash: one leaf is itself;
otherwise `node(MTH(D[0:k]), MTH(D[k:n]))`, with `k` the largest power of two below

View file

@ -21,7 +21,10 @@ reason, head or anchor path — does not conform.
**`confirm`** (§7.4) runs on an item and reports `confirmed` with the origin, key-id,
head `revision_hash` and anchor path — and, where they apply, `moved_to`,
`retired_until`, `untimed=true`, `expected=` and `also_delivers=` — or `unconfirmed` or
`refused` at a §7.4 step (`1`–`7`, or `6a`) with a reason of §7.7. The item is either
`refused` at a §7.4 step (`1`–`7`, or `6a`) with a reason of §7.7. For a
`reconstruct-session` claim whose `energy` is not `null`, the verdict goes on with
`; figure ` and the confirmation of its energy figure, an item of its own (§7.5) that needs
`reconstruct-session` and the energy authority of its class. The item is either
`receipt`, a claim's wire form, which is first verified under §3.1 (a claim that fails
is `reject step=<n> reason=<label>`, and is never confirmed), or `context.item`: the
`kind` (`group` or `family`), `signer_pubkey`, `signer_id`, `classes` (for a group,
@ -64,8 +67,9 @@ to, 0x63 and 0x66 delivery keys (0x66 also meters), 0x64 and 0x69 metering keys,
and 0x6e lawful-basis keys, 0x6b and 0x6c module-granting keys, 0x60 a key added later,
0x67, 0x6a and 0x6f keys an attacker holds, 0x70, 0x71 and 0x72 the root, committed key
and module-granting key of a second origin, 0x73 and 0x74 a measurement key and an
operating-point key, 0x75 a key that signs family receipts with a ModelBased figure, and
0x76 and 0x77 a sink's key for `stage` and a relay's for `delivery-gap`. jwp-receipts §7.8 says such a key must
operating-point key, 0x75 a key that signs family receipts with a ModelBased figure,
0x76 and 0x77 a sink's key for `stage` and a relay's for `delivery-gap`, and 0x78 and 0x79
a sink's keys for `reconstruct-session`, the second also holding `energy/OnChipCounter`. jwp-receipts §7.8 says such a key must
never be listed in a real key log: every anchored verdict here carries
`note=published_test_key`, which is what a verifier reports on finding one. Origins are
reserved example names (`publisher.example`, with and without port 8443,

View file

@ -423,6 +423,14 @@ def claim_authorities(c):
return out
def figure_authorities(c):
"""§7.5: the energy figure of a `reconstruct-session` claim is an item of its own,
needing `reconstruct-session` and the energy authority of the class its `energy`
writes; None for a claim of another class, or a session that was not metered."""
e = c["body"].get("energy") if c["class"] == "reconstruct-session" else None
return None if e is None else ["reconstruct-session", "energy/" + e["energy_provenance"]]
def item_authorities(kind, classes):
if kind == "group":
# delivery, and energy/<class> for each class the group's labelled leaves and
@ -645,13 +653,17 @@ def verdict(v):
except CV.Reject as r:
return f"reject step={r.step} reason={r.reason}"
pub, sid, auth, time = bytes.fromhex(cl["signer_pubkey"]), cl["signer_id"], claim_authorities(cl), cl["issued_at"]
figure = figure_authorities(cl)
else:
it = c["item"]
pub, sid, time = bytes.fromhex(it["signer_pubkey"]), it["signer_id"], it["time"]
auth = item_authorities(it["kind"], it["classes"])
auth, figure = item_authorities(it["kind"], it["classes"]), None
exp = c.get("expected_origin")
return confirm(pub, sid, auth, time, held_revisions(c["revisions"]), c["anchors"],
None if exp is None else [exp])
held, expected = held_revisions(c["revisions"]), None if exp is None else [exp]
out = confirm(pub, sid, auth, time, held, c["anchors"], expected)
if figure is not None:
out += "; figure " + confirm(pub, sid, figure, time, held, c["anchors"], expected)
return out
if v["procedure"] == "anchor":
cur = c.get("current")
current = (cur["anchor"], held_revisions(cur["held"])) if cur else None

View file

@ -4237,6 +4237,144 @@
]
},
"expected": "confirmed origin=https://publisher.example key_id=rl head=9bf903e081921a6a321c8dc427369f53640d12828a8c813f79d701fe3e43bfd5 anchor=fetched"
},
{
"name": "session_needs_its_class",
"about": "an unmetered session by a key holding delivery and receipt: delivering objects is not reconstructing them",
"procedure": "confirm",
"receipt": "{\"body\":{\"classical_declared\":null,\"classical_source\":null,\"counted_discarded\":0,\"counted_fallback\":0,\"counted_primary\":4000,\"deadline_us\":null,\"decoders\":[{\"capability\":\"wai.neural.int_hyper\",\"decoder\":\"builtin:[email protected]\",\"leaves\":1,\"module_status\":null,\"object\":\"ca24d5e6429bfd54aaac853e291d82491fb5304ae050120c8f797ee4f27f7a82\",\"path\":\"primary\",\"pin\":\"1d279149536528e910f4da130bb6473bbcc76df60a933030e9750aabee9d4ae7\",\"rendition\":null,\"tier\":\"decode-equivalence\",\"variant\":null}],\"energy\":null,\"fallback_units\":0,\"framing\":\"registered\",\"host\":{\"build\":{\"profile\":\"release\",\"target\":null,\"toolchain\":null},\"profile\":\"native-ffi\",\"target\":null,\"threads\":1},\"leaf_count\":1,\"media\":\"image\",\"missed\":0,\"missed_units\":[],\"output_root\":\"f422613aae3364b94e36f4d9df35224db0b02f0c16259b364b4e357d306b3c7c\",\"primary_units\":1,\"receipt_bytes\":0,\"session_nonce\":\"b2b2ef087cf5a13c070b6da6801b114b1f8af06e94b9d264e2398db989125d35\",\"stage_sets\":[],\"switches\":[],\"switches_total\":0,\"transferred\":4000,\"uncounted\":0,\"units\":1},\"class\":\"reconstruct-session\",\"issued_at\":\"2026-09-18T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"d0aaa0d6d1ea47fc4110718b999f18dc30b94de24f200a605dbd205cb79464bb1dd737d37b82007b34e85ac4294d6e39e147d9e4cada43f7eef77edc9e0efc0a\",\"signer_id\":\"did:web:publisher.example#d1\",\"signer_pubkey\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"subject\":\"ca24d5e6429bfd54aaac853e291d82491fb5304ae050120c8f797ee4f27f7a82\",\"subject_type\":\"object\"}",
"context": {
"revisions": [
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-01T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":null,\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":0,\"sig\":\"44f48a651622c387576e25fd7f62e99a9409636794600f07a2635c2a3b09c8426d0bb87aa5716abb77274b46958e7a93722f3d8c5c15484f93522fb95bfe7205\",\"status_max_age_s\":3600}",
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-10T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"d2\",\"key\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"may_sign\":[\"delivery\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m2\",\"key\":\"e34d3a01b6112e1429ead61668405f4ef4be4f8853abee5079d28ac6c13ffdd0\",\"may_sign\":[\"energy/CalibratedInstrument\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op2\",\"key\":\"014e4d922c596a380adfc9eed722e835822f1236761693e4babb61e1afbb04f1\",\"may_sign\":[\"lawful-basis/decision\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modr\",\"key\":\"6d44f939f829d43598bcdedda90de01db69faa50caa33e8c6c328a4b900d4dac\",\"may_sign\":[\"module/reconstruct\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":\"c32f6eec946ad4c1df808293983ced11e6e56b79f62cb9cb843380118f294990\",\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":1,\"sig\":\"021c605ed69ec195bdba7ad60b625a82d3ac0bdb26771266f26b42bc2f730b95d6d04e599df12b596c9b938779e6a0ac27658f8881421f9b450722b59172b608\",\"status_max_age_s\":3600}",
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-17T06:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"d2\",\"key\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"may_sign\":[\"delivery\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m2\",\"key\":\"e34d3a01b6112e1429ead61668405f4ef4be4f8853abee5079d28ac6c13ffdd0\",\"may_sign\":[\"energy/CalibratedInstrument\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op2\",\"key\":\"014e4d922c596a380adfc9eed722e835822f1236761693e4babb61e1afbb04f1\",\"may_sign\":[\"lawful-basis/decision\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modr\",\"key\":\"6d44f939f829d43598bcdedda90de01db69faa50caa33e8c6c328a4b900d4dac\",\"may_sign\":[\"module/reconstruct\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"ss\",\"key\":\"ee31f83c88a71219a6fcf9bee0da9bc22620588f5a15a6145553504df9649e5c\",\"may_sign\":[\"reconstruct-session\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"se\",\"key\":\"4745d19c66ec9179fa25097056409ef16967b6a1a74bfd8f7afd528f59630615\",\"may_sign\":[\"reconstruct-session\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":\"07d45f0d191520d6911f76016392e1ab61a37bca3bf22a3e819d6ae5954c6058\",\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":2,\"sig\":\"22023479736f2f8e314c1413db4a64be40b3d49585863c4850ab6765efd4061d3a5a4df0d2dded9da544936fa3ed2604598a396f448b65fc06322056be85140e\",\"status_max_age_s\":3600}"
],
"anchors": [
{
"origin": "https://publisher.example",
"revision": "d0935bd1f41dea1d3014cac0d128c51aa101b534cad59eb6433ef10e2192d1c8",
"path": "fetched",
"acquired_at": "2026-09-25T00:00:00Z",
"pinned_revision": null
}
]
},
"expected": "refused step=6 reason=authority_not_granted"
},
{
"name": "session_confirmed",
"about": "an unmetered session by a sink's key holding reconstruct-session, under a fetched anchor: not pinned-only, and no figure to confirm",
"procedure": "confirm",
"receipt": "{\"body\":{\"classical_declared\":null,\"classical_source\":null,\"counted_discarded\":0,\"counted_fallback\":0,\"counted_primary\":4000,\"deadline_us\":null,\"decoders\":[{\"capability\":\"wai.neural.int_hyper\",\"decoder\":\"builtin:[email protected]\",\"leaves\":1,\"module_status\":null,\"object\":\"ca24d5e6429bfd54aaac853e291d82491fb5304ae050120c8f797ee4f27f7a82\",\"path\":\"primary\",\"pin\":\"1d279149536528e910f4da130bb6473bbcc76df60a933030e9750aabee9d4ae7\",\"rendition\":null,\"tier\":\"decode-equivalence\",\"variant\":null}],\"energy\":null,\"fallback_units\":0,\"framing\":\"registered\",\"host\":{\"build\":{\"profile\":\"release\",\"target\":null,\"toolchain\":null},\"profile\":\"native-ffi\",\"target\":null,\"threads\":1},\"leaf_count\":1,\"media\":\"image\",\"missed\":0,\"missed_units\":[],\"output_root\":\"f422613aae3364b94e36f4d9df35224db0b02f0c16259b364b4e357d306b3c7c\",\"primary_units\":1,\"receipt_bytes\":0,\"session_nonce\":\"b2b2ef087cf5a13c070b6da6801b114b1f8af06e94b9d264e2398db989125d35\",\"stage_sets\":[],\"switches\":[],\"switches_total\":0,\"transferred\":4000,\"uncounted\":0,\"units\":1},\"class\":\"reconstruct-session\",\"issued_at\":\"2026-09-18T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"97bbf4648716f5b483f60ae0e7125d2c9782b39a3c927606f9f392155a0c8ed88456bb13fabd3fb856e4f0a10bf3883b98b2a39c85d46fac0aa4032515b3980b\",\"signer_id\":\"did:web:publisher.example#ss\",\"signer_pubkey\":\"ee31f83c88a71219a6fcf9bee0da9bc22620588f5a15a6145553504df9649e5c\",\"subject\":\"ca24d5e6429bfd54aaac853e291d82491fb5304ae050120c8f797ee4f27f7a82\",\"subject_type\":\"object\"}",
"context": {
"revisions": [
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-01T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":null,\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":0,\"sig\":\"44f48a651622c387576e25fd7f62e99a9409636794600f07a2635c2a3b09c8426d0bb87aa5716abb77274b46958e7a93722f3d8c5c15484f93522fb95bfe7205\",\"status_max_age_s\":3600}",
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-10T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"d2\",\"key\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"may_sign\":[\"delivery\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m2\",\"key\":\"e34d3a01b6112e1429ead61668405f4ef4be4f8853abee5079d28ac6c13ffdd0\",\"may_sign\":[\"energy/CalibratedInstrument\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op2\",\"key\":\"014e4d922c596a380adfc9eed722e835822f1236761693e4babb61e1afbb04f1\",\"may_sign\":[\"lawful-basis/decision\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modr\",\"key\":\"6d44f939f829d43598bcdedda90de01db69faa50caa33e8c6c328a4b900d4dac\",\"may_sign\":[\"module/reconstruct\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":\"c32f6eec946ad4c1df808293983ced11e6e56b79f62cb9cb843380118f294990\",\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":1,\"sig\":\"021c605ed69ec195bdba7ad60b625a82d3ac0bdb26771266f26b42bc2f730b95d6d04e599df12b596c9b938779e6a0ac27658f8881421f9b450722b59172b608\",\"status_max_age_s\":3600}",
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-17T06:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"d2\",\"key\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"may_sign\":[\"delivery\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m2\",\"key\":\"e34d3a01b6112e1429ead61668405f4ef4be4f8853abee5079d28ac6c13ffdd0\",\"may_sign\":[\"energy/CalibratedInstrument\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op2\",\"key\":\"014e4d922c596a380adfc9eed722e835822f1236761693e4babb61e1afbb04f1\",\"may_sign\":[\"lawful-basis/decision\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modr\",\"key\":\"6d44f939f829d43598bcdedda90de01db69faa50caa33e8c6c328a4b900d4dac\",\"may_sign\":[\"module/reconstruct\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"ss\",\"key\":\"ee31f83c88a71219a6fcf9bee0da9bc22620588f5a15a6145553504df9649e5c\",\"may_sign\":[\"reconstruct-session\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"se\",\"key\":\"4745d19c66ec9179fa25097056409ef16967b6a1a74bfd8f7afd528f59630615\",\"may_sign\":[\"reconstruct-session\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":\"07d45f0d191520d6911f76016392e1ab61a37bca3bf22a3e819d6ae5954c6058\",\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":2,\"sig\":\"22023479736f2f8e314c1413db4a64be40b3d49585863c4850ab6765efd4061d3a5a4df0d2dded9da544936fa3ed2604598a396f448b65fc06322056be85140e\",\"status_max_age_s\":3600}"
],
"anchors": [
{
"origin": "https://publisher.example",
"revision": "d0935bd1f41dea1d3014cac0d128c51aa101b534cad59eb6433ef10e2192d1c8",
"path": "fetched",
"acquired_at": "2026-09-25T00:00:00Z",
"pinned_revision": null
}
]
},
"expected": "confirmed origin=https://publisher.example key_id=ss head=d0935bd1f41dea1d3014cac0d128c51aa101b534cad59eb6433ef10e2192d1c8 anchor=fetched"
},
{
"name": "session_figure_not_granted",
"about": "a metered session by a key holding reconstruct-session but not energy/OnChipCounter, under a pinned anchor: the session is confirmed and its figure refused",
"procedure": "confirm",
"receipt": "{\"body\":{\"classical_declared\":null,\"classical_source\":null,\"counted_discarded\":0,\"counted_fallback\":0,\"counted_primary\":4000,\"deadline_us\":null,\"decoders\":[{\"capability\":\"wai.neural.int_hyper\",\"decoder\":\"builtin:[email protected]\",\"leaves\":1,\"module_status\":null,\"object\":\"ca24d5e6429bfd54aaac853e291d82491fb5304ae050120c8f797ee4f27f7a82\",\"path\":\"primary\",\"pin\":\"1d279149536528e910f4da130bb6473bbcc76df60a933030e9750aabee9d4ae7\",\"rendition\":null,\"tier\":\"decode-equivalence\",\"variant\":null}],\"energy\":{\"energy_calibration_ref\":null,\"energy_counter_filtering\":\"Unknown\",\"energy_provenance\":\"OnChipCounter\",\"energy_uncertainty\":{\"relative_ppm\":20000,\"window_us\":1000000},\"joules_micro\":90000},\"fallback_units\":0,\"framing\":\"registered\",\"host\":{\"build\":{\"profile\":\"release\",\"target\":null,\"toolchain\":null},\"profile\":\"native-ffi\",\"target\":null,\"threads\":1},\"leaf_count\":1,\"media\":\"image\",\"missed\":0,\"missed_units\":[],\"output_root\":\"f422613aae3364b94e36f4d9df35224db0b02f0c16259b364b4e357d306b3c7c\",\"primary_units\":1,\"receipt_bytes\":0,\"session_nonce\":\"b2b2ef087cf5a13c070b6da6801b114b1f8af06e94b9d264e2398db989125d35\",\"stage_sets\":[],\"switches\":[],\"switches_total\":0,\"transferred\":4000,\"uncounted\":0,\"units\":1},\"class\":\"reconstruct-session\",\"issued_at\":\"2026-09-18T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"6ee5fae8ea62fb0b8e6a8d38c79c114b803117362cb62c52866db79084cff90d29c98a8d2c614a99c1489e56b8661ab94dbdfe131512c2e2d64c8c7794d50a08\",\"signer_id\":\"did:web:publisher.example#ss\",\"signer_pubkey\":\"ee31f83c88a71219a6fcf9bee0da9bc22620588f5a15a6145553504df9649e5c\",\"subject\":\"ca24d5e6429bfd54aaac853e291d82491fb5304ae050120c8f797ee4f27f7a82\",\"subject_type\":\"object\"}",
"context": {
"revisions": [
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-01T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":null,\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":0,\"sig\":\"44f48a651622c387576e25fd7f62e99a9409636794600f07a2635c2a3b09c8426d0bb87aa5716abb77274b46958e7a93722f3d8c5c15484f93522fb95bfe7205\",\"status_max_age_s\":3600}",
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-10T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"d2\",\"key\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"may_sign\":[\"delivery\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m2\",\"key\":\"e34d3a01b6112e1429ead61668405f4ef4be4f8853abee5079d28ac6c13ffdd0\",\"may_sign\":[\"energy/CalibratedInstrument\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op2\",\"key\":\"014e4d922c596a380adfc9eed722e835822f1236761693e4babb61e1afbb04f1\",\"may_sign\":[\"lawful-basis/decision\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modr\",\"key\":\"6d44f939f829d43598bcdedda90de01db69faa50caa33e8c6c328a4b900d4dac\",\"may_sign\":[\"module/reconstruct\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":\"c32f6eec946ad4c1df808293983ced11e6e56b79f62cb9cb843380118f294990\",\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":1,\"sig\":\"021c605ed69ec195bdba7ad60b625a82d3ac0bdb26771266f26b42bc2f730b95d6d04e599df12b596c9b938779e6a0ac27658f8881421f9b450722b59172b608\",\"status_max_age_s\":3600}",
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-17T06:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"d2\",\"key\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"may_sign\":[\"delivery\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m2\",\"key\":\"e34d3a01b6112e1429ead61668405f4ef4be4f8853abee5079d28ac6c13ffdd0\",\"may_sign\":[\"energy/CalibratedInstrument\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op2\",\"key\":\"014e4d922c596a380adfc9eed722e835822f1236761693e4babb61e1afbb04f1\",\"may_sign\":[\"lawful-basis/decision\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modr\",\"key\":\"6d44f939f829d43598bcdedda90de01db69faa50caa33e8c6c328a4b900d4dac\",\"may_sign\":[\"module/reconstruct\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"ss\",\"key\":\"ee31f83c88a71219a6fcf9bee0da9bc22620588f5a15a6145553504df9649e5c\",\"may_sign\":[\"reconstruct-session\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"se\",\"key\":\"4745d19c66ec9179fa25097056409ef16967b6a1a74bfd8f7afd528f59630615\",\"may_sign\":[\"reconstruct-session\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":\"07d45f0d191520d6911f76016392e1ab61a37bca3bf22a3e819d6ae5954c6058\",\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":2,\"sig\":\"22023479736f2f8e314c1413db4a64be40b3d49585863c4850ab6765efd4061d3a5a4df0d2dded9da544936fa3ed2604598a396f448b65fc06322056be85140e\",\"status_max_age_s\":3600}"
],
"anchors": [
{
"origin": "https://publisher.example",
"revision": "d0935bd1f41dea1d3014cac0d128c51aa101b534cad59eb6433ef10e2192d1c8",
"path": "pinned",
"acquired_at": null,
"pinned_revision": "d0935bd1f41dea1d3014cac0d128c51aa101b534cad59eb6433ef10e2192d1c8"
}
]
},
"expected": "confirmed origin=https://publisher.example key_id=ss head=d0935bd1f41dea1d3014cac0d128c51aa101b534cad59eb6433ef10e2192d1c8 anchor=pinned; figure refused step=6 reason=authority_not_granted"
},
{
"name": "session_figure_unpinned",
"about": "a metered session by a key holding both authorities, under a fetched anchor: the session is confirmed and its hardware-class figure unconfirmed",
"procedure": "confirm",
"receipt": "{\"body\":{\"classical_declared\":null,\"classical_source\":null,\"counted_discarded\":0,\"counted_fallback\":0,\"counted_primary\":4000,\"deadline_us\":null,\"decoders\":[{\"capability\":\"wai.neural.int_hyper\",\"decoder\":\"builtin:[email protected]\",\"leaves\":1,\"module_status\":null,\"object\":\"ca24d5e6429bfd54aaac853e291d82491fb5304ae050120c8f797ee4f27f7a82\",\"path\":\"primary\",\"pin\":\"1d279149536528e910f4da130bb6473bbcc76df60a933030e9750aabee9d4ae7\",\"rendition\":null,\"tier\":\"decode-equivalence\",\"variant\":null}],\"energy\":{\"energy_calibration_ref\":null,\"energy_counter_filtering\":\"Unknown\",\"energy_provenance\":\"OnChipCounter\",\"energy_uncertainty\":{\"relative_ppm\":20000,\"window_us\":1000000},\"joules_micro\":90000},\"fallback_units\":0,\"framing\":\"registered\",\"host\":{\"build\":{\"profile\":\"release\",\"target\":null,\"toolchain\":null},\"profile\":\"native-ffi\",\"target\":null,\"threads\":1},\"leaf_count\":1,\"media\":\"image\",\"missed\":0,\"missed_units\":[],\"output_root\":\"f422613aae3364b94e36f4d9df35224db0b02f0c16259b364b4e357d306b3c7c\",\"primary_units\":1,\"receipt_bytes\":0,\"session_nonce\":\"b2b2ef087cf5a13c070b6da6801b114b1f8af06e94b9d264e2398db989125d35\",\"stage_sets\":[],\"switches\":[],\"switches_total\":0,\"transferred\":4000,\"uncounted\":0,\"units\":1},\"class\":\"reconstruct-session\",\"issued_at\":\"2026-09-18T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"845a22b8d9750a2644760a4b51482a7a7f686a5f93488d946a6444d18aec2b61c1b43857930da7af5dbf5e8037b273b9efa5019f30bd2de870f1a75075ba630a\",\"signer_id\":\"did:web:publisher.example#se\",\"signer_pubkey\":\"4745d19c66ec9179fa25097056409ef16967b6a1a74bfd8f7afd528f59630615\",\"subject\":\"ca24d5e6429bfd54aaac853e291d82491fb5304ae050120c8f797ee4f27f7a82\",\"subject_type\":\"object\"}",
"context": {
"revisions": [
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-01T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":null,\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":0,\"sig\":\"44f48a651622c387576e25fd7f62e99a9409636794600f07a2635c2a3b09c8426d0bb87aa5716abb77274b46958e7a93722f3d8c5c15484f93522fb95bfe7205\",\"status_max_age_s\":3600}",
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-10T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"d2\",\"key\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"may_sign\":[\"delivery\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m2\",\"key\":\"e34d3a01b6112e1429ead61668405f4ef4be4f8853abee5079d28ac6c13ffdd0\",\"may_sign\":[\"energy/CalibratedInstrument\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op2\",\"key\":\"014e4d922c596a380adfc9eed722e835822f1236761693e4babb61e1afbb04f1\",\"may_sign\":[\"lawful-basis/decision\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modr\",\"key\":\"6d44f939f829d43598bcdedda90de01db69faa50caa33e8c6c328a4b900d4dac\",\"may_sign\":[\"module/reconstruct\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":\"c32f6eec946ad4c1df808293983ced11e6e56b79f62cb9cb843380118f294990\",\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":1,\"sig\":\"021c605ed69ec195bdba7ad60b625a82d3ac0bdb26771266f26b42bc2f730b95d6d04e599df12b596c9b938779e6a0ac27658f8881421f9b450722b59172b608\",\"status_max_age_s\":3600}",
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-17T06:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"d2\",\"key\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"may_sign\":[\"delivery\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m2\",\"key\":\"e34d3a01b6112e1429ead61668405f4ef4be4f8853abee5079d28ac6c13ffdd0\",\"may_sign\":[\"energy/CalibratedInstrument\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op2\",\"key\":\"014e4d922c596a380adfc9eed722e835822f1236761693e4babb61e1afbb04f1\",\"may_sign\":[\"lawful-basis/decision\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modr\",\"key\":\"6d44f939f829d43598bcdedda90de01db69faa50caa33e8c6c328a4b900d4dac\",\"may_sign\":[\"module/reconstruct\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"ss\",\"key\":\"ee31f83c88a71219a6fcf9bee0da9bc22620588f5a15a6145553504df9649e5c\",\"may_sign\":[\"reconstruct-session\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"se\",\"key\":\"4745d19c66ec9179fa25097056409ef16967b6a1a74bfd8f7afd528f59630615\",\"may_sign\":[\"reconstruct-session\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":\"07d45f0d191520d6911f76016392e1ab61a37bca3bf22a3e819d6ae5954c6058\",\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":2,\"sig\":\"22023479736f2f8e314c1413db4a64be40b3d49585863c4850ab6765efd4061d3a5a4df0d2dded9da544936fa3ed2604598a396f448b65fc06322056be85140e\",\"status_max_age_s\":3600}"
],
"anchors": [
{
"origin": "https://publisher.example",
"revision": "d0935bd1f41dea1d3014cac0d128c51aa101b534cad59eb6433ef10e2192d1c8",
"path": "fetched",
"acquired_at": "2026-09-25T00:00:00Z",
"pinned_revision": null
}
]
},
"expected": "confirmed origin=https://publisher.example key_id=se head=d0935bd1f41dea1d3014cac0d128c51aa101b534cad59eb6433ef10e2192d1c8 anchor=fetched; figure unconfirmed step=6a reason=anchor_not_pinned"
},
{
"name": "session_figure_confirmed",
"about": "a metered session by a key holding both authorities, under a pinned anchor: both confirmed, and the figure's key does not deliver",
"procedure": "confirm",
"receipt": "{\"body\":{\"classical_declared\":null,\"classical_source\":null,\"counted_discarded\":0,\"counted_fallback\":0,\"counted_primary\":4000,\"deadline_us\":null,\"decoders\":[{\"capability\":\"wai.neural.int_hyper\",\"decoder\":\"builtin:[email protected]\",\"leaves\":1,\"module_status\":null,\"object\":\"ca24d5e6429bfd54aaac853e291d82491fb5304ae050120c8f797ee4f27f7a82\",\"path\":\"primary\",\"pin\":\"1d279149536528e910f4da130bb6473bbcc76df60a933030e9750aabee9d4ae7\",\"rendition\":null,\"tier\":\"decode-equivalence\",\"variant\":null}],\"energy\":{\"energy_calibration_ref\":null,\"energy_counter_filtering\":\"Unknown\",\"energy_provenance\":\"OnChipCounter\",\"energy_uncertainty\":{\"relative_ppm\":20000,\"window_us\":1000000},\"joules_micro\":90000},\"fallback_units\":0,\"framing\":\"registered\",\"host\":{\"build\":{\"profile\":\"release\",\"target\":null,\"toolchain\":null},\"profile\":\"native-ffi\",\"target\":null,\"threads\":1},\"leaf_count\":1,\"media\":\"image\",\"missed\":0,\"missed_units\":[],\"output_root\":\"f422613aae3364b94e36f4d9df35224db0b02f0c16259b364b4e357d306b3c7c\",\"primary_units\":1,\"receipt_bytes\":0,\"session_nonce\":\"b2b2ef087cf5a13c070b6da6801b114b1f8af06e94b9d264e2398db989125d35\",\"stage_sets\":[],\"switches\":[],\"switches_total\":0,\"transferred\":4000,\"uncounted\":0,\"units\":1},\"class\":\"reconstruct-session\",\"issued_at\":\"2026-09-18T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"845a22b8d9750a2644760a4b51482a7a7f686a5f93488d946a6444d18aec2b61c1b43857930da7af5dbf5e8037b273b9efa5019f30bd2de870f1a75075ba630a\",\"signer_id\":\"did:web:publisher.example#se\",\"signer_pubkey\":\"4745d19c66ec9179fa25097056409ef16967b6a1a74bfd8f7afd528f59630615\",\"subject\":\"ca24d5e6429bfd54aaac853e291d82491fb5304ae050120c8f797ee4f27f7a82\",\"subject_type\":\"object\"}",
"context": {
"revisions": [
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-01T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":null,\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":0,\"sig\":\"44f48a651622c387576e25fd7f62e99a9409636794600f07a2635c2a3b09c8426d0bb87aa5716abb77274b46958e7a93722f3d8c5c15484f93522fb95bfe7205\",\"status_max_age_s\":3600}",
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-10T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"d2\",\"key\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"may_sign\":[\"delivery\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m2\",\"key\":\"e34d3a01b6112e1429ead61668405f4ef4be4f8853abee5079d28ac6c13ffdd0\",\"may_sign\":[\"energy/CalibratedInstrument\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op2\",\"key\":\"014e4d922c596a380adfc9eed722e835822f1236761693e4babb61e1afbb04f1\",\"may_sign\":[\"lawful-basis/decision\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modr\",\"key\":\"6d44f939f829d43598bcdedda90de01db69faa50caa33e8c6c328a4b900d4dac\",\"may_sign\":[\"module/reconstruct\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":\"c32f6eec946ad4c1df808293983ced11e6e56b79f62cb9cb843380118f294990\",\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":1,\"sig\":\"021c605ed69ec195bdba7ad60b625a82d3ac0bdb26771266f26b42bc2f730b95d6d04e599df12b596c9b938779e6a0ac27658f8881421f9b450722b59172b608\",\"status_max_age_s\":3600}",
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-17T06:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"d2\",\"key\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"may_sign\":[\"delivery\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m2\",\"key\":\"e34d3a01b6112e1429ead61668405f4ef4be4f8853abee5079d28ac6c13ffdd0\",\"may_sign\":[\"energy/CalibratedInstrument\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op2\",\"key\":\"014e4d922c596a380adfc9eed722e835822f1236761693e4babb61e1afbb04f1\",\"may_sign\":[\"lawful-basis/decision\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modr\",\"key\":\"6d44f939f829d43598bcdedda90de01db69faa50caa33e8c6c328a4b900d4dac\",\"may_sign\":[\"module/reconstruct\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"ss\",\"key\":\"ee31f83c88a71219a6fcf9bee0da9bc22620588f5a15a6145553504df9649e5c\",\"may_sign\":[\"reconstruct-session\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"se\",\"key\":\"4745d19c66ec9179fa25097056409ef16967b6a1a74bfd8f7afd528f59630615\",\"may_sign\":[\"reconstruct-session\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":\"07d45f0d191520d6911f76016392e1ab61a37bca3bf22a3e819d6ae5954c6058\",\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":2,\"sig\":\"22023479736f2f8e314c1413db4a64be40b3d49585863c4850ab6765efd4061d3a5a4df0d2dded9da544936fa3ed2604598a396f448b65fc06322056be85140e\",\"status_max_age_s\":3600}"
],
"anchors": [
{
"origin": "https://publisher.example",
"revision": "d0935bd1f41dea1d3014cac0d128c51aa101b534cad59eb6433ef10e2192d1c8",
"path": "pinned",
"acquired_at": null,
"pinned_revision": "d0935bd1f41dea1d3014cac0d128c51aa101b534cad59eb6433ef10e2192d1c8"
}
]
},
"expected": "confirmed origin=https://publisher.example key_id=se head=d0935bd1f41dea1d3014cac0d128c51aa101b534cad59eb6433ef10e2192d1c8 anchor=pinned; figure confirmed origin=https://publisher.example key_id=se head=d0935bd1f41dea1d3014cac0d128c51aa101b534cad59eb6433ef10e2192d1c8 anchor=pinned also_delivers=false"
},
{
"name": "session_under_did_key",
"about": "a metered session signed under a did:key, the normal case for a sink's ephemeral key: unconfirmed, and so is its figure",
"procedure": "confirm",
"receipt": "{\"body\":{\"classical_declared\":null,\"classical_source\":null,\"counted_discarded\":0,\"counted_fallback\":0,\"counted_primary\":4000,\"deadline_us\":null,\"decoders\":[{\"capability\":\"wai.neural.int_hyper\",\"decoder\":\"builtin:[email protected]\",\"leaves\":1,\"module_status\":null,\"object\":\"ca24d5e6429bfd54aaac853e291d82491fb5304ae050120c8f797ee4f27f7a82\",\"path\":\"primary\",\"pin\":\"1d279149536528e910f4da130bb6473bbcc76df60a933030e9750aabee9d4ae7\",\"rendition\":null,\"tier\":\"decode-equivalence\",\"variant\":null}],\"energy\":{\"energy_calibration_ref\":null,\"energy_counter_filtering\":\"Unknown\",\"energy_provenance\":\"OnChipCounter\",\"energy_uncertainty\":{\"relative_ppm\":20000,\"window_us\":1000000},\"joules_micro\":90000},\"fallback_units\":0,\"framing\":\"registered\",\"host\":{\"build\":{\"profile\":\"release\",\"target\":null,\"toolchain\":null},\"profile\":\"native-ffi\",\"target\":null,\"threads\":1},\"leaf_count\":1,\"media\":\"image\",\"missed\":0,\"missed_units\":[],\"output_root\":\"f422613aae3364b94e36f4d9df35224db0b02f0c16259b364b4e357d306b3c7c\",\"primary_units\":1,\"receipt_bytes\":0,\"session_nonce\":\"b2b2ef087cf5a13c070b6da6801b114b1f8af06e94b9d264e2398db989125d35\",\"stage_sets\":[],\"switches\":[],\"switches_total\":0,\"transferred\":4000,\"uncounted\":0,\"units\":1},\"class\":\"reconstruct-session\",\"issued_at\":\"2026-09-18T12:00:00Z\",\"kind\":\"claim\",\"parent_receipt_hash\":null,\"seq\":0,\"sig\":\"41033086df07fbb39325d6163c1e43e64395c74323dbd1b074928baeddadacda3e55170595b20dbbda8f2859f66bafd26c162a4afe1d894fd3595fe80823ee06\",\"signer_id\":\"did:key:z6MkjFUjszw2hrv4RXs95YXbtwuiuAuHhsqxMzPCcY9pXT3S\",\"signer_pubkey\":\"4745d19c66ec9179fa25097056409ef16967b6a1a74bfd8f7afd528f59630615\",\"subject\":\"ca24d5e6429bfd54aaac853e291d82491fb5304ae050120c8f797ee4f27f7a82\",\"subject_type\":\"object\"}",
"context": {
"revisions": [
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-01T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":null,\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":0,\"sig\":\"44f48a651622c387576e25fd7f62e99a9409636794600f07a2635c2a3b09c8426d0bb87aa5716abb77274b46958e7a93722f3d8c5c15484f93522fb95bfe7205\",\"status_max_age_s\":3600}",
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-10T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"d2\",\"key\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"may_sign\":[\"delivery\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m2\",\"key\":\"e34d3a01b6112e1429ead61668405f4ef4be4f8853abee5079d28ac6c13ffdd0\",\"may_sign\":[\"energy/CalibratedInstrument\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op2\",\"key\":\"014e4d922c596a380adfc9eed722e835822f1236761693e4babb61e1afbb04f1\",\"may_sign\":[\"lawful-basis/decision\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modr\",\"key\":\"6d44f939f829d43598bcdedda90de01db69faa50caa33e8c6c328a4b900d4dac\",\"may_sign\":[\"module/reconstruct\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":\"c32f6eec946ad4c1df808293983ced11e6e56b79f62cb9cb843380118f294990\",\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":1,\"sig\":\"021c605ed69ec195bdba7ad60b625a82d3ac0bdb26771266f26b42bc2f730b95d6d04e599df12b596c9b938779e6a0ac27658f8881421f9b450722b59172b608\",\"status_max_age_s\":3600}",
"{\"grant_max_s\":43200,\"issued_at\":\"2026-09-17T06:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"d2\",\"key\":\"34b4d9043156cb6dcf0beb0a2949b7559c940d2bcb6dbe8c53a9b30278e3a746\",\"may_sign\":[\"delivery\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m2\",\"key\":\"e34d3a01b6112e1429ead61668405f4ef4be4f8853abee5079d28ac6c13ffdd0\",\"may_sign\":[\"energy/CalibratedInstrument\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op2\",\"key\":\"014e4d922c596a380adfc9eed722e835822f1236761693e4babb61e1afbb04f1\",\"may_sign\":[\"lawful-basis/decision\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modr\",\"key\":\"6d44f939f829d43598bcdedda90de01db69faa50caa33e8c6c328a4b900d4dac\",\"may_sign\":[\"module/reconstruct\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"ss\",\"key\":\"ee31f83c88a71219a6fcf9bee0da9bc22620588f5a15a6145553504df9649e5c\",\"may_sign\":[\"reconstruct-session\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"se\",\"key\":\"4745d19c66ec9179fa25097056409ef16967b6a1a74bfd8f7afd528f59630615\",\"may_sign\":[\"reconstruct-session\",\"energy/OnChipCounter\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":\"07d45f0d191520d6911f76016392e1ab61a37bca3bf22a3e819d6ae5954c6058\",\"revoked_modules\":[],\"root\":\"af06a3e3291714e4f356c19c9b15cd1951ec6e6662aa77be07547f289383341d\",\"seq\":2,\"sig\":\"22023479736f2f8e314c1413db4a64be40b3d49585863c4850ab6765efd4061d3a5a4df0d2dded9da544936fa3ed2604598a396f448b65fc06322056be85140e\",\"status_max_age_s\":3600}"
],
"anchors": [
{
"origin": "https://publisher.example",
"revision": "d0935bd1f41dea1d3014cac0d128c51aa101b534cad59eb6433ef10e2192d1c8",
"path": "pinned",
"acquired_at": null,
"pinned_revision": "d0935bd1f41dea1d3014cac0d128c51aa101b534cad59eb6433ef10e2192d1c8"
}
]
},
"expected": "unconfirmed step=1 reason=signer_id_not_origin_bound; figure unconfirmed step=1 reason=signer_id_not_origin_bound"
}
]
}

View file

@ -9,8 +9,8 @@ every object, split into envelope header, manifest and payload (§3), and their
stage and per prefix, per base and over the title; the bytes of each base's parameter
set; the first-presentable bytes (§5.1); every presentation's output digest (SPEC §7
*Digests*), with its quality integers against the source and against the final prefix
(§4), and every prefix's pooled quality; the decoder entries and the output tree's root
and leaf count (Appendix C); the layering overhead's rate–quality points, the full title's and each
(§4), and every prefix's pooled quality; the decoder entries — capability, pin and the
envelope decoded — and the output tree's root and leaf count (Appendix C); the layering overhead's rate–quality points, the full title's and each
single-layer reference's (§6); the deduplication totals (§9); and the title's content
identity (Appendix D). Across the corpus: the record-wide deduplication totals (§9.2).
@ -91,6 +91,20 @@ steps (the curve of §6.3).
byte for byte, simulates every case (`stage_measure::sim`), checks each claim, and
requires `sim/results.json` byte for byte; `wai_stage_measure sim <model> <plan>`
runs one.
- `wai_stage_measure bench fixtures/<name>` measures a title on the machine it runs on
and is never run in CI (§12.2): the decode time of each case — one base decoded or one
layer applied, for the (unit, stage) outputs it gives, with its preparation, a base's
comparison with its stage 0 and the hashing of its outputs outside the clock — over
interleaved rounds whose order rotates, every run's outputs compared with the
evaluation's and the bench aborted on a difference (§7.1–§7.3), with each run, the order and the operator's statement of
whether the machine was idle in the record (§7.4); and, with `--energy`, its decode
figure after a probe of the meter's rail at rest (§8.6). With
`--sign` it seals the session claim a sink decoding the title would seal
(jwp-receipts §2.4.12), whose output tree must be the title's — the root and leaf count
`goldens.json` states — and, beside a clean figure, the `measurement` claim on it
(§8.7). A lib test seals every fixture's session and checks its tree against the
evaluation's, and `tests/stage_measure.rs` that every case, run on its own, gives the
evaluation's outputs.
- `sim_verify.py .`: pure Python with the standard library, sharing no code with
`wai-rs`: Appendix B implemented from the text, with each fixture's model taken from
`measure_verify.py`'s own evaluation. It reproduces every result, checks every claim,

View file

@ -182,11 +182,23 @@
"decoders": [
{
"capability": "wai.audio.int_codec",
"pin": "98b382c92112660a7d3909f07da831125fcdb75a548aa0cf64ad17a0901bd9ef"
"pin": "98b382c92112660a7d3909f07da831125fcdb75a548aa0cf64ad17a0901bd9ef",
"object": "d45db2bc98dd38873553beb15fdb026a4e6b7fd7c8c7dbe4f4a0b6fcc0709a54"
},
{
"capability": "wai.audio.int_refine",
"pin": null
"pin": null,
"object": "14c630b04c617542d3df5a40b398c2fa5c960d6020f84a961046ff5f7831dc69"
},
{
"capability": "wai.audio.int_refine",
"pin": null,
"object": "4f935f7fade9bd09ba65d65fca37083e18b57c6251c4f1ded91d7bab2c1640b0"
},
{
"capability": "wai.audio.int_refine",
"pin": null,
"object": "ed29fe08a0e7aafba5c71016408cbab959ddcda07914ee0ca08cc58f2d7cf549"
}
],
"presentations": [
@ -229,7 +241,7 @@
{
"unit": 0,
"layers": 3,
"decoder": 1,
"decoder": 2,
"shape": [
16384,
1,
@ -252,7 +264,7 @@
{
"unit": 0,
"layers": 4,
"decoder": 1,
"decoder": 3,
"shape": [
16384,
1,
@ -273,7 +285,7 @@
}
}
],
"output_root": "cbefe0dba0775d039b833791d17f22c15649959a3f1e9cd84473fea68a58985c",
"output_root": "b2241df38a1249d823c0896c8570826b1ee9417718cd776fa8fff50ad6709d43",
"leaf_count": 4,
"overhead": [
{
@ -620,11 +632,58 @@
"decoders": [
{
"capability": "wai.video.int_hyper",
"pin": "13e8b0d18e001eb9f0160309a02755b35a14f71ba0bcaa13f373e32b931ab818"
"pin": "13e8b0d18e001eb9f0160309a02755b35a14f71ba0bcaa13f373e32b931ab818",
"object": "85f50eb426d09ee3a5006544e10cbba572fc5e05cb62f3992bd095d9cb3c7e8f"
},
{
"capability": "wai.video.int_refine",
"pin": null
"pin": null,
"object": "431bfb81f4e5bda35152786d2a580d3378c5cfd6196d271a6cddd122e93f09f0"
},
{
"capability": "wai.video.int_refine",
"pin": null,
"object": "836a7a4314b389304bf1e12113468b063e2853420e66ab41b4ac51ea42a97fd3"
},
{
"capability": "wai.video.int_refine",
"pin": null,
"object": "3ba12c0683b76c4e1aec05de9bab726ad3c5e4ef5ab729c4610ef3e6ed1704fc"
},
{
"capability": "wai.video.int_refine",
"pin": null,
"object": "9ebbe2a5888366479c4fe8f17f61cec3a0b9513953dee4ae5a35f0c25175840d"
},
{
"capability": "wai.video.int_refine",
"pin": null,
"object": "8367e3e2a08e02cbd0c44edfc9b1265d653cd14cdd22a202fec6c8dd81fbd210"
},
{
"capability": "wai.video.int_refine",
"pin": null,
"object": "dc49106d143d7ef3cc73ef3b286f411e863ba885c1db361012bef09966c9cf26"
},
{
"capability": "wai.video.int_refine",
"pin": null,
"object": "8e8ce0262bb55022c8f9670985535f99804b3f364de631e04e117273f794f285"
},
{
"capability": "wai.video.int_refine",
"pin": null,
"object": "a29103cc845996ca04557fca953b58401163940793d8a729ffc804688a70166a"
},
{
"capability": "wai.video.int_refine",
"pin": null,
"object": "43e35ee383faa6a0b86c5c8270447635ebffe2a890818371f34fb6819ec0eda5"
},
{
"capability": "wai.video.int_refine",
"pin": null,
"object": "130c3ca0cba51a191e896e9e9da1a6eec1300bae5637a3390493f53b69a714b4"
}
],
"presentations": [
@ -685,7 +744,7 @@
{
"unit": 0,
"layers": 3,
"decoder": 1,
"decoder": 6,
"shape": [
16,
16,
@ -739,7 +798,7 @@
{
"unit": 1,
"layers": 2,
"decoder": 1,
"decoder": 2,
"shape": [
16,
16,
@ -766,7 +825,7 @@
{
"unit": 1,
"layers": 3,
"decoder": 1,
"decoder": 7,
"shape": [
16,
16,
@ -820,7 +879,7 @@
{
"unit": 2,
"layers": 2,
"decoder": 1,
"decoder": 3,
"shape": [
16,
16,
@ -847,7 +906,7 @@
{
"unit": 2,
"layers": 3,
"decoder": 1,
"decoder": 8,
"shape": [
16,
16,
@ -901,7 +960,7 @@
{
"unit": 3,
"layers": 2,
"decoder": 1,
"decoder": 4,
"shape": [
16,
16,
@ -928,7 +987,7 @@
{
"unit": 3,
"layers": 3,
"decoder": 1,
"decoder": 9,
"shape": [
16,
16,
@ -982,7 +1041,7 @@
{
"unit": 4,
"layers": 2,
"decoder": 1,
"decoder": 5,
"shape": [
16,
16,
@ -1009,7 +1068,7 @@
{
"unit": 4,
"layers": 3,
"decoder": 1,
"decoder": 10,
"shape": [
16,
16,
@ -1034,7 +1093,7 @@
}
}
],
"output_root": "c372d238be5ce75f44c296045cbf7b39009f72556e246bb20b9b68f576867261",
"output_root": "d900320befcd0ad652f7ee3d6f422159e47cd5dbcf038de467e71ed4d2833175",
"leaf_count": 15,
"overhead": [
{
@ -1251,11 +1310,18 @@
"decoders": [
{
"capability": "wai.splat.int_codec",
"pin": "033a37c9d29d7ea9675a0b4a3e269d74f2ab874b5a3b1425eec69bf3bad8b26f"
"pin": "033a37c9d29d7ea9675a0b4a3e269d74f2ab874b5a3b1425eec69bf3bad8b26f",
"object": "bc283dfb0eb717c129db5a1cadad67daefc788e4f6880ecf4c7ac8a2d35204d8"
},
{
"capability": "wai.splat.int_refine",
"pin": null
"pin": null,
"object": "de3eb7d9fb70a8f7f745b572a164c3edc49f474cc0403616e6a90c2b122fa5ff"
},
{
"capability": "wai.splat.int_refine",
"pin": null,
"object": "d041bed2c68c8f943051a11a51ff8b7595f751f83e452d179bfb23b8b31bfe5a"
}
],
"presentations": [
@ -1308,7 +1374,7 @@
{
"unit": 0,
"layers": 3,
"decoder": 1,
"decoder": 2,
"shape": [
14,
32,
@ -1329,7 +1395,7 @@
}
}
],
"output_root": "78b1c64959dddb38ddd8efd963bffd5fe0517084e12259d929db388e9717b2be",
"output_root": "5606cbc346af4fdb1e021537935e7349d52326e0fb0d889203e51ededc6899f8",
"leaf_count": 3,
"overhead": [
{
@ -1558,11 +1624,23 @@
"decoders": [
{
"capability": "wai.neural.int_hyper",
"pin": "13e8b0d18e001eb9f0160309a02755b35a14f71ba0bcaa13f373e32b931ab818"
"pin": "13e8b0d18e001eb9f0160309a02755b35a14f71ba0bcaa13f373e32b931ab818",
"object": "aaf90792d6ff5318e5f306fbb000dfcbe73637e244aad1a2eff6dae537cd6b5f"
},
{
"capability": "wai.image.int_refine",
"pin": null
"pin": null,
"object": "e35e4888e6dbca631928480d0f3e4c5404f337ed7fc4cfeab8b54bdb46968d05"
},
{
"capability": "wai.image.int_refine",
"pin": null,
"object": "b8f85108e6f3eb6b2671ddb326aa2fd22ca634187d904dff6ea9dc0d67fb6b92"
},
{
"capability": "wai.image.int_refine",
"pin": null,
"object": "469e20139ecc596d8aa1fb210e05d60fea605d63ee294ac099cf6eb6329ceb37"
}
],
"presentations": [
@ -1609,7 +1687,7 @@
{
"unit": 0,
"layers": 3,
"decoder": 1,
"decoder": 2,
"shape": [
32,
32,
@ -1636,7 +1714,7 @@
{
"unit": 0,
"layers": 4,
"decoder": 1,
"decoder": 3,
"shape": [
32,
32,
@ -1661,7 +1739,7 @@
}
}
],
"output_root": "4afc5e26ef90233e520d7c5e44273cbc5a36d329a2bf41c48a09d7457f4c0fee",
"output_root": "794547e56fbe86cf7be5613dda706062fb7cf60ca1fce48ae7d3c4dbb58c7eba",
"leaf_count": 4,
"overhead": [
{

View file

@ -342,13 +342,14 @@ class Fixture:
def replay(bases, layers, cost=None):
"""bases: list of dicts (hash, cap, pin, first, units: [(shape, samples)]);
layers: list of (path, env). Returns per unit (global, base index) the list of
stages (layers count, capability, pin, shape, samples)."""
stages (layers count, capability, pin, shape, samples, the content hash of the
envelope that produced it)."""
traces = {}
producer = {}
for bi, b in enumerate(bases):
for u, (shape, s) in enumerate(b["units"]):
g = b["first"] + u
traces[g] = [(1, b["cap"], b["pin"], shape, s)]
traces[g] = [(1, b["cap"], b["pin"], shape, s, b["hash"])]
producer[g] = b["hash"]
parsed = []
for path, env in layers:
@ -376,11 +377,11 @@ def replay(bases, layers, cost=None):
want = SV.producer_for(ref, i)
if stage != k - 1 or producer[g] != want:
raise ValueError(f"{path}: unit {g} is not ready for layer {k}")
_, _, _, shape, s = traces[g][-1]
_, _, _, shape, s, _ = traces[g][-1]
if not SV.accepts_below(w, shape):
raise ValueError(f"{path}: unit {g}'s shape")
out = SV.apply_unit(w, i, s, shape)
traces[g].append((k + 1, cap, None, w["d"], out))
traces[g].append((k + 1, cap, None, w["d"], out, h))
producer[g] = h
return [traces[g] for g in sorted(traces)]
@ -458,12 +459,12 @@ def evaluate(f):
at += n
if at != len(sb):
raise ValueError("source length")
# decoders, by first use as presentations run by (layers, unit)
# decoders (capability, pin, object), by first use as presentations run by (layers, unit)
order = sorted(((st[0], tr_i, s_i) for tr_i, tr in enumerate(traces) for s_i, st in enumerate(tr)))
decoders = []
for _, tr_i, s_i in order:
st = traces[tr_i][s_i]
key = (st[1], st[2])
key = (st[1], st[2], st[5])
if key not in decoders:
decoders.append(key)
units_g = sorted(bases[bi]["first"] + u for bi in range(len(bases)) for u in range(len(bases[bi]["units"])))
@ -474,7 +475,7 @@ def evaluate(f):
fin_bytes = canonical(cls, fin[4])
for st in tr:
digest = sha(canonical(cls, st[4]))
dec = decoders.index((st[1], st[2]))
dec = decoders.index((st[1], st[2], st[5]))
presentations.append({
"unit": units_g[tr_i], "layers": st[0], "decoder": dec, "shape": list(st[3]), "out_sha256": digest,
"vs_source": pooled(cls, [(st[4], st[3], src[tr_i], fin[3])]) if src is not None else None,
@ -543,7 +544,7 @@ def evaluate(f):
"objects": objects,
"bfpf_min": bfpf,
"prefixes": prefixes,
"decoders": [{"capability": c, "pin": p} for c, p in decoders],
"decoders": [{"capability": c, "pin": p, "object": o} for c, p, o in decoders],
"presentations": presentations,
"output_root": root,
"leaf_count": len(presentations),

View file

@ -801,7 +801,9 @@ required-features = ["score"]
# Staged-delivery measurement (extensions/staged-measurement.md): `eval` one title,
# `verify` the corpus staged-measure-conformance/ (fixtures regenerated in memory,
# evaluations, the fixture tool's own reconstruction, goldens), `generate-goldens`
# and `gen-fixtures` (never in CI).
# and `gen-fixtures` (never in CI), and `bench` (never in CI): one title's decode time
# and decode figure on this machine, and, with `receipt_claims`, its session claim
# (`--sign`).
[[bin]]
name = "wai_stage_measure"
path = "src/bin/wai_stage_measure.rs"

View file

@ -7,6 +7,9 @@
//! wai_stage_measure verify <corpus>
//! wai_stage_measure generate-goldens <corpus>
//! wai_stage_measure gen-fixtures <corpus>
//! wai_stage_measure bench <fixture dir> [--rounds N] [--warmup W] [--energy macmon|rapl:<domain>|none]
//! [--record <file>] [--sign <seed hex>] [--signer-id <id>] [--idle yes|no]
//! [--limit OUT,SYMBOLS,WORKING,MACS]
//!
//! `eval` evaluates one title (`stage_measure::run::evaluate`) and prints its figures,
//! or with `--json` the block `goldens.json` holds for it. `sim` runs one delivery plan
@ -19,6 +22,26 @@
//! `sim/results.json`;
//! `gen-fixtures` writes the fixtures. Neither runs in CI: CI runs `verify`.
//!
//! `bench` measures one title on this machine and build, never in CI (§7.4, §12.2): the
//! decode time of each case — one base decoded, or one layer applied, for the (unit,
//! stage) outputs it gives (§7.1) — over `--rounds` rounds after `--warmup` discarded
//! rounds, the cases interleaved and their order rotated each round (§7.2), every run's
//! outputs compared with the evaluation's and the bench aborted on a difference (§7.3);
//! and, with `--energy`, its decode figure. A meter's
//! rail is probed at rest first (§8.6): a rail absent or stale gives no bracket and the
//! decode is unmetered, with the reason. Otherwise the figure comes from a from-rest
//! bracket of completions (energy-measurement §3) with its acquisition class, and is
//! published only where its verdict is clean (§8.3). With `--sign`, a 32-byte Ed25519
//! seed in hex, `bench` seals the session a sink decoding the title would seal
//! (jwp-receipts §2.4.12): its output tree is the title's, checked against the
//! evaluation's, and its `energy` is the clean figure or `null`. Beside a figure it
//! seals the `measurement` claim on it, with the bracket, signed with the same key and
//! chained after the session (energy-measurement §6). `--signer-id` names the key; by
//! default its `did:key`. `--idle` records the operator's statement that the machine was,
//! or was not, otherwise idle (§7.4). `--record` writes the machine, the build, the load
//! before and after each block, the idle statement, each case's runs and quantiles with
//! the order the cases ran in, the energy facts and the claims as JSON.
//!
//! The fixtures are staged sets over bases the staged-delivery corpus already holds
//! (`staged-conformance/cases/*/base.wai`, decoded against their pinned parameter
//! sets), with refinement layers coded here by `staged::wir1::encode` toward a source
@ -1038,6 +1061,311 @@ fn eval(dir: &str, as_json: bool, limit: Option<wai::int_payload::DeclaredCost>)
Ok(())
}
/// One case's runs and their quantiles, in microseconds: index ⌊q·(n−1)⌋ of the sorted
/// times for q = 0, 0.1, 0.5 and 0.9 (§7.1). The runs are kept in the order they ran.
fn timing_json(runs: &[u64]) -> Value {
let mut us = runs.to_vec();
us.sort_unstable();
let at = |q10: usize| us.get(q10 * (us.len().saturating_sub(1)) / 10).copied();
json!({"runs_us": runs, "min_us": at(0), "p10_us": at(1), "median_us": at(5), "p90_us": at(9)})
}
/// The decode time of each case of `t` (staged-measurement §7): a case is one decode step —
/// a base decoded, or one layer applied — timed for the (unit, stage) outputs it gives.
/// Each round runs every case, made ready by the steps before it untimed, in the order of
/// the round before it rotated by one (§7.2); the first `warmup` rounds are discarded.
/// Every run's outputs must be the evaluation's for its (unit, layers): a run that differs
/// is not a timing, and the bench aborts (§7.3).
fn time_steps(t: &Title, e: &Evaluation, rounds: u32, warmup: u32) -> Result<Value, String> {
use wai::stage_measure::decode::{decode_base, differs_from, prepare, steps, Step};
let reference: BTreeMap<(u64, u16), [u8; 32]> = e.presentations.iter().map(|p| ((p.p.unit, p.p.layers), p.p.out_digest)).collect();
let bases: Vec<_> = t.bases.iter().map(|b| decode_base(b, &t.limits)).collect::<Result<_, _>>().map_err(|x| x.to_string())?;
let cases = steps(t, &bases).map_err(|x| x.to_string())?;
let mut runs: Vec<Vec<u64>> = vec![Vec::new(); cases.len()];
let mut pairs: Vec<Vec<(u64, u16)>> = vec![Vec::new(); cases.len()];
let mut orders: Vec<Vec<usize>> = Vec::new();
for round in 0..warmup + rounds {
let order: Vec<usize> = (0..cases.len()).map(|i| (i + round as usize) % cases.len()).collect();
for &c in &order {
let ready = prepare(t, &bases, cases[c]).map_err(|x| x.to_string())?;
let start = std::time::Instant::now();
let ran = ready.run().map_err(|x| x.to_string())?;
let us = start.elapsed().as_micros() as u64;
let out = ran.outputs().map_err(|x| x.to_string())?;
if let Some((u, l)) = differs_from(&reference, &out) {
return Err(format!("round {round}: unit {u} at {l} layers is not the reference's output; no timing is kept (staged-measurement §7.3)"));
}
if round >= warmup {
runs[c].push(us);
}
pairs[c] = out.iter().map(|(u, l, _)| (*u, *l)).collect();
}
if round >= warmup {
orders.push(order);
}
}
let label = |s: &Step| match s {
Step::Base(b) => format!("base {}", t.bases[*b].obj.path),
Step::Layer(i) => format!("layer {}", t.layers[*i].path),
};
Ok(json!({
"work": "per case: one base decoded by decode_base_pinned or one layer offered to the sink, single-threaded, wall clock; untimed: the case's preparation (its bases taken and every earlier layer applied), the base's comparison with its stage 0, and the hashing of every output",
"warmup_rounds": warmup,
"rounds": rounds,
"cases": cases.iter().enumerate().map(|(i, s)| {
let mut o = timing_json(&runs[i]);
o["case"] = label(s).into();
o["outputs"] = pairs[i].iter().map(|(u, l)| json!([u, l])).collect::<Vec<_>>().into();
o
}).collect::<Vec<_>>(),
"order": orders,
}))
}
/// Fresh random bytes for a session nonce, from the operating system: derived from nothing
/// that identifies a person, a device or an account.
#[cfg(feature = "receipt_claims")]
fn fresh_nonce() -> Result<[u8; 32], String> {
use std::io::Read;
let mut b = [0u8; 32];
if let Ok(mut f) = std::fs::File::open("/dev/urandom")
&& f.read_exact(&mut b).is_ok()
{
return Ok(b);
}
// Where there is no /dev/urandom: the standard library's per-process hash keys, which
// it takes from the operating system's random source.
use std::hash::{BuildHasher, Hasher};
let mut words = Vec::new();
for i in 0..8u64 {
let mut h = std::collections::hash_map::RandomState::new().build_hasher();
h.write_u64(i);
words.extend_from_slice(&h.finish().to_le_bytes());
}
Ok(wai::pin::sha256(&words))
}
/// What a meter gave for one completion's decode.
struct Metered {
meter: String,
power_reader: Option<String>,
liveness: Option<wai::energy_report::RailLiveness>,
bracket: Option<wai::energy_burst::Bracket>,
/// The figure and its class, where the bracket seals one under a hardware class.
figure: Option<(u64, wai::energy_class::EnergyProvenance)>,
verdict: Option<wai::energy_report::Attribution>,
unmetered: Option<String>,
}
impl Metered {
fn unmetered(meter: &str, why: impl Into<String>) -> Metered {
Metered { meter: meter.into(), power_reader: None, liveness: None, bracket: None, figure: None, verdict: None, unmetered: Some(why.into()) }
}
/// The figure published: only a clean one (§8.3).
#[cfg_attr(not(feature = "receipt_claims"), allow(dead_code))]
fn published(&self) -> Option<&(u64, wai::energy_class::EnergyProvenance)> {
self.figure.as_ref().filter(|_| self.verdict == Some(wai::energy_report::Attribution::Clean))
}
fn to_json(&self) -> Value {
use wai::energy_report::Attribution;
let label = match (&self.figure, self.verdict) {
(Some(_), Some(Attribution::Clean)) => Some("figure"),
(Some(_), Some(Attribution::RelativeOnly)) => Some("relative comparison only, not a figure"),
(Some(_), _) => Some("not shown"),
(None, _) => None,
};
let verdict = self.verdict.map(|v| match v {
Attribution::Clean => "clean",
Attribution::RelativeOnly => "relative-only",
Attribution::Noisy => "noisy",
Attribution::Unusable => "unusable",
});
let values = self.bracket.as_ref().and_then(|b| b.values());
json!({
"meter": self.meter,
"power_reader": self.power_reader,
"liveness_at_rest": self.liveness.map(|l| l.label()),
"bracket": values.map(|v| json!({
"idle": {"n": v.idle.n, "mean_uw": v.idle.mean_uw, "sd_uw": v.idle.sd_uw, "live_intervals": v.idle.live_intervals, "longest_live_run": v.idle.longest_live_run},
"busy": {"n": v.busy.n, "mean_uw": v.busy.mean_uw, "sd_uw": v.busy.sd_uw, "live_intervals": v.busy.live_intervals, "longest_live_run": v.busy.longest_live_run},
"window_us": v.window_us, "completions": v.completions, "in_flight": v.in_flight,
})),
"joules_micro": self.figure.as_ref().map(|f| f.0),
"class": self.figure.as_ref().map(|f| f.1.label()),
"verdict": verdict,
"label": label,
"unmetered_reason": self.unmetered.clone().or_else(|| self.bracket.as_ref().and_then(|b| b.unsealed_reason()).map(str::to_owned)),
})
}
}
/// Probe the meter's rail at rest, then bracket completions of `t` from rest
/// (energy-measurement §3), or say why the decode is unmetered.
fn meter(spec: &str, t: &Title) -> Metered {
use wai::energy_burst::{macmon_cpu_power, measure_on, power_of_two_workers, power_reader, rapl_power_reader};
use wai::energy_report::{rail_liveness, RailLiveness};
let (mut reader, name): (Box<dyn FnMut(u32) -> Vec<f64>>, String) = match spec {
"none" => return Metered::unmetered(spec, "no meter was asked for"),
"macmon" => (Box::new(macmon_cpu_power), power_reader().to_owned()),
s if s.starts_with("rapl:") => {
let domain = &s["rapl:".len()..];
match rapl_power_reader(domain) {
Some(r) => (Box::new(r), format!("powercap {domain} energy_uj")),
None => return Metered::unmetered(spec, format!("the powercap counter of {domain} cannot be read")),
}
}
other => return Metered::unmetered(other, "not a meter this tool knows (macmon, rapl:<domain>, none)"),
};
let t0 = std::time::Instant::now();
let probe = reader(10);
let liveness = rail_liveness(&probe, t0.elapsed().as_micros() as u64);
if liveness != RailLiveness::Live {
return Metered { liveness: Some(liveness), power_reader: Some(name), ..Metered::unmetered(spec, format!("the rail is {} at rest", liveness.label())) };
}
let item = std::sync::Arc::new(t.clone());
let work = |t: &std::sync::Arc<Title>| {
let _ = wai::stage_measure::run::completion(t);
};
// A power of two of workers: the bracket a session's report publishes states its
// workers as the session does (jwp-receipts §2.4.12, §6.7).
let Some(b) = measure_on("completion", &item, work, &mut reader, power_of_two_workers()) else {
return Metered { liveness: Some(liveness), power_reader: Some(name), ..Metered::unmetered(spec, "too few samples for a bracket") };
};
let figure = b.on_chip_figure(wai::energy_class::CounterFiltering::Unknown);
let verdict = b.values().and_then(|v| v.attribution());
let unmetered = figure.is_none().then(|| b.unsealed_reason().unwrap_or("no figure").to_owned());
Metered { meter: spec.into(), power_reader: Some(name), liveness: Some(liveness), bracket: Some(b), figure, verdict, unmetered }
}
/// The session claim, and the `measurement` claim on its figure where it carries one.
#[cfg(feature = "receipt_claims")]
fn sign(t: &Title, e: &Evaluation, m: &Metered, seed: &str, signer_id: Option<&str>) -> Result<(wai::claim_receipt::ClaimReceipt, Option<wai::claim_receipt::ClaimReceipt>), String> {
use wai::claim_receipt::{rfc3339_utc, SessionHost};
use wai::energy_report::BuildRecord;
let bytes: [u8; 32] = (0..32)
.map(|i| seed.get(2 * i..2 * i + 2).and_then(|h| u8::from_str_radix(h, 16).ok()))
.collect::<Option<Vec<u8>>>()
.filter(|_| seed.len() == 64)
.and_then(|v| v.try_into().ok())
.ok_or("--sign takes a 32-byte seed as 64 hexadecimal digits")?;
let key = ed25519_dalek::SigningKey::from_bytes(&bytes);
let did = wai::signer_id::did_key(&key.verifying_key().to_bytes());
let signer_id = signer_id.unwrap_or(&did);
let build = BuildRecord::of_this_build();
let host = SessionHost { profile: "native-ffi".into(), target: build.target.clone(), build, threads: 1 };
let figure = m.published().map(|(uj, class)| wai::stage_measure::session::MeasuredFigure {
joules_micro: *uj,
class: class.clone(),
bracket: m.bracket.as_ref().and_then(|b| b.values()),
power_reader: m.power_reader.clone().unwrap_or_else(|| "unknown".into()),
});
// Where a bracket is attached, the session states the workers it ran on.
let host = wai::stage_measure::session::host_for(host, figure.as_ref());
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs());
wai::stage_measure::session::seal_session(t, e, host, fresh_nonce()?, figure.as_ref(), &key, signer_id, &rfc3339_utc(now))
}
/// A sealed claim, in a build that can seal one.
#[cfg(feature = "receipt_claims")]
type Claim = wai::claim_receipt::ClaimReceipt;
#[cfg(not(feature = "receipt_claims"))]
type Claim = ();
/// A claim's wire form and link hash, as a record writes them.
#[cfg(feature = "receipt_claims")]
fn claim_values(c: Option<&Claim>) -> (Value, Value) {
(c.map_or(Value::Null, |c| c.to_json().into()), c.map_or(Value::Null, |c| hex(&c.link_hash()).into()))
}
#[cfg(not(feature = "receipt_claims"))]
fn claim_values(_: Option<&Claim>) -> (Value, Value) {
(Value::Null, Value::Null)
}
fn bench(dir: &str, rest: &[&str]) -> Result<(), String> {
let (mut rounds, mut warmup, mut energy, mut record, mut seed, mut signer_id, mut limit) = (30u32, 3u32, "none".to_string(), None, None, None, None);
let mut idle: Option<bool> = None;
let mut it = rest.iter();
while let Some(a) = it.next() {
let mut val = || it.next().map(|s| s.to_string()).ok_or(format!("{a} needs a value"));
match *a {
"--rounds" => rounds = val()?.parse().map_err(|e| format!("--rounds: {e}"))?,
"--warmup" => warmup = val()?.parse().map_err(|e| format!("--warmup: {e}"))?,
"--energy" => energy = val()?,
"--record" => record = Some(val()?),
"--sign" => seed = Some(val()?),
"--signer-id" => signer_id = Some(val()?),
"--limit" => limit = Some(wai::staged::parse_limit(&val()?)?),
"--idle" => {
idle = Some(match val()?.as_str() {
"yes" => true,
"no" => false,
other => return Err(format!("--idle: yes or no, not {other}")),
})
}
other => return Err(format!("bench: unknown argument {other}")),
}
}
if rounds < 1 {
return Err("--rounds: at least 1".into());
}
let (t, e) = load_eval_with(dir, limit)?;
let load = || wai::bench_env::loadavg().map(|l| [l.0, l.1, l.2]);
let decode_before = load();
let decode = time_steps(&t, &e, rounds, warmup)?;
let decode_after = load();
let energy_before = load();
let m = meter(&energy, &t);
let energy_after = load();
let (session, report): (Option<Claim>, Option<Claim>) = match &seed {
None => (None, None),
#[cfg(feature = "receipt_claims")]
Some(seed) => {
let (s, r) = sign(&t, &e, &m, seed, signer_id.as_deref())?;
(Some(s), r)
}
#[cfg(not(feature = "receipt_claims"))]
Some(_) => return Err("--sign needs a build with the receipt_claims feature".into()),
};
#[cfg(not(feature = "receipt_claims"))]
let _ = &signer_id;
let claim_json = |c: &Option<Claim>| -> (Value, Value) { claim_values(c.as_ref()) };
let (session_wire, session_link) = claim_json(&session);
let (report_wire, report_link) = claim_json(&report);
let build = wai::energy_report::BuildRecord::of_this_build();
let out = json!({
"format": "wai-stage-bench/1",
"title": e.title,
"content_id": hex(&e.content_id),
"framing": e.framing,
"output_root": e.output.map(|c| hex(&c.root)),
"leaf_count": e.output.map_or(0, |c| c.leaf_count),
"machine": wai::bench_env::machine().to_string(),
"power_source": wai::bench_env::power_source(),
"load": {"decode": {"before": decode_before, "after": decode_after}, "energy": {"before": energy_before, "after": energy_after}},
"idle": idle,
"idle_stated_by": if idle.is_some() { "the operator (--idle)" } else { "not stated" },
"build": {"profile": build.profile.label(), "toolchain": build.toolchain, "target": build.target},
"decode": decode,
"energy": m.to_json(),
"claims": {"session": session_wire, "session_link_hash": session_link, "measurement": report_wire, "measurement_link_hash": report_link},
});
println!("title {} ({} leaves), {} cases over {} rounds after {} warm-up", e.title, e.output.map_or(0, |c| c.leaf_count), out["decode"]["cases"].as_array().map_or(0, Vec::len), rounds, warmup);
for c in out["decode"]["cases"].as_array().into_iter().flatten() {
println!(" {:<48} median {} us (p10 {}, p90 {}; min {})", c["case"].as_str().unwrap_or(""), c["median_us"], c["p10_us"], c["p90_us"], c["min_us"]);
}
println!("energy: {}", out["energy"]);
if !out["claims"]["session_link_hash"].is_null() {
println!("session claim {}; measurement claim {}", out["claims"]["session_link_hash"], out["claims"]["measurement_link_hash"]);
}
if let Some(path) = record {
std::fs::write(&path, pretty(&out)).map_err(|e| format!("{path}: {e}"))?;
println!("record written to {path}");
}
Ok(())
}
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
let r = match args.iter().map(String::as_str).collect::<Vec<_>>().as_slice() {
@ -1065,8 +1393,9 @@ fn main() -> ExitCode {
}),
["sim", model, plan] => sim_one(model, plan),
["gen-fixtures", corpus] => Corpus::new(corpus).and_then(|c| gen_fixtures(&c)),
["bench", dir, rest @ ..] => bench(dir, rest),
_ => {
eprintln!("usage: wai_stage_measure eval <fixture dir> [--json] [--limit OUT,SYMBOLS,WORKING,MACS] | sim <model.json | fixture dir> <plan.json> | verify <corpus> | generate-goldens <corpus> | gen-fixtures <corpus>");
eprintln!("usage: wai_stage_measure eval <fixture dir> [--json] [--limit OUT,SYMBOLS,WORKING,MACS] | sim <model.json | fixture dir> <plan.json> | verify <corpus> | generate-goldens <corpus> | gen-fixtures <corpus> | bench <fixture dir> [--rounds N] [--warmup W] [--energy macmon|rapl:<domain>|none] [--record <file>] [--sign <seed hex>] [--signer-id <id>] [--idle yes|no] [--limit OUT,SYMBOLS,WORKING,MACS]");
return ExitCode::from(2);
}
};

View file

@ -34,7 +34,11 @@
//! hash of what was presented, and every layer object with its status and the evidence
//! of its delivery receipt;
//! - `delivery-gap` — a relay's statement that it did not forward an object, and why
//! (jwp-receipts §4.2).
//! (jwp-receipts §4.2);
//! - `reconstruct-session` — what a sink reconstructed in one session ([`session`]): its
//! decoders and host, paths and switches, late presentations, the output tree, its
//! counted and transferred bytes, the classical rendition it did not need, the `stage`
//! claims it signed, and its decode energy where metered.
//!
//! A `measurement` or `operating-point` claim names the receipt that seals its figure
//! by a figure profile ([`crate::energy_figure`], energy-measurement §6.1) and that
@ -123,6 +127,11 @@ pub const DOMAIN_CLAIM_LINK: &[u8] = b"moq-jwp:claim-link\x01";
// names here so every existing caller of this module's API stays valid.
pub use crate::jwp_hash::{content_hash as object_content_hash, DOMAIN_OBJECT};
pub mod session;
pub use session::{
DecoderEntry, Missed, ObjectSeen, SessionClaim, SessionEnergy, SessionHost, SessionPath, StageSetRef, Switch,
};
/// The largest integer a claim may carry: I-JSON's (RFC 7493) exact-integer bound,
/// the canonical form's (`crate::jcs::MAX_INTEGER`).
pub(crate) const MAX_INTEGER: u64 = crate::jcs::MAX_INTEGER;
@ -299,6 +308,59 @@ pub enum Reason {
/// A `delivery-gap` claim whose subject is not the content hash of the object receipt
/// at its coordinates.
GapSubjectMismatch,
/// A session whose primary and fallback units do not sum to its units.
SessionPathSum,
/// A session whose entries' leaves are not its leaf count, whose leaf count is below
/// its units, or one of whose units and leaf count is 0 and the other not.
SessionLeafSum,
/// A session with an output root and no leaf, or leaves and no root.
SessionRootPresence,
/// A session whose host's thread count is not a power of two.
SessionHostMalformed,
/// A decoder entry not of its table: the decoder's grammar, a rendition past 65535, a
/// tier without a pin or a pin without a tier or an unknown tier, a module status
/// that does not fit its decoder, no leaf; or two entries that differ only in leaves.
SessionDecoderMalformed,
/// A decoder entry whose tier exceeds its capability's class tier.
SessionTierExceedsClass,
/// A switch between equal paths or for an unlisted reason, switches out of order,
/// more than 1024 written or more than their total.
SessionSwitchInvalid,
/// Missed presentations above the leaves or without a deadline, or a list of them
/// that is too long, out of order or holds an impossible entry.
SessionMissedInvalid,
/// Stage sets out of order, or one listed twice.
SessionStageSetsInvalid,
/// More discarded bytes than were counted.
SessionCountedInvalid,
/// A classical source without a classical length or the converse, or an unknown
/// source.
SessionClassicalInvalid,
/// The presentations do not give the session's output root over its leaf count.
SessionOutputMismatch,
/// The presentations give other units, leaves, first-use order or paths than the
/// session states, or name an entry it does not have.
SessionOutputInconsistent,
/// A byte-exact presentation decoded again gives another digest.
SessionOutputIrreproducible,
/// The session's entry object is not among the objects it received.
SessionEntryNotReceived,
/// A decoder entry's object, or an envelope held as the session's, is not among the
/// objects it received.
SessionSourceNotReceived,
/// The objects received give other counted, uncounted or transferred bytes.
SessionCountedMismatch,
/// An envelope held as the session's is not a `WAI1` or `WAI2` envelope whose
/// payloads are plaintext.
SessionEnvelopeUnreadable,
/// A decoder entry's capability or rendition is not what its object's envelope
/// gives, or it names a rendition of an envelope not held.
SessionSourceMismatch,
/// The envelopes do not give the session's classical length.
SessionClassicalMismatch,
/// A `stage` claim the session names counts units the session's outputs do not hold
/// at its stages.
SessionStageDisagrees,
}
impl Reason {
@ -376,6 +438,27 @@ impl Reason {
Reason::StageUnitsUnsupported => "stage_units_unsupported",
Reason::StageDeliveredDisagrees => "stage_delivered_disagrees",
Reason::GapSubjectMismatch => "gap_subject_mismatch",
Reason::SessionPathSum => "session_path_sum",
Reason::SessionLeafSum => "session_leaf_sum",
Reason::SessionRootPresence => "session_root_presence",
Reason::SessionHostMalformed => "session_host_malformed",
Reason::SessionDecoderMalformed => "session_decoder_malformed",
Reason::SessionTierExceedsClass => "session_tier_exceeds_class",
Reason::SessionSwitchInvalid => "session_switch_invalid",
Reason::SessionMissedInvalid => "session_missed_invalid",
Reason::SessionStageSetsInvalid => "session_stage_sets_invalid",
Reason::SessionCountedInvalid => "session_counted_invalid",
Reason::SessionClassicalInvalid => "session_classical_invalid",
Reason::SessionOutputMismatch => "session_output_mismatch",
Reason::SessionOutputInconsistent => "session_output_inconsistent",
Reason::SessionOutputIrreproducible => "session_output_irreproducible",
Reason::SessionEntryNotReceived => "session_entry_not_received",
Reason::SessionSourceNotReceived => "session_source_not_received",
Reason::SessionCountedMismatch => "session_counted_mismatch",
Reason::SessionEnvelopeUnreadable => "session_envelope_unreadable",
Reason::SessionSourceMismatch => "session_source_mismatch",
Reason::SessionClassicalMismatch => "session_classical_mismatch",
Reason::SessionStageDisagrees => "session_stage_disagrees",
}
}
}
@ -445,6 +528,7 @@ pub enum ClaimClass {
OperatingPoint,
Stage,
DeliveryGap,
ReconstructSession,
}
/// The class of `group` and `object` receipts. A claim naming it is rejected.
@ -462,6 +546,7 @@ impl ClaimClass {
ClaimClass::OperatingPoint => "operating-point",
ClaimClass::Stage => "stage",
ClaimClass::DeliveryGap => "delivery-gap",
ClaimClass::ReconstructSession => "reconstruct-session",
}
}
@ -476,6 +561,7 @@ impl ClaimClass {
"operating-point" => Some(ClaimClass::OperatingPoint),
"stage" => Some(ClaimClass::Stage),
"delivery-gap" => Some(ClaimClass::DeliveryGap),
"reconstruct-session" => Some(ClaimClass::ReconstructSession),
_ => None,
}
}
@ -485,7 +571,8 @@ impl ClaimClass {
/// everything a group receipt covers; a report about a figure takes the subject type
/// its figure profile fixes (checked at step 4); every other class is about one
/// object — a `stage` claim about the stage set's base, a `delivery-gap` claim about
/// the object not forwarded.
/// the object not forwarded, a `reconstruct-session` claim about the session's entry
/// object.
pub fn admits(self, t: SubjectType) -> bool {
match self {
ClaimClass::LogInclusion => t == SubjectType::Receipt,
@ -494,7 +581,8 @@ impl ClaimClass {
| ClaimClass::SoftBinding
| ClaimClass::BindingRecovery
| ClaimClass::Stage
| ClaimClass::DeliveryGap => t == SubjectType::Object,
| ClaimClass::DeliveryGap
| ClaimClass::ReconstructSession => t == SubjectType::Object,
}
}
}
@ -1455,6 +1543,7 @@ pub enum ClaimBody {
OperatingPoint(OperatingPointClaim),
Stage(StageClaim),
DeliveryGap(DeliveryGapClaim),
ReconstructSession(SessionClaim),
}
/// The energy body's members, in the order step 2 reads them.
@ -1547,6 +1636,7 @@ impl ClaimBody {
ClaimBody::OperatingPoint(_) => ClaimClass::OperatingPoint,
ClaimBody::Stage(_) => ClaimClass::Stage,
ClaimBody::DeliveryGap(_) => ClaimClass::DeliveryGap,
ClaimBody::ReconstructSession(_) => ClaimClass::ReconstructSession,
}
}
@ -1702,6 +1792,7 @@ impl ClaimBody {
m.insert("reason".into(), g.reason.label().into());
m.insert("observed_at".into(), g.observed_at.clone().into());
}
ClaimBody::ReconstructSession(c) => return c.to_value(),
}
Value::Object(m)
}
@ -1924,6 +2015,7 @@ impl ClaimBody {
}
ClaimBody::DeliveryGap(DeliveryGapClaim { track_alias, group_id, object_id, reason, observed_at })
}
ClaimClass::ReconstructSession => ClaimBody::ReconstructSession(SessionClaim::read(m)?),
ClaimClass::BindingRecovery => {
members(m, RECOVERY_MEMBERS)?;
ClaimBody::BindingRecovery(BindingRecoveryClaim {
@ -2056,6 +2148,7 @@ impl ClaimBody {
}
Ok(())
}
ClaimBody::ReconstructSession(c) => c.check_values(),
}
}
@ -2153,6 +2246,7 @@ impl ClaimBody {
// A gap has no rule of its own beyond its types; its subject is checked
// against the object receipt at its coordinates at step 6.
ClaimBody::DeliveryGap(_) => Ok(()),
ClaimBody::ReconstructSession(c) => c.rules(),
}
}
}
@ -3378,7 +3472,7 @@ pub(crate) fn rfc3339(s: &str) -> bool {
/// fails instead of passing on nothing. (A function rather than a public constant so
/// cbindgen does not export it into the C header.)
pub fn corpus_vector_count() -> usize {
302
459
}
/// The conformance corpus (`claim-conformance/vectors.json`), regenerated. Ed25519
@ -3416,6 +3510,8 @@ mod corpus {
use curve25519_dalek::{edwards::CompressedEdwardsY, EdwardsPoint, Scalar};
use serde_json::json;
mod session;
const T0: &str = "2026-09-27T12:00:00Z";
const WM_ALG: &str = "org.example.watermark.1";
const FP_ALG: &str = "org.example.fingerprint.1";
@ -4612,6 +4708,8 @@ mod corpus {
]);
v.extend(report_vectors());
v.extend(stage_vectors());
// §2.4.12, appended: the vectors above are an unchanged prefix.
v.extend(session::vectors());
v
}
@ -5341,7 +5439,7 @@ mod corpus {
let mut doc = Map::new();
doc.insert(
"about".into(),
"jwp-receipts section 2.4.3-2.4.11, 2.6 and 3.1-3.2: typed claim receipts and the soft \
"jwp-receipts section 2.4.3-2.4.12, 2.6 and 3.1-3.2: typed claim receipts and the soft \
binding class. Every vector is synthetic: keys are Ed25519 from fixed 32-byte seeds \
(0x11 publisher, 0x22 meter, 0x33 operator, 0x44 sink, 0x55 resolver, 0x66 relay) \
and attest nothing; figures are invented; algorithm names, endpoints, logs and registries use \
@ -5462,6 +5560,39 @@ mod corpus {
return Ok(Err(e));
}
}
// §2.4.12: a session against its outputs, the objects it received, the
// envelopes it received and the stage claims it names, in that order.
let outputs = c.get("outputs").map(presentations_of).transpose()?;
if let Some(ps) = &outputs {
let found = redecoded_of(c.get("redecoded"))?;
if let Err(e) = r.check_session_outputs(ps, &|i| found.get(&i).copied()) {
return Ok(Err(e));
}
}
let objects = c.get("objects").map(objects_of).transpose()?;
if let Some(os) = &objects
&& let Err(e) = r.check_session_objects(os)
{
return Ok(Err(e));
}
if let Some(e) = c.get("envelopes") {
let envs = envelopes_of(e)?;
let refs: Vec<&[u8]> = envs.iter().map(Vec::as_slice).collect();
if let Err(e) = r.check_session_envelopes(&refs, objects.as_deref()) {
return Ok(Err(e));
}
}
if let Some(w) = c.get("stage_claims") {
let wires = w
.as_array()
.ok_or("stage_claims must be an array")?
.iter()
.map(|x| x.as_str().ok_or("a stage claim is given as its wire form"))
.collect::<Result<Vec<&str>, _>>()?;
if let Err(e) = r.check_session_stage_claims(&wires, outputs.as_deref(), objects.as_deref()) {
return Ok(Err(e));
}
}
if let Some(w) = ctx_wire("declaration")? {
// §2.6.3: the declaration is context only together with the publisher
// key; a corpus that supplies one without the other is malformed, not
@ -5491,6 +5622,66 @@ mod corpus {
}
}
/// An `outputs` context: the presentations a session commits to.
fn presentations_of(v: &Value) -> Result<Vec<crate::output_tree::Presentation>, String> {
v.as_array()
.ok_or("outputs must be an array")?
.iter()
.map(|p| {
let n = |k: &str| p.get(k).and_then(Value::as_u64).ok_or(format!("bad output {k}"));
Ok(crate::output_tree::Presentation {
unit: n("unit")?,
layers: u16::try_from(n("layers")?).map_err(|_| "bad output layers")?,
decoder: u16::try_from(n("decoder")?).map_err(|_| "bad output decoder")?,
out_digest: hash_of(p.get("out_digest"), "output out_digest")?,
})
})
.collect()
}
/// A `redecoded` context: what a verifier found when it decoded the presentations at
/// these indices of `outputs` again.
fn redecoded_of(v: Option<&Value>) -> Result<BTreeMap<usize, [u8; 32]>, String> {
let Some(v) = v else { return Ok(BTreeMap::new()) };
v.as_array()
.ok_or("redecoded must be an array")?
.iter()
.map(|x| {
let i = x.get("index").and_then(Value::as_u64).ok_or("bad redecoded index")?;
Ok((usize::try_from(i).map_err(|_| "bad redecoded index")?, hash_of(x.get("out_digest"), "redecoded out_digest")?))
})
.collect()
}
/// An `objects` context: each object a session received.
fn objects_of(v: &Value) -> Result<Vec<ObjectSeen>, String> {
v.as_array()
.ok_or("objects must be an array")?
.iter()
.map(|o| {
Ok(ObjectSeen {
content_hash: hash_of(o.get("content_hash"), "object content_hash")?,
path: o.get("path").and_then(Value::as_str).and_then(SessionPath::from_label).ok_or("bad object path")?,
counted: match o.get("counted") {
Some(Value::Null) => None,
c => Some(c.and_then(Value::as_u64).ok_or("bad object counted")?),
},
transferred: o.get("transferred").and_then(Value::as_u64).ok_or("bad object transferred")?,
presented: o.get("presented").and_then(Value::as_bool).ok_or("bad object presented")?,
})
})
.collect()
}
/// An `envelopes` context: the bytes of each `WAI2` envelope the session received.
fn envelopes_of(v: &Value) -> Result<Vec<Vec<u8>>, String> {
v.as_array()
.ok_or("envelopes must be an array")?
.iter()
.map(|e| e.get("envelope").and_then(Value::as_str).and_then(unhex).ok_or_else(|| "bad envelope".to_owned()))
.collect()
}
/// A `figure` context: the receipt that carries the figure a report names — an
/// `energy` claim's wire form (`claim`), viewed as `crate::energy_figure` views it, or
/// a receipt the verifier has verified, given as its view (`view`): `{profile,
@ -6252,6 +6443,77 @@ mod tests {
assert_eq!(hex(digest.as_bytes()), "55d483624eca626b710c93ea954f854df57fd16029aaf6d6fa78edc1771b484c");
}
/// The vectors the corpus held before the `reconstruct-session` class was added,
/// pinned as the tests above pin theirs. The digest was computed from the corpus as
/// it stood before the class was added and must never be edited to make the test pass.
#[test]
fn the_vectors_before_the_session_are_unchanged() {
let doc: Value = crate::oer::load(conformance_vectors().as_bytes()).unwrap();
let vs = doc["vectors"].as_array().unwrap();
let first = Value::Array(vs[..302].to_vec());
let digest = blake3::hash(serde_json::to_string(&first).unwrap().as_bytes());
assert_eq!(hex(digest.as_bytes()), "41465e7b57ca795b8ee4b998f11553a3a2b720fe994e6f0d53866af73f281ba1");
}
/// A session claim's grammars (§2.4.12): `media`, a decoder identifier, and the
/// rounding of its worker count.
#[test]
fn session_grammars() {
use super::session::{decoder_ok, media_ok};
for ok in ["builtin:[email protected]", "builtin:a@1", "builtin:[email protected]+build", &format!("module:sha256:{}", "ab".repeat(32))] {
assert!(decoder_ok(ok), "{ok}");
}
for bad in [
"builtin:wai",
"builtin:@1",
"builtin:wai@",
"builtin:wai@v1",
"builtin:Wai@1",
"builtin:wai@1@2",
"builtin:wai@1 ",
"module:sha256:",
&format!("module:sha256:{}", "AB".repeat(32)),
&format!("module:sha256:{}", "ab".repeat(33)),
&format!("builtin:{}@1", "a".repeat(65)),
&format!("builtin:a@1{}", "0".repeat(64)),
"[email protected]",
] {
assert!(!decoder_ok(bad), "{bad}");
}
assert!(media_ok("video") && media_ok("a") && media_ok(&"a".repeat(32)) && media_ok("x-1"));
assert!(!media_ok("") && !media_ok(&"a".repeat(33)) && !media_ok("Video") && !media_ok("a b") && !media_ok("a.b"));
let r = SessionHost::rounded_threads;
assert_eq!([r(0), r(1), r(2), r(3), r(4), r(7), r(8), r(1023), r(u64::MAX)], [1, 1, 2, 2, 4, 4, 8, 512, 1 << 63]);
}
/// A session claim round-trips through its wire form, its figure is viewed under the
/// claim-link profile only where it was metered, and its figure is a second item for
/// confirmation, needing the session's authority and the class's.
#[test]
fn a_session_round_trips_and_its_figure_is_its_own_item() {
use crate::energy_figure::EnergyFigures;
let doc: Value = crate::oer::load(conformance_vectors().as_bytes()).unwrap();
let wire = |name: &str| {
doc["vectors"].as_array().unwrap().iter().find(|v| v["name"] == name).unwrap()["receipt"].as_str().unwrap().to_owned()
};
let unmetered = verify_claim_json(&wire("session_accept")).unwrap();
let metered = verify_claim_json(&wire("session_metered_accept")).unwrap();
for c in [&unmetered, &metered] {
assert_eq!(c.class(), ClaimClass::ReconstructSession);
assert_eq!(ClaimReceipt::from_json(&c.to_json()).unwrap(), *c);
assert_eq!(c.authorities(), vec!["reconstruct-session".to_owned()]);
}
assert_eq!(unmetered.figure_authorities(), None);
assert_eq!(metered.figure_authorities(), Some(vec!["reconstruct-session".to_owned(), "energy/OnChipCounter".to_owned()]));
let v = unmetered.figure_view().unwrap();
assert_eq!((v.profile.as_str(), v.identity, v.figures.len()), ("moq-jwp:claim-link", unmetered.link_hash(), 0));
let v = metered.figure_view().unwrap();
assert_eq!((v.profile.as_str(), v.identity, v.signer), ("moq-jwp:claim-link", metered.link_hash(), Some(metered.signer_pubkey)));
let f = v.figure(None).unwrap();
assert_eq!(f.joules_micro, 250_000);
assert_eq!(f.class.as_ref().map(|c| c.label()), Some("OnChipCounter"));
}
/// §2.4.10's rules, each refused at step 4 by `seal` as a verifier refuses it; the
/// delivered stage a reader derives from the objects; rule 5 not applied when no
/// stage is stated as delivered; and whether a commitment was met, derived.

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -312,6 +312,28 @@ fn glossary_links_agree_with_the_registry() {
}
}
/// `is_classical` is the glossary's classical era: exactly the capabilities an entry of
/// era `classical` links, which a session claim's classical rendition is read from
/// (jwp-receipts §2.4.12).
#[test]
fn classical_is_the_glossary_era() {
let doc: Value = serde_json::from_str(&read("glossary/codecs.json")).expect("codecs.json parses");
let mut linked: Vec<String> = doc["entries"]
.as_array()
.expect("entries")
.iter()
.filter(|e| e["era"] == "classical")
.flat_map(|e| e["wai"].as_array().cloned().unwrap_or_default())
.map(|l| l["capability"].as_str().expect("a linked capability").to_owned())
.collect();
linked.sort();
linked.dedup();
let mut ours: Vec<String> = known_capabilities().iter().filter(|c| is_classical(c)).map(|c| c.to_string()).collect();
ours.sort();
assert_eq!(ours, linked);
assert!(CLASSICAL.iter().all(|c| known_capabilities().contains(c)), "every classical capability is registered");
}
/// A valid fixture, then one mutation per rule: removing any rule from
/// `glossary_problems` fails its row.
#[test]

View file

@ -1445,6 +1445,29 @@ impl DeterminismTier {
}
}
/// The capabilities that decode a classical codec: those the codec glossary
/// (`wai/glossary/codecs.json`) links from an entry whose era is `classical`, the
/// registered standards' own formats. WAI's own formats, the neural codecs, records,
/// derivations and parameter sets are not among them. A test holds this list to the
/// glossary. A `reconstruct-session` claim's classical rendition is one of these
/// (`extensions/jwp-receipts.md` §2.4.12).
const CLASSICAL: &[&str] = &[
CAP_IMAGE_JPEG, CAP_IMAGE_JP2, CAP_IMAGE_AVIF, CAP_IMAGE_JXL, CAP_IMAGE_PNG,
CAP_VIDEO_AVC, CAP_VIDEO_HEVC, CAP_VIDEO_VVC, CAP_VIDEO_LCEVC, CAP_VIDEO_EVC, CAP_VIDEO_EVC_BASELINE,
CAP_VIDEO_AVS3, CAP_VIDEO_VP9, CAP_VIDEO_AV1, CAP_VIDEO_AV1_LOSSLESS, CAP_VIDEO_AV2, CAP_VIDEO_J2K,
CAP_AUDIO_AAC, CAP_AUDIO_MPEGH, CAP_AUDIO_AVS3, CAP_AUDIO_IAMF, CAP_AUDIO_FLAC, CAP_AUDIO_OPUS,
CAP_AUDIO_AC4, CAP_AUDIO_IVAS,
CAP_TEXT_ZSTD, CAP_TEXT_XZ,
CAP_MESH_GLB, CAP_MESH_DRACO, CAP_PC_GPCC, CAP_PC_VPCC, CAP_SPLAT_SPZ,
CAP_HAPTIC_MIHS,
CAP_META_HDR10PLUS, CAP_META_DOVI,
];
/// Whether `cap` decodes a classical codec ([`CLASSICAL`]).
pub fn is_classical(cap: &str) -> bool {
CLASSICAL.contains(&cap)
}
/// The tier `cap`'s registry class gives (SPEC §3.1, *Tier*): `decode-equivalence`
/// for `NeuralIntegerExact`, `entropy-consistency` for `NeuralEntropyExact`, `none`
/// for every other class. `None` for an unregistered capability.

View file

@ -0,0 +1,29 @@
//! Terms of `extensions/delivery-telemetry.md` that other extensions read: the switch
//! reasons of its §5, which a `reconstruct-session` claim's switches name
//! (`extensions/jwp-receipts.md` §2.4.12). Ungated and dependency-free, so a claim
//! verifier reads the one list without building the telemetry module.
/// The switch reasons of delivery-telemetry §5, a closed list a later extension adds to.
pub const SWITCH_REASONS: &[&str] = &[
"pin-malformed",
"form-not-registered",
"form-unreadable",
"form-required",
"prior-revoked",
"prior-not-authorised",
"prior-not-held",
"digest-superseded",
"set-malformed",
"part-mismatch",
"set-incomplete",
"missing-capability",
"module-unverified",
"module-revoked",
"trap",
"fuel-exhausted",
"memory-exhausted",
"deadline-threshold",
"unit-hash-mismatch",
"budget-exhausted",
"recovered",
];

View file

@ -436,6 +436,15 @@ pub fn default_workers() -> usize {
std::thread::available_parallelism().map(|n| n.get().saturating_sub(1).clamp(1, 10)).unwrap_or(6)
}
/// The worker count a meter bursts on where its bracket is published beside a session
/// claim: [`default_workers`] rounded down to a power of two, as a session writes its
/// host's workers (jwp-receipts §2.4.12). The bracket's `in_flight` is then the
/// session's `threads`, and names no more of the machine than it does (§6.7).
pub fn power_of_two_workers() -> usize {
let n = default_workers().max(1);
1 << (usize::BITS - 1 - n.leading_zeros())
}
/// Run `work` continuously on `workers` threads that are held at a start
/// barrier and released together as the window opens; sample the busy bracket
/// with `sample_busy` while they run; close the window; stop the workers.
@ -503,7 +512,17 @@ where
/// The reader is a parameter so a reader that misbehaves — a frozen rail, a
/// rail refreshed in batches — can be put in front of the protocol and shown
/// to be refused.
pub fn measure_with<T, W, R>(label: &str, item: &T, work: W, mut reader: R) -> Option<Bracket>
pub fn measure_with<T, W, R>(label: &str, item: &T, work: W, reader: R) -> Option<Bracket>
where
T: Clone + Send + 'static,
W: Fn(&T) + Send + Sync + 'static,
R: FnMut(u32) -> Vec<f64>,
{
measure_on(label, item, work, reader, default_workers())
}
/// [`measure_with`], the burst on `workers` threads.
pub fn measure_on<T, W, R>(label: &str, item: &T, work: W, mut reader: R, workers: usize) -> Option<Bracket>
where
T: Clone + Send + 'static,
W: Fn(&T) + Send + Sync + 'static,
@ -516,12 +535,92 @@ where
if idle_w.len() < 3 {
return None;
}
let workers = default_workers();
eprintln!(" {label}: burst on {workers} threads, released together …");
let (busy_w, window_s, count) = burst_from_rest(workers, item, work, || reader(BUSY_SAMPLES));
Some(Bracket { idle_w, busy_w, window_s, idle_span_s, count })
}
/// The most power, in watts, an interval of a counter read as power may imply: above the
/// draw of any processor rail, so an interval implying more is a counter that was reset,
/// misread or is not what it says, read as a wrap, and not a measurement.
const COUNTER_MAX_PLAUSIBLE_W: u64 = 5_000;
/// The longest interval between two reads of a counter read as power, as a multiple of
/// the reader's sampling interval: a longer one is a reader that stalled, across which a
/// reset can imply a plausible power, and is not a measurement.
const COUNTER_MAX_INTERVAL_FACTOR: u64 = 10;
/// Power samples, in watts, from a cumulative energy counter in microjoules: `n`
/// successive differences of the counter, each over the time between its two reads
/// (a microjoule per microsecond is a watt), across the counter's wrap at `wrap_at`
/// (its range: its maximum plus one). `read_uj` reads the counter, `clock_us` a
/// monotonic microsecond clock, and `pause` waits `interval_us`, the sampling interval,
/// between reads. A read that fails ends the samples there, so a counter that cannot be
/// read gives fewer than `n` and a bracket refuses them. A counter that goes down is read
/// as having wrapped, and one reset to a low value instead reads as nearly its whole
/// range in one interval, so the samples also end at an interval that is not a reading:
/// one whose difference implies more than 5 kW (`COUNTER_MAX_PLAUSIBLE_W`), which no
/// processor rail draws; one longer than ten sampling intervals
/// (`COUNTER_MAX_INTERVAL_FACTOR`), over which a reset can imply less; and one whose
/// difference is more than half the counter's range. An interval in which neither the
/// clock nor the counter moved gives 0 W, which is not an advance (energy-measurement
/// §3.1). The source is injected, so the arithmetic is tested without the hardware.
pub fn counter_power_samples(
mut read_uj: impl FnMut() -> Option<u64>,
mut clock_us: impl FnMut() -> u64,
mut pause: impl FnMut(),
n: u32,
interval_us: u64,
wrap_at: u64,
) -> Vec<f64> {
let mut out = Vec::new();
let Some(mut c0) = read_uj() else { return out };
let mut t0 = clock_us();
for _ in 0..n {
pause();
let Some(c1) = read_uj() else { break };
let t1 = clock_us();
let delta = if c1 >= c0 { c1 - c0 } else { wrap_at.saturating_sub(c0).saturating_add(c1) };
let dt = t1.saturating_sub(t0);
// A microjoule per microsecond is a watt: no more than the bound per microsecond,
// over no more than the longest interval, and less than half the range.
if u128::from(delta) > u128::from(COUNTER_MAX_PLAUSIBLE_W) * u128::from(dt)
|| u128::from(dt) > u128::from(COUNTER_MAX_INTERVAL_FACTOR) * u128::from(interval_us)
|| delta > wrap_at / 2
{
break;
}
out.push(if dt == 0 { 0.0 } else { delta as f64 / dt as f64 });
(c0, t0) = (c1, t1);
}
out
}
/// A power reader over the Linux powercap (RAPL) counter of `domain`
/// (`/sys/class/powercap/<domain>/energy_uj`, the file [`crate::energy_meter::CounterMeter::rapl`]
/// reads), for [`measure_with`]: `reader(n)` gives `n` samples, one per sampling interval,
/// by [`counter_power_samples`]. `None` where the counter or its range cannot be read —
/// every non-Linux target, a Linux without powercap, or one that does not let this
/// process read it.
pub fn rapl_power_reader(domain: &str) -> Option<impl FnMut(u32) -> Vec<f64> + use<>> {
let base = format!("/sys/class/powercap/{domain}");
let energy = format!("{base}/energy_uj");
let read = |p: &str| std::fs::read_to_string(p).ok()?.trim().parse::<u64>().ok();
let wrap_at = read(&format!("{base}/max_energy_range_uj"))?.saturating_add(1);
read(&energy)?;
let origin = Instant::now();
Some(move |n: u32| {
counter_power_samples(
|| read(&energy),
|| origin.elapsed().as_micros() as u64,
|| std::thread::sleep(std::time::Duration::from_millis(u64::from(SAMPLE_INTERVAL_MS))),
n,
u64::from(SAMPLE_INTERVAL_MS) * 1_000,
wrap_at,
)
})
}
/// The power reader the reference meters name in a measurement report: the
/// open-source reader and the reading it takes.
pub fn power_reader() -> &'static str {
@ -568,6 +667,135 @@ mod tests {
(0..n).map(|i| pair[i % 2]).collect()
}
/// A counter read as power: each difference over its interval, across a wrap; a
/// frozen counter reads as no advance; a failed read ends the samples; an interval
/// the clock did not advance gives 0 W.
#[test]
fn a_counter_reads_as_power_across_its_wrap() {
use std::cell::Cell;
// 5 W for 200 ms is 1,000,000 uJ per interval; the counter wraps at 3,500,000.
let (c, t) = (Cell::new(500_000u64), Cell::new(0u64));
let pause = || {
t.set(t.get() + 200_000);
c.set((c.get() + 1_000_000) % 3_500_000);
};
let w = counter_power_samples(|| Some(c.get()), || t.get(), pause, 6, 200_000, 3_500_000);
assert_eq!(w, vec![5.0; 6], "a wrap reads as the difference across it");
assert_eq!(Liveness::of_power_samples(&w).live_intervals, 1, "a constant power is one advance");
// A rising draw is live over the probe; the same counter frozen is not.
let (c, t, k) = (Cell::new(0u64), Cell::new(0u64), Cell::new(0u64));
let rising = || {
k.set(k.get() + 1);
t.set(t.get() + 200_000);
c.set(c.get() + 1_000_000 + k.get() * 1_000);
};
let w = counter_power_samples(|| Some(c.get()), || t.get(), rising, 6, 200_000, u64::MAX);
assert_eq!(w.len(), 6);
assert!(Liveness::of_power_samples(&w).is_live_over(1_200_000), "{w:?}");
let t = Cell::new(0u64);
let frozen = counter_power_samples(|| Some(42), || t.get(), || t.set(t.get() + 200_000), 6, 200_000, u64::MAX);
assert_eq!(frozen, vec![0.0; 6]);
assert!(!Liveness::of_power_samples(&frozen).is_live());
// A read that fails ends the samples; none at all gives none.
let reads = Cell::new(0u32);
let failing = || {
reads.set(reads.get() + 1);
(reads.get() <= 3).then_some(u64::from(reads.get()) * 1_000)
};
let t = Cell::new(0u64);
assert_eq!(counter_power_samples(failing, || t.get(), || t.set(t.get() + 1_000), 6, 1_000, u64::MAX), vec![1.0, 1.0]);
assert!(counter_power_samples(|| None, || 0, || {}, 6, 1_000, u64::MAX).is_empty());
// A clock that did not move gives 0 W where the counter did not move either, never
// a division by zero; a counter that moved while the clock stood still is no reading.
assert_eq!(counter_power_samples(|| Some(5), || 7, || {}, 2, 1_000, u64::MAX), vec![0.0, 0.0]);
let c = Cell::new(0u64);
assert!(counter_power_samples(|| Some(c.get()), || 7, || c.set(c.get() + 10), 2, 1_000, u64::MAX).is_empty());
}
/// A counter reset mid-probe — down to a low value, not across its range — reads as a
/// wrap of nearly the whole range in one interval: more than any processor rail draws.
/// The samples end there, keeping those before it; a genuine wrap at the same rate is
/// read across. The range is a powercap package domain's.
#[test]
fn a_counter_reset_ends_the_samples() {
use std::cell::Cell;
let wrap = 262_143_328_850u64 + 1;
let (c, t, k) = (Cell::new(50_000_000u64), Cell::new(0u64), Cell::new(0u32));
let reset = || {
k.set(k.get() + 1);
t.set(t.get() + 100_000);
c.set(if k.get() == 3 { 10 } else { c.get() + 500_000 + u64::from(k.get()) });
};
let w = counter_power_samples(|| Some(c.get()), || t.get(), reset, 6, 100_000, wrap);
assert_eq!(w.len(), 2, "the reset ends the samples: {w:?}");
assert!(w.iter().all(|x| (5.0..5.1).contains(x)), "{w:?}");
// The same counter wrapping across its range at 5 W is read through.
let (c, t) = (Cell::new(wrap - 700_000), Cell::new(0u64));
let across = || {
t.set(t.get() + 100_000);
c.set((c.get() + 500_000) % wrap);
};
assert_eq!(counter_power_samples(|| Some(c.get()), || t.get(), across, 6, 100_000, wrap), vec![5.0; 6]);
// At the bound and one past it.
let (c, t) = (Cell::new(0u64), Cell::new(0u64));
let at = || {
t.set(t.get() + 1);
c.set(c.get() + 5_000);
};
assert_eq!(counter_power_samples(|| Some(c.get()), || t.get(), at, 3, 1, u64::MAX), vec![5_000.0; 3]);
let (c, t) = (Cell::new(0u64), Cell::new(0u64));
let past = || {
t.set(t.get() + 1);
c.set(c.get() + 5_001);
};
assert!(counter_power_samples(|| Some(c.get()), || t.get(), past, 3, 1, u64::MAX).is_empty());
}
/// A counter reset across a long interval implies less than 5 kW: a powercap package
/// domain's range over a minute is about 4.4 kW. An interval longer than ten sampling
/// intervals ends the samples, and so does a difference of more than half the range,
/// each on its own and each at its edge.
#[test]
fn a_reset_across_a_long_interval_ends_the_samples() {
use std::cell::Cell;
let wrap = 262_143_328_850u64 + 1;
// Two intervals at 5 W, then a minute's stall across which the counter is reset.
let (c, t, k) = (Cell::new(50_000_000u64), Cell::new(0u64), Cell::new(0u32));
let stall = || {
k.set(k.get() + 1);
if k.get() == 3 {
t.set(t.get() + 60_000_000);
c.set(10);
} else {
t.set(t.get() + 100_000);
c.set(c.get() + 500_000);
}
};
assert_eq!(counter_power_samples(|| Some(c.get()), || t.get(), stall, 6, 100_000, wrap), vec![5.0; 2]);
// The stall alone, the counter advancing at 5 W across it: ten intervals are read,
// ten and one more are not.
for (gap, kept) in [(1_000_000u64, 6usize), (1_000_001, 2)] {
let (c, t, k) = (Cell::new(0u64), Cell::new(0u64), Cell::new(0u32));
let slow = || {
k.set(k.get() + 1);
let dt = if k.get() == 3 { gap } else { 100_000 };
t.set(t.get() + dt);
c.set(c.get() + 5 * dt);
};
assert_eq!(counter_power_samples(|| Some(c.get()), || t.get(), slow, 6, 100_000, wrap).len(), kept, "gap {gap}");
}
// The half range alone, at 15 W over a 3.5 J range: half the range is read, half
// and one more is not.
for (step, kept) in [(1_750_000u64, 4usize), (1_750_001, 0)] {
let (c, t) = (Cell::new(0u64), Cell::new(0u64));
let half = || {
t.set(t.get() + 200_000);
c.set((c.get() + step) % 3_500_000);
};
assert_eq!(counter_power_samples(|| Some(c.get()), || t.get(), half, 4, 200_000, 3_500_000).len(), kept, "step {step}");
}
}
#[test]
fn the_figure_divides_by_the_work_the_window_paid_for() {
// 10 W marginal over 3 s = 30 J; 10 workers from rest, 60 completions:

View file

@ -137,10 +137,18 @@ pub trait EnergyFigures {
}
/// An `energy` claim (`moq-jwp:claim-link`): its figure and class, identified by its
/// link hash. A claim of another class carries no figure.
/// link hash; and a `reconstruct-session` claim whose `energy` is not `null`: the
/// session's decode figure and its class (jwp-receipts §2.4.12). A claim of another class,
/// and a session that was not metered, carry no figure.
impl EnergyFigures for ClaimReceipt {
fn figure_view(&self) -> Option<FigureView> {
self.verify().ok()?;
if let ClaimBody::ReconstructSession(s) = &self.body
&& let Some(e) = &s.energy
{
let class = e.acquisition_class().ok()?;
return Some(FigureView::single("moq-jwp:claim-link", self.link_hash(), e.joules_micro, Some(class), self.signer_pubkey));
}
let ClaimBody::Energy(e) = &self.body else {
return Some(FigureView {
profile: "moq-jwp:claim-link".into(),

View file

@ -123,6 +123,45 @@ impl Liveness {
}
}
/// What a power reader's rail is at rest, before a tool brackets a decode with it
/// (staged-measurement §8.6): a probe of samples read with nothing running.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RailLiveness {
/// The probe shows a live rail under §3.1.
Live,
/// The reader gave samples that do not show a live rail: a frozen rail, a value
/// held, a rail refreshed in batches, or too few samples or too short a span to tell.
Stale,
/// The reader gave no sample: it is not there.
Absent,
}
impl RailLiveness {
pub fn label(self) -> &'static str {
match self {
RailLiveness::Live => "live",
RailLiveness::Stale => "stale",
RailLiveness::Absent => "absent",
}
}
}
/// Classify a probe of power samples, in watts, read at rest over `span_us`: `Absent`
/// for none, `Live` where they show a live rail by §3.1's rule
/// ([`Liveness::of_power_samples`], [`Liveness::is_live_over`]), `Stale` otherwise. The
/// rule is the bracket's, so a rail that passes the probe can still fail its brackets,
/// and a figure is sealed only from brackets that pass (`energy_burst`). A rail that
/// fails the probe is not bracketed at all, and the decode is unmetered with its reason.
pub fn rail_liveness(samples_w: &[f64], span_us: u64) -> RailLiveness {
if samples_w.is_empty() {
RailLiveness::Absent
} else if Liveness::of_power_samples(samples_w).is_live_over(span_us) {
RailLiveness::Live
} else {
RailLiveness::Stale
}
}
/// One bracket of a report, as integers (§6.2).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BracketSummary {
@ -728,6 +767,28 @@ mod tests {
assert_eq!(Liveness::of_power_samples(&[5.0, 5.1, 5.1, 5.2, 5.3, 5.4]), Liveness { intervals: 6, live_intervals: 5, longest_live_run: 3 });
}
/// A probe at rest (staged-measurement §8.6): no sample is an absent reader; a frozen
/// rail, a held value, a batch refresh, a probe too short in samples or in time are
/// stale; a rail advancing in four of every five samples over 300 ms is live.
#[test]
fn a_rail_probe_is_absent_stale_or_live() {
assert_eq!(rail_liveness(&[], 2_000_000), RailLiveness::Absent);
assert_eq!(rail_liveness(&[0.0; 10], 2_000_000), RailLiveness::Stale, "frozen at zero");
assert_eq!(rail_liveness(&[4.2; 10], 2_000_000), RailLiveness::Stale, "a value held");
let batch = [0.0, 0.0, 0.0, 7.5, 7.6, 0.0, 0.0, 0.0, 0.0, 0.0];
assert_eq!(rail_liveness(&batch, 2_000_000), RailLiveness::Stale, "one refresh in two steps");
let live = [5.0, 5.1, 5.0, 5.2, 5.1, 5.3, 5.2, 5.0, 5.1, 5.2];
assert_eq!(rail_liveness(&live, 2_000_000), RailLiveness::Live);
assert_eq!(rail_liveness(&live, 299_999), RailLiveness::Stale, "under 300 ms");
assert_eq!(rail_liveness(&live[..4], 2_000_000), RailLiveness::Stale, "four advances are not five");
// Four of every five: one repeat in ten passes, three do not.
let one_repeat = [5.0, 5.1, 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 5.8];
assert_eq!(rail_liveness(&one_repeat, 2_000_000), RailLiveness::Live);
let three_repeats = [5.0, 5.0, 5.1, 5.1, 5.2, 5.2, 5.3, 5.4, 5.5, 5.6];
assert_eq!(rail_liveness(&three_repeats, 2_000_000), RailLiveness::Stale);
assert_eq!([RailLiveness::Live, RailLiveness::Stale, RailLiveness::Absent].map(RailLiveness::label), ["live", "stale", "absent"]);
}
/// A reader of the report refuses a flat bracket under a hardware class,
/// whatever liveness facts it carries: equal samples mean at most one
/// advance, so facts that say otherwise are not the bracket's.

View file

@ -928,6 +928,21 @@ impl Item {
}
}
/// The energy figure of a `reconstruct-session` claim accepted under §3.1, as an item of
/// its own (§7.5): it needs `reconstruct-session` and the energy authority of its class
/// ([`ClaimReceipt::figure_authorities`]), and its time is the claim's `issued_at`. A
/// verifier confirms it beside the claim's own item and reports both, so a session can
/// be confirmed with a figure its origin did not give its key. `None` for a claim of
/// another class, and for a session that was not metered.
pub fn figure_of_claim(c: &ClaimReceipt) -> Option<Item> {
Some(Item {
signer_pubkey: c.signer_pubkey,
signer_id: c.signer_id.clone(),
authorities: c.figure_authorities()?,
time: Some(c.issued_at.clone()),
})
}
/// A group receipt and the object receipts under it (§7.5), from what a verified group
/// receipt and its objects carry ([`Item::from_group`] reads them): `delivery`, and
/// `energy/<class>` for each acquisition class `classes` names — the classes the

View file

@ -6,9 +6,10 @@
use super::*;
use crate::claim_receipt::{
stage_point, verify_claim_json, BindingType, ClaimBody, ClaimReceipt, DeclaredUncertainty, DeliveryGapClaim,
EnergyClaim, FigureHeader, GapReason, LawfulBasisClaim, MeasurementClaim, OperatingPointClaim, Purpose,
SoftBindingClaim, StageClaim, SubjectType, VectorResult, STAGE_VOCABULARY,
stage_point, verify_claim_json, BindingType, ClaimBody, ClaimReceipt, DecoderEntry, DeclaredUncertainty,
DeliveryGapClaim, EnergyClaim, FigureHeader, GapReason, LawfulBasisClaim, MeasurementClaim, OperatingPointClaim,
Purpose, SessionClaim, SessionEnergy, SessionHost, SessionPath, SoftBindingClaim, StageClaim, SubjectType,
VectorResult, STAGE_VOCABULARY,
};
use crate::energy_report::{BuildProfile, BuildRecord};
use serde_json::json;
@ -41,6 +42,8 @@ const OPM: u8 = 0x74; // operating-point, energy/ModelBased
const FR: u8 = 0x75; // receipt, energy/ModelBased
const SK: u8 = 0x76; // stage
const RL: u8 = 0x77; // delivery-gap
const SS: u8 = 0x78; // reconstruct-session
const SE: u8 = 0x79; // reconstruct-session, energy/OnChipCounter
const GRANT_MAX: u64 = 43_200;
const STATUS_MAX: u64 = 3_600;
@ -112,6 +115,9 @@ struct Logs {
rf: Revision,
/// r1 with a sink's key for `stage` and a relay's for `delivery-gap`.
rs: Revision,
/// r1 with two sink keys for `reconstruct-session`, one also holding
/// `energy/OnChipCounter`.
rr: Revision,
q0: Revision,
u0: Revision,
}
@ -196,9 +202,19 @@ fn logs() -> Logs {
R0,
unsigned(P, Some(&r1), "2026-09-17T00:00:00Z", R1, with_keys(&r1, vec![e("sk", SK, &["stage"]), e("rl", RL, &["delivery-gap"])])),
);
let rr = sealed(
R0,
unsigned(
P,
Some(&r1),
"2026-09-17T06:00:00Z",
R1,
with_keys(&r1, vec![e("ss", SS, &["reconstruct-session"]), e("se", SE, &["reconstruct-session", "energy/OnChipCounter"])]),
),
);
let q0 = sealed(R0, unsigned(P8443, None, "2026-09-01T00:00:00Z", R1, vec![e("d1", D1, &["delivery", "receipt"])]));
let u0 = sealed(STRANGER, unsigned(P, None, "2026-09-02T00:00:00Z", R2, vec![e("d1", ATT, &["delivery", "receipt"])]));
Logs { r0, r1, r2, t2, o2, o3, r2_revoked, r2_retired, r2_module_revoked, r2_module_key_revoked, rm, rf, rs, q0, u0 }
Logs { r0, r1, r2, t2, o2, o3, r2_revoked, r2_retired, r2_module_revoked, r2_module_key_revoked, rm, rf, rs, rr, q0, u0 }
}
// ───────────────────────────── items and claims ─────────────────────────────
@ -286,6 +302,61 @@ fn gap_body() -> ClaimBody {
})
}
/// A `reconstruct-session` claim: one unit presented once through a pinned base, metered
/// under `OnChipCounter` or not.
fn session_body(metered: bool) -> ClaimBody {
let ps = [crate::output_tree::Presentation { unit: 0, layers: 1, decoder: 0, out_digest: crate::pin::sha256(b"signer-conformance: an output") }];
ClaimBody::ReconstructSession(SessionClaim {
session_nonce: crate::pin::sha256(b"signer-conformance: a session nonce"),
media: "image".into(),
host: SessionHost {
profile: "native-ffi".into(),
target: None,
build: BuildRecord { profile: BuildProfile::Release, toolchain: None, target: None },
threads: 1,
},
decoders: vec![DecoderEntry {
capability: "wai.neural.int_hyper".into(),
rendition: None,
object: subject(),
path: SessionPath::Primary,
decoder: "builtin:[email protected]".into(),
variant: None,
pin: Some(crate::pin::sha256(b"signer-conformance: a parameter set")),
tier: Some("decode-equivalence".into()),
module_status: None,
leaves: 1,
}],
units: 1,
leaf_count: 1,
output_root: crate::output_tree::root(&ps).expect("one presentation"),
primary_units: 1,
fallback_units: 0,
switches: vec![],
switches_total: 0,
deadline_us: None,
missed: 0,
missed_units: vec![],
stage_sets: vec![],
framing: "registered".into(),
counted_primary: 4_000,
counted_fallback: 0,
counted_discarded: 0,
uncounted: 0,
transferred: 4_000,
receipt_bytes: 0,
classical_declared: None,
classical_source: None,
energy: metered.then(|| SessionEnergy {
joules_micro: 90_000,
energy_provenance: "OnChipCounter".into(),
energy_uncertainty: Some(DeclaredUncertainty { relative_ppm: 20_000, window_us: 1_000_000 }),
energy_counter_filtering: Some("Unknown".into()),
energy_calibration_ref: None,
}),
})
}
fn declaration_body() -> ClaimBody {
ClaimBody::LawfulBasis(LawfulBasisClaim::Declaration {
vocabulary: DPV.into(),
@ -427,6 +498,10 @@ fn unconfirmed(step: &str, reason: &str) -> String {
fn refused(step: &str, reason: &str) -> String {
format!("refused step={step} reason={reason}")
}
/// A claim's verdict with its energy figure's, an item of its own (§7.5).
fn and_figure(claim: String, figure: String) -> String {
format!("{claim}; figure {figure}")
}
const NOTE: &str = " note=published_test_key";
fn anchored(r: &Revision, path: &str) -> String {
format!("anchored revision={} anchor={path}{NOTE}", h(r))
@ -1727,6 +1802,58 @@ fn vectors() -> Vec<V> {
fetch_rs(),
confirmed_rs("rl"),
));
// ── §7.5: a session needs `reconstruct-session`; its energy figure is an item of its own ──
let with_rr: &[&Revision] = &[&l.r0, &l.r1, &l.rr];
let session_at = |key_id: &str, anchor: &str| format!("confirmed origin={P} key_id={key_id} head={} anchor={anchor}", h(&l.rr));
v.push(confirm_claim(
"session_needs_its_class",
"an unmetered session by a key holding delivery and receipt: delivering objects is not reconstructing them",
claim(D1, &id(PID, "d1"), session_body(false)),
with_rr,
fetched(&l.rr),
refused("6", "authority_not_granted"),
));
v.push(confirm_claim(
"session_confirmed",
"an unmetered session by a sink's key holding reconstruct-session, under a fetched anchor: not pinned-only, and no figure to confirm",
claim(SS, &id(PID, "ss"), session_body(false)),
with_rr,
fetched(&l.rr),
session_at("ss", "fetched"),
));
v.push(confirm_claim(
"session_figure_not_granted",
"a metered session by a key holding reconstruct-session but not energy/OnChipCounter, under a pinned anchor: the session is confirmed and its figure refused",
claim(SS, &id(PID, "ss"), session_body(true)),
with_rr,
pinned(&l.rr),
and_figure(session_at("ss", "pinned"), refused("6", "authority_not_granted")),
));
v.push(confirm_claim(
"session_figure_unpinned",
"a metered session by a key holding both authorities, under a fetched anchor: the session is confirmed and its hardware-class figure unconfirmed",
claim(SE, &id(PID, "se"), session_body(true)),
with_rr,
fetched(&l.rr),
and_figure(session_at("se", "fetched"), unconfirmed("6a", "anchor_not_pinned")),
));
v.push(confirm_claim(
"session_figure_confirmed",
"a metered session by a key holding both authorities, under a pinned anchor: both confirmed, and the figure's key does not deliver",
claim(SE, &id(PID, "se"), session_body(true)),
with_rr,
pinned(&l.rr),
and_figure(session_at("se", "pinned"), format!("{} also_delivers=false", session_at("se", "pinned"))),
));
v.push(confirm_claim(
"session_under_did_key",
"a metered session signed under a did:key, the normal case for a sink's ephemeral key: unconfirmed, and so is its figure",
claim(SE, &crate::signer_id::did_key(&pk(SE)), session_body(true)),
with_rr,
pinned(&l.rr),
and_figure(unconfirmed("1", "signer_id_not_origin_bound"), unconfirmed("1", "signer_id_not_origin_bound")),
));
v
}
@ -1834,15 +1961,21 @@ fn run_one(v: &Value) -> Result<String, String> {
let c = v.get("context").and_then(Value::as_object).unwrap_or(&empty);
match v.get("procedure").and_then(Value::as_str) {
Some("confirm") => {
let it = match v.get("receipt").and_then(Value::as_str) {
let (it, figure) = match v.get("receipt").and_then(Value::as_str) {
Some(wire) => match verify_claim_json(wire) {
Ok(cl) => Item::from_claim(&cl),
Ok(cl) => (Item::from_claim(&cl), Item::figure_of_claim(&cl)),
Err(r) => return Ok(format!("reject step={} reason={}", r.step, r.reason.label())),
},
None => item_of(c.get("item").ok_or("no item")?)?,
None => (item_of(c.get("item").ok_or("no item")?)?, None),
};
let expected = c.get("expected_origin").map(expectation_of).transpose()?;
Ok(confirm(&it, &revisions_of(c, "revisions")?, &anchors_of(c)?, expected.as_ref()).verdict())
let (revisions, anchors) = (revisions_of(c, "revisions")?, anchors_of(c)?);
let claim = confirm(&it, &revisions, &anchors, expected.as_ref()).verdict();
// A session's energy figure is confirmed as an item of its own, beside it (§7.5).
Ok(match figure {
Some(f) => and_figure(claim, confirm(&f, &revisions, &anchors, expected.as_ref()).verdict()),
None => claim,
})
}
Some("anchor") => {
let origin = s(c, "origin")?;

View file

@ -236,11 +236,39 @@ fn claim_authorities_follow_the_table() {
assert_eq!(a, want);
seen.insert(a[0].split('/').next().unwrap().to_owned());
}
for class in ["energy", "lawful-basis", "measurement", "operating-point", "stage", "delivery-gap"] {
for class in ["energy", "lawful-basis", "measurement", "operating-point", "stage", "delivery-gap", "reconstruct-session"] {
assert!(seen.contains(class), "the claim corpus holds no confirmable {class} claim: {seen:?}");
}
}
/// A session's energy figure is an item of its own (§7.5): `reconstruct-session` and the
/// energy authority of the class its `energy` writes, read from the nested member; an
/// unmetered session, and every other class, has none.
#[test]
fn a_session_figure_is_its_own_item() {
let doc: Value = serde_json::from_str(include_str!("../../../claim-conformance/vectors.json")).unwrap();
let (mut metered, mut unmetered) = (0, 0);
for v in doc["vectors"].as_array().unwrap() {
let Some(wire) = v.get("receipt").and_then(Value::as_str) else { continue };
let Ok(c) = verify_claim_json(wire) else { continue };
let body = serde_json::from_str::<Value>(wire).unwrap()["body"].clone();
let want = match (c.class().label(), body.get("energy")) {
("reconstruct-session", Some(Value::Object(e))) => {
metered += 1;
Some(vec!["reconstruct-session".to_owned(), format!("energy/{}", e["energy_provenance"].as_str().unwrap())])
}
("reconstruct-session", _) => {
unmetered += 1;
None
}
_ => None,
};
assert_eq!(Item::figure_of_claim(&c).map(|i| i.authorities().to_vec()), want);
assert_eq!(c.figure_authorities(), want);
}
assert!(metered > 0 && unmetered > 0, "the claim corpus holds metered and unmetered sessions: {metered} {unmetered}");
}
/// A pinned record moved forward by a retrieval keeps the revision that was pinned. A key
/// a later, fetched revision adds with a pre-emption authority is not confirmed, though
/// the record stays pinned for the hardware and lawful-basis authorities.

View file

@ -338,22 +338,28 @@ pub mod http_binding;
// Staged-delivery measurement (extensions/staged-measurement.md): the output tree
// (Appendix C), the counted bytes of an envelope (§3), and the evaluation of a staged
// title (§3–§6, §9) through the staged sink.
#[cfg(feature = "stage_measure")]
// title (§3–§6, §9) through the staged sink. The output tree is also what a session
// claim commits to (jwp-receipts §2.4.12), so `receipt_claims` builds it too.
#[cfg(any(feature = "stage_measure", feature = "receipt_claims"))]
pub mod output_tree;
#[cfg(feature = "stage_measure")]
pub mod measure_framing;
#[cfg(feature = "stage_measure")]
pub mod stage_measure;
// The terms of delivery telemetry other extensions read (its §5 switch reasons): ungated,
// so a session claim's verifier names them without the telemetry module.
pub mod delivery_terms;
// Delivery telemetry (extensions/delivery-telemetry.md): advisory, unsigned observations
// about delivery, as CMCD version 2 and CMSD reports, and session metrics.
#[cfg(feature = "telemetry")]
pub mod telemetry;
// Typed receipt classes (extensions/jwp-receipts.md §2.4.3–§2.4.7, §2.6, §3.1–§3.2):
// energy, lawful-basis, log-inclusion, soft-binding and binding-recovery statements
// about delivered objects, on one `claim` envelope and one signature rule beside the
// Typed receipt classes (extensions/jwp-receipts.md §2.4.3–§2.4.12, §2.6, §3.1–§3.2):
// energy, lawful-basis, log-inclusion, soft-binding, binding-recovery, measurement,
// operating-point, stage, delivery-gap and reconstruct-session statements about
// delivered objects, on one `claim` envelope and one signature rule beside the
// delivery class `group`/`object` carry. A recovery through a soft binding attests the
// ancestor, never the object in hand. An OER receipt an energy claim names is read and
// verified by `oer` under oer-receipts' own rules.
@ -381,6 +387,11 @@ pub mod jwp_delivery;
#[cfg(feature = "receipt_claims")]
pub mod energy_figure;
// A sink's record of one session, tallied and sealed as a `reconstruct-session` claim
// (jwp-receipts §2.4.12) and checked against what it recorded before it leaves the sink.
#[cfg(feature = "receipt_claims")]
pub mod session_recorder;
// jwp-receipts §7.1: what a `signer_id` names — an origin-bound `did:web`, an Ed25519
// `did:key`, or a label. Ungated and dependency-free, so every surface that shows a
// signer can say which.

View file

@ -0,0 +1,543 @@
//! A sink's record of one session, sealed as a `reconstruct-session` claim
//! (`extensions/jwp-receipts.md` §2.4.12).
//!
//! A [`SessionRecorder`] is told what the sink does as it does it: the decoders it uses, the
//! objects it receives, each presentation with its output's digest and, where it missed its
//! deadline, by how much; each switch of path; the `stage` claims it signs about the stage
//! sets it observes; the classical rendition it did not need; and the decode figure where
//! it was metered. [`SessionRecorder::finish`] tallies them as the class states them —
//! decoder entries in first-use order, the leaves, units and paths, the output tree, the
//! first 1024 switches and 256 missed presentations in order, the counted sums — seals the
//! claim, and checks it against the presentations, the objects and the `stage` claims it
//! names (§3.1 step 6) before it returns it. A recorder never emits a claim a verifier
//! holding what it recorded refuses.
use crate::claim_receipt::{
session::SESSION_CLASSICAL_SOURCES, ClaimBody, ClaimClass, ClaimReceipt, DecoderEntry, Missed, ObjectSeen,
Rejection, SessionClaim, SessionEnergy, SessionHost, SessionPath, StageSetRef, SubjectType, Switch,
};
use crate::output_tree::{self, Presentation};
use ed25519_dalek::SigningKey;
use std::collections::{BTreeMap, BTreeSet};
/// The most switches and missed presentations a claim writes (§2.4.12).
const MAX_SWITCHES: usize = 1024;
const MAX_MISSED: usize = 256;
/// A decoder entry without its leaves: what the sink names when it uses a decoder on an
/// object.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DecoderKey {
pub capability: String,
pub rendition: Option<u64>,
/// The content hash of the object decoded from: the `WAI2` envelope whose rendition it
/// is, or the `WAI1` envelope.
pub object: [u8; 32],
pub path: SessionPath,
pub decoder: String,
pub variant: Option<String>,
pub pin: Option<[u8; 32]>,
pub tier: Option<String>,
pub module_status: Option<String>,
}
/// Why a recorder refused what it was told.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RecorderError {
/// A presentation named a decoder the recorder was not given.
UnknownDecoder(usize),
/// A presentation at no layer.
NoLayers,
/// A (unit, layers) presented twice.
Duplicate { unit: u64, layers: u16 },
/// A presentation late, with no deadline to be late against, or late by nothing.
NotLate,
/// A switch between one path and itself, or for a reason delivery-telemetry §5 does not
/// list.
Switch,
/// A claim named as a stage set's that is not a `stage` claim, does not verify (§3.1
/// steps 1–4), or is another key's than the stage claims named before it.
NotAStageClaim,
}
impl std::fmt::Display for RecorderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownDecoder(d) => write!(f, "no decoder {d} was named"),
Self::NoLayers => f.write_str("a presentation at no layer"),
Self::Duplicate { unit, layers } => write!(f, "unit {unit} at {layers} layers is presented twice"),
Self::NotLate => f.write_str("a presentation late with no deadline, or late by nothing"),
Self::Switch => f.write_str("a switch to the same path, or for an unlisted reason"),
Self::NotAStageClaim => f.write_str("not a verified stage claim of this key"),
}
}
}
impl std::error::Error for RecorderError {}
/// A sealed session: the claim, and what a verifier checks it against at step 6.
#[derive(Clone, Debug)]
pub struct RecordedSession {
pub claim: ClaimReceipt,
/// The presentations, their decoder indices those of the claim's entries.
pub presentations: Vec<Presentation>,
pub objects: Vec<ObjectSeen>,
/// The wire forms of the `stage` claims the session names.
pub stage_claims: Vec<String>,
}
/// One session at a sink (§2.4.12).
#[derive(Clone, Debug)]
pub struct SessionRecorder {
session_nonce: [u8; 32],
media: String,
host: SessionHost,
deadline_us: Option<u64>,
keys: Vec<DecoderKey>,
/// (unit, layers, decoder handle, out_digest).
presentations: Vec<(u64, u16, usize, [u8; 32])>,
seen: BTreeSet<(u64, u16)>,
late: Vec<Missed>,
switches: Vec<Switch>,
objects: Vec<ObjectSeen>,
receipt_bytes: u64,
classical: Option<u64>,
stage_sets: BTreeSet<StageSetRef>,
stage_signer: Option<[u8; 32]>,
/// The wire forms of the `stage` claims named, each once.
stage_claims: Vec<String>,
energy: Option<SessionEnergy>,
}
impl SessionRecorder {
/// A session with a fresh `session_nonce` — fresh random bytes, derived from nothing
/// that identifies a person, a device or an account — on `host`, whose entry object's
/// manifest `media` is `media`, under a per-unit deadline or none.
pub fn new(session_nonce: [u8; 32], media: &str, host: SessionHost, deadline_us: Option<u64>) -> SessionRecorder {
SessionRecorder {
session_nonce,
media: media.to_owned(),
host,
deadline_us,
keys: Vec::new(),
presentations: Vec::new(),
seen: BTreeSet::new(),
late: Vec::new(),
switches: Vec::new(),
objects: Vec::new(),
receipt_bytes: 0,
classical: None,
stage_sets: BTreeSet::new(),
stage_signer: None,
stage_claims: Vec::new(),
energy: None,
}
}
/// The handle of a decoder the sink uses: the same key gives the same handle.
pub fn decoder(&mut self, key: DecoderKey) -> usize {
match self.keys.iter().position(|k| *k == key) {
Some(i) => i,
None => {
self.keys.push(key);
self.keys.len() - 1
}
}
}
/// An object received, whole or as byte ranges.
pub fn received(&mut self, seen: ObjectSeen) {
self.objects.push(seen);
}
/// Bytes of receipts and claims received.
pub fn receipt_bytes(&mut self, n: u64) {
self.receipt_bytes = self.receipt_bytes.saturating_add(n);
}
/// A presentation: `unit` at `layers`, through the decoder `decoder` names, its output's
/// SHA-256, and how late it was where it missed its deadline.
pub fn presented(
&mut self,
unit: u64,
layers: u16,
decoder: usize,
out_digest: [u8; 32],
late_us: Option<u64>,
) -> Result<(), RecorderError> {
if decoder >= self.keys.len() {
return Err(RecorderError::UnknownDecoder(decoder));
}
if layers == 0 {
return Err(RecorderError::NoLayers);
}
if late_us.is_some_and(|l| l == 0) || (late_us.is_some() && self.deadline_us.is_none()) {
return Err(RecorderError::NotLate);
}
if !self.seen.insert((unit, layers)) {
return Err(RecorderError::Duplicate { unit, layers });
}
self.presentations.push((unit, layers, decoder, out_digest));
if let Some(late_us) = late_us {
self.late.push(Missed { unit, layers: u64::from(layers), late_us });
}
Ok(())
}
/// A switch of path at `unit`, for a delivery-telemetry §5 reason.
pub fn switched(&mut self, unit: u64, from: SessionPath, to: SessionPath, reason: &str) -> Result<(), RecorderError> {
if from == to || !crate::delivery_terms::SWITCH_REASONS.contains(&reason) {
return Err(RecorderError::Switch);
}
self.switches.push(Switch { unit, from, to, reason: reason.to_owned() });
Ok(())
}
/// A `stage` claim this sink signed about a stage set of the session, whose unit 0 is the
/// session's unit `first_unit`. A sink names one for each epoch of a set: it signs the
/// claim of an epoch before it forgets the set's base (staged-delivery §4.4, §14), and
/// of the last at the session's end, and names every one it signed, since the claims
/// of all the epochs together bound what it presented (§2.4.12). It must verify (§3.1
/// steps 1–4), and every stage claim is the same key's: the key that finishes the
/// session, which [`seal_checked`] confirms when it checks the session against them.
pub fn stage_set(&mut self, first_unit: u64, claim: &ClaimReceipt) -> Result<(), RecorderError> {
if claim.class() != ClaimClass::Stage
|| claim.verify().is_err()
|| self.stage_signer.is_some_and(|k| k != claim.signer_pubkey)
{
return Err(RecorderError::NotAStageClaim);
}
self.stage_signer = Some(claim.signer_pubkey);
self.stage_sets.insert(StageSetRef { first_unit, claim: claim.link_hash() });
let wire = claim.to_json();
if !self.stage_claims.contains(&wire) {
self.stage_claims.push(wire);
}
Ok(())
}
/// An entry envelope's classical rendition the sink did not need: its length as the
/// envelope's `WAI2` rendition table declares it.
pub fn classical(&mut self, declared_len: u64) {
self.classical = Some(self.classical.unwrap_or(0).saturating_add(declared_len));
}
/// The decode figure of one completion of the presentations, or `None` where the decode
/// was not metered.
pub fn energy(&mut self, e: Option<SessionEnergy>) {
self.energy = e;
}
/// The wire forms of the `stage` claims the session names.
pub fn stage_claims(&self) -> &[String] {
&self.stage_claims
}
/// The body, the presentations and the objects the record gives: entries in first-use
/// order (presentations taken by (layers, unit)), the decoders never used left out.
pub fn tally(&self) -> (SessionClaim, Vec<Presentation>, Vec<ObjectSeen>) {
let mut by_use: Vec<&(u64, u16, usize, [u8; 32])> = self.presentations.iter().collect();
by_use.sort_by_key(|p| (p.1, p.0));
let mut order: Vec<usize> = Vec::new();
for p in &by_use {
if !order.contains(&p.2) {
order.push(p.2);
}
}
let index: BTreeMap<usize, u16> = order.iter().enumerate().map(|(i, h)| (*h, i as u16)).collect();
let ps: Vec<Presentation> = self
.presentations
.iter()
.map(|&(unit, layers, h, out_digest)| Presentation { unit, layers, decoder: index[&h], out_digest })
.collect();
let decoders: Vec<DecoderEntry> = order
.iter()
.enumerate()
.map(|(i, h)| {
let k = &self.keys[*h];
DecoderEntry {
capability: k.capability.clone(),
rendition: k.rendition,
object: k.object,
path: k.path,
decoder: k.decoder.clone(),
variant: k.variant.clone(),
pin: k.pin,
tier: k.tier.clone(),
module_status: k.module_status.clone(),
leaves: ps.iter().filter(|p| usize::from(p.decoder) == i).count() as u64,
}
})
.collect();
let mut finals: BTreeMap<u64, &Presentation> = BTreeMap::new();
for p in &ps {
let e = finals.entry(p.unit).or_insert(p);
if p.layers > e.layers {
*e = p;
}
}
let primary = finals.values().filter(|p| decoders[usize::from(p.decoder)].path == SessionPath::Primary).count() as u64;
let mut switches = self.switches.clone();
switches.sort_by_key(|s| s.unit);
let switches_total = switches.len() as u64;
switches.truncate(MAX_SWITCHES);
let mut missed_units = self.late.clone();
missed_units.sort_by_key(|m| (m.unit, m.layers));
let missed = missed_units.len() as u64;
missed_units.truncate(MAX_MISSED);
let sum = |f: &dyn Fn(&ObjectSeen) -> bool| -> u64 {
self.objects.iter().filter(|o| f(o)).filter_map(|o| o.counted).fold(0u64, u64::saturating_add)
};
let body = SessionClaim {
session_nonce: self.session_nonce,
media: self.media.clone(),
host: self.host.clone(),
decoders,
units: finals.len() as u64,
leaf_count: ps.len() as u64,
output_root: output_tree::root(&ps).ok().flatten(),
primary_units: primary,
fallback_units: finals.len() as u64 - primary,
switches,
switches_total,
deadline_us: self.deadline_us,
missed,
missed_units,
stage_sets: self.stage_sets.iter().copied().collect(),
framing: crate::claim_receipt::session::SESSION_FRAMINGS[0].to_owned(),
counted_primary: sum(&|o| o.path == SessionPath::Primary),
counted_fallback: sum(&|o| o.path == SessionPath::Fallback),
counted_discarded: sum(&|o| !o.presented),
uncounted: self.objects.iter().filter(|o| o.counted.is_none()).count() as u64,
transferred: self.objects.iter().map(|o| o.transferred).fold(0u64, u64::saturating_add),
receipt_bytes: self.receipt_bytes,
classical_declared: self.classical,
classical_source: self.classical.map(|_| SESSION_CLASSICAL_SOURCES[0].to_owned()),
energy: self.energy.clone(),
};
(body, ps, self.objects.clone())
}
/// Tally the record ([`SessionRecorder::tally`]) and seal it with [`seal_checked`]. The
/// claim's subject is the session's entry object; `seq` and `parent` continue the
/// key's claim chain (§2.4.3), after the `stage` claims it signed.
#[allow(clippy::too_many_arguments)]
pub fn finish(
self,
subject: [u8; 32],
key: &SigningKey,
signer_id: &str,
seq: u64,
parent: Option<[u8; 32]>,
issued_at: &str,
) -> Result<RecordedSession, Rejection> {
let (body, ps, objects) = self.tally();
seal_checked(body, ps, objects, self.stage_claims, subject, key, signer_id, seq, parent, issued_at)
}
}
/// Seal a session body and check the claim against the presentations, the objects and the
/// `stage` claims it was tallied from (§3.1 steps 1–4 by [`ClaimReceipt::seal`], step 6
/// here), so a claim those refuse never leaves the sink. A `stage` claim another key
/// signed is refused there (`context_mismatch`): the key that signs the session signs the
/// stage claims it names.
#[allow(clippy::too_many_arguments)]
pub fn seal_checked(
body: SessionClaim,
presentations: Vec<Presentation>,
objects: Vec<ObjectSeen>,
stage_claims: Vec<String>,
subject: [u8; 32],
key: &SigningKey,
signer_id: &str,
seq: u64,
parent: Option<[u8; 32]>,
issued_at: &str,
) -> Result<RecordedSession, Rejection> {
let claim = ClaimReceipt::seal(key, signer_id, SubjectType::Object, subject, seq, issued_at, ClaimBody::ReconstructSession(body), parent)?;
claim.check_session_outputs(&presentations, &|_| None)?;
claim.check_session_objects(&objects)?;
let wires: Vec<&str> = stage_claims.iter().map(String::as_str).collect();
claim.check_session_stage_claims(&wires, Some(&presentations), Some(&objects))?;
Ok(RecordedSession { claim, presentations, objects, stage_claims })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::claim_receipt::Reason;
use crate::energy_report::{BuildProfile, BuildRecord};
fn host() -> SessionHost {
SessionHost { profile: "native-ffi".into(), target: None, build: BuildRecord { profile: BuildProfile::Release, toolchain: None, target: None }, threads: 1 }
}
fn key(capability: &str, path: SessionPath, object: [u8; 32]) -> DecoderKey {
DecoderKey {
capability: capability.into(),
rendition: None,
object,
path,
decoder: "builtin:[email protected]".into(),
variant: None,
pin: None,
tier: None,
module_status: None,
}
}
fn d(unit: u64, layers: u16) -> [u8; 32] {
crate::pin::sha256(format!("{unit}/{layers}").as_bytes())
}
const T: &str = "2026-10-06T12:00:00Z";
/// A session told out of order — refinements before the fallback, a decoder named and
/// never used — tallies into first-use order and seals; what it gives checks against the
/// claim, and the claim against it.
#[test]
fn a_recorder_tallies_and_seals_a_session() {
let k = SigningKey::from_bytes(&[0x44; 32]);
let mut r = SessionRecorder::new([7; 32], "video", host(), Some(40_000));
let refine = r.decoder(key("wai.video.int_refine", SessionPath::Primary, [2; 32]));
let _unused = r.decoder(key("wai.video.avc", SessionPath::Fallback, [9; 32]));
let base = r.decoder(key("wai.video.int_motion", SessionPath::Primary, [1; 32]));
let fallback = r.decoder(key("wai.video.av1", SessionPath::Fallback, [3; 32]));
assert_eq!(r.decoder(key("wai.video.int_motion", SessionPath::Primary, [1; 32])), base, "one key, one handle");
assert_ne!(r.decoder(key("wai.video.int_motion", SessionPath::Primary, [4; 32])), base, "another object, another entry");
for u in 0..3 {
r.presented(u, 1, base, d(u, 1), None).unwrap();
}
r.presented(0, 2, refine, d(0, 2), None).unwrap();
r.presented(1, 2, refine, d(1, 2), Some(900)).unwrap();
r.presented(3, 1, fallback, d(3, 1), Some(1_200)).unwrap();
r.switched(3, SessionPath::Primary, SessionPath::Fallback, "set-incomplete").unwrap();
r.received(ObjectSeen { content_hash: [1; 32], path: SessionPath::Primary, counted: Some(5_000), transferred: 5_000, presented: true });
r.received(ObjectSeen { content_hash: [2; 32], path: SessionPath::Primary, counted: Some(700), transferred: 700, presented: true });
r.received(ObjectSeen { content_hash: [6; 32], path: SessionPath::Primary, counted: Some(300), transferred: 300, presented: false });
r.received(ObjectSeen { content_hash: [3; 32], path: SessionPath::Fallback, counted: None, transferred: 3_000, presented: true });
r.classical(700);
let s = r.finish([1; 32], &k, "did:example:sink", 0, None, T).unwrap();
let ClaimBody::ReconstructSession(b) = &s.claim.body else { panic!("a session") };
let caps: Vec<&str> = b.decoders.iter().map(|d| d.capability.as_str()).collect();
assert_eq!(caps, ["wai.video.int_motion", "wai.video.av1", "wai.video.int_refine"], "first use by (layers, unit); the unused decoder left out");
assert_eq!(b.decoders.iter().map(|d| d.leaves).collect::<Vec<_>>(), [3, 1, 2]);
assert_eq!((b.units, b.leaf_count, b.primary_units, b.fallback_units), (4, 6, 3, 1));
assert_eq!((b.missed, b.missed_units.len()), (2, 2));
assert_eq!((b.counted_primary, b.counted_fallback, b.counted_discarded, b.uncounted, b.transferred), (6_000, 0, 300, 1, 9_000));
assert_eq!((b.classical_declared, b.classical_source.as_deref()), (Some(700), Some("wai2-table")));
assert_eq!(b.output_root, output_tree::root(&s.presentations).unwrap());
s.claim.verify().unwrap();
s.claim.check_session_outputs(&s.presentations, &|_| None).unwrap();
s.claim.check_session_objects(&s.objects).unwrap();
// The recorder refuses what it cannot record.
let mut r = SessionRecorder::new([7; 32], "video", host(), None);
let h = r.decoder(key("wai.video.int_motion", SessionPath::Primary, [1; 32]));
assert_eq!(r.presented(0, 1, h + 1, d(0, 1), None), Err(RecorderError::UnknownDecoder(h + 1)));
assert_eq!(r.presented(0, 0, h, d(0, 0), None), Err(RecorderError::NoLayers));
assert_eq!(r.presented(0, 1, h, d(0, 1), Some(5)), Err(RecorderError::NotLate), "late with no deadline");
r.presented(0, 1, h, d(0, 1), None).unwrap();
assert_eq!(r.presented(0, 1, h, d(0, 1), None), Err(RecorderError::Duplicate { unit: 0, layers: 1 }));
assert_eq!(r.switched(0, SessionPath::Primary, SessionPath::Primary, "recovered"), Err(RecorderError::Switch));
assert_eq!(r.switched(0, SessionPath::Primary, SessionPath::Fallback, "too-slow"), Err(RecorderError::Switch));
}
/// A body that was mis-tallied is refused at step 6 before it leaves the sink: a leaf
/// moved between entries, the units counted on the other path, an object's counted
/// bytes changed. Each passes steps 1–4.
#[test]
fn cannot_emit_inconsistent() {
let k = SigningKey::from_bytes(&[0x44; 32]);
let mut r = SessionRecorder::new([7; 32], "video", host(), None);
let base = r.decoder(key("wai.video.int_motion", SessionPath::Primary, [1; 32]));
let refine = r.decoder(key("wai.video.int_refine", SessionPath::Primary, [2; 32]));
for u in 0..3 {
r.presented(u, 1, base, d(u, 1), None).unwrap();
r.presented(u, 2, refine, d(u, 2), None).unwrap();
}
r.received(ObjectSeen { content_hash: [1; 32], path: SessionPath::Primary, counted: Some(5_000), transferred: 5_000, presented: true });
r.received(ObjectSeen { content_hash: [2; 32], path: SessionPath::Primary, counted: Some(700), transferred: 700, presented: true });
let (body, ps, objects) = r.tally();
let seal = |b: SessionClaim, o: Vec<ObjectSeen>| seal_checked(b, ps.clone(), o, vec![], [1; 32], &k, "did:example:sink", 0, None, T).map(|_| ()).map_err(|e| (e.step, e.reason));
assert_eq!(seal(body.clone(), objects.clone()), Ok(()));
let mut moved = body.clone();
moved.decoders[0].leaves -= 1;
moved.decoders[1].leaves += 1;
assert_eq!(seal(moved, objects.clone()), Err((6, Reason::SessionOutputInconsistent)));
let paths = SessionClaim { primary_units: 2, fallback_units: 1, ..body.clone() };
assert_eq!(seal(paths, objects.clone()), Err((6, Reason::SessionOutputInconsistent)));
let mut fewer = objects.clone();
fewer[0].counted = Some(4_999);
assert_eq!(seal(body.clone(), fewer), Err((6, Reason::SessionCountedMismatch)));
// An entry naming an object the sink did not receive.
let mut elsewhere = body.clone();
elsewhere.decoders[1].object = [8; 32];
assert_eq!(seal(elsewhere, objects.clone()), Err((6, Reason::SessionSourceNotReceived)));
// And steps 1-4 run first: a decoder identifier of no grammar is refused there.
let mut bad = body;
bad.decoders[0].decoder = "wai".into();
assert_eq!(seal(bad, objects), Err((4, Reason::SessionDecoderMalformed)));
}
/// A `stage` claim about the set of `n` units over `base`, every unit at stage 0, signed
/// with `k`.
fn stage_claim(k: &SigningKey, base: [u8; 32], n: u64) -> ClaimReceipt {
use crate::claim_receipt::{stage_point, StageClaim, STAGE_VOCABULARY};
let body = ClaimBody::Stage(StageClaim {
vocabulary: STAGE_VOCABULARY.into(),
op_requested: None,
op_committed: None,
op_delivered: None,
presented: stage_point(0),
units_by_stage: vec![n],
tier: "none".into(),
stage_hash: None,
objects: vec![],
conflicts: 0,
deadline_misses: 0,
bytes_envelopes: 4_000,
});
ClaimReceipt::seal(k, "did:example:sink", SubjectType::Object, base, 0, T, body, None).unwrap()
}
/// A recorder of `units` units over a base it received, with `stage` named for them.
fn staged(units: u64, stage: &ClaimReceipt) -> SessionRecorder {
let mut r = SessionRecorder::new([7; 32], "video", host(), None);
let h = r.decoder(key("wai.video.int_motion", SessionPath::Primary, stage.subject));
for u in 0..units {
r.presented(u, 1, h, d(u, 1), None).unwrap();
}
r.received(ObjectSeen { content_hash: stage.subject, path: SessionPath::Primary, counted: Some(4_000), transferred: 4_000, presented: true });
r.stage_set(0, stage).unwrap();
r
}
/// A recorder checks the session against the `stage` claims it names before it returns
/// it, as a verifier holding them does: a stage claim of another key, or one counting
/// units the session never presented, never leaves the sink beside the session (the
/// review's R1 and R2); a stage claim that does not verify is not named at all.
#[test]
fn a_recorder_checks_its_stage_claims() {
let sink = SigningKey::from_bytes(&[0x44; 32]);
let base = [5; 32];
let stage = stage_claim(&sink, base, 4);
let finish = |r: SessionRecorder, k: &SigningKey| r.finish(base, k, "did:example:sink", 1, None, T).map(|_| ()).map_err(|e| (e.step, e.reason));
// As honest: four units presented, the stage claim's key finishes.
let s = staged(4, &stage).finish(base, &sink, "did:example:sink", 1, None, T).unwrap();
assert_eq!(s.stage_claims, vec![stage.to_json()]);
let wires: Vec<&str> = s.stage_claims.iter().map(String::as_str).collect();
s.claim.check_session_stage_claims(&wires, Some(&s.presentations), Some(&s.objects)).unwrap();
// R1: another key finishes the session.
assert_eq!(finish(staged(4, &stage), &SigningKey::from_bytes(&[0x45; 32])), Err((6, Reason::ContextMismatch)));
// R2: two of the four units the stage claim counts presented.
assert_eq!(finish(staged(2, &stage), &sink), Err((6, Reason::SessionStageDisagrees)));
// A stage claim altered after signing, or of another class, is refused when named.
let mut altered = stage.clone();
altered.seq += 1;
let mut r = SessionRecorder::new([7; 32], "video", host(), None);
assert_eq!(r.stage_set(0, &altered), Err(RecorderError::NotAStageClaim));
let other_key = stage_claim(&SigningKey::from_bytes(&[0x45; 32]), base, 4);
r.stage_set(0, &stage).unwrap();
assert_eq!(r.stage_set(4, &other_key), Err(RecorderError::NotAStageClaim));
}
}

View file

@ -38,10 +38,20 @@ pub struct DecodedBase {
/// the capability it was decoded through, whether that was the fallback, its tier and
/// whether its pin resolved. A refusal carries the decode's code.
pub fn decode_base(b: &TitleBase, limits: &Limits) -> Result<DecodedBase, MeasureError> {
let at = &b.obj.path;
checked(b, decode_pinned(b, limits)?)
}
/// The base's decode alone: `staged::decode_base_pinned` against its parameter set, under
/// the title's cost limit.
fn decode_pinned(b: &TitleBase, limits: &Limits) -> Result<crate::staged::BaseDecode, MeasureError> {
let set: BTreeMap<String, &[u8]> = b.set.iter().map(|s| (s.member.clone(), s.bytes.as_slice())).collect();
let d = crate::staged::decode_base_pinned(&b.obj.bytes, None, &set, Some(&limits.cost), None)
.map_err(|e| MeasureError::Refused { at: at.clone(), code: e.code().to_string(), detail: e.to_string() })?;
crate::staged::decode_base_pinned(&b.obj.bytes, None, &set, Some(&limits.cost), None)
.map_err(|e| MeasureError::Refused { at: b.obj.path.clone(), code: e.code().to_string(), detail: e.to_string() })
}
/// A base's decode checked against what its case states.
fn checked(b: &TitleBase, d: crate::staged::BaseDecode) -> Result<DecodedBase, MeasureError> {
let at = &b.obj.path;
let mut info = d.info;
info.first_unit = b.first_unit;
if let Some(s0) = &b.stage0 {
@ -88,6 +98,8 @@ pub struct StageEvent<'a> {
pub capability: &'a str,
/// The pin the base's decode verified, for stage 0.
pub pin: Option<&'a str>,
/// The content hash of the envelope that produced it: the base's, or the layer's.
pub object: [u8; 32],
pub buf: &'a UnitBuf,
}
@ -149,7 +161,7 @@ pub fn replay(
if !seen.insert(unit) {
return Err(invalid(format!("two bases give the global unit {unit}")));
}
on_stage(StageEvent { unit, base: bi, layers: 1, capability: &b.info.capability, pin: b.pin.as_deref(), buf })?;
on_stage(StageEvent { unit, base: bi, layers: 1, capability: &b.info.capability, pin: b.pin.as_deref(), object: b.info.content_hash, buf })?;
}
let reports = sink.add_base(b.info.clone(), b.units.clone());
if let Some(r) = reports.iter().find(|r| r.envelope == Some(StageRefusal::ChainOverLimit)) {
@ -189,7 +201,15 @@ pub fn replay(
for &u in &r.applied {
let st = chain.unit(u).expect("an applied unit");
let unit = base.info.first_unit + u as u64;
on_stage(StageEvent { unit, base: info.base, layers: st.stage as u16 + 1, capability: &info.capability, pin: None, buf: &st.buf })?;
on_stage(StageEvent {
unit,
base: info.base,
layers: st.stage as u16 + 1,
capability: &info.capability,
pin: None,
object: r.content_hash,
buf: &st.buf,
})?;
}
}
}
@ -203,3 +223,131 @@ pub fn replay(
finals.sort_by_key(|f| f.unit);
Ok(finals)
}
/// One decode step of a title, the case staged-measurement §7.1 times: a base decoded,
/// giving its units at one layer, or one layer applied, giving the units it covers at its
/// stage.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Step {
/// The title's base at this index.
Base(usize),
/// The title's layer at this index.
Layer(usize),
}
/// The steps of a title in the order [`replay`] takes them: every base, then each layer
/// index in turn and, within one, the layers in title order.
pub fn steps(t: &super::title::Title, bases: &[DecodedBase]) -> Result<Vec<Step>, MeasureError> {
let infos: Vec<LayerInfo> = t.layers.iter().map(|o| layer_info(&o.bytes, bases, &o.path)).collect::<Result<_, _>>()?;
let mut out: Vec<Step> = (0..t.bases.len()).map(Step::Base).collect();
let top = infos.iter().map(|l| l.layer).max().unwrap_or(0);
for k in 1..=top {
out.extend(infos.iter().enumerate().filter(|(_, l)| l.layer == k).map(|(i, _)| Step::Layer(i)));
}
Ok(out)
}
/// A step made ready to run: every step before it in [`steps`] order done, so running it
/// does the step's own work and nothing else.
pub enum Prepared<'a> {
Base { base: &'a TitleBase, limits: &'a Limits },
Layer { sink: StageSink, name: &'a str, bytes: &'a [u8], index: u64, first_unit: u64, base: [u8; 32] },
}
/// Make `step` of `t` ready: for a layer, the bases (already decoded, `bases`) taken by a
/// sink under the title's limits and every earlier layer applied, each to every unit it
/// covers, as [`replay`] applies them.
pub fn prepare<'a>(t: &'a super::title::Title, bases: &[DecodedBase], step: Step) -> Result<Prepared<'a>, MeasureError> {
let order = steps(t, bases)?;
match step {
Step::Base(b) => {
let base = t.bases.get(b).ok_or_else(|| invalid(format!("no base {b}")))?;
Ok(Prepared::Base { base, limits: &t.limits })
}
Step::Layer(i) => {
let obj = t.layers.get(i).ok_or_else(|| invalid(format!("no layer {i}")))?;
let mut sink = StageSink::new(chain_config(&t.limits));
for b in bases {
sink.add_base(b.info.clone(), b.units.clone());
}
for s in order.iter().take_while(|s| **s != step) {
if let Step::Layer(j) = s {
let o = &t.layers[*j];
apply(&mut sink, &o.path, &o.bytes, *j as u64)?;
}
}
let info = layer_info(&obj.bytes, bases, &obj.path)?;
let b = &bases[info.base];
Ok(Prepared::Layer { sink, name: &obj.path, bytes: &obj.bytes, index: i as u64, first_unit: b.info.first_unit, base: b.info.content_hash })
}
}
}
/// Offer one layer and require it applied to every unit it covers, as [`replay`] does;
/// gives the units it applied to.
fn apply(sink: &mut StageSink, name: &str, bytes: &[u8], index: u64) -> Result<Vec<u32>, MeasureError> {
let reports = sink.offer(bytes, index, ReceiptEvidence::None).map_err(|e| invalid(format!("{name}: {e}")))?;
let r = reports.first().filter(|_| reports.len() == 1).ok_or_else(|| invalid(format!("{name}: {} reports for one offer", reports.len())))?;
if let Some(why) = r.envelope {
return Err(MeasureError::Refused { at: name.to_string(), code: why.code().into(), detail: r.detail.unwrap_or("the layer is refused").into() });
}
if let Some(&(u, why)) = r.refused.first() {
return Err(MeasureError::Refused { at: name.to_string(), code: why.code().into(), detail: format!("unit {u}") });
}
if !r.held.is_empty() || r.held_for_base {
return Err(invalid(format!("{name}: does not apply to every unit it covers")));
}
Ok(r.applied.clone())
}
/// A step that ran, its outputs not yet read: what a bench times is [`Prepared::run`]
/// alone, and [`Ran::outputs`] checks and hashes them after the clock stops.
pub enum Ran<'a> {
Base { base: &'a TitleBase, decoded: crate::staged::BaseDecode },
Layer { sink: StageSink, name: &'a str, applied: Vec<u32>, first_unit: u64, base: [u8; 32] },
}
impl<'a> Prepared<'a> {
/// Run the step's own work and nothing else: `staged::decode_base_pinned` for a base,
/// the sink's offer of the layer for a layer.
pub fn run(self) -> Result<Ran<'a>, MeasureError> {
match self {
Prepared::Base { base, limits } => Ok(Ran::Base { base, decoded: decode_pinned(base, limits)? }),
Prepared::Layer { mut sink, name, bytes, index, first_unit, base } => {
let applied = apply(&mut sink, name, bytes, index)?;
Ok(Ran::Layer { sink, name, applied, first_unit, base })
}
}
}
}
impl Ran<'_> {
/// The outputs the step produced, as (unit, layers, SHA-256 of its canonical buffer),
/// the unit's global index; a base checked first against its case's stage 0 and
/// decode, as [`decode_base`] checks one.
pub fn outputs(self) -> Result<Vec<(u64, u16, [u8; 32])>, MeasureError> {
use crate::output_tree::out_digest;
match self {
Ran::Base { base, decoded } => {
let d = checked(base, decoded)?;
Ok(d.units.iter().enumerate().map(|(u, buf)| (d.info.first_unit + u as u64, 1, out_digest(&buf.samples.canonical_bytes()))).collect())
}
Ran::Layer { sink, name, applied, first_unit, base } => {
let chain = sink.chain(&base).ok_or_else(|| invalid(format!("{name}: its base is gone")))?;
applied
.iter()
.map(|&u| {
let st = chain.unit(u).ok_or_else(|| invalid(format!("{name}: unit {u} applied and not held")))?;
Ok((first_unit + u as u64, st.stage as u16 + 1, out_digest(&st.buf.samples.canonical_bytes())))
})
.collect()
}
}
}
}
/// The first output of a run that is not the reference's: a (unit, layers) the reference
/// gives another digest, or none (staged-measurement §7.3: such a run is not a timing).
pub fn differs_from(reference: &BTreeMap<(u64, u16), [u8; 32]>, out: &[(u64, u16, [u8; 32])]) -> Option<(u64, u16)> {
out.iter().find(|(u, l, d)| reference.get(&(*u, *l)) != Some(d)).map(|(u, l, _)| (*u, *l))
}

View file

@ -12,12 +12,16 @@
//! (Appendix C) — and its check against the fixture tool's own reconstruction.
//! - [`sim`]: the staged delivery simulator (§10, Appendix B), its stall bound and the
//! check of a plan against it.
//! - `session` (with `receipt_claims`): the `reconstruct-session` claim a sink would seal
//! over a title's evaluation (§8.7, jwp-receipts §2.4.12), its output tree the title's.
//!
//! Everything here is integer and reads no clock and no file outside [`title::Title::load`],
//! so it builds for wasm32 and every machine computes the same figures.
pub mod decode;
pub mod run;
#[cfg(feature = "receipt_claims")]
pub mod session;
pub mod sim;
pub mod title;

View file

@ -219,8 +219,9 @@ pub struct Evaluation {
pub objects: Vec<CountedObject>,
pub bfpf_min: Option<u64>,
pub prefixes: Vec<PrefixResult>,
/// Decoder entries in first-use order: capability and the pin it verified.
pub decoders: Vec<(String, Option<String>)>,
/// Decoder entries in first-use order: capability, the pin it verified, and the
/// content hash of the envelope it decoded (staged-measurement Appendix C).
pub decoders: Vec<(String, Option<String>, [u8; 32])>,
pub presentations: Vec<PresentationResult>,
/// The output tree's root and leaf count (Appendix C).
pub output: Option<crate::output_tree::OutputCommitment>,
@ -249,6 +250,7 @@ struct StageRecord {
layers: u16,
capability: String,
pin: Option<String>,
object: [u8; 32],
digest: [u8; 32],
shape: (u32, u32, u32),
}
@ -298,6 +300,7 @@ pub fn evaluate(t: &Title) -> Result<Evaluation, MeasureError> {
layers: e.layers,
capability: e.capability.to_string(),
pin: e.pin.map(str::to_string),
object: e.object,
digest: out_digest(&e.buf.samples.canonical_bytes()),
shape: e.buf.shape,
});
@ -309,9 +312,9 @@ pub fn evaluate(t: &Title) -> Result<Evaluation, MeasureError> {
// Decoder entries, in first use as presentations run by (layers, unit).
records.sort_by_key(|r| (r.layers, r.unit));
let mut decoders: Vec<(String, Option<String>)> = Vec::new();
let mut decoders: Vec<(String, Option<String>, [u8; 32])> = Vec::new();
for r in &records {
let key = (r.capability.clone(), r.pin.clone());
let key = (r.capability.clone(), r.pin.clone(), r.object);
if !decoders.contains(&key) {
decoders.push(key);
}
@ -350,7 +353,7 @@ pub fn evaluate(t: &Title) -> Result<Evaluation, MeasureError> {
}
let mut presentations = Vec::new();
for r in &records {
let decoder = decoders.iter().position(|d| d.0 == r.capability && d.1 == r.pin).expect("listed") as u16;
let decoder = decoders.iter().position(|d| d.0 == r.capability && d.1 == r.pin && d.2 == r.object).expect("listed") as u16;
let (vs_source, vs_final) = by_stage.remove(&(r.unit, r.layers)).ok_or_else(|| invalid("the two replays disagree"))?;
presentations.push(PresentationResult {
p: Presentation { unit: r.unit, layers: r.layers, decoder, out_digest: r.digest },
@ -448,6 +451,22 @@ pub fn evaluate(t: &Title) -> Result<Evaluation, MeasureError> {
})
}
/// One completion of a title (staged-measurement §8.1): every base decoded and every
/// layer applied under the title's limits, as [`evaluate`]'s replay applies them, and
/// nothing measured. Gives the number of (unit, stage) decodes it made — the
/// presentations a session over the title commits to. This is the work a decode time or a
/// decode figure is stated per.
pub fn completion(t: &Title) -> Result<u64, MeasureError> {
let decoded: Vec<DecodedBase> = t.bases.iter().map(|b| decode_base(b, &t.limits)).collect::<Result<_, _>>()?;
let named: Vec<(&str, &[u8])> = t.layers.iter().map(|o| (o.path.as_str(), o.bytes.as_slice())).collect();
let mut stages = 0u64;
replay(&decoded, &named, &t.limits, &mut |_| {
stages += 1;
Ok(())
})?;
Ok(stages)
}
/// A title that exists to be refused: its evaluation must fail with the code it states.
pub fn check_refusal(t: &Title) -> Result<(), MeasureError> {
let Some(want) = &t.refused else { return Err(invalid(format!("{}: states no refusal", t.dir))) };
@ -483,7 +502,7 @@ impl Evaluation {
"per_base": p.per_base.iter().map(|(s, c)| json!({"stage_counted": s, "counted": c})).collect::<Vec<_>>(),
"units": p.units, "vs_source": opt_json(p.vs_source), "vs_final": opt_json(p.vs_final),
})).collect::<Vec<_>>(),
"decoders": self.decoders.iter().map(|(c, p)| json!({"capability": c, "pin": p})).collect::<Vec<_>>(),
"decoders": self.decoders.iter().map(|(c, p, o)| json!({"capability": c, "pin": p, "object": hex(o)})).collect::<Vec<_>>(),
"presentations": self.presentations.iter().map(|r| json!({
"unit": r.p.unit, "layers": r.p.layers, "decoder": r.p.decoder, "shape": [r.shape.0, r.shape.1, r.shape.2],
"out_sha256": hex(&r.p.out_digest), "vs_source": opt_json(r.vs_source), "vs_final": opt_json(r.vs_final),

View file

@ -0,0 +1,325 @@
//! A title's evaluation as a sink's session (`extensions/staged-measurement.md` §8.7,
//! jwp-receipts §2.4.12).
//!
//! A tool that decodes a title as a sink does — every base, then every layer, all on the
//! primary path, no deadline — records the session a [`SessionRecorder`] would: one
//! decoder entry per (capability, pin, object) of the evaluation, in its first-use order; every
//! presentation with its output's digest; every object of the title received whole and
//! presented. Its output tree is the title's (staged-measurement Appendix C), so a session
//! claim over a fixture commits to the root its golden states.
use crate::claim_receipt::{ObjectSeen, SessionHost, SessionPath};
use crate::codecs::DeclaredTier;
use crate::container::Wai;
use crate::session_recorder::{DecoderKey, SessionRecorder};
use super::run::Evaluation;
use super::title::Title;
use super::MeasureError;
/// This build's decoders, as a decoder entry names them: `builtin:wai@<version>`.
pub fn builtin_decoder() -> String {
format!("builtin:wai@{}", env!("CARGO_PKG_VERSION"))
}
fn unhex32(s: &str) -> Option<[u8; 32]> {
if s.len() != 64 {
return None;
}
let mut out = [0u8; 32];
for (i, b) in out.iter_mut().enumerate() {
*b = u8::from_str_radix(s.get(2 * i..2 * i + 2)?, 16).ok()?;
}
Some(out)
}
/// The recorder of the session that decoding `t` as a sink does is: `host` the decoder
/// host, `session_nonce` fresh. Gives it with the session's entry object, the title's
/// first base: the first envelope a sink dispatches. The record carries no energy; the
/// caller adds a figure where it measured one.
pub fn recorder_of(t: &Title, e: &Evaluation, host: SessionHost, session_nonce: [u8; 32]) -> Result<(SessionRecorder, [u8; 32]), MeasureError> {
let invalid = |s: String| MeasureError::Invalid(s);
let first = e.bases.first().ok_or_else(|| invalid(format!("{}: no base", t.dir)))?;
// The entry object's manifest `media`, as written.
let base = t.bases.first().ok_or_else(|| invalid(format!("{}: no base", t.dir)))?;
let media = Wai::from_bytes(&base.obj.bytes).map_err(|x| invalid(format!("{}: {x}", base.obj.path)))?.media().to_owned();
let mut r = SessionRecorder::new(session_nonce, &media, host, None);
let mut handles = Vec::new();
for (capability, pin, object) in &e.decoders {
let pin = pin.as_deref().map(|p| unhex32(p).ok_or_else(|| invalid(format!("{}: pin {p} is not a SHA-256", t.dir)))).transpose()?;
// That pin's declared tier as SPEC §3.1 reads it, from the base envelope the entry
// decoded, which pinned it: an unregistered token, or none, reads as `none`.
let tier = pin.map(|_| {
t.bases
.iter()
.zip(&e.bases)
.filter(|(_, (d, _, _))| d.info.content_hash == *object)
.find_map(|(b, _)| Wai::from_bytes(&b.obj.bytes).ok()?.prior()?.tier().and_then(DeclaredTier::from_token))
.unwrap_or(DeclaredTier::None)
.token()
.to_owned()
});
handles.push(r.decoder(DecoderKey {
capability: capability.clone(),
rendition: None,
object: *object,
path: SessionPath::Primary,
decoder: builtin_decoder(),
variant: None,
pin,
tier,
module_status: None,
}));
}
for p in &e.presentations {
let h = *handles.get(usize::from(p.p.decoder)).ok_or_else(|| invalid(format!("{}: a presentation names no decoder", t.dir)))?;
r.presented(p.p.unit, p.p.layers, h, p.p.out_digest, None).map_err(|x| invalid(format!("{}: {x}", t.dir)))?;
}
for o in &e.objects {
r.received(ObjectSeen {
content_hash: o.content_hash,
path: SessionPath::Primary,
counted: Some(o.counted.total),
transferred: o.counted.total,
presented: true,
});
}
Ok((r, first.0.info.content_hash))
}
/// A decode figure measured under energy-measurement §3, as a session seals it: the figure
/// of one completion with its class, the bracket's values, and the power reader.
#[derive(Clone, Debug)]
pub struct MeasuredFigure {
pub joules_micro: u64,
pub class: crate::energy_class::EnergyProvenance,
pub bracket: Option<crate::energy_report::BracketValues>,
pub power_reader: String,
}
/// The host a session states where its figure was bracketed: the decode workers the
/// bracket's burst ran on (its `in_flight`), written as §2.4.12 writes them. A session
/// whose figure has no bracket keeps `host`. A bracket [`seal_session`] publishes ran on a
/// power of two of workers, so this is its `in_flight` itself.
pub fn host_for(host: SessionHost, figure: Option<&MeasuredFigure>) -> SessionHost {
match figure.and_then(|f| f.bracket) {
Some(b) => SessionHost { threads: SessionHost::rounded_threads(b.in_flight), ..host },
None => host,
}
}
/// Seal the session a sink decoding `t` would seal, with `figure` as its `energy` where
/// one was measured and published (staged-measurement §8.7), and, beside a figure, the
/// `measurement` claim on it — its bracket and build, signed with the same key and
/// chained after the session (energy-measurement §6). The session's output tree is
/// checked to be the evaluation's before anything is returned, and, where the figure was
/// bracketed, the bracket to have run on a power of two of workers and the host's
/// `threads` to be that number ([`host_for`]): the session and its paired `measurement`
/// claim state one machine, and the claim's `in_flight` names no more of it than the
/// session's rounded `threads` (jwp-receipts §6.7). A meter bursts on
/// [`crate::energy_burst::power_of_two_workers`] for that.
#[allow(clippy::too_many_arguments)]
pub fn seal_session(
t: &Title,
e: &Evaluation,
host: SessionHost,
session_nonce: [u8; 32],
figure: Option<&MeasuredFigure>,
key: &ed25519_dalek::SigningKey,
signer_id: &str,
issued_at: &str,
) -> Result<(crate::claim_receipt::ClaimReceipt, Option<crate::claim_receipt::ClaimReceipt>), String> {
use crate::claim_receipt::{ClaimBody, SessionEnergy};
use crate::energy_figure::EnergyFigures;
if let Some(b) = figure.and_then(|f| f.bracket) {
if b.in_flight != SessionHost::rounded_threads(b.in_flight) {
return Err(format!(
"the figure's bracket ran on {} workers, which its report would publish beside a session that rounds them to {}; burst on a power of two",
b.in_flight,
SessionHost::rounded_threads(b.in_flight)
));
}
if host.threads != b.in_flight {
return Err(format!("the session's host states {} decode workers, its figure's bracket ran on {}", host.threads, b.in_flight));
}
}
let (mut r, entry) = recorder_of(t, e, host, session_nonce).map_err(|x| x.to_string())?;
r.energy(figure.map(|f| SessionEnergy::new(f.joules_micro, &f.class)));
let s = r.finish(entry, key, signer_id, 0, None, issued_at).map_err(|x| format!("the session is refused ({x})"))?;
let ClaimBody::ReconstructSession(b) = &s.claim.body else { unreachable!("a recorder seals a session") };
if b.output_root != e.output.map(|c| c.root) || Some(b.leaf_count) != e.output.map(|c| c.leaf_count) {
return Err("the session's output tree is not the title's".into());
}
let report = match figure {
None => None,
Some(f) => {
let view = s.claim.figure_view().ok_or("the session does not verify")?;
let report = crate::energy_figure::meter_report::emit(
&view,
None,
f.bracket,
crate::energy_report::BuildRecord::of_this_build(),
&f.power_reader,
key,
signer_id,
Some(&s.claim),
issued_at,
)?;
check_host_agrees(&s.claim, &report)?;
Some(report)
}
};
Ok((s.claim, report))
}
/// Whether a session and the `measurement` claim on its figure state one machine, each
/// rounded as §2.4.12 writes a worker count: where the report carries a bracket, its
/// `in_flight` is the session's host `threads` — the same power of two.
pub fn check_host_agrees(session: &crate::claim_receipt::ClaimReceipt, report: &crate::claim_receipt::ClaimReceipt) -> Result<(), String> {
use crate::claim_receipt::ClaimBody;
let (ClaimBody::ReconstructSession(s), ClaimBody::Measurement(m)) = (&session.body, &report.body) else {
return Err("not a session and a measurement claim".into());
};
match m.bracket {
Some(b) if s.host.threads != b.in_flight => Err(format!(
"the session states {} decode workers and its report's bracket {}",
s.host.threads, b.in_flight
)),
_ => Ok(()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::claim_receipt::ClaimBody;
use crate::energy_report::BuildRecord;
use ed25519_dalek::SigningKey;
/// Every fixture of the corpus, decoded as a sink and sealed as a session, commits to
/// the output tree its evaluation gives — the root and leaf count its golden states —
/// and checks against what it recorded; one completion decodes as many stages as the
/// session has leaves.
#[test]
fn a_fixtures_session_commits_to_its_evaluation_tree() {
let wai = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
let corpus = wai.join("staged-measure-conformance/fixtures");
let mut names: Vec<String> = std::fs::read_dir(&corpus)
.unwrap()
.filter_map(|d| d.ok())
.filter(|d| d.path().join("title.json").is_file())
.map(|d| d.file_name().to_string_lossy().into_owned())
.collect();
names.sort();
assert!(!names.is_empty());
let key = SigningKey::from_bytes(&[0x44; 32]);
let host = SessionHost { profile: "native-ffi".into(), target: None, build: BuildRecord::of_this_build(), threads: 1 };
for name in names {
let t = Title::load(&wai, &format!("staged-measure-conformance/fixtures/{name}")).unwrap();
let e = super::super::run::evaluate(&t).unwrap();
let (r, entry) = recorder_of(&t, &e, host.clone(), [3; 32]).unwrap();
assert_eq!(entry, e.objects[0].content_hash, "{name}: the entry object is the first base");
let s = r.finish(entry, &key, "did:example:sink", 0, None, "2026-10-06T12:00:00Z").unwrap();
let ClaimBody::ReconstructSession(b) = &s.claim.body else { panic!("a session") };
let c = e.output.unwrap();
assert_eq!((b.output_root, b.leaf_count), (Some(c.root), c.leaf_count), "{name}: the session's tree is the title's");
assert_eq!(b.decoders.len(), e.decoders.len(), "{name}");
assert_eq!(b.counted_primary, e.objects.iter().map(|o| o.counted.total).sum::<u64>(), "{name}");
for (d, (cap, pin, object)) in b.decoders.iter().zip(&e.decoders) {
assert_eq!((&d.capability, &d.object), (cap, object));
assert_eq!(d.pin.is_some(), pin.is_some());
assert_eq!(d.tier.is_some(), pin.is_some());
}
s.claim.verify().unwrap();
s.claim.check_session_outputs(&s.presentations, &|_| None).unwrap();
s.claim.check_session_objects(&s.objects).unwrap();
assert_eq!(super::super::run::completion(&t).unwrap(), c.leaf_count, "{name}: one completion is every stage");
}
}
/// A metered session, as `wai_stage_measure bench --sign` seals one: the figure a live
/// from-rest bracket gives under `OnChipCounter`, in the session's `energy`, and the
/// `measurement` claim on it by the same key, chained after the session and checked
/// against its figure. An unmetered session gets no report.
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn a_metered_session_is_sealed_with_its_report() {
use crate::claim_receipt::{verify_claim_json, ClaimBody};
use crate::energy_burst::Bracket;
use crate::energy_class::{CompletionCount, CounterFiltering, WindowOpening};
let wai = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
let t = Title::load(&wai, "staged-measure-conformance/fixtures/still").unwrap();
let e = super::super::run::evaluate(&t).unwrap();
let key = SigningKey::from_bytes(&[0x44; 32]);
let host = SessionHost { profile: "native-ffi".into(), target: None, build: BuildRecord::of_this_build(), threads: 1 };
// A live, quiet idle bracket and a 10 W marginal over 3 s, 60 completions from
// rest, on `n` workers.
let rep = |a: f64, b: f64, n: usize| (0..n).map(|i| if i % 2 == 0 { a } else { b }).collect::<Vec<f64>>();
let figure_on = |n: u64| {
let b = Bracket {
idle_w: rep(19.9, 20.1, 10),
busy_w: rep(29.9, 30.1, 14),
window_s: 3.0,
idle_span_s: 2.0,
count: CompletionCount { in_flight: n, completions: 60, opening: WindowOpening::FromRest },
};
let (uj, class) = b.on_chip_figure(CounterFiltering::Unknown).expect("a figure");
MeasuredFigure { joules_micro: uj, class, bracket: b.values(), power_reader: "a counter reader".into() }
};
// A bracket on ten workers would publish ten beside a session that writes eight:
// refused, whatever the host states.
let ten = figure_on(10);
for h in [host.clone(), host_for(host.clone(), Some(&ten))] {
let refused = seal_session(&t, &e, h, [5; 32], Some(&ten), &key, "did:example:sink", "2026-10-06T12:00:00Z");
let why = refused.expect_err("a bracket on a worker count the session rounds is refused");
assert!(why.contains("burst on a power of two"), "refused before anything is sealed: {why}");
}
// On eight, a host stating one is refused, and the host the figure gives states
// eight.
let f = figure_on(8);
let uj = f.joules_micro;
let refused = seal_session(&t, &e, host.clone(), [5; 32], Some(&f), &key, "did:example:sink", "2026-10-06T12:00:00Z");
assert!(refused.is_err(), "a host that misstates the bracket's workers is refused");
let metered_host = host_for(host.clone(), Some(&f));
assert_eq!(metered_host.threads, 8);
let (session, report) = seal_session(&t, &e, metered_host.clone(), [5; 32], Some(&f), &key, "did:example:sink", "2026-10-06T12:00:00Z").unwrap();
let ClaimBody::ReconstructSession(body) = &session.body else { panic!("a session") };
assert_eq!(body.energy.as_ref().map(|x| (x.joules_micro, x.energy_provenance.as_str())), Some((uj, "OnChipCounter")));
let report = report.expect("a report beside a figure");
let report = verify_claim_json(&report.to_json()).unwrap();
assert_eq!((report.subject, report.seq, report.parent_receipt_hash), (session.link_hash(), 1, Some(session.link_hash())));
report.check_figure_against(crate::energy_figure::EnergyFigures::figure_view(&session).as_ref()).unwrap();
report.check_parent(Some(&session)).unwrap();
check_host_agrees(&session, &report).unwrap();
let ClaimBody::ReconstructSession(sb) = &session.body else { panic!("a session") };
let ClaimBody::Measurement(mb) = &report.body else { panic!("a report") };
assert_eq!(sb.host.threads, mb.bracket.unwrap().in_flight, "the report names the session's rounded workers");
assert!(crate::energy_burst::power_of_two_workers().is_power_of_two());
// A session that states eight workers beside a report that publishes ten — sealed
// by hand, as seal_session never would — does not agree: the report names more of
// the machine than the session.
let (mut r, entry) = recorder_of(&t, &e, metered_host.clone(), [6; 32]).unwrap();
r.energy(Some(crate::claim_receipt::SessionEnergy::new(ten.joules_micro, &ten.class)));
let eight = r.finish(entry, &key, "did:example:sink", 0, None, "2026-10-06T12:00:00Z").unwrap().claim;
let view = crate::energy_figure::EnergyFigures::figure_view(&eight).unwrap();
let ten_report = crate::energy_figure::meter_report::emit(
&view,
None,
ten.bracket,
crate::energy_report::BuildRecord::of_this_build(),
&ten.power_reader,
&key,
"did:example:sink",
Some(&eight),
"2026-10-06T12:00:00Z",
)
.unwrap();
assert!(check_host_agrees(&eight, &ten_report).is_err(), "eight workers beside a bracket of ten");
let (unmetered, none) = seal_session(&t, &e, host, [5; 32], None, &key, "did:example:sink", "2026-10-06T12:00:00Z").unwrap();
assert!(none.is_none());
assert!(check_host_agrees(&unmetered, &report).is_err(), "a session of one worker beside a bracket of eight");
let session = unmetered;
let ClaimBody::ReconstructSession(body) = &session.body else { panic!("a session") };
assert_eq!(body.energy, None, "an unmetered session writes null, never 0");
}
}

View file

@ -467,3 +467,319 @@ fn a_claim_never_spans_two_epochs() {
assert_eq!(claim.units_by_stage, vec![2]);
seal(h, claim).expect("it seals");
}
/// The layer a layer envelope refines its base at, if it reads as one.
fn refines_layer(env: &[u8]) -> Option<(String, u8)> {
let w = Wai::from_bytes(env).ok()?;
Refines::from_value(w.manifest.refines.as_ref()?, RefForm::ContentHash).ok().map(|r| (r.base, r.layer))
}
/// What an honest sink records of one corpus case: the session, sealed by its recorder,
/// and how many `stage` claims it names and whether a base was forgotten.
struct Replayed {
sealed: Result<crate::session_recorder::RecordedSession, crate::claim_receipt::Rejection>,
claims: usize,
forgot: bool,
}
/// Replay a corpus case as a sink does and record the session it presents: every (unit,
/// layers) it presented, through the envelope it decoded it from; every object it
/// received; and the `stage` claim of every epoch of every base it decoded — signed as
/// the epoch ended, before the sink forgot the base, over the chain it held then.
fn replay_session(dir: &std::path::Path) -> Option<Replayed> {
use crate::claim_receipt::{ObjectSeen, SessionHost, SessionPath};
use crate::energy_report::{BuildProfile, BuildRecord};
use crate::session_recorder::{DecoderKey, SessionRecorder};
let key = ed25519_dalek::SigningKey::from_bytes(&[0x44; 32]);
let case: Value = serde_json::from_slice(&std::fs::read(dir.join("case.json")).expect("case.json")).expect("JSON");
let mut bases = Vec::new();
for b in case["bases"].as_array().expect("bases") {
let env = std::fs::read(dir.join(b["file"].as_str().expect("file"))).expect("base");
let stage0 = std::fs::read(dir.join(b["stage0"].as_str().expect("stage0"))).expect("stage0");
let info = BaseInfo {
content_hash: chash(&env),
capability: b["decoded_via"].as_str().expect("decoded_via").into(),
via_fallback: b["via_fallback"] == true,
tier: tier::from_token(b["tier"].as_str().expect("tier")),
attested: b["attested"] == true,
first_unit: b["first_unit"].as_u64().expect("first_unit"),
};
bases.push((info, units_from_stage0(b, &stage0), env));
}
let files: BTreeMap<String, Vec<u8>> = case["files"]
.as_array()
.expect("files")
.iter()
.map(|f| {
let n = f["file"].as_str().expect("file").to_owned();
let b = std::fs::read(dir.join(&n)).unwrap_or_default();
(n, b)
})
.collect();
let mut sink = StageSink::new(cfg_from(&case["config"]));
let epochs = |sink: &StageSink| bases.iter().map(|(i, _, _)| sink.base_epoch(&i.content_hash)).collect::<Vec<u64>>();
let mut attested_now = vec![false; bases.len()];
let mut attested_at_first: Vec<Option<bool>> = vec![None; bases.len()];
let mut seen_epoch = vec![0u64; bases.len()];
let mut events = Vec::new();
// (unit, layers) presented, with the envelope it was decoded from: the first time.
let mut presented: BTreeMap<(u64, u16), [u8; 32]> = BTreeMap::new();
// Layer objects received: their bytes, and whether any unit was presented through them.
let mut arrived: BTreeMap<[u8; 32], (u64, bool)> = BTreeMap::new();
let mut added: BTreeSet<usize> = BTreeSet::new();
// Each epoch that ended: (base, its epoch, its chain then, the events before, delivered).
let mut ended: Vec<(usize, u64, super::StageChain, usize, bool)> = Vec::new();
let mut forgot = false;
for step in case["steps"].as_array().expect("steps") {
let before: Vec<(u64, Option<super::StageChain>, bool)> = bases
.iter()
.enumerate()
.map(|(i, (info, _, _))| {
(sink.base_epoch(&info.content_hash), sink.chain(&info.content_hash).cloned(), attested_at_first[i].unwrap_or(attested_now[i]))
})
.collect();
let upto = events.len();
let reports = match step["op"].as_str().expect("op") {
"offer" => {
let env = &files[step["file"].as_str().expect("file")];
let ev = ReceiptEvidence::from_token(step["evidence"].as_str().expect("evidence")).expect("a token");
if let Some(b) = refines_base(env) {
for (i, (info, _, _)) in bases.iter().enumerate() {
if hex(&info.content_hash) == b && attested_at_first[i].is_none() {
attested_at_first[i] = Some(attested_now[i]);
}
}
}
events.push(Event::Arrived(env.clone(), ev, epochs(&sink)));
arrived.entry(chash(env)).or_insert((env.len() as u64, false));
sink.offer(env, step["at_ms"].as_u64().expect("at_ms"), ev).unwrap_or_default()
}
"base" => {
let i = step["index"].as_u64().expect("index") as usize;
let (info, units, _) = bases[i].clone();
attested_now[i] |= info.attested;
added.insert(i);
for u in 0..units.len() as u64 {
presented.entry((info.first_unit + u, 1)).or_insert(info.content_hash);
}
sink.add_base(info, units)
}
"expire" => sink.expire(step["at_ms"].as_u64().expect("at_ms")),
"remove" => {
let i = step["index"].as_u64().expect("index") as usize;
sink.remove_base(&bases[i].0.content_hash).unwrap_or_default()
}
"attest" => {
let i = step["index"].as_u64().expect("index") as usize;
attested_now[i] = true;
sink.mark_attested(&bases[i].0.content_hash);
vec![]
}
op => panic!("op {op}"),
};
let now = epochs(&sink);
for r in &reports {
if r.applied.is_empty() {
continue;
}
let env = files.values().find(|f| chash(f) == r.content_hash).expect("a layer of the case");
let (b, layer) = refines_layer(env).expect("a layer");
let i = bases.iter().position(|(info, _, _)| hex(&info.content_hash) == b).expect("its base");
for &u in &r.applied {
presented.entry((bases[i].0.first_unit + u64::from(u), u16::from(layer) + 1)).or_insert(r.content_hash);
}
if let Some(a) = arrived.get_mut(&r.content_hash) {
a.1 = true;
}
}
for (i, e) in now.iter().enumerate() {
if *e != before[i].0
&& let Some(chain) = before[i].1.clone()
{
forgot = true;
ended.push((i, before[i].0, chain, upto, before[i].2));
}
if *e != seen_epoch[i] {
attested_now[i] = false;
attested_at_first[i] = None;
seen_epoch[i] = *e;
}
}
events.push(Event::Reports(reports, now));
}
if added.is_empty() {
return None;
}
for (i, (info, _, _)) in bases.iter().enumerate() {
if let Some(chain) = sink.chain(&info.content_hash) {
ended.push((i, sink.base_epoch(&info.content_hash), chain.clone(), events.len(), attested_at_first[i].unwrap_or(attested_now[i])));
}
}
let host = SessionHost { profile: "native-ffi".into(), target: None, build: BuildRecord { profile: BuildProfile::Release, toolchain: None, target: None }, threads: 1 };
let mut rec = SessionRecorder::new([7; 32], "video", host, None);
let mut handles = BTreeMap::new();
for (&(unit, layers), source) in &presented {
let capability = match bases.iter().find(|(info, _, _)| info.content_hash == *source) {
Some((info, _, _)) => info.capability.clone(),
None => files.values().find(|f| chash(f) == *source).and_then(|f| Wai::from_bytes(f).ok()).expect("a layer").capability().to_owned(),
};
let h = *handles.entry((capability.clone(), *source)).or_insert_with(|| {
rec.decoder(DecoderKey {
capability,
rendition: None,
object: *source,
path: SessionPath::Primary,
decoder: "builtin:[email protected]".into(),
variant: None,
pin: None,
tier: None,
module_status: None,
})
});
rec.presented(unit, layers, h, crate::pin::sha256(format!("{unit}/{layers}").as_bytes()), None).expect("one presentation each");
}
for &i in &added {
let env = &bases[i].2;
rec.received(ObjectSeen { content_hash: chash(env), path: SessionPath::Primary, counted: None, transferred: env.len() as u64, presented: true });
}
for (h, (bytes, applied)) in &arrived {
rec.received(ObjectSeen { content_hash: *h, path: SessionPath::Primary, counted: None, transferred: *bytes, presented: *applied });
}
let mut claims = 0;
for (n, (i, epoch, chain, upto, delivered)) in ended.iter().enumerate() {
let (info, _, env) = &bases[*i];
let mut obs = StageObserver::new(info.content_hash, env.len() as u64, *delivered);
for e in &events[..*upto] {
match e {
Event::Arrived(env, ev, at) => {
let _ = obs.arrived(at[*i], env, *ev);
}
Event::Reports(r, at) => obs.reports(at[*i], r),
}
}
if *epoch > 0 {
obs.set_base_delivered(*delivered);
}
let claim = obs.claim(chain, *epoch);
let sc = ClaimReceipt::seal(&key, "did:example:sink", SubjectType::Object, info.content_hash, n as u64, "2026-10-06T00:00:00Z", ClaimBody::Stage(claim), None)
.expect("the observer states a claim that seals");
rec.stage_set(info.first_unit, &sc).expect("a stage claim of the sink's key");
claims += 1;
}
let entry = chash(&bases[*added.iter().next().expect("a base")].2);
Some(Replayed { sealed: rec.finish(entry, &key, "did:example:sink", ended.len() as u64, None, "2026-10-06T00:00:00Z"), claims, forgot })
}
/// jwp-receipts §2.4.12 over the whole corpus: every case replayed as a sink does, and the
/// session an honest sink records — naming the `stage` claim of every epoch of every base
/// it decoded — seals, its recorder having checked it against those claims, the
/// presentations and the objects (§3.1 step 6). A case whose sink forgot a base, and one
/// whose bases cover the same units, are among them.
#[test]
fn every_corpus_case_seals_an_honest_session() {
let mut cases: Vec<PathBuf> =
std::fs::read_dir(corpus().join("cases")).expect("the corpus's cases").flatten().map(|e| e.path()).collect();
cases.sort();
let (mut sealed, mut forgetting, mut claims, mut problems) = (0usize, 0usize, 0usize, Vec::new());
for dir in &cases {
let name = dir.file_name().expect("a name").to_string_lossy().into_owned();
let Some(r) = replay_session(dir) else { continue };
claims += r.claims;
match r.sealed {
Ok(_) => sealed += 1,
Err(e) => problems.push(format!("{name}: {} stage claim(s), forgot={}: refused at step {} ({})", r.claims, r.forgot, e.step, e.reason.label())),
}
if r.forgot {
forgetting += 1;
}
}
assert!(problems.is_empty(), "{} honest session(s) refused:\n{}", problems.len(), problems.join("\n"));
assert!(sealed >= cases.len() / 2 && claims >= sealed, "{sealed} sessions, {claims} stage claims over {} cases", cases.len());
assert!(forgetting > 0, "a case forgets a base");
}
/// The forget vectors of `claim-conformance/` (`session_forget_*`) are what a sink states:
/// `record_budget`'s first base refined at stage 1 by its first layer, forgotten under the
/// record budget and decoded again with no layer sent again. Its observer's `stage` claim
/// of each epoch is the corpus's, body for body; a session naming both seals, and one
/// naming only the last is refused, the units presented through the first epoch's layer
/// being bounded by nothing else.
#[test]
fn the_forget_vectors_are_what_the_sink_states() {
use crate::claim_receipt::{ObjectSeen, SessionHost, SessionPath};
use crate::energy_report::{BuildProfile, BuildRecord};
use crate::session_recorder::{DecoderKey, SessionRecorder};
let key = ed25519_dalek::SigningKey::from_bytes(&[0x44; 32]);
let (b0, u0, e0) = corpus_base("record_budget", "base_0.wai", false);
let (mut b1, u1, _) = corpus_base("record_budget", "base_1.wai", false);
b1.first_unit = 100;
let (l0, l1) = (layer_file("record_budget", "L1a_0.wai"), layer_file("record_budget", "L1a_1.wai"));
let h = b0.content_hash;
let mut sink = StageSink::new(ChainConfig { record_budget: 112, ..cfg() });
let mut obs = StageObserver::new(h, e0.len() as u64, false);
let r = sink.add_base(b0.clone(), u0.clone());
obs.reports(sink.base_epoch(&h), &r);
obs.arrived(sink.base_epoch(&h), &l0, ReceiptEvidence::None).unwrap();
let r = sink.offer(&l0, 3, ReceiptEvidence::None).unwrap();
let applied: Vec<u32> = r[0].applied.clone();
assert_eq!(applied, vec![0, 1]);
obs.reports(sink.base_epoch(&h), &r);
let epoch0 = obs.claim(sink.chain(&h).unwrap(), sink.base_epoch(&h));
let r = sink.remove_base(&h).unwrap_or_default();
obs.reports(sink.base_epoch(&h), &r);
sink.add_base(b1.clone(), u1);
sink.offer(&l1, 7, ReceiptEvidence::None).unwrap();
sink.remove_base(&b1.content_hash);
let r = sink.add_base(b0.clone(), u0.clone());
assert_eq!(sink.base_epoch(&h), 1, "the base was forgotten");
obs.reports(sink.base_epoch(&h), &r);
let epoch1 = obs.claim(sink.chain(&h).unwrap(), sink.base_epoch(&h));
// The corpus states these bodies.
let doc: Value = serde_json::from_str(&crate::claim_receipt::conformance_vectors()).unwrap();
let v = doc["vectors"].as_array().unwrap().iter().find(|v| v["name"] == "session_forget_accept").expect("the forget vector");
let corpus_bodies: Vec<Value> = v["context"]["stage_claims"]
.as_array()
.unwrap()
.iter()
.map(|w| serde_json::from_str::<Value>(w.as_str().unwrap()).unwrap()["body"].clone())
.collect();
let body = |c: &StageClaim| {
let r = ClaimReceipt::seal(&key, "did:example:sink", SubjectType::Object, h, 0, "2026-10-06T00:00:00Z", ClaimBody::Stage(c.clone()), None).unwrap();
serde_json::from_str::<Value>(&r.to_json()).unwrap()["body"].clone()
};
assert_eq!(corpus_bodies, vec![body(&epoch0), body(&epoch1)]);
// The session, naming the epochs' claims.
let seal = |n: u64, c: &StageClaim| ClaimReceipt::seal(&key, "did:example:sink", SubjectType::Object, h, n, "2026-10-06T00:00:00Z", ClaimBody::Stage(c.clone()), None).unwrap();
let (s0, s1) = (seal(0, &epoch0), seal(1, &epoch1));
let session = |named: &[&ClaimReceipt]| {
let host = SessionHost { profile: "native-ffi".into(), target: None, build: BuildRecord { profile: BuildProfile::Release, toolchain: None, target: None }, threads: 1 };
let mut rec = SessionRecorder::new([7; 32], "video", host, None);
let key_of = |capability: &str, object: [u8; 32]| DecoderKey {
capability: capability.into(),
rendition: None,
object,
path: SessionPath::Primary,
decoder: "builtin:[email protected]".into(),
variant: None,
pin: None,
tier: None,
module_status: None,
};
let (bd, ld) = (rec.decoder(key_of(&b0.capability, h)), rec.decoder(key_of("wai.video.int_refine", chash(&l0))));
for u in 0..u0.len() as u64 {
rec.presented(u, 1, bd, [1; 32], None).unwrap();
}
for &u in &applied {
rec.presented(u64::from(u), 2, ld, [2; 32], None).unwrap();
}
for e in [&e0, &l0] {
rec.received(ObjectSeen { content_hash: chash(e), path: SessionPath::Primary, counted: None, transferred: e.len() as u64, presented: true });
}
for c in named {
rec.stage_set(0, c).unwrap();
}
rec.finish(h, &key, "did:example:sink", 2, None, "2026-10-06T00:00:00Z").map(|_| ()).map_err(|e| (e.step, e.reason.label()))
};
assert_eq!(session(&[&s0, &s1]), Ok(()), "every epoch named");
assert_eq!(session(&[&s1]), Err((6, "session_stage_disagrees")), "only the last epoch named");
}

View file

@ -23,12 +23,8 @@ pub const EVENTS: &[&str] = &[
/// The object types a request may name (CTA-5004-B `ot`).
pub const OBJECT_TYPES: &[&str] = &["m", "a", "v", "av", "i", "c", "tt", "k", "o"];
/// The switch reasons of §5.
pub const SWITCH_REASONS: &[&str] = &[
"pin-malformed", "form-not-registered", "form-unreadable", "form-required", "prior-revoked", "prior-not-authorised",
"prior-not-held", "digest-superseded", "set-malformed", "part-mismatch", "set-incomplete", "missing-capability", "module-unverified", "module-revoked", "trap",
"fuel-exhausted", "memory-exhausted", "deadline-threshold", "unit-hash-mismatch", "budget-exhausted", "recovered",
];
/// The switch reasons of §5: one list, kept where a claim verifier reads it too.
pub use crate::delivery_terms::SWITCH_REASONS;
/// The dispatch outcomes a `capable` observation reports (§2.3).
pub const CAPABLE_OUTCOMES: &[&str] = &["not-reached", "missing-capability", "pin-refused", "module-refused", "below-rate", "capable"];

View file

@ -242,3 +242,44 @@ fn the_matrix_rows_not_countable_are_not() {
}
}
}
/// Each decode step of every fixture (staged-measurement §7.1's case: a base decoded or
/// one layer applied), made ready and run on its own, in any order, gives exactly the
/// outputs the evaluation gives for its (unit, layers) pairs, and every presentation
/// comes from one step: a bench that times steps one by one and compares each run's
/// digests (§7.3) times the decodes the evaluation commits to.
#[test]
fn every_step_gives_the_evaluations_outputs() {
use wai::stage_measure::decode::{decode_base, prepare, steps};
let dir = wai_root().join("staged-measure-conformance/fixtures");
let mut names: Vec<String> = std::fs::read_dir(&dir).unwrap().flatten().filter(|e| e.path().join("title.json").is_file()).map(|e| e.file_name().to_string_lossy().into_owned()).collect();
names.sort();
assert!(!names.is_empty());
for name in names {
let t = Title::load(&wai_root(), &format!("staged-measure-conformance/fixtures/{name}")).unwrap();
let e = evaluate(&t).unwrap();
let bases: Vec<_> = t.bases.iter().map(|b| decode_base(b, &t.limits).unwrap()).collect();
let order = steps(&t, &bases).unwrap();
let want: BTreeMap<(u64, u16), [u8; 32]> = e.presentations.iter().map(|p| ((p.p.unit, p.p.layers), p.p.out_digest)).collect();
let mut got = BTreeMap::new();
// Backwards: each step is prepared on its own, whatever ran before it.
for s in order.iter().rev() {
for (u, l, d) in prepare(&t, &bases, *s).unwrap().run().unwrap().outputs().unwrap() {
assert!(got.insert((u, l), d).is_none(), "{name}: ({u}, {l}) from two steps");
}
}
assert_eq!(got, want, "{name}");
}
}
/// A run whose outputs are not the reference's is found, by the first (unit, layers) that
/// differs or that the reference does not give: the bench aborts on it (§7.3).
#[test]
fn a_run_that_differs_is_found() {
use wai::stage_measure::decode::differs_from;
let reference: BTreeMap<(u64, u16), [u8; 32]> = [((0, 1), [1; 32]), ((1, 1), [2; 32]), ((0, 2), [3; 32])].into_iter().collect();
assert_eq!(differs_from(&reference, &[(0, 1, [1; 32]), (1, 1, [2; 32])]), None);
assert_eq!(differs_from(&reference, &[(0, 1, [1; 32]), (1, 1, [9; 32])]), Some((1, 1)));
assert_eq!(differs_from(&reference, &[(0, 3, [3; 32])]), Some((0, 3)), "a stage the reference never gives");
assert_eq!(differs_from(&reference, &[]), None);
}