wai: staged measurement and the session claim in the browser; a session bound to the envelopes it names (W5-4) #187
72 changed files with 13421 additions and 1060 deletions
3
.github/workflows/wai-conformance.yml
vendored
3
.github/workflows/wai-conformance.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
7
.github/workflows/wai-http.yml
vendored
7
.github/workflows/wai-http.yml
vendored
|
|
@ -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/**'
|
||||
|
|
|
|||
34
.github/workflows/wai-web-wasm.yml
vendored
34
.github/workflows/wai-web-wasm.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
148
wai/CHANGELOG.md
148
wai/CHANGELOG.md
|
|
@ -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
|
||||
|
|
|
|||
33
wai/SPEC.md
33
wai/SPEC.md
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"}},
|
||||
|
|
|
|||
15
wai/http-conformance/falsify/conforming-withheld.patch
Normal file
15
wai/http-conformance/falsify/conforming-withheld.patch
Normal 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()
|
||||
15
wai/http-conformance/falsify/nonconforming-offered.patch
Normal file
15
wai/http-conformance/falsify/nonconforming-offered.patch
Normal 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()
|
||||
15
wai/http-conformance/falsify/refines-read-whole.patch
Normal file
15
wai/http-conformance/falsify/refines-read-whole.patch
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
"2c15a9ca86337508733305f81ef5e193e0796d61e870ccc7abf8af92334c98a2"
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
"48be900eeba898a279b604db9e839ad6486ebb9dcdc521d11413db99ad1b4b94"
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
"4e89b2896be32390caaa435b09f7ae0e448103b03b534c20a4a173c735197f9d"
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
"b36600ee5a7498d8c8246b8440d072b8e6a4cdaee317fc6dfa7ca29841413d90"
|
||||
Binary file not shown.
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
BIN
wai/staged-measure-conformance/counting/wai2-classical.wai
Normal file
BIN
wai/staged-measure-conformance/counting/wai2-classical.wai
Normal file
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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}"))?;
|
||||
|
|
|
|||
|
|
@ -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)?)),
|
||||
])
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
21
wai/wai-web/Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
73
wai/wai-web/demo/verify_measure.sh
Executable file
73
wai/wai-web/demo/verify_measure.sh
Executable 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"
|
||||
389
wai/wai-web/demo/verify_measure_wasm.mjs
Normal file
389
wai/wai-web/demo/verify_measure_wasm.mjs
Normal 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);
|
||||
|
|
@ -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
843
wai/wai-web/src/measure.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue