From cc5b5afddd6ed7dbf4506d3fd213784c39ce32cf Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Wed, 21 Oct 2026 05:06:38 -0400 Subject: [PATCH 1/4] =?UTF-8?q?moq-streaming-format=20=C2=A74.2:=20wai=5Fp?= =?UTF-8?q?riors,=20pinned=20parameter=20sets=20in=20the=20catalog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A track of packaging `wai` may list, in its MSF catalog entry, the pins its objects carry (capability, sha256, form, advisory tier), so a sink can tell before it subscribes whether it holds the parameter sets the track needs. The catalog never stands in for the envelope: an object whose pin the member does not list, or lists differently, is refused with the track (`catalog_prior_mismatch`); a member that does not read refuses the track (`catalog_priors_malformed`). A listed tier is advisory. §5 now states the WAI_RECEIPT property's value layout and its interim type 0x3A41 from MOQT's application range, as joule-moq writes it. - wai-rs staged::moq: CatalogPriors::read / check / check_manifest, PriorsRefusal, with unit tests for each malformation and each mismatch. - wai_staged_vectors: moq/priors.json (generated; verify re-checks it). - staged_verify.py: an independent reading and check of §4.2. - codecs::registry: the `wai_priors` wire member. The corpus file and the registry export are regenerated in the next commit. Co-Authored-By: Claude Opus 5.5 --- wai/extensions/moq-streaming-format.md | 81 ++++++ wai/staged-conformance/README.md | 12 +- wai/staged-conformance/staged_verify.py | 91 +++++- wai/wai-rs/src/bin/wai_staged_vectors.rs | 157 ++++++++++- wai/wai-rs/src/codecs/registry.rs | 1 + wai/wai-rs/src/staged/moq.rs | 340 ++++++++++++++++++++++- 6 files changed, 674 insertions(+), 8 deletions(-) diff --git a/wai/extensions/moq-streaming-format.md b/wai/extensions/moq-streaming-format.md index 6aac407cb..c5608c642 100644 --- a/wai/extensions/moq-streaming-format.md +++ b/wai/extensions/moq-streaming-format.md @@ -80,6 +80,7 @@ fields, the capabilities a sink needs to dispatch what the track carries: | `wai_receipts` | boolean | Whether objects carry receipts per §5. | | `wai_receipt_alias` | integer | The `track_alias` the publisher signs in this track's receipts ([jwp-receipts](jwp-receipts.md) §2.4.1), from 0 to 2^53 − 1. | | `wai_receipts_track` | string | The name, in this track's namespace, of the track that carries this track's receipts (§4.1). | +| `wai_priors` | array of object | OPTIONAL. The parameter sets the track's objects pin (SPEC §3.1), by digest, each with the tier it is offered at (§4.2). Absent means the catalog states none. | - `wai_receipt_alias` names the track's receipt chain, which a verifier keeps per publisher key and `track_alias` (jwp-receipts §3 step 3). MOQT's own Track Alias is @@ -349,6 +350,71 @@ prototype header fields of staged-delivery §13 are not read. "isLive": true } ``` +### 4.2 Pinned parameter sets + +A capability that decodes against a parameter set names that set in each envelope, by a +pin: the SHA-256 of the whole set, in a declared form, with the tier the sender claims +(SPEC §3.1). A track **MAY** also state, in its catalog entry, the pins its objects carry. +A sink can then tell before it subscribes whether it holds the parameter sets the track +needs, and which tier each is offered at: + +```json +"wai_priors": [ + { "capability": "wai.neural.int_hyper", + "sha256": "<64 lowercase hexadecimal digits>", + "form": "wai.prior.set", "tier": "decode-equivalence" } +] +``` + +| member | presence | meaning | +|---|---|---| +| `capability` | REQUIRED, string | A capability the track's `wai_capabilities` lists. | +| `sha256` | REQUIRED, string | A pin's `sha256`: 64 lowercase hexadecimal digits. | +| `form` | OPTIONAL, string | The pin's `form`. Absent, or `null`, where the pin declares none. | +| `tier` | OPTIONAL, string | The tier the publisher offers the set at, from SPEC §3.1's vocabulary. Advisory (below). | + +An entry **lists** a pin for a capability when its `capability` is that capability, its +`sha256` is the pin's, and its `form` is the pin's: both absent, or the same string. A +member of an entry that this revision does not define is ignored. + +- **Reading.** `wai_priors`, where present and not `null`, is an array of objects, each + with the members above of their types and no two with the same `capability`, `sha256` + and `form`, and each `capability` one the track's `wai_capabilities` lists. A sink that + reads anything else refuses the track (`catalog_priors_malformed`): it does not + subscribe to it. A `tier` this revision does not register is read as `none` (SPEC §3.1). +- **Exhaustive.** Where a track carries `wai_priors`, the publisher **MUST** list in it + every pin its objects carry. An object **breaks the catalog** when: + 1. it carries a pin that no entry lists, either for its `capability` or for its + `fallback`; or + 2. an entry names its `capability`, or its `fallback` (not `null`), and the object + carries no pin, or carries a pin that no entry lists for that capability. + + A publisher whose objects' pin can decode through the fallback (SPEC §4 step 5) + **SHOULD** list the set for the fallback as well, so that a sink that holds only the + fallback can make the same decision before it subscribes. +- **Mismatch.** A sink checks each object of such a track against the track's + `wai_priors` before it dispatches the object (SPEC §4). An object that breaks the + catalog is refused (`catalog_prior_mismatch`). The track is refused with it: the sink + dispatches no further object of the track, and **MAY** unsubscribe from it. The + catalog never stands in for the envelope. A sink verifies an object's pin under SPEC + §3.1 whether or not the catalog lists it. An entry never satisfies a pin, never + supplies a pin the envelope lacks, and never changes which set a decode reads. +- **Tier.** A listed `tier` is advisory. A publisher **MUST NOT** list a tier above the + one the capability's registry class gives (SPEC §3.1). A sink **MUST NOT** claim a tier + on the strength of a listed tier: the tier it claims for an object is the one its own + decode earns, under SPEC §3.1 and §7 and [determinism-tiers](determinism-tiers.md) §3, + whatever the catalog says. A listed tier that differs from the tier an object's pin + states is not a mismatch. +- `wai_determinism` (§4) still states the weakest tier any object on the track claims. + +Reference implementation: `staged::moq::CatalogPriors` reads the member +(`CatalogPriors::read`) and checks an object against it (`CatalogPriors::check`, +`CatalogPriors::check_manifest`). `staged-conformance/moq/priors.json` holds a case for +each reason and for each way an object can break the catalog. `wai_staged_vectors` +checks the cases, and so does `staged_verify.py`, which shares no code with `wai-rs`. +The browser sink of `joule-moq-web` dispatches no capability that decodes against a +parameter set, and does not read the member. + ## 5. The per-object property MOQT carries metadata a relay can see as **Properties** of a track or of an object @@ -369,6 +435,21 @@ commitment: | `acquisition_evidence` | bytes | For `OnChipCounter` and `CalibratedInstrument`, the evidence that follows the tag byte in `energy-measurement.md` §2.7 (declared uncertainty, then filtering state or calibration reference). Empty for every other class. | | `group_receipt` | 32 B | The link hash of the group receipt covering this object (`jwp-receipts` §3 step 3). | +On the wire the property is one Key-Value-Pair (MOQT §8.3) of an odd type. Its value is +the five fields in the order of the table, with nothing after them: `content_hash` (32 +bytes); `joules_micro` (a MOQT variable-length integer, MOQT §8.1); `acquisition` (a +variable-length integer giving its length, then its UTF-8 bytes); `acquisition_evidence` +(a variable-length integer giving its length, then the bytes); and `group_receipt` (32 +bytes). A sink refuses a value that ends early, has bytes left after `group_receipt`, or +whose `acquisition` is not UTF-8 (`property_invalid`). The publisher sets the property +inside the object's Immutable Properties (MOQT §10.7), which no relay may change or +remove. No property type is registered for it. The Internet-Draft source in `wai/ietf/` +requests one from the Specification Required range of MOQT's property registry (MOQT +§16.8). Until one is assigned, a track of packaging `wai` carries the property as type +`0x3A41`, from the range MOQT §8.4 sets aside for an application's own use. Another +application may give that type another meaning, so a sink reads it only on a track of +packaging `wai`. + - A non-zero `joules_micro` **MUST** be accompanied by `acquisition`. A joule figure without its acquisition class is exactly what `energy-measurement.md` §2 forbids, and putting it on the wire does not make it acceptable. diff --git a/wai/staged-conformance/README.md b/wai/staged-conformance/README.md index fe8a212d7..236b0f0c5 100644 --- a/wai/staged-conformance/README.md +++ b/wai/staged-conformance/README.md @@ -62,7 +62,15 @@ receipt for it, so that step 5 requires `delivered-verified` of its layers. track), conversion to on demand, and tracks that inherit the catalog track's namespace, checked under that namespace, under another and with none known — and, given the catalog track's namespace each case states, for each set the first rule it - fails. `trace.json` carries the envelopes of + fails. `priors.json` holds track entries around §4.2's `wai_priors`, each with the + manifests of objects on the track: members that read (absent, `null`, an advisory tier + that differs from the pin's, an unregistered tier, a `null` form, an undefined member, + a fallback no entry names, a pin listed only for its fallback, two sets for one + capability), each way an object breaks the catalog (a pin listed for another + capability, another digest, a form declared or dropped, no pin, another set for the + fallback, an empty list, a second object that breaks it) and each way the member does + not read, with the verdict a sink gives the track and each object + (`catalog_priors_malformed`, `catalog_prior_mismatch`). `trace.json` carries the envelopes of `cases/video_int_motion_gop_perframe/` on those tracks, one object to a group (object 0, which each receipt names), through one relay to four subscribers: one of every track; one of the base and layer 1 tracks only; one of those two whose relay drops the @@ -142,7 +150,7 @@ Every layer application, in every case, is checked by both. exploding a `WAI2` component (reading the manifest's members as the text they are) with the refinement role rules; what a packer refuses; `rate_port.json`, from its own integer predictor and its own f64 form; and `moq/`: the catalog - check, each timeout as its slack, the priorities, each object's fate at the relay, + check, the `wai_priors` reading and check of §4.2, each timeout as its slack, the priorities, each object's fate at the relay, and each subscriber's steps replayed through its sink. It takes BLAKE3 from [`../claim-conformance/_blake3.py`](../claim-conformance/_blake3.py) and the rANS decoder and the `WIV1` keyframe-0 decode from diff --git a/wai/staged-conformance/staged_verify.py b/wai/staged-conformance/staged_verify.py index fc15fda07..cd99b4bdc 100644 --- a/wai/staged-conformance/staged_verify.py +++ b/wai/staged-conformance/staged_verify.py @@ -29,7 +29,9 @@ specifications, and implements the rest: - MoQ carriage (moq-streaming-format §4.1): the catalog check, a layer object's timeout as its slack, the priorities, and the trace of moq/: each object's fate at a relay by MOQT's per-hop delivery timeout, each subscriber's sink steps, replayed through this - sink, and the status each layer object comes to. + sink, and the status each layer object comes to; +- a track's wai_priors (moq-streaming-format §4.2): whether it reads, and whether each + object of moq/priors.json breaks it. For every case it starts from the case's stage0.bin, checks its SHA-256, decodes the base independently where it can (WIV1 keyframe 0, PNG, FLAC) and compares, derives the base's @@ -1862,6 +1864,86 @@ def check_catalog(c, inherited=None): return [{"set": n, "reason": check_set(n, sets[n], cat)} for n in sorted(sets)] +# moq-streaming-format §4.2, Pinned parameter sets. +PRIORS_MALFORMED, PRIORS_MISMATCH = "catalog_priors_malformed", "catalog_prior_mismatch" +HEX_LOWER = set("0123456789abcdef") + + +def read_priors(track): + """§4.2, Reading. ("absent", None) where the track's wai_priors is absent or null; + ("malformed", None) where it does not read; else ("ok", entries), each entry + (capability, sha256, form or None, tier read as a registered token, or None).""" + if not isinstance(track, dict) or track.get("wai_priors") is None: + return "absent", None + v = track["wai_priors"] + if not isinstance(v, list): + return "malformed", None + caps = track.get("wai_capabilities") + if not (isinstance(caps, list) and all(isinstance(c, str) for c in caps)): + caps = None + entries = [] + for e in v: + if not isinstance(e, dict): + return "malformed", None + cap, digest, form, tier = e.get("capability"), e.get("sha256"), e.get("form"), e.get("tier") + if not isinstance(cap, str) or not isinstance(digest, str): + return "malformed", None + if len(digest) != 64 or any(ch not in HEX_LOWER for ch in digest): + return "malformed", None + if (form is not None and not isinstance(form, str)) or (tier is not None and not isinstance(tier, str)): + return "malformed", None + if caps is None or cap not in caps: + return "malformed", None + if any((x[0], x[1], x[2]) == (cap, digest, form) for x in entries): + return "malformed", None + if tier is not None and tier not in (DE, EC, NONE): + tier = NONE # an unregistered token reads as none (SPEC §3.1) + entries.append((cap, digest, form, tier)) + return "ok", entries + + +def priors_object(entries, manifest): + """§4.2, Mismatch: catalog_prior_mismatch where the object (its manifest) breaks the + catalog, else None. Its pin is conditioning.prior's sha256 and form; a pin whose + sha256 or form has the wrong type is listed by nothing.""" + m = manifest if isinstance(manifest, dict) else {} + req = m.get("model_requirement") if isinstance(m.get("model_requirement"), dict) else {} + cap = req.get("capability") if isinstance(req.get("capability"), str) else "" + fb = req.get("fallback") if isinstance(req.get("fallback"), str) else None + cond = m.get("conditioning") if isinstance(m.get("conditioning"), dict) else {} + prior = cond.get("prior") + if prior is None: + pin = None + else: + p = prior if isinstance(prior, dict) else {} + digest, form = p.get("sha256"), p.get("form") + pin = (digest, form) if isinstance(digest, str) and (form is None or isinstance(form, str)) else ("", None) + names = [cap] + ([fb] if fb is not None else []) + + def listed(c): + return pin is not None and any(e[0] == c and e[1] == pin[0] and e[2] == pin[1] for e in entries) + + # 1. A pin no entry lists for the capability or for the fallback. + if pin is not None and not any(listed(c) for c in names): + return PRIORS_MISMATCH + # 2. A capability or fallback an entry names, with no pin, or a pin not listed for it. + for c in names: + if any(e[0] == c for e in entries) and not listed(c): + return PRIORS_MISMATCH + return None + + +def priors_expect(track, objects): + """What §4.2 gives a track and its objects (see moq/priors.json's `about`).""" + st, entries = read_priors(track) + if st == "malformed": + return {"track": PRIORS_MALFORMED, "objects": None} + if st == "absent": + return {"track": None, "objects": [None for _ in objects]} + per = [priors_object(entries, m) for m in objects] + return {"track": next((x for x in per if x is not None), None), "objects": per} + + def presentation(clock, unit): return min(clock["t0_ms"] + unit * clock["unit_ms"] + clock["target_latency_ms"], U64) @@ -2114,6 +2196,13 @@ def verify_moq(d, fails): got = check_catalog(c["catalog"], c["namespace"]) if got != c["expect"]: fail(f"catalogs.json {c['name']}: {got} != {c['expect']}") + pcases = json.load(open(os.path.join(md, "priors.json")))["cases"] + if not pcases: + fail("priors.json has no case") + for c in pcases: + got = priors_expect(c["track"], c["objects"]) + if got != c["expect"]: + fail(f"priors.json {c['name']}: {got} != {c['expect']}") t = json.load(open(os.path.join(md, "trace.json"))) pr = [x["priority"] for x in t["tracks"]] if not (all(pr[0] < x for x in pr[1:]) and all(a <= b for a, b in zip(pr[1:], pr[2:]))): diff --git a/wai/wai-rs/src/bin/wai_staged_vectors.rs b/wai/wai-rs/src/bin/wai_staged_vectors.rs index f207fe0cd..6eb53b37c 100644 --- a/wai/wai-rs/src/bin/wai_staged_vectors.rs +++ b/wai/wai-rs/src/bin/wai_staged_vectors.rs @@ -11,7 +11,8 @@ //! conforming sink reports at each step and holds at the end. It also writes //! `schedule.json` (arrival traces and what a sink presents), `index.json`, and `moq/`: //! the MSF catalog of a stage set carried over MoQ, catalogs around its check -//! (moq-streaming-format §4.1), and a trace of the set's objects through a relay that +//! (moq-streaming-format §4.1), track entries whose `wai_priors` (§4.2) does or does not +//! read with objects that do or do not match it, and a trace of the set's objects through a relay that //! drops late layers by their delivery timeout, with what each subscriber's sink then //! does. Every file is a function of those inputs, so a second run writes the same bytes. //! @@ -39,15 +40,15 @@ use serde_json::{json, Value}; use wai::codecs::{ DeclaredTier, CAP_AUDIO_FLAC, CAP_AUDIO_INT_CODEC, CAP_AUDIO_INT_REFINE, CAP_IMAGE_INT_REFINE, CAP_IMAGE_PNG, - CAP_NEURAL_INT_HYPER, CAP_PRIOR_SET, CAP_SPLAT_INT_CODEC, CAP_SPLAT_INT_REFINE, CAP_VIDEO_INT_HYPER, + CAP_NEURAL_INT_HYPER, CAP_NEURAL_INT_SYNTH, CAP_PRIOR_SET, CAP_SPLAT_INT_CODEC, CAP_SPLAT_INT_REFINE, CAP_VIDEO_INT_HYPER, CAP_VIDEO_INT_MOTION, CAP_VIDEO_INT_REFINE, }; use wai::container::{Conditioning, Manifest, ModelRequirement, Prev, PrevRun, Prior, Refines, Rendition, RenditionMeta, Span, Wai, WaiMulti}; use wai::int_payload::{decode_int, resolve, IntDecoded}; use wai::pin::HeldBytes; use wai::staged::moq::{ - check_catalog, check_catalog_in, depends_for, effective_timeout_ms, expired_at_hop, layer_timeout_ms, priorities_ok, receipts_track_json, SetRefusal, - SetTrack, StagedTrack, + check_catalog, check_catalog_in, depends_for, effective_timeout_ms, expired_at_hop, layer_timeout_ms, priorities_ok, receipts_track_json, CatalogPriors, + SetRefusal, SetTrack, StagedTrack, }; use wai::staged::schedule::{deadline_misses, present, UnitClock}; use wai::staged::wir1::{self, Below, Next, Predictor, RefineParams, UnitKind, Wir1Cost}; @@ -2492,10 +2493,147 @@ fn moq_files(all: &[Case]) -> Vec<(String, Vec)> { vec![ ("moq/catalog.json".into(), json_bytes(&moq_catalog())), ("moq/catalogs.json".into(), json_bytes(&moq_catalogs_json())), + ("moq/priors.json".into(), json_bytes(&moq_priors_json())), ("moq/trace.json".into(), json_bytes(&trace)), ] } +// ───────────────────────── Pinned parameter sets (moq-streaming-format §4.2) ───────────────────────── + +/// Two parameter-set digests the cases pin: 64 lowercase hexadecimal digits each. +const PRIORS_A: &str = "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"; +const PRIORS_B: &str = "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2"; + +/// The catalog entry of an on-demand track of packaging `wai` that declares `int_hyper`, +/// `int_synth` and PNG, with `priors` as its `wai_priors` where given. +fn priors_track(priors: Option) -> Value { + let mut t = json!({"namespace": "example/net", "name": "pictures", "packaging": "wai", "isLive": false, "role": "video", + "wai_capabilities": [CAP_NEURAL_INT_HYPER, CAP_NEURAL_INT_SYNTH, CAP_IMAGE_PNG], "wai_receipts": false}); + if let Some(p) = priors { + t["wai_priors"] = p; + } + t +} + +/// An entry of `wai_priors`. +fn priors_entry(cap: &str, sha: &str, form: Option<&str>, tier: Option<&str>) -> Value { + let mut e = json!({"capability": cap, "sha256": sha}); + if let Some(f) = form { + e["form"] = json!(f); + } + if let Some(t) = tier { + e["tier"] = json!(t); + } + e +} + +/// A `WAI1` manifest (SPEC §3) of `cap`, falling back to `fb`, pinned to (`sha`, `form`) +/// at `tier`, or unpinned. +fn priors_manifest(cap: &str, fb: Option<&str>, pin: Option<(&str, Option<&str>, &str)>) -> Value { + let mut m = json!({"wai": "1.0", "media": "image", "intent": "replicate", + "model_requirement": {"capability": cap, "fallback": fb}, "conditioning": {"kind": "int"}}); + if let Some((sha, form, tier)) = pin { + let mut p = json!({"id": "set", "sha256": sha, "determinism": "int-rans+int-synth"}); + if let Some(f) = form { + p["form"] = json!(f); + } + p["tier"] = json!(tier); + m["conditioning"]["prior"] = p; + } + m +} + +/// What §4.2 gives a track and its objects: `catalog_priors_malformed` where the member +/// does not read (and no object is checked); otherwise each object's verdict, and the +/// track's, the first object's refusal; nothing checked where the member is absent. +fn moq_priors_expect(track: &Value, objects: &[Value]) -> Value { + match CatalogPriors::read(track) { + Err(r) => json!({"track": r.code(), "objects": null}), + Ok(None) => json!({"track": null, "objects": objects.iter().map(|_| Value::Null).collect::>()}), + Ok(Some(p)) => { + let per: Vec = objects.iter().map(|m| p.check_manifest(m).err().map_or(Value::Null, |r| json!(r.code()))).collect(); + let track = per.iter().find(|v| !v.is_null()).cloned().unwrap_or(Value::Null); + json!({"track": track, "objects": per}) + } + } +} + +/// The cases: a track entry, the manifests of its objects, and what each states. +fn moq_priors_cases() -> Vec<(&'static str, &'static str, Value, Vec)> { + let (h, s, png, set) = (CAP_NEURAL_INT_HYPER, CAP_NEURAL_INT_SYNTH, CAP_IMAGE_PNG, CAP_PRIOR_SET); + let (a, b) = (PRIORS_A, PRIORS_B); + let de = "decode-equivalence"; + let track = |entries: Vec| priors_track(Some(Value::Array(entries))); + let pinned = |cap: &'static str, fb: Option<&'static str>, sha: &'static str, form: Option<&'static str>| priors_manifest(cap, fb, Some((sha, form, de))); + let unpinned = |cap: &'static str| priors_manifest(cap, None, None); + let mut no_caps = priors_track(Some(json!([priors_entry(h, a, Some(set), None)]))); + no_caps.as_object_mut().expect("entry").remove("wai_capabilities"); + vec![ + // Members that read, and objects that match. + ("listed", "the object's pin is the one listed for its capability", track(vec![priors_entry(h, a, Some(set), Some(de))]), vec![pinned(h, None, a, Some(set))]), + ("absent", "no wai_priors: nothing is checked, pinned or not", priors_track(None), vec![pinned(h, None, a, Some(set)), unpinned(h)]), + ("null", "wai_priors null is absent", priors_track(Some(Value::Null)), vec![pinned(h, None, b, None)]), + ( + "tier_advisory", + "the listed tier differs from the pin's: advisory, no mismatch", + track(vec![priors_entry(h, a, Some(set), Some(de))]), + vec![priors_manifest(h, None, Some((a, Some(set), "entropy-consistency")))], + ), + ("tier_unregistered", "a tier token SPEC §3.1 does not register reads as none, and the member reads", track(vec![priors_entry(h, a, Some(set), Some("bit-exact"))]), vec![pinned(h, None, a, Some(set))]), + ("form_null", "a null form, listed and pinned, is an absent one", track(vec![json!({"capability": h, "sha256": a, "form": null})]), vec![pinned(h, None, a, None)]), + ("unknown_member", "a member of an entry that §4.2 does not define is ignored", track(vec![json!({"capability": h, "sha256": a, "form": set, "id": "int_hyper/q6"})]), vec![pinned(h, None, a, Some(set))]), + ("fallback_unnamed", "a fallback no entry names needs nothing listed", track(vec![priors_entry(h, a, Some(set), None)]), vec![pinned(h, Some(png), a, Some(set))]), + ("listed_for_fallback", "a pin listed only for the fallback, where it decodes", track(vec![priors_entry(s, a, Some(set), None)]), vec![pinned("wai.neural.unregistered", Some(s), a, Some(set))]), + ("two_sets", "two sets for one capability: either pin matches", track(vec![priors_entry(h, a, Some(set), None), priors_entry(h, b, Some(set), None)]), vec![pinned(h, None, b, Some(set)), pinned(h, None, a, Some(set))]), + ("unpinned_unnamed", "an unpinned object whose capability no entry names", track(vec![priors_entry(h, a, Some(set), None)]), vec![unpinned(s)]), + // An object that breaks the catalog: refused, and the track with it. + ("mismatch_capability", "the pin is listed, but for a capability the object does not name", track(vec![priors_entry(s, a, Some(set), None)]), vec![pinned(h, None, a, Some(set))]), + ("mismatch_digest", "the object's capability is listed with another digest", track(vec![priors_entry(h, b, Some(set), None)]), vec![pinned(h, None, a, Some(set))]), + ("mismatch_form", "the pin declares a form; the entry lists none", track(vec![priors_entry(h, a, None, None)]), vec![pinned(h, None, a, Some(set))]), + ("mismatch_form_dropped", "the entry lists a form; the pin declares none", track(vec![priors_entry(h, a, Some(set), None)]), vec![pinned(h, None, a, None)]), + ("mismatch_unpinned", "the catalog names the capability; the object carries no pin", track(vec![priors_entry(h, a, Some(set), None)]), vec![unpinned(h)]), + ( + "mismatch_fallback", + "the capability's set is listed, the fallback's is another", + track(vec![priors_entry(h, a, Some(set), None), priors_entry(s, b, Some(set), None)]), + vec![pinned(h, Some(s), a, Some(set))], + ), + ("mismatch_empty", "an empty list: an object that carries a pin breaks it", track(vec![]), vec![pinned(h, None, a, Some(set))]), + ( + "mismatch_second_object", + "the first object matches, the second does not: the track is refused at the second", + track(vec![priors_entry(h, a, Some(set), None)]), + vec![pinned(h, None, a, Some(set)), pinned(h, None, b, Some(set))], + ), + // A member that does not read: the sink does not subscribe. + ("malformed_not_array", "wai_priors is an object", priors_track(Some(json!({"capability": h, "sha256": a}))), vec![]), + ("malformed_entry", "an entry that is not an object", track(vec![json!(a)]), vec![]), + ("malformed_no_capability", "an entry without capability", track(vec![json!({"sha256": a})]), vec![]), + ("malformed_undeclared", "a capability the track's wai_capabilities does not list", track(vec![priors_entry("wai.neural.elic", a, None, None)]), vec![]), + ("malformed_no_capabilities", "a track with no wai_capabilities to list the capability", no_caps, vec![]), + ("malformed_uppercase", "a digest in upper-case digits", track(vec![priors_entry(h, &PRIORS_A.to_uppercase(), None, None)]), vec![]), + ("malformed_short", "a digest of 62 digits", track(vec![priors_entry(h, &PRIORS_A[..62], None, None)]), vec![]), + ("malformed_form", "a form that is not a string", track(vec![json!({"capability": h, "sha256": a, "form": 5})]), vec![]), + ("malformed_tier", "a tier that is not a string", track(vec![json!({"capability": h, "sha256": a, "tier": true})]), vec![]), + ("malformed_duplicate", "the same capability, digest and form twice", track(vec![priors_entry(h, a, Some(set), None), priors_entry(h, a, Some(set), Some(de))]), vec![]), + ] +} + +fn moq_priors_json() -> Value { + let cases: Vec = moq_priors_cases() + .into_iter() + .map(|(name, about, track, objects)| { + let expect = moq_priors_expect(&track, &objects); + json!({"name": name, "about": about, "track": track, "objects": objects, "expect": expect}) + }) + .collect(); + json!({ + "format": "wai-staged-moq-priors/1", + "about": "moq-streaming-format §4.2, Pinned parameter sets: a track's catalog entry, the manifests of objects on it in order, and what a sink gives them. `expect.track` is catalog_priors_malformed where wai_priors does not read (then no object is checked, and `expect.objects` is null); otherwise the first object's catalog_prior_mismatch, or null. `expect.objects` gives each object's verdict: catalog_prior_mismatch where it breaks the catalog, null otherwise, and null for every object of a track without wai_priors.", + "cases": cases, + }) +} + /// The MoQ files, checked from what is committed: the catalog passes §4.1's check; each /// catalog case gives its stated reasons; and the trace's timeouts, priorities and expected /// outcome follow from its inputs and the case's committed envelopes and stage 0. @@ -2510,6 +2648,17 @@ fn verify_moq(dir: &Path, fail: &mut dyn FnMut(String)) { fail(format!("moq/catalogs.json {}: the check gives other reasons", c["name"])); } } + let priors = read("moq/priors.json"); + let priors_cases = priors["cases"].as_array().cloned().unwrap_or_default(); + if priors_cases.is_empty() { + fail("moq/priors.json: no case".into()); + } + for c in &priors_cases { + let objects = c["objects"].as_array().cloned().unwrap_or_default(); + if moq_priors_expect(&c["track"], &objects) != c["expect"] { + fail(format!("moq/priors.json {}: the check gives other verdicts", c["name"])); + } + } let trace = read("moq/trace.json"); let u = |v: &Value| v.as_u64().unwrap_or(u64::MAX); let tracks = trace["tracks"].as_array().cloned().unwrap_or_default(); diff --git a/wai/wai-rs/src/codecs/registry.rs b/wai/wai-rs/src/codecs/registry.rs index 4cd0e0c41..f90b3e696 100644 --- a/wai/wai-rs/src/codecs/registry.rs +++ b/wai/wai-rs/src/codecs/registry.rs @@ -560,6 +560,7 @@ pub const WIRE_IDENTIFIERS: &[WireIdentifier] = &[ wire("wai_receipts", WireKind::Catalog, MOQ_FORMAT, "§4", WireStatus::Defined), wire("wai_receipt_alias", WireKind::Catalog, MOQ_FORMAT, "§4", WireStatus::Defined), wire("wai_receipts_track", WireKind::Catalog, MOQ_FORMAT, "§4", WireStatus::Defined), + wire("wai_priors", WireKind::Catalog, MOQ_FORMAT, "§4.2", WireStatus::Defined), wire("wai_staged", WireKind::Catalog, MOQ_FORMAT, "§4.1", WireStatus::Defined), wire("wai-receipts", WireKind::Catalog, MOQ_FORMAT, "§4.1", WireStatus::Defined), wire("WAI_RECEIPT", WireKind::Catalog, MOQ_FORMAT, "§5", WireStatus::Defined), diff --git a/wai/wai-rs/src/staged/moq.rs b/wai/wai-rs/src/staged/moq.rs index 01044fbea..bd1956c58 100644 --- a/wai/wai-rs/src/staged/moq.rs +++ b/wai/wai-rs/src/staged/moq.rs @@ -13,6 +13,10 @@ //! - [`effective_timeout_ms`] and [`expired_at_hop`] are MOQT's `OBJECT_DELIVERY_TIMEOUT` //! rule (draft-ietf-moq-transport-22 §5.2) at one hop; [`layer_timeout_ms`] and //! [`base_timeout_floor_ms`] are §4.1's bounds on the timeouts a publisher sets. +//! - [`CatalogPriors`] is a track's `wai_priors` (§4.2), on any track of packaging `wai`: +//! [`CatalogPriors::read`] reads it, giving [`PriorsRefusal::Malformed`] where it does +//! not read, and [`CatalogPriors::check`] / [`CatalogPriors::check_manifest`] check an +//! object against it before dispatch ([`PriorsRefusal::Mismatch`]). //! //! Pure functions of their inputs, no I/O: the corpus's MoQ trace //! (`staged-conformance/moq/`) is replayed the same way here, by joule-moq's relay and @@ -23,6 +27,7 @@ use std::collections::{BTreeMap, BTreeSet}; use serde_json::{json, Map, Value}; use super::schedule::{presentation_ms, UnitClock}; +use crate::codecs::DeclaredTier; /// The `packaging` of a track whose objects are WAI envelopes (moq-streaming-format §3). pub const PACKAGING: &str = "wai"; @@ -436,6 +441,191 @@ fn check_receipts(es: &[Entry], cat: &Catalog<'_>) -> Result<(), SetRefusal> { Ok(()) } +// ── Pinned parameter sets (§4.2) ──────────────────────────────────────────── + +/// Why a sink refuses a track over its `wai_priors` (moq-streaming-format §4.2). +/// +/// cbindgen:ignore +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum PriorsRefusal { + /// The member does not read: the sink does not subscribe to the track. + Malformed, + /// An object breaks the catalog: the sink refuses the object, and the track with it. + Mismatch, +} + +impl PriorsRefusal { + /// Every reason, in §4.2's order. + /// + /// cbindgen:ignore + pub const ALL: [PriorsRefusal; 2] = [Self::Malformed, Self::Mismatch]; + + /// The token §4.2 names. + pub fn code(self) -> &'static str { + match self { + Self::Malformed => "catalog_priors_malformed", + Self::Mismatch => "catalog_prior_mismatch", + } + } + + /// The reason whose token is `code`. + pub fn from_code(code: &str) -> Option { + Self::ALL.into_iter().find(|r| r.code() == code) + } +} + +/// One entry of a track's `wai_priors` (§4.2): a pin an object on the track carries, for +/// one capability, and the tier the publisher offers its set at. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CatalogPrior { + /// A capability the track's `wai_capabilities` lists. + pub capability: String, + /// The pin's `sha256`: 64 lowercase hexadecimal digits. + pub sha256: String, + /// The pin's `form`; `None` where the pin declares none. + pub form: Option, + /// The listed tier, as a sink reads it: `None` where none is listed, and + /// [`DeclaredTier::None`] for a token SPEC §3.1 does not register. Advisory: a sink + /// claims the tier its own decode earns, never this one. + pub tier: Option, +} + +impl CatalogPrior { + /// Whether this entry lists the pin (`sha256`, `form`) for `capability`. + pub fn lists(&self, capability: &str, sha256: &str, form: Option<&str>) -> bool { + self.capability == capability && self.sha256 == sha256 && self.form.as_deref() == form + } + + /// The entry as JSON; an absent `form` or `tier` is not written. + pub fn to_json(&self) -> Value { + let mut o = Map::new(); + o.insert("capability".into(), json!(self.capability)); + o.insert("sha256".into(), json!(self.sha256)); + if let Some(f) = &self.form { + o.insert("form".into(), json!(f)); + } + if let Some(t) = self.tier { + o.insert("tier".into(), json!(t.token())); + } + Value::Object(o) + } +} + +/// A track's `wai_priors` (§4.2), read. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct CatalogPriors { + pub entries: Vec, +} + +/// 64 lowercase hexadecimal digits, as SPEC §3.1 writes a pin's `sha256`. +fn is_sha256_hex(s: &str) -> bool { + s.len() == 64 && s.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) +} + +/// An OPTIONAL string member: `Ok(None)` absent or `null`, `Err` for any other type. +fn opt_str(o: &Map, k: &str) -> Result, PriorsRefusal> { + match o.get(k) { + None | Some(Value::Null) => Ok(None), + Some(Value::String(s)) => Ok(Some(s.clone())), + Some(_) => Err(PriorsRefusal::Malformed), + } +} + +impl CatalogPriors { + /// The `wai_priors` of a track's catalog entry (§4.2, *Reading*): `Ok(None)` where it + /// is absent or `null`; otherwise its entries, or [`PriorsRefusal::Malformed`] where it + /// is not an array of objects, an entry lacks `capability` or `sha256` or holds a + /// member of the wrong type, a `sha256` is not 64 lowercase hexadecimal digits, two + /// entries share `capability`, `sha256` and `form`, or an entry's `capability` is not + /// one the entry's `wai_capabilities` lists. A member of an entry §4.2 does not define + /// is ignored. + pub fn read(track: &Value) -> Result, PriorsRefusal> { + let m = PriorsRefusal::Malformed; + let list = match track.get("wai_priors") { + None | Some(Value::Null) => return Ok(None), + Some(Value::Array(a)) => a, + Some(_) => return Err(m), + }; + let caps: Option> = + track.get("wai_capabilities").and_then(Value::as_array).and_then(|a| a.iter().map(Value::as_str).collect()); + let mut entries: Vec = Vec::with_capacity(list.len()); + for v in list { + let o = v.as_object().ok_or(m)?; + let capability = o.get("capability").and_then(Value::as_str).ok_or(m)?.to_string(); + let sha256 = o.get("sha256").and_then(Value::as_str).filter(|s| is_sha256_hex(s)).ok_or(m)?.to_string(); + let form = opt_str(o, "form")?; + let tier = opt_str(o, "tier")?.map(|t| DeclaredTier::from_token(&t).unwrap_or(DeclaredTier::None)); + if !caps.as_ref().is_some_and(|c| c.contains(&capability.as_str())) { + return Err(m); + } + if entries.iter().any(|e| e.lists(&capability, &sha256, form.as_deref())) { + return Err(m); + } + entries.push(CatalogPrior { capability, sha256, form, tier }); + } + Ok(Some(CatalogPriors { entries })) + } + + /// The member as JSON, entries in order. + pub fn to_json(&self) -> Value { + Value::Array(self.entries.iter().map(CatalogPrior::to_json).collect()) + } + + /// Whether some entry names `capability`. + pub fn names(&self, capability: &str) -> bool { + self.entries.iter().any(|e| e.capability == capability) + } + + /// Whether some entry lists the pin (`sha256`, `form`) for `capability`. + pub fn lists(&self, capability: &str, sha256: &str, form: Option<&str>) -> bool { + self.entries.iter().any(|e| e.lists(capability, sha256, form)) + } + + /// §4.2, *Mismatch*: check one object, whose envelope names `capability` and + /// `fallback` and carries `pin` (its `sha256` and `form`) or none, before it is + /// dispatched. [`PriorsRefusal::Mismatch`] where it carries a pin that no entry lists + /// for its capability or for its fallback; or where an entry names its capability, or + /// its fallback, and it carries no pin, or a pin no entry lists for that capability. + /// Passing proves nothing about the pin: a sink still verifies it under SPEC §3.1. + pub fn check(&self, capability: &str, fallback: Option<&str>, pin: Option<(&str, Option<&str>)>) -> Result<(), PriorsRefusal> { + let named: Vec<&str> = std::iter::once(capability).chain(fallback).collect(); + let listed = |c: &str| pin.is_some_and(|(sha, form)| self.lists(c, sha, form)); + if pin.is_some() && !named.iter().any(|c| listed(c)) { + return Err(PriorsRefusal::Mismatch); + } + if named.iter().any(|c| self.names(c) && !listed(c)) { + return Err(PriorsRefusal::Mismatch); + } + Ok(()) + } + + /// [`CatalogPriors::check`] over an envelope's manifest (SPEC §3): its + /// `model_requirement.capability` and `.fallback`, and the `sha256` and `form` of its + /// `conditioning.prior`, where that is present and not `null` (a `null` `form` is an + /// absent one, SPEC §3.1). A member of the wrong type makes a manifest malformed, + /// which no sink dispatches (SPEC §3.1). Here such a capability reads as the empty + /// string, and a pin whose `sha256` or `form` has the wrong type as a pin no entry + /// lists. + pub fn check_manifest(&self, manifest: &Value) -> Result<(), PriorsRefusal> { + let req = manifest.get("model_requirement"); + let capability = req.and_then(|r| r.get("capability")).and_then(Value::as_str).unwrap_or(""); + let fallback = req.and_then(|r| r.get("fallback")).and_then(Value::as_str); + let pin = match manifest.get("conditioning").and_then(|c| c.get("prior")) { + None | Some(Value::Null) => None, + Some(p) => { + let sha = p.get("sha256").and_then(Value::as_str); + match (sha, p.get("form")) { + (Some(s), None | Some(Value::Null)) => Some((s, None)), + (Some(s), Some(Value::String(f))) => Some((s, Some(f.as_str()))), + // No entry's digest is empty (`read` requires 64 digits). + _ => Some(("", None)), + } + } + }; + self.check(capability, fallback, pin) + } +} + #[cfg(test)] mod tests { use super::*; @@ -685,8 +875,156 @@ mod tests { let i = text[at..].find(&tok).unwrap_or_else(|| panic!("{tok} is not in §4.1 after the one before it")); at += i + tok.len(); } - for name in ["\"wai_staged\"", "`wai_receipt_alias`", "`wai_receipts_track`", "\"wai-receipts\""] { + for name in ["\"wai_staged\"", "`wai_receipt_alias`", "`wai_receipts_track`", "\"wai-receipts\"", "`wai_priors`"] { assert!(text.contains(name), "{name}"); } + let at42 = text.find("### 4.2 Pinned parameter sets").expect("§4.2"); + let mut at = at42; + for r in PriorsRefusal::ALL { + let tok = format!("`{}`", r.code()); + let i = text[at..].find(&tok).unwrap_or_else(|| panic!("{tok} is not in §4.2 after the one before it")); + at += i + tok.len(); + } + } + + const A: &str = "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"; + const B: &str = "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2"; + const HYPER: &str = "wai.neural.int_hyper"; + const SYNTH: &str = "wai.neural.int_synth"; + const SET: &str = "wai.prior.set"; + + /// A track entry declaring `int_hyper`, `int_synth` and PNG, with `priors` as its + /// `wai_priors`. + fn priors_track(priors: Value) -> Value { + json!({"namespace": "example/net", "name": "pic", "packaging": "wai", "isLive": false, + "wai_capabilities": [HYPER, SYNTH, "wai.image.png"], "wai_receipts": false, "wai_priors": priors}) + } + + fn entry(cap: &str, sha: &str, form: Option<&str>) -> Value { + let mut e = json!({"capability": cap, "sha256": sha}); + if let Some(f) = form { + e["form"] = json!(f); + } + e + } + + fn read(priors: Value) -> Result, PriorsRefusal> { + CatalogPriors::read(&priors_track(priors)) + } + + /// A manifest for `cap`, falling back to `fb`, pinned to (`sha`, `form`) or unpinned. + fn manifest(cap: &str, fb: Option<&str>, pin: Option<(&str, Option<&str>)>) -> Value { + let mut m = json!({"wai": "1.0", "media": "image", "intent": "replicate", + "model_requirement": {"capability": cap, "fallback": fb}, "conditioning": {"kind": "int"}}); + if let Some((sha, form)) = pin { + let mut p = json!({"id": "set", "sha256": sha, "determinism": "int", "tier": "decode-equivalence"}); + if let Some(f) = form { + p["form"] = json!(f); + } + m["conditioning"]["prior"] = p; + } + m + } + + /// §4.2, *Reading*: the member reads where it is an array of entries of their types, + /// and every other value is `catalog_priors_malformed`. + #[test] + fn catalog_priors_read_and_refuse_each_malformation() { + assert_eq!(CatalogPriors::read(&json!({"wai_capabilities": [HYPER]})), Ok(None), "absent"); + assert_eq!(read(json!(null)), Ok(None), "null is absent"); + assert_eq!(read(json!([])), Ok(Some(CatalogPriors::default())), "an empty list: the objects carry no pin"); + let ok = read(json!([{"capability": HYPER, "sha256": A, "form": SET, "tier": "decode-equivalence", "id": "x"}])).unwrap().unwrap(); + assert_eq!( + ok.entries, + vec![CatalogPrior { capability: HYPER.into(), sha256: A.into(), form: Some(SET.into()), tier: Some(DeclaredTier::DecodeEquivalence) }], + "an undefined member (`id`) is ignored" + ); + assert_eq!(CatalogPriors::read(&priors_track(ok.to_json())), Ok(Some(ok.clone())), "round trip"); + let unregistered = read(json!([{"capability": HYPER, "sha256": A, "tier": "bit-exact"}])).unwrap().unwrap(); + assert_eq!(unregistered.entries[0].tier, Some(DeclaredTier::None), "an unregistered tier reads as none, and is no malformation"); + let null_form = read(json!([{"capability": HYPER, "sha256": A, "form": null, "tier": null}])).unwrap().unwrap(); + assert_eq!((null_form.entries[0].form.as_deref(), null_form.entries[0].tier), (None, None), "null members are absent ones"); + let m = Err(PriorsRefusal::Malformed); + for (bad, why) in [ + (json!({}), "not an array"), + (json!("a1"), "not an array"), + (json!([5]), "an entry that is not an object"), + (json!([{"sha256": A}]), "no capability"), + (json!([{"capability": 7, "sha256": A}]), "a capability that is not a string"), + (json!([{"capability": "wai.neural.elic", "sha256": A}]), "a capability wai_capabilities does not list"), + (json!([{"capability": HYPER}]), "no sha256"), + (json!([{"capability": HYPER, "sha256": A.to_uppercase()}]), "upper-case digits"), + (json!([{"capability": HYPER, "sha256": &A[..62]}]), "62 digits"), + (json!([{"capability": HYPER, "sha256": format!("{}g", &A[..63])}]), "a digit that is not hexadecimal"), + (json!([{"capability": HYPER, "sha256": A, "form": 5}]), "a form that is not a string"), + (json!([{"capability": HYPER, "sha256": A, "tier": true}]), "a tier that is not a string"), + (json!([entry(HYPER, A, Some(SET)), entry(HYPER, A, Some(SET))]), "the same entry twice"), + ] { + assert_eq!(read(bad.clone()), m, "{why}: {bad}"); + } + // The same digest under two forms, or for two capabilities, is two entries. + assert!(read(json!([entry(HYPER, A, Some(SET)), entry(HYPER, A, None), entry(SYNTH, A, Some(SET))])).unwrap().is_some()); + let no_caps = json!({"name": "pic", "packaging": "wai", "wai_priors": [entry(HYPER, A, None)]}); + assert_eq!(CatalogPriors::read(&no_caps), m, "no wai_capabilities to list the capability"); + for r in PriorsRefusal::ALL { + assert_eq!(PriorsRefusal::from_code(r.code()), Some(r)); + } + } + + /// §4.2, *Mismatch*: an object whose pin the catalog does not list, or lists + /// differently, is refused; a catalog entry never stands in for a pin; a listed tier + /// is advisory and decides nothing. + #[test] + fn catalog_priors_refuse_each_mismatch() { + let p = |v: Value| read(v).unwrap().unwrap(); + let mismatch = Err(PriorsRefusal::Mismatch); + let hyper_a = p(json!([{"capability": HYPER, "sha256": A, "form": SET, "tier": "decode-equivalence"}])); + // Listed: passes, whatever tier the pin states. + assert_eq!(hyper_a.check_manifest(&manifest(HYPER, None, Some((A, Some(SET))))), Ok(())); + let mut ec = manifest(HYPER, None, Some((A, Some(SET)))); + ec["conditioning"]["prior"]["tier"] = json!("entropy-consistency"); + assert_eq!(hyper_a.check_manifest(&ec), Ok(()), "a listed tier is advisory: a different pin tier is no mismatch"); + // A fallback the catalog does not name needs nothing listed. + assert_eq!(hyper_a.check_manifest(&manifest(HYPER, Some("wai.image.png"), Some((A, Some(SET))))), Ok(())); + // Capability: the digest is listed, but for a capability the object does not name. + let synth_a = p(json!([entry(SYNTH, A, Some(SET))])); + assert_eq!(synth_a.check_manifest(&manifest(HYPER, None, Some((A, Some(SET))))), mismatch, "capability"); + // Digest. + let hyper_b = p(json!([entry(HYPER, B, Some(SET))])); + assert_eq!(hyper_b.check_manifest(&manifest(HYPER, None, Some((A, Some(SET))))), mismatch, "digest"); + // Form, either way. + let hyper_a_noform = p(json!([entry(HYPER, A, None)])); + assert_eq!(hyper_a_noform.check_manifest(&manifest(HYPER, None, Some((A, Some(SET))))), mismatch, "form declared, none listed"); + assert_eq!(hyper_a.check_manifest(&manifest(HYPER, None, Some((A, None)))), mismatch, "form listed, none declared"); + let mut null_form = manifest(HYPER, None, Some((A, None))); + null_form["conditioning"]["prior"]["form"] = Value::Null; + assert_eq!(hyper_a_noform.check_manifest(&null_form), Ok(()), "a null form is an absent one"); + // Unpinned: the catalog names the capability, the envelope carries no pin. + assert_eq!(hyper_a.check_manifest(&manifest(HYPER, None, None)), mismatch, "unpinned"); + assert_eq!(hyper_a.check_manifest(&manifest(SYNTH, None, None)), Ok(()), "an unpinned capability the catalog does not name"); + // Fallback: the capability's set is listed, the fallback's is another. + let both = p(json!([entry(HYPER, A, Some(SET)), entry(SYNTH, B, Some(SET))])); + assert_eq!(both.check_manifest(&manifest(HYPER, Some(SYNTH), Some((A, Some(SET))))), mismatch, "fallback"); + let both_a = p(json!([entry(HYPER, A, Some(SET)), entry(SYNTH, A, Some(SET))])); + assert_eq!(both_a.check_manifest(&manifest(HYPER, Some(SYNTH), Some((A, Some(SET))))), Ok(())); + // A pin listed only for the fallback passes: it decodes there. + assert_eq!(synth_a.check_manifest(&manifest("wai.neural.unregistered", Some(SYNTH), Some((A, Some(SET))))), Ok(())); + // An empty list: no object may carry a pin. + let none = p(json!([])); + assert_eq!(none.check_manifest(&manifest(HYPER, None, Some((A, Some(SET))))), mismatch, "empty list, pinned object"); + assert_eq!(none.check_manifest(&manifest(HYPER, None, None)), Ok(())); + // Two sets for one capability: either pin passes. + let two = p(json!([entry(HYPER, A, Some(SET)), entry(HYPER, B, Some(SET))])); + assert_eq!(two.check_manifest(&manifest(HYPER, None, Some((B, Some(SET))))), Ok(())); + // A mistyped pin member is listed by nothing. + let mut bad_form = manifest(HYPER, None, Some((A, None))); + bad_form["conditioning"]["prior"]["form"] = json!(5); + assert_eq!(hyper_a_noform.check_manifest(&bad_form), mismatch, "a form of the wrong type"); + let mut bad_sha = manifest(HYPER, None, Some((A, None))); + bad_sha["conditioning"]["prior"]["sha256"] = json!(5); + assert_eq!(hyper_a_noform.check_manifest(&bad_sha), mismatch, "a digest of the wrong type"); + // The typed form gives the same verdicts. + assert_eq!(hyper_a.check(HYPER, None, Some((A, Some(SET)))), Ok(())); + assert_eq!(hyper_a.check(HYPER, None, None), mismatch); } } -- 2.54.0 (Apple Git-157) From 24d6183a26cfb7c8f8f62db673696994491fd417 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Wed, 21 Oct 2026 05:09:19 -0400 Subject: [PATCH 2/4] staged-conformance/moq/priors.json and the registry export for wai_priors Generated by `wai_staged_vectors gen` and `wai registry export` on the build host. 30 cases: members that read, each way an object breaks the catalog (capability, digest, form either way, unpinned, fallback, empty list, a second object), and each way the member does not read. Checked by `wai_staged_vectors verify` and by `staged_verify.py`. Co-Authored-By: Claude Opus 5.5 --- wai/REGISTRY.md | 1 + wai/registry/capabilities.json | 7 + wai/staged-conformance/moq/priors.json | 1284 ++++++++++++++++++++++++ 3 files changed, 1292 insertions(+) create mode 100644 wai/staged-conformance/moq/priors.json diff --git a/wai/REGISTRY.md b/wai/REGISTRY.md index 5e28e271c..4fa883fbf 100644 --- a/wai/REGISTRY.md +++ b/wai/REGISTRY.md @@ -372,6 +372,7 @@ and a role are fixed once registered. | `wai_machine_generated` | catalog | defined | timed-text §7.4 | | `wai_model` | catalog | defined | timed-text §7.2 | | `wai_origin` | catalog | defined | timed-text §7.2 | +| `wai_priors` | catalog | defined | moq-streaming-format §4.2 | | `wai_receipt_alias` | catalog | defined | moq-streaming-format §4 | | `wai_receipts` | catalog | defined | moq-streaming-format §4 | | `wai_receipts_track` | catalog | defined | moq-streaming-format §4 | diff --git a/wai/registry/capabilities.json b/wai/registry/capabilities.json index f3a940393..530f1b100 100644 --- a/wai/registry/capabilities.json +++ b/wai/registry/capabilities.json @@ -2389,6 +2389,13 @@ "section": "§7.2", "status": "defined" }, + { + "name": "wai_priors", + "kind": "catalog", + "document": "extensions/moq-streaming-format.md", + "section": "§4.2", + "status": "defined" + }, { "name": "wai_receipt_alias", "kind": "catalog", diff --git a/wai/staged-conformance/moq/priors.json b/wai/staged-conformance/moq/priors.json new file mode 100644 index 000000000..9007c7805 --- /dev/null +++ b/wai/staged-conformance/moq/priors.json @@ -0,0 +1,1284 @@ +{ + "format": "wai-staged-moq-priors/1", + "about": "moq-streaming-format §4.2, Pinned parameter sets: a track's catalog entry, the manifests of objects on it in order, and what a sink gives them. `expect.track` is catalog_priors_malformed where wai_priors does not read (then no object is checked, and `expect.objects` is null); otherwise the first object's catalog_prior_mismatch, or null. `expect.objects` gives each object's verdict: catalog_prior_mismatch where it breaks the catalog, null otherwise, and null for every object of a track without wai_priors.", + "cases": [ + { + "name": "listed", + "about": "the object's pin is the one listed for its capability", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": [ + { + "capability": "wai.neural.int_hyper", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "form": "wai.prior.set", + "tier": "decode-equivalence" + } + ] + }, + "objects": [ + { + "wai": "1.0", + "media": "image", + "intent": "replicate", + "model_requirement": { + "capability": "wai.neural.int_hyper", + "fallback": null + }, + "conditioning": { + "kind": "int", + "prior": { + "id": "set", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "determinism": "int-rans+int-synth", + "form": "wai.prior.set", + "tier": "decode-equivalence" + } + } + } + ], + "expect": { + "track": null, + "objects": [ + null + ] + } + }, + { + "name": "absent", + "about": "no wai_priors: nothing is checked, pinned or not", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false + }, + "objects": [ + { + "wai": "1.0", + "media": "image", + "intent": "replicate", + "model_requirement": { + "capability": "wai.neural.int_hyper", + "fallback": null + }, + "conditioning": { + "kind": "int", + "prior": { + "id": "set", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "determinism": "int-rans+int-synth", + "form": "wai.prior.set", + "tier": "decode-equivalence" + } + } + }, + { + "wai": "1.0", + "media": "image", + "intent": "replicate", + "model_requirement": { + "capability": "wai.neural.int_hyper", + "fallback": null + }, + "conditioning": { + "kind": "int" + } + } + ], + "expect": { + "track": null, + "objects": [ + null, + null + ] + } + }, + { + "name": "null", + "about": "wai_priors null is absent", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": null + }, + "objects": [ + { + "wai": "1.0", + "media": "image", + "intent": "replicate", + "model_requirement": { + "capability": "wai.neural.int_hyper", + "fallback": null + }, + "conditioning": { + "kind": "int", + "prior": { + "id": "set", + "sha256": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "determinism": "int-rans+int-synth", + "tier": "decode-equivalence" + } + } + } + ], + "expect": { + "track": null, + "objects": [ + null + ] + } + }, + { + "name": "tier_advisory", + "about": "the listed tier differs from the pin's: advisory, no mismatch", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": [ + { + "capability": "wai.neural.int_hyper", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "form": "wai.prior.set", + "tier": "decode-equivalence" + } + ] + }, + "objects": [ + { + "wai": "1.0", + "media": "image", + "intent": "replicate", + "model_requirement": { + "capability": "wai.neural.int_hyper", + "fallback": null + }, + "conditioning": { + "kind": "int", + "prior": { + "id": "set", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "determinism": "int-rans+int-synth", + "form": "wai.prior.set", + "tier": "entropy-consistency" + } + } + } + ], + "expect": { + "track": null, + "objects": [ + null + ] + } + }, + { + "name": "tier_unregistered", + "about": "a tier token SPEC §3.1 does not register reads as none, and the member reads", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": [ + { + "capability": "wai.neural.int_hyper", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "form": "wai.prior.set", + "tier": "bit-exact" + } + ] + }, + "objects": [ + { + "wai": "1.0", + "media": "image", + "intent": "replicate", + "model_requirement": { + "capability": "wai.neural.int_hyper", + "fallback": null + }, + "conditioning": { + "kind": "int", + "prior": { + "id": "set", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "determinism": "int-rans+int-synth", + "form": "wai.prior.set", + "tier": "decode-equivalence" + } + } + } + ], + "expect": { + "track": null, + "objects": [ + null + ] + } + }, + { + "name": "form_null", + "about": "a null form, listed and pinned, is an absent one", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": [ + { + "capability": "wai.neural.int_hyper", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "form": null + } + ] + }, + "objects": [ + { + "wai": "1.0", + "media": "image", + "intent": "replicate", + "model_requirement": { + "capability": "wai.neural.int_hyper", + "fallback": null + }, + "conditioning": { + "kind": "int", + "prior": { + "id": "set", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "determinism": "int-rans+int-synth", + "tier": "decode-equivalence" + } + } + } + ], + "expect": { + "track": null, + "objects": [ + null + ] + } + }, + { + "name": "unknown_member", + "about": "a member of an entry that §4.2 does not define is ignored", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": [ + { + "capability": "wai.neural.int_hyper", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "form": "wai.prior.set", + "id": "int_hyper/q6" + } + ] + }, + "objects": [ + { + "wai": "1.0", + "media": "image", + "intent": "replicate", + "model_requirement": { + "capability": "wai.neural.int_hyper", + "fallback": null + }, + "conditioning": { + "kind": "int", + "prior": { + "id": "set", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "determinism": "int-rans+int-synth", + "form": "wai.prior.set", + "tier": "decode-equivalence" + } + } + } + ], + "expect": { + "track": null, + "objects": [ + null + ] + } + }, + { + "name": "fallback_unnamed", + "about": "a fallback no entry names needs nothing listed", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": [ + { + "capability": "wai.neural.int_hyper", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "form": "wai.prior.set" + } + ] + }, + "objects": [ + { + "wai": "1.0", + "media": "image", + "intent": "replicate", + "model_requirement": { + "capability": "wai.neural.int_hyper", + "fallback": "wai.image.png" + }, + "conditioning": { + "kind": "int", + "prior": { + "id": "set", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "determinism": "int-rans+int-synth", + "form": "wai.prior.set", + "tier": "decode-equivalence" + } + } + } + ], + "expect": { + "track": null, + "objects": [ + null + ] + } + }, + { + "name": "listed_for_fallback", + "about": "a pin listed only for the fallback, where it decodes", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": [ + { + "capability": "wai.neural.int_synth", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "form": "wai.prior.set" + } + ] + }, + "objects": [ + { + "wai": "1.0", + "media": "image", + "intent": "replicate", + "model_requirement": { + "capability": "wai.neural.unregistered", + "fallback": "wai.neural.int_synth" + }, + "conditioning": { + "kind": "int", + "prior": { + "id": "set", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "determinism": "int-rans+int-synth", + "form": "wai.prior.set", + "tier": "decode-equivalence" + } + } + } + ], + "expect": { + "track": null, + "objects": [ + null + ] + } + }, + { + "name": "two_sets", + "about": "two sets for one capability: either pin matches", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": [ + { + "capability": "wai.neural.int_hyper", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "form": "wai.prior.set" + }, + { + "capability": "wai.neural.int_hyper", + "sha256": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "form": "wai.prior.set" + } + ] + }, + "objects": [ + { + "wai": "1.0", + "media": "image", + "intent": "replicate", + "model_requirement": { + "capability": "wai.neural.int_hyper", + "fallback": null + }, + "conditioning": { + "kind": "int", + "prior": { + "id": "set", + "sha256": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "determinism": "int-rans+int-synth", + "form": "wai.prior.set", + "tier": "decode-equivalence" + } + } + }, + { + "wai": "1.0", + "media": "image", + "intent": "replicate", + "model_requirement": { + "capability": "wai.neural.int_hyper", + "fallback": null + }, + "conditioning": { + "kind": "int", + "prior": { + "id": "set", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "determinism": "int-rans+int-synth", + "form": "wai.prior.set", + "tier": "decode-equivalence" + } + } + } + ], + "expect": { + "track": null, + "objects": [ + null, + null + ] + } + }, + { + "name": "unpinned_unnamed", + "about": "an unpinned object whose capability no entry names", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": [ + { + "capability": "wai.neural.int_hyper", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "form": "wai.prior.set" + } + ] + }, + "objects": [ + { + "wai": "1.0", + "media": "image", + "intent": "replicate", + "model_requirement": { + "capability": "wai.neural.int_synth", + "fallback": null + }, + "conditioning": { + "kind": "int" + } + } + ], + "expect": { + "track": null, + "objects": [ + null + ] + } + }, + { + "name": "mismatch_capability", + "about": "the pin is listed, but for a capability the object does not name", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": [ + { + "capability": "wai.neural.int_synth", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "form": "wai.prior.set" + } + ] + }, + "objects": [ + { + "wai": "1.0", + "media": "image", + "intent": "replicate", + "model_requirement": { + "capability": "wai.neural.int_hyper", + "fallback": null + }, + "conditioning": { + "kind": "int", + "prior": { + "id": "set", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "determinism": "int-rans+int-synth", + "form": "wai.prior.set", + "tier": "decode-equivalence" + } + } + } + ], + "expect": { + "track": "catalog_prior_mismatch", + "objects": [ + "catalog_prior_mismatch" + ] + } + }, + { + "name": "mismatch_digest", + "about": "the object's capability is listed with another digest", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": [ + { + "capability": "wai.neural.int_hyper", + "sha256": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "form": "wai.prior.set" + } + ] + }, + "objects": [ + { + "wai": "1.0", + "media": "image", + "intent": "replicate", + "model_requirement": { + "capability": "wai.neural.int_hyper", + "fallback": null + }, + "conditioning": { + "kind": "int", + "prior": { + "id": "set", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "determinism": "int-rans+int-synth", + "form": "wai.prior.set", + "tier": "decode-equivalence" + } + } + } + ], + "expect": { + "track": "catalog_prior_mismatch", + "objects": [ + "catalog_prior_mismatch" + ] + } + }, + { + "name": "mismatch_form", + "about": "the pin declares a form; the entry lists none", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": [ + { + "capability": "wai.neural.int_hyper", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1" + } + ] + }, + "objects": [ + { + "wai": "1.0", + "media": "image", + "intent": "replicate", + "model_requirement": { + "capability": "wai.neural.int_hyper", + "fallback": null + }, + "conditioning": { + "kind": "int", + "prior": { + "id": "set", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "determinism": "int-rans+int-synth", + "form": "wai.prior.set", + "tier": "decode-equivalence" + } + } + } + ], + "expect": { + "track": "catalog_prior_mismatch", + "objects": [ + "catalog_prior_mismatch" + ] + } + }, + { + "name": "mismatch_form_dropped", + "about": "the entry lists a form; the pin declares none", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": [ + { + "capability": "wai.neural.int_hyper", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "form": "wai.prior.set" + } + ] + }, + "objects": [ + { + "wai": "1.0", + "media": "image", + "intent": "replicate", + "model_requirement": { + "capability": "wai.neural.int_hyper", + "fallback": null + }, + "conditioning": { + "kind": "int", + "prior": { + "id": "set", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "determinism": "int-rans+int-synth", + "tier": "decode-equivalence" + } + } + } + ], + "expect": { + "track": "catalog_prior_mismatch", + "objects": [ + "catalog_prior_mismatch" + ] + } + }, + { + "name": "mismatch_unpinned", + "about": "the catalog names the capability; the object carries no pin", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": [ + { + "capability": "wai.neural.int_hyper", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "form": "wai.prior.set" + } + ] + }, + "objects": [ + { + "wai": "1.0", + "media": "image", + "intent": "replicate", + "model_requirement": { + "capability": "wai.neural.int_hyper", + "fallback": null + }, + "conditioning": { + "kind": "int" + } + } + ], + "expect": { + "track": "catalog_prior_mismatch", + "objects": [ + "catalog_prior_mismatch" + ] + } + }, + { + "name": "mismatch_fallback", + "about": "the capability's set is listed, the fallback's is another", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": [ + { + "capability": "wai.neural.int_hyper", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "form": "wai.prior.set" + }, + { + "capability": "wai.neural.int_synth", + "sha256": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "form": "wai.prior.set" + } + ] + }, + "objects": [ + { + "wai": "1.0", + "media": "image", + "intent": "replicate", + "model_requirement": { + "capability": "wai.neural.int_hyper", + "fallback": "wai.neural.int_synth" + }, + "conditioning": { + "kind": "int", + "prior": { + "id": "set", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "determinism": "int-rans+int-synth", + "form": "wai.prior.set", + "tier": "decode-equivalence" + } + } + } + ], + "expect": { + "track": "catalog_prior_mismatch", + "objects": [ + "catalog_prior_mismatch" + ] + } + }, + { + "name": "mismatch_empty", + "about": "an empty list: an object that carries a pin breaks it", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": [] + }, + "objects": [ + { + "wai": "1.0", + "media": "image", + "intent": "replicate", + "model_requirement": { + "capability": "wai.neural.int_hyper", + "fallback": null + }, + "conditioning": { + "kind": "int", + "prior": { + "id": "set", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "determinism": "int-rans+int-synth", + "form": "wai.prior.set", + "tier": "decode-equivalence" + } + } + } + ], + "expect": { + "track": "catalog_prior_mismatch", + "objects": [ + "catalog_prior_mismatch" + ] + } + }, + { + "name": "mismatch_second_object", + "about": "the first object matches, the second does not: the track is refused at the second", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": [ + { + "capability": "wai.neural.int_hyper", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "form": "wai.prior.set" + } + ] + }, + "objects": [ + { + "wai": "1.0", + "media": "image", + "intent": "replicate", + "model_requirement": { + "capability": "wai.neural.int_hyper", + "fallback": null + }, + "conditioning": { + "kind": "int", + "prior": { + "id": "set", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "determinism": "int-rans+int-synth", + "form": "wai.prior.set", + "tier": "decode-equivalence" + } + } + }, + { + "wai": "1.0", + "media": "image", + "intent": "replicate", + "model_requirement": { + "capability": "wai.neural.int_hyper", + "fallback": null + }, + "conditioning": { + "kind": "int", + "prior": { + "id": "set", + "sha256": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2", + "determinism": "int-rans+int-synth", + "form": "wai.prior.set", + "tier": "decode-equivalence" + } + } + } + ], + "expect": { + "track": "catalog_prior_mismatch", + "objects": [ + null, + "catalog_prior_mismatch" + ] + } + }, + { + "name": "malformed_not_array", + "about": "wai_priors is an object", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": { + "capability": "wai.neural.int_hyper", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1" + } + }, + "objects": [], + "expect": { + "track": "catalog_priors_malformed", + "objects": null + } + }, + { + "name": "malformed_entry", + "about": "an entry that is not an object", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": [ + "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1" + ] + }, + "objects": [], + "expect": { + "track": "catalog_priors_malformed", + "objects": null + } + }, + { + "name": "malformed_no_capability", + "about": "an entry without capability", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": [ + { + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1" + } + ] + }, + "objects": [], + "expect": { + "track": "catalog_priors_malformed", + "objects": null + } + }, + { + "name": "malformed_undeclared", + "about": "a capability the track's wai_capabilities does not list", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": [ + { + "capability": "wai.neural.elic", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1" + } + ] + }, + "objects": [], + "expect": { + "track": "catalog_priors_malformed", + "objects": null + } + }, + { + "name": "malformed_no_capabilities", + "about": "a track with no wai_capabilities to list the capability", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_priors": [ + { + "capability": "wai.neural.int_hyper", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "form": "wai.prior.set" + } + ], + "wai_receipts": false + }, + "objects": [], + "expect": { + "track": "catalog_priors_malformed", + "objects": null + } + }, + { + "name": "malformed_uppercase", + "about": "a digest in upper-case digits", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": [ + { + "capability": "wai.neural.int_hyper", + "sha256": "A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1" + } + ] + }, + "objects": [], + "expect": { + "track": "catalog_priors_malformed", + "objects": null + } + }, + { + "name": "malformed_short", + "about": "a digest of 62 digits", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": [ + { + "capability": "wai.neural.int_hyper", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1" + } + ] + }, + "objects": [], + "expect": { + "track": "catalog_priors_malformed", + "objects": null + } + }, + { + "name": "malformed_form", + "about": "a form that is not a string", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": [ + { + "capability": "wai.neural.int_hyper", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "form": 5 + } + ] + }, + "objects": [], + "expect": { + "track": "catalog_priors_malformed", + "objects": null + } + }, + { + "name": "malformed_tier", + "about": "a tier that is not a string", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": [ + { + "capability": "wai.neural.int_hyper", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "tier": true + } + ] + }, + "objects": [], + "expect": { + "track": "catalog_priors_malformed", + "objects": null + } + }, + { + "name": "malformed_duplicate", + "about": "the same capability, digest and form twice", + "track": { + "namespace": "example/net", + "name": "pictures", + "packaging": "wai", + "isLive": false, + "role": "video", + "wai_capabilities": [ + "wai.neural.int_hyper", + "wai.neural.int_synth", + "wai.image.png" + ], + "wai_receipts": false, + "wai_priors": [ + { + "capability": "wai.neural.int_hyper", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "form": "wai.prior.set" + }, + { + "capability": "wai.neural.int_hyper", + "sha256": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "form": "wai.prior.set", + "tier": "decode-equivalence" + } + ] + }, + "objects": [], + "expect": { + "track": "catalog_priors_malformed", + "objects": null + } + } + ] +} -- 2.54.0 (Apple Git-157) From 996873efc3331a683b05b849375f9d288c8bfeac Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Wed, 21 Oct 2026 05:09:19 -0400 Subject: [PATCH 3/4] =?UTF-8?q?jwp-receipts=20=C2=A73=20step=203:=20state?= =?UTF-8?q?=20the=20link=20hash's=20domain=20separator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The text said "the resulting hash" without saying which. Both reference verifiers (wai-rs jwp_delivery, joule-moq receipt) and jwp_verify.py compute BLAKE3("moq-jwp:receipt-link\x01" || canonical_bytes(G)); the text now says so, and §2.4.1's parent_receipt_hash row points to it. No byte changes. Co-Authored-By: Claude Opus 5.5 --- wai/extensions/jwp-receipts.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/wai/extensions/jwp-receipts.md b/wai/extensions/jwp-receipts.md index b51584496..4a0ef1606 100644 --- a/wai/extensions/jwp-receipts.md +++ b/wai/extensions/jwp-receipts.md @@ -233,7 +233,7 @@ Required fields: | `signer_pubkey` | 32 B hex | Ed25519 verifying key per RFC 8032. | | `signer_id` | string | Bound identity string (DID, URN, or display). In §7.1's origin-bound form it names the web origin whose key log lists `signer_pubkey`; an Ed25519 `did:key` names one key; any other form is a label, and a verifier reads nothing from it. | | `sig` | 64 B hex | Ed25519 signature over §2.3 payload. | -| `parent_receipt_hash` | 32 B hex or `null` | BLAKE3 of the previous group receipt's canonical bytes under this publisher key and `track_alias` (its link hash, §3 step 3); `null` for the first group of that track. | +| `parent_receipt_hash` | 32 B hex or `null` | The link hash (§3 step 3: BLAKE3, domain-separated, over the canonical bytes) of the previous group receipt under this publisher key and `track_alias`; `null` for the first group of that track. | **The total's class (labelled form).** A group whose total carries a class of its own adds these members and is signed over the labelled payload (§2.3): @@ -1780,7 +1780,8 @@ match the algorithm style of WAI SPEC.md §4 (numbered, terminating). the head of its chain whether or not any of its objects arrives (§4.4), so a dropped object does not break the link of its track's next group. `canonical_bytes(G)` is G's §2.3 signing payload, in the form G was - signed in, followed by its 64-byte `sig`. The resulting hash is G's + signed in, followed by its 64-byte `sig`, and + `BLAKE3("moq-jwp:receipt-link\x01" || canonical_bytes(G))` is G's **link hash**: the identity a `claim` names G by (§2.4.3), and a `WAI_RECEIPT` property's `group_receipt` ([moq-streaming-format](moq-streaming-format.md) §5). For a group in -- 2.54.0 (Apple Git-157) From 0e56b9696313c0512ec7ea6c7860765c7866494d Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Wed, 21 Oct 2026 05:19:38 -0400 Subject: [PATCH 4/4] wai/ietf: draft-charlot-moq-wai-packaging-00 and a comment on NMSF The Internet-Draft (xml2rfc v3 source and its text rendering) for WAI as an MSF packaging: the "wai" and "wai-receipts" packaging values; the envelope, dispatch with fallback, the parameter-set pin by SHA-256 and the determinism tiers; catalog fields wai_capabilities, wai_determinism, wai_priors, the receipts fields and wai_staged; the receipts track (WJR1), its verification and the WAI_RECEIPT object property; the codecs parameter; security and IANA considerations (WAI_RECEIPT requested from the Specification Required range, 0x3A41 meanwhile; no action for the packaging values; application/wai registered). 36 pages; idnits 3.3.0 reports only the three normative references to Internet-Drafts. Not submitted, and the NMSF comment is not sent: both are the owner's decision. Author email is a placeholder until the owner confirms it. Co-Authored-By: Claude Opus 5.5 --- .../draft-charlot-moq-wai-packaging-00.txt | 2016 +++++++++++++++++ .../draft-charlot-moq-wai-packaging-00.xml | 1601 +++++++++++++ wai/ietf/nmsf-comment.md | 98 + 3 files changed, 3715 insertions(+) create mode 100644 wai/ietf/draft-charlot-moq-wai-packaging-00.txt create mode 100644 wai/ietf/draft-charlot-moq-wai-packaging-00.xml create mode 100644 wai/ietf/nmsf-comment.md diff --git a/wai/ietf/draft-charlot-moq-wai-packaging-00.txt b/wai/ietf/draft-charlot-moq-wai-packaging-00.txt new file mode 100644 index 000000000..fb0352001 --- /dev/null +++ b/wai/ietf/draft-charlot-moq-wai-packaging-00.txt @@ -0,0 +1,2016 @@ + + + + +Media Over QUIC D. Charlot +Internet-Draft Transaction Science +Intended status: Informational 21 October 2026 +Expires: 24 April 2027 + + + WAI Packaging for the MOQT Streaming Format + draft-charlot-moq-wai-packaging-00 + +Abstract + + This document specifies how WAI envelopes are carried as a packaging + of the MOQT Streaming Format (MSF) over Media over QUIC Transport + (MOQT). A WAI envelope names the decoding capability its payload + needs and a fallback, and can name, by SHA-256 digest, the parameter + set a learned decoder must hold to decode it. This document defines + the MSF packaging value "wai", in which every MOQT object is exactly + one WAI envelope. It defines track-level catalog fields that declare + a track's capabilities, the parameter sets its objects pin, by + digest, and the reproducibility tier of each. It defines a staged + form in which a base and its refinement layers travel on separate + tracks. It defines a receipts track that carries an Ed25519-signed + Merkle commitment to each MOQT group's objects, and an object + property that carries an object's receipt commitment. It also + registers the media type application/wai. + +About This Document + + This note is to be removed before publishing as an RFC. + + The source of this document is kept with the WAI specification it + draws on, in the directory wai/ietf of + https://git.transaction.science/Transaction-Science/open-standards. + The WAI extension texts it restates are cited as informative + references in Appendix C. + +Status of This Memo + + This Internet-Draft is submitted in full conformance with the + provisions of BCP 78 and BCP 79. + + Internet-Drafts are working documents of the Internet Engineering + Task Force (IETF). Note that other groups may also distribute + working documents as Internet-Drafts. The list of current Internet- + Drafts is at https://datatracker.ietf.org/drafts/current/. + + + + + + +Charlot Expires 24 April 2027 [Page 1] + +Internet-Draft WAI Packaging for MSF October 2026 + + + Internet-Drafts are draft documents valid for a maximum of six months + and may be updated, replaced, or obsoleted by other documents at any + time. It is inappropriate to use Internet-Drafts as reference + material or to cite them other than as "work in progress." + + This Internet-Draft will expire on 24 April 2027. + +Copyright Notice + + Copyright (c) 2026 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents (https://trustee.ietf.org/ + license-info) in effect on the date of publication of this document. + Please review these documents carefully, as they describe your rights + and restrictions with respect to this document. Code Components + extracted from this document must include Revised BSD License text as + described in Section 4.e of the Trust Legal Provisions and are + provided without warranty as described in the Revised BSD License. + +Table of Contents + + 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 3 + 2. Conventions and Definitions . . . . . . . . . . . . . . . . . 4 + 3. MSF Extension . . . . . . . . . . . . . . . . . . . . . . . . 5 + 4. The WAI Envelope . . . . . . . . . . . . . . . . . . . . . . 5 + 4.1. Layout . . . . . . . . . . . . . . . . . . . . . . . . . 5 + 4.2. Manifest . . . . . . . . . . . . . . . . . . . . . . . . 6 + 4.3. Capability Dispatch . . . . . . . . . . . . . . . . . . . 6 + 4.4. Parameter-Set Pins . . . . . . . . . . . . . . . . . . . 7 + 4.5. Determinism Tiers . . . . . . . . . . . . . . . . . . . . 9 + 5. Catalog . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 + 5.1. Packaging Values . . . . . . . . . . . . . . . . . . . . 10 + 5.2. Track Fields . . . . . . . . . . . . . . . . . . . . . . 11 + 5.2.1. Capabilities . . . . . . . . . . . . . . . . . . . . 12 + 5.2.2. Determinism . . . . . . . . . . . . . . . . . . . . . 12 + 5.2.3. Receipts Fields . . . . . . . . . . . . . . . . . . . 13 + 5.3. Pinned Parameter Sets . . . . . . . . . . . . . . . . . . 13 + 5.4. Staged Tracks . . . . . . . . . . . . . . . . . . . . . . 15 + 6. Receipts . . . . . . . . . . . . . . . . . . . . . . . . . . 17 + 6.1. Overview . . . . . . . . . . . . . . . . . . . . . . . . 18 + 6.2. Content Hash . . . . . . . . . . . . . . . . . . . . . . 18 + 6.3. Merkle Tree . . . . . . . . . . . . . . . . . . . . . . . 18 + 6.4. Energy Class Encoding . . . . . . . . . . . . . . . . . . 19 + 6.5. Group Signature . . . . . . . . . . . . . . . . . . . . . 19 + 6.5.1. Link Hash . . . . . . . . . . . . . . . . . . . . . . 20 + 6.6. Binary Forms . . . . . . . . . . . . . . . . . . . . . . 21 + + + +Charlot Expires 24 April 2027 [Page 2] + +Internet-Draft WAI Packaging for MSF October 2026 + + + 6.7. The Receipts Track . . . . . . . . . . . . . . . . . . . 21 + 6.8. Verification . . . . . . . . . . . . . . . . . . . . . . 22 + 6.9. The WAI_RECEIPT Object Property . . . . . . . . . . . . . 24 + 6.10. Relays . . . . . . . . . . . . . . . . . . . . . . . . . 25 + 7. The codecs Parameter . . . . . . . . . . . . . . . . . . . . 25 + 8. Implementation Status . . . . . . . . . . . . . . . . . . . . 26 + 9. Security Considerations . . . . . . . . . . . . . . . . . . . 26 + 9.1. Receipt Keys . . . . . . . . . . . . . . . . . . . . . . 27 + 9.2. Replay . . . . . . . . . . . . . . . . . . . . . . . . . 27 + 9.3. Relay Transparency . . . . . . . . . . . . . . . . . . . 27 + 9.4. Catalog Integrity . . . . . . . . . . . . . . . . . . . . 28 + 9.5. Parameter Sets . . . . . . . . . . . . . . . . . . . . . 28 + 9.6. Resource Consumption . . . . . . . . . . . . . . . . . . 28 + 9.7. Privacy . . . . . . . . . . . . . . . . . . . . . . . . . 28 + 10. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 29 + 10.1. MSF Packaging Values . . . . . . . . . . . . . . . . . . 29 + 10.2. MOQ Properties . . . . . . . . . . . . . . . . . . . . . 29 + 10.3. Media Type application/wai . . . . . . . . . . . . . . . 29 + 10.4. Capability Strings . . . . . . . . . . . . . . . . . . . 30 + 11. References . . . . . . . . . . . . . . . . . . . . . . . . . 30 + 11.1. Normative References . . . . . . . . . . . . . . . . . . 30 + 11.2. Informative References . . . . . . . . . . . . . . . . . 32 + Appendix A. Catalog Example . . . . . . . . . . . . . . . . . . 33 + Appendix B. Refusal Reasons . . . . . . . . . . . . . . . . . . 35 + Appendix C. Correspondence with the WAI Texts . . . . . . . . . 35 + Author's Address . . . . . . . . . . . . . . . . . . . . . . . . 36 + +1. Introduction + + MOQT [MOQT] moves objects, in groups, on tracks, and orders them by + priority; it does not say what an object means. MSF [MSF] says what + objects mean: its catalog names, for each track, a "packaging" that + fixes how the track's objects are encapsulated, and a sibling + document can add a packaging value together with the catalog fields + it needs [CMSF]. + + WAI [WAI] is a container and capability-dispatch format. A WAI + envelope carries a manifest and a payload. The manifest names the + capability a receiver needs to decode the payload, a fallback + capability for a receiver that lacks it, and, for a capability that + decodes against a parameter set (for example the weights of a learned + decoder), a pin: the SHA-256 digest of that set. A receiver that + cannot verify the pin does not decode the payload against another + set. WAI defines no transport. + + This document defines how WAI envelopes are carried as MSF objects. + Its aims are: + + + + +Charlot Expires 24 April 2027 [Page 3] + +Internet-Draft WAI Packaging for MSF October 2026 + + + * A receiver can decide before it subscribes whether it can decode a + track: which capabilities the track's objects use, which parameter + sets they pin, by digest, and which reproducibility tier each is + offered at (Section 5). + + * A receiver can establish that each object it received is the one + the publisher's key committed to, at that MOQT group and object, + and can account for an object it did not receive (Section 6). + + * A relay needs no knowledge of WAI. Every rule here uses MOQT's + own objects, groups, priorities, properties and delivery timeouts, + and a relay treats every payload as opaque, as Section 7.8 of + [MOQT] requires. + + This document changes nothing in MOQT or MSF. It defines no + transport and no codec. + +2. Conventions and Definitions + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and + "OPTIONAL" in this document are to be interpreted as described in BCP + 14 [RFC2119] [RFC8174] when, and only when, they appear in all + capitals, as shown here. + + Section numbers of [MOQT] and [MSF] are those of the revisions + referenced. Where a later revision of either moves or changes what + is cited, that revision governs and the citation is what changes. + + Envelope: A WAI container in the form of Section 4.1, beginning with + the four bytes "WAI1". + + Capability: A string naming how a payload is decoded, such as + "wai.image.png" or "wai.neural.int_hyper". Capabilities are + registered by [WAI], Section 5. + + Parameter set: Every byte, other than the payload, that a + capability's decode reads and that its output depends on: + networks, entropy tables, normative constants. A receiver holds + it; an envelope never carries it. + + Pin: A manifest member that names a parameter set by the SHA-256 + digest of its bytes (Section 4.4). + + Tier: A reproducibility guarantee of a decode (Section 4.5). + + Publisher, receiver, relay: The MOQT original publisher of a track; + + + + +Charlot Expires 24 April 2027 [Page 4] + +Internet-Draft WAI Packaging for MSF October 2026 + + + an endpoint that subscribes to a track and decodes its objects; + and an MOQT relay. + + Stage set: A base and the refinement layers that improve it, + delivered as staged delivery (Section 5.4). + + Link hash: The identity of a group receipt (Section 6.5.1). + + In byte layouts, "||" is concatenation; u8, u32_be and u64_be are + unsigned integers of 1, 4 and 8 bytes in network byte order; u32_le + is a 4-byte unsigned integer in little-endian order; a string is + u32_be(length) || its UTF-8 bytes; and a quoted domain string such as + "moq-jwp:object\x01" is its ASCII bytes followed by the single byte + that \x01 denotes. BLAKE3 is the 32-byte output of the BLAKE3 hash + [BLAKE3], and SHA-256 is that of [SHS]. Digests written in JSON are + 64 lowercase hexadecimal digits. + +3. MSF Extension + + All of the specifications, requirements and terminology defined in + [MSF] apply to implementations of this extension unless explicitly + noted otherwise in this document. Tracks of the packaging defined + here can share a catalog, and a render group, with tracks of any + other packaging. + +4. The WAI Envelope + +4.1. Layout + + +--------+---------+---------------+-------------+---------+ + | "WAI1" | man_len | manifest | payload_len | payload | + | 4 B | u32_le | man_len bytes | u32_le | | + +--------+---------+---------------+-------------+---------+ + + * A reader MUST refuse bytes whose first four are not "WAI1" (0x57 + 0x41 0x49 0x31). + + * The manifest is a JSON object [RFC8259] in UTF-8, serialized with + no insignificant whitespace. No member name occurs twice in one + object, at any depth. A member that must be an integer is written + as one: digits, with a minus sign only before a non-zero value, + and no fraction or exponent. + + * The payload's interpretation is fixed by the capability that + decodes it (Section 4.3) and, for a capability that decodes + against a parameter set, by that set. + + + + + +Charlot Expires 24 April 2027 [Page 5] + +Internet-Draft WAI Packaging for MSF October 2026 + + + * A reader MUST NOT refuse a manifest for its size while it is at + most 2^27 bytes long, nests arrays and objects at most 127 deep + (the manifest object counting as one), and holds at most 2^22 JSON + values. It MAY refuse a larger one, from man_len alone for its + length, before it reads it. + + [WAI] Section 7.1 also defines a multi-rendition container, "WAI2". + This document carries "WAI1" envelopes only. + +4.2. Manifest + + The members this document reads are: + + wai: + The version, "1.0". A reader refuses an unknown major version and + accepts an unknown minor one. A reader ignores a member it does + not understand. + + model_requirement.capability: + The capability that decodes the payload. + + model_requirement.fallback: + A capability a receiver MAY decode the payload with if it lacks + the first, or if the pin does not resolve for the first; or null, + in which case the envelope is inert without the first. + + conditioning.prior: + OPTIONAL. The pin (Section 4.4) of the parameter set the payload + was coded against. It applies to whichever of the capability and + the fallback decodes the payload. + + intent: + "replicate" states that every conforming receiver reconstructs the + source as the capability's tier promises (Section 4.5). + + refines: + Present exactly when the capability is refinement-only: what the + payload refines (Section 5.4). + + security: + OPTIONAL. Whether the payload is encrypted, as [WAI] Section 6 + defines. The capability and the pin always describe the + plaintext. + +4.3. Capability Dispatch + + A receiver dispatches an envelope as follows: + + + + +Charlot Expires 24 April 2027 [Page 6] + +Internet-Draft WAI Packaging for MSF October 2026 + + + 1. If the major version in "wai" is unknown, refuse the envelope. + + 2. Let cap be the capability. If the receiver can decode cap, cap + is dispatchable, and either no pin applies to cap or the pin + resolves for cap (Section 4.4), decode the payload with cap, + against the verified parameter set, and stop. A pin that does + not resolve makes cap unsupported for this envelope; keep its + reason. + + 3. Otherwise, if the fallback is not null and step 2's conditions + hold for it, decode with the fallback and stop; keep a pin's + reason if it does not resolve. + + 4. Otherwise the envelope is inert at this receiver. Report the + reason kept at step 2, else the one kept at step 3, else + "missing_capability". + + A capability is dispatchable unless [WAI] Section 4 registers it as + something a receiver does not decode on its own: a candidate, a + companion-only, a refinement-only, a record or a derivation string. + A string [WAI] does not register is dispatchable by a receiver that + has a decoder for it. A pin that does not resolve is never a reason + to decode against another parameter set: a learned decode against the + wrong parameters yields plausible output, not an error. + +4.4. Parameter-Set Pins + + A pin is a JSON object: + + + + + + + + + + + + + + + + + + + + + + + +Charlot Expires 24 April 2027 [Page 7] + +Internet-Draft WAI Packaging for MSF October 2026 + + + +=============+===========+=========================================+ + | Member | Presence | Meaning | + +=============+===========+=========================================+ + | id | REQUIRED, | A name for people. A receiver | + | | string | MUST NOT select or accept a | + | | | parameter set by it. | + +-------------+-----------+-----------------------------------------+ + | sha256 | REQUIRED, | The SHA-256 of the parameter | + | | string | set in its form, as 64 | + | | | lowercase hexadecimal digits. | + +-------------+-----------+-----------------------------------------+ + | determinism | REQUIRED, | How the decode is made | + | | string | reproducible, in words. | + +-------------+-----------+-----------------------------------------+ + | form | OPTIONAL, | The form the digest is computed | + | | string | over (below). | + +-------------+-----------+-----------------------------------------+ + | tier | OPTIONAL, | The tier the sender claims: | + | | string | "decode-equivalence", "entropy- | + | | | consistency" or "none". | + +-------------+-----------+-----------------------------------------+ + + Table 1: Pin Members + + A member of a pin with another JSON type makes the manifest + malformed. A null optional member is an absent one. + + With form absent, sha256 is the SHA-256 of the one file that holds + the set; that is admissible only for a capability [WAI] registers + with a one-file set. With form "wai.prior.set", sha256 is the + SHA-256 of the set's listing. The listing has one line per file: the + file's SHA-256 as 64 lowercase hexadecimal digits, two spaces, the + file's path below the set's root, and a line feed. A path is one or + more segments joined by "/"; a segment begins with one of A-Z, a-z, + 0-9, "_" and "-", continues with those and ".", and does not end with + ".". Lines are in strictly ascending order of their paths as byte + strings; no two paths are equal once A-Z are mapped to a-z, and no + path followed by "/" begins another. The listing has at least one + line and nothing else. [WAI-PRIOR] registers further forms. + + + + + + + + + + + + +Charlot Expires 24 April 2027 [Page 8] + +Internet-Draft WAI Packaging for MSF October 2026 + + + A receiver that is to decode through a capability to which a pin + applies MUST verify the pin first. The pin resolves when the + receiver holds a parameter set whose digest, in the pin's form, + equals sha256: for "wai.prior.set", a listing with that digest + together with every file it names, each with the digest its line + gives. Nothing else satisfies a pin: not a matching id, and not a + set in another form whose unpacked parameters compare equal. A pin + that does not resolve is refused with the first of these reasons that + applies: + + 1. "pin_malformed": sha256 is not 64 lowercase hexadecimal digits. + + 2. "form_not_registered": form names no registered form. + + 3. "form_unreadable": the receiver has no reader for the form. + + 4. "form_required": form is absent and the capability is not + registered with a one-file set. + + 5. "prior_revoked" or "prior_not_authorised": the receiver holds a + key log and refuses the set under it, as [WAI] Section 3.1 + specifies. A receiver that holds no key log skips this check. + + 6. "prior_not_held": the receiver holds no set with that digest + ("digest_superseded" where it holds the set that the pin's + optional supersedes member names). + + 7. "set_malformed": the listing breaks a rule above. + + 8. "part_mismatch": a file the listing names is missing, unreadable, + or has another digest. + + A pinned decode MUST read its parameters only from the set it + verified; a decode that needs a file the set lacks is refused + ("set_incomplete") and is treated as a pin that did not resolve. A + refused pin makes its capability unsupported for that envelope; it is + not a reason to decode anyway. + +4.5. Determinism Tiers + + A learned decode evaluated in floating point does not in general + reproduce its output across devices. WAI names two reproducibility + guarantees [WAI-TIERS]: + + + + + + + + +Charlot Expires 24 April 2027 [Page 9] + +Internet-Draft WAI Packaging for MSF October 2026 + + + entropy-consistency: + The entropy decode reproduces byte for byte on every conforming + receiver: the decoded symbols have a portable hash. The + reconstruction to the final medium can differ between conforming + receivers. + + decode-equivalence: + The whole decode, to the final medium, reproduces byte for byte on + every conforming receiver: the reconstructed samples have a + portable hash. The capability's registration specifies the decode + in integer or fixed-point arithmetic, entropy stage and synthesis + alike. + + "none" claims neither. The tiers are ordered none < entropy- + consistency < decode-equivalence. A tier is a property of a + capability as registered: [WAI] gives each capability a registry + class, and the class gives the tier. A pin's tier above its + capability's does not conform ("tier_exceeds_class"), and a receiver + MUST NOT rely on it. A receiver reads a tier token it does not + recognize as "none". + + An envelope with intent "replicate" over a learned capability MUST be + over a capability registered at decode-equivalence, with a pin that + declares decode-equivalence; entropy-consistency MUST NOT back + "replicate". A tier covers the medium a receiver emits: a receiver + that alters the decoded medium (for example with a post-decode + filter) MUST NOT claim the tier for the result. + + At the time of writing, [WAI] registers "wai.neural.int_synth", + "wai.neural.int_hyper", "wai.video.int_hyper", "wai.audio.int_codec", + "wai.splat.int_codec" and "wai.neural.int_mlicpp" at decode- + equivalence, and "wai.image.jpegai" at entropy-consistency. This + list is informative; the registry governs. + +5. Catalog + +5.1. Packaging Values + + This specification extends the allowed packaging values defined in + Section 5.2.4 of [MSF] with two entries: + + + + + + + + + + + +Charlot Expires 24 April 2027 [Page 10] + +Internet-Draft WAI Packaging for MSF October 2026 + + + +==============+==============+===============+ + | Name | Value | Reference | + +==============+==============+===============+ + | WAI | wai | This document | + +--------------+--------------+---------------+ + | WAI receipts | wai-receipts | Section 6.7 | + +--------------+--------------+---------------+ + + Table 2: Packaging Values + + Every track whose objects are WAI envelopes MUST declare packaging + "wai". On such a track: + + * every object MUST be exactly one whole envelope. An object that + holds part of an envelope, or more than one, is not this + packaging; + + * the object boundary MUST be the envelope boundary, so that the + receipt's commitment to an object (Section 6) and MOQT's object + identity are the same unit; + + * a receiver that does not recognize the packaging value "wai" MUST + NOT attempt to interpret the objects. MSF's rule that a parser + ignores fields it does not understand covers unknown fields, not + unknown packaging values. + +5.2. Track Fields + + This specification adds the following track-level catalog fields for + tracks with packaging "wai". They are custom fields under Section 5 + of [MSF]; their "wai_" prefix keeps them from colliding with fields + MSF defines. + + + + + + + + + + + + + + + + + + + +Charlot Expires 24 April 2027 [Page 11] + +Internet-Draft WAI Packaging for MSF October 2026 + + + +==============+====================+========+========+============+ + | Field | Name |Required|JSON | Definition | + | | | |Type | | + +==============+====================+========+========+============+ + | Capabilities | wai_capabilities |Yes |Array of| Section | + | | | |String | 5.2.1 | + +--------------+--------------------+--------+--------+------------+ + | Determinism | wai_determinism |No |String | Section | + | | | | | 5.2.2 | + +--------------+--------------------+--------+--------+------------+ + | Pinned | wai_priors |No |Array of| Section | + | parameter | | |Object | 5.3 | + | sets | | | | | + +--------------+--------------------+--------+--------+------------+ + | Receipts | wai_receipts |Yes |Boolean | Section | + | | | | | 5.2.3 | + +--------------+--------------------+--------+--------+------------+ + | Receipt | wai_receipt_alias |Cond. |Number | Section | + | alias | | | | 5.2.3 | + +--------------+--------------------+--------+--------+------------+ + | Receipts | wai_receipts_track |Cond. |String | Section | + | track | | | | 5.2.3 | + +--------------+--------------------+--------+--------+------------+ + | Stage | wai_staged |Cond. |Object | Section | + | | | | | 5.4 | + +--------------+--------------------+--------+--------+------------+ + + Table 3: Track Fields + +5.2.1. Capabilities + + wai_capabilities lists every capability that an object on the track + names as its capability or as its fallback. The list MUST be + exhaustive for the track: its purpose is to let a receiver decide + before subscribing whether it can decode what it is about to receive. + A receiver MUST NOT subscribe on the assumption that an undeclared + capability will not appear. A receiver MUST treat an object whose + envelope names, as the capability it is about to dispatch to, a + capability the list does not hold as a protocol error, and MUST NOT + fall back silently. + +5.2.2. Determinism + + wai_determinism is the weakest tier (Section 4.5) that any object on + the track claims, not the strongest. Absent means no tier is + claimed. A receiver reads a value it does not recognize as "none". + The field helps a receiver choose a track; the tier a receiver claims + for an object is the one its own decode earns. + + + +Charlot Expires 24 April 2027 [Page 12] + +Internet-Draft WAI Packaging for MSF October 2026 + + +5.2.3. Receipts Fields + + wai_receipts: + Whether the track's objects are covered by receipts (Section 6). + A receiver reads an absent value as false. + + wai_receipt_alias: + An integer from 0 to 2^53-1, written as an integer: the + track_alias that the track's receipts sign (Section 6.5). MOQT's + own Track Alias is chosen per session, by each hop (Section 3.1.3 + of [MOQT]), so past the first hop it is not the number the + receipts sign. Two tracks of a catalog MUST NOT carry the same + value. + + wai_receipts_track: + The name, in this track's namespace, of the track of packaging + "wai-receipts" that carries this track's receipts (Section 6.7). + Two tracks of a catalog MUST NOT name the same receipts track. + + A track of a stage set that declares wai_receipts true MUST carry + wai_receipt_alias and wai_receipts_track (Section 5.4). On another + track both are OPTIONAL; a receiver finds a track's receipts chain + and receipts track by them. A clone (Section 5.1.6 of [MSF]) + inherits every field of its parent but its name, so a clone of a + track that declares receipts MUST override both. + +5.3. Pinned Parameter Sets + + A track MAY state, in wai_priors, the pins its objects carry. A + receiver can then tell before it subscribes whether it holds the + parameter sets the track needs, and at which tier each is offered. + + "wai_priors": [ + { "capability": "wai.neural.int_hyper", + "sha256": "<64 lowercase hexadecimal digits>", + "form": "wai.prior.set", + "tier": "decode-equivalence" } + ] + + + + + + + + + + + + + +Charlot Expires 24 April 2027 [Page 13] + +Internet-Draft WAI Packaging for MSF October 2026 + + + +============+==================+===================================+ + | Member | Presence | Meaning | + +============+==================+===================================+ + | capability | REQUIRED, | A capability the track's | + | | string | wai_capabilities lists. | + +------------+------------------+-----------------------------------+ + | sha256 | REQUIRED, | A pin's sha256: 64 lowercase | + | | string | hexadecimal digits. | + +------------+------------------+-----------------------------------+ + | form | OPTIONAL, | The pin's form. Absent, or null, | + | | string | where the pin declares none. | + +------------+------------------+-----------------------------------+ + | tier | OPTIONAL, | The tier the publisher offers the | + | | string | set at. Advisory. | + +------------+------------------+-----------------------------------+ + + Table 4: Members of a wai_priors Entry + + An entry lists a pin for a capability when its capability is that + capability, its sha256 is the pin's, and its form is the pin's (both + absent, or the same string). A member of an entry that this document + does not define is ignored. + + Reading. + wai_priors, where present and not null, is an array of objects, + each with the members above of their types, each capability one + that the track's wai_capabilities lists, and no two with the same + capability, sha256 and form. A receiver that reads anything else + refuses the track ("catalog_priors_malformed") and does not + subscribe to it. A tier this document does not define is read as + "none". + + Exhaustive. + Where a track carries wai_priors, the publisher MUST list in it + every pin that its objects carry. An object breaks the catalog + when: + + 1. it carries a pin that no entry lists for its capability or for + its fallback; or + + 2. an entry names its capability, or its fallback (when not + null), and the object carries no pin, or carries a pin that no + entry lists for that capability. + + A publisher whose objects' pin can decode through the fallback + SHOULD list the set for the fallback as well. + + + + + +Charlot Expires 24 April 2027 [Page 14] + +Internet-Draft WAI Packaging for MSF October 2026 + + + Mismatch. + A receiver checks each object of such a track against the track's + wai_priors before it dispatches the object. An object that breaks + the catalog is refused ("catalog_prior_mismatch"), and the track + is refused with it: the receiver dispatches no further object of + the track, and MAY unsubscribe from it. The catalog never stands + in for the envelope. A receiver verifies an object's pin + (Section 4.4) whether or not the catalog lists it; an entry never + satisfies a pin, never supplies a pin the envelope lacks, and + never changes which set a decode reads. + + Tier. + A listed tier is advisory. A publisher MUST NOT list a tier above + the one the capability's registry class gives. A receiver MUST + NOT claim a tier on the strength of a listed tier: the tier it + claims for an object is the one its own decode earns + (Section 4.5). A listed tier that differs from the tier an + object's pin states is not a mismatch. + +5.4. Staged Tracks + + WAI's staged delivery [WAI-STAGED] sends a base first and refinement + layers after it; a receiver presents the base, and applies each layer + that arrives in time. Each layer is a WAI envelope whose capability + is refinement-only and whose manifest names, in refines, what it + refines. A stage set is carried as one track per layer: a base + track, which carries the set's bases, and a layer track for each + layer index k, which carries the layers of index k. A receiver + chooses its stage by the tracks it subscribes to; a relay forwards + tracks and never reads an envelope. + + Each track of a set MUST carry, in its catalog entry, a wai_staged + object: + + "wai_staged": { "set": "cam1", "layer": 1, "lead_ms": 40 } + + set: + A non-empty string, the same on every track of the set. + + layer: + An integer from 0 to 255: 0 on a base track, k on a track that + carries layer k. + + lead_ms: + On a layer track of a live set, an integer from 0 to 2^53-1: the + time before a unit's presentation by which the layer must arrive + to be applied to it. null on a base track. A receiver ignores it + when isLive is false. + + + +Charlot Expires 24 April 2027 [Page 15] + +Internet-Draft WAI Packaging for MSF October 2026 + + + A set's extent is the tracks present: layers 0 to the highest layer a + track of the set states. More than one track MAY carry one layer, as + renditions of it, each with a receipts chain of its own. A publisher + puts on a track of layer k only envelopes whose refines.layer is k, + and on a base track only envelopes with no refines; a receiver + applies a layer by its own refines, whichever track it came on. A + layer track's wai_capabilities lists refinement capabilities, so a + receiver that does not implement staged delivery finds them + unsupported and has no reason to subscribe. + + The tracks of a set MUST share one namespace, one isLive + (Section 5.2.7 of [MSF]) and one renderGroup (Section 5.2.11 of + [MSF]), present on each and written as an integer. Every entry of a + track's depends (Section 5.2.14 of [MSF]) MUST name a track of the + catalog in the track's namespace; the depends of a track of layer k + MUST name at least one track of the set at each layer below k and + none at k or above, and a base track's MUST NOT name a track of the + set. A layer track MUST NOT carry altGroup, temporalId or spatialId + (Section 5.2.12 of [MSF], Section 5.2.16 of [MSF], Section 5.2.17 of + [MSF]); an altGroup MAY group the base tracks of alternative sets. + Every track of a set that declares wai_receipts true carries its own + wai_receipt_alias and receipts track (Section 5.2.3). + + A receiver checks each set's entries in this order, stops at the + first check that fails, and treats the layer tracks of a set that + fails as absent (its base tracks remain tracks of packaging "wai"): + + 1. wai_staged has its three members, of their types and ranges; + packaging is "wai"; isLive is a boolean; the track's name is a + string and its namespace absent or a string, and no other track + has that namespace and name; renderGroup, if present, is an + integer below 2^64; depends, if present, is an array of strings + ("staged-malformed"). + + 2. The set has a track at each layer from 0 to its highest ("staged- + set-incomplete"). + + 3. The depends rules above hold ("staged-depends"). + + 4. The tracks share one namespace, one isLive and one renderGroup, + present on each, and no layer track carries altGroup, temporalId + or spatialId ("staged-grouping"). + + 5. On a live set, lead_ms is an integer on every layer track and + null on every base track ("staged-lead"). + + + + + + +Charlot Expires 24 April 2027 [Page 16] + +Internet-Draft WAI Packaging for MSF October 2026 + + + 6. Every wai_receipt_alias of the catalog is an integer from 0 to + 2^53-1; each track of the set that declares receipts carries an + alias and names a receipts track of packaging "wai-receipts" in + its namespace; and no other track carries that alias or names + that receipts track ("staged-receipts"). + + A track that declares no namespace inherits the catalog track's + (Section 5.2.2 of [MSF]). A checker that does not know the catalog + track's namespace refuses a set that mixes declared and inherited + namespaces ("staged-malformed"). + + Groups and objects. On a live track each group MUST carry exactly + one object, which travels in a subgroup of its own (Section 2.2 of + [MOQT]). A group of one object is complete when its object is, so + its group receipt is signed before the object is sent. Group and + object identifiers of different tracks need not align: refines names + what a layer refines. + + Priority. The base track's publisher priority (Section 5.1 of + [MOQT]) MUST be strictly below every layer track's, and the priority + of a track of layer k MUST be no greater than that of any track of a + layer above k. This holds for each track's + DEFAULT_PUBLISHER_PRIORITY (Section 10.4 of [MOQT]) and for every + subgroup that sets its own. A receipts track's priority is its media + track's. A subscriber that wants the publisher's order SHOULD give + its requests for a set's tracks one subscriber priority. + + Delivery timeouts. A publisher SHOULD set a live layer object's + OBJECT_DELIVERY_TIMEOUT (Section 5.2 of [MOQT]) to no more than its + slack: the time from publication to the presentation of the last unit + the object covers, less the layer's lead_ms, less the publisher's + estimate of the path. An object whose slack is zero or less cannot + arrive in time, and the publisher MAY withhold it. A publisher MUST + NOT set on a base track a delivery timeout of either kind shorter + than the time from a base's publication to the presentation of its + last unit. A delivery timeout releases bandwidth hop by hop; it is + not an end-to-end deadline, and whether a layer is late is decided by + the receiver, as [WAI-STAGED] specifies. + + Capping. A receiver caps its stage by subscribing to the base + tracks, to the tracks of layers 1 to n, and to their receipts tracks, + and to no others. A relay MUST NOT read an envelope to cap layers. + +6. Receipts + + + + + + + +Charlot Expires 24 April 2027 [Page 17] + +Internet-Draft WAI Packaging for MSF October 2026 + + +6.1. Overview + + A publisher that declares wai_receipts true commits to every object + of a track, group by group. For each MOQT group it computes each + object's content hash, builds a Merkle tree over them, and signs the + root and the group's metadata with its Ed25519 key [RFC8032]: the + group receipt. Each object has an object receipt that carries its + Merkle inclusion proof. Group receipts form a chain for each + publisher key and receipt alias: each names the link hash of the one + before. The receipts of group g travel as one object in group g of + the track's receipts track (Section 6.7), so that a receiver can + verify the tracks it subscribes to without the receipts of others, + and can account for an object it did not receive. + + The rules here are those of [WAI-JWP] for the binary form that this + carriage uses. That document also defines a JSON form, signed claims + and key discovery, which this document does not need. + +6.2. Content Hash + + content_hash = BLAKE3("moq-jwp:object\x01" || envelope) + + envelope is the object's payload: the whole envelope, from "WAI1" to + the last payload byte, with nothing inserted or reframed. + +6.3. Merkle Tree + + A group's objects are its leaves, in MOQT Object ID order, starting + at 0. An object's leaf takes the first form below when its receipt + carries no energy label, and the second when it does (Section 6.4): + + leaf_v1 = BLAKE3("moq-jwp:merkle-leaf\x01" || content_hash) + leaf_v2 = BLAKE3("moq-jwp:merkle-leaf\x02" || content_hash + || u64_be(joules_micro) || class + || u32_be(len(origin)) || origin) + node = BLAKE3("moq-jwp:merkle-node\x01" || left || right) + + When a level has an odd number of nodes, its last node is duplicated + to make a pair. The root is the single node at the top; the root of + an empty group is 32 zero bytes. Since an object receipt's object_id + is both its MOQT Object ID (Section 6.7) and its leaf index + (Section 6.8), a publisher that declares receipts numbers each + group's objects from 0, without gaps. joules_micro is the energy, in + microjoules, that producing the object took, and 0 is the unmetered + marker, never a figure of zero joules. origin is a UTF-8 label of + where the object was produced. + + + + + +Charlot Expires 24 April 2027 [Page 18] + +Internet-Draft WAI Packaging for MSF October 2026 + + +6.4. Energy Class Encoding + + An energy figure's acquisition class says how the figure was obtained + [WAI-ENERGY]. Where a receipt signs it, the class is one tag byte + followed by its evidence: + + +=====+======================+=====================================+ + | Tag | Class | Followed by | + +=====+======================+=====================================+ + | 0 | unmetered | nothing | + +-----+----------------------+-------------------------------------+ + | 1 | HwShunt (legacy) | nothing | + +-----+----------------------+-------------------------------------+ + | 2 | ModelBased | nothing | + +-----+----------------------+-------------------------------------+ + | 3 | Estimator | nothing | + +-----+----------------------+-------------------------------------+ + | 4 | OnChipCounter | u32_be(relative_ppm) || | + | | | u64_be(window_us) || u8(filtering: | + | | | 0 unknown, 1 off, 2 on) | + +-----+----------------------+-------------------------------------+ + | 5 | CalibratedInstrument | u32_be(relative_ppm) || | + | | | u64_be(window_us) || u32_be(len) || | + | | | calibration reference, UTF-8 | + +-----+----------------------+-------------------------------------+ + + Table 5: Acquisition Classes + + relative_ppm is the standard relative uncertainty of the figure in + parts per million, and window_us the integration window in + microseconds. A class in a receipt is valid only when: tag 0 stands + beside a zero figure and every other tag beside a non-zero one; tag 1 + does not appear (it is accepted only in receipts that predate the + labels); and for tags 4 and 5, relative_ppm and window_us are both + non-zero and figure x relative_ppm is at least 288676, the least + uncertainty a whole number of microjoules can carry. The classes are + ordered, strongest first: CalibratedInstrument, OnChipCounter, + HwShunt, ModelBased, Estimator. + +6.5. Group Signature + + The publisher signs, with pure Ed25519 [RFC8032], one of two signing + payloads: + + + + + + + + +Charlot Expires 24 April 2027 [Page 19] + +Internet-Draft WAI Packaging for MSF October 2026 + + + legacy = "moq-jwp:group-sign\x01" + || u64_be(track_alias) ; the track's wai_receipt_alias + || u64_be(group_id) ; the MOQT Group ID + || merkle_root ; 32 bytes + || u64_be(leaf_count) ; objects in the group + || u64_be(total_joules_micro) ; sum over the group's objects + || string(group_start) ; RFC 3339, first object + || string(group_end) ; RFC 3339, last object + || publisher_key ; 32 bytes, Ed25519 public key + || string(signer_id) ; identity label + + labelled = "moq-jwp:group-sign\x02" + || (the fields of legacy after its domain, in order) + || class ; the total's class + || u8(coverage) ; 0 Whole, 1 Partial + || u8(has_parent) || parent ; 1 and the parent's link hash, + ; or 0 and 32 zero bytes + + group_start and group_end are timestamps of the form of [RFC3339]. + signer_id names the signer; a receiver reads nothing from it unless + it can confirm it by a means outside this document. + + The labelled payload signs the link to the parent group; the legacy + payload does not. A labelled group's class is the one its objects + compose to: + + * if an object with a non-zero figure carries no label, the group + MUST be signed in the legacy form; + + * if no object has a non-zero figure, the class is tag 0 and the + coverage 0; + + * otherwise the class is the weakest of the objects' classes among + those with a non-zero figure, and the coverage is 1 exactly when + some object's figure is zero. For a hardware class (tag 4 or 5), + relative_ppm is at least the ceiling of (sum of figure_i x + relative_ppm_i) / (sum of figure_i), and window_us is the longest + of the objects'. A tag-4 total's filtering state is the objects' + common one, or 0. A tag-5 total needs one calibration reference + shared by every object, or the group is signed in the legacy form. + +6.5.1. Link Hash + + canonical_bytes = signing_payload || signature ; 64-byte signature + link_hash = BLAKE3("moq-jwp:receipt-link\x01" || canonical_bytes) + + + + + + +Charlot Expires 24 April 2027 [Page 20] + +Internet-Draft WAI Packaging for MSF October 2026 + + + signing_payload is the payload of the form the group was signed in. + A group's parent is the link hash of the previous group of its chain, + or none for the first. A legacy group's link hash does not cover its + parent link, which the legacy payload does not sign. + +6.6. Binary Forms + + group_bin = canonical_bytes ; labelled form + | canonical_bytes ; legacy form + || u8(has_parent) || parent ; parent: 32 bytes + + object_body = u64_be(object_id) + || content_hash ; 32 bytes + || u64_be(joules_micro) + || string(origin) + || u8(n) || sibling x n ; 32 bytes each, leaf up + || u64_be(direction_bits) + || u64_be(leaf_index) + || u8(leaf_version) ; 1 leaf_v1, 2 leaf_v2 + || class ; leaf_version 2 only + + receipts_bin = "WJR1" + || group_bin + || u32_be(count) + || object_body x count + + The first 19 bytes of group_bin, the signing domain, give its form. + Every field's length is known when the bytes are read in order. Bit + i of direction_bits is 1 when the node at level i is the right child. + An object receipt inside receipts_bin takes the group's track_alias + and group_id. + + A reader reads exactly the bytes it is given and refuses them as + "malformed" at the first of these that holds: the magic is not + "WJR1"; the signing domain is neither of Section 6.5; the bytes end + before a field, or bytes follow the last object; a string is not UTF- + 8; a class tag is not one of Section 6.4 or its evidence does not + read; a coverage byte, parent flag or filtering byte is out of range; + a parent flag of 0 is followed by bytes that are not zero; or + leaf_version is neither 1 nor 2. The form is canonical: a receipt + has one binary form, and a writer reproduces the bytes it read. + +6.7. The Receipts Track + + A receipts track has packaging "wai-receipts" and is named by its + media track's wai_receipts_track. Each media track has a receipts + track of its own. + + + + +Charlot Expires 24 April 2027 [Page 21] + +Internet-Draft WAI Packaging for MSF October 2026 + + + * Group g of a receipts track carries one object, the receipts_bin + of group g of its media track: its group receipt and object + receipts. The publisher publishes it no later than that group's + first object. + + * A receipt is about one MOQT object. The group receipt in group g + of a receipts track has group_id g; each object receipt in it has + that group_id and, as object_id, the MOQT Object ID of its object; + its publisher_key is the publisher's key and its track_alias the + media track's wai_receipt_alias. A receiver refuses a receipt + that breaks this ("moqt_mismatch") before it verifies its + signature, and holds nothing of it. + + * A receipts track has no delivery timeout of either kind: the + publisher sets none, and a subscriber sets none on its + subscription (Section 9.20.3 of [MOQT], Section 9.20.4 of [MOQT]). + Its publisher priority is its media track's. + + * Its objects are receipts, not envelopes; Section 5.1's object + rules do not apply to them. + + A receiver learns the publisher's key by a means outside this + document: configuration, the session in which it subscribed, or key + discovery as [WAI-JWP] Section 7 specifies. + +6.8. Verification + + A receiver verifies each group receipt and each object receipt it + uses in these steps, in order: + + 1. Read. The receipts_bin reads (Section 6.6); otherwise + "malformed". A labelled group's class reads and is valid with + the group alone: tag 1 does not appear, tag 0 stands exactly + beside a zero total with coverage 0, and a hardware class meets + the floor of Section 6.4; otherwise "group_label_invalid". + + 2. Signature. The publisher key and the signature pass, in order: + the key is the one encoding of its point that Section 5.1.3 of + [RFC8032] allows; the key's point is not of small order; S is + less than L; R is not of small order; and the cofactorless + equation holds: the point S*B - k*A, with k = SHA-512(R || A || + M) mod L over the bytes of R and A as the signature and key give + them, encodes to R byte for byte. A key that fails one of the + first two checks is "bad_signer_key"; a signature that fails a + later one is "signature_verify_failed". These checks fix what + [RFC8032] leaves to the verifier, so that every conforming + receiver accepts the same signatures. The receiver then holds a + verified root. + + + +Charlot Expires 24 April 2027 [Page 22] + +Internet-Draft WAI Packaging for MSF October 2026 + + + 3. Chain. Chains are kept per (publisher key, track_alias). With + no group of the chain held, a group with no parent starts it. A + group whose parent is the head's link hash extends it. A group + already admitted, offered again with the same bytes, passes and + changes nothing. A second group with no parent, or one whose + parent is the link hash of a group of the chain other than its + head, is "chain_break". A group whose parent the receiver does + not hold is "parent_not_held": the receiver MAY hold it while it + recovers the parent, by a FETCH (Section 3.2 of [MOQT]) of the + receipts track, and MUST NOT accept it until then. A legacy + group whose group_id does not exceed its chain head's is + "group_id_not_increasing". + + 4. Content hash. For an object the receiver received, the content + hash of its bytes (Section 6.2) equals the object receipt's; + otherwise "content_hash_mismatch". + + 5. Inclusion. A labelled object receipt's class is valid + (Section 6.4); otherwise "object_label_invalid". leaf_index is + below leaf_count, the proof holds exactly ceil(log2(leaf_count)) + siblings (none for one leaf), direction_bits equals leaf_index, + and object_id equals leaf_index; otherwise + "proof_position_mismatch". The object's leaf, in the form its + leaf_version gives, walked up the proof, reaches the verified + root; otherwise "inclusion_proof_failed". + + 6. Accept. The object's bytes are bound to the publisher key at + (track_alias, group_id, object_id). Where its leaf is labelled, + its figure, class and origin are bound with them; otherwise they + are the publisher's unsigned statements. + + A rejection at steps 1 to 5 is a hard failure for the object. A + receiver MUST NOT accept a partial verification, such as a valid + proof against an unverified root. + + An object receipt checked by steps 1, 2, 3 and 5 for an object the + receiver did not receive is "inclusion-verified": the publisher + signed an object with that content hash at that position. A receiver + MUST report it as such, and MUST NOT report it, or anything read from + it, as delivered, or dispatch on it. + + A receiver that holds an accepted object receipt for every leaf of a + group MUST check that their figures sum to the group's total + ("group_total_mismatch"), and, for a labelled group, that the group's + class, coverage and uncertainty are those its objects compose to + (Section 6.5). + + + + + +Charlot Expires 24 April 2027 [Page 23] + +Internet-Draft WAI Packaging for MSF October 2026 + + + Holding and joining. A receiver that holds groups for missing + parents MUST state a finite bound on what it holds: per chain, and + across all its chains, since any key makes a chain. Past the bound + it refuses what it holds ("parent_not_recovered") and re-anchors the + chain at the next validly signed group whose group_id exceeds the + head's. A receiver that subscribes to a live track after its first + group (Section 3.5 of [MOQT]) anchors each chain at the first validly + signed group it receives. An anchor establishes only that the key + signed that group; a receiver MUST report a chain it anchored as + anchored, with the anchor's group_id, and MUST NOT report the groups + the anchor skipped as verified. [WAI-JWP] Section 3 specifies how + the groups of such a gap, fetched later, are admitted as history. + +6.9. The WAI_RECEIPT Object Property + + An object on a track of packaging "wai" MAY carry its receipt + commitment as an MOQT Object Property (Section 8.4 of [MOQT]), + WAI_RECEIPT. Its value is: + + value = content_hash ; 32 bytes + || vi64(joules_micro) + || vi64(len) || acquisition ; UTF-8 class name, or empty + || vi64(len) || acquisition_evidence + || group_receipt ; 32 bytes + + vi64 is the variable-length integer of Section 8.1 of [MOQT]. + acquisition is the class name of Section 6.4 ("ModelBased", + "Estimator", "OnChipCounter", "CalibratedInstrument", or "HwShunt"), + or empty; acquisition_evidence is, for tags 4 and 5, the evidence + that follows the tag byte, and empty otherwise; group_receipt is the + link hash of the group receipt covering the object. The publisher + MUST place the property inside Immutable Properties (Section 10.7 of + [MOQT]), which no relay may change or remove. + + * A receiver refuses a value that ends early, has bytes after + group_receipt, or whose acquisition is not UTF-8 + ("property_invalid"). + + * A non-zero joules_micro MUST be accompanied by acquisition. A + zero joules_micro is the unmetered marker: acquisition and + acquisition_evidence MUST be empty, and a receiver MUST NOT + present it as zero joules. acquisition_evidence MUST be non-empty + for OnChipCounter and CalibratedInstrument and empty for every + other class. A receiver MUST discard a property that breaks this + item ("property_invalid"). + + + + + + +Charlot Expires 24 April 2027 [Page 24] + +Internet-Draft WAI Packaging for MSF October 2026 + + + * No signature covers the property. A receiver MUST NOT report its + class as signed unless the class, evidence and figure equal those + in the object's labelled leaf, once the object receipt is accepted + against the group receipt group_receipt names; where they differ, + the receiver MUST discard the property ("property_disagrees"). + + * A relay MUST forward the property unchanged. + + * The property rides on its object and is lost with it; the receipts + track carries the receipts of an object that is not delivered. + Absence of the property is not evidence that an object has no + receipt. + + Until a type is assigned (Section 10.2), a track of packaging "wai" + carries the property as type 0x3A41, from the range Section 8.4 of + [MOQT] sets aside for an application's own use. Another application + can give that type another meaning, so a receiver reads it only on a + track of packaging "wai". + +6.10. Relays + + An MOQT relay forwards tracks of packaging "wai" and "wai-receipts" + as it forwards any track, their payloads opaque (Section 7.8 of + [MOQT]). It needs no knowledge of this document and verifies no + receipt: the receiver verifies every receipt itself. A relay that + does not forward an object, because a delivery timeout passed or at + its discretion, still forwards the receipts track's objects, which + are separate MOQT objects. A relay that verifies receipts before + forwarding them is permitted, and [WAI-JWP] Section 4 states what it + must preserve. + +7. The codecs Parameter + + The optional codecs parameter of application/wai (Section 10.3) lists + the capabilities a sequence of objects uses, in the form of + [RFC6381]. Its WAI elements follow this grammar [RFC5234]: + + wai-element = wai-prefix "." alt *( "|" alt ) + wai-prefix = "wai1" / "wai2" + alt = cap *( "+" cap ) + cap = 1*lcalnum 1*( "." 1*( lcalnum / "_" ) ) + lcalnum = %x61-7A / %x30-39 + + + + + + + + + +Charlot Expires 24 April 2027 [Page 25] + +Internet-Draft WAI Packaging for MSF October 2026 + + + A cap is a capability string without its leading "wai.". In an alt, + the first cap is a capability a receiver can select, and each "+cap" + is one whose payload is applied with it. The element lists every + capability that any object it describes may dispatch to or apply. An + element that begins with "wai", in any case, MUST match the grammar. + [WAI-HTTP] Section 4 gives the remaining rules. + +8. Implementation Status + + (Note to the RFC Editor: please remove this section before + publication, as [RFC7942] describes.) + + One open-source implementation family exists, in the repository named + in the note at the start of this document, under the Apache License + 2.0: + + * "wai-rs", the WAI reference library: the envelope, dispatch and + pin verification (Section 4), the tier rules, the catalog check of + staged tracks and the reading and check of wai_priors (Section 5), + and a receipts verifier (Section 6). + + * "joule-moq", an MOQT implementation of [MOQT]: the receipts track + in its binary form, receipt chains per key and alias, the + WAI_RECEIPT property at type 0x3A41 inside Immutable Properties, + and a test relay that drops late objects and still forwards their + receipts. A browser receiver built on it refuses an object whose + capability its track does not declare. + + * Conformance corpora for the catalog checks, wai_priors, the + receipts and the property. Each is checked by the library and by + an independent verifier, written in Python from the specification + text, that shares no code with the library. + + Not implemented at the time of writing: no publisher in the + implementation writes wai_priors, and the browser receiver does not + read it (it dispatches no capability that decodes against a parameter + set); no receiver reads wai_determinism; and no type is assigned for + WAI_RECEIPT. + +9. Security Considerations + + This document relies on MOQT [MOQT] for transport security: QUIC + protects each hop. The receipts of Section 6 add integrity from the + publisher to the receiver across relays; they add no confidentiality. + + + + + + + +Charlot Expires 24 April 2027 [Page 26] + +Internet-Draft WAI Packaging for MSF October 2026 + + +9.1. Receipt Keys + + Each group receipt has one signer, the publisher key in it. Forging + one requires the private key or a break of Ed25519. A valid + signature shows that a key signed; it does not show whose key it is. + A receiver MUST NOT present a receipt as the publisher's unless it + holds the key as the publisher's by a means it trusts (configuration, + the session, or key discovery as [WAI-JWP] Section 7 specifies). + Publishers MUST generate keys with a cryptographically secure random + number generator and MUST NOT use one key for distinct publisher + identities. The strict verification of Section 6.8 step 2 refuses + keys of small order, under which a signature can verify without any + private key, and makes every conforming receiver accept the same + signatures. A compromised key can sign any group; rotation and + revocation are outside this document. + +9.2. Replay + + A group receipt binds the publisher key, track_alias and group_id + under its signature, and an object receipt is bound to its leaf + position, so neither can be moved to another track, group or position + without detection. The receipts-track rule of Section 6.7 binds each + receipt to the MOQT group and object it is carried for. + + Chains are kept per key and track. The labelled payload signs the + link to the parent group; the legacy payload does not, so in a legacy + chain anyone on the path can rewrite the link, and the rule that a + legacy group's group_id exceeds its head's is the only order that + chain signs. A relay can drop a legacy group and rewrite the next + one's link to hide the gap; only the labelled payload rules that out. + + An anchor (Section 6.8) proves nothing about the groups before it: a + relay can withhold groups, or offer a joining receiver an old group + that is validly signed. A receiver reports anchors and gaps, and MAY + refuse a live group whose group_end is older than a freshness window + it states; it applies such a window only to the live edge, never to + history it fetches. + +9.3. Relay Transparency + + A relay forwards payloads without reading them. It cannot alter an + envelope without changing its content hash, nor alter a receipt + without breaking its signature. It can drop, delay or withhold + objects, whole groups of a receipts track, or whole tracks; the + receipts of Section 6 make an undelivered object accountable, as + inclusion-verified, but they do not make delivery happen. + + + + + +Charlot Expires 24 April 2027 [Page 27] + +Internet-Draft WAI Packaging for MSF October 2026 + + + The WAI_RECEIPT property is not signed. Immutable Properties forbid + a relay to change it, but a receiver cannot detect a relay that + breaks that rule from the property alone, which is why Section 6.9 + lets a receiver report its class only when it matches a signed leaf. + + A relay sees what MOQT exposes: track names, catalogs (unless they + are encrypted), object sizes, timing and properties, including + WAI_RECEIPT. + +9.4. Catalog Integrity + + Receipts do not cover the catalog. Where the catalog is not + protected end to end, an on-path party able to rewrite it can + misdeclare a track's capabilities, tiers or pinned parameter sets. + The envelope remains the authority: a receiver verifies every pin + before it decodes (Section 4.4), and an object that breaks wai_priors + refuses its track (Section 5.3), so a misdeclared catalog leads to + refusal, not to a decode against the wrong parameters. A listed tier + is advisory and confers nothing. + +9.5. Parameter Sets + + A learned decoder run against parameters other than the ones a + payload was coded against produces plausible but wrong output, with + no error. The pin makes the parameter set part of what the receiver + checks, by digest. A receiver that decodes from bytes other than the + ones it verified (for example by reading a file again after checking + it) loses that protection; a receiver SHOULD decode from the very + bytes it verified. + +9.6. Resource Consumption + + A receiver bounds what it parses and holds: the manifest limits of + Section 4.1; a bound on the receipts it holds for missing parents + (Section 6.8); and the bounds of [WAI] Section 2 on decoded payloads. + A receiver SHOULD check a receipts_bin's object count, and a Merkle + proof's sibling count, against the bytes that remain before it + allocates anything for them. + +9.7. Privacy + + Receipts carry the publisher key, a signer label, origin labels, + timestamps and energy figures in cleartext, and a receipts track is + visible to every relay that forwards it. They reveal when and where + objects were produced, and what producing them cost. A publisher can + send the unmetered marker in place of a figure and an empty origin. + A deployment that needs these hidden from relays encrypts the + receipts track's objects end to end, for example with + + + +Charlot Expires 24 April 2027 [Page 28] + +Internet-Draft WAI Packaging for MSF October 2026 + + + [SECURE-OBJECTS]; the receipts then commit to the bytes carried. + +10. IANA Considerations + +10.1. MSF Packaging Values + + This document requests no IANA action for the packaging values "wai" + and "wai-receipts". [MSF] lists its allowed packaging values in its + own text (Section 5.2.4 of [MSF]) and creates no registry for them, + and this document extends that list (Section 5.1), as other MSF + packaging documents do. + +10.2. MOQ Properties + + This document requests that IANA register the following entry in the + "MOQ Properties" registry established by Section 16.8 of [MOQT], with + a type from the range 0x80 to 0x37FF, whose registration policy is + Specification Required: + + +===========+=============+========+==============================+ + | Type | Name | Scope | Specification | + +===========+=============+========+==============================+ + | TBD (odd) | WAI_RECEIPT | Object | Section 6.9 of this document | + +-----------+-------------+--------+------------------------------+ + + Table 6: WAI_RECEIPT Property + + The type is odd, since the value is a byte sequence (Section 8.3 of + [MOQT]). Until a type is assigned, implementations use 0x3A41, from + the range 0x3800 to 0x3FFF that Section 8.4 of [MOQT] reserves for + application-specific use; no registration is possible in that range, + and none is requested for it. + +10.3. Media Type application/wai + + This document requests that IANA register the media type application/ + wai in the "Media Types" registry, in the standards tree, as + [RFC6838] provides for a registration made by an IETF document. + + Type name: application + + Subtype name: wai + + Required parameters: N/A + + Optional parameters: codecs, as Section 7 of this document defines. + + Encoding considerations: binary + + + +Charlot Expires 24 April 2027 [Page 29] + +Internet-Draft WAI Packaging for MSF October 2026 + + + Security considerations: See Section 9 of this document. A WAI + object names the decoder its payload needs and does not carry + executable code. + + Interoperability considerations: An object is a "WAI1" envelope + (Section 4.1) or a "WAI2" envelope ([WAI] Section 7.1). A reader + refuses an unknown major version. + + Published specification: This document, and [WAI]. + + Applications that use this media type: Media publishers, relays, + caches and players that carry WAI envelopes over MOQT or HTTP. + + Fragment identifier considerations: None defined by this document. + + Additional information: Deprecated alias names for this type: N/A + + Magic number(s): 57 41 49 31 ("WAI1") or 57 41 49 32 ("WAI2") at + offset 0 + + File extension(s): .wai + + Macintosh file type code(s): N/A + + Person and email address to contact for further information: The + author of this document. + + Intended usage: COMMON + + Restrictions on usage: N/A + + Author: See the Author's Address section of this document. + + Change controller: IETF + +10.4. Capability Strings + + This document creates no registry for capability strings or tier + names. [WAI] registers them under its own registry policy. + +11. References + +11.1. Normative References + + [BLAKE3] "The BLAKE3 Hashing Framework", Work in Progress, + Internet-Draft, draft-aumasson-blake3-00, July 2024, + . + + + +Charlot Expires 24 April 2027 [Page 30] + +Internet-Draft WAI Packaging for MSF October 2026 + + + [MOQT] "Media over QUIC Transport", Work in Progress, Internet- + Draft, draft-ietf-moq-transport-22, 1 October 2026, + . + + [MSF] "MOQT Streaming Format", Work in Progress, Internet-Draft, + draft-ietf-moq-msf-01, 2 June 2026, + . + + [RFC2119] "Key words for use in RFCs to Indicate Requirement + Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, March + 1997, . + + [RFC3339] "Date and Time on the Internet: Timestamps", RFC 3339, + DOI 10.17487/RFC3339, July 2002, + . + + [RFC5234] "Augmented BNF for Syntax Specifications: ABNF", STD 68, + RFC 5234, DOI 10.17487/RFC5234, January 2008, + . + + [RFC6381] "The 'Codecs' and 'Profiles' Parameters for "Bucket" Media + Types", RFC 6381, DOI 10.17487/RFC6381, August 2011, + . + + [RFC6838] "Media Type Specifications and Registration Procedures", + BCP 13, RFC 6838, DOI 10.17487/RFC6838, January 2013, + . + + [RFC8032] "Edwards-Curve Digital Signature Algorithm (EdDSA)", + RFC 8032, DOI 10.17487/RFC8032, January 2017, + . + + [RFC8174] "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key + Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, May 2017, + . + + [RFC8259] "The JavaScript Object Notation (JSON) Data Interchange + Format", STD 90, RFC 8259, DOI 10.17487/RFC8259, December + 2017, . + + [SHS] National Institute of Standards and Technology, "Secure + Hash Standard (SHS)", FIPS PUB 180-4, + DOI 10.6028/NIST.FIPS.180-4, August 2015, + . + + + + + +Charlot Expires 24 April 2027 [Page 31] + +Internet-Draft WAI Packaging for MSF October 2026 + + + [WAI] "WAI v1.0: Web AI Media Transport and Execution", Draft + Standard, open-standards repository, wai/SPEC.md, October + 2026, . + +11.2. Informative References + + [CMSF] "CMSF- a CMAF compliant implementation of MOQT Streaming + Format", Work in Progress, Internet-Draft, draft-ietf-moq- + cmsf-01, 3 June 2026, + . + + [RFC7942] "Improving Awareness of Running Code: The Implementation + Status Section", BCP 205, RFC 7942, DOI 10.17487/RFC7942, + July 2016, . + + [SECURE-OBJECTS] + "End-to-End Secure Objects for Media over QUIC Transport", + Work in Progress, Internet-Draft, draft-ietf-moq-secure- + objects-01, 2026, . + + [WAI-ENERGY] + "WAI Extension: Energy Measurement", open-standards + repository, wai/extensions/energy-measurement.md, October + 2026, . + + [WAI-HTTP] "WAI Extension: HTTP Delivery", open-standards repository, + wai/extensions/http-delivery.md, October 2026, + . + + [WAI-JWP] "WAI Extension: JWP Receipt Chain", open-standards + repository, wai/extensions/jwp-receipts.md, October 2026, + . + + [WAI-MOQ] "WAI Extension: MoQ Streaming Format", open-standards + repository, wai/extensions/moq-streaming-format.md, + October 2026, . + + + + +Charlot Expires 24 April 2027 [Page 32] + +Internet-Draft WAI Packaging for MSF October 2026 + + + [WAI-PRIOR] + "WAI Extension: Prior Carriage", open-standards + repository, wai/extensions/prior-carriage.md, October + 2026, . + + [WAI-STAGED] + "WAI Extension: Staged Delivery", open-standards + repository, wai/extensions/staged-delivery.md, October + 2026, . + + [WAI-TIERS] + "WAI Extension: Determinism Tiers for Neural Decode", + open-standards repository, wai/extensions/determinism- + tiers.md, October 2026, . + +Appendix A. Catalog Example + + A live catalog with a stage set (a base track and one layer track, + each with its receipts track) and an on-demand track of a learned + image capability that states its pinned parameter set. The digest is + a placeholder. + + + + + + + + + + + + + + + + + + + + + + + +Charlot Expires 24 April 2027 [Page 33] + +Internet-Draft WAI Packaging for MSF October 2026 + + + { "version": 1, + "tracks": [ + { "namespace": "example/cam1", "name": "cam1/base", + "packaging": "wai", "isLive": true, "targetLatency": 300, + "role": "video", "renderGroup": 1, + "wai_capabilities": ["wai.video.int_motion"], + "wai_determinism": "decode-equivalence", + "wai_receipts": true, "wai_receipt_alias": 7, + "wai_receipts_track": "cam1/base/receipts", + "wai_staged": { "set": "cam1", "layer": 0, + "lead_ms": null } }, + { "namespace": "example/cam1", "name": "cam1/L1", + "packaging": "wai", "isLive": true, "targetLatency": 300, + "role": "video", "renderGroup": 1, "depends": ["cam1/base"], + "wai_capabilities": ["wai.video.int_refine"], + "wai_determinism": "decode-equivalence", + "wai_receipts": true, "wai_receipt_alias": 8, + "wai_receipts_track": "cam1/L1/receipts", + "wai_staged": { "set": "cam1", "layer": 1, "lead_ms": 20 } }, + { "namespace": "example/cam1", "name": "cam1/base/receipts", + "packaging": "wai-receipts", "isLive": true }, + { "namespace": "example/cam1", "name": "cam1/L1/receipts", + "packaging": "wai-receipts", "isLive": true }, + { "namespace": "example/net", "name": "pictures", + "packaging": "wai", "isLive": false, "role": "video", + "wai_capabilities": ["wai.neural.int_hyper", + "wai.image.png"], + "wai_determinism": "decode-equivalence", + "wai_priors": [ + { "capability": "wai.neural.int_hyper", + "sha256": + "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "form": "wai.prior.set", "tier": "decode-equivalence" } ], + "wai_receipts": true, "wai_receipt_alias": 9, + "wai_receipts_track": "pictures/receipts" }, + { "namespace": "example/net", "name": "pictures/receipts", + "packaging": "wai-receipts", "isLive": false } ] } + + An object on "pictures" is a WAI envelope whose capability is + "wai.neural.int_hyper", whose fallback is "wai.image.png", and whose + pin names the listed set. A receiver that holds that set decodes the + learned payload; one that does not falls back to PNG. An object + whose pin named another set would refuse the track. + + + + + + + + +Charlot Expires 24 April 2027 [Page 34] + +Internet-Draft WAI Packaging for MSF October 2026 + + +Appendix B. Refusal Reasons + + +=================================================+=========+ + | Reason | Raised | + | | by | + +=================================================+=========+ + | missing_capability | Section | + | | 4.3 | + +-------------------------------------------------+---------+ + | pin_malformed, form_not_registered, | Section | + | form_unreadable, form_required, prior_revoked, | 4.4 | + | prior_not_authorised, prior_not_held, | | + | digest_superseded, set_malformed, | | + | part_mismatch, set_incomplete | | + +-------------------------------------------------+---------+ + | tier_exceeds_class | Section | + | | 4.5 | + +-------------------------------------------------+---------+ + | catalog_priors_malformed, | Section | + | catalog_prior_mismatch | 5.3 | + +-------------------------------------------------+---------+ + | staged-malformed, staged-set-incomplete, | Section | + | staged-depends, staged-grouping, staged-lead, | 5.4 | + | staged-receipts | | + +-------------------------------------------------+---------+ + | moqt_mismatch | Section | + | | 6.7 | + +-------------------------------------------------+---------+ + | malformed, group_label_invalid, bad_signer_key, | Section | + | signature_verify_failed, chain_break, | 6.8 | + | parent_not_held, parent_not_recovered, | | + | group_id_not_increasing, content_hash_mismatch, | | + | object_label_invalid, proof_position_mismatch, | | + | inclusion_proof_failed, group_total_mismatch | | + +-------------------------------------------------+---------+ + | property_invalid, property_disagrees | Section | + | | 6.9 | + +-------------------------------------------------+---------+ + + Table 7: Refusal Reasons + +Appendix C. Correspondence with the WAI Texts + + This document restates, for carriage over MOQT, parts of the WAI + specification and its extensions. Where this document and the cited + text differ, the difference is a defect in one of them. + + + + + +Charlot Expires 24 April 2027 [Page 35] + +Internet-Draft WAI Packaging for MSF October 2026 + + + +============================+===================================+ + | This document | WAI text | + +============================+===================================+ + | Section 4.1, Section 4.2 | [WAI] Sections 2 and 3 | + +----------------------------+-----------------------------------+ + | Section 4.3 | [WAI] Section 4 | + +----------------------------+-----------------------------------+ + | Section 4.4 | [WAI] Section 3.1; [WAI-PRIOR] | + +----------------------------+-----------------------------------+ + | Section 4.5 | [WAI-TIERS] Sections 2 and 3 | + +----------------------------+-----------------------------------+ + | Section 5.1, Section 5.2 | [WAI-MOQ] Sections 3 and 4 | + +----------------------------+-----------------------------------+ + | Section 5.3 | [WAI-MOQ] Section 4.2 | + +----------------------------+-----------------------------------+ + | Section 5.4 | [WAI-MOQ] Section 4.1 | + +----------------------------+-----------------------------------+ + | Section 6.2 to Section 6.8 | [WAI-JWP] Sections 2, 2.7 and 3; | + | | [WAI-ENERGY] Sections 2.3 and 2.7 | + +----------------------------+-----------------------------------+ + | Section 6.9 | [WAI-MOQ] Section 5 | + +----------------------------+-----------------------------------+ + | Section 7, Section 10.3 | [WAI-HTTP] Sections 3 and 4 | + +----------------------------+-----------------------------------+ + + Table 8: Correspondence + +Author's Address + + David Charlot + Transaction Science + Email: TBD (owner confirms at submission) + + + + + + + + + + + + + + + + + + + +Charlot Expires 24 April 2027 [Page 36] diff --git a/wai/ietf/draft-charlot-moq-wai-packaging-00.xml b/wai/ietf/draft-charlot-moq-wai-packaging-00.xml new file mode 100644 index 000000000..2e38d9368 --- /dev/null +++ b/wai/ietf/draft-charlot-moq-wai-packaging-00.xml @@ -0,0 +1,1601 @@ + + + + + + WAI Packaging for the MOQT Streaming Format + + + Transaction Science +
+ TBD (owner confirms at submission) +
+
+ + Web and Internet Transport + Media Over QUIC + MoQ + MSF + packaging + receipts + learned codecs + + + This document specifies how WAI envelopes are carried as a packaging of + the MOQT Streaming Format (MSF) over Media over QUIC Transport (MOQT). A WAI + envelope names the decoding capability its payload needs and a fallback, and + can name, by SHA-256 digest, the parameter set a learned decoder must hold + to decode it. This document defines the MSF packaging value "wai", in which + every MOQT object is exactly one WAI envelope. It defines track-level catalog + fields that declare a track's capabilities, the parameter sets its objects + pin, by digest, and the reproducibility tier of each. It defines a staged + form in which a base and its refinement layers travel on separate tracks. It + defines a receipts track that carries an Ed25519-signed Merkle commitment to + each MOQT group's objects, and an object property that carries an object's + receipt commitment. It also registers the media type application/wai. + + + + About This Document + The source of this document is kept with the WAI specification it + draws on, in the directory wai/ietf of + . + The WAI extension texts it restates are cited as informative references + in . + +
+ + + +
+ Introduction + MOQT moves objects, in groups, on tracks, and + orders them by priority; it does not say what an object means. MSF + says what objects mean: its catalog names, for each + track, a "packaging" that fixes how the track's objects are encapsulated, + and a sibling document can add a packaging value together with the + catalog fields it needs . + WAI is a container and capability-dispatch format. + A WAI envelope carries a manifest and a payload. The manifest names the + capability a receiver needs to decode the payload, a fallback capability + for a receiver that lacks it, and, for a capability that decodes against a + parameter set (for example the weights of a learned decoder), a pin: the + SHA-256 digest of that set. A receiver that cannot verify the pin does not + decode the payload against another set. WAI defines no transport. + This document defines how WAI envelopes are carried as MSF objects. Its + aims are: +
    +
  • A receiver can decide before it subscribes whether it can decode a + track: which capabilities the track's objects use, which parameter sets + they pin, by digest, and which reproducibility tier each is offered at + ().
  • +
  • A receiver can establish that each object it received is the one the + publisher's key committed to, at that MOQT group and object, and can + account for an object it did not receive ().
  • +
  • A relay needs no knowledge of WAI. Every rule here uses MOQT's own + objects, groups, priorities, properties and delivery timeouts, and a + relay treats every payload as opaque, as + requires.
  • +
+ This document changes nothing in MOQT or MSF. It defines no transport + and no codec. +
+ +
+ Conventions and Definitions + The key words "MUST", "MUST NOT", + "REQUIRED", "SHALL", "SHALL + NOT", "SHOULD", "SHOULD NOT", + "RECOMMENDED", "NOT RECOMMENDED", + "MAY", and "OPTIONAL" in this document are + to be interpreted as described in BCP 14 + when, and only when, they appear in all capitals, + as shown here. + Section numbers of and are + those of the revisions referenced. Where a later revision of either moves + or changes what is cited, that revision governs and the citation is what + changes. +
+
Envelope:
+
A WAI container in the form of , beginning + with the four bytes "WAI1".
+
Capability:
+
A string naming how a payload is decoded, such as + "wai.image.png" or "wai.neural.int_hyper". Capabilities are registered + by , Section 5.
+
Parameter set:
+
Every byte, other than the payload, that a capability's decode reads + and that its output depends on: networks, entropy tables, normative + constants. A receiver holds it; an envelope never carries it.
+
Pin:
+
A manifest member that names a parameter set by the SHA-256 digest + of its bytes ().
+
Tier:
+
A reproducibility guarantee of a decode + ().
+
Publisher, receiver, relay:
+
The MOQT original publisher of a track; an endpoint that subscribes + to a track and decodes its objects; and an MOQT relay.
+
Stage set:
+
A base and the refinement layers that improve it, delivered as + staged delivery ().
+
Link hash:
+
The identity of a group receipt ().
+
+ In byte layouts, "||" is concatenation; u8, u32_be and u64_be are + unsigned integers of 1, 4 and 8 bytes in network byte order; u32_le is a + 4-byte unsigned integer in little-endian order; a string is u32_be(length) + || its UTF-8 bytes; and a quoted domain string such as + "moq-jwp:object\x01" is its ASCII bytes followed by the single byte that + \x01 denotes. BLAKE3 is the 32-byte output of the BLAKE3 hash + , and SHA-256 is that of . + Digests written in JSON are 64 lowercase hexadecimal digits. +
+ +
+ MSF Extension + All of the specifications, requirements and terminology defined in + apply to implementations of this extension unless + explicitly noted otherwise in this document. Tracks of the packaging + defined here can share a catalog, and a render group, with tracks of + any other packaging. +
+ +
+ The WAI Envelope + +
+ Layout + +
    +
  • A reader MUST refuse bytes whose first four are + not "WAI1" (0x57 0x41 0x49 0x31).
  • +
  • The manifest is a JSON object in UTF-8, + serialized with no insignificant whitespace. No member name occurs + twice in one object, at any depth. A member that must be an integer is + written as one: digits, with a minus sign only before a non-zero value, + and no fraction or exponent.
  • +
  • The payload's interpretation is fixed by the capability that + decodes it () and, for a capability that + decodes against a parameter set, by that set.
  • +
  • A reader MUST NOT refuse a manifest for its size + while it is at most 2^27 bytes long, nests arrays and objects at most + 127 deep (the manifest object counting as one), and holds at most 2^22 + JSON values. It MAY refuse a larger one, from man_len + alone for its length, before it reads it.
  • +
+ Section 7.1 also defines a multi-rendition + container, "WAI2". This document carries "WAI1" envelopes only. +
+ +
+ Manifest + The members this document reads are: +
+
wai:
+
The version, "1.0". A reader refuses an unknown major version and + accepts an unknown minor one. A reader ignores a member it does not + understand.
+
model_requirement.capability:
+
The capability that decodes the payload.
+
model_requirement.fallback:
+
A capability a receiver MAY decode the payload with + if it lacks the first, or if the pin does not resolve for the first; + or null, in which case the envelope is inert without the first.
+
conditioning.prior:
+
OPTIONAL. The pin () of the parameter set the + payload was coded against. It applies to whichever of the capability + and the fallback decodes the payload.
+
intent:
+
"replicate" states that every conforming receiver reconstructs the + source as the capability's tier promises ().
+
refines:
+
Present exactly when the capability is refinement-only: what the + payload refines ().
+
security:
+
OPTIONAL. Whether the payload is encrypted, as + Section 6 defines. The capability and the pin + always describe the plaintext.
+
+
+ +
+ Capability Dispatch + A receiver dispatches an envelope as follows: +
    +
  1. If the major version in "wai" is unknown, refuse the envelope.
  2. +
  3. Let cap be the capability. If the receiver can decode cap, cap is + dispatchable, and either no pin applies to cap or the pin resolves for + cap (), decode the payload with cap, against the + verified parameter set, and stop. A pin that does not resolve makes + cap unsupported for this envelope; keep its reason.
  4. +
  5. Otherwise, if the fallback is not null and step 2's conditions hold + for it, decode with the fallback and stop; keep a pin's reason if it + does not resolve.
  6. +
  7. Otherwise the envelope is inert at this receiver. Report the reason + kept at step 2, else the one kept at step 3, else + "missing_capability".
  8. +
+ A capability is dispatchable unless Section 4 + registers it as something a receiver does not decode on its own: a + candidate, a companion-only, a refinement-only, a record or a derivation + string. A string does not register is dispatchable + by a receiver that has a decoder for it. A pin that does not resolve is + never a reason to decode against another parameter set: a learned + decode against the wrong parameters yields plausible output, not an + error. +
+ +
+ Parameter-Set Pins + A pin is a JSON object: + + Pin Members + + + + + + + + + + +
MemberPresenceMeaning
idREQUIRED, stringA name for people. A + receiver MUST NOT select or accept a parameter set by + it.
sha256REQUIRED, stringThe SHA-256 of the + parameter set in its form, as 64 lowercase hexadecimal + digits.
determinismREQUIRED, stringHow the decode is + made reproducible, in words.
formOPTIONAL, stringThe form the digest is + computed over (below).
tierOPTIONAL, stringThe tier the sender + claims: "decode-equivalence", "entropy-consistency" or + "none".
+ A member of a pin with another JSON type makes the manifest + malformed. A null optional member is an absent one. + With form absent, sha256 is the SHA-256 of the one file that holds + the set; that is admissible only for a capability + registers with a one-file set. With form "wai.prior.set", sha256 is the + SHA-256 of the set's listing. The listing has one line per file: the + file's SHA-256 as 64 lowercase hexadecimal digits, two spaces, the + file's path below the set's root, and a line feed. A path is one or more + segments joined by "/"; a segment begins with one of A-Z, a-z, 0-9, "_" + and "-", continues with those and ".", and does not end with ".". Lines + are in strictly ascending order of their paths as byte strings; no two + paths are equal once A-Z are mapped to a-z, and no path followed by "/" + begins another. The listing has at least one line and nothing else. + registers further forms. + A receiver that is to decode through a capability to which a pin + applies MUST verify the pin first. The pin resolves when + the receiver holds a parameter set whose digest, in the pin's form, + equals sha256: for "wai.prior.set", a listing with that digest together + with every file it names, each with the digest its line gives. Nothing + else satisfies a pin: not a matching id, and not a set in another form + whose unpacked parameters compare equal. A pin that does not resolve is + refused with the first of these reasons that applies: +
    +
  1. "pin_malformed": sha256 is not 64 lowercase hexadecimal + digits.
  2. +
  3. "form_not_registered": form names no registered form.
  4. +
  5. "form_unreadable": the receiver has no reader for the form.
  6. +
  7. "form_required": form is absent and the capability is not + registered with a one-file set.
  8. +
  9. "prior_revoked" or "prior_not_authorised": the receiver holds a key + log and refuses the set under it, as Section 3.1 + specifies. A receiver that holds no key log skips this check.
  10. +
  11. "prior_not_held": the receiver holds no set with that digest + ("digest_superseded" where it holds the set that the pin's optional + supersedes member names).
  12. +
  13. "set_malformed": the listing breaks a rule above.
  14. +
  15. "part_mismatch": a file the listing names is missing, unreadable, + or has another digest.
  16. +
+ A pinned decode MUST read its parameters only from the + set it verified; a decode that needs a file the set lacks is refused + ("set_incomplete") and is treated as a pin that did not resolve. A + refused pin makes its capability unsupported for that envelope; it is + not a reason to decode anyway. +
+ +
+ Determinism Tiers + A learned decode evaluated in floating point does not in general + reproduce its output across devices. WAI names two reproducibility + guarantees : +
+
entropy-consistency:
+
The entropy decode reproduces byte for byte on every conforming + receiver: the decoded symbols have a portable hash. The reconstruction + to the final medium can differ between conforming receivers.
+
decode-equivalence:
+
The whole decode, to the final medium, reproduces byte for byte on + every conforming receiver: the reconstructed samples have a portable + hash. The capability's registration specifies the decode in integer or + fixed-point arithmetic, entropy stage and synthesis alike.
+
+ "none" claims neither. The tiers are ordered none < + entropy-consistency < decode-equivalence. A tier is a property of a + capability as registered: gives each capability a + registry class, and the class gives the tier. A pin's tier above its + capability's does not conform ("tier_exceeds_class"), and a receiver + MUST NOT rely on it. A receiver reads a tier token it + does not recognize as "none". + An envelope with intent "replicate" over a learned capability + MUST be over a capability registered at + decode-equivalence, with a pin that declares decode-equivalence; + entropy-consistency MUST NOT back "replicate". A tier + covers the medium a receiver emits: a receiver that alters the decoded + medium (for example with a post-decode filter) MUST NOT + claim the tier for the result. + At the time of writing, registers + "wai.neural.int_synth", "wai.neural.int_hyper", "wai.video.int_hyper", + "wai.audio.int_codec", "wai.splat.int_codec" and "wai.neural.int_mlicpp" + at decode-equivalence, and "wai.image.jpegai" at entropy-consistency. + This list is informative; the registry governs. +
+
+ +
+ Catalog + +
+ Packaging Values + This specification extends the allowed packaging values defined in + with two entries: + + Packaging Values + + + + + + + +
NameValueReference
WAIwaiThis document
WAI receiptswai-receipts
+ Every track whose objects are WAI envelopes MUST + declare packaging "wai". On such a track: +
    +
  • every object MUST be exactly one whole envelope. An + object that holds part of an envelope, or more than one, is not this + packaging;
  • +
  • the object boundary MUST be the envelope boundary, so + that the receipt's commitment to an object () + and MOQT's object identity are the same unit;
  • +
  • a receiver that does not recognize the packaging value "wai" + MUST NOT attempt to interpret the objects. MSF's rule + that a parser ignores fields it does not understand covers unknown + fields, not unknown packaging values.
  • +
+
+ +
+ Track Fields + This specification adds the following track-level catalog fields for + tracks with packaging "wai". They are custom fields under + ; their "wai_" prefix keeps them from + colliding with fields MSF defines. + + Track Fields + + + + + + + + + + + + +
FieldNameRequiredJSON TypeDefinition
Capabilitieswai_capabilitiesYesArray of String
Determinismwai_determinismNoString
Pinned parameter setswai_priorsNoArray of Object
Receiptswai_receiptsYesBoolean
Receipt aliaswai_receipt_aliasCond.Number
Receipts trackwai_receipts_trackCond.String
Stagewai_stagedCond.Object
+ +
+ Capabilities + wai_capabilities lists every capability that an object on the track + names as its capability or as its fallback. The list + MUST be exhaustive for the track: its purpose is to let a + receiver decide before subscribing whether it can decode what it is + about to receive. A receiver MUST NOT subscribe on the + assumption that an undeclared capability will not appear. A receiver + MUST treat an object whose envelope names, as the + capability it is about to dispatch to, a capability the list does not + hold as a protocol error, and MUST NOT fall back + silently. +
+ +
+ Determinism + wai_determinism is the weakest tier () that any + object on the track claims, not the strongest. Absent means no tier is + claimed. A receiver reads a value it does not recognize as "none". The + field helps a receiver choose a track; the tier a receiver claims for + an object is the one its own decode earns. +
+ +
+ Receipts Fields +
+
wai_receipts:
+
Whether the track's objects are covered by receipts + (). A receiver reads an absent value as + false.
+
wai_receipt_alias:
+
An integer from 0 to 2^53-1, written as an integer: the + track_alias that the track's receipts sign + (). MOQT's own Track Alias is chosen per + session, by each hop (), so past + the first hop it is not the number the receipts sign. Two tracks of a + catalog MUST NOT carry the same value.
+
wai_receipts_track:
+
The name, in this track's namespace, of the track of packaging + "wai-receipts" that carries this track's receipts + (). Two tracks of a catalog + MUST NOT name the same receipts track.
+
+ A track of a stage set that declares wai_receipts true + MUST carry wai_receipt_alias and wai_receipts_track + (). On another track both are + OPTIONAL; a receiver finds a track's receipts chain + and receipts track by them. A clone + () inherits every field of its + parent but its name, so a clone of a track that declares receipts + MUST override both. +
+
+ +
+ Pinned Parameter Sets + A track MAY state, in wai_priors, the pins its objects + carry. A receiver can then tell before it subscribes whether it holds + the parameter sets the track needs, and at which tier each is + offered. + ", + "form": "wai.prior.set", + "tier": "decode-equivalence" } +] +]]> + + Members of a wai_priors Entry + + + + + + + + + +
MemberPresenceMeaning
capabilityREQUIRED, stringA capability the + track's wai_capabilities lists.
sha256REQUIRED, stringA pin's sha256: 64 + lowercase hexadecimal digits.
formOPTIONAL, stringThe pin's form. Absent, + or null, where the pin declares none.
tierOPTIONAL, stringThe tier the publisher + offers the set at. Advisory.
+ An entry lists a pin for a capability when its capability is that + capability, its sha256 is the pin's, and its form is the pin's (both + absent, or the same string). A member of an entry that this document does + not define is ignored. +
+
Reading.
+
wai_priors, where present and not null, is an array of objects, + each with the members above of their types, each capability one that + the track's wai_capabilities lists, and no two with the same + capability, sha256 and form. A receiver that reads anything else + refuses the track ("catalog_priors_malformed") and does not subscribe + to it. A tier this document does not define is read as "none".
+
Exhaustive.
+
+ Where a track carries wai_priors, the publisher + MUST list in it every pin that its objects carry. An + object breaks the catalog when: +
    +
  1. it carries a pin that no entry lists for its capability or for + its fallback; or
  2. +
  3. an entry names its capability, or its fallback (when not null), + and the object carries no pin, or carries a pin that no entry lists + for that capability.
  4. +
+ A publisher whose objects' pin can decode through the fallback + SHOULD list the set for the fallback as well. +
+
Mismatch.
+
A receiver checks each object of such a track against the track's + wai_priors before it dispatches the object. An object that breaks the + catalog is refused ("catalog_prior_mismatch"), and the track is refused + with it: the receiver dispatches no further object of the track, and + MAY unsubscribe from it. The catalog never stands in for + the envelope. A receiver verifies an object's pin + () whether or not the catalog lists it; an entry + never satisfies a pin, never supplies a pin the envelope lacks, and + never changes which set a decode reads.
+
Tier.
+
A listed tier is advisory. A publisher MUST NOT list + a tier above the one the capability's registry class gives. A receiver + MUST NOT claim a tier on the strength of a listed tier: + the tier it claims for an object is the one its own decode earns + (). A listed tier that differs from the tier an + object's pin states is not a mismatch.
+
+
+ +
+ Staged Tracks + WAI's staged delivery sends a base first + and refinement layers after it; a receiver presents the base, and + applies each layer that arrives in time. Each layer is a WAI envelope + whose capability is refinement-only and whose manifest names, in + refines, what it refines. A stage set is carried as one track per + layer: a base track, which carries the set's bases, and a layer track + for each layer index k, which carries the layers of index k. A receiver + chooses its stage by the tracks it subscribes to; a relay forwards + tracks and never reads an envelope. + Each track of a set MUST carry, in its catalog entry, a + wai_staged object: + +
+
set:
+
A non-empty string, the same on every track of the set.
+
layer:
+
An integer from 0 to 255: 0 on a base track, k on a track that + carries layer k.
+
lead_ms:
+
On a layer track of a live set, an integer from 0 to 2^53-1: the + time before a unit's presentation by which the layer must arrive to be + applied to it. null on a base track. A receiver ignores it when isLive + is false.
+
+ A set's extent is the tracks present: layers 0 to the highest layer a + track of the set states. More than one track MAY carry one + layer, as renditions of it, each with a receipts chain of its own. A + publisher puts on a track of layer k only envelopes whose refines.layer + is k, and on a base track only envelopes with no refines; a receiver + applies a layer by its own refines, whichever track it came on. A layer + track's wai_capabilities lists refinement capabilities, so a receiver + that does not implement staged delivery finds them unsupported and has + no reason to subscribe. + The tracks of a set MUST share one namespace, one + isLive () and one renderGroup + (), present on each and written as + an integer. Every entry of a track's depends + () MUST name a track + of the catalog in the track's namespace; the depends of a track of layer + k MUST name at least one track of the set at each layer + below k and none at k or above, and a base track's MUST + NOT name a track of the set. A layer track MUST + NOT carry altGroup, temporalId or spatialId + (, , + ); an altGroup MAY + group the base tracks of alternative sets. Every track of a set that + declares wai_receipts true carries its own wai_receipt_alias and + receipts track (). + A receiver checks each set's entries in this order, stops at the + first check that fails, and treats the layer tracks of a set that fails + as absent (its base tracks remain tracks of packaging "wai"): +
    +
  1. wai_staged has its three members, of their types and ranges; + packaging is "wai"; isLive is a boolean; the track's name is a string + and its namespace absent or a string, and no other track has that + namespace and name; renderGroup, if present, is an integer below 2^64; + depends, if present, is an array of strings + ("staged-malformed").
  2. +
  3. The set has a track at each layer from 0 to its highest + ("staged-set-incomplete").
  4. +
  5. The depends rules above hold ("staged-depends").
  6. +
  7. The tracks share one namespace, one isLive and one renderGroup, + present on each, and no layer track carries altGroup, temporalId or + spatialId ("staged-grouping").
  8. +
  9. On a live set, lead_ms is an integer on every layer track and null + on every base track ("staged-lead").
  10. +
  11. Every wai_receipt_alias of the catalog is an integer from 0 to + 2^53-1; each track of the set that declares receipts carries an alias + and names a receipts track of packaging "wai-receipts" in its + namespace; and no other track carries that alias or names that + receipts track ("staged-receipts").
  12. +
+ A track that declares no namespace inherits the catalog track's + (). A checker that does not know the + catalog track's namespace refuses a set that mixes declared and + inherited namespaces ("staged-malformed"). + Groups and objects. On a live track each group MUST + carry exactly one object, which travels in a subgroup of its own + (). A group of one object is complete + when its object is, so its group receipt is signed before the object is + sent. Group and object identifiers of different tracks need not align: + refines names what a layer refines. + Priority. The base track's publisher priority + () MUST be strictly + below every layer track's, and the priority of a track of layer k + MUST be no greater than that of any track of a layer above + k. This holds for each track's DEFAULT_PUBLISHER_PRIORITY + () and for every subgroup that sets + its own. A receipts track's priority is its media track's. A subscriber + that wants the publisher's order SHOULD give its requests + for a set's tracks one subscriber priority. + Delivery timeouts. A publisher SHOULD set a live layer + object's OBJECT_DELIVERY_TIMEOUT () to + no more than its slack: the time from publication to the presentation of + the last unit the object covers, less the layer's lead_ms, less the + publisher's estimate of the path. An object whose slack is zero or less + cannot arrive in time, and the publisher MAY withhold it. + A publisher MUST NOT set on a base track a delivery + timeout of either kind shorter than the time from a base's publication + to the presentation of its last unit. A delivery timeout releases + bandwidth hop by hop; it is not an end-to-end deadline, and whether a + layer is late is decided by the receiver, as + specifies. + Capping. A receiver caps its stage by subscribing to the base tracks, + to the tracks of layers 1 to n, and to their receipts tracks, and to no + others. A relay MUST NOT read an envelope to cap + layers. +
+
+ +
+ Receipts + +
+ Overview + A publisher that declares wai_receipts true commits to every object of + a track, group by group. For each MOQT group it computes each object's + content hash, builds a Merkle tree over them, and signs the root and the + group's metadata with its Ed25519 key : the + group receipt. Each object has an object receipt that carries its + Merkle inclusion proof. Group receipts form a chain for each publisher + key and receipt alias: each names the link hash of the one before. The + receipts of group g travel as one object in group g of the track's + receipts track (), so that a receiver can + verify the tracks it subscribes to without the receipts of others, and + can account for an object it did not receive. + The rules here are those of for the binary + form that this carriage uses. That document also defines a JSON form, + signed claims and key discovery, which this document does not + need. +
+ +
+ Content Hash + + envelope is the object's payload: the whole envelope, from "WAI1" to + the last payload byte, with nothing inserted or reframed. +
+ +
+ Merkle Tree + A group's objects are its leaves, in MOQT Object ID order, starting + at 0. An object's leaf takes the first form below when its receipt + carries no energy label, and the second when it does + (): + + When a level has an odd number of nodes, its last node is duplicated + to make a pair. The root is the single node at the top; the root of an + empty group is 32 zero bytes. Since an object receipt's object_id is + both its MOQT Object ID () and its leaf + index (), a publisher that declares + receipts numbers each group's objects from 0, without gaps. joules_micro is the energy, in + microjoules, that producing the object took, and 0 is the unmetered + marker, never a figure of zero joules. origin is a UTF-8 label of where + the object was produced. +
+ +
+ Energy Class Encoding + An energy figure's acquisition class says how the figure was + obtained . Where a receipt signs it, the class + is one tag byte followed by its evidence: + + Acquisition Classes + + + + + + + + + + + +
TagClassFollowed by
0unmeterednothing
1HwShunt (legacy)nothing
2ModelBasednothing
3Estimatornothing
4OnChipCounteru32_be(relative_ppm) || u64_be(window_us) || u8(filtering: 0 unknown, 1 off, 2 on)
5CalibratedInstrumentu32_be(relative_ppm) || u64_be(window_us) || u32_be(len) || calibration reference, UTF-8
+ relative_ppm is the standard relative uncertainty of the figure in + parts per million, and window_us the integration window in + microseconds. A class in a receipt is valid only when: tag 0 stands + beside a zero figure and every other tag beside a non-zero one; tag 1 + does not appear (it is accepted only in receipts that predate the + labels); and for tags 4 and 5, relative_ppm and window_us are both + non-zero and figure x relative_ppm is at least 288676, the least + uncertainty a whole number of microjoules can carry. The classes are + ordered, strongest first: CalibratedInstrument, OnChipCounter, HwShunt, + ModelBased, Estimator. +
+ +
+ Group Signature + The publisher signs, with pure Ed25519 , one + of two signing payloads: + + group_start and group_end are timestamps of the form of + . signer_id names the signer; a receiver reads + nothing from it unless it can confirm it by a means outside this + document. + The labelled payload signs the link to the parent group; the legacy + payload does not. A labelled group's class is the one its objects + compose to: +
    +
  • if an object with a non-zero figure carries no label, the group + MUST be signed in the legacy form;
  • +
  • if no object has a non-zero figure, the class is tag 0 and the + coverage 0;
  • +
  • otherwise the class is the weakest of the objects' classes among + those with a non-zero figure, and the coverage is 1 exactly when some + object's figure is zero. For a hardware class (tag 4 or 5), + relative_ppm is at least the ceiling of (sum of figure_i x + relative_ppm_i) / (sum of figure_i), and window_us is the longest of + the objects'. A tag-4 total's filtering state is the objects' common + one, or 0. A tag-5 total needs one calibration reference shared by + every object, or the group is signed in the legacy form.
  • +
+
+ Link Hash + + signing_payload is the payload of the form the group was signed in. + A group's parent is the link hash of the previous group of its chain, + or none for the first. A legacy group's link hash does not cover its + parent link, which the legacy payload does not sign. +
+
+ +
+ Binary Forms + + The first 19 bytes of group_bin, the signing domain, give its form. + Every field's length is known when the bytes are read in order. Bit i + of direction_bits is 1 when the node at level i is the right child. An + object receipt inside receipts_bin takes the group's track_alias and + group_id. + A reader reads exactly the bytes it is given and refuses them as + "malformed" at the first of these that holds: the magic is not "WJR1"; + the signing domain is neither of ; the bytes + end before a field, or bytes follow the last object; a string is not + UTF-8; a class tag is not one of or its + evidence does not read; a coverage byte, parent flag or filtering byte + is out of range; a parent flag of 0 is followed by bytes that are not + zero; or leaf_version is neither 1 nor 2. The form is canonical: a + receipt has one binary form, and a writer reproduces the bytes it + read. +
+ +
+ The Receipts Track + A receipts track has packaging "wai-receipts" and is named by its + media track's wai_receipts_track. Each media track has a receipts track + of its own. +
    +
  • Group g of a receipts track carries one object, the receipts_bin of + group g of its media track: its group receipt and object receipts. The + publisher publishes it no later than that group's first object.
  • +
  • A receipt is about one MOQT object. The group receipt in group g of + a receipts track has group_id g; each object receipt in it has that + group_id and, as object_id, the MOQT Object ID of its object; its + publisher_key is the publisher's key and its track_alias the media + track's wai_receipt_alias. A receiver refuses a receipt that breaks this + ("moqt_mismatch") before it verifies its signature, and holds nothing + of it.
  • +
  • A receipts track has no delivery timeout of either kind: the + publisher sets none, and a subscriber sets none on its subscription + (, + ). Its publisher priority is its + media track's.
  • +
  • Its objects are receipts, not envelopes; 's + object rules do not apply to them.
  • +
+ A receiver learns the publisher's key by a means outside this + document: configuration, the session in which it subscribed, or key + discovery as Section 7 specifies. +
+ +
+ Verification + A receiver verifies each group receipt and each object receipt it + uses in these steps, in order: +
    +
  1. Read. The receipts_bin reads (); otherwise + "malformed". A labelled group's class reads and is valid with the + group alone: tag 1 does not appear, tag 0 stands exactly beside a zero + total with coverage 0, and a hardware class meets the floor of + ; otherwise "group_label_invalid".
  2. +
  3. Signature. The publisher key and the signature pass, in order: + the key is the one encoding of its point that + allows; the key's point is not + of small order; S is less than L; R is not of small order; and the + cofactorless equation holds: the point S*B - k*A, with + k = SHA-512(R || A || M) mod L over the bytes of R and A as the + signature and key give them, encodes to R byte for byte. A key that fails one of the first two checks is + "bad_signer_key"; a signature that fails a later one is + "signature_verify_failed". These checks fix what + leaves to the verifier, so that every + conforming receiver accepts the same signatures. The receiver then + holds a verified root.
  4. +
  5. Chain. Chains are kept per (publisher key, track_alias). With no + group of the chain held, a group with no parent starts it. A group + whose parent is the head's link hash extends it. A group already + admitted, offered again with the same bytes, passes and changes + nothing. A second group with no parent, or one whose parent is the + link hash of a group of the chain other than its head, is + "chain_break". A group whose parent the receiver does not hold is + "parent_not_held": the receiver MAY hold it while it + recovers the parent, by a FETCH () + of the receipts track, and MUST NOT accept it until + then. A legacy group whose group_id does not exceed its chain head's + is "group_id_not_increasing".
  6. +
  7. Content hash. For an object the receiver received, the content + hash of its bytes () equals the object + receipt's; otherwise "content_hash_mismatch".
  8. +
  9. Inclusion. A labelled object receipt's class is valid + (); otherwise "object_label_invalid". + leaf_index is below leaf_count, the proof holds exactly + ceil(log2(leaf_count)) siblings (none for one leaf), direction_bits + equals leaf_index, and object_id equals leaf_index; otherwise + "proof_position_mismatch". The object's leaf, in the form its + leaf_version gives, walked up the proof, reaches the verified root; + otherwise "inclusion_proof_failed".
  10. +
  11. Accept. The object's bytes are bound to the publisher key at + (track_alias, group_id, object_id). Where its leaf is labelled, its + figure, class and origin are bound with them; otherwise they are the + publisher's unsigned statements.
  12. +
+ A rejection at steps 1 to 5 is a hard failure for the object. A + receiver MUST NOT accept a partial verification, such as + a valid proof against an unverified root. + An object receipt checked by steps 1, 2, 3 and 5 for an object the + receiver did not receive is "inclusion-verified": the publisher signed an + object with that content hash at that position. A receiver + MUST report it as such, and MUST NOT + report it, or anything read from it, as delivered, or dispatch on + it. + A receiver that holds an accepted object receipt for every leaf of a + group MUST check that their figures sum to the group's + total ("group_total_mismatch"), and, for a labelled group, that the + group's class, coverage and uncertainty are those its objects compose to + (). + Holding and joining. A receiver that holds groups for missing parents + MUST state a finite bound on what it holds: per chain, + and across all its chains, since any key makes a chain. Past the bound + it refuses what it holds ("parent_not_recovered") and re-anchors the + chain at the next validly signed group whose group_id exceeds the + head's. A receiver that subscribes to a live track after its first group + () anchors each chain at the first + validly signed group it receives. An anchor establishes only that the + key signed that group; a receiver MUST report a chain it + anchored as anchored, with the anchor's group_id, and MUST + NOT report the groups the anchor skipped as verified. + Section 3 specifies how the groups of such a + gap, fetched later, are admitted as history. +
+ +
+ The WAI_RECEIPT Object Property + An object on a track of packaging "wai" MAY carry its + receipt commitment as an MOQT Object Property + (), WAI_RECEIPT. Its value is: + + vi64 is the variable-length integer of + . acquisition is the class name of + ("ModelBased", "Estimator", + "OnChipCounter", "CalibratedInstrument", or "HwShunt"), or empty; + acquisition_evidence is, for tags 4 and 5, the evidence that follows + the tag byte, and empty otherwise; group_receipt is the link hash of the + group receipt covering the object. The publisher + MUST place the property inside Immutable Properties + (), which no relay may change or + remove. +
    +
  • A receiver refuses a value that ends early, has bytes after + group_receipt, or whose acquisition is not UTF-8 + ("property_invalid").
  • +
  • A non-zero joules_micro MUST be accompanied by + acquisition. A zero joules_micro is the unmetered marker: + acquisition and acquisition_evidence MUST be empty, and + a receiver MUST NOT present it as zero joules. + acquisition_evidence MUST be non-empty for + OnChipCounter and CalibratedInstrument and empty for every other + class. A receiver MUST discard a property that breaks + this item ("property_invalid").
  • +
  • No signature covers the property. A receiver MUST + NOT report its class as signed unless the class, evidence and + figure equal those in the object's labelled leaf, once the object + receipt is accepted against the group receipt group_receipt names; + where they differ, the receiver MUST discard the + property ("property_disagrees").
  • +
  • A relay MUST forward the property unchanged.
  • +
  • The property rides on its object and is lost with it; the + receipts track carries the receipts of an object that is not + delivered. Absence of the property is not evidence that an object + has no receipt.
  • +
+ Until a type is assigned (), a track of + packaging "wai" carries the property as type 0x3A41, from the range + sets aside for an application's own + use. Another application can give that type another meaning, so a + receiver reads it only on a track of packaging "wai". +
+ +
+ Relays + An MOQT relay forwards tracks of packaging "wai" and "wai-receipts" + as it forwards any track, their payloads opaque + (). It needs no knowledge of this + document and verifies no receipt: the receiver verifies every receipt + itself. A relay that does not forward an object, because a delivery + timeout passed or at its discretion, still forwards the receipts track's + objects, which are separate MOQT objects. A relay that verifies receipts + before forwarding them is permitted, and + Section 4 states what it must preserve. +
+
+ +
+ The codecs Parameter + The optional codecs parameter of application/wai + () lists the capabilities a sequence of + objects uses, in the form of . Its WAI elements + follow this grammar : + + A cap is a capability string without its leading "wai.". In an alt, + the first cap is a capability a receiver can select, and each "+cap" is + one whose payload is applied with it. The element lists every + capability that any object it describes may dispatch to or apply. An + element that begins with "wai", in any case, MUST match + the grammar. Section 4 gives the remaining + rules. +
+ +
+ Implementation Status + (Note to the RFC Editor: please remove this section before + publication, as describes.) + One open-source implementation family exists, in the repository named + in the note at the start of this document, under the Apache License + 2.0: +
    +
  • "wai-rs", the WAI reference library: the envelope, dispatch and + pin verification (), the tier rules, the + catalog check of staged tracks and the reading and check of wai_priors + (), and a receipts verifier + ().
  • +
  • "joule-moq", an MOQT implementation of + : the receipts track in its binary form, receipt + chains per key and alias, the WAI_RECEIPT property at type 0x3A41 inside + Immutable Properties, and a test relay that drops late objects and still + forwards their receipts. A browser receiver built on it refuses an + object whose capability its track does not declare.
  • +
  • Conformance corpora for the catalog checks, wai_priors, the + receipts and the property. Each is checked by the library and by an + independent verifier, written in Python from the specification text, + that shares no code with the library.
  • +
+ Not implemented at the time of writing: no publisher in the + implementation writes wai_priors, and the browser receiver does not read + it (it dispatches no capability that decodes against a parameter set); + no receiver reads wai_determinism; and no type is assigned for + WAI_RECEIPT. +
+ +
+ Security Considerations + This document relies on MOQT for transport + security: QUIC protects each hop. The receipts of + add integrity from the publisher to the + receiver across relays; they add no confidentiality. + +
+ Receipt Keys + Each group receipt has one signer, the publisher key in it. Forging + one requires the private key or a break of Ed25519. A valid signature + shows that a key signed; it does not show whose key it is. A receiver + MUST NOT present a receipt as the publisher's unless it + holds the key as the publisher's by a means it trusts (configuration, + the session, or key discovery as Section 7 + specifies). Publishers MUST generate keys with a + cryptographically secure random number generator and MUST + NOT use one key for distinct publisher identities. The strict + verification of step 2 refuses keys of + small order, under which a signature can verify without any private + key, and makes every conforming receiver accept the same signatures. A + compromised key can sign any group; rotation and revocation are outside + this document. +
+ +
+ Replay + A group receipt binds the publisher key, track_alias and group_id + under its signature, and an object receipt is bound to its leaf + position, so neither can be moved to another track, group or position + without detection. The receipts-track rule of + binds each receipt to the MOQT group and + object it is carried for. + Chains are kept per key and track. The labelled payload signs the + link to the parent group; the legacy payload does not, so in a legacy + chain anyone on the path can rewrite the link, and the rule that a + legacy group's group_id exceeds its head's is the only order that chain + signs. A relay can drop a legacy group and rewrite the next one's link + to hide the gap; only the labelled payload rules that out. + An anchor () proves nothing about the + groups before it: a relay can withhold groups, or offer a joining + receiver an old group that is validly signed. A receiver reports + anchors and gaps, and MAY refuse a live group whose + group_end is older than a freshness window it states; it applies such + a window only to the live edge, never to history it fetches. +
+ +
+ Relay Transparency + A relay forwards payloads without reading them. It cannot alter an + envelope without changing its content hash, nor alter a receipt without + breaking its signature. It can drop, delay or withhold objects, whole + groups of a receipts track, or whole tracks; the receipts of + make an undelivered object accountable, as + inclusion-verified, but they do not make delivery happen. + The WAI_RECEIPT property is not signed. Immutable Properties forbid a + relay to change it, but a receiver cannot detect a relay that breaks + that rule from the property alone, which is why + lets a receiver report its class only when it + matches a signed leaf. + A relay sees what MOQT exposes: track names, catalogs (unless they + are encrypted), object sizes, timing and properties, including + WAI_RECEIPT. +
+ +
+ Catalog Integrity + Receipts do not cover the catalog. Where the catalog is not protected + end to end, an on-path party able to rewrite it can misdeclare a track's + capabilities, tiers or pinned parameter sets. The envelope remains the + authority: a receiver verifies every pin before it decodes + (), and an object that breaks wai_priors refuses + its track (), so a misdeclared catalog leads to + refusal, not to a decode against the wrong parameters. A listed tier is + advisory and confers nothing. +
+ +
+ Parameter Sets + A learned decoder run against parameters other than the ones a + payload was coded against produces plausible but wrong output, with no + error. The pin makes the parameter set part of what the receiver + checks, by digest. A receiver that decodes from bytes other than the + ones it verified (for example by reading a file again after checking + it) loses that protection; a receiver SHOULD decode from + the very bytes it verified. +
+ +
+ Resource Consumption + A receiver bounds what it parses and holds: the manifest limits of + ; a bound on the receipts it holds for missing + parents (); and the bounds of + Section 2 on decoded payloads. A receiver + SHOULD check a receipts_bin's object count, and a Merkle + proof's sibling count, against the bytes that remain before it + allocates anything for them. +
+ +
+ Privacy + Receipts carry the publisher key, a signer label, origin labels, + timestamps and energy figures in cleartext, and a receipts track is + visible to every relay that forwards it. They reveal when and where + objects were produced, and what producing them cost. A publisher can + send the unmetered marker in place of a figure and an empty origin. A + deployment that needs these hidden from relays encrypts the receipts + track's objects end to end, for example with + ; the receipts then commit to the bytes + carried. +
+
+ +
+ IANA Considerations + +
+ MSF Packaging Values + This document requests no IANA action for the packaging values "wai" + and "wai-receipts". lists its allowed packaging + values in its own text () and + creates no registry for them, and this document extends that list + (), as other MSF packaging documents do. +
+ +
+ MOQ Properties + This document requests that IANA register the following entry in the + "MOQ Properties" registry established by + , with a type from the range + 0x80 to 0x37FF, whose registration policy is Specification Required: + + WAI_RECEIPT Property + + + + + + +
TypeNameScopeSpecification
TBD (odd)WAI_RECEIPTObject of this document
+ The type is odd, since the value is a byte sequence + (). Until a type is assigned, + implementations use 0x3A41, from the range 0x3800 to 0x3FFF that + reserves for application-specific + use; no registration is possible in that range, and none is + requested for it. +
+ +
+ Media Type application/wai + This document requests that IANA register the media type + application/wai in the "Media Types" registry, in the standards tree, + as provides for a registration made by an IETF + document. +
+
Type name:
application
+
Subtype name:
wai
+
Required parameters:
N/A
+
Optional parameters:
codecs, as + of this document defines.
+
Encoding considerations:
binary
+
Security considerations:
See + of this document. A WAI object names the + decoder its payload needs and does not carry executable code.
+
Interoperability considerations:
An object is a "WAI1" + envelope () or a "WAI2" envelope + ( Section 7.1). A reader refuses an unknown major + version.
+
Published specification:
This document, and + .
+
Applications that use this media type:
Media publishers, + relays, caches and players that carry WAI envelopes over MOQT or + HTTP.
+
Fragment identifier considerations:
None defined by this + document.
+
Additional information:
+
+ Deprecated alias names for this type: N/A + Magic number(s): 57 41 49 31 ("WAI1") or 57 41 49 32 ("WAI2") + at offset 0 + File extension(s): .wai + Macintosh file type code(s): N/A +
+
Person and email address to contact for further + information:
The author of this document.
+
Intended usage:
COMMON
+
Restrictions on usage:
N/A
+
Author:
See the Author's Address section of this + document.
+
Change controller:
IETF
+
+
+ +
+ Capability Strings + This document creates no registry for capability strings or tier + names. registers them under its own registry + policy. +
+
+
+ + + + References + + Normative References + + + + Media over QUIC Transport + + + + + + + + + MOQT Streaming Format + + + + + + + + + WAI v1.0: Web AI Media Transport and Execution + + + + Draft Standard, open-standards repository, wai/SPEC.md + + + + + The BLAKE3 Hashing Framework + + + + + + + + + Secure Hash Standard (SHS) + National Institute of Standards and Technology + + + + + + + + + Key words for use in RFCs to Indicate Requirement Levels + + + + + + + + + + + Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words + + + + + + + + + + + Edwards-Curve Digital Signature Algorithm (EdDSA) + + + + + + + + + + The JavaScript Object Notation (JSON) Data Interchange Format + + + + + + + + + + + Date and Time on the Internet: Timestamps + + + + + + + + + + Augmented BNF for Syntax Specifications: ABNF + + + + + + + + + + + The 'Codecs' and 'Profiles' Parameters for "Bucket" Media Types + + + + + + + + + + Media Type Specifications and Registration Procedures + + + + + + + + + + + Informative References + + + + CMSF- a CMAF compliant implementation of MOQT Streaming Format + + + + + + + + + End-to-End Secure Objects for Media over QUIC Transport + + + + + + + + + Improving Awareness of Running Code: The Implementation Status Section + + + + + + + + + + + WAI Extension: MoQ Streaming Format + + + + open-standards repository, wai/extensions/moq-streaming-format.md + + + + + WAI Extension: JWP Receipt Chain + + + + open-standards repository, wai/extensions/jwp-receipts.md + + + + + WAI Extension: Determinism Tiers for Neural Decode + + + + open-standards repository, wai/extensions/determinism-tiers.md + + + + + WAI Extension: Prior Carriage + + + + open-standards repository, wai/extensions/prior-carriage.md + + + + + WAI Extension: Staged Delivery + + + + open-standards repository, wai/extensions/staged-delivery.md + + + + + WAI Extension: Energy Measurement + + + + open-standards repository, wai/extensions/energy-measurement.md + + + + + WAI Extension: HTTP Delivery + + + + open-standards repository, wai/extensions/http-delivery.md + + + + +
+ Catalog Example + A live catalog with a stage set (a base track and one layer track, + each with its receipts track) and an on-demand track of a learned + image capability that states its pinned parameter set. The digest is a + placeholder. + + An object on "pictures" is a WAI envelope whose capability is + "wai.neural.int_hyper", whose fallback is "wai.image.png", and whose pin + names the listed set. A receiver that holds that set decodes the learned + payload; one that does not falls back to PNG. An object whose pin named + another set would refuse the track. +
+ +
+ Refusal Reasons + + Refusal Reasons + + + + + + + + + + + + + +
ReasonRaised by
missing_capability
pin_malformed, form_not_registered, form_unreadable, form_required, prior_revoked, prior_not_authorised, prior_not_held, digest_superseded, set_malformed, part_mismatch, set_incomplete
tier_exceeds_class
catalog_priors_malformed, catalog_prior_mismatch
staged-malformed, staged-set-incomplete, staged-depends, staged-grouping, staged-lead, staged-receipts
moqt_mismatch
malformed, group_label_invalid, bad_signer_key, signature_verify_failed, chain_break, parent_not_held, parent_not_recovered, group_id_not_increasing, content_hash_mismatch, object_label_invalid, proof_position_mismatch, inclusion_proof_failed, group_total_mismatch
property_invalid, property_disagrees
+
+ +
+ Correspondence with the WAI Texts + This document restates, for carriage over MOQT, parts of the WAI + specification and its extensions. Where this document and the cited text + differ, the difference is a defect in one of them. + + Correspondence + + + + + + + + + + + + + + + +
This documentWAI text
, Sections 2 and 3
Section 4
Section 3.1;
Sections 2 and 3
, Sections 3 and 4
Section 4.2
Section 4.1
to Sections 2, 2.7 and 3; Sections 2.3 and 2.7
Section 5
, Sections 3 and 4
+
+
+
diff --git a/wai/ietf/nmsf-comment.md b/wai/ietf/nmsf-comment.md new file mode 100644 index 000000000..659d33171 --- /dev/null +++ b/wai/ietf/nmsf-comment.md @@ -0,0 +1,98 @@ +# Comment on `draft-herz-moq-nmsf-01`: a model digest and a determinism field + +> Status: draft comment for the authors of `draft-herz-moq-nmsf-01` and the moq list. +> **Not sent.** Sending it is the owner's decision. Section numbers below are those of +> `draft-herz-moq-nmsf-01` unless another document is named. + +Thank you for the draft. Two catalog fields would let a subscriber check two things +the current text leaves to agreement between the endpoints: which model weights a +track needs, and whether a decoder on another device reproduces what the encoder's +context assumes. Both proposals are small and OPTIONAL, and neither changes the +object wire format of §3.2. + +## 1. A model digest + +§3.9's `modelVersion` is a version string. Two endpoints can hold different weights +under the same string, through a fine-tune, a re-export or a quantised variant. A +learned decoder run against weights other than the encoder's does not fail: it +produces a plausible picture that is wrong. + +Proposal: an OPTIONAL `modelDigest` member in the `nvc` object, the SHA-256 of the +weights' bytes as 64 lowercase hexadecimal digits, with two rules: + +- a subscriber that holds no weights with that digest SHOULD NOT subscribe, which + extends §3.9's existing "SHOULD NOT subscribe" rule to a check a subscriber can + make; +- a decoder MUST NOT decode a track against weights whose digest differs. + +Where the weights span several files (networks, entropy tables), the digest can +cover a listing of per-file digests. The WAI specification defines one form for such +a listing: one line per file, giving its SHA-256 and its path, in byte order of the +paths. A checksum utility can verify it without WAI software (WAI `SPEC.md` §3.1, +"What the digest covers"). + +## 2. A determinism field + +§4.6 has the encoder run the synthesis transform so that its context buffer "contains +exactly the same data that a decoder would produce". That holds only where the +decoder computes the same values as the encoder, on whatever device it runs: + +- **Entropy decode.** §4.2 step 4 entropy-decodes the latent with CDF parameters + derived from the decoded hyperprior. If that derivation runs in floating point, two + devices can derive different parameters from one bitstream, select different + distributions, and decode different symbols from then on. +- **Context.** §4.1 feeds each reconstructed frame back as the next frame's context. + Where the reconstruction differs between devices, the difference carries forward + until the next Intra frame. Where the entropy model also reads that context (a + temporal prior), the entropy decode itself diverges. + +Floating-point basic operations are correctly rounded, but runtimes choose +accumulation order, fused multiply-add, vector width and reduced precision per +device, so the same network does not in general give the same bits on two devices. + +Proposal: an OPTIONAL `entropyDeterminism` member in the `nvc` object, with two +values: + +- `integer-exact`: the decoder derives every entropy-coding parameter in integer or + fixed-point arithmetic, or reads it from the bitstream, and so decodes the same + symbols on every device; +- `unchecked`: no such guarantee is made. This is the meaning when the member is + absent. + +A codec whose whole decode, synthesis and context included, is integer could also +state that, so that a subscriber knows the reconstructed frames themselves reproduce. +WAI names these two levels `entropy-consistency` and `decode-equivalence`, and +explains why parameter transmission reaches the first but not the second +(`extensions/determinism-tiers.md` §2). + +Both levels are attainable in practice. WAI registers learned decoders whose +hyper-decoder, entropy-parameter derivation and synthesis are integer +(`wai.neural.int_hyper`, and `wai.video.int_hyper` for integer motion-compensated +P-frames after such a keyframe). The repository's CI decodes their committed test +vectors on Linux x86_64, Linux arm64 and macOS arm64, and requires the same output +hashes on each (`extensions/determinism-tiers.md` §6). Those vectors are small, and +the full-scale model is checked against its exporter's integer reference on one +machine per run. + +## 3. Integrity, for completeness + +§8 relies on hop-by-hop TLS. TLS protects each hop, but not a payload against a relay +that alters it. A publisher signature per group, over the hashes of the group's +objects, carried on a separate track, lets a subscriber detect alteration across +relays and account for objects a relay dropped. If that is of interest, the WAI +receipts track defines one way to do it (`draft-charlot-moq-wai-packaging-00` §6; +`extensions/jwp-receipts.md` §2 and §3). + +## Pointers + +- WAI `SPEC.md` §3.1: the parameter-set pin, by SHA-256 over the whole set, its + verification order and its refusal reasons. +- `extensions/determinism-tiers.md` §2 and §3: the two tiers and what each lets a + receiver hash. +- `extensions/moq-streaming-format.md` §4 and §4.2: catalog fields that declare a + track's capabilities, its weakest tier, and the parameter sets its objects pin, by + digest. +- `draft-charlot-moq-wai-packaging-00` (in preparation): the same, as an MSF packaging. + +All in the WAI repository: https://git.transaction.science/Transaction-Science/open-standards +(directory `wai/`). -- 2.54.0 (Apple Git-157)