wai: the signed session claim — bound to its stage claims, its objects and its decoders #173
39 changed files with 18412 additions and 117 deletions
105
wai/CHANGELOG.md
105
wai/CHANGELOG.md
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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` |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
1363
wai/wai-rs/src/claim_receipt/corpus/session.rs
Normal file
1363
wai/wai-rs/src/claim_receipt/corpus/session.rs
Normal file
File diff suppressed because it is too large
Load diff
1051
wai/wai-rs/src/claim_receipt/session.rs
Normal file
1051
wai/wai-rs/src/claim_receipt/session.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
29
wai/wai-rs/src/delivery_terms.rs
Normal file
29
wai/wai-rs/src/delivery_terms.rs
Normal 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",
|
||||
];
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")?;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
543
wai/wai-rs/src/session_recorder.rs
Normal file
543
wai/wai-rs/src/session_recorder.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
325
wai/wai-rs/src/stage_measure/session.rs
Normal file
325
wai/wai-rs/src/stage_measure/session.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue