wai: staged measurement and the session claim in the browser; a session bound to the envelopes it names (W5-4) #187

Merged
dcharlot merged 25 commits from wai/w5-4-measure-web into main 2026-10-08 09:58:09 -04:00
72 changed files with 13421 additions and 1060 deletions

View file

@ -263,6 +263,9 @@ jobs:
# The staged-measurement core is wasm32-clean: a browser computes the same figures.
# Built, not only checked, so codegen and linking of the library run for wasm32.
cargo build --lib --target wasm32-unknown-unknown --no-default-features --features stage_measure
# With the claim classes too: a browser sink seals its reconstruct-session claim
# (jwp-receipts §2.4.12), and wai-web's measurement binding signs with them.
cargo build --lib --target wasm32-unknown-unknown --no-default-features --features receipt_claims,stage_measure
# So is delivery telemetry: a browser sink writes the same reports.
cargo build --lib --target wasm32-unknown-unknown --no-default-features --features telemetry
# carriage: every registered capability round-trips byte-exact. cli_wrap_intent and

View file

@ -9,7 +9,8 @@ name: WAI HTTP delivery
# union, layers' place in it and their dispatch, the wai prefix's case, duplicate
# capabilities, a record beside a fallback, the trailing-byte rule, the missing-digest
# plan, the class merge, the base-only threshold, refused refinement responses, stale
# bytes after a mismatch, an unanswered base, the selection digest test; and on the
# bytes after a mismatch, an unanswered base, the selection digest test, the component
# conformance that withholds an envelope's refinements; and on the
# playlist side adjacency, rendering, bandwidths, durations, the classical master, the
# playlist receipt and its chain, listed cids, and the adaptation policies); it
# is applied to a fresh worktree, the build must succeed, and `wai_http_vectors verify`
@ -58,6 +59,8 @@ on:
# http_verify.py reads staged_verify.py (and through it int_payload_verify.py), and
# the corpus carries the staged ladder's base and layers.
- 'wai/staged-conformance/staged_verify.py'
# staged_verify.py's component rules (SPEC §7.1) read the capability registry.
- 'wai/registry/capabilities.json'
- 'wai/staged-conformance/cases/audio_flac_rate_ladder/**'
- 'wai/int-payload-conformance/int_payload_verify.py'
- 'wai/registry/**'
@ -86,6 +89,8 @@ on:
# http_verify.py reads staged_verify.py (and through it int_payload_verify.py), and
# the corpus carries the staged ladder's base and layers.
- 'wai/staged-conformance/staged_verify.py'
# staged_verify.py's component rules (SPEC §7.1) read the capability registry.
- 'wai/registry/capabilities.json'
- 'wai/staged-conformance/cases/audio_flac_rate_ladder/**'
- 'wai/int-payload-conformance/int_payload_verify.py'
- 'wai/registry/**'

View file

@ -16,6 +16,11 @@ name: wai-web wasm32
# - demo/verify_staged_wasm.mjs against the staged-delivery corpus
# wai/staged-conformance/: every case replayed through the wasm32 StageSink
# must give the reports, final units and stage hashes the native sink wrote;
# - demo/verify_measure.sh against the staged-measurement corpus
# wai/staged-measure-conformance/: every figure of the wasm32 measurement
# binding equals the corpus and the native tool's byte for byte, and the
# session claims it seals are verified natively and by the independent
# Python verifier with the same verdicts;
# - a byte comparison of the demo's staged int_synth copies with the corpus;
# - the crate's wasm_tests under wasm-bindgen-test-runner, and its native unit
# tests on the host.
@ -52,6 +57,9 @@ on:
- 'wai/wai-quantum/**'
- 'wai/neural-conformance/**'
- 'wai/staged-conformance/**'
- 'wai/staged-measure-conformance/**'
- 'wai/int-payload-conformance/**'
- 'wai/claim-conformance/**'
- 'wai/registry/**'
- '.github/workflows/wai-web-wasm.yml'
pull_request:
@ -61,6 +69,9 @@ on:
- 'wai/wai-quantum/**'
- 'wai/neural-conformance/**'
- 'wai/staged-conformance/**'
- 'wai/staged-measure-conformance/**'
- 'wai/int-payload-conformance/**'
- 'wai/claim-conformance/**'
- 'wai/registry/**'
- '.github/workflows/wai-web-wasm.yml'
@ -118,7 +129,9 @@ jobs:
# writers `pack` and `packMulti` refuse a payload that is not the format SPEC §5
# pins its integer capability to, and a manifest that names a member twice (tests); the
# staged-delivery binding's pure half replays the staged corpus and reads a layer's
# header and cost (stage::tests). The count check turns a test that
# header and cost (stage::tests); the measurement binding's pure half evaluates the
# staged-measurement corpus to its goldens and seals a page-recorded session equal
# to the native tool's (measure::tests). The count check turns a test that
# stopped being compiled into a failure instead of a silent "0 passed".
- name: wai-web native unit tests
shell: bash
@ -126,7 +139,7 @@ jobs:
run: |
set -euo pipefail
cargo test --locked --lib 2>&1 | tee "$RUNNER_TEMP/wai-web-native.log"
grep -q "test result: ok. 14 passed; 0 failed" "$RUNNER_TEMP/wai-web-native.log"
grep -q "test result: ok. 24 passed; 0 failed" "$RUNNER_TEMP/wai-web-native.log"
- name: wasm32 int_synth decode equals the frozen native corpus
shell: bash
working-directory: wai/wai-web
@ -148,6 +161,23 @@ jobs:
shell: bash
working-directory: wai/wai-web
run: node demo/verify_staged_wasm.mjs ./pkg-node/wai_web.js ../staged-conformance
# The staged-measurement binding (src/measure.rs) over wai/staged-measure-conformance/:
# every fixture's evaluation, the refusal titles, the counted objects, the quality
# pairs and every simulator case equal the corpus, and the evaluations equal the native
# `wai_stage_measure eval --json` byte for byte; every session the wasm32 build seals
# is the native tool's claim, commits to the golden output tree, and gets the same
# verdicts from the wasm32 verifier, `wai_claim_vectors verdicts` and claim_verify.py.
# It uses the package built above; a missing artifact fails the step.
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install the Ed25519 library (claim_verify.py)
shell: bash
run: python -m pip install cryptography
- name: wasm32 staged measurement equals the native tool and the corpus
shell: bash
working-directory: wai/wai-web
run: bash demo/verify_measure.sh ./demo/pkg-node/wai_web.js
- name: The browser menu is registered and names no candidate
shell: bash
working-directory: wai/wai-web

View file

@ -6,6 +6,154 @@ wire format may change and such changes are recorded here.
## v0.4.0 — current
### Staged measurement and the session claim in the browser
- **The measurement core, bound for JS** ([staged-measurement](extensions/staged-measurement.md)).
`wai-web` binds `wai::stage_measure` (`src/measure.rs`), the code `wai_stage_measure`
runs, compiled to wasm32: `evaluateTitle` gives the text `eval --json` prints for a
title (counted bytes, quality integers, first presentable bytes, the layering
overhead's points, deduplication, the output tree), `simulate` and `simulateModel` the
text `sim` prints, `countObject` one envelope's counted bytes or the code it is not
countable with, `qualityRgb8` one picture pair's integers, and `outputRoot` the output
tree's root. A title is read from a map of files; `titleMissing` names the next file it
needs, so a page fetches them one at a time. Sums of squares are decimal strings, as the
corpus writes them.
- **The session claim, sealed in the browser** ([jwp-receipts](extensions/jwp-receipts.md)
§2.4.12). The claim classes (`receipt_claims`) build for wasm32 with the measurement
core, and CI builds them so. `sessionBegin` records a session at its entry object
(`wai::session_recorder::SessionRecorder`): the decoder that decoded each envelope, each
presentation with its output's digest, each object received. `finish` seals the claim
with a key whose seed the page supplies, checked against the record first; a browser
session's `energy` is `null`. `verifyClaim` checks a claim against the presentations and
the objects as the claim-conformance runner does.
- **Proved byte for byte.** `demo/verify_measure.sh`, run in the wasm job, requires every
golden, refusal title, counted object, quality pair and simulator result of
`staged-measure-conformance/` from the wasm32 build, and the native `eval --json` of every
fixture to equal the wasm32 evaluation. A session recorded the way a page records one is
the native tool's claim byte for byte and commits to the golden output tree; the wasm32,
native and Python verifiers give the same verdict on it and on its altered variants.
- **A recorder that holds the envelopes checks the claim against them.** A sink hands
`SessionRecorder` each object's bytes (`received_envelope`): the recorder derives its
content hash, counted and transferred bytes, and whether an entry names it, and
`seal_checked` checks the claim against every envelope it holds, as a verifier holding
the `envelopes` context does, so an entry naming a capability its envelope does not give
never leaves the sink (`session_source_mismatch`). An object the sink does not hold is
`received_unheld`, by its content hash; the recorder no longer takes an object's figures
as told. The native tool's session over a title and the browser's `SessionHandle` both
hold their envelopes; `decoderFor` refuses a capability that is neither the envelope's
nor its fallback and requires the decoder's `first_unit`, `received` takes the bytes
alone — bytes that are not an envelope included, recorded as an inert object neither
counted nor presented, so the session accounts for every byte transferred —
`verifyClaim` takes the envelopes, and integers from JS are refused unless whole and in
range.
- **An object is listed once, as its envelope gives it** (§2.4.12, the objects). No
(content hash, path) is listed twice (`session_object_duplicate`): a sink that fetches
an object again lists it once, its transferred bytes every byte of every fetch, as the
recorder merges repeats. After the sums, an object is `presented` exactly when a decoder
entry names it (`session_presented_mismatch`). With the envelopes, an object listed with
a held envelope's content hash states that envelope's counted bytes, `null` exactly when
it is not countable (`session_counted_mismatch`).
- **Envelopes no entry names are received, not refused.** Inert bytes — of neither
magic, or whose `security` member is not a security object — are refused as unreadable
only where a decoder entry names them (`session_envelope_unreadable`); those, and an
encrypted envelope, no entry names are a received object, uncounted and not presented,
and one listed with counted bytes, even 0, is refused (`session_counted_mismatch`). A
page that receives such bytes lists them and seals its session.
- **The layers and units an envelope gives.** A decoder entry states `first_unit`, the
unit at which its object's first unit is presented. With the outputs and the
envelopes, each presentation through an entry whose envelope is held is at the layer
count that envelope gives — 1 for a base or a rendition with no `refines`,
`refines.layer + 1` for a layer (`session_layers_mismatch`) — and of a unit it covers:
from `first_unit` plus the span's start, the span's count of units, or the base's units
where a layer has no span or the object is a base (`session_unit_outside_object`). A
re-decode decodes unit `unit − first_unit` of the entry's object. A base's units are read
in its payload's own format — of its manifest capability's and its fallback's pinned
formats, the one whose magic the payload starts with — whichever capability the entry
names. `claim_verify.py` reads
the units from the integer payloads as `wai-rs` does, and `differential.py` adds random
sessions holding a base's and its layers' envelopes, as `WAI1` envelopes and as `WAI2`
components.
- **A layer is one a sink can apply, at its base's first unit** (§2.4.12). An entry naming
a held layer a sink refuses is refused (`session_layer_not_applicable`): not a
refinement or with a fallback, its `refines` malformed or absent, a `WAI2` refinement
component that does not conform (SPEC §7.1), its base a layer, not admitted by the
layer's capability or decoded only through its fallback, its span or runs outside a
held base's units, or its payload not `WIR1` with a unit for each it covers
(staged-delivery §4.1 steps 1–4 and 6). A `WAI2` component and its exploded envelope
are one base in either form (staged-delivery §9). A held layer's entry states the
`first_unit` of an entry it is applied over — naming its base through its own
capability — and an entry naming a `stage` claim's base or a layer it lists states the
unit `stage_sets` names that claim at (`session_first_unit_mismatch`).
- **An entry names what a sink dispatches** (§2.4.12 rule 4): never a `WAI2` companion,
a rendition or refinement whose payload does not match its `sha256`, a capability that
is not dispatchable, or the fallback of a `WAI1` whose capability is companion-only
(`session_source_mismatch`).
- **One verdict for either name, by capability, and for a key holder** (§2.4.12). An entry
naming a `WAI2` rendition by its exploded envelope's content hash, with only the `WAI2`
held, is read as naming the rendition by rules 4–8; the exploded envelope's own bytes,
held, are a separate delivery. A manifest or rendition is a layer only when its
capability is refinement-only: a base carrying a stray `refines` is a base, as
dispatch and selection already treat it (SPEC §4, staged-delivery §3), and `wai stage
apply` now routes by capability. An encrypted envelope an entry names is accepted —
a sink holding its key decrypts and dispatches it (SPEC §6.4 step 3) — and what the
entry states of it is the signer's statement.
- **A WAI2 offers its refinements only when every component conforms** (SPEC §7.1,
*Selection*). The text now states the reference sink's rule: an envelope one of whose
components breaks a rule of §7.1 or §5 (`check_component_roles`, now on `ManifestV2`)
offers none of its refinements, and may still present the rendition it picked. `wai
stage explode` follows it, refusing to explode a component of such an envelope that is
of a refinement-only capability or marked a refinement; rule 5 matches it
for the whole envelope; and http-delivery §7 step 4 states it for a client planning
from a verified head: each refinement of a nonconforming envelope is dropped
(`components_nonconforming`), never requested, one stating no `sha256` dropped for that
too, on the range path and the whole-object path alike. A refinement is one whose
`refines.base`, read on its own, is the pick's id (`container::refinements_naming`), so
one whose `refines` is otherwise malformed is reported too; of a conforming envelope
that is exactly the refinements whose `refines` is well formed. The HTTP corpus adds
ONC's conforming twin, OCF, whose refinement is fetched and delivered on both paths,
and ONM and ONX, a `refines` of the pick's id alone (reported) and of no entry's (not
reported); `OR`'s refinement, which names the pick, is now reported. `falsify/` adds
`conforming-withheld` and `refines-read-whole`, each with its `http_verify.py
--mutate`.
- **An encrypted envelope's clear parts are checked** (§2.4.12). SPEC §6.4 encrypts
payloads only: rules 4–9 now read an encrypted envelope's manifest and rendition table
as any held envelope's — the rendition an entry names, its capability, the component
rules, a base's layer count, a companion-only capability, a fallback, and the classical
length its table declares — and leave to the signer only what needs a payload: whether
a payload matches its `sha256`, a base's units and a layer's `WIR1` payload. A `sha256`
that is not 64 lowercase hex digits matches no payload, so an entry naming its rendition
is refused, encrypted or not (rule 4, `session_source_mismatch`).
- **The recorder states the classical length.** `SessionRecorder` derives
`classical_declared` at the tally from the `WAI2` envelopes it holds, readable or
encrypted, exactly as rule 9 counts them (`claim_receipt::session::classical_length`,
which rule 9 calls too); `SessionRecorder::classical`, which a sink called by hand and
the browser binding never could, is removed. A page that receives a `WAI2` it does not
decode now seals its session. `verify_measure` seals two such sessions in wasm32 — over
`counting/wai2-classical.wai`, whose second rendition is PNG, and its encrypted twin,
both new in the counting set — checks the length against the rendition table, and the
native and Python verifiers give the same verdicts. A limitation of this revision: the
length comes from the `WAI2` envelopes held in full, so a sink that received a `WAI2`
only as byte ranges, holding its head but not the envelope, cannot yet state that
envelope's classical rendition (§2.4.12, `classical_declared` and rule 9). Recording
such a session is HTTP session work still to come.
- **A record or a derivation is no component in any role** (SPEC §7.1, *Component
identity*; `record-or-derivation-as-component`).
- **A record or a derivation is no companion** (SPEC §7.1, rule 1). A companion's
capability is companion-only or unregistered; a capability registered in another
kind — a payload, a record or a derivation — is refused
(`standalone-as-companion`), which the reference now reads from the registry rather
than from its list of media capabilities, which left records and derivations out.
- **Every refusal accounted for.** The claim-conformance README's table lists each
refusal a sink makes that decides whether a presentation can happen — staged-delivery
§3, §4.1, §4.3–§4.5, §9; SPEC §4, §5, §7.1 — with the §2.4.12 rule and vector that
refuse it, or the sentence of §2.4.12 that leaves it to the signer's statement.
- **The browser claim is a release build.** `verify_measure_wasm.mjs` requires the wasm32
claim's `host.build.profile` to be `release` and its target `wasm32-unknown-unknown`.
- **The build profile is the optimisation level.** `release` is a build at `opt-level`
2, 3, `s` or `z`, whatever its profile is named, and `debug` one at 0 or 1
([jwp-receipts](extensions/jwp-receipts.md) §2.4.8's `build`): a WebAssembly build
optimised for size is `release`.
### One Ed25519 rule for every signature
- **SPEC §7 *Signatures*.** Every Ed25519 signature WAI defines is verified strictly: the

View file

@ -422,8 +422,12 @@ payload is companion data, so step 5 is not taken either (§5).
A WAI **payload always corresponds to exactly one capability**. v1.0
does not support multi-codec multiplexed payloads.
A sink never takes step 3 or step 5 with a capability §5 registers as
**refinement-only**, and a `WAI1` whose `cap` is refinement-only takes neither:
Dispatch goes by `cap`: a `WAI1` whose `cap` is not refinement-only is dispatched as
above whatever else its manifest carries, a `refines` member included, which such a
manifest MUST NOT carry and a sink ignores
([staged-delivery](extensions/staged-delivery.md) §3). A sink never takes step 3 or
step 5 with a capability §5 registers as **refinement-only**, and a `WAI1` whose `cap`
is refinement-only takes neither:
its payload refines the output of a base, and
[staged-delivery](extensions/staged-delivery.md) §4 processes it. A sink that does
not implement that extension finds the capability unsupported, and the envelope is
@ -1724,7 +1728,10 @@ by which capability happens to be installed.
companion that does not match is not applied. An entry whose `id`, `sha256` or
`companion_of_id` is not a string (`null` is the same as absent) makes the
manifest malformed, as a mistyped pin member does (§3.1), and the reader rejects
the envelope.
the envelope. An entry's `capability` names a payload format: a capability §5
registers as a record or a derivation (*Kinds of registered string*) names none,
so an entry of one, in any role, does not conform
(`record-or-derivation-as-component`).
- **Exploding a component.** The exploded envelope of entry *i* is the `WAI1`
envelope (§2) whose payload is entry *i*'s rendition bytes. Its manifest is the
entry's `envelope_manifest` when it carries one, written exactly as it is in the
@ -1836,9 +1843,10 @@ video bitstream / a manifest color field and need no companion.
A component with `role = "companion"` conforms only if:
1. its capability is companion-only, or is not registered in this revision. A
registered capability that is not companion-only is standalone media — its
payload decodes on its own — and MUST be carried as a rendition, not as a
companion;
capability registered in any other kind (§5, *Kinds of registered string*) is
not a companion: a payload capability is standalone media — its payload decodes
on its own — and MUST be carried as a rendition, and a record or a derivation is
no component in any role (*Component identity*, above);
2. `companion_of` is present and names a capability the companion applies to.
An HDR dynamic-metadata descriptor, registered as a companion to a video
payload, applies to a registered capability whose payload is video — the
@ -1888,8 +1896,17 @@ component*, above), so it is the same object as the layer carried on its own.
- **Selection.** Selection never picks one (step 2: it is not selectable). A sink
picks a rendition as above, then offers to staged-delivery §4, in `renditions`
order, the exploded envelopes of the refinements whose `refines.base` is the picked
entry's `id`, up to the stage its deployer policy chooses. A refinement whose
payload does not match its `sha256` is not offered.
entry's `id`, up to the stage its deployer policy chooses. It offers them only from
an envelope every component of which conforms — each refinement below, each
companion (*Companion components*), each `id` (*Component identity*) and each
capability in the role §5 gives it (the reference `WaiMulti::check_component_roles`,
which reads the manifest alone): an envelope one component of which breaks a rule
offers none of its refinements. It MAY still present the rendition it picked. A
refinement whose payload does not match its `sha256` is not offered. A refinement's
exploded envelope a sink receives on its own, as a `WAI1` object
([staged-delivery](extensions/staged-delivery.md) §9), is a separate delivery, which
staged-delivery §4 processes as any layer, whatever the envelope it was exploded
from.
- **Conformance.** A refinement component conforms only if, in this order: its
capability is refinement-only (§5); it carries `id` and `sha256`; its `refines`
is well formed (staged-delivery §3, with ids in place of hashes); `refines.base`

View file

@ -37,10 +37,15 @@ inclusion at least: `{track_alias, group_id, object_id, content_hash}`), and, fo
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
not countable, each (content hash, path) once), `envelopes` (each `WAI2` envelope the
session received and any `WAI1` envelope the verifier holds, `{envelope}`, its bytes in
hex, inert bytes included; an object's `counted` is checked against its envelope's
counted bytes, a decoder entry naming one as its `object` is checked against it, a held
layer's against what a sink can apply and its `first_unit` against its base's entry's, a
presentation through such an entry against the layer count and the units it gives, and
the classical rendition is read from each held `WAI2` envelope, readable or encrypted, by
§2.4.12's rule 9 — envelopes held in full: a `WAI2` a sink received only as byte ranges is
not among them, and a session cannot yet state its classical rendition) 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
@ -182,12 +187,14 @@ The vectors cover:
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.
set over a pinned base — the session's entry object, a `WAI1` envelope holding a
four-frame `WIV1` clip — 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, a
one-frame clip, 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.
Each object's figures are what its envelope gives, where the envelope is held: its
counted bytes (staged-measurement §3), its length, and whether an entry names it.
The vectors cover:
- **Accepted:** the model unmetered and metered under `OnChipCounter`, each against every
@ -199,7 +206,14 @@ The vectors cover:
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
observer states them), naming both epochs' claims and only the first's; the base
fetched twice and listed once; an encrypted `WAI2` envelope received and named by no
entry, uncounted, and one named by an entry, decoded by a sink holding its key; inert
bytes no entry names; entries naming a `WAI2` rendition by its exploded envelope's
content hash; bases carrying a stray `refines`; refinements beside a conforming
companion; a session holding its base's and a
layer's envelopes, and a layer with no span through which a unit of its base is
presented, with its base held and not; 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
@ -233,9 +247,34 @@ The vectors cover:
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,
discarded bytes differ, and an object's counted bytes stated above what its held
envelope gives (`session_counted_mismatch`); an object listed twice
(`session_object_duplicate`); an object a decoder entry names marked not presented, and
one no entry names marked presented, the sums restated to agree
(`session_presented_mismatch`); a `WAI1` envelope with no manifest members and bytes
of neither magic, each named by an entry (`session_envelope_unreadable`); an
encrypted envelope named by one, checked against its manifest and rendition table in
the clear, refused for a classical length unstated, a rendition out of its table,
another capability, no rendition, a base at two layers, a companion-only capability,
a nonconforming refinement and a rendition whose `sha256` is malformed (uppercase, 63
digits, not hex) named by its rendition and by its exploded envelope's content hash,
and accepted with a well-formed `sha256` its payload does not match; a held inert
envelope listed with counted bytes 0
(`session_counted_mismatch`); entries a sink cannot have decoded through — a `WAI2`
companion, a rendition or refinement whose payload does not match its `sha256`, a
`WAI1` whose capability is companion-only, a fallback that is
(`session_source_mismatch`); held layers a sink cannot apply, each row of the table
below that names rule 5 (`session_layer_not_applicable`); a layer's entry at another
`first_unit` than its base's entry's, as a `WAI1` layer, as a `WAI2` refinement
component and across a `WAI2` component and its exploded envelope, and the entries of
a `stage` claim's base and of a layer it lists at another than the unit `stage_sets`
names the claim at (`session_first_unit_mismatch`); a unit presented at more layers
than its layer envelope, its refinement component or its base gives
(`session_layers_mismatch`); a unit past the base's frames, the same past the frames
of a base decoded through its manifest's `fallback`, in the fallback's format where
the capability's is another, outside a layer's span, a `WAI2` rendition's frame, a
refinement component's base or a `WAI2` base a `WAI1` layer refines, and through an
entry whose `first_unit` is moved (`session_unit_outside_object`); 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
@ -247,11 +286,51 @@ The vectors cover:
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;
only its last epoch's claim, which lists no layer, or presenting a unit, through a
layer it did not hold whole, 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.
### The sink's refusals, and what the session claim does with each
Every refusal a sink makes that decides whether a presentation can happen —
staged-delivery §3, §4.1 (each step), §4.3–§4.5 and §9; SPEC §4 (step 5, the fallback),
§5 and §7.1; and `WaiMulti::check_component_roles`'s refinement codes — is either refused
by a rule of jwp-receipts §2.4.12 against the envelopes, with a vector here that reaches
it, or left to the signer's statement by the sentence of §2.4.12 that names it ("So,
deliberately, is each refusal a sink makes that the rules above do not check"). A row
with neither is a hole; there is none.
| source | refusal | §2.4.12 | vectors |
|---|---|---|---|
| staged-delivery §3 | `refines` malformed: member types and ranges, `prev`'s forms, adjacent runs of one producer, an array of one run, a producer other than `base` at layer 1, runs that do not sum to the span | rule 5, `session_layer_not_applicable` | `session_refines_layer_zero`, `session_refines_span_empty`, `session_wai2_refines_id_malformed` |
| staged-delivery §3 | `refines` absent on a refinement-only capability | rule 5 | `session_refines_absent` |
| staged-delivery §3, SPEC §4 | `refines` on a manifest or rendition whose capability is not refinement-only: a sink routes by capability, so it is a base and the `refines` is ignored (in a `WAI2`, a broken component: no refinement of that envelope is offered) | rules 4 and 7 read it as a base | accepted: `session_stray_refines_base_accept`, `session_stray_refines_layer_over_it_accept`, `session_stray_refines_wai2_rendition_accept`; refused: `session_refines_on_a_base_capability` (at two layers, `session_layers_mismatch`), `session_wai2_refines_on_a_rendition` |
| staged-delivery §3, §4.1 step 2 | with the base: a span outside its units, runs that do not sum to the units covered | rule 5 | `session_refines_span_past_base_alone`, `session_wai2_refines_span_past_base_alone` (the payload's count right, so the span check alone decides), `session_refines_span_past_base`, `session_wai2_refines_span_past_base`, `session_refines_runs_short`; accepted: `session_refines_runs_whole_accept` |
| §4.1 step 1 | `capability-not-refinement`: a layer with a fallback (an envelope of any other capability is a base, the row above) | rule 5 | `session_layer_with_fallback` |
| §4.1 step 2 | `base-absent`: no base decoded with the content hash `refines.base` | where the base is held and is a layer, rule 5; otherwise the signer's statement | `session_refines_base_is_a_layer`, `session_wai2_refines_base_is_a_refinement` |
| §4.1 step 3 | `base-via-fallback` | rule 5 (applied over an entry decoding the base through its own capability); rule 6 ties the layer only to such entries | `session_layer_base_via_fallback`, `session_layer_tied_to_its_base_not_the_fallback` |
| §4.1 step 4 | `base-not-admitted` (SPEC §5, *Base admission*) | rule 5 | `session_layer_base_not_admitted`, `session_layer_base_not_admitted_unnamed`, `session_wai2_layer_base_not_admitted` |
| §4.1 step 5 | `refinement-unattested` | the signer's statement: no context carries a delivery receipt | — |
| §4.1 step 6 | `payload-malformed`: the payload does not parse, or holds another number of units than the layer covers | rule 5, where the count of units covered is read | `session_layer_payload_not_wir1`, `session_layer_payload_units_short` |
| §4.1 step 6 | `payload-malformed` for a stream that does not decode canonically; `cost-over-limit`; `chain-over-limit` | the signer's statement; a re-decode of a byte-exact presentation checks its output | — |
| §4.1 steps 7–10 | `layer-conflict`, `prev-mismatch`, `layer-gap`, `geometry-mismatch`, `deadline-missed` | the signer's statement; rules 7 and 8 bound a layer's presentations to its count and units, and the `stage` claims the stages a unit reached | `session_layers_past_the_object`, `session_unit_outside_span`, `session_stage_beyond_applied` |
| §4.1, after step 10 | a unit presented from a layer that failed for it | rules 7 and 8 | as above |
| §4.3–§4.5 | the hold bound, batches, the chain budget for a base | the signer's statement | — |
| staged-delivery §9 | a `WAI2` component and its exploded envelope are one object | rules 5, 6 and 8 read a base in either form, and rules 4–8 read an entry naming a rendition by its exploded envelope's content hash, with only the `WAI2` held, as naming the rendition; the exploded envelope's own bytes, held, are a separate delivery, and whether they were received on their own is the signer's statement | `session_alias_layer_on_component_accept`, `session_alias_layer_first_unit_not_its_base`, `session_alias_layer_unit_past_the_base`, `session_alias_exploded_held_first_unit`, `session_alias_component_layer_named_as_exploded`, `session_alias_entry_accept`, `session_alias_entry_relabelled`, `session_alias_entry_unit_past_the_base`, `session_alias_refinement_entry_first_unit`, `session_alias_refinement_entry_not_admitted`; a separate delivery: `session_roles_exploded_layer_delivered_alone_accept` |
| SPEC §4 steps 3, 5 | a `WAI1` decoded through its `capability`, else its `fallback`, and nothing else | rule 4, `session_source_mismatch` | `session_relabel_wai1_base`, `session_relabel_av1` |
| SPEC §4 | a capability that is not dispatchable never taken; a `WAI1` whose capability is companion-only inert, its fallback never taken | rule 4 | `session_wai1_fallback_not_dispatchable`, `session_wai1_companion_only_capability` |
| SPEC §4 step 5 | the fallback decodes the same payload | rule 8 reads the units in the payload's own format | `session_unit_fallback_past_the_title`, `session_unit_fallback_format_past_the_title`, `session_unit_fallback_format_audio`; accepted: `session_unit_fallback_accept`, `session_unit_fallback_format_accept` |
| SPEC §3.1, §4 | a pin that does not resolve, a module not held, a key log's revocation | the signer's statement, which a decoder entry's `pin` and `tier` state | — |
| SPEC §6.4 | an encrypted envelope is inert at a sink without its key; one holding the key decrypts and dispatches it (step 3), its manifest and rendition table in the clear | rule 3 admits an entry naming it; rules 4–9 read its manifest and table as any held envelope's, a `sha256` that is not well formed included, and what needs a payload — whether a payload matches a well-formed `sha256`, a base's units, a layer's `WIR1` payload — is the signer's statement. Rule 9 reads the classical length from `WAI2` envelopes held in full: of one a sink received only as byte ranges it holds the head and not the envelope, and this revision cannot yet state that envelope's classical rendition (a limitation; recording such a session is HTTP session work still to come) | `session_envelope_encrypted`, `session_envelope_encrypted_layer_accept`, `session_envelope_encrypted_sha256_unmatched_accept`; refused: `session_envelope_encrypted_sha256_uppercase`, `session_envelope_encrypted_sha256_63_digits`, `session_envelope_encrypted_sha256_not_hex` (each `_exploded` too), `session_envelope_encrypted_classical_unstated`, `session_envelope_encrypted_rendition_out_of_range`, `session_envelope_encrypted_relabelled`, `session_envelope_encrypted_no_rendition`, `session_envelope_encrypted_base_layers`, `session_envelope_encrypted_companion_only`, `session_envelope_encrypted_roles` |
| SPEC §5 | a refinement-only capability is never a `capability` or `fallback` a sink dispatches, a selectable rendition or a companion | rules 4 and 5 | `session_layer_with_fallback`, `session_wai2_refines_on_a_rendition` |
| SPEC §7.1, *Selection algorithm* | a companion, or a capability that is not dispatchable, is never picked | rule 4 | `session_wai2_entry_names_a_companion` |
| SPEC §7.1, *Component identity* | `component_digest_mismatch`: a rendition whose payload does not match its `sha256` is skipped, a refinement is not offered | rule 4 | `session_wai2_rendition_digest_mismatch`, `session_wai2_refinement_digest_mismatch` |
| SPEC §7.1, *Component identity* | an `id`, `sha256` or `companion_of_id` that is not a string: the reader rejects the envelope | rule 3 | `session_wai2_component_id_not_a_string` |
| SPEC §7.1, *Component identity* | a component, in any role, whose capability is a record or a derivation (`record-or-derivation-as-component`): the envelope does not conform | rule 5, the whole envelope | `session_roles_record_rendition`, `session_roles_record_companion`, `session_roles_derivation_companion`; `pin-conformance/selection.json`: `record_as_rendition`, `derivation_as_rendition`, `record_as_companion`, `derivation_as_companion` |
| SPEC §7.1, *Refinement components*, *Selection* (every `WaiMulti::check_component_roles` code: the refinement codes, `refines-on-non-refinement`, `refinement-only-as-component`, the companion codes, `component-id-malformed`, `duplicate-component-id`) | a sink offers a `WAI2`'s refinements only when every component conforms | rule 5, the whole envelope | `session_wai2_refinement_identity_missing`, `session_wai2_refines_base_missing`, `session_wai2_refines_base_companion`, `session_wai2_layer_base_not_admitted`, `session_wai2_refinement_prev_missing`, `session_wai2_refinement_before_selectable`, `session_roles_refinement_before_selectable_exploded`, `session_wai2_refines_on_a_rendition`, `session_roles_base_without_sha256`, `session_roles_layer_before_its_prev`, `session_roles_unbound_companion`, `session_roles_record_companion`, `session_roles_derivation_companion`, `session_roles_record_rendition`; accepted: `session_roles_companion_and_refinement_accept`, `session_roles_companion_by_id_and_refinement_accept` |
| SPEC §7.1, *Companion components* | a companion that breaks rules 1–3 or its `companion_of_id`; an enhancement layer whose base is not unique | for the rendition picked, no bearing: a sink does not apply the companion and may still present the rendition, the output's digest holding whatever was applied; for the envelope's refinements, the row above | `session_roles_unbound_companion` |
## 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
@ -369,14 +448,41 @@ sink states it, then given a defect or two in the body or in a context (units, l
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:
and that do not, objects counted otherwise, listed twice or marked presented otherwise,
counted bytes that are not a held envelope's, a `first_unit` moved, inert bytes received
and named by an entry or not, every acquisition class), checked against its presentations,
objects, WAI2 envelopes and a `stage` claim; and sessions holding their envelopes — a base
of one to six frames, a `WAI1` envelope whose manifest may name a fallback its entry
decodes through, of the capability's format or another, or a companion-only capability,
the base component of a `WAI2` envelope, named by its envelope or by its exploded
envelope, held or not, or such a component that `WAI1` layers refine by its exploded
envelope's content hash, and layers refining it at
layer 1 or 2 over a span or all of its units, as `WAI1` envelopes or refinement
components, now and then naming another base or a layer as their base, at layer 0, with
an empty span or one past the base, with no `refines`, of a capability that does not
admit the base, with a `WIR1` payload a unit short, or, as components, with no `sha256`,
no role or another payload's digest, over a base that is a companion — with presentations through
each at one to three layers, within what each envelope covers and past it, entries whose
`first_unit` is the base's or one off, the base's and the layers' each, and objects whose
figures are each envelope's or one told otherwise, listed twice, fetched again, or beside
inert bytes, the base held or not, and entries naming a `WAI2` base or refinement by its
exploded envelope's content hash, its own bytes held or not; and, over every registered
capability and an unregistered string, a refinement beside a companion of that
capability bound to the base, and beside an HDR10+ descriptor and an enhancement layer
bound to a rendition of it (SPEC §7.1's component rules, which decide whether the
envelope offers its refinements); and the model session with its `WAI2` envelope
encrypted, its `security` well formed or not, the entry decoding it naming a rendition
in its table, out of it or none, its capability or another, the named rendition's
`sha256` kept, absent, well formed and unmatched, or malformed, and the classical length
stated as the table gives it or not. 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
hold or lack the set's base and layers (both bounds of §2.4.12's check), the base's or
the layer's entry now and then at another first unit than the set's; 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
further on, presented within and past what the claims holding each unit applied, and an
entry now and then at another first unit than the claims'; 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

View file

@ -38,6 +38,9 @@ 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, os.path.join(HERE, "..", "staged-measure-conformance"))
import measure_verify as MV # noqa: E402 (staged-measurement §3's counted bytes, written from that text)
import int_payload_verify as IPV # noqa: E402 (the integer payload parsers; measure_verify puts them on the path)
sys.path.insert(0, HERE)
from cryptography.hazmat.primitives import serialization # noqa: E402
@ -239,7 +242,7 @@ 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_DECODER = ["capability", "rendition", "object", "first_unit", "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"]
@ -514,6 +517,7 @@ def check_session_body(b):
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(is_int(d["first_unit"]), 2)
need(d["path"] in PATHS, 2)
need(isinstance(d["decoder"], str), 2)
need(d["variant"] is None or is_token(d["variant"]), 2)
@ -727,8 +731,13 @@ def objects_seen(v):
def check_session_objects(c, objs):
"""§2.4.12 against the objects received: each (content hash, path) once; the entry
object and each entry's object among them; the counted sums; and an object presented
exactly when a decoder entry names it."""
b = c["body"]
need(c["class"] == "reconstruct-session", 6, "context_mismatch")
keys = [(o["content_hash"], o["path"]) for o in objs]
need(len(set(keys)) == len(keys), 6, "session_object_duplicate")
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")
@ -746,6 +755,8 @@ def check_session_objects(c, objs):
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")
named = {d["object"] for d in b["decoders"]}
need(all(o["presented"] == (o["content_hash"] in named) for o in objs), 6, "session_presented_mismatch")
def wai2_read(data):
@ -822,55 +833,351 @@ def envelopes(v):
return out
def check_session_envelopes(c, envs, objs):
def counted_of(e):
"""staged-measurement §3: an envelope's counted bytes, or None where it is not
countable, for whatever reason (measure_verify.py's reading of the byte rule)."""
try:
return MV.count_any(e)["total"]
except Exception: # noqa: BLE001 (every way an object fails the byte rule is "not countable")
return None
def payload_units(cap, p):
"""§2.4.12: the units a base payload holds where WAI reads them from its format: a
WIV1 clip's frames, one for a WIH1 picture, a WIA1 clip or a WIS1 tensor; None for a
capability with no pinned integer format, or a payload not in its own."""
m = IPV.CAP_MAGIC.get(cap)
if m is None:
return None
try:
if m == b"WIV1":
return IPV.parse_wiv1(p, None)["n_frames"]
{b"WIH1": IPV.parse_wih1, b"WIA1": IPV.parse_wia1, b"WIS1": IPV.parse_wis1}[m](p)
return 1
except Exception: # noqa: BLE001 (a payload that does not parse has no units to read)
return None
def read_envelope(data):
"""A WAI1 or WAI2 envelope whose payloads are plaintext, read: ("one", manifest,
payload) or ("multi", manifest, payloads); None for any other bytes (inert)."""
if data[:4] == b"WAI2":
r = wai2_read(data)
if r is None or not plaintext_v2(r[0]):
return None
m, lengths = r
o = 8 + struct.unpack_from("<I", data, 4)[0] + 2 + 8 * len(lengths)
payloads = []
for ln in lengths:
payloads.append(data[o:o + ln])
o += ln
return ("multi", m, payloads)
if data[:4] == b"WAI1":
try:
m, _, payload = SV.read_wai1(data)
except SV.NotWai1:
return None
return ("one", m, payload) if SV.plaintext(m) else None
return None
def read_any(data):
"""A held envelope, read: (read_envelope's form, sealed). An encrypted one (SPEC §6.4)
is read too — its manifest and rendition table are in the clear — with sealed true,
since its payloads are not; None for any other bytes."""
r = read_envelope(data)
if r is not None:
return r, False
if not encrypted_envelope(data):
return None
if data[:4] == b"WAI2":
m, lengths = wai2_read(data)
o = 8 + struct.unpack_from("<I", data, 4)[0] + 2 + 8 * len(lengths)
payloads = []
for ln in lengths:
payloads.append(data[o:o + ln])
o += ln
return ("multi", m, payloads), True
m, _, payload = SV.read_wai1(data)
return ("one", m, payload), True
def base_units(cap, fallback, p):
"""§2.4.12: the units a WAI1 base's payload holds, read in the payload's own format: of
the formats pinned for the manifest's capability and its fallback, which decodes the
same payload (SPEC §4 step 5), the one whose magic the payload starts with."""
for c in (cap, fallback):
if isinstance(c, str) and IPV.CAP_MAGIC.get(c) == p[:4]:
return payload_units(c, p)
return None
def layer_units(ref, n):
"""A well-formed refines and its base's units where read: (layers, span), or None where
its span lies outside those units or its runs do not sum to the units it covers
(staged-delivery §3, §4.1 step 2)."""
if n is not None:
try:
SV.check_units(ref, n)
except SV.Malformed:
return None
span = ref["span"] if ref["span"] is not None else (None if n is None else (0, n))
return ref["layer"] + 1, span
def dispatchable(cap):
"""SPEC §4: a capability a sink decodes on its own — one the registry does not list,
or a payload that is not a candidate."""
c = REGISTRY.get(cap)
return c is None or (c["kind"] == "payload" and c["status"] != "candidate")
def companion_only(cap):
c = REGISTRY.get(cap)
return c is not None and c["kind"] == "companion"
def digest_matches(e, payload):
"""SPEC §7.1: a rendition's payload matches its stated sha256, if it states one."""
h = e.get("sha256")
return h is None or (SV.is_hash(h) and hashlib.sha256(payload).hexdigest() == h)
def digest_well_formed(e):
"""A rendition's sha256 absent or well formed (64 lowercase hex digits), read from the
entry alone: a malformed one matches no payload, so no sink decodes the rendition. Of an
encrypted envelope, whose payloads are not read, this is what an entry gives of its
digest (§2.4.12 rule 4)."""
h = e.get("sha256")
return h is None or SV.is_hash(h)
def encrypted_envelope(data):
"""SPEC §6.4: an envelope a sink holding its key decrypts and dispatches — well formed,
with a security object naming an encryption."""
if data[:4] == b"WAI2":
r = wai2_read(data)
if r is None:
return False
sec = r[0].get("security")
return isinstance(sec, dict) and plaintext_v2(r[0]) is False and isinstance(sec.get("encryption"), str) and (
sec.get("kid") is None or (isinstance(sec["kid"], int) and not isinstance(sec["kid"], bool) and 0 <= sec["kid"] < 2 ** 64))
if data[:4] == b"WAI1":
try:
m, _, _ = SV.read_wai1(data)
except SV.NotWai1:
return False
return m.get("security") is not None and not SV.plaintext(m)
return False
def component_roles(rs):
"""SPEC §5 / §7.1's component rules over a WAI2 table (staged_verify.component_roles,
which http_verify.py reads too): None where every component conforms, else the first
break's code. A sink offers the envelope's refinements only when it conforms."""
return SV.component_roles(rs)
def check_session_envelopes(c, envs, objs, ps):
"""§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."""
one the verifier holds, with the objects and the presentations where those are held.
In order: each among the objects received; each object listed with an envelope's
content hash states its counted bytes; each envelope a decoder entry names held,
readable or encrypted; each entry naming what its envelope gives, of an encrypted one a
rendition whose sha256 is absent or well formed; each held layer an entry names one a
sink can apply; each held layer's entry at the first unit of an entry naming its base;
each presentation through a held entry at the layer count its envelope gives, and of a
unit it covers; then the classical length over the held WAI2 envelopes, readable or
encrypted, whose rendition tables are in the clear. Of an encrypted envelope the rules
read the manifest and the table, and nothing that needs a payload: whether a payload
matches a well-formed sha256, a base's units and a layer's WIR1 payload are the
signer's statement."""
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 e in envs:
h, n = SV.content_hash(e), counted_of(e)
need(all(o["counted"] == n for o in objs if o["content_hash"] == h), 6, "session_counted_mismatch")
by_hash, inert, encrypted, held_envs, exploded = {}, set(), set(), [], []
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))
got = read_any(data)
if got is None:
inert.add(SV.content_hash(data))
else:
raise Reject(6, "session_envelope_unreadable")
by_hash = {}
for h, kind, r in read:
by_hash.setdefault(h, (kind, r))
r, sealed = got
h = SV.content_hash(data)
if sealed:
encrypted.add(h)
if r[0] == "multi":
# Each rendition is also its exploded envelope, the same object (SPEC §7.1,
# staged-delivery §9).
try:
each = SV.explode_each(data)
except Exception: # noqa: BLE001 (an envelope that does not explode has no aliases)
each = []
exploded += [(SV.content_hash(x), h, j) for j, x in enumerate(each) if x is not None]
by_hash.setdefault(h, r)
held_envs.append(r)
need(not any(d["object"] in inert and d["object"] not in by_hash for d in b["decoders"]), 6, "session_envelope_unreadable")
# An encrypted envelope's payloads are not read: its digests, its units and a layer's
# WIR1 payload are its signer's statement.
sealed = lambda h: h in encrypted # noqa: E731
# Each entry as the envelopes resolve it: one naming a held WAI2 rendition by its exploded
# envelope's content hash, with no rendition, names that rendition, unless the exploded
# envelope's own bytes are held (a separate delivery).
resolved = []
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"))
hit = next(((w, j) for x, w, j in exploded if x == d["object"]), None)
if d["rendition"] is None and d["object"] not in by_hash and hit is not None:
resolved.append(hit)
else:
resolved.append((d["object"], d["rendition"]))
refinement_only = lambda cap: cap in SV.UNIT_KIND # noqa: E731
# A layer is decided by its capability; any other is a base, a refines on it ignored.
layer_entry = lambda e: refinement_only(e["capability"]) # noqa: E731
layer_one = lambda m: refinement_only(m["model_requirement"]["capability"]) # noqa: E731
for d0, (o, rn) in zip(b["decoders"], resolved):
d = dict(d0, object=o, rendition=rn)
r = by_hash.get(d["object"])
if r is not None and r[0] == "multi":
rs, i = r[1]["renditions"], d["rendition"]
gives = (i is not None and i < len(rs) and rs[i]["capability"] == d["capability"]
and (selectable(rs[i]) or layer_entry(rs[i]))
and (digest_well_formed(rs[i]) if sealed(d["object"]) else digest_matches(rs[i], r[2][i])))
elif r is not None:
mr = r[1]["model_requirement"]
gives = (d["rendition"] is None and d["capability"] in (mr["capability"], mr.get("fallback"))
and (layer_one(r[1]) or (dispatchable(d["capability"]) and not companion_only(mr["capability"]))))
else:
gives = d["rendition"] is None
need(gives, 6, "session_source_mismatch")
def component(m, payloads, j, seal):
e = m["renditions"][j]
return {"capability": e["capability"], "units": None if seal else payload_units(e["capability"], payloads[j]),
"is_base": selectable(e)}
def base_by_hash(h):
r = by_hash.get(h)
if r is not None and r[0] == "one":
mr = r[1]["model_requirement"]
return {"capability": mr["capability"], "units": None if sealed(h) else base_units(mr["capability"], mr.get("fallback"), r[2]),
"is_base": not layer_one(r[1])}
hit = next(((w, j) for x, w, j in exploded if x == h), None)
if hit is None or by_hash[hit[0]][0] != "multi":
return None
return component(by_hash[hit[0]][1], by_hash[hit[0]][2], hit[1], sealed(hit[0]))
def names_of(hash_form, pair):
hashes = [hash_form] if hash_form is not None else []
pairs = [pair] if pair is not None else []
for x, w, j in exploded:
if hash_form == x:
pairs.append((w, j))
if pair == (w, j):
hashes.append(x)
return hashes, pairs
ds = b["decoders"]
def layer_of(cap, ref, base, names, payload):
"""A layer of capability cap over the base (where held) named by names: ("layer",
layers, span, first units of the entries it is applied over), or ("na",)."""
n = None
if base is not None:
if not base["is_base"] or base["capability"] not in SV.ADMIT.get(cap, ()):
return ("na",)
n = base["units"]
hashes, pairs = names
naming = [e for e, (o, rn) in zip(ds, resolved)
if e["object"] in hashes or o in hashes or (rn is not None and (o, rn) in pairs)]
over = [e["first_unit"] for e in naming
if e["capability"] in SV.ADMIT.get(cap, ()) and (base is None or e["capability"] == base["capability"])]
if naming and not over:
return ("na",)
lu = layer_units(ref, n)
if lu is None:
return ("na",)
if cap not in SV.UNIT_KIND:
return ("na",)
if payload is not None:
try:
w = SV.parse_wir1(payload, SV.UNIT_KIND[cap])
except SV.Refused:
return ("na",)
if lu[1] is not None and w["n_units"] != lu[1][1]:
return ("na",)
return ("layer", lu[0], lu[1], over)
def given(k):
"""What entry k's held envelope is: ("base", units or None), ("layer", layers, span
or None, first units), or ("na",) for a layer a sink cannot apply; None where the
envelope is not held."""
obj, rendition = resolved[k]
r = by_hash.get(obj)
if r is None:
return None
if r[0] == "one":
m, payload = r[1], r[2]
mr = m["model_requirement"]
if not layer_one(m):
return ("base", None if sealed(obj) else base_units(mr["capability"], mr.get("fallback"), payload))
try:
ref = SV.parse_refines(m.get("refines"))
except SV.Malformed:
return ("na",)
if mr.get("fallback") is not None:
return ("na",)
return layer_of(mr["capability"], ref, base_by_hash(ref["base"]), names_of(ref["base"], None), None if sealed(obj) else payload)
i = rendition
if i is None:
return None
m, payloads = r[1], r[2]
rs = m["renditions"]
if i >= len(rs):
return None
e = rs[i]
if not layer_entry(e):
return ("base", None if sealed(obj) else payload_units(e["capability"], payloads[i]))
# SPEC §7.1: a sink offers a WAI2's refinements only when every component conforms.
if component_roles(rs) is not None:
return ("na",)
ref = SV.parse_refines_ids(e["refines"])
bj = next(j for j, y in enumerate(rs) if y.get("id") == ref["base"])
return layer_of(e["capability"], ref, component(m, payloads, bj, sealed(obj)), names_of(None, (obj, bj)), None if sealed(obj) else payloads[i])
need(not any((given(k) or ("",))[0] == "na" for k in range(len(ds))), 6, "session_layer_not_applicable")
for k, d in enumerate(ds):
g = given(k)
if g is not None and g[0] == "layer":
need(not g[3] or d["first_unit"] in g[3], 6, "session_first_unit_mismatch")
if ps is not None:
for p in ps:
g = given(p["decoder"]) if p["decoder"] < len(ds) else None
if g is not None and g[0] in ("base", "layer"):
need(p["layers"] == (1 if g[0] == "base" else g[1]), 6, "session_layers_mismatch")
for p in ps:
g = given(p["decoder"]) if p["decoder"] < len(ds) else None
covers = None
if g is not None and g[0] == "base" and g[1] is not None:
covers = (0, g[1])
elif g is not None and g[0] == "layer" and g[2] is not None:
covers = g[2]
if covers is not None:
f = ds[p["decoder"]]["first_unit"] + covers[0]
need(f <= p["unit"] < f + covers[1], 6, "session_unit_outside_object")
# 9. The classical length, over the held WAI2 envelopes, readable or encrypted.
decoded = {d["capability"] for d in b["decoders"]}
total = None
for _, kind, r in read:
if kind != "multi":
for r in held_envs:
if r[0] != "multi":
continue
m, lengths = r
m = r[1]
i = classical_rendition(m)
if i is not None and m["renditions"][i]["capability"] not in decoded:
total = (total or 0) + lengths[i]
total = (total or 0) + len(r[2][i])
need(total == b["classical_declared"], 6, "session_classical_mismatch")
@ -898,6 +1205,13 @@ def check_session_stage_claims(c, wires, ps, objs):
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))
# Each entry naming a claim's base, or a layer object it lists, has a first unit at
# which stage_sets names a claim listing that object.
at = {}
for s, named in claims:
for h in [s["subject"]] + [o["object"] for o in s["body"]["objects"]]:
at.setdefault(h, set()).update(named)
need(all(d["first_unit"] in at[d["object"]] for d in b["decoders"] if d["object"] in at), 6, "session_first_unit_mismatch")
if ps is None:
return
fin = finals(ps)
@ -1360,7 +1674,7 @@ def check_context(c, ctx):
if objs is not None:
check_session_objects(c, objs)
if "envelopes" in ctx:
check_session_envelopes(c, envelopes(ctx["envelopes"]), objs)
check_session_envelopes(c, envelopes(ctx["envelopes"]), objs, ps)
if "stage_claims" in ctx:
check_session_stage_claims(c, ctx["stage_claims"], ps, objs)
if "declaration" in ctx:

View file

@ -25,7 +25,13 @@ reconstruct-session claims built from random presentation lists, around every ru
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 —
reach their stages or miss them, and sessions holding a base's and its layers' envelopes
— WAI1 envelopes or a WAI2 envelope's components, a base named by its exploded
envelope or by its envelope and rendition, a base whose fallback is of another format,
some layers a sink cannot apply —
presented within and past the layers and units each envelope gives, through entries at
their base's first unit or another, with objects whose figures are each envelope's or
told otherwise —
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,7 +54,7 @@ there is still a difference in the claim verdict, and is reported like any other
listing marks it, since its cause is in the two OER verifiers rather than the claim rules.
Those two are compared on their own by `../oer-conformance/fuzz_differential.py`.
"""
import argparse, copy, io, json, os, random, subprocess, sys, tempfile
import argparse, base64, copy, hashlib, io, json, os, random, struct, subprocess, sys, tempfile
from decimal import Decimal, localcontext
HERE = os.path.dirname(os.path.abspath(__file__))
@ -591,6 +597,25 @@ def session_stage_claim(rng, n_set, at_stage_1, signer, subject, late=0):
return CV.jcs(sign(c, signer))
def restate(body, objects):
"""The session's counted sums and transfer restated from `objects`, as a sink states
them from the objects it lists."""
body["counted_primary"] = sum(o["counted"] for o in objects if o["counted"] is not None and o["path"] == "primary")
body["counted_fallback"] = sum(o["counted"] for o in objects if o["counted"] is not None and o["path"] == "fallback")
body["counted_discarded"] = sum(o["counted"] for o in objects if o["counted"] is not None and not o["presented"])
body["uncounted"] = sum(1 for o in objects if o["counted"] is None)
body["transferred"] = sum(o["transferred"] for o in objects)
def encrypted_envelope(wai2):
"""The model session's WAI2 envelope with a `security` member that encrypts it: inert
(SPEC §6.4)."""
ml = int.from_bytes(wai2[4:8], "little")
m = json.loads(wai2[8:8 + ml])
m["security"] = {"encryption": "mls-sframe", "kid": 1}
return wai2_with(json.dumps(m, separators=(",", ":")), wai2[8 + ml:])
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
@ -598,6 +623,7 @@ def sessions(ins, rng, n):
base = ins.receipt("session_accept")
wai2 = model_envelope(ins, b"WAI2")
envelope, wai2_hash = wai2.hex(), SV.content_hash(wai2)
encrypted = encrypted_envelope(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
@ -635,7 +661,12 @@ def sessions(ins, rng, n):
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,
# Its first unit: the first unit decoded through it, as an honest sink states it,
# or one off; a rendition of one frame covers that unit alone.
used = [p["unit"] for p in ps if p["decoder"] == j]
first = min(used) if used else 0
decoders.append({"capability": cap, "rendition": rendition, "object": source,
"first_unit": rng.choice([first, first, first, first + 1, max(0, first - 1), 0]), "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,
@ -656,12 +687,14 @@ def sessions(ins, rng, n):
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 entry object's counted bytes: its envelope's, or a figure that is not.
objects.insert(rng.randint(0, len(objects)), obj(entry, counted=CV.counted_of(base_env) if rng.random() < 0.9 else 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])))
held = h == wai2_hash and rng.random() < 0.9
objects.append(obj(h, counted=CV.counted_of(wai2) if held else 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
@ -685,6 +718,10 @@ def sessions(ins, rng, n):
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))
# Presented exactly when an entry names it, as an honest sink states it.
named = {d["object"] for d in decoders}
for o in objects:
o["presented"] = o["content_hash"] in named
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,
@ -699,8 +736,9 @@ def sessions(ins, rng, n):
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.
inert = None
for _ in range(rng.choice([0, 1, 1, 2])):
k = rng.randrange(26)
k = rng.randrange(31)
ds = body["decoders"]
if k == 0:
body["primary_units"] += rng.choice([1, -1]) if body["primary_units"] else 1
@ -759,6 +797,28 @@ def sessions(ins, rng, n):
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:]
elif k == 26 and objects:
# An object listed twice, or a fetch listed apart from the one before it, on
# the other path; the sums restated to agree.
o = dict(rng.choice(objects))
if rng.random() < 0.3:
o["path"] = "fallback" if o["path"] == "primary" else "primary"
objects.append(o)
restate(body, objects)
elif k == 27 and objects:
o = rng.choice(objects)
o["presented"] = not o["presented"]
restate(body, objects)
elif k == 28:
inert = rng.choice(["encrypted", "bytes"])
elif k == 29 and ds:
d = rng.choice(ds)
d["first_unit"] = d["first_unit"] + 1 if d["first_unit"] == 0 or rng.random() < 0.5 else d["first_unit"] - 1
elif k == 30 and objects:
# A held envelope's object told a figure other than its counted bytes.
o = rng.choice(objects)
o["counted"] = rng.choice([None, 0, (o["counted"] or 0) + 1, max(0, (o["counted"] or 0) - 1)])
restate(body, objects)
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]
@ -768,6 +828,21 @@ def sessions(ins, rng, n):
if n2:
body["classical_declared"], body["classical_source"] = rng.choice(
[(700, "wai2-table"), (700 * n2, "wai2-table"), (701, "wai2-table"), (None, None)])
if inert:
# Inert bytes received — an encrypted envelope, or bytes of neither magic — listed
# as an object neither counted nor presented, held or not, and named by an entry
# or not.
data = encrypted if inert == "encrypted" else b"differential: inert bytes %d" % i
h = SV.content_hash(data)
if rng.random() < 0.8:
ctx.setdefault("envelopes", []).append({"envelope": data.hex()})
if body["decoders"] and rng.random() < 0.3:
d = rng.choice(body["decoders"])
d["object"], d["rendition"] = h, None
if "objects" in ctx:
named = {d["object"] for d in body["decoders"]}
ctx["objects"].append(obj(h, counted=None, presented=h in named))
restate(body, ctx["objects"])
if stage_wires:
ctx["stage_claims"] = stage_wires
if rng.random() < 0.1:
@ -805,13 +880,17 @@ def session_stage_bounds(ins, rng, n):
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)))
if rng.random() < 0.15:
# The base's entry, or its layer's, at another first unit than the set's.
rng.choice(decoders)["first_unit"] += 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})
objects.append({"content_hash": h, "path": "primary", "counted": cnt, "transferred": cnt,
"presented": h in {d["object"] for d in decoders}})
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,
@ -898,6 +977,12 @@ def session_stage_epochs(ins, rng, n):
# 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)))
# Presented exactly when an entry names it, as an honest sink states it.
for o in objects:
o["presented"] = o["content_hash"] in {d["object"] for d in decoders}
if rng.random() < 0.15:
# The base's entry, or a layer's, at another first unit than the claims'.
rng.choice(decoders)["first_unit"] += rng.choice([1, 2])
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})
@ -912,6 +997,350 @@ def session_stage_epochs(ins, rng, n):
ins.add(f"session_epochs_{i}", CV.jcs(sign(dict(base, subject=base_hash, body=body), VIEWER)), ctx)
def wiv1(frames, keyframe):
"""A WIV1 clip of `frames` frames, 16 by 16, built as the corpus generator builds its
own: `keyframe` its WIH1 keyframe, its residual stream filler that nothing decodes."""
block = 1 if frames == 1 else 16
nb = (16 // block) ** 2
return CV.IPV.wiv1_from_json({"n_frames": frames, "h": 16, "w": 16, "block": block, "q": 1, "offset": 0,
"cdf": [0, 32768, 65536], "mvs": [[]] + [[[0, 0]] * nb for _ in range(frames - 1)],
"rans_b64": base64.b64encode(b"\x33" * 8).decode()}, keyframe=keyframe)
def wai2_of(entries, payloads):
"""A WAI2 envelope of the manifest entries `entries`, each with its payload's SHA-256
unless it states its own, over `payloads`."""
m = {"wai": "1.1", "media": "video", "intent": "replicate",
"renditions": [dict({"sha256": hashlib.sha256(p).hexdigest()}, **e) for e, p in zip(entries, payloads)], "target": None}
t = json.dumps(m, separators=(",", ":")).encode()
table, o = b"", 0
for p in payloads:
table += o.to_bytes(4, "little") + len(p).to_bytes(4, "little")
o += len(p)
return b"WAI2" + len(t).to_bytes(4, "little") + t + len(payloads).to_bytes(2, "little") + table + b"".join(payloads)
def wir1_units(n, _cache={}):
"""A WIR1 payload of `n` units that parses (staged-delivery Appendix A): the corpus's
four-unit layer's header and table, its first unit's stream for every unit. Nothing
decodes it."""
if "four" not in _cache:
raise RuntimeError("wir1_units before session_held read the corpus's layer")
p = _cache["four"]
w = SV.parse_wir1(p, "rgb8")
cdf_len = struct.unpack_from("<H", p, 40)[0]
head = p[:8] + struct.pack("<I", n) + p[12:44]
tables = p[44:44 + 4 * cdf_len * (n if w["per_unit"] else 1)] if not w["per_unit"] else p[44:44 + 4 * cdf_len] * n
s0 = w["streams"][0]
return head + tables + struct.pack("<I", len(s0)) * n + s0 * n
def session_held(ins, rng, n):
"""§2.4.12 step 6 against the envelopes with the outputs and the objects, around the
rules that read what a held envelope gives: a base of one to six frames — a WAI1
envelope whose manifest may name a fallback the base's entry decodes through, or the
base component of a WAI2 envelope — and layers refining it at layer 1 or 2 over a span
or over all of its units, as WAI1 envelopes or as refinement components of that WAI2
envelope, now and then naming another base or one that is a layer, at layer 0, with an
empty span or one past the base, or with no `refines`; presentations through each at
one to three layers, of units within what the envelope covers and past it, through
entries whose `first_unit` is the base's first unit or one off, the base's and the
layers' each; the objects each envelope's figures — counted bytes, length, fetched once
or more, presented exactly when an entry names it — or one told otherwise, listed
twice, or inert bytes received beside them; the base held or not."""
model = ins.by_name["session_held_layer_accept"]
base = json.loads(model["receipt"])
envs = [bytes.fromhex(e["envelope"]) for e in model["context"]["envelopes"]]
whole = [bytes.fromhex(e["envelope"]) for e in ins.by_name["session_unit_whole_layer_accept"]["context"]["envelopes"]][1]
base_m, _, base_p = SV.read_wai1(envs[0])
keyframe = CV.IPV.parse_wiv1(base_p)["wih1"]
one_m, _, one_p = SV.read_wai1(envs[1]) # a layer coding one unit
_, _, four_p = SV.read_wai1(whole) # and one coding four
wir1_units.__defaults__[0]["four"] = four_p
jtext = lambda m: json.dumps(m, separators=(",", ":")).encode()
dec = {d["capability"]: d for d in base["body"]["decoders"]}
encrypted = encrypted_envelope(model_envelope(ins, b"WAI2"))
HYPER, REFINE = "wai.video.int_hyper", "wai.video.int_refine"
for i in range(n):
frames = rng.choice([1, 2, 3, 4, 4, 6])
first = rng.choice([0, 0, 1, 2, 5])
form = rng.choice(["wai1", "wai1", "wai2", "alias"])
listed_only = [] # objects received and listed whose bytes the verifier does not hold
# The layers: at layer 1 or 2, over a span or the whole base, and now and then one a
# sink cannot apply.
specs = []
for k in range(rng.choice([0, 1, 1, 2, 3])):
level = 1 if k == 0 or rng.random() < 0.5 else 2
frm = rng.randint(0, frames)
count = rng.randint(1, max(1, frames - min(frm, frames - 1)))
span = None if rng.random() < 0.3 else {"from": frm, "count": count}
bad = rng.choice(["layer0", "empty", "absent", "past", "on_layer"]) if rng.random() < 0.12 else None
specs.append((level, span, bad))
def refines(base_ref, prev, level, span, bad, on_layer):
r = {"base": base_ref, "prev": prev, "layer": 0 if bad == "layer0" else level}
if bad == "empty":
r["span"] = {"from": 0, "count": 0}
elif bad == "past":
r["span"] = {"from": frames, "count": 2}
elif span is not None:
r["span"] = span
if bad == "on_layer" and on_layer is not None:
r["base"] = r["prev"] = on_layer
r["layer"] = 1
return r
# A layer's WIR1 payload: a unit for each unit it covers, or now and then one fewer.
payload_of = lambda span: wir1_units(max(1, (span["count"] if span is not None and span["count"] else frames) # noqa: E731
- (1 if rng.random() < 0.08 else 0)))
refine_cap = lambda: "wai.image.int_refine" if rng.random() < 0.05 else REFINE # noqa: E731
if form in ("wai1", "alias"):
if form == "wai1":
# The base's capability and fallback: one format, or a fallback of another
# format than the capability's, or a companion-only capability.
cap, fallback = rng.choice([(HYPER, None), (HYPER, None), ("wai.video.int_motion", None), (HYPER, "wai.video.av1"),
("wai.neural.int_hyper", HYPER), ("wai.audio.int_codec", "wai.video.int_motion"),
("wai.meta.hdr10plus", HYPER)])
mr = {"capability": cap, "fallback": fallback} if fallback else {"capability": cap}
base_env = SV.wai1(jtext(dict(base_m, model_requirement=mr)), wiv1(frames, keyframe) if rng.random() < 0.95 else b"\x33" * 16)
subject = layer_base = SV.content_hash(base_env)
held_envs = [base_env]
base_entry = dict(dec[HYPER], capability=cap, rendition=None, object=subject, first_unit=first)
if fallback and rng.random() < 0.5:
base_entry["capability"] = fallback # decoded through the fallback
else:
# The base a WAI2 envelope's component, which the layers refine by its
# exploded envelope, and which its entry names in either form.
w = wai2_of([{"capability": HYPER, "kind": "int_hyper", "id": "b"}], [wiv1(frames, keyframe)])
x = SV.explode_each(w)[0]
layer_base = SV.content_hash(x)
if rng.random() < 0.7:
subject, held_envs = SV.content_hash(w), [w] + ([x] if rng.random() < 0.3 else [])
base_entry = dict(dec[HYPER], rendition=0, object=subject, first_unit=first)
else:
# The exploded envelope's own bytes held, or only the WAI2 the entry
# resolves to, or neither.
subject = layer_base
held_envs = ([x] if rng.random() < 0.5 else []) + ([w] if rng.random() < 0.8 else [])
listed_only = [] if x in held_envs else [x]
base_entry = dict(dec[HYPER], rendition=None, object=subject, first_unit=first)
if base_entry["capability"] != HYPER:
base_entry.update(pin=None, tier=None) # no pin above its capability's class
layer_objects, layer_caps = [], []
for k, (level, span, bad) in enumerate(specs):
ref_base = layer_base if rng.random() < 0.9 else CV.blake3(b"differential: another base %d" % i).hex()
prev = ref_base if level == 1 or not layer_objects else layer_objects[-1]
lm = copy.deepcopy(one_m)
lc = refine_cap()
lm["model_requirement"] = dict(lm["model_requirement"], capability=lc)
if bad == "absent":
lm.pop("refines", None)
else:
lm["refines"] = refines(ref_base, prev, level, span, bad, layer_objects[0] if layer_objects else None)
e = SV.wai1(jtext(lm), payload_of(span))
held_envs.append(e)
layer_objects.append(SV.content_hash(e))
layer_caps.append(lc)
layer_entries = [dict(dec[REFINE], capability=lc, rendition=None, object=h) for h, lc in zip(layer_objects, layer_caps)]
else:
comps, payloads = [{"capability": HYPER, "kind": "int_hyper", "id": "b"}], [wiv1(frames, keyframe)]
if rng.random() < 0.05:
comps[0]["role"] = "companion"
for k, (level, span, bad) in enumerate(specs):
base_id = "b" if rng.random() < 0.92 else rng.choice(["c", "B", "r0"])
prev = base_id if level == 1 or k == 0 else f"r{k - 1}"
e = {"capability": refine_cap(), "kind": "refine", "id": f"r{k}", "role": "refinement"}
if bad != "absent":
e["refines"] = refines(base_id, prev, level, span, bad, "r0" if k else None)
# Now and then a refinement that does not conform, or does not match its digest.
flaw = rng.choice(["no_sha", "no_role", "wrong_sha"]) if rng.random() < 0.08 else None
if flaw == "no_sha":
e["sha256"] = None
elif flaw == "no_role":
del e["role"]
elif flaw == "wrong_sha":
e["sha256"] = hashlib.sha256(b"differential: another payload").hexdigest()
comps.append(e)
payloads.append(payload_of(span))
env = wai2_of(comps, payloads)
subject = SV.content_hash(env)
held_envs = [env]
base_entry = dict(dec[HYPER], rendition=0, object=subject, first_unit=first)
each = SV.explode_each(env)
if rng.random() < 0.25 and each[0] is not None:
# The base component named by its exploded envelope, its own bytes held or
# not (then the entry resolves to the WAI2's rendition).
x = each[0]
if rng.random() < 0.5:
held_envs.append(x)
else:
listed_only.append(x)
subject = SV.content_hash(x)
base_entry = dict(dec[HYPER], rendition=None, object=subject, first_unit=first)
layer_entries = []
for k in range(len(specs)):
xk = each[k + 1] if k + 1 < len(each) else None
if xk is not None and rng.random() < 0.25:
# A refinement named by its exploded envelope's content hash.
layer_entries.append(dict(dec[REFINE], capability=comps[k + 1]["capability"], rendition=None, object=SV.content_hash(xk)))
if rng.random() < 0.3:
held_envs.append(xk)
else:
listed_only.append(xk)
else:
layer_entries.append(dict(dec[REFINE], capability=comps[k + 1]["capability"], rendition=k + 1, object=SV.content_hash(env)))
for d in layer_entries:
d["first_unit"] = first if rng.random() < 0.9 else rng.choice([0, first + 1])
# Each unit at one layer through the base, and some at two or three through a layer
# of that level — one covering the unit, or any.
units = list(range(first, first + frames))
if rng.random() < 0.2:
units.append(rng.choice([first + frames, max(0, first - 1)]))
ps = []
for u in sorted(set(units)):
ps.append({"unit": u, "layers": 1, "decoder": 0})
for level in (1, 2):
cands = [j for j, (lv, _, _) in enumerate(specs) if lv == level]
cover = [j for j in cands if specs[j][1] is None or specs[j][1]["from"] <= u - first < specs[j][1]["from"] + specs[j][1]["count"]]
pick = cover if cover and rng.random() < 0.85 else cands
if not pick or rng.random() < 0.4:
break
ps.append({"unit": u, "layers": level + 1, "decoder": 1 + rng.choice(pick)})
if rng.random() < 0.15 and specs:
# A presentation through an entry whose envelope gives another layer count.
p = rng.choice(ps)
p["decoder"] = 1 + rng.randrange(len(specs)) if p["layers"] == 1 or rng.random() < 0.6 else 0
for p in ps:
p["out_digest"] = CV.blake3(b"differential: held %d %d %d" % (i, p["unit"], p["layers"])).hex()
# Entries in first-use order, the unused layers named by none.
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"])
all_entries = [base_entry] + layer_entries
decoders = []
for j, old in enumerate(order):
d = dict(all_entries[old])
d["leaves"] = sum(1 for p in ps if p["decoder"] == j)
decoders.append(d)
if rng.random() < 0.1:
decoders[0]["first_unit"] = rng.choice([first + 1, max(0, first - 1)])
held = list(held_envs)
named = {d["object"] for d in decoders}
listed = held + [e for e in listed_only if e not in held]
objects = [{"content_hash": SV.content_hash(e), "path": "primary", "counted": CV.counted_of(e),
"transferred": len(e) * rng.choice([1, 1, 1, 2, 3]), "presented": SV.content_hash(e) in named}
for e in listed]
# One or two defects among the objects and envelopes.
inert = None
for _ in range(rng.choice([0, 0, 1, 1, 2])):
k = rng.randrange(6)
o = rng.choice(objects)
if k == 0:
objects.append(dict(o))
elif k == 1:
o["presented"] = not o["presented"]
elif k == 2:
o["counted"] = rng.choice([None, 0, (o["counted"] or 0) + 1, max(0, (o["counted"] or 0) - 1)])
elif k == 3 and len(held) > 1:
held = held[1:] # the base not held
elif k == 4:
inert = rng.choice([encrypted, b"differential: inert bytes %d" % i])
elif k == 5:
objects.append(dict(o, path="fallback")) # fetched again on the other path
envelopes = list(held)
if inert is not None:
ih = SV.content_hash(inert)
envelopes.append(inert)
if rng.random() < 0.3 and len(decoders) > 1:
decoders[-1]["object"] = ih
objects.append({"content_hash": ih, "path": "primary", "counted": None, "transferred": len(inert),
"presented": ih in {d["object"] for d in decoders}})
rng.shuffle(envelopes)
fin = CV.finals(ps)
ordered = sorted(ps, key=lambda p: (p["unit"], p["layers"]))
body = dict(base["body"], decoders=decoders, units=len(fin), leaf_count=len(ps), primary_units=len(fin),
fallback_units=0, output_root=CV.out_mth([CV.out_leaf(p) for p in ordered]).hex())
restate(body, objects)
ctx = {"outputs": ps, "objects": objects, "envelopes": [{"envelope": e.hex()} for e in envelopes]}
if rng.random() < 0.05:
del ctx["outputs"]
ins.add(f"session_held_{form}_{i}", CV.jcs(sign(dict(base, subject=subject, body=body), VIEWER)), ctx)
def session_encrypted(ins, rng, n):
"""§2.4.12 against an encrypted envelope, whose manifest and rendition table are in the
clear (SPEC §6.4): the model session with its WAI2 envelope encrypted — its security
well formed or not — and the entry decoding it naming a rendition in the table, out
of it or none, its capability or another; the named rendition's sha256 kept, absent,
well formed and unmatched, or malformed (uppercase, 63 digits, not hex), which matches
no payload whether or not the envelope is encrypted; the classical length stated as
the table gives it, or not; the plaintext envelope held beside it or not."""
model = ins.by_name["session_envelope_encrypted"]
base = json.loads(model["receipt"])
enc = bytes.fromhex(model["context"]["envelopes"][0]["envelope"])
ml = int.from_bytes(enc[4:8], "little")
m0, rest = json.loads(enc[8:8 + ml]), enc[8 + ml:]
plain = model_envelope(ins, b"WAI2")
for i in range(n):
m = copy.deepcopy(m0)
m["security"] = rng.choice([m0["security"], m0["security"], {"encryption": "mls-sframe", "kid": -1}, {"encryption": 5}, {"encryption": "none"}])
r0 = m["renditions"][0]
good = r0.get("sha256") or hashlib.sha256(b"a payload").hexdigest()
form = rng.choice(["keep", "keep", "keep", "absent", "unmatched", "upper", "short", "not_hex"])
if form == "absent":
r0.pop("sha256", None)
elif form != "keep":
r0["sha256"] = {"unmatched": hashlib.sha256(b"another payload").hexdigest(), "upper": good.upper(),
"short": good[:63], "not_hex": "z" * 64}[form]
e = wai2_with(json.dumps(m, separators=(",", ":")), rest)
body = copy.deepcopy(base["body"])
d = body["decoders"][1]
d["object"] = SV.content_hash(e)
d["rendition"] = rng.choice([0, 0, 1, 2, 9, None])
d["capability"] = rng.choice(["wai.video.int_hyper", "wai.video.int_hyper", "wai.video.avc", "wai.audio.opus"])
if d["capability"] != "wai.video.int_hyper":
d["pin"], d["tier"] = None, None
envs = [e] + ([plain] if rng.random() < 0.3 else [])
body["classical_declared"] = rng.choice([700, 700, None, 700 * len(envs), 701])
body["classical_source"] = None if body["classical_declared"] is None else "wai2-table"
ctx = dict(model["context"], envelopes=[{"envelope": x.hex()} for x in envs])
ins.add(f"session_encrypted_{i}", CV.jcs(sign(dict(base, body=body), VIEWER)), ctx)
def session_roles_sweep(ins):
"""SPEC §7.1's component rules, which decide whether a WAI2 offers its refinements, over
every registered capability and an unregistered string: a refinement beside a companion
of each capability bound to the base, and beside an HDR10+ descriptor and an
enhancement layer bound to a rendition of each capability."""
model = ins.by_name["session_roles_companion_and_refinement_accept"]
base = json.loads(model["receipt"])
w = bytes.fromhex(model["context"]["envelopes"][0]["envelope"])
_, m, payloads = CV.read_envelope(w)
b, comp, ref = m["renditions"]
for cap in sorted(CV.REGISTRY) + ["org.example.meta"]:
cases = [
("companion", [b, dict(comp, capability=cap), ref], [payloads[0], payloads[1], payloads[2]], 2),
("hdr_over", [b, {"capability": cap, "kind": "k", "id": "x"}, dict(comp, companion_of=cap, companion_of_id=None), ref],
[payloads[0], payloads[0], payloads[1], payloads[2]], 3),
("lcevc_over", [b, {"capability": cap, "kind": "k", "id": "x"}, dict(comp, capability="wai.video.lcevc", companion_of=cap, companion_of_id=None), ref],
[payloads[0], payloads[0], payloads[1], payloads[2]], 3),
]
for tag, entries, pls, ri in cases:
entries = [{k: v for k, v in e.items() if v is not None} for e in entries]
e2 = wai2_of(entries, pls)
h = SV.content_hash(e2)
body = copy.deepcopy(base["body"])
body["decoders"][0]["object"] = h
body["decoders"][1].update(object=h, rendition=ri)
objects = [{"content_hash": h, "path": "primary", "counted": CV.counted_of(e2), "transferred": len(e2), "presented": True}]
restate(body, objects)
ctx = dict(model["context"], objects=objects, envelopes=[{"envelope": e2.hex()}])
ins.add(f"session_roles_{tag}_{cap}", CV.jcs(sign(dict(base, subject=h, 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."""
@ -1618,9 +2047,12 @@ def main(argv):
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_held(ins, random.Random(f"session held {a.seed}"), a.random) # its own stream, as classes()
session_envelopes(ins)
session_wai1_envelopes(ins)
session_classical_sweep(ins)
session_roles_sweep(ins)
session_encrypted(ins, random.Random(f"session encrypted {a.seed}"), a.random) # its own stream, as classes()
doc = text_of({"about": "differential inputs; no outcome is declared", "vectors": ins.out})
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as f:
f.write(doc)

File diff suppressed because one or more lines are too long

View file

@ -323,7 +323,11 @@ The procedure:
the component-digest test to the bytes. A deployer policy MAY reorder and restrict
the entries considered, as SPEC §7.1 allows. The result is the picked entry, the
companions SPEC §7.1 binds to it, and the refinement components whose
`refines.base` is its `id`.
`refines.base` is its `id`. SPEC §7.1 offers an envelope's refinements only when every
one of its components conforms, which the verified head's manifest decides: where one
does not, each of those refinement components is dropped
(`components_nonconforming`) and never requested, one that states no `sha256` being
dropped for that too (step 5), and the base's path does not change.
5. If the picked entry, or a bound companion the sink will apply that is not a
refinement (§2), states no `sha256`, go to step 7 (`component_digest_absent`). A
refinement that states none is dropped: the client does not request it, and the
@ -595,7 +599,8 @@ or beside the base component's. A client that abandons an enhancement companion
presents the base, as [SPEC](../SPEC.md) §7.1 lets a sink that does not apply a
companion present the rendition; one that abandons a refinement component presents the
stage it has ([staged-delivery](staged-delivery.md) §7). A refinement that states no
`sha256`, or does not match it, is dropped (§7 steps 5 and 6), and its base is
`sha256`, or does not match it, is dropped (§7 steps 5 and 6), as is every refinement
of an envelope whose components do not all conform (§7 step 4), and its base is
unaffected.
### 10.4 Layer objects
@ -887,6 +892,7 @@ is a share of sessions, not of devices or people.
| `head_proof_malformed`, `head_proof_mismatch` | §6.3 |
| `http_status_unexpected`, `content_coding_present`, `content_range_mismatch`, `complete_length_mismatch` | §7.1 |
| `component_digest_absent` | §7 step 5 (a reason to fetch whole, never a skip) |
| `components_nonconforming` | §7 step 4 (a refinement dropped, never a skip) |
| `component_digest_mismatch` | §7 step 6 ([SPEC](../SPEC.md) §7.1's reason) |
| `object_digest_mismatch` | §7 step 7, or any whole fetch |
| `receipt_name_mismatch`, `receipt_not_about_object` | §8.1, §8.2, §8.6 |

View file

@ -670,7 +670,7 @@ receipt's identity for every other profile.
| `energy_counter_filtering` | string or `null` | As §2.4.5. |
| `energy_calibration_ref` | string or `null` | As §2.4.5. |
| `bracket` | object or `null` | For a figure obtained under energy-measurement §3: `{ "idle": summary, "busy": summary, "window_us": integer, "completions": integer, "in_flight": integer, "opening": "from-rest" or "running" }`, a summary being `{ "n": integer, "mean_uw": integer, "sd_uw": integer, "live_intervals": integer, "longest_live_run": integer }` as energy-measurement §6.2 defines them. `null` otherwise. |
| `build` | object | `{ "profile": "release" or "debug", "toolchain": string or null, "target": term or null }`: the build of the code measured (energy-measurement §5, §8). `release` is a build optimised as shipped; any other build is `debug`. A finer vocabulary is a new registration. |
| `build` | object | `{ "profile": "release" or "debug", "toolchain": string or null, "target": term or null }`: the build of the code measured (energy-measurement §5, §8). `release` is an optimised build (for a Rust build, `opt-level` 2, 3, `s` or `z`, whatever its profile is named); any other build is `debug`. A finer vocabulary is a new registration. |
| `power_reader` | string or `null` | The reader of the power interface (energy-measurement §8). Required under a hardware class (rule 4). |
A report carries none of what follows from its values: neither the §4 ratio nor
@ -967,10 +967,10 @@ from its `stage` claim. Step 6 checks the two against each other.
| `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. |
| `uncounted` | integer | The objects received that are not countable (staged-measurement §3, *Only a checked format is counted*). Their bytes are in no counted member. |
| `transferred` | integer | Bytes of WAI objects as received, whole or as byte ranges, without transport or receipts. |
| `receipt_bytes` | integer | Bytes of receipts and claims received (staged-measurement §3.5). |
| `classical_declared` | integer or `null` | The sum, over the `WAI2` envelopes the session received whose classical rendition (below) is of a capability no decoder entry decoded, of that rendition's length as the envelope's rendition table declares it (SPEC §7.1). `null` where there is none. Never estimated. |
| `classical_declared` | integer or `null` | The sum, over the `WAI2` envelopes the session received whose classical rendition (below) is of a capability no decoder entry decoded, of that rendition's length as the envelope's rendition table declares it (SPEC §7.1). `null` where there is none. Never estimated. This revision derives it from the `WAI2` envelopes the sink holds in full, the ones rule 9 reads: a sink that received a `WAI2` envelope only as byte ranges holds its head but not the envelope, and cannot yet state that envelope's classical rendition. Recording such a session is HTTP session work still to come. |
| `classical_source` | term or `null` | Where `classical_declared` comes from: `wai2-table`, or a term a later extension registers here. |
| `energy` | object or `null` | `null` when the decode was unmetered. Otherwise `{ "joules_micro", "energy_provenance", "energy_uncertainty", "energy_counter_filtering", "energy_calibration_ref" }`, each written as §2.4.5 writes it (below). |
@ -992,6 +992,7 @@ A **decoder entry**:
| `capability` | term | The capability decoded. |
| `rendition` | integer or `null` | Its `WAI2` rendition index (SPEC §7.1): `capability` is that rendition's capability. `null` for a `WAI1` envelope. |
| `object` | 32 B hex | The content hash (§2.1) of the object it decoded from: the `WAI2` envelope whose rendition it is, or the `WAI1` envelope — a base, or a layer that refines one. An entry decodes one object, so a sink that decodes several through one decoder names an entry for each. |
| `first_unit` | integer | The title's unit that is unit 0 of the object's base: of the object itself for a base or a rendition with no `refines`, and of the base it refines for a layer. A presentation through the entry of the title's unit `u` is of the object's unit `u − first_unit`. Where a verifier holds a layer's envelope and an entry names its base, the layer's entry has that entry's `first_unit`; where it holds a `stage` claim of a set, an entry naming the set's base or a layer the claim lists has the unit `stage_sets` names that claim at (step 6). |
| `path` | string | `primary` or `fallback`. |
| `decoder` | string | `builtin:<crate>@<version>` — `<crate>` 1 to 64 of `a`–`z`, `0`–`9`, `_` and `-`, `<version>` 1 to 64 of ASCII letters, digits, `.`, `+` and `-`, beginning with a digit — or `module:sha256:` followed by a module's digest (§7.9), 64 lowercase hexadecimal digits. |
| `variant` | term or `null` | A decode path within the decoder, where it has more than one, such as an exact and a floating-point path. |
@ -1062,20 +1063,27 @@ at, the index of the decoder entry that produced it, and the SHA-256 of its outp
`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.
(SPEC §7) — the entry's object's unit `unit − first_unit`, at `layers` — and then
**MUST** find its `out_digest` (`session_output_irreproducible`). Every other
presentation is checked for membership only.
**Against the objects** (§3.1 step 6). The context lists each object received:
`{ "content_hash", "path", "counted", "transferred", "presented" }`, with `counted` the
object's counted bytes, or `null` for one that is not countable. In order:
1. The session's `subject`, its entry object, is the content hash of an object received
object's counted bytes, or `null` for one that is not countable; `transferred` every byte
received for it; and `presented` whether a decoder entry names it. An object received more
than once on a path — fetched again, or in byte ranges — is listed once, its `transferred`
the bytes of every receipt. These rules need the objects and the claim alone. In order:
1. No `content_hash` is listed twice with one `path` (`session_object_duplicate`).
2. The session's `subject`, its entry object, is the content hash of an object received
(`session_entry_not_received`).
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
3. So is each decoder entry's `object` (`session_source_not_received`).
4. `counted_primary` and `counted_fallback` are the sums of `counted` over the objects of
each path, `counted_discarded` its sum over the objects not presented, `uncounted` the
number of objects whose `counted` is `null`, and `transferred` the sum of
`transferred` (`session_counted_mismatch`).
5. An object is `presented` exactly when a decoder entry's `object` is its content hash
(`session_presented_mismatch`). The sums above can agree with figures a sink stated
falsely; this cannot.
**The classical rendition** of a `WAI2` envelope is the first of its renditions, in
manifest order, that is selectable — not a companion or a refinement, and a capability a
@ -1088,24 +1096,114 @@ 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:
a layer — the verifier holds. An envelope is **readable** when it reads as a `WAI1` or
`WAI2` envelope (SPEC §2, §7.1) whose payloads are plaintext, and **encrypted** when it
reads as one whose `security` is a security object naming an encryption (SPEC §6.1): a
sink holding the key decrypts and dispatches it (§6.4 step 3), and its manifest and
rendition table are in the clear. Both are **held**: the rules below read every held
envelope's manifest and table, and read an encrypted one's payloads not at all. Bytes
that are neither — one whose `security` member is not a security object, or bytes that
are not an envelope at all — are **inert**. An inert or encrypted envelope no decoder
entry names is an object received, neither counted (its `counted` is `null`,
staged-measurement §3, *Only a checked format is counted*) nor presented; a sink lists
it like any other, so the session accounts for every byte it transferred. Each
rendition of a held `WAI2` envelope is also its exploded envelope (SPEC §7.1, *Exploding a
component*), the same object (staged-delivery §9): a decoder entry names it either by
that `WAI2` envelope and its `rendition`, or by the exploded envelope's content hash. An
entry naming it by that content hash, with no `rendition`, is read as naming the
rendition, by every rule below — unless the verifier holds the exploded envelope's own
bytes, a separate delivery (staged-delivery §9, [http-delivery](http-delivery.md)
§10.4) that the rules read as the `WAI1` envelope it is. A **held layer** is a held
`WAI1` envelope of a refinement-only capability (SPEC §5), or a rendition of a held
`WAI2` envelope of one: dispatch and selection go by capability, so an envelope or a
rendition of any other capability is a base, a `refines` it carries ignored (SPEC §4,
staged-delivery §3). Its
**base** is named by `refines.base`: in a `WAI1` layer by a content hash, of a `WAI1`
envelope or of a `WAI2` rendition's exploded envelope; in a `WAI2` rendition by the
`id` of a rendition of the same envelope. The base is **held** when the verifier holds a
held envelope that is it, in either form; its own capability is the `WAI1`
manifest's `capability`, or the rendition's; and an entry **names** it when it names it
in either form. A layer is **applied over** the entries naming its base whose
capability the layer's admits (SPEC §5, *Base admission*) and, where the base is held,
is the base's own. Rules 1 and 2 need the objects as well, rules 7 and 8 the outputs;
the others need the envelopes and the claim. In order:
1. Where the verifier holds the objects as well, each envelope is among the objects
received, by its content hash (`session_source_not_received`).
2. 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
2. Where it holds the objects as well, each object listed with an envelope's content hash
states as `counted` that envelope's counted bytes (staged-measurement §3), and `null`
exactly when it is not countable (`session_counted_mismatch`).
3. Each envelope a decoder entry names is held, readable or encrypted
(`session_envelope_unreadable`). The verifier holds no key: of an encrypted envelope,
the rules below check everything its manifest and rendition table give, and nothing
that needs a payload — whether a payload matches a well-formed `sha256` (rule 4), the
units a base holds, a layer's `WIR1` payload and the span read against its base's units
(rules 5 and 8) — which are the signer's statement.
4. Each decoder entry whose `object` is a held envelope names what that envelope
gives, as a sink dispatches it: of a `WAI2` envelope, a `rendition`, the capability
that rendition has, a rendition selectable or a layer — never a companion, which is
applied with the rendition a sink picks — whose payload matches its stated `sha256`
(SPEC §7.1, *Selection algorithm*, *Component identity*), and of an encrypted envelope
whose `sha256`, where stated, is well formed: 64 lowercase hex digits, the only form a
payload can match, so a rendition whose `sha256` is not (uppercase, another length,
not hex) is one no sink decodes, whatever its payload; of a `WAI1` envelope that
is not a layer, no `rendition` and the manifest's `capability` or its `fallback`, a
capability a sink dispatches, in a manifest whose `capability` is not companion-only
(SPEC §4); of a `WAI1` layer, no `rendition` and its `capability`. And each decoder
entry whose `rendition` is not `null` has its `object` among the held
envelopes, which hold every `WAI2` envelope the session received
(`session_source_mismatch`). A signer cannot relabel a decoder entry whose envelope
the verifier holds, and so cannot move a presentation through it out of the reach of
the re-decode check above.
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`).
(`session_source_mismatch`). A
signer cannot relabel a decoder entry whose envelope the verifier holds, and so cannot
move a presentation through it out of the reach of the re-decode check above.
5. Each decoder entry whose `object` is a held layer names one a sink can apply
(`session_layer_not_applicable`); a sink refuses any other (staged-delivery §4.1, SPEC
§7.1), so no presentation through it is possible:
- in a `WAI1` manifest, its `fallback` is `null` (`capability-not-refinement`), and
its `refines` is present and reads as staged-delivery §3 defines it
(`refines-malformed`);
- a `WAI2` rendition is offered only from an envelope every component of which
conforms (SPEC §7.1, *Refinement components*; the reference
`WaiMulti::check_component_roles`): a refinement component that does not, or a
companion, an `id` or another component that breaks a rule of §7.1 or §5, leaves
none of the envelope's refinements offered;
- its base, where held, is a base — not a layer, and a selectable rendition where it
is a `WAI2` rendition — whose own capability the layer's admits
(`base-not-admitted`, step 4);
- where entries name its base, it is applied over one of them: a sink applies a layer
only over a base decoded through a capability the layer admits, and through the
base's own capability, not its fallback (`base-via-fallback`, step 3);
- where its base's units are read (rule 8), its span lies within them and its runs
sum to the units it covers (`refines-malformed`, step 2);
- its payload parses as `WIR1` for its capability's unit kind (staged-delivery
Appendix A) and holds a unit for each unit it covers, where that count is read
(`payload-malformed`, step 6).
6. Each decoder entry whose `object` is a held layer that is applied over an entry has
the `first_unit` of one of the entries it is applied over
(`session_first_unit_mismatch`). A layer's units are its base's, so its entry states
its base's first unit, whichever form names the base.
7. Where it holds the outputs as well, each presentation through a decoder entry whose
envelope is held is at the layer count that envelope gives: 1 for a base, or a
rendition that is not a layer, and `refines.layer + 1` for a layer
(`session_layers_mismatch`).
8. Where it holds the outputs as well, each presentation through a decoder entry whose
envelope is held is of a unit that envelope covers: its `unit` is at least
`first_unit + a` and below `first_unit + a + n`, where for a base, or a rendition that
is not a layer, `a` is 0 and `n` the units its payload holds — a `WIV1` clip's frames,
and one for a `WIH1` picture, a `WIA1` clip and a `WIS1` tensor — read in the
payload's own format: of the formats SPEC §5 pins for a `WAI1` manifest's
`capability` and its `fallback`, which decodes the same payload (SPEC §4 step 5), the
one whose magic the payload starts with, whichever capability the entry names, and a
rendition's capability's; for a layer, `a` and `n` are its `span`'s `from` and
`count`, or, with no span, 0 and the units of its base, read where the base is held,
in either form (`session_unit_outside_object`). Where WAI reads no unit count — a
payload in no format SPEC §5 pins, one that does not parse as its own, or a base not
held — the presentation is not checked.
9. The lengths of the held `WAI2` envelopes' classical renditions whose capability no
decoder entry decoded — encrypted or not, since the rendition table declares them in
the clear — sum to `classical_declared`, which is `null` exactly when there is none
(`session_classical_mismatch`). In this revision a held envelope is one held in full:
of a `WAI2` envelope a sink received only as byte ranges it holds the head and not the
envelope, so the session cannot yet state that envelope's classical rendition, nor this
rule read it. Recording such a session is HTTP session work still to come.
**Against the stage claims** (§3.1 step 6). The context is the wire form of each `stage`
claim the verifier holds of those `stage_sets` names. A set has a claim for each of its
@ -1116,7 +1214,11 @@ epochs, and bases that cover the same units of the title each have their own. In
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
4. each decoder entry whose `object` is the `subject` of one of them, or a layer object
one lists, has as its `first_unit` the `first_unit` of an entry of `stage_sets` naming
a claim of that object (`session_first_unit_mismatch`): a set's base and its layers
are at the units the session names the set at;
5. 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;
@ -1125,7 +1227,7 @@ epochs, and bases that cover the same units of the title each have their own. In
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
6. 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
@ -1142,7 +1244,27 @@ object it names, which binds it; that the entry's outputs were decoded from that
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
signer's statements (energy-measurement §5). So, deliberately, is each refusal a sink
makes that the rules above do not check, since no context of this claim holds what
decides it: that a layer's base was decoded before the layer was applied, where no
decoder entry names the base (staged-delivery §4.1 step 2, `base-absent`), since a sink
may present a base's units only once a layer has refined them; a layer's delivery
receipt (step 5, `refinement-unattested`), which no context carries; the decode of a
layer's streams to their last symbol, its declared cost and the chain budget (step 6,
`payload-malformed` for a stream, `cost-over-limit`, `chain-over-limit`), which a
verifier that decodes a byte-exact presentation again checks, and which otherwise turn
on the sink's limits; each unit's producer, conflict, geometry and deadline (steps
7–10) and the hold bound and batches (§4.3–§4.4), which turn on the order and the times
of arrival the sink alone saw, while the `stage` claims bound the stages a unit reached
and a re-decode its output; a pin's resolution at the sink, its module authorisation and
its key log (SPEC §3.1, §4), which a decoder entry's `pin` and `tier` state; what an
encrypted envelope's payloads hold — whether each matches its well-formed `sha256`, the
units a base holds and a layer's `WIR1` payload (SPEC §6.4); whether a refinement's exploded envelope that the verifier
holds as bytes of their own was received on its own, a separate delivery that rule 5
reads as a `WAI1` layer and not through its envelope's component rules (staged-delivery
§9); and a
companion's conformance (SPEC §7.1), which does not decide whether the rendition it binds
to is presented. Nothing binds a `stage` claim to a session
beyond its base and its key: a `stage` claim the same key signed about the same base in
an earlier session of the title can be named, and the checks above hold it as they hold
one of this session. The `claim` an
@ -1854,11 +1976,17 @@ and never folds it in: confirmation changes the outcome of no step here.
| 6 | `session_output_mismatch` | a session's presentations are not `leaf_count`, repeat a (unit, layers), or give another `output_root` |
| 6 | `session_output_inconsistent` | the presentations name an entry that does not exist or a `layers` of 0, or give another `units`, another entry's `leaves`, another first-use order, other `primary_units` or `fallback_units`, or no presentation for a `missed_units` entry |
| 6 | `session_output_irreproducible` | a presentation decoded again, through a decoder its replicate class makes byte-exact, gives another `out_digest` |
| 6 | `session_object_duplicate` | an object received is listed twice with one path |
| 6 | `session_entry_not_received` | a session's entry object is not among the objects it received |
| 6 | `session_source_not_received` | a decoder entry's `object`, or an envelope held as the session's, is not among the objects it received |
| 6 | `session_counted_mismatch` | the objects received give other counted, uncounted or transferred bytes |
| 6 | `session_envelope_unreadable` | an envelope held as the session's is not a `WAI1` or `WAI2` envelope whose payloads are plaintext |
| 6 | `session_counted_mismatch` | the objects received give other counted, uncounted or transferred bytes, or an object whose envelope is held states other counted bytes than that envelope gives |
| 6 | `session_presented_mismatch` | an object is listed as presented and no decoder entry names it, or as not presented and one does |
| 6 | `session_envelope_unreadable` | an envelope a decoder entry names is neither a `WAI1` or `WAI2` envelope whose payloads are plaintext nor a well-formed encrypted one |
| 6 | `session_source_mismatch` | a decoder entry names other than its object's envelope gives — another rendition, a capability that rendition or that `WAI1` manifest does not have, or a rendition of a `WAI1` envelope — or names a rendition and its envelope is not held |
| 6 | `session_layer_not_applicable` | a decoder entry names a held layer a sink cannot apply: not a refinement, its `refines` malformed or absent, a nonconforming refinement component, its base not a base its capability admits or decoded only through a fallback, its span outside a held base's units, or its payload not `WIR1` with a unit for each it covers |
| 6 | `session_first_unit_mismatch` | a decoder entry's `first_unit` is not its base's: a held layer's, against the entries naming its base; or an entry naming a `stage` claim's base or layer, against the unit `stage_sets` names the claim at |
| 6 | `session_layers_mismatch` | a presentation through a decoder entry whose envelope is held is at another layer count than that envelope gives |
| 6 | `session_unit_outside_object` | a presentation through a decoder entry whose envelope is held is of a unit that envelope does not cover, from its `first_unit` |
| 6 | `session_classical_mismatch` | the envelopes' classical renditions the session did not decode give another `classical_declared` |
| 6 | `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 |
@ -2348,7 +2476,52 @@ capability another envelope has — refused against the envelope of the object i
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,
bytes of neither magic, each named by an entry (`session_envelope_unreadable`), and
accepted where no entry names them; an object listed twice
(`session_object_duplicate`) and one fetched twice and listed once (accepted); figures a
sink stated rather than derived — counted bytes above what the held envelope gives
(`session_counted_mismatch`), and an object marked presented that no entry names, or
not presented that one does (`session_presented_mismatch`); a session holding its base's
and a layer's envelopes, accepted, and a presentation at more layers than its envelope
gives (`session_layers_mismatch`), of a unit past the base's frames, outside the layer's
span, or outside a `WAI2` rendition's frame, through an entry whose `first_unit` is
moved, past the frames of a base decoded through its manifest's `fallback`, the same
where the fallback's format and not the capability's is the payload's, and past a
`WAI2` base component a `WAI1` layer refines by its exploded envelope
(`session_unit_outside_object`); a held inert envelope listed with counted bytes 0
(`session_counted_mismatch`); an encrypted envelope an entry names, accepted with the classical length its table
declares, and refused for that length unstated, a rendition its table does not hold,
another capability, no rendition, a base at two layers, a companion-only capability, a
refinement before a selectable rendition, and a rendition whose `sha256` is malformed
(uppercase, 63 digits, not hex), named by its rendition and by its exploded envelope's
content hash, all read from the manifest in the clear, and accepted with a well-formed
`sha256` its payload does not match;
an encrypted layer over an encrypted base, accepted; entries naming a `WAI2` rendition by its exploded envelope's
content hash, held as the `WAI2` alone — accepted, relabelled, past its frames, a
refinement off its base's first unit and one whose envelope does not conform — and a
refinement's exploded envelope received on its own, accepted; bases whose manifest or
rendition carries a stray `refines`, accepted, and a layer over one; a refinement of an
envelope that does not conform — a refinement before a selectable rendition, a base
rendition with no `sha256`, a layer before its producer, an unbound companion, a record
component with no role — and two
that do, beside a companion; entries a sink cannot have decoded through — a `WAI2`
companion, a rendition or a refinement whose payload does not match its `sha256`, a
`WAI1` envelope whose capability is companion-only, and a fallback that is
(`session_source_mismatch`); held layers a sink cannot apply — layer 0, a span past or
empty over a held base, runs that do not sum to the base's units, no `refines` on a
refinement-only capability, `refines` on one that is not, a fallback on a layer, a base
that is itself a layer, a base decoded only through its fallback, a base the layer does
not admit, a payload that is not `WIR1` or holds too few units, and in a `WAI2` envelope
a base that is no rendition, no component id, another refinement component or a
companion, a base it does not admit, a refinement with no `sha256`, a producer the
envelope does not hold, a selectable rendition after the refinement, and a span past the
base (`session_layer_not_applicable`); a layer's entry at another `first_unit` than its
base's, in a `WAI1` layer and a `WAI2` refinement component, and across the two forms of
one base — a `WAI1` layer over a `WAI2` component, with its exploded envelope held and
not, and a refinement component whose base an entry names by its exploded envelope —
and the base's and a layer's entries at another than the unit `stage_sets` names their
`stage` claim at (`session_first_unit_mismatch`); `WAI2` refinement components presented
within and past the layers and units they give; a `stage` claim that verifies, one that does not, one by another key,
one the session does not name, one about a base the session never received, one whose
arrived layers the session never received, and sessions presenting stages its applied
layers cannot have given — through layers it lists as late or refused among them — and

View file

@ -40,7 +40,11 @@ base, by the base's publisher or by a party the sink accepts (§10.1).
A `WAI1` manifest whose `model_requirement.capability` is refinement-only **MUST**
carry `refines`, and its `model_requirement.fallback` **MUST** be `null`. A manifest
whose capability is not refinement-only **MUST NOT** carry it.
whose capability is not refinement-only **MUST NOT** carry it. A sink routes an
envelope by its capability: one whose capability is refinement-only is a layer, which
§4 processes; any other is a base, dispatched by SPEC §4, and a `refines` it carries is
ignored. Likewise a `WAI2` rendition is a layer only when its capability is
refinement-only (SPEC §7.1).
```json
"refines": { "base": "<64 hex>",
@ -79,7 +83,10 @@ revision does not define is ignored (SPEC §8).
### 4.1 Algorithm
A sink that implements this extension **MUST** process each received envelope *R*
whose capability is refinement-only as follows. Steps 1–6 decide for the whole
whose capability is refinement-only as follows. A sink routes by capability (§3), so an
envelope whose capability is not refinement-only reaches step 1 only where a caller
offers it to this algorithm as a layer; step 1 refuses it, as it refuses a
refinement-only envelope whose fallback is not `null` (`capability-not-refinement`). Steps 1–6 decide for the whole
envelope; steps 7–10 decide for each unit *u* the layer covers. The first step that
fails is the reason for the envelope (1–6) or for the unit (7–10).

View file

@ -66,9 +66,11 @@ reference adaptation policies. Two implementations check every case and must agr
| OT | two renditions the first request holds whole | bytes changed in the first response are not reused |
| OF | a `WAI1` naming a record beside a payload fallback | the union lists what SPEC §4 dispatches |
| OE1, OE2s, OE2t | an encrypted `WAI1`; `WAI2`s whose `security` is a string, `true` | SPEC §6.4 on both containers |
| OR | a refinement component whose `refines` is malformed | it refines nothing |
| OR | a refinement component whose `refines` is `{"base": "b"}`, the pick's id and nothing else | the envelope does not conform, and the refinement, whose `refines.base` read on its own names the pick, is reported dropped (`components_nonconforming`, §7 step 4) |
| OPf, OPe, OPw | a `WAI1` naming `wai.text.zstd` whose `refines` is `false`, `{}`, and well formed | a payload carrying `refines` is an ordinary object: dispatched, described by its capability, `not_a_layer` when listed as a layer (§10.4) |
| ORN | an AV1 base most of the object and a refinement component stating no `sha256` | the refinement dropped (`component_digest_absent`) on the whole path and the range path alike |
| ONC, OCF | a base most of the object, an HDR10+ companion, and a refinement with its `sha256`; the companion bound to nothing (ONC) or to the base's capability (OCF) | ONC does not conform, so its refinement is dropped (`components_nonconforming`) and never requested; every component of OCF conforms, so its refinement is fetched and delivered; on the whole path and the range path alike (§7 step 4) |
| ONM, ONX | ONC's base and refinement, the refinement's `refines` the base's id alone (ONM) or `x`, no entry's (ONX) | ONM's refinement is reported dropped (`components_nonconforming`); ONX's names no entry, so it is no refinement of the pick and is not reported |
| `bad/OK.wai`, `bad/OKN.wai` | a string key id; a numeric `kind` | the manifest's member types |
| O11a, O11b, O11bL1 | the staged-delivery pair of `../staged-conformance/cases/live_multi_base/`: two `WAI1` int_motion bases of two frames each, and a layer over the second | `#WAI-LAYER` after one segment only; the layer joined to its own base in the union; depth over a base with one layer |
| O12s0, O12s1, O12p0_00 – O12p1_11 | a low-latency set: two segments of 3.84 s, each its own object, and their twenty-four 0.32 s parts, each its own object that decodes alone | Live-P adjacency, parts listed under aliases with cids, the live revision chain, parts verified against the cid the first revision listing them lists |
@ -83,7 +85,8 @@ the same bytes.
The HDR10+ block, the AVC bases and the LCEVC enhancements are synthetic: they carry
the right start codes and decode to nothing, which transport does not need. OD's and
OT's payloads are byte patterns, as are OE2s', OE2t's, OR's and ORN's. Every other payload is real: the integer renditions are
OT's payloads are byte patterns, as are OE2s', OE2t's, OR's, ORN's, ONC's, OCF's,
ONM's and ONX's. Every other payload is real: the integer renditions are
the committed vectors; the images and the AV1 streams are in `src/`, encoded once
outside this repository, and the AV1 streams were checked to decode.
@ -163,7 +166,9 @@ fails the self-test; `base-at-done`, `tick-noop` and `refinement-unclipped` fail
`timed_*` cases; `pdt-ignored`, `dseq-ignored`, `parts-after-extinf`, `average-floor`,
`render-digest-off`, `name-unchecked`, `chain-unchecked`, `ilog2-no-interp`,
`bola-waits`, `unlabelled-as-zero` and `round-half-down` fail the playlist, receipt and
adaptation cases they reach), and the job checks a differential: `wai_http_vectors diff`
adaptation cases they reach; `nonconforming-offered`, `conforming-withheld` and
`refines-read-whole` fail the ORN, ONC, OCF, ONM and OR fetches; `falsify/run.sh` runs
every one), and the job checks a differential: `wai_http_vectors diff`
writes random envelopes' covered prefixes and proofs, `http_verify.py diff` verifies
every one, and, with one chaining-value byte changed in ten of them, refuses exactly
those ten.

View file

@ -57,6 +57,7 @@
{"name":"head_proof_ok_O9a","op":"head_proof","input":{"object":"O9a"},"expect":{"covered_len":1009,"n_cv":5,"envelope_len":21034}},
{"name":"head_proof_ok_O9b","op":"head_proof","input":{"object":"O9b"},"expect":{"covered_len":1009,"n_cv":5,"envelope_len":18004}},
{"name":"head_proof_ok_O9c","op":"head_proof","input":{"object":"O9c"},"expect":{"covered_len":1009,"n_cv":5,"envelope_len":18381}},
{"name":"head_proof_ok_OCF","op":"head_proof","input":{"object":"OCF"},"expect":{"covered_len":1009,"n_cv":6,"envelope_len":33015}},
{"name":"head_proof_ok_OD","op":"head_proof","input":{"object":"OD"},"expect":{"covered_len":1009,"n_cv":6,"envelope_len":53794}},
{"name":"head_proof_ok_OE1","op":"head_proof","input":{"object":"OE1"},"expect":{"covered_len":299,"n_cv":0,"envelope_len":299}},
{"name":"head_proof_ok_OE2s","op":"head_proof","input":{"object":"OE2s"},"expect":{"covered_len":1009,"n_cv":3,"envelope_len":5251}},
@ -64,6 +65,9 @@
{"name":"head_proof_ok_OF","op":"head_proof","input":{"object":"OF"},"expect":{"covered_len":283,"n_cv":0,"envelope_len":283}},
{"name":"head_proof_ok_OH","op":"head_proof","input":{"object":"OH"},"expect":{"covered_len":6129,"n_cv":3,"envelope_len":23519}},
{"name":"head_proof_ok_ON","op":"head_proof","input":{"object":"ON"},"expect":{"covered_len":1009,"n_cv":6,"envelope_len":38384}},
{"name":"head_proof_ok_ONC","op":"head_proof","input":{"object":"ONC"},"expect":{"covered_len":1009,"n_cv":6,"envelope_len":32978}},
{"name":"head_proof_ok_ONM","op":"head_proof","input":{"object":"ONM"},"expect":{"covered_len":1009,"n_cv":5,"envelope_len":32726}},
{"name":"head_proof_ok_ONX","op":"head_proof","input":{"object":"ONX"},"expect":{"covered_len":1009,"n_cv":5,"envelope_len":32726}},
{"name":"head_proof_ok_OPe","op":"head_proof","input":{"object":"OPe"},"expect":{"covered_len":264,"n_cv":0,"envelope_len":264}},
{"name":"head_proof_ok_OPf","op":"head_proof","input":{"object":"OPf"},"expect":{"covered_len":267,"n_cv":0,"envelope_len":267}},
{"name":"head_proof_ok_OPw","op":"head_proof","input":{"object":"OPw"},"expect":{"covered_len":421,"n_cv":0,"envelope_len":421}},
@ -276,7 +280,7 @@
{"name":"select_OE2s_security_string","op":"select","input":{"object":"OE2s","advertises":["wai.text.zstd"]},"expect":{"inert":[[0,"unsupported_encryption"]]}},
{"name":"select_OE2t_security_true","op":"select","input":{"object":"OE2t","advertises":["wai.text.zstd"]},"expect":{"inert":[[0,"unsupported_encryption"]]}},
{"name":"fetch_OE2s_security_string","op":"fetch","input":{"object":"OE2s","advertises":["wai.text.zstd"]},"expect":{"error":"inert","skipped":[[0,"unsupported_encryption"]]}},
{"name":"fetch_OR_malformed_refines","op":"fetch","input":{"object":"OR","advertises":["wai.video.int_motion"],"applies":["wai.video.int_refine"]},"expect":{"role":"base","pick":0,"verified_by":"head-proof","why_whole":null,"body_bytes":9615,"requests":2,"companions":[],"refinements":[],"abandoned":[],"dropped":[],"skipped":[],"component_sha256":"6b1bcc071f58c5fb62613d029be744e824494acf931a1c46d89641a548b4aa91"}},
{"name":"fetch_OR_malformed_refines","op":"fetch","input":{"object":"OR","advertises":["wai.video.int_motion"],"applies":["wai.video.int_refine"]},"expect":{"role":"base","pick":0,"verified_by":"head-proof","why_whole":null,"body_bytes":9615,"requests":2,"companions":[],"refinements":[],"abandoned":[],"dropped":[[1,"components_nonconforming"]],"skipped":[],"component_sha256":"6b1bcc071f58c5fb62613d029be744e824494acf931a1c46d89641a548b4aa91"}},
{"name":"union_OR_malformed_refines","op":"union_floor","input":{"objects":["OR"]},"expect":{"union":"wai2.video.int_motion","floor":"wai2.video.int_motion"}},
{"name":"parse_head_kid_string","op":"parse_head","input":{"file":"bad/OK.wai"},"expect":{"error":"head_malformed"}},
{"name":"parse_head_kind_number","op":"parse_head","input":{"file":"bad/OKN.wai"},"expect":{"error":"head_malformed"}},
@ -304,8 +308,16 @@
{"name":"fetch_OPw_listed_not_layer","op":"fetch","input":{"object":"OPw","listed_as_layer":true,"advertises":["wai.text.zstd"]},"expect":{"error":"not_a_layer"}},
{"name":"union_OPw","op":"union_floor","input":{"objects":["OPw"]},"expect":{"union":"wai1.text.zstd","floor":"wai1.text.zstd"}},
{"name":"fetch_OPw_declared","op":"fetch","input":{"object":"OPw","advertises":["wai.text.zstd"],"declared":"wai1.text.zstd"},"expect":{"role":"base","pick":0,"verified_by":"whole-object","why_whole":"single_component","body_bytes":474,"requests":2,"companions":[],"refinements":[],"abandoned":[],"dropped":[],"skipped":[],"component_sha256":"591f780863bd571c1b868efabdf64a30ff0734b22582dad3f89486d0a97663c1"}},
{"name":"fetch_ORN_whole","op":"fetch","input":{"object":"ORN","fragment":"#n=32659&h=1009","advertises":["wai.video.av1"],"applies":["wai.video.int_refine"],"threshold_permille":900},"expect":{"role":"base","pick":0,"verified_by":"whole-object","why_whole":"range_threshold","body_bytes":32872,"requests":3,"companions":[],"refinements":[],"abandoned":[],"dropped":[[1,"component_digest_absent"]],"skipped":[],"component_sha256":"7898322c774e80346b83405764069cbf1d06484da8824fab90e18e2ce0188f8c"}},
{"name":"fetch_ORN_ranged","op":"fetch","input":{"object":"ORN","fragment":"#n=32659&h=1009","advertises":["wai.video.av1"],"applies":["wai.video.int_refine"],"threshold_permille":1001},"expect":{"role":"base","pick":0,"verified_by":"head-proof","why_whole":null,"body_bytes":30572,"requests":3,"companions":[],"refinements":[],"abandoned":[],"dropped":[[1,"component_digest_absent"]],"skipped":[],"component_sha256":"7898322c774e80346b83405764069cbf1d06484da8824fab90e18e2ce0188f8c"}},
{"name":"fetch_ORN_whole","op":"fetch","input":{"object":"ORN","fragment":"#n=32659&h=1009","advertises":["wai.video.av1"],"applies":["wai.video.int_refine"],"threshold_permille":900},"expect":{"role":"base","pick":0,"verified_by":"whole-object","why_whole":"range_threshold","body_bytes":32872,"requests":3,"companions":[],"refinements":[],"abandoned":[],"dropped":[[1,"component_digest_absent"],[1,"components_nonconforming"]],"skipped":[],"component_sha256":"7898322c774e80346b83405764069cbf1d06484da8824fab90e18e2ce0188f8c"}},
{"name":"fetch_ORN_ranged","op":"fetch","input":{"object":"ORN","fragment":"#n=32659&h=1009","advertises":["wai.video.av1"],"applies":["wai.video.int_refine"],"threshold_permille":1001},"expect":{"role":"base","pick":0,"verified_by":"head-proof","why_whole":null,"body_bytes":30572,"requests":3,"companions":[],"refinements":[],"abandoned":[],"dropped":[[1,"component_digest_absent"],[1,"components_nonconforming"]],"skipped":[],"component_sha256":"7898322c774e80346b83405764069cbf1d06484da8824fab90e18e2ce0188f8c"}},
{"name":"fetch_ONC_whole","op":"fetch","input":{"object":"ONC","fragment":"#n=32978&h=1009","advertises":["wai.video.int_hyper"],"applies":["wai.video.int_refine"],"threshold_permille":900},"expect":{"role":"base","pick":0,"verified_by":"whole-object","why_whole":"range_threshold","body_bytes":33223,"requests":3,"companions":[],"refinements":[],"abandoned":[],"dropped":[[2,"components_nonconforming"]],"skipped":[],"component_sha256":"49bb512e4d956426f1b3fa3f029bb60fe7bdfdb1ad8b2bb47bdd9c8ad6c9dc3a"}},
{"name":"fetch_ONC_ranged","op":"fetch","input":{"object":"ONC","fragment":"#n=32978&h=1009","advertises":["wai.video.int_hyper"],"applies":["wai.video.int_refine"],"threshold_permille":1001},"expect":{"role":"base","pick":0,"verified_by":"head-proof","why_whole":null,"body_bytes":30859,"requests":3,"companions":[],"refinements":[],"abandoned":[],"dropped":[[2,"components_nonconforming"]],"skipped":[],"component_sha256":"49bb512e4d956426f1b3fa3f029bb60fe7bdfdb1ad8b2bb47bdd9c8ad6c9dc3a"}},
{"name":"fetch_OCF_whole","op":"fetch","input":{"object":"OCF","fragment":"#n=33015&h=1009","advertises":["wai.video.int_hyper"],"applies":["wai.video.int_refine"],"threshold_permille":900},"expect":{"role":"base","pick":0,"verified_by":"whole-object","why_whole":"range_threshold","body_bytes":33260,"requests":3,"companions":[],"refinements":[2],"abandoned":[],"dropped":[],"skipped":[],"component_sha256":"49bb512e4d956426f1b3fa3f029bb60fe7bdfdb1ad8b2bb47bdd9c8ad6c9dc3a"}},
{"name":"fetch_OCF_ranged","op":"fetch","input":{"object":"OCF","fragment":"#n=33015&h=1009","advertises":["wai.video.int_hyper"],"applies":["wai.video.int_refine"],"threshold_permille":1001},"expect":{"role":"base","pick":0,"verified_by":"head-proof","why_whole":null,"body_bytes":33196,"requests":4,"companions":[],"refinements":[2],"abandoned":[],"dropped":[],"skipped":[],"component_sha256":"49bb512e4d956426f1b3fa3f029bb60fe7bdfdb1ad8b2bb47bdd9c8ad6c9dc3a"}},
{"name":"fetch_ONM_whole","op":"fetch","input":{"object":"ONM","fragment":"#n=32726&h=1009","advertises":["wai.video.int_hyper"],"applies":["wai.video.int_refine"],"threshold_permille":900},"expect":{"role":"base","pick":0,"verified_by":"whole-object","why_whole":"range_threshold","body_bytes":32939,"requests":3,"companions":[],"refinements":[],"abandoned":[],"dropped":[[1,"components_nonconforming"]],"skipped":[],"component_sha256":"49bb512e4d956426f1b3fa3f029bb60fe7bdfdb1ad8b2bb47bdd9c8ad6c9dc3a"}},
{"name":"fetch_ONM_ranged","op":"fetch","input":{"object":"ONM","fragment":"#n=32726&h=1009","advertises":["wai.video.int_hyper"],"applies":["wai.video.int_refine"],"threshold_permille":1001},"expect":{"role":"base","pick":0,"verified_by":"head-proof","why_whole":null,"body_bytes":30639,"requests":3,"companions":[],"refinements":[],"abandoned":[],"dropped":[[1,"components_nonconforming"]],"skipped":[],"component_sha256":"49bb512e4d956426f1b3fa3f029bb60fe7bdfdb1ad8b2bb47bdd9c8ad6c9dc3a"}},
{"name":"fetch_ONX_whole","op":"fetch","input":{"object":"ONX","fragment":"#n=32726&h=1009","advertises":["wai.video.int_hyper"],"applies":["wai.video.int_refine"],"threshold_permille":900},"expect":{"role":"base","pick":0,"verified_by":"whole-object","why_whole":"range_threshold","body_bytes":32939,"requests":3,"companions":[],"refinements":[],"abandoned":[],"dropped":[],"skipped":[],"component_sha256":"49bb512e4d956426f1b3fa3f029bb60fe7bdfdb1ad8b2bb47bdd9c8ad6c9dc3a"}},
{"name":"fetch_ONX_ranged","op":"fetch","input":{"object":"ONX","fragment":"#n=32726&h=1009","advertises":["wai.video.int_hyper"],"applies":["wai.video.int_refine"],"threshold_permille":1001},"expect":{"role":"base","pick":0,"verified_by":"head-proof","why_whole":null,"body_bytes":30639,"requests":3,"companions":[],"refinements":[],"abandoned":[],"dropped":[],"skipped":[],"component_sha256":"49bb512e4d956426f1b3fa3f029bb60fe7bdfdb1ad8b2bb47bdd9c8ad6c9dc3a"}},
{"name":"fetch_OS_threshold_stall","op":"fetch","input":{"advertises":["wai.video.avc"],"applies":["wai.video.lcevc"],"fragment":"#n=38460&h=1009","object":"OS","tamper":{"kind":"no_refinement"}},"expect":{"role":"base","pick":0,"verified_by":"head-proof","why_whole":null,"body_bytes":8705,"requests":4,"companions":[],"refinements":[],"abandoned":[1],"dropped":[],"skipped":[],"component_sha256":"e93f5c55dc5938a8cadd00477e4110d567d4a725b647a3cddf0c70c5e59dcd18"}},
{"name":"fetch_OS_threshold_stall_1001","op":"fetch","input":{"advertises":["wai.video.avc"],"applies":["wai.video.lcevc"],"fragment":"#n=38460&h=1009","object":"OS","tamper":{"kind":"no_refinement"},"threshold_permille":1001},"expect":{"role":"base","pick":0,"verified_by":"head-proof","why_whole":null,"body_bytes":8705,"requests":4,"companions":[],"refinements":[],"abandoned":[1],"dropped":[],"skipped":[],"component_sha256":"e93f5c55dc5938a8cadd00477e4110d567d4a725b647a3cddf0c70c5e59dcd18"}},
{"name":"fetch_OS_full","op":"fetch","input":{"advertises":["wai.video.avc"],"applies":["wai.video.lcevc"],"fragment":"#n=38460&h=1009","object":"OS"},"expect":{"role":"base","pick":0,"verified_by":"head-proof","why_whole":null,"body_bytes":38705,"requests":4,"companions":[1],"refinements":[],"abandoned":[],"dropped":[],"skipped":[],"component_sha256":"e93f5c55dc5938a8cadd00477e4110d567d4a725b647a3cddf0c70c5e59dcd18"}},

View file

@ -0,0 +1,15 @@
# must fail: fetch_OCF_whole fetch_OCF_ranged
# the plan withholds the refinements of an envelope every component of which conforms (session::nonconforming_refinements)
diff --git a/wai/wai-rs/src/http_binding/session.rs b/wai/wai-rs/src/http_binding/session.rs
index fd00472d..24c7063d 100644
--- a/wai/wai-rs/src/http_binding/session.rs
+++ b/wai/wai-rs/src/http_binding/session.rs
@@ -205,7 +205,7 @@ impl Selector {
/// conform (SPEC §7.1, `ManifestV2::check_component_roles` over the head's manifest) does
/// not offer: every one of them where it does not conform, none where it does.
fn nonconforming_refinements(m: &ManifestV2, wanted: &[(usize, Class)]) -> Vec<usize> {
- if m.check_component_roles().is_ok() {
+ if false && m.check_component_roles().is_ok() {
return Vec::new();
}
wanted.iter().map(|&(i, _)| i).filter(|&i| m.renditions.get(i).is_some_and(|e| e.is_refinement())).collect()

View file

@ -0,0 +1,15 @@
# must fail: fetch_ONC_whole fetch_ONC_ranged fetch_ORN_whole fetch_ORN_ranged fetch_ONM_whole fetch_ONM_ranged fetch_OR_malformed_refines
# the plan offers the refinements of an envelope whose components do not all conform (session::nonconforming_refinements)
diff --git a/wai/wai-rs/src/http_binding/session.rs b/wai/wai-rs/src/http_binding/session.rs
index 32c85293..2f462787 100644
--- a/wai/wai-rs/src/http_binding/session.rs
+++ b/wai/wai-rs/src/http_binding/session.rs
@@ -201,7 +201,7 @@ impl Selector {
/// conform (SPEC §7.1, `ManifestV2::check_component_roles` over the head's manifest) does
/// not offer: every one of them where it does not conform, none where it does.
fn nonconforming_refinements(m: &ManifestV2, wanted: &[(usize, Class)]) -> Vec<usize> {
- if m.check_component_roles().is_ok() {
+ if true || m.check_component_roles().is_ok() {
return Vec::new();
}
wanted.iter().map(|&(i, _)| i).filter(|&i| m.renditions.get(i).is_some_and(|e| e.is_refinement())).collect()

View file

@ -0,0 +1,15 @@
# must fail: fetch_ONM_whole fetch_ONM_ranged fetch_OR_malformed_refines
# the plan finds a refinement only by a well-formed refines, so one naming the pick with the rest malformed is dropped unreported (Selector::applied)
diff --git a/wai/wai-rs/src/http_binding/session.rs b/wai/wai-rs/src/http_binding/session.rs
index fd00472d..55d75f69 100644
--- a/wai/wai-rs/src/http_binding/session.rs
+++ b/wai/wai-rs/src/http_binding/session.rs
@@ -196,7 +196,7 @@ impl Selector {
.filter(|&i| (self.applies)(&e[i]))
.map(|i| (i, if crate::codecs::is_enhancement_layer(&e[i].capability) { Class::Refinement } else { Class::Base }))
.collect();
- out.extend(refinements_naming(e, index).into_iter().filter(|&i| (self.applies)(&e[i])).map(|i| (i, Class::Refinement)));
+ out.extend(crate::container::refinement_indices(e, index).into_iter().filter(|&i| (self.applies)(&e[i])).map(|i| (i, Class::Refinement)));
out
}
}

View file

@ -53,7 +53,7 @@ for m in left-half no-root counter-zero floor-as-codecs blake3-iv base-at-done t
unlabelled-escapes abr-unbounded gap-carries receipt-duplicates receipt-surrogates session-data-anywhere \
dash-r-expanded xml-attributes-scanned dash-timelines-unbounded budget-raises \
uri-unresolved gap-withdraws reanchor-backwards walk-exceeded-wedges walk-name-unchecked walk-verify-unchecked bindings-unbounded \
file-numbers-unbounded; do
file-numbers-unbounded nonconforming-offered conforming-withheld refines-read-whole; do
if python3 http_verify.py . --mutate "$m" > "$WORK/py-$m.log" 2>&1; then
echo "::error::http_verify.py --mutate $m passed"; fail=1
else

View file

@ -1,15 +1,15 @@
# must fail: fetch_ORN_whole
# the whole path applies a refinement that states no digest (session::FetchSession::advance_rest)
# the whole path drops a refinement that states no digest without its own reason (session::FetchSession::advance_rest)
diff --git a/wai/wai-rs/src/http_binding/session.rs b/wai/wai-rs/src/http_binding/session.rs
index 47a906ec..c65b2871 100644
index 32c85293..bc971a72 100644
--- a/wai/wai-rs/src/http_binding/session.rs
+++ b/wai/wai-rs/src/http_binding/session.rs
@@ -816,7 +816,7 @@ impl FetchSession {
for (i, class) in self.sel.applied(&w.manifest, index) {
// A refinement that states no digest is dropped on this path as on the
// range path (§7 step 5, §10.3).
- if class == Class::Refinement && w.manifest.renditions[i].sha256.is_none() {
+ if false && class == Class::Refinement && w.manifest.renditions[i].sha256.is_none() {
dropped.push((i, WhyWhole::DigestAbsent { index: i }));
@@ -871,7 +871,7 @@ impl FetchSession {
// §7 step 4: not offered from an envelope that does not conform; one stating
// no digest is dropped for that too, as on the range path.
if nonconforming.contains(&i) {
- if w.manifest.renditions[i].sha256.is_none() {
+ if false && w.manifest.renditions[i].sha256.is_none() {
dropped.push((i, WhyWhole::DigestAbsent { index: i }));
}
continue;
}

View file

@ -22,7 +22,8 @@ stops the verifier, never passes.
python3 http_verify.py [dir] verify every case and the self-test
python3 http_verify.py [dir] --mutate NAME the same with a deliberate defect:
left-half | no-root | counter-zero | floor-as-codecs | blake3-iv | base-at-done |
tick-noop | refinement-unclipped
tick-noop | refinement-unclipped | nonconforming-offered | conforming-withheld |
refines-read-whole | ... (falsify/run.sh runs every one)
python3 http_verify.py cases FILE [dir]
give every case of a case file (wai_http_vectors fuzz) its stated outcome
python3 http_verify.py diff FILE [--flip N --seed S]
@ -404,6 +405,22 @@ def refinements_of(entries, i):
return out
def refinements_naming(entries, i):
"""The refinement components whose `refines.base`, read on its own, is entry i's id:
refinements_of without the rest of `refines` read. Of a conforming envelope the two
agree; of one that does not, these are the refinements a plan drops and reports
(components_nonconforming, http-delivery §7 step 4), a malformed `refines` naming the
pick among them."""
if MUTATE == "refines-read-whole":
return refinements_of(entries, i)
bid = entries[i].get("id")
if bid is None:
return []
return [j for j, e in enumerate(entries) if e.get("role") == "refinement"
and isinstance(e.get("refines"), dict) and isinstance(e["refines"].get("base"), str)
and e["refines"]["base"] == bid]
def payload_ok(e, payload):
return e.get("sha256") is None or e["sha256"] == sha(payload)
@ -909,11 +926,20 @@ class Session:
if cov < n and self.have(cov, n) is None:
self.queue(("rest", (cov, n - 1), cl))
def nonconforming(self, entries, wanted):
"""§7 step 4: the refinement components among `wanted` of an envelope whose
components do not all conform (SPEC §7.1) — every one of them where it does not,
none where it does."""
conforms = SV.component_roles(entries) is None and MUTATE != "conforming-withheld"
if conforms or MUTATE == "nonconforming-offered":
return []
return [i for i, _ in wanted if entries[i].get("role") == "refinement"]
def applied(self, entries, pick):
adv, applies = self.sink[0], self.sink[1]
out = [(j, "refinement" if enhancement(entries[j]["capability"]) else "base")
for j in companions_of(entries, pick) if entries[j]["capability"] in applies]
out += [(j, "refinement") for j in refinements_of(entries, pick) if entries[j]["capability"] in applies]
out += [(j, "refinement") for j in refinements_naming(entries, pick) if entries[j]["capability"] in applies]
return out
def advance_head(self):
@ -945,13 +971,15 @@ class Session:
ordr = order(head) if callable(order) else (order if order is not None else list(range(len(entries))))
pick, skipped = select(head["manifest"], ordr, adv, held_sets, lambda i, e: True)
wanted = [(pick, "base")] + self.applied(entries, pick)
pl = plan(head, wanted, [(a, b) for a, b, _ in self.held], self.permille)
nc = self.nonconforming(entries, wanted)
planned = [(i, c) for i, c in wanted if i not in nc or entries[i].get("sha256") is None]
pl = plan(head, planned, [(a, b) for a, b, _ in self.held], self.permille)
if pl[0] == "rest":
self.to_rest(pl[2], "base")
else:
for (a, b, cl) in pl[1]:
self.queue(("range", (a, b), cl))
self.dropped = pl[2]
self.dropped = pl[2] + [[i, "components_nonconforming"] for i in nc]
gone = [i for i, _ in pl[2]]
self.stage = ("components", pick, skipped, [(i, c) for i, c in wanted if i not in gone])
if self.stage[0] == "components":
@ -1040,7 +1068,15 @@ class Session:
ordr = order(head) if callable(order) else (order if order is not None else list(range(len(entries))))
pick, skipped = select(m, ordr, adv, held_sets, lambda i, e: payload_ok(e, pay[i]))
comps, refs, dropped = [], [], []
for j, cl in self.applied(entries, pick):
applied = self.applied(entries, pick)
nc = self.nonconforming(entries, applied)
for j, cl in applied:
# §7 step 4: not offered from an envelope that does not conform; one stating no
# digest is dropped for that too, as on the range path.
if j in nc:
if entries[j].get("sha256") is None:
dropped.append([j, "component_digest_absent"])
continue
# A refinement stating no digest is dropped here as on the range path (§7 step 5).
if cl == "refinement" and entries[j].get("sha256") is None:
dropped.append([j, "component_digest_absent"])
@ -1050,6 +1086,7 @@ class Session:
dropped.append([j, "component_digest_mismatch"])
continue
(comps if entries[j].get("role") == "companion" else refs).append(j)
dropped += [[j, "components_nonconforming"] for j in nc]
self.finish(self.fetched("base", pick, pay[pick], comps, refs, [], dropped, skipped, "whole-object", why))

View file

@ -223,6 +223,38 @@
"codecs": "wai2.video.av1+video.int_refine",
"what": "WAI2: an AV1 base and a refinement stating no sha256"
},
"ONC": {
"cid": "2c15a9ca86337508733305f81ef5e193e0796d61e870ccc7abf8af92334c98a2",
"path": "objects/2c15a9ca86337508733305f81ef5e193e0796d61e870ccc7abf8af92334c98a2.wai",
"n": 32978,
"h": 1009,
"codecs": "wai2.video.int_hyper+video.int_refine",
"what": "WAI2: a base, a companion bound to nothing, and a refinement with its sha256"
},
"OCF": {
"cid": "4e89b2896be32390caaa435b09f7ae0e448103b03b534c20a4a173c735197f9d",
"path": "objects/4e89b2896be32390caaa435b09f7ae0e448103b03b534c20a4a173c735197f9d.wai",
"n": 33015,
"h": 1009,
"codecs": "wai2.video.int_hyper+meta.hdr10plus+video.int_refine",
"what": "ONC with its companion bound to the base's capability: every component conforms"
},
"ONM": {
"cid": "b36600ee5a7498d8c8246b8440d072b8e6a4cdaee317fc6dfa7ca29841413d90",
"path": "objects/b36600ee5a7498d8c8246b8440d072b8e6a4cdaee317fc6dfa7ca29841413d90.wai",
"n": 32726,
"h": 1009,
"codecs": "wai2.video.int_hyper",
"what": "WAI2: a base and a refinement whose refines is {\"base\": \"b\"}, the base's id and nothing else"
},
"ONX": {
"cid": "48be900eeba898a279b604db9e839ad6486ebb9dcdc521d11413db99ad1b4b94",
"path": "objects/48be900eeba898a279b604db9e839ad6486ebb9dcdc521d11413db99ad1b4b94.wai",
"n": 32726,
"h": 1009,
"codecs": "wai2.video.int_hyper",
"what": "ONM with refines {\"base\": \"x\"}, naming no entry"
},
"O11a": {
"cid": "8f3a701e2f904ad90663466c2c12cafed3410eeb84ad41db82acdc8f9a10adf7",
"path": "objects/8f3a701e2f904ad90663466c2c12cafed3410eeb84ad41db82acdc8f9a10adf7.wai",

View file

@ -0,0 +1 @@
"2c15a9ca86337508733305f81ef5e193e0796d61e870ccc7abf8af92334c98a2"

View file

@ -0,0 +1 @@
"48be900eeba898a279b604db9e839ad6486ebb9dcdc521d11413db99ad1b4b94"

View file

@ -0,0 +1 @@
"4e89b2896be32390caaa435b09f7ae0e448103b03b534c20a4a173c735197f9d"

View file

@ -0,0 +1 @@
"b36600ee5a7498d8c8246b8440d072b8e6a4cdaee317fc6dfa7ca29841413d90"

View file

@ -526,6 +526,134 @@
],
"roles": "CompanionOnlyAsRendition"
}
},
{
"name": "record_as_companion",
"manifest": {
"wai": "1.1",
"media": "video",
"intent": "replicate",
"renditions": [
{
"capability": "wai.video.av1",
"kind": "av1",
"id": "a"
},
{
"capability": "wai.video.manifest",
"kind": "k",
"role": "companion",
"companion_of": "wai.video.av1"
}
],
"target": null
},
"payloads": [
"AV1",
"REC"
],
"advertised": "all",
"held": [],
"expect": {
"picked": 0,
"skipped": [],
"roles": "RecordOrDerivationAsComponent"
}
},
{
"name": "derivation_as_companion",
"manifest": {
"wai": "1.1",
"media": "video",
"intent": "replicate",
"renditions": [
{
"capability": "wai.video.av1",
"kind": "av1",
"id": "a"
},
{
"capability": "wai.derive.crop",
"kind": "k",
"role": "companion",
"companion_of": "wai.video.av1"
}
],
"target": null
},
"payloads": [
"AV1",
"REC"
],
"advertised": "all",
"held": [],
"expect": {
"picked": 0,
"skipped": [],
"roles": "RecordOrDerivationAsComponent"
}
},
{
"name": "record_as_rendition",
"manifest": {
"wai": "1.1",
"media": "video",
"intent": "replicate",
"renditions": [
{
"capability": "wai.video.av1",
"kind": "av1",
"id": "a"
},
{
"capability": "wai.video.manifest",
"kind": "k"
}
],
"target": null
},
"payloads": [
"AV1",
"REC"
],
"advertised": "all",
"held": [],
"expect": {
"picked": 0,
"skipped": [],
"roles": "RecordOrDerivationAsComponent"
}
},
{
"name": "derivation_as_rendition",
"manifest": {
"wai": "1.1",
"media": "video",
"intent": "replicate",
"renditions": [
{
"capability": "wai.video.av1",
"kind": "av1",
"id": "a"
},
{
"capability": "wai.derive.crop",
"kind": "k"
}
],
"target": null
},
"payloads": [
"AV1",
"REC"
],
"advertised": "all",
"held": [],
"expect": {
"picked": 0,
"skipped": [],
"roles": "RecordOrDerivationAsComponent"
}
}
]
}

View file

@ -4278,7 +4278,7 @@
"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\"}",
"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]\",\"first_unit\":0,\"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\":\"3c551db8e32d45ad49aeb49a18598a5b2f70fdeaf2869cc9e76bdcfc7eafd0a597fe564b9742286ed56d2fd482fe07b73230f3c34cea4ffcc9e6566d15964a0c\",\"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}",
@ -4301,7 +4301,7 @@
"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\"}",
"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]\",\"first_unit\":0,\"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\":\"88b3e76214d4522aed4f0af165add4c9486d05fab0485f7eccff6fc02cd29d569d5bcdbaa29cb1ca5ad0cc579f962c5a68c31b0dd71fb2030425f484c591b301\",\"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}",
@ -4324,7 +4324,7 @@
"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\"}",
"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]\",\"first_unit\":0,\"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\":\"203dd7a51dc303e44737c322b507979096a0736caa227f67545df8fb919191e5c18c084afc33e9d3151864a3d678bead38f3e81f93eaf97d89d03a3ef259540c\",\"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}",
@ -4347,7 +4347,7 @@
"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\"}",
"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]\",\"first_unit\":0,\"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\":\"1e04120e4379d1483b6bc02ecdfae24c1667f1aab8927e4d57b68a04aaabffe80be8d73a3ad0a1a222af6a911c90fa2a7c3421e963e65806c50519bfe05dbf08\",\"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}",
@ -4370,7 +4370,7 @@
"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\"}",
"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]\",\"first_unit\":0,\"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\":\"1e04120e4379d1483b6bc02ecdfae24c1667f1aab8927e4d57b68a04aaabffe80be8d73a3ad0a1a222af6a911c90fa2a7c3421e963e65806c50519bfe05dbf08\",\"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}",
@ -4393,7 +4393,7 @@
"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\"}",
"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]\",\"first_unit\":0,\"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\":\"3a77dd4376dd32d34c744c0ffbc57aa7f8e088c2387bd51175ce9691c2061662e501baf0e0ee2ea44a6cf62bffbeaa0ec182a8abefe36b746bb58fd1590b6d09\",\"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}",

View file

@ -16,6 +16,11 @@ applied, held and refused (a unit held and decided within the same step is repor
its decision); `held_for_base` marks an envelope held for a base the sink has not
decoded. The order of a step's reports is not part of the outcome.
**Offers.** A case's `offer` step hands its envelope to §4 as a layer, as a caller of a
sink's offer does. A sink routes an envelope by its capability (§3), so it never offers
one whose capability is not refinement-only: the case `capability_not_refinement` tests
that caller-offers path, which §4.1 step 1 refuses (`capability-not-refinement`).
**Evidence.** Each offer states what the sink verified of the layer's delivery receipt
(§10.1), as a token: `delivered-verified`, a receipt verified through jwp-receipts §3
step 6 over the envelope's bytes as received; `inclusion-verified`, a receipt verified

View file

@ -1,7 +1,7 @@
{
"format": "wai-staged-case/1",
"name": "capability_not_refinement",
"about": "refines on a wai.video.int_motion envelope: capability-not-refinement",
"about": "refines on a wai.video.int_motion envelope, offered to §4 as a layer by a caller (a sink routes it by its capability, as a base, and never offers it): capability-not-refinement",
"bases": [
{
"file": "base.wai",

View file

@ -216,7 +216,7 @@
},
{
"name": "capability_not_refinement",
"about": "refines on a wai.video.int_motion envelope: capability-not-refinement",
"about": "refines on a wai.video.int_motion envelope, offered to §4 as a layer by a caller (a sink routes it by its capability, as a base, and never offers it): capability-not-refinement",
"clip_sha256": [
"39d743f21b782cc4ea1d1bacb50e0244af72459a21746ef19e6571a9a712c3ad"
]

View file

@ -2163,5 +2163,160 @@ def main(d):
return 1 if fails else 0
# ------------------------------------------------- SPEC §5 / §7.1's component rules
_REGISTRY = None
def _registry():
"""The capability registry (../registry/capabilities.json), by capability."""
global _REGISTRY
if _REGISTRY is None:
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"]}
return _REGISTRY
def _reg_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 _companion_only(cap):
c = _registry().get(cap)
return c is not None and c["kind"] == "companion"
_VIDEO_OUTSIDE_NAMESPACE = {"wai.neural.video_bmshj2018", "wai.neural.dcvc_rt", "wai.neural.dcvc_fm", "wai.film.linear", "wai.film.reel"}
_ENHANCEMENT = {"wai.video.lcevc": {"wai.video.avc", "wai.video.hevc", "wai.video.vvc", "wai.video.evc", "wai.video.evc.baseline",
"wai.video.vp9", "wai.video.av1", "wai.video.av1.lossless"}}
def _known(cap):
"""A capability the reference's component rules know (`codecs::is_known_capability`):
a registered payload, companion or refinement; a record or a derivation is not one."""
c = _registry().get(cap)
return c is not None and c["kind"] in ("payload", "companion", "refinement")
def _video_class(cap):
return _known(cap) and ((cap.startswith("wai.video.") and not _companion_only(cap) and cap not in UNIT_KIND)
or cap in _VIDEO_OUTSIDE_NAMESPACE)
def _companion_applies_to(companion, base):
if _companion_only(base) or base in UNIT_KIND:
return False
if companion in ("wai.meta.hdr10plus", "wai.meta.dovi"):
return not _known(base) or _video_class(base)
return base in _ENHANCEMENT.get(companion, ())
def component_roles(rs):
"""SPEC §5 / §7.1's component rules over a WAI2 table, in the order
`WaiMulti::check_component_roles` reads them: None where every component conforms,
else the first break's code. A sink offers the envelope's refinements only when it
conforms."""
by_id = lambda x: next((j for j, y in enumerate(rs) if y.get("id") == x), None) # noqa: E731
role = lambda e: e.get("role") # noqa: E731
for i, m in enumerate(rs):
if m.get("id") is not None:
if not is_component_id(m["id"]):
return "component-id-malformed"
if any(e.get("id") == m["id"] for e in rs[:i]):
return "duplicate-component-id"
def refinement(i):
m = rs[i]
if m["capability"] not in UNIT_KIND:
return "refinement-not-refinement-only"
if m.get("id") is None or m.get("sha256") is None:
return "refinement-identity-missing"
if m.get("refines") is None:
return "refines-malformed"
try:
ref = parse_refines_ids(m["refines"])
except Malformed:
return "refines-malformed"
b = by_id(ref["base"])
if b is None or not _reg_selectable(rs[b]):
return "refinement-base-missing"
if rs[b].get("sha256") is None:
return "refinement-identity-missing"
if rs[b]["capability"] not in ADMIT.get(m["capability"], ()):
return "refinement-base-not-admitted"
for p, _ in ref["runs"]:
e = by_id(p)
if e is None or not (_reg_selectable(rs[e]) or (e < i and role(rs[e]) == "refinement")):
return "refinement-prev-missing"
if rs[e].get("sha256") is None:
return "refinement-identity-missing"
if any(_reg_selectable(rs[j]) for j in range(i + 1, len(rs))):
return "refinement-before-selectable"
return None
def bound_base(i):
m = rs[i]
if m.get("companion_of_id") is not None:
return by_id(m["companion_of_id"])
t = m.get("companion_of")
if t is None:
return None
hits = [j for j, b in enumerate(rs) if _reg_selectable(b) and b["capability"] == t]
return hits[0] if len(hits) == 1 else None
for i, m in enumerate(rs):
cap = m["capability"]
# A record or a derivation names no payload format: no component in any role.
if _registry().get(cap, {}).get("kind") in ("record", "derivation"):
return "record-or-derivation-as-component"
if role(m) == "refinement":
why = refinement(i)
if why:
return why
continue
if m.get("refines") is not None:
return "refines-on-non-refinement"
if cap in UNIT_KIND:
return "refinement-only-as-component"
co = _companion_only(cap)
if role(m) != "companion":
if co:
return "companion-only-as-rendition"
continue
if not co and cap in _registry(): # a payload: standalone media (a record or a derivation was refused above)
return "standalone-as-companion"
target = m.get("companion_of")
if target is None:
return "unbound-companion"
if co and not _companion_applies_to(cap, target):
return "incompatible-base"
if m.get("companion_of_id") is not None:
b = by_id(m["companion_of_id"])
if b is None:
return "companion-id-unresolved"
if rs[b]["capability"] != target:
return "companion-id-capability-mismatch"
if not _reg_selectable(rs[b]):
return "unbound-companion"
else:
n = sum(1 for e in rs if _reg_selectable(e) and e["capability"] == target)
if cap in _ENHANCEMENT:
if n != 1:
return "enhancement-base-not-unique"
elif n == 0:
return "unbound-companion"
if cap in _ENHANCEMENT:
bi = bound_base(i)
if any(role(rs[e]) == "companion" and rs[e]["capability"] in _ENHANCEMENT and bound_base(e) is not None
and bound_base(e) == bi for e in range(i)):
return "duplicate-enhancement-layer"
return None
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else HERE))

View file

@ -32,7 +32,10 @@ identity (Appendix D). Across the corpus: the record-wide deduplication totals (
capability (`no-format-check`); `WIH1` under a `wai.video.int_motion` fallback
(`format_capability_mismatch`) and under a fallback WAI checks no format for
(counted); a `WAI2` component with another payload's `sha256` (`sha256-mismatch`); an
encrypted payload; and bytes after an envelope (`trailing`).
encrypted payload; bytes after an envelope (`trailing`); and a `WAI2` whose second
rendition, PNG, is its classical rendition (`no-format-check`), with its encrypted twin
(`encrypted`), which a session receiving either states the classical length of
(jwp-receipts §2.4.12).
- `refusals/<name>/title.json` — titles a tool must refuse, each with the code it states
(`refused`): a base whose units pass the chain budget its title states
(`chain-over-limit`, over the staged-delivery corpus's `base_over_budget` case); the

View file

@ -105,6 +105,18 @@
"counted": null,
"refused": "trailing",
"component": null
},
{
"file": "staged-measure-conformance/counting/wai2-classical.wai",
"counted": null,
"refused": "no-format-check",
"component": 1
},
{
"file": "staged-measure-conformance/counting/wai2-classical-encrypted.wai",
"counted": null,
"refused": "encrypted",
"component": null
}
]
}

View file

@ -207,9 +207,11 @@ telemetry = []
# §2.6, §3.1–§3.2): `claim` receipts of class energy / lawful-basis / log-inclusion,
# the soft-binding class, and recovery under the ancestor rule. Needs `provenance`
# for the coverage vocabulary and `oer` for the evidential grades and the OER receipt
# an energy claim may name, which it reads and verifies through `crate::oer`. Its one
# direct dependency, curve25519-dalek, is already in the graph under ed25519-dalek.
receipt_claims = ["provenance", "oer", "jwp_hash", "dep:curve25519-dalek"]
# an energy claim may name, which it reads and verifies through `crate::oer`; and
# `staged`, so a session verifier holding envelopes counts a refinement layer's `WIR1`
# payload (staged-measurement §3) and reads its `refines`. Its one direct dependency,
# curve25519-dalek, is already in the graph under ed25519-dalek.
receipt_claims = ["provenance", "oer", "jwp_hash", "staged", "dep:curve25519-dalek"]
# Key discovery and signing authority (extensions/jwp-receipts.md §7): the origin key
# log a `did:web` signer_id names, anchoring, the confirmation reported beside a receipt's
# or claim's verdict, and the authorisation and revocation of decoder and reconstruct

View file

@ -26,18 +26,17 @@ fn main() {
}
/// Record the build for a measurement report (`extensions/energy-measurement.md`
/// §5, §8): the profile, from cargo's `PROFILE` and `OPT_LEVEL`
/// (`src/build_info.rs`), the target triple, and the compiler's version line.
/// §5, §8): the profile, from cargo's `OPT_LEVEL` (`src/build_info.rs`), the
/// target triple, and the compiler's version line.
/// A part that cannot be determined is left unset and reads as `null`.
fn record_build() {
println!("cargo:rerun-if-env-changed=PROFILE");
println!("cargo:rerun-if-env-changed=OPT_LEVEL");
println!("cargo:rerun-if-env-changed=TARGET");
println!("cargo:rerun-if-env-changed=RUSTC");
let var = |k: &str| std::env::var(k).unwrap_or_default();
println!(
"cargo:rustc-env=WAI_BUILD_PROFILE={}",
build_info::profile_label(&var("PROFILE"), &var("OPT_LEVEL"))
build_info::profile_label(&var("OPT_LEVEL"))
);
let target = var("TARGET");
if !target.is_empty() {

View file

@ -244,6 +244,37 @@ fn objects(dir: &Path) -> Result<Vec<(&'static str, Vec<u8>, &'static str)>, Str
wai::container::ManifestV2::new("video", "replicate", vec![RenditionMeta::new("wai.video.av1", "av1").with_id("b").with_sha256(sha(&bn)), rnm]),
vec![bn, rn],
).map_err(|e| e.to_string())?.to_bytes().map_err(|e| e.to_string())?;
// A refinement with its digest, beside a companion bound to nothing (SPEC §7.1, rule 2):
// the envelope does not conform, so its refinement is dropped on either path (§7 step 4).
let (bc, rc, hc) = (pattern(30_000, 11, 3), pattern(2300, 13, 1), pattern(64, 17, 1));
let mut rcm = RenditionMeta::new("wai.video.int_refine", "wir1").with_role("refinement").with_id("r").with_sha256(sha(&rc));
rcm.refines = Some(json!({ "base": "b", "prev": "b", "layer": 1 }));
let mut hcm = RenditionMeta::new("wai.meta.hdr10plus", "hdr10plus").with_role("companion").with_id("h").with_sha256(sha(&hc));
hcm.companion_of = None;
let onc = WaiMulti::from_parts(
wai::container::ManifestV2::new("video", "replicate", vec![RenditionMeta::new("wai.video.int_hyper", "int_hyper").with_id("b").with_sha256(sha(&bc)), hcm.clone(), rcm.clone()]),
vec![bc.clone(), hc.clone(), rc.clone()],
).map_err(|e| e.to_string())?.to_bytes().map_err(|e| e.to_string())?;
// Its conforming twin: the companion bound to the base's capability. Every component
// conforms, so the refinement is offered and fetched on either path (§7 step 4).
let mut hcf = hcm.clone();
hcf.companion_of = Some("wai.video.int_hyper".into());
let ocf = WaiMulti::from_parts(
wai::container::ManifestV2::new("video", "replicate", vec![RenditionMeta::new("wai.video.int_hyper", "int_hyper").with_id("b").with_sha256(sha(&bc)), hcf, rcm.clone()]),
vec![bc.clone(), hc.clone(), rc.clone()],
).map_err(|e| e.to_string())?.to_bytes().map_err(|e| e.to_string())?;
// A refinement whose `refines` is malformed — `base` alone — beside the base it names:
// the envelope does not conform for it, and §7 step 4 reports it dropped, since its
// `refines.base`, read on its own, is the pick's id. Its twin names `x`, no entry, and
// is no refinement of the pick at all.
let malformed = |base: &str| -> Result<Vec<u8>, String> {
let mut r = rcm.clone();
r.refines = Some(json!({ "base": base }));
WaiMulti::from_parts(
wai::container::ManifestV2::new("video", "replicate", vec![RenditionMeta::new("wai.video.int_hyper", "int_hyper").with_id("b").with_sha256(sha(&bc)), r]),
vec![bc.clone(), rc.clone()],
).map_err(|e| e.to_string())?.to_bytes().map_err(|e| e.to_string())
};
Ok(vec![
("O1", o1, "WAI1, pinned, below one chunk is not reached: the head proof covers it whole"),
("O2", o2, "WAI2 image: a pinned integer learned rendition, AVIF, JPEG XL and PNG of one picture"),
@ -273,6 +304,10 @@ fn objects(dir: &Path) -> Result<Vec<(&'static str, Vec<u8>, &'static str)>, Str
("OPe", with_refines(json!({}))?, "WAI1 text whose refines is {}: an ordinary object"),
("OPw", with_refines(json!({ "base": o5_cid, "prev": o5_cid, "layer": 1 }))?, "WAI1 text with a well-formed refines: an ordinary object"),
("ORN", orn, "WAI2: an AV1 base and a refinement stating no sha256"),
("ONC", onc, "WAI2: a base, a companion bound to nothing, and a refinement with its sha256"),
("OCF", ocf, "ONC with its companion bound to the base's capability: every component conforms"),
("ONM", malformed("b")?, "WAI2: a base and a refinement whose refines is {\"base\": \"b\"}, the base's id and nothing else"),
("ONX", malformed("x")?, "ONM with refines {\"base\": \"x\"}, naming no entry"),
])
}
@ -1037,6 +1072,21 @@ fn cases(objs: &BTreeMap<String, Vec<u8>>, extra: &mut BTreeMap<String, Vec<u8>>
v.push(case(name, "fetch", json!({ "object": "ORN", "fragment": frag("ORN", Some(1009)), "advertises": ["wai.video.av1"],
"applies": ["wai.video.int_refine"], "threshold_permille": permille })));
}
// A refinement of an envelope that does not conform is dropped whichever path fetches
// its base (§7 step 4).
for (name, permille) in [("fetch_ONC_whole", 900), ("fetch_ONC_ranged", 1001)] {
v.push(case(name, "fetch", json!({ "object": "ONC", "fragment": frag("ONC", Some(1009)), "advertises": ["wai.video.int_hyper"],
"applies": ["wai.video.int_refine"], "threshold_permille": permille })));
}
// ONC's conforming twin offers its refinement on either path; a refinement whose
// malformed `refines` names the pick is dropped and reported, one naming no entry is no
// refinement of it (§7 step 4).
for id in ["OCF", "ONM", "ONX"] {
for (path, permille) in [("whole", 900), ("ranged", 1001)] {
v.push(case(format!("fetch_{id}_{path}"), "fetch", json!({ "object": id, "fragment": frag(id, Some(1009)), "advertises": ["wai.video.int_hyper"],
"applies": ["wai.video.int_refine"], "threshold_permille": permille })));
}
}
// Item 3: a refinement never moves its base.
let os_frag = frag("OS", Some(1009));
let lcevc_sink = |extra: Value| -> Value {
@ -1270,10 +1320,35 @@ fn generate(dir: &Path) -> Result<BTreeMap<String, Vec<u8>>, String> {
}
for (name, why) in [("fetch_ORN_whole", json!("range_threshold")), ("fetch_ORN_ranged", Value::Null)] {
let f = outcome_of(name)?;
if f["dropped"] != json!([[1, "component_digest_absent"]]) || f["why_whole"] != why || f["refinements"] != json!([]) {
// ORN's refinement states no digest, and the envelope does not conform for it: it is
// dropped for both.
if f["dropped"] != json!([[1, "component_digest_absent"], [1, "components_nonconforming"]]) || f["why_whole"] != why || f["refinements"] != json!([]) {
return Err(format!("{name} does not drop the refinement with no digest: {f}"));
}
}
for (name, why) in [("fetch_ONC_whole", json!("range_threshold")), ("fetch_ONC_ranged", Value::Null)] {
let f = outcome_of(name)?;
if f["dropped"] != json!([[2, "components_nonconforming"]]) || f["why_whole"] != why || f["refinements"] != json!([]) || f["pick"] != 0 {
return Err(format!("{name} does not drop the refinement of a nonconforming envelope: {f}"));
}
}
for (id, refinements, dropped) in [
("OCF", json!([2]), json!([])),
("ONM", json!([]), json!([[1, "components_nonconforming"]])),
("ONX", json!([]), json!([])),
] {
for (path, why) in [("whole", json!("range_threshold")), ("ranged", Value::Null)] {
let name = format!("fetch_{id}_{path}");
let f = outcome_of(&name)?;
if f["refinements"] != refinements || f["dropped"] != dropped || f["why_whole"] != why || f["pick"] != 0 {
return Err(format!("{name}: refinements {refinements} and dropped {dropped} expected: {f}"));
}
}
}
let f = outcome_of("fetch_OR_malformed_refines")?;
if f["dropped"] != json!([[1, "components_nonconforming"]]) || f["refinements"] != json!([]) {
return Err(format!("fetch_OR_malformed_refines does not report the refinement naming its pick: {f}"));
}
for (k, b) in extra {
if k.starts_with("cases/") {
files.insert(k, b);

View file

@ -314,7 +314,9 @@ fn apply(args: &[String]) -> Result<(), String> {
let object = wai::jwp_hash::content_hash(&bytes);
match Wai::from_bytes(&bytes) {
Err(_) => ("offer", object, Err("not-wai1"), vec![]),
Ok(w) if w.manifest.refines.is_some() || wai::codecs::is_refinement_only(w.capability()) => {
// Routed by capability (SPEC §4, staged-delivery §3): a refinement-only
// capability is a layer; any other is a base, a `refines` on it ignored.
Ok(w) if wai::codecs::is_refinement_only(w.capability()) => {
chain_of = w.refines().and_then(Result::ok).and_then(|r| unhex32(&r.base));
match sink.offer(&bytes, i as u64, ReceiptEvidence::None) {
Ok(r) => ("offer", object, Ok(r), vec![]),
@ -646,7 +648,15 @@ fn explode(args: &[String]) -> Result<(), String> {
[p, i] => (p, i.parse::<usize>().map_err(|e| e.to_string())?),
_ => return Err("wai stage explode <x.wai> <index> [-o out.wai]".into()),
};
let bytes = WaiMulti::explode(&read(path)?, i).map_err(|e| e.to_string())?;
let envelope = read(path)?;
// A sink offers a WAI2's refinements only when its components all conform (SPEC §7.1,
// *Refinement components*): explode gives no layer of a nonconforming envelope — a
// component of a refinement-only capability, or one marked a refinement.
let w = WaiMulti::from_bytes(&envelope).map_err(|e| e.to_string())?;
if w.manifest.renditions.get(i).is_some_and(|m| wai::codecs::is_refinement_only(&m.capability) || m.is_refinement()) {
w.check_component_roles().map_err(|e| format!("{path}: no refinement is offered from it: {e}"))?;
}
let bytes = WaiMulti::explode(&envelope, i).map_err(|e| e.to_string())?;
println!("component {i}: {} B content hash {}", bytes.len(), hex(&wai::jwp_hash::content_hash(&bytes)));
if let Some(out) = opt(args, "-o") {
std::fs::write(out, &bytes).map_err(|e| format!("write {out}: {e}"))?;

View file

@ -446,6 +446,22 @@ fn counting_cases(c: &Corpus) -> Result<Vec<(String, Option<Vec<u8>>)>, String>
enc.security = Some(Security::new("aes-256-gcm", 1));
let mut trailing = one(hyper, None, wih1.clone());
trailing.push(0);
// A WAI2 whose second rendition, PNG, is its classical rendition (jwp-receipts §2.4.12),
// its payloads plaintext or encrypted: the rendition table, in the clear either way,
// declares the classical length a session receiving it states.
let png = Wai::from_bytes(&std::fs::read(c.wai.join("staged-conformance/cases/image_png_up2/base.wai")).map_err(|e| e.to_string())?)
.map_err(|e| e.to_string())?
.payload;
let classical = |encrypted: bool| -> Result<Vec<u8>, String> {
let mut w = WaiMulti::new("image", "replicate", Value::Null, vec![
Rendition::media(hyper, "int_hyper", wih1.clone()).with_payload_digest(),
Rendition::media(wai::codecs::CAP_IMAGE_PNG, "png", png.clone()).with_payload_digest(),
]);
if encrypted {
w.manifest.extra.insert("security".into(), json!({ "encryption": "mls-sframe", "kid": 1 }));
}
w.to_bytes().map_err(|e| e.to_string())
};
Ok(vec![
("wih1".into(), Some(one(hyper, None, wih1.clone()))),
("staged-measure-conformance/fixtures/still/L01.wai".into(), None),
@ -461,6 +477,8 @@ fn counting_cases(c: &Corpus) -> Result<Vec<(String, Option<Vec<u8>>)>, String>
("wai2-sha256-mismatch".into(), Some(wai2(false)?)),
("encrypted".into(), Some(Wai::new(enc, wih1.clone()).to_bytes().map_err(|e| e.to_string())?)),
("trailing".into(), Some(trailing)),
("wai2-classical".into(), Some(classical(false)?)),
("wai2-classical-encrypted".into(), Some(classical(true)?)),
])
}

View file

@ -931,7 +931,7 @@ fn cases(ctx: &Ctx) -> Vec<Case> {
let a = small(59);
let (l1, _) = l1_of(&a, 60, 4);
let bad = edit(&l1, |w| w.manifest.model_requirement.capability = CAP_VIDEO_INT_MOTION.into());
out.push(simple("capability_not_refinement", "refines on a wai.video.int_motion envelope: capability-not-refinement", a, vec![("L1.wai".into(), bad)], vec![base_at(0, 0), offer("L1.wai", 1)]));
out.push(simple("capability_not_refinement", "refines on a wai.video.int_motion envelope, offered to §4 as a layer by a caller (a sink routes it by its capability, as a base, and never offers it): capability-not-refinement", a, vec![("L1.wai".into(), bad)], vec![base_at(0, 0), offer("L1.wai", 1)]));
}
{
let a = small(61);

View file

@ -4,16 +4,17 @@
//! build applies is the one the crate's tests check. It uses nothing but
//! `core`.
/// The build-profile label for cargo's `PROFILE` and `OPT_LEVEL`: `release` for
/// a build optimised as shipped — cargo's `release` profile at `opt-level` 2
/// or 3 — and `debug` for any other, including a `release` profile turned down
/// to a lower level and the size levels `s` and `z`, which are not how this
/// crate ships. The label is a statement about optimisation, not about debug
/// assertions: a release build with assertions on is still `release`, and a
/// dev build with them off is still `debug`.
pub fn profile_label(profile: &str, opt_level: &str) -> &'static str {
match (profile, opt_level) {
("release", "2" | "3") => "release",
/// The build-profile label for cargo's `OPT_LEVEL`: `release` for an optimised
/// build — `opt-level` 2 or 3, or the size levels `s` and `z`, which is how a
/// WebAssembly build ships — and `debug` for `opt-level` 0 or 1, or a level cargo
/// did not report. Cargo's `PROFILE` is not read: the label is decided by the
/// optimisation the code was built with, whatever the profile is named, so a dev
/// profile turned up to an optimising level is `release` and a release profile
/// turned down to 0 or 1 is `debug`. Nor is it a statement about debug
/// assertions: an optimised build with assertions on is still `release`.
pub fn profile_label(opt_level: &str) -> &'static str {
match opt_level {
"2" | "3" | "s" | "z" => "release",
_ => "debug",
}
}
@ -23,14 +24,12 @@ mod tests {
use super::profile_label;
#[test]
fn release_means_the_release_profile_optimised() {
assert_eq!(profile_label("release", "3"), "release");
assert_eq!(profile_label("release", "2"), "release");
assert_eq!(profile_label("release", "1"), "debug");
assert_eq!(profile_label("release", "0"), "debug");
assert_eq!(profile_label("release", "s"), "debug");
assert_eq!(profile_label("debug", "3"), "debug", "a dev profile turned up is not the shipped build");
assert_eq!(profile_label("debug", "0"), "debug");
assert_eq!(profile_label("", ""), "debug");
fn release_means_an_optimised_build() {
for level in ["2", "3", "s", "z"] {
assert_eq!(profile_label(level), "release", "opt-level {level} is optimised");
}
for level in ["0", "1", "", "4", "S"] {
assert_eq!(profile_label(level), "debug", "opt-level {level:?} is not an optimised level cargo reports");
}
}
}

View file

@ -343,19 +343,41 @@ pub enum Reason {
SessionOutputInconsistent,
/// A byte-exact presentation decoded again gives another digest.
SessionOutputIrreproducible,
/// An object received is listed twice, on one path.
SessionObjectDuplicate,
/// 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.
/// The objects received give other counted, uncounted or transferred bytes, or an
/// object whose envelope is held states other counted bytes than the envelope's.
SessionCountedMismatch,
/// An envelope held as the session's is not a `WAI1` or `WAI2` envelope whose
/// An object is listed as presented and no decoder entry names it, or as not
/// presented and one does.
SessionPresentedMismatch,
/// An envelope a decoder entry names 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,
/// A decoder entry names a held layer a sink cannot apply (staged-delivery §3, §4.1
/// steps 1–4 and 6; SPEC §7.1, *Refinement components*): its capability or
/// `refines` is not a layer's, its base is not a base the layer admits or was decoded
/// only through a fallback, its span lies outside a held base's units, or its payload
/// is not `WIR1` with a unit for each unit it covers.
SessionLayerNotApplicable,
/// A decoder entry's `first_unit` is not its base's: a held layer's, against the
/// entries naming the base it refines, or an entry naming a `stage` claim's base or
/// layer, against the `first_unit` `stage_sets` names that claim at.
SessionFirstUnitMismatch,
/// A presentation through an entry whose envelope is held is at another layer count
/// than that envelope gives.
SessionLayersMismatch,
/// A presentation through an entry whose envelope is held is of a unit that envelope
/// does not cover.
SessionUnitOutsideObject,
/// 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
@ -452,11 +474,17 @@ impl Reason {
Reason::SessionOutputMismatch => "session_output_mismatch",
Reason::SessionOutputInconsistent => "session_output_inconsistent",
Reason::SessionOutputIrreproducible => "session_output_irreproducible",
Reason::SessionObjectDuplicate => "session_object_duplicate",
Reason::SessionEntryNotReceived => "session_entry_not_received",
Reason::SessionSourceNotReceived => "session_source_not_received",
Reason::SessionCountedMismatch => "session_counted_mismatch",
Reason::SessionPresentedMismatch => "session_presented_mismatch",
Reason::SessionEnvelopeUnreadable => "session_envelope_unreadable",
Reason::SessionSourceMismatch => "session_source_mismatch",
Reason::SessionLayerNotApplicable => "session_layer_not_applicable",
Reason::SessionFirstUnitMismatch => "session_first_unit_mismatch",
Reason::SessionLayersMismatch => "session_layers_mismatch",
Reason::SessionUnitOutsideObject => "session_unit_outside_object",
Reason::SessionClassicalMismatch => "session_classical_mismatch",
Reason::SessionStageDisagrees => "session_stage_disagrees",
}
@ -3441,7 +3469,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 {
459
567
}
/// The conformance corpus (`claim-conformance/vectors.json`), regenerated. Ed25519
@ -5547,7 +5575,7 @@ mod corpus {
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()) {
if let Err(e) = r.check_session_envelopes(&refs, objects.as_deref(), outputs.as_deref()) {
return Ok(Err(e));
}
}

File diff suppressed because it is too large Load diff

View file

@ -72,7 +72,7 @@ const SESSION_MEMBERS: &[&str] = &[
];
const HOST_MEMBERS: &[&str] = &["profile", "target", "build", "threads"];
const DECODER_MEMBERS: &[&str] =
&["capability", "rendition", "object", "path", "decoder", "variant", "pin", "tier", "module_status", "leaves"];
&["capability", "rendition", "object", "first_unit", "path", "decoder", "variant", "pin", "tier", "module_status", "leaves"];
const SWITCH_MEMBERS: &[&str] = &["unit", "from", "to", "reason"];
const MISSED_MEMBERS: &[&str] = &["unit", "layers", "late_us"];
const STAGE_SET_MEMBERS: &[&str] = &["first_unit", "claim"];
@ -128,6 +128,9 @@ pub struct DecoderEntry {
/// 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.
pub object: [u8; 32],
/// The title's unit that is unit 0 of the object's base: of the object itself for a
/// base or a rendition, of the base it refines for a layer.
pub first_unit: u64,
pub path: SessionPath,
/// `builtin:<crate>@<version>` or `module:sha256:<hex>`.
pub decoder: String,
@ -142,7 +145,7 @@ pub struct DecoderEntry {
}
/// A decoder entry's members but `leaves`, borrowed.
type EntryKey<'a> = (&'a str, Option<u64>, [u8; 32], SessionPath, &'a str, Option<&'a str>, Option<[u8; 32]>, Option<&'a str>, Option<&'a str>);
type EntryKey<'a> = (&'a str, Option<u64>, [u8; 32], u64, SessionPath, &'a str, Option<&'a str>, Option<[u8; 32]>, Option<&'a str>, Option<&'a str>);
impl DecoderEntry {
/// Everything but `leaves`: two entries with one key are one entry.
@ -151,6 +154,7 @@ impl DecoderEntry {
&self.capability,
self.rendition,
self.object,
self.first_unit,
self.path,
&self.decoder,
self.variant.as_deref(),
@ -358,6 +362,7 @@ impl SessionClaim {
o.insert("capability".into(), d.capability.clone().into());
o.insert("rendition".into(), d.rendition.map_or(Value::Null, Value::from));
o.insert("object".into(), hex(&d.object).into());
o.insert("first_unit".into(), d.first_unit.into());
o.insert("path".into(), d.path.label().into());
o.insert("decoder".into(), d.decoder.clone().into());
o.insert("variant".into(), opt_str_value(&d.variant));
@ -466,6 +471,7 @@ impl SessionClaim {
capability,
rendition: get_opt_int(o, "rendition")?,
object: get_hash(o, "object")?,
first_unit: get_int(o, "first_unit")?,
path: get_path(o, "path")?,
decoder: get_str(o, "decoder")?,
variant: get_opt_term(o, "variant")?,
@ -577,6 +583,7 @@ impl SessionClaim {
if let Some(r) = d.rendition {
int_ok(r)?;
}
int_ok(d.first_unit)?;
int_ok(d.leaves)?;
}
for n in [self.units, self.leaf_count, self.primary_units, self.fallback_units, self.switches_total, self.missed] {
@ -742,6 +749,57 @@ pub fn classical_rendition(w: &crate::container::WaiMulti) -> Option<usize> {
})
}
/// An envelope a verifier holding the `envelopes` context holds (§2.4.12).
enum HeldEnvelope {
One(crate::container::Wai),
Multi(crate::container::WaiMulti),
}
/// `bytes` as a held envelope (§2.4.12): one that reads as a `WAI1` or `WAI2` envelope
/// whose payloads are plaintext, or encrypted — well formed, its `security` a security
/// object naming an encryption, which a sink holding the key decrypts and dispatches (SPEC
/// §6.4 step 3). An encrypted envelope's manifest and rendition table are in the clear
/// (§6.1): the rules read them, and nothing that needs a payload. `true` for encrypted;
/// `None` for inert bytes.
fn read_held(bytes: &[u8]) -> Option<(HeldEnvelope, bool)> {
use crate::container::{Wai, WaiMulti};
match bytes.get(..4) {
Some(b"WAI2") => {
let w = WaiMulti::from_bytes(bytes).ok()?;
match crate::container::manifest_v2_security(&w.manifest) {
Ok(s) => Some((HeldEnvelope::Multi(w), s.as_ref().is_some_and(|x| !x.is_plaintext()))),
Err(_) => None,
}
}
Some(b"WAI1") => {
let w = Wai::from_bytes(bytes).ok()?;
let sealed = w.manifest.security.as_ref().is_some_and(|x| !x.is_plaintext());
Some((HeldEnvelope::One(w), sealed))
}
_ => None,
}
}
/// The classical length §2.4.12 rule 9 reads from `envelopes`: the sum, over the held
/// `WAI2` envelopes among them — readable or encrypted, since the rendition table
/// declares it in the clear — of the length of each one's classical rendition
/// ([`classical_rendition`]) whose capability no entry of `decoders` decoded. `None`
/// where there is none. [`ClaimReceipt::check_session_envelopes`] requires it of
/// `classical_declared`, and a [`crate::session_recorder::SessionRecorder`] states it so,
/// from the envelopes it holds.
pub fn classical_length(envelopes: &[&[u8]], decoders: &[DecoderEntry]) -> Option<u128> {
let mut sum: Option<u128> = None;
for bytes in envelopes {
let Some((HeldEnvelope::Multi(w), _)) = read_held(bytes) else { continue };
if let Some(i) = classical_rendition(&w)
&& !decoders.iter().any(|d| d.capability == w.manifest.renditions[i].capability)
{
*sum.get_or_insert(0) += w.renditions[i].len() as u128;
}
}
sum
}
/// Whether a re-decode of a presentation through this capability is checked against its
/// digest: its replicate class makes it byte-exact (`neural-int-exact`,
/// `standard-defined`).
@ -753,6 +811,50 @@ pub fn redecode_checked(capability: &str) -> bool {
)
}
/// The units a base payload holds (§2.4.12, step 6 against the envelopes), where WAI reads
/// them from its format: a `WIV1` clip's frames, and one for a `WIH1` picture, a `WIA1`
/// clip and a `WIS1` attribute tensor (integer-payloads, staged-delivery §2). `None` for a
/// capability with no pinned integer format, or a payload that does not parse as its own.
pub fn payload_units(capability: &str, payload: &[u8]) -> Option<u64> {
use crate::int_wire as w;
let magic = crate::codecs::pinned_int_payload(capability)?;
if magic == w::MAGIC_WIV1 {
return w::parse_wiv1(payload, None).ok().map(|v| u64::from(v.n_frames));
}
let one = if magic == w::MAGIC_WIH1 {
w::parse_wih1(payload).is_ok()
} else if magic == w::MAGIC_WIA1 {
w::parse_wia1(payload).is_ok()
} else {
w::parse_wis1(payload).is_ok()
};
one.then_some(1)
}
/// The units a `WAI1` base's payload holds (§2.4.12, step 6 against the envelopes), read in
/// the payload's own format: of the formats pinned for the manifest's capability and its
/// fallback, which decodes the same payload (SPEC §4 step 5), the one whose magic the
/// payload starts with. `None` where neither pins a format the payload is in, or the
/// payload does not parse as it.
pub fn base_units(capability: &str, fallback: Option<&str>, payload: &[u8]) -> Option<u64> {
let own = |c: &&str| crate::codecs::pinned_int_payload(c).is_some_and(|m| payload.get(..4) == Some(&m[..]));
payload_units([Some(capability), fallback].into_iter().flatten().find(own)?, payload)
}
/// A well-formed `refines` with the units of its base where they are read (`n`): the
/// layer, or, where its span lies outside those units or its runs do not sum to the units
/// it covers, a layer a sink cannot apply (staged-delivery §3, §4.1 step 2).
fn layer_of_refines(r: &crate::container::Refines, n: Option<u64>) -> Result<(u64, Option<(u64, u64)>), ()> {
if let Some(n) = n {
r.check_units(u32::try_from(n).map_err(|_| ())?).map_err(|_| ())?;
}
let span = match r.span {
Some(sp) => Some((u64::from(sp.from), u64::from(sp.count))),
None => n.map(|n| (0, n)),
};
Ok((u64::from(r.layer) + 1, span))
}
impl ClaimReceipt {
fn session(&self) -> Result<&SessionClaim, Rejection> {
match &self.body {
@ -832,13 +934,20 @@ impl ClaimReceipt {
Ok(())
}
/// Step 6 (§2.4.12), against the objects received: the entry object is among them
/// (`session_entry_not_received`); so is each decoder entry's object
/// (`session_source_not_received`); then the counted sums per path and over the objects
/// not presented, the count of objects not countable, and the transferred bytes
/// (`session_counted_mismatch`).
/// Step 6 (§2.4.12), against the objects received, in order: each (content hash,
/// path) is listed once (`session_object_duplicate`) — an object received more than
/// once on a path is listed once, its `transferred` every byte received for it; the
/// entry object is among them (`session_entry_not_received`); so is each decoder
/// entry's object (`session_source_not_received`); the counted sums per path and over
/// the objects not presented, the count of objects not countable, and the transferred
/// bytes (`session_counted_mismatch`); and an object is `presented` exactly when a
/// decoder entry names it (`session_presented_mismatch`).
pub fn check_session_objects(&self, objects: &[ObjectSeen]) -> Result<(), Rejection> {
let s = self.session()?;
let mut listed = std::collections::BTreeSet::new();
if objects.iter().any(|o| !listed.insert((o.content_hash, o.path))) {
return Err(Rejection::at(6, Reason::SessionObjectDuplicate));
}
if !objects.iter().any(|o| o.content_hash == self.subject) {
return Err(Rejection::at(6, Reason::SessionEntryNotReceived));
}
@ -871,79 +980,341 @@ impl ClaimReceipt {
if (primary, fallback, discarded, uncounted, transferred) != want {
return Err(Rejection::at(6, Reason::SessionCountedMismatch));
}
let named: std::collections::BTreeSet<[u8; 32]> = s.decoders.iter().map(|d| d.object).collect();
if objects.iter().any(|o| o.presented != named.contains(&o.content_hash)) {
return Err(Rejection::at(6, Reason::SessionPresentedMismatch));
}
Ok(())
}
/// Step 6 (§2.4.12), against the envelopes the session received, as their bytes: every
/// `WAI2` envelope among them, and any `WAI1` envelope the verifier holds — a base or a
/// layer — with the objects received where it holds them too. In order: with the
/// objects, each envelope is among them by its content hash
/// (`session_source_not_received`); each reads as a `WAI1` or `WAI2` envelope (SPEC §2,
/// §7.1) whose payloads are plaintext — an encrypted one is inert (SPEC §6.4)
/// (`session_envelope_unreadable`); each decoder entry whose object is one of them names
/// what it gives — a rendition of a `WAI2` envelope and that rendition's capability,
/// or no rendition and a `WAI1` envelope's capability or fallback — and each entry
/// naming a rendition has its object among them (`session_source_mismatch`); and the
/// lengths of the `WAI2` envelopes' classical renditions ([`classical_rendition`])
/// whose capability no decoder entry decoded sum to `classical_declared`, which is
/// `null` exactly when there is none (`session_classical_mismatch`).
pub fn check_session_envelopes(&self, envelopes: &[&[u8]], objects: Option<&[ObjectSeen]>) -> Result<(), Rejection> {
use crate::container::{manifest_v2_plaintext, Wai, WaiMulti};
/// layer — with the objects received and the presentations where it holds them too.
/// In order:
/// 1. with the objects, each envelope is among them by its content hash
/// (`session_source_not_received`);
/// 2. with the objects, every object listed with an envelope's content hash states that
/// envelope's counted bytes (staged-measurement §3), and `null` exactly when it is not
/// countable (`session_counted_mismatch`);
/// 3. each envelope a decoder entry names is held: it reads as a `WAI1` or `WAI2`
/// envelope (SPEC §2, §7.1) whose payloads are plaintext, or is encrypted — a sink
/// holding its key decrypts and dispatches it (SPEC §6.4 step 3)
/// (`session_envelope_unreadable`). The rules below read every held envelope's
/// manifest and rendition table, an encrypted one's in the clear, and of an encrypted
/// one nothing that needs a payload: whether a payload matches a well-formed `sha256`,
/// a base's units and a layer's `WIR1` payload are the signer's statement. An
/// encrypted envelope no entry names is a received object neither counted nor
/// presented, as are inert bytes — a `security` member that is not a security object,
/// or bytes that are not an envelope at all. An entry naming a held `WAI2` rendition by
/// its exploded envelope's content hash, with no rendition, is read from here on as
/// naming that rendition, unless the exploded envelope's own bytes are held;
/// 4. each decoder entry whose object is one of them names what it gives, as a sink
/// dispatches it — a rendition of a `WAI2` envelope and that rendition's capability,
/// a rendition selectable or a layer, whose payload matches its stated `sha256`, and
/// of an encrypted envelope whose `sha256` is absent or well formed, since a malformed
/// one matches no payload ([`crate::container::RenditionMeta::digest_well_formed`])
/// (SPEC §7.1, *Selection algorithm*); or no rendition and a `WAI1` envelope's
/// capability or fallback, dispatchable, of a manifest whose capability is not
/// companion-only (SPEC §4), where the envelope is not a layer — and each entry
/// naming a rendition has its object among the readable or encrypted envelopes
/// (`session_source_mismatch`);
/// 5. each decoder entry whose object is a held layer — a `WAI1` envelope, or a `WAI2`
/// rendition, of a refinement-only capability; any other is a base, a `refines` it
/// carries ignored — names one a sink can apply (`session_layer_not_applicable`): a
/// `WAI1` layer with no fallback and its `refines` well formed (staged-delivery §3,
/// §4.1 step 1); a `WAI2` rendition of an envelope every component of which conforms
/// (SPEC §7.1, [`crate::container::ManifestV2::check_component_roles`]); its base,
/// where held — a `WAI1` envelope by its content hash, or a `WAI2` rendition by its
/// component id or its exploded envelope's content hash, the same object
/// (staged-delivery §9) — a base whose capability the layer admits (§4.1 step 4);
/// where entries name the base, one decoding it through a capability the layer
/// admits, and the base's own where it is held, not its fallback (§4.1 step 3); where
/// the base's units are read, a span within them and runs that sum to those it covers
/// (§4.1 step 2); and a `WIR1` payload of its unit kind with a unit for each it
/// covers, where that count is read (§4.1 step 6);
/// 6. each decoder entry whose object is a held layer whose base entries name has the
/// `first_unit` of one of the entries it is applied over (`session_first_unit_mismatch`);
/// 7. with the presentations, each one through an entry whose envelope is held is at the
/// layer count that envelope gives: 1 for a base, or a rendition with no `refines`,
/// and `refines.layer + 1` for a layer (`session_layers_mismatch`);
/// 8. with the presentations, each one through an entry whose envelope is held is of a
/// unit that envelope covers: from the entry's `first_unit` plus `a`, `n` units, where
/// for a base or a rendition `a` is 0 and `n` the units its payload holds, read in the
/// payload's own format ([`base_units`]), and for a layer `a` and `n` are its `span`,
/// or 0 and its base's units with no span; a payload whose units WAI does not read,
/// or a base not held, leaves the presentation unchecked
/// (`session_unit_outside_object`);
/// 9. the lengths of the held `WAI2` envelopes' classical renditions
/// ([`classical_rendition`]) whose capability no decoder entry decoded — readable or
/// encrypted, since the rendition table declares them in the clear — sum to
/// `classical_declared`, which is `null` exactly when there is none
/// (`session_classical_mismatch`, [`classical_length`]).
pub fn check_session_envelopes(&self, envelopes: &[&[u8]], objects: Option<&[ObjectSeen]>, outputs: Option<&[Presentation]>) -> Result<(), Rejection> {
use crate::container::{RefForm, Refines, Wai, WaiMulti};
let s = self.session()?;
let hash = crate::jwp_hash::content_hash;
// 1. Each envelope was received.
if let Some(objects) = objects
&& envelopes.iter().any(|e| !objects.iter().any(|o| o.content_hash == crate::jwp_hash::content_hash(e)))
&& envelopes.iter().any(|e| !objects.iter().any(|o| o.content_hash == hash(e)))
{
return Err(Rejection::at(6, Reason::SessionSourceNotReceived));
}
enum Read {
One(Wai),
Multi(WaiMulti),
// 2. An object's counted bytes are its envelope's.
if let Some(objects) = objects {
for e in envelopes {
let (h, counted) = (hash(e), crate::measure_framing::count_object(e).ok().map(|c| c.total));
if objects.iter().any(|o| o.content_hash == h && o.counted != counted) {
return Err(Rejection::at(6, Reason::SessionCountedMismatch));
}
}
}
let unreadable = Rejection::at(6, Reason::SessionEnvelopeUnreadable);
use HeldEnvelope as Read;
let read_one = read_held;
let mut read: Vec<([u8; 32], Read)> = Vec::new();
let mut inert: Vec<[u8; 32]> = Vec::new();
let mut encrypted: Vec<[u8; 32]> = Vec::new();
// Each rendition of a readable WAI2 envelope is also its exploded envelope (SPEC
// §7.1), the same object (staged-delivery §9): (the exploded envelope's content
// hash, the envelope's, the rendition).
let mut exploded: Vec<([u8; 32], [u8; 32], u64)> = Vec::new();
for bytes in envelopes {
let r = match bytes.get(..4) {
Some(b"WAI2") => {
let w = WaiMulti::from_bytes(bytes).map_err(|_| unreadable)?;
if !manifest_v2_plaintext(&w.manifest) {
return Err(unreadable);
match read_one(bytes) {
Some((r, sealed)) => {
let h = hash(bytes);
if sealed {
encrypted.push(h);
}
Read::Multi(w)
}
Some(b"WAI1") => {
let w = Wai::from_bytes(bytes).map_err(|_| unreadable)?;
if w.manifest.security.as_ref().is_some_and(|x| !x.is_plaintext()) {
return Err(unreadable);
if matches!(r, Read::Multi(_))
&& let Ok(each) = WaiMulti::explode_each(bytes)
{
for (j, e) in each.into_iter().enumerate() {
if let Ok(e) = e {
exploded.push((hash(&e), h, j as u64));
}
}
}
Read::One(w)
read.push((h, r));
}
_ => return Err(unreadable),
};
read.push((crate::jwp_hash::content_hash(bytes), r));
None => inert.push(hash(bytes)),
}
}
for d in &s.decoders {
let gives = match read.iter().find(|(h, _)| *h == d.object).map(|(_, r)| r) {
Some(Read::Multi(w)) => usize::try_from(d.rendition.unwrap_or(u64::MAX))
.ok()
.and_then(|r| w.manifest.renditions.get(r))
.is_some_and(|m| m.capability == d.capability),
Some(Read::One(w)) => d.rendition.is_none() && (w.capability() == d.capability || w.fallback() == Some(d.capability.as_str())),
let held = |object: &[u8; 32]| read.iter().find(|(h, _)| h == object).map(|(_, r)| r);
// An encrypted envelope's payloads are not read: its digests, its units and a
// layer's WIR1 payload are its signer's statement.
let sealed = |object: &[u8; 32]| encrypted.contains(object);
// 3. An envelope a decoder entry names is readable or encrypted.
if s.decoders.iter().any(|d| inert.contains(&d.object) && held(&d.object).is_none()) {
return Err(Rejection::at(6, Reason::SessionEnvelopeUnreadable));
}
// Each entry as the envelopes resolve it: an entry naming a held WAI2 rendition by
// its exploded envelope's content hash, with no rendition, names that rendition —
// unless the exploded envelope's own bytes are held, a separate delivery.
let resolved: Vec<([u8; 32], Option<u64>)> = s
.decoders
.iter()
.map(|d| {
if d.rendition.is_none()
&& held(&d.object).is_none()
&& let Some(&(_, w, j)) = exploded.iter().find(|(x, ..)| *x == d.object)
{
return (w, Some(j));
}
(d.object, d.rendition)
})
.collect();
let refinement_only = crate::codecs::is_refinement_only;
// A layer is decided by its capability (SPEC §4, staged-delivery §3): a manifest or a
// rendition of a refinement-only capability; any other is a base, a `refines` it
// carries ignored.
let layer_entry = |m: &crate::container::RenditionMeta| refinement_only(&m.capability);
let layer_one = |w: &Wai| refinement_only(w.capability());
// 4. Each entry names what its envelope gives, as a sink dispatches it.
for (d, &(object, rendition)) in s.decoders.iter().zip(&resolved) {
let d = &DecoderEntry { object, rendition, ..d.clone() };
let gives = match held(&d.object) {
Some(Read::Multi(w)) => usize::try_from(d.rendition.unwrap_or(u64::MAX)).ok().is_some_and(|i| {
w.manifest.renditions.get(i).is_some_and(|m| {
m.capability == d.capability
&& (m.is_selectable() || layer_entry(m))
&& if sealed(&d.object) { m.digest_well_formed() } else { m.payload_digest_matches(&w.renditions[i]) }
})
}),
Some(Read::One(w)) => {
d.rendition.is_none()
&& (w.capability() == d.capability || w.fallback() == Some(d.capability.as_str()))
&& (layer_one(w) || (crate::codecs::is_dispatchable(&d.capability) && !crate::codecs::is_companion_only(w.capability())))
}
None => d.rendition.is_none(),
};
if !gives {
return Err(Rejection::at(6, Reason::SessionSourceMismatch));
}
}
let mut sum: Option<u128> = None;
for (_, r) in &read {
let Read::Multi(w) = r else { continue };
if let Some(i) = classical_rendition(w)
&& !s.decoders.iter().any(|d| d.capability == w.manifest.renditions[i].capability)
// A base: its own capability, the units its payload holds where WAI reads them, and
// whether it is one (a base rendition, not a layer).
struct Base {
capability: String,
units: Option<u64>,
is_base: bool,
}
let component = |w: &WaiMulti, j: usize, sealed: bool| -> Base {
let m = &w.manifest.renditions[j];
let units = if sealed { None } else { payload_units(&m.capability, &w.renditions[j]) };
Base { capability: m.capability.clone(), units, is_base: m.is_selectable() }
};
// The base a layer names, where held: a WAI1 envelope by its content hash, or a WAI2
// rendition, by its exploded envelope's content hash or by its envelope and index.
let base_by_hash = |h: &[u8; 32]| -> Option<Base> {
if let Some(Read::One(b)) = held(h) {
let units = if sealed(h) { None } else { base_units(b.capability(), b.fallback(), &b.payload) };
return Some(Base { capability: b.capability().to_string(), units, is_base: !layer_one(b) });
}
let &(_, w, j) = exploded.iter().find(|(x, ..)| x == h)?;
match held(&w)? {
Read::Multi(m) => Some(component(m, j as usize, sealed(&w))),
Read::One(_) => None,
}
};
// The names of a base: its content hashes (a WAI1 base's, a rendition's exploded
// envelope's) and its (envelope, rendition) pairs.
let names_of = |hash_form: Option<[u8; 32]>, pair: Option<([u8; 32], u64)>| -> (Vec<[u8; 32]>, Vec<([u8; 32], u64)>) {
let mut hashes: Vec<[u8; 32]> = hash_form.into_iter().collect();
let mut pairs: Vec<([u8; 32], u64)> = pair.into_iter().collect();
for &(x, w, j) in &exploded {
if hash_form == Some(x) {
pairs.push((w, j));
}
if pair == Some((w, j)) {
hashes.push(x);
}
}
(hashes, pairs)
};
// What an entry's held envelope is.
enum Gives {
Base(Option<u64>),
Layer { layers: u64, span: Option<(u64, u64)>, first_units: Vec<u64> },
NotApplicable,
}
// A layer of capability `cap` over the base `b` (where held) named by `names`, with
// `r` its refines and `payload` its WIR1 payload: what a sink applies, or nothing.
let layer_of = |cap: &str, r: &Refines, b: Option<Base>, names: (Vec<[u8; 32]>, Vec<([u8; 32], u64)>), payload: Option<&[u8]>| -> Gives {
let n = match &b {
Some(b) if !b.is_base || !crate::codecs::admits(cap, &b.capability) => return Gives::NotApplicable,
Some(b) => b.units,
None => None,
};
// The entries naming the base, and those a sink applies the layer over: through
// a capability the layer admits, and the base's own where it is held.
let (hashes, pairs) = names;
let naming: Vec<&DecoderEntry> = s
.decoders
.iter()
.zip(&resolved)
.filter(|(e, (o, r))| hashes.contains(&e.object) || hashes.contains(o) || r.is_some_and(|j| pairs.contains(&(*o, j))))
.map(|(e, _)| e)
.collect();
let over: Vec<u64> = naming
.iter()
.filter(|e| crate::codecs::admits(cap, &e.capability) && b.as_ref().is_none_or(|b| e.capability == b.capability))
.map(|e| e.first_unit)
.collect();
if !naming.is_empty() && over.is_empty() {
return Gives::NotApplicable;
}
let Ok((layers, span)) = layer_of_refines(r, n) else { return Gives::NotApplicable };
// Its payload, where it is read, parses as WIR1 for its unit kind, with a unit for
// each it covers.
let Some(kind) = crate::staged::unit_kind(cap) else { return Gives::NotApplicable };
if let Some(payload) = payload {
let Ok(p) = crate::staged::wir1::parse(payload, kind) else { return Gives::NotApplicable };
if span.is_some_and(|(_, count)| u64::from(p.n_units) != count) {
return Gives::NotApplicable;
}
}
Gives::Layer { layers, span, first_units: over }
};
let gives = |k: usize| -> Option<Gives> {
let (object, rendition) = resolved[k];
match held(&object)? {
Read::One(w) => {
if !layer_one(w) {
return Some(Gives::Base(if sealed(&object) { None } else { base_units(w.capability(), w.fallback(), &w.payload) }));
}
let Some(Ok(r)) = w.refines() else { return Some(Gives::NotApplicable) };
if w.fallback().is_some() {
return Some(Gives::NotApplicable);
}
let base = unhex(&r.base).and_then(|v| <[u8; 32]>::try_from(v).ok())?;
let payload = (!sealed(&object)).then_some(w.payload.as_slice());
Some(layer_of(w.capability(), &r, base_by_hash(&base), names_of(Some(base), None), payload))
}
Read::Multi(w) => {
let i = usize::try_from(rendition?).ok()?;
let rs = &w.manifest.renditions;
let m = rs.get(i)?;
if !layer_entry(m) {
return Some(Gives::Base(if sealed(&object) { None } else { payload_units(&m.capability, &w.renditions[i]) }));
}
// SPEC §7.1: a sink offers a WAI2's refinements only when every one of its
// components conforms (`check_component_roles`).
if w.check_component_roles().is_err() {
return Some(Gives::NotApplicable);
}
let r = Refines::from_value(m.refines.as_ref()?, RefForm::ComponentId).ok()?;
let j = rs.iter().position(|e| e.id.as_deref() == Some(r.base.as_str()))?;
let payload = (!sealed(&object)).then_some(w.renditions[i].as_slice());
Some(layer_of(&m.capability, &r, Some(component(w, j, sealed(&object))), names_of(None, Some((object, j as u64))), payload))
}
}
};
// 5. A held layer an entry names is one a sink can apply.
if (0..s.decoders.len()).any(|k| matches!(gives(k), Some(Gives::NotApplicable))) {
return Err(Rejection::at(6, Reason::SessionLayerNotApplicable));
}
// 6. A held layer's entry has the first unit of an entry it is applied over.
for (k, d) in s.decoders.iter().enumerate() {
if let Some(Gives::Layer { first_units, .. }) = gives(k)
&& !first_units.is_empty()
&& !first_units.contains(&d.first_unit)
{
*sum.get_or_insert(0) += w.renditions[i].len() as u128;
return Err(Rejection::at(6, Reason::SessionFirstUnitMismatch));
}
}
if sum != s.classical_declared.map(u128::from) {
if let Some(ps) = outputs {
// 7. Each presentation through a held entry is at the layers its envelope gives.
for p in ps {
let k = usize::from(p.decoder);
if k >= s.decoders.len() {
continue;
}
let layers = match gives(k) {
Some(Gives::Base(_)) => 1,
Some(Gives::Layer { layers, .. }) => layers,
_ => continue,
};
if u64::from(p.layers) != layers {
return Err(Rejection::at(6, Reason::SessionLayersMismatch));
}
}
// 8. ... and of a unit it covers.
for p in ps {
let k = usize::from(p.decoder);
let Some(d) = s.decoders.get(k) else { continue };
let covers = match gives(k) {
Some(Gives::Base(Some(n))) => (0, n),
Some(Gives::Layer { span: Some(span), .. }) => span,
_ => continue,
};
let from = u128::from(d.first_unit) + u128::from(covers.0);
let unit = u128::from(p.unit);
if unit < from || unit >= from + u128::from(covers.1) {
return Err(Rejection::at(6, Reason::SessionUnitOutsideObject));
}
}
}
// 9. The classical length, over the held WAI2 envelopes, readable or encrypted.
if classical_length(envelopes, &s.decoders) != s.classical_declared.map(u128::from) {
return Err(Rejection::at(6, Reason::SessionClassicalMismatch));
}
Ok(())
@ -956,7 +1327,10 @@ impl ClaimReceipt {
/// title each have their own. In order: for each claim, it passes §3.1 steps 1–4
/// (`context_invalid`); it is a `stage` claim, signed by this claim's key, named by an
/// entry of `stage_sets` (`context_mismatch`); with the objects, its base and every
/// layer object that arrived are among them (`session_stage_disagrees`). Then, with the
/// layer object that arrived are among them (`session_stage_disagrees`). Each decoder
/// entry whose object is a claim's base, or a layer object a claim lists, has a
/// `first_unit` at which `stage_sets` names a claim of that object
/// (`session_first_unit_mismatch`). Then, with the
/// presentations (`session_stage_disagrees`): for each claim and each entry naming it,
/// every unit of its set is presented, and for each stage `s` from 1 up the units of
/// the set presented at more than `s` layers are no fewer than it counts at `s` or
@ -990,6 +1364,18 @@ impl ClaimReceipt {
}
claims.push((c, named));
}
// Each entry naming a claim's base, or a layer object it lists, has a first unit at
// which `stage_sets` names a claim listing that object.
let mut at: std::collections::BTreeMap<[u8; 32], Vec<u64>> = std::collections::BTreeMap::new();
for (c, named) in &claims {
let ClaimBody::Stage(st) = &c.body else { continue };
for h in std::iter::once(c.subject).chain(st.objects.iter().map(|o| o.object)) {
at.entry(h).or_default().extend(named.iter().copied());
}
}
if s.decoders.iter().any(|d| at.get(&d.object).is_some_and(|fs| !fs.contains(&d.first_unit))) {
return Err(Rejection::at(6, Reason::SessionFirstUnitMismatch));
}
let Some(ps) = outputs else { return Ok(()) };
let finals = SessionClaim::finals(ps);
// (set-local unit, largest layers) of every unit presented in [f, f + n).

View file

@ -637,8 +637,10 @@ pub fn is_known_capability(cap: &str) -> bool {
/// payload; an LCEVC enhancement adds residuals to one. Such a capability is
/// never dispatched standalone — not as a v1.0 `capability` or `fallback`, not
/// as a v1.1 selectable rendition — only as a v1.1 companion. The converse
/// holds too: every other registered capability is standalone media, and is
/// never a companion (SPEC §7.1, rule 1).
/// holds too: no other registered capability is ever a companion (SPEC §7.1, rule
/// 1). Every other payload capability is standalone media; a refinement-only
/// capability is a layer; and a record or a derivation names no payload format, so
/// it is no `WAI2` component in any role.
pub fn is_companion_only(cap: &str) -> bool {
matches!(cap, CAP_META_HDR10PLUS | CAP_META_DOVI | CAP_VIDEO_LCEVC)
}

View file

@ -1678,6 +1678,15 @@ impl RenditionMeta {
Some(want) => is_sha256_hex(want) && crate::pin::sha256_hex(payload) == want,
}
}
/// Whether this entry's `sha256` is absent or well formed — 64 lowercase hex digits
/// ([`is_sha256_hex`]) — read from the entry alone: one that is not matches no payload
/// ([`Self::payload_digest_matches`]), so no sink decodes the entry whatever its
/// payload. Of an encrypted envelope, whose payloads a verifier without the key does
/// not read, this is what the entry gives of its digest (jwp-receipts §2.4.12 rule 4).
pub fn digest_well_formed(&self) -> bool {
self.sha256.as_deref().is_none_or(is_sha256_hex)
}
}
/// An envelope that breaks the component rules of SPEC §5 / §7.1. Returned by
@ -1695,9 +1704,13 @@ pub enum RoleError {
/// A companion-only capability (HDR dynamic metadata, an enhancement
/// layer) listed as a selectable rendition instead of as a companion.
CompanionOnlyAsRendition { index: usize, capability: String },
/// A registered standalone capability (one that is not companion-only, such
/// as `wai.audio.iamf` or `wai.video.hevc`) marked `role = "companion"`.
/// A registered payload capability, standalone media (one that is not
/// companion-only, such as `wai.audio.iamf` or `wai.video.hevc`), marked
/// `role = "companion"`.
StandaloneAsCompanion { index: usize, capability: String },
/// A component, in any role, whose capability is a record or a derivation (SPEC §5,
/// *Kinds of registered string*), which names no payload format.
RecordOrDerivationAsComponent { index: usize, capability: String },
/// A companion without `companion_of`, or whose `companion_of` (or
/// `companion_of_id`) names no selectable rendition in this envelope.
UnboundCompanion { index: usize, capability: String },
@ -1762,6 +1775,7 @@ impl RoleError {
RoleError::CompanionOnlyAsFallback { .. } => "companion-only-as-fallback",
RoleError::CompanionOnlyAsRendition { .. } => "companion-only-as-rendition",
RoleError::StandaloneAsCompanion { .. } => "standalone-as-companion",
RoleError::RecordOrDerivationAsComponent { .. } => "record-or-derivation-as-component",
RoleError::UnboundCompanion { .. } => "unbound-companion",
RoleError::IncompatibleBase { .. } => "incompatible-base",
RoleError::EnhancementBaseNotUnique { .. } => "enhancement-base-not-unique",
@ -1800,6 +1814,8 @@ impl std::fmt::Display for RoleError {
f, "component {index} ({capability}) is companion-only but is not marked role=\"companion\""),
RoleError::StandaloneAsCompanion { index, capability } => write!(
f, "component {index} ({capability}) is standalone media, not a companion; carry it as a rendition"),
RoleError::RecordOrDerivationAsComponent { index, capability } => write!(
f, "component {index} ({capability}) is a record or a derivation, which names no payload format: no component of a WAI2"),
RoleError::UnboundCompanion { index, capability } => write!(
f, "companion {index} ({capability}) names no selectable rendition in companion_of"),
RoleError::IncompatibleBase { index, capability, base } => {
@ -2107,6 +2123,191 @@ pub struct ManifestV2 {
}
impl ManifestV2 {
/// The index of the base a companion at `index` binds to, when it binds to one
/// entry: the entry `companion_of_id` names, or the one selectable entry of
/// capability `companion_of`.
fn bound_base(&self, index: usize) -> Option<usize> {
let metas = &self.renditions;
let m = metas.get(index)?;
if let Some(id) = m.companion_of_id.as_deref() {
return metas.iter().position(|b| b.id.as_deref() == Some(id));
}
let target = m.companion_of.as_deref()?;
let mut it = metas.iter().enumerate().filter(|(_, b)| b.is_selectable() && b.capability == target);
match (it.next(), it.next()) {
(Some((i, _)), None) => Some(i),
_ => None,
}
}
/// SPEC §7.1, *Refinement components*, for the entry at `index`, in this order:
/// its capability is refinement-only; it carries `id` and `sha256`; its `refines`
/// is well formed with component ids; `refines.base` names a selectable entry with
/// an `id` and a `sha256`; its capability admits that entry's (SPEC §5, *Base
/// admission*); every producer of `prev` names a selectable entry or a refinement
/// before it, with an `id` and a `sha256`; and it follows every selectable entry.
fn check_refinement(&self, index: usize) -> Result<(), RoleError> {
let metas = &self.renditions;
let m = &metas[index];
if !crate::codecs::is_refinement_only(&m.capability) {
return Err(RoleError::RefinementNotRefinementOnly { index, capability: m.capability.clone() });
}
if m.id.is_none() || m.sha256.is_none() {
return Err(RoleError::RefinementIdentityMissing { index });
}
let refines = match m.refines.as_ref().map(|v| Refines::from_value(v, RefForm::ComponentId)) {
Some(Ok(r)) => r,
Some(Err(e)) => return Err(RoleError::RefinesMalformed { index: Some(index), detail: e.0 }),
None => return Err(RoleError::RefinesMalformed { index: Some(index), detail: "a refinement carries no refines" }),
};
let by_id = |id: &str| metas.iter().position(|e| e.id.as_deref() == Some(id));
let Some(b) = by_id(&refines.base).filter(|&b| metas[b].is_selectable()) else {
return Err(RoleError::RefinementBaseMissing { index, base: refines.base.clone() });
};
if metas[b].sha256.is_none() {
return Err(RoleError::RefinementIdentityMissing { index });
}
if !crate::codecs::admits(&m.capability, &metas[b].capability) {
return Err(RoleError::RefinementBaseNotAdmitted { index, capability: m.capability.clone(), base: metas[b].capability.clone() });
}
for p in refines.producers() {
let ok = by_id(p).filter(|&e| metas[e].is_selectable() || (e < index && metas[e].is_refinement()));
match ok {
None => return Err(RoleError::RefinementPrevMissing { index, producer: p.to_string() }),
Some(e) if metas[e].sha256.is_none() => return Err(RoleError::RefinementIdentityMissing { index }),
Some(_) => {}
}
}
if let Some(selectable) = (index + 1..metas.len()).find(|&j| metas[j].is_selectable()) {
return Err(RoleError::RefinementBeforeSelectable { index, selectable });
}
Ok(())
}
/// Check the component table against SPEC §5 / §7.1, using the capability
/// registry, in component order (the first violation is returned):
///
/// - every `id` is a component id ([`is_component_id`]) and unique
/// ([`RoleError::ComponentIdMalformed`], [`RoleError::DuplicateComponentId`]);
/// - a companion-only capability is carried with `role = "companion"`
/// ([`RoleError::CompanionOnlyAsRendition`]);
/// - a `role = "refinement"` entry obeys *Refinement components*
/// ([`Self::check_refinement`]'s rules); `refines` sits on no other entry, and a
/// refinement-only capability on no other role
/// ([`RoleError::RefinesOnNonRefinement`], [`RoleError::RefinementOnlyAsComponent`]);
///
/// and, for each `role = "companion"` component, SPEC §7.1's three rules:
///
/// 1. its capability is companion-only or unregistered: not a payload
/// ([`RoleError::StandaloneAsCompanion`]); a record or a derivation is no component
/// in any role ([`RoleError::RecordOrDerivationAsComponent`], checked first);
/// 2. `companion_of` is present ([`RoleError::UnboundCompanion`]) and names a
/// capability it applies to ([`RoleError::IncompatibleBase`]); a
/// `companion_of_id` resolves to an entry ([`RoleError::CompanionIdUnresolved`])
/// whose capability is `companion_of`
/// ([`RoleError::CompanionIdCapabilityMismatch`]);
/// 3. a selectable rendition of that capability is present
/// ([`RoleError::UnboundCompanion`]) — with `companion_of_id`, exactly that
/// entry; for an enhancement layer bound by capability alone, exactly one
/// ([`RoleError::EnhancementBaseNotUnique`]); and a base takes at most one
/// enhancement layer, checked by index ([`RoleError::DuplicateEnhancementLayer`]).
///
/// A descriptor companion (HDR dynamic metadata) bound by capability may bind
/// to several renditions of one capability, and several descriptors may bind
/// to one base. A companion whose capability this revision does not register is
/// held to rule 3's presence check only.
pub fn check_component_roles(&self) -> Result<(), RoleError> {
let metas = &self.renditions;
for (index, m) in metas.iter().enumerate() {
if let Some(id) = m.id.as_deref() {
if !is_component_id(id) {
return Err(RoleError::ComponentIdMalformed { index });
}
if let Some(first) = metas[..index].iter().position(|e| e.id.as_deref() == Some(id)) {
return Err(RoleError::DuplicateComponentId { index, first });
}
}
}
for (index, m) in metas.iter().enumerate() {
let capability = m.capability.clone();
// A record or a derivation names no payload format: no component in any role.
if matches!(crate::codecs::capability_kind(&m.capability), Some(crate::codecs::Kind::Record | crate::codecs::Kind::Derivation)) {
return Err(RoleError::RecordOrDerivationAsComponent { index, capability });
}
if m.is_refinement() {
self.check_refinement(index)?;
continue;
}
if m.refines.is_some() {
return Err(RoleError::RefinesOnNonRefinement { index });
}
if crate::codecs::is_refinement_only(&m.capability) {
return Err(RoleError::RefinementOnlyAsComponent { index, capability });
}
let companion_only = crate::codecs::is_companion_only(&m.capability);
if !m.is_companion() {
if companion_only {
return Err(RoleError::CompanionOnlyAsRendition { index, capability });
}
continue;
}
// Rule 1: only a companion-only (or unregistered) capability is a companion; a
// payload is standalone media (a record or a derivation was refused above).
if !companion_only && crate::codecs::capability_kind(&m.capability).is_some() {
return Err(RoleError::StandaloneAsCompanion { index, capability });
}
let Some(target) = m.companion_of.as_deref() else {
return Err(RoleError::UnboundCompanion { index, capability });
};
// Rule 2: the named base is one this companion applies to.
if companion_only && !crate::codecs::companion_applies_to(&m.capability, target) {
let base = target.to_string();
return Err(RoleError::IncompatibleBase { index, capability, base });
}
// Rule 2, by id: the named entry exists and has the named capability.
if let Some(id) = m.companion_of_id.as_deref() {
let Some(b) = metas.iter().position(|e| e.id.as_deref() == Some(id)) else {
return Err(RoleError::CompanionIdUnresolved { index, id: id.to_string() });
};
if metas[b].capability != target {
return Err(RoleError::CompanionIdCapabilityMismatch {
index, id: id.to_string(), capability: metas[b].capability.clone() });
}
// Rule 3: that entry is a selectable rendition.
if !metas[b].is_selectable() {
return Err(RoleError::UnboundCompanion { index, capability });
}
} else {
// Rule 3: the base is present, and unique for an enhancement layer.
let bases = metas.iter()
.filter(|b| b.is_selectable() && b.capability == target)
.count();
if crate::codecs::is_enhancement_layer(&m.capability) {
if bases != 1 {
return Err(RoleError::EnhancementBaseNotUnique { index, capability, matches: bases });
}
} else if bases == 0 {
return Err(RoleError::UnboundCompanion { index, capability });
}
}
// At most one enhancement layer per base, by index.
if crate::codecs::is_enhancement_layer(&m.capability) {
let base_index = self.bound_base(index);
let earlier = (0..index).find(|&e| {
metas[e].is_companion()
&& crate::codecs::is_enhancement_layer(&metas[e].capability)
&& self.bound_base(e).is_some()
&& self.bound_base(e) == base_index
});
if let Some(first) = earlier {
let base = target.to_string();
return Err(RoleError::DuplicateEnhancementLayer { index, capability, base, first });
}
}
}
Ok(())
}
/// A `"1.1"` manifest with a `null` target.
pub fn new(media: impl Into<String>, intent: impl Into<String>, renditions: Vec<RenditionMeta>) -> Self {
Self {
@ -2489,66 +2690,6 @@ impl WaiMulti {
Ok(())
}
/// The index of the base a companion at `index` binds to, when it binds to one
/// entry: the entry `companion_of_id` names, or the one selectable entry of
/// capability `companion_of`.
fn bound_base(&self, index: usize) -> Option<usize> {
let metas = &self.manifest.renditions;
let m = metas.get(index)?;
if let Some(id) = m.companion_of_id.as_deref() {
return metas.iter().position(|b| b.id.as_deref() == Some(id));
}
let target = m.companion_of.as_deref()?;
let mut it = metas.iter().enumerate().filter(|(_, b)| b.is_selectable() && b.capability == target);
match (it.next(), it.next()) {
(Some((i, _)), None) => Some(i),
_ => None,
}
}
/// SPEC §7.1, *Refinement components*, for the entry at `index`, in this order:
/// its capability is refinement-only; it carries `id` and `sha256`; its `refines`
/// is well formed with component ids; `refines.base` names a selectable entry with
/// an `id` and a `sha256`; its capability admits that entry's (SPEC §5, *Base
/// admission*); every producer of `prev` names a selectable entry or a refinement
/// before it, with an `id` and a `sha256`; and it follows every selectable entry.
fn check_refinement(&self, index: usize) -> Result<(), RoleError> {
let metas = &self.manifest.renditions;
let m = &metas[index];
if !crate::codecs::is_refinement_only(&m.capability) {
return Err(RoleError::RefinementNotRefinementOnly { index, capability: m.capability.clone() });
}
if m.id.is_none() || m.sha256.is_none() {
return Err(RoleError::RefinementIdentityMissing { index });
}
let refines = match m.refines.as_ref().map(|v| Refines::from_value(v, RefForm::ComponentId)) {
Some(Ok(r)) => r,
Some(Err(e)) => return Err(RoleError::RefinesMalformed { index: Some(index), detail: e.0 }),
None => return Err(RoleError::RefinesMalformed { index: Some(index), detail: "a refinement carries no refines" }),
};
let by_id = |id: &str| metas.iter().position(|e| e.id.as_deref() == Some(id));
let Some(b) = by_id(&refines.base).filter(|&b| metas[b].is_selectable()) else {
return Err(RoleError::RefinementBaseMissing { index, base: refines.base.clone() });
};
if metas[b].sha256.is_none() {
return Err(RoleError::RefinementIdentityMissing { index });
}
if !crate::codecs::admits(&m.capability, &metas[b].capability) {
return Err(RoleError::RefinementBaseNotAdmitted { index, capability: m.capability.clone(), base: metas[b].capability.clone() });
}
for p in refines.producers() {
let ok = by_id(p).filter(|&e| metas[e].is_selectable() || (e < index && metas[e].is_refinement()));
match ok {
None => return Err(RoleError::RefinementPrevMissing { index, producer: p.to_string() }),
Some(e) if metas[e].sha256.is_none() => return Err(RoleError::RefinementIdentityMissing { index }),
Some(_) => {}
}
}
if let Some(selectable) = (index + 1..metas.len()).find(|&j| metas[j].is_selectable()) {
return Err(RoleError::RefinementBeforeSelectable { index, selectable });
}
Ok(())
}
/// The refinements of the entry at `index`, in table order: each entry with
/// `role: "refinement"` whose `refines.base` names that entry's `id`, and whose
@ -2570,122 +2711,10 @@ impl WaiMulti {
.collect()
}
/// Check the component table against SPEC §5 / §7.1, using the capability
/// registry, in component order (the first violation is returned):
///
/// - every `id` is a component id ([`is_component_id`]) and unique
/// ([`RoleError::ComponentIdMalformed`], [`RoleError::DuplicateComponentId`]);
/// - a companion-only capability is carried with `role = "companion"`
/// ([`RoleError::CompanionOnlyAsRendition`]);
/// - a `role = "refinement"` entry obeys *Refinement components*
/// ([`Self::check_refinement`]'s rules); `refines` sits on no other entry, and a
/// refinement-only capability on no other role
/// ([`RoleError::RefinesOnNonRefinement`], [`RoleError::RefinementOnlyAsComponent`]);
///
/// and, for each `role = "companion"` component, SPEC §7.1's three rules:
///
/// 1. its capability is companion-only or unregistered
/// ([`RoleError::StandaloneAsCompanion`]);
/// 2. `companion_of` is present ([`RoleError::UnboundCompanion`]) and names a
/// capability it applies to ([`RoleError::IncompatibleBase`]); a
/// `companion_of_id` resolves to an entry ([`RoleError::CompanionIdUnresolved`])
/// whose capability is `companion_of`
/// ([`RoleError::CompanionIdCapabilityMismatch`]);
/// 3. a selectable rendition of that capability is present
/// ([`RoleError::UnboundCompanion`]) — with `companion_of_id`, exactly that
/// entry; for an enhancement layer bound by capability alone, exactly one
/// ([`RoleError::EnhancementBaseNotUnique`]); and a base takes at most one
/// enhancement layer, checked by index ([`RoleError::DuplicateEnhancementLayer`]).
///
/// A descriptor companion (HDR dynamic metadata) bound by capability may bind
/// to several renditions of one capability, and several descriptors may bind
/// to one base. A companion whose capability this revision does not register is
/// held to rule 3's presence check only.
/// Check the component table against SPEC §5 / §7.1 ([`ManifestV2::check_component_roles`]).
/// A sink offers the envelope's refinements to staged-delivery §4 only when it passes.
pub fn check_component_roles(&self) -> Result<(), RoleError> {
let metas = &self.manifest.renditions;
for (index, m) in metas.iter().enumerate() {
if let Some(id) = m.id.as_deref() {
if !is_component_id(id) {
return Err(RoleError::ComponentIdMalformed { index });
}
if let Some(first) = metas[..index].iter().position(|e| e.id.as_deref() == Some(id)) {
return Err(RoleError::DuplicateComponentId { index, first });
}
}
}
for (index, m) in metas.iter().enumerate() {
let capability = m.capability.clone();
if m.is_refinement() {
self.check_refinement(index)?;
continue;
}
if m.refines.is_some() {
return Err(RoleError::RefinesOnNonRefinement { index });
}
if crate::codecs::is_refinement_only(&m.capability) {
return Err(RoleError::RefinementOnlyAsComponent { index, capability });
}
let companion_only = crate::codecs::is_companion_only(&m.capability);
if !m.is_companion() {
if companion_only {
return Err(RoleError::CompanionOnlyAsRendition { index, capability });
}
continue;
}
// Rule 1: only a companion-only (or unregistered) capability is a companion.
if !companion_only && crate::codecs::is_known_capability(&m.capability) {
return Err(RoleError::StandaloneAsCompanion { index, capability });
}
let Some(target) = m.companion_of.as_deref() else {
return Err(RoleError::UnboundCompanion { index, capability });
};
// Rule 2: the named base is one this companion applies to.
if companion_only && !crate::codecs::companion_applies_to(&m.capability, target) {
let base = target.to_string();
return Err(RoleError::IncompatibleBase { index, capability, base });
}
// Rule 2, by id: the named entry exists and has the named capability.
if let Some(id) = m.companion_of_id.as_deref() {
let Some(b) = metas.iter().position(|e| e.id.as_deref() == Some(id)) else {
return Err(RoleError::CompanionIdUnresolved { index, id: id.to_string() });
};
if metas[b].capability != target {
return Err(RoleError::CompanionIdCapabilityMismatch {
index, id: id.to_string(), capability: metas[b].capability.clone() });
}
// Rule 3: that entry is a selectable rendition.
if !metas[b].is_selectable() {
return Err(RoleError::UnboundCompanion { index, capability });
}
} else {
// Rule 3: the base is present, and unique for an enhancement layer.
let bases = metas.iter()
.filter(|b| b.is_selectable() && b.capability == target)
.count();
if crate::codecs::is_enhancement_layer(&m.capability) {
if bases != 1 {
return Err(RoleError::EnhancementBaseNotUnique { index, capability, matches: bases });
}
} else if bases == 0 {
return Err(RoleError::UnboundCompanion { index, capability });
}
}
// At most one enhancement layer per base, by index.
if crate::codecs::is_enhancement_layer(&m.capability) {
let base_index = self.bound_base(index);
let earlier = (0..index).find(|&e| {
metas[e].is_companion()
&& crate::codecs::is_enhancement_layer(&metas[e].capability)
&& self.bound_base(e).is_some()
&& self.bound_base(e) == base_index
});
if let Some(first) = earlier {
let base = target.to_string();
return Err(RoleError::DuplicateEnhancementLayer { index, capability, base, first });
}
}
}
Ok(())
self.manifest.check_component_roles()
}
/// All companions (HDR dynamic metadata, enhancement layers) carried in
@ -2815,6 +2844,21 @@ pub fn refinement_indices(entries: &[RenditionMeta], index: usize) -> Vec<usize>
.collect()
}
/// The refinement components whose `refines.base`, read on its own, is the `id` of the
/// entry at `index`, in table order: [`refinement_indices`] without the rest of
/// `refines` read. Where every component conforms (SPEC §7.1) the two agree, since each
/// refinement's `refines` is then well formed; where one does not, these are the
/// refinements a client planning from the head drops and reports
/// (`components_nonconforming`, http-delivery §7 step 4), a malformed `refines` that
/// names the pick among them.
pub fn refinements_naming(entries: &[RenditionMeta], index: usize) -> Vec<usize> {
let Some(base_id) = entries.get(index).and_then(|b| b.id.as_deref()) else { return Vec::new() };
entries.iter().enumerate()
.filter(|(_, m)| m.is_refinement() && m.refines.as_ref().and_then(|v| v.get("base")).and_then(Value::as_str) == Some(base_id))
.map(|(i, _)| i)
.collect()
}
/// Sniff the magic and return which envelope version is in this byte
/// blob. Lets callers branch on `Wai::from_bytes` vs `WaiMulti::from_bytes`
/// without parsing twice.

View file

@ -448,9 +448,9 @@ impl BracketValues {
/// (jwp-receipts §2.4.4).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BuildProfile {
/// Optimised as shipped: cargo's `release` profile at `opt-level` 2 or 3.
/// An optimised build: `opt-level` 2, 3, `s` or `z`, whatever the profile is named.
Release,
/// Any other build.
/// Any other build: `opt-level` 0 or 1.
Debug,
}
@ -471,7 +471,7 @@ impl BuildProfile {
}
/// The profile this crate was built under, as `build.rs` recorded it from
/// cargo's `PROFILE` and `OPT_LEVEL` (`src/build_info.rs`).
/// cargo's `OPT_LEVEL` (`src/build_info.rs`).
pub fn of_this_build() -> BuildProfile {
BuildProfile::from_label(env!("WAI_BUILD_PROFILE")).unwrap_or(BuildProfile::Debug)
}
@ -859,7 +859,7 @@ mod tests {
assert_eq!(BuildProfile::from_label("debug"), Some(BuildProfile::Debug));
assert_eq!(BuildProfile::from_label("Release"), None);
// This test binary is built under the dev profile at opt-level 0.
if cfg!(debug_assertions) && crate::build_info::profile_label("debug", "0") == "debug" {
if cfg!(debug_assertions) && crate::build_info::profile_label("0") == "debug" {
assert_eq!(BuildProfile::of_this_build().label(), env!("WAI_BUILD_PROFILE"));
}
let r = BuildRecord::of_this_build();

View file

@ -61,11 +61,14 @@ pub enum WhyWhole {
DigestMismatch { index: usize },
/// §7: the planned base ranges reach the policy's share of the rest.
Threshold,
/// §7 step 4: a refinement of an envelope whose components do not all conform (SPEC
/// §7.1): dropped, never requested.
ComponentsNonconforming { index: usize },
}
impl WhyWhole {
/// `below_one_chunk`, `single_component`, `layer`, `component_digest_absent`,
/// `component_digest_mismatch` or `range_threshold`.
/// `component_digest_mismatch`, `range_threshold` or `components_nonconforming`.
pub fn code(self) -> &'static str {
match self {
WhyWhole::BelowOneChunk => "below_one_chunk",
@ -74,6 +77,7 @@ impl WhyWhole {
WhyWhole::DigestAbsent { .. } => "component_digest_absent",
WhyWhole::DigestMismatch { .. } => "component_digest_mismatch",
WhyWhole::Threshold => "range_threshold",
WhyWhole::ComponentsNonconforming { .. } => "components_nonconforming",
}
}
}

View file

@ -16,7 +16,7 @@ use super::head_proof::HeadProof;
use super::plan::{plan_components, plan_head, rest_of, Class, FetchPolicy, Plan, WhyWhole};
use super::{ByteRange, Fragment, RangeRefusal};
use crate::container::{
companion_indices, refinement_indices, select_entries, ManifestV2, PinRefusal, Prior, RenditionMeta, Skip, Wai,
companion_indices, refinements_naming, select_entries, ManifestV2, PinRefusal, Prior, RenditionMeta, Skip, Wai,
WaiMulti,
};
@ -184,7 +184,11 @@ impl Selector {
}
/// The companions and refinements of entry `index` the sink applies, with their
/// classes: an enhancement layer or a refinement is a refinement (§10.1).
/// classes: an enhancement layer or a refinement is a refinement (§10.1). A
/// refinement is one whose `refines.base`, read on its own, is the entry's `id`
/// ([`refinements_naming`]): of a conforming envelope, exactly those whose `refines` is
/// well formed; of one that does not conform, each is dropped and reported (§7 step 4),
/// one whose `refines` is otherwise malformed included.
fn applied(&self, m: &ManifestV2, index: usize) -> Vec<(usize, Class)> {
let e = &m.renditions;
let mut out: Vec<(usize, Class)> = companion_indices(e, index)
@ -192,11 +196,21 @@ impl Selector {
.filter(|&i| (self.applies)(&e[i]))
.map(|i| (i, if crate::codecs::is_enhancement_layer(&e[i].capability) { Class::Refinement } else { Class::Base }))
.collect();
out.extend(refinement_indices(e, index).into_iter().filter(|&i| (self.applies)(&e[i])).map(|i| (i, Class::Refinement)));
out.extend(refinements_naming(e, index).into_iter().filter(|&i| (self.applies)(&e[i])).map(|i| (i, Class::Refinement)));
out
}
}
/// The refinement components among `wanted` that an envelope whose components do not all
/// conform (SPEC §7.1, `ManifestV2::check_component_roles` over the head's manifest) does
/// not offer: every one of them where it does not conform, none where it does.
fn nonconforming_refinements(m: &ManifestV2, wanted: &[(usize, Class)]) -> Vec<usize> {
if m.check_component_roles().is_ok() {
return Vec::new();
}
wanted.iter().map(|&(i, _)| i).filter(|&i| m.renditions.get(i).is_some_and(|e| e.is_refinement())).collect()
}
/// Whether `s` is one or more ASCII digits: the `1*DIGIT` of RFC 9110 §14.4. No sign,
/// separator, space or other script's digit.
fn digits(s: &str) -> Option<u64> {
@ -659,9 +673,18 @@ impl FetchSession {
let (index, skipped) = self.sel.select(&head, &m, |_, _| true).map_err(RangeRefusal::Selection)?;
let mut wanted = vec![(index, Class::Base)];
wanted.extend(self.sel.applied(&m, index));
// §7 step 4: an envelope whose components do not all conform offers none of
// its refinements (SPEC §7.1); each is dropped, and one stating no digest is
// dropped for that too (step 5).
let nonconforming = nonconforming_refinements(&m, &wanted);
let present: Vec<bool> = m.renditions.iter().map(|e| e.sha256.is_some()).collect();
let held: Vec<ByteRange> = self.held.iter().map(|(r, _)| *r).collect();
let plan: Plan = plan_components(&head, &wanted, &present, &held, &self.policy);
let planned: Vec<(usize, Class)> =
wanted.iter().copied().filter(|(i, _)| !nonconforming.contains(i) || !present[*i]).collect();
let mut plan: Plan = plan_components(&head, &planned, &present, &held, &self.policy);
if plan.whole_rest.is_none() {
plan.dropped.extend(nonconforming.iter().map(|&i| (i, WhyWhole::ComponentsNonconforming { index: i })));
}
self.manifest = Some(m);
match plan.whole_rest {
Some((_, why)) => self.to_rest(&head, why, Class::Base),
@ -846,7 +869,17 @@ impl FetchSession {
let digest_ok = |i: usize, e: &RenditionMeta| payloads.get(i).is_some_and(|p| e.payload_digest_matches(p));
let (index, skipped) = self.sel.select(&head, &w.manifest, digest_ok).map_err(RangeRefusal::Selection)?;
let (mut companions, mut refinements, mut dropped) = (Vec::new(), Vec::new(), Vec::new());
for (i, class) in self.sel.applied(&w.manifest, index) {
let applied = self.sel.applied(&w.manifest, index);
let nonconforming = nonconforming_refinements(&w.manifest, &applied);
for (i, class) in applied {
// §7 step 4: not offered from an envelope that does not conform; one stating
// no digest is dropped for that too, as on the range path.
if nonconforming.contains(&i) {
if w.manifest.renditions[i].sha256.is_none() {
dropped.push((i, WhyWhole::DigestAbsent { index: i }));
}
continue;
}
// A refinement that states no digest is dropped on this path as on the
// range path (§7 step 5, §10.3).
if class == Class::Refinement && w.manifest.renditions[i].sha256.is_none() {
@ -866,6 +899,7 @@ impl FetchSession {
refinements.push((i, w.renditions[i].clone()));
}
}
dropped.extend(nonconforming.iter().map(|&i| (i, WhyWhole::ComponentsNonconforming { index: i })));
base(index, w.renditions[index].clone(), companions, refinements, skipped, dropped, head, why)
}
};

View file

@ -1116,7 +1116,9 @@ fn base_ready_before_refinements() {
assert_eq!(s.done().unwrap().companions.len(), 1);
}
/// N4: a refinement with no `sha256` is dropped on the whole path as on the range path.
/// N4: a refinement with no `sha256` is dropped on the whole path as on the range path —
/// for that (§7 step 5) and, since it leaves its envelope nonconforming (SPEC §7.1), for
/// that too (§7 step 4).
#[test]
fn refinement_without_digest_dropped_on_every_path() {
let (b, r) = (pattern(20_000, 7, 3), pattern(2000, 5, 1));
@ -1133,7 +1135,11 @@ fn refinement_without_digest_dropped_on_every_path() {
sink(&["wai.video.int_motion"], &["wai.video.int_refine"], None));
let f = drive(&mut s, |w| served.answer(w)).unwrap();
assert_eq!(f.why_whole, why, "{permille}");
assert_eq!(f.dropped.iter().map(|(i, w)| (*i, w.code())).collect::<Vec<_>>(), vec![(1, "component_digest_absent")], "{permille}");
assert_eq!(
f.dropped.iter().map(|(i, w)| (*i, w.code())).collect::<Vec<_>>(),
vec![(1, "component_digest_absent"), (1, "components_nonconforming")],
"{permille}"
);
assert!(f.refinements.is_empty());
}
}

View file

@ -319,6 +319,7 @@ fn session_body(metered: bool) -> ClaimBody {
capability: "wai.neural.int_hyper".into(),
rendition: None,
object: subject(),
first_unit: 0,
path: SessionPath::Primary,
decoder: "builtin:[email protected]".into(),
variant: None,

View file

@ -346,10 +346,11 @@ 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. The output tree is also what a session
// claim commits to (jwp-receipts §2.4.12), so `receipt_claims` builds it too.
// claim commits to (jwp-receipts §2.4.12), and a session verifier holding envelopes
// counts them, so `receipt_claims` builds both.
#[cfg(any(feature = "stage_measure", feature = "receipt_claims"))]
pub mod output_tree;
#[cfg(feature = "stage_measure")]
#[cfg(any(feature = "stage_measure", feature = "receipt_claims"))]
pub mod measure_framing;
#[cfg(feature = "stage_measure")]
pub mod stage_measure;

View file

@ -4,16 +4,30 @@
//! 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
//! sets it observes; 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, the classical length — 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.
//!
//! A sink tells the recorder what it received, and never an object's figures: the bytes
//! of an object it received whole ([`SessionRecorder::received_envelope`]) — an envelope,
//! an encrypted one, or bytes that are not one — from which the recorder derives its
//! content hash and counted and transferred bytes, and holds it, so `finish` checks the
//! claim against it as a verifier holding the envelopes does (§2.4.12 step 6, the
//! `envelopes` context); or the content hash and the bytes of an object it did not hold
//! whole ([`SessionRecorder::received_unheld`]), which is not countable. The session
//! accounts for every byte transferred. Whether an object was presented is derived from
//! the decoder entries, and an object received more than once on a path is one object,
//! its `transferred` every byte received for it. The classical length,
//! `classical_declared`, is derived at the tally from the `WAI2` envelopes the recorder
//! holds, readable or encrypted, exactly as §2.4.12 rule 9 counts them
//! ([`crate::claim_receipt::session::classical_length`]): a sink never states it.
use crate::claim_receipt::{
session::SESSION_CLASSICAL_SOURCES, ClaimBody, ClaimClass, ClaimReceipt, DecoderEntry, Missed, ObjectSeen,
session::{classical_length, SESSION_CLASSICAL_SOURCES}, ClaimBody, ClaimClass, ClaimReceipt, DecoderEntry, Missed, ObjectSeen,
Rejection, SessionClaim, SessionEnergy, SessionHost, SessionPath, StageSetRef, SubjectType, Switch,
};
use crate::output_tree::{self, Presentation};
@ -33,6 +47,9 @@ pub struct DecoderKey {
/// The content hash of the object decoded from: the `WAI2` envelope whose rendition it
/// is, or the `WAI1` envelope.
pub object: [u8; 32],
/// The title's unit that is unit 0 of the object's base (§2.4.12): of the object for
/// a base or a rendition, of the base it refines for a layer.
pub first_unit: u64,
pub path: SessionPath,
pub decoder: String,
pub variant: Option<String>,
@ -84,6 +101,8 @@ pub struct RecordedSession {
pub objects: Vec<ObjectSeen>,
/// The wire forms of the `stage` claims the session names.
pub stage_claims: Vec<String>,
/// The envelopes the recorder held, in the order it received them.
pub envelopes: Vec<Vec<u8>>,
}
/// One session at a sink (§2.4.12).
@ -101,12 +120,13 @@ pub struct SessionRecorder {
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>,
/// The envelopes received whole, each once.
held: Vec<Vec<u8>>,
}
impl SessionRecorder {
@ -126,11 +146,11 @@ impl SessionRecorder {
switches: Vec::new(),
objects: Vec::new(),
receipt_bytes: 0,
classical: None,
stage_sets: BTreeSet::new(),
stage_signer: None,
stage_claims: Vec::new(),
energy: None,
held: Vec::new(),
}
}
@ -145,9 +165,45 @@ impl SessionRecorder {
}
}
/// An object received, whole or as byte ranges.
pub fn received(&mut self, seen: ObjectSeen) {
self.objects.push(seen);
/// An object received whole on `path`, as its bytes: a `WAI1` or `WAI2` envelope, its
/// payloads plaintext or encrypted, or bytes that are not an envelope at all. Every one
/// is recorded, so the session accounts for every byte transferred. The recorder
/// derives its content hash, its counted bytes (staged-measurement §3; none where it is
/// not countable, as an encrypted envelope and bytes that are not one never are) and its
/// length, and at the tally whether a decoder entry names it. It holds the bytes, and
/// [`SessionRecorder::finish`] checks the claim against every envelope it holds
/// (§2.4.12 step 6): an encrypted envelope's manifest and rendition table are in the
/// clear, so a `WAI2` one's classical rendition counts in the classical length the
/// tally derives, as a plaintext one's does; an encrypted envelope, or bytes that are
/// not one, no entry names is an object received, neither counted nor presented.
/// Received again on the same path, it is the same object, its `transferred` the bytes
/// of both receipts.
pub fn received_envelope(&mut self, envelope: &[u8], path: SessionPath) {
let content_hash = crate::jwp_hash::content_hash(envelope);
if !self.held.iter().any(|h| crate::jwp_hash::content_hash(h) == content_hash) {
self.held.push(envelope.to_vec());
}
let counted = crate::measure_framing::count_object(envelope).ok().map(|c| c.total);
self.add_object(content_hash, path, counted, envelope.len() as u64);
}
/// An object received on `path` that the sink did not hold whole — as byte ranges, or
/// bytes it did not keep — of `transferred` bytes: its content hash only, so it is not
/// countable. Received again on the same path, it is the same object.
pub fn received_unheld(&mut self, content_hash: [u8; 32], path: SessionPath, transferred: u64) {
self.add_object(content_hash, path, None, transferred);
}
fn add_object(&mut self, content_hash: [u8; 32], path: SessionPath, counted: Option<u64>, transferred: u64) {
match self.objects.iter_mut().find(|o| o.content_hash == content_hash && o.path == path) {
Some(o) => {
o.transferred = o.transferred.saturating_add(transferred);
if o.counted.is_none() {
o.counted = counted;
}
}
None => self.objects.push(ObjectSeen { content_hash, path, counted, transferred, presented: false }),
}
}
/// Bytes of receipts and claims received.
@ -216,12 +272,6 @@ impl SessionRecorder {
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>) {
@ -259,6 +309,7 @@ impl SessionRecorder {
capability: k.capability.clone(),
rendition: k.rendition,
object: k.object,
first_unit: k.first_unit,
path: k.path,
decoder: k.decoder.clone(),
variant: k.variant.clone(),
@ -285,9 +336,14 @@ impl SessionRecorder {
missed_units.sort_by_key(|m| (m.unit, m.layers));
let missed = missed_units.len() as u64;
missed_units.truncate(MAX_MISSED);
let objects = self.objects_seen();
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)
objects.iter().filter(|o| f(o)).filter_map(|o| o.counted).fold(0u64, u64::saturating_add)
};
// The classical length, as §2.4.12 rule 9 reads it from the envelopes held: a
// length past u64 cannot be stated, and the check at the seal refuses it.
let held: Vec<&[u8]> = self.held.iter().map(Vec::as_slice).collect();
let classical = classical_length(&held, &decoders).map(|n| u64::try_from(n).unwrap_or(u64::MAX));
let body = SessionClaim {
session_nonce: self.session_nonce,
media: self.media.clone(),
@ -308,14 +364,24 @@ impl SessionRecorder {
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),
uncounted: objects.iter().filter(|o| o.counted.is_none()).count() as u64,
transferred: 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()),
classical_declared: classical,
classical_source: classical.map(|_| SESSION_CLASSICAL_SOURCES[0].to_owned()),
energy: self.energy.clone(),
};
(body, ps, self.objects.clone())
(body, ps, objects)
}
/// The objects received, each one's `presented` derived from the presentations:
/// whether a presentation's decoder names it.
fn objects_seen(&self) -> Vec<ObjectSeen> {
let mut objects = self.objects.clone();
for o in &mut objects {
o.presented = self.presentations.iter().any(|p| self.keys[p.2].object == o.content_hash);
}
objects
}
/// Tally the record ([`SessionRecorder::tally`]) and seal it with [`seal_checked`]. The
@ -332,21 +398,25 @@ impl SessionRecorder {
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)
let envelopes = self.held;
seal_checked(body, ps, objects, self.stage_claims, envelopes, 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.
/// Seal a session body and check the claim against the presentations, the objects, the
/// envelopes it holds (none, or every one it received as bytes) 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 decoder entry naming a capability its envelope
/// does not give, for one, is refused (`session_source_mismatch`). 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>,
envelopes: Vec<Vec<u8>>,
subject: [u8; 32],
key: &SigningKey,
signer_id: &str,
@ -357,9 +427,13 @@ pub fn seal_checked(
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)?;
if !envelopes.is_empty() {
let refs: Vec<&[u8]> = envelopes.iter().map(Vec::as_slice).collect();
claim.check_session_envelopes(&refs, Some(&objects), Some(&presentations))?;
}
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 })
Ok(RecordedSession { claim, presentations, objects, stage_claims, envelopes })
}
#[cfg(test)]
@ -377,6 +451,7 @@ mod tests {
capability: capability.into(),
rendition: None,
object,
first_unit: 0,
path,
decoder: "builtin:[email protected]".into(),
variant: None,
@ -412,11 +487,13 @@ mod tests {
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);
// Objects it did not hold whole: neither countable, `presented` derived; [6] was
// received twice and never presented.
r.received_unheld([1; 32], SessionPath::Primary, 5_000);
r.received_unheld([2; 32], SessionPath::Primary, 700);
r.received_unheld([6; 32], SessionPath::Primary, 300);
r.received_unheld([6; 32], SessionPath::Primary, 300);
r.received_unheld([3; 32], SessionPath::Fallback, 3_000);
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();
@ -424,8 +501,10 @@ mod tests {
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.counted_primary, b.counted_fallback, b.counted_discarded, b.uncounted, b.transferred), (0, 0, 0, 4, 9_300));
assert_eq!(s.objects.iter().map(|o| (o.content_hash[0], o.presented, o.transferred)).collect::<Vec<_>>(),
[(1, true, 5_000), (2, true, 700), (6, false, 600), (3, true, 3_000)], "a repeat is one object; presented derived");
assert_eq!((b.classical_declared, b.classical_source.as_deref()), (None, None), "no WAI2 envelope held: no classical length");
assert_eq!(b.output_root, output_tree::root(&s.presentations).unwrap());
s.claim.verify().unwrap();
s.claim.check_session_outputs(&s.presentations, &|_| None).unwrap();
@ -444,7 +523,8 @@ mod tests {
/// 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.
/// bytes changed, an object listed twice, an object marked not presented that an entry
/// names. Each passes steps 1–4.
#[test]
fn cannot_emit_inconsistent() {
let k = SigningKey::from_bytes(&[0x44; 32]);
@ -455,10 +535,10 @@ mod tests {
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 });
r.received_unheld([1; 32], SessionPath::Primary, 5_000);
r.received_unheld([2; 32], SessionPath::Primary, 700);
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));
let seal = |b: SessionClaim, o: Vec<ObjectSeen>| seal_checked(b, ps.clone(), o, vec![], 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;
@ -469,6 +549,12 @@ mod tests {
let mut fewer = objects.clone();
fewer[0].counted = Some(4_999);
assert_eq!(seal(body.clone(), fewer), Err((6, Reason::SessionCountedMismatch)));
let mut twice = objects.clone();
twice.push(objects[1].clone());
assert_eq!(seal(body.clone(), twice), Err((6, Reason::SessionObjectDuplicate)));
let mut unpresented = objects.clone();
unpresented[1].presented = false;
assert_eq!(seal(body.clone(), unpresented), Err((6, Reason::SessionPresentedMismatch)));
// An entry naming an object the sink did not receive.
let mut elsewhere = body.clone();
elsewhere.decoders[1].object = [8; 32];
@ -479,6 +565,113 @@ mod tests {
assert_eq!(seal(bad, objects), Err((4, Reason::SessionDecoderMalformed)));
}
/// A recorder handed the envelopes themselves derives each object's figures — counted
/// and transferred bytes from the bytes, `presented` from the presentations, so an
/// envelope received and never presented counts as discarded, and a repeat is one
/// object with the bytes of both — and checks the claim against the envelopes before it
/// leaves the sink, as a verifier holding them does: a decoder entry naming a capability
/// its envelope does not give (`session_source_mismatch`), a presentation at a layer
/// count its envelope does not give (`session_layers_mismatch`) or of a unit it does not
/// cover (`session_unit_outside_object`) is refused there. Bytes that are not an envelope
/// are refused when received; an encrypted envelope no entry names is held, uncounted.
#[test]
fn a_recorder_holding_its_envelopes_checks_them() {
let wai = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
let read = |p: &str| std::fs::read(wai.join(p)).unwrap_or_else(|e| panic!("{p}: {e}"));
let base = read("staged-conformance/cases/image_int_hyper_2l/base.wai");
let l1 = read("staged-measure-conformance/fixtures/still/L01.wai");
let l2 = read("staged-measure-conformance/fixtures/still/L02.wai");
let encrypted = read("staged-measure-conformance/counting/encrypted.wai");
let hash = crate::jwp_hash::content_hash;
let k = SigningKey::from_bytes(&[0x44; 32]);
// `base_capability` for the base's entry; `extra` a presentation (unit, layers, entry)
// added to the honest ones.
let record = |base_capability: &str, extra: Option<(u64, u16, usize)>| {
let mut r = SessionRecorder::new([7; 32], "image", host(), None);
let b = r.decoder(key(base_capability, SessionPath::Primary, hash(&base)));
let l = r.decoder(key("wai.image.int_refine", SessionPath::Primary, hash(&l1)));
r.presented(0, 1, b, d(0, 1), None).unwrap();
r.presented(0, 2, l, d(0, 2), None).unwrap();
if let Some((u, layers, entry)) = extra {
r.presented(u, layers, [b, l][entry], d(u, layers), None).unwrap();
}
for e in [base.as_slice(), &l1, &l2, &l2, &encrypted, &base[..7], b"not an envelope at all"] {
r.received_envelope(e, SessionPath::Primary);
}
r
};
let s = record("wai.neural.int_hyper", None).finish(hash(&base), &k, "did:example:sink", 0, None, T).unwrap();
assert_eq!(s.objects.iter().map(|o| o.presented).collect::<Vec<_>>(), [true, true, false, false, false, false], "presented: named by an entry");
let want: Vec<(Option<u64>, u64)> = vec![
(Some(base.len() as u64), base.len() as u64),
(Some(l1.len() as u64), l1.len() as u64),
(Some(l2.len() as u64), 2 * l2.len() as u64),
(None, encrypted.len() as u64),
(None, 7),
(None, 22),
];
assert_eq!(
s.objects.iter().map(|o| (o.counted, o.transferred)).collect::<Vec<_>>(),
want,
"received twice: one object; encrypted, and bytes that are not an envelope: uncounted, every byte transferred"
);
let ClaimBody::ReconstructSession(body) = &s.claim.body else { panic!("a session") };
assert_eq!((body.counted_discarded, body.uncounted), (l2.len() as u64, 3), "received and never presented");
assert_eq!(body.transferred, (base.len() + l1.len() + 2 * l2.len() + encrypted.len() + 7 + 22) as u64, "every byte transferred");
let refs: Vec<&[u8]> = s.envelopes.iter().map(Vec::as_slice).collect();
assert_eq!(refs.len(), 6, "each object held once");
s.claim.check_session_envelopes(&refs, Some(&s.objects), Some(&s.presentations)).unwrap();
let refused = |r: SessionRecorder| r.finish(hash(&base), &k, "did:example:sink", 0, None, T).map(|_| ()).map_err(|e| (e.step, e.reason));
assert_eq!(refused(record("wai.audio.int_codec", None)), Err((6, Reason::SessionSourceMismatch)));
assert_eq!(refused(record("wai.neural.int_hyper", Some((0, 3, 1)))), Err((6, Reason::SessionLayersMismatch)), "the layer gives two layers");
assert_eq!(refused(record("wai.neural.int_hyper", Some((1, 1, 0)))), Err((6, Reason::SessionUnitOutsideObject)), "the base holds one unit");
}
/// The classical length is the recorder's, never the sink's: from the `WAI2` envelopes
/// it holds, plaintext or encrypted — whose rendition tables are in the clear — each
/// one's classical rendition no entry decoded counts its table's length, as §2.4.12
/// rule 9 reads it, so a session that received one seals and verifies against its
/// envelopes.
#[test]
fn a_recorder_derives_the_classical_length_from_its_envelopes() {
let wai = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
let read = |p: &str| std::fs::read(wai.join(p)).unwrap_or_else(|e| panic!("{p}: {e}"));
let base = read("staged-conformance/cases/image_int_hyper_2l/base.wai");
let plain = read("staged-measure-conformance/counting/wai2-classical.wai");
let encrypted = read("staged-measure-conformance/counting/wai2-classical-encrypted.wai");
let png = |w: &[u8]| crate::container::WaiMulti::from_bytes(w).expect("a WAI2 envelope").renditions[1].len() as u64;
assert_eq!(png(&plain), png(&encrypted), "one table, in the clear either way");
let hash = crate::jwp_hash::content_hash;
let k = SigningKey::from_bytes(&[0x44; 32]);
// The base presented, each of `received` received whole; with `decode_png`, the
// plaintext WAI2's PNG rendition decoded too.
let seal = |received: &[&[u8]], decode_png: bool| {
let mut r = SessionRecorder::new([7; 32], "image", host(), None);
let b = r.decoder(key("wai.neural.int_hyper", SessionPath::Primary, hash(&base)));
r.presented(0, 1, b, d(0, 1), None).unwrap();
if decode_png {
let p = r.decoder(DecoderKey { rendition: Some(1), ..key("wai.image.png", SessionPath::Primary, hash(&plain)) });
r.presented(1, 1, p, d(1, 1), None).unwrap();
}
r.received_envelope(&base, SessionPath::Primary);
for e in received {
r.received_envelope(e, SessionPath::Primary);
}
let s = r.finish(hash(&base), &k, "did:example:sink", 0, None, T).expect("the session seals");
let refs: Vec<&[u8]> = s.envelopes.iter().map(Vec::as_slice).collect();
s.claim.check_session_envelopes(&refs, Some(&s.objects), Some(&s.presentations)).unwrap();
let ClaimBody::ReconstructSession(body) = &s.claim.body else { panic!("a session") };
(body.classical_declared, body.classical_source.clone())
};
let table = |n: u64| (Some(n), Some("wai2-table".to_owned()));
assert_eq!(seal(&[], false), (None, None), "no WAI2 held");
assert_eq!(seal(&[&plain], false), table(png(&plain)), "a plaintext WAI2");
assert_eq!(seal(&[&encrypted], false), table(png(&encrypted)), "an encrypted WAI2: its table is in the clear");
assert_eq!(seal(&[&plain, &encrypted], false), table(2 * png(&plain)), "both");
assert_eq!(seal(&[&plain, &plain], false), table(png(&plain)), "received twice, held once");
assert_eq!(seal(&[&plain], true), (None, None), "its classical rendition decoded: the session needed it");
}
/// 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 {
@ -507,7 +700,7 @@ mod tests {
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.received_unheld(stage.subject, SessionPath::Primary, 4_000);
r.stage_set(0, stage).unwrap();
r
}

View file

@ -4,11 +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.
//! presentation with its output's digest; every object of the title received whole, as its
//! bytes, so the recorder derives its figures and checks the claim against the envelopes
//! when it seals it. 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::claim_receipt::{SessionHost, SessionPath};
use crate::codecs::DeclaredTier;
use crate::container::Wai;
use crate::session_recorder::{DecoderKey, SessionRecorder};
@ -59,10 +60,20 @@ pub fn recorder_of(t: &Title, e: &Evaluation, host: SessionHost, session_nonce:
.token()
.to_owned()
});
// The title's unit that is unit 0 of the object's base: the base's own, for a layer
// too.
let first_unit = e
.objects
.iter()
.find(|o| o.content_hash == *object)
.and_then(|o| e.bases.get(o.base))
.map(|(d, _, _)| d.info.first_unit)
.ok_or_else(|| invalid(format!("{}: a decoder names an object the title does not hold", t.dir)))?;
handles.push(r.decoder(DecoderKey {
capability: capability.clone(),
rendition: None,
object: *object,
first_unit,
path: SessionPath::Primary,
decoder: builtin_decoder(),
variant: None,
@ -76,13 +87,9 @@ pub fn recorder_of(t: &Title, e: &Evaluation, host: SessionHost, session_nonce:
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,
});
let held = t.bases.iter().map(|b| &b.obj).chain(&t.layers).find(|x| x.path == o.path);
let bytes = held.ok_or_else(|| invalid(format!("{}: {} is not one of the title's objects", t.dir, o.path)))?;
r.received_envelope(&bytes.bytes, SessionPath::Primary);
}
Ok((r, first.0.info.content_hash))
}
@ -233,6 +240,10 @@ mod tests {
s.claim.verify().unwrap();
s.claim.check_session_outputs(&s.presentations, &|_| None).unwrap();
s.claim.check_session_objects(&s.objects).unwrap();
assert_eq!(s.envelopes.len(), e.objects.len(), "{name}: the recorder holds every object it received");
let held: Vec<&[u8]> = s.envelopes.iter().map(Vec::as_slice).collect();
s.claim.check_session_envelopes(&held, Some(&s.objects), Some(&s.presentations)).unwrap();
assert!(s.objects.iter().all(|o| o.presented), "{name}: every object of a completion is presented");
assert_eq!(super::super::run::completion(&t).unwrap(), c.leaf_count, "{name}: one completion is every stage");
}
}

View file

@ -487,7 +487,7 @@ struct Replayed {
/// 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::claim_receipt::{SessionHost, SessionPath};
use crate::energy_report::{BuildProfile, BuildRecord};
use crate::session_recorder::{DecoderKey, SessionRecorder};
let key = ed25519_dalek::SigningKey::from_bytes(&[0x44; 32]);
@ -619,15 +619,22 @@ fn replay_session(dir: &std::path::Path) -> Option<Replayed> {
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(),
// The capability, and the title's unit that is unit 0 of the object's base.
let (capability, first_unit) = match bases.iter().find(|(info, _, _)| info.content_hash == *source) {
Some((info, _, _)) => (info.capability.clone(), info.first_unit),
None => {
let w = files.values().find(|f| chash(f) == *source).and_then(|f| Wai::from_bytes(f).ok()).expect("a layer");
let base = w.refines().and_then(Result::ok).map(|r| r.base).expect("a layer refines a base");
let first = bases.iter().find(|(info, _, _)| hex(&info.content_hash) == base).map_or(0, |(info, _, _)| info.first_unit);
(w.capability().to_owned(), first)
}
};
let h = *handles.entry((capability.clone(), *source)).or_insert_with(|| {
rec.decoder(DecoderKey {
capability,
rendition: None,
object: *source,
first_unit,
path: SessionPath::Primary,
decoder: "builtin:[email protected]".into(),
variant: None,
@ -638,12 +645,13 @@ fn replay_session(dir: &std::path::Path) -> Option<Replayed> {
});
rec.presented(unit, layers, h, crate::pin::sha256(format!("{unit}/{layers}").as_bytes()), None).expect("one presentation each");
}
// Objects the recorder is not handed whole: uncounted, `presented` derived.
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 });
rec.received_unheld(chash(env), SessionPath::Primary, env.len() as u64);
}
for (h, (bytes, applied)) in &arrived {
rec.received(ObjectSeen { content_hash: *h, path: SessionPath::Primary, counted: None, transferred: *bytes, presented: *applied });
for (h, (bytes, _applied)) in &arrived {
rec.received_unheld(*h, SessionPath::Primary, *bytes);
}
let mut claims = 0;
for (n, (i, epoch, chain, upto, delivered)) in ended.iter().enumerate() {
@ -706,7 +714,7 @@ fn every_corpus_case_seals_an_honest_session() {
/// being bounded by nothing else.
#[test]
fn the_forget_vectors_are_what_the_sink_states() {
use crate::claim_receipt::{ObjectSeen, SessionHost, SessionPath};
use crate::claim_receipt::{SessionHost, SessionPath};
use crate::energy_report::{BuildProfile, BuildRecord};
use crate::session_recorder::{DecoderKey, SessionRecorder};
let key = ed25519_dalek::SigningKey::from_bytes(&[0x44; 32]);
@ -758,6 +766,7 @@ fn the_forget_vectors_are_what_the_sink_states() {
capability: capability.into(),
rendition: None,
object,
first_unit: 0,
path: SessionPath::Primary,
decoder: "builtin:[email protected]".into(),
variant: None,
@ -773,7 +782,7 @@ fn the_forget_vectors_are_what_the_sink_states() {
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 });
rec.received_envelope(e, SessionPath::Primary);
}
for c in named {
rec.stage_set(0, c).unwrap();

View file

@ -127,6 +127,33 @@ fn pack_then_explode_is_the_identity() {
}
}
/// `wai stage explode` gives no layer of a `WAI2` that does not conform (SPEC §7.1): not a
/// refinement placed before a selectable rendition, not a component of a refinement-only
/// capability that is not marked a refinement (with `refines` or without); a base of
/// such an envelope, and a layer of one that conforms, explode as before.
#[test]
fn explode_gives_no_layer_of_a_nonconforming_envelope() {
let refused = |path: &Path, i: usize, why: &str| {
let out = scratch("refused_layer.wai");
let o = wai(&[os("stage"), os("explode"), path.as_os_str(), os(&i.to_string()), os("-o"), out.as_os_str()]);
assert!(!o.status.success() && text(&o).contains("no refinement is offered"), "{why}: {}", text(&o));
assert!(!out.exists(), "{why}: nothing written");
};
refused(&corpus("wai2/refinement_before_selectable.wai"), 1, "a refinement before a selectable rendition");
refused(&corpus("wai2/refinement_only_as_component.wai"), 1, "a refinement-only capability with no role and no refines");
// The conforming envelope's refinement with its role taken away, its refines kept.
let mut w = wai::container::WaiMulti::from_bytes(&std::fs::read(corpus("wai2/ok_explode.wai")).unwrap()).unwrap();
w.manifest.renditions[1].role = None;
let roleless = scratch("roleless_with_refines.wai");
std::fs::write(&roleless, w.to_bytes().unwrap()).unwrap();
refused(&roleless, 1, "a refinement-only capability with refines and no role");
for (path, i) in [(corpus("wai2/refinement_before_selectable.wai"), 0), (corpus("wai2/ok_explode.wai"), 1)] {
let out = scratch("exploded_ok.wai");
let o = wai(&[os("stage"), os("explode"), path.as_os_str(), os(&i.to_string()), os("-o"), out.as_os_str()]);
assert!(o.status.success() && out.exists(), "{} {i}: {}", path.display(), text(&o));
}
}
/// `wai wrap` writes no `refines`, so it refuses a refinement capability and writes
/// nothing.
#[test]
@ -302,7 +329,7 @@ fn apply_json_replays_the_corpus() {
let mut names: Vec<String> = std::fs::read_dir(&root).unwrap().flatten().map(|e| e.file_name().to_string_lossy().into_owned()).collect();
names.sort();
let default_cfg = serde_json::json!({"bound": {"max_envelopes": 16, "max_age_ms": 1000}, "cost_limit": null, "budget": 1u64 << 30, "record_budget": 1u64 << 20, "live": null});
let (mut same, mut skipped, mut extra) = (0, 0, 0);
let (mut same, mut skipped, mut extra, mut routed_as_base) = (0, 0, 0, 0);
let mut differ: Vec<String> = Vec::new();
for name in names {
let d = root.join(&name);
@ -329,6 +356,24 @@ fn apply_json_replays_the_corpus() {
skipped += 1;
continue;
}
// An envelope the case offers to §4 whose capability is not refinement-only: the
// tool routes by capability (staged-delivery §3, SPEC §4), so it reads it as a base,
// a `refines` it carries ignored, and reports a base step; the case offers it to §4
// directly, which refuses it (`capability-not-refinement`).
let routed = steps.iter().position(|s| {
s["op"] == "offer"
&& wai::container::Wai::from_bytes(&std::fs::read(d.join(s["file"].as_str().unwrap())).unwrap())
.is_ok_and(|w| !wai::codecs::is_refinement_only(w.capability()))
});
if let Some(i) = routed {
let o = wai(&[os("stage"), os("apply"), os("--json"), d.join("base.wai").as_os_str(), d.join(steps[i]["file"].as_str().unwrap()).as_os_str()]);
let got: serde_json::Value = serde_json::from_slice(&o.stdout).unwrap_or_default();
if got["steps"][1]["op"] != "base" {
differ.push(format!("{name}: the tool does not route the envelope offered at step {i} as a base: {}", text(&o)));
}
routed_as_base += 1;
continue;
}
if stage0_base || name == "budget_partly_applied" {
extra += 1;
}
@ -403,7 +448,8 @@ fn apply_json_replays_the_corpus() {
}
same += 1;
}
println!("apply --json against the corpus: {same} cases agree ({extra} through --stage0 or a budget that never binds), {} differ, {skipped} need what the tool's sink does not do", differ.len());
println!("apply --json against the corpus: {same} cases agree ({extra} through --stage0 or a budget that never binds), {} differ, {skipped} need what the tool's sink does not do, {routed_as_base} offer an envelope the tool routes as a base", differ.len());
assert!(routed_as_base > 0, "the corpus offers no envelope the tool routes as a base by its capability");
assert!(differ.is_empty(), "{}", differ.join("\n"));
assert!(same >= 30, "only {same} cases compared");
assert_eq!(extra, 5, "the four PNG and FLAC cases and budget_partly_applied replay");

21
wai/wai-web/Cargo.lock generated
View file

@ -394,6 +394,25 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "num-bigint"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
dependencies = [
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -724,7 +743,9 @@ version = "0.3.0"
dependencies = [
"blake3",
"cbindgen",
"curve25519-dalek",
"ed25519-dalek",
"num-bigint",
"pkg-config",
"serde",
"serde_json",

View file

@ -19,7 +19,7 @@ js-sys = "0.3"
# with the codec chain off. No second implementation: replay-equivalence
# in the browser is the identical Rust that generated the conformance
# corpus hashes.
wai = { path = "../wai-rs", default-features = false, features = ["world", "audio_scene", "haptic", "splat4d", "provenance", "score", "film", "neural_int", "generate", "video_int", "audio_int", "world_model", "world_int", "splat_int", "quantum", "quantum_ops", "quantum_cal", "quantum_mitigate", "quantum_qec", "quantum_control", "quantum_noise", "quantum_compile", "quantum_atom", "quantum_qbom", "quantum_receipt", "quantum_toolchain", "quantum_mps", "quantum_stabilizer", "quantum_pauli", "quantum_vml", "quantum_phasor", "quantum_qfhrr", "quantum_kernel", "quantum_phasor_meter", "quantum_qdata", "video_workflow", "staged"] }
wai = { path = "../wai-rs", default-features = false, features = ["world", "audio_scene", "haptic", "splat4d", "provenance", "score", "film", "neural_int", "generate", "video_int", "audio_int", "world_model", "world_int", "splat_int", "quantum", "quantum_ops", "quantum_cal", "quantum_mitigate", "quantum_qec", "quantum_control", "quantum_noise", "quantum_compile", "quantum_atom", "quantum_qbom", "quantum_receipt", "quantum_toolchain", "quantum_mps", "quantum_stabilizer", "quantum_pauli", "quantum_vml", "quantum_phasor", "quantum_qfhrr", "quantum_kernel", "quantum_phasor_meter", "quantum_qdata", "video_workflow", "staged", "stage_measure", "receipt_claims"] }
ed25519-dalek = "2" # actor keys for browser-side op authoring (same version as wai-rs)
blake3 = "1" # in-page receipt check: hash a decoded image against a pinned digest

View file

@ -79,3 +79,39 @@ wasm-bindgen --target nodejs --out-dir demo/pkg-node --out-name wai_web \
target/wasm32-unknown-unknown/release/wai_web.wasm
node demo/verify_staged_wasm.mjs ./pkg-node/wai_web.js ../staged-conformance
```
## Staged measurement — the native figures in the browser
`src/measure.rs` binds `wai::stage_measure`, the code the native measurement tool
`wai_stage_measure` runs (`extensions/staged-measurement.md`), for JS. A title is read
from a `Map` of files by their path below `wai/`: `titleMissing(dir, files)` names the
next file it needs, so a page fetches files one at a time until it holds them all.
`evaluateTitle` (or `new MeasureTitle(dir, files).json()`) gives the text
`wai_stage_measure eval --json` prints: counted bytes per object, stage and prefix,
quality integers per presentation and per prefix, first presentable bytes, the layering
overhead's points, deduplication and the output tree. `simulate` and `simulateModel`
give the text `wai_stage_measure sim` prints. `countObject` counts one envelope (or
names the code it is not countable with), `qualityRgb8` gives one picture pair's
integers (sums of squares as decimal strings), and `outputRoot` the output tree's root
over presentations.
`sessionBegin(entry, host, nonce)` is `wai::session_recorder::SessionRecorder` over the
session's entry object: `decoderFor` names the decoder that decoded an envelope,
`presented` and `received` record what the page did, and `finish(seed, signer_id,
issued_at)` seals the `reconstruct-session` claim (`extensions/jwp-receipts.md`
§2.4.12), checked against the record first; a browser session states no energy figure.
`verifyClaim(claim, outputs, objects)` checks a claim against the presentations and
the objects received, and gives `""` or the reason.
`verify_measure.sh` builds the nodejs package (or takes one already built), runs
`verify_measure_wasm.mjs` over `staged-measure-conformance/` — every golden, refusal
title, counted object, quality pair and simulator case, byte for byte, and a session per
fixture recorded the way a page records one, which must be the native tool's claim byte
for byte — then requires the native `wai_stage_measure eval --json` to equal the wasm32
evaluation of every fixture, and the native and independent Python claim verifiers to
give the wasm32 build's verdicts on every session it sealed:
```
# from wai-web/ (claim_verify.py needs the `cryptography` package)
bash demo/verify_measure.sh
```

View file

@ -0,0 +1,73 @@
#!/usr/bin/env bash
# The wasm32 measurement binding gives the native figures byte for byte, over the corpus
# wai/staged-measure-conformance/:
#
# 1. demo/verify_measure_wasm.mjs runs the wasm32 build under Node: every fixture's
# evaluation against goldens.json, the refusal titles, counting.json, quality.json,
# sim/results.json, the output tree, and the session claim the browser seals;
# 2. the native tool's `wai_stage_measure eval --json` for every fixture must equal the
# wasm32 evaluation byte for byte (cmp);
# 3. every session the wasm32 build sealed, and its altered variants, are verified by
# the native claim verifier (`wai_claim_vectors verdicts`) and by the independent
# Python verifier (`claim-conformance/claim_verify.py verdicts`), and each verdict list
# must equal the one the wasm32 build gave, line for line.
#
# from wai-web/: bash demo/verify_measure.sh [path/to/pkg-node/wai_web.js]
#
# With no argument the nodejs package is built here (cargo, then the wasm-bindgen CLI at
# the version Cargo.lock pins); with one, that package is used, as CI does after building
# it. The native binaries are built in debug under their own target directory, so a
# CARGO_TARGET_DIR shared with other feature sets is not disturbed. claim_verify.py needs
# the `cryptography` package. Any missing artifact fails the run.
set -euo pipefail
cd "$(dirname "$0")/.." # wai-web/
WEB="$(pwd)"
WAI="$(cd .. && pwd)"
for tool in cargo node python3; do
command -v "$tool" >/dev/null || { echo "verify_measure: needs $tool"; exit 1; }
done
OUT="$(mktemp -d)"
trap 'rm -rf "$OUT"' EXIT
PKG="${1:-}"
if [ -z "$PKG" ]; then
command -v wasm-bindgen >/dev/null || { echo "verify_measure: needs the wasm-bindgen CLI"; exit 1; }
# Build output is not discarded: a failed build must show why.
cargo build --locked --release --target wasm32-unknown-unknown
wasm-bindgen --target nodejs --out-dir demo/pkg-node --out-name wai_web \
"${CARGO_TARGET_DIR:-target}/wasm32-unknown-unknown/release/wai_web.wasm"
PKG="$WEB/demo/pkg-node/wai_web.js"
fi
PKG="$(cd "$(dirname "$PKG")" && pwd)/$(basename "$PKG")"
[ -f "$PKG" ] || { echo "verify_measure: missing artifact $PKG"; exit 1; }
echo "== 1. the wasm32 binding over the corpus"
node demo/verify_measure_wasm.mjs "$PKG" "$WAI/staged-measure-conformance" --out "$OUT"
echo "== 2. the native evaluation equals the wasm32 evaluation, byte for byte"
NATIVE_TARGET="${CARGO_TARGET_DIR:-$WAI/wai-rs/target}/verify-measure-native"
( cd "$WAI/wai-rs" && cargo build --locked --no-default-features --features stage_measure,receipt_claims \
--target-dir "$NATIVE_TARGET" --bin wai_stage_measure --bin wai_claim_vectors )
BIN="$NATIVE_TARGET/debug"
for b in wai_stage_measure wai_claim_vectors; do
[ -x "$BIN/$b" ] || { echo "verify_measure: missing artifact $BIN/$b"; exit 1; }
done
n=0
for dir in "$WAI"/staged-measure-conformance/fixtures/*/; do
name="$(basename "$dir")"
[ -f "$OUT/eval-$name.json" ] || { echo "verify_measure: no wasm32 evaluation of $name"; exit 1; }
"$BIN/wai_stage_measure" eval "$dir" --json > "$OUT/native-$name.json"
cmp "$OUT/native-$name.json" "$OUT/eval-$name.json"
echo " $name: identical ($(wc -c < "$OUT/native-$name.json" | tr -d ' ') bytes)"
n=$((n + 1))
done
[ "$n" -ge 4 ] || { echo "verify_measure: only $n fixtures"; exit 1; }
echo "== 3. the browser-sealed sessions, verified natively and independently"
[ -s "$OUT/sessions.json" ] && [ -s "$OUT/verdicts-wasm.txt" ] || { echo "verify_measure: no sessions written"; exit 1; }
"$BIN/wai_claim_vectors" verdicts "$OUT/sessions.json" > "$OUT/verdicts-native.txt"
diff "$OUT/verdicts-wasm.txt" "$OUT/verdicts-native.txt"
python3 "$WAI/claim-conformance/claim_verify.py" verdicts "$OUT/sessions.json" > "$OUT/verdicts-python.txt"
diff "$OUT/verdicts-wasm.txt" "$OUT/verdicts-python.txt"
echo " $(wc -l < "$OUT/verdicts-wasm.txt" | tr -d ' ') verdicts: wasm32, native and Python agree"
echo "verify_measure: OK"

View file

@ -0,0 +1,389 @@
// The wasm32 measurement binding computes the native figures byte for byte, over the
// corpus wai/staged-measure-conformance/ (extensions/staged-measurement.md §12.3), which
// wai_stage_measure wrote and wai_stage_measure verify and measure_verify.py check:
//
// - goldens.json: every fixture, its files fetched one at a time as titleMissing names
// them (as a page fetches them), evaluated by evaluateTitle and by MeasureTitle, must
// print its block of goldens.json byte for byte. The block is printed here with
// JSON.stringify(_, null, 2); the run first requires that this printer reproduces
// goldens.json itself byte for byte, and that every number in it is an integer
// JavaScript holds exactly, so equal text is equal figures;
// - refusals/: every refusal title refused, by checkRefusal, with the code it states;
// and a fixture with a byte of a layer changed is refused by evaluateTitle;
// - counting.json: countObject gives every object's counted bytes, or the code it is
// not countable with and the WAI2 entry that refused;
// - quality.json: qualityRgb8 gives every picture pair's integers, each picture drawn
// from the formula the file states;
// - sim/results.json: every case of sim/cases.json simulated (simulate over a
// fixture, simulateModel over a model file) and the results document rebuilt from
// the wasm32 output must be sim/results.json byte for byte;
// - the output tree: outputRoot over each golden's presentations is its output_root;
// - the session claim: for every fixture, the session MeasureTitle.session records (the
// native tool's recorder) and the session recorded the way a page records one
// (sessionBegin at the entry object, decoderFor each envelope, presented, received),
// both sealed with one key, nonce and time, are the same claim byte for byte; it
// commits to the golden output_root and leaf_count, verifyClaim accepts it with and
// without the envelopes, and refuses it against an altered output
// (session_output_mismatch), with an object left out (session_source_not_received) and
// with an object miscounted (session_counted_mismatch);
// - the recorder's own checks: decoderFor refuses a capability the envelope neither
// carries nor falls back to, and a decoder whose first unit the page does not state; an
// object received and never presented is recorded as not presented, whatever the page
// would say; bytes that are not an envelope, and an encrypted envelope, are recorded as
// objects neither counted nor presented, every byte transferred; and integers from JS (a unit, a layer
// count, a decoder handle, a picture's width and height) that are not whole numbers in
// range are refused, never truncated or wrapped;
// - the classical length: a session that received a WAI2 it did not decode — counting/
// wai2-classical.wai and its encrypted twin — seals, stating the classical length its
// rendition table declares, which the page never states; verifyClaim accepts it with
// the envelopes and refuses it without that WAI2 (session_classical_mismatch), and the
// native and Python verifiers give the same verdicts.
//
// node verify_measure_wasm.mjs [path/to/wai_web.js] [path/to/staged-measure-conformance] [--out <dir>]
//
// With --out, each fixture's evaluation is written to <dir>/eval-<name>.json, and every
// session above to <dir>/sessions.json as a claim-conformance vector document with the
// verdict expected of each in <dir>/verdicts-wasm.txt (`name<TAB>verdict`), so that
// verify_measure.sh can compare them with the native tool and the other verifiers.
//
// Exit 0 iff every check passes. A missing package or corpus file fails the run.
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const here = dirname(fileURLToPath(import.meta.url));
const args = process.argv.slice(2);
let out = null;
const pos = [];
for (let i = 0; i < args.length; i++) {
if (args[i] === '--out') out = resolve(args[++i]);
else pos.push(args[i]);
}
const pkg = resolve(here, pos[0] ?? './pkg-node/wai_web.js');
const corpus = resolve(pos[1] ?? resolve(here, '../../staged-measure-conformance'));
const waiDir = resolve(corpus, '..');
for (const p of [pkg, join(corpus, 'goldens.json')]) {
if (!existsSync(p)) { console.log(`FAIL missing: ${p}`); process.exit(1); }
}
const wai = await import(pathToFileURL(pkg).href);
if (out) mkdirSync(out, { recursive: true });
let fails = 0;
let passes = 0;
const check = (ok, what) => { if (ok) passes++; else { fails++; console.log(`FAIL ${what}`); } };
const text = (p) => readFileSync(p, 'utf8');
const json = (p) => JSON.parse(text(p));
const sha = (b) => createHash('sha256').update(b).digest('hex');
const printed = (v) => JSON.stringify(v, null, 2) + '\n';
const rel = (p) => p.slice(waiDir.length + 1);
// Every number in a document is an integer JavaScript holds exactly.
const exact = (v) => {
if (typeof v === 'number') return Number.isSafeInteger(v);
if (Array.isArray(v)) return v.every(exact);
if (v && typeof v === 'object') return Object.values(v).every(exact);
return true;
};
// The files a title needs, fetched as titleMissing names them.
function fetchTitle(dir, patch) {
const files = new Map();
for (let i = 0; i < 1000; i++) {
const p = wai.titleMissing(dir, files);
if (p === undefined) return files;
if (files.has(p)) throw new Error(`${p} named twice`);
let b = new Uint8Array(readFileSync(join(waiDir, p)));
if (patch) b = patch(p, b);
files.set(p, b);
}
throw new Error(`${dir}: the title names no end of files`);
}
// ── goldens.json ──────────────────────────────────────────────────────────────
const goldensText = text(join(corpus, 'goldens.json'));
const goldens = JSON.parse(goldensText);
check(printed(goldens) === goldensText, 'JSON.stringify(_, null, 2) reproduces goldens.json byte for byte (the printer this run compares with)');
check(exact(goldens), 'every number in goldens.json is an integer JavaScript holds exactly');
const names = readdirSync(join(corpus, 'fixtures'), { withFileTypes: true })
.filter((d) => d.isDirectory() && existsSync(join(corpus, 'fixtures', d.name, 'title.json'))).map((d) => d.name).sort();
check(names.length >= 4 && names.length === goldens.fixtures.length, `the fixtures are the goldens' (${names.length})`);
const titles = new Map();
names.forEach((name, i) => {
const dir = `${rel(corpus)}/fixtures/${name}`;
const golden = goldens.fixtures[i];
check(golden && golden.title === name, `${name}: goldens.json lists it in this place`);
const files = fetchTitle(dir);
const got = wai.evaluateTitle(dir, files);
check(got === printed(golden), `${name}: evaluateTitle prints its goldens.json block byte for byte`);
const t = new wai.MeasureTitle(dir, files);
check(t.json() === got, `${name}: MeasureTitle gives evaluateTitle's text`);
titles.set(name, { dir, files, t, golden });
if (out) writeFileSync(join(out, `eval-${name}.json`), got);
// Each object, counted on its own, is the evaluation's.
for (const o of golden.objects) {
const c = wai.countObject(files.get(o.file));
check(JSON.stringify(c.counted) === JSON.stringify(o.counted) && c.framing === golden.framing, `${name}: countObject(${o.file}) is the golden's`);
}
// The output tree over the golden presentations.
const outputs = golden.presentations.map((p) => ({ unit: p.unit, layers: p.layers, decoder: p.decoder, out_digest: p.out_sha256 }));
check(wai.outputRoot(JSON.stringify(outputs)) === golden.output_root, `${name}: outputRoot over its presentations is its output_root`);
check(wai.sha256Hex(files.get(golden.objects[0].file)) === sha(files.get(golden.objects[0].file)), `${name}: sha256Hex`);
});
// A layer changed by one byte: the title is refused, never measured.
{
const name = names.includes('still') ? 'still' : names[0];
const dir = `${rel(corpus)}/fixtures/${name}`;
const target = goldens.fixtures[names.indexOf(name)].objects.find((o) => o.layer === 2)?.file;
const files = fetchTitle(dir, (p, b) => {
if (p !== target) return b;
const c = b.slice();
c[c.length - 9] ^= 0x01;
return c;
});
let threw = null;
try { wai.evaluateTitle(dir, files); } catch (e) { threw = String(e.message ?? e); }
check(target && threw !== null, `${name}: a layer-2 object changed by one byte is refused (${threw})`);
}
// ── refusals/ ─────────────────────────────────────────────────────────────────
const refusals = readdirSync(join(corpus, 'refusals'), { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
check(refusals.length >= 5, `the refusal titles are present (${refusals.length})`);
for (const r of refusals) {
const dir = `${rel(corpus)}/refusals/${r}`;
const want = json(join(corpus, 'refusals', r, 'title.json')).refused;
let got;
try { got = wai.checkRefusal(dir, fetchTitle(dir)); } catch (e) { got = `error: ${e.message ?? e}`; }
check(got === want, `refusals/${r}: refused ${want} (got ${got})`);
let threw = false;
try { wai.evaluateTitle(dir, fetchTitle(dir)); } catch (e) { threw = String(e.message ?? e).startsWith(`refused ${want}`); }
check(threw, `refusals/${r}: evaluateTitle's error starts "refused ${want}"`);
}
// ── counting.json ─────────────────────────────────────────────────────────────
const counting = json(join(corpus, 'counting.json'));
check(counting.cases.length >= 10, 'counting.json holds its cases');
for (const k of counting.cases) {
const c = wai.countObject(new Uint8Array(readFileSync(join(waiDir, k.file))));
check(JSON.stringify([c.counted, c.refused, c.component]) === JSON.stringify([k.counted, k.refused, k.component]), `counting: ${k.file} (${c.refused ?? 'counted'})`);
}
// ── quality.json ──────────────────────────────────────────────────────────────
const quality = json(join(corpus, 'quality.json'));
const picture = (w, h, seed, mul) => {
const v = new Uint8Array(3 * w * h);
let i = 0;
for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) for (let c = 0; c < 3; c++) {
v[i++] = (x * mul + y * (mul + 3) + 29 * c + 17 * seed + 5 * ((x * y + seed) % 13)) % 256;
}
return v;
};
check(quality.cases.length >= 6, 'quality.json holds its cases');
for (const k of quality.cases) {
const q = wai.qualityRgb8(k.w, k.h, picture(k.w, k.h, ...k.reference), picture(k.w, k.h, ...k.test));
const pick = (o) => JSON.stringify([o.n_samples, o.sse_rgb, o.sse_y, o.ssim_y_q32, o.windows]);
check(pick(q) === pick(k) && typeof q.sse_rgb === 'string', `quality: ${k.w}×${k.h} ${JSON.stringify(k.reference)} against ${JSON.stringify(k.test)}`);
}
// ── sim/results.json ──────────────────────────────────────────────────────────
const resultsText = text(join(corpus, 'sim/results.json'));
const results = JSON.parse(resultsText);
check(printed(results) === resultsText && exact(results), 'sim/results.json is printed as this run prints it, its numbers exact');
const cases = json(join(corpus, 'sim/cases.json')).cases;
const rebuilt = [];
for (const k of cases) {
const plan = text(join(corpus, 'sim', k.plan));
const f = k.model.startsWith('fixture:') ? k.model.slice('fixture:'.length) : null;
let s;
if (f) {
const t = titles.get(f);
s = JSON.parse(wai.simulate(t.dir, t.files, plan));
check(t.t.simulate(plan) === wai.simulate(t.dir, t.files, plan), `sim ${k.name}: MeasureTitle.simulate gives simulate's text`);
} else {
s = JSON.parse(wai.simulateModel(text(join(corpus, 'sim', k.model)), plan));
}
check(s.label === 'simulated', `sim ${k.name}: labelled simulated`);
rebuilt.push({ name: k.name, stall_bound_us: s.stall_bound_us, max_stall_us: s.max_stall_us, result: s.result });
}
check(printed({ ...results, results: rebuilt }) === resultsText, `sim/results.json rebuilt from the wasm32 simulator byte for byte (${rebuilt.length} cases)`);
// ── the session claim ─────────────────────────────────────────────────────────
const HOST = JSON.stringify({ profile: 'browser-baseline', target: 'wasm32-unknown-unknown', threads: 1 });
const SEED = '44'.repeat(32);
const NONCE = '07'.repeat(32);
const T = '2026-10-06T12:00:00Z';
const vectors = [];
const expected = [];
const flip = (h) => (h[0] === '0' ? '1' : '0') + h.slice(1);
// The title's unit that is unit 0 of an object's base: the base's own, for a layer too.
const firstUnitOf = (golden, object) => golden.bases[golden.objects.find((o) => o.object === object).base].first_unit;
for (const [name, { files, t, golden }] of titles) {
const native = t.session(HOST, NONCE).finish(SEED, undefined, T);
// The page's recording.
const envelopeOf = new Map(golden.objects.map((o) => [o.object, files.get(o.file)]));
const entry = files.get(golden.objects[0].file);
const h = wai.sessionBegin(entry, HOST, NONCE, undefined);
for (const p of golden.presentations) {
const d = golden.decoders[p.decoder];
const handle = h.decoderFor(envelopeOf.get(d.object), d.capability, d.pin !== null, firstUnitOf(golden, d.object));
h.presented(p.unit, p.layers, handle, p.out_sha256, undefined);
}
for (const o of golden.objects) h.received(files.get(o.file));
const page = h.finish(SEED, undefined, T);
let again = null;
try { h.finish(SEED, undefined, T); } catch (e) { again = String(e.message ?? e); }
check(again !== null, `${name}: a session seals once`);
check(page.claim === native.claim && page.link_hash === native.link_hash, `${name}: the page's recording seals the native tool's claim byte for byte`);
const body = JSON.parse(page.claim).body;
check(body.output_root === golden.output_root && body.leaf_count === golden.leaf_count, `${name}: the session commits to the golden output tree`);
check(body.host.build.profile === 'release' && body.host.build.target === 'wasm32-unknown-unknown', `${name}: the browser claim states a release build for wasm32 (${body.host.build.profile})`);
check(body.counted_primary === golden.objects.reduce((a, o) => a + o.counted.total, 0) && body.energy === null, `${name}: counted bytes summed, no energy figure`);
const o = JSON.stringify(page.outputs), j = JSON.stringify(page.objects);
check(wai.outputRoot(o) === golden.output_root, `${name}: outputRoot over the session's outputs`);
const envelopes = golden.objects.map((x) => files.get(x.file));
check(wai.verifyClaim(page.claim, o, j) === '', `${name}: verifyClaim accepts the session`);
check(wai.verifyClaim(page.claim, o, j, envelopes) === '', `${name}: verifyClaim accepts the session with the envelopes`);
const altered = page.outputs.map((p, i) => (i === 0 ? { ...p, out_digest: flip(p.out_digest) } : p));
const fewer = page.objects.slice(0, -1);
const miscounted = page.objects.map((x, i) => (i === 0 ? { ...x, counted: x.counted + 1 } : x));
const variants = [
[`${name}/accept`, page.outputs, page.objects, `accept link_hash=${page.link_hash}`, ''],
[`${name}/accept-with-envelopes`, page.outputs, page.objects, `accept link_hash=${page.link_hash}`, '', envelopes],
[`${name}/output-altered`, altered, page.objects, 'reject step=6 reason=session_output_mismatch', 'session_output_mismatch'],
[`${name}/object-left-out`, page.outputs, fewer, 'reject step=6 reason=session_source_not_received', 'session_source_not_received'],
[`${name}/object-miscounted`, page.outputs, miscounted, 'reject step=6 reason=session_counted_mismatch', 'session_counted_mismatch'],
];
for (const [vname, outputs, objects, verdict, reason, envs] of variants) {
check(wai.verifyClaim(page.claim, JSON.stringify(outputs), JSON.stringify(objects), envs) === reason, `${vname}: verifyClaim gives "${reason}"`);
const context = envs ? { outputs, objects, envelopes: envs.map((e) => ({ envelope: Buffer.from(e).toString('hex') })) } : { outputs, objects };
vectors.push({ name: vname, procedure: 'claim', receipt: page.claim, context });
expected.push(`${vname}\t${verdict}`);
}
}
// ── the recorder's own checks ──────────────────────────────────────────────────
{
const { files, golden } = titles.get(names.includes('still') ? 'still' : names[0]);
const env = (o) => files.get(o.file);
const refusedBy = (f) => { try { f(); return null; } catch (e) { return String(e.message ?? e); } };
const base = golden.objects[0];
const h = wai.sessionBegin(env(base), HOST, NONCE, undefined);
// A decoder through a capability the envelope neither carries nor falls back to.
check(/neither the envelope's capability/.test(refusedBy(() => h.decoderFor(env(base), 'wai.audio.int_codec', true, 0)) ?? ''),
'decoderFor refuses a capability its envelope neither carries nor falls back to');
check((refusedBy(() => h.decoderFor(env(base), undefined, true, undefined)) ?? '').startsWith('first_unit: a whole number'),
'decoderFor refuses a decoder whose first unit the page does not state');
const p0 = golden.presentations.find((x) => x.layers === 1);
// Integers from JS: whole numbers in range, never truncated or wrapped. Each case on a
// fresh handle, refused for its own field and not for anything an earlier case left.
const fresh = () => {
const x = wai.sessionBegin(env(base), HOST, NONCE, undefined);
return [x, x.decoderFor(env(base), golden.decoders[0].capability, golden.decoders[0].pin !== null, 0)];
};
for (const [what, field, f] of [
['unit 0.5', 'unit', (x, dd) => x.presented(0.5, 1, dd, p0.out_sha256, undefined)],
['layers 1.9', 'layers', (x, dd) => x.presented(0, 1.9, dd, p0.out_sha256, undefined)],
['layers 2^32 + 2', 'layers', (x, dd) => x.presented(1, 2 ** 32 + 2, dd, p0.out_sha256, undefined)],
['layers 65536', 'layers', (x, dd) => x.presented(1, 65536, dd, p0.out_sha256, undefined)],
['decoder 0.99', 'decoder', (x, dd) => x.presented(2, 1, dd + 0.99, p0.out_sha256, undefined)],
['decoder -1', 'decoder', (x) => x.presented(2, 1, -1, p0.out_sha256, undefined)],
['qualityRgb8 width 1.5', 'w', () => wai.qualityRgb8(1.5, 1, new Uint8Array(3), new Uint8Array(3))],
['qualityRgb8 height 2^32 + 1', 'h', () => wai.qualityRgb8(1, 2 ** 32 + 1, new Uint8Array(3), new Uint8Array(3))],
]) {
const [x, dd] = fresh();
const why = refusedBy(() => f(x, dd)) ?? '';
check(why.startsWith(`${field}: a whole number`), `${what}: refused for its ${field} (${why || 'accepted'})`);
x.free();
}
// The same calls with whole numbers in range are recorded.
{
const [x, dd] = fresh();
check(refusedBy(() => x.presented(0, 1, dd, p0.out_sha256, undefined)) === null, 'a whole unit, layer count and decoder are recorded');
x.free();
}
// Present every stage but the last layer's; receive every object, the last layer included.
const top = Math.max(...golden.objects.map((o) => o.layer));
const last = golden.objects.find((o) => o.layer === top);
const handles = new Map();
for (const p of golden.presentations) {
const dd = golden.decoders[p.decoder];
if (dd.object === last.object) continue;
const key = `${dd.object}`;
if (!handles.has(key)) handles.set(key, h.decoderFor(env(golden.objects.find((o) => o.object === dd.object)), dd.capability, dd.pin !== null, firstUnitOf(golden, dd.object)));
h.presented(p.unit, p.layers, handles.get(key), p.out_sha256, undefined);
}
for (const o of golden.objects) h.received(env(o));
// A repeat is one object with the bytes of both; bytes that are not an envelope, and an
// encrypted envelope, no entry names are held, uncounted and not presented, so the
// session accounts for every byte transferred.
h.received(env(golden.objects[0]));
const inert = [['seven bytes of the base', env(base).subarray(0, 7)], ['a broken magic', Uint8Array.from([0x57, 0x41, 0x49, 0x33, 0, 0, 0, 0])],
['an encrypted envelope', new Uint8Array(readFileSync(join(corpus, 'counting/encrypted.wai')))]];
for (const [what, bytes] of inert) check(refusedBy(() => h.received(bytes)) === null, `received records ${what}`);
const r = h.finish(SEED, undefined, T);
for (const [what, bytes] of inert) {
const o = r.objects.find((x) => x.content_hash === wai.stageContentHash(bytes));
check(o && o.counted === null && o.presented === false && o.transferred === bytes.length, `${what}: listed uncounted and not presented, every byte transferred`);
}
check(JSON.parse(r.claim).body.transferred === r.objects.reduce((a, o) => a + o.transferred, 0), 'the session transferred every byte of every object received');
check(r.objects.find((o) => o.content_hash === base.object)?.transferred === 2 * env(base).length, 'the base received twice: one object, transferred twice its length');
vectors.push({ name: 'recorder/inert-and-repeat', procedure: 'claim', receipt: r.claim,
context: { outputs: r.outputs, objects: r.objects, envelopes: [...golden.objects.map(env), ...inert.map(([, b]) => b)].map((e) => ({ envelope: Buffer.from(e).toString('hex') })) } });
expected.push(`recorder/inert-and-repeat\taccept link_hash=${r.link_hash}`);
const seen = r.objects.find((o) => o.content_hash === last.object);
check(seen && seen.presented === false && JSON.parse(r.claim).body.counted_discarded === last.counted.total,
'an object received and never presented is recorded as not presented, and counted as discarded');
check(wai.verifyClaim(r.claim, JSON.stringify(r.outputs), JSON.stringify(r.objects), [...golden.objects.map(env), ...inert.map(([, b]) => b)]) === '',
'that session verifies against the envelopes received');
}
// ── a WAI2 received and not decoded: the classical length the page never states ──────
// A page receives a WAI2 whose second rendition, PNG, is its classical rendition, its
// payloads plaintext or encrypted. Its rendition table is in the clear either way, so the
// recorder states the classical length that table declares (jwp-receipts §2.4.12 rule 9),
// read here from the table itself: magic, manifest length, manifest, rendition count, then
// (offset, length) per rendition.
{
const { files, golden } = titles.get(names.includes('still') ? 'still' : names[0]);
const base = golden.objects[0];
const env = files.get(base.file);
const tableLength = (w, i) => {
const dv = new DataView(w.buffer, w.byteOffset, w.byteLength);
const ml = dv.getUint32(4, true);
return dv.getUint32(8 + ml + 2 + 8 * i + 4, true);
};
for (const which of ['wai2-classical', 'wai2-classical-encrypted']) {
const w = new Uint8Array(readFileSync(join(corpus, `counting/${which}.wai`)));
const h = wai.sessionBegin(env, HOST, NONCE, undefined);
const bd = golden.decoders.find((x) => x.object === base.object);
const dd = h.decoderFor(env, bd.capability, bd.pin !== null, firstUnitOf(golden, base.object));
const p0 = golden.presentations.find((x) => golden.decoders[x.decoder].object === base.object && x.layers === 1);
h.presented(p0.unit, 1, dd, p0.out_sha256, undefined);
h.received(env);
h.received(w);
let r = null;
try { r = h.finish(SEED, undefined, T); } catch (e) { check(false, `${which}: the session seals (${e.message ?? e})`); continue; }
const body = JSON.parse(r.claim).body;
check(body.classical_declared === tableLength(w, 1) && body.classical_source === 'wai2-table',
`${which}: the session states the classical length its table declares (${body.classical_declared})`);
const o = JSON.stringify(r.outputs), j = JSON.stringify(r.objects);
check(wai.verifyClaim(r.claim, o, j, [env, w]) === '', `${which}: verifyClaim accepts the session with the envelopes`);
check(wai.verifyClaim(r.claim, o, j, [env]) === 'session_classical_mismatch', `${which}: without the WAI2, no classical length is read`);
for (const [vname, envs, verdict] of [
[`classical/${which}/accept`, [env, w], `accept link_hash=${r.link_hash}`],
[`classical/${which}/wai2-not-held`, [env], 'reject step=6 reason=session_classical_mismatch'],
]) {
vectors.push({ name: vname, procedure: 'claim', receipt: r.claim,
context: { outputs: r.outputs, objects: r.objects, envelopes: envs.map((e) => ({ envelope: Buffer.from(e).toString('hex') })) } });
expected.push(`${vname}\t${verdict}`);
}
}
}
if (out) {
writeFileSync(join(out, 'sessions.json'), printed({ about: 'Sessions sealed by the wasm32 binding over the staged-measure corpus (verify_measure_wasm.mjs).', vectors }));
writeFileSync(join(out, 'verdicts-wasm.txt'), expected.join('\n') + '\n');
}
console.log(`${passes} checks passed, ${fails} failed: ${names.length} fixtures, ${refusals.length} refusal titles, ${counting.cases.length} counted objects, ${quality.cases.length} picture pairs, ${cases.length} simulator cases, ${vectors.length} session verdicts`);
process.exit(fails === 0 ? 0 : 1);

View file

@ -158,6 +158,13 @@ pub mod quantum_qbom;
// and stage hash read back, and a layer's declared cost read before it is offered.
pub mod stage;
// Staged-delivery measurement (extensions/staged-measurement.md): wai::stage_measure, the
// code the native measurement tool runs, bound for JS — a title's counted bytes, quality
// integers, first presentable bytes, layering overhead, deduplication and output tree, the
// delivery simulator, and the reconstruct-session claim a browser sink seals over what it
// presented (jwp-receipts §2.4.12), with its verifier.
pub mod measure;
// ---- Capability menu ----
// The capabilities this browser sink advertises: a subset of the registry

843
wai/wai-web/src/measure.rs Normal file
View file

@ -0,0 +1,843 @@
//! Staged-delivery measurement in the browser (`extensions/staged-measurement.md`), and
//! the session claim a browser sink seals over what it presented (`extensions/jwp-receipts.md`
//! §2.4.12).
//!
//! Every figure here is `wai::stage_measure`'s, the code the native tool
//! `wai_stage_measure` runs, compiled to wasm32: a title's counted bytes, quality
//! integers, first presentable bytes, layering overhead, deduplication and output tree
//! (`evaluateTitle`, the text `wai_stage_measure eval --json` prints), the delivery
//! simulator (`simulate`, the text `wai_stage_measure sim` prints), one object's counted
//! bytes (`countObject`), one picture pair's quality integers (`qualityRgb8`), and an
//! output tree's root (`outputRoot`); an output's digest is the crate's `sha256Hex`.
//! Nothing is a second implementation, so the browser computes the native figures byte
//! for byte; `demo/verify_measure_wasm.mjs` proves it over the corpus
//! `staged-measure-conformance/`.
//!
//! A title is read from a map of files (path below `wai/` → bytes) and needs no file
//! system: `titleMissing` names the next file the title needs and does not hold, so a
//! page fetches it and asks again.
//!
//! [`SessionHandle`] is `wai::session_recorder::SessionRecorder`: a page tells it the
//! decoder that decoded each envelope, every presentation with its output's digest, and
//! the bytes of every object it received. The recorder derives each object's figures from
//! its bytes — whether any of its outputs was presented from the presentations — and holds
//! the envelopes, so `finish` seals the `reconstruct-session` claim with a key whose seed
//! the page supplies only after checking it against the presentations, the objects and the
//! envelopes, as a verifier holding all three does. The decode is never metered here, so a
//! browser session's `energy` is `null`. `verifyClaim` runs the conformance runner of
//! `claim-conformance/` on the claim with the presentations, the objects and, where given,
//! the envelopes (§3.1, step 6 included).
//!
//! Integers that can pass 2^53 − 1 (the sums of squares) are decimal strings, as the
//! corpus writes them; every other figure is a number.
//!
//! The pure-Rust half is what the native tests run; the `#[wasm_bindgen]` wrappers only
//! convert.
use std::collections::BTreeMap;
use serde_json::{json, Value};
use wai::claim_receipt::{SessionHost, SessionPath};
use wai::codecs::DeclaredTier;
use wai::container::Wai;
use wai::measure_framing::{count_object as count_framed, FramingError};
use wai::output_tree::{commit, Presentation};
use wai::session_recorder::{DecoderKey, RecordedSession, SessionRecorder};
use wai::stage_measure::run::{self, Evaluation, QualityKind};
use wai::stage_measure::sim::{self, Model, Plan};
use wai::stage_measure::title::Title;
use wai::stage_measure::MeasureError;
use wasm_bindgen::prelude::*;
/// Files by their path below `wai/`.
pub(crate) type Files = BTreeMap<String, Vec<u8>>;
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
fn unhex(s: &str, n: usize, what: &str) -> Result<Vec<u8>, String> {
let b = s.as_bytes();
if b.len() != 2 * n || !b.iter().all(|c| c.is_ascii_digit() || (b'a'..=b'f').contains(c)) {
return Err(format!("{what}: {n} bytes as lowercase hexadecimal"));
}
(0..n).map(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).map_err(|e| e.to_string())).collect()
}
fn unhex32(s: &str, what: &str) -> Result<[u8; 32], String> {
Ok(unhex(s, 32, what)?.try_into().expect("32 bytes"))
}
// ───────────────────────── the pure-Rust half ─────────────────────────
/// A JSON value as the native tools print it: pretty, then a newline.
pub(crate) fn printed(v: &Value) -> String {
let mut s = serde_json::to_string_pretty(v).expect("a JSON value prints");
s.push('\n');
s
}
/// The next file the title in `dir` needs and `files` does not hold, or `None` when it
/// holds every one. A title that reads as invalid before a file is missing is an error.
pub(crate) fn title_missing(dir: &str, files: &Files) -> Result<Option<String>, String> {
match Title::from_files(dir, files) {
Ok(_) => Ok(None),
Err(MeasureError::Missing(p)) => Ok(Some(p)),
Err(e) => Err(e.to_string()),
}
}
/// A title, evaluated under the tools' stated limits and checked against the fixture
/// tool's own reconstruction (`expect.json`), as `wai_stage_measure eval` evaluates one.
pub(crate) struct Measured {
pub t: Title,
pub e: Evaluation,
}
/// A refusal's message, starting with its code: `refused <code>: <where>: <detail>`.
fn measure_err(e: MeasureError) -> String {
match e {
MeasureError::Refused { at, code, detail } => format!("refused {code}: {at}: {detail}"),
other => other.to_string(),
}
}
pub(crate) fn measured(dir: &str, files: &Files) -> Result<Measured, String> {
let t = Title::from_files(dir, files).map_err(measure_err)?;
let e = run::evaluate(&t).map_err(measure_err)?;
run::check_expect(&t, &e).map_err(measure_err)?;
Ok(Measured { t, e })
}
/// A title that exists to be refused (its `refused`): the code it was refused with, which
/// is the code it states; anything else is an error.
pub(crate) fn refusal(dir: &str, files: &Files) -> Result<String, String> {
let t = Title::from_files(dir, files).map_err(measure_err)?;
run::check_refusal(&t).map_err(measure_err)?;
Ok(t.refused.clone().unwrap_or_default())
}
/// One simulation, as `wai_stage_measure sim` prints it: the plan, its stall bound, the
/// largest stall and the result, labelled simulated.
pub(crate) fn simulated(model: &Model, plan_json: &str) -> Result<String, String> {
let v: Value = serde_json::from_str(plan_json).map_err(|e| format!("plan: {e}"))?;
let p = Plan::from_json(&v).map_err(|e| e.to_string())?;
let r = sim::simulate(model, &p).map_err(|e| e.to_string())?;
Ok(printed(&json!({"label": "simulated", "plan": p.to_json(), "stall_bound_us": sim::stall_bound(&p), "max_stall_us": r.max_stall(), "result": r.to_json()})))
}
/// A `wai-sim-model/1` model from its JSON.
pub(crate) fn model_of(model_json: &str) -> Result<Model, String> {
let v: Value = serde_json::from_str(model_json).map_err(|e| format!("model: {e}"))?;
Model::from_json(&v).map_err(|e| e.to_string())
}
/// One object's counted bytes (§3), in the corpus's form (`counting.json`): the split and
/// its framing, or the code it is not countable with and the `WAI2` entry that refused.
pub(crate) fn count_value(envelope: &[u8]) -> Value {
match count_framed(envelope) {
Ok(k) => json!({
"counted": {"header": k.header, "manifest": k.manifest, "payload": k.payload, "total": k.total},
"framing": k.framing, "refused": null, "component": null, "detail": null,
}),
Err(e) => json!({
"counted": null, "framing": null, "refused": e.code(),
"component": match &e { FramingError::NotCountable { component, .. } => json!(component), _ => Value::Null },
"detail": e.to_string(),
}),
}
}
/// §4.2's integers for one picture pair, as a record writes them.
pub(crate) fn quality_rgb8_value(w: u32, h: u32, reference: &[u8], test: &[u8]) -> Result<Value, String> {
let (w, h) = (w as usize, h as usize);
let q = wai::quality::image(wai::quality::Rgb8 { w, h, px: reference }, wai::quality::Rgb8 { w, h, px: test }).map_err(|e| e.to_string())?;
Ok(QualityKind::Rgb8(q).to_json())
}
/// The presentations of a claim-conformance `outputs` context: `{unit, layers, decoder,
/// out_digest}`.
pub(crate) fn presentations_of(v: &Value) -> Result<Vec<Presentation>, String> {
v.as_array()
.ok_or("outputs: an array")?
.iter()
.map(|p| {
let n = |k: &str| p.get(k).and_then(Value::as_u64).ok_or(format!("outputs: `{k}` is not an integer"));
Ok(Presentation {
unit: n("unit")?,
layers: u16::try_from(n("layers")?).map_err(|_| "outputs: layers past 65535")?,
decoder: u16::try_from(n("decoder")?).map_err(|_| "outputs: decoder past 65535")?,
out_digest: unhex32(p.get("out_digest").and_then(Value::as_str).unwrap_or(""), "outputs: out_digest")?,
})
})
.collect()
}
/// The output tree's root (staged-measurement Appendix C) over presentations in the
/// `outputs` form, or `None` for none.
pub(crate) fn output_root_of(outputs_json: &str) -> Result<Option<String>, String> {
let v: Value = serde_json::from_str(outputs_json).map_err(|e| format!("outputs: {e}"))?;
Ok(commit(&presentations_of(&v)?).map_err(|e| e.to_string())?.map(|c| hex(&c.root)))
}
/// The decoder host from its JSON, `{profile, target, threads}`: `threads` is the number
/// of decode workers, written as §2.4.12 writes it (the largest power of two not above
/// it); the build is this build's.
pub(crate) fn host_of(host_json: &str) -> Result<SessionHost, String> {
let v: Value = serde_json::from_str(host_json).map_err(|e| format!("host: {e}"))?;
let o = v.as_object().ok_or("host: an object")?;
for k in o.keys() {
if !["profile", "target", "threads"].contains(&k.as_str()) {
return Err(format!("host: no member {k}"));
}
}
Ok(SessionHost {
profile: v["profile"].as_str().ok_or("host: profile is a string")?.to_owned(),
target: match &v["target"] {
Value::Null => None,
t => Some(t.as_str().ok_or("host: target is a string or null")?.to_owned()),
},
build: wai::energy_report::BuildRecord::of_this_build(),
threads: SessionHost::rounded_threads(v["threads"].as_u64().ok_or("host: threads is an integer")?),
})
}
/// The decoder entry key for decoding `envelope` here: through `via` (its manifest's
/// capability where absent; otherwise that capability or the manifest's fallback, and
/// nothing else, SPEC §4), on the primary path, with this build's decoders; `pinned`
/// when its decode verified the parameter set its manifest pins, whose SHA-256 is then
/// the entry's `pin` and whose declared tier (SPEC §3.1: an unregistered token, or none,
/// reads as `none`) its `tier`; `first_unit` the title's unit that is unit 0 of the
/// object's base (of the object itself for a base, of the base it refines for a layer).
pub(crate) fn decoder_key(envelope: &[u8], via: Option<&str>, pinned: bool, first_unit: u64) -> Result<DecoderKey, String> {
let w = Wai::from_bytes(envelope).map_err(|e| e.to_string())?;
if let Some(v) = via {
if v != w.capability() && w.fallback() != Some(v) {
return Err(format!("via: {v} is neither the envelope's capability ({}) nor its fallback", w.capability()));
}
}
let (pin, tier) = if pinned {
let p = w.prior().ok_or("pinned, but the envelope pins no parameter set")?;
let tier = p.tier().and_then(DeclaredTier::from_token).unwrap_or(DeclaredTier::None).token().to_owned();
(Some(unhex32(&p.sha256, "the pin's sha256")?), Some(tier))
} else {
(None, None)
};
Ok(DecoderKey {
capability: via.unwrap_or(w.capability()).to_owned(),
rendition: None,
object: wai::jwp_hash::content_hash(envelope),
first_unit,
path: SessionPath::Primary,
decoder: wai::stage_measure::session::builtin_decoder(),
variant: None,
pin,
tier,
module_status: None,
})
}
/// A sealed session as a page reads it: the claim's wire form (JCS), its link hash, and
/// what a verifier checks it against at step 6 in the claim-conformance context form.
pub(crate) fn recorded_value(s: &RecordedSession) -> Value {
json!({
"claim": s.claim.to_json(),
"link_hash": hex(&s.claim.link_hash()),
"outputs": s.presentations.iter().map(|p| json!({"unit": p.unit, "layers": p.layers, "decoder": p.decoder, "out_digest": hex(&p.out_digest)})).collect::<Vec<_>>(),
"objects": s.objects.iter().map(|o| json!({
"content_hash": hex(&o.content_hash), "path": o.path.label(), "counted": o.counted,
"transferred": o.transferred, "presented": o.presented,
})).collect::<Vec<_>>(),
})
}
/// Seal a recorder's session: `subject` its entry object's content hash, the key from a
/// 32-byte seed, `signer_id` the key's `did:key` where none is given.
pub(crate) fn finish(r: SessionRecorder, subject: [u8; 32], seed_hex: &str, signer_id: Option<&str>, issued_at: &str) -> Result<Value, String> {
let key = ed25519_dalek::SigningKey::from_bytes(&unhex32(seed_hex, "seed")?);
let id = match signer_id {
Some(s) if !s.is_empty() => s.to_owned(),
_ => wai::signer_id::did_key(&key.verifying_key().to_bytes()),
};
let s = r.finish(subject, &key, &id, 0, None, issued_at).map_err(|e| format!("the session is refused: {e}"))?;
Ok(recorded_value(&s))
}
/// A `reconstruct-session` claim checked as the claim-conformance runner checks one
/// (`wai::claim_receipt::corpus_verdicts`): §3.1 steps 1–4, then step 6 against the
/// presentations, the objects and, where given, the envelopes the session received.
/// `""` when accepted, otherwise the reason.
pub(crate) fn verify_session(claim: &str, outputs_json: &str, objects_json: &str, envelopes: Option<&[Vec<u8>]>) -> Result<String, String> {
let outputs: Value = serde_json::from_str(outputs_json).map_err(|e| format!("outputs: {e}"))?;
let objects: Value = serde_json::from_str(objects_json).map_err(|e| format!("objects: {e}"))?;
let mut context = json!({"outputs": outputs, "objects": objects});
if let Some(es) = envelopes {
context["envelopes"] = es.iter().map(|e| json!({"envelope": hex(e)})).collect::<Vec<_>>().into();
}
let doc = json!({"vectors": [{"name": "session", "procedure": "claim", "receipt": claim, "context": context}]});
let vs = wai::claim_receipt::corpus_verdicts(&doc.to_string())?;
let verdict = vs.first().map(|(_, v)| v.as_str()).unwrap_or("error");
if verdict.starts_with("accept") {
return Ok(String::new());
}
match verdict.split_once(" reason=") {
Some((_, reason)) if verdict.starts_with("reject ") => Ok(reason.to_owned()),
_ => Err("the claim or its context could not be read".into()),
}
}
// ───────────────────────── the JS surface ─────────────────────────
fn js_err(e: impl std::fmt::Display) -> JsError {
JsError::new(&e.to_string())
}
fn to_js(v: &Value) -> Result<JsValue, JsError> {
use serde::Serialize;
v.serialize(&serde_wasm_bindgen::Serializer::json_compatible()).map_err(js_err)
}
/// A JS `Map` of path → `Uint8Array`.
fn files_of(m: &js_sys::Map) -> Result<Files, JsError> {
let mut out = Files::new();
for entry in m.entries() {
let pair = js_sys::Array::from(&entry.map_err(|_| JsError::new("files: unreadable entry"))?);
let path = pair.get(0).as_string().ok_or_else(|| JsError::new("files: a path is a string"))?;
let bytes = pair.get(1).dyn_into::<js_sys::Uint8Array>().map_err(|_| JsError::new("files: a file's bytes are a Uint8Array"))?.to_vec();
out.insert(path, bytes);
}
Ok(out)
}
/// A count from JS: a whole number from 0 to `max` (at most 2^53 − 1). A JS number is
/// read as given, never truncated or wrapped as wasm-bindgen's integer types would.
pub(crate) fn int_in(v: f64, max: u64, what: &str) -> Result<u64, String> {
let max = max.min((1 << 53) - 1);
if !(0.0..=max as f64).contains(&v) || v.fract() != 0.0 {
return Err(format!("{what}: a whole number from 0 to {max}"));
}
Ok(v as u64)
}
fn int(v: f64, what: &str) -> Result<u64, JsError> {
int_in(v, u64::MAX, what).map_err(js_err)
}
/// The next file the title in `dir` (below `wai/`) needs and `files` does not hold, or
/// `undefined` once it holds every one.
#[wasm_bindgen(js_name = titleMissing)]
pub fn js_title_missing(dir: &str, files: &js_sys::Map) -> Result<Option<String>, JsError> {
title_missing(dir, &files_of(files)?).map_err(js_err)
}
/// A title's deterministic figures: the text `wai_stage_measure eval --json` prints for
/// it, which is its block of the corpus's `goldens.json`. A refusal's message starts
/// `refused <code>`.
#[wasm_bindgen(js_name = evaluateTitle)]
pub fn evaluate_title(dir: &str, files: &js_sys::Map) -> Result<String, JsError> {
Ok(printed(&measured(dir, &files_of(files)?).map_err(js_err)?.e.to_json()))
}
/// A title that exists to be refused: the code it was refused with, which is the one it
/// states (`refused`). A title evaluated, or refused with another code, is an error.
#[wasm_bindgen(js_name = checkRefusal)]
pub fn check_refusal(dir: &str, files: &js_sys::Map) -> Result<String, JsError> {
refusal(dir, &files_of(files)?).map_err(js_err)
}
/// Simulate a `wai-sim/1` plan over a title's objects and counted bytes: the text
/// `wai_stage_measure sim <fixture> <plan>` prints.
#[wasm_bindgen]
pub fn simulate(dir: &str, files: &js_sys::Map, plan_json: &str) -> Result<String, JsError> {
let m = measured(dir, &files_of(files)?).map_err(js_err)?;
simulated(&Model::from_evaluation(&m.e), plan_json).map_err(js_err)
}
/// Simulate a plan over a `wai-sim-model/1` model: the text `wai_stage_measure sim
/// <model.json> <plan>` prints.
#[wasm_bindgen(js_name = simulateModel)]
pub fn simulate_model(model_json: &str, plan_json: &str) -> Result<String, JsError> {
simulated(&model_of(model_json).map_err(js_err)?, plan_json).map_err(js_err)
}
/// One object's counted bytes (§3): `{counted: {header, manifest, payload, total},
/// framing}` or, where it is not countable, `{counted: null, refused: <code>, component,
/// detail}`.
#[wasm_bindgen(js_name = countObject)]
pub fn count_object(envelope: &[u8]) -> Result<JsValue, JsError> {
to_js(&count_value(envelope))
}
/// §4.2's integers for two RGB8 pictures of `w × h`: `{kind, n_samples, sse_rgb, sse_y,
/// ssim_y_q32, windows}`, the sums of squares as decimal strings.
#[wasm_bindgen(js_name = qualityRgb8)]
pub fn quality_rgb8(w: f64, h: f64, reference: &[u8], test: &[u8]) -> Result<JsValue, JsError> {
let w = int_in(w, u32::MAX.into(), "w").map_err(js_err)? as u32;
let h = int_in(h, u32::MAX.into(), "h").map_err(js_err)? as u32;
to_js(&quality_rgb8_value(w, h, reference, test).map_err(js_err)?)
}
/// The output tree's root over presentations given as a JSON array of `{unit, layers,
/// decoder, out_digest}`, or `undefined` for none. A (unit, layers) given twice is an
/// error.
#[wasm_bindgen(js_name = outputRoot)]
pub fn output_root(outputs_json: &str) -> Result<Option<String>, JsError> {
output_root_of(outputs_json).map_err(js_err)
}
/// A title evaluated once, for a page that reads several things of it.
#[wasm_bindgen]
pub struct MeasureTitle {
m: Measured,
}
#[wasm_bindgen]
impl MeasureTitle {
/// The title in `dir` from `files`, evaluated and checked against its expectation.
#[wasm_bindgen(constructor)]
pub fn new(dir: &str, files: &js_sys::Map) -> Result<MeasureTitle, JsError> {
Ok(MeasureTitle { m: measured(dir, &files_of(files)?).map_err(js_err)? })
}
/// The text `evaluateTitle` gives.
pub fn json(&self) -> String {
printed(&self.m.e.to_json())
}
/// The simulator's model of the title: `wai-sim-model/1`.
pub fn model(&self) -> String {
printed(&Model::from_evaluation(&self.m.e).to_json())
}
/// The text `simulate` gives.
pub fn simulate(&self, plan_json: &str) -> Result<String, JsError> {
simulated(&Model::from_evaluation(&self.m.e), plan_json).map_err(js_err)
}
/// The session the native tool records over the title (`stage_measure::session::recorder_of`):
/// every base, then every layer, decoded on the primary path with no deadline, every
/// object received whole and presented. `host_json` as `sessionBegin` reads it.
pub fn session(&self, host_json: &str, nonce_hex: &str) -> Result<SessionHandle, JsError> {
let host = host_of(host_json).map_err(js_err)?;
let nonce = unhex32(nonce_hex, "session_nonce").map_err(js_err)?;
let (r, subject) = wai::stage_measure::session::recorder_of(&self.m.t, &self.m.e, host, nonce).map_err(js_err)?;
Ok(SessionHandle { r: Some(r), subject })
}
}
/// One session at this sink, recorded as it happens and sealed as a `reconstruct-session`
/// claim (`wai::session_recorder::SessionRecorder`).
#[wasm_bindgen]
pub struct SessionHandle {
r: Option<SessionRecorder>,
/// The entry object's content hash: the claim's subject.
subject: [u8; 32],
}
/// A session's recorder over its entry object `entry` (the first envelope the sink
/// dispatches), whose manifest `media` the claim states and whose content hash is its
/// subject.
pub(crate) fn begin(entry: &[u8], host: SessionHost, nonce: [u8; 32], deadline_us: Option<u64>) -> Result<(SessionRecorder, [u8; 32]), String> {
let w = Wai::from_bytes(entry).map_err(|e| format!("the entry object: {e}"))?;
Ok((SessionRecorder::new(nonce, w.media(), host, deadline_us), wai::jwp_hash::content_hash(entry)))
}
/// Begin a session at its entry object `entry`, the first envelope the sink dispatches:
/// the claim states its manifest's `media` and names it as the subject. `host_json` is the
/// decoder host, `{profile, target, threads}` (`profile` one of jwp-receipts §2.4.12's,
/// `target` a term or `null`, `threads` the decode workers); `nonce_hex` 32 fresh random
/// bytes derived from nothing that identifies a person, a device or an account;
/// `deadline_us` a per-unit deadline, or `undefined` for none.
#[wasm_bindgen(js_name = sessionBegin)]
pub fn session_begin(entry: &[u8], host_json: &str, nonce_hex: &str, deadline_us: Option<f64>) -> Result<SessionHandle, JsError> {
let host = host_of(host_json).map_err(js_err)?;
let nonce = unhex32(nonce_hex, "session_nonce").map_err(js_err)?;
let deadline = deadline_us.map(|d| int(d, "deadline_us")).transpose()?;
let (r, subject) = begin(entry, host, nonce, deadline).map_err(js_err)?;
Ok(SessionHandle { r: Some(r), subject })
}
#[wasm_bindgen]
impl SessionHandle {
fn rec(&mut self) -> Result<&mut SessionRecorder, JsError> {
self.r.as_mut().ok_or_else(|| JsError::new("the session is finished"))
}
/// The handle of the decoder that decoded `envelope` here, through `via` (its
/// manifest's capability where absent; otherwise that capability or its fallback, and
/// any other is refused); `pinned` when the decode verified the parameter set the
/// manifest pins; `first_unit`, which the page states, the title's unit that is unit 0
/// of the object's base (of the object for a base, of the base it refines for a layer).
/// The recorder holds the envelopes, so a `first_unit` that is not a layer's base's, or
/// that puts a presentation outside the units its envelope covers, is refused when the
/// session is sealed. The same decoder on the same object gives the same handle.
#[wasm_bindgen(js_name = decoderFor)]
pub fn decoder_for(&mut self, envelope: &[u8], via: Option<String>, pinned: bool, first_unit: f64) -> Result<u32, JsError> {
let first_unit = int(first_unit, "first_unit")?;
let k = decoder_key(envelope, via.as_deref(), pinned, first_unit).map_err(js_err)?;
Ok(self.rec()?.decoder(k) as u32)
}
/// An object received whole on the primary path, as its bytes — every object the page
/// received, presented or not, and one it refused too: an envelope whose payloads are
/// encrypted, or bytes that are not an envelope at all, are recorded as an object
/// neither counted nor presented, so the session accounts for every byte transferred.
/// The recorder derives its figures, and whether a decoder entry names it, and holds
/// it, so `finish` checks the claim against it. An encrypted envelope's manifest and
/// rendition table are in the clear: a `WAI2` envelope's classical rendition, encrypted
/// or not, counts in the classical length `finish` derives from the envelopes held
/// (jwp-receipts §2.4.12 rule 9), which the page never states. Received again, it is one
/// object with the bytes of both.
pub fn received(&mut self, envelope: &[u8]) -> Result<(), JsError> {
self.rec()?.received_envelope(envelope, SessionPath::Primary);
Ok(())
}
/// A presentation: `unit` at `layers` (1 for the base alone), through the decoder
/// `decoder` names, the SHA-256 of its output's canonical buffer, and how late it was
/// where it missed the session's deadline.
pub fn presented(&mut self, unit: f64, layers: f64, decoder: f64, out_digest_hex: &str, late_us: Option<f64>) -> Result<(), JsError> {
let unit = int(unit, "unit")?;
let layers = int_in(layers, u16::MAX.into(), "layers").map_err(js_err)? as u16;
let decoder = int_in(decoder, u32::MAX.into(), "decoder").map_err(js_err)? as usize;
let digest = unhex32(out_digest_hex, "out_digest").map_err(js_err)?;
let late = late_us.map(|l| int(l, "late_us")).transpose()?;
self.rec()?.presented(unit, layers, decoder, digest, late).map_err(js_err)
}
/// Seal the session, about its entry object: the key from the 32-byte `seed_hex`;
/// `signer_id` the key's `did:key` where absent; `issued_at` an RFC 3339 time. Gives
/// `{claim, link_hash, outputs, objects}`: the claim's wire form (JCS) and what
/// `verifyClaim` checks it against. The recorder checks the claim against what it
/// recorded first — the presentations, the objects and the envelopes it holds — and
/// refuses one a verifier holding them would refuse. A handle seals once.
pub fn finish(&mut self, seed_hex: &str, signer_id: Option<String>, issued_at: &str) -> Result<JsValue, JsError> {
let r = self.r.take().ok_or_else(|| JsError::new("the session is finished"))?;
to_js(&finish(r, self.subject, seed_hex, signer_id.as_deref(), issued_at).map_err(js_err)?)
}
}
/// Check a `reconstruct-session` claim (its wire form) against the presentations
/// (`outputs`, `[{unit, layers, decoder, out_digest}]`), the objects received
/// (`[{content_hash, path, counted, transferred, presented}]`) and, where given, the
/// envelopes received (an array of `Uint8Array`): the signature and the class's rules,
/// the output tree, the consistency of the members the presentations give, the byte sums,
/// and that each decoder entry names what its envelope gives. `""` when accepted,
/// otherwise the reason (jwp-receipts §3.1).
#[wasm_bindgen(js_name = verifyClaim)]
pub fn verify_claim(claim: &str, outputs_json: &str, objects_json: &str, envelopes: Option<js_sys::Array>) -> Result<String, JsError> {
let envs = match envelopes {
None => None,
Some(a) => Some(
a.iter()
.map(|e| e.dyn_into::<js_sys::Uint8Array>().map(|u| u.to_vec()).map_err(|_| JsError::new("envelopes: each is a Uint8Array")))
.collect::<Result<Vec<_>, _>>()?,
),
};
verify_session(claim, outputs_json, objects_json, envs.as_deref()).map_err(js_err)
}
#[cfg(test)]
mod tests {
use super::*;
fn wai_dir() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
}
fn corpus() -> std::path::PathBuf {
wai_dir().join("staged-measure-conformance")
}
fn read_json(p: &std::path::Path) -> Value {
serde_json::from_slice(&std::fs::read(p).unwrap_or_else(|e| panic!("{}: {e}", p.display()))).unwrap()
}
/// The files a title needs, fetched one at a time as `titleMissing` names them, the
/// way a page fetches them.
fn fetch(dir: &str) -> Files {
let mut files = Files::new();
while let Some(p) = title_missing(dir, &files).unwrap_or_else(|e| panic!("{dir}: {e}")) {
let b = std::fs::read(wai_dir().join(&p)).unwrap_or_else(|e| panic!("{p}: {e}"));
assert!(files.insert(p.clone(), b).is_none(), "{p} named twice");
}
files
}
fn fixture_names() -> Vec<String> {
let mut names: Vec<String> = std::fs::read_dir(corpus().join("fixtures"))
.unwrap()
.flatten()
.filter(|d| d.path().join("title.json").is_file())
.map(|d| d.file_name().to_string_lossy().into_owned())
.collect();
names.sort();
names
}
/// Every fixture, its files fetched as `titleMissing` names them and evaluated
/// through the binding, prints its block of `goldens.json` byte for byte (the golden
/// block printed as the native tool prints `eval --json`); the goldens list the
/// fixtures in this order.
#[test]
fn every_fixture_evaluates_to_its_golden() {
let goldens = read_json(&corpus().join("goldens.json"));
let blocks = goldens["fixtures"].as_array().unwrap();
let names = fixture_names();
assert!(names.len() >= 4, "the corpus is present");
assert_eq!(blocks.len(), names.len());
for (name, block) in names.iter().zip(blocks) {
let dir = format!("staged-measure-conformance/fixtures/{name}");
let m = measured(&dir, &fetch(&dir)).unwrap_or_else(|e| panic!("{name}: {e}"));
assert_eq!(printed(&m.e.to_json()), printed(block), "{name}: the evaluation is the golden");
}
}
/// Every refusal title is refused with the code it states.
#[test]
fn every_refusal_title_is_refused_with_its_code() {
let mut n = 0;
for d in std::fs::read_dir(corpus().join("refusals")).unwrap().flatten() {
let name = d.file_name().to_string_lossy().into_owned();
let dir = format!("staged-measure-conformance/refusals/{name}");
let want = read_json(&d.path().join("title.json"))["refused"].as_str().unwrap().to_owned();
assert_eq!(refusal(&dir, &fetch(&dir)).unwrap_or_else(|e| panic!("{name}: {e}")), want, "{name}");
let why = measured(&dir, &fetch(&dir)).err().unwrap_or_else(|| panic!("{name}: evaluated"));
assert!(why.starts_with(&format!("refused {want}: ")), "{name}: evaluateTitle's error names the code first: {why}");
n += 1;
}
assert!(n >= 5, "the refusal titles are present");
}
/// `counting.json`, every object's counted bytes or the code it is not countable with.
#[test]
fn the_counting_cases_count_as_the_corpus_states() {
let c = read_json(&corpus().join("counting.json"));
let cases = c["cases"].as_array().unwrap();
assert!(cases.len() >= 10);
for k in cases {
let file = k["file"].as_str().unwrap();
let got = count_value(&std::fs::read(wai_dir().join(file)).unwrap());
assert_eq!((&got["counted"], &got["refused"], &got["component"]), (&k["counted"], &k["refused"], &k["component"]), "{file}");
}
}
/// `quality.json`'s picture pairs, drawn from its stated formula, give its integers.
#[test]
fn the_quality_cases_give_the_corpus_integers() {
let q = read_json(&corpus().join("quality.json"));
let pic = |w: usize, h: usize, seed: usize, mul: usize| -> Vec<u8> {
let mut v = Vec::with_capacity(3 * w * h);
for y in 0..h {
for x in 0..w {
for c in 0..3 {
v.push(((x * mul + y * (mul + 3) + 29 * c + 17 * seed + 5 * ((x * y + seed) % 13)) % 256) as u8);
}
}
}
v
};
let cases = q["cases"].as_array().unwrap();
assert!(cases.len() >= 6);
for k in cases {
let (w, h) = (k["w"].as_u64().unwrap() as usize, k["h"].as_u64().unwrap() as usize);
let a = pic(w, h, k["reference"][0].as_u64().unwrap() as usize, k["reference"][1].as_u64().unwrap() as usize);
let b = pic(w, h, k["test"][0].as_u64().unwrap() as usize, k["test"][1].as_u64().unwrap() as usize);
let got = quality_rgb8_value(w as u32, h as u32, &a, &b).unwrap();
for m in ["n_samples", "sse_rgb", "sse_y", "ssim_y_q32", "windows"] {
assert_eq!(got[m], k[m], "{w}×{h}: {m}");
}
}
}
/// Every simulator case of `sim/cases.json` gives its result in `sim/results.json`.
#[test]
fn every_simulator_case_gives_its_result() {
let cases = read_json(&corpus().join("sim/cases.json"));
let results = read_json(&corpus().join("sim/results.json"));
let mut evals: BTreeMap<String, Measured> = BTreeMap::new();
let cs = cases["cases"].as_array().unwrap();
assert_eq!(cs.len(), results["results"].as_array().unwrap().len());
for (k, want) in cs.iter().zip(results["results"].as_array().unwrap()) {
let name = k["name"].as_str().unwrap();
let plan = std::fs::read_to_string(corpus().join("sim").join(k["plan"].as_str().unwrap())).unwrap();
let model = match k["model"].as_str().unwrap().strip_prefix("fixture:") {
Some(f) => {
let dir = format!("staged-measure-conformance/fixtures/{f}");
let m = evals.entry(f.to_owned()).or_insert_with(|| measured(&dir, &fetch(&dir)).unwrap());
Model::from_evaluation(&m.e)
}
None => model_of(&std::fs::read_to_string(corpus().join("sim").join(k["model"].as_str().unwrap())).unwrap()).unwrap(),
};
let got: Value = serde_json::from_str(&simulated(&model, &plan).unwrap()).unwrap();
assert_eq!(want["name"], name);
for m in ["stall_bound_us", "max_stall_us", "result"] {
assert_eq!(got[m], want[m], "{name}: {m}");
}
}
}
const T: &str = "2026-10-06T12:00:00Z";
const HOST: &str = r#"{"profile": "browser-baseline", "target": "wasm32-unknown-unknown", "threads": 1}"#;
/// A session recorded the way a page records one — each base and each layer named by
/// the envelope its decoder decoded, every presentation with its output's digest, every
/// object received — seals the claim the native tool's recorder seals over the same
/// title, byte for byte, for every fixture; it commits to the golden output tree and
/// counted bytes, and verifies. An altered output, an object left out and an object
/// miscounted are each refused with their reason.
#[test]
fn a_page_recorded_session_is_the_native_session() {
let goldens = read_json(&corpus().join("goldens.json"));
let seed = "44".repeat(32);
let nonce = "07".repeat(32);
for (name, golden) in fixture_names().iter().zip(goldens["fixtures"].as_array().unwrap()) {
let dir = format!("staged-measure-conformance/fixtures/{name}");
let files = fetch(&dir);
let m = measured(&dir, &files).unwrap();
// The native tool's recording.
let host = host_of(HOST).unwrap();
let (native, entry) = wai::stage_measure::session::recorder_of(&m.t, &m.e, host.clone(), unhex32(&nonce, "n").unwrap()).unwrap();
let want = finish(native, entry, &seed, None, T).unwrap();
// The page's: begun at the entry object, decoders named by envelope, in
// presentation order.
let (mut r, subject) = begin(&m.t.bases[0].obj.bytes, host, unhex32(&nonce, "n").unwrap(), None).unwrap();
assert_eq!(subject, entry, "{name}: the entry object is the first base");
let envelope_of = |object: &[u8; 32]| -> &[u8] {
m.t.bases.iter().map(|b| &b.obj).chain(&m.t.layers).find(|o| wai::jwp_hash::content_hash(&o.bytes) == *object).map(|o| o.bytes.as_slice()).unwrap()
};
for p in &m.e.presentations {
let (cap, pin, object) = &m.e.decoders[usize::from(p.p.decoder)];
let first = m.e.objects.iter().find(|o| o.content_hash == *object).map(|o| m.e.bases[o.base].0.info.first_unit).unwrap();
let h = r.decoder(decoder_key(envelope_of(object), Some(cap), pin.is_some(), first).unwrap());
r.presented(p.p.unit, p.p.layers, h, p.p.out_digest, None).unwrap();
}
for o in &m.e.objects {
r.received_envelope(envelope_of(&o.content_hash), SessionPath::Primary);
}
let got = finish(r, subject, &seed, None, T).unwrap();
assert_eq!(got["claim"], want["claim"], "{name}: the page's session is the native tool's");
let claim: Value = serde_json::from_str(got["claim"].as_str().unwrap()).unwrap();
assert_eq!(claim["body"]["output_root"], golden["output_root"], "{name}: the golden root");
assert_eq!(claim["body"]["leaf_count"], golden["leaf_count"], "{name}: the golden leaf count");
let outputs = got["outputs"].to_string();
let objects = got["objects"].to_string();
assert_eq!(output_root_of(&outputs).unwrap().as_deref(), golden["output_root"].as_str(), "{name}: outputRoot");
assert_eq!(verify_session(got["claim"].as_str().unwrap(), &outputs, &objects, None).unwrap(), "", "{name}: accepted");
let envelopes: Vec<Vec<u8>> = m.e.objects.iter().map(|o| envelope_of(&o.content_hash).to_vec()).collect();
assert_eq!(verify_session(got["claim"].as_str().unwrap(), &outputs, &objects, Some(&envelopes)).unwrap(), "", "{name}: accepted with the envelopes");
let mut bad = got["outputs"].clone();
let d = bad[0]["out_digest"].as_str().unwrap().to_owned();
bad[0]["out_digest"] = json!(format!("{}{}", if d.starts_with('0') { "1" } else { "0" }, &d[1..]));
assert_eq!(verify_session(got["claim"].as_str().unwrap(), &bad.to_string(), &objects, None).unwrap(), "session_output_mismatch", "{name}");
let mut fewer = got["objects"].clone();
fewer.as_array_mut().unwrap().pop();
assert_eq!(verify_session(got["claim"].as_str().unwrap(), &outputs, &fewer.to_string(), None).unwrap(), "session_source_not_received", "{name}");
let mut miscounted = got["objects"].clone();
let c = miscounted[0]["counted"].as_u64().unwrap();
miscounted[0]["counted"] = json!(c + 1);
assert_eq!(verify_session(got["claim"].as_str().unwrap(), &outputs, &miscounted.to_string(), None).unwrap(), "session_counted_mismatch", "{name}");
}
}
/// A decoder is named only through the envelope's own capability or its fallback
/// (SPEC §4); any other `via` is refused before anything is recorded. An entry naming a
/// capability its envelope does not give, recorded past that check, is refused when the
/// session is sealed, against the envelope the recorder holds (`session_source_mismatch`).
/// Bytes that are not an envelope, and an encrypted envelope, no entry names are held,
/// uncounted and not presented, and the session seals.
#[test]
fn a_decoder_is_named_through_its_envelopes_capability() {
let dir = "staged-measure-conformance/fixtures/still";
let files = fetch(dir);
let m = measured(dir, &files).unwrap();
let base = &m.t.bases[0].obj.bytes;
let cap = Wai::from_bytes(base).unwrap().capability().to_owned();
assert_eq!(decoder_key(base, Some(&cap), true, 0).unwrap().capability, cap);
assert_eq!(decoder_key(base, None, true, 0).unwrap().capability, cap);
let why = decoder_key(base, Some("wai.audio.int_codec"), true, 0).unwrap_err();
assert!(why.contains("neither the envelope's capability"), "{why}");
let encrypted = std::fs::read(wai_dir().join("staged-measure-conformance/counting/encrypted.wai")).unwrap();
let p = &m.e.presentations[0];
let session = |capability: Option<&str>| {
let (mut r, subject) = begin(base, host_of(HOST).unwrap(), [7; 32], None).unwrap();
let mut k = decoder_key(base, None, true, 0).unwrap();
if let Some(c) = capability {
k.capability = c.into();
}
let h = r.decoder(k);
r.presented(p.p.unit, p.p.layers, h, p.p.out_digest, None).unwrap();
r.received_envelope(base, SessionPath::Primary);
r.received_envelope(&encrypted, SessionPath::Primary);
r.received_envelope(&base[..7], SessionPath::Primary);
finish(r, subject, &"44".repeat(32), None, T)
};
let s = session(None).unwrap();
let (claim, o, j) = (s["claim"].as_str().unwrap(), s["outputs"].to_string(), s["objects"].to_string());
assert_eq!(s["objects"][1]["counted"], Value::Null, "the encrypted envelope: uncounted");
assert_eq!((s["objects"][2]["counted"].clone(), s["objects"][2]["transferred"].clone()), (Value::Null, Value::from(7)), "bytes that are not an envelope: uncounted, transferred");
assert_eq!(verify_session(claim, &o, &j, Some(&[base.clone(), encrypted.clone(), base[..7].to_vec()])).unwrap(), "", "objects no entry names, uncounted");
let why = session(Some("wai.audio.int_codec")).unwrap_err();
assert!(why.contains("session_source_mismatch"), "{why}");
}
/// A page that receives a `WAI2` envelope it does not decode — its payloads plaintext or
/// encrypted, its rendition table in the clear either way — seals its session: `finish`
/// states the classical length the envelopes held give (jwp-receipts §2.4.12 rule 9),
/// which the page has no way to state, and a verifier holding them accepts it; one not
/// holding that envelope reads none.
#[test]
fn a_session_that_received_a_wai2_states_its_classical_length() {
let dir = "staged-measure-conformance/fixtures/still";
let files = fetch(dir);
let m = measured(dir, &files).unwrap();
let base = &m.t.bases[0].obj.bytes;
let p = &m.e.presentations[0];
for name in ["wai2-classical", "wai2-classical-encrypted"] {
let w = std::fs::read(wai_dir().join(format!("staged-measure-conformance/counting/{name}.wai"))).unwrap();
let png = wai::container::WaiMulti::from_bytes(&w).unwrap().renditions[1].len() as u64;
let (mut r, subject) = begin(base, host_of(HOST).unwrap(), [7; 32], None).unwrap();
let h = r.decoder(decoder_key(base, None, true, 0).unwrap());
r.presented(p.p.unit, p.p.layers, h, p.p.out_digest, None).unwrap();
r.received_envelope(base, SessionPath::Primary);
r.received_envelope(&w, SessionPath::Primary);
let s = finish(r, subject, &"44".repeat(32), None, T).unwrap_or_else(|e| panic!("{name}: {e}"));
let (claim, o, j) = (s["claim"].as_str().unwrap(), s["outputs"].to_string(), s["objects"].to_string());
let body: Value = serde_json::from_str::<Value>(claim).unwrap()["body"].clone();
assert_eq!((body["classical_declared"].as_u64(), body["classical_source"].as_str()), (Some(png), Some("wai2-table")), "{name}");
assert_eq!(verify_session(claim, &o, &j, Some(&[base.clone(), w.clone()])).unwrap(), "", "{name}");
assert_eq!(verify_session(claim, &o, &j, Some(&[base.clone()])).unwrap(), "session_classical_mismatch", "{name}: the WAI2 not held");
}
}
/// Integers from JS are whole numbers in range, read as given: never truncated (0.5,
/// 1.9, 0.99) or wrapped (2^32 + 2), the cases a review found wasm-bindgen's integer
/// types coerced.
#[test]
fn integers_from_js_are_whole_and_in_range() {
for (v, max) in [(0.5, u64::MAX), (1.9, 65_535), (4_294_967_298.0, 65_535), (0.99, u64::from(u32::MAX)), (-1.0, 9), (f64::NAN, 9), (f64::INFINITY, u64::MAX), (65_536.0, 65_535)] {
assert!(int_in(v, max, "x").is_err(), "{v} refused under {max}");
}
assert_eq!(int_in(65_535.0, 65_535, "layers"), Ok(65_535));
assert_eq!(int_in(0.0, 9, "x"), Ok(0));
assert_eq!(int_in(9_007_199_254_740_991.0, u64::MAX, "unit"), Ok(9_007_199_254_740_991));
assert!(int_in(9_007_199_254_740_992.0, u64::MAX, "unit").is_err(), "past 2^53 - 1");
}
/// A browser session states its host as given, its workers rounded down to a power of
/// two, and writes no energy figure.
#[test]
fn a_browser_session_states_its_host_and_no_energy() {
let h = host_of(r#"{"profile": "browser-baseline", "target": null, "threads": 6}"#).unwrap();
assert_eq!((h.profile.as_str(), h.target.as_deref(), h.threads), ("browser-baseline", None, 4));
assert!(host_of(r#"{"profile": "browser-baseline", "target": null, "threads": 1, "energy": 0}"#).is_err(), "no other member");
let dir = "staged-measure-conformance/fixtures/still";
let m = measured(dir, &fetch(dir)).unwrap();
let (r, entry) = wai::stage_measure::session::recorder_of(&m.t, &m.e, h, [1; 32]).unwrap();
let s = finish(r, entry, &"11".repeat(32), Some("a label"), T).unwrap();
let claim: Value = serde_json::from_str(s["claim"].as_str().unwrap()).unwrap();
assert_eq!(claim["body"]["energy"], Value::Null);
assert_eq!(claim["body"]["host"]["threads"], 4);
assert_eq!(claim["signer_id"], "a label");
}
}