From 2496c954107a9a3839f57aca81c9eb567eee118d Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 1 Oct 2026 23:41:45 -0400 Subject: [PATCH 01/36] wai: the rANS encoder writes the bypass escape; an integer CDF from a histogram RansEncoder::encode now codes a value outside its table's span through the bypass escape, exactly as RansDecoder reads it: the escape symbol, the count of 4-bit groups as a run of 15s closed by a group below 15, then the raw value's groups least significant first. It returns false only where no stream can code the value: a table shorter than three entries or longer than the slice, a frequency that is not positive, or value - offset outside i32. Every stream it writes still ends canonically. int_entropy::hist_to_cdf builds the encoder's table from a histogram in integer arithmetic alone: the escape is the last bin with frequency 1, every symbol gets at least 1, the total is exactly 2^precision, and the leftover goes by largest remainder with ties to the lower index. ternary::rans_encode_trits called encode inside debug_assert!, so a release build coded no symbol; the call now runs in every build. Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/src/int_entropy.rs | 98 ++++++++++++++++++++ wai/wai-rs/src/rans.rs | 167 +++++++++++++++++++++++++++------- wai/wai-rs/src/ternary.rs | 3 +- 3 files changed, 236 insertions(+), 32 deletions(-) diff --git a/wai/wai-rs/src/int_entropy.rs b/wai/wai-rs/src/int_entropy.rs index c36bb5325..88272b1ba 100644 --- a/wai/wai-rs/src/int_entropy.rs +++ b/wai/wai-rs/src/int_entropy.rs @@ -96,6 +96,53 @@ pub fn dequantize(sym: i32, frac: u32, base: i64) -> Option { i64::try_from(((sym as i128) << frac) + base as i128).ok() } +/// The quantised CDF an encoder writes for a histogram, integer only: one table of +/// `hist.len() + 2` entries over the `hist.len()` symbols and the escape, the rANS +/// decoder's layout (`extensions/integer-payloads.md` §2; the escape is the last +/// symbol, `cdf_len − 2`). Every symbol, the escape included, gets frequency at least +/// 1, and the frequencies sum to exactly `2^precision`: the escape gets 1, each +/// symbol `1 + ⌊R·h_i / H⌋` with `R = 2^precision − hist.len() − 1` and `H` the +/// histogram's total, and the `R − Σ⌊·⌋` left over go one each to the symbols with +/// the largest remainders `R·h_i mod H`, ties to the lower index. An all-zero +/// histogram counts every symbol once. So the table is strictly increasing, from 0 to +/// `2^precision`, and the same histogram gives the same table on every machine. +/// +/// `None` when the histogram is empty, when `precision` is outside `1..=16` (the +/// decoder's precision is 16), or when the symbols and the escape cannot each have +/// frequency 1 (`hist.len() + 1 > 2^precision`). +pub fn hist_to_cdf(hist: &[u64], precision: u32) -> Option> { + if hist.is_empty() || !(1..=16).contains(&precision) { + return None; + } + let total: u128 = 1u128 << precision; + let n = hist.len() as u128; + if n + 1 > total { + return None; + } + let r = total - n - 1; + let all_zero = hist.iter().all(|&h| h == 0); + let weight = |h: u64| -> u128 { if all_zero { 1 } else { h as u128 } }; + let h_total: u128 = hist.iter().map(|&h| weight(h)).sum(); + let mut freq: Vec = hist.iter().map(|&h| 1 + r * weight(h) / h_total).collect(); + let given: u128 = freq.iter().map(|f| f - 1).sum(); + let mut order: Vec = (0..hist.len()).collect(); + // Largest remainder first; equal remainders to the lower index (a stable sort). + order.sort_by_key(|&i| std::cmp::Reverse(r * weight(hist[i]) % h_total)); + for &i in order.iter().take((r - given) as usize) { + freq[i] += 1; + } + freq.push(1); // the escape + let mut cdf = Vec::with_capacity(freq.len() + 1); + let mut acc: u128 = 0; + cdf.push(0i32); + for f in freq { + acc += f; + cdf.push(acc as i32); + } + debug_assert_eq!(acc, total); + Some(cdf) +} + // ───────────────────────────────────────────────────────────────────── // Kani proof harnesses — the soundness hinge, checked rather than asserted // ───────────────────────────────────────────────────────────────────── @@ -212,6 +259,57 @@ mod tests { const FRAC: u32 = 16; // Q16: grid step ~1.5e-5 + /// `hist_to_cdf`'s table: total exactly 2^16, every frequency at least 1, + /// strictly increasing, the escape last with frequency 1, the same table for the + /// same histogram, and frequencies ordered as the counts are. Includes histograms + /// far larger than the total, all-zero and one-symbol histograms, and a ladder + /// where the remainders tie. + #[test] + fn hist_to_cdf_properties() { + let cases: Vec> = vec![ + vec![0], + vec![5], + vec![0, 0, 0], + vec![1, 1, 1, 1, 1, 1, 1], + vec![1_000_000_000_000, 1, 0, 3, u64::MAX / 4], + (0..4095u64).map(|i| (i * 2654435761) % 1000).collect(), + (0..300u64).map(|i| if i == 150 { 1 << 40 } else { 0 }).collect(), + ]; + for hist in &cases { + let cdf = hist_to_cdf(hist, 16).expect("a table"); + assert_eq!(cdf.len(), hist.len() + 2); + assert_eq!(cdf[0], 0); + assert_eq!(*cdf.last().unwrap(), 1 << 16, "the total is exactly 2^16"); + assert!(cdf.windows(2).all(|w| w[1] > w[0]), "every frequency is at least 1"); + assert_eq!(cdf[cdf.len() - 1] - cdf[cdf.len() - 2], 1, "the escape is the last bin, frequency 1"); + assert_eq!(hist_to_cdf(hist, 16).unwrap(), cdf, "deterministic"); + let f: Vec = cdf.windows(2).map(|w| w[1] - w[0]).collect(); + for i in 0..hist.len() { + for j in 0..hist.len() { + if hist[i] > hist[j] { + assert!(f[i] >= f[j], "a larger count never gets a smaller frequency"); + } + } + } + } + // Equal counts: the leftover goes to the lower indices. + let cdf = hist_to_cdf(&[1, 1, 1], 16).unwrap(); + let f: Vec = cdf.windows(2).map(|w| w[1] - w[0]).collect(); + assert_eq!(f, vec![21845, 21845, 21845, 1]); + assert_eq!(hist_to_cdf(&[1, 1, 1, 1], 16).unwrap()[1..].iter().zip(&cdf_ladder()).filter(|(a, b)| a != b).count(), 0); + // Refusals: empty, a precision the decoder does not use, too many symbols. + assert!(hist_to_cdf(&[], 16).is_none()); + assert!(hist_to_cdf(&[1], 0).is_none()); + assert!(hist_to_cdf(&[1], 17).is_none()); + assert!(hist_to_cdf(&vec![1; 65536], 16).is_none()); + assert!(hist_to_cdf(&vec![1; 65535], 16).is_some()); + } + + /// Four equal counts: R = 65531, 16382 each with 3 left over, to indices 0, 1, 2. + fn cdf_ladder() -> Vec { + vec![16384, 32768, 49152, 65535, 65536] + } + #[test] fn integer_bucketing_matches_float_in_bucket_interiors() { let st = scale_table(); diff --git a/wai/wai-rs/src/rans.rs b/wai/wai-rs/src/rans.rs index 5303e1f16..0af9a457f 100644 --- a/wai/wai-rs/src/rans.rs +++ b/wai/wai-rs/src/rans.rs @@ -296,44 +296,84 @@ fn kani_decoder_new_is_total() { } } -/// rANS **encoder** matching [`RansDecoder`] byte-for-byte (rans64, precision 16): -/// the missing half for an entropy-coded integer codec. Symbols are buffered in -/// forward order and emitted in reverse on [`flush`](Self::flush) — the LIFO -/// rANS discipline. Encode + decode are pure integer, so the round-trip is +/// rANS **encoder** matching [`RansDecoder`] byte-for-byte (rans64, precision 16, +/// bypass 4): the encoder half of the integer codecs' entropy layer. Symbols are +/// buffered in forward order and emitted in reverse on [`flush`](Self::flush), the +/// LIFO rANS discipline. Encode and decode are pure integer, so the round trip is /// lossless and deterministic on every machine. /// -/// This core covers the **in-range** path (the symbol's value lands inside the -/// CDF's `[0, cdf_len-2)` span); the decoder's out-of-range *bypass* escape is -/// not produced here, so callers keep latents within range (the common case for -/// a clamped Gaussian-conditional codec). Out-of-range encoding is a follow-on. -// The decode side is wired into every neural codec; the encode side is the -// entropy half of the not-yet-assembled conditional integer codec (and the -// round-trip tests), so it is unused in the library build for now. -#[allow(dead_code)] +/// A value whose symbol lies outside the table's span (`value − offset` below 0 or +/// at least `cdf_len − 2`) is coded through the **bypass escape**, exactly as the +/// decoder reads it (`extensions/integer-payloads.md` §2): the escape symbol +/// `cdf_len − 2`; then the count of 4-bit groups, as a run of 15s closed by a group +/// below 15; then the groups of the raw value, least significant first. The raw value +/// is `2·(v − max)` for `v ≥ max` and `2·(−v − 1) + 1` for `v < 0`, where +/// `v = value − offset` and `max = cdf_len − 2`. Each group is a symbol of uniform +/// frequency at precision 4. The encoder's state starts at `2^31` and it writes the +/// words it flushes and no others, so every stream it writes ends canonically +/// ([`RansDecoder::is_canonical_end`]). #[derive(Default)] pub struct RansEncoder { - syms: Vec<(u32, u32)>, // (start, freq), forward order + syms: Vec, // forward order +} + +/// One buffered symbol: a table symbol `(start, freq)` at precision 16, or a 4-bit +/// bypass group. +#[derive(Clone, Copy)] +enum Sym { + Table { start: u32, freq: u32 }, + Bits(u32), } -#[allow(dead_code)] impl RansEncoder { pub fn new() -> Self { Self::default() } - /// Buffer one symbol: `value` against `cdf[0..cdf_len]` with `offset`, - /// exactly the arguments [`RansDecoder::decode`] consumes. Returns `false` - /// if the symbol is out of range (bypass not produced) — the caller must - /// keep `value - offset` in `[0, cdf_len-2)`. + /// Buffer one symbol: `value` against `cdf[0..cdf_len]` with `offset`, exactly + /// the arguments [`RansDecoder::decode`] consumes, so the decoder returns `value`. + /// A symbol outside the table's span is coded through the bypass escape. Returns + /// `false`, buffering nothing, only where no stream can code `value`: a table + /// shorter than 3 entries or longer than `cdf`, a frequency that is not positive, + /// or `value − offset` outside `i32` (the decoder forms the escaped value in + /// `i32`). pub fn encode(&mut self, value: i32, cdf: &[i32], cdf_len: usize, offset: i32) -> bool { - let max_value = cdf_len as i32 - 2; - let v = value - offset; - if v < 0 || v >= max_value { + if cdf_len < 3 || cdf_len > cdf.len() { return false; } - let start = cdf[v as usize] as u32; - let freq = (cdf[v as usize + 1] - cdf[v as usize]) as u32; - self.syms.push((start, freq)); + let max_value = cdf_len as i64 - 2; + let Ok(v) = i32::try_from(value as i64 - offset as i64) else { + return false; + }; + let v = v as i64; + let (sym, raw) = if v < 0 { + (max_value, Some((-2 * v - 1) as u64)) + } else if v >= max_value { + (max_value, Some(2 * (v - max_value) as u64)) + } else { + (v, None) + }; + let (lo, hi) = (cdf[sym as usize], cdf[sym as usize + 1]); + if lo < 0 || hi <= lo || hi > (1 << PRECISION) { + return false; + } + self.syms.push(Sym::Table { start: lo as u32, freq: (hi - lo) as u32 }); + if let Some(raw) = raw { + // The raw value is at most 2^32 − 1, so at most eight groups. + let mut n_bypass = 0u32; + while (raw >> (n_bypass * BYPASS_PRECISION)) != 0 { + n_bypass += 1; + } + let mut count = n_bypass; + while count >= MAX_BYPASS_VAL { + self.syms.push(Sym::Bits(MAX_BYPASS_VAL)); + count -= MAX_BYPASS_VAL; + } + self.syms.push(Sym::Bits(count)); + for j in 0..n_bypass { + self.syms.push(Sym::Bits(((raw >> (j * BYPASS_PRECISION)) & MAX_BYPASS_VAL as u64) as u32)); + } + } true } @@ -342,14 +382,30 @@ impl RansEncoder { pub fn flush(self) -> Vec { let mut state: u64 = RANS64_L; let mut renorm: Vec = Vec::new(); - for &(start, freq) in self.syms.iter().rev() { - // renorm so the division stays in range: emit low 32 bits while too big. - let x_max = ((RANS64_L >> PRECISION) << 32) * freq as u64; - while state >= x_max { - renorm.push(state as u32); - state >>= 32; + for &s in self.syms.iter().rev() { + match s { + Sym::Table { start, freq } => { + // renorm so the division stays in range: emit low 32 bits while too big. + let x_max = ((RANS64_L >> PRECISION) << 32) * freq as u64; + while state >= x_max { + renorm.push(state as u32); + state >>= 32; + } + state = ((state / freq as u64) << PRECISION) + (state % freq as u64) + start as u64; + } + Sym::Bits(val) => { + // A group of BYPASS_PRECISION bits is a symbol of frequency + // 2^(PRECISION − BYPASS_PRECISION): the state takes the bits + // directly, the inverse of `RansDecoder::get_bits`. + let freq = 1u64 << (PRECISION - BYPASS_PRECISION); + let x_max = ((RANS64_L >> PRECISION) << 32) * freq; + if state >= x_max { + renorm.push(state as u32); + state >>= 32; + } + state = (state << BYPASS_PRECISION) | val as u64; + } } - state = ((state / freq as u64) << PRECISION) + (state % freq as u64) + start as u64; } renorm.reverse(); // decoder consumes these forward let mut words = vec![state as u32, (state >> 32) as u32]; // flush: lo, hi (decoder seeds hi<<32|lo) @@ -401,6 +457,55 @@ mod tests { assert_eq!(out, syms, "rANS round-trip must be lossless"); } + /// The bypass escape round-trips every `i32` the decoder can return, against + /// tables of several lengths and offsets, the boundaries `±2^31` included, and + /// every stream ends canonically. A value no stream can code (`value − offset` + /// outside `i32`) is refused and buffers nothing. + #[test] + fn encoder_bypass_roundtrip() { + let mut x: u64 = 0x9e37_79b9_7f4a_7c15; + let mut next = || { + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + x + }; + for (n, offset) in [(1usize, 0i32), (4, -2), (16, -8), (255, 7)] { + let cdf = gauss_cdf(2.0, n); + let cdf_len = cdf.len(); + let mut vals: Vec = vec![i32::MIN, i32::MAX, i32::MIN + 1, i32::MAX - 1, -1, 0, 1, offset, offset - 1, + offset + n as i32, offset + n as i32 - 1, offset + n as i32 + 1]; + for _ in 0..400 { + let r = next(); + vals.push(match r % 4 { + 0 => r as i32, + 1 => ((r >> 8) % (n as u64 + 4)) as i32 + offset - 2, + 2 => ((r >> 8) as i32) >> ((r >> 40) % 31), + _ => offset + ((r >> 8) % n as u64) as i32, + }); + } + let mut enc = RansEncoder::new(); + let mut coded = Vec::new(); + for &v in &vals { + let fits = i32::try_from(v as i64 - offset as i64).is_ok(); + assert_eq!(enc.encode(v, &cdf, cdf_len, offset), fits, "value {v} offset {offset}"); + if fits { + coded.push(v); + } + } + let bits = enc.flush(); + let mut dec = RansDecoder::new(&bits).unwrap(); + let out: Vec = (0..coded.len()).map(|_| dec.decode(&cdf, cdf_len, offset)).collect(); + assert_eq!(out, coded, "bypass round-trip (n={n}, offset={offset})"); + assert!(dec.is_canonical_end(), "the encoder's stream ends canonically (n={n})"); + } + // A table too short to have an escape, or a length past the slice, codes nothing. + let mut enc = RansEncoder::new(); + assert!(!enc.encode(0, &[0, 65536], 2, 0)); + assert!(!enc.encode(0, &[0, 1, 65536], 4, 0)); + assert_eq!(enc.flush(), RansEncoder::new().flush()); + } + #[test] fn conditional_integer_entropy_round_trips_via_int_entropy() { // The codec's entropy LAYER end-to-end, pure integer: each latent's CDF diff --git a/wai/wai-rs/src/ternary.rs b/wai/wai-rs/src/ternary.rs index ab459892b..0a37b2f4d 100644 --- a/wai/wai-rs/src/ternary.rs +++ b/wai/wai-rs/src/ternary.rs @@ -573,7 +573,8 @@ pub fn rans_encode_trits(trits: &[i8]) -> Vec { let mut enc = crate::rans::RansEncoder::new(); for &t in trits { let v = (t.clamp(-1, 1) + 1) as i32; - debug_assert!(enc.encode(v, &cdf, 5, 0)); + let coded = enc.encode(v, &cdf, 5, 0); + debug_assert!(coded, "a trit is always in the table's span"); } let body = enc.flush(); let mut out = Vec::with_capacity(10 + body.len()); -- 2.54.0 (Apple Git-157) From ac5a0b2bf4434a9c4bd261e5c08b6f26f5c93f7f Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 1 Oct 2026 23:48:15 -0400 Subject: [PATCH 02/36] wai: WIR1, the residual refinement payload, and its predictors staged::wir1 parses, decodes and encodes WIR1: one rANS stream per unit, and for each element clamp(pred + r*q) in the element's range. The parser checks in a stated order and refuses with the first rule that fails (magic, version, header, reserved field, header fields, the unit kind's rules, caps, tables, stream lengths, streams, trailing bytes), with the codes the integer payloads use for the same faults. A unit's decode refuses the wrong shape below before any symbol, a stream at the first symbol it runs out on, and an end that is not canonical. check_streams decides whether a payload is well formed without the stage below. The header declares the layer's output, symbols and working bytes before anything is decoded. staged::predict holds the predictors: identity, up2 (the half-pel arithmetic of int_video, odd sizes included) and rate (linear interpolation rounded half away from zero, channels interleaved). staged::wir1_arith holds the one arithmetic step. Both are self-contained so the model checker and the byte-equality crate can include them. Feature `staged` = neural_int + jwp_hash. Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/Cargo.toml | 5 + wai/wai-rs/src/lib.rs | 6 + wai/wai-rs/src/staged/mod.rs | 9 + wai/wai-rs/src/staged/predict.rs | 127 +++++ wai/wai-rs/src/staged/tests.rs | 235 ++++++++++ wai/wai-rs/src/staged/wir1.rs | 698 ++++++++++++++++++++++++++++ wai/wai-rs/src/staged/wir1_arith.rs | 20 + 7 files changed, 1100 insertions(+) create mode 100644 wai/wai-rs/src/staged/mod.rs create mode 100644 wai/wai-rs/src/staged/predict.rs create mode 100644 wai/wai-rs/src/staged/tests.rs create mode 100644 wai/wai-rs/src/staged/wir1.rs create mode 100644 wai/wai-rs/src/staged/wir1_arith.rs diff --git a/wai/wai-rs/Cargo.toml b/wai/wai-rs/Cargo.toml index cebff6519..8911faebd 100644 --- a/wai/wai-rs/Cargo.toml +++ b/wai/wai-rs/Cargo.toml @@ -179,6 +179,11 @@ oer = ["dep:blake3", "dep:ed25519-dalek", "dep:num-bigint", "serde_json/float_ro # jwp-receipts §2.1's object content hash (src/jwp_hash.rs), on its own so any # receipt family that names an object can compute it with BLAKE3 alone. jwp_hash = ["dep:blake3"] +# Staged delivery (extensions/staged-delivery.md): a base first, then refinements as +# their own objects, each bound to what it refines by content hash. The residual form +# (WIR1) is integer rANS (neural_int); content hashes are jwp_hash's BLAKE3; stage +# hashes are SHA-256, always on. +staged = ["neural_int", "jwp_hash"] # Typed receipt classes on the JWP chain (extensions/jwp-receipts.md §2.4.3–§2.4.7, # §2.6, §3.1–§3.2): `claim` receipts of class energy / lawful-basis / log-inclusion, # the soft-binding class, and recovery under the ancestor rule. Needs `provenance` diff --git a/wai/wai-rs/src/lib.rs b/wai/wai-rs/src/lib.rs index 450522584..8b3bbec37 100644 --- a/wai/wai-rs/src/lib.rs +++ b/wai/wai-rs/src/lib.rs @@ -299,6 +299,12 @@ pub mod c2pa_emit; #[cfg(feature = "jwp_hash")] pub mod jwp_hash; +// Staged delivery (extensions/staged-delivery.md): a base first, then refinements as +// their own objects, bound to what they refine by content hash; the residual payload +// WIR1, its predictors, the per-unit application algorithm and the live schedule. +#[cfg(feature = "staged")] +pub mod staged; + // Typed receipt classes (extensions/jwp-receipts.md §2.4.3–§2.4.7, §2.6, §3.1–§3.2): // energy, lawful-basis, log-inclusion, soft-binding and binding-recovery statements // about delivered objects, on one `claim` envelope and one signature rule beside the diff --git a/wai/wai-rs/src/staged/mod.rs b/wai/wai-rs/src/staged/mod.rs new file mode 100644 index 000000000..e6dabf6d7 --- /dev/null +++ b/wai/wai-rs/src/staged/mod.rs @@ -0,0 +1,9 @@ +//! Staged delivery (`extensions/staged-delivery.md`): a base first, refinements +//! after, each its own object bound to what it refines by content hash. + +pub mod predict; +pub mod wir1; +pub mod wir1_arith; + +#[cfg(test)] +mod tests; diff --git a/wai/wai-rs/src/staged/predict.rs b/wai/wai-rs/src/staged/predict.rs new file mode 100644 index 000000000..2d135b87f --- /dev/null +++ b/wai/wai-rs/src/staged/predict.rs @@ -0,0 +1,127 @@ +//! The predictors of a residual refinement (`WIR1`, `extensions/staged-delivery.md` +//! Appendix A.1): what the stage below predicts for each element of the next stage. +//! +//! - **identity**: the element of the stage below at the same position; +//! - **up2**: a picture at twice the resolution, by the half-pel arithmetic of +//! `int_video` (`(a + b + 1) >> 1`, `(a + b + c + d + 2) >> 2`), odd sizes included; +//! - **rate**: PCM at another sample rate, by linear interpolation rounded half away +//! from zero (`rdiv`), channels interleaved. +//! +//! Every value is an integer of at most 64 bits, and every index is in bounds for the +//! shapes the caller checked (the Kani harnesses of `wai-verify` check `up2_at`'s +//! bounds, `rdiv` against its definition and `rate_at`'s indices). This file is +//! self-contained (no `crate::` or `super::` path, `core` only) so the model checker +//! and the cross-architecture byte-equality crate include it as it is. + +/// `n / d` rounded to the nearest integer, halves away from zero: +/// `sign(n)·⌊(2|n| + d) / 2d⌋`, for `d ≥ 1`. Formed in `i128`, so it is total for +/// every `n` and every `d ≥ 1`; `d < 1` gives 0. +pub fn rdiv(n: i64, d: i64) -> i64 { + if d < 1 { + return 0; + } + let (n, d) = (n as i128, d as i128); + let q = (2 * n.abs() + d) / (2 * d); + (if n < 0 { -q } else { q }) as i64 +} + +/// The up2 prediction of element `(y, x, ch)` of an `out_h × out_w` picture from +/// `prev`, the `h2 × w2 × c` picture below it (rows, then columns, then channels), +/// where `h2 = ⌈out_h/2⌉` and `w2 = ⌈out_w/2⌉`. A neighbour past the last row or +/// column is that row or column (`min(Y, h2 − 1)`, `min(X, w2 − 1)`). +/// +/// The caller holds `prev.len() == h2·w2·c`, `y < 2·h2`, `x < 2·w2` and `ch < c`; +/// anything else gives 0 rather than reading out of bounds. +pub fn up2_at(prev: &[u8], h2: usize, w2: usize, c: usize, y: usize, x: usize, ch: usize) -> i64 { + if h2 == 0 || w2 == 0 || c == 0 || ch >= c || y >= 2 * h2 || x >= 2 * w2 { + return 0; + } + let Some(n) = h2.checked_mul(w2).and_then(|v| v.checked_mul(c)) else { return 0 }; + if prev.len() != n { + return 0; + } + let p = |yy: usize, xx: usize| -> i64 { prev[(yy.min(h2 - 1) * w2 + xx.min(w2 - 1)) * c + ch] as i64 }; + let (iy, fy, ix, fx) = (y >> 1, y & 1, x >> 1, x & 1); + match (fy, fx) { + (0, 0) => p(iy, ix), + (0, _) => (p(iy, ix) + p(iy, ix + 1) + 1) >> 1, + (_, 0) => (p(iy, ix) + p(iy + 1, ix) + 1) >> 1, + _ => (p(iy, ix) + p(iy, ix + 1) + p(iy + 1, ix) + p(iy + 1, ix + 1) + 2) >> 2, + } +} + +/// The source indices the rate prediction of output sample `i` reads, and its +/// interpolation phase: `s = i·den`, `lo = s div num`, `Ï� = s mod num`, +/// `hi = min(lo + 1, n − 1)`, for `n` source samples per channel. `None` when +/// `num`, `den` or `n` is 0, or when `lo ≥ n` (an `i` past `⌊n·num/den⌋ − 1`). +pub fn rate_index(i: u64, n: u64, num: u64, den: u64) -> Option<(u64, u64, u64)> { + if num == 0 || den == 0 || n == 0 { + return None; + } + let s = (i as u128) * (den as u128); + let lo = s / num as u128; + let rho = (s % num as u128) as u64; + if lo >= n as u128 { + return None; + } + let lo = lo as u64; + Some((lo, (lo + 1).min(n - 1), rho)) +} + +/// The rate prediction of channel `ch` of output sample `i` from `prev`, `n` samples of +/// `c` interleaved channels at the source rate `den`, for output at the rate `num`: +/// `rdiv(a·num + (b − a)·Ï�, num)` with `a = prev[lo·c + ch]`, `b = prev[hi·c + ch]` +/// ([`rate_index`]). This is `a + (b − a)·Ï�/num` rounded half away from zero, the +/// linear interpolation at source position `i·den/num`, in exact integer arithmetic. +/// 0 where the indices are out of range (the caller checked the shapes). +pub fn rate_at(prev: &[i16], c: usize, num: u32, den: u32, i: usize, ch: usize) -> i64 { + if c == 0 || ch >= c || prev.len() % c != 0 { + return 0; + } + let n = (prev.len() / c) as u64; + let Some((lo, hi, rho)) = rate_index(i as u64, n, num as u64, den as u64) else { return 0 }; + let a = prev[lo as usize * c + ch] as i64; + let b = prev[hi as usize * c + ch] as i64; + rdiv(a * num as i64 + (b - a) * rho as i64, num as i64) +} + +/// The number of output samples per channel the rate predictor gives from `n` source +/// samples: `⌊n·num/den⌋`, or `None` for `den` 0 or a count past `u64`. +pub fn rate_len(n: u64, num: u32, den: u32) -> Option { + if den == 0 { + return None; + } + u64::try_from((n as u128) * (num as u128) / (den as u128)).ok() +} + +#[cfg(test)] +mod tests { + use super::*; + + /// `rdiv` rounds halves away from zero, on both signs. + #[test] + fn rdiv_rounds_halves_away_from_zero() { + for (n, d, want) in [(5, 2, 3), (-5, 2, -3), (4, 2, 2), (-4, 2, -2), (7, 3, 2), (-7, 3, -2), (8, 3, 3), + (-8, 3, -3), (1, 2, 1), (-1, 2, -1), (0, 5, 0), (i64::MAX, 1, i64::MAX), (i64::MIN + 1, 1, i64::MIN + 1)] { + assert_eq!(rdiv(n, d), want, "rdiv({n}, {d})"); + } + assert_eq!(rdiv(3, 0), 0); + } + + /// up2 on a 2×2 picture to 3×3 (odd), at every position: the half-pel + /// arithmetic, with the last row and column repeated. + #[test] + fn up2_matches_the_half_pel_rule_on_odd_sizes() { + let prev = [10u8, 20, 30, 41]; + let want = [[10, 15, 20], [20, 25, 31], [30, 36, 41]]; + for (y, row) in want.iter().enumerate() { + for (x, &v) in row.iter().enumerate() { + assert_eq!(up2_at(&prev, 2, 2, 1, y, x, 0), v, "({y},{x})"); + } + } + // Past the picture, a wrong buffer length, or a channel out of range: 0, no panic. + assert_eq!(up2_at(&prev, 2, 2, 1, 4, 0, 0), 0); + assert_eq!(up2_at(&prev[..3], 2, 2, 1, 0, 0, 0), 0); + assert_eq!(up2_at(&prev, 2, 2, 1, 0, 0, 1), 0); + } +} diff --git a/wai/wai-rs/src/staged/tests.rs b/wai/wai-rs/src/staged/tests.rs new file mode 100644 index 000000000..93ba9d9fb --- /dev/null +++ b/wai/wai-rs/src/staged/tests.rs @@ -0,0 +1,235 @@ +//! Tests of the staged-delivery core. + +use super::predict::{rate_at, rdiv}; +use super::wir1::{apply_unit, encode, parse, Below, Next, Predictor, RefineParams, UnitKind, Wir1Refusal}; + +/// A deterministic byte pattern: smooth gradients plus a texture, so residuals have +/// a spread and a few large outliers. +fn picture(h: usize, w: usize, seed: u32) -> Vec { + let mut v = Vec::with_capacity(h * w * 3); + let mut x = seed.wrapping_mul(2654435761) | 1; + for y in 0..h { + for xx in 0..w { + for c in 0..3 { + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + let base = (y * 255 / h.max(1) + xx * 7 + c * 40) as u32; + let noise = if x % 97 == 0 { x % 256 } else { x % 9 }; + v.push(((base + noise) % 256) as u8); + } + } + } + v +} + +fn pcm(n: usize, ch: usize, seed: i64) -> Vec { + (0..n * ch) + .map(|i| { + let t = (i / ch) as i64; + let c = (i % ch) as i64; + let v = ((t * (37 + c * 11) + seed) % 2000 - 1000) * 30 + if t % 101 == 0 { 20000 } else { 0 }; + v.clamp(-32768, 32767) as i16 + }) + .collect() +} + +fn params(q: u32, predictor: Predictor, max_symbols: u32, table_per_unit: bool) -> RefineParams { + RefineParams { q, predictor, rate: (0, 0), table_per_unit, max_symbols } +} + +fn as_u8(n: &Next) -> &[u8] { + match n { + Next::U8(v) => v, + Next::I16(_) => panic!("u8 expected"), + } +} + +fn as_i16(n: &Next) -> &[i16] { + match n { + Next::I16(v) => v, + Next::U8(_) => panic!("i16 expected"), + } +} + +/// Test 12 (`wir1::roundtrip_all_predictors`): for identity, up2 (even and odd sizes) +/// and rate (mono and stereo), on u8 and i16, with one table and with a table per +/// unit, and with tables narrow enough that most residuals take the bypass escape: +/// a sink's parse and per-unit decode give the encoder's reconstruction exactly; at +/// `q = 1` with no clamp in play the reconstruction is the target. +#[test] +fn roundtrip_all_predictors() { + // identity, RGB8, two units, q = 1 → lossless. + let below: Vec> = vec![picture(9, 7, 1), picture(9, 7, 2)]; + let target: Vec> = vec![picture(9, 7, 3), picture(9, 7, 4)]; + let b: Vec = below.iter().map(|v| Below::U8(v)).collect(); + let t: Vec = target.iter().map(|v| Below::U8(v)).collect(); + for (max_symbols, per_unit) in [(4095, false), (5, true), (1, false)] { + let (bytes, recon) = encode(UnitKind::Rgb8, &b, (9, 7, 3), &t, (9, 7, 3), ¶ms(1, Predictor::Identity, max_symbols, per_unit)).unwrap(); + let w = parse(&bytes, UnitKind::Rgb8).unwrap(); + assert_eq!(w.tables.len(), if per_unit { 2 } else { 1 }); + for u in 0..2 { + let got = apply_unit(&w, u, b[u as usize], (9, 7, 3)).unwrap(); + assert_eq!(got, recon[u as usize]); + assert_eq!(as_u8(&got), &target[u as usize][..], "q = 1 identity is lossless (max_symbols {max_symbols})"); + } + } + // up2, even (8×8 → 16×16) and odd (8×8 → 15×15, 13×13 → 25×25), q = 1 and q = 6. + for (h2, w2, h, w) in [(8usize, 8usize, 16u32, 16u32), (8, 8, 15, 15), (13, 13, 25, 25), (1, 1, 1, 2)] { + for q in [1u32, 6] { + let below = picture(h2, w2, 5); + let target = picture(h as usize, w as usize, 6); + let (bytes, recon) = encode(UnitKind::Rgb8, &[Below::U8(&below)], (h2 as u32, w2 as u32, 3), &[Below::U8(&target)], (h, w, 3), + ¶ms(q, Predictor::Up2, 7, false)).unwrap(); + let w1 = parse(&bytes, UnitKind::Rgb8).unwrap(); + let got = apply_unit(&w1, 0, Below::U8(&below), (h2 as u32, w2 as u32, 3)).unwrap(); + assert_eq!(got, recon[0]); + if q == 1 { + assert_eq!(as_u8(&got), &target[..], "q = 1 up2 is lossless ({h2}x{w2} → {h}x{w})"); + } else { + let worst = as_u8(&got).iter().zip(&target).map(|(a, b)| (*a as i32 - *b as i32).abs()).max().unwrap(); + assert!(worst <= (q as i32 + 1) / 2, "the error is at most q/2 ({worst})"); + } + } + } + // rate, i16, mono and stereo, up and down, with outliers that bypass. + for (n_src, ch, num, den) in [(100usize, 1usize, 3u32, 1u32), (64, 2, 3, 2), (90, 2, 1, 3), (5, 1, 48000, 8000)] { + let below = pcm(n_src, ch, 7); + let n_out = (n_src as u64 * num as u64 / den as u64) as u32; + let target = pcm(n_out as usize, ch, 9); + let p = RefineParams { q: 1, predictor: Predictor::Rate, rate: (num, den), table_per_unit: false, max_symbols: 3 }; + let (bytes, recon) = encode(UnitKind::Pcm, &[Below::I16(&below)], (n_src as u32, 1, ch as u32), &[Below::I16(&target)], (n_out, 1, ch as u32), &p).unwrap(); + let w = parse(&bytes, UnitKind::Pcm).unwrap(); + let got = apply_unit(&w, 0, Below::I16(&below), (n_src as u32, 1, ch as u32)).unwrap(); + assert_eq!(got, recon[0]); + assert_eq!(as_i16(&got), &target[..], "q = 1 rate is lossless ({n_src}×{ch} at {num}/{den})"); + } + // identity, i16 at q = 300: the clamp bounds the extremes. + let below = vec![i16::MIN, -1, 0, 1, i16::MAX, 12345]; + let target = vec![i16::MAX, i16::MIN, 300, -300, i16::MIN, 0]; + let p = params(300, Predictor::Identity, 2, false); + let (bytes, recon) = encode(UnitKind::Pcm, &[Below::I16(&below)], (6, 1, 1), &[Below::I16(&target)], (6, 1, 1), &p).unwrap(); + let w = parse(&bytes, UnitKind::Pcm).unwrap(); + assert_eq!(apply_unit(&w, 0, Below::I16(&below), (6, 1, 1)).unwrap(), recon[0]); + assert!(as_i16(&recon[0]).iter().zip(&target).all(|(a, b)| (*a as i32 - *b as i32).abs() <= 150)); + // splat attributes, identity. + let below = picture(5, 5, 8)[..14 * 4].to_vec(); + let target = picture(5, 5, 9)[..14 * 4].to_vec(); + let (bytes, recon) = encode(UnitKind::Attr, &[Below::U8(&below)], (14, 2, 2), &[Below::U8(&target)], (14, 2, 2), ¶ms(1, Predictor::Identity, 9, false)).unwrap(); + let w = parse(&bytes, UnitKind::Attr).unwrap(); + assert_eq!(as_u8(&apply_unit(&w, 0, Below::U8(&below), (14, 2, 2)).unwrap()), &target[..]); + assert_eq!(as_u8(&recon[0]), &target[..]); +} + +/// The rate predictor is `a + (b − a)·Ï�/num` rounded half away from zero: a ramp +/// doubles exactly, and a negative half rounds away from zero. +#[test] +fn rate_interpolates_with_rdiv() { + let prev = [0i16, 10, -3, -4]; + // 2/1: positions 0, .5, 1, 1.5, 2, 2.5, 3, 3.5 (the last two hold at the end). + let got: Vec = (0..8).map(|i| rate_at(&prev, 1, 2, 1, i, 0)).collect(); + assert_eq!(got, vec![0, 5, 10, rdiv(10 * 2 + (-13), 2), -3, rdiv(-3 * 2 - 1, 2), -4, -4]); + assert_eq!(got[3], 4, "3.5 rounds away from zero to 4"); + assert_eq!(got[5], -4, "-3.5 rounds away from zero to -4"); +} + +/// A valid one-unit RGB8 payload to mutate. +fn fixture() -> Vec { + let below = picture(2, 2, 1); + let target = picture(2, 2, 2); + encode(UnitKind::Rgb8, &[Below::U8(&below)], (2, 2, 3), &[Below::U8(&target)], (2, 2, 3), ¶ms(2, Predictor::Identity, 9, false)).unwrap().0 +} + +fn code(p: &[u8], kind: UnitKind) -> &'static str { + match parse(p, kind) { + Ok(_) => "ok", + Err(e) => e.code(), + } +} + +/// The parser refuses in the appendix's order, one fixture per code, and a fixture +/// with two faults reports the earlier one. +#[test] +fn wir1_refusal_order() { + let ok = fixture(); + assert_eq!(code(&ok, UnitKind::Rgb8), "ok"); + let set = |at: usize, v: &[u8]| { + let mut p = ok.clone(); + p[at..at + v.len()].copy_from_slice(v); + p + }; + assert_eq!(code(b"WIR", UnitKind::Rgb8), "bad_magic"); + assert_eq!(code(&set(0, b"WIV1"), UnitKind::Rgb8), "bad_magic"); + assert_eq!(code(&ok[..4], UnitKind::Rgb8), "stream_length"); + assert_eq!(code(&set(4, &[2]), UnitKind::Rgb8), "version"); + assert_eq!(code(&ok[..43], UnitKind::Rgb8), "stream_length"); + assert_eq!(code(&set(42, &[1]), UnitKind::Rgb8), "reserved_nonzero"); + assert_eq!(code(&set(5, &[3]), UnitKind::Rgb8), "field_range"); + assert_eq!(code(&set(6, &[3]), UnitKind::Rgb8), "field_range"); + assert_eq!(code(&set(7, &[2]), UnitKind::Rgb8), "field_range"); + assert_eq!(code(&set(8, &0u32.to_le_bytes()), UnitKind::Rgb8), "field_range"); + assert_eq!(code(&set(24, &1u32.to_le_bytes()), UnitKind::Rgb8), "field_range", "rate fields on identity"); + assert_eq!(code(&set(32, &0u32.to_le_bytes()), UnitKind::Rgb8), "field_range", "q = 0"); + assert_eq!(code(&set(32, &65536u32.to_le_bytes()), UnitKind::Rgb8), "field_range"); + assert_eq!(code(&set(40, &2u16.to_le_bytes()), UnitKind::Rgb8), "field_range"); + assert_eq!(code(&ok, UnitKind::Pcm), "field_range", "u8 elements for a PCM unit"); + assert_eq!(code(&set(20, &4u32.to_le_bytes()), UnitKind::Rgb8), "field_range", "RGB8 has three channels"); + assert_eq!(code(&set(12, &(1u32 << 25).to_le_bytes()), UnitKind::Rgb8), "too_large"); + assert_eq!(code(&set(8, &((1u32 << 20) + 1).to_le_bytes()), UnitKind::Rgb8), "too_large"); + assert_eq!(code(&set(44, &1u32.to_le_bytes()), UnitKind::Rgb8), "cdf_malformed"); + let cdf_len = u16::from_le_bytes([ok[40], ok[41]]) as usize; + let lens_at = 44 + 4 * cdf_len; + assert_eq!(code(&ok[..lens_at - 1], UnitKind::Rgb8), "stream_length", "a table cut short"); + assert_eq!(code(&set(lens_at, &6u32.to_le_bytes()), UnitKind::Rgb8), "stream_length"); + assert_eq!(code(&set(lens_at, &4u32.to_le_bytes()), UnitKind::Rgb8), "stream_length"); + assert_eq!(code(&ok[..ok.len() - 1], UnitKind::Rgb8), "stream_length"); + let mut trailing = ok.clone(); + trailing.push(0); + assert_eq!(code(&trailing, UnitKind::Rgb8), "trailing"); + // Order: reserved before fields, fields before caps, caps before tables. + let mut two = set(42, &[1]); + two[5] = 9; + assert_eq!(code(&two, UnitKind::Rgb8), "reserved_nonzero"); + let mut two = set(5, &[9]); + two[12..16].copy_from_slice(&(1u32 << 25).to_le_bytes()); + assert_eq!(code(&two, UnitKind::Rgb8), "field_range"); + let mut two = set(12, &(1u32 << 25).to_le_bytes()); + two[44] = 1; + assert_eq!(code(&two, UnitKind::Rgb8), "too_large"); + for r in Wir1Refusal::ALL { + assert_eq!(Wir1Refusal::from_code(r.code()), Some(r)); + } +} + +/// A stream that runs out is refused at its first missing symbol, and a word after +/// the last symbol is not canonical; both before any unit is applied, by +/// `check_streams`, and by the per-unit decode. +#[test] +fn wir1_stream_ends() { + let ok = fixture(); + let w = parse(&ok, UnitKind::Rgb8).unwrap(); + assert!(w.check_streams().is_ok()); + let cdf_len = u16::from_le_bytes([ok[40], ok[41]]) as usize; + let lens_at = 44 + 4 * cdf_len; + let len = u32::from_le_bytes(ok[lens_at..lens_at + 4].try_into().unwrap()); + // One more word after the stream: the stream length grows by 4. + let mut extra = ok.clone(); + extra[lens_at..lens_at + 4].copy_from_slice(&(len + 4).to_le_bytes()); + extra.extend_from_slice(&[0, 0, 0, 0]); + let we = parse(&extra, UnitKind::Rgb8).unwrap(); + assert_eq!(we.check_streams().unwrap_err().1.code(), "stream_noncanonical"); + let below = picture(2, 2, 1); + assert_eq!(apply_unit(&we, 0, Below::U8(&below), (2, 2, 3)).unwrap_err().code(), "stream_noncanonical"); + // A unit of four times the elements runs the stream out. + let mut big = ok.clone(); + big[12..16].copy_from_slice(&4u32.to_le_bytes()); + big[16..20].copy_from_slice(&4u32.to_le_bytes()); + let wb = parse(&big, UnitKind::Rgb8).unwrap(); + assert_eq!(wb.check_streams().unwrap_err().1.code(), "stream_exhausted"); + let below4 = picture(4, 4, 1); + assert_eq!(apply_unit(&wb, 0, Below::U8(&below4), (4, 4, 3)).unwrap_err().code(), "stream_exhausted"); + // The wrong shape below is refused before any symbol. + assert_eq!(apply_unit(&w, 0, Below::U8(&below4), (4, 4, 3)).unwrap_err().code(), "geometry_mismatch"); + assert_eq!(apply_unit(&w, 0, Below::I16(&[0; 12]), (2, 2, 3)).unwrap_err().code(), "geometry_mismatch"); + assert_eq!(apply_unit(&w, 1, Below::U8(&below), (2, 2, 3)).unwrap_err().code(), "geometry_mismatch"); +} diff --git a/wai/wai-rs/src/staged/wir1.rs b/wai/wai-rs/src/staged/wir1.rs new file mode 100644 index 000000000..c0e22deaf --- /dev/null +++ b/wai/wai-rs/src/staged/wir1.rs @@ -0,0 +1,698 @@ +//! `WIR1`, the residual refinement payload (`extensions/staged-delivery.md` §6.1, +//! Appendix A.1): a parser with a closed list of refusal codes in a stated order, the +//! cost a header declares before anything is decoded, the per-unit decode, and the +//! encoder the corpus generator and `wai stage refine` use. +//! +//! One rANS stream per unit, so units decode independently. For each element of a +//! unit, in raster order, the next stage is `clamp(pred + r·q)`: `pred` is the +//! predictor ([`super::predict`]) on the unit's current output, `r` the decoded +//! residual, `q` the step, and the clamp the element type's range +//! ([`super::wir1_arith::step`]). +//! +//! The parser and the decode follow `extensions/integer-payloads.md`'s discipline: +//! every size formed in `u64` with checked arithmetic and capped alike on every +//! target; nothing allocated beyond what the payload's bytes justify; a stream refused +//! at the first symbol during whose decode it runs out (`stream_exhausted`) and at an +//! end that is not canonical (`stream_noncanonical`). A refinement reads no parameter +//! set, so there is nothing for SPEC §3.1's check 5 to run over here; the base it +//! refines was checked when it was decoded. +//! +//! This file names only the rANS coder (`crate::rans`), the histogram table +//! (`crate::int_entropy::hist_to_cdf`) and its two siblings (`super::predict`, +//! `super::wir1_arith`), so the byte-equality crate includes it beside them. + +use super::predict::{rate_at, rate_len, rdiv, up2_at}; +use super::wir1_arith::{step, I16_RANGE, U8_RANGE}; +use crate::rans::{RansDecoder, RansEncoder}; + +/// cbindgen:ignore +pub const MAGIC: [u8; 4] = *b"WIR1"; +/// The one version this revision defines. +/// +/// cbindgen:ignore +pub const VERSION: u8 = 1; +/// The bytes before the tables. +/// +/// cbindgen:ignore +pub const HEADER: usize = 44; +/// The most elements one unit may hold (`d0·d1·d2`). +/// +/// cbindgen:ignore +pub const MAX_UNIT: u64 = 1 << 26; +/// The most units one payload may hold. +/// +/// cbindgen:ignore +pub const MAX_UNITS: u64 = 1 << 20; +/// The most elements one payload may hold (`n_units·d0·d1·d2`). +/// +/// cbindgen:ignore +pub const MAX_ELEMENTS: u64 = 1 << 30; +/// The largest step. +/// +/// cbindgen:ignore +pub const MAX_Q: u32 = 65_535; +/// The bounds on a table's length. +/// +/// cbindgen:ignore +pub const CDF_LEN_MIN: usize = 3; +/// cbindgen:ignore +pub const CDF_LEN_MAX: usize = 4097; + +/// Why a `WIR1` payload was refused: the closed list of staged-delivery Appendix A.1, +/// whose codes are those of `extensions/integer-payloads.md` §8 for the same faults. +/// The first rule that fails, in the order the appendix gives, is the reason. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +/// cbindgen:ignore +pub enum Wir1Refusal { + /// The payload does not start with `WIR1`. + BadMagic, + /// A version this revision does not define. + Version, + /// A reserved field that is not zero. + ReservedNonzero, + /// A header field outside its range, or not the one the unit's kind allows. + FieldRange, + /// A size past the caps. + TooLarge, + /// A table that breaks the table rules. + CdfMalformed, + /// The payload ends before a field or section it declares; or a stream length that + /// is not a multiple of 4, or is under 8. + StreamLength, + /// Bytes after the last stream. + Trailing, + /// A stream that ran out before its last symbol. + StreamExhausted, + /// A stream that does not end canonically after its last symbol. + StreamNoncanonical, + /// The unit's current output is not the shape the payload's predictor reads. + GeometryMismatch, +} + +impl Wir1Refusal { + /// Every refusal, in the order of the appendix. + /// + /// cbindgen:ignore + pub const ALL: [Wir1Refusal; 11] = [ + Self::BadMagic, Self::Version, Self::ReservedNonzero, Self::FieldRange, Self::TooLarge, Self::CdfMalformed, + Self::StreamLength, Self::Trailing, Self::StreamExhausted, Self::StreamNoncanonical, Self::GeometryMismatch, + ]; + + /// The code. + pub fn code(self) -> &'static str { + match self { + Self::BadMagic => "bad_magic", + Self::Version => "version", + Self::ReservedNonzero => "reserved_nonzero", + Self::FieldRange => "field_range", + Self::TooLarge => "too_large", + Self::CdfMalformed => "cdf_malformed", + Self::StreamLength => "stream_length", + Self::Trailing => "trailing", + Self::StreamExhausted => "stream_exhausted", + Self::StreamNoncanonical => "stream_noncanonical", + Self::GeometryMismatch => "geometry_mismatch", + } + } + + /// The refusal whose code is `code`. + pub fn from_code(code: &str) -> Option { + Self::ALL.into_iter().find(|r| r.code() == code) + } +} + +/// A refusal and what failed. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Wir1Error { + pub refusal: Wir1Refusal, + pub detail: &'static str, +} + +impl Wir1Error { + pub fn code(&self) -> &'static str { + self.refusal.code() + } +} + +impl std::fmt::Display for Wir1Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: {}", self.refusal.code(), self.detail) + } +} + +impl std::error::Error for Wir1Error {} + +const fn err(refusal: Wir1Refusal, detail: &'static str) -> Wir1Error { + Wir1Error { refusal, detail } +} + +/// What a unit's output is, which fixes the element type and the predictors a +/// payload may use: the unit kind of the refinement capability. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UnitKind { + /// A picture or a frame, RGB8 (`wai.image.int_refine`, `wai.video.int_refine`): + /// `d0` rows, `d1` columns, `d2 = 3`; elements u8; identity or up2. + Rgb8, + /// PCM (`wai.audio.int_refine`): `d0` samples per channel, `d1 = 1`, `d2` + /// channels interleaved; elements i16; identity or rate. + Pcm, + /// A splat attribute tensor (`wai.splat.int_refine`): `d0` planes of `d1 × d2`; + /// elements u8; identity. + Attr, +} + +/// A payload's predictor. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Predictor { + /// The element at the same position. + Identity, + /// Twice the resolution (`h' = ⌈d0/2⌉`, `w' = ⌈d1/2⌉`). + Up2, + /// Another sample rate: `rate_num` output samples per `rate_den` source samples. + Rate, +} + +impl Predictor { + fn from_byte(b: u8) -> Option { + match b { + 0 => Some(Self::Identity), + 1 => Some(Self::Up2), + 2 => Some(Self::Rate), + _ => None, + } + } + /// The header byte. + pub fn byte(self) -> u8 { + match self { + Self::Identity => 0, + Self::Up2 => 1, + Self::Rate => 2, + } + } + /// The token `wai stage` and the corpus use. + pub fn token(self) -> &'static str { + match self { + Self::Identity => "identity", + Self::Up2 => "up2", + Self::Rate => "rate", + } + } +} + +/// A parsed `WIR1` payload. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Wir1<'a> { + pub kind: UnitKind, + pub predictor: Predictor, + /// One table for every unit (`false`), or one per unit (`true`). + pub table_per_unit: bool, + pub n_units: u32, + pub d0: u32, + pub d1: u32, + pub d2: u32, + pub rate_num: u32, + pub rate_den: u32, + pub q: u32, + pub offset: i32, + /// One table, or one per unit. + pub tables: Vec>, + /// One stream per unit. + pub streams: Vec<&'a [u8]>, +} + +/// The cost a `WIR1` header declares, read before any symbol is decoded: the layer's +/// output over its units, its symbols, and the most bytes its decode holds at once +/// beyond the payload and the stage below (the parsed tables, and one unit's output +/// while it is built, since a unit is applied whole). A residual runs no network, so +/// it has no multiply-accumulates. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Wir1Cost { + pub output_bytes: u64, + pub symbols: u64, + pub working_bytes: u64, +} + +struct Cur<'a> { + b: &'a [u8], + p: usize, +} + +impl<'a> Cur<'a> { + fn take(&mut self, n: u64, what: &'static str) -> Result<&'a [u8], Wir1Error> { + let n = usize::try_from(n).ok().filter(|&n| n <= self.b.len() - self.p).ok_or(err(Wir1Refusal::StreamLength, what))?; + let s = &self.b[self.p..self.p + n]; + self.p += n; + Ok(s) + } + fn u32(&mut self, what: &'static str) -> Result { + let s = self.take(4, what)?; + Ok(u32::from_le_bytes([s[0], s[1], s[2], s[3]])) + } +} + +fn le32(b: &[u8], at: usize) -> u32 { + u32::from_le_bytes([b[at], b[at + 1], b[at + 2], b[at + 3]]) +} + +/// The table rules: `3 ≤ len ≤ 4097`, `cdf[0] = 0`, `cdf[len − 1] = 2^16`, strictly +/// increasing. +fn check_cdf(cdf: &[i32]) -> Result<(), Wir1Error> { + if cdf.len() < CDF_LEN_MIN || cdf.len() > CDF_LEN_MAX { + return Err(err(Wir1Refusal::CdfMalformed, "a table's length is outside 3..=4097")); + } + if cdf[0] != 0 || cdf[cdf.len() - 1] != 1 << 16 { + return Err(err(Wir1Refusal::CdfMalformed, "a table does not run from 0 to 65536")); + } + if cdf.windows(2).any(|w| w[0] >= w[1]) { + return Err(err(Wir1Refusal::CdfMalformed, "a table is not strictly increasing")); + } + Ok(()) +} + +/// Parse a `WIR1` payload whose units are of `kind`, checking in the appendix's order +/// and refusing with the first rule that fails: +/// +/// 1. the magic (`bad_magic`); +/// 2. the version byte present (`stream_length`) and 1 (`version`); +/// 3. the 44-byte header present (`stream_length`); +/// 4. the reserved field at 42 (`reserved_nonzero`); +/// 5. the header's fields (`field_range`), in order: `elem` (1 or 2), `predictor` +/// (0–2), `table_mode` (0 or 1), `n_units ≥ 1`, `d0`, `d1`, `d2 ≥ 1`, the rate +/// fields (both ≥ 1 for the rate predictor, both 0 otherwise), `q` in 1..=65535, +/// `cdf_len` in 3..=4097; then the unit kind's rules: `elem`, the predictors the kind +/// allows, and its fixed dimension (`d2 = 3` for RGB8, `d1 = 1` for PCM); +/// 6. the caps (`too_large`): a unit's `d0·d1·d2 ≤ 2^26`, `n_units ≤ 2^20`, the +/// payload's `n_units·d0·d1·d2 ≤ 2^30`; +/// 7. the tables, one at a time, present (`stream_length`) then checked +/// (`cdf_malformed`); the stream lengths present (`stream_length`), each a multiple +/// of 4 and at least 8 (`stream_length`); the streams present (`stream_length`); +/// 8. nothing after the last stream (`trailing`). +pub fn parse(p: &[u8], kind: UnitKind) -> Result, Wir1Error> { + use Wir1Refusal::*; + if p.get(0..4) != Some(&MAGIC[..]) { + return Err(err(BadMagic, "the payload does not start with WIR1")); + } + let version = *p.get(4).ok_or(err(StreamLength, "the version byte"))?; + if version != VERSION { + return Err(err(Version, "a version this revision does not define")); + } + if p.len() < HEADER { + return Err(err(StreamLength, "the header")); + } + if p[42] != 0 || p[43] != 0 { + return Err(err(ReservedNonzero, "the reserved field at 42")); + } + let (elem, pred_b, table_mode) = (p[5], p[6], p[7]); + let (n_units, d0, d1, d2) = (le32(p, 8), le32(p, 12), le32(p, 16), le32(p, 20)); + let (rate_num, rate_den, q) = (le32(p, 24), le32(p, 28), le32(p, 32)); + let offset = le32(p, 36) as i32; + let cdf_len = u16::from_le_bytes([p[40], p[41]]) as usize; + if elem != 1 && elem != 2 { + return Err(err(FieldRange, "elem is not 1 (u8) or 2 (i16)")); + } + let predictor = Predictor::from_byte(pred_b).ok_or(err(FieldRange, "predictor is not 0, 1 or 2"))?; + if table_mode > 1 { + return Err(err(FieldRange, "table_mode is not 0 or 1")); + } + if n_units == 0 { + return Err(err(FieldRange, "n_units is 0")); + } + if d0 == 0 || d1 == 0 || d2 == 0 { + return Err(err(FieldRange, "a dimension is 0")); + } + let rate_ok = match predictor { + Predictor::Rate => rate_num >= 1 && rate_den >= 1, + _ => rate_num == 0 && rate_den == 0, + }; + if !rate_ok { + return Err(err(FieldRange, "the rate fields are not both ≥ 1 for the rate predictor, and both 0 otherwise")); + } + if q == 0 || q > MAX_Q { + return Err(err(FieldRange, "q is outside 1..=65535")); + } + if !(CDF_LEN_MIN..=CDF_LEN_MAX).contains(&cdf_len) { + return Err(err(FieldRange, "cdf_len is outside 3..=4097")); + } + let kind_ok = match kind { + UnitKind::Rgb8 => elem == 1 && matches!(predictor, Predictor::Identity | Predictor::Up2) && d2 == 3, + UnitKind::Pcm => elem == 2 && matches!(predictor, Predictor::Identity | Predictor::Rate) && d1 == 1, + UnitKind::Attr => elem == 1 && predictor == Predictor::Identity, + }; + if !kind_ok { + return Err(err(FieldRange, "the element type, predictor or fixed dimension is not one the unit kind allows")); + } + let unit = (d0 as u64).checked_mul(d1 as u64).and_then(|v| v.checked_mul(d2 as u64)).unwrap_or(u64::MAX); + if unit > MAX_UNIT { + return Err(err(TooLarge, "a unit holds more than 2^26 elements")); + } + if n_units as u64 > MAX_UNITS { + return Err(err(TooLarge, "more than 2^20 units")); + } + if unit.saturating_mul(n_units as u64) > MAX_ELEMENTS { + return Err(err(TooLarge, "the payload holds more than 2^30 elements")); + } + let mut c = Cur { b: p, p: HEADER }; + let n_tables = if table_mode == 1 { n_units } else { 1 }; + let mut tables = Vec::new(); + for _ in 0..n_tables { + let s = c.take(cdf_len as u64 * 4, "a table")?; + let t: Vec = s + .chunks_exact(4) + .map(|w| i32::try_from(u32::from_le_bytes([w[0], w[1], w[2], w[3]])).unwrap_or(i32::MAX)) + .collect(); + check_cdf(&t)?; + tables.push(t); + } + let mut lens = Vec::new(); + for _ in 0..n_units { + let l = c.u32("a stream length")?; + if l % 4 != 0 || l < 8 { + return Err(err(StreamLength, "a stream length is not a multiple of 4, or is under 8")); + } + lens.push(l); + } + let mut streams = Vec::new(); + for l in lens { + streams.push(c.take(l as u64, "a stream")?); + } + if c.p != p.len() { + return Err(err(Trailing, "bytes follow the last stream")); + } + Ok(Wir1 { + kind, + predictor, + table_per_unit: table_mode == 1, + n_units, + d0, + d1, + d2, + rate_num, + rate_den, + q, + offset, + tables, + streams, + }) +} + +impl Wir1<'_> { + /// Elements per unit. + pub fn unit_elements(&self) -> u64 { + self.d0 as u64 * self.d1 as u64 * self.d2 as u64 + } + + /// Bytes per element of the output. + pub fn elem_bytes(&self) -> u64 { + match self.kind { + UnitKind::Pcm => 2, + _ => 1, + } + } + + /// The cost the header declares ([`Wir1Cost`]). + pub fn declared_cost(&self) -> Wir1Cost { + let unit_bytes = self.unit_elements() * self.elem_bytes(); + let tables: u64 = self.tables.iter().map(|t| 4 * t.len() as u64).sum(); + Wir1Cost { + output_bytes: unit_bytes * self.n_units as u64, + symbols: self.unit_elements() * self.n_units as u64, + working_bytes: tables + unit_bytes, + } + } + + fn table(&self, unit: usize) -> &[i32] { + if self.table_per_unit { &self.tables[unit] } else { &self.tables[0] } + } + + /// The shape the stage below a unit must have for this payload's predictor: + /// `(d0, d1, d2)` for identity, `(⌈d0/2⌉, ⌈d1/2⌉, d2)` for up2; for rate, any + /// `(n, 1, d2)` with `⌊n·rate_num/rate_den⌋ = d0`. + pub fn accepts_below(&self, below: (u32, u32, u32)) -> bool { + let (b0, b1, b2) = below; + match self.predictor { + Predictor::Identity => below == (self.d0, self.d1, self.d2), + Predictor::Up2 => below == (self.d0.div_ceil(2), self.d1.div_ceil(2), self.d2), + Predictor::Rate => { + b1 == 1 && b2 == self.d2 && b0 >= 1 && rate_len(b0 as u64, self.rate_num, self.rate_den) == Some(self.d0 as u64) + } + } + } + + /// Decode every unit's symbols, without the stage below, and check each stream + /// runs to its last symbol and ends canonically: whether the payload is well + /// formed (a payload malformed for one unit is malformed for all). Units in order; + /// the first refusal. + pub fn check_streams(&self) -> Result<(), (u32, Wir1Error)> { + let n = self.unit_elements(); + let cdf_len = self.tables[0].len(); + for (u, s) in self.streams.iter().enumerate() { + let fail = |e| (u as u32, e); + let mut dec = RansDecoder::new(s).map_err(|_| fail(err(Wir1Refusal::StreamLength, "a stream")))?; + let t = self.table(u); + for _ in 0..n { + dec.decode(t, cdf_len, self.offset); + if dec.is_exhausted() { + return Err(fail(err(Wir1Refusal::StreamExhausted, "a stream ran out before its last symbol"))); + } + } + if !dec.is_canonical_end() { + return Err(fail(err(Wir1Refusal::StreamNoncanonical, "a stream does not end canonically"))); + } + } + Ok(()) + } +} + +/// The stage below a unit, as the decode reads it. +#[derive(Debug, Clone, Copy)] +pub enum Below<'a> { + U8(&'a [u8]), + I16(&'a [i16]), +} + +/// A unit's next stage. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Next { + U8(Vec), + I16(Vec), +} + +/// The prediction of element `i` of a unit (raster order, `i = (a·d1 + b)·d2 + c`). +fn pred_at(w: &Wir1<'_>, below: Below<'_>, below_shape: (u32, u32, u32), i: u64) -> i64 { + let (d1, d2) = (w.d1 as u64, w.d2 as u64); + let (a, b, c) = (i / (d1 * d2), (i / d2) % d1, i % d2); + match (w.predictor, below) { + (Predictor::Identity, Below::U8(p)) => p[i as usize] as i64, + (Predictor::Identity, Below::I16(p)) => p[i as usize] as i64, + (Predictor::Up2, Below::U8(p)) => { + up2_at(p, below_shape.0 as usize, below_shape.1 as usize, below_shape.2 as usize, a as usize, b as usize, c as usize) + } + (Predictor::Rate, Below::I16(p)) => rate_at(p, w.d2 as usize, w.rate_num, w.rate_den, a as usize, c as usize), + _ => 0, + } +} + +/// Decode unit `unit` of `w` over `below`, the unit's current output of shape +/// `below_shape`: `clamp(pred + r·q)` for every element in raster order. Refuses +/// `geometry_mismatch` before any symbol when the shape is not one the predictor +/// reads ([`Wir1::accepts_below`]) or the buffer's type or length disagrees with it; +/// then `stream_exhausted` at the first symbol whose decode runs the stream out, and +/// `stream_noncanonical` at an end that is not canonical. Nothing is returned for a +/// refused unit, so a caller never holds a partly decoded one. +pub fn apply_unit(w: &Wir1<'_>, unit: u32, below: Below<'_>, below_shape: (u32, u32, u32)) -> Result { + use Wir1Refusal::*; + let u = unit as usize; + if u >= w.streams.len() { + return Err(err(GeometryMismatch, "the unit is outside the payload")); + } + let below_len = below_shape.0 as u64 * below_shape.1 as u64 * below_shape.2 as u64; + let type_ok = matches!((w.kind, below), (UnitKind::Pcm, Below::I16(_)) | (UnitKind::Rgb8 | UnitKind::Attr, Below::U8(_))); + let len = match below { + Below::U8(p) => p.len() as u64, + Below::I16(p) => p.len() as u64, + }; + if !type_ok || len != below_len || !w.accepts_below(below_shape) { + return Err(err(GeometryMismatch, "the unit's current output is not the shape the predictor reads")); + } + let n = w.unit_elements(); + let t = w.table(u); + let mut dec = RansDecoder::new(w.streams[u]).map_err(|_| err(StreamLength, "a stream"))?; + // Built in the element type (the clamp puts every value in its range), so a unit + // holds the bytes its declared cost counts; grown as symbols arrive, so a stream + // that runs out stops before the declared size is reached. + let mut out = match w.kind { + UnitKind::Pcm => Next::I16(Vec::new()), + _ => Next::U8(Vec::new()), + }; + for i in 0..n { + let r = dec.decode(t, t.len(), w.offset); + if dec.is_exhausted() { + return Err(err(StreamExhausted, "a stream ran out before its last symbol")); + } + let pred = pred_at(w, below, below_shape, i); + match &mut out { + Next::I16(v) => v.push(step(pred, r as i64, w.q as i64, I16_RANGE.0, I16_RANGE.1) as i16), + Next::U8(v) => v.push(step(pred, r as i64, w.q as i64, U8_RANGE.0, U8_RANGE.1) as u8), + } + } + if !dec.is_canonical_end() { + return Err(err(StreamNoncanonical, "a stream does not end canonically")); + } + Ok(out) +} + +/// What [`encode`] writes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RefineParams { + pub q: u32, + pub predictor: Predictor, + /// For the rate predictor: output samples per `rate.1` source samples. + pub rate: (u32, u32), + pub table_per_unit: bool, + /// The most real symbols a table holds (its length less 2); residuals outside the + /// table's span are coded through the bypass escape. At most 4095. + pub max_symbols: u32, +} + +/// Why [`encode`] could not write a payload. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EncodeError(pub String); + +impl std::fmt::Display for EncodeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +impl std::error::Error for EncodeError {} + +fn enc_err(s: impl Into) -> EncodeError { + EncodeError(s.into()) +} + +/// Encode a layer that brings each unit from `belows[u]` (of shape `below_shape`) +/// toward `targets[u]` (of shape `dims`), and return the payload and each unit's +/// exact reconstruction, the next stage a decoder computes. The residual is +/// `rdiv(target − pred, q)`. The table spans the residuals' range, or `max_symbols` +/// of it around the median, from [`crate::int_entropy::hist_to_cdf`]; residuals +/// outside it take the bypass escape. The payload is parsed and every unit decoded +/// as a sink does before it is returned, and each decode must equal the +/// reconstruction. +pub fn encode(kind: UnitKind, belows: &[Below<'_>], below_shape: (u32, u32, u32), targets: &[Below<'_>], dims: (u32, u32, u32), p: &RefineParams) -> Result<(Vec, Vec), EncodeError> { + let n_units = belows.len(); + if n_units == 0 || targets.len() != n_units { + return Err(enc_err("one target per unit, at least one unit")); + } + if p.max_symbols == 0 || p.max_symbols > (CDF_LEN_MAX - 2) as u32 { + return Err(enc_err("max_symbols is outside 1..=4095")); + } + let (rate_num, rate_den) = if p.predictor == Predictor::Rate { p.rate } else { (0, 0) }; + let elem: u8 = if kind == UnitKind::Pcm { 2 } else { 1 }; + // A header with a placeholder table, to plan predictions through the parser's + // own `Wir1` (one source for every predictor's geometry). + let plan = Wir1 { + kind, predictor: p.predictor, table_per_unit: p.table_per_unit, n_units: n_units as u32, + d0: dims.0, d1: dims.1, d2: dims.2, rate_num, rate_den, q: p.q, offset: 0, + tables: vec![vec![0, 1, 1 << 16]], streams: vec![&[][..]; n_units], + }; + if !plan.accepts_below(below_shape) { + return Err(enc_err("the stage below is not the shape the predictor reads")); + } + let n = plan.unit_elements(); + let (lo, hi) = if kind == UnitKind::Pcm { I16_RANGE } else { U8_RANGE }; + let target_at = |t: &Below<'_>, i: u64| -> Option { + match t { + Below::U8(v) => v.get(i as usize).map(|&x| x as i64), + Below::I16(v) => v.get(i as usize).map(|&x| x as i64), + } + }; + let mut residuals: Vec> = Vec::with_capacity(n_units); + for u in 0..n_units { + let len = match targets[u] { + Below::U8(v) => v.len() as u64, + Below::I16(v) => v.len() as u64, + }; + if len != n { + return Err(enc_err("a target is not the payload's unit shape")); + } + let rs: Vec = (0..n) + .map(|i| rdiv(target_at(&targets[u], i).unwrap_or(0) - pred_at(&plan, belows[u], below_shape, i), p.q as i64)) + .collect(); + residuals.push(rs); + } + // The table's window: every residual when they fit, else max_symbols around the median. + let mut all: Vec = residuals.iter().flatten().copied().collect(); + all.sort_unstable(); + let (min_r, max_r) = (all[0], all[all.len() - 1]); + let span = (max_r - min_r + 1) as u64; + let width = span.min(p.max_symbols as u64) as i64; + let start = if span as i64 == width { min_r } else { (all[all.len() / 2] - width / 2).clamp(min_r, max_r - width + 1) }; + let offset = i32::try_from(start).map_err(|_| enc_err("the residuals' range is outside i32"))?; + let hist = |rs: &[i64]| -> Vec { + let mut h = vec![0u64; width as usize]; + for &r in rs { + if r >= start && r < start + width { + h[(r - start) as usize] += 1; + } + } + h + }; + let tables: Vec> = if p.table_per_unit { + residuals.iter().map(|rs| crate::int_entropy::hist_to_cdf(&hist(rs), 16)).collect::>() + } else { + crate::int_entropy::hist_to_cdf(&hist(&all), 16).map(|t| vec![t]) + } + .ok_or_else(|| enc_err("no table for these residuals"))?; + let cdf_len = tables[0].len(); + let mut streams = Vec::with_capacity(n_units); + for (u, rs) in residuals.iter().enumerate() { + let t = if p.table_per_unit { &tables[u] } else { &tables[0] }; + let mut e = RansEncoder::new(); + for &r in rs { + let r = i32::try_from(r).map_err(|_| enc_err("a residual is outside i32"))?; + if !e.encode(r, t, cdf_len, offset) { + return Err(enc_err("a residual the coder cannot write")); + } + } + streams.push(e.flush()); + } + let mut out = Vec::new(); + out.extend_from_slice(&MAGIC); + out.extend_from_slice(&[VERSION, elem, p.predictor.byte(), p.table_per_unit as u8]); + for v in [n_units as u32, dims.0, dims.1, dims.2, rate_num, rate_den, p.q] { + out.extend_from_slice(&v.to_le_bytes()); + } + out.extend_from_slice(&offset.to_le_bytes()); + out.extend_from_slice(&(cdf_len as u16).to_le_bytes()); + out.extend_from_slice(&[0, 0]); + for t in &tables { + for &v in t { + out.extend_from_slice(&(v as u32).to_le_bytes()); + } + } + for s in &streams { + out.extend_from_slice(&(s.len() as u32).to_le_bytes()); + } + for s in &streams { + out.extend_from_slice(s); + } + // Parse and decode as a sink does; the decode is the reconstruction. + let w = parse(&out, kind).map_err(|e| enc_err(format!("the encoder wrote a payload the parser refuses: {e}")))?; + let mut recon = Vec::with_capacity(n_units); + for (u, b) in belows.iter().enumerate() { + let next = apply_unit(&w, u as u32, *b, below_shape).map_err(|e| enc_err(format!("unit {u}: {e}")))?; + // The reconstruction the encoder intended, element by element. + for (i, &r) in residuals[u].iter().enumerate() { + let want = step(pred_at(&w, *b, below_shape, i as u64), r, p.q as i64, lo, hi); + let got = match &next { + Next::U8(v) => v[i] as i64, + Next::I16(v) => v[i] as i64, + }; + if want != got { + return Err(enc_err(format!("unit {u}: the decode differs from the reconstruction at {i}"))); + } + } + recon.push(next); + } + Ok((out, recon)) +} diff --git a/wai/wai-rs/src/staged/wir1_arith.rs b/wai/wai-rs/src/staged/wir1_arith.rs new file mode 100644 index 000000000..9fd051ef0 --- /dev/null +++ b/wai/wai-rs/src/staged/wir1_arith.rs @@ -0,0 +1,20 @@ +//! The one arithmetic step of a residual refinement (`WIR1`, +//! `extensions/staged-delivery.md` Appendix A.1): `clamp(pred + r·q, lo, hi)`. +//! +//! Self-contained (`core` only), so the model checker (`wai-verify`, harness +//! `step_in_range`) proves it as the decoder runs it: for every `r` the rANS decoder +//! can return (`[−2^31, 2^31 − 1]`, the bypass saturates there), every step +//! `q ∈ [1, 65535]` and every prediction in the element's range, no operation +//! overflows and the result is in the range. + +/// The u8 element range. +pub const U8_RANGE: (i64, i64) = (0, 255); +/// The i16 element range. +pub const I16_RANGE: (i64, i64) = (-32768, 32767); + +/// `clamp(pred + r·q, lo, hi)`. With `|r| ≤ 2^31`, `1 ≤ q < 2^16` and `|pred| < 2^16`, +/// `|r·q| < 2^47`, so every step fits `i64`; outside those bounds the sum saturates +/// before the clamp, so the function is total for every input. +pub fn step(pred: i64, r: i64, q: i64, lo: i64, hi: i64) -> i64 { + pred.saturating_add(r.saturating_mul(q)).clamp(lo, hi.max(lo)) +} -- 2.54.0 (Apple Git-157) From 7fb55e8c172eece0aecc7be548f10ebe90b21d17 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 2 Oct 2026 00:02:12 -0400 Subject: [PATCH 03/36] wai: the refinement-only role, base admission as one table, and refines SPEC registers the residual refinement capabilities wai.image.int_refine, wai.video.int_refine, wai.audio.int_refine and wai.splat.int_refine, each refinement-only: applied in a declared order to the output of the stage below it, bound to that output's producer by content hash, and never decoded on its own. Section 4 never takes step 3 or 5 with one; section 8 adds the role and lists the envelopes its registration makes non-conforming. SPEC section 5's Base admission table holds the bases of every capability applied to a base's output, the refinements and the LCEVC enhancement layer. codecs::BASE_ADMISSION is that table; enhancement_bases, companion_applies_to and admits read it, and no other admission logic remains. The registry gains the refinement kind and role. A refinement is not dispatchable, never selected, never a companion or a companion's base, and not in the video media class; its class is StandardDefined and, like the audio scene, it composes another envelope's decoded output, so it is as exact as the base it refines. The registry export is regenerated. A WAI1 manifest gains the optional member refines, kept as JSON so a malformed value still parses; container::Refines reads it and reports refines-malformed. Wai::check_component_roles reports a refinement without refines, with a fallback, as a fallback, and refines on any other capability. The emit paths parse a refinement's WIR1 payload. extensions/staged-delivery.md defines stage sets, the refines member, the per-unit application algorithm with its closed list of reasons, holds and batches, tiers and stage hashes, the residual form, deferral, live deadlines, attestation, byte counting, and the WIR1 payload with its check order, caps, declared cost and refusal codes. Co-Authored-By: Claude Opus 5.5 --- wai/REGISTRY.md | 23 +- wai/SPEC.md | 80 ++++- wai/extensions/determinism-tiers.md | 3 + wai/extensions/staged-delivery.md | 438 ++++++++++++++++++++++++ wai/registry/capabilities.json | 56 +++ wai/registry/capabilities.schema.json | 2 + wai/vocab/v2.shapes.ttl | 23 +- wai/vocab/v2.ttl | 50 +++ wai/wai-rs/src/codecs/glossary_check.rs | 8 +- wai/wai-rs/src/codecs/mod.rs | 181 ++++++++-- wai/wai-rs/src/codecs/registry.rs | 82 +++-- wai/wai-rs/src/container.rs | 329 +++++++++++++++++- wai/wai-rs/src/staged/mod.rs | 13 + 13 files changed, 1219 insertions(+), 69 deletions(-) create mode 100644 wai/extensions/staged-delivery.md diff --git a/wai/REGISTRY.md b/wai/REGISTRY.md index 3f7f47bbd..5ff7d8222 100644 --- a/wai/REGISTRY.md +++ b/wai/REGISTRY.md @@ -27,6 +27,13 @@ Every registered string has one kind. - **companion** — a companion-only capability (SPEC §5, §7.1): its payload is applied with a base rendition and never on its own, so it is only ever a `WAI2` companion. +- **refinement** — a refinement-only capability (SPEC §5, *Refinement + capabilities*): its payload is applied to the output of the stage below it, + bound to that output's producer by content hash, and never decoded on its own. + It is a `WAI1` `capability` with `refines`, or a `WAI2` component with + `role: "refinement"` ([staged-delivery](extensions/staged-delivery.md)). The + bases each one applies over are SPEC §5's *Base admission* table, which also + holds the bases of the enhancement layer `wai.video.lcevc`. - **derivation** — an operation of ISO/IEC 23001-16 (`wai.derive.*`, [derived-tracks](extensions/derived-tracks.md)). It acts on samples another capability decoded and names no payload format. @@ -36,7 +43,8 @@ Every registered string has one kind. A derivation or record string is not a payload capability, so it is not dispatchable (SPEC §4): dispatch never takes an envelope's capability that names -one, and step 5 may still take a usable fallback. +one, and step 5 may still take a usable fallback. A refinement is not dispatchable +either; staged-delivery §4 processes it. Two further tables hold strings that look like capabilities and are not: @@ -64,7 +72,8 @@ Two further tables hold strings that look like capabilities and are not: Every dispatch and selection function uses one predicate, `codecs::is_dispatchable(cap)`, SPEC §4's *dispatchable*: false for a -companion-only capability, a candidate, a record and a derivation; true for a +companion-only capability, a refinement-only capability, a candidate, a record and +a derivation; true for a registered payload capability that is not a candidate, and for a string this revision does not register, which a later MINOR revision may register (SPEC §8) and which a sink decodes only if it advertises it. A new dispatch or selection @@ -84,7 +93,7 @@ then carries that date). ## Licence classes -Every payload and companion capability carries a licence class: +Every payload, companion and refinement capability carries a licence class: `royalty-free`, `patent-pool`, `proprietary` or `non-commercial` (`codecs::capability_license`). It is a property of the capability, not a per-file claim. The classification has no default: a capability registered @@ -100,8 +109,8 @@ gives it; a record carries none. ## How to register a string -1. Add the capability constant to `wai-rs/src/codecs/mod.rs` and, for a payload or - companion capability, list it in `known_capabilities()` and give it an arm in +1. Add the capability constant to `wai-rs/src/codecs/mod.rs` and, for a payload, + companion or refinement capability, list it in `known_capabilities()` and give it an arm in `capability_tier`, `capability_license` and `capability_replicate_class` (each function has no default, and the tests reject a registered capability without one). A record goes in `RECORDS`, a derivation in `codecs::derivation`. @@ -133,6 +142,7 @@ and a role are fixed once registered. | `wai.audio.flac` | payload | registered | audio | standalone | royalty-free | standard-defined | floor | §5 Mandatory floor | | `wai.audio.iamf` | payload | registered | audio | standalone | royalty-free | standard-tolerance | — | §5 Immersive audio, cinema, volumetric, haptics, HDR | | `wai.audio.int_codec` | payload | registered | audio | standalone | royalty-free | neural-int-exact | — | §5 Interactive, temporal & computed content | +| `wai.audio.int_refine` | refinement | registered | audio | refinement | royalty-free | standard-defined | — | §5 Refinement capabilities (staged delivery) | | `wai.audio.ivas` | payload | registered | audio | standalone | patent-pool | standard-tolerance | — | §5 Immersive audio, cinema, volumetric, haptics, HDR | | `wai.audio.mpegh` | payload | registered | audio | standalone | patent-pool | standard-unclassified | — | §5 Broadcast and streaming codecs | | `wai.audio.opus` | payload | registered | audio | standalone | royalty-free | standard-tolerance | recommended | §5 Recommended modern set | @@ -156,6 +166,7 @@ and a role are fixed once registered. | `wai.haptic.mihs` | payload | registered | haptic | standalone | patent-pool | standard-unclassified | — | §5 Immersive audio, cinema, volumetric, haptics, HDR | | `wai.haptic.signal` | payload | registered | haptic | standalone | royalty-free | standard-defined | — | §5 Interactive, temporal & computed content | | `wai.image.avif` | payload | registered | image | standalone | royalty-free | standard-tolerance | recommended | §5 Recommended modern set | +| `wai.image.int_refine` | refinement | registered | image | refinement | royalty-free | standard-defined | — | §5 Refinement capabilities (staged delivery) | | `wai.image.jp2` | payload | registered | image | standalone | royalty-free | standard-tolerance | — | §5 Immersive audio, cinema, volumetric, haptics, HDR | | `wai.image.jpeg` | payload | registered | image | standalone | royalty-free | standard-tolerance | recommended | §5 Recommended modern set | | `wai.image.jpegai` | payload | registered | image | standalone | royalty-free | neural-entropy-exact | — | §5 Neural capabilities | @@ -194,6 +205,7 @@ and a role are fixed once registered. | `wai.semantic.perco2` | payload | registered | image | standalone | non-commercial | behavioral | — | §5 Neural capabilities | | `wai.splat.4d` | payload | registered | volumetric | standalone | royalty-free | standard-defined | — | §5 Interactive, temporal & computed content | | `wai.splat.int_codec` | payload | registered | volumetric | standalone | royalty-free | neural-int-exact | — | §5 Interactive, temporal & computed content | +| `wai.splat.int_refine` | refinement | registered | volumetric | refinement | royalty-free | standard-defined | — | §5 Refinement capabilities (staged delivery) | | `wai.splat.sog` | payload | candidate | volumetric | standalone | patent-pool | standard-unclassified | — | §5 Immersive audio, cinema, volumetric, haptics, HDR | | `wai.splat.spz` | payload | registered | volumetric | standalone | royalty-free | standard-unclassified | — | §5 Interactive, temporal & computed content | | `wai.tensor.nnc` | payload | candidate | binary | standalone | patent-pool | standard-unclassified | — | §5 Prior artifacts, general tensors and derivation operations | @@ -211,6 +223,7 @@ and a role are fixed once registered. | `wai.video.hevc` | payload | registered | video | standalone | patent-pool | standard-defined | — | §5 Broadcast and streaming codecs | | `wai.video.int_hyper` | payload | registered | video | standalone | royalty-free | neural-int-exact | — | §5 Neural capabilities | | `wai.video.int_motion` | payload | registered | video | standalone | royalty-free | standard-defined | — | §5 Neural capabilities | +| `wai.video.int_refine` | refinement | registered | video | refinement | royalty-free | standard-defined | — | §5 Refinement capabilities (staged delivery) | | `wai.video.j2k` | payload | registered | video | standalone | royalty-free | standard-tolerance | — | §5 Immersive audio, cinema, volumetric, haptics, HDR | | `wai.video.keys` | record | registered | — | — | — | — | — | §5 Neural capabilities | | `wai.video.lcevc` | companion | registered | video | enhancement | patent-pool | standard-unclassified | — | §5 Broadcast and streaming codecs | diff --git a/wai/SPEC.md b/wai/SPEC.md index 582086957..2ffc0f0db 100644 --- a/wai/SPEC.md +++ b/wai/SPEC.md @@ -90,6 +90,7 @@ All WAI multi-byte integers are little-endian unless stated otherwise. "fallback": "" | null }, "conditioning": { "kind": "", "prior": { ... } }, "security": { "encryption": "" | "none", "kid": }, + "refines": { ... }, "target": { ... } } ``` @@ -143,6 +144,10 @@ All WAI multi-byte integers are little-endian unless stated otherwise. payload is plaintext (the default). Otherwise the payload is encrypted and MUST be recovered before dispatch; see §6. The capability, media, and conditioning fields always describe the **plaintext** codec. +- `refines` — OPTIONAL. Carried exactly when `model_requirement.capability` is + refinement-only (§5, *Refinement capabilities*): what the payload refines, by + content hash ([staged-delivery](extensions/staged-delivery.md) §3). A manifest + that carries it has a `null` `fallback`. - `target` — codec-specific reconstruction parameters (dimensions, sample rate, duration, frame count). Informational; the payload is self-describing per the codec's own format. @@ -390,8 +395,9 @@ pinned rendition per set in a `WAI2` envelope (§7.1). A capability is **dispatchable** unless this revision registers it as something a sink does not decode on its own: a candidate (§5, *Candidate capabilities*), a -companion-only capability (§5, *Companion-only capabilities*), a record or a -derivation (§5, *Kinds of registered string*). A sink MUST NOT take step 3 or +companion-only capability (§5, *Companion-only capabilities*), a refinement-only +capability (§5, *Refinement capabilities*), a record or a derivation (§5, *Kinds +of registered string*). A sink MUST NOT take step 3 or step 5 with a capability that is not dispatchable, whatever it advertises. A string this revision does not register is dispatchable: a later MINOR revision may register it (§8), and a sink that has a codec for it says so by advertising @@ -401,6 +407,13 @@ payload is companion data, so step 5 is not taken either (§5). A WAI **payload always corresponds to exactly one capability**. v1.0 does not support multi-codec multiplexed payloads. +A sink never takes step 3 or step 5 with a capability §5 registers as +**refinement-only**, and a `WAI1` whose `cap` is refinement-only takes neither: +its payload refines the output of a base, and +[staged-delivery](extensions/staged-delivery.md) §4 processes it. A sink that does +not implement that extension finds the capability unsupported, and the envelope is +inert. + If the manifest declares `security.encryption` other than `"none"` (§6), the sink MUST recover the plaintext payload before step 3 and run this algorithm on the plaintext; see §6.4. @@ -475,6 +488,9 @@ kind: rendition names the codec its payload is decoded with; - a **companion** capability: companion-only (*Companion-only capabilities*, below), carried only as a `WAI2` companion; +- a **refinement** capability: refinement-only (*Refinement capabilities*, + below), applied to the output of the stage below it and never decoded on its + own; - a **derivation** operation (`wai.derive.*`): an operation on samples another capability decoded, which names no payload format; - a **record**: a signed record a WAI tool emits and verifies (a receipt kind — @@ -482,8 +498,8 @@ kind: delivery records `wai.video.manifest`, `wai.video.ssai`, `wai.video.keys`, `wai.video.package` and `wai.video.channel`), which names no payload format. -Only payload and companion capabilities have a media class, a fidelity tier, a -replicate class and a role. A derivation or record string is not a payload +Only payload, companion and refinement capabilities have a media class, a +fidelity tier, a replicate class and a role. A derivation or record string is not a payload capability and is not dispatchable (§4): step 3 does not take an envelope's `capability` that names one, and step 5 may still take a usable fallback. A row of the tables below that registers a record says what the record attests, not a payload format. Named @@ -801,6 +817,45 @@ capability `codecs::is_dispatchable` accepts (§4's dispatchable). Its `wai wrap envelope packers (`wai_envelope_pack`, `wai_envelope_pack_multi`) and its browser writers (`pack`, `packMulti`) refuse to write one. +### Refinement capabilities (staged delivery) + +| capability | staged form | payload | registry class | +|---|---|---|---| +| `wai.image.int_refine` | residual | `WIR1` ([staged-delivery](extensions/staged-delivery.md) Appendix A.1), units RGB8 | StandardDefined | +| `wai.video.int_refine` | residual | `WIR1`, units RGB8 frames | StandardDefined | +| `wai.audio.int_refine` | residual | `WIR1`, units `i16` PCM | StandardDefined | +| `wai.splat.int_refine` | residual | `WIR1`, units attribute tensors | StandardDefined | + +Each is **refinement-only** (§8): its payload is applied, in a declared order, to +the output of the stage below it, bound to that output's producer by content hash, +and never decoded on its own. It is carried only as a `WAI1` with `refines` (§3), +or as a `WAI2` component with `role: "refinement"` (§7.1). It is never a +`capability` or `fallback` that §4 dispatches, a selectable rendition or a +companion. Each is royalty-free and WAI-native, its payload format is pinned by +this registration, it reads no parameter set, and its tier is `Lossy`: its residual +is quantised. Its next stage is exact given the stage below, so its class is +StandardDefined and its output is as exact as the base it refines +([staged-delivery](extensions/staged-delivery.md) §5). + +### Base admission + +A capability applied to the output of a base — a refinement-only capability, or an +enhancement layer — applies only over the bases listed for it here. A later +revision MAY add a base, which widens the capability (§8). + +| capability | admitted bases | +|---|---| +| `wai.image.int_refine` | `wai.neural.int_hyper`, `wai.neural.int_mlicpp`, `wai.neural.int_synth`, `wai.image.png` | +| `wai.video.int_refine` | `wai.video.int_motion`, `wai.video.int_hyper` | +| `wai.audio.int_refine` | `wai.audio.int_codec`, `wai.audio.flac` | +| `wai.splat.int_refine` | `wai.splat.int_codec` | +| `wai.video.lcevc` | `wai.video.avc`, `wai.video.hevc`, `wai.video.vvc`, `wai.video.evc`, `wai.video.evc.baseline`, `wai.video.vp9`, `wai.video.av1`, `wai.video.av1.lossless` (*Enhancement layers (LCEVC)*, below) | + +A refinement is admitted over a base when its `refines.base` names an envelope that +a sink decoded through one of these capabilities. The reference implementation +holds this table once (`codecs::BASE_ADMISSION`), and every admission check reads +it. + ### Broadcast and streaming codecs (registered, sink-supplied) The elementary video and audio bitstreams that terrestrial and satellite @@ -1864,6 +1919,18 @@ envelope that uses any string a later revision registers. envelope stops conforming, and a reader of an earlier revision that reports the new binding as not conforming does not apply that companion and can still present its base. +- This revision adds a third role, **refinement-only**: a payload applied, in a + declared order, to the output of the stage below it, bound to that output's + producer by content hash ([staged-delivery](extensions/staged-delivery.md)), and + never decoded on its own. It differs from companion-only, which is unordered, + names its base by capability and travels only inside a `WAI2`: a refinement is + ordered, names its base by content and travels as its own object. Changing a + capability into or out of either role redefines it; admitting another base (§5, + *Base admission*) widens it. The manifest member `refines` (§3) is an OPTIONAL + member defined under the rule above for MINOR revisions: an envelope that does + not carry it keeps its bytes and its meaning. A reader of an earlier revision + does not advertise a refinement-only capability, so it finds such an envelope's + capability unsupported and the envelope inert. - A registration of a capability that decodes against a parameter set also fixes that set's shape (§5, §3.1). Changing one file to several narrows it and requires a new MAJOR; several to one widens it, which a MINOR revision MAY do. @@ -1897,7 +1964,10 @@ envelope that uses any string a later revision registers. a `WAI1` `capability` or `fallback` or as a selectable rendition, that bound it to a base or a number of bases its registration does not admit, or that bound another companion to it (no companion applies to a companion-only - capability). Such envelopes still parse. + capability); registering a refinement-only capability makes non-conforming an + envelope that carried it as a `WAI1` `capability` without `refines`, as a + `fallback`, or as a selectable rendition or a companion, and one that bound a + companion to it. Such envelopes still parse. --- diff --git a/wai/extensions/determinism-tiers.md b/wai/extensions/determinism-tiers.md index 66616e558..9297817b4 100644 --- a/wai/extensions/determinism-tiers.md +++ b/wai/extensions/determinism-tiers.md @@ -329,6 +329,9 @@ thing. binding over `wai.image.jpegai` as a binding over its exact decoder's pinned reconstruction; that binding is checkable only by that decoder under the conditions in §5–§6. +- **Staged delivery.** A unit refined in stages holds the weakest tier of its base + and every layer applied to it, and `entropy-consistency` does not pass through a + layer ([staged-delivery](staged-delivery.md) §5). ## 4. Framing diff --git a/wai/extensions/staged-delivery.md b/wai/extensions/staged-delivery.md new file mode 100644 index 000000000..41cc4a3e3 --- /dev/null +++ b/wai/extensions/staged-delivery.md @@ -0,0 +1,438 @@ +# WAI Extension: Staged Delivery + +> Status: Draft. A base first, refinements after, each its own object, bound to what +> it refines by content hash. Every stage is presentable, every prefix of a stage set +> states its determinism tier, and a refinement that never arrives leaves the stages +> below it intact. Reference impl: `wai-rs` — `staged::{StageChain, StageSink}`, +> `staged::{wir1, predict, wir1_arith, tier, schedule}`, `container::Refines`, +> `codecs::BASE_ADMISSION`; the `wai stage` tool; the corpus +> [`staged-conformance/`](../staged-conformance/). Keywords **MUST**, **MUST NOT**, +> **SHOULD**, **MAY** are RFC 2119/8174. + +## 1. Scope and model + +A **stage set** is a **base** and **layers**. The base is any envelope a sink decodes +under SPEC §4 or §7.1. A layer is its own `WAI1` envelope whose capability is +**refinement-only** (SPEC §5, *Refinement capabilities*) and which names, by content +hash, the base and the envelopes it refines. + +A sink holds, for every **unit** of the base's output, a **stage** (0 after the base +decodes) and a **producer**: the content hash of the envelope that produced the +unit's current output, the base at stage 0. Layers apply per unit, and every stage is +presentable. A base never names its layers, so a layer MAY be produced after its +base, by the base's publisher or by a party the sink accepts (§10). + +## 2. Terms + +- **Content hash**: [jwp-receipts](jwp-receipts.md) §2.1, + `BLAKE3("moq-jwp:object\x01" ‖ envelope_bytes)` over the envelope bytes exactly as + received, written as 64 lowercase hexadecimal digits. +- **Unit**: the smallest presentable piece of a base's output, fixed by the base's + capability: one frame of video; the whole picture of an image; the whole clip of + audio; the whole attribute tensor of a splat cloud. +- **Span**: a contiguous run of a base's units. +- **Producer**: for a unit, the content hash of the envelope that produced its current + output. +- **Refinement-only capability**: a capability registered with that role (SPEC §5, + §8). + +## 3. The `refines` member + +A `WAI1` manifest whose `model_requirement.capability` is refinement-only **MUST** +carry `refines`, and its `model_requirement.fallback` **MUST** be `null`. A manifest +whose capability is not refinement-only **MUST NOT** carry it. + +```json +"refines": { "base": "<64 hex>", + "prev": "<64 hex>" | [ {"producer": "<64 hex>", "count": 4}, ... ], + "layer": 2, + "span": { "from": 0, "count": 4 } } +``` + +| member | presence | meaning | +|---|---|---| +| `base` | REQUIRED | Content hash of the stage set's base. | +| `prev` | REQUIRED | The producer each unit the layer covers is expected to have. A string means every such unit. An array of runs gives, in unit order, `count` consecutive units whose producer is `producer`. | +| `layer` | REQUIRED | Integer 1–255: the stage this layer brings each unit to. | +| `span` | OPTIONAL | `from` (integer ≥ 0) and `count` (integer ≥ 1): the units the layer covers. Absent: every unit of the base. | + +Rules: + +- Hashes **MUST** be 64 lowercase hexadecimal digits (in a `WAI2` entry they are + component ids; SPEC §7.1, *Refinement components*). +- When `layer` is 1, every producer `prev` names **MUST** be `base`. +- A run array **MUST NOT** contain two adjacent runs with the same producer. A layer + whose units have one producer **MUST** write `prev` as a string, so an array of one + run is malformed. +- With a `span`, the run counts **MUST** sum to its `count`. The span **MUST** lie + within the base's units, and with no span the run counts **MUST** sum to the base's + unit count; a sink checks these once it holds the base (§4.1 step 2). +- `from + count` **MUST** fit in 32 bits, and so **MUST** each `count`. + +Any other JSON type, or a value outside these rules, makes `refines` malformed +(`refines-malformed`); the envelope still parses. A member of `refines` this +revision does not define is ignored (SPEC §8). + +## 4. Applying layers + +### 4.1 Algorithm + +A sink that implements this extension **MUST** process each received envelope *R* +whose capability is refinement-only as follows. Steps 1–6 decide for the whole +envelope; steps 7–10 decide for each unit *u* the layer covers. The first step that +fails is the reason for the envelope (1–6) or for the unit (7–10). + +1. `refines` is present and well formed (§3) (`refines-malformed`). *R*'s capability + is refinement-only (`capability-not-refinement`). +2. The sink has decoded a base whose content hash is `refines.base`. Otherwise it + **MAY** hold *R* until its bound (§4.3), and then discards it (`base-absent`). + With the base, `refines` is checked against the base's unit count (§3) + (`refines-malformed`). +3. The base was decoded through its manifest's `model_requirement.capability`, or + through the `WAI2` component whose exploded envelope (SPEC §7.1) has the content + hash `refines.base`, and not through a fallback (`base-via-fallback`). +4. The capability the base was decoded through is admitted by *R*'s capability (SPEC + §5, *Base admission*) (`base-not-admitted`). +5. Where the sink verified a delivery receipt for the base, it has verified one for + *R* as §10 requires (`refinement-unattested`). +6. The payload is well formed for *R*'s capability, in this order: it parses + (Appendix A) (`payload-malformed`); its unit count is the number of units the + layer covers (`payload-malformed`); its declared cost (Appendix A.5) is within the + limit the sink applies, if it applies one (`cost-over-limit`); and every unit's + stream decodes to its last symbol and ends canonically (`payload-malformed`). A + payload malformed for one unit is malformed for all. +7. Let *P(u)* be the producer `prev` gives for *u*. *u* is **ready** when its stage is + `layer − 1` and its producer is *P(u)*. A unit whose stage is below `layer − 1` + keeps *R* held for it, and is evaluated again when its stage advances; at the + bound *R* is discarded for that unit (`layer-gap`). Any other unit is not ready + (`prev-mismatch`). +8. *u* is **conflicted** when it is frozen, or when an envelope with different + content, the same `base` and the same `layer`, whose producer for *u* is also + *P(u)*, has passed step 7 for *u* (earlier, or in the same batch, §4.4) + (`layer-conflict`). A conflicted unit is **frozen**: the sink **MUST** apply no + further layer to it, it keeps the output it had (a layer applied to it before the + conflict stays applied), and the sink **MUST** report the conflict. +9. *R*'s payload reads *u*'s current output in the shape its staged form needs + (`geometry-mismatch`). +10. Under a live schedule, *R* arrived at least `lead_ms` before *u*'s presentation + time (§8) (`deadline-missed`). + +The sink then computes, for every unit that passed, the next stage by *R*'s staged +form (§6), and sets the unit's stage to `layer` and its producer to *R*'s content +hash. Application to one unit is atomic. A sink **MUST NOT** present a unit's output +from a layer that failed for that unit, and **MUST NOT** present a partly applied +unit. + +### 4.2 Reasons + +| step | reason | scope | effect | +|---|---|---|---| +| 1 | `refines-malformed`, `capability-not-refinement` | envelope | discarded | +| 2 | `base-absent` | envelope | held, then discarded | +| 2 | `refines-malformed` | envelope | discarded | +| 3 | `base-via-fallback` | envelope | discarded | +| 4 | `base-not-admitted` | envelope | discarded | +| 5 | `refinement-unattested` | envelope | discarded | +| 6 | `payload-malformed`, `cost-over-limit` | envelope | discarded | +| 7 | `prev-mismatch` | unit | not applied to the unit | +| 7 | `layer-gap` | unit | held for the unit, then not applied | +| 8 | `layer-conflict` | unit | not applied; the unit is frozen | +| 9 | `geometry-mismatch` | unit | not applied to the unit | +| 10 | `deadline-missed` | unit | not applied to the unit | + +The list is closed. Two conforming sinks that receive the same envelopes in the same +order, at the same times, with the same receipts, hold bound and cost limit, report +the same reasons per envelope and per unit and reach the same stages and producers. +Order changes the outcome only through steps 8 and 10. + +### 4.3 Holding + +A sink **MUST** declare the bound to which it holds envelopes it cannot yet apply, as +a count of envelopes and a duration from arrival. An envelope held past the duration +is discarded with its hold reason (`base-absent`, or `layer-gap` for each unit it is +held for). An envelope that would exceed the count is discarded on arrival with the +reason it would have been held for. A unit refined by a held envelope reaches the same +output as one refined in order. + +### 4.4 Batches + +When the base an envelope names is decoded, the envelopes held for it are released +together; when a unit's stage advances, the envelopes held for that unit at the next +layer are released together. Each envelope released, or offered on arrival, passes +steps 1–7 alone. The (envelope, unit) pairs that are ready then form one **batch**: +step 8 compares every pair of the batch with the others and with the envelopes that +passed step 7 earlier, and only then do steps 9 and 10 and application run. So two +conflicting layers released together are both refused and the unit frozen, whichever +arrived first; a layer that conflicts with one already applied is refused, and the +unit frozen at the stage the applied layer gave it. Applying a batch can release +another, which is evaluated the same way. + +## 5. The tier of a stage, and the stage hash + +- **The base step**: `decode-equivalence` when the capability the base was decoded + through has the registry class `StandardDefined`, or `NeuralIntegerExact` with a pin + that resolved (SPEC §3.1) and declares `decode-equivalence`; + `entropy-consistency` when the class is `NeuralEntropyExact`; otherwise none. +- **A layer step**: the tier its registration gives (§6); `decode-equivalence` for the + residual form. + +A unit's tier is the weakest step of its chain, composed as +[derived-tracks](derived-tracks.md) §3 rule 1. A layer acts on reconstructed output, +so `entropy-consistency` does not pass through it (derived-tracks §3 rule 2): a unit +over such a base holds no tier once a layer applies. + +The **stage hash** of a unit is the SHA-256 of its output in the canonical buffer of +SPEC §7 *Digests*: RGB8 for a picture or a frame; the samples as signed 16-bit +little-endian integers, channels interleaved, for audio; the attribute tensor's bytes, +plane after plane, for a splat. The stage hash of several units is the SHA-256 of +their 32-byte stage hashes concatenated in unit order, as that section defines a clip +digest. A stage hash **MUST NOT** be published, compared as a conformance value, or +sealed in a receipt unless every unit it covers holds `decode-equivalence`. + +## 6. Staged forms + +A refinement-only capability's registration fixes its staged form, payload format, +admitted bases (SPEC §5, *Base admission*), tier and truncation points. + +### 6.1 Residual (`WIR1`) + +`wai.image.int_refine`, `wai.video.int_refine`, `wai.audio.int_refine` and +`wai.splat.int_refine` carry a `WIR1` payload (Appendix A): one rANS stream per +unit. For each element of a unit, the next stage is `clamp(pred + r · q)`. `pred` is +the payload's predictor applied to the unit's current output; `r` is the decoded +residual; `q` is the payload's step; and the clamp is to the element type's range. +Every operation is on integers of at most 64 bits. + +| capability | unit | element | predictors | +|---|---|---|---| +| `wai.image.int_refine` | the picture, RGB8 | u8 | identity, up2 | +| `wai.video.int_refine` | one frame, RGB8 | u8 | identity, up2 | +| `wai.audio.int_refine` | the clip, `i16` PCM, channels interleaved | i16 | identity, rate | +| `wai.splat.int_refine` | the attribute tensor | u8 | identity | + +A residual layer reads no parameter set: everything its decode reads is in its +payload and in the unit's current output. The base's parameter set was checked when +the base was decoded (SPEC §3.1, check 5 among them), and nothing a layer reads needs +the check again. Tier: `decode-equivalence`. Every layer boundary is a truncation +point. + +### 6.2 Other staged forms + +This revision registers the residual form only. A refinement-only capability of +another staged form (the continuation of a world's state, an enhancement layer +carried as its own object, a byte prefix of a progressive codestream, the further +codebooks of a residual-vector-quantised code) is registered by a revision that fixes +its form, payload, admitted bases, tier and truncation points. Where the form decodes +a prefix of a base codec's own codestream, that revision cites the clause of the +codec's standard that defines the decode of such a prefix, and its reference sink +decodes one. + +### 6.3 Truncation points + +A refinement capability declares truncation points only where its staged form defines +the decode of a prefix. For the residual form that is every layer boundary. + +## 7. Deferral + +A layer **MAY** be published after its base, served only on request, or never. A sink +**MUST** present the current stage of a unit without waiting for a layer it has not +been told to expect. A later revision of a refinement is a new chain from the same +`base`, distinguished by `prev`. When two chains conflict, §4.1 step 8 applies. + +## 8. Live deadlines + +Under a live schedule, every unit has a presentation time, given by the base's timing +and the target latency the sink applies, and every layer has a lead, `lead_ms`: the +time before a unit's presentation by which the layer must have arrived, and been +verified under §10, to be applied to it. A sink **MUST NOT** delay presenting a unit's +current stage to wait for any layer. A layer that arrived less than `lead_ms` before a +unit's presentation time is not applied to that unit (`deadline-missed`). It **MAY** +be applied to units of its span that are not yet due. A still image **MAY** be +refined after it is presented. A sink **MUST** count every unit presented below the +stage its expected layers would have reached on time. Verifying a layer's receipt +(§10) adds the time to receive and verify that receipt to the layer's arrival, which +sets the least `lead_ms` that can be met. + +## 9. Carriage + +A layer is a `WAI1` envelope, carried and stored as any envelope is, or a `WAI2` +component with `role: "refinement"` (SPEC §7.1, *Refinement components*). A component +and its exploded envelope (SPEC §7.1) are the same object for every rule that names +an object by content hash, so a layer reaches the same stages whichever way it +travelled. This revision binds no other carriage. + +## 10. Attestation of layers + +Each layer is an object under [jwp-receipts](jwp-receipts.md). A sink that verified a +delivery receipt for a base **MUST NOT** apply a layer unless it has verified a +delivery receipt for the layer, through that extension's §3 step 6, over the layer's +envelope bytes as received. That receipt must be signed by the base's signer, or by a +key the sink holds with the authority `delivery` under the same origin +(jwp-receipts §7.5). A receipt verified without the layer's envelope bytes (§3 steps 1, +2, 3 and 5, step 4 not run) shows that a publisher signed an object with that content +hash; it never authorizes an application. A content hash in `refines` binds a layer +to a base, not to its publisher. + +## 11. Counting bytes + +A figure of delivered bytes for a stage set: + +- **MUST** count the bytes of the `WAI1` and `WAI2` envelopes as delivered (SPEC §2: + magic, length prefixes, manifest and payload); +- **MUST** state separately the receipt, index and transport framing bytes, and the + parameter-set bytes a sink fetched; +- **MUST NOT** count a text re-encoding of a payload (JSON, base64) in place of the + payload's registered binary format. + +A figure over a payload in any other form is not a staged-delivery byte figure. + +## 12. Conformance + +A conforming sink of this extension implements §4, §5, §8 and §10, and every staged +form whose capabilities it advertises. The corpus +[`staged-conformance/`](../staged-conformance/) holds the cases: for each, a +conforming sink reports the stated reason per envelope and unit, and reaches the +stated stage, producer, tier and stage hash per unit. Its `README.md` states the +cases, what each base's stage 0 is checked against, and the independent verifier +`staged_verify.py`. + +## 13. Earlier layered carriage + +A prototype MoQ binding in this repository carries a layer index in an object +header (`mrl_layer`, with `mrl_total_layers` and a subscriber's `max_mrl_layer`). For +an object that is a `WAI1` envelope with `refines`, a sink reads the layer from +`refines.layer`, which sits inside the envelope and so inside its content hash, and +ignores the header. + +## 14. Security considerations + +- A layer is not authenticated by its `refines`; §10 authenticates it. +- Deferral lets anyone mint a chain for a public base hash. §10 and §4.1 step 8 bound + what a sink presents. A conflict between two attested chains is evidence that an + authorized signer signed both, and the sink reports it. +- A hold bound (§4.3) and a cost limit (Appendix A.5) are resource limits. A layer's + header declares its output and its symbols before any symbol is decoded, so a sink + refuses a layer past its limit without decoding it. +- An encrypted layer ([confidentiality](confidentiality.md)) binds to the content hash + of its base's envelope as delivered, ciphertext included, and is decrypted before + §4.1 step 6. +- A frozen unit stays at its stage: a party that can sign conflicting layers can stop + a unit's refinement, and cannot change what it presents. + +## Appendix A. `WIR1` + +### A.1 Layout + +Every multi-byte field is little-endian; offsets are in bytes. + +| off | size | field | rule | +|---|---|---|---| +| 0 | 4 | magic `WIR1` | | +| 4 | 1 | version | `1` | +| 5 | 1 | `elem` | `1` = u8, `2` = i16 | +| 6 | 1 | `predictor` | `0` identity, `1` up2, `2` rate | +| 7 | 1 | `table_mode` | `0` one table for every unit, `1` one per unit | +| 8 | 4 | `n_units` | ≥ 1 | +| 12 | 4 | `d0` | ≥ 1: rows (picture), samples per channel (audio), planes (splat) | +| 16 | 4 | `d1` | ≥ 1: columns, `1`, the planes' width | +| 20 | 4 | `d2` | ≥ 1: channels (`3` for RGB8), channels, the planes' height | +| 24 | 4 | `rate_num` | predictor 2: the output rate, ≥ 1; otherwise `0` | +| 28 | 4 | `rate_den` | predictor 2: the rate of the stage below, ≥ 1; otherwise `0` | +| 32 | 4 | `q` | 1..=65535 | +| 36 | 4 | `offset` | `i32`: the symbol offset of every table | +| 40 | 2 | `cdf_len` | 3..=4097 | +| 42 | 2 | reserved | `0` | +| 44 | T·`cdf_len`·4 | the tables | T = 1 or `n_units`; each a table of [integer-payloads](integer-payloads.md) §2 | +| … | `n_units`·4 | stream lengths | `u32` each, a multiple of 4, ≥ 8 | +| … | Σ | the streams | one per unit, in unit order; nothing follows | + +Each stream is decoded as [integer-payloads](integer-payloads.md) §2 decodes a rANS +stream (precision 16, the bypass escape of 4-bit groups), against the unit's table at +`offset`. + +### A.2 The unit kind + +A capability fixes what its units hold (§6.1), and so the fields a payload may carry: + +- RGB8 (`wai.image.int_refine`, `wai.video.int_refine`): `elem` 1, `predictor` 0 or + 1, `d2` = 3; +- PCM (`wai.audio.int_refine`): `elem` 2, `predictor` 0 or 2, `d1` = 1; +- attribute tensor (`wai.splat.int_refine`): `elem` 1, `predictor` 0. + +### A.3 Order of the checks + +A parser checks, in this order, and refuses with the first that fails: + +1. the magic (`bad_magic`); +2. the version byte: present (`stream_length`), and 1 (`version`); +3. the 44-byte header: present (`stream_length`); +4. the reserved field (`reserved_nonzero`); +5. the header's fields, in the order `elem`, `predictor`, `table_mode`, `n_units`, + `d0`/`d1`/`d2`, the rate fields, `q`, `cdf_len`; then the unit kind's rules + (A.2) (`field_range`); +6. the caps, in this order (`too_large`): a unit's `d0·d1·d2 ≤ 2^26`; + `n_units ≤ 2^20`; `n_units·d0·d1·d2 ≤ 2^30`; +7. the tables, one at a time, each present (`stream_length`) then checked + (`cdf_malformed`); the stream lengths, present (`stream_length`), each a multiple + of 4 and at least 8 (`stream_length`); the streams, present (`stream_length`); +8. nothing after the last stream (`trailing`). + +Every size is formed in 64-bit arithmetic with overflow checked, and a size past 64 +bits is past the caps, so a 32-bit sink refuses exactly what a 64-bit one does. A +parser reads tables and lengths from the bytes the payload holds and allocates +nothing a payload's bytes do not justify. + +### A.4 Decode + +For each unit *u* the layer is applied to, with the unit's current output of shape +`(e0, e1, e2)`: + +1. The shape must be the one the predictor reads (`geometry_mismatch`), before any + symbol is decoded: `(d0, d1, d2)` for identity; `(⌈d0/2⌉, ⌈d1/2⌉, d2)` for up2; + `(n′, 1, d2)` with `⌊n′·rate_num/rate_den⌋ = d0` for rate. +2. For element *i* in raster order (`i = (a·d1 + b)·d2 + c`), decode the residual `r` + from *u*'s stream. A stream that runs out during a symbol's decode is refused at + that symbol (`stream_exhausted`), and nothing after it is decoded. The element is + `clamp(pred(i) + r·q)`, to `[0, 255]` for u8 and `[−32768, 32767]` for i16. `r` lies + in `[−2^31, 2^31 − 1]` and `|r·q| < 2^47`, so every step fits `i64`. +3. After the unit's last element, the stream ends canonically + ([integer-payloads](integer-payloads.md) §2) (`stream_noncanonical`). + +**Predictors**, with `prev` the unit's current output: + +- **identity**: `pred(i) = prev(i)`. +- **up2**: `prev` is `h′ × w′ × c` with `h′ = ⌈d0/2⌉`, `w′ = ⌈d1/2⌉` and `c = d2`, + so odd sizes are allowed. For output `(y, x, ch)`: `iy = y >> 1`, `fy = y & 1`, + `ix = x >> 1`, `fx = x & 1`, and `p(Y, X) = prev[min(Y, h′ − 1), min(X, w′ − 1), ch]`. + The prediction is `p(iy, ix)` when both are 0; + `(p(iy, ix) + p(iy, ix + 1) + 1) >> 1` when only `fx` is 1; + `(p(iy, ix) + p(iy + 1, ix) + 1) >> 1` when only `fy` is 1; and + `(p(iy, ix) + p(iy, ix + 1) + p(iy + 1, ix) + p(iy + 1, ix + 1) + 2) >> 2` when + both are. This is the half-pel arithmetic of [integer-payloads](integer-payloads.md) + §3. +- **rate**: `prev` holds `n′` samples of `c = d2` interleaved channels. For output + sample *i* and channel *ch*: `s = i·rate_den`, `lo = s div rate_num`, + `Ï� = s mod rate_num`, `hi = min(lo + 1, n′ − 1)`, `a = prev[lo·c + ch]`, + `b = prev[hi·c + ch]`, and the prediction is `rdiv(a·rate_num + (b − a)·Ï�, rate_num)`, + where `rdiv(n, d) = sign(n)·⌊(2|n| + d) / 2d⌋` rounds half away from zero. This is + `a + (b − a)·Ï�/rate_num` rounded half away from zero, the linear interpolation at + source position `i·rate_den/rate_num`, in exact integer arithmetic. + +### A.5 Declared cost + +A payload's header declares, before any symbol is decoded: its output, +`n_units·d0·d1·d2` elements of 1 byte (u8) or 2 bytes (i16); its symbols, +`n_units·d0·d1·d2`, one entropy-decode step each; and the most bytes its decode holds +at once beyond the payload and the stages below, its parsed tables (`4·T·cdf_len`) +and one unit's output while it is built. It runs no network. A sink that applies a +limit compares this cost with it after the parse and before any stream is decoded +(§4.1 step 6, `cost-over-limit`). + +### A.6 Refusal codes + +The list is closed, and its codes are those [integer-payloads](integer-payloads.md) §8 +uses for the same faults: `bad_magic`, `version`, `reserved_nonzero`, `field_range`, +`too_large`, `cdf_malformed`, `stream_length`, `trailing`, `stream_exhausted`, +`stream_noncanonical` and `geometry_mismatch`. A sink reports the first that applies, +in the order of A.3 and A.4. In §4.1, `geometry_mismatch` is the unit's +`geometry-mismatch`, and every other code is the envelope's `payload-malformed`. diff --git a/wai/registry/capabilities.json b/wai/registry/capabilities.json index 97be35c03..ee08dbeea 100644 --- a/wai/registry/capabilities.json +++ b/wai/registry/capabilities.json @@ -101,6 +101,20 @@ "edition": null, "registered": "2026-06-27" }, + { + "capability": "wai.audio.int_refine", + "kind": "refinement", + "status": "registered", + "media": "audio", + "role": "refinement", + "tier": "lossy", + "licence": "royalty-free", + "replicate": "standard-defined", + "sets": [], + "spec_section": "§5 Refinement capabilities (staged delivery)", + "edition": null, + "registered": "2026-10-01" + }, { "capability": "wai.audio.ivas", "kind": "payload", @@ -427,6 +441,20 @@ "edition": null, "registered": "2026-06-12" }, + { + "capability": "wai.image.int_refine", + "kind": "refinement", + "status": "registered", + "media": "image", + "role": "refinement", + "tier": "lossy", + "licence": "royalty-free", + "replicate": "standard-defined", + "sets": [], + "spec_section": "§5 Refinement capabilities (staged delivery)", + "edition": null, + "registered": "2026-10-01" + }, { "capability": "wai.image.jp2", "kind": "payload", @@ -965,6 +993,20 @@ "edition": null, "registered": "2026-06-28" }, + { + "capability": "wai.splat.int_refine", + "kind": "refinement", + "status": "registered", + "media": "volumetric", + "role": "refinement", + "tier": "lossy", + "licence": "royalty-free", + "replicate": "standard-defined", + "sets": [], + "spec_section": "§5 Refinement capabilities (staged delivery)", + "edition": null, + "registered": "2026-10-01" + }, { "capability": "wai.splat.sog", "kind": "payload", @@ -1211,6 +1253,20 @@ "edition": null, "registered": "2026-06-27" }, + { + "capability": "wai.video.int_refine", + "kind": "refinement", + "status": "registered", + "media": "video", + "role": "refinement", + "tier": "lossy", + "licence": "royalty-free", + "replicate": "standard-defined", + "sets": [], + "spec_section": "§5 Refinement capabilities (staged delivery)", + "edition": null, + "registered": "2026-10-01" + }, { "capability": "wai.video.j2k", "kind": "payload", diff --git a/wai/registry/capabilities.schema.json b/wai/registry/capabilities.schema.json index cbec87f0c..1d1db5385 100644 --- a/wai/registry/capabilities.schema.json +++ b/wai/registry/capabilities.schema.json @@ -43,6 +43,7 @@ "enum": [ "payload", "companion", + "refinement", "derivation", "record" ] @@ -72,6 +73,7 @@ "standalone", "companion", "enhancement", + "refinement", null ] }, diff --git a/wai/vocab/v2.shapes.ttl b/wai/vocab/v2.shapes.ttl index 24016e876..dd4bfb0e9 100644 --- a/wai/vocab/v2.shapes.ttl +++ b/wai/vocab/v2.shapes.ttl @@ -5,10 +5,11 @@ # A WAI manifest, typed or not: every manifest has a wai (version) field. A WAI1 manifest names a # capability, a WAI2 manifest a renditions list, and never both. The capability is a -# registered payload or companion capability that is not a candidate; the fallback a -# registered payload capability that is not a candidate. (A WAI1 naming a companion-only -# capability parses but does not conform; the reference implementation's role check -# reports it.) media is informational, and a reader accepts a value it does not know +# registered payload, companion or refinement capability that is not a candidate; the +# fallback a registered payload capability that is not a candidate. (A WAI1 naming a +# companion-only capability, or a refinement-only one without refines, parses but does +# not conform; the reference implementation's role check reports it.) media is +# informational, and a reader accepts a value it does not know # (SPEC sections 3 and 8), so only its form is checked. wai:ManifestShape a sh:NodeShape ; sh:targetClass wai:Manifest ; @@ -22,6 +23,7 @@ wai:ManifestShape a sh:NodeShape ; wai:wai.audio.flac wai:wai.audio.iamf wai:wai.audio.int_codec + wai:wai.audio.int_refine wai:wai.audio.ivas wai:wai.audio.mpegh wai:wai.audio.opus @@ -35,6 +37,7 @@ wai:ManifestShape a sh:NodeShape ; wai:wai.haptic.mihs wai:wai.haptic.signal wai:wai.image.avif + wai:wai.image.int_refine wai:wai.image.jp2 wai:wai.image.jpeg wai:wai.image.jpegai @@ -71,6 +74,7 @@ wai:ManifestShape a sh:NodeShape ; wai:wai.semantic.perco2 wai:wai.splat.4d wai:wai.splat.int_codec + wai:wai.splat.int_refine wai:wai.splat.spz wai:wai.ternary.tt wai:wai.text.xz @@ -85,6 +89,7 @@ wai:ManifestShape a sh:NodeShape ; wai:wai.video.hevc wai:wai.video.int_hyper wai:wai.video.int_motion + wai:wai.video.int_refine wai:wai.video.j2k wai:wai.video.lcevc wai:wai.video.vp9 @@ -178,9 +183,9 @@ wai:ManifestShape a sh:NodeShape ; [ sh:property [ sh:path wai:renditions ; sh:minCount 1 ] ] ) . -# A WAI2 component, in either role: its capability is a registered payload or companion -# capability that is not a candidate, and the base a companion names is a registered -# payload capability that is not a candidate. +# A WAI2 component, in any role: its capability is a registered payload, companion or +# refinement capability that is not a candidate, and the base a companion names is a +# registered payload capability that is not a candidate. wai:ComponentShape a sh:NodeShape ; sh:targetClass wai:Component ; sh:targetObjectsOf wai:renditions ; @@ -192,6 +197,7 @@ wai:ComponentShape a sh:NodeShape ; wai:wai.audio.flac wai:wai.audio.iamf wai:wai.audio.int_codec + wai:wai.audio.int_refine wai:wai.audio.ivas wai:wai.audio.mpegh wai:wai.audio.opus @@ -205,6 +211,7 @@ wai:ComponentShape a sh:NodeShape ; wai:wai.haptic.mihs wai:wai.haptic.signal wai:wai.image.avif + wai:wai.image.int_refine wai:wai.image.jp2 wai:wai.image.jpeg wai:wai.image.jpegai @@ -241,6 +248,7 @@ wai:ComponentShape a sh:NodeShape ; wai:wai.semantic.perco2 wai:wai.splat.4d wai:wai.splat.int_codec + wai:wai.splat.int_refine wai:wai.splat.spz wai:wai.ternary.tt wai:wai.text.xz @@ -255,6 +263,7 @@ wai:ComponentShape a sh:NodeShape ; wai:wai.video.hevc wai:wai.video.int_hyper wai:wai.video.int_motion + wai:wai.video.int_refine wai:wai.video.j2k wai:wai.video.lcevc wai:wai.video.vp9 diff --git a/wai/vocab/v2.ttl b/wai/vocab/v2.ttl index 2f52a640f..9747b7ac6 100644 --- a/wai/vocab/v2.ttl +++ b/wai/vocab/v2.ttl @@ -31,6 +31,7 @@ wai:ParameterSetPin a owl:Class ; rdfs:label "Parameter-set pin (SPEC section 3. wai:kind-payload a wai:Kind ; rdfs:label "payload" . wai:kind-companion a wai:Kind ; rdfs:label "companion" . +wai:kind-refinement a wai:Kind ; rdfs:label "refinement" . wai:kind-derivation a wai:Kind ; rdfs:label "derivation" . wai:kind-record a wai:Kind ; rdfs:label "record" . wai:status-registered a wai:Status ; rdfs:label "registered" . @@ -47,6 +48,7 @@ wai:media-binary a wai:Media ; rdfs:label "binary" . wai:role-standalone a wai:Role ; rdfs:label "standalone" . wai:role-companion a wai:Role ; rdfs:label "companion" . wai:role-enhancement a wai:Role ; rdfs:label "enhancement" . +wai:role-refinement a wai:Role ; rdfs:label "refinement" . wai:tier-lossless a wai:Tier ; rdfs:label "lossless" . wai:tier-lossy a wai:Tier ; rdfs:label "lossy" . wai:tier-perceptual a wai:Tier ; rdfs:label "perceptual" . @@ -181,6 +183,18 @@ wai:wai.audio.int_codec a wai:Capability ; wai:specSection "§5 Interactive, temporal & computed content" ; wai:registeredOn "2026-06-27"^^xsd:date . +wai:wai.audio.int_refine a wai:Capability ; + rdfs:label "wai.audio.int_refine" ; + wai:kind wai:kind-refinement ; + wai:status wai:status-registered ; + wai:media wai:media-audio ; + wai:role wai:role-refinement ; + wai:tier wai:tier-lossy ; + wai:licence wai:licence-royalty-free ; + wai:replicateClass wai:replicate-standard-defined ; + wai:specSection "§5 Refinement capabilities (staged delivery)" ; + wai:registeredOn "2026-10-01"^^xsd:date . + wai:wai.audio.ivas a wai:Capability ; rdfs:label "wai.audio.ivas" ; wai:kind wai:kind-payload ; @@ -428,6 +442,18 @@ wai:wai.image.avif a wai:Capability ; wai:specSection "§5 Recommended modern set" ; wai:registeredOn "2026-06-12"^^xsd:date . +wai:wai.image.int_refine a wai:Capability ; + rdfs:label "wai.image.int_refine" ; + wai:kind wai:kind-refinement ; + wai:status wai:status-registered ; + wai:media wai:media-image ; + wai:role wai:role-refinement ; + wai:tier wai:tier-lossy ; + wai:licence wai:licence-royalty-free ; + wai:replicateClass wai:replicate-standard-defined ; + wai:specSection "§5 Refinement capabilities (staged delivery)" ; + wai:registeredOn "2026-10-01"^^xsd:date . + wai:wai.image.jp2 a wai:Capability ; rdfs:label "wai.image.jp2" ; wai:kind wai:kind-payload ; @@ -876,6 +902,18 @@ wai:wai.splat.int_codec a wai:Capability ; wai:specSection "§5 Interactive, temporal & computed content" ; wai:registeredOn "2026-06-28"^^xsd:date . +wai:wai.splat.int_refine a wai:Capability ; + rdfs:label "wai.splat.int_refine" ; + wai:kind wai:kind-refinement ; + wai:status wai:status-registered ; + wai:media wai:media-volumetric ; + wai:role wai:role-refinement ; + wai:tier wai:tier-lossy ; + wai:licence wai:licence-royalty-free ; + wai:replicateClass wai:replicate-standard-defined ; + wai:specSection "§5 Refinement capabilities (staged delivery)" ; + wai:registeredOn "2026-10-01"^^xsd:date . + wai:wai.splat.sog a wai:Capability ; rdfs:label "wai.splat.sog" ; wai:kind wai:kind-payload ; @@ -1077,6 +1115,18 @@ wai:wai.video.int_motion a wai:Capability ; wai:specSection "§5 Neural capabilities" ; wai:registeredOn "2026-06-27"^^xsd:date . +wai:wai.video.int_refine a wai:Capability ; + rdfs:label "wai.video.int_refine" ; + wai:kind wai:kind-refinement ; + wai:status wai:status-registered ; + wai:media wai:media-video ; + wai:role wai:role-refinement ; + wai:tier wai:tier-lossy ; + wai:licence wai:licence-royalty-free ; + wai:replicateClass wai:replicate-standard-defined ; + wai:specSection "§5 Refinement capabilities (staged delivery)" ; + wai:registeredOn "2026-10-01"^^xsd:date . + wai:wai.video.j2k a wai:Capability ; rdfs:label "wai.video.j2k" ; wai:kind wai:kind-payload ; diff --git a/wai/wai-rs/src/codecs/glossary_check.rs b/wai/wai-rs/src/codecs/glossary_check.rs index c2d42196a..45cf1c157 100644 --- a/wai/wai-rs/src/codecs/glossary_check.rs +++ b/wai/wai-rs/src/codecs/glossary_check.rs @@ -102,6 +102,10 @@ const GLOSSARY_NO_ENTRY: &[(&str, &str)] = &[ (CAP_WORLD_MODEL, WAI_NATIVE), (CAP_WORLD_REPLAY, WAI_NATIVE), (CAP_WORLD_SCENE, WAI_NATIVE), + (CAP_IMAGE_INT_REFINE, WAI_NATIVE), + (CAP_VIDEO_INT_REFINE, WAI_NATIVE), + (CAP_AUDIO_INT_REFINE, WAI_NATIVE), + (CAP_SPLAT_INT_REFINE, WAI_NATIVE), (CAP_NEURAL_BMSHJ2018, "the 2018 scale-hyperprior entry describes that paper's hyperprior model, not its factorized-prior baseline, which this capability carries"), (CAP_NEURAL_MBT2018_MEAN, "the entry for the 2018 joint-prior paper describes its autoregressive context model, not the mean-scale hyperprior this capability carries"), (CAP_NEURAL_VIDEO_BMSHJ2018, "per-frame use of the factorized-prior image model; no glossary codec describes it"), @@ -314,8 +318,10 @@ fn glossary_links_agree_with_the_registry() { fn glossary_check_names_each_broken_rule() { use serde_json::json; let bodies = vec!["ITU-T".to_string()]; + // The fixture links the payload and companion capabilities a codec could + // describe; a refinement is never linked (its payload format is WAI's own). let caps: Vec<&str> = known_capabilities().iter().copied() - .filter(|c| !matches!(capability_media(c), Some(Media::Interactive | Media::Binary))) + .filter(|c| !matches!(capability_media(c), Some(Media::Interactive | Media::Binary)) && !is_refinement_only(c)) .collect(); let links: Vec<(&str, &str)> = caps.iter().map(|c| (*c, *c)).collect(); let no_entry: Vec<(&str, &str)> = known_capabilities().iter().copied() diff --git a/wai/wai-rs/src/codecs/mod.rs b/wai/wai-rs/src/codecs/mod.rs index 506fb5211..cf96bf6b6 100644 --- a/wai/wai-rs/src/codecs/mod.rs +++ b/wai/wai-rs/src/codecs/mod.rs @@ -186,6 +186,74 @@ pub fn pinned_int_payload(cap: &str) -> Option<[u8; 4]> { PINNED_INT_PAYLOADS.iter().find(|(c, _)| *c == cap).map(|&(_, m)| m) } +// Refinement capabilities (SPEC §5, *Refinement capabilities*; +// extensions/staged-delivery.md): each is **refinement-only**, a payload applied to the +// output of the stage below it, bound to that output's producer by content hash, and +// never decoded on its own. The residual form, payload WIR1 (staged-delivery §6.1). +pub const CAP_IMAGE_INT_REFINE: &str = "wai.image.int_refine"; +pub const CAP_VIDEO_INT_REFINE: &str = "wai.video.int_refine"; +pub const CAP_AUDIO_INT_REFINE: &str = "wai.audio.int_refine"; +pub const CAP_SPLAT_INT_REFINE: &str = "wai.splat.int_refine"; + +/// How a capability applied to a base's output computes what it gives (SPEC §5, *Base +/// admission*). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AppliedForm { + /// A refinement-only capability of the residual form (staged-delivery §6.1): the + /// next stage is the stage below, predicted and corrected by a decoded residual. + Residual, + /// An enhancement layer (SPEC §5, *Enhancement layers*): its standard's own + /// decode over one base encode's output. Carried as a companion (SPEC §7.1). + Enhancement, +} + +/// SPEC §5, *Base admission*: the one table of every capability applied to a base's +/// output, with its form and the base capabilities it applies over. A refinement-only +/// capability is admitted over a base when `refines.base` names an envelope decoded +/// through one of these (staged-delivery §4.1 step 4); an enhancement layer binds to a +/// rendition of one of them (SPEC §7.1, rule 2). Admitting another base widens a +/// capability (SPEC §8). Every admission check of the reference implementation reads +/// this table ([`admits`], [`enhancement_bases`], [`companion_applies_to`]). +/// +/// cbindgen:ignore +pub static BASE_ADMISSION: &[(&str, AppliedForm, &[&str])] = &[ + (CAP_IMAGE_INT_REFINE, AppliedForm::Residual, + &[CAP_NEURAL_INT_HYPER, CAP_NEURAL_INT_MLICPP, CAP_NEURAL_INT_SYNTH, CAP_IMAGE_PNG]), + (CAP_VIDEO_INT_REFINE, AppliedForm::Residual, &[CAP_VIDEO_INT_MOTION, CAP_VIDEO_INT_HYPER]), + (CAP_AUDIO_INT_REFINE, AppliedForm::Residual, &[CAP_AUDIO_INT_CODEC, CAP_AUDIO_FLAC]), + (CAP_SPLAT_INT_REFINE, AppliedForm::Residual, &[CAP_SPLAT_INT_CODEC]), + (CAP_VIDEO_LCEVC, AppliedForm::Enhancement, LCEVC_BASES), +]; + +/// The form of a capability applied to a base's output ([`BASE_ADMISSION`]), or `None` +/// for any other string. +pub fn applied_form(cap: &str) -> Option { + BASE_ADMISSION.iter().find(|(c, ..)| *c == cap).map(|&(_, f, _)| f) +} + +/// The base capabilities `cap` applies over ([`BASE_ADMISSION`]); empty for a +/// capability not applied to a base. +pub fn admitted_bases(cap: &str) -> &'static [&'static str] { + BASE_ADMISSION.iter().find(|(c, ..)| *c == cap).map(|&(.., b)| b).unwrap_or(&[]) +} + +/// Whether the capability `applied` applies over a base decoded through `base` +/// (SPEC §5, *Base admission*). +pub fn admits(applied: &str, base: &str) -> bool { + admitted_bases(applied).contains(&base) +} + +/// Whether a registered capability is **refinement-only** (SPEC §5, §8): a payload +/// applied, in a declared order, to the output of the stage below it, bound to that +/// output's producer by content hash (`extensions/staged-delivery.md`), and never +/// decoded on its own. Such a capability is never a `WAI1` `capability` dispatched by +/// SPEC §4 or a `fallback`, never a selectable rendition and never a companion; it is +/// carried as a `WAI1` with `refines`, or as a `WAI2` component with +/// `role: "refinement"`. +pub fn is_refinement_only(cap: &str) -> bool { + applied_form(cap) == Some(AppliedForm::Residual) +} + // Haptics (extensions/haptics): per-channel intensity/sharpness envelope, // reconstructed to an actuator signal (sample-equivalence). Engine // behind the `haptic` feature. @@ -536,6 +604,8 @@ pub fn known_capabilities() -> &'static [&'static str] { CAP_VIDEO_LCEVC, // broadcast and streaming audio (sink-supplied; none is a default) CAP_AUDIO_MPEGH, CAP_AUDIO_AVS3, CAP_AUDIO_AAC, + // refinement-only: residual refinements of an integer or lossless base + CAP_IMAGE_INT_REFINE, CAP_VIDEO_INT_REFINE, CAP_AUDIO_INT_REFINE, CAP_SPLAT_INT_REFINE, ] } @@ -600,10 +670,10 @@ const LCEVC_BASES: &[&str] = &[ /// The capabilities an enhancement layer may be bound to: the base codecs its /// registration names (SPEC §5), which for LCEVC are WAI's choice rather than a -/// restriction of its standard. Empty for a capability that is not an -/// enhancement layer. +/// restriction of its standard, read from [`BASE_ADMISSION`]. Empty for a +/// capability that is not an enhancement layer. pub fn enhancement_bases(cap: &str) -> &'static [&'static str] { - if cap == CAP_VIDEO_LCEVC { LCEVC_BASES } else { &[] } + if applied_form(cap) == Some(AppliedForm::Enhancement) { admitted_bases(cap) } else { &[] } } /// The registered capabilities outside the `wai.video.*` namespace whose payload @@ -621,7 +691,8 @@ const VIDEO_CLASS_OUTSIDE_NAMESPACE: &[&str] = &[ /// manifest's `media` field is informational; the capability is /// authoritative): /// -/// - every registered `wai.video.*` capability that is not companion-only; +/// - every registered `wai.video.*` capability that is neither companion-only nor +/// refinement-only; /// - the neural video codecs `wai.neural.video_bmshj2018`, `wai.neural.dcvc_rt` /// and `wai.neural.dcvc_fm`; /// - the films `wai.film.linear` and `wai.film.reel`. @@ -631,7 +702,7 @@ const VIDEO_CLASS_OUTSIDE_NAMESPACE: &[&str] = &[ /// in that revision. pub fn is_video_class(cap: &str) -> bool { is_known_capability(cap) - && ((cap.starts_with("wai.video.") && !is_companion_only(cap)) + && ((cap.starts_with("wai.video.") && !is_companion_only(cap) && !is_refinement_only(cap)) || VIDEO_CLASS_OUTSIDE_NAMESPACE.contains(&cap)) } @@ -654,9 +725,9 @@ pub fn is_video_class(cap: &str) -> bool { /// companion (rule 1), and for a capability this revision does not register the /// registry knows no bases — [`crate::container::WaiMulti::check_component_roles`] /// checks such a companion only for a present base (rule 3). No companion -/// applies to a companion-only `base`. +/// applies to a companion-only or a refinement-only `base`. pub fn companion_applies_to(companion: &str, base: &str) -> bool { - if is_companion_only(base) { + if is_companion_only(base) || is_refinement_only(base) { return false; } match companion { @@ -758,7 +829,10 @@ CAP_WORLD_SCENE | CAP_WORLD_REPLAY | CAP_WORLD_LIVE | CAP_WORLD_MODEL | CAP_MESH // guarantee. LCEVC is lossy residual enhancement of a lossy base. | CAP_VIDEO_AV2 | CAP_VIDEO_VVC | CAP_VIDEO_HEVC | CAP_VIDEO_AVC | CAP_VIDEO_VP9 | CAP_VIDEO_EVC | CAP_VIDEO_EVC_BASELINE | CAP_VIDEO_AVS3 | CAP_VIDEO_LCEVC - | CAP_AUDIO_MPEGH | CAP_AUDIO_AVS3 | CAP_AUDIO_AAC => Lossy, + | CAP_AUDIO_MPEGH | CAP_AUDIO_AVS3 | CAP_AUDIO_AAC + // A residual refinement quantises its residual by its step q: lossy against + // its target, whatever its base; its decode is exact given the stage below. + | CAP_IMAGE_INT_REFINE | CAP_VIDEO_INT_REFINE | CAP_AUDIO_INT_REFINE | CAP_SPLAT_INT_REFINE => Lossy, // Generative / similarity-conformant. CAP_SEMANTIC_CLIP | CAP_SEMANTIC_PERCO2 => Perceptual, _ => return None, @@ -861,6 +935,7 @@ pub fn capability_license(cap: &str) -> Option { | CAP_AUDIO_SCENE | CAP_AUDIO_SCORE | CAP_HAPTIC_SIGNAL | CAP_FEED_STATE | CAP_AVATAR_POSE | CAP_SPLAT_4D | CAP_FILM_LINEAR | CAP_FILM_REEL | CAP_QUANTUM_CIRCUIT | CAP_TERNARY_TT | CAP_PRIOR_SET + | CAP_IMAGE_INT_REFINE | CAP_VIDEO_INT_REFINE | CAP_AUDIO_INT_REFINE | CAP_SPLAT_INT_REFINE // Open asset formats: glTF (Khronos), SPZ (open source). | CAP_MESH_GLB | CAP_SPLAT_SPZ // JPEG 2000 Part 1 is fee-free under ITU-T policy 2.1 (extensions / @@ -1202,7 +1277,11 @@ pub fn capability_replicate_class(cap: &str) -> Option { | CAP_QUANTUM_CIRCUIT | CAP_TERNARY_TT // A parameter-set listing: SPEC §3.1 fixes its bytes exactly, and they are // its own output. - | CAP_PRIOR_SET => StandardDefined, + | CAP_PRIOR_SET + // A residual refinement: integer prediction, residual and clamp + // (staged-delivery §6.1), exact given the stage below; it composes its base's + // decoded output (`composes_decoded_sources`), so it is as exact as its base. + | CAP_IMAGE_INT_REFINE | CAP_VIDEO_INT_REFINE | CAP_AUDIO_INT_REFINE | CAP_SPLAT_INT_REFINE => StandardDefined, // A criterion around a reference, not identical output (clauses in the // variant's doc and in research/media-codec-registry.md): Opus (RFC 6716 // §6), JPEG (T.81 §7, A.3.3), JPEG XL (18181-3 §4.2), JPEG 2000 (15444-4), @@ -1481,14 +1560,16 @@ pub fn replicate_refusal( /// Whether a capability's output is computed from **other envelopes' decoded /// output** — sources it references rather than carries — so it is only as exact -/// as they are. `wai.audio.scene` is the one such capability: its payload names -/// each object's source by the content hash of a `wai.audio.*` envelope, and its -/// mixdown is exact given the PCM those sources decode to -/// (`extensions/spatial-audio.md` §4), whatever their codec. The other WAI-native -/// capabilities hash only state they carry: a world's, a film's or an avatar's -/// assets are presentation and never enter its state hash. +/// as they are. `wai.audio.scene`'s payload names each object's source by the +/// content hash of a `wai.audio.*` envelope, and its mixdown is exact given the PCM +/// those sources decode to (`extensions/spatial-audio.md` §4), whatever their codec. +/// A refinement-only capability names, by content hash, the base whose output it +/// refines (`extensions/staged-delivery.md` §3), and its next stage is exact given +/// that output. The other WAI-native capabilities hash only state they carry: a +/// world's, a film's or an avatar's assets are presentation and never enter its +/// state hash. pub fn composes_decoded_sources(cap: &str) -> bool { - cap == CAP_AUDIO_SCENE + cap == CAP_AUDIO_SCENE || is_refinement_only(cap) } /// Whether the class promises byte identity for the capability's own output. @@ -1529,10 +1610,12 @@ pub fn is_cross_decoder_exact(cap: &str) -> bool { /// Whether `source` is a capability a composing capability's payload may /// reference: for `wai.audio.scene`, a `wai.audio.*` envelope — the asset table -/// names each object's source as one (`extensions/spatial-audio.md` §2.2). +/// names each object's source as one (`extensions/spatial-audio.md` §2.2); for a +/// refinement-only capability, a base it admits (SPEC §5, *Base admission*). fn is_source_of(cap: &str, source: &str) -> bool { match cap { CAP_AUDIO_SCENE => source.starts_with("wai.audio."), + c if is_refinement_only(c) => admits(c, source), _ => false, } } @@ -1810,6 +1893,58 @@ mod tests { assert!(!is_companion_only("wai.image.nope")); } + /// Test 16: every refinement-only capability is complete in the registry, and + /// *Base admission* is one table. Each refinement has the residual form, its + /// admitted bases (pinned here in both directions), the royalty-free licence, the + /// StandardDefined class, the Lossy tier, the refinement kind and role; none is a + /// candidate, a companion, dispatchable or in the video media class; and it is as + /// exact as the base it refines. LCEVC's bases are its row of the same table: + /// `enhancement_bases` and `companion_applies_to` read it, and no other string has + /// an applied form. + #[test] + fn refinement_registry_is_complete() { + use registry::{capability_kind, capability_role, Kind}; + let pinned: &[(&str, &[&str])] = &[ + (CAP_IMAGE_INT_REFINE, &[CAP_NEURAL_INT_HYPER, CAP_NEURAL_INT_MLICPP, CAP_NEURAL_INT_SYNTH, CAP_IMAGE_PNG]), + (CAP_VIDEO_INT_REFINE, &[CAP_VIDEO_INT_MOTION, CAP_VIDEO_INT_HYPER]), + (CAP_AUDIO_INT_REFINE, &[CAP_AUDIO_INT_CODEC, CAP_AUDIO_FLAC]), + (CAP_SPLAT_INT_REFINE, &[CAP_SPLAT_INT_CODEC]), + ]; + let refinements: Vec<&str> = known_capabilities().iter().copied().filter(|c| is_refinement_only(c)).collect(); + assert_eq!(refinements, pinned.iter().map(|(c, _)| *c).collect::>()); + for &(cap, bases) in pinned { + assert_eq!(applied_form(cap), Some(AppliedForm::Residual), "{cap}"); + assert_eq!(admitted_bases(cap), bases, "{cap}"); + assert_eq!(capability_license(cap), Some(LicenseClass::RoyaltyFree), "{cap}"); + assert_eq!(capability_replicate_class(cap), Some(ReplicateClass::StandardDefined), "{cap}"); + assert_eq!(capability_tier(cap), Some(FidelityTier::Lossy), "{cap}"); + assert_eq!(capability_kind(cap), Some(Kind::Refinement), "{cap}"); + assert_eq!(capability_role(cap), Some("refinement"), "{cap}"); + assert!(!is_candidate(cap) && !is_companion_only(cap) && !is_dispatchable(cap), "{cap}"); + assert!(!is_video_class(cap) && pinned_int_payload(cap).is_none(), "{cap}"); + assert!(!decodes_against_parameter_set(cap), "{cap}: a refinement reads no parameter set"); + assert!(!is_cross_decoder_exact(cap), "{cap}: exact only given its base"); + for &other in known_capabilities() { + assert_eq!(admits(cap, other), bases.contains(&other), "{cap} over {other}"); + assert_eq!(is_cross_decoder_exact_with_sources(cap, &[other]), + bases.contains(&other) && is_cross_decoder_exact(other), "{cap} over {other}"); + assert!(!companion_applies_to(other, cap), "no companion applies to {cap}"); + } + } + // LCEVC: one row of the same table, with the bases SPEC §5 lists. + assert_eq!(applied_form(CAP_VIDEO_LCEVC), Some(AppliedForm::Enhancement)); + assert_eq!(enhancement_bases(CAP_VIDEO_LCEVC), admitted_bases(CAP_VIDEO_LCEVC)); + assert_eq!(admitted_bases(CAP_VIDEO_LCEVC), LCEVC_BASES); + for &b in known_capabilities() { + assert_eq!(companion_applies_to(CAP_VIDEO_LCEVC, b), admits(CAP_VIDEO_LCEVC, b), "{b}"); + } + // No other string has an applied form, and the table names each once. + let applied: Vec<&str> = known_capabilities().iter().copied().filter(|c| applied_form(c).is_some()).collect(); + assert_eq!(applied.len(), pinned.len() + 1); + assert_eq!(BASE_ADMISSION.len(), applied.len()); + assert!(applied_form("wai.image.nope").is_none() && admitted_bases("wai.image.nope").is_empty()); + } + /// SPEC §5 and §7.1, rule 2: what each companion-only capability applies to. /// Pinned in both directions, so widening or narrowing a base set is a /// reviewed change to this test. @@ -1818,7 +1953,7 @@ mod tests { // The video media class, pinned: every standalone wai.video.* capability, // the neural video codecs SPEC §5 lists under *Video:*, and the films. let mut video: Vec<&str> = known_capabilities().iter().copied() - .filter(|c| c.starts_with("wai.video.") && !is_companion_only(c)).collect(); + .filter(|c| c.starts_with("wai.video.") && !is_companion_only(c) && !is_refinement_only(c)).collect(); assert!(video.contains(&CAP_VIDEO_J2K) && video.contains(&CAP_VIDEO_AV2)); video.extend([CAP_NEURAL_VIDEO_BMSHJ2018, CAP_NEURAL_DCVC_RT, CAP_NEURAL_DCVC_FM, CAP_FILM_LINEAR, CAP_FILM_REEL]); @@ -2171,6 +2306,10 @@ mod tests { (CAP_QUANTUM_CIRCUIT, "extensions/quantum-sim.md: statevector-equivalence"), (CAP_TERNARY_TT, "extensions/ternary-tt.md: reconstruction-equivalence"), (CAP_PRIOR_SET, "SPEC.md §3.1: a listing's bytes are fixed exactly and are their own output"), + (CAP_IMAGE_INT_REFINE, "extensions/staged-delivery.md §6.1: integer predictor, residual and clamp; staged-conformance"), + (CAP_VIDEO_INT_REFINE, "extensions/staged-delivery.md §6.1: integer predictor, residual and clamp; staged-conformance"), + (CAP_AUDIO_INT_REFINE, "extensions/staged-delivery.md §6.1: integer predictor, residual and clamp; staged-conformance"), + (CAP_SPLAT_INT_REFINE, "extensions/staged-delivery.md §6.1: integer predictor, residual and clamp; staged-conformance"), ]; /// Every capability classed NeuralIntegerExact, with the basis its class rests @@ -2267,10 +2406,12 @@ mod tests { /// does not make it portable by itself. #[test] fn a_scene_is_only_as_exact_as_its_sources() { - // The one capability that composes other envelopes' decoded output. + // The capabilities that compose other envelopes' decoded output: the scene, + // and the refinements, each exact given the base it refines. let composing: Vec<&str> = known_capabilities().iter().copied() .filter(|c| composes_decoded_sources(c)).collect(); - assert_eq!(composing, vec![CAP_AUDIO_SCENE]); + assert_eq!(composing, vec![CAP_AUDIO_SCENE, CAP_IMAGE_INT_REFINE, CAP_VIDEO_INT_REFINE, + CAP_AUDIO_INT_REFINE, CAP_SPLAT_INT_REFINE]); assert_eq!(capability_replicate_class(CAP_AUDIO_SCENE), Some(ReplicateClass::StandardDefined)); // Not portable from the capability alone... diff --git a/wai/wai-rs/src/codecs/registry.rs b/wai/wai-rs/src/codecs/registry.rs index 29381fcff..1b2c5b8fd 100644 --- a/wai/wai-rs/src/codecs/registry.rs +++ b/wai/wai-rs/src/codecs/registry.rs @@ -52,6 +52,11 @@ pub enum Kind { /// A companion-only capability ([`is_companion_only`]): its payload is applied /// with a base rendition, never on its own. Companion, + /// A refinement-only capability ([`is_refinement_only`]): its payload is applied, + /// in a declared order, to the output of the stage below it, bound to that + /// output's producer by content hash, never on its own + /// (`extensions/staged-delivery.md`). + Refinement, /// A derivation operation of ISO/IEC 23001-16 (`wai.derive.*`): it acts on /// samples another capability decoded and names no payload format. Derivation, @@ -62,14 +67,15 @@ pub enum Kind { impl Kind { /// Every kind, in declaration order. - pub const fn all() -> [Self; 4] { - [Self::Payload, Self::Companion, Self::Derivation, Self::Record] + pub const fn all() -> [Self; 5] { + [Self::Payload, Self::Companion, Self::Refinement, Self::Derivation, Self::Record] } /// The label the export uses. pub fn label(self) -> &'static str { match self { Self::Payload => "payload", Self::Companion => "companion", + Self::Refinement => "refinement", Self::Derivation => "derivation", Self::Record => "record", } @@ -164,15 +170,19 @@ impl ReplicateClass { } /// The role labels `wai capabilities` prints and the export records for a -/// payload or companion capability. -pub const ROLE_LABELS: [&str; 3] = ["standalone", "companion", "enhancement"]; +/// payload, companion or refinement capability. +pub const ROLE_LABELS: [&str; 4] = ["standalone", "companion", "enhancement", "refinement"]; -/// How dispatch treats a payload or companion capability's payload: standalone -/// media, a companion applied with a base rendition, or an enhancement layer bound -/// to exactly one base encode (SPEC §5, §7.1). `None` for any other string. +/// How dispatch treats a payload, companion or refinement capability's payload: +/// standalone media, a companion applied with a base rendition, an enhancement layer +/// bound to exactly one base encode (SPEC §5, §7.1), or a refinement applied to the +/// output of the stage below it (SPEC §5, *Refinement capabilities*). `None` for any +/// other string. pub fn capability_role(cap: &str) -> Option<&'static str> { if !is_known_capability(cap) { None + } else if is_refinement_only(cap) { + Some("refinement") } else if is_enhancement_layer(cap) { Some("enhancement") } else if is_companion_only(cap) { @@ -299,6 +309,10 @@ const ROWS: &[(&str, &str, Option<&str>, &str)] = &[ (CAP_AUDIO_MPEGH, S_BROADCAST, None, "2026-09-28"), (CAP_AUDIO_AVS3, S_BROADCAST, Some("T/AI 109.3-2023"), "2026-09-28"), (CAP_AUDIO_AAC, S_BROADCAST, None, "2026-09-28"), + (CAP_IMAGE_INT_REFINE, S_REFINEMENT, None, "2026-10-01"), + (CAP_VIDEO_INT_REFINE, S_REFINEMENT, None, "2026-10-01"), + (CAP_AUDIO_INT_REFINE, S_REFINEMENT, None, "2026-10-01"), + (CAP_SPLAT_INT_REFINE, S_REFINEMENT, None, "2026-10-01"), ]; const S_FLOOR: &str = "§5 Mandatory floor"; @@ -308,6 +322,7 @@ const S_PRIOR: &str = "§5 Prior artifacts, general tensors and derivation opera const S_INTERACTIVE: &str = "§5 Interactive, temporal & computed content"; const S_IMMERSIVE: &str = "§5 Immersive audio, cinema, volumetric, haptics, HDR"; const S_BROADCAST: &str = "§5 Broadcast and streaming codecs"; +const S_REFINEMENT: &str = "§5 Refinement capabilities (staged delivery)"; /// The date the derivation operations were registered. const DERIVATIONS_REGISTERED: &str = "2026-09-28"; @@ -375,8 +390,8 @@ pub struct Registration { pub status: RegistrationStatus, /// The media class, for a payload or companion capability. pub media: Option, - /// `standalone`, `companion` or `enhancement`, for a payload or companion - /// capability ([`capability_role`]). + /// `standalone`, `companion`, `enhancement` or `refinement`, for a payload, + /// companion or refinement capability ([`capability_role`]). pub role: Option<&'static str>, pub tier: Option, pub licence: Option, @@ -435,7 +450,13 @@ pub fn registrations() -> Vec { /// not register. pub fn capability_kind(cap: &str) -> Option { if is_known_capability(cap) { - Some(if is_companion_only(cap) { Kind::Companion } else { Kind::Payload }) + Some(if is_companion_only(cap) { + Kind::Companion + } else if is_refinement_only(cap) { + Kind::Refinement + } else { + Kind::Payload + }) } else if derivation::is_derivation_operation(cap) { Some(Kind::Derivation) } else if RECORDS.contains(&cap) { @@ -463,8 +484,10 @@ pub fn is_candidate(cap: &str) -> bool { /// Whether a sink may dispatch or select `cap`, as far as this revision's /// registry decides. False for every string this revision registers as something /// that is not a pinned payload: a companion-only capability (applied with a base, -/// never on its own), a candidate (SPEC §5, *Candidate capabilities*), a record and -/// a derivation (neither names a payload format). True for a registered payload +/// never on its own), a refinement-only capability (applied to the stage below it, +/// never on its own; `extensions/staged-delivery.md` §4 processes it), a candidate +/// (SPEC §5, *Candidate capabilities*), a record and a derivation (neither names a +/// payload format). True for a registered payload /// capability whose payload is pinned, and for a string this revision does not /// register: a later MINOR revision may register it (SPEC §8), and whether a sink /// can decode it is the sink's to say through what it advertises (SPEC §4, step 3). @@ -479,7 +502,7 @@ pub fn is_dispatchable(cap: &str) -> bool { match capability_kind(cap) { None => true, Some(Kind::Payload) => !is_candidate(cap), - Some(Kind::Companion | Kind::Derivation | Kind::Record) => false, + Some(Kind::Companion | Kind::Refinement | Kind::Derivation | Kind::Record) => false, } } @@ -855,13 +878,14 @@ pub fn vocab_v2_ttl() -> String { o } -/// The registered strings a manifest may name as its `capability`: payload and -/// companion capabilities whose status is registered (a candidate MUST NOT be -/// emitted). A `WAI1` `fallback` additionally excludes the companions. +/// The registered strings a manifest may name as its `capability`: payload, +/// companion and refinement capabilities whose status is registered (a candidate +/// MUST NOT be emitted). A `WAI1` `fallback` additionally excludes the companions +/// and the refinements. fn emittable() -> Vec { registrations() .into_iter() - .filter(|r| matches!(r.kind, Kind::Payload | Kind::Companion) && r.status == RegistrationStatus::Registered) + .filter(|r| matches!(r.kind, Kind::Payload | Kind::Companion | Kind::Refinement) && r.status == RegistrationStatus::Registered) .collect() } @@ -894,10 +918,11 @@ pub fn vocab_v2_shapes() -> String { o.push_str("@prefix xsd: .\n\n"); o.push_str("# A WAI manifest, typed or not: every manifest has a wai (version) field. A WAI1 manifest names a\n"); o.push_str("# capability, a WAI2 manifest a renditions list, and never both. The capability is a\n"); - o.push_str("# registered payload or companion capability that is not a candidate; the fallback a\n"); - o.push_str("# registered payload capability that is not a candidate. (A WAI1 naming a companion-only\n"); - o.push_str("# capability parses but does not conform; the reference implementation's role check\n"); - o.push_str("# reports it.) media is informational, and a reader accepts a value it does not know\n"); + o.push_str("# registered payload, companion or refinement capability that is not a candidate; the\n"); + o.push_str("# fallback a registered payload capability that is not a candidate. (A WAI1 naming a\n"); + o.push_str("# companion-only capability, or a refinement-only one without refines, parses but does\n"); + o.push_str("# not conform; the reference implementation's role check reports it.) media is\n"); + o.push_str("# informational, and a reader accepts a value it does not know\n"); o.push_str("# (SPEC sections 3 and 8), so only its form is checked.\n"); o.push_str("wai:ManifestShape a sh:NodeShape ;\n"); o.push_str(" sh:targetClass wai:Manifest ;\n"); @@ -908,9 +933,9 @@ pub fn vocab_v2_shapes() -> String { o.push_str(" sh:property [ sh:path wai:media ; sh:nodeKind sh:IRI ; sh:maxCount 1 ] ;\n"); o.push_str(" sh:property [ sh:path wai:intent ; sh:datatype xsd:string ; sh:minCount 1 ; sh:maxCount 1 ] ;\n"); o.push_str(" sh:xone (\n [ sh:property [ sh:path wai:capability ; sh:minCount 1 ] ]\n [ sh:property [ sh:path wai:renditions ; sh:minCount 1 ] ]\n ) .\n\n"); - o.push_str("# A WAI2 component, in either role: its capability is a registered payload or companion\n"); - o.push_str("# capability that is not a candidate, and the base a companion names is a registered\n"); - o.push_str("# payload capability that is not a candidate.\n"); + o.push_str("# A WAI2 component, in any role: its capability is a registered payload, companion or\n"); + o.push_str("# refinement capability that is not a candidate, and the base a companion names is a\n"); + o.push_str("# registered payload capability that is not a candidate.\n"); o.push_str("wai:ComponentShape a sh:NodeShape ;\n"); o.push_str(" sh:targetClass wai:Component ;\n"); o.push_str(" sh:targetObjectsOf wai:renditions ;\n"); @@ -1073,10 +1098,11 @@ mod tests { } assert!(!is_candidate("wai.image.nope")); assert_eq!(capability_status("wai.image.nope"), None); - // Among registered payload and companion capabilities, dispatchable is - // exactly: not companion-only, not a candidate. + // Among registered payload, companion and refinement capabilities, + // dispatchable is exactly: not companion-only, not refinement-only, not a + // candidate. for &cap in known_capabilities() { - assert_eq!(is_dispatchable(cap), !is_companion_only(cap) && !is_candidate(cap), "{cap}"); + assert_eq!(is_dispatchable(cap), !is_companion_only(cap) && !is_refinement_only(cap) && !is_candidate(cap), "{cap}"); } // Records and derivations name no payload format: never dispatched. for &cap in RECORDS { @@ -1266,7 +1292,7 @@ mod tests { for r in registrations() { let iri = format!("wai:{}\n", r.capability); let in_shapes = shapes.contains(&format!(" {iri}")); - let emittable = matches!(r.kind, Kind::Payload | Kind::Companion) && r.status == RegistrationStatus::Registered; + let emittable = matches!(r.kind, Kind::Payload | Kind::Companion | Kind::Refinement) && r.status == RegistrationStatus::Registered; assert_eq!(in_shapes, emittable, "{}", r.capability); assert!(vocab_v2_ttl().contains(&format!("wai:{} a wai:Capability", r.capability)), "{}", r.capability); } diff --git a/wai/wai-rs/src/container.rs b/wai/wai-rs/src/container.rs index 1d5223d6d..f069be124 100644 --- a/wai/wai-rs/src/container.rs +++ b/wai/wai-rs/src/container.rs @@ -753,6 +753,12 @@ pub struct Manifest { pub conditioning: Conditioning, #[serde(default, skip_serializing_if = "is_none")] pub security: Option, + /// `refines` (`extensions/staged-delivery.md` §3): what a refinement-only + /// capability's payload refines, by content hash. Kept as the JSON it is, so a + /// malformed value still parses ([`Refines::from_value`] reports it, + /// `refines-malformed`). + #[serde(default, skip_serializing_if = "is_none")] + pub refines: Option, #[serde(default)] pub target: Value, /// Members this revision does not define, kept so they survive a round trip. @@ -775,6 +781,7 @@ impl Manifest { model_requirement, conditioning, security: None, + refines: None, target: Value::Null, extra: Map::new(), } @@ -784,6 +791,11 @@ impl Manifest { self.target = target; self } + /// Set `refines` (`extensions/staged-delivery.md` §3). + pub fn with_refines(mut self, refines: &Refines) -> Self { + self.refines = Some(refines.to_value()); + self + } /// Set the `wai` version string. pub fn with_wai(mut self, wai: impl Into) -> Self { self.wai = wai.into(); @@ -796,6 +808,226 @@ impl Manifest { } } +/// How the hashes of a `refines` member name objects: a content hash +/// (`jwp-receipts` §2.1, 64 lowercase hexadecimal digits) in a `WAI1` manifest, a +/// component `id` ([`is_component_id`]) in a `WAI2` entry, which names the entry +/// whose exploded envelope is the object (SPEC §7.1). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RefForm { + ContentHash, + ComponentId, +} + +impl RefForm { + fn accepts(self, s: &str) -> bool { + match self { + RefForm::ContentHash => s.len() == 64 && s.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)), + RefForm::ComponentId => is_component_id(s), + } + } +} + +/// `refines.prev` (`extensions/staged-delivery.md` §3): the producer each unit of +/// the span is expected to have — one for every unit, or runs in span order. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Prev { + One(String), + Runs(Vec), +} + +/// `count` consecutive units of a span whose producer is `producer`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PrevRun { + pub producer: String, + pub count: u32, +} + +/// `refines.span`: `count` consecutive units from `from`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Span { + pub from: u32, + pub count: u32, +} + +/// A well-formed `refines` member (`extensions/staged-delivery.md` §3). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Refines { + pub base: String, + pub prev: Prev, + pub layer: u8, + pub span: Option, +} + +/// Why a `refines` member is malformed (`refines-malformed`): what failed. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RefinesMalformed(pub &'static str); + +impl std::fmt::Display for RefinesMalformed { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "refines-malformed: {}", self.0) + } +} + +impl std::error::Error for RefinesMalformed {} + +impl Refines { + /// Read `refines` as staged-delivery §3 defines it, with its references in `form`. + /// Members it does not define are ignored (SPEC §8). Malformed: a value that is + /// not an object; `base` or a producer that is not a string of `form`; `prev` + /// neither such a string nor a non-empty array of runs; a run that is not an + /// object of `producer` and an integer `count` ≥ 1; two adjacent runs with the + /// same producer, or an array of one run (a single producer is written as a + /// string); `layer` not an integer in 1..=255; at layer 1 a producer other than + /// `base`; `span` not an object of integer `from` ≥ 0 and `count` ≥ 1, or past + /// `u32`; with a span, run counts that do not sum to its `count`. Whether the + /// span lies within the base's units, and, with no span, whether the runs sum to + /// them, needs the base ([`Refines::check_units`]). + pub fn from_value(v: &Value, form: RefForm) -> Result { + let bad = |why: &'static str| RefinesMalformed(why); + let o = v.as_object().ok_or(bad("refines is not an object"))?; + let base = o.get("base").and_then(Value::as_str).filter(|b| form.accepts(b)).ok_or(bad("base is not a reference"))?; + let layer = o + .get("layer") + .and_then(Value::as_u64) + .filter(|l| (1..=255).contains(l)) + .ok_or(bad("layer is not an integer in 1..=255"))? as u8; + let span = match o.get("span") { + None => None, + Some(sp) => { + let so = sp.as_object().ok_or(bad("span is not an object"))?; + let from = so.get("from").and_then(Value::as_u64).and_then(|f| u32::try_from(f).ok()).ok_or(bad("span.from is not an integer ≥ 0 within u32"))?; + let count = so + .get("count") + .and_then(Value::as_u64) + .and_then(|c| u32::try_from(c).ok()) + .filter(|&c| c >= 1) + .ok_or(bad("span.count is not an integer ≥ 1 within u32"))?; + if from.checked_add(count).is_none() { + return Err(bad("the span runs past u32")); + } + Some(Span { from, count }) + } + }; + let prev = match o.get("prev") { + Some(Value::String(p)) if form.accepts(p) => Prev::One(p.clone()), + Some(Value::Array(a)) if !a.is_empty() => { + let mut runs = Vec::with_capacity(a.len().min(1 << 16)); + for r in a { + let ro = r.as_object().ok_or(bad("a prev run is not an object"))?; + let producer = ro.get("producer").and_then(Value::as_str).filter(|p| form.accepts(p)).ok_or(bad("a run's producer is not a reference"))?; + let count = ro + .get("count") + .and_then(Value::as_u64) + .and_then(|c| u32::try_from(c).ok()) + .filter(|&c| c >= 1) + .ok_or(bad("a run's count is not an integer ≥ 1 within u32"))?; + if runs.last().is_some_and(|l: &PrevRun| l.producer == producer) { + return Err(bad("two adjacent runs name the same producer")); + } + runs.push(PrevRun { producer: producer.to_string(), count }); + } + if runs.len() == 1 { + return Err(bad("prev is an array of one run; a single producer is written as a string")); + } + Prev::Runs(runs) + } + _ => return Err(bad("prev is neither a reference nor an array of runs")), + }; + let r = Refines { base: base.to_string(), prev, layer, span }; + if layer == 1 && r.producers().any(|p| p != r.base) { + return Err(bad("at layer 1 every producer prev names must be base")); + } + if let (Some(sp), Prev::Runs(runs)) = (span, &r.prev) { + if runs.iter().map(|x| x.count as u64).sum::() != sp.count as u64 { + return Err(bad("the run counts do not sum to the span's count")); + } + } + Ok(r) + } + + /// The JSON this member is written as: `base`, `prev`, `layer`, then `span` when + /// present. + pub fn to_value(&self) -> Value { + let mut o = Map::new(); + o.insert("base".into(), Value::from(self.base.clone())); + o.insert( + "prev".into(), + match &self.prev { + Prev::One(p) => Value::from(p.clone()), + Prev::Runs(runs) => Value::Array( + runs.iter() + .map(|r| { + let mut m = Map::new(); + m.insert("producer".into(), Value::from(r.producer.clone())); + m.insert("count".into(), Value::from(r.count)); + Value::Object(m) + }) + .collect(), + ), + }, + ); + o.insert("layer".into(), Value::from(self.layer)); + if let Some(sp) = self.span { + let mut m = Map::new(); + m.insert("from".into(), Value::from(sp.from)); + m.insert("count".into(), Value::from(sp.count)); + o.insert("span".into(), Value::Object(m)); + } + Value::Object(o) + } + + /// Every producer `prev` names, in span order. + pub fn producers(&self) -> impl Iterator { + let v: Vec<&str> = match &self.prev { + Prev::One(p) => vec![p.as_str()], + Prev::Runs(runs) => runs.iter().map(|r| r.producer.as_str()).collect(), + }; + v.into_iter() + } + + /// The units the layer covers, `(from, count)`, of a base of `base_units` units: + /// its span, or every unit. + pub fn units(&self, base_units: u32) -> (u32, u32) { + match self.span { + Some(sp) => (sp.from, sp.count), + None => (0, base_units), + } + } + + /// The checks that need the base's unit count (staged-delivery §3): the span lies + /// within the base's units, and the runs sum to the units the layer covers. + pub fn check_units(&self, base_units: u32) -> Result<(), RefinesMalformed> { + let (from, count) = self.units(base_units); + if from as u64 + count as u64 > base_units as u64 || count == 0 { + return Err(RefinesMalformed("the span lies outside the base's units")); + } + if let Prev::Runs(runs) = &self.prev { + if runs.iter().map(|x| x.count as u64).sum::() != count as u64 { + return Err(RefinesMalformed("the run counts do not sum to the units the layer covers")); + } + } + Ok(()) + } + + /// The producer `prev` gives for the `i`-th unit of the span (0-based), or + /// `None` past the runs. + pub fn producer_for(&self, i: u32) -> Option<&str> { + match &self.prev { + Prev::One(p) => Some(p), + Prev::Runs(runs) => { + let mut at = 0u64; + for r in runs { + at += r.count as u64; + if (i as u64) < at { + return Some(&r.producer); + } + } + None + } + } + } +} + pub struct Wai { pub manifest: Manifest, pub payload: Vec, @@ -922,6 +1154,12 @@ impl Wai { self.manifest.model_requirement.fallback.as_deref() } + /// The manifest's `refines`, read with content-hash references, or `None` when it + /// carries none (`extensions/staged-delivery.md` §3). + pub fn refines(&self) -> Option> { + self.manifest.refines.as_ref().map(|v| Refines::from_value(v, RefForm::ContentHash)) + } + /// SPEC §4 dispatch at a sink that advertises the capabilities `advertises` /// accepts and verifies pins with `verify` (SPEC §3.1). First, SPEC §6.4 step 2: /// a manifest that declares an encryption other than `"none"` is inert here @@ -941,6 +1179,10 @@ impl Wai { /// fallback, step 5 is skipped, and step 3 is unaffected. So an envelope that /// [`Self::check_component_roles`] reports for its fallback alone still /// decodes at a sink that advertises its capability; + /// - a refinement-only capability ([`crate::codecs::is_refinement_only`]) is + /// handled alike (SPEC §4): its payload refines the stage below it and is + /// processed by `extensions/staged-delivery.md` §4 (`crate::staged`), never + /// decoded here; /// - a candidate ([`crate::codecs::is_candidate`]) is treated as unsupported /// (SPEC §5, *Candidate capabilities*): as the capability, step 5 may still /// take a usable fallback; as the fallback, it is never taken; @@ -962,7 +1204,10 @@ impl Wai { return Err(encryption_inert(format!("{:?}", sec.encryption.as_deref().unwrap_or_default()))); } let primary = self.capability(); - if crate::codecs::is_companion_only(primary) { + // A companion-only payload is companion data, and a refinement-only payload + // refines the stage below it (staged-delivery §4 processes it): neither is + // decoded on its own, so step 5 is not taken either. + if crate::codecs::is_companion_only(primary) || crate::codecs::is_refinement_only(primary) { return Err(Inert::MissingCapability); } let mut first_refusal: Option = None; @@ -1040,6 +1285,12 @@ impl Wai { /// MUST NOT be the envelope's `capability` — nor its `fallback`, because /// dispatch step 5 would decode the payload as that capability on its own. /// The v1.1 counterpart is [`WaiMulti::check_component_roles`]. + /// + /// A refinement-only capability ([`crate::codecs::is_refinement_only`]) is carried + /// as a `WAI1` `capability` only with a well-formed `refines` and a `null` + /// `fallback`, and is never a `fallback`; a manifest whose capability is not + /// refinement-only carries no `refines` (`extensions/staged-delivery.md` §3; SPEC + /// §8). Checked in that order, after the companion rules. pub fn check_component_roles(&self) -> Result<(), RoleError> { let capability = self.capability(); if crate::codecs::is_companion_only(capability) { @@ -1050,7 +1301,21 @@ impl Wai { return Err(RoleError::CompanionOnlyAsFallback { capability: fb.to_string() }); } } - Ok(()) + let refinement = crate::codecs::is_refinement_only(capability); + if let Some(fb) = self.fallback() { + if crate::codecs::is_refinement_only(fb) { + return Err(RoleError::RefinementOnlyAsFallback { capability: fb.to_string() }); + } + } + match (refinement, self.refines()) { + (true, None) => Err(RoleError::RefinementWithoutRefines { capability: capability.to_string() }), + (true, Some(Err(e))) => Err(RoleError::RefinesMalformed { index: None, detail: e.0 }), + (true, Some(Ok(_))) if self.fallback().is_some() => { + Err(RoleError::RefinementWithFallback { capability: capability.to_string() }) + } + (false, Some(_)) => Err(RoleError::RefinesWithoutRefinement { capability: capability.to_string() }), + _ => Ok(()), + } } /// Check that this envelope names no candidate capability @@ -1280,6 +1545,22 @@ pub enum RoleError { /// A companion whose `companion_of_id` names a component whose capability is /// `capability`, not the companion's `companion_of`. CompanionIdCapabilityMismatch { index: usize, id: String, capability: String }, + /// v1.0: a refinement-only capability named as the `WAI1` `fallback`. It is never + /// decoded on its own. + RefinementOnlyAsFallback { capability: String }, + /// v1.0: a refinement-only `capability` with no `refines` + /// (`extensions/staged-delivery.md` §3). + RefinementWithoutRefines { capability: String }, + /// v1.0: a refinement-only `capability` with a `fallback`; its fallback MUST be + /// `null`. + RefinementWithFallback { capability: String }, + /// v1.0: `refines` on a manifest whose capability is not refinement-only. + RefinesWithoutRefinement { capability: String }, + /// A `refines` member that is malformed (`refines-malformed`): the `WAI2` + /// component's index, or `None` for a `WAI1` manifest. + RefinesMalformed { index: Option, detail: &'static str }, + /// v1.1: a refinement-only capability carried as a rendition or a companion. + RefinementOnlyAsComponent { index: usize, capability: String }, } impl std::fmt::Display for RoleError { @@ -1318,6 +1599,18 @@ impl std::fmt::Display for RoleError { f, "companion {index} names companion_of_id '{id}', which no component has"), RoleError::CompanionIdCapabilityMismatch { index, id, capability } => write!( f, "companion {index} names companion_of_id '{id}', whose capability {capability} is not its companion_of"), + RoleError::RefinementOnlyAsFallback { capability } => write!( + f, "{capability} is refinement-only and cannot be a WAI1 fallback; it is never decoded on its own"), + RoleError::RefinementWithoutRefines { capability } => write!( + f, "{capability} is refinement-only and its manifest carries no refines (extensions/staged-delivery.md \u{a7}3)"), + RoleError::RefinementWithFallback { capability } => write!( + f, "{capability} is refinement-only, so its fallback must be null"), + RoleError::RefinesWithoutRefinement { capability } => write!( + f, "refines is carried with {capability}, which is not refinement-only"), + RoleError::RefinesMalformed { index: None, detail } => write!(f, "refines-malformed: {detail}"), + RoleError::RefinesMalformed { index: Some(i), detail } => write!(f, "component {i}: refines-malformed: {detail}"), + RoleError::RefinementOnlyAsComponent { index, capability } => write!( + f, "component {index} ({capability}) is refinement-only and is not carried with role=\"refinement\""), } } } @@ -1414,7 +1707,11 @@ pub struct FramingError { impl std::fmt::Display for FramingError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let want = crate::codecs::pinned_int_payload(&self.capability).map(|m| String::from_utf8_lossy(&m).into_owned()); + let want = if crate::codecs::is_refinement_only(&self.capability) { + Some("WIR1".to_string()) + } else { + crate::codecs::pinned_int_payload(&self.capability).map(|m| String::from_utf8_lossy(&m).into_owned()) + }; write!(f, "the payload is not the {} payload SPEC \u{a7}5 pins {} to ({}: {}; extensions/integer-payloads.md)", want.as_deref().unwrap_or("?"), self.capability, self.code, self.detail) } @@ -1433,6 +1730,9 @@ impl std::error::Error for FramingError {} /// it is decrypted (SPEC §6.4), and its emit paths skip the check. pub fn check_int_payload_framing(capability: &str, payload: &[u8]) -> Result<(), FramingError> { use crate::int_wire as w; + if crate::codecs::is_refinement_only(capability) { + return check_refinement_framing(capability, payload); + } let parsed = match crate::codecs::pinned_int_payload(capability) { None => return Ok(()), Some(w::MAGIC_WIV1) => w::parse_wiv1(payload, Some((capability == crate::codecs::CAP_VIDEO_INT_HYPER) as u8)).map(|_| ()), @@ -1443,6 +1743,26 @@ pub fn check_int_payload_framing(capability: &str, payload: &[u8]) -> Result<(), parsed.map_err(|e| FramingError { capability: capability.to_string(), code: e.code(), detail: e.detail }) } +/// The framing check of a refinement-only capability's payload: `WIR1`, parsed for +/// the capability's unit kind (`extensions/staged-delivery.md` Appendix A.1). A build +/// without the `staged` feature has no `WIR1` parser, so it refuses to emit one +/// (`unsupported_capability`). +#[cfg(feature = "staged")] +fn check_refinement_framing(capability: &str, payload: &[u8]) -> Result<(), FramingError> { + let fail = |code: &'static str, detail: &'static str| FramingError { capability: capability.to_string(), code, detail }; + let kind = crate::staged::unit_kind(capability).ok_or(fail("unsupported_capability", "no unit kind for this refinement"))?; + crate::staged::wir1::parse(payload, kind).map(|_| ()).map_err(|e| fail(e.code(), e.detail)) +} + +#[cfg(not(feature = "staged"))] +fn check_refinement_framing(capability: &str, _: &[u8]) -> Result<(), FramingError> { + Err(FramingError { + capability: capability.to_string(), + code: "unsupported_capability", + detail: "this build cannot check a WIR1 payload (feature staged)", + }) +} + /// What an integer payload's header declares about its decode, read before any /// symbol is decoded and with no parameter set (`extensions/integer-payloads.md` §7). /// A sink applies its own size policy to it and refuses a payload past that policy @@ -2009,6 +2329,9 @@ impl WaiMulti { } for (index, m) in metas.iter().enumerate() { let capability = m.capability.clone(); + if crate::codecs::is_refinement_only(&m.capability) && m.role.as_deref() != Some("refinement") { + return Err(RoleError::RefinementOnlyAsComponent { index, capability }); + } let companion_only = crate::codecs::is_companion_only(&m.capability); if !m.is_companion() { if companion_only { diff --git a/wai/wai-rs/src/staged/mod.rs b/wai/wai-rs/src/staged/mod.rs index e6dabf6d7..2900728cf 100644 --- a/wai/wai-rs/src/staged/mod.rs +++ b/wai/wai-rs/src/staged/mod.rs @@ -7,3 +7,16 @@ pub mod wir1_arith; #[cfg(test)] mod tests; + +use crate::codecs::{CAP_AUDIO_INT_REFINE, CAP_IMAGE_INT_REFINE, CAP_SPLAT_INT_REFINE, CAP_VIDEO_INT_REFINE}; + +/// The unit kind of a residual refinement capability: what its `WIR1` payload's units +/// hold (`extensions/staged-delivery.md` §6.1). `None` for any other string. +pub fn unit_kind(cap: &str) -> Option { + match cap { + CAP_IMAGE_INT_REFINE | CAP_VIDEO_INT_REFINE => Some(wir1::UnitKind::Rgb8), + CAP_AUDIO_INT_REFINE => Some(wir1::UnitKind::Pcm), + CAP_SPLAT_INT_REFINE => Some(wir1::UnitKind::Attr), + _ => None, + } +} -- 2.54.0 (Apple Git-157) From 9aa98b03ce50bf0acac19324c822b21518a5aa3a Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 2 Oct 2026 00:11:45 -0400 Subject: [PATCH 04/36] wai: the stage chain applies layers per unit, in the extension's order staged::StageChain holds one base's units, each with its stage, producer, tier and current output, and runs staged-delivery 4.1 for every layer it is offered: steps 2-6 decide for the envelope (base present, refines against the base's units, not via a fallback, admitted, attested, payload well formed and within the cost limit), steps 7-10 for each unit (conflict, readiness, geometry, deadline). A unit is applied whole or not at all. Layers a unit is not yet ready for are held to the bound and released when its stage advances; the pairs a release makes ready run as one batch, so two conflicting layers released together are both refused and the unit frozen, whichever arrived first. A layer that conflicts with one that already passed freezes the unit at the stage it has. staged::StageSink holds layers whose base it has not decoded and releases them together when the base arrives. staged::tier composes a unit's tier as a pure function: the weakest step wins and entropy-consistency does not pass through a layer; a stage hash is given only at decode-equivalence. staged::schedule holds the live schedule: presentation times, the lead rule, the layers a throughput affords, and what a sink presents for a trace of arrivals. The extension's step order puts every envelope-scoped step before the unit steps, and the conflict check before readiness, so a layer that differs from one already applied is a conflict rather than a mismatch. Co-Authored-By: Claude Opus 5.5 --- wai/extensions/staged-delivery.md | 53 +- wai/wai-rs/src/staged/mod.rs | 820 +++++++++++++++++++++++++++++- wai/wai-rs/src/staged/schedule.rs | 108 ++++ wai/wai-rs/src/staged/tests.rs | 507 ++++++++++++++++++ wai/wai-rs/src/staged/tier.rs | 64 +++ 5 files changed, 1525 insertions(+), 27 deletions(-) create mode 100644 wai/wai-rs/src/staged/schedule.rs create mode 100644 wai/wai-rs/src/staged/tier.rs diff --git a/wai/extensions/staged-delivery.md b/wai/extensions/staged-delivery.md index 41cc4a3e3..dd748ff13 100644 --- a/wai/extensions/staged-delivery.md +++ b/wai/extensions/staged-delivery.md @@ -101,17 +101,17 @@ fails is the reason for the envelope (1–6) or for the unit (7–10). limit the sink applies, if it applies one (`cost-over-limit`); and every unit's stream decodes to its last symbol and ends canonically (`payload-malformed`). A payload malformed for one unit is malformed for all. -7. Let *P(u)* be the producer `prev` gives for *u*. *u* is **ready** when its stage is - `layer − 1` and its producer is *P(u)*. A unit whose stage is below `layer − 1` - keeps *R* held for it, and is evaluated again when its stage advances; at the - bound *R* is discarded for that unit (`layer-gap`). Any other unit is not ready - (`prev-mismatch`). -8. *u* is **conflicted** when it is frozen, or when an envelope with different - content, the same `base` and the same `layer`, whose producer for *u* is also - *P(u)*, has passed step 7 for *u* (earlier, or in the same batch, §4.4) - (`layer-conflict`). A conflicted unit is **frozen**: the sink **MUST** apply no - further layer to it, it keeps the output it had (a layer applied to it before the - conflict stays applied), and the sink **MUST** report the conflict. +7. Let *P(u)* be the producer `prev` gives for *u*. *u* is **conflicted** when it is + frozen, or when an envelope with different content, the same `base` and the same + `layer`, whose producer for *u* is also *P(u)*, has passed step 8 for *u*, earlier + or in the same batch (§4.4) (`layer-conflict`). A conflicted unit is **frozen**: + the sink **MUST** apply no further layer to it, it keeps the output it had (a layer + applied to it before the conflict stays applied), and the sink **MUST** report the + conflict. +8. *u* is **ready** when its stage is `layer − 1` and its producer is *P(u)*. A unit + whose stage is below `layer − 1` keeps *R* held for it, and is evaluated again when + its stage advances; at the bound *R* is discarded for that unit (`layer-gap`). Any + other unit is not ready (`prev-mismatch`). 9. *R*'s payload reads *u*'s current output in the shape its staged form needs (`geometry-mismatch`). 10. Under a live schedule, *R* arrived at least `lead_ms` before *u*'s presentation @@ -134,16 +134,16 @@ unit. | 4 | `base-not-admitted` | envelope | discarded | | 5 | `refinement-unattested` | envelope | discarded | | 6 | `payload-malformed`, `cost-over-limit` | envelope | discarded | -| 7 | `prev-mismatch` | unit | not applied to the unit | -| 7 | `layer-gap` | unit | held for the unit, then not applied | -| 8 | `layer-conflict` | unit | not applied; the unit is frozen | +| 7 | `layer-conflict` | unit | not applied; the unit is frozen | +| 8 | `prev-mismatch` | unit | not applied to the unit | +| 8 | `layer-gap` | unit | held for the unit, then not applied | | 9 | `geometry-mismatch` | unit | not applied to the unit | | 10 | `deadline-missed` | unit | not applied to the unit | The list is closed. Two conforming sinks that receive the same envelopes in the same order, at the same times, with the same receipts, hold bound and cost limit, report the same reasons per envelope and per unit and reach the same stages and producers. -Order changes the outcome only through steps 8 and 10. +Order changes the outcome only through steps 7 and 10. ### 4.3 Holding @@ -157,15 +157,16 @@ output as one refined in order. ### 4.4 Batches When the base an envelope names is decoded, the envelopes held for it are released -together; when a unit's stage advances, the envelopes held for that unit at the next -layer are released together. Each envelope released, or offered on arrival, passes -steps 1–7 alone. The (envelope, unit) pairs that are ready then form one **batch**: -step 8 compares every pair of the batch with the others and with the envelopes that -passed step 7 earlier, and only then do steps 9 and 10 and application run. So two -conflicting layers released together are both refused and the unit frozen, whichever -arrived first; a layer that conflicts with one already applied is refused, and the -unit frozen at the stage the applied layer gave it. Applying a batch can release -another, which is evaluated the same way. +together; when a unit's stage advances, the envelopes held for that unit are released +together. Each envelope released, or offered on arrival, passes steps 1–6, and for +each unit step 7 against the envelopes that passed step 8 earlier and step 8, alone. +The (envelope, unit) pairs that are ready then form one **batch**: step 7 compares +every pair of the batch with the others, and only then do steps 9 and 10 and +application run. So two conflicting layers released together are both refused and +the unit frozen, whichever arrived first; a layer that conflicts with one that passed +step 8 earlier is refused, and the unit frozen at the stage it has. When a unit is +frozen, every envelope held for it is refused for it (`layer-conflict`). Applying a +batch can release another, which is evaluated the same way. ## 5. The tier of a stage, and the stage hash @@ -237,7 +238,7 @@ the decode of a prefix. For the residual form that is every layer boundary. A layer **MAY** be published after its base, served only on request, or never. A sink **MUST** present the current stage of a unit without waiting for a layer it has not been told to expect. A later revision of a refinement is a new chain from the same -`base`, distinguished by `prev`. When two chains conflict, §4.1 step 8 applies. +`base`, distinguished by `prev`. When two chains conflict, §4.1 step 7 applies. ## 8. Live deadlines @@ -307,7 +308,7 @@ ignores the header. ## 14. Security considerations - A layer is not authenticated by its `refines`; §10 authenticates it. -- Deferral lets anyone mint a chain for a public base hash. §10 and §4.1 step 8 bound +- Deferral lets anyone mint a chain for a public base hash. §10 and §4.1 step 7 bound what a sink presents. A conflict between two attested chains is evidence that an authorized signer signed both, and the sink reports it. - A hold bound (§4.3) and a cost limit (Appendix A.5) are resource limits. A layer's diff --git a/wai/wai-rs/src/staged/mod.rs b/wai/wai-rs/src/staged/mod.rs index 2900728cf..21a805fb8 100644 --- a/wai/wai-rs/src/staged/mod.rs +++ b/wai/wai-rs/src/staged/mod.rs @@ -1,14 +1,28 @@ //! Staged delivery (`extensions/staged-delivery.md`): a base first, refinements //! after, each its own object bound to what it refines by content hash. +//! +//! [`StageSink`] runs the extension's §4.1 for every layer a sink receives: it holds +//! a layer whose base it has not decoded, and releases it when the base arrives. +//! [`StageChain`] holds one base's units (their stage, producer, tier and current +//! output) and applies layers to them per unit. Every outcome is a reason from the +//! closed list of §4.2 ([`StageRefusal`]), reported per envelope and per unit +//! ([`OfferReport`]). The residual form's payload is [`wir1`]; its predictors are +//! [`predict`]; tiers compose by [`tier`]; the live schedule is [`schedule`]. pub mod predict; +pub mod schedule; +pub mod tier; pub mod wir1; pub mod wir1_arith; #[cfg(test)] mod tests; -use crate::codecs::{CAP_AUDIO_INT_REFINE, CAP_IMAGE_INT_REFINE, CAP_SPLAT_INT_REFINE, CAP_VIDEO_INT_REFINE}; +use std::collections::BTreeMap; + +use crate::codecs::{admits, is_refinement_only, DeterminismTier, CAP_AUDIO_INT_REFINE, CAP_IMAGE_INT_REFINE, CAP_SPLAT_INT_REFINE, CAP_VIDEO_INT_REFINE}; +use crate::container::{RefForm, Refines, Wai}; +use wir1::{Below, Next, Wir1Cost, Wir1Refusal}; /// The unit kind of a residual refinement capability: what its `WIR1` payload's units /// hold (`extensions/staged-delivery.md` §6.1). `None` for any other string. @@ -20,3 +34,807 @@ pub fn unit_kind(cap: &str) -> Option { _ => None, } } + +/// Why a layer, or a layer for one unit, was not applied: the closed list of +/// staged-delivery §4.2, in the order of §4.1's steps. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[non_exhaustive] +pub enum StageRefusal { + RefinesMalformed, + CapabilityNotRefinement, + BaseAbsent, + BaseViaFallback, + BaseNotAdmitted, + RefinementUnattested, + PayloadMalformed, + CostOverLimit, + LayerConflict, + PrevMismatch, + LayerGap, + GeometryMismatch, + DeadlineMissed, +} + +impl StageRefusal { + /// Every reason, in §4.2's order. + pub const ALL: [StageRefusal; 13] = [ + Self::RefinesMalformed, Self::CapabilityNotRefinement, Self::BaseAbsent, Self::BaseViaFallback, + Self::BaseNotAdmitted, Self::RefinementUnattested, Self::PayloadMalformed, Self::CostOverLimit, + Self::LayerConflict, Self::PrevMismatch, Self::LayerGap, Self::GeometryMismatch, Self::DeadlineMissed, + ]; + + /// The token §4.2 names. + pub fn code(self) -> &'static str { + match self { + Self::RefinesMalformed => "refines-malformed", + Self::CapabilityNotRefinement => "capability-not-refinement", + Self::BaseAbsent => "base-absent", + Self::BaseViaFallback => "base-via-fallback", + Self::BaseNotAdmitted => "base-not-admitted", + Self::RefinementUnattested => "refinement-unattested", + Self::PayloadMalformed => "payload-malformed", + Self::CostOverLimit => "cost-over-limit", + Self::PrevMismatch => "prev-mismatch", + Self::LayerGap => "layer-gap", + Self::LayerConflict => "layer-conflict", + Self::GeometryMismatch => "geometry-mismatch", + Self::DeadlineMissed => "deadline-missed", + } + } + + /// The reason whose token is `code`. + pub fn from_code(code: &str) -> Option { + Self::ALL.into_iter().find(|r| r.code() == code) + } + + /// Whether the reason decides for the whole envelope (§4.1 steps 1–6) rather than + /// for one unit. + pub fn is_envelope_scoped(self) -> bool { + self < Self::LayerConflict + } +} + +/// What the sink verified of a layer's delivery receipt (§10): through acceptance over +/// the layer's envelope bytes, by inclusion alone (without the bytes), or nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReceiptEvidence { + DeliveredVerified, + InclusionVerified, + None, +} + +impl ReceiptEvidence { + /// `delivered-verified`, `inclusion-verified` or `none`. + pub fn token(self) -> &'static str { + match self { + Self::DeliveredVerified => "delivered-verified", + Self::InclusionVerified => "inclusion-verified", + Self::None => "none", + } + } + /// The evidence a token names. + pub fn from_token(s: &str) -> Option { + match s { + "delivered-verified" => Some(Self::DeliveredVerified), + "inclusion-verified" => Some(Self::InclusionVerified), + "none" => Some(Self::None), + _ => None, + } + } +} + +/// A unit's samples, in the element type of its kind. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Samples { + U8(Vec), + I16(Vec), +} + +impl Samples { + /// The canonical buffer of SPEC §7 *Digests*: the bytes for u8; for i16, each + /// sample as two little-endian bytes in order. + pub fn canonical_bytes(&self) -> Vec { + match self { + Samples::U8(v) => v.clone(), + Samples::I16(v) => v.iter().flat_map(|s| s.to_le_bytes()).collect(), + } + } + fn below(&self) -> Below<'_> { + match self { + Samples::U8(v) => Below::U8(v), + Samples::I16(v) => Below::I16(v), + } + } + fn len(&self) -> usize { + match self { + Samples::U8(v) => v.len(), + Samples::I16(v) => v.len(), + } + } +} + +/// One unit of a base's output as decoded: its shape `(d0, d1, d2)` (rows, columns +/// and channels of a picture; samples, 1 and channels of audio; planes and the planes' +/// sides of a splat attribute tensor) and its samples. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnitBuf { + pub shape: (u32, u32, u32), + pub samples: Samples, +} + +/// A unit's state (§1): its stage, its producer, whether it is frozen (§4.1 step 8), +/// its tier (§5) and its current output. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnitState { + pub stage: u8, + pub producer: [u8; 32], + pub frozen: bool, + pub tier: Option, + pub buf: UnitBuf, +} + +/// How long and how much a sink holds (§4.3). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HoldBound { + /// The most envelopes held at once. + pub max_envelopes: usize, + /// How long an envelope is held from its arrival. + pub max_age_ms: u64, +} + +/// A live schedule (§8): the units' clock, and the lead of each layer (`lead_ms[l]` +/// for layer `l + 1`; a layer past the list has lead 0). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Live { + pub clock: schedule::UnitClock, + pub lead_ms: Vec, +} + +/// What a sink applies to a base's layers besides §4.1: its hold bound, the cost limit +/// it applies to a layer's declared cost (Appendix A.5), and its live schedule. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ChainConfig { + pub bound: HoldBound, + pub cost_limit: Option, + pub live: Option, +} + +/// The outcome of offering, releasing or expiring one envelope: the reason for the +/// envelope, or per unit what happened. A refusal of the envelope names no unit. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OfferReport { + pub content_hash: [u8; 32], + /// An envelope-scoped reason (§4.1 steps 1–6), or `None`. + pub envelope: Option, + /// For `payload-malformed` and `cost-over-limit`, the `WIR1` code or the figure + /// (Appendix A.6); `None` otherwise. + pub detail: Option<&'static str>, + /// The envelope is held for its base (`base-absent` until the bound). + pub held_for_base: bool, + /// Units it was applied to, ascending. + pub applied: Vec, + /// Units it is held for (`layer-gap` at the bound), ascending. + pub held: Vec, + /// Units it was not applied to, with the reason, ascending by unit. + pub refused: Vec<(u32, StageRefusal)>, +} + +impl OfferReport { + fn new(content_hash: [u8; 32]) -> Self { + OfferReport { content_hash, envelope: None, detail: None, held_for_base: false, applied: vec![], held: vec![], refused: vec![] } + } + fn refuse(content_hash: [u8; 32], why: StageRefusal, detail: Option<&'static str>) -> Self { + OfferReport { envelope: Some(why), detail, ..Self::new(content_hash) } + } + fn sort(&mut self) { + self.applied.sort_unstable(); + self.held.sort_unstable(); + self.refused.sort_unstable(); + } +} + +/// Why an envelope could not be offered at all: it is not a `WAI1` this sink reads. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum OfferError { + /// The bytes are not a `WAI1` envelope. + NotWai1(String), + /// The manifest declares an encryption this sink cannot open (SPEC §6.4); a layer + /// is decrypted before §4.1 step 6 (§14). + Encrypted, +} + +impl std::fmt::Display for OfferError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + OfferError::NotWai1(e) => write!(f, "not a WAI1 envelope: {e}"), + OfferError::Encrypted => f.write_str("an encrypted envelope this sink cannot open"), + } + } +} + +impl std::error::Error for OfferError {} + +/// A layer that passed §4.1 steps 1–6 for a chain, kept for its conflict keys and +/// while units are held for it. +#[derive(Debug, Clone)] +struct Layer { + payload: Vec, + cap: String, + refines: Refines, + arrival_ms: u64, + /// The order in which layers passed step 6 (release order). + seq: u64, +} + +/// The base's identity and how it was decoded (§4.1 steps 2–4). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BaseInfo { + /// The base envelope's content hash. + pub content_hash: [u8; 32], + /// The capability the base was decoded through. + pub capability: String, + /// Whether that capability was the manifest's fallback. + pub via_fallback: bool, + /// The base step's tier (§5, [`tier::base_tier`]). + pub tier: Option, + /// Whether the sink verified a delivery receipt for the base (§10). + pub attested: bool, +} + +/// One base and its units (§1), and the layers applied or held for them. +#[derive(Debug, Clone)] +pub struct StageChain { + base: BaseInfo, + cfg: ChainConfig, + units: Vec, + layers: BTreeMap<[u8; 32], Layer>, + /// Units held for a layer (§4.1 step 8): `(layer hash, unit)`. + held: Vec<([u8; 32], u32)>, + /// Pairs that passed step 8: `(layer, unit, P(u))` → the layer's content hash. + seen: BTreeMap<(u8, u32, String), [u8; 32]>, + seq: u64, +} + +fn hex(h: &[u8; 32]) -> String { + h.iter().map(|b| format!("{b:02x}")).collect() +} + +impl StageChain { + /// A chain over a decoded base: `units` are the base's units in order (§2), each + /// at stage 0 with the base as its producer and the base step's tier. + pub fn new(base: BaseInfo, units: Vec, cfg: ChainConfig) -> Self { + let units = units + .into_iter() + .map(|buf| UnitState { stage: 0, producer: base.content_hash, frozen: false, tier: base.tier, buf }) + .collect(); + StageChain { base, cfg, units, layers: BTreeMap::new(), held: vec![], seen: BTreeMap::new(), seq: 0 } + } + + /// The base this chain refines. + pub fn base(&self) -> &BaseInfo { + &self.base + } + + /// The number of units. + pub fn n_units(&self) -> u32 { + self.units.len() as u32 + } + + /// Unit `u`'s state. + pub fn unit(&self, u: u32) -> Option<&UnitState> { + self.units.get(u as usize) + } + + /// Every unit's state, in order. + pub fn units(&self) -> &[UnitState] { + &self.units + } + + /// Unit `u`'s tier (§5). + pub fn tier(&self, u: u32) -> Option { + self.units.get(u as usize).and_then(|s| s.tier) + } + + /// Unit `u`'s stage hash, or `None` unless it holds `decode-equivalence` (§5). + pub fn unit_sha256(&self, u: u32) -> Option<[u8; 32]> { + let s = self.units.get(u as usize)?; + (s.tier == Some(DeterminismTier::DecodeEquivalence)).then(|| crate::pin::sha256(&s.buf.samples.canonical_bytes())) + } + + /// The stage hash of every unit, the SHA-256 of their stage hashes in order + /// (SPEC §7 *Digests*), or `None` unless every unit holds `decode-equivalence`. + pub fn clip_sha256(&self) -> Option<[u8; 32]> { + let mut cat = Vec::with_capacity(32 * self.units.len()); + for u in 0..self.n_units() { + cat.extend_from_slice(&self.unit_sha256(u)?); + } + Some(crate::pin::sha256(&cat)) + } + + /// The envelopes held, with the units each is held for. + pub fn held(&self) -> Vec<([u8; 32], u32)> { + self.held.clone() + } + + /// How many envelopes hold at least one unit. + pub fn held_envelopes(&self) -> usize { + self.held.iter().map(|(h, _)| *h).collect::>().len() + } + + /// §4.1 steps 2–6 for an envelope whose `refines` and capability passed step 1: + /// the envelope reason, or the layer, kept. + fn admit(&mut self, hash: [u8; 32], cap: &str, refines: Refines, payload: &[u8], arrival_ms: u64, evidence: ReceiptEvidence) -> Result<(), (StageRefusal, Option<&'static str>)> { + use StageRefusal::*; + if refines.base != hex(&self.base.content_hash) { + return Err((BaseAbsent, None)); + } + refines.check_units(self.n_units()).map_err(|_| (RefinesMalformed, None))?; + if self.base.via_fallback { + return Err((BaseViaFallback, None)); + } + if !admits(cap, &self.base.capability) { + return Err((BaseNotAdmitted, None)); + } + if self.base.attested && evidence != ReceiptEvidence::DeliveredVerified { + return Err((RefinementUnattested, None)); + } + let kind = unit_kind(cap).ok_or((PayloadMalformed, Some("unsupported_capability")))?; + let w = wir1::parse(payload, kind).map_err(|e| (PayloadMalformed, Some(e.code())))?; + let (_, count) = refines.units(self.n_units()); + if w.n_units != count { + return Err((PayloadMalformed, Some("geometry_mismatch"))); + } + if let Some(limit) = self.cfg.cost_limit { + let c = w.declared_cost(); + let over = if c.output_bytes > limit.output_bytes { + Some("output bytes") + } else if c.symbols > limit.symbols { + Some("symbols") + } else if c.working_bytes > limit.working_bytes { + Some("working bytes") + } else { + None + }; + if let Some(what) = over { + return Err((CostOverLimit, Some(what))); + } + } + w.check_streams().map_err(|(_, e)| (PayloadMalformed, Some(e.code())))?; + self.seq += 1; + self.layers.insert(hash, Layer { payload: payload.to_vec(), cap: cap.to_string(), refines, arrival_ms, seq: self.seq }); + Ok(()) + } + + /// The conflict key of unit `u` under layer `hash` (§4.1 step 7): the layer, the + /// unit, and the producer the layer's `prev` gives for it. + fn key(&self, hash: &[u8; 32], u: u32) -> (u8, u32, String) { + let l = &self.layers[hash]; + let (from, _) = l.refines.units(self.n_units()); + (l.refines.layer, u, l.refines.producer_for(u - from).unwrap_or_default().to_string()) + } + + /// §4.1 step 7 before the batch: unit `u` is frozen, or another envelope already + /// passed step 8 for `u` under the same key. + fn conflicted_before(&self, hash: &[u8; 32], u: u32) -> bool { + self.units[u as usize].frozen || self.seen.get(&self.key(hash, u)).is_some_and(|s| s != hash) + } + + /// §4.1 step 8 for unit `u` of layer `hash`: ready, held, or not ready. + fn readiness(&self, hash: &[u8; 32], u: u32) -> Readiness { + let l = &self.layers[hash]; + let (from, _) = l.refines.units(self.n_units()); + let Some(p) = l.refines.producer_for(u - from) else { return Readiness::Not }; + let s = &self.units[u as usize]; + let want = l.refines.layer - 1; + if s.stage == want && hex(&s.producer) == p { + Readiness::Ready + } else if s.stage < want { + Readiness::Held + } else { + Readiness::Not + } + } + + /// Evaluate a fresh layer's units (step 7), then run the batch of its ready units + /// and every batch that releases. + fn evaluate(&mut self, hash: [u8; 32], outside_held: usize, reports: &mut Reports) { + let l = &self.layers[&hash]; + let (from, count) = l.refines.units(self.n_units()); + let mut ready = Vec::new(); + let mut newly_held = Vec::new(); + for u in from..from + count { + if self.conflicted_before(&hash, u) { + self.freeze(u, reports); + reports.get(hash).refused.push((u, StageRefusal::LayerConflict)); + continue; + } + match self.readiness(&hash, u) { + Readiness::Ready => ready.push((hash, u)), + Readiness::Held => newly_held.push(u), + Readiness::Not => reports.get(hash).refused.push((u, StageRefusal::PrevMismatch)), + } + } + if !newly_held.is_empty() { + let already = self.held.iter().any(|(h, _)| *h == hash); + if !already && self.held_envelopes() + outside_held >= self.cfg.bound.max_envelopes { + for u in newly_held { + reports.get(hash).refused.push((u, StageRefusal::LayerGap)); + } + } else { + for u in newly_held { + if !self.held.contains(&(hash, u)) { + self.held.push((hash, u)); + } + reports.get(hash).held.push(u); + } + } + } + self.run_batches(ready, reports); + } + + /// Run a batch (§4.4) and every batch it releases. + fn run_batches(&mut self, mut batch: Vec<([u8; 32], u32)>, reports: &mut Reports) { + while !batch.is_empty() { + let advanced = self.run_batch(&batch, reports); + batch = self.release(&advanced, reports); + } + } + + /// One batch: step 7 over every pair, then steps 9 and 10 and application. + /// Returns the units whose stage advanced. + fn run_batch(&mut self, batch: &[([u8; 32], u32)], reports: &mut Reports) -> Vec { + // Step 7: a pair conflicts with a pair passed earlier (`seen`) or with another + // pair of this batch under the same key with different content, or its unit is + // frozen. + let keys: Vec<(u8, u32, String)> = batch.iter().map(|(h, u)| self.key(h, *u)).collect(); + let mut conflicted = vec![false; batch.len()]; + for i in 0..batch.len() { + let (h, u) = batch[i]; + if self.units[u as usize].frozen { + conflicted[i] = true; + } + if self.seen.get(&keys[i]).is_some_and(|s| *s != h) { + conflicted[i] = true; + } + for j in 0..batch.len() { + if i != j && keys[j] == keys[i] && batch[j].0 != h { + conflicted[i] = true; + } + } + } + for (i, &(h, u)) in batch.iter().enumerate() { + if conflicted[i] { + self.freeze(u, reports); + reports.get(h).refused.push((u, StageRefusal::LayerConflict)); + } else { + self.seen.entry(keys[i].clone()).or_insert(h); + } + } + let mut advanced = Vec::new(); + for (i, &(h, u)) in batch.iter().enumerate() { + if conflicted[i] { + continue; + } + match self.apply(&h, u) { + Ok(()) => { + reports.get(h).applied.push(u); + advanced.push(u); + } + Err(why) => reports.get(h).refused.push((u, why)), + } + } + advanced + } + + /// Steps 9 and 10 and application for one pair. + fn apply(&mut self, hash: &[u8; 32], u: u32) -> Result<(), StageRefusal> { + let l = &self.layers[hash]; + let kind = unit_kind(&l.cap).ok_or(StageRefusal::PayloadMalformed)?; + let w = wir1::parse(&l.payload, kind).map_err(|_| StageRefusal::PayloadMalformed)?; + let (from, _) = l.refines.units(self.n_units()); + let s = &self.units[u as usize]; + let below_ok = matches!((kind, &s.buf.samples), (wir1::UnitKind::Pcm, Samples::I16(_)) | (wir1::UnitKind::Rgb8 | wir1::UnitKind::Attr, Samples::U8(_))); + let shape = s.buf.shape; + if !below_ok || s.buf.samples.len() as u64 != shape.0 as u64 * shape.1 as u64 * shape.2 as u64 || !w.accepts_below(shape) { + return Err(StageRefusal::GeometryMismatch); + } + if let Some(live) = &self.cfg.live { + let lead = live.lead_ms.get(l.refines.layer as usize - 1).copied().unwrap_or(0); + if !schedule::on_time(&live.clock, u, l.arrival_ms, lead) { + return Err(StageRefusal::DeadlineMissed); + } + } + let next = wir1::apply_unit(&w, u - from, s.buf.samples.below(), shape).map_err(|e| match e.refusal { + Wir1Refusal::GeometryMismatch => StageRefusal::GeometryMismatch, + _ => StageRefusal::PayloadMalformed, + })?; + let layer = l.refines.layer; + let shape = (w.d0, w.d1, w.d2); + let s = &mut self.units[u as usize]; + s.buf = UnitBuf { shape, samples: match next { Next::U8(v) => Samples::U8(v), Next::I16(v) => Samples::I16(v) } }; + s.stage = layer; + s.producer = *hash; + s.tier = tier::compose(s.tier, &[Some(DeterminismTier::DecodeEquivalence)], &[true]); + Ok(()) + } + + /// Freeze unit `u`: no further layer applies to it, and every layer held for it is + /// refused for it (`layer-conflict`). + fn freeze(&mut self, u: u32, reports: &mut Reports) { + self.units[u as usize].frozen = true; + let (gone, keep): (Vec<_>, Vec<_>) = self.held.iter().copied().partition(|(_, x)| *x == u); + self.held = keep; + for (h, x) in gone { + reports.get(h).refused.push((x, StageRefusal::LayerConflict)); + } + } + + /// The pairs held for `advanced` units that a stage change decides: those now ready + /// form the next batch, in the order their layers were admitted; those no longer + /// waiting are refused (`prev-mismatch`). + fn release(&mut self, advanced: &[u32], reports: &mut Reports) -> Vec<([u8; 32], u32)> { + let mut batch = Vec::new(); + let mut keep = Vec::new(); + let held = std::mem::take(&mut self.held); + for (h, u) in held { + if !advanced.contains(&u) { + keep.push((h, u)); + continue; + } + if self.conflicted_before(&h, u) { + self.held.extend(keep.drain(..)); + self.freeze(u, reports); + keep = std::mem::take(&mut self.held); + reports.get(h).refused.push((u, StageRefusal::LayerConflict)); + continue; + } + match self.readiness(&h, u) { + Readiness::Ready => batch.push((h, u)), + Readiness::Held => keep.push((h, u)), + Readiness::Not => reports.get(h).refused.push((u, StageRefusal::PrevMismatch)), + } + } + self.held = keep; + batch.sort_by_key(|(h, u)| (self.layers[h].seq, *u)); + batch + } + + /// Offer a layer that passed step 1 to this chain at `now_ms` (§4.1 steps 2–10): + /// the envelope's report first, then a report for every held layer the offer + /// decided units of, in the order those layers were admitted. + #[allow(clippy::too_many_arguments)] + fn offer_checked(&mut self, hash: [u8; 32], cap: &str, refines: Refines, payload: &[u8], now_ms: u64, evidence: ReceiptEvidence, outside_held: usize) -> Vec { + let mut reports = Reports::new(hash); + if self.layers.contains_key(&hash) { + // The same envelope again: steps 2–6 passed before; step 7 decides again. + } else if let Err((why, detail)) = self.admit(hash, cap, refines, payload, now_ms, evidence) { + return vec![OfferReport::refuse(hash, why, detail)]; + } + self.evaluate(hash, outside_held, &mut reports); + reports.finish(&self.layers) + } + + /// Offer the envelope `bytes` at `now_ms` with the receipt `evidence` (§4.1). An + /// envelope that names another base is refused `base-absent`: [`StageSink`] holds + /// such envelopes. + pub fn offer(&mut self, bytes: &[u8], now_ms: u64, evidence: ReceiptEvidence) -> Result, OfferError> { + let (hash, cap, refines, payload) = match step1(bytes)? { + Ok(x) => x, + Err(r) => return Ok(vec![r]), + }; + Ok(self.offer_checked(hash, &cap, refines, &payload, now_ms, evidence, 0)) + } + + /// Release a batch of layers that passed step 1 at once (§4.4): each passes steps + /// 2–7 alone, then their ready pairs run as one batch. + fn offer_batch(&mut self, items: Vec) -> Vec { + let order: Vec<[u8; 32]> = items.iter().map(|p| p.hash).collect(); + let mut reports = Reports::new(order.first().copied().unwrap_or([0; 32])); + let mut ready = Vec::new(); + let mut refused = Vec::new(); + for p in items { + if !self.layers.contains_key(&p.hash) { + if let Err((why, detail)) = self.admit(p.hash, &p.cap, p.refines.clone(), &p.payload, p.arrival_ms, p.evidence) { + refused.push(OfferReport::refuse(p.hash, why, detail)); + continue; + } + } + reports.get(p.hash); + let l = &self.layers[&p.hash]; + let (from, count) = l.refines.units(self.n_units()); + for u in from..from + count { + if self.conflicted_before(&p.hash, u) { + self.freeze(u, &mut reports); + reports.get(p.hash).refused.push((u, StageRefusal::LayerConflict)); + continue; + } + match self.readiness(&p.hash, u) { + Readiness::Ready => ready.push((p.hash, u)), + Readiness::Held => { + self.held.push((p.hash, u)); + reports.get(p.hash).held.push(u); + } + Readiness::Not => reports.get(p.hash).refused.push((u, StageRefusal::PrevMismatch)), + } + } + } + self.run_batches(ready, &mut reports); + let mut out = reports.finish(&self.layers); + out.extend(refused); + let pos = |h: &[u8; 32]| order.iter().position(|x| x == h).unwrap_or(usize::MAX); + out.sort_by_key(|r| pos(&r.content_hash)); + out + } + + /// Discard what has been held past the bound at `now_ms` (§4.3): each unit a + /// layer is held for, with `layer-gap`. + pub fn expire(&mut self, now_ms: u64) -> Vec { + let mut reports = Reports::new([0; 32]); + let held = std::mem::take(&mut self.held); + let mut keep = Vec::new(); + for (h, u) in held { + let l = &self.layers[&h]; + if now_ms.saturating_sub(l.arrival_ms) > self.cfg.bound.max_age_ms { + reports.get(h).refused.push((u, StageRefusal::LayerGap)); + } else { + keep.push((h, u)); + } + } + self.held = keep; + reports.finish(&self.layers).into_iter().filter(|r| !r.refused.is_empty()).collect() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Readiness { + Ready, + Held, + Not, +} + +/// Reports under construction, by content hash, with the first-offered envelope's +/// report first. +struct Reports { + first: [u8; 32], + by: BTreeMap<[u8; 32], OfferReport>, +} + +impl Reports { + fn new(first: [u8; 32]) -> Self { + let mut by = BTreeMap::new(); + by.insert(first, OfferReport::new(first)); + Reports { first, by } + } + fn get(&mut self, h: [u8; 32]) -> &mut OfferReport { + self.by.entry(h).or_insert_with(|| OfferReport::new(h)) + } + /// The first envelope's report, then the others by the order their layers were + /// admitted. + fn finish(mut self, layers: &BTreeMap<[u8; 32], Layer>) -> Vec { + let mut out = Vec::new(); + if let Some(mut r) = self.by.remove(&self.first) { + r.sort(); + out.push(r); + } + let mut rest: Vec = self.by.into_values().collect(); + rest.sort_by_key(|r| layers.get(&r.content_hash).map(|l| l.seq).unwrap_or(u64::MAX)); + for mut r in rest { + r.sort(); + out.push(r); + } + out + } +} + +/// §4.1 step 1: the envelope's content hash, capability and `refines`, or its +/// report. `Err` when the bytes are not a `WAI1` this sink reads. +#[allow(clippy::type_complexity)] +fn step1(bytes: &[u8]) -> Result), OfferReport>, OfferError> { + let hash = crate::jwp_hash::content_hash(bytes); + let w = Wai::from_bytes(bytes).map_err(|e| OfferError::NotWai1(e.to_string()))?; + if w.manifest.security.as_ref().is_some_and(|s| !s.is_plaintext()) { + return Err(OfferError::Encrypted); + } + let refines = match w.manifest.refines.as_ref().map(|v| Refines::from_value(v, RefForm::ContentHash)) { + Some(Ok(r)) => r, + None | Some(Err(_)) => return Ok(Err(OfferReport::refuse(hash, StageRefusal::RefinesMalformed, None))), + }; + if !is_refinement_only(w.capability()) { + return Ok(Err(OfferReport::refuse(hash, StageRefusal::CapabilityNotRefinement, None))); + } + let cap = w.capability().to_string(); + Ok(Ok((hash, cap, refines, w.payload))) +} + +/// An envelope held for its base (§4.1 step 2). +#[derive(Debug, Clone)] +struct Pending { + hash: [u8; 32], + cap: String, + refines: Refines, + payload: Vec, + arrival_ms: u64, + evidence: ReceiptEvidence, +} + +/// A sink's stage sets: every base it decoded, and the layers it holds for bases it +/// has not (§4.1 step 2). Bases and layers may arrive in any order. +#[derive(Debug, Clone)] +pub struct StageSink { + cfg: ChainConfig, + chains: BTreeMap<[u8; 32], StageChain>, + pending: Vec, +} + +impl StageSink { + /// A sink holding no base, with the bound, cost limit and schedule every chain it + /// creates uses. + pub fn new(cfg: ChainConfig) -> Self { + StageSink { cfg, chains: BTreeMap::new(), pending: vec![] } + } + + /// The chain of the base whose content hash is `base`. + pub fn chain(&self, base: &[u8; 32]) -> Option<&StageChain> { + self.chains.get(base) + } + + /// Record a decoded base and release the layers held for it, together (§4.4). + pub fn add_base(&mut self, base: BaseInfo, units: Vec) -> Vec { + let hash = base.content_hash; + let mut chain = StageChain::new(base, units, self.cfg.clone()); + let hexed = hex(&hash); + let (mine, rest): (Vec, Vec) = std::mem::take(&mut self.pending).into_iter().partition(|p| p.refines.base == hexed); + self.pending = rest; + let reports = if mine.is_empty() { vec![] } else { chain.offer_batch(mine) }; + self.chains.insert(hash, chain); + reports + } + + /// Offer the envelope `bytes` at `now_ms` with the receipt `evidence` (§4.1). A + /// layer whose base this sink has not decoded is held for it, up to the bound + /// (§4.3). + pub fn offer(&mut self, bytes: &[u8], now_ms: u64, evidence: ReceiptEvidence) -> Result, OfferError> { + let (hash, cap, refines, payload) = match step1(bytes)? { + Ok(x) => x, + Err(r) => return Ok(vec![r]), + }; + let base = self.chains.keys().copied().find(|k| hex(k) == refines.base); + match base { + Some(b) => { + let outside = self.pending.len() + + self.chains.iter().filter(|(k, _)| **k != b).map(|(_, c)| c.held_envelopes()).sum::(); + Ok(self.chains.get_mut(&b).expect("present").offer_checked(hash, &cap, refines, &payload, now_ms, evidence, outside)) + } + None => { + if self.pending.iter().any(|p| p.hash == hash) { + let mut r = OfferReport::new(hash); + r.held_for_base = true; + return Ok(vec![r]); + } + let held_layers = self.pending.len() + self.chains.values().map(StageChain::held_envelopes).sum::(); + if held_layers >= self.cfg.bound.max_envelopes { + return Ok(vec![OfferReport::refuse(hash, StageRefusal::BaseAbsent, None)]); + } + self.pending.push(Pending { hash, cap, refines, payload, arrival_ms: now_ms, evidence }); + let mut r = OfferReport::new(hash); + r.held_for_base = true; + Ok(vec![r]) + } + } + } + + /// Discard what has been held past the bound at `now_ms` (§4.3): layers held for an + /// absent base (`base-absent`), then, chain by chain in base order, units held for a + /// layer (`layer-gap`). + pub fn expire(&mut self, now_ms: u64) -> Vec { + let age = self.cfg.bound.max_age_ms; + let (gone, keep): (Vec, Vec) = + std::mem::take(&mut self.pending).into_iter().partition(|p| now_ms.saturating_sub(p.arrival_ms) > age); + self.pending = keep; + let mut out: Vec = gone.into_iter().map(|p| OfferReport::refuse(p.hash, StageRefusal::BaseAbsent, None)).collect(); + for c in self.chains.values_mut() { + out.extend(c.expire(now_ms)); + } + out + } +} diff --git a/wai/wai-rs/src/staged/schedule.rs b/wai/wai-rs/src/staged/schedule.rs new file mode 100644 index 000000000..c0a7ce636 --- /dev/null +++ b/wai/wai-rs/src/staged/schedule.rs @@ -0,0 +1,108 @@ +//! The live schedule of a stage set (`extensions/staged-delivery.md` §8): when each +//! unit is presented, whether a layer arrived in time to be applied to it, how many +//! layers a sink can fetch before a unit is due, and what a sink presents for a trace +//! of arrivals. Pure integer functions of their inputs, so the corpus's `schedule.json` +//! is replayed exactly by the Rust and the Python verifiers. + +/// The timing of a stage set's units: unit `u` is presented at +/// `t0_ms + u·unit_ms + target_latency_ms`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct UnitClock { + pub t0_ms: u64, + pub unit_ms: u64, + pub target_latency_ms: u64, +} + +/// One layer a publisher offers: its index, its lead (the time before a unit's +/// presentation by which it must arrive), and its bytes as delivered. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct LayerPlan { + pub layer: u8, + pub lead_ms: u64, + pub bytes: u64, +} + +/// When unit `unit` is presented (saturating at `u64::MAX`). +pub fn presentation_ms(c: &UnitClock, unit: u32) -> u64 { + c.t0_ms.saturating_add((unit as u64).saturating_mul(c.unit_ms)).saturating_add(c.target_latency_ms) +} + +/// Whether a layer that arrived at `arrival_ms` with lead `lead_ms` is in time for +/// unit `unit`: it arrived at least `lead_ms` before the unit's presentation (§8). +pub fn on_time(c: &UnitClock, unit: u32, arrival_ms: u64, lead_ms: u64) -> bool { + arrival_ms.checked_add(lead_ms).is_some_and(|t| t <= presentation_ms(c, unit)) +} + +/// The milliseconds `bytes` take at `throughput_bps` bits per second, rounded up; +/// `u64::MAX` at a throughput of 0. +pub fn transfer_ms(bytes: u64, throughput_bps: u64) -> u64 { + if throughput_bps == 0 { + return u64::MAX; + } + let bits = (bytes as u128) * 8 * 1000; + u64::try_from(bits.div_ceil(throughput_bps as u128)).unwrap_or(u64::MAX) +} + +/// How many layers a sink fetching from `now_ms` at `throughput_bps` can apply to a +/// unit due at `due_ms`: the largest `k` such that, for every `j ≤ k`, layers `1..=j` +/// fetched in order arrive at least `plan[j].lead_ms` before `due_ms`. `plan` is in +/// layer order. Integer arithmetic only, so the answer is the same everywhere. +pub fn choose_layers(throughput_bps: u64, now_ms: u64, due_ms: u64, plan: &[LayerPlan]) -> u8 { + let mut total: u64 = 0; + let mut k: u8 = 0; + for p in plan { + total = total.saturating_add(p.bytes); + let arrives = now_ms.saturating_add(transfer_ms(total, throughput_bps)); + if arrives.checked_add(p.lead_ms).is_none_or(|t| t > due_ms) { + break; + } + k = k.saturating_add(1); + } + k +} + +/// What a sink presents for one unit of a trace: when, and at which stage. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Presented { + pub at_ms: u64, + pub stage: u8, + /// The stage the layers the trace holds for this unit would have reached had + /// each arrived in time. + pub expected: u8, +} + +/// What a sink presents for a trace (§8). `base_ms[u]` is when unit `u`'s base +/// arrived; `layers[l][u]`, when layer `l + 1` arrived for unit `u` (`None`: it +/// never did); `lead_ms[l]`, layer `l + 1`'s lead. A unit is presented at its +/// presentation time, or when its base arrives if that is later: the sink never waits +/// for a layer. Its stage is the longest run of layers `1, 2, …` each of which arrived +/// in time; a layer that arrived late is not applied, and no layer above it is. Its +/// expected stage is the longest run of layers the trace holds for it. +pub fn present(c: &UnitClock, base_ms: &[u64], layers: &[Vec>], lead_ms: &[u64]) -> Vec { + base_ms + .iter() + .enumerate() + .map(|(u, &b)| { + let unit = u as u32; + let at_ms = presentation_ms(c, unit).max(b); + let mut stage = 0u8; + let mut expected = 0u8; + let mut in_time = true; + for (l, arrivals) in layers.iter().enumerate() { + let Some(Some(t)) = arrivals.get(u) else { break }; + expected = expected.saturating_add(1); + in_time = in_time && on_time(c, unit, *t, lead_ms.get(l).copied().unwrap_or(0)); + if in_time { + stage = stage.saturating_add(1); + } + } + Presented { at_ms, stage, expected } + }) + .collect() +} + +/// The units a trace presents below the stage their layers would have reached on +/// time (§8): the count a sink keeps. +pub fn deadline_misses(p: &[Presented]) -> u64 { + p.iter().filter(|x| x.stage < x.expected).count() as u64 +} diff --git a/wai/wai-rs/src/staged/tests.rs b/wai/wai-rs/src/staged/tests.rs index 93ba9d9fb..c89cddde5 100644 --- a/wai/wai-rs/src/staged/tests.rs +++ b/wai/wai-rs/src/staged/tests.rs @@ -233,3 +233,510 @@ fn wir1_stream_ends() { assert_eq!(apply_unit(&w, 0, Below::I16(&[0; 12]), (2, 2, 3)).unwrap_err().code(), "geometry_mismatch"); assert_eq!(apply_unit(&w, 1, Below::U8(&below), (2, 2, 3)).unwrap_err().code(), "geometry_mismatch"); } + +// ───────────────────────── the stage chain (§4) ───────────────────────── + +use super::schedule::{present, presentation_ms, UnitClock}; +use super::tier::compose; +use super::{BaseInfo, ChainConfig, HoldBound, Live, OfferReport, ReceiptEvidence, Samples, StageChain, StageRefusal, StageSink, UnitBuf}; +use crate::codecs::{DeterminismTier, CAP_IMAGE_JPEG, CAP_VIDEO_INT_MOTION, CAP_VIDEO_INT_REFINE}; +use crate::container::{Conditioning, Manifest, ModelRequirement, Prev, PrevRun, RefForm, Refines, Span, Wai}; +use serde_json::json; + +const H: u32 = 4; +const W: u32 = 6; + +fn hexs(h: &[u8; 32]) -> String { + h.iter().map(|b| format!("{b:02x}")).collect() +} + +fn envelope(cap: &str, refines: Option, payload: Vec) -> Vec { + let mut m = Manifest::new("video", "replicate", ModelRequirement::new(cap), Conditioning::new("x")); + m.refines = refines; + Wai::new(m, payload).to_bytes().unwrap() +} + +fn frame(seed: u32) -> Vec { + picture(H as usize, W as usize, seed) +} + +/// A base of four frames, its envelope's hash, and the frames. +fn base4() -> (Vec, BaseInfo, Vec) { + let env = envelope(CAP_VIDEO_INT_MOTION, None, b"a base payload".to_vec()); + let info = BaseInfo { + content_hash: crate::jwp_hash::content_hash(&env), + capability: CAP_VIDEO_INT_MOTION.into(), + via_fallback: false, + tier: Some(DeterminismTier::DecodeEquivalence), + attested: false, + }; + let units = (0..4).map(|f| UnitBuf { shape: (H, W, 3), samples: Samples::U8(frame(f)) }).collect(); + (env, info, units) +} + +fn cfg() -> ChainConfig { + ChainConfig { bound: HoldBound { max_envelopes: 8, max_age_ms: 1000 }, cost_limit: None, live: None } +} + +/// A residual layer over `belows` (units `from..from+belows.len()`) toward frames of +/// `seed`, with `prev` as given. +fn layer(base: &[u8; 32], prev: Prev, layer: u8, span: Option, belows: &[Vec], seed: u32) -> Vec { + let b: Vec = belows.iter().map(|v| Below::U8(v)).collect(); + let targets: Vec> = (0..belows.len()).map(|i| frame(seed + i as u32)).collect(); + let t: Vec = targets.iter().map(|v| Below::U8(v)).collect(); + let (payload, _) = encode(UnitKind::Rgb8, &b, (H, W, 3), &t, (H, W, 3), ¶ms(3, Predictor::Identity, 31, false)).unwrap(); + let r = Refines { base: hexs(base), prev, layer, span }; + envelope(CAP_VIDEO_INT_REFINE, Some(r.to_value()), payload) +} + +fn one(base: &[u8; 32]) -> Prev { + Prev::One(hexs(base)) +} + +fn h(env: &[u8]) -> [u8; 32] { + crate::jwp_hash::content_hash(env) +} + +fn unit_frame(c: &StageChain, u: u32) -> Vec { + match &c.unit(u).unwrap().buf.samples { + Samples::U8(v) => v.clone(), + Samples::I16(_) => panic!(), + } +} + +/// Test 1: each malformation of §3 is `refines-malformed`, and a well-formed member +/// reads back as written. +#[test] +fn refines_check_rejects_each_malformation() { + let a = "a".repeat(64); + let b = "b".repeat(64); + let read = |v: serde_json::Value| Refines::from_value(&v, RefForm::ContentHash); + let ok = json!({"base": a, "prev": [{"producer": a, "count": 2}, {"producer": b, "count": 1}], "layer": 2, "span": {"from": 1, "count": 3}}); + let r = read(ok.clone()).unwrap(); + assert_eq!(r.to_value(), ok, "a well-formed member reads back as written"); + assert_eq!(r.producer_for(0), Some(a.as_str())); + assert_eq!(r.producer_for(2), Some(b.as_str())); + assert_eq!(r.producer_for(3), None); + assert!(read(json!({"base": a, "prev": a, "layer": 1})).is_ok()); + assert!(read(json!({"base": a, "prev": a, "layer": 1, "unknown": [1, 2]})).is_ok(), "an undefined member is ignored"); + let bad = [ + json!({"base": a.to_uppercase(), "prev": a, "layer": 1}), + json!({"base": &a[..63], "prev": &a[..63], "layer": 1}), + json!({"base": a, "prev": a, "layer": 0}), + json!({"base": a, "prev": a, "layer": 256}), + json!({"base": a, "prev": b, "layer": 1}), + json!({"base": a, "prev": a, "layer": 2, "span": {"from": 0, "count": 0}}), + json!({"base": a, "prev": [{"producer": a, "count": 2}, {"producer": b, "count": 2}], "layer": 2, "span": {"from": 0, "count": 3}}), + json!({"base": a, "prev": [{"producer": a, "count": 1}, {"producer": a, "count": 1}], "layer": 2}), + json!({"base": a, "prev": [{"producer": a, "count": 2}], "layer": 2}), + json!({"base": a, "prev": [], "layer": 2}), + json!({"base": a, "prev": a, "layer": "2"}), + json!({"base": a, "prev": a, "layer": 2.0}), + json!({"base": 7, "prev": a, "layer": 2}), + json!({"base": a, "prev": {"producer": a}, "layer": 2}), + json!({"base": a, "prev": [{"producer": a, "count": 1.5}, {"producer": b, "count": 1}], "layer": 2}), + json!({"base": a, "prev": a, "layer": 2, "span": [0, 1]}), + json!({"base": a, "prev": a, "layer": 2, "span": {"from": -1, "count": 1}}), + json!({"base": a, "prev": a, "layer": 2, "span": {"from": u32::MAX, "count": 2}}), + json!({"prev": a, "layer": 1}), + json!([a, a, 1]), + ]; + for (i, v) in bad.iter().enumerate() { + assert!(read(v.clone()).is_err(), "case {i} is malformed: {v}"); + } + // The checks that need the base: a span past the base, runs not summing to it. + let r = read(json!({"base": a, "prev": a, "layer": 2, "span": {"from": 3, "count": 2}})).unwrap(); + assert!(r.check_units(4).is_err() && r.check_units(5).is_ok()); + let r = read(json!({"base": a, "prev": [{"producer": a, "count": 2}, {"producer": b, "count": 1}], "layer": 2})).unwrap(); + assert!(r.check_units(4).is_err() && r.check_units(3).is_ok()); + // A component id is a reference in a WAI2 entry, and a hash is not required there. + assert!(Refines::from_value(&json!({"base": "base", "prev": "base", "layer": 1}), RefForm::ComponentId).is_ok()); + assert!(Refines::from_value(&json!({"base": "Base", "prev": "Base", "layer": 1}), RefForm::ComponentId).is_err()); +} + +/// The WAI1 role rules (SPEC §3, §8) and dispatch (SPEC §4): a refinement-only +/// capability carries well-formed `refines` and no fallback, is never a fallback, and +/// is never dispatched; `refines` sits only on a refinement-only capability. +#[test] +fn refinement_roles_v1_and_dispatch() { + use crate::container::RoleError; + let a = "a".repeat(64); + let good = json!({"base": a, "prev": a, "layer": 1}); + let mk = |cap: &str, fb: Option<&str>, refines: Option| { + let mut mr = ModelRequirement::new(cap); + if let Some(f) = fb { + mr = mr.with_fallback(f); + } + let mut m = Manifest::new("video", "replicate", mr, Conditioning::new("x")); + m.refines = refines; + Wai::new(m, vec![1, 2, 3]) + }; + assert!(mk(CAP_VIDEO_INT_REFINE, None, Some(good.clone())).check_component_roles().is_ok()); + assert!(matches!(mk(CAP_VIDEO_INT_REFINE, None, None).check_component_roles(), Err(RoleError::RefinementWithoutRefines { .. }))); + assert!(matches!(mk(CAP_VIDEO_INT_REFINE, None, Some(json!({"base": a}))).check_component_roles(), Err(RoleError::RefinesMalformed { index: None, .. }))); + assert!(matches!(mk(CAP_VIDEO_INT_REFINE, Some(CAP_VIDEO_INT_MOTION), Some(good.clone())).check_component_roles(), Err(RoleError::RefinementWithFallback { .. }))); + assert!(matches!(mk(CAP_VIDEO_INT_MOTION, Some(CAP_VIDEO_INT_REFINE), None).check_component_roles(), Err(RoleError::RefinementOnlyAsFallback { .. }))); + assert!(matches!(mk(CAP_VIDEO_INT_MOTION, None, Some(good.clone())).check_component_roles(), Err(RoleError::RefinesWithoutRefinement { .. }))); + // Dispatch: never taken, at a sink that advertises everything, and step 5 is not + // taken either. + let w = mk(CAP_VIDEO_INT_REFINE, Some(CAP_VIDEO_INT_MOTION), Some(good)); + assert_eq!(w.dispatch_verified(|_| true, crate::container::no_parameter_sets()).unwrap_err().code(), "missing_capability"); + // A manifest round-trips its refines through the envelope bytes. + let bytes = mk(CAP_VIDEO_INT_REFINE, None, Some(json!({"base": a, "prev": a, "layer": 1}))).to_bytes().unwrap(); + let back = Wai::from_bytes(&bytes).unwrap(); + assert_eq!(back.refines().unwrap().unwrap().layer, 1); + // A malformed refines still parses. + let bytes = mk(CAP_VIDEO_INT_REFINE, None, Some(json!("not an object"))).to_bytes().unwrap(); + assert!(Wai::from_bytes(&bytes).unwrap().refines().unwrap().is_err()); +} + +fn codes(r: &OfferReport) -> Vec<(u32, &'static str)> { + r.refused.iter().map(|(u, w)| (*u, w.code())).collect() +} + +/// Test 2: one fixture per §4.2 reason, each failing exactly its step; and with two +/// faults, the earlier step is the reason. +#[test] +fn offer_order_of_checks() { + let (_, info, units) = base4(); + let b = info.content_hash; + let below: Vec> = (0..4).map(frame).collect(); + let l1 = layer(&b, one(&b), 1, None, &below, 50); + let env_reason = |c: &mut StageChain, env: &[u8], ev: ReceiptEvidence| c.offer(env, 0, ev).unwrap()[0].envelope.map(|r| r.code()); + let fresh = |info: BaseInfo, cfg: ChainConfig| StageChain::new(info, units.clone(), cfg); + let dv = ReceiptEvidence::DeliveredVerified; + // 1: refines malformed; capability not refinement. + let mut bad_refines = Wai::from_bytes(&l1).unwrap(); + bad_refines.manifest.refines = Some(json!({"base": hexs(&b), "prev": hexs(&b), "layer": 0})); + let bad_refines = bad_refines.to_bytes().unwrap(); + assert_eq!(env_reason(&mut fresh(info.clone(), cfg()), &bad_refines, dv), Some("refines-malformed")); + let mut not_ref = Wai::from_bytes(&l1).unwrap(); + not_ref.manifest.model_requirement.capability = CAP_VIDEO_INT_MOTION.into(); + let not_ref = not_ref.to_bytes().unwrap(); + assert_eq!(env_reason(&mut fresh(info.clone(), cfg()), ¬_ref, dv), Some("capability-not-refinement")); + // 2: another base. + let other = layer(&[7; 32], Prev::One(hexs(&[7; 32])), 1, None, &below, 50); + assert_eq!(env_reason(&mut fresh(info.clone(), cfg()), &other, dv), Some("base-absent")); + // 2: a span past the base's units. + let past = layer(&b, one(&b), 1, Some(Span { from: 3, count: 2 }), &below[..2], 50); + assert_eq!(env_reason(&mut fresh(info.clone(), cfg()), &past, dv), Some("refines-malformed")); + // 3: the base came through a fallback. + let fb = BaseInfo { via_fallback: true, ..info.clone() }; + assert_eq!(env_reason(&mut fresh(fb, cfg()), &l1, dv), Some("base-via-fallback")); + // 4: not admitted. + let jpeg = BaseInfo { capability: CAP_IMAGE_JPEG.into(), ..info.clone() }; + assert_eq!(env_reason(&mut fresh(jpeg.clone(), cfg()), &l1, dv), Some("base-not-admitted")); + // 5: attested base, layer verified by inclusion only (or not at all). + let att = BaseInfo { attested: true, ..info.clone() }; + assert_eq!(env_reason(&mut fresh(att.clone(), cfg()), &l1, ReceiptEvidence::InclusionVerified), Some("refinement-unattested")); + assert_eq!(env_reason(&mut fresh(att.clone(), cfg()), &l1, ReceiptEvidence::None), Some("refinement-unattested")); + assert_eq!(env_reason(&mut fresh(att, cfg()), &l1, dv), None); + // 6: a malformed payload, a cost past the limit. + let mut bad_payload = Wai::from_bytes(&l1).unwrap(); + bad_payload.payload[44] = 1; // the table's first entry + let bad_payload = bad_payload.to_bytes().unwrap(); + let r = &fresh(info.clone(), cfg()).offer(&bad_payload, 0, dv).unwrap()[0]; + assert_eq!((r.envelope.map(|x| x.code()), r.detail), (Some("payload-malformed"), Some("cdf_malformed"))); + let tight = ChainConfig { cost_limit: Some(super::wir1::Wir1Cost { output_bytes: 10, symbols: u64::MAX, working_bytes: u64::MAX }), ..cfg() }; + let r = &fresh(info.clone(), tight).offer(&l1, 0, dv).unwrap()[0]; + assert_eq!((r.envelope.map(|x| x.code()), r.detail), (Some("cost-over-limit"), Some("output bytes"))); + // 8: prev-mismatch: the same L1 again, to units already at stage 1. + let mut c = fresh(info.clone(), cfg()); + assert_eq!(c.offer(&l1, 0, dv).unwrap()[0].applied, vec![0, 1, 2, 3]); + assert_eq!(codes(&c.offer(&l1, 0, dv).unwrap()[0]), vec![(0, "prev-mismatch"), (1, "prev-mismatch"), (2, "prev-mismatch"), (3, "prev-mismatch")]); + // 8: prev-mismatch: an L2 whose prev names another producer than the units have. + let wrong = layer(&b, Prev::One(hexs(&[9; 32])), 2, None, &below, 61); + assert_eq!(codes(&c.offer(&wrong, 0, dv).unwrap()[0]), vec![(0, "prev-mismatch"), (1, "prev-mismatch"), (2, "prev-mismatch"), (3, "prev-mismatch")]); + // 7: a different L1 for units that passed step 8 under L1: conflict, frozen. + let l1b = layer(&b, one(&b), 1, None, &below, 60); + assert_eq!(codes(&c.offer(&l1b, 0, dv).unwrap()[0]), vec![(0, "layer-conflict"), (1, "layer-conflict"), (2, "layer-conflict"), (3, "layer-conflict")]); + assert!((0..4).all(|u| c.unit(u).unwrap().frozen)); + // 9: geometry: a layer coded for 2×2 frames over 4×6 frames. + let small: Vec> = (0..4).map(|f| picture(2, 2, f)).collect(); + let sb: Vec = small.iter().map(|v| Below::U8(v)).collect(); + let (pl, _) = encode(UnitKind::Rgb8, &sb, (2, 2, 3), &sb, (2, 2, 3), ¶ms(3, Predictor::Identity, 9, false)).unwrap(); + let geo = envelope(CAP_VIDEO_INT_REFINE, Some(Refines { base: hexs(&b), prev: one(&b), layer: 1, span: None }.to_value()), pl); + assert_eq!(codes(&fresh(info.clone(), cfg()).offer(&geo, 0, dv).unwrap()[0]), vec![(0, "geometry-mismatch"), (1, "geometry-mismatch"), (2, "geometry-mismatch"), (3, "geometry-mismatch")]); + // 10: deadline: units presented at 100, 140, 180, 220 with a 50 ms lead; arrival at 100. + let live = ChainConfig { live: Some(Live { clock: UnitClock { t0_ms: 100, unit_ms: 40, target_latency_ms: 0 }, lead_ms: vec![50] }), ..cfg() }; + let r = &fresh(info.clone(), live).offer(&l1, 100, dv).unwrap()[0]; + assert_eq!(codes(r), vec![(0, "deadline-missed"), (1, "deadline-missed")]); + assert_eq!(r.applied, vec![2, 3]); + // Two faults: the earlier step is the reason. + let mut two = Wai::from_bytes(&bad_payload).unwrap(); + two.manifest.refines = Some(Refines { base: hexs(&[7; 32]), prev: Prev::One(hexs(&[7; 32])), layer: 1, span: None }.to_value()); + let two = two.to_bytes().unwrap(); + assert_eq!(env_reason(&mut fresh(info.clone(), cfg()), &two, dv), Some("base-absent"), "step 2 before step 6"); + assert_eq!(env_reason(&mut fresh(jpeg, cfg()), &bad_payload, dv), Some("base-not-admitted"), "step 4 before step 6"); + let fb_att = BaseInfo { via_fallback: true, attested: true, ..info.clone() }; + assert_eq!(env_reason(&mut fresh(fb_att, cfg()), &l1, ReceiptEvidence::None), Some("base-via-fallback"), "step 3 before step 5"); + for r in StageRefusal::ALL { + assert_eq!(StageRefusal::from_code(r.code()), Some(r)); + } +} + +/// Test 3: per-frame layers 1 (span f,1; prev the base) apply, one unit each. +#[test] +fn per_frame_layers_do_not_conflict() { + let (_, info, units) = base4(); + let b = info.content_hash; + let mut c = StageChain::new(info, units, cfg()); + for f in 0..4u32 { + let l = layer(&b, one(&b), 1, Some(Span { from: f, count: 1 }), &[frame(f)], 70 + f); + let r = &c.offer(&l, 0, ReceiptEvidence::None).unwrap()[0]; + assert_eq!((r.envelope, r.applied.clone(), r.refused.len()), (None, vec![f], 0), "frame {f}"); + assert_eq!(c.unit(f).unwrap().producer, h(&l)); + } + assert!((0..4).all(|f| c.unit(f).unwrap().stage == 1 && !c.unit(f).unwrap().frozen)); +} + +/// Per-frame layers 1 over the base: the four frames after them, and their hashes. +fn per_frame_l1s(b: &[u8; 32], c: &mut StageChain) -> (Vec>, Vec<[u8; 32]>) { + let mut hs = Vec::new(); + for f in 0..4u32 { + let l = layer(b, one(b), 1, Some(Span { from: f, count: 1 }), &[frame(f)], 70 + f); + c.offer(&l, 0, ReceiptEvidence::None).unwrap(); + hs.push(h(&l)); + } + ((0..4).map(|f| unit_frame(c, f)).collect(), hs) +} + +fn runs(hs: &[[u8; 32]]) -> Prev { + Prev::Runs(hs.iter().map(|x| PrevRun { producer: hexs(x), count: 1 }).collect()) +} + +/// Test 4: a whole-GOP layer 2 whose prev is the run form over four per-frame +/// producers applies to all four units. +#[test] +fn whole_gop_l2_over_per_frame_l1_runs() { + let (_, info, units) = base4(); + let b = info.content_hash; + let mut c = StageChain::new(info, units, cfg()); + let (frames, hs) = per_frame_l1s(&b, &mut c); + let l2 = layer(&b, runs(&hs), 2, None, &frames, 90); + let r = &c.offer(&l2, 0, ReceiptEvidence::None).unwrap()[0]; + assert_eq!((r.envelope, r.applied.clone()), (None, vec![0, 1, 2, 3])); + assert!((0..4).all(|f| c.unit(f).unwrap().stage == 2 && c.unit(f).unwrap().producer == h(&l2))); +} + +/// Test 5: layer 1 for frame 2 missed its deadline; a run-form layer 2 applies to +/// frames 0, 1 and 3, and frame 2, still at stage 0, holds it until the bound +/// (`layer-gap`). +#[test] +fn mixed_stage_span_applies_ready_units() { + let (_, info, units) = base4(); + let b = info.content_hash; + let live = ChainConfig { live: Some(Live { clock: UnitClock { t0_ms: 0, unit_ms: 100, target_latency_ms: 1000 }, lead_ms: vec![10, 10] }), ..cfg() }; + let mut c = StageChain::new(info, units, live); + let mut hs = Vec::new(); + for f in 0..4u32 { + let l = layer(&b, one(&b), 1, Some(Span { from: f, count: 1 }), &[frame(f)], 70 + f); + // Frame 2 is presented at 1200; its layer 1 arrives at 1195, inside the lead. + let at = if f == 2 { 1195 } else { 0 }; + let r = &c.offer(&l, at, ReceiptEvidence::None).unwrap()[0]; + if f == 2 { + assert_eq!(codes(r), vec![(2, "deadline-missed")]); + } + hs.push(h(&l)); + } + let frames: Vec> = (0..4).map(|f| unit_frame(&c, f)).collect(); + let l2 = layer(&b, runs(&hs), 2, None, &frames, 90); + let r = &c.offer(&l2, 0, ReceiptEvidence::None).unwrap()[0]; + assert_eq!((r.applied.clone(), r.held.clone()), (vec![0, 1, 3], vec![2])); + assert_eq!(c.unit(2).unwrap().stage, 0); + let e = c.expire(2000); + assert_eq!(e.len(), 1); + assert_eq!(codes(&e[0]), vec![(2, "layer-gap")]); +} + +/// Test 6: layer 2 offered before layer 1 reaches the same stage hashes as in order. +#[test] +fn out_of_order_equals_in_order() { + let (_, info, units) = base4(); + let b = info.content_hash; + let below: Vec> = (0..4).map(frame).collect(); + let l1 = layer(&b, one(&b), 1, None, &below, 50); + let mut inorder = StageChain::new(info.clone(), units.clone(), cfg()); + inorder.offer(&l1, 0, ReceiptEvidence::None).unwrap(); + let mid: Vec> = (0..4).map(|f| unit_frame(&inorder, f)).collect(); + let l2 = layer(&b, Prev::One(hexs(&h(&l1))), 2, None, &mid, 60); + inorder.offer(&l2, 0, ReceiptEvidence::None).unwrap(); + let mut ooo = StageChain::new(info, units, cfg()); + let r = &ooo.offer(&l2, 0, ReceiptEvidence::None).unwrap()[0]; + assert_eq!(r.held, vec![0, 1, 2, 3]); + let rs = ooo.offer(&l1, 1, ReceiptEvidence::None).unwrap(); + assert_eq!(rs[0].applied, vec![0, 1, 2, 3]); + assert_eq!((rs[1].content_hash, rs[1].applied.clone()), (h(&l2), vec![0, 1, 2, 3]), "the release reports layer 2"); + assert_eq!(ooo.clip_sha256(), inorder.clip_sha256()); + assert!(ooo.clip_sha256().is_some()); + for u in 0..4 { + assert_eq!(ooo.unit(u), inorder.unit(u)); + } +} + +/// Test 7: a held layer 2 is discarded per unit at the bound (`layer-gap`), and a +/// layer that would exceed the bound's count is refused on arrival. +#[test] +fn bound_expires_to_layer_gap() { + let (_, info, units) = base4(); + let b = info.content_hash; + let below: Vec> = (0..4).map(frame).collect(); + let l1 = layer(&b, one(&b), 1, None, &below, 50); + let l2 = layer(&b, Prev::One(hexs(&h(&l1))), 2, None, &below, 60); + let mut c = StageChain::new(info.clone(), units.clone(), cfg()); + assert_eq!(c.offer(&l2, 0, ReceiptEvidence::None).unwrap()[0].held, vec![0, 1, 2, 3]); + assert!(c.expire(1000).is_empty(), "held to the bound, not past it"); + let e = c.expire(1001); + assert_eq!(codes(&e[0]), vec![(0, "layer-gap"), (1, "layer-gap"), (2, "layer-gap"), (3, "layer-gap")]); + assert!(c.held().is_empty()); + // Layer 1 later finds no held layer 2. + assert_eq!(c.offer(&l1, 1002, ReceiptEvidence::None).unwrap().len(), 1); + // A bound of one envelope: a second held layer is refused on arrival. + let one_env = ChainConfig { bound: HoldBound { max_envelopes: 1, max_age_ms: 1000 }, ..cfg() }; + let mut c = StageChain::new(info, units, one_env); + let l2b = layer(&b, Prev::One(hexs(&h(&l1))), 2, None, &below, 61); + assert_eq!(c.offer(&l2, 0, ReceiptEvidence::None).unwrap()[0].held, vec![0, 1, 2, 3]); + assert_eq!(codes(&c.offer(&l2b, 0, ReceiptEvidence::None).unwrap()[0]), vec![(0, "layer-gap"), (1, "layer-gap"), (2, "layer-gap"), (3, "layer-gap")]); +} + +/// Test 8: two layers 1 for one unit, both held for the base and released together: +/// neither is applied and the unit is frozen. One applied, then the other: the second +/// is refused, the unit frozen at stage 1, and no layer 2 applies to it. +#[test] +fn conflict_both_orders() { + let (_, info, units) = base4(); + let b = info.content_hash; + let la = layer(&b, one(&b), 1, Some(Span { from: 1, count: 1 }), &[frame(1)], 80); + let lb = layer(&b, one(&b), 1, Some(Span { from: 1, count: 1 }), &[frame(1)], 81); + assert_ne!(h(&la), h(&lb)); + // Both held for the base. + let mut sink = StageSink::new(cfg()); + assert!(sink.offer(&la, 0, ReceiptEvidence::None).unwrap()[0].held_for_base); + assert!(sink.offer(&lb, 1, ReceiptEvidence::None).unwrap()[0].held_for_base); + let rs = sink.add_base(info.clone(), units.clone()); + assert_eq!(rs.len(), 2); + assert_eq!((rs[0].content_hash, codes(&rs[0])), (h(&la), vec![(1, "layer-conflict")])); + assert_eq!((rs[1].content_hash, codes(&rs[1])), (h(&lb), vec![(1, "layer-conflict")])); + let c = sink.chain(&b).unwrap(); + assert_eq!((c.unit(1).unwrap().stage, c.unit(1).unwrap().frozen), (0, true)); + // The other order gives the same: the batch does not depend on arrival. + let mut sink2 = StageSink::new(cfg()); + sink2.offer(&lb, 0, ReceiptEvidence::None).unwrap(); + sink2.offer(&la, 1, ReceiptEvidence::None).unwrap(); + sink2.add_base(info.clone(), units.clone()); + assert_eq!(sink2.chain(&b).unwrap().units(), c.units()); + // Applied first, then the other. + let mut c = StageChain::new(info, units, cfg()); + assert_eq!(c.offer(&la, 0, ReceiptEvidence::None).unwrap()[0].applied, vec![1]); + let applied = c.unit(1).unwrap().clone(); + assert_eq!(codes(&c.offer(&lb, 0, ReceiptEvidence::None).unwrap()[0]), vec![(1, "layer-conflict")]); + let u1 = c.unit(1).unwrap(); + assert!(u1.frozen && u1.stage == 1 && u1.producer == h(&la) && u1.buf == applied.buf, "frozen at the applied layer"); + let l2 = layer(&b, Prev::One(hexs(&h(&la))), 2, Some(Span { from: 1, count: 1 }), &[unit_frame(&c, 1)], 85); + assert_eq!(codes(&c.offer(&l2, 0, ReceiptEvidence::None).unwrap()[0]), vec![(1, "layer-conflict")]); + assert_eq!(c.unit(1).unwrap().stage, 1); +} + +/// Test 9: a base decoded through a fallback refuses every layer. +#[test] +fn fallback_base_refuses_layers() { + let (_, info, units) = base4(); + let b = info.content_hash; + let below: Vec> = (0..4).map(frame).collect(); + let l1 = layer(&b, one(&b), 1, None, &below, 50); + let mut c = StageChain::new(BaseInfo { via_fallback: true, ..info }, units, cfg()); + let r = &c.offer(&l1, 0, ReceiptEvidence::DeliveredVerified).unwrap()[0]; + assert_eq!(r.envelope, Some(StageRefusal::BaseViaFallback)); + assert!((0..4).all(|u| c.unit(u).unwrap().stage == 0)); +} + +/// Test 10: with an attested base, a layer whose receipt verified by inclusion only +/// is never applied (`refinement-unattested`). +#[test] +fn inclusion_evidence_never_applies() { + let (_, info, units) = base4(); + let b = info.content_hash; + let below: Vec> = (0..4).map(frame).collect(); + let l1 = layer(&b, one(&b), 1, None, &below, 50); + let mut c = StageChain::new(BaseInfo { attested: true, ..info }, units, cfg()); + let r = &c.offer(&l1, 0, ReceiptEvidence::InclusionVerified).unwrap()[0]; + assert_eq!(r.envelope, Some(StageRefusal::RefinementUnattested)); + assert!(r.applied.is_empty()); + assert_eq!(c.offer(&l1, 0, ReceiptEvidence::DeliveredVerified).unwrap()[0].applied, vec![0, 1, 2, 3]); +} + +/// Test 11: the tier table, and a stage hash only at decode-equivalence. +#[test] +fn compose_table() { + use DeterminismTier::*; + let de = Some(DecodeEquivalence); + let ec = Some(EntropyConsistency); + assert_eq!(compose(de, &[de, de], &[true, true]), de); + assert_eq!(compose(de, &[], &[]), de); + assert_eq!(compose(ec, &[], &[]), ec, "a base alone keeps its tier"); + assert_eq!(compose(ec, &[de], &[true]), None, "entropy-consistency does not pass through a residual step"); + assert_eq!(compose(ec, &[de], &[false]), ec, "a step on symbols keeps the weakest"); + assert_eq!(compose(None, &[de], &[true]), None, "an unpinned integer base"); + assert_eq!(compose(de, &[None], &[true]), None); + assert_eq!(compose(de, &[ec], &[false]), ec); + // Base tiers by class and pin. + use super::tier::base_tier; + use crate::codecs::{CAP_IMAGE_JPEGAI, CAP_NEURAL_INT_HYPER}; + use crate::container::Prior; + assert_eq!(base_tier(CAP_VIDEO_INT_MOTION, None), de); + assert_eq!(base_tier(CAP_NEURAL_INT_HYPER, None), None, "unpinned"); + let pin = Prior::new("x", "a".repeat(64), "int-rans/p16+int-hyper/i64").with_tier(crate::codecs::DeclaredTier::DecodeEquivalence); + assert_eq!(base_tier(CAP_NEURAL_INT_HYPER, Some(&pin)), de); + assert_eq!(base_tier(CAP_IMAGE_JPEGAI, None), ec); + assert_eq!(base_tier(CAP_IMAGE_JPEG, None), None); + // A chain over an entropy-consistency base: no stage hash, before or after a layer. + let (_, info, units) = base4(); + let b = info.content_hash; + let mut c = StageChain::new(BaseInfo { tier: ec, ..info }, units, cfg()); + assert!(c.unit_sha256(0).is_none() && c.clip_sha256().is_none()); + assert_eq!(c.tier(0), ec); + let below: Vec> = (0..4).map(frame).collect(); + c.offer(&layer(&b, one(&b), 1, None, &below, 50), 0, ReceiptEvidence::None).unwrap(); + assert_eq!(c.tier(0), None); + assert!(c.unit_sha256(0).is_none()); +} + +/// Test 22: for random arrival traces, a unit's stage-0 output is presented at its +/// presentation time whenever its base arrived by then, whatever its layers do, and +/// its stage is the run of layers that arrived in time. +#[test] +fn base_never_waits() { + let c = UnitClock { t0_ms: 40, unit_ms: 33, target_latency_ms: 100 }; + let mut x: u64 = 0x1234_5678_9abc_def1; + let mut next = |m: u64| { + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + x % m + }; + for _ in 0..200 { + let n = 1 + next(12) as usize; + let base: Vec = (0..n).map(|u| presentation_ms(&c, u as u32).saturating_sub(next(150)) + next(60)).collect(); + let layers: Vec>> = + (0..3).map(|_| (0..n).map(|_| if next(4) == 0 { None } else { Some(next(800)) }).collect()).collect(); + let lead = vec![next(30), next(30), next(30)]; + let p = present(&c, &base, &layers, &lead); + for u in 0..n { + let due = presentation_ms(&c, u as u32); + if base[u] <= due { + assert_eq!(p[u].at_ms, due, "unit {u} presented on time"); + } else { + assert_eq!(p[u].at_ms, base[u], "a late base is presented when it arrives"); + } + let mut want = 0u8; + for l in 0..3 { + match layers[l][u] { + Some(t) if t + lead[l] <= due && want as usize == l => want += 1, + _ => break, + } + } + assert_eq!(p[u].stage, want, "unit {u}"); + assert!(p[u].stage <= p[u].expected); + } + } +} diff --git a/wai/wai-rs/src/staged/tier.rs b/wai/wai-rs/src/staged/tier.rs new file mode 100644 index 000000000..e5ff9b264 --- /dev/null +++ b/wai/wai-rs/src/staged/tier.rs @@ -0,0 +1,64 @@ +//! The determinism tier of a stage (`extensions/staged-delivery.md` §5). +//! +//! A unit's tier is the weakest step of its chain: the base's decode, then each layer +//! applied to it. A layer acts on reconstructed output, so `entropy-consistency`, which +//! fixes only the entropy-decoded symbols, does not pass through it +//! (`extensions/derived-tracks.md` §3 rules 1 and 2). [`compose`] is a pure function, +//! so the rule is tested over every combination, whether or not any admitted base +//! holds a given tier. + +use crate::codecs::{capability_replicate_class, DeterminismTier, ReplicateClass}; +use crate::container::Prior; + +/// The tier of a chain: `base`, then each of `steps` in order, where +/// `on_reconstruction[i]` says whether step `i` acts on reconstructed output. The +/// weakest tier wins, and a step on reconstructed output over `entropy-consistency` +/// gives no tier. `None` is no tier. A step whose `on_reconstruction` flag is missing +/// counts as acting on reconstructed output. +pub fn compose(base: Option, steps: &[Option], on_reconstruction: &[bool]) -> Option { + let mut t = base; + for (i, step) in steps.iter().enumerate() { + let recon = on_reconstruction.get(i).copied().unwrap_or(true); + t = match (t, *step) { + (None, _) | (_, None) => None, + (Some(DeterminismTier::EntropyConsistency), Some(_)) if recon => None, + (Some(a), Some(b)) => Some(a.min(b)), + }; + } + t +} + +/// The tier of a base's step (§5): `decode-equivalence` when `cap`'s registry class is +/// `StandardDefined`, or `NeuralIntegerExact` with a pin that resolved (`pin` is the +/// pin a sink verified, `None` when the decode was unpinned) and declares +/// `decode-equivalence`; `entropy-consistency` when the class is `NeuralEntropyExact`; +/// otherwise none. +pub fn base_tier(cap: &str, pin: Option<&Prior>) -> Option { + match capability_replicate_class(cap)? { + ReplicateClass::StandardDefined => Some(DeterminismTier::DecodeEquivalence), + ReplicateClass::NeuralIntegerExact => { + pin.and_then(Prior::declared_tier).filter(|t| *t == DeterminismTier::DecodeEquivalence) + } + ReplicateClass::NeuralEntropyExact => Some(DeterminismTier::EntropyConsistency), + _ => None, + } +} + +/// The token a tier is written as: `decode-equivalence`, `entropy-consistency`, or +/// `none` for no tier. +pub fn token(t: Option) -> &'static str { + match t { + Some(DeterminismTier::DecodeEquivalence) => "decode-equivalence", + Some(DeterminismTier::EntropyConsistency) => "entropy-consistency", + None => "none", + } +} + +/// The tier a token names; `none` and anything else is no tier. +pub fn from_token(s: &str) -> Option { + match s { + "decode-equivalence" => Some(DeterminismTier::DecodeEquivalence), + "entropy-consistency" => Some(DeterminismTier::EntropyConsistency), + _ => None, + } +} -- 2.54.0 (Apple Git-157) From 52440061a72f16c3968511f82b4db795a6d9f3b8 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 2 Oct 2026 00:16:10 -0400 Subject: [PATCH 05/36] wai: the stage refusal list stays out of the C header Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/src/staged/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/wai/wai-rs/src/staged/mod.rs b/wai/wai-rs/src/staged/mod.rs index 21a805fb8..16f041b75 100644 --- a/wai/wai-rs/src/staged/mod.rs +++ b/wai/wai-rs/src/staged/mod.rs @@ -39,6 +39,7 @@ pub fn unit_kind(cap: &str) -> Option { /// staged-delivery §4.2, in the order of §4.1's steps. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] #[non_exhaustive] +/// cbindgen:ignore pub enum StageRefusal { RefinesMalformed, CapabilityNotRefinement, @@ -57,6 +58,8 @@ pub enum StageRefusal { impl StageRefusal { /// Every reason, in §4.2's order. + /// + /// cbindgen:ignore pub const ALL: [StageRefusal; 13] = [ Self::RefinesMalformed, Self::CapabilityNotRefinement, Self::BaseAbsent, Self::BaseViaFallback, Self::BaseNotAdmitted, Self::RefinementUnattested, Self::PayloadMalformed, Self::CostOverLimit, -- 2.54.0 (Apple Git-157) From ce9207ac3cdd818c15f9d4f55822532b9f147f0e Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 2 Oct 2026 00:27:50 -0400 Subject: [PATCH 06/36] wai: the staged-delivery corpus and its independent verifier staged-conformance/ holds stage sets and the steps a sink takes with them: image layers over a pinned integer image base (identity, up2 to even and odd sizes) and over a PNG base; video layers per frame and per GOP, with prev runs, over integer motion bases and over a pinned neural-keyframe clip; audio layers over a pinned integer audio base, and a stereo rate ladder over a FLAC base; a splat attribute layer; and one case per refusal and order rule (another base, a wrong producer, a missing layer, conflicts held and after an application, out of order, a fallback base, geometry, truncation, a malformed table, q of 0, a word after the last symbol, a stream that runs out, clamped and bypassed residuals, malformed refines, a capability that is not a refinement, a base not admitted, inclusion-only evidence, a cost limit, a missed deadline, the hold bound). Each case's case.json states what a sink reports at every step and holds at the end; schedule.json holds arrival traces and what a sink presents. wai_staged_vectors gen writes the corpus; verify regenerates it byte for byte, replays every case through StageSink, decodes the integer bases with their pins against stage0.bin and checks their tiers. staged_verify.py is a standard-library reimplementation of the envelope, refines, WIR1, the sink's algorithm, tiers and stage hashes, and the PNG, FLAC and WIV1 keyframe-0 decodes; it checks every layer application in every case. The pure-Python BLAKE3 moves from claim_verify.py to _blake3.py, which both verifiers import. A unit held and decided within one step is reported by its decision alone. Co-Authored-By: Claude Opus 5.5 --- wai/claim-conformance/_blake3.py | 75 + wai/claim-conformance/claim_verify.py | 70 +- wai/staged-conformance/.gitattributes | 2 + wai/staged-conformance/README.md | 59 + .../cases/audio_flac_rate_ladder/L1.wai | Bin 0 -> 12711 bytes .../cases/audio_flac_rate_ladder/L2.wai | Bin 0 -> 18367 bytes .../cases/audio_flac_rate_ladder/base.wai | Bin 0 -> 1825 bytes .../cases/audio_flac_rate_ladder/case.json | 121 ++ .../cases/audio_flac_rate_ladder/stage0.bin | Bin 0 -> 1600 bytes .../cases/audio_int_codec_1l/L1.wai | Bin 0 -> 19375 bytes .../cases/audio_int_codec_1l/base.wai | Bin 0 -> 1794 bytes .../cases/audio_int_codec_1l/case.json | 100 ++ .../cases/audio_int_codec_1l/stage0.bin | Bin 0 -> 16384 bytes .../cases/base_not_admitted/L1.wai | Bin 0 -> 415 bytes .../cases/base_not_admitted/base.wai | Bin 0 -> 555 bytes .../cases/base_not_admitted/case.json | 116 ++ .../cases/base_not_admitted/stage0.bin | 1 + .../cases/base_via_fallback/L1.wai | Bin 0 -> 583 bytes .../cases/base_via_fallback/base.wai | Bin 0 -> 574 bytes .../cases/base_via_fallback/case.json | 116 ++ .../cases/base_via_fallback/stage0.bin | 1 + .../cases/bypass_extremes/L1.wai | Bin 0 -> 1807 bytes .../cases/bypass_extremes/base.wai | Bin 0 -> 555 bytes .../cases/bypass_extremes/case.json | 119 ++ .../cases/bypass_extremes/stage0.bin | 1 + .../cases/capability_not_refinement/L1.wai | Bin 0 -> 583 bytes .../cases/capability_not_refinement/base.wai | Bin 0 -> 551 bytes .../cases/capability_not_refinement/case.json | 116 ++ .../capability_not_refinement/stage0.bin | 1 + .../cases/cdf_malformed/L1.wai | Bin 0 -> 583 bytes .../cases/cdf_malformed/base.wai | Bin 0 -> 559 bytes .../cases/cdf_malformed/case.json | 116 ++ .../cases/cdf_malformed/stage0.bin | Bin 0 -> 384 bytes .../cases/clamp_extremes/L1.wai | Bin 0 -> 495 bytes .../cases/clamp_extremes/base.wai | Bin 0 -> 555 bytes .../cases/clamp_extremes/case.json | 119 ++ .../cases/clamp_extremes/stage0.bin | 1 + .../cases/conflict_after_apply/L1a.wai | Bin 0 -> 587 bytes .../cases/conflict_after_apply/L1b.wai | Bin 0 -> 591 bytes .../cases/conflict_after_apply/L2.wai | Bin 0 -> 799 bytes .../cases/conflict_after_apply/base.wai | Bin 0 -> 551 bytes .../cases/conflict_after_apply/case.json | 181 +++ .../cases/conflict_after_apply/stage0.bin | 1 + .../cases/conflict_held/L1a.wai | Bin 0 -> 579 bytes .../cases/conflict_held/L1b.wai | Bin 0 -> 591 bytes .../cases/conflict_held/base.wai | Bin 0 -> 551 bytes .../cases/conflict_held/case.json | 175 +++ .../cases/conflict_held/stage0.bin | 1 + .../cases/cost_over_limit/L1.wai | Bin 0 -> 575 bytes .../cases/cost_over_limit/base.wai | Bin 0 -> 551 bytes .../cases/cost_over_limit/case.json | 120 ++ .../cases/cost_over_limit/stage0.bin | 1 + .../cases/deadline_missed/L1_f0.wai | Bin 0 -> 535 bytes .../cases/deadline_missed/L1_f1.wai | Bin 0 -> 531 bytes .../cases/deadline_missed/base.wai | Bin 0 -> 555 bytes .../cases/deadline_missed/case.json | 152 ++ .../cases/deadline_missed/stage0.bin | 1 + .../cases/geometry_mismatch/L1.wai | Bin 0 -> 415 bytes .../cases/geometry_mismatch/base.wai | Bin 0 -> 547 bytes .../cases/geometry_mismatch/case.json | 125 ++ .../cases/geometry_mismatch/stage0.bin | 1 + .../cases/hold_bound_count/L1a.wai | Bin 0 -> 575 bytes .../cases/hold_bound_count/L1b.wai | Bin 0 -> 587 bytes .../cases/hold_bound_count/base.wai | Bin 0 -> 555 bytes .../cases/hold_bound_count/case.json | 151 ++ .../cases/hold_bound_count/stage0.bin | 1 + .../cases/image_int_hyper_2l/L1.wai | Bin 0 -> 1071 bytes .../cases/image_int_hyper_2l/L2.wai | Bin 0 -> 631 bytes .../cases/image_int_hyper_2l/base.wai | Bin 0 -> 460 bytes .../cases/image_int_hyper_2l/case.json | 126 ++ .../cases/image_int_hyper_2l/stage0.bin | Bin 0 -> 768 bytes .../cases/image_int_hyper_unpinned/L1.wai | Bin 0 -> 1071 bytes .../cases/image_int_hyper_unpinned/base.wai | Bin 0 -> 258 bytes .../cases/image_int_hyper_unpinned/case.json | 102 ++ .../cases/image_int_hyper_unpinned/stage0.bin | Bin 0 -> 768 bytes .../cases/image_png_up2/L1.wai | Bin 0 -> 3439 bytes .../cases/image_png_up2/base.wai | Bin 0 -> 1009 bytes .../cases/image_png_up2/case.json | 97 ++ .../cases/image_png_up2/stage0.bin | 1 + .../cases/image_up2_from_int_hyper/L1.wai | Bin 0 -> 3119 bytes .../cases/image_up2_from_int_hyper/L2.wai | Bin 0 -> 1219 bytes .../cases/image_up2_from_int_hyper/base.wai | Bin 0 -> 460 bytes .../cases/image_up2_from_int_hyper/case.json | 126 ++ .../cases/image_up2_from_int_hyper/stage0.bin | Bin 0 -> 768 bytes .../cases/image_up2_odd/L1.wai | Bin 0 -> 3331 bytes .../cases/image_up2_odd/L2.wai | Bin 0 -> 4843 bytes .../cases/image_up2_odd/base.wai | Bin 0 -> 460 bytes .../cases/image_up2_odd/case.json | 126 ++ .../cases/image_up2_odd/stage0.bin | Bin 0 -> 768 bytes .../cases/inclusion_only_evidence/L1.wai | Bin 0 -> 587 bytes .../cases/inclusion_only_evidence/base.wai | Bin 0 -> 551 bytes .../cases/inclusion_only_evidence/case.json | 136 ++ .../cases/inclusion_only_evidence/stage0.bin | 1 + .../cases/layer_zero/L1.wai | Bin 0 -> 587 bytes .../cases/layer_zero/base.wai | Bin 0 -> 555 bytes .../cases/layer_zero/case.json | 116 ++ .../cases/layer_zero/stage0.bin | 1 + .../cases/missing_layer/L2.wai | Bin 0 -> 795 bytes .../cases/missing_layer/base.wai | Bin 0 -> 555 bytes .../cases/missing_layer/case.json | 143 ++ .../cases/missing_layer/stage0.bin | 1 + .../cases/out_of_order/L1.wai | Bin 0 -> 579 bytes .../cases/out_of_order/L2.wai | Bin 0 -> 791 bytes .../cases/out_of_order/base.wai | Bin 0 -> 555 bytes .../cases/out_of_order/case.json | 156 ++ .../cases/out_of_order/stage0.bin | 1 + .../cases/payload_truncated/L1.wai | Bin 0 -> 584 bytes .../cases/payload_truncated/base.wai | Bin 0 -> 551 bytes .../cases/payload_truncated/case.json | 116 ++ .../cases/payload_truncated/stage0.bin | 1 + .../cases/pending_l1_l2/L1.wai | Bin 0 -> 579 bytes .../cases/pending_l1_l2/L2.wai | Bin 0 -> 791 bytes .../cases/pending_l1_l2/base.wai | Bin 0 -> 551 bytes .../cases/pending_l1_l2/case.json | 163 ++ .../cases/pending_l1_l2/stage0.bin | 1 + wai/staged-conformance/cases/q_zero/L1.wai | Bin 0 -> 583 bytes wai/staged-conformance/cases/q_zero/base.wai | Bin 0 -> 555 bytes wai/staged-conformance/cases/q_zero/case.json | 116 ++ .../cases/q_zero/stage0.bin | 1 + .../cases/refinement_without_base/L1.wai | Bin 0 -> 587 bytes .../cases/refinement_without_base/base.wai | Bin 0 -> 551 bytes .../cases/refinement_without_base/case.json | 96 ++ .../cases/refinement_without_base/stage0.bin | 1 + .../cases/refines_malformed/L1.wai | Bin 0 -> 587 bytes .../cases/refines_malformed/base.wai | Bin 0 -> 555 bytes .../cases/refines_malformed/case.json | 116 ++ .../cases/refines_malformed/stage0.bin | 1 + .../cases/splat_attr_1l/L1.wai | Bin 0 -> 5836 bytes .../cases/splat_attr_1l/base.wai | Bin 0 -> 3319 bytes .../cases/splat_attr_1l/case.json | 100 ++ .../cases/splat_attr_1l/stage0.bin | Bin 0 -> 14336 bytes .../cases/stream_exhausted/L1.wai | Bin 0 -> 523 bytes .../cases/stream_exhausted/base.wai | Bin 0 -> 555 bytes .../cases/stream_exhausted/case.json | 116 ++ .../cases/stream_exhausted/stage0.bin | 1 + .../cases/stream_noncanonical/L1.wai | Bin 0 -> 583 bytes .../cases/stream_noncanonical/base.wai | Bin 0 -> 555 bytes .../cases/stream_noncanonical/case.json | 116 ++ .../cases/stream_noncanonical/stage0.bin | 1 + .../cases/video_int_hyper_keyframe/L1.wai | Bin 0 -> 5967 bytes .../cases/video_int_hyper_keyframe/base.wai | Bin 0 -> 2519 bytes .../cases/video_int_hyper_keyframe/case.json | 186 +++ .../cases/video_int_hyper_keyframe/stage0.bin | Bin 0 -> 3840 bytes .../video_int_motion_gop_perframe/base_0.wai | Bin 0 -> 1443 bytes .../video_int_motion_gop_perframe/base_1.wai | Bin 0 -> 1239 bytes .../video_int_motion_gop_perframe/base_2.wai | Bin 0 -> 1171 bytes .../video_int_motion_gop_perframe/case.json | 721 +++++++++ .../g0_L1_f0.wai | Bin 0 -> 587 bytes .../g0_L1_f1.wai | Bin 0 -> 587 bytes .../g0_L1_f2.wai | Bin 0 -> 583 bytes .../g0_L1_f3.wai | Bin 0 -> 583 bytes .../video_int_motion_gop_perframe/g0_L2.wai | Bin 0 -> 1518 bytes .../g1_L1_f0.wai | Bin 0 -> 583 bytes .../g1_L1_f1.wai | Bin 0 -> 587 bytes .../g1_L1_f2.wai | Bin 0 -> 591 bytes .../g1_L1_f3.wai | Bin 0 -> 591 bytes .../video_int_motion_gop_perframe/g1_L2.wai | Bin 0 -> 1542 bytes .../g2_L1_f0.wai | Bin 0 -> 591 bytes .../g2_L1_f1.wai | Bin 0 -> 587 bytes .../g2_L1_f2.wai | Bin 0 -> 591 bytes .../g2_L1_f3.wai | Bin 0 -> 587 bytes .../video_int_motion_gop_perframe/g2_L2.wai | Bin 0 -> 1566 bytes .../stage0_0.bin | 1 + .../stage0_1.bin | 1 + .../stage0_2.bin | 1 + .../cases/video_mixed_stage_l2/L1_f0.wai | Bin 0 -> 591 bytes .../cases/video_mixed_stage_l2/L1_f1.wai | Bin 0 -> 587 bytes .../cases/video_mixed_stage_l2/L1_f2.wai | Bin 0 -> 587 bytes .../cases/video_mixed_stage_l2/L1_f3.wai | Bin 0 -> 583 bytes .../cases/video_mixed_stage_l2/L2.wai | Bin 0 -> 1546 bytes .../cases/video_mixed_stage_l2/base.wai | Bin 0 -> 1439 bytes .../cases/video_mixed_stage_l2/case.json | 259 +++ .../cases/video_mixed_stage_l2/stage0.bin | 1 + .../cases/wrong_base/L1_other.wai | Bin 0 -> 591 bytes .../cases/wrong_base/base.wai | Bin 0 -> 551 bytes .../cases/wrong_base/case.json | 131 ++ .../cases/wrong_base/stage0.bin | 1 + .../cases/wrong_prev/L1.wai | Bin 0 -> 583 bytes .../cases/wrong_prev/L2_wrong_prev.wai | Bin 0 -> 795 bytes .../cases/wrong_prev/base.wai | Bin 0 -> 551 bytes .../cases/wrong_prev/case.json | 150 ++ .../cases/wrong_prev/stage0.bin | 1 + wai/staged-conformance/index.json | 260 +++ wai/staged-conformance/schedule.json | 191 +++ wai/staged-conformance/staged_verify.py | 919 +++++++++++ wai/wai-rs/Cargo.toml | 5 + wai/wai-rs/src/bin/wai_staged_vectors.rs | 1399 +++++++++++++++++ wai/wai-rs/src/staged/mod.rs | 7 +- 188 files changed, 8288 insertions(+), 69 deletions(-) create mode 100644 wai/claim-conformance/_blake3.py create mode 100644 wai/staged-conformance/.gitattributes create mode 100644 wai/staged-conformance/README.md create mode 100644 wai/staged-conformance/cases/audio_flac_rate_ladder/L1.wai create mode 100644 wai/staged-conformance/cases/audio_flac_rate_ladder/L2.wai create mode 100644 wai/staged-conformance/cases/audio_flac_rate_ladder/base.wai create mode 100644 wai/staged-conformance/cases/audio_flac_rate_ladder/case.json create mode 100644 wai/staged-conformance/cases/audio_flac_rate_ladder/stage0.bin create mode 100644 wai/staged-conformance/cases/audio_int_codec_1l/L1.wai create mode 100644 wai/staged-conformance/cases/audio_int_codec_1l/base.wai create mode 100644 wai/staged-conformance/cases/audio_int_codec_1l/case.json create mode 100644 wai/staged-conformance/cases/audio_int_codec_1l/stage0.bin create mode 100644 wai/staged-conformance/cases/base_not_admitted/L1.wai create mode 100644 wai/staged-conformance/cases/base_not_admitted/base.wai create mode 100644 wai/staged-conformance/cases/base_not_admitted/case.json create mode 100644 wai/staged-conformance/cases/base_not_admitted/stage0.bin create mode 100644 wai/staged-conformance/cases/base_via_fallback/L1.wai create mode 100644 wai/staged-conformance/cases/base_via_fallback/base.wai create mode 100644 wai/staged-conformance/cases/base_via_fallback/case.json create mode 100644 wai/staged-conformance/cases/base_via_fallback/stage0.bin create mode 100644 wai/staged-conformance/cases/bypass_extremes/L1.wai create mode 100644 wai/staged-conformance/cases/bypass_extremes/base.wai create mode 100644 wai/staged-conformance/cases/bypass_extremes/case.json create mode 100644 wai/staged-conformance/cases/bypass_extremes/stage0.bin create mode 100644 wai/staged-conformance/cases/capability_not_refinement/L1.wai create mode 100644 wai/staged-conformance/cases/capability_not_refinement/base.wai create mode 100644 wai/staged-conformance/cases/capability_not_refinement/case.json create mode 100644 wai/staged-conformance/cases/capability_not_refinement/stage0.bin create mode 100644 wai/staged-conformance/cases/cdf_malformed/L1.wai create mode 100644 wai/staged-conformance/cases/cdf_malformed/base.wai create mode 100644 wai/staged-conformance/cases/cdf_malformed/case.json create mode 100644 wai/staged-conformance/cases/cdf_malformed/stage0.bin create mode 100644 wai/staged-conformance/cases/clamp_extremes/L1.wai create mode 100644 wai/staged-conformance/cases/clamp_extremes/base.wai create mode 100644 wai/staged-conformance/cases/clamp_extremes/case.json create mode 100644 wai/staged-conformance/cases/clamp_extremes/stage0.bin create mode 100644 wai/staged-conformance/cases/conflict_after_apply/L1a.wai create mode 100644 wai/staged-conformance/cases/conflict_after_apply/L1b.wai create mode 100644 wai/staged-conformance/cases/conflict_after_apply/L2.wai create mode 100644 wai/staged-conformance/cases/conflict_after_apply/base.wai create mode 100644 wai/staged-conformance/cases/conflict_after_apply/case.json create mode 100644 wai/staged-conformance/cases/conflict_after_apply/stage0.bin create mode 100644 wai/staged-conformance/cases/conflict_held/L1a.wai create mode 100644 wai/staged-conformance/cases/conflict_held/L1b.wai create mode 100644 wai/staged-conformance/cases/conflict_held/base.wai create mode 100644 wai/staged-conformance/cases/conflict_held/case.json create mode 100644 wai/staged-conformance/cases/conflict_held/stage0.bin create mode 100644 wai/staged-conformance/cases/cost_over_limit/L1.wai create mode 100644 wai/staged-conformance/cases/cost_over_limit/base.wai create mode 100644 wai/staged-conformance/cases/cost_over_limit/case.json create mode 100644 wai/staged-conformance/cases/cost_over_limit/stage0.bin create mode 100644 wai/staged-conformance/cases/deadline_missed/L1_f0.wai create mode 100644 wai/staged-conformance/cases/deadline_missed/L1_f1.wai create mode 100644 wai/staged-conformance/cases/deadline_missed/base.wai create mode 100644 wai/staged-conformance/cases/deadline_missed/case.json create mode 100644 wai/staged-conformance/cases/deadline_missed/stage0.bin create mode 100644 wai/staged-conformance/cases/geometry_mismatch/L1.wai create mode 100644 wai/staged-conformance/cases/geometry_mismatch/base.wai create mode 100644 wai/staged-conformance/cases/geometry_mismatch/case.json create mode 100644 wai/staged-conformance/cases/geometry_mismatch/stage0.bin create mode 100644 wai/staged-conformance/cases/hold_bound_count/L1a.wai create mode 100644 wai/staged-conformance/cases/hold_bound_count/L1b.wai create mode 100644 wai/staged-conformance/cases/hold_bound_count/base.wai create mode 100644 wai/staged-conformance/cases/hold_bound_count/case.json create mode 100644 wai/staged-conformance/cases/hold_bound_count/stage0.bin create mode 100644 wai/staged-conformance/cases/image_int_hyper_2l/L1.wai create mode 100644 wai/staged-conformance/cases/image_int_hyper_2l/L2.wai create mode 100644 wai/staged-conformance/cases/image_int_hyper_2l/base.wai create mode 100644 wai/staged-conformance/cases/image_int_hyper_2l/case.json create mode 100644 wai/staged-conformance/cases/image_int_hyper_2l/stage0.bin create mode 100644 wai/staged-conformance/cases/image_int_hyper_unpinned/L1.wai create mode 100644 wai/staged-conformance/cases/image_int_hyper_unpinned/base.wai create mode 100644 wai/staged-conformance/cases/image_int_hyper_unpinned/case.json create mode 100644 wai/staged-conformance/cases/image_int_hyper_unpinned/stage0.bin create mode 100644 wai/staged-conformance/cases/image_png_up2/L1.wai create mode 100644 wai/staged-conformance/cases/image_png_up2/base.wai create mode 100644 wai/staged-conformance/cases/image_png_up2/case.json create mode 100644 wai/staged-conformance/cases/image_png_up2/stage0.bin create mode 100644 wai/staged-conformance/cases/image_up2_from_int_hyper/L1.wai create mode 100644 wai/staged-conformance/cases/image_up2_from_int_hyper/L2.wai create mode 100644 wai/staged-conformance/cases/image_up2_from_int_hyper/base.wai create mode 100644 wai/staged-conformance/cases/image_up2_from_int_hyper/case.json create mode 100644 wai/staged-conformance/cases/image_up2_from_int_hyper/stage0.bin create mode 100644 wai/staged-conformance/cases/image_up2_odd/L1.wai create mode 100644 wai/staged-conformance/cases/image_up2_odd/L2.wai create mode 100644 wai/staged-conformance/cases/image_up2_odd/base.wai create mode 100644 wai/staged-conformance/cases/image_up2_odd/case.json create mode 100644 wai/staged-conformance/cases/image_up2_odd/stage0.bin create mode 100644 wai/staged-conformance/cases/inclusion_only_evidence/L1.wai create mode 100644 wai/staged-conformance/cases/inclusion_only_evidence/base.wai create mode 100644 wai/staged-conformance/cases/inclusion_only_evidence/case.json create mode 100644 wai/staged-conformance/cases/inclusion_only_evidence/stage0.bin create mode 100644 wai/staged-conformance/cases/layer_zero/L1.wai create mode 100644 wai/staged-conformance/cases/layer_zero/base.wai create mode 100644 wai/staged-conformance/cases/layer_zero/case.json create mode 100644 wai/staged-conformance/cases/layer_zero/stage0.bin create mode 100644 wai/staged-conformance/cases/missing_layer/L2.wai create mode 100644 wai/staged-conformance/cases/missing_layer/base.wai create mode 100644 wai/staged-conformance/cases/missing_layer/case.json create mode 100644 wai/staged-conformance/cases/missing_layer/stage0.bin create mode 100644 wai/staged-conformance/cases/out_of_order/L1.wai create mode 100644 wai/staged-conformance/cases/out_of_order/L2.wai create mode 100644 wai/staged-conformance/cases/out_of_order/base.wai create mode 100644 wai/staged-conformance/cases/out_of_order/case.json create mode 100644 wai/staged-conformance/cases/out_of_order/stage0.bin create mode 100644 wai/staged-conformance/cases/payload_truncated/L1.wai create mode 100644 wai/staged-conformance/cases/payload_truncated/base.wai create mode 100644 wai/staged-conformance/cases/payload_truncated/case.json create mode 100644 wai/staged-conformance/cases/payload_truncated/stage0.bin create mode 100644 wai/staged-conformance/cases/pending_l1_l2/L1.wai create mode 100644 wai/staged-conformance/cases/pending_l1_l2/L2.wai create mode 100644 wai/staged-conformance/cases/pending_l1_l2/base.wai create mode 100644 wai/staged-conformance/cases/pending_l1_l2/case.json create mode 100644 wai/staged-conformance/cases/pending_l1_l2/stage0.bin create mode 100644 wai/staged-conformance/cases/q_zero/L1.wai create mode 100644 wai/staged-conformance/cases/q_zero/base.wai create mode 100644 wai/staged-conformance/cases/q_zero/case.json create mode 100644 wai/staged-conformance/cases/q_zero/stage0.bin create mode 100644 wai/staged-conformance/cases/refinement_without_base/L1.wai create mode 100644 wai/staged-conformance/cases/refinement_without_base/base.wai create mode 100644 wai/staged-conformance/cases/refinement_without_base/case.json create mode 100644 wai/staged-conformance/cases/refinement_without_base/stage0.bin create mode 100644 wai/staged-conformance/cases/refines_malformed/L1.wai create mode 100644 wai/staged-conformance/cases/refines_malformed/base.wai create mode 100644 wai/staged-conformance/cases/refines_malformed/case.json create mode 100644 wai/staged-conformance/cases/refines_malformed/stage0.bin create mode 100644 wai/staged-conformance/cases/splat_attr_1l/L1.wai create mode 100644 wai/staged-conformance/cases/splat_attr_1l/base.wai create mode 100644 wai/staged-conformance/cases/splat_attr_1l/case.json create mode 100644 wai/staged-conformance/cases/splat_attr_1l/stage0.bin create mode 100644 wai/staged-conformance/cases/stream_exhausted/L1.wai create mode 100644 wai/staged-conformance/cases/stream_exhausted/base.wai create mode 100644 wai/staged-conformance/cases/stream_exhausted/case.json create mode 100644 wai/staged-conformance/cases/stream_exhausted/stage0.bin create mode 100644 wai/staged-conformance/cases/stream_noncanonical/L1.wai create mode 100644 wai/staged-conformance/cases/stream_noncanonical/base.wai create mode 100644 wai/staged-conformance/cases/stream_noncanonical/case.json create mode 100644 wai/staged-conformance/cases/stream_noncanonical/stage0.bin create mode 100644 wai/staged-conformance/cases/video_int_hyper_keyframe/L1.wai create mode 100644 wai/staged-conformance/cases/video_int_hyper_keyframe/base.wai create mode 100644 wai/staged-conformance/cases/video_int_hyper_keyframe/case.json create mode 100644 wai/staged-conformance/cases/video_int_hyper_keyframe/stage0.bin create mode 100644 wai/staged-conformance/cases/video_int_motion_gop_perframe/base_0.wai create mode 100644 wai/staged-conformance/cases/video_int_motion_gop_perframe/base_1.wai create mode 100644 wai/staged-conformance/cases/video_int_motion_gop_perframe/base_2.wai create mode 100644 wai/staged-conformance/cases/video_int_motion_gop_perframe/case.json create mode 100644 wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L1_f0.wai create mode 100644 wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L1_f1.wai create mode 100644 wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L1_f2.wai create mode 100644 wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L1_f3.wai create mode 100644 wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L2.wai create mode 100644 wai/staged-conformance/cases/video_int_motion_gop_perframe/g1_L1_f0.wai create mode 100644 wai/staged-conformance/cases/video_int_motion_gop_perframe/g1_L1_f1.wai create mode 100644 wai/staged-conformance/cases/video_int_motion_gop_perframe/g1_L1_f2.wai create mode 100644 wai/staged-conformance/cases/video_int_motion_gop_perframe/g1_L1_f3.wai create mode 100644 wai/staged-conformance/cases/video_int_motion_gop_perframe/g1_L2.wai create mode 100644 wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L1_f0.wai create mode 100644 wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L1_f1.wai create mode 100644 wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L1_f2.wai create mode 100644 wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L1_f3.wai create mode 100644 wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L2.wai create mode 100644 wai/staged-conformance/cases/video_int_motion_gop_perframe/stage0_0.bin create mode 100644 wai/staged-conformance/cases/video_int_motion_gop_perframe/stage0_1.bin create mode 100644 wai/staged-conformance/cases/video_int_motion_gop_perframe/stage0_2.bin create mode 100644 wai/staged-conformance/cases/video_mixed_stage_l2/L1_f0.wai create mode 100644 wai/staged-conformance/cases/video_mixed_stage_l2/L1_f1.wai create mode 100644 wai/staged-conformance/cases/video_mixed_stage_l2/L1_f2.wai create mode 100644 wai/staged-conformance/cases/video_mixed_stage_l2/L1_f3.wai create mode 100644 wai/staged-conformance/cases/video_mixed_stage_l2/L2.wai create mode 100644 wai/staged-conformance/cases/video_mixed_stage_l2/base.wai create mode 100644 wai/staged-conformance/cases/video_mixed_stage_l2/case.json create mode 100644 wai/staged-conformance/cases/video_mixed_stage_l2/stage0.bin create mode 100644 wai/staged-conformance/cases/wrong_base/L1_other.wai create mode 100644 wai/staged-conformance/cases/wrong_base/base.wai create mode 100644 wai/staged-conformance/cases/wrong_base/case.json create mode 100644 wai/staged-conformance/cases/wrong_base/stage0.bin create mode 100644 wai/staged-conformance/cases/wrong_prev/L1.wai create mode 100644 wai/staged-conformance/cases/wrong_prev/L2_wrong_prev.wai create mode 100644 wai/staged-conformance/cases/wrong_prev/base.wai create mode 100644 wai/staged-conformance/cases/wrong_prev/case.json create mode 100644 wai/staged-conformance/cases/wrong_prev/stage0.bin create mode 100644 wai/staged-conformance/index.json create mode 100644 wai/staged-conformance/schedule.json create mode 100644 wai/staged-conformance/staged_verify.py create mode 100644 wai/wai-rs/src/bin/wai_staged_vectors.rs diff --git a/wai/claim-conformance/_blake3.py b/wai/claim-conformance/_blake3.py new file mode 100644 index 000000000..89865deb7 --- /dev/null +++ b/wai/claim-conformance/_blake3.py @@ -0,0 +1,75 @@ +"""BLAKE3 in hash mode, written from its specification in pure Python. + +Shared by the claim-conformance verifier (`claim_verify.py`) and the staged-delivery +verifier (`../staged-conformance/staged_verify.py`), which import `blake3` from here. It +shares no code with wai-rs, which computes the same hashes with the `blake3` crate. +""" + +_IV = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19] +_PERM = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8] +_CHUNK_START, _CHUNK_END, _PARENT, _ROOT = 1, 2, 4, 8 +_M32 = 0xFFFFFFFF + + +def _rotr(x, n): + return ((x >> n) | (x << (32 - n))) & _M32 + + +def _g(s, a, b, c, d, x, y): + s[a] = (s[a] + s[b] + x) & _M32 + s[d] = _rotr(s[d] ^ s[a], 16) + s[c] = (s[c] + s[d]) & _M32 + s[b] = _rotr(s[b] ^ s[c], 12) + s[a] = (s[a] + s[b] + y) & _M32 + s[d] = _rotr(s[d] ^ s[a], 8) + s[c] = (s[c] + s[d]) & _M32 + s[b] = _rotr(s[b] ^ s[c], 7) + + +def _compress(cv, m, counter, blen, flags): + s = list(cv) + _IV[:4] + [counter & _M32, (counter >> 32) & _M32, blen, flags] + m = list(m) + for r in range(7): + _g(s, 0, 4, 8, 12, m[0], m[1]); _g(s, 1, 5, 9, 13, m[2], m[3]) + _g(s, 2, 6, 10, 14, m[4], m[5]); _g(s, 3, 7, 11, 15, m[6], m[7]) + _g(s, 0, 5, 10, 15, m[8], m[9]); _g(s, 1, 6, 11, 12, m[10], m[11]) + _g(s, 2, 7, 8, 13, m[12], m[13]); _g(s, 3, 4, 9, 14, m[14], m[15]) + if r < 6: + m = [m[i] for i in _PERM] + return [s[i] ^ s[i + 8] for i in range(8)] + [s[i + 8] ^ cv[i] for i in range(8)] + + +def _words(block): + block = block + b"\0" * (64 - len(block)) + return [int.from_bytes(block[i:i + 4], "little") for i in range(0, 64, 4)] + + +def _chunk_output(chunk, counter): + blocks = [chunk[i:i + 64] for i in range(0, len(chunk), 64)] or [b""] + cv = _IV + for j, b in enumerate(blocks[:-1]): + cv = _compress(cv, _words(b), counter, 64, _CHUNK_START if j == 0 else 0)[:8] + last = blocks[-1] + return (cv, _words(last), counter, len(last), _CHUNK_END | (_CHUNK_START if len(blocks) == 1 else 0)) + + +def _cv(out): + cv, w, c, bl, f = out + return _compress(cv, w, c, bl, f)[:8] + + +def blake3(data: bytes) -> bytes: + chunks = [data[i:i + 1024] for i in range(0, len(data), 1024)] or [b""] + stack = [] + for idx, ch in enumerate(chunks[:-1]): + cv = _cv(_chunk_output(ch, idx)) + total = idx + 1 + while total & 1 == 0: + cv = _cv((_IV, stack.pop() + cv, 0, 64, _PARENT)) + total >>= 1 + stack.append(cv) + out = _chunk_output(chunks[-1], len(chunks) - 1) + while stack: + out = (_IV, stack.pop() + _cv(out), 0, 64, _PARENT) + cv, w, _, bl, f = out + return b"".join(x.to_bytes(4, "little") for x in _compress(cv, w, 0, bl, f | _ROOT)[:8]) diff --git a/wai/claim-conformance/claim_verify.py b/wai/claim-conformance/claim_verify.py index 0ccfd8930..f12577aa8 100644 --- a/wai/claim-conformance/claim_verify.py +++ b/wai/claim-conformance/claim_verify.py @@ -36,6 +36,7 @@ from decimal import localcontext HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.join(HERE, "..", "oer-conformance")) import oer2_verify as OER # noqa: E402 (the OER reference verifier: grades + receipt checks) +sys.path.insert(0, HERE) from cryptography.exceptions import InvalidSignature # noqa: E402 from cryptography.hazmat.primitives import serialization # noqa: E402 @@ -43,74 +44,7 @@ from cryptography.hazmat.primitives.asymmetric import ed25519 # noqa: E402 # ---------------------------------------------------------------- BLAKE3 (hash mode) -_IV = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19] -_PERM = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8] -_CHUNK_START, _CHUNK_END, _PARENT, _ROOT = 1, 2, 4, 8 -_M32 = 0xFFFFFFFF - - -def _rotr(x, n): - return ((x >> n) | (x << (32 - n))) & _M32 - - -def _g(s, a, b, c, d, x, y): - s[a] = (s[a] + s[b] + x) & _M32 - s[d] = _rotr(s[d] ^ s[a], 16) - s[c] = (s[c] + s[d]) & _M32 - s[b] = _rotr(s[b] ^ s[c], 12) - s[a] = (s[a] + s[b] + y) & _M32 - s[d] = _rotr(s[d] ^ s[a], 8) - s[c] = (s[c] + s[d]) & _M32 - s[b] = _rotr(s[b] ^ s[c], 7) - - -def _compress(cv, m, counter, blen, flags): - s = list(cv) + _IV[:4] + [counter & _M32, (counter >> 32) & _M32, blen, flags] - m = list(m) - for r in range(7): - _g(s, 0, 4, 8, 12, m[0], m[1]); _g(s, 1, 5, 9, 13, m[2], m[3]) - _g(s, 2, 6, 10, 14, m[4], m[5]); _g(s, 3, 7, 11, 15, m[6], m[7]) - _g(s, 0, 5, 10, 15, m[8], m[9]); _g(s, 1, 6, 11, 12, m[10], m[11]) - _g(s, 2, 7, 8, 13, m[12], m[13]); _g(s, 3, 4, 9, 14, m[14], m[15]) - if r < 6: - m = [m[i] for i in _PERM] - return [s[i] ^ s[i + 8] for i in range(8)] + [s[i + 8] ^ cv[i] for i in range(8)] - - -def _words(block): - block = block + b"\0" * (64 - len(block)) - return [int.from_bytes(block[i:i + 4], "little") for i in range(0, 64, 4)] - - -def _chunk_output(chunk, counter): - blocks = [chunk[i:i + 64] for i in range(0, len(chunk), 64)] or [b""] - cv = _IV - for j, b in enumerate(blocks[:-1]): - cv = _compress(cv, _words(b), counter, 64, _CHUNK_START if j == 0 else 0)[:8] - last = blocks[-1] - return (cv, _words(last), counter, len(last), _CHUNK_END | (_CHUNK_START if len(blocks) == 1 else 0)) - - -def _cv(out): - cv, w, c, bl, f = out - return _compress(cv, w, c, bl, f)[:8] - - -def blake3(data: bytes) -> bytes: - chunks = [data[i:i + 1024] for i in range(0, len(data), 1024)] or [b""] - stack = [] - for idx, ch in enumerate(chunks[:-1]): - cv = _cv(_chunk_output(ch, idx)) - total = idx + 1 - while total & 1 == 0: - cv = _cv((_IV, stack.pop() + cv, 0, 64, _PARENT)) - total >>= 1 - stack.append(cv) - out = _chunk_output(chunks[-1], len(chunks) - 1) - while stack: - out = (_IV, stack.pop() + _cv(out), 0, 64, _PARENT) - cv, w, _, bl, f = out - return b"".join(x.to_bytes(4, "little") for x in _compress(cv, w, 0, bl, f | _ROOT)[:8]) +from _blake3 import blake3 # noqa: E402 (written from its specification; shared with staged_verify.py) # ------------------------------------------------ Ed25519 (RFC 8032), §3.1 step 3's rule diff --git a/wai/staged-conformance/.gitattributes b/wai/staged-conformance/.gitattributes new file mode 100644 index 000000000..eff1df73a --- /dev/null +++ b/wai/staged-conformance/.gitattributes @@ -0,0 +1,2 @@ +# Hashed files: a line-ending conversion on checkout would change every digest. +* -text diff --git a/wai/staged-conformance/README.md b/wai/staged-conformance/README.md new file mode 100644 index 000000000..3c2ec9920 --- /dev/null +++ b/wai/staged-conformance/README.md @@ -0,0 +1,59 @@ +# staged-conformance — staged delivery + +The corpus of [`extensions/staged-delivery.md`](../extensions/staged-delivery.md) §12: +stage sets of a base and refinement layers, and the steps a sink takes with them. + +**Stage-equivalence.** For each case, a conforming sink that offers its layers, decodes +its bases and expires its holds in the order and at the times `case.json` states +reports, at every step, the reasons the case states per envelope and per unit, and +ends with every unit at the stated stage, producer, frozen flag, tier, shape and stage +hash (SHA-256 of the unit's canonical buffer, SPEC §7 *Digests*; stated only where the +unit holds `decode-equivalence`). The reasons are §4.2's; a `payload-malformed` +report names the `WIR1` code (Appendix A.6). + +- `cases//` — one case: `case.json`, each base's envelope (`base*.wai`) and its + stage-0 output (`stage0*.bin`: the units' canonical buffers concatenated, in unit + order), and the layers (`*.wai`). `case.json` states each base's capability it was + decoded through, whether through a fallback, whether the sink verified a delivery + receipt for it, whether its pin resolved, its tier and its units; the sink's hold + bound, cost limit and live schedule; the steps; and what the sink reports and holds. +- `schedule.json` — arrival traces, and the unit-by-unit presentation and stage a + sink gives each (§8), with the count of units presented below the stage their + layers would have reached on time. +- `index.json` — the cases and their final clip hashes. + +**The content is synthetic**: pictures, frames and PCM computed by integer functions in +the generator. The bases of the integer codecs are the committed vectors under +[`../wai-rs/tests/vectors/`](../wai-rs/tests/vectors/), decoded against their +parameter sets with their pins; the `wai.video.int_motion` bases are coded by a +keyframe-0 encoder in the generator (zero motion, one block per frame), and the PNG +and FLAC bases are written by small writers in the generator (stored deflate with +the five row filters; VERBATIM subframes). + +**What checks each base's stage 0.** + +| base | checked by | +|---|---| +| `wai.video.int_motion` (`WIV1`, keyframe 0) | `wai_staged_vectors verify` (the reference decode) and `staged_verify.py` (an independent decode) | +| `wai.image.png` | `staged_verify.py` (zlib and the row filters); a fixture to `wai_staged_vectors`, which builds without `codecs` | +| `wai.audio.flac` | `staged_verify.py` (VERBATIM subframes, both CRCs); a fixture to `wai_staged_vectors` | +| `wai.neural.int_hyper`, `wai.video.int_hyper` (keyframe 1), `wai.audio.int_codec`, `wai.splat.int_codec` | `wai_staged_vectors verify` (the reference decode, pinned); no independent decoder here, so `staged_verify.py` starts from `stage0.bin` and its SHA-256, which the cross-architecture goldens of [`../byte-exact-conformance/`](../byte-exact-conformance/) also hold | + +Every layer application, in every case, is checked by both. + +**Verifiers.** + +- `wai_staged_vectors verify .` (from `../wai-rs`, features + `staged,video_int,audio_int,splat_int`): regenerates every file and requires it + byte for byte, replays every case through `staged::StageSink`, decodes the bases it + can and checks their tiers, and replays the schedule. `wai_staged_vectors gen .` + writes the corpus. +- `staged_verify.py .`: pure Python with the standard library, sharing no code with + `wai-rs`. It reimplements the envelope and the content hash, `refines`, `WIR1` (its + parse in order, its cost, its streams, the three predictors and `rdiv`), the sink's + algorithm with its holds, batches, conflicts and deadlines, tiers and stage hashes, + the PNG and FLAC decodes, and the schedule. It takes BLAKE3 from + [`../claim-conformance/_blake3.py`](../claim-conformance/_blake3.py) and the rANS + decoder and the `WIV1` keyframe-0 decode from + [`../int-payload-conformance/int_payload_verify.py`](../int-payload-conformance/int_payload_verify.py), + both written from their specifications. diff --git a/wai/staged-conformance/cases/audio_flac_rate_ladder/L1.wai b/wai/staged-conformance/cases/audio_flac_rate_ladder/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..38836a01bb5af34ce736c36d24b6316971890ba0 GIT binary patch literal 12711 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 QPuA(Di#IxzEqUbs0Dxx6c>n+a literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/audio_flac_rate_ladder/L2.wai b/wai/staged-conformance/cases/audio_flac_rate_ladder/L2.wai new file mode 100644 index 0000000000000000000000000000000000000000..ce9b7ec72c4ffa5d9643cdd4424abf39fe66d115 GIT binary patch literal 18367 zcmXxrWsns|+c01txNC5?1PEbqO9&3Z-DMXRSlr#+-QC^Y-QC^YT@wQD#q-toi^fU-H)1}z;?lE z0)m5b<;)%w9F#pUcVMn;d9vilo-KPo!$!G-as=gSm?dY9;Ou#_WXYB_C?I#Pz&zP< z<<1op9FU_?K=xd@v*gYanA_F1ZX4WDPdTy$X3w1^N3I4zdGZA1&fTbC)@(U~@&x3} zkt?Hp<#CASg$U!0bV}vuDkoBOoAaV2;2X!GU?Q=gyuZcVLj7ng@0XZksA^ zR#)0Sux;aDEpJ9}L;>Wok<*EHgSkcJborV2}npH5|f0aBqKQ~NJ%PE zlZLdUBR#+HD;da0CVnF``DsWnjc800LTE}en$v=ow4ya_XiGcV(}9k3qBC9SN;kUG zgP!!FH+|?!Kl(F(fed0WLm0|1hBJbZjAArn7|S@uGl7XrVlq>h$~2}kgPF`?HglNE zJm#~2g)Cw*OIXS>ma~GDtYS55Sj#%rvw@9lVl!LV$~LyMgPrVRH+$I2KK65fgB;>8 zM>xtcj&p*OoZ>WRILkTCbAgLo;xbpb$~CTYgPYvqHg~woJ?`^>hdkmjPk72Rp7Vm2 zyy7))c*{H9^MQ|i;xk|PhkyCXe|+ORKY}GRVfcx#gd;o=h)5(N6NRWmBRVmNNi1R$ zhq%P!XX2B9gd`#{Nk~dEl9Pgzq#`wGNJ~1>^9#R{fsAD0H!|}(SqLC2*~m@~a*~VO z>6Q1&n=e*!0uXxQH-tvz3 zeBdLW_{s z5B{V8e^HP^6s8D8DMoQhP?EnXMQO@VmU5J*0u`x5WvWn>YE-8NHK|2y>QI+@)F+T2 z8qknn8qt_0gwT{`G^YhEX+>+=(3WeG#AU83dBtnq@RoPH=K~-4#Am+n z5C8I&|M{6=PeCkp{&B^%kvK~8d!n>^$tANl!%KPkXp6r>P^ zDMC?-QJfN#!nJ#pt8{O$aPkPatKJ=v@{TaYO1~Hf+3}qO@8NoS|UJKW_S_j$lW9`TqbJmneBdBICw@tQZhVO$ebW&1g;wTGEQv zw4p8SXio<^(uvM=p)1|!P7iw0i{A91Fa7Ax00uIM!3<$2!x+v8Mly=gjA1O}7|#SI zGKtAdVJg#@&J1QUi`mR!F7uer0v57}#Vlbd%UI3|RfTJ9zPSG1SBL8iAh3Il98Mgq$CxoNkdxFk)B`pl?-Gg z6Tgv}-^oG%S;LqVTw?cViczYCHb3Dl%@=2DMxuK zP?1VhrV3T5Ms;dXlUmfK4t1$VeF6!h0SyVJ5shg=2u*25b6U`nRY(34*DrVoATM}Gz|kUW_xyE&FaFbiy<_>qc$9*2~kVib` z2~T;(b6)V0SG?v8Z+XXiKJbxGeC7-P@GoEak8gY@RCw`E7=9uw;RsIzA`*$nL?J5C zh)xV*5{uZxAujRwnfN3iA&E##5|WaPiGNFfSSgrXFqI3*~_-;|;>WhhHI%2R=gRH8Cfs7f`e zQ-hk+qBeD?OFilnNDvKZNHC3POcO$AN;8_%f|j(RHEn21JKEEMj&!0kUFb?Ty3>Q6 z^rAO?=u1EPGk}2%VlYD($}omAf{~13G-DXcIL0%9iA-WLQ<%y$rZaeG z#AU83dBtnq@RoPH=K~-4#Am+n5C8I& z|MC(P7ZRCi`?WPFZsyNAN)xH{-Pj-C`=KGQjFr1 zpd^1&iqe#!EafOq1u9aXK!RvMLxO2UW10}c7{)S=@l0SMlbFmDrZSD`%wQ(7n9UsK zGLQKzU?GcG%o3KejODCgC97D?8rHIo^=x1xo7l`2wz7?T+~)xgdCv#>MHE{MU?77S z%n*h$jNy!6B%>J37{)S=@l0SMlbFmDrZSD`%wQ(7n9UsKGLQKzU?GcG%o3KejOA>N zWSxj?ouCIj=|yiwa*EMW%&TbT6%mL?Bq9@qs6-<=F^EYlViSkB#N%h;lYoRIA~8uw zN-~m@f|R5pHEBpo8OlHNAm8eV=s#1;W)SxD{s7)Q}Qjhwy;3J>-%oqOQU%pa3f%#V0 ze6j(&)rRTMHcA7?N;a}nh{6=1D8(pF2}<%er6^4q%2JN~>h4QpA)dN#0;O>AZhTiM2Tj&h9SoZuv$po0o|P*^rRQP=|f-o(VqbfWDtWH z!cc~>knFF0hfVCKY<}yvvz2XZX9qjk#cuYnmwoK#00%k5VUBQ=V;tuMCppDw&Ty7< zoaX`;xx{6zaFuIZ=LR>q#cl3zmwVjj0S|e^W1jGoXFTTxFL}jl-td-pyypWS`NTmR z_=h;m5sq?;I&HLPVF>)F6YHnEv4Y-JnU*}+bBv70^Y zWgq)Fz(Edim?IqJ7{@umNltN^Go0ld=efW|F7beeJmN7=c*--L^MaSW;x%u0%RAol zfscIRGhg_JfBDLPeB(Pm5-~y(hMx#aIKmTwh(sbXQHV-3q7#Fd#3D9vh)XVO$ebW&1g;wTGEQvw4p8SXio<^(uvM=p)1|!P7iw0 zi{A91Fa7Ax00uIM!3<$2!x+v8Mly=gjA1O}7|#SIGKtAdVJg#@&J1QUi`mR!F7uer z0v57}#Vlbd%UI3|RiGNFfSSgrXFq zI3*~_-;|;>WhhHI%2R=gRH8Cfs7f`eQ-hk+qBeD?OFilnNDvKZNHC3POcO$AN;8_% zf|j(RHEn21JKEEMj&!0kUFb?Ty3>Q6^rAO?=u1EPGk}2%VlYD($}omAf{~13G-DXc zIL0%9iA-WLQ<%y$rZa|!^2*vmflbAW>!;xI=z$}x^}f|H!$G-o)=InHx|i(KL|SGdYGu5*K% z+~PKOxXV56^MHpu;xSKn$}^txf|tDFHE(#!JKpnwk9^`YU-*ZA`O1HM<2yf+977X^ zp9o7h!V`grL?SX#h)Oh~6N8wL<1TUOd}f8gb{lo#;##y3&pA^q?ob=uIE`(vSWOU?77S%n*h$jNy!6B%>J37{)S=@l0SMlbFmD zrZSD`%wQ(7n9UsKGLQKzU?GcG%o3KejODCgC97D?8rHIo^=x1xo7l`2wz7@w>|iIm z*v%gHvXA{7;2?)M%n^=qjN_c(B&Rsd8P0N!^IYH}m$=Lou5yj*+~6j+xXm5za*z8w z;31EA%oCpSjOV=IC9inR8{YDc_k7?ZpZLrd{^4J~@*m&$&W{A=(1hV9!V-?~L?9xO zh)fis5{>A@ASSVhO&sD9kDrN80uqvl#3Ugp$w*ELQj&_)q#-ToNY5|)N(M5LiQmY~ z?_?o>tYjlQImk&aa+8O=hfil%qTqs7NI$ zQ-!KjqdGOHNiAwqhq~0GK7j<$fQAIqh{iM_gr+p3IW1^OD_YZrwzQ)?9q33WI@5)& zbfY^x=t(bn(}%wFqdx-}$RGwYgrN*$I3pOzC`L1ev5aFp6PU;(CNqVpOk+ATn8_?= zGl#j%V?GO5$RZZAgrzKFIV)JnDps?GwX9=38`#JuHnWATY-2k+*vT$-vxmLxV?PHt z$RQ4MgrgkeI43yCDNb{Svz+5R7r4kJE^~#eT;n=7xXCSUbBDX!<30~~$Ri%}gr_{? zIWKt0D_--4x4h#$ANa^8KJ$ft_?NHz$2Y$73XgyAQ`5{~dhAR>{7OcbILjp)Q6 zCb5W39O4p>pNUTb5|W6-Bq1ruNKOh;l8V%%AuZ`h&oBH+1~QU~-^k4GWFdg8WFtE{ z$Vo18lZU+IBR_xeCk6P6f)t`KMJP%!ic^A;{7orJQ--pXqdXO;NF^#$g{oAeIyIrZl5DEoezATGNKMw4*&8=tw6z(}k{dqdPt5NiTZS zhraZqKLZ%ZAO&aK$t-3whq=sSJ_}gL zA{MiRr7UAPD_F@YR>(8$u4%YhrR4$KL}a>$Rs8+g{e$qIy0EbEM_x@xy)le3s}e^7PEw-V?7(#$R;+kg{^F3J3H9PE_Snrz3gK@2RO(f4s(Q~9OF1AILRqabB42= z<2)C*$R#dwg{xfSIybn$ul&b1zVqXy#?XY}C&Chr@I)XYk%&wbq7seh#2_ZIh)o>g5|5vWPXZE>h{Plz zDalAq3R04a)TALT=}6Bn{7MEgl8N8Q%I4f|C4ADN0j@vXrAd6{tuhDpQ53RHHgIs7WnqQ-`|LqdtKI(SU{o(}>12 zA%vzhqd6^TNh?~@hPJe$Jss#sCpy!Gu5_b2J?Kdlxi z$tXrMhOvxeJQJA6BqlS3sZ3)!GnmONW;2Jm%ws+aSjZw4vxKEAV>v5W$tqT}hPA9? zJsa4_CN{H$t!!gEJJ`uCcC&}Q>|;L%ILILmbA+QD<2WZc$tg~AhO?aGJQujgB`$M? zt6bwcH@L|yZgYpb+~YnEc*r9j^Mt27<2f&Q$tzy-hPS-qJsq|6YJ{cbts-^3C^b@4tVK^FDj1 zqM-`?5v9lRXFZFaoO|YGxS@*<{aV`Xx!hM2ON2rx+j6c5g zbm8NBD8eHI3YR^p>9cA{3bv=W(M%t#aO9 z^R8~AT%)JeyK|^i!N=F1ugEs9z@f{h^JS{^@lwY5gZh4oKebKDgUenezHl+tnf%Yv z>}YV|<*oPwia(n6?f&=G%X1d{9C~}Zq+^bTeiy3PpSz-*?NcGckmzII_nP#u;i4Ju z`yE@`y2z+&rCKNZkS|=Qypb;4Sv~R5quh)Cn3euY=;RSL_K2S5{-b662bS(UwpHl2 zaoePuet7h_bH#G~nj!wd{?j90n7SeJ(HLi^1%Gc;A=CYz-u(XX+x>Z+7tAX-XV=UL zI}T-SG$`TLb5~=Z>UQv2=($-QESO(HX zcUtK;35(wB6?Oig^eb=PKH0bb`a+HCJZtx<_wbE74#tadG5ncSH9luQ^QmF=MMak0 z7&_SPnHKwfp3gf{Y)qIv-L!JEnneCSet(J4c@o?huxP`l;qRjE zS=OOfvAhe<#wh$KQsIc_()_jYT*?+}x*VPSG~?L7STTlI_%!hLqb>~&m0xGN7Uc8 zJ;C@*D+{eHmjCsHxm$lNoVi=rBU3x>KU%NaoOOS^oZh|vxr8}->`E5xOxI2s#f#q_cRFLlYVmh;$dkNsn~!tS z4mq`8T#|$-rcHa6tMRj>U*~Uo^JMR!iBG3S82@-)n1Z=)|5hVi>g2u0JjpaXZ@f5V z&d=U>x9r?!sapiy3i&-_%&^_t9gF$BX|Ld!>xXV`GQMrW=mj%J$@K2X?vD9BENb1X z!JsW4rldc(zuvyxA@jB*8da}G*VC)({C;Bpi1M+nb|}z&WtoNdUk2@Me6Gdkr}0v4 z95rWbh1gv(Eua7NTAoBn!i1}Lwbh4ir5|_SGy1o7TX&Uuy7f_{Jp*$NsTR1S!O~9E zT8)ep+;vph`?!Om`HZTWlODdDJ;LMmzf8On(7%0>jjeMgSaNh@hQX6|W(s}eQ@a=y zug@ErJ6Xc5^XH}e&^}Z76W3dOX2+&0r@*lm@~Rl#Jzd?rHy=}Vd4y% zr(Die=JCB-#W${An=as8v(9}NtjgA4dHxyy-OIPLQjePBR@RD}B-QK!1LD4oFkn;u zk5lKata9PO=Y~)7{*iEdyKtw+Mrq!t_oQRz!@fPT^6}?_34=NYWjq*m)4n(%6ZBb ztN&_n+VVlmvSm3rc-_zIH#EFG?8wHTlbauwnwIiyzrpLP9bj@%?4v!V1@@em>fq5Q6LWTH80X~nzfR^q60OM9h~HACT018F Z@E&;|HjP?7OWe-sO3n!X=krY^{tu38*#iIo literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/audio_flac_rate_ladder/base.wai b/wai/staged-conformance/cases/audio_flac_rate_ladder/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..5ee396a2374541db51672f589487ffa9d86eaf61 GIT binary patch literal 1825 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 U4m~`BlA95Y!IQhp$8#S41&I4?=l}o! literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/base_not_admitted/L1.wai b/wai/staged-conformance/cases/base_not_admitted/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..0f83e75c7abccb960df7cf890c619000c878f569 GIT binary patch literal 415 zcmb`D%TB{E5JeMU!FO0Jt0+#K=CMPpxuwI@Gx4`GR$H>7_hG%K91<2U;K$_ zIZA~V!-;5$OqPg*0*GwU0XuJ)9-P5|gG1M`W@7K?%#5Swv;7MmW=F{bkfTdenYx87 zZ(E^-DS4Y#nJRcLTFnc|vpUZ-S4Ex{pt;;iQL0MKloqw9Xxq`l=l>*Gc>>QGPMZel tJD|I`qqXPfD2n#G7ar3&Z>Yat|1H()yIY&9cDc+#{KrNQZ{G{D)*pE#Yb5{x literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/base_not_admitted/base.wai b/wai/staged-conformance/cases/base_not_admitted/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..1ba05ab6252de1322a4cd40a5c3a9251f067c87f GIT binary patch literal 555 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1M7#SGCJ;Mwcfix4)xg2Qt z)3I~#EHq(!m(we4^9AKgn*+L?28{SD^!)hoVPOP$gYReTh^J>h3w{LP#7XHPx7 zczFBg-p^0I-Me|FaaM@6o8^^P9-AI|aE8v=7!h6io?9z(*O^1CP02->e9Zj8E@3to i*e(5ktvPeOeCgg2#$)_(X@19Fh|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_P%(Ztf+z%tRq#1!b- zf}+$i>X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZYJV^=Fob&s88QNclL^S?fMRAK4FfDd z#-IQH|8qkH!dMv?7{!3t3Wy^Z7((YVFs$0gz_9WW14GD928RFtK?)fee1IefH*Hya ztX-UeVdB^0UsAsv*OfXgb?IDqxPSJOt#y6xkMLwwe+>V$`RdF1{Zn^7F3$~MauC{< zJmdA_{d{rnz1X(tJv}TXlNTWQyz^8IN6k8Y0iaVED$LklYvmrRyTyMkOEq!Q%bA*+ z6D4cZa!=?j6~1?OX|Ujh=^LM~{r7MB%bTk!WKM4Sk~Sf>W>u`mrkW0me}1c!=3e@y F4**k*!R-J5 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/base_via_fallback/base.wai b/wai/staged-conformance/cases/base_via_fallback/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..4cc03dde038c723427327769cce115dfd3182ecc GIT binary patch literal 574 zcmWG{^fcVXz`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&LpJSj00OE5D4F9hK>3cvmPZ$_hzX0NQK(YToJ_FE^KyUxslV}_(qHS$4BNg`8`shJlA^XH~h;oPQS~LnEuAa`R0MU`?b&bx-j1OI<|UWN6tq37G*KNyuT;ryPll-O~K+9GD#-Ou-UXb&8{q^ zRO>~&thV66%R=|+K~e?Li>4q7UQ91uw;nuqlD6sg(Mk z@NVAn1Mf!reBR2tvu@5W`IS4|207eD3$+bq znCpI!G{>EbbLJ%H&Uu`q5+kF<{HU9Aiv=e!>ZIJUR4U~;`Y|ctj9l&V!uf`-jmEpSL|CHe#WFfEWI8(BTYUl<)4-Y2e40}ul{OCzt9Ar$pGZ7ghPNyGCk|OI6LY_|Ej=(bkx~8}cct_jz@7lM zBYh~xgSaql;{FJ^k=xRuvjv~K6luTvrrv7Eo|CE#=TM6)bSot1KI&S*x7t$r z6=L1v?)TCf%{9p2Na?#~yLZ~2hO|pg>f6O0;tz14jj%tVs%D0Gb!r-koU7zG#l#F~ zF3Qn7n|v-2lG3jlW|n>~!!-9D$-c+tk!-rT8M^#ui5bVIlXCieC(U-M&*8P}5HpMa zGV3xk#y>bvx(AnEr2bxvuGNpG_&0?;fa%b;D#IJWg|>Zz%XUaj6s(Xl=RmeWK9s#j z6-=&vXy(_zf32@W?5CCym<tvScD3^Y3($~}4&C%Q1De2~#WK7R$)Jsp=(gliy(w8IIaWV;W z^QcB|+S=uQq+R<{+-c$05VkX+?zXiXF2*xW4KTmwJNeYwF}j(8`Gz*py~*QC;^13l zXJ-}PU{5_OroU9N)Km}pwRuIpHYwE~a~jNqftZ?>7)qhjk1xP94Ku?zKC133{dT0| zKf%!`gIpuLS&boK`=(Ja`c)mK`36=I!E0a$%XhdG28Q)7`fckVf^D2T0ry9HCP=t< zO2iu2i8L(34e1X#TqSaAgCdU;J5JWTubg5nlgEjQQ6tlibR15dl5v*V1l#aCI<{vX U$@ozOsOXr9{!Pawk4VRV0a9dS#{d8T literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/bypass_extremes/base.wai b/wai/staged-conformance/cases/bypass_extremes/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..d942bb986640d59ab7d369f114a054a7aead2657 GIT binary patch literal 555 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1M7#SGCJ;Mwcfix4)xg2Qt zjaER~iGgAH6b6PsV0Z*BW?=ZU63E}c!0>k~14Gb$Abpg9VZ}KheG^DO0^(;3 z48gA%82)_(;;#%0A>SDo{{IKN0PK!4K=KdEa?9RwF@{iXHMUdy8@9h!GCaJ((oQi^ zeplsMZH{;EbX~q)*0s}~A#^Nv`l6!y+Qtci_Es|X|I}K_I@c|ad{=7u_R?yOhBtTX z*xCLw@f!YW&F?zLZV|Xxo-^rB!kgZ$+IAC81XR3UIXO#tmF1h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaJo@`6Ze8c}lf9 zO3C?oDVZQnW?s6IRkc!fW?l+Nd1jFzNV+IBEi*5*7$lRFSey!!u{5?YGqN;Fv`nEdJ z01J@u=l}oz+)#lqpk$aR5Ss&WC<8-iF9XA>4Gat`Z!s{0{A6JG{~x4~k--N@g75{Q zDL+lC_!*A!&+6Z1yvB6mOclk(on1j@zBlwb)bF`GefrCBU)5H{d2YLdueDScpA(T@ z^l@ojY~bY^xBrRHcznHTtAxStN!20mOg1{QM1QjcnfWjOtB9{wYs%hvyOte0k=v*s zmhh0NaZ32PO^16Xw7Z8r$(5P0_{XLx={NorA2MCJFyhprJu-}LZ0Q|-!pqrh+REqt F1ps(azwiJ6 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/capability_not_refinement/base.wai b/wai/staged-conformance/cases/capability_not_refinement/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..ae977f88b4fe040e91ffc2824731957c127db547 GIT binary patch literal 551 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp2H7#SGCJ;Mwcfix4)xg2Qt z>e-wyMGcc^U2E=!O>K+66FM;?y zP~ArchW}rI_&?YMAcElp5bLpc*OzQ$Wq8LT9kX3}pK^vq)K+i3^Lw*J-7n8l6ne5v zN$BCNo>O|awIe<5eyG~?yE^OE!G*yZ$ClXLjN1DpldnGemDSF#vFD!^eklD`%c67g zQ~aGh|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_t0G0oV-IN8v`z%0!q(a6BSz|zP#HObV_$RyRoz#_%O)D-C2 zf}+$i>X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZYJV^=Fob&s88QNclL^S?fMRAK4FfDd z#-IQH|8qkH!hn*D5j7^n Bz`Fnd literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/cdf_malformed/base.wai b/wai/staged-conformance/cases/cdf_malformed/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..db3a11d95b50732c842ec52f16a7b72ba10366e0 GIT binary patch literal 559 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp217#SGCJ;Mwcfix4)xg2Qt z!~g&Pm4RX)uo|dT za|Q->E1;P+3=Esx7#Nn%V_*nc3dAdccs&qrXJA;dhk@bmQ6PN=h%W)z_kr{?Ab!KZ z5d4vWA>=auZUq4hhNwk i-`An~uh|P{yJy{~{hH8xN%qJ6iP18ZNt|}aRkZ;wM!)j_ literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/cdf_malformed/case.json b/wai/staged-conformance/cases/cdf_malformed/case.json new file mode 100644 index 000000000..385c8ef5c --- /dev/null +++ b/wai/staged-conformance/cases/cdf_malformed/case.json @@ -0,0 +1,116 @@ +{ + "format": "wai-staged-case/1", + "name": "cdf_malformed", + "about": "a table entry set to 0 in mid-table: payload-malformed (cdf_malformed)", + "bases": [ + { + "file": "base.wai", + "object": "92b42e2c7a7db02e114f5cc92abe65af343c1806f4a2000923eb5124e408d455", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "e7c50678b697bf23050f7b9ddc7256e5da1c9f2e4ce7c3e3f4db8407583057a7", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "82b9092649e9b1386370377ba5e3a432488d773df2294148edcedc94300519ca", + "bytes": 583 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "82b9092649e9b1386370377ba5e3a432488d773df2294148edcedc94300519ca", + "envelope": "payload-malformed", + "detail": "cdf_malformed", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "92b42e2c7a7db02e114f5cc92abe65af343c1806f4a2000923eb5124e408d455", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "cd22012ce1f60626fe6cc008e8783a28d80a043023d6b45178536560aa3aa1f5" + }, + { + "stage": 0, + "producer": "92b42e2c7a7db02e114f5cc92abe65af343c1806f4a2000923eb5124e408d455", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "b5d83ed2343c5f54ae126fa8eebf3e1c4e96791c85a31adab9722b24d2b468f9" + } + ], + "clip_sha256": "a691f382b2ad20d285b30a25c07273f155b5c0e73245c1ae76a4b4a7e86810ff", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/cdf_malformed/stage0.bin b/wai/staged-conformance/cases/cdf_malformed/stage0.bin new file mode 100644 index 0000000000000000000000000000000000000000..81cef287995d523744b051d05dace93ad56307c9 GIT binary patch literal 384 zcmZ|K5pV@D6oui3#l_;o#fOW*U@#b53J3niqH%1G^|ySUHtA>^Vo7iCtpI^+Cfk&Dn96lqp{73-ef2cbCj zxQ|#5F3tzP#Vw?;kAwpX=jEb zwTF-*K+G8scWhWjZAa_J?U$eNJmaSMKfhWeKhpf@hhKBF+F0#8Q%^rjLQ=+>)vosK l^mjs{Z~yAw@hXjocM!qp5A5GQKDqQO{`C_PmR{Rn{}-X{*c|`> literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/clamp_extremes/L1.wai b/wai/staged-conformance/cases/clamp_extremes/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..72d581b5f5f6ca40dc71a2336bf33bc60ea5dfba GIT binary patch literal 495 zcmb_YOH0E*5Z)@HSM@HqD2kMlZPK)fAU+T&J@`0S1W`80q%OuyP1BZA@&mkxr`DU| zNeb%8qj>eE(39ZBll}{x)C&Fp2WEG^2Oq-}m-1{SK}eI^MKtN4~n}$3n0p3McJukW>gcS2zEP>}7 zz3+;>`oZJud-vR2o7-HR95a`z xowNL{KXFyqskc{O_pYZ}T5)`Vkh^yDd2=#tX>$8?G+THcUf=L8S~G8dz2EEamI?p> literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/clamp_extremes/base.wai b/wai/staged-conformance/cases/clamp_extremes/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..b236007f44c046d4baf0d195cb482bdb86f79589 GIT binary patch literal 555 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1M7#SGCJ;Mwcfix4)xg2Qt z!~g&Pm4RX)um*%#fa=&77$Vq#n3sWJod6JvFfgoDU|@(;WMEjY!oa|)1;ly`4AI66 z3>z&N7-B6M7}%W{7=mUpF#H9E$%-Wm48f}z7*?)hU4o>46A-HF#P`yas|+#KyU(x4>B{}GMK{4z#@3!i2&z%zpV;2@jH#V zs^-dOK6KG)ua(qyR@-Uj8=$J3rhMpmYUk-f9_eF?@BV(fs`B^VgU!*`*4{8x-rQCv z^-uX&_iwphjQb?&8qc$v2LIYHVZom=r8`?^9b jm}%U-`=#_$*%ah|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZYX2}WFob&s88QNclL^S?fMRAK4FfDd z#-IQH|MNfv7}HEK)YZ?s`8)Z+GbDQWi37%Bn`M~U56rbDKc7w~pPGz^Zv+vvVYQgTF zFL`bYH+7$wIcek48w-vxFYaSbmHT5WpT5Hl4-TVY9=y==Fcc|Mz8du$!|`03jh z-X~xFUR11maYO3AU6UO0)~1%cIIn$at%2_BRzBUUYZ)gWZP>e`v{1t$D_v1S>}-l} I);{m~04lb_<^TWy literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/conflict_after_apply/L1b.wai b/wai/staged-conformance/cases/conflict_after_apply/L1b.wai new file mode 100644 index 0000000000000000000000000000000000000000..008aa8566eceaded2c11b5931d6ebbe09eeed2f2 GIT binary patch literal 591 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZY8imR815Nl$OxpF7#J8hpqLp*!vG7A z@#p{l|2$BE)j$D8J_d$0DnQzcfg!Y&fgxli14Hm-28Mrcf%rEA!~g#v#f%IAK+*?@ z|1&+#1esZP_S6!O%kqCq_b>Mjly#9iaq>{c`@e@y9gALJIA_!JXQD4&>`1#?+p-~T zW%d)1%{F@3Ts{d*o6@f>_l`92*caBg>yLJ_;a;ytOcM8a*E2EHZa=u~_}xgg13|9c zLCW*zG`1d-N;rEwy2@S8E^33~RRQB(y{>DiR^@wm?|)q!m^+Cu^Y9>bY z81@zkLZK1$U_>T9RAgnT6j(ujkWlpEOFczVZ`&EO3*WumbIu&@oUYb3axe>EOc>EE zAuf=xDAWrBoLM^FhAqaEc(-hq+b#wio=RAT?sBgv$#|m2;e$h#!v{9=V}hZlbZR9m zcND9!Wq7ki7#m=vY1v#DuNRD@%`DeS+LqlX#K(kw%Vu7=qj5{+qG6&W{pROTY(5pL3mA{#{IF+);`De4Sgn{xOtcH-2K zxuOvsku}Ajq9SWlr3#CgqNt08sA!VJOim(1i{Y-|ipi=>iAW5Y(I|FG=%d^b;u5~p z)tx?$bFqgKiSai$#;&%bBn!`N0I+k556}P0i$Bj~GW)jzon9>leJC5p7~mx;7XgAuKx^nU0T@Lg37`U1qAFC4-lBKtJ*tZU z)T0IzMyqE5@)rRz8Sm$_;4tnTXkxCY1;+r|h}OPuSWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp2H7#SGCJ;Mwcfix4)xg2Qt z|N0piR?c8x_&*znf#DLe97wMP;;lgTejq*yls^ZguQD*Kx&^fC0RzM8 zXF&W0h(7?;{Rg`ML@=BH;w{`fJC6A?Fcfp$bMuke>a$P()~4IjqdWqn_k2>(ZN0un z!+htenH<{#G?FJjIeNg@ea4f%x0_AuMNAV~`u_$gnyi*yZ?&I6#P;yA@OI=#v=(IsDjv+0E2dA4nn#C!@K9h^Ha ZV1sbKw#fN)nyY4fJ?Weo`hQtSF#rQa!b$)D literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/conflict_after_apply/case.json b/wai/staged-conformance/cases/conflict_after_apply/case.json new file mode 100644 index 000000000..f747f0095 --- /dev/null +++ b/wai/staged-conformance/cases/conflict_after_apply/case.json @@ -0,0 +1,181 @@ +{ + "format": "wai-staged-case/1", + "name": "conflict_after_apply", + "about": "layer 1a applies; a different layer 1b conflicts and freezes the units at stage 1; a layer 2 over 1a is then refused", + "bases": [ + { + "file": "base.wai", + "object": "1d319df65b75012b3d2cff45bdd6589e70dece53e0be4efb463c17e8c271f0ad", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "5f44ba0bb608d0e5b3edbbe7ac5d6199a23dce06f59aa79bddc324f7a18da6bf", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1a.wai", + "object": "ebc3e691e4396cb0639b7b6d8f00a0c06922dfe14198e1466668373b101c3db5", + "bytes": 587 + }, + { + "file": "L1b.wai", + "object": "511d1ebed578caa52a9eb4edbc5306dd65baeb1767a4d9b1b1e318e1a8b686c3", + "bytes": 591 + }, + { + "file": "L2.wai", + "object": "3532844b0c4ba684437fc6901f2ab5366bf120d00132355cd31600a119b5a617", + "bytes": 799 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1a.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1b.wai", + "at_ms": 2, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 3, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "ebc3e691e4396cb0639b7b6d8f00a0c06922dfe14198e1466668373b101c3db5", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "511d1ebed578caa52a9eb4edbc5306dd65baeb1767a4d9b1b1e318e1a8b686c3", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + } + ], + [ + { + "object": "3532844b0c4ba684437fc6901f2ab5366bf120d00132355cd31600a119b5a617", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "ebc3e691e4396cb0639b7b6d8f00a0c06922dfe14198e1466668373b101c3db5", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "64869721cf6c98101a5dc9384c743251506554cdccc52f95eb85f944cf79561f" + }, + { + "stage": 1, + "producer": "ebc3e691e4396cb0639b7b6d8f00a0c06922dfe14198e1466668373b101c3db5", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "27322b811fd94a706da3f59122e54f2b202eacfd59f7f84e8f024158ad8bc950" + } + ], + "clip_sha256": "4a856cfc54c1f88820a2319f3e995165e3ae3821541149342e2b65d148c48a2b", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/conflict_after_apply/stage0.bin b/wai/staged-conformance/cases/conflict_after_apply/stage0.bin new file mode 100644 index 000000000..9e828123d --- /dev/null +++ b/wai/staged-conformance/cases/conflict_after_apply/stage0.bin @@ -0,0 +1 @@ +((@(8P8PP8P`P`pXppXpˆp€ˆ8HP8H`HXpXhp`hˆpxˆx€�ˆ˜ HXhXhh Èø`xˆpˆ�ˆˆ ˆ °  °`hx¨Øø¸ØÿÈèÿˆ ¨˜¨°¨¸¸¨°È¸èÿÀðÿÈøÿàÿÿèÿÿ°°È¸ÈÐÀÐØˆ˜ Ðøÿèÿÿèÿÿ¨ÀÐÈÈàÈÐðØè𘨰¨°¸øÿÿ¸ÐÐÀØèÈàðØàÿððÿ¨¸È¸ÀàÈÐèÐØèàèðàðÿðÿÿøÿÿ((@(8P8PP8P`P`pXpp`p€p€ˆ8HP8H`HXpXhp`hˆpxˆx€�ˆ˜ HXhXhhXh€`x€pˆˆˆˆ˜€ °  °`hx`x€px—€ˆ—Ðÿÿ˜¨° °¸¨°Èpˆ‡x�Ÿ€˜§àÿÿèÿÿðÿÿ¸ÈÈÀÐØˆ˜ �˜·èÿÿàÿÿðÿÿÿÿÿÿÿÿÐè𘨰¨°¸°·ÏøÿÿÿÿÿÿÿÿØàÿèðÿ¨¸È¸ÀØÈÐèÐØèÿÿÿàðÿðÿÿøÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/conflict_held/L1a.wai b/wai/staged-conformance/cases/conflict_held/L1a.wai new file mode 100644 index 0000000000000000000000000000000000000000..5ad11056c01e1aec5c25a9f1b4dccc8a5b7d4bd0 GIT binary patch literal 579 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_j!q~*j!pPFXJjKG$ z(k#s)G1)jJ&BWL|(a_L5H7(7^#M~mqFf}DD&C zf}+$i>X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZYQHcrFob&s88QNclL^S?fMRAK4FfDd z#-IQH|8qkH7}~(1|J~l0mL8O7Kn=L zF)$ctg&$bC7<|~ z2JGu{U3Y))oqb(b@Kup=%Jsb(v-l>MFRwhO#l*0Mw=Ra4yK{}_luL$+4|jRWCI;7t zI6QcCP5pE2?$;^z-uybPKCx|&^vXMX+NG0cT%Yx)<-$DwIf;>~4=$A#^)Lef-r>5y literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/conflict_held/L1b.wai b/wai/staged-conformance/cases/conflict_held/L1b.wai new file mode 100644 index 0000000000000000000000000000000000000000..22c7ad445ce534c3d99466a9d0468b81e8650268 GIT binary patch literal 591 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_j!q~*j!pPFXJjKG$ z(k#s)G1)jJ&BWL|(a_L5H7(7^#M~mqFf}DD&C zf}+$i>X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZY8imR815Nl$OxpF7#J8hpqLp*!vG7A z@#p{l|2$9uMxa2rC=eSnFfc|iFsz!wz_4-~1H+0t3=DriGBEu6&A{;gKS(hnLjaKU z0pdsd9{!yO)WdpE>Ga|Mt7@jGWh}4bVpsif_Sw%}(zkE*rnNt)VXxb)qxA9R?^$lk z%B7PWrC;eyzgXn;=11n=_bZ=RuH`-KxYTa4GE3X)w{;x5_s-wV$-p3VR(<&-DQ8V% z>F)PRd+*Hiu#$cpJ?qXX`S(?i7i?2pCt!7wMb+!cLb2N1&h&3WzM_{T3?DzXc*I!z O{$G&2c+Bb8z^MRPC&an{ literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/conflict_held/base.wai b/wai/staged-conformance/cases/conflict_held/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..096b8afcba7d259b31372cbb6ac52cea0487ba78 GIT binary patch literal 551 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp2H7#SGCJ;Mwcfix4)xg2Qt zH)3lMWLFfj8lFtG43Fsv72V2F}rU|^MFV2D;^VA!a}z`(A> zz_3Y&fgx6(fg#40fnmi228Q673=IEf1MxxzhLy`07(&(q>1{xIFOWV46h9ASUj?%7 zF)*xp1jNr67(!nIE&jy7@c%!^1waP?!3iK!^wF zlz;M!vmI35By?+iKUZb>TdhF;@WU5c4<~Mz8L1StGCm{gqh9&i&t+0O=FG0^+Rhhq z?8*&!*UzOHOcy?JTwFPM*7xVmKJq75DQ|wrerDaWJNFW9SGy@bzf(Wy_xIh|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_IK|vFHPPH4 z)ilj8Imz7I#4y>+$jms!!o+z`{Jq9OR{f zqSP|#n3R)PnOdY|We9X>Nn%lYDllB~N^^2*KQJ&bgnI@VG6I8>3CQMvVrC!>11vzs zpa1{=b3+Bz0O@cbCtRI@VRaM(!^(LK4F7gAFsyh4WPf5{`2QcIkdeUyNP=+p-a8YQ zy-n1%QD5_Bt1F9Z$o%H;-esp=dhM=sczSteKu(p4gPEDt!*gZZrix$Qy#M2|^xsVE zkr$jK?p0sZK6SFt%iJ?@zp{Al9iDxLK!>FsGwD*6oa!%V-(~0SvC1l~GP>S_t9<&M ueYPr<$7dg2edy+`v}5truHRSenQYC;)H=uOY+G66j2nM*a&;D3i2wlUBe}!? literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/cost_over_limit/base.wai b/wai/staged-conformance/cases/cost_over_limit/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..499cb8b2115c98e1fc6cd8162c20bd8210a06b6b GIT binary patch literal 551 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp2H7#SGCJ;Mwcfix4)xg2Qt z!~g&Pm4RX)um*^kfQllR85q{F05KN>0}CGm11kuNF)&0+GBB{o0kH}LLyRf|!$ut- ztbf|5jbE_E&eY&=T&-EhlLdi>+7&A6UQSuRJ9>|~RNti~vL_kV&Hgs$?Oql6 zf6Lix&d69zy;%4n;l)))%Pp0WefN$u&fofDMc8TmbEdD~2q!+bcA0o9eaeK#w;xRS zv|K9o`^s9`=!k89eTysu`&GXcwZ4hvW^LfJ6HR!^Tpw}#4C5&V1CFr60>LgjuXr9! aez14)CK1zhs*gA9_~_9$KeVUsODOh|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WM!zMR9ujl2XbXvQGTwHl>x|OrJ!IktOfd_B(W$x70Age&B>`< z!@$50?ipmr2n=cI*EZHWGVy0%54k` z!8aKg{=EmX|1mK9{|{2k$lwDcRd_2`cZ>8@?&d2xDrGJ2@W#^n@}rl_53BlF3khu8 z@#N_On|b@D)fyg&QM9vaSRtW!?dVF^xS9Tj2DAJ<<>&s~^z7h91LvgYd3z>Y0|1Ay Bp=JO8 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/deadline_missed/L1_f1.wai b/wai/staged-conformance/cases/deadline_missed/L1_f1.wai new file mode 100644 index 0000000000000000000000000000000000000000..83077a2bcf869206a46039d9e96eb9c6020b034b GIT binary patch literal 531 zcmWG{^fZiTWMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WM!zMR9ujl2XbXvQGPC1AUVGj6ikM-Kwp$37Nw^GIeDcyIkhVo z7#PAmgA5sgK@CI<98k;*q+x&s$oTXB|9>8+z#1k71||^(h6qKV-arP1|FanwR_+7h zdkhT!J_G50K(YTI#f%IdKr(|fgDcFzTXcy;?@!;Sg83%j<2an1MD`wFo!0&P&#HN9 t>y(+A*baZF*ERm_{48=`%FT`DCz~w1oB3B%dt|)76ymPW!X9}o5CC-So@D?4 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/deadline_missed/base.wai b/wai/staged-conformance/cases/deadline_missed/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..a6916acd0ce794c6a3cf5251fb4410ae62b595b0 GIT binary patch literal 555 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1M7#SGCJ;Mwcfix4)xg2Qt zxFYppQv<|%#=0Kvy;5P&GB2MeFFed`@Y5i literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/deadline_missed/case.json b/wai/staged-conformance/cases/deadline_missed/case.json new file mode 100644 index 000000000..fb9dd2ad4 --- /dev/null +++ b/wai/staged-conformance/cases/deadline_missed/case.json @@ -0,0 +1,152 @@ +{ + "format": "wai-staged-case/1", + "name": "deadline_missed", + "about": "units presented at 140 and 180 with a 30 ms lead: frame 0's layer arrives at 120 (late), frame 1's at 120 (in time)", + "bases": [ + { + "file": "base.wai", + "object": "9b139e7ac5c3231d15e871001ff8cf0649f60391eba0306b83bc0267190f6739", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "d3e3133bbca15a03251e0b8c00c0d62e81151f1cc910210d446683c168c2574a", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1_f0.wai", + "object": "783e92af6358629fbc76984280c5b6a88b81f631915d5fdd0074e51befeaa7db", + "bytes": 535 + }, + { + "file": "L1_f1.wai", + "object": "11f6d840df88f4ec276d3430307474dd4cc6c213fa687bcb20d092edc153e650", + "bytes": 531 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "live": { + "t0_ms": 100, + "unit_ms": 40, + "target_latency_ms": 40, + "lead_ms": [ + 30 + ] + } + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1_f0.wai", + "at_ms": 120, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_f1.wai", + "at_ms": 120, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "783e92af6358629fbc76984280c5b6a88b81f631915d5fdd0074e51befeaa7db", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "deadline-missed" + ] + ] + } + ], + [ + { + "object": "11f6d840df88f4ec276d3430307474dd4cc6c213fa687bcb20d092edc153e650", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "9b139e7ac5c3231d15e871001ff8cf0649f60391eba0306b83bc0267190f6739", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "6c9b90150098dfe58322f95d98e6b27e4897bc07b911459e609f3e0789f09210" + }, + { + "stage": 1, + "producer": "11f6d840df88f4ec276d3430307474dd4cc6c213fa687bcb20d092edc153e650", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "66f3a1cc649317d5a1e32bdb8048bd03d2879ad3876dd5003102a9caee49b2a5" + } + ], + "clip_sha256": "1bdf1771136b8f541afd56214d709a0f3e0dde069b41422c9003d0c29e18c771", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/deadline_missed/stage0.bin b/wai/staged-conformance/cases/deadline_missed/stage0.bin new file mode 100644 index 000000000..f32ab0a54 --- /dev/null +++ b/wai/staged-conformance/cases/deadline_missed/stage0.bin @@ -0,0 +1 @@ + 00 0H0HH@PPHP`P`x`hxpp�(@H8HP@P`H`p`hphx€xx�x��HH`HXh¨Èè`xxpx�p��ˆ�¨�¨¨Xh€¨Èÿ¸ÐÿÈèÿˆ�¨�¨¨˜°¸°°È°ØÿÀàÿÈèÿÐÿÿèÿÿ¨°À¨¸ÈÀÈЈˆ Øðÿàÿÿàÿÿ¨¸À¸ÈÐÈÈàÈà𘠰 ¸Àøÿÿ°ÈØ¸ØØÈØðàèðèðÿ¨°È¸ÀÐÀØØÐÐèØàøàèøðøÿøÿÿ 00 0H0HH@PXHP`P`x`hxppˆ0@H8HP@P`Php`hphx€xx�x��HP`PXp`hp`xxp€�x��ˆ�¨�¨¨Xh€`h‡pp‡€ˆ—Èðÿˆ¨¨˜°¸¨°Èhx‡x€—€ˆ—Èÿÿèÿÿðÿÿ¨¸È¸ÈЈˆ ��§ØÿÿàÿÿðÿÿÿÿÿÿÿÿÈàð� ° °À°¿ÇøÿÿÿÿÿÿÿÿØèðèðÿ¨°È¸ÀÐÀÐØÈÐàÿÿÿàèøðøÿøÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/geometry_mismatch/L1.wai b/wai/staged-conformance/cases/geometry_mismatch/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..0ed012e58bc94e9261cb59d4d27676e5c09398c3 GIT binary patch literal 415 zcmb_YK~BRk5OfL$p1|4XDvGg_I5~kh^@h0gh-_zFtw^2VrbSg$iJzBsp}c^V*5lb3 zjaIvUT8Zb3v6+7w>|h{{VS+HxAi~(AGZQ4!&_Vwe;bV^xwikWo z&Wy%lkJFLd6q#Qlata`)#{m4a*vjbA#L3grI zT2|V)LN#8S(t9t3R8?KO#=Ekxx;B*+R+iGL%GO$ywk(}jvR1V17~$|gNxeBjM(K#3Nzy0kS!fT~js_X20PAp@0^rn7X+Y8LSWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1c7#SGCJ;Mwcfix4)xg2Qt zq0384XM}YV^OMCc>KZm6`1)Mh?(OkQiUv){_-5FUyYZYgoZ<1el zpnGEe*RP_7t{>4{JvFdbd{6z#!~gg0n!{G``TU%(vqa<~cilI9`XKnI!T-|_SM3(O z$$#?Mq>u0SYQ?^O!_s|zLgdf+ul9=kDQJ3hh0Q1R#i^w-$A7;1byvM)+atqNwtpJa zSBA713l~>iI34KHRX9P$KQ-ATZWD_=$BEm)Z2R~fo=EaI&WI1WaOzy-+8*EIGQSpY X__8-j^6`b*^(M|~dYkOE6IDF{tDVS2 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/geometry_mismatch/case.json b/wai/staged-conformance/cases/geometry_mismatch/case.json new file mode 100644 index 000000000..aaa1529e4 --- /dev/null +++ b/wai/staged-conformance/cases/geometry_mismatch/case.json @@ -0,0 +1,125 @@ +{ + "format": "wai-staged-case/1", + "name": "geometry_mismatch", + "about": "a layer coded for 4×4 frames over 8×8 frames: geometry-mismatch for each unit", + "bases": [ + { + "file": "base.wai", + "object": "e28d3948368ac049f8a5fff3134677c9fc50b87a6b1b353b46b7845b55cf4374", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "a351f27f93e94c491659d57aec589a1faeee7f5959eff2ecead77096eab14be6", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "cbd4beb0765824c6f587b115842f0779b8b74c5a7db1f16990b3d34c2c1ba46c", + "bytes": 415 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "cbd4beb0765824c6f587b115842f0779b8b74c5a7db1f16990b3d34c2c1ba46c", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "geometry-mismatch" + ], + [ + 1, + "geometry-mismatch" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "e28d3948368ac049f8a5fff3134677c9fc50b87a6b1b353b46b7845b55cf4374", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "718de625810da6093c2a2bf758421dbc9d2fba12cfaf32a9a66af6980674552e" + }, + { + "stage": 0, + "producer": "e28d3948368ac049f8a5fff3134677c9fc50b87a6b1b353b46b7845b55cf4374", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "44a045223af10d71d4d19f12c0e272407638bd221e78a05a19aa66e24a13ca48" + } + ], + "clip_sha256": "313eabaa3073a65f3307278dd1ef33d59b156aee8a4748eda0fa9571a1e6ac41", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/geometry_mismatch/stage0.bin b/wai/staged-conformance/cases/geometry_mismatch/stage0.bin new file mode 100644 index 000000000..9a91c4fd4 --- /dev/null +++ b/wai/staged-conformance/cases/geometry_mismatch/stage0.bin @@ -0,0 +1 @@ + 8H8@H8PXHPpPhp`hˆh€�xˆ�8@XHP`P`x``xpp€p€ˆˆ� ��°Hhp`pp Ðøhxˆx�˜��¨�¨¸˜°¸`hˆ°èÿ¸ðÿÐðÿ� ¸˜°¸¨¸À°ÀÈÀèÿÈøÿÐÿÿàÿÿðÿÿ¸¸È¸ÐØÐÐè�˜¨àÿÿàÿÿðÿÿ°ÀàÈÐàÐØèÐèÿ˜°¸°¸ÀÿÿÿÀÐØÐØðØàøèøøðÿÿ¸ÈÐ¸ÐØÈàèÐèøØðÿðøÿÿÿÿÿÿÿ @H8@H8PXHPpPhp`hˆh€ˆxˆ�8@XHP`P`x``xpp€p€ˆˆ� ˆ�°Hhp`px`p€pxˆx�˜��¨� ¸˜°¸`hˆhˆ‡x�—ˆ�ŸØÿÿ˜°¸¨¸¸°ÀÈxˆ�€˜§ˆ§¯àÿÿðÿÿøÿÿ¸ÐØÈÐè�˜ ˜§·àÿÿðÿÿøÿÿÿÿÿÿÿÿÐèÿ˜°¸°¸À·ÇÇÿÿÿÿÿÿÿÿÿàøøðÿÿ¸ÀÐ¸ÐØÀàèÐèøÿÿÿðøÿÿÿÿÿÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/hold_bound_count/L1a.wai b/wai/staged-conformance/cases/hold_bound_count/L1a.wai new file mode 100644 index 0000000000000000000000000000000000000000..72dad16677882242a612eaaec9490e66ebdace8a GIT binary patch literal 575 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZYCkYAFob&s88QNclL^S?fMRAK4FfDd z#^3+{|8qkH!hnp`8bFR01H->|28I_@lRPH<_hHTFJr>`ZX(#3C2JW>aAqwt2wPWly-R!vqAdwNn|k||kn-|lDH w_MMj=`RjSyStw($;8&^5fkk@uV!{*mS+lk%&&;%GX?XK}=fqh|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZYX2}WFob&s88QNclL^S?fMRAK4FfDd z#-IQH|MNfv7=Z#zKn{}<5Zf^@gw``K{9nw#@b5eW!-@|;`Y(|GAEcO(!3Ri!uxCIX z-waPyhDDFgJ$}3`cf#jhjjhT5GoA*jy}h8jMkv*=*}VK;agS$VxYyz6Fxg_>r+;sK z$h4incX+W*eBbBXmZwiszhqdi?*Ad~*qV4ZUL9!Wp`MQo0dg9a>pVN|TTGVboLH@% z^zdEQt8}F*Mx(`T-k(z$yPKwQ{JkH-@!L~WtKp~j{0EYk*G$-0_JGsoVeT59{NpnK DTEoH0 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/hold_bound_count/base.wai b/wai/staged-conformance/cases/hold_bound_count/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..ea6d9f2096da699d23bc1d5367084c5915b728a7 GIT binary patch literal 555 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1M7#SGCJ;Mwcfix4)xg2Qt znHF43WGH46J+%3>!ok7@{Q^7&giCS3CSTFtCgGW_Zt>pHidIlFm&wYc}@MH}01rhTmZ_FYl% zyOr|I(`!Yxot@sI`TIo7`OCl3yIz?dcywVy?7b^x>wIoIoZbKRxBHQu+f22u{hAW} z?~{XN-0HlqSs7_Jlj4M5&dfI`jd9z0E+JPamhC|J=f_J+viT?OiWIptJ2ER+V!KZZ e+yC=|yLN57?@=i)HD6*$?4_x0T1|G#H97%(Z^Tsq literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/hold_bound_count/case.json b/wai/staged-conformance/cases/hold_bound_count/case.json new file mode 100644 index 000000000..32830ed3a --- /dev/null +++ b/wai/staged-conformance/cases/hold_bound_count/case.json @@ -0,0 +1,151 @@ +{ + "format": "wai-staged-case/1", + "name": "hold_bound_count", + "about": "a bound of one held envelope: the first layer is held for its base, the second is base-absent on arrival", + "bases": [ + { + "file": "base.wai", + "object": "62e1cdcdbea1c01f911ef377af4f4c70511ccff2160fa089cce2c5e64472891c", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "356c145b9dcaae2533dcf092657319ec7b0123a9024f14539a2ae52ba461ad71", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1a.wai", + "object": "cabbab54708aba4e296c646fd4a02d16106c778fa3597a56aeafac6bdb0d1909", + "bytes": 575 + }, + { + "file": "L1b.wai", + "object": "4f74a6d56381c49084a446bef4e7940055f7fee8e41c944329e3f00b94b4c28d", + "bytes": 587 + } + ], + "config": { + "bound": { + "max_envelopes": 1, + "max_age_ms": 1000 + }, + "cost_limit": null, + "live": null + }, + "steps": [ + { + "op": "offer", + "file": "L1a.wai", + "at_ms": 0, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1b.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "base", + "index": 0, + "at_ms": 2 + } + ], + "expect": { + "reports": [ + [ + { + "object": "cabbab54708aba4e296c646fd4a02d16106c778fa3597a56aeafac6bdb0d1909", + "envelope": null, + "detail": null, + "held_for_base": true, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "4f74a6d56381c49084a446bef4e7940055f7fee8e41c944329e3f00b94b4c28d", + "envelope": "base-absent", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "cabbab54708aba4e296c646fd4a02d16106c778fa3597a56aeafac6bdb0d1909", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "cabbab54708aba4e296c646fd4a02d16106c778fa3597a56aeafac6bdb0d1909", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "d05d257f52caba6d9d4fd3a60f4a197cd57a44354c37e879a093f746772784c8" + }, + { + "stage": 1, + "producer": "cabbab54708aba4e296c646fd4a02d16106c778fa3597a56aeafac6bdb0d1909", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "30b28e1cf0b5745f0463c7af8735ffcca8c7a0ec213ef75e694a2660c6dd63d7" + } + ], + "clip_sha256": "ac1fa9507d1004bcc1eaaf366da0013acd8c196dd5fdc2448232847348d75058", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/hold_bound_count/stage0.bin b/wai/staged-conformance/cases/hold_bound_count/stage0.bin new file mode 100644 index 000000000..b7d04f4a9 --- /dev/null +++ b/wai/staged-conformance/cases/hold_bound_count/stage0.bin @@ -0,0 +1 @@ + 8 0H8HP@HXHXhX`xXh€pxˆ(@P@HP@`hX`pXpphpˆp€˜xˆ¨@PhX`h Èøhx€xˆ�xˆ ˆ�¨� ¨X`x°Øø°ØÿÀàÿˆ˜ � ¨˜°¸¨°À¸ØÿÀèÿÀøÿÐøÿàÿÿ¨¸À°ÀÈÀÈØ€� Èðÿàÿÿèÿÿ¸¸Ð¸ÐØÀÐØÈàð� ¸ °ÈðÿÿÀÈÐÈØàÐØèØððèðÿ¨¸È°ÀظÐàÐØðÐàøèðÿèøÿøÿÿ (8 8H8HP@HXH`hXhxXh€px€(@P@HP@`hX`p`pxhpˆp€˜€ˆ¨@PhX`hXh€hx€x€�€ˆ ˆ� � ¨X`xhx€hx�x€ŸÐøÿ�˜¨�°¸¨°Àpx�xˆ�x˜—ÐøÿØÿÿðÿÿ°ÀÈÀÈØ€ˆ ˆ�§àÿÿèÿÿÿÿÿÿÿÿÿÿÿÈàð� ¸ °È¨¿ÏÿÿÿÿÿÿÿÿÿØððèðÿ¨°È°ÀظÈàÐØèÿÿÿèèÿèøÿøÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/image_int_hyper_2l/L1.wai b/wai/staged-conformance/cases/image_int_hyper_2l/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..3ecbdd2cf2db066a240b0391385f1d38cb9c5eb5 GIT binary patch literal 1071 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$^Wa}(23f&9$8lGMBspg>V-K~83J zVhKnvH$NpcC%!1Pur#wMH5V*jt(2TtkeHO2lUY&;R1Gvz4{U@U&;X#yw9LFzrCJ@O zf zXpw4~W^7=VmXu^*V33lQoRn&AY-VhpYGh)Pn3!UiXq1|kW|oqeYHDnroMM(_ng(=j zK~ZWMbxg`htV}IZvN8m^v?Q@8Jrx)(d8IiywGWsW7{Wb+3>ksJ2}BG6P|OUZVSoq7 zc>VwXe_g1+8X(OCRLl%iw2qB|frX2KAyN=XivqC}1H*bbAgv0-+CXds#FjvL2cUWn z28OkPKpY9wp9G{cfOZ!EaVgO51|Z!C6mJHqYXhq91mZrR-pLFMQPUY1SZ6aZY?#l$ zuyG*}FJoX}13H3j6HsgyP|Y!*J1zkA-T|8PjDcazYasoRf#Lsukb@Z+{s7$?&yZun z0pv|(czv3wA!wm_Q&aCmR+}GfyqUENgcS?|Cf%L=L_GU?{!E7zj~lqNH@3%o3TEW^ z`iMJ3KVniBk3;_yOKokv71B_*X9ZdoYJ9lhtlhE7t|BIc0KRRpH$#*SE)Ob~sLY zm=xz%Z82@xN#zque2dSozVT&CxQV1o)Xw=kqxU?0WqK+3M&4PyS7{G6O=M0@$r66w z<6P@%6?Elh|%1uqlOa$^Wa}(23f&9$8lGMBspg>V-K~83J zVhKnvH$NpcC%!1Pur#wMH5V*jt(2TtkeHO2lUY&;R1Gvz4{U@U&;X#yw9LFzrCJ@O zf zXpw4~W^7=VmXu^*V33lQoRn&AY-VhpYGh)Pn3!UiXq1|kW|oqeYHDnroMM(_ng(=j zK~ZWM&?J+zWW%HsOOvD&6N@CHWW$uy6idrw0|Qgz)YKGn6C2wK1xC=e|o^`@S*6M&13KefQuNcB4-|2Vw$l*6?0__IiEHd98qu%_(AT{`5wo)@ZW`<+uFZw({w!tm&smltK z60R&O-(4OR+bLER5$*Q*edg9onF=ZO1s66=DUorS$^GK;%Howm&d(YCi#IAC^5zr_ S$f){emu$!#&hU;)`5*uwM&3*S literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/image_int_hyper_2l/base.wai b/wai/staged-conformance/cases/image_int_hyper_2l/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..b5d7ce17dbff752ddd5e603df9bbc808df9cd449 GIT binary patch literal 460 zcmY*VJxc>Y5M9N>!p1^DY-DW&^Rb&VpN&|ly`X|9vv-qZ@NVziY?Kfp2!gi$1+lOe z3;%=YpRl&Fuyrn?XfZJJ=FOWov%9gayAwhT%Q}$#Byt~(hO0~u_I>3fek#3+;8cnmUbWH`S393@BR!vX^ zz1FU41ImN|I%o&=0E3EY1{zF5H&`n|k1iogz2w_O(ShxiGhJI{N5!%0GS$|Y+D={0 z$?qB)EcMVc?1}Gcu~5h_DdrD3g&1v!a>Unqa2=+#Y(I`idqRla?JdnO7gK*?lJlRK z=KT77{cUE|i51S5-2VK*{j9hXmqqhBH~%3Xb`~aPA1}n_%jeVi?egK(&EnJf^B1Dx Bj3)p9 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/image_int_hyper_2l/case.json b/wai/staged-conformance/cases/image_int_hyper_2l/case.json new file mode 100644 index 000000000..703575a8c --- /dev/null +++ b/wai/staged-conformance/cases/image_int_hyper_2l/case.json @@ -0,0 +1,126 @@ +{ + "format": "wai-staged-case/1", + "name": "image_int_hyper_2l", + "about": "a pinned wai.neural.int_hyper base, then two identity layers", + "bases": [ + { + "file": "base.wai", + "object": "aaf90792d6ff5318e5f306fbb000dfcbe73637e244aad1a2eff6dae537cd6b5f", + "decoded_via": "wai.neural.int_hyper", + "via_fallback": false, + "attested": false, + "pin_resolved": true, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "dce7282d46f88f03d5dc0fd42f325756d310fe1fc982b6e1792a1fbfbeb482cf", + "decode": { + "kind": "int", + "set": { + "entropy.json": "wai-rs/tests/vectors/int_hyper/tiny_entropy.json", + "g_s.whm": "wai-rs/tests/vectors/int_hyper/tiny_g_s.bin", + "h_s.whm": "wai-rs/tests/vectors/int_hyper/tiny_h_s.bin" + } + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "4fc1bd94bd48b2c1ded99c0053eed742f5399cec09dbd2f8694cca6a30e44875", + "bytes": 1071 + }, + { + "file": "L2.wai", + "object": "7bf0f413db62aa185969b9ccf925d40f0281601268899bb7f2e97344487f4189", + "bytes": 631 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 2, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "4fc1bd94bd48b2c1ded99c0053eed742f5399cec09dbd2f8694cca6a30e44875", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "7bf0f413db62aa185969b9ccf925d40f0281601268899bb7f2e97344487f4189", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 2, + "producer": "7bf0f413db62aa185969b9ccf925d40f0281601268899bb7f2e97344487f4189", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 16, + 16, + 3 + ], + "stage_sha256": "a73f8f964dd0b93d32f14b35a80c3c96bb0c98ce77359bc208125cce96804ff0" + } + ], + "clip_sha256": "6f693b31869a008c460dfd0563ca14f213cf3634776362841a398da7b8369caa", + "n_units": 1 + } + ] + } +} diff --git a/wai/staged-conformance/cases/image_int_hyper_2l/stage0.bin b/wai/staged-conformance/cases/image_int_hyper_2l/stage0.bin new file mode 100644 index 0000000000000000000000000000000000000000..4b754aa32c1b82d6a4870219b073e2fbe1fd36d8 GIT binary patch literal 768 zcmZXOi4npu5ChW@{m_3O=60nEVQ?Pn^x5Z}%XO}KW|)z;km13rlZeiLRttW4gHh}? zW1S`Unaf+0s#db~^gV6sk literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/image_int_hyper_unpinned/L1.wai b/wai/staged-conformance/cases/image_int_hyper_unpinned/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..742c9067b4795d96443a93215ddda85b054d8bd3 GIT binary patch literal 1071 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$^Wa}(23f&9$8lGMBspg>V-K~83J zVhKnvH$NpcC%!1Pur#wMH5V*jt(2TtkeHO2lUY&;R1Gvz4{U@U&;X#yw9LFzrCJ@O zN|nB52$xC14Gnw1_svI3=A9QGcasi z2*k@67}$W0VA})~+XYl}4CsyvK)rW><~(CySo0c4e`H|z{~zRFMutB?x5hK%m~a4j zQyE^LW@-ppXx`M+JCW7qM;mWu?E+y1gMdkQXFn0ozMenRVa4MH?(B{2F`t4NIleyP z4$+U8)Wze_KSfhlWJ~7}QAdn!9(-AK`FXmv?kxp4CK! z1J0f;a*tZJm#_Xg!+e7&Kg-%Am54m^a>=*d&mzj#EP8xi@BEx^Wp!!pE8KVOU;DjF zQr=H(yS={RYfa;uzb?7*{LwE9T6aPvdt>4TjU)5i)sqA}yMIJ%d{k$0IrYH>ox3~h z&ntz#sMvic*9}pR6d^n(dT5f+tkW+L2FK#+*eh2x61YH@tPfulO86; z`Bhs?TXs_U#1h})^Q&)s*%EFd=@PYb{?6z^NYIpHOiu|vCc`jzF2FTWD|pHdk2rXh!aSBACZU(>)1m#fmR>^+_D>v{Uo7p)J^ Lb*EOK!Kvvf}G6c z#1fETZhlH?PJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUbUS4WxQDTlB(1iGm%7WA) zrCJ@OV6sk literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/image_png_up2/L1.wai b/wai/staged-conformance/cases/image_png_up2/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..a126806dfcbf253456f6daee597c711475463d4f GIT binary patch literal 3439 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 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/image_png_up2/base.wai b/wai/staged-conformance/cases/image_png_up2/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..7da83fe954bced50f4390ea60dcc4aacd9ba800e GIT binary patch literal 1009 zcmXX_Yitx%6h1R|?wvdLF*CbuZEPi){lUXv6%b7LArxzLk=92jR5ZfUZtBE#Tbix5 zX=`E=F&d-Rppw`>L{p!s1QCc*)+o}}v_(R+4?z&zAoxIQBVl)EuG7Zko_q3r-^q7Q zPVU`W8DG%{09~Q#vE{MmWP_Iok=LAPOorG?rIV?2NVFweo4oo&dY)`< zX-GD0Z%giK_u7)p3wl?qKGB-k={0%j&agVPtXQz9XiYW7dX~rPTT%^P+G|O9Azs@R zd&)~SgmwR`=|o#&GVGFSZ))n*>mi3P{JoxC& z>aDvTPo}oj_h^yw)xfYL3zlyetOD0bUf4xJp%8KOJQB>e0>@{*Ktx(xDO;C9P;6pS zj7^|!RI^Hod7$AEfAQwRfq8p#+!XcS6M+RO(B5LWV5JB9?bDa}k z!pcpx-3KOL*njTz1Cx6)7y3uD{ol=gbYgD*>A8b5s1W^We&wy+H}^i*|6$+W zPqn-2c2;lcj!=v-#kPMPmmnyVU=kQWEyxV+GmL+6K5DT%CE~h4bx{zq%rS(}EeFd| zA#<)nlnb~NC@QL z!15295iNQTJJK9aG1tmZb7&$P|ytj=(8Ibw-m}XN=qqg+5fH8uhwX^s0tjb(c znV;3jW%d14H8ZDXe05Q&tWxhB8hQ7ViNPbM508$2F@Em&#FW#D$!ge|Q9-7ytugzMd>bR0?3ODM|y5 z8FOtEuifV8+D|jt3un*%cIN!;OJc)A-%e#`XR}$PD<0uY-QY&4hN6u49EotBQmVo= zrAteq$%U-lmy`ry#mz*iHW4No8YW_sx4 z)Y!?3!>4|q$o@V4YyO8{{`&Dxb?Fb4y{7(A>QqPWOl^Ac%i(W<_`Nk%gBy1A{s-%^ BK5qa3 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/image_png_up2/case.json b/wai/staged-conformance/cases/image_png_up2/case.json new file mode 100644 index 000000000..615daa77b --- /dev/null +++ b/wai/staged-conformance/cases/image_png_up2/case.json @@ -0,0 +1,97 @@ +{ + "format": "wai-staged-case/1", + "name": "image_png_up2", + "about": "a PNG base (a stage-0 fixture here), an up2 layer at q = 1", + "bases": [ + { + "file": "base.wai", + "object": "4c8d9bd9c4bb6aee9a7fdefb3dc880021e0a8152722d638d007aaafe6ddeba69", + "decoded_via": "wai.image.png", + "via_fallback": false, + "attested": false, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "308e58555d50408420a50a50eadbfd02ca35d3293c022ccc56a38721661e2b47", + "decode": { + "kind": "png" + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "364ef2830bb6b63e18d2524c521b5a9bc45813775eb98542fe5d4d3c2c835500", + "bytes": 3439 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "364ef2830bb6b63e18d2524c521b5a9bc45813775eb98542fe5d4d3c2c835500", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "364ef2830bb6b63e18d2524c521b5a9bc45813775eb98542fe5d4d3c2c835500", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 32, + 32, + 3 + ], + "stage_sha256": "9eecf15c0bedefea68924d3895462dd5a1958603a4ed0d85976519dcb37a9ac0" + } + ], + "clip_sha256": "d90170c38e38b10a4c6c4ebf129dc5aa577521373ecdf020b6b66207027f004e", + "n_units": 1 + } + ] + } +} diff --git a/wai/staged-conformance/cases/image_png_up2/stage0.bin b/wai/staged-conformance/cases/image_png_up2/stage0.bin new file mode 100644 index 000000000..8d82dad59 --- /dev/null +++ b/wai/staged-conformance/cases/image_png_up2/stage0.bin @@ -0,0 +1 @@ +)&5)2'A6?*3M(BK7@J4NXCLV@[dOYbMWq\enZc}5%: 26)/D%8@0:F+FT>DP:U_Xb‹®Þœ¶ã¡»ë¦ÉèŸÎý¢Îù·Óÿµ×ÿ½àÿ|Ž €’–…�¨Ž—£–�¯�¡·JTn˜Èí Âï£Äú«Öÿ«Û÷²Ýÿ¶ãÿºîÿÅåÿ‡�¦Š•¡–�¯›¢¶¢£¸¦©½�¿ó£Äø¨Êþ®Ñÿ³ÖÿºÝÿÁåÿÉìÿÂõÿÇûÿÏøÿ•Ÿº›¥¿¡«Æ©²½°ºÄcm‡±Íü¸Òÿ¶çÿ¼êÿÀäÿÅèÿÑñÿ×úÿÖÿÿ•§½–­Âœ®Ê¯»Ç°¹ÇºÀÌpy‚ºåÿ¿çÿ¿áÿÈéÿÕïÿÚôÿÚýÿãÿÿåÿÿ£¶È¤¯Î®¸Ó·ÉÍ»ÍßÄÎØ|…�»ìÿÇêÿÐìÿÙóÿÕÿÿàÿÿßÿÿèÿÿñÿÿ¨ÃȪ¾Ò±ÃÖ¿ÆàÂÐæÎÚæw’›‚”¦ÓöÿÖüÿÓÿÿåÿÿæÿÿóÿÿíÿÿ°ÂÖ¼ÆÐ·ÊÒ»ÓÝËÝÞÍÕðØêô„ž§’›§�¢¸•¬¸Ÿ±»ñÿÿ²¼ÉªÂÖ½ÅϹÅÑÅÖÛÈÏìÑãìÏã÷Øâðãéþ�ª´Ÿ©°–¨º£µ¼±»Å­·Ì¬¿Ñ»ÎÓÁÌÖÊÔêÒÓçÌÞôÙãïÛåüÞñÿëþÿœ¦Àœ´½©¶Ì¦ÂεÇÙ¾ÅÙ½ÏØÌÏãÇÑìÎåñÍèûÖäðÛîÿçíÿå÷ÿóÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/image_up2_from_int_hyper/L1.wai b/wai/staged-conformance/cases/image_up2_from_int_hyper/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..a9b962d659a0485f074cdbf267df3f1a9ddd4f0e GIT binary patch literal 3119 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 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/image_up2_from_int_hyper/base.wai b/wai/staged-conformance/cases/image_up2_from_int_hyper/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..b5d7ce17dbff752ddd5e603df9bbc808df9cd449 GIT binary patch literal 460 zcmY*VJxc>Y5M9N>!p1^DY-DW&^Rb&VpN&|ly`X|9vv-qZ@NVziY?Kfp2!gi$1+lOe z3;%=YpRl&Fuyrn?XfZJJ=FOWov%9gayAwhT%Q}$#Byt~(hO0~u_I>3fek#3+;8cnmUbWH`S393@BR!vX^ zz1FU41ImN|I%o&=0E3EY1{zF5H&`n|k1iogz2w_O(ShxiGhJI{N5!%0GS$|Y+D={0 z$?qB)EcMVc?1}Gcu~5h_DdrD3g&1v!a>Unqa2=+#Y(I`idqRla?JdnO7gK*?lJlRK z=KT77{cUE|i51S5-2VK*{j9hXmqqhBH~%3Xb`~aPA1}n_%jeVi?egK(&EnJf^B1Dx Bj3)p9 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/image_up2_from_int_hyper/case.json b/wai/staged-conformance/cases/image_up2_from_int_hyper/case.json new file mode 100644 index 000000000..f0aea95b2 --- /dev/null +++ b/wai/staged-conformance/cases/image_up2_from_int_hyper/case.json @@ -0,0 +1,126 @@ +{ + "format": "wai-staged-case/1", + "name": "image_up2_from_int_hyper", + "about": "a 16×16 base, an up2 layer to 32×32, then an identity layer", + "bases": [ + { + "file": "base.wai", + "object": "aaf90792d6ff5318e5f306fbb000dfcbe73637e244aad1a2eff6dae537cd6b5f", + "decoded_via": "wai.neural.int_hyper", + "via_fallback": false, + "attested": false, + "pin_resolved": true, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "dce7282d46f88f03d5dc0fd42f325756d310fe1fc982b6e1792a1fbfbeb482cf", + "decode": { + "kind": "int", + "set": { + "entropy.json": "wai-rs/tests/vectors/int_hyper/tiny_entropy.json", + "g_s.whm": "wai-rs/tests/vectors/int_hyper/tiny_g_s.bin", + "h_s.whm": "wai-rs/tests/vectors/int_hyper/tiny_h_s.bin" + } + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "90f3de688b569aca302e82601b0f8ad60e9835c9e359185e84e79964118c1487", + "bytes": 3119 + }, + { + "file": "L2.wai", + "object": "f6c3dafa7ad80b2e36846b5cd26b887505be2498336b5473d5c7071d34474811", + "bytes": 1219 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 2, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "90f3de688b569aca302e82601b0f8ad60e9835c9e359185e84e79964118c1487", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "f6c3dafa7ad80b2e36846b5cd26b887505be2498336b5473d5c7071d34474811", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 2, + "producer": "f6c3dafa7ad80b2e36846b5cd26b887505be2498336b5473d5c7071d34474811", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 32, + 32, + 3 + ], + "stage_sha256": "00137b8b9fe5ea5b802d961ca247e7f4e5bbf501c51d741215ee22dfda465d11" + } + ], + "clip_sha256": "095a966d4a0e7f066e7c8cd5e7593c77951c6f554d87d402e32225dc55f7109d", + "n_units": 1 + } + ] + } +} diff --git a/wai/staged-conformance/cases/image_up2_from_int_hyper/stage0.bin b/wai/staged-conformance/cases/image_up2_from_int_hyper/stage0.bin new file mode 100644 index 0000000000000000000000000000000000000000..4b754aa32c1b82d6a4870219b073e2fbe1fd36d8 GIT binary patch literal 768 zcmZXOi4npu5ChW@{m_3O=60nEVQ?Pn^x5Z}%XO}KW|)z;km13rlZeiLRttW4gHh}? zW1S`Unaf+0s#db~^gV6sk literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/image_up2_odd/L1.wai b/wai/staged-conformance/cases/image_up2_odd/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..dadc6156e46c1bc520bbe77639f5dd5d8ca8febe GIT binary patch literal 3331 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 ud=Ja#JoBsm*6q5%4 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/image_up2_odd/L2.wai b/wai/staged-conformance/cases/image_up2_odd/L2.wai new file mode 100644 index 0000000000000000000000000000000000000000..c2784df8fef7792204dd7480bae8adf664cfbc74 GIT binary patch literal 4843 zcmXw*cR1Dm`^Vq*$~aa;;aG(@=WrZkWo0INB%H|!hmes~R`$q@%HG){J3ASlD9TD? z?~w>6=X*Zi-}m>ruE+Iy-S_>xpVxief4!`3>p?Xs03ZPDi^PMKz))!jSPJZh#o&?T z2=9h;!jfY=!5d5PCMP_x9z;U>fnie^ce4nb^C8U0E|X@AW?W%ythAD zO}3Q&uZ1+(fULyf30QED6d3JJz~H^{?gTu+39J+VcEJ-c|H|>6(0|MSC-nNaf z$rU6LrwBnP!Z7kU99$NvfQ92^A@VpB3Ic&(aA*`3AuBJ7z{2F@kVp&^3B%%W@)#r* zE{i~8&Q0nV>-qi0b zsQ?w~uX(;ilp~JyT7K>7T}Z03U!bO}K81P~_#52@MS*GBc0dh117B;G1u2fZ zwM(Mg)c3gM&7(&yd5zL#AEV8cz=EPz${w0Nzx3v)CYf=VUi_}(C)bjNn)k!aII2cI ze(%(^?BW?+6{VElZLhwNCiR;}_9-m33&V=|8t?5WA^J1|>^lqX#KgU#)n`aDtkch2 z40VeR5XjT>@a&6G5L=hC?sEdY9Bf_KW%`){Vmlr^9eUQy7&==6?*%r)DQ(Qd=x(Z6X_;6|Ut~CZ$Duq-t=N zYPf}SY!7Xv)9u1&__*lh)u^(^y>G2O--Uk`hO1wxUm6mTo8INw;z2Iva3|ip>JGck(H#Y zs<*<5CJdl3YWhaQvS&%Evc16WKMB?&>+e`QL|~Kgzo;D;Q$=%%DsRY{S+`pU=0A#S z#Z4+XUhcX`({l>BDP)0_c{{DiOr>C!cZr`P3SQ4}m(GiBP=_T|t*}U6ze#xeuF9m3 zKwb1xQ%z;!UZovh#}EoyzWY$-_ESO*tfiS_&4MVwb5+dhHBJ$h5sls^0Pl;O7A8L6 z;?ypI75U`;VQuO>69s*yU5jb|kB4vz1$ZNfjJJh!V zLz7ZyNcq@u|01RJF)71_lQhoQM6x43C4E-QW-3HeUzi$0FDRpPNe=}xNjAh9(j%{C z>XlnJX{u;}uA?^Xgqs0by=$r=RtlZ7ZL-?!;aCzAIQ98$P7$9Y!odll4Ux-2v=Md~#ti%#X&4GAeJZr#sWsf(XORXQVgJXZ|CPT~*Gm*xPuKzC3PulE1NENS#+}FoFOyRVF*WwN~blPE6nm2WM31YM=-~2V9=SXb9*aFNm5kw7h%S5$*6{>Zlx@dNqTe)|$z(e>uyRODBKLdfZ?XIot!Qe3tzdgE=R!@-!`F+$v9l1h8z(#E zWtPeyODfu1{05m4>or5B<;G>_j;T`==B1S;>!W4@Qp!1Ss}R(pUx(}cUcF1yNjit) z10^DxPkhYHJ|;M9Mx1Jgy{I*o@}aHc8sRW4xp-WDlB5mmf0=PZLou;uIhA+Hx4Uez z1)}NxJ|FkU-G=WaK0udrQ=zrU`OxfB1amT1>!Vb0Bz zT;+;U_bWOU`^J4{ujz)1{W=+DC#_a0WBxE2;b5KllEzU>3S~8dTo-4bY(R(%9smLb;=TKg9NSZ}V^od6=tvv!UL@vnabcd(E9Q2KwHxpC<>80% zH%0?1x+AAsMj^D$adSjy{#UPaqlbcY%Hd;ld9jlkMYWV;RB3y1e#WBQAiKN?QKMqg z-P@Zg1~9>5cCk12v0O~(3Jq12z#lT($zra4Xo)t{A2RaVR-@9S9ljj)%TWgD{oi@`9vEN-@ zOminF>2aQG27D8}yAUG0?=hZg*L^v9$m9LWZ->Gyh#z#yu-YIYF0@@n{zH_c;(H&P zmUfThMe{;QHc}0j{G!UMv(n($juqH_arja1#KimIxOe_M3eTDWxC@jIB(#7(fcu%=$JDMu5+bpF>7tRF{ris0_{F+PUd~koi^n9mCGkU*t-tEvm z-I8e7K$#|BN>rLy2}z?kXPZ%g$ee1kqY_>`Exuq2(0t3ut7RM1zyTf1+2 z6wu*mGU16@tdVH(G_F1g%gf26Y}-SxmIUp<^P(gA9R9l=Y*ekmF&;hv&Is9wdbi$@ zW8W*lA6aK5T##-^T|wKO9~PO~!b8_*>+&ayGq&%P5GHmHEU+2{TgzU7C^XfIAqCTw z3EvOttG=Y~nl8O!4H>l+ty=m|V;1Sp4$Q;~Zh2;&NmChw80+i~$7%~~_RQ4h6uh4fhP)ey&TG!sT^*~>z% zpC`kJsjQC&B~yaRI7TS^T|D}2sxzunoxSF>M$)%RqAeDtSC`Zh{qOo{%AH$Vzwao5 zlRg0w`BcOkfho?AONOJ-_5~hVWr~wC2j=J(_iiS`&l0KN*qBRVo`dkTqQVapPg_L% zZ0~L`H{H|x@yYE3vn}xP+B-XGk_`0;;5#^5KDb0$CX9`?hvFJlN+=bH zXIA6k>iK7T+5* zC6#=Q{j?I$Mt@(z|$md0Fye9dtS@4yj^=h%teLNCclaf?v< zZ1z$&{c~g&R>Ei0I;dGcyPxzbi2Y>KHrpIjF1l2R70hUPXYghw_*nD(#+$8g-$u7= zSgN-deIC>ecuridSX%j&)n#K&xhXD8^K2xImk*OHxAm=50vbcxknp^_Z za5G?d6zf_wAjChv7Br>j`1AXRweuLqfInETZF@cc#XY&h@)!-&=Z?K&#=`KuJUJw> z{iPs=sZoGIV{`kY*}5b)9oI0F<%k}^Ba0kxZT8CH0|!k(B3^~RenE9QD8p-(Qp zRhci_7+wG68^+-FC-+L~u;B4^S9M-$OC?Sqf?w(}TW;p$#>a?&YfwI=KN0CS zDy4P$j6m|{$3hf`6GcFtXvL3LVG@t3fRl2*BymdY5TAa<@ryVKHLPB`?crvS#%zcC zSZ-s*`?x-@fCzMY2+*wtq^ZZRnS-T=t&vpOd_a*P2 zJ6$s_S~yBeMOfMPw~NzVErEsS}|`uP2$wC0l!W?*eY8Y5M9N>!p1^DY-DW&^Rb&VpN&|ly`X|9vv-qZ@NVziY?Kfp2!gi$1+lOe z3;%=YpRl&Fuyrn?XfZJJ=FOWov%9gayAwhT%Q}$#Byt~(hO0~u_I>3fek#3+;8cnmUbWH`S393@BR!vX^ zz1FU41ImN|I%o&=0E3EY1{zF5H&`n|k1iogz2w_O(ShxiGhJI{N5!%0GS$|Y+D={0 z$?qB)EcMVc?1}Gcu~5h_DdrD3g&1v!a>Unqa2=+#Y(I`idqRla?JdnO7gK*?lJlRK z=KT77{cUE|i51S5-2VK*{j9hXmqqhBH~%3Xb`~aPA1}n_%jeVi?egK(&EnJf^B1Dx Bj3)p9 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/image_up2_odd/case.json b/wai/staged-conformance/cases/image_up2_odd/case.json new file mode 100644 index 000000000..bf8574ddb --- /dev/null +++ b/wai/staged-conformance/cases/image_up2_odd/case.json @@ -0,0 +1,126 @@ +{ + "format": "wai-staged-case/1", + "name": "image_up2_odd", + "about": "up2 to an odd size, then up2 from it: 16×16 → 31×31 → 61×61", + "bases": [ + { + "file": "base.wai", + "object": "aaf90792d6ff5318e5f306fbb000dfcbe73637e244aad1a2eff6dae537cd6b5f", + "decoded_via": "wai.neural.int_hyper", + "via_fallback": false, + "attested": false, + "pin_resolved": true, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "dce7282d46f88f03d5dc0fd42f325756d310fe1fc982b6e1792a1fbfbeb482cf", + "decode": { + "kind": "int", + "set": { + "entropy.json": "wai-rs/tests/vectors/int_hyper/tiny_entropy.json", + "g_s.whm": "wai-rs/tests/vectors/int_hyper/tiny_g_s.bin", + "h_s.whm": "wai-rs/tests/vectors/int_hyper/tiny_h_s.bin" + } + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "6a80778b94cdfd81f5b7e7af0b10d0e7717cc8b050202a3ebef961fc7fbcc4bf", + "bytes": 3331 + }, + { + "file": "L2.wai", + "object": "4d11da89364c6163e0c2034697a0a9d14f1e7f36cffe504fe4b774befd9fc46e", + "bytes": 4843 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 2, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "6a80778b94cdfd81f5b7e7af0b10d0e7717cc8b050202a3ebef961fc7fbcc4bf", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "4d11da89364c6163e0c2034697a0a9d14f1e7f36cffe504fe4b774befd9fc46e", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 2, + "producer": "4d11da89364c6163e0c2034697a0a9d14f1e7f36cffe504fe4b774befd9fc46e", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 61, + 61, + 3 + ], + "stage_sha256": "384c55f3535d2443939c0c22508935c6de3ec5a1af1a3b3e3d525631a8c1f894" + } + ], + "clip_sha256": "64dba25135e5965fd511247477f911b85aa5a9b2fae263b64e791b72c9c43397", + "n_units": 1 + } + ] + } +} diff --git a/wai/staged-conformance/cases/image_up2_odd/stage0.bin b/wai/staged-conformance/cases/image_up2_odd/stage0.bin new file mode 100644 index 0000000000000000000000000000000000000000..4b754aa32c1b82d6a4870219b073e2fbe1fd36d8 GIT binary patch literal 768 zcmZXOi4npu5ChW@{m_3O=60nEVQ?Pn^x5Z}%XO}KW|)z;km13rlZeiLRttW4gHh}? zW1S`Unaf+0s#db~^gV6sk literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/inclusion_only_evidence/L1.wai b/wai/staged-conformance/cases/inclusion_only_evidence/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..c5b70bb9ef30fbc8cae3457ca12fce4c760e68b6 GIT binary patch literal 587 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZYX2}WFob&s88QNclL^S?fMRAK4FfDd z#-IQH|MNfv7=Z$71Q-|?Er2+Tf#H8Y1H-?gK>Uz_A^0;8{|AZzF;JGl2S|eO^%pKH z%$G4RDD%wexUBU5R;|AMo<*M%opx+V+RyyqbVZEvlP#}*Eq3JHE2J1&?RNcbqQyni zpc`M6MV#Y%H(od^xMx1U5Yx`1lg|`RyYl|M7|5NcwfwaH98Sx*^l=9BlSWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp2H7#SGCJ;Mwcfix4)xg2Qt z#P~uBZE-u30a)t#+TB+s{9O z{0d);YVWRU#F@iCbd{SIcsK?yHgOu1M?= d<#L=6bF!TA%H%$^qD_nU#w^To?C|1G2LOANyWIc) literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/inclusion_only_evidence/case.json b/wai/staged-conformance/cases/inclusion_only_evidence/case.json new file mode 100644 index 000000000..47cd8919d --- /dev/null +++ b/wai/staged-conformance/cases/inclusion_only_evidence/case.json @@ -0,0 +1,136 @@ +{ + "format": "wai-staged-case/1", + "name": "inclusion_only_evidence", + "about": "an attested base: the layer verified by inclusion only is refinement-unattested; the same layer delivered-verified applies", + "bases": [ + { + "file": "base.wai", + "object": "cccd9393d0329558e25ddefda3ac7a013c49873fd50e912be574d98b4ddad419", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": true, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "37c2525de082ab17d6d5470b382702053b6d7c2db4399b5ccf601b8d271f21fd", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "c2df321aa4133a60d01dd78fcc3210669175cc93fab003c4e33a6785bec8b24c", + "bytes": 587 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "inclusion-verified" + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 2, + "evidence": "delivered-verified" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "c2df321aa4133a60d01dd78fcc3210669175cc93fab003c4e33a6785bec8b24c", + "envelope": "refinement-unattested", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "c2df321aa4133a60d01dd78fcc3210669175cc93fab003c4e33a6785bec8b24c", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "c2df321aa4133a60d01dd78fcc3210669175cc93fab003c4e33a6785bec8b24c", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "67d4c6297199350f0f53db1036c869026b9698209007793eb4c0bd6d70b06812" + }, + { + "stage": 1, + "producer": "c2df321aa4133a60d01dd78fcc3210669175cc93fab003c4e33a6785bec8b24c", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "e5cd07ceba0338fae1880e57026e7d3d466e9f0935a12706435e8ac69d378e6e" + } + ], + "clip_sha256": "b00fb4f9ce8446663603cd9b7911308473bc53b02daf6603cf68a3bca6eaf22d", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/inclusion_only_evidence/stage0.bin b/wai/staged-conformance/cases/inclusion_only_evidence/stage0.bin new file mode 100644 index 000000000..2a0a3f4e0 --- /dev/null +++ b/wai/staged-conformance/cases/inclusion_only_evidence/stage0.bin @@ -0,0 +1 @@ +08((H8@H8@`@X`P`hX`xhx€ @H0HX@XXPXpXhphpxh€�€ˆ�8X`@Ph˜Èð`hxh€�p€˜ˆ˜ �˜¨P`x¨Èð°Ðÿ°àÿ€˜ �˜¨�¨° °¸°Øø¸èÿÀèÿÐøÿØÿÿ¨¨¸¨ÀȰÀØ€� ÈðÿØÿÿàÿÿ °Ð¨ÀÐÀÈØÈàà� ¨ ¨Àèÿÿ°ÀظÐàÈÐèØèèàðø¨¸À¸ÀȰÈÐÈØàÈàðàèÿàðÿèÿÿ08((H8@H8@`@X`P`hX`€px€ @H0HXHXXPXpXhphp€h€�€ˆ�8X`@XhPhx`hxh€ˆp€˜€˜ �˜¨P`x`hxhp‡h€—Èøÿˆ˜¨�¨¨ °¸hx€xˆ‡€ˆ—ÐøÿØÿÿèÿÿ¨ÀȰÀØ€� €�ŸØÿÿàÿÿèÿÿðÿÿÿÿÿÈàà� ¨ ¨À¨·ÇøÿÿÿÿÿÿÿÿØèèàðø¨¸À¸ÀȰÈÐÈØàÿÿÿàèÿàðÿèÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/layer_zero/L1.wai b/wai/staged-conformance/cases/layer_zero/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..4d7c1032b6c67d5c429599592a0616480fae5a80 GIT binary patch literal 587 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_W>EZNf7EG5m-!qn8r(9qI6H8Ig3)zHW?*}}xkI1%XD zf}+$i>X?+1SeaU+WMu$!X-Q&HdMYqn@=9}ZYX2}WFob&s88QNclL^S?fMRAK4FfDd z#-IQH|MNfv7=en`2m!GP5QhN;dl(p2ZDC;e_ZW!3Gcc_D$H4IaKS(hngAb4d;RlZ^ z&!1V~&Y(5#?1!g^CV2(CkUX+|!7BSY?~PZQ&p#7&6nKB)wcW{<4LaGmf7&YyRpcJ! z{=J&&bm?&%`!^@Q;4|TOHc!tfYVo?$(I0Yl$@UasptZBFSxmNPe83>FL|0qBVs`cY zQ0C%kFWqOXIGY%MfOS%ywSm#qDG$yG`t&L4N|&2{>Hg5M^`QK@&f3puf*WN87;0Q^ F0|0X9#1jAj literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/layer_zero/base.wai b/wai/staged-conformance/cases/layer_zero/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..bcb277889a50a01457c31043a558d029aac0da94 GIT binary patch literal 555 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1M7#SGCJ;Mwcfix4)xg2Qt zs{pYE1H*b91_o9m28L); z28I}OAa-P62$~MW!0`CHn1SKnQU-?LwLrWX$leL04+G`SGcf$Q4Wu73Fsyh6#IJyI z?->|Ez5?-228RFt!7cz13}=A2gr$NX=oE(ItiKoQOE}(5RcEs((tK!t%am33iOjAC zR&ozE8ktD#o;~;YU03<4+h5;jJ>4*~P3Fp~kWFz0fnPNDrFhDD&%P0{(mT)WVBz-n zmnTLYUA)PKXPdv$!=nX0o4(ff+Zx5Ni)@7dsuNSFP8b%O3{FAvD0P- fX@@(_R%ktXvtp~H;{T6(f5$wi-BkQ>ThB)TU3A7% literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/layer_zero/case.json b/wai/staged-conformance/cases/layer_zero/case.json new file mode 100644 index 000000000..b6bc4f028 --- /dev/null +++ b/wai/staged-conformance/cases/layer_zero/case.json @@ -0,0 +1,116 @@ +{ + "format": "wai-staged-case/1", + "name": "layer_zero", + "about": "refines.layer 0: refines-malformed", + "bases": [ + { + "file": "base.wai", + "object": "bfc96bd6d03e8e8586fd76b661675bbe476c936df985521197eaa0e129c8463a", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "925dae60c8857f8ced546f9f4833e2362a814356337e4931912a5aa30404c5d4", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "8af94471bee69da28154e7586a4f71668c82d23a07d92f85c05d6eb808343cc7", + "bytes": 587 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "8af94471bee69da28154e7586a4f71668c82d23a07d92f85c05d6eb808343cc7", + "envelope": "refines-malformed", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "bfc96bd6d03e8e8586fd76b661675bbe476c936df985521197eaa0e129c8463a", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "2d8629ca2dec91c1ae5516b6d5dcb1c61470ef765e7074290d92ed0881c33605" + }, + { + "stage": 0, + "producer": "bfc96bd6d03e8e8586fd76b661675bbe476c936df985521197eaa0e129c8463a", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "67286c7bced5bd82702cf02941771d28d539e224fa1288b9942dcbf50e63430e" + } + ], + "clip_sha256": "cab5c1a48b2d4754278d448d7a67e55ffccce8355d648010cb20e2b9bcf718b3", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/layer_zero/stage0.bin b/wai/staged-conformance/cases/layer_zero/stage0.bin new file mode 100644 index 000000000..17c267fed --- /dev/null +++ b/wai/staged-conformance/cases/layer_zero/stage0.bin @@ -0,0 +1 @@ +0(8(08(HP@H`PX`PXxXpx(080@H@@P@PhH`hXhp`xˆx€ˆ@HX@H`�ÀèX`p`xˆhx�xˆ €˜¨PX` Àè¨Èø¸Øÿxˆ €˜¨� ¨ ¨¸°Ðð¸àøÀàÿÈðÿØøÿ�¨¸ °À°ÀÈhˆ�ÀøÿÈðÿØøÿ °À°ÀȸÀÐÈÐèˆ�¨� ¸èÿÿ ¸Ð°ÈиÈàÈàðÐè𠨸¨¸È¸ÀØÀÈØÀÐèØØðèèøððÿ008(08(HP@H`PX`PXxXpx(080@H@@P@PhP`h`hx`xˆx€ˆ@HX@H`H`pX`p`xˆhx�xˆ €˜¨PX`X`p`h€px‡Àèÿ€˜ � ¨ ¨¸hpxp€€x€—ÈðÿØøÿØÿÿ °¸°ÀÈpˆ�x˜ŸÈðÿØøÿàÿÿøÿÿøÿÿÈÐè€�¨� ° §¿èÿÿøÿÿÿÿÿÈàðÐè蘨¸¨¸È¸ÀØÀÈØÿÿÿØØðàèøððÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/missing_layer/L2.wai b/wai/staged-conformance/cases/missing_layer/L2.wai new file mode 100644 index 0000000000000000000000000000000000000000..60d3eb4b3ce37625e73499b24ce9f8099865d343 GIT binary patch literal 795 zcmXxgO-L0{6ae6(X@6vmGio@@ za;UCQB2$H#Opz@q zq(1T58QzQA8;uUl<25$bHpnR$QvqP>hD{j%Kgs%wL?RK{DBQz%3g??qIyN_>EvN-Q zI)_?O8`_ZuFpX$Bz!O}|G61r60%W7zh-U&^K)Klfar7K@<^s4VzW^Yw4F6k>`F?;H zDyRV1i@Fa0%pC;qDgg=u07b~J0_a7>B0ve6NBz|RuO)y|1)vP=L*?i#T14;Ad-MSf zs{oZjfB>pOBC19b>bVT?bQ&O$NM1k+9K&}Vx_ff=;kH|b*cc!C_I-Uf4MhuS4=Nso+N$#N a4*&jibh>b9cuqQ#v-0fKjnC?@$A1CXkJYFE literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/missing_layer/base.wai b/wai/staged-conformance/cases/missing_layer/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..dad245526772ffd31b42711ef15b7c39a39f4115 GIT binary patch literal 555 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1M7#SGCJ;Mwcfix4)xg2Qt zd<+aM5)2HiQVa~y@(c_yiVO^!RDf6u zXqFxWL!2=K!)7}MhJO=*csc_^FfdG3E@EK#zZ{4+GBAX02jcw<469B7`4@rsDg#5v zeIR}c6nh1<^c@g?0*e0!xd7-$AUFfW8q7B@0iD6Ho|{=9gfHFGMSZ=QsD9ny+-cVx zf5@GRIV;Ce=%8kjYdKH4C|atc`na0WhpmQZ>r&QC+}wKIJ7wL|UkaY9j~oj-Hl1_P zm+hu~&wnak*d#D5G$~@^%x_D%pI$iJUGjW)>V_5N_d0(DUv9oy@ny=dp*j|Gi3hX>+Sz8nPJWCrpG@ePH-%_ttudDIPdmc i(Ga#A{k+{b{;)1E6qzi(Fo=6y#U=3?uAZvp=ZgS0Y{KyX literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/missing_layer/case.json b/wai/staged-conformance/cases/missing_layer/case.json new file mode 100644 index 000000000..75f56fedd --- /dev/null +++ b/wai/staged-conformance/cases/missing_layer/case.json @@ -0,0 +1,143 @@ +{ + "format": "wai-staged-case/1", + "name": "missing_layer", + "about": "a layer 2 whose layer 1 never arrives: held for each unit, then layer-gap at the bound", + "bases": [ + { + "file": "base.wai", + "object": "3a7ed8febfd72da1d2e9e21b06ff332771fb99549e7134f86528cf7dfb7bb20c", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "0ce93a7aa092b57a582b90756adf68bb6da7b28ac3852f9d547de506116c641b", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L2.wai", + "object": "58939ab250220b6de7646a0d6c73bcdc073d210d258b987782a12bac52393a0d", + "bytes": 795 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "expire", + "at_ms": 1002 + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "58939ab250220b6de7646a0d6c73bcdc073d210d258b987782a12bac52393a0d", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [ + 0, + 1 + ], + "refused": [] + } + ], + [ + { + "object": "58939ab250220b6de7646a0d6c73bcdc073d210d258b987782a12bac52393a0d", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-gap" + ], + [ + 1, + "layer-gap" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "3a7ed8febfd72da1d2e9e21b06ff332771fb99549e7134f86528cf7dfb7bb20c", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "f8a641924c3a6c023b640b14f2995330c28bd60efe7fdb19de4b1983b6fd4ef7" + }, + { + "stage": 0, + "producer": "3a7ed8febfd72da1d2e9e21b06ff332771fb99549e7134f86528cf7dfb7bb20c", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "345b86e77fcaef21b81cf8ccad9556e085914009f058f85a47620f14c60ea839" + } + ], + "clip_sha256": "d34c967288ab8c16232bd8db4552445490c0e316910f5169d39e23eb8f1eff6a", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/missing_layer/stage0.bin b/wai/staged-conformance/cases/missing_layer/stage0.bin new file mode 100644 index 000000000..1402d3a82 --- /dev/null +++ b/wai/staged-conformance/cases/missing_layer/stage0.bin @@ -0,0 +1 @@ +(8(880@P8H`H``P`pXhxppˆ0HH0HX@HhH`p`hp`x€xx�x� @XXH`h Èè`pxpx�p��ˆ�¨�¨¨Xhx¨Øø¸àÿÈèÿˆ�¨�¨¨˜°°¨¸À°Øÿ¸àÿÈøÿÈÿÿèÿÿ ¨È°¸ÐÀÐØ€˜ Øðÿàÿÿàÿÿ¨ÀÈÀÐØÀÐèÐØè� ¨ °¸ðÿÿ¸ÀÈÀÈØÐàèØàøØøÿ¨¸È¨ÀظÈàÀØèàèðØðÿðøÿøÿÿ(8(8@8@P8H`H``P`pXhxppˆ0HP0HX@PhH`p`hp`x€xx�x� @XXHhhXhp`pxpx�p��ˆ�¨�¨¨Xh€`x€p€‡€ˆ—Èðÿˆ¨¨˜°°¨¸¸hx‡p€—€˜—Èÿÿèÿÿèÿÿ°¸ÐÀÈØ€˜ ��§ØÿÿàÿÿèÿÿÿÿÿÿÿÿÐØè�˜¨ °°¨·ÏøÿÿÿÿÿÿÿÿÐàøØðÿ¨¸È¨ÀظÈàÀÐèÿÿÿØðÿðøÿøÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/out_of_order/L1.wai b/wai/staged-conformance/cases/out_of_order/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..bfed4011a5e1a82c0a124792a777f74562cb0920 GIT binary patch literal 579 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_II5ox0#MsQ-+%(O? z!Yswa#K_#jJlV`7E!Ef}%`nl-($q95F)b-AG1Vm5G$qa0Fx9}s!pPXvz}&<*(HQ93 zf}+$i>X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZYQHcrFob&s88QNclL^S?fMRAK4FfDd z#-IQH|MNfv!hn)%fE-3Opg<4rGivap&@PRY~7F`_Aef zt(m83-Pu3??~l(j`{fTkt6sjKBQAgUa(e}!dAr^nTl2^L)2(M#>UX>joc6S}{o%EW y!}*W-w#~H%)*do0-m6#Lq;Qmnb(MbobGvzml%`KBzb5);_JvG`hfMcQF#!O+h{09> literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/out_of_order/L2.wai b/wai/staged-conformance/cases/out_of_order/L2.wai new file mode 100644 index 0000000000000000000000000000000000000000..cf7ee64b3b3c8d9b9c02cfb59c333fe329247777 GIT binary patch literal 791 zcmXxgT}TvB6ae63YRdW-miB{2jb$jZ&Hv0Ag_@8;Q1rLGnB1AUv)&qKw{ceyHSHe) zDX8c{dg-O;K`IKeQd9de%hCvXiI9j03M>j=Lbfwz1K%9(Ip?0abHmm3!uAw^4z7(b zu96diJXgUrQ;QK?wK9uFan0N~b>o=uXv|?IiBrERYEh@rqem0Wqsry3bx2wA?)HM(~uSXtZj(08PWs+Cpn~@dR(Q5 zCyf&?Lh)YQgyVFL<28os_XsH%mjFQKqEw9kA3y&jnM_tKCJy*}P%0MEP&&GWZlgP> z0XH8)jVO=?aBnF@dv1HfZc zSPC$LMp5%702f8e0AlD_IY3bbKrvby1bBfa&`Z?18K4buJisJ+jozRs5nvj1N&sEx z80tnn=r}roPNG#80E{t!WYYin6sW`ZPPDr(aIvht0lq)&{BW)BR7=57Ms?w~x7{aOejdI&m;QNb`9#+F!0K;1Rs?V4&Yt7l!1b{owWEUr!|!)hk#)V7-i=q)j&Q#> z;yqpcHMe!fk=6Y-i=~pL2V2G`qqE_!BlhfG|GZ&HStKyJq&sKaqXn!GXUH^Vkmn literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/out_of_order/base.wai b/wai/staged-conformance/cases/out_of_order/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..fd86bf6092a2f6c79fa1e42df0450ec220c65b56 GIT binary patch literal 555 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1M7#SGCJ;Mwcfix4)xg2Qt zfnUNpZ}+!- z`@A5#=i*n{sSE#FFZjswI^^uK+0S2|-)?il_K*CX)$jeDZ#OXdxR!IrT^m))7m1%I zy~?*|@HJd^(52zwno9Sj203o6>>10R@D#V__%iQa$bYxU`2@?O{2EcW{~RUa`70t{ fNE#eVSL|JWnTgTwd-BiisZ05qHY@C}xpx`>qSe4k literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/out_of_order/case.json b/wai/staged-conformance/cases/out_of_order/case.json new file mode 100644 index 000000000..28899304c --- /dev/null +++ b/wai/staged-conformance/cases/out_of_order/case.json @@ -0,0 +1,156 @@ +{ + "format": "wai-staged-case/1", + "name": "out_of_order", + "about": "layer 2 arrives before layer 1: held, and applied when layer 1 releases it; the stages equal an in-order arrival", + "bases": [ + { + "file": "base.wai", + "object": "153ed6436775f886d442787c64fe38f1a6955bafbfae4c5df31e0482350743a3", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "3012a36a676a8c7e3b3738446ce38203c929f7f3c19630602ac2600bd15238eb", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "da0bcb8350d785db361b705889ae72b5a5af61f20d9b8303cb45367fb24c9711", + "bytes": 579 + }, + { + "file": "L2.wai", + "object": "7cdf23c7c5f770ce83444dc97048bfa1a6ef867b4663d15b7a5a16543cb3e3b1", + "bytes": 791 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 2, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "7cdf23c7c5f770ce83444dc97048bfa1a6ef867b4663d15b7a5a16543cb3e3b1", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [ + 0, + 1 + ], + "refused": [] + } + ], + [ + { + "object": "da0bcb8350d785db361b705889ae72b5a5af61f20d9b8303cb45367fb24c9711", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + }, + { + "object": "7cdf23c7c5f770ce83444dc97048bfa1a6ef867b4663d15b7a5a16543cb3e3b1", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 2, + "producer": "7cdf23c7c5f770ce83444dc97048bfa1a6ef867b4663d15b7a5a16543cb3e3b1", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "f77d9b57d38184c06026341a650573517cb6159d5f6930197939553958dccd2a" + }, + { + "stage": 2, + "producer": "7cdf23c7c5f770ce83444dc97048bfa1a6ef867b4663d15b7a5a16543cb3e3b1", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "0aff5da55b5d7caef17b53f885d6eb60ea6f2551c2ecf577e7cd07704623605d" + } + ], + "clip_sha256": "23f2853e24968ceb200465105f49e760b874bec4d5bb2b92cf477ee786316ae4", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/out_of_order/stage0.bin b/wai/staged-conformance/cases/out_of_order/stage0.bin new file mode 100644 index 000000000..5f908ddee --- /dev/null +++ b/wai/staged-conformance/cases/out_of_order/stage0.bin @@ -0,0 +1 @@ +(8H(8H0PPHXhH`pPhx`xˆpˆ�0HP@PXX`p`hp`hˆhx�x� ˆ� P`pPhp¨Èÿhx�p� ˆˆ ˜ ¨ ¨¸Xp€¨Øÿ¸èÿÈèÿ� ¨˜¨¸¨°È¨ÀÐÀØÿÀðÿÐøÿØÿÿèÿÿ¨ÀȸÀÐÈÐØˆ˜ Øÿÿèÿÿðÿÿ¸ÈÐÈÐàÈØðØàð °À °Èøÿÿ¸ÀàÈÐàØèðØøøðøÿ¨ÀÐÀÈàÐÐèØØðàèÿððÿðÿÿøÿÿ(8H(8P8PPHXhP`xPhx`xˆpˆ�0HP@PXX`p`hp`pˆh€�€� ˆ� P`pPhp`h‡hx�x�˜ˆˆ � ¨  ¸Xp€`x‡pˆ—€ˆ§Øÿÿ˜ ¸¨°È¨ÀÐxx�x�—�˜§Øÿÿàÿÿèÿÿ¸ÀÐÈÐØ€˜ �Ÿ·èÿÿðÿÿÿÿÿÿÿÿÿÿÿØàè ¨À °È°·×ÿÿÿÿÿÿÿÿÿØøøèøÿ¨ÀÐÀÈàÐÐèØØðÿÿÿððÿðÿÿøÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/payload_truncated/L1.wai b/wai/staged-conformance/cases/payload_truncated/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..49b053cc79d7a9f6fbda49f840b8c2536e753041 GIT binary patch literal 584 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_)x<2x*ucOt z&Cnppz|_>-I5jod+%&~BEhRC@z#=&%*}^h0B`GPz)Xd1pEX^RzDA^=6*}&9173A82 zqSP|#n3R)PnOdY|We9X>Nn%lYDllB~N^^2*e=;yIgnI@VG6I8>3CQMvVrC!>11vzs zpa1{=^FRewvoSC*3NbK*=>r9#7#LPgU|?9Wje#NPE)aibUuaY9<{z1`-8wJ&bHwPVp(+r9Y8q!(9C&sI~FSzop4PSf-wzvq8FI(x^uuG4c3 zW`-}5&1AbeantkZQvX6YUE zb)39*NwsZ*?SnZAzt=UdJeOa6B1BY|UF)y^;%Q%VVjL{lPYF7O=C1LdC7>{a`+9fS F8UTAU!{-11 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/payload_truncated/base.wai b/wai/staged-conformance/cases/payload_truncated/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..8535b6bcfab1e30243f061c141225b4cf49fffb1 GIT binary patch literal 551 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp2H7#SGCJ;Mwcfix4)xg2Qt zseQ+ zh;cJ$JUnKoxAjGb_=fXu)#@Uq+g+Ra!ig(FVL|mIrtK~FSr=%h*lqLn`uimM-j&1e z%y%sO?q#$&KYPua%Ma5ZaJg%*&p(iKu+GvkXTFyFRruh9XpJfJZWb(h c7ax;ye*5$izOO#I7f#mCujIO1ci^)J08hccDgXcg literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/payload_truncated/case.json b/wai/staged-conformance/cases/payload_truncated/case.json new file mode 100644 index 000000000..e576db97d --- /dev/null +++ b/wai/staged-conformance/cases/payload_truncated/case.json @@ -0,0 +1,116 @@ +{ + "format": "wai-staged-case/1", + "name": "payload_truncated", + "about": "a WIR1 payload three bytes short: payload-malformed (stream_length)", + "bases": [ + { + "file": "base.wai", + "object": "55bf69e46b3009f10b05573eec75d5fdab08cdc89adbbd56226f0f2c4ec057ee", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "a61d13e8c653747697bcb2456cecade400ad5537dcdd8588dacc936244eb907d", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "d6ffd0949d5830a14f34c4d0b964fe060bd69021e48e836ed40a83769d3c85bd", + "bytes": 584 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "d6ffd0949d5830a14f34c4d0b964fe060bd69021e48e836ed40a83769d3c85bd", + "envelope": "payload-malformed", + "detail": "stream_length", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "55bf69e46b3009f10b05573eec75d5fdab08cdc89adbbd56226f0f2c4ec057ee", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "bc6bf016c17a3e64d9815a4006ac9d150ad88f764aa116a2ecc71acfd7b3c5bc" + }, + { + "stage": 0, + "producer": "55bf69e46b3009f10b05573eec75d5fdab08cdc89adbbd56226f0f2c4ec057ee", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "0b99849af91786185bce80c1de17b89f1a0914aeedadea425a5c8c4048068d75" + } + ], + "clip_sha256": "627035f9387f55f17561c73517c670fe1e9a473485d5b7e3991c8aa29959146c", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/payload_truncated/stage0.bin b/wai/staged-conformance/cases/payload_truncated/stage0.bin new file mode 100644 index 000000000..f394311d2 --- /dev/null +++ b/wai/staged-conformance/cases/payload_truncated/stage0.bin @@ -0,0 +1 @@ +  0 0 0808H8H`HXXHXp(0(08(@H@HX@HhH`hX`xXp€(0@0@Xˆ¨Ø@`hX`pXp€hxˆx€ @PPˆÀà Àð Èø`€ˆpˆ˜ˆˆ˜ˆ °�Àè Ðð¨Øÿ¸àÿÈèÿˆ�¨˜ ° ¨¸hh€°àÿ¸èÿÈðÿ˜ °˜°À¨ÀȰ¸Øx��€�¨Øøÿ  À¨ÀÀ¸ÀȸÐàÀÐðˆ ¨˜¨À¨°À¸ÀÈÀÈÐÐØèØèèØðø  0 0 0808H8H`HX`HXp(0(08(@H@HX@HhH`hXhxXp€(0@0@X@H`@`h``pXp€hxˆx€˜@PX@`hX`xXh€¨àÿpˆ˜ˆˆ˜ˆ °H`pXpx`x‡¸ØÿÈèÿÐðÿ˜ ° ¨¸hp€p€�¸èÿÈðÿàÿÿàÿÿèÿÿ°¸Øx��€ˆ¨�˜¯èÿÿðÿÿÿÿÿ¸ÐØÀÐðˆ ¨˜¨À¨°¸¸ÀÈÿÿÿÈØàÐèèØðø \ No newline at end of file diff --git a/wai/staged-conformance/cases/pending_l1_l2/L1.wai b/wai/staged-conformance/cases/pending_l1_l2/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..13fb40ecb2e971ebf44163ba692fcb2ce6676604 GIT binary patch literal 579 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX__4z$C@o*w84& z!pPDvEzvN|BF!i%)yT{!ImOU0InmJ2IK|w+EZM*^#WXR+!Z6Xm*wWI}!otujISJ_6 zf}+$i>X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZYQHcrFob&s88QNclL^S?fMRAK4FfDd z#-IQH|8qkH!hn*DVhjvn`am4azz{N-fnnt~28Q5=KpJS^|NkI`j0`?N(gTPu1YGT1 z$Q-~RJ2!-xPLH`OPPq_T0$TdYxi6KkC6fOW%!O6;uTRJm#`n-pEqknfs>j z&WpHv*14?rC&Y^QXKdYdX?}()cibg^ALZ0j=|9Xrd49Xvr0IXwa+PvH!HRi@wARj8 z=XP>~Yl8yoqTL4qZEcU1Zk^gR_h*vS&&3UW5-A4`bNnspkhw3qclN4H^_9&47tp>X literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/pending_l1_l2/L2.wai b/wai/staged-conformance/cases/pending_l1_l2/L2.wai new file mode 100644 index 0000000000000000000000000000000000000000..8fbae02c11f4c2c1b384cfaa443481f107339d18 GIT binary patch literal 791 zcmXxgTSyd97y#gZwTo74>Xu#1%-J+A>pEvH>&zl#_>#~|Qds*?oSAdRMt5gzTyHE( zPlXUggiw%RZ$TvcWQB#6Bq-f1>4p(m1`$4Xp~E~Em28F!eSu;Y&Hv35*G8rKzB}8ToV#pMArc$O6D$2U9Y8nZd2JW`Y<-Isb z*JUo@D{4ferV&!PMpeVmL$by~io{hz;fi7s%CJwBDQ6;)48_z!%oHsvd+sP5;I0ss zaHk3CwsX7}rzaXs&EPe*wX_ls#vB01Ta}CP|C7x>%49N4tBG3}N6~ToCxX^t!+Mm5 z^3fQ&jqaekYXH1Bbn^y)0%UCjDBJ{4gnVcMO?m+ypob`02r#t;pm;Yxryp~BF;@W) zL)$9>cA%YA03~Qw4S-tSWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp2H7#SGCJ;Mwcfix4)xg2Qt zU&d459BC82M%^vK$yR{sO8NZ(>xB=VVK=f{SSv$4TfUMcE_9XhK}eE;Hn z%U3g)lfGV;d)+)ep|)?vMCY`3>Jp6R^Ag+xF0N#o<{EUY`i-|R!y0*^1&aHQ|2+0R gtG$Z#P*U%PC0dPf;^&m~SI2mNiWHar)MO+J0G0f|>Hq)$ literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/pending_l1_l2/case.json b/wai/staged-conformance/cases/pending_l1_l2/case.json new file mode 100644 index 000000000..7f55645ff --- /dev/null +++ b/wai/staged-conformance/cases/pending_l1_l2/case.json @@ -0,0 +1,163 @@ +{ + "format": "wai-staged-case/1", + "name": "pending_l1_l2", + "about": "layers 2 and 1 both arrive before their base: released together, layer 1 applies and releases layer 2, which applies", + "bases": [ + { + "file": "base.wai", + "object": "aa52fc04d7312d8291fa1f8f2be262cd11ca113d706c09d5ad81a039958816cb", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "1d646761414563e2a25db990dde6d1e285251bd25459062d3048ef36b4b04790", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "993e2e2e0818acb65e8a5bb9638d642e5b4e44c1ad3e253aed012b4c86dc0ffe", + "bytes": 579 + }, + { + "file": "L2.wai", + "object": "7fae2a51d3500637907d520a1773ec17b17e34b9934a4bae08d5a417628bc13a", + "bytes": 791 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "live": null + }, + "steps": [ + { + "op": "offer", + "file": "L2.wai", + "at_ms": 0, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "base", + "index": 0, + "at_ms": 2 + } + ], + "expect": { + "reports": [ + [ + { + "object": "7fae2a51d3500637907d520a1773ec17b17e34b9934a4bae08d5a417628bc13a", + "envelope": null, + "detail": null, + "held_for_base": true, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "993e2e2e0818acb65e8a5bb9638d642e5b4e44c1ad3e253aed012b4c86dc0ffe", + "envelope": null, + "detail": null, + "held_for_base": true, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "7fae2a51d3500637907d520a1773ec17b17e34b9934a4bae08d5a417628bc13a", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + }, + { + "object": "993e2e2e0818acb65e8a5bb9638d642e5b4e44c1ad3e253aed012b4c86dc0ffe", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 2, + "producer": "7fae2a51d3500637907d520a1773ec17b17e34b9934a4bae08d5a417628bc13a", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "e86f1440ad1a1324cbf98f42cdc76a51e1331d64c6a205d6f929ead5d9ee83cc" + }, + { + "stage": 2, + "producer": "7fae2a51d3500637907d520a1773ec17b17e34b9934a4bae08d5a417628bc13a", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "501ea9070aa9944871c02386298a3304def37b26ea2076a2adc2a15c10538a84" + } + ], + "clip_sha256": "63165f2901cceb0953886df8df37c7851157b8c4c8cbb54b9fdf871f7aa0dd8c", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/pending_l1_l2/stage0.bin b/wai/staged-conformance/cases/pending_l1_l2/stage0.bin new file mode 100644 index 000000000..96f5e60ec --- /dev/null +++ b/wai/staged-conformance/cases/pending_l1_l2/stage0.bin @@ -0,0 +1 @@ + 88 8H0HXHHhHhhPhx`xˆxx�0@X@H`@`hXhp`xˆpxˆx€˜x�¨@``P`p¨Øøpxˆx€�x� ˆ ¨˜ ÀXp€¸Øÿ¸àÿÐèÿˆ ¨�¨¸¨¨À¨¸ÀÀàÿÀðÿÈøÿØÿÿèÿÿ¨°Ð°ÈظØàˆ� ØÿÿØÿÿèÿÿ°ÈØÀÈØÐØàØè𘰰 °Àøÿÿ¸ÈÐÀØèÐØøàèÿèøÿ°¸ÐÀÀÐÀØàÈàðØðøàðÿèøÿÿÿÿ 88(8H0H`HHhHhhPhx`xˆxx�0HX@HhH`hXhp`xˆx€ˆx€˜€� @``P`p`x€pxˆx€�€� ˆ˜¨˜ ÀXp€px‡p€—ˆˆŸÈÿÿ�¨¸¨¨¸¨¸Àx€�x�—€˜§Øÿÿèÿÿðÿÿ°ÈظÐàˆ� �§¯ØÿÿèÿÿðÿÿÿÿÿÿÿÿØè𘰰 °À°¿Çøÿÿÿÿÿÿÿÿàèÿèøÿ¨¸È¸ÀиØàÈàðÿÿÿàðÿèøÿÿÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/q_zero/L1.wai b/wai/staged-conformance/cases/q_zero/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..56ed2b4c0ff1a19eb7b5d8e6526b88fee969cb0b GIT binary patch literal 583 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_;A}!V2 zAju%n#KbbuD8(QdC~T3IY-wqhXqIAOkZNjSo@!)ZmYkB7lwx6&m}Y2bW^S5j0d#Fa zQEC}=Ov*{DOf6EfG6cG`B(W$x6&Nmgr8zmZKNuJo!aaiw8G*sc1Y~nSF*A@x27mtl z|IZB-U}R%pV3c5BSYrUBLx6%k3=FIGF)*xr0>s}K821?x_vY@owPTyd z`?Is10=5~}F?V@R;ptoJCw!9qZ*}K(uJgJ~40AW{Fx>t@GQs$${X1{31+NZy8#Q@K zHw0AaU#@62&DC1Pz3PsY-Ju)CDc!4Pt@hvhaqWcV7oOh!v!FBUDc9W4-Dev(S^+Y@ B#Z3SJ literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/q_zero/base.wai b/wai/staged-conformance/cases/q_zero/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..45707720c81166ce1780665885a856ccd5ec933f GIT binary patch literal 555 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1M7#SGCJ;Mwcfix4)xg2Qt z z4Q4=W!@v;b&cF~bn}K2J5+Gi|!0>x514H0uAl}Wu@aG_qJ`Kc|f#UZW7?wQ)vR^YW zEdRj3@b@zVL-2P7hLyh<82i%hgD>j7W_8gQIs?2^*rBL)E6}O$0ZqUsY%L~TzvUjJJkd?6mIXfI9hOQc~&vU z`F(Ox2c)N&+;^7zx8~fmQ`QNMMR%XDpI;x}_hYl=%Qdd*&6|H6iJ5)DdRqST)rP)1 zx23H)cVlhVJB}%GW`3E#=x_igbT_h->HA_LWnsxp@*5p_EW{%gN#fk mVq#j0!Yce2>nGYu#qr%%v@Xox}@=s=v+v literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/q_zero/case.json b/wai/staged-conformance/cases/q_zero/case.json new file mode 100644 index 000000000..63ef736e2 --- /dev/null +++ b/wai/staged-conformance/cases/q_zero/case.json @@ -0,0 +1,116 @@ +{ + "format": "wai-staged-case/1", + "name": "q_zero", + "about": "a step q of 0: payload-malformed (field_range)", + "bases": [ + { + "file": "base.wai", + "object": "fd60503ddd8fe70b0a449a2d0c0508fc996a6d80e587e206cdfbd82af11675a8", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "5896afd7ae24197e11dfeb175fa3292c3802b0838cb1247a624f3193bbfffe6c", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "ff4495d5f5594c6c9f1a247bcb864d8ea056d69561fdc6c267a13fdae0b614cc", + "bytes": 583 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "ff4495d5f5594c6c9f1a247bcb864d8ea056d69561fdc6c267a13fdae0b614cc", + "envelope": "payload-malformed", + "detail": "field_range", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "fd60503ddd8fe70b0a449a2d0c0508fc996a6d80e587e206cdfbd82af11675a8", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "051d74b233e79babef8457b02d9181caba57726215e1e7dd9247a4194c082234" + }, + { + "stage": 0, + "producer": "fd60503ddd8fe70b0a449a2d0c0508fc996a6d80e587e206cdfbd82af11675a8", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "6af52f9678efda5113e9f4a23b4f06e7ebc8873860095c7a05b5ff8c531a4477" + } + ], + "clip_sha256": "e1c0b26d1993566ed46788477da676478a9bc99ca9305de3e060e4df230869ee", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/q_zero/stage0.bin b/wai/staged-conformance/cases/q_zero/stage0.bin new file mode 100644 index 000000000..8350ee3fa --- /dev/null +++ b/wai/staged-conformance/cases/q_zero/stage0.bin @@ -0,0 +1 @@ +  008(0@0HP@PP@PpX`p(8(0@(8P0HXHX`X`pXhxhx€(8P@HPˆ¸ÐPXpPpxhp€px�xˆ˜@Ph˜¸à Èð°Ðøpˆˆxˆ˜ˆ�˜�¨¨ Àè¨Øø°Øÿ¸àÿÈðÿ� ¨˜¨°¨¨Àpxˆ¸èÿÈøÿÐðÿ  ¸ ¸À¨¸Ð°ÀØx�˜€˜¨àÿÿ ¨È°ÀȸÈÐÀÐØÈØð�¨¨˜°À°°À¸ÀÐÈÈàÐÐèÐèðàèø( 008(0@0HP@PP@PpXhp08(0@08P0HXHX`X`pXpxhx€(8P@HP@XXPXpPpxhx€xxˆxˆ˜@PhPXhXhxhp€¸èÿx€˜ˆ�˜�¨¨``p`x€hx�¸àÿÀèÿØÿÿ˜¨°¨¨Àpxˆpˆ—ÈøÿÈðÿàÿÿèÿÿðÿÿ°ÀØx�˜€�¨˜Ÿ¯èÿÿøÿÿÿÿÿÀÐØÈØðˆ¨¨˜°À°°À¸ÀÐÿÿÿÐÐèÐèðØèø \ No newline at end of file diff --git a/wai/staged-conformance/cases/refinement_without_base/L1.wai b/wai/staged-conformance/cases/refinement_without_base/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..8665eb0e3db0699e69ee4429ca079ee46200c238 GIT binary patch literal 587 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_F1iG{&u_!$i7%q9GIXSg|7#JABJ%bDxfx*cHWOG079iu# z|NsAapaN@v0&B#8`iy}%oPi;9G6O@%K?a6@Pl5O=1H;OH3=IGOgA_9|_y9=|cGYc( zcpAsSaBkhNMTdfKwi*Xax%^1h@zS=A##v?e+8%4zdK~cdF8p+U7T=anKLmeH&}R14 zXia$G=EUglEG+V0?&QAsJ@{rqz~cY_ literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/refinement_without_base/base.wai b/wai/staged-conformance/cases/refinement_without_base/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..3adf4d542f28c25fb4f72d16030dd86fa8c3b47a GIT binary patch literal 551 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp2H7#SGCJ;Mwcfix4)xg2Qt zV8#b1Ep|3NMQIuZy@0P!y74J$7Ixz4;s!tOE=I-2Se+P>*JHQS5uIr@n| z|F%_`f8Vykm71(V=iTqDQVP*9-Qp57HTnBZ?dexu-~U^)W!8~-9?QI*pSi9mIOB!c z<_$m3O#6Q=tKRbbm*{uDeOG+ua9uCD)WZ3A3s#8pvFb9WmZdUp dagAY*KNhzyHAymycbC)A^)?qSReG*-0RVSczxV(E literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/refinement_without_base/case.json b/wai/staged-conformance/cases/refinement_without_base/case.json new file mode 100644 index 000000000..abc69b0e4 --- /dev/null +++ b/wai/staged-conformance/cases/refinement_without_base/case.json @@ -0,0 +1,96 @@ +{ + "format": "wai-staged-case/1", + "name": "refinement_without_base", + "about": "a layer whose base never arrives: held, then base-absent at the bound", + "bases": [ + { + "file": "base.wai", + "object": "76793cb2d9d368c511d8e2188b3d1748b99308d26d218e98424ce92880c3858e", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "5bcbca1cf9487c63a9004ee51ef21693320981076eb7fe6c6d195ed6df49f0a5", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "267f0623e5b06746003c95ad5df5d029dcd556ca4c89b9ad539a94c8aeaac6a9", + "bytes": 587 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "live": null + }, + "steps": [ + { + "op": "offer", + "file": "L1.wai", + "at_ms": 0, + "evidence": "none" + }, + { + "op": "expire", + "at_ms": 1001 + } + ], + "expect": { + "reports": [ + [ + { + "object": "267f0623e5b06746003c95ad5df5d029dcd556ca4c89b9ad539a94c8aeaac6a9", + "envelope": null, + "detail": null, + "held_for_base": true, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "267f0623e5b06746003c95ad5df5d029dcd556ca4c89b9ad539a94c8aeaac6a9", + "envelope": "base-absent", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + null + ] + } +} diff --git a/wai/staged-conformance/cases/refinement_without_base/stage0.bin b/wai/staged-conformance/cases/refinement_without_base/stage0.bin new file mode 100644 index 000000000..403b30a73 --- /dev/null +++ b/wai/staged-conformance/cases/refinement_without_base/stage0.bin @@ -0,0 +1 @@ + 0 8@08H8@XHXXHXp`hx`pˆ08H8HP8P`H``Xpxhx€pxˆxˆ˜8PXHXp Èèhp€px€x��ˆˆ˜�˜¨Hhp°Ðø¸Øø¸èÿˆˆ˜�˜ � ¸ ¨È¨Ðÿ°àÿÈèÿÈðÿØÿÿ °È°ÀȸÀØx€ ÈðÿØÿÿèÿÿ°ÀȸÀÐÀÈØÀØà�˜¸ ¨Àèÿÿ°ÀȸÀàÈÐèØèèàèÿ¨°È¸¸È°ÈÐÈØàÐàðààøèøÿðÿÿ 0 8@0@H8@XHX`PXp`hxhpˆ08H8PP8P`H``Xpxhx€pxˆxˆ˜8PXHXpXhphp€px€xˆ�ˆˆ˜�˜ Phphp€px€xˆ—Ðèÿ�˜ � ¸˜¨È`p‡h€�ˆˆ—ÈðÿØÿÿèÿÿ°Àȸ¸Øx€˜€�¯ØÿÿèÿÿðÿÿÿÿÿÿÿÿÀØà�˜¸ ¨À ¯ÇøÿÿÿÿÿÿÿÿØèèàèÿ¨°È¸¸È°ÈÐÈØàÿÿÿààøèøÿðÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/refines_malformed/L1.wai b/wai/staged-conformance/cases/refines_malformed/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..10b07f376095b1b2abbec8588a9c781a86d9d494 GIT binary patch literal 587 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_m$kfQq)X2oZ#l*?U+}O?0$kD{n)XmJ)(aZ$MG&goMc5yXxGcs{8b~JQ#2D-MO zD7B0_CgmhnrWPq#83J8el30|U3JjOL(wv;yKMV{E;hsT;jKJVz0EdJ01J@u z=l}oz+)#nlAVqROY{S5?Du;pL|4askknIc%D{lemuR#8PkU~a=03hiD#F+_K)7gNu z((7}RXBXt1p0)qaE^9TZ`@EGJ&c+`t(>K|@KPkECK=8DsTbouJ@OSWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1M7#SGCJ;Mwcfix4)xg2Qt z=r=UiGd+#Dg(pvISdSc7cwxcSi-;%yqbaG-)07em3x5f<3M~C$iE7t?*i$k zK=C&W3?c6s7*>5^UK#|zQOFM;A_Xtu#mTfeJ}r^=QnitcAqpl zXt_hNW}TVr8i@ogfm8pjdHaQ9nvOB-hB{ifL0vuV+@MbXi~9ksz%F2joui$H&m;R$ zm)qUN5$wVJDDcRH*}tzd#y zB48^~Fu%2c5I?V=wdmuvE^bh-|36BOU>~R(P@L~^QxCA44fN3$xTm9IfHye+V4$MS zhw zyZ`{u=>Lcq0NO|Gaa^YohR6^AWKHG9u;^gOJ#|`Tc4Tyz=yc_DIYe8*KI@))r7O^c zu!G^Z=3!i0Mtbq+6O? zm^*v~qbu^43?aJ3xVkHrM+3Co3cXApzf2`hp##QqkzqT6WWY~i!1&CGY@(z&&p z$C{P5SUNAqC?LJ`*_w>28ZnZ=CP2jK=|a{+0@D>2j45$geE8D(+lV7D)rL)TY7I#W`mPMWFnu zY+qeLAPa{or_s=`wCj}N1EF(OjOiR5xwlZV(7-+0Dk`YtDafDqm2k2apW&bw>j!$= z%Tu#ahE&t=p(lww;v$$*C(Y|~m;#G&EgpLjZ9>xR`<21Dg(rhXuU7mNIw%Y`4~4|H zmxWdX3W48UW)q~R%MU>_J^Stp!kpZ8(7`gKx} zq6PG5H^dfX=d`N*OY&t7chcjWwvA|jd%5F!Kl1P5T}@36{xkQbPNAd*fo7=W`B}3w zE+-B_bL+m!_|qHeox4Nh)OpjcbSY2Vp|5HlX96wSK363e#Xg0fCOG$a+ju~m`}NG~ zIlqs7=WnumseK?O-T$kh7d5FkS-2p&Y_R2?sIC)~`iW>Ku=0B2bz$Aax>9~pQFwkf z7Lci)F@IU`wO^Ibdsm9ZX5 z+RPSbG>K^Y8I^f#n{hoFT)Etoqrjv=u(t(q3*yBfSJ4%0|jR-s(t1f`DRkRXRHnxo&UIn-LSzJ4UPA(nbN%wU+a*KoujL z8;alpk%{@aSn& z)uphUDw4QajgVx|Hs?^DV<`I%_icUC{dZb=>sZ8d(zW6&nMBCl0lQNlHZmVd7WJG! zcy&*My=PPJxcC9$EXG@=0)js?tqljNz z7ovr63jv7 zXW5xkZ3$Un!(D{)x&k!A{_s5E24gA?f_g#s?OEtYu6+^3B`g&K%&1vnp!vy}pnr4h zncG`EG-U?Gs}y8NkEU=jGi_Dm+Ekb?g-P6vk0mWf*8yN#LL@zo;v8 zhEE(~gTus?8m}+-bM{fVc)vV(V=SnL<8dc*-^M_<<yp%OkLiyO(w{$@_p=am?kW z->3P10jxi3UT_G&L{%06KPJuCa+JP5)a&$}7>z>G)>cH#63|;OQP(`{hHl(O>it}k z=5lkoubp?)Solnq{!P-Y+i2P~kWw8WWbPlnpWS`@hB4K8L-+^fB?>9v53D~X2m)s_ z%QuxuwUv7nVV>xP47Ix*jLKaIZYD&N@4@II#a!$Jl~|B zFp+L!u=E$s-$R-d0`cAMy*u*uYb_l!runQEldZU*~zX|Z^A2P!SYmH^FfE0HCO}cAtgJl!L z{$IbAB;_V%PneKNnSBcq-vOQsttKC8f7ihfX@~1m<#az16{q{=Pc5G1npaVWxT>=W zE7gclnVfofTTV-YP|eB1*>bBix^YSE?wb{9!1zvJYJekbagu@(m-5Ni@!;2`-@2k= zj<~|eLr6KyxvlGAxAQs{MA|K0U7??y_Anck$ue3q>!od3HNrH~jkL|MgR`U>xbo~_ zzE5@?YDi+ZU%y_-etRmJW5ej!NT$#Y%NuA2)Kc>nk;5*TXsm6Z((JZ;?wAryQ>bim zQ97~9sZ2Ed(RSweEp7Op2tL^#*gGGa#xsd8>{M zpRE+#dHYO$lxVbV_@PR#5-C5dj7%awtdpp0Ti|F_Q@-5x#d|^OSvvd@C?$Hk7vZf7 z8q1FCc+)~lD)J_i;3ibW*i6hOGDY~Rjrw->2dQh`z4Vlqfc@sHe54~w*iLb59KO2M;xgJq=a4R@H`EJU7u)D1)&r>xSR-Zq30Yj*|$>QC|gxp9}gXw!h+g z%37-H1nXqXyq4sdoEZ~Fo;1_bZ+yv~C99WtzzW99@WdMR$CPSGS#6%r$9X7F8*n09 z3p;I%dRw*ao>&l8(x;prDWULglQK}HYa`H(>^nd6F8&sGBwug}Iqi}FRV?a8x24s4 zrl!YV48wIp$s;n_!k8t0dXsZx5tpVdlV{Ge?nv7#$H?n29LWxg{eD4Lubz-|8009Q$P%r!iT4&f`~04#b&^5Et4;J~ z&KR6jHtI-2ni(b->9+QpQMVB@z^{zKw0MTiBgmiSpyl>*E{*BcYrU?n(E6$~CytOL z^-S43S|N+Ke_Ht89!^q+80P)$D~)W%(~8j~ef ziEiW33}-Bf2MJzF-*O8Rd!dFgT2EtiB`xOk=S!DbhhjK!^vy z->Y?Z3boRV<3tJ*^NVxbdXqn#Lhs zx}+NeFsbrm*@1WT&BFx2SwTP&(he$+(Mf&WXn>(q_#x3nuRee7xH8rdCyaCA0>rva zEnr%Wv15>RJvylNRilJ#u*@S|&}Y!fy4P4MnnpN$4LSpAA{v}1!TPo1b;rR>iTA?@5n!4wkADgDSktMelrMVI2 za;YD4j^E9eyjgv=8nY*o^i%kGKuQw-@0NCUR_ zDVxr$&Z`VD2L-%}p(pM~E>TuLezL55A3FiOG+*O|-bgzBvnJ86EC0BXv@^B#=F{@4 zSI~0H#8DZa)K)2?Uv973B~OqTM9yAH!e*qZu0KS=#qfT#Vjn2O`H&iit4R&XQP!19{D$KU@A#gYiP2fRyLx2wi+0{?2!B zdUex>k67S>dnG%0`*W0Lg`^9~pQ3f~gj7dXhFHs03`saq#qAeXL+5M$@v`I~Uzh6k z`E*OVXoFCH%-ob_qm%F_NBI`=&&;2OxJu$@v}sDFnr;ioCEMKY>8zJ{~-lOGlwyTyuE8V5@Dm@$MDqg*VUGQlvDOphB3rm_Pvgq6BvXAYQCxV8Sy z{PR1n^(C2ZPDNAkD@@BhwU$V%+Is*A&fr)}3o?J1xc?MkndHx?7>n5_4B2I0jz4i* zob(j?Ah8wr_X)GG5Kh&$jTST#bmn97POk?mI*aht1eS|_F*p$_^g@9v{oX7^nhjASaXa_=D`@MAedC|0DQ?4{st~gbGyR*L$bSXcqKWM! z{q~n2BD#_j%tGvcE(RWWtuH8=Y(U4aU&QpS=J))?TGE?t_UM1wk`l5zH<+f7r=OEv z+cLH{>|L;$oUWWpxU-OxmmwCRg^%L~C?k18icUod?&5O_b_b(#|4xh4x%DY1t3)-r z6JMeeq}eVI=W?=I=(^&Kg>DJ76aF0fQZ9RkG zv>A`lnwroGaOfqm9t^*}9UzNmU|`@Gf4qA;?P(892bqW~N;kAG%o{pek3`Q7kKjw|?-;b5EX7GvXD&!4IRwZXP2mhwL zD5sIqv2XD?rlGF(k(q9kZFid5V71@B{_cV@h*>H#^bJ$H&m1~VvXu%7&&sb4q9@m1 zwt8i(^}Eg?HB7CRVau~3vSUvMh1=>70na7L)}bg(Qv4)uekvo^h0_@s;-4%|(SOEE z6%>3;I4Wu{&mK+GsgBFv6*s88Cu`^k{n6RzJ76!@u%-=3_$`JcEhjCnYvxsWt|Yx> z!4=}nmw5-mJogvc$(lg7l|I1$_EE*#$*CXmIFm)6`R_(kILM z2VBf|(V*scw%(}_hW@Z({cw2Nx(1n2T3D{jjld{FLZ@B1rrSFOLZs9a^18j&Y&yM` zr?$i^!X50v*kTUFAD;8em&r5wU(lq}(`98>F^p}h_i>rL3E3=&QN0+Ie*MxydM>pS zJybJj-<CX`8+S$}{xB7980Q<*q#z>)(yIPM_;@jXv`rogq)=FxIvBO5sh3_fKRfS65;L6E-u7Ipj|1Gizmj)SYs%FY zOH6}Sv=u&LXZ|4||1}`MGfekah?qG#6WOj%EwbaJllfli)M$W=Sirs%LH1?OSagwG z)$SX_QLaW-AT!p2vqM=XU5eG5yopp7A9{VhcWdDLYT^M8$=W z?rvIBLe*YouP4`bY$#@QQO26HR65JyjjXeGatMGXk_59Yls+xgzSeFo4{?(-gLJ_e zaJV=;TW8uWGNNr`h9{DvL+L1kh$0`FS^kmpP85lqlt%o++9F*eeOWX)d+C6?{B^dN zmx)L~=yLs}T0EpF{8LT$nBX4qwpLA>gGOV03Gqi>HxK4_)s|mYQDf3p@7qSvrgrsL zX+b==S*{L7S~1@KvGNuEiAnrJ4SKd6%5PM!^$v;&C>5i31>|q?*5_xQRFV3cq^X;~ z)6sJ*Fr~#;Cxqm7eh!S&SK%qF(XH98b=x{VbiW|!NMY>zUL5{Bw`4=g!7W0o#IMYw zn^B6eRMh8F>Jl#K2;&OemEre)^G=2NWdku@R4`-{3FBWV)U zp+M+LPAB_<#NQ7x2PxQNlv$lP1nt1^CJhCBW+LJd_N!dO7`Q%;-cErDnf5qQ8_)Op`I4ke_S|f_qvQT&~^6x^uDQzhb+nwJk ROUAp^cUB9yB+AVr{s)YyEuR1Y literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/splat_attr_1l/base.wai b/wai/staged-conformance/cases/splat_attr_1l/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..76c814a8d79e9dd67704173247587434dcec84c2 GIT binary patch literal 3319 zcmb7E3sh5A7QGar*Uqe~dTq5=Uj`#!)?K5b3b&3id_pMB2R z=iD0^9OfP=AS6wc!f+yAk-LkVXuc?pRdNjeC2LekajZ_uDPZK(I##WNRm;Y!I0d6) zDN>_kRnb~DA&Jwnar}OoNWsK2a!$qRJ_H)PE{X9fM&|;8Xa(>Jk$%2Np;0S29j8%q z>KKu4nrIEDRs#Kh%zRP2meXkI9yhYX^SCCn3LQ4)ixOiQiI)^!ZXO~#i|voNRPsFj5daaOzI?+ zN}oHGkjSuQ?g9%!UMGaSG`#4$6&9I9$kN%nDoe$?0$1ehtvs|fBQW1Qt8)8?S%LX~ zd-UdaHR1lFp9RMB%6}Tu1F8G7pNRDieyy-qult7=AsK;3V^n@m*z~~DWi!0C52gp+ zs0j}Un!Y~Jl1wF2u&js$e(Bem*uY{*CiA~m{F?);R`?DfEU{w+izR*yMx&9g{^3R6 z`-b1?a{!CldWUTkM{Oha5nuYh8;kmkaM0(oi~5eS&LPC~1KPNkO3jx)jRvN1YKL4H|4%H?EIem&4SAau}V{(EXgWw&o!xnNffV&smCKrK4 zPS5FQ4V)h^(ct`Mh|}cWaPFgP9pOcf`k`U(h&ITV(0n@(*MKM1n4E;LO#&|#jwmrA zw*fIBQ$8TXd?&WbhUz9wzs|$=dloc9&O-R{b1^zs#N+1(TJ^3Onj_&EU~eHlos02u zr1e1$g95x13mwMPL3MQ{#4Hv$qX#Cm2jk(uMK4Ap@*MC0=Q7l}6+Y$gfmZx%jLsR{ zG(JCfiwT*WfjH=i={fizj!7H*ko&l2fc8W-H2>y@;RC!7coX19=J2_`LmwK;R0$kn z@HJpopTj)UZ~k1O7CNg2C&YmUy@>Z1d3HmuhZ;gUG6|Vlh`iAgvl{TC&ZjwJ?#8Wo zSf@d%6Xt==b`SdXcoS+y4|sjivvK}P~-d{5!SYlk|=oGawUYbFLaG@|EEcn-Yi$+-6t@B>s|;QKHuJTLMX z!;2gS>DieBEar;dPxOuyLT~<#qWsYMxjs^EVEY#Wp9m~^Bxv}BJWwA!-+g}Ij=_EQ z3FJBle$4r}cNu!f-#50X)fK(h0b2%s#29>nT(ExaUf}P3)MW@A(rQu(IMDZT?>DLe zt%=5iUOmvjEO$r)evZ4x_Pxo3kf^OSMw?45WM0L){tH9H_qPZ9JtuWlu-MD4xFExF zZDgxXu`y+3mOg2wqFHlxR&G^oRDF(e-%YpKCxeB1-|(u~aBaD5^&Phh9{cyq5SQfc z9@H(eIDg4l-R)``*6yR?-^>LJs(7R^H3pVc8NE-gh>8Fz-fZ zb^G3FCpxb#$v8SQ*E_k4jb3)E;fVgmy+w8Y0^?%8g2o5ae?FaXP4QK;b5~jGcQfq2 z{zy<%oUFSRT@4anH_hFnDOr`!C|OtQz2hbG&p-Zj zu1AXft94O=xbh;?;Cbx#r4~c1Yh7Wfm0I zT5hu6e6lvA=#>SfKZ)Y17TcEA1ay6MsKnJY_PAqoj@1l@i0q_4hs$w70M(-YIBSYna%f zv@W1*?b^ad7g1N%ms^fs4v!f4w&1Y*$Ck}{uI`97H*88X*LnA#>GG@2TDSVJ6|#R> zZM$>qb3?q}PvU*eJMw?%c<@$)L>f0cf2n_)G0UP}-}U>L6>U$Vd{f&h>YC5rn(VEs zS$FN*^n&6{=8LV1vRVKB&WvgD%1*(&{fREInUN1Q)}HwRt^V6ubiqwo+lwp8-_2W+ zVm94gexIo_X5as!{DCy+Qgg>g7tX|HC#1f2afM@9?<=i0rgR5Qwn=?mw)`_?TG6)M z$KEe?^}Xx2Emqwh?_ISw z51#*QNpbbfko=Oogu2JNw;e6Fw6FOl#9d8>! literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/splat_attr_1l/case.json b/wai/staged-conformance/cases/splat_attr_1l/case.json new file mode 100644 index 000000000..f18f8ff36 --- /dev/null +++ b/wai/staged-conformance/cases/splat_attr_1l/case.json @@ -0,0 +1,100 @@ +{ + "format": "wai-staged-case/1", + "name": "splat_attr_1l", + "about": "a pinned wai.splat.int_codec base, then an identity layer over its attribute tensor", + "bases": [ + { + "file": "base.wai", + "object": "bc283dfb0eb717c129db5a1cadad67daefc788e4f6880ecf4c7ac8a2d35204d8", + "decoded_via": "wai.splat.int_codec", + "via_fallback": false, + "attested": false, + "pin_resolved": true, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 14, + 32, + 32 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "f07679de87ff7c3d4fc4890b80919ef857fb85bd4d9e4cfa16105211d580427f", + "decode": { + "kind": "int", + "set": { + "g_s.whm": "wai-rs/tests/vectors/int_splat/splat_gs.bin" + } + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "68e006b588f94e686b17ef64f96ed0a2f7073d6a2276c68c589aa83f54a56094", + "bytes": 5836 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "68e006b588f94e686b17ef64f96ed0a2f7073d6a2276c68c589aa83f54a56094", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "68e006b588f94e686b17ef64f96ed0a2f7073d6a2276c68c589aa83f54a56094", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 14, + 32, + 32 + ], + "stage_sha256": "3a2afaf39022a3e4fc1244f10c221bab5275e86be8ec78094dd1165dee8c40b4" + } + ], + "clip_sha256": "26681e76f0371922e180e05b31d7f8fb99644bfcebd8ce40433d3287bedacac3", + "n_units": 1 + } + ] + } +} diff --git a/wai/staged-conformance/cases/splat_attr_1l/stage0.bin b/wai/staged-conformance/cases/splat_attr_1l/stage0.bin new file mode 100644 index 0000000000000000000000000000000000000000..e898b6688efc4696d90ee49b70ea6a2b7a22f418 GIT binary patch literal 14336 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 X!b3?xMn#GTW(oqT%wThYf&M=Lg;31~ literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/stream_exhausted/L1.wai b/wai/staged-conformance/cases/stream_exhausted/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..a7e9a5d13901ca48a64f85447015a206c1effda1 GIT binary patch literal 523 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZYWFZOFob&s88QNclL^S?fMRAK4FfDd z#_#|C|MNlx)&S|XK*0!Mpk7M`hSdoS4F4xGF#Oxizz}>NNPlHu2>A;X`w!B<2sYCP z#AIM-01<7q>HY6btQnT>o%Zy|k9=1Pi#ZILl1vAL)bDV0`tUYce0kRKLSVtIo4IHA n2VXvMY3-Nx7Y~Gw?-DXy(GV)7v14}HJlht_Ma+NO=QRTWoYbQ0 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/stream_exhausted/base.wai b/wai/staged-conformance/cases/stream_exhausted/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..461dccef462404568890d9f0b379f0d7b74738a1 GIT binary patch literal 555 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1M7#SGCJ;Mwcfix4)xg2Qt z!~g&Pm4RX)um*%#7#P;FF)&2312I1X1B(Cy!+H@Qt-!z#rO3drL4|=KS`&y385m-W z85m+s85lNO0cmFjhQHGp82&6~U|7Ba$X?CBuwoMfL-0-@eE^7$F)##O1j^qA;>SSt zOQ4$fK>Qh~<~svJ$WI1_|NnswU;sK42u=X;8b-%gdPS!d_i;K0g^C^!nX2~e<5zR{ zOFc?*tc5Dw0t<{%%9E`2R!VB@vgOn6IIg(RfLle4)sJEQ=Y#KMU*Hz^AXa zf13X02X;wKTMo$VHM=qC^{4t4nKjLCWzJqMT)E<9BG2!W@ktLYjvW3}p1WA%R3Nw97C-0<)09x5F~7n9 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/stream_exhausted/case.json b/wai/staged-conformance/cases/stream_exhausted/case.json new file mode 100644 index 000000000..d2fcf189a --- /dev/null +++ b/wai/staged-conformance/cases/stream_exhausted/case.json @@ -0,0 +1,116 @@ +{ + "format": "wai-staged-case/1", + "name": "stream_exhausted", + "about": "unit 0's stream codes no symbol: it runs out at the first, payload-malformed (stream_exhausted)", + "bases": [ + { + "file": "base.wai", + "object": "f26597cbf487451efbca1be1c4d12a5afbee09a48097bb62b7789cf9ccce0daf", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "1296c291d5d09baed6c85b4d667a9fbf5ceba83b6725c78375c24e2d30ae206d", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "6c774d497f78fdcb4b749f05cad21e8d0804577fece4bc2722209696f75298d7", + "bytes": 523 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "6c774d497f78fdcb4b749f05cad21e8d0804577fece4bc2722209696f75298d7", + "envelope": "payload-malformed", + "detail": "stream_exhausted", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "f26597cbf487451efbca1be1c4d12a5afbee09a48097bb62b7789cf9ccce0daf", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "16da7e243b54905531cea9ebd321f83e7185cde180f14067b0856ad9ee9bc5f9" + }, + { + "stage": 0, + "producer": "f26597cbf487451efbca1be1c4d12a5afbee09a48097bb62b7789cf9ccce0daf", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "49c8c17836a437bfe8a29608326b2747ff810a4701967d2e4414fdcbec7e2419" + } + ], + "clip_sha256": "eb8da278015a66532af62551560d52a3ca545a3def8e711f6322c2f6c50dcb9e", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/stream_exhausted/stage0.bin b/wai/staged-conformance/cases/stream_exhausted/stage0.bin new file mode 100644 index 000000000..b05fe6bce --- /dev/null +++ b/wai/staged-conformance/cases/stream_exhausted/stage0.bin @@ -0,0 +1 @@ +  8 88(8P8HX8XXPXhXhp 0808H0@X@H`PXhX`xXp€ppˆ0HX0PPˆ¸ØPXxXp€ppˆx€ˆ€ˆ HP`˜¸à Ðð°Ðÿxˆˆx� �� �¨° Àè°Øø°àÿÀàÿÐðÿ�˜°¨¨° ÀÈp€ˆ¸èÿÐèÿÐøÿ ¨À °È¸ÀÈÀÐØ€˜˜ˆ�°àÿÿ¨°¸¨ÀиÀàÈÐèÐàè�¨¸˜¨À¨ÀÈ°ÈØÀØàÈØðààðèðÿ( 8 88(8P8PX8XXXXhXhp 0808H0HX@H`PXhX`xXx€pxˆ0HX8PP@X`P`xXp€ppˆx€ˆ€ˆ PP`PXhXpxhp‡Ààÿx� �� �¨°X`phxxh€�ÀàÿÐèÿØøÿ ¨° ÀÈp€ˆpˆ—ÐèÿÈøÿèÿÿèÿÿÿÿÿÀÐØ€�˜ˆ�°˜¯¯ðÿÿðÿÿÿÿÿÈÐèÐàè� °˜¨¸¨ÀÈ°ÈØÿÿÿÈÐðààðèðÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/stream_noncanonical/L1.wai b/wai/staged-conformance/cases/stream_noncanonical/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..52f009e0056e7730a6dae5c18be6e8b45969c377 GIT binary patch literal 583 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_ImyV((jqA} z#UjX?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZYJV^=Fob&s88QNclL^S?fMRAK4FfDd z#-IQH|8qkH)&K><`5742=rAy>j$>e0HG_d6WG4f|$|pb!H1Pj_kU~a=03hiB#9t*o zFA@dPw@&?XC|b|4`%zl!Q}gn`o|rA0ZaX;1_eP#uD1RleGuZqx$L2ZF%|X`$&d#1*7E8 zyKXO#QT%Z(=g*$~hxwa#|1+-o_xzC1#g^+UEq`P!*}2H%oTYU_+QP(b$M-rW_`U!D D?jFCC literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/stream_noncanonical/base.wai b/wai/staged-conformance/cases/stream_noncanonical/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..18cfe3863189c546b45ce482d48ecd3ba0c7476b GIT binary patch literal 555 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 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/base_0.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/base_0.wai new file mode 100644 index 0000000000000000000000000000000000000000..26e919713dbd35f7ba3c180edb8f9442671d5bde GIT binary patch literal 1443 zcmY+AdpOg39LK-g+(mMqF|6BiR}|{#xD1sdsf5}yw%EajCYO$mj!ViVJ+eH>BIG=w zh$y)wTjmlrITlGchz_G|#+dV~KhAT`=XpNg@8|vfyq@>_dA?_zj#xhc09?Vu637?_ zjJ1Uo#taidB9aM^x=bdLsE{U8Xe0^^dWa@PhLeK{G}3p7N+gBjqe)RQoHvV#17HLNfIJ$2eMSJFu>kbFf|?0H0vq~}#bMr^i_v7F4g)-G)zK>@s4-n3_WPx@JJe1k786vaeJS3_uU*Hc-({w%)h@APzuUmux=7LuxT_kbc|2V)`irVsF29+5 zsQC%G;4OoJ8?kNRu2xVFPbE7$E&NrtD3BThK7)NpTpgFlBI=p6NBVIf z9B6x~z%V%{y6e1VZ=HJ_<@Id!q=!R*IqcZ|vcvG&r3M)mW!5^tIH4irGD=((6te$jzD zU%TpEbLzA-)azWn(nQg^8%hNj$ zjQ%x-f;pf2Td9ORux6*CcrDhKV{V?&ADl-|W8F|6I^0>+!*e|82VMdvmqqS&SbnBp% z+#u_r=JVu-tz(z)E**X8h*3ONxk3HsNj+VO>rok>$~}dPql%)?{4WVu#h(hE#-RjE z`{SHIwIHR?NTs6*-TQlLRB8wD{h=q)EJCx?RX+I*URXL=o8feat%a@hEhynm84q$q c+GATkGab78TQbp2*6KEO%(eVyEd87R0BW|8f&c&j literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/base_1.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/base_1.wai new file mode 100644 index 0000000000000000000000000000000000000000..0fc522d1937cd47f431dd8e897a4ff6e72b7a63b GIT binary patch literal 1239 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 CX2THx literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L1_f1.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L1_f1.wai new file mode 100644 index 0000000000000000000000000000000000000000..6d73e1e77546b5c74d990d4eb71c8097ed2d89d1 GIT binary patch literal 587 zcmWG{^fZiTWMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_b*)Y2f?$O!1# zf}+$i>X?+1SeaU+WM!zMR9ujl2XbXvQGPC1AUVGj6ikM-Kwp$37Nw^GIeDcyIkgWM z7#PAmgA5sgK@CI<0#M8hq+x&s$oLNgtS|u=28LDZL1G{QMus^+GC|5Uyor~Ap-lJ0 zT!HOB)~v0bzKgw9J)Lv!DvK}bIr>je<*@`F`O#R;8m9OBs9=Vy+=mHDcjia?zfpR= z=KUU%{b#Hk_1{XaxZe8x?iud8$)%w`Q`i4BbGY<2{OOtJ6%!{u%PffJ?p~JT?bOaO zM@z_bLRgt~*KD?;RIAnh((gup%C0{lo~e9s=W_{v?b^tH_LG@T|GjpiXi`Wt3jkY# B#Fzj8 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L1_f2.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L1_f2.wai new file mode 100644 index 0000000000000000000000000000000000000000..ac10aedda52531be5245b3b223089ba33fd93477 GIT binary patch literal 583 zcmWG{^fZiTWMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_b*)Y2f?$O!1# zf}+$i>X?+1SeaU+WM!zMR9ujl2XbXvQGTwHl@Z8erJ!IktOfd_B(W$x70Age&B>{~ z!@$50?ipmr2n=c@vyPd|=-r-sBNqI{I*rCwZFFdlb*0FQDQyU z$z|1L8ymfLbx%I~_G;jdCCfK!tDU&9h-XgG^S>Gkx+aDt=`D!6k>>GXicd8F8rQ;k literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L1_f3.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L1_f3.wai new file mode 100644 index 0000000000000000000000000000000000000000..f9ba211b7cf27f8c62382e71ad8c810d3307f847 GIT binary patch literal 583 zcmWG{^fZiTWMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_b*)Y2f?$O!1# zf}+$i>X?+1SeaU+WM!zMR9ujl2XbXvQGTwHl`+U;rJ!IktOfd_B(W$x70Age&B>{~ z!@$50?ipmr2n=c-2C!U{fv|0cKZ-?CLbVa^o(Gh}D}(V{CqY_v~^d2iH@SGasTUq$<|#w5n) zTd&2w+?Mh4)1}U>h9R literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L2.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L2.wai new file mode 100644 index 0000000000000000000000000000000000000000..e012f80a0d9ec24febf72fd52d5d5c34c16ed72f GIT binary patch literal 1518 zcmZ{hdsK{R9LL{Ex<^NLQEszttx@m&Qdu^aHX;+9&{FT^dF$0QlWA(ug(G4K=gbOq zY}xGY5MfgeQ(2cbqBaTRC`#C}d$ibYYVGVh*4eZB$3Evgx8L{se4gL={h~h&L)O_t zP`bjvN{T=Q;twm7iXI9Y}ho zrz?0indKz4q~9pJWu*T*gue`souZ@xicF<~*J=bwFKIQBCSDPku1J(Lg3T`J5S#qB zF~z2EYzmMS7DsU!VL6gwX&MMHjH4`IS%IW6Mx-c|Cj}O!0gM8XM`4x%2r9rdfshQr zz&wh|*(U2inj$bJT|Q`qR37MLEX5HxMzb^qNRcFXoDxKyB1sAsMG}KWmctO9q)47b z5uV{h3h+EHP#g+ijur_Vm$AGy)m9iJQ~4hDYM+>5W^BhI9BE~U*K}1geN))jFC$lKQ0-O^O0`M5cQwSn(0Ehq<5RyV+9tI*t zVSqp}PQVDc1~7@?9K(wuPIHt%{y!OE1q2rnIY(Z8!14gYaad#kOfoQmp$yG39Lew` z4bvRJI3DF`3MCkUAXpSeF!tZ%HA;n=-DoQ^DmPTm>f(W1B~7YYow>mQf}+DB5j#5w z8U{ho$oEqY|1)F>`qOH)I=zo1j)tJT2nbqXhM?y!A;@aA33gDuyk37lYvg@)ReGOg zUcX~WRbgLq(ALu-b|qOwW&UPAqR-q_S4l4ox2vjqd+2IQOh8*uGaOd|**C1TpXG|o zk(SxL$Z`B8NbG$}eY^ckd+mik)}LSPmAHyYy}8s0X}B5jq59#e0VKY_t7iOnzq?oT zRUPW7U#H(Xz4_D&@9T@IpU?GBS53}Tr6x~#{ZzSZ&gVrTZ&qNI=_S?0=9Tx%1xGW+ zM-54lGhf6Qdx`l@A>l3+myO5wXVcO3OJ2_MNLQO27OBJQq#biRGCPmK_E)A|XA9k; z%e0;93;Sw@mwwb3+34cbwm4&7_P#vB(X1Hlk%vmxgr>(4tx&Ge4Px6LU~88e>6PTdsrr0SZNw|RA@-nlNP zv6^lz8hv7MbZMn&SM;irW6ZVv{XZL~jyh6q>G9tAafh=pG^e(3_c+6HccuF^@JM^s zT9}fx!|zqXgf+7*ZI3HdX{Ea!8qf84o^Ide7%*_-)-=DAZmX&IuEB9^>z!nGxi`0m zPo5vpG4V;OYQIC#-Rc>G!F|VvHyH2db+zoyD|gH0?vEHW-P*`p*}NuSZB>2Q8IenG z$&HJc(7UAg(SaDtHnP7p=gtS^PaS((E_+l|hF;Az%QDxZs*u}qO6 zZEaLnVg05w1b>k4b>owskhErfL3zH~=U_#5V4aWZlC2*nI=KU}m>ZP!%72(=d+o)< zguqQrKQ&wIm((9{nNr&x9x>7B-We(`YC_w>v!0bB_9WNruUXqYqxZ5AglqP;h7Owj z#YJ0aAU4DoH?8(9o$2?(;kq%ryS`ibsth#oqkPGXD6=oVV6>&LIH571ZSeI->6mNQ znXXi+zccFOysnBf~=C&6EIURgLPC)Qy2G1uhbdMGl; eh|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J 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 KZk-J|?f(K%+;1oV literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L1_f0.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L1_f0.wai new file mode 100644 index 0000000000000000000000000000000000000000..0fa5fa01a55b89a196eceea198682da736b6e294 GIT binary patch literal 591 zcmWG{^fZiTWMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WM!zMR9ujl2XbXvQGTwHl>x|OrJ!IktOfd_B(W$x70Age&B>{K z!oa`~?ipmr2n=cc2Bgw?`hM$H>|I^-H6OkecEAT|MTO|xAigW F^8xLM$9w<) literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L1_f1.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L1_f1.wai new file mode 100644 index 0000000000000000000000000000000000000000..275cc90a46857c04346a5bc877066c65a0ad992a GIT binary patch literal 587 zcmWG{^fZiTWMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WM!zMR9ujl2XbXvQGPC1AUVGj6ikM-Kwp$37Nw^GIeDcyIkgWM z7#PAmgA5sgK@CI<0#M8hq+x&s$oLNgtT2H928LA&Kw=;PMus^+@`h)XsF?(}_~HH5 zo{n{{c9#Md2Zk+h)c>Qo=xCgGuhG;B)~%-Ox;7P>&$cD|MyM`kdzt2_x7crzuE8>n z&35Z!VkU6C*!lI=&ayZUlZi2nk!L?X`y{ooR(tZsJIcSf#a)~yT)yBlbDP_-HpA)J z>lZP8x8`V&ZP|RA|CUq5;*S=_CeB~=l`RtMkFQ+z=~l{G#h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WM!zMR9ujl2XbXvQGTwHl@Z8erJ!IktOfd_B(W$x70Age&B>{K z!oa`~?ipmr2n=cc2Bgw?`hM$H>|I^-H6OkecEAT|MTO|xAigW F^8xRW$9@0+ literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L1_f3.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L1_f3.wai new file mode 100644 index 0000000000000000000000000000000000000000..951a7f1505259db6eb2767b4e529403a5b622f55 GIT binary patch literal 587 zcmWG{^fZiTWMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WM!zMR9ujl2XbXvQGTwHl`+U;rJ!IktOfd_B(W$x70Age&B>{K zz`(!|?ipmr2n=cDN4>>&4Emw|17rd6-O$X^cGk@!2P-jkVg7H{MbH#VzjQJmK;MpPAd-jUFI zvMAGhE1kAw>t)ajA`GTXk-5kmGnqwHFe)5dY#HJftQ;D%c{`U}vmg8e+B)0!?40lC zv(LVt&$l$c0QwUEXjJNntK=v!TUESDjo>bkzs)WeA^CTMQ4x&FMnr@IE+bJ9Cxy5O zY>0?Wb#6q|EZ~g_BVj__fE#U)Y$=(&AdxKvB-3$<U0UEEME!)C~pQpijwZRI1$| zkJH@!=K=|#F=Qda9cb9PVHnWV z4Aau7MHxbxEe~dnP90?5TA5(s2*IGHnI?5CTbE8%4Pj==r_hnDLKEtgFe-gmTq9JP zav+rB>sZomtt{_6Kqh$M^BO!1-7K<&q(dk?U zKu0+M>pB5A{3`&lSRC+xQF-571HI>e@JXRVWuA=n+OqG~JXxGsvTk=;>$6?yJ)1I0 z_x3LzFYj1+d7_|oWzzD>J-)U>d7(Prz^S64i&L-FM825c`bPfY9YcJkYg_KO@H{YDed5c=X}$n{li;U zy|Ygl-9E9mJ7t50z{%Dl{>EU;$y*yxR($*En^o5y|9HP&Y45F0+L2ZMeNX+t+fv7} zviio8&#(1-G_|Gs#ScB@7OL#*yMF2zzEpMj<;l~LyVGP)lONlr+Ir{5|$ zNeidy3;olNJUlYJ>+FF~em_u@+m@CP%ba`v&+OFj{2y;m2g_1kpVj+k{TnOt2Jg9j zX7JwkekyAIviJG1eZ%$7r4*gHZ_nz}&BMQ(*jWES@Uuj(UYl^%yt-;Iu(jdJq69oP z+FzC4x4z-bTgBn#7j|B$9C_+Es5h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_3+0x9=z#`eg zGBMRM$uQN(*xbm{+#oSAEy>c@FxAr3$j~_1GReTm)Y!-@Eh))3*&@}@D8<0U+ydy@ zf}+$i>X?+1SeaU+WM!zMR9ujl2XbXvQGTwHl>x|OrJ!IktOfd_B(W$x70Age&B>{K z!oa`~?ipmr2n=c zrX4h`HBFA%sLnitu zsZZtj{i1zEw9HYhql+89Z0T2cwsg-)H9e~%&dGif29N93<^NhRdx@M_gMF!B_57T# zYg+xp9<$1Qi^!|LxxqZ9wQ*5dpu_XnOQ&<3Oy<9k+LCg~`s>PR6Hi$jce;~94 E05QkJlmGw# literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/video_mixed_stage_l2/L1_f1.wai b/wai/staged-conformance/cases/video_mixed_stage_l2/L1_f1.wai new file mode 100644 index 0000000000000000000000000000000000000000..3c5080479c7ef4637878509ade995665837b866e GIT binary patch literal 587 zcmWG{^fZiTWMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_3+0x9=z#`eg zGBMRM$uQN(*xbm{+#oSAEy>c@FxAr3$j~_1GReTm)Y!-@Eh))3*&@}@D8<0U+ydy@ zf}+$i>X?+1SeaU+WM!zMR9ujl2XbXvQGPC1AUVGj6ikM-Kwp$37Nw^GIeDcyIkgWM z7#PAmgA5sgK@CI<0#M8hq+x&s$oLNgtS|vr28Pg;ATf{tBf}gZnaPxDaQ{0uL+fHC z@fkA3$}R7Uo)>kma{DW_Y-Y{h7AK3z2ZUmGlm#8r=`ER%Q=hwNllSyh|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_3+0x9=z#`eg zGBMRM$uQN(*xbm{+#oSAEy>c@FxAr3$j~_1GReTm)Y!-@Eh))3*&@}@D8<0U+ydy@ zf}+$i>X?+1SeaU+WM!zMR9ujl2XbXvQGTwHl@Z8erJ!IktOfd_B(W$x70Age&B>{K zz`(!|?ipmr2n=c0Oj zg5K_t+Uc-w$!9^Q?a#Xm_RULCpL=TZQ~S(($?m1f^;HEv2iEvMdwe+K^~v;Q617V! z64o`G-X$TwWrO1Ze)gabEmGoh?y=2TusnZ_IGeT9Qi|mH008dU B#9jaZ literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/video_mixed_stage_l2/L1_f3.wai b/wai/staged-conformance/cases/video_mixed_stage_l2/L1_f3.wai new file mode 100644 index 0000000000000000000000000000000000000000..e6b7cbd05961926700df0af4d7e3a104e1e846f4 GIT binary patch literal 583 zcmWG{^fZiTWMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_3+0x9=z#`eg zGBMRM$uQN(*xbm{+#oSAEy>c@FxAr3$j~_1GReTm)Y!-@Eh))3*&@}@D8<0U+ydy@ zf}+$i>X?+1SeaU+WM!zMR9ujl2XbXvQGTwHl`+U;rJ!IktOfd_B(W$x70Age&B>{~ z!@$50?ipmr2n=c2{1Cu0Ftx#3m#sZo6cZt z_rQ6JVOo65$+P=|}zY4@a literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/video_mixed_stage_l2/L2.wai b/wai/staged-conformance/cases/video_mixed_stage_l2/L2.wai new file mode 100644 index 0000000000000000000000000000000000000000..bf443df7ab51dc8bdd4163c11491551276bb4dfa GIT binary patch literal 1546 zcmaJ@1bFe zW@NP@rdq?$5-E>R7qgHeD|*PIrck6j%9u3g8f(_fA2VmI-`RWrzVCedJKx?*<^&K4 z!!Rsen#wDsUQ)sXm%2+65mfTxDn$vAMqDe^I;7T#4J}GiDFt4KOe76Nsu(R=rB`ZE z;zvAPD)33XT&Ys((nPi>>G4s*Llh913Z)uJ4enAwqlQYIQlnO? z!UX?COg15MJ{gG!DNu+50+10nCvbpAKu#c%VoAW_Jg<-gia>xN2}%HRoMb3crjW}i zfkOldahl~sw@F%*BK3+&7bhC17m!wzWf=kqfMjTbqBsg590Dkp;W$C#6bD!a@f@Og zj*+uCU{9}NJKRfu_z5Sf0GpyiY}SMY?%szSg8 zjzbEF01I#)vH~SW#3DJs>HqNzAxqH;K>-O4Q8)z|f~84O0s;l4AfOBhG9afp9_I;0 zZ16b40004Cc~-`YJN(o03U{fBPcwx~iU+FWwQ)#HNv&6@413HlY)L>EA(3F1Ifh|& zpGs{1{fH3uVQ6S*#HY@eE*N$(6vKwfFzim3I1ZTr33f+(*NJuGQ^7NZ_2y85c{W%i z=3CS*POqBmFn?Ne&CxE$p4M!7L9&}uY=;ECJ=x%yI^|W;?X0nbL24u6lhvY|!H?5u za|gn@?`P`!lCK~4KM-mCyvnQNiH5Qo=3!j8!oA&ldH)lwH6I@AJ$qhBRz}B7zh$p; zh0hlkI98R6`^GFZ*0_CYVYDGO*78EL%Qj_1dDn(5)oWKUk6eGx=q{SenBQxq)lCxH z@L<1P^^+qi9+oq{V5OZyIdr};%IK2jO`mqO-WhM`$ysC-?Y1LyNnX{$L(fOoPx|4D zO-+*VPJ4u$Q&XFJ4^$oDGW*wY`FVy#I*FG>V(}nmX_@)*t7KzYQ$e58fZd|_>l=>* zSk2GzxmRoP$N55aYs%Au&UK$1$l8y$f^tm1kzPx`@N_MU6G}_konFWO(zf*8s_Nv< z!orC*j#my<+&JX-J)5nI8Y4S0?^Z(bgnhG5v>k37D1n7ni;d9{+3Q1`a(3kxT>S0& zPp+Opwz6;20;85MD=vuctgma@GOgrjQ*zbjNgJ-H5{8AnS~t~wjqPAXJnp}Y^=@iy zu5ApzUo648Z|q<7VOZ_h$Y0m!_Ez6bJs5DWVS~0T`10ggq5ZZ!?-J^wCQf)0xADE@ zn$IWC@ z&Rb24w(cpoV2pg~U>FFG+)dv!^MACbf|6z!_SBTjo~b*BWj>>8yQ2!j+H_HUZzjzM ztU2d?)hRyMKP%++6y?qU=e*4$k8Q0V9=BF-dpJ8dHTvyb-=Woe2TwkTKVxAm${5M?*j(N(V$#;qu8c~n@#TSa#p>+un4K*Wm)^FoTer-cPL^h8pLNp57?0Pq F{0UEuVc-A& literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/video_mixed_stage_l2/base.wai b/wai/staged-conformance/cases/video_mixed_stage_l2/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..af3eb82cb53d811833181df2d8d85b0463da280a GIT binary patch literal 1439 zcmY*VdpOg382>W23KM2)rm@KVvXD#82|4DLbesqsTC=ocHk`T6I7COOh~#v^kw3$JsN^)Xkwps2}=2pq2Q#6BkRNMMLucix|Yj7&5ez zmPDnpq)GsU`i9^Y6aauyp!E$lLixO0E;oSQ^{|5QToHgzFaYXR0m$0~fVU<9cMt%y zApzKdhV=RXR6+v}umCuk0r0dD`hr#fR@(rW-~yoC1HhUHfTke`;{bd=4Ph35sd)ed z3joNMAjsFZK?i*k0nl+Fu)MC+*9XB1}q^n~f7>5(jJiNy4V=2Q5L_kQ%zaujMl{xeF&*v3ezcpEDt zTs5fXv={|9baH!I=xAgFpZ{{dWO%swB#&L*fw9FLw6AE~>C~x_u-VN&a8^}pH1~j3 zKUR@3Hi5@f@Ry>Bn-3p(^>UKj)aGys6913p`E1F~Q0g`oP6$apMMBq$qfy zN^r=muS3JLsppRA{ly?v?CKt|?-y*HITr?3?ArM#oA+**JvoxMgO^!wVPL|9CqKhB zlpD>($qJW9JgT^*^E?vQZ+2{C!XWv{yrX)=l1cVq@zgL=MhK_&2_3S=(aW9VV`9CR zF}<>75mryO>a8g&tlzsvaChO$h?AoR9NA=piI77}^(=@ci&o@YV0?0f z`QS~d;(Hc#1Lt5^&p1uXUPg#Ix7^=-c;#F^OT5SO$n=WI+DAL?f@CqM|-5 zv;P)xN`-^bV>3%k%1qlhW5LF%X`bn1+}-Y8+})b7y|){c{O0o~?FzH8A?qO*8vnKPHEMJPAhlJ>BmDvy{z zBX39Sb!~KcqWxpKWyGzYrOK`%sXYkq8&!#=`kCY|8P&%JE^JP42vblV5zQmmuvT{Y zsKTscM*XebevVt!a&*h;GX0XhS3b4%v(%Iwa23zwBKH_+ewRMdJ{$*NNiA=3v*PQE?!H;SUgj7_6QzjW!-aj#d|1h z*7IuRI$_=!a1kjwj&ESs`gB^2)Ncxk5m~VLFuRnbqLM*d>p11!Nuk4eYS~a?b@m32 z0m4IGqc*?8$HgnGAS5_QBelEI6x(W9blGC2wp3sR*L>Yqhicc@_deDm$(E_(P8s_f#t zq7-A8bK)vOrl|IkR2GqY^rk6t6wbOkkFWdXz$m(fqZDwx*|?#L{}$~JPdz<5s8Tc3 zf7X0XH(>C?^db1tJ>hVoR%40Rk{-Ljr~4zA4ca}8(MDdgutnAPdDRZ$kd!9JHXn5q U_t%RTJcSh|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_++{nx{&CJrmz$`5_%>?M$ zf}+$i>X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZY8imR815Nl$OxpF7#J8hpqLp*!vG7A z@%#V(|GZFvH9(q)je%jU2vDy+1H+mS28LBV3=AvxFfjak48&i7?0*al|NnzDFf#Z6 zNf3^nUtAvagOB-)gydyDU9WeOF6_{pocCjb|B6PI&P!zxf7uQnzw~tTvAAo~6idHF z1pEF!8?lujUA6snqXk KH{NxoTmS&Qoxt+| literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/wrong_base/base.wai b/wai/staged-conformance/cases/wrong_base/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..c6865103505633afc5039a49b511891c494e2873 GIT binary patch literal 551 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp2H7#SGCJ;Mwcfix4)xg2Qt zfguPOCVv+*FsxVu#9J5`f_DS)Q6N4G8CBeTB7_!ggic13HxzI({djrYzRSS$be`XAN%3wC{0-my7h z(fvJ-m0ssO+Ss@8*1D>nx1Wd2ab4rlg-wD7Ug{wYt72Dux1T-hb9lu~!QJnfe?0BfYfVE_OC literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/wrong_base/case.json b/wai/staged-conformance/cases/wrong_base/case.json new file mode 100644 index 000000000..f55209da1 --- /dev/null +++ b/wai/staged-conformance/cases/wrong_base/case.json @@ -0,0 +1,131 @@ +{ + "format": "wai-staged-case/1", + "name": "wrong_base", + "about": "a layer of another stage set, whose base this sink never decodes: held for its base, then base-absent at the bound", + "bases": [ + { + "file": "base.wai", + "object": "490b4d6126bcf59ea28b8b9562d0a5f64aee20802071ec86870c9e20331011d2", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "6620a5f72b982626e0b60f299c86edbc659001db5522bc6ba5a57caf3c03933c", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1_other.wai", + "object": "6cd79d9e2b416d87bed384d14aa5b30934487118f4c9a200c3bf165193bc3629", + "bytes": 591 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1_other.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "expire", + "at_ms": 1002 + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "6cd79d9e2b416d87bed384d14aa5b30934487118f4c9a200c3bf165193bc3629", + "envelope": null, + "detail": null, + "held_for_base": true, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "6cd79d9e2b416d87bed384d14aa5b30934487118f4c9a200c3bf165193bc3629", + "envelope": "base-absent", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "490b4d6126bcf59ea28b8b9562d0a5f64aee20802071ec86870c9e20331011d2", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "3d1ef19796ebd5b762a6c36d63c19bea1fff96b0e1479e4d101501e17eed69e7" + }, + { + "stage": 0, + "producer": "490b4d6126bcf59ea28b8b9562d0a5f64aee20802071ec86870c9e20331011d2", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "3a26cc2ec09c0b32314c0ae704bbec97a6cc450a570e22244d3e2ec086f743e3" + } + ], + "clip_sha256": "67b9c039ec748b3c1b0e38822db0a185936ead9d711fc08b814918cecda6cf3a", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/wrong_base/stage0.bin b/wai/staged-conformance/cases/wrong_base/stage0.bin new file mode 100644 index 000000000..8a434ce4f --- /dev/null +++ b/wai/staged-conformance/cases/wrong_base/stage0.bin @@ -0,0 +1 @@ + 0 0@0@@8@HHP`PPpPpp`px08H0@H8PXPPpX`p`pˆpxˆp€˜8@XPP` ¸èXp€px€px˜x� ˆ  P`p¨Èð°Øø¸Øÿ€ˆ ˆ˜ ˜ ¨ ¨¸¨ÐÿÀØÿ¸ðÿÈøÿØÿÿ  ¸¨°À¸¸Ðx€�ÐøÿÐÿÿØÿÿ °À°¸ÈÀÈàÀàè� °˜¨°èÿÿ°ÀиÀØÈØØÐàèÐèø˜°¸¨¸Ð°ÀظÈàØØèØàÿèðøðÿÿ 0 8@0@@8@PHX`PPpPpp`p€08H0@H8PXPXpX`p`pˆpxˆp€˜8HXPP`XXpXp€p€€x€˜xˆ ˆ  P`p`hxhx€px—Èèÿˆ˜ ˜ ¨ ¨¸`p‡xx�x��ÈøÿØÿÿèÿÿ¨°À¸¸Ðx€�ˆ˜ŸÐÿÿØÿÿèÿÿøÿÿÿÿÿÀàè�˜°˜¨° ·ÇøÿÿÿÿÿÿÿÿÐàèÐèø˜°¸¨¸Ð°ÀиÈàÿÿÿØàÿèðøðÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/wrong_prev/L1.wai b/wai/staged-conformance/cases/wrong_prev/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..436a1b20d833e97c65f6f2cb8484eafeb2bcefd2 GIT binary patch literal 583 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_Nn%lYDllB~N^^2*e=smGgnI@VG6I8>3CQMvVrC!>11vzs zpa1{=^FRew0~Ipz0kIwfLs&2a!^$ZP3@dgqFa+IYVEFr)fg$)m1H*qH2C8820g@g- zoD->-wL*xKfv@vcaL@BA+f$J1Z?79mDQc{v03QW3K@9w;#Z;rD%yJ{xe zA;LXGL7`EH4rYfA;wl88A(9fNtd$5sT{;wXET~(-_8qhTpn=ak^FGfr4=+!9W3zI? zMKml8G9g7IMfOWRsfX)=;WQw09>-Z&Np2;Puz9a3Vl2s>rnt_{i#G4;7dG$NtPe{u z*2|*86v-iUW2C$}LdF1e8p7gI$|uF*mM)SaZV9VXiVREL!qS~~VJlAgZ)3u#L|KBP z!otCDls9mdHE0@FG{2#7zp90~5;l0qFalag;n9#0R-#5wiNtX80$;^6Z$JE%5j`hRNzEh6^ zW%rCT1+DW7-#0Q=N$c7B-szp{m#z=zC+6Kvv5^Vi?b7PYb6pi*-=FkWwLRG=tTCjv SY>Paa__eZn=SKafyMF*7Qq#@= literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/wrong_prev/base.wai b/wai/staged-conformance/cases/wrong_prev/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..3d4bba18dfd102ded72a5d0e9b5cc610999f17fa GIT binary patch literal 551 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp2H7#SGCJ;Mwcfix4)xg2Qt zufb@1C-Veklf%q~I-vo+3U|{(7 zlz}1SH3P${4?z5df#LsukPCp01cDPlEX2|z(`CfKFq`Y3nz*dqfnO?`)zt={mK~K^ z=YC4@R;i9*TK_G5=d6DkiubE7oQhcC*6H`K{|Cp6bp_G;f7~;OS?n6TWQE?h6UPkN z%HFhGG}v9?I{EgmtsV~@-iEH}*z)&CQvLEtCSQO2f1Mh0bE){shu1geZ{ofpa$||5 z`JLZt{U7STI!EM(#5$Tf-3w9v@+a7L`nH$?mPob-G0q$c1=C|q9aR|D@BPT7)$QNb fo{*4VDkO8@N8lq5E1vNE6|4&Lg + +Exit status 0 only if every case and the schedule verify. +""" +import hashlib +import json +import os +import struct +import sys +import zlib + +HERE = os.path.dirname(os.path.abspath(__file__)) +WAI = os.path.normpath(os.path.join(HERE, "..")) +sys.path.insert(0, os.path.join(WAI, "claim-conformance")) +sys.path.insert(0, os.path.join(WAI, "int-payload-conformance")) +from _blake3 import blake3 # noqa: E402 +import int_payload_verify as IPV # noqa: E402 + +DE, EC, NONE = "decode-equivalence", "entropy-consistency", "none" + +# ---------------------------------------------------------------- envelope and registry + + +def content_hash(env): + return blake3(b"moq-jwp:object\x01" + env).hex() + + +def parse_wai1(b): + if len(b) < 8 or b[:4] != b"WAI1": + raise ValueError("not a WAI1 envelope") + ml = struct.unpack_from(" len(b): + raise ValueError("truncated manifest") + m = json.loads(b[8:8 + ml]) + pl = struct.unpack_from(" len(b): + raise ValueError("truncated payload") + return m, b[12 + ml:12 + ml + pl] + + +# SPEC §5, Refinement capabilities and Base admission (the refinement rows). +UNIT_KIND = { + "wai.image.int_refine": "rgb8", + "wai.video.int_refine": "rgb8", + "wai.audio.int_refine": "pcm", + "wai.splat.int_refine": "attr", +} +ADMIT = { + "wai.image.int_refine": {"wai.neural.int_hyper", "wai.neural.int_mlicpp", "wai.neural.int_synth", "wai.image.png"}, + "wai.video.int_refine": {"wai.video.int_motion", "wai.video.int_hyper"}, + "wai.audio.int_refine": {"wai.audio.int_codec", "wai.audio.flac"}, + "wai.splat.int_refine": {"wai.splat.int_codec"}, +} +# The registry classes of the bases the corpus uses (SPEC §7). +CLASS = { + "wai.image.png": "StandardDefined", + "wai.audio.flac": "StandardDefined", + "wai.video.int_motion": "StandardDefined", + "wai.neural.int_hyper": "NeuralIntegerExact", + "wai.video.int_hyper": "NeuralIntegerExact", + "wai.audio.int_codec": "NeuralIntegerExact", + "wai.splat.int_codec": "NeuralIntegerExact", +} + + +def base_tier(cap, manifest, pin_resolved): + cls = CLASS.get(cap) + if cls == "StandardDefined": + return DE + if cls == "NeuralIntegerExact": + pin = (manifest.get("conditioning") or {}).get("prior") + return DE if pin_resolved and isinstance(pin, dict) and pin.get("tier") == DE else NONE + return NONE + + +# ---------------------------------------------------------------- refines (§3) + + +class Malformed(Exception): + pass + + +def is_int(v): + return isinstance(v, int) and not isinstance(v, bool) + + +def is_hash(v): + return isinstance(v, str) and len(v) == 64 and all(c in "0123456789abcdef" for c in v) + + +def u32_int(v, least): + return is_int(v) and least <= v <= 0xFFFFFFFF + + +def parse_refines(r): + if not isinstance(r, dict): + raise Malformed("not an object") + base = r.get("base") + if not is_hash(base): + raise Malformed("base") + layer = r.get("layer") + if not (is_int(layer) and 1 <= layer <= 255): + raise Malformed("layer") + span = None + if "span" in r: + s = r["span"] + if not isinstance(s, dict): + raise Malformed("span") + if not u32_int(s.get("from"), 0) or not u32_int(s.get("count"), 1): + raise Malformed("span members") + if s["from"] + s["count"] > 0xFFFFFFFF: + raise Malformed("span past u32") + span = (s["from"], s["count"]) + p = r.get("prev") + if is_hash(p): + runs = [(p, None)] + elif isinstance(p, list) and p: + runs = [] + for x in p: + if not isinstance(x, dict) or not is_hash(x.get("producer")) or not u32_int(x.get("count"), 1): + raise Malformed("a run") + if runs and runs[-1][0] == x["producer"]: + raise Malformed("adjacent runs with one producer") + runs.append((x["producer"], x["count"])) + if len(runs) == 1: + raise Malformed("an array of one run") + else: + raise Malformed("prev") + if layer == 1 and any(h != base for h, _ in runs): + raise Malformed("layer 1 names a producer other than base") + if span is not None and runs[0][1] is not None and sum(c for _, c in runs) != span[1]: + raise Malformed("runs do not sum to the span") + return {"base": base, "layer": layer, "span": span, "runs": runs} + + +def units_of(ref, n): + return ref["span"] if ref["span"] is not None else (0, n) + + +def check_units(ref, n): + frm, count = units_of(ref, n) + if frm + count > n or count == 0: + raise Malformed("the span lies outside the base's units") + if ref["runs"][0][1] is not None and sum(c for _, c in ref["runs"]) != count: + raise Malformed("runs do not sum to the units covered") + + +def producer_for(ref, i): + if ref["runs"][0][1] is None: + return ref["runs"][0][0] + at = 0 + for h, c in ref["runs"]: + at += c + if i < at: + return h + return None + + +# ---------------------------------------------------------------- WIR1 (Appendix A) + + +class Refused(Exception): + def __init__(self, code): + super().__init__(code) + self.code = code + + +def parse_wir1(p, kind): + if p[:4] != b"WIR1": + raise Refused("bad_magic") + if len(p) < 5: + raise Refused("stream_length") + if p[4] != 1: + raise Refused("version") + if len(p) < 44: + raise Refused("stream_length") + if p[42] or p[43]: + raise Refused("reserved_nonzero") + elem, pred, mode = p[5], p[6], p[7] + n_units, d0, d1, d2, num, den, q = struct.unpack_from("<7I", p, 8) + offset = struct.unpack_from(" 1 << 26 or n_units > 1 << 20 or unit * n_units > 1 << 30: + raise Refused("too_large") + at = 44 + tables = [] + for _ in range(n_units if mode == 1 else 1): + if at + 4 * cdf_len > len(p): + raise Refused("stream_length") + t = list(struct.unpack_from(f"<{cdf_len}I", p, at)) + at += 4 * cdf_len + try: + IPV.check_cdf(t) + except IPV.Refused: + raise Refused("cdf_malformed") + tables.append(t) + lens = [] + for _ in range(n_units): + if at + 4 > len(p): + raise Refused("stream_length") + n = struct.unpack_from(" len(p): + raise Refused("stream_length") + streams.append(p[at:at + n]) + at += n + if at != len(p): + raise Refused("trailing") + return dict(kind=kind, pred=pred, per_unit=mode == 1, n_units=n_units, d=(d0, d1, d2), num=num, den=den, + q=q, offset=offset, tables=tables, streams=streams) + + +def table(w, u): + return w["tables"][u] if w["per_unit"] else w["tables"][0] + + +def check_streams(w): + n = w["d"][0] * w["d"][1] * w["d"][2] + for u, s in enumerate(w["streams"]): + d = IPV.Rans(s) + for _ in range(n): + d.decode(table(w, u), w["offset"]) + if d.exhausted: + raise Refused("stream_exhausted") + if d.exhausted or d.i != len(d.w) or d.x != 1 << 31: + raise Refused("stream_noncanonical") + + +def cost(w): + unit = w["d"][0] * w["d"][1] * w["d"][2] + eb = 2 if w["kind"] == "pcm" else 1 + return (unit * eb * w["n_units"], unit * w["n_units"], sum(4 * len(t) for t in w["tables"]) + unit * eb) + + +def rdiv(n, d): + q = (2 * abs(n) + d) // (2 * d) + return -q if n < 0 else q + + +def accepts_below(w, shape): + d0, d1, d2 = w["d"] + if w["pred"] == 0: + return shape == (d0, d1, d2) + if w["pred"] == 1: + return shape == (-(-d0 // 2), -(-d1 // 2), d2) + n = shape[0] + return shape[1] == 1 and shape[2] == d2 and n >= 1 and n * w["num"] // w["den"] == d0 + + +def predictor(w, prev, shape): + d0, d1, d2 = w["d"] + if w["pred"] == 0: + return lambda i: prev[i] + if w["pred"] == 1: + h2, w2, c = shape + + def up2(i): + y, x, ch = i // (d1 * d2), (i // d2) % d1, i % d2 + + def p(yy, xx): + return prev[(min(yy, h2 - 1) * w2 + min(xx, w2 - 1)) * c + ch] + iy, fy, ix, fx = y >> 1, y & 1, x >> 1, x & 1 + if not fy and not fx: + return p(iy, ix) + if not fy: + return (p(iy, ix) + p(iy, ix + 1) + 1) >> 1 + if not fx: + return (p(iy, ix) + p(iy + 1, ix) + 1) >> 1 + return (p(iy, ix) + p(iy, ix + 1) + p(iy + 1, ix) + p(iy + 1, ix + 1) + 2) >> 2 + return up2 + num, den, n_src = w["num"], w["den"], shape[0] + + def rate(i): + t, ch = i // d2, i % d2 + s = t * den + lo, rho = s // num, s % num + hi = min(lo + 1, n_src - 1) + a, b = prev[lo * d2 + ch], prev[hi * d2 + ch] + return rdiv(a * num + (b - a) * rho, num) + return rate + + +def apply_unit(w, u, prev, shape): + """The unit's next stage (the shape was checked).""" + n = w["d"][0] * w["d"][1] * w["d"][2] + lo, hi = (-32768, 32767) if w["kind"] == "pcm" else (0, 255) + pred = predictor(w, prev, shape) + d = IPV.Rans(w["streams"][u]) + out = [] + for i in range(n): + r = d.decode(table(w, u), w["offset"]) + out.append(min(max(pred(i) + r * w["q"], lo), hi)) + return out + + +# ---------------------------------------------------------------- the sink (§4) + +REASONS = ["refines-malformed", "capability-not-refinement", "base-absent", "base-via-fallback", "base-not-admitted", + "refinement-unattested", "payload-malformed", "cost-over-limit", "layer-conflict", "prev-mismatch", + "layer-gap", "geometry-mismatch", "deadline-missed"] + + +class Report: + def __init__(self, h): + self.h, self.envelope, self.detail, self.held_for_base = h, None, None, False + self.applied, self.held, self.refused = [], [], [] + + def json(self): + applied = sorted(self.applied) + refused = sorted(self.refused, key=lambda x: (x[0], REASONS.index(x[1]))) + decided = set(applied) | {u for u, _ in refused} + held = sorted({u for u in self.held if u not in decided}) + return {"object": self.h, "envelope": self.envelope, "detail": self.detail, "held_for_base": self.held_for_base, + "applied": applied, "held": held, "refused": [[u, r] for u, r in refused]} + + +def refuse(h, why, detail=None): + r = Report(h) + r.envelope, r.detail = why, detail + return r + + +class Reports: + def __init__(self, first): + self.first, self.by = first, {first: Report(first)} + + def get(self, h): + if h not in self.by: + self.by[h] = Report(h) + return self.by[h] + + def finish(self, layers): + out = [self.by.pop(self.first)] + rest = sorted(self.by.values(), key=lambda r: layers[r.h]["seq"] if r.h in layers else float("inf")) + return out + rest + + +class Chain: + def __init__(self, base, units, cfg): + self.base, self.cfg = base, cfg + self.units = [dict(stage=0, producer=base["hash"], frozen=False, tier=base["tier"], shape=s, elem=e, data=d) + for (s, e, d) in units] + self.layers, self.held, self.seen, self.seq = {}, [], {}, 0 + + def n(self): + return len(self.units) + + def held_envelopes(self): + return len({h for h, _ in self.held}) + + def admit(self, h, cap, ref, payload, arrival, evidence): + if ref["base"] != self.base["hash"]: + return ("base-absent", None) + try: + check_units(ref, self.n()) + except Malformed: + return ("refines-malformed", None) + if self.base["via_fallback"]: + return ("base-via-fallback", None) + if self.base["capability"] not in ADMIT[cap]: + return ("base-not-admitted", None) + if self.base["attested"] and evidence != "delivered-verified": + return ("refinement-unattested", None) + try: + w = parse_wir1(payload, UNIT_KIND[cap]) + except Refused as e: + return ("payload-malformed", e.code) + if w["n_units"] != units_of(ref, self.n())[1]: + return ("payload-malformed", "geometry_mismatch") + lim = self.cfg["cost_limit"] + if lim is not None: + c = cost(w) + for got, key, what in zip(c, ("output_bytes", "symbols", "working_bytes"), ("output bytes", "symbols", "working bytes")): + if got > lim[key]: + return ("cost-over-limit", what) + try: + check_streams(w) + except Refused as e: + return ("payload-malformed", e.code) + self.seq += 1 + self.layers[h] = dict(w=w, ref=ref, arrival=arrival, seq=self.seq) + return None + + def key(self, h, u): + ref = self.layers[h]["ref"] + frm = units_of(ref, self.n())[0] + return (ref["layer"], u, producer_for(ref, u - frm) or "") + + def conflicted_before(self, h, u): + k = self.key(h, u) + return self.units[u]["frozen"] or (k in self.seen and self.seen[k] != h) + + def readiness(self, h, u): + ref = self.layers[h]["ref"] + frm = units_of(ref, self.n())[0] + p = producer_for(ref, u - frm) + if p is None: + return "not" + s = self.units[u] + want = ref["layer"] - 1 + if s["stage"] == want and s["producer"] == p: + return "ready" + return "held" if s["stage"] < want else "not" + + def freeze(self, u, reps): + self.units[u]["frozen"] = True + gone = [x for x in self.held if x[1] == u] + self.held = [x for x in self.held if x[1] != u] + for h, x in gone: + reps.get(h).refused.append((x, "layer-conflict")) + + def evaluate(self, h, outside, reps): + ref = self.layers[h]["ref"] + frm, count = units_of(ref, self.n()) + ready, newly = [], [] + for u in range(frm, frm + count): + if self.conflicted_before(h, u): + self.freeze(u, reps) + reps.get(h).refused.append((u, "layer-conflict")) + continue + r = self.readiness(h, u) + if r == "ready": + ready.append((h, u)) + elif r == "held": + newly.append(u) + else: + reps.get(h).refused.append((u, "prev-mismatch")) + if newly: + already = any(x == h for x, _ in self.held) + if not already and self.held_envelopes() + outside >= self.cfg["bound"]["max_envelopes"]: + for u in newly: + reps.get(h).refused.append((u, "layer-gap")) + else: + for u in newly: + if (h, u) not in self.held: + self.held.append((h, u)) + reps.get(h).held.append(u) + self.run_batches(ready, reps) + + def run_batches(self, batch, reps): + while batch: + adv = self.run_batch(batch, reps) + batch = self.release(adv, reps) + + def run_batch(self, batch, reps): + keys = [self.key(h, u) for h, u in batch] + conflicted = [] + for i, (h, u) in enumerate(batch): + c = self.units[u]["frozen"] or (keys[i] in self.seen and self.seen[keys[i]] != h) + c = c or any(j != i and keys[j] == keys[i] and batch[j][0] != h for j in range(len(batch))) + conflicted.append(c) + for i, (h, u) in enumerate(batch): + if conflicted[i]: + self.freeze(u, reps) + reps.get(h).refused.append((u, "layer-conflict")) + else: + self.seen.setdefault(keys[i], h) + adv = [] + for i, (h, u) in enumerate(batch): + if conflicted[i]: + continue + why = self.apply(h, u) + if why is None: + reps.get(h).applied.append(u) + adv.append(u) + else: + reps.get(h).refused.append((u, why)) + return adv + + def apply(self, h, u): + L = self.layers[h] + w, ref = L["w"], L["ref"] + frm = units_of(ref, self.n())[0] + s = self.units[u] + elem_ok = (s["elem"] == "i16") == (w["kind"] == "pcm") + if not elem_ok or len(s["data"]) != s["shape"][0] * s["shape"][1] * s["shape"][2] or not accepts_below(w, s["shape"]): + return "geometry-mismatch" + live = self.cfg["live"] + if live is not None: + leads = live["lead_ms"] + lead = leads[ref["layer"] - 1] if ref["layer"] - 1 < len(leads) else 0 + due = live["t0_ms"] + u * live["unit_ms"] + live["target_latency_ms"] + if L["arrival"] + lead > due: + return "deadline-missed" + s["data"] = apply_unit(w, u - frm, s["data"], s["shape"]) + s["shape"] = w["d"] + s["stage"] = ref["layer"] + s["producer"] = h + s["tier"] = DE if s["tier"] == DE else NONE + return None + + def release(self, adv, reps): + batch, keep = [], [] + held, self.held = self.held, [] + for h, u in held: + if u not in adv: + keep.append((h, u)) + continue + if self.conflicted_before(h, u): + self.held = keep + self.freeze(u, reps) + keep, self.held = self.held, [] + reps.get(h).refused.append((u, "layer-conflict")) + continue + r = self.readiness(h, u) + if r == "ready": + batch.append((h, u)) + elif r == "held": + keep.append((h, u)) + else: + reps.get(h).refused.append((u, "prev-mismatch")) + self.held = keep + batch.sort(key=lambda x: (self.layers[x[0]]["seq"], x[1])) + return batch + + def offer_checked(self, h, cap, ref, payload, at, ev, outside): + reps = Reports(h) + if h not in self.layers: + why = self.admit(h, cap, ref, payload, at, ev) + if why is not None: + return [refuse(h, *why)] + self.evaluate(h, outside, reps) + return reps.finish(self.layers) + + def offer_batch(self, items): + order = [p["hash"] for p in items] + reps = Reports(order[0]) + ready, refused = [], [] + for p in items: + h = p["hash"] + if h not in self.layers: + why = self.admit(h, p["cap"], p["ref"], p["payload"], p["arrival"], p["evidence"]) + if why is not None: + refused.append(refuse(h, *why)) + continue + reps.get(h) + frm, count = units_of(self.layers[h]["ref"], self.n()) + for u in range(frm, frm + count): + if self.conflicted_before(h, u): + self.freeze(u, reps) + reps.get(h).refused.append((u, "layer-conflict")) + continue + r = self.readiness(h, u) + if r == "ready": + ready.append((h, u)) + elif r == "held": + self.held.append((h, u)) + reps.get(h).held.append(u) + else: + reps.get(h).refused.append((u, "prev-mismatch")) + self.run_batches(ready, reps) + out = reps.finish(self.layers) + refused + out.sort(key=lambda r: order.index(r.h) if r.h in order else len(order)) + return out + + def expire(self, now): + reps = {} + keep = [] + for h, u in self.held: + if now - self.layers[h]["arrival"] > self.cfg["bound"]["max_age_ms"]: + reps.setdefault(h, Report(h)).refused.append((u, "layer-gap")) + else: + keep.append((h, u)) + self.held = keep + return sorted(reps.values(), key=lambda r: self.layers[r.h]["seq"]) + + def unit_sha(self, u): + s = self.units[u] + if s["tier"] != DE: + return None + return hashlib.sha256(canonical(s["elem"], s["data"])).hexdigest() + + def clip_sha(self): + hs = [self.unit_sha(u) for u in range(self.n())] + if any(x is None for x in hs): + return None + return hashlib.sha256(b"".join(bytes.fromhex(x) for x in hs)).hexdigest() + + +def canonical(elem, data): + return struct.pack(f"<{len(data)}h", *data) if elem == "i16" else bytes(data) + + +def step1(env): + h = content_hash(env) + m, payload = parse_wai1(env) + sec = m.get("security") + if isinstance(sec, dict) and sec.get("encryption") not in (None, "none"): + raise ValueError("encrypted") + if "refines" not in m or m["refines"] is None: + return refuse(h, "refines-malformed") + try: + ref = parse_refines(m["refines"]) + except Malformed: + return refuse(h, "refines-malformed") + cap = m["model_requirement"]["capability"] + if cap not in UNIT_KIND: + return refuse(h, "capability-not-refinement") + return (h, cap, ref, payload) + + +class Sink: + def __init__(self, cfg): + self.cfg, self.chains, self.pending = cfg, {}, [] + + def offer(self, env, at, ev): + s = step1(env) + if isinstance(s, Report): + return [s] + h, cap, ref, payload = s + if ref["base"] in self.chains: + outside = len(self.pending) + sum(c.held_envelopes() for b, c in self.chains.items() if b != ref["base"]) + return self.chains[ref["base"]].offer_checked(h, cap, ref, payload, at, ev, outside) + r = Report(h) + if any(p["hash"] == h for p in self.pending): + r.held_for_base = True + return [r] + if len(self.pending) + sum(c.held_envelopes() for c in self.chains.values()) >= self.cfg["bound"]["max_envelopes"]: + return [refuse(h, "base-absent")] + self.pending.append(dict(hash=h, cap=cap, ref=ref, payload=payload, arrival=at, evidence=ev)) + r.held_for_base = True + return [r] + + def add_base(self, base, units): + chain = Chain(base, units, self.cfg) + mine = [p for p in self.pending if p["ref"]["base"] == base["hash"]] + self.pending = [p for p in self.pending if p["ref"]["base"] != base["hash"]] + reps = chain.offer_batch(mine) if mine else [] + self.chains[base["hash"]] = chain + return reps + + def expire(self, now): + age = self.cfg["bound"]["max_age_ms"] + gone = [p for p in self.pending if now - p["arrival"] > age] + self.pending = [p for p in self.pending if not now - p["arrival"] > age] + out = [refuse(p["hash"], "base-absent") for p in gone] + for b in sorted(self.chains): + out += self.chains[b].expire(now) + return out + + +# ---------------------------------------------------------------- base decodes + + +def paeth(a, b, c): + p = a + b - c + pa, pb, pc = abs(p - a), abs(p - b), abs(p - c) + return a if pa <= pb and pa <= pc else (b if pb <= pc else c) + + +def decode_png(b): + if b[:8] != b"\x89PNG\r\n\x1a\n": + raise ValueError("not a PNG") + at, idat, ihdr = 8, b"", None + while at < len(b): + n = struct.unpack_from(">I", b, at)[0] + kind, data = b[at + 4:at + 8], b[at + 8:at + 8 + n] + if zlib.crc32(kind + data) != struct.unpack_from(">I", b, at + 8 + n)[0]: + raise ValueError("a chunk CRC") + if kind == b"IHDR": + ihdr = struct.unpack(">IIBBBBB", data) + elif kind == b"IDAT": + idat += data + at += 12 + n + w, h, depth, ctype = ihdr[0], ihdr[1], ihdr[2], ihdr[3] + if (depth, ctype) != (8, 2): + raise ValueError("only 8-bit RGB is read here") + raw, stride = zlib.decompress(idat), w * 3 + out = bytearray() + prev = bytearray(stride) + for y in range(h): + ft, row = raw[y * (stride + 1)], bytearray(raw[y * (stride + 1) + 1:(y + 1) * (stride + 1)]) + for i in range(stride): + a = row[i - 3] if i >= 3 else 0 + c = prev[i - 3] if i >= 3 else 0 + pr = [0, a, prev[i], (a + prev[i]) // 2, paeth(a, prev[i], c)][ft] + row[i] = (row[i] + pr) & 255 + out += row + prev = row + return (h, w, 3), list(out) + + +class BitReader: + def __init__(self, b, at): + self.b, self.bit = b, at * 8 + + def get(self, n): + v = 0 + for _ in range(n): + v = (v << 1) | ((self.b[self.bit >> 3] >> (7 - (self.bit & 7))) & 1) + self.bit += 1 + return v + + +def crc8(data): + c = 0 + for x in data: + c ^= x + for _ in range(8): + c = ((c << 1) ^ 0x07) & 0xFF if c & 0x80 else (c << 1) & 0xFF + return c + + +def crc16(data): + c = 0 + for x in data: + c ^= x << 8 + for _ in range(8): + c = ((c << 1) ^ 0x8005) & 0xFFFF if c & 0x8000 else (c << 1) & 0xFFFF + return c + + +def decode_flac(b): + """FLAC with 16-bit independent channels and VERBATIM subframes, both CRCs checked.""" + if b[:4] != b"fLaC": + raise ValueError("not FLAC") + at, last, info = 4, False, None + while not last: + last, kind = bool(b[at] & 0x80), b[at] & 0x7F + n = int.from_bytes(b[at + 1:at + 4], "big") + if kind == 0: + r = BitReader(b, at + 4) + r.get(16), r.get(16), r.get(24), r.get(24) + info = dict(rate=r.get(20), ch=r.get(3) + 1, bps=r.get(5) + 1, total=r.get(36)) + at += 4 + n + if info["bps"] != 16: + raise ValueError("only 16 bits per sample are read here") + ch, out, total = info["ch"], [], 0 + while total < info["total"]: + r = BitReader(b, at) + if r.get(14) != 0b11111111111110 or r.get(1) or r.get(1): + raise ValueError("frame sync") + bs_code, sr_code, chan, ss, _ = r.get(4), r.get(4), r.get(4), r.get(3), r.get(1) + if chan != ch - 1 or ss != 0b100 or sr_code != 0 or bs_code != 0b0111: + raise ValueError("a frame header this reader does not read") + r.get(8) # a one-byte frame number + length = r.get(16) + 1 + hdr_end = r.bit >> 3 + if crc8(b[at:hdr_end]) != r.get(8): + raise ValueError("header CRC-8") + chans = [] + for _ in range(ch): + if r.get(1) or r.get(6) != 1 or r.get(1): + raise ValueError("not a VERBATIM subframe") + chans.append([(lambda v: v - 65536 if v >= 32768 else v)(r.get(16)) for _ in range(length)]) + r.bit = (r.bit + 7) & ~7 + end = r.bit >> 3 + if crc16(b[at:end]) != int.from_bytes(b[end:end + 2], "big"): + raise ValueError("frame CRC-16") + for i in range(length): + out += [chans[c][i] for c in range(ch)] + total += length + at = end + 2 + return (info["total"], 1, ch), out + + +# ---------------------------------------------------------------- the schedule (§8) + + +def present(clock, base_ms, layers, lead): + out = [] + for u, b in enumerate(base_ms): + due = clock["t0_ms"] + u * clock["unit_ms"] + clock["target_latency_ms"] + stage = expected = 0 + in_time = True + for li, arr in enumerate(layers): + t = arr[u] if u < len(arr) else None + if t is None: + break + expected += 1 + in_time = in_time and t + (lead[li] if li < len(lead) else 0) <= due + if in_time: + stage += 1 + out.append({"at_ms": max(due, b), "stage": stage, "expected": expected}) + return out + + +# ---------------------------------------------------------------- main + + +def verify_case(cdir, fails): + case = json.load(open(os.path.join(cdir, "case.json"))) + name = case["name"] + + def fail(m): + fails.append(f"{name}: {m}") + print(f"FAIL {name}: {m}") + bases = [] + for b in case["bases"]: + env = open(os.path.join(cdir, b["file"]), "rb").read() + s0 = open(os.path.join(cdir, b["stage0"]), "rb").read() + if hashlib.sha256(s0).hexdigest() != b["stage0_sha256"]: + fail(f"{b['stage0']} does not have its SHA-256") + if content_hash(env) != b["object"]: + fail(f"{b['file']} does not have its content hash") + m, payload = parse_wai1(env) + units, at = [], 0 + for u in b["units"]: + shape, elem = tuple(u["shape"]), u["elem"] + n = shape[0] * shape[1] * shape[2] + if elem == "u8": + data = list(s0[at:at + n]) + at += n + else: + data = list(struct.unpack_from(f"<{n}h", s0, at)) + at += 2 * n + units.append((shape, elem, data)) + via = b["decoded_via"] + kind = b["decode"]["kind"] + independent = None + if kind == "png": + independent = [decode_png(payload)] + elif kind == "flac": + independent = [decode_flac(payload)] + elif via == "wai.video.int_motion": + v = IPV.parse_wiv1(payload, 0) + independent = [((v["h"], v["w"], 3), list(f)) for f in IPV.decode_wiv1_k0(v)] + if independent is not None: + if [(s, d) for s, _, d in units] != independent: + fail(f"the independent decode of {b['file']} is not its stage0") + if base_tier(via, m, b["pin_resolved"]) != b["tier"]: + fail(f"the tier of {b['file']} is not {b['tier']}") + bases.append(dict(hash=content_hash(env), capability=via, via_fallback=b["via_fallback"], + attested=b["attested"], tier=b["tier"]), ) + bases[-1]["units"] = units + files = {f["file"]: open(os.path.join(cdir, f["file"]), "rb").read() for f in case["files"]} + for f in case["files"]: + if content_hash(files[f["file"]]) != f["object"] or len(files[f["file"]]) != f["bytes"]: + fail(f"{f['file']} is not the object the case names") + cfg = case["config"] + sink = Sink(cfg) + got = [] + for st in case["steps"]: + if st["op"] == "offer": + reps = sink.offer(files[st["file"]], st["at_ms"], st["evidence"]) + elif st["op"] == "base": + b = bases[st["index"]] + reps = sink.add_base(b, b["units"]) + else: + reps = sink.expire(st["at_ms"]) + got.append([r.json() for r in reps]) + for i, (g, e) in enumerate(zip(got, case["expect"]["reports"])): + if g != e: + fail(f"step {i}: reports {g} != {e}") + if len(got) != len(case["expect"]["reports"]): + fail("a different number of steps") + for bi, b in enumerate(bases): + exp = case["expect"]["bases"][bi] + c = sink.chains.get(b["hash"]) + if c is None: + if exp is not None: + fail(f"base {bi} was never decoded") + continue + units = [{"stage": s["stage"], "producer": s["producer"], "frozen": s["frozen"], "tier": s["tier"], + "shape": list(s["shape"]), "stage_sha256": c.unit_sha(u)} for u, s in enumerate(c.units)] + if units != exp["units"]: + fail(f"base {bi}: final units differ") + if c.clip_sha() != exp["clip_sha256"]: + fail(f"base {bi}: the clip hash differs") + + +def main(d): + fails = [] + cases = sorted(os.listdir(os.path.join(d, "cases"))) + for name in cases: + verify_case(os.path.join(d, "cases", name), fails) + sched = json.load(open(os.path.join(d, "schedule.json"))) + for i, t in enumerate(sched["traces"]): + p = present(sched["clock"], t["base_ms"], t["layers_ms"], t["lead_ms"]) + misses = sum(1 for x in p if x["stage"] < x["expected"]) + if p != t["presented"] or misses != t["deadline_misses"]: + fails.append(f"schedule trace {i}") + print(f"FAIL schedule trace {i}") + print(f"{len(cases)} cases, schedule: {len(sched['traces'])} traces, {len(fails)} failure(s)") + return 1 if fails else 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else HERE)) diff --git a/wai/wai-rs/Cargo.toml b/wai/wai-rs/Cargo.toml index 8911faebd..058c9e68c 100644 --- a/wai/wai-rs/Cargo.toml +++ b/wai/wai-rs/Cargo.toml @@ -483,6 +483,11 @@ name = "wai_int_payload_vectors" path = "src/bin/wai_int_payload_vectors.rs" required-features = ["video_int", "audio_int", "splat_int"] +[[bin]] +name = "wai_staged_vectors" +path = "src/bin/wai_staged_vectors.rs" +required-features = ["staged", "video_int", "audio_int", "splat_int"] + [[bin]] name = "wai_film_vectors" path = "src/bin/wai_film_vectors.rs" diff --git a/wai/wai-rs/src/bin/wai_staged_vectors.rs b/wai/wai-rs/src/bin/wai_staged_vectors.rs new file mode 100644 index 000000000..7481aefdf --- /dev/null +++ b/wai/wai-rs/src/bin/wai_staged_vectors.rs @@ -0,0 +1,1399 @@ +//! `wai_staged_vectors` — writes and verifies the staged-delivery corpus +//! (`extensions/staged-delivery.md` §12, `wai/staged-conformance/`). +//! +//! wai_staged_vectors gen +//! wai_staged_vectors verify +//! +//! `gen` builds every case from inputs it computes or reads from the committed +//! integer vectors (`wai-rs/tests/vectors/`): each case's bases and their stage-0 +//! output (`stage0.bin`), its layers (`*.wai`), and `case.json`, which states the +//! steps a sink takes (offer a layer, decode a base, expire holds) and what a +//! conforming sink reports at each step and holds at the end. It also writes +//! `schedule.json` (arrival traces and what a sink presents) and `index.json`. Every +//! file is a function of those inputs, so a second run writes the same bytes. +//! +//! `verify` regenerates every file in memory and requires it to equal the committed +//! one byte for byte; replays every case from its committed files through +//! `staged::StageSink` and requires every report and the final stage, producer, +//! frozen flag, tier and stage hash of every unit; checks each `stage0.bin` against its +//! SHA-256 and, for a base this build decodes (the integer codecs), against the base's +//! decode through `int_payload::decode_int` with its pinned set; and replays +//! `schedule.json`. A PNG or FLAC base is a fixture here (this build has no `codecs`), +//! decoded by the independent verifier `staged_verify.py`. Exit status 1 on any +//! failure. +//! +//! The content is synthetic: pictures, frames and PCM computed by integer functions in +//! this file. The base payloads of the integer codecs are the committed vectors; the +//! `wai.video.int_motion` bases are coded by a keyframe-0 encoder confined to this file +//! (zero motion, one block per frame), which the decode checks. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use std::process::ExitCode; + +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_VIDEO_INT_MOTION, CAP_VIDEO_INT_REFINE, +}; +use wai::container::{Conditioning, Manifest, ModelRequirement, Prev, PrevRun, Prior, Refines, Span, Wai}; +use wai::int_payload::{decode_int, resolve, IntDecoded}; +use wai::pin::HeldBytes; +use wai::staged::schedule::{deadline_misses, present, UnitClock}; +use wai::staged::wir1::{self, Below, Next, Predictor, RefineParams, UnitKind, Wir1Cost}; +use wai::staged::{tier, BaseInfo, ChainConfig, HoldBound, Live, OfferReport, ReceiptEvidence, Samples, StageSink, UnitBuf}; + +fn hex(b: &[u8]) -> String { + b.iter().map(|x| format!("{x:02x}")).collect() +} + +fn sha(b: &[u8]) -> String { + hex(&wai::pin::sha256(b)) +} + +fn chash(b: &[u8]) -> [u8; 32] { + wai::jwp_hash::content_hash(b) +} + +const VEC: &str = "wai-rs/tests/vectors"; + +// ───────────────────────── synthetic content ───────────────────────── + +/// A picture of `h × w` RGB8: gradients, a disc and a fine texture, moved by `t`. +fn picture(h: u32, w: u32, seed: u32, t: u32) -> Vec { + let mut out = Vec::with_capacity((h * w * 3) as usize); + let (cy, cx) = ((h / 2 + t) as i64, (w / 3 + 2 * t) as i64); + let r2 = ((h.min(w) / 3).max(1) as i64).pow(2); + for y in 0..h { + for x in 0..w { + let d = (y as i64 - cy).pow(2) + (x as i64 - cx).pow(2); + for c in 0..3u32 { + let grad = (y * 160 / h.max(1) + x * 90 / w.max(1)) as i64; + let disc = if d <= r2 { 70 + 25 * c as i64 } else { 0 }; + let tex = (((x * 7 + y * 13 + c * 5 + seed * 11) ^ (x * y + seed)) % 17) as i64 - 8; + out.push((grad + disc + tex + (seed as i64 % 40) + c as i64 * 15).clamp(0, 255) as u8); + } + } + } + out +} + +/// `n` samples of `ch` interleaved channels at `rate` hertz: two triangle tones per +/// channel and a click every 997 samples. +fn pcm(n: u32, ch: u32, rate: u32, seed: u32) -> Vec { + let tri = |t: u64, period: u64, amp: i64| -> i64 { + let p = (t % period) as i64; + let half = period as i64 / 2; + amp * (if p < half { 2 * p - half } else { 3 * half - 2 * p }) / half.max(1) + }; + let mut out = Vec::with_capacity((n * ch) as usize); + for i in 0..n as u64 { + for c in 0..ch as u64 { + let t = i * 48_000 / rate as u64; + let v = tri(t, 109 + 23 * c + seed as u64, 9000) + tri(t, 31 + 7 * c, 2500) + if i % 997 == 3 { 12_000 } else { 0 }; + out.push(v.clamp(-32768, 32767) as i16); + } + } + out +} + +// ───────────────────────── envelopes ───────────────────────── + +fn envelope(media: &str, cap: &str, fallback: Option<&str>, kind: &str, prior: Option, refines: Option<&Refines>, payload: Vec) -> Vec { + let mut mr = ModelRequirement::new(cap); + if let Some(f) = fallback { + mr = mr.with_fallback(f); + } + let mut cond = Conditioning::new(kind); + if let Some(p) = prior { + cond = cond.with_prior(p); + } + let mut m = Manifest::new(media, "replicate", mr, cond); + if let Some(r) = refines { + m = m.with_refines(r); + } + Wai::new(m, payload).to_bytes().expect("serialise") +} + +// ───────────────────────── a keyframe-0 WIV1 encoder (this generator only) ───────────────────────── + +/// Code `frames` (each `h × w` RGB8) as `wai.video.int_motion` (`WIV1`, keyframe 0) +/// with zero motion and one block per frame: frame 0 predicted from grey, each later +/// frame from the previous decoded frame. Returns the payload and its decode. +fn encode_wiv1(frames: &[Vec], h: u32, w: u32, q: u32) -> (Vec, Vec>) { + let mut recon: Vec> = Vec::new(); + let mut rs: Vec = Vec::new(); + for (f, src) in frames.iter().enumerate() { + let mut cur = Vec::with_capacity(src.len()); + for (i, &t) in src.iter().enumerate() { + let pred = if f == 0 { 128 } else { recon[f - 1][i] as i64 }; + let r = wai::staged::predict::rdiv(t as i64 - pred, q as i64); + rs.push(r); + cur.push((pred + r * q as i64).clamp(0, 255) as u8); + } + recon.push(cur); + } + let lo = *rs.iter().min().unwrap(); + let hi = *rs.iter().max().unwrap(); + let width = ((hi - lo + 1) as u64).min(4095); + let mut hist = vec![0u64; width as usize]; + for &r in &rs { + if r >= lo && r < lo + width as i64 { + hist[(r - lo) as usize] += 1; + } + } + let cdf = wai::int_entropy::hist_to_cdf(&hist, 16).expect("table"); + let mut e = wai::rans::RansEncoder::new(); + for &r in &rs { + assert!(e.encode(r as i32, &cdf, cdf.len(), lo as i32)); + } + let stream = e.flush(); + let n = frames.len() as u32; + let block = h.max(w); + let v = wai::int_wire::Wiv1 { + mv_precision: 1, + keyframe: 0, + n_frames: n, + h, + w, + block: if n == 1 { 1 } else { block }, + q, + offset: lo as i32, + cdf, + mvs: vec![(0, 0); (n as usize - 1) * if n == 1 { 0 } else { 1 }], + rans: &stream, + wih1: None, + }; + let payload = wai::int_wire::write_wiv1(&v); + (payload, recon) +} + +// ───────────────────────── PNG and FLAC writers (fixtures) ───────────────────────── + +fn crc32(data: &[u8]) -> u32 { + let mut c = 0xFFFF_FFFFu32; + for &b in data { + c ^= b as u32; + for _ in 0..8 { + c = if c & 1 != 0 { 0xEDB8_8320 ^ (c >> 1) } else { c >> 1 }; + } + } + !c +} + +fn adler32(data: &[u8]) -> u32 { + let (mut a, mut b) = (1u32, 0u32); + for &x in data { + a = (a + x as u32) % 65521; + b = (b + a) % 65521; + } + (b << 16) | a +} + +/// An RGB8 PNG, filter type `row % 5` on each row, stored (uncompressed) deflate. +fn png(rgb: &[u8], h: u32, w: u32) -> Vec { + let stride = (w * 3) as usize; + let mut raw = Vec::new(); + for y in 0..h as usize { + let ft = (y % 5) as u8; + raw.push(ft); + let row = &rgb[y * stride..(y + 1) * stride]; + let up: Vec = if y == 0 { vec![0; stride] } else { rgb[(y - 1) * stride..y * stride].to_vec() }; + for i in 0..stride { + let a = if i >= 3 { row[i - 3] as i16 } else { 0 }; + let b = up[i] as i16; + let c = if i >= 3 { up[i - 3] as i16 } else { 0 }; + let pred = match ft { + 0 => 0, + 1 => a, + 2 => b, + 3 => (a + b) / 2, + _ => { + let p = a + b - c; + let (pa, pb, pc) = ((p - a).abs(), (p - b).abs(), (p - c).abs()); + if pa <= pb && pa <= pc { a } else if pb <= pc { b } else { c } + } + }; + raw.push((row[i] as i16 - pred) as u8); + } + } + let mut z = vec![0x78, 0x01]; + for (k, block) in raw.chunks(65535).enumerate() { + let last = (k + 1) * 65535 >= raw.len(); + z.push(last as u8); + z.extend_from_slice(&(block.len() as u16).to_le_bytes()); + z.extend_from_slice(&(!(block.len() as u16)).to_le_bytes()); + z.extend_from_slice(block); + } + z.extend_from_slice(&adler32(&raw).to_be_bytes()); + let mut out = vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A]; + let mut chunk = |kind: &[u8], data: &[u8]| { + out.extend_from_slice(&(data.len() as u32).to_be_bytes()); + let mut body = kind.to_vec(); + body.extend_from_slice(data); + out.extend_from_slice(&body); + out.extend_from_slice(&crc32(&body).to_be_bytes()); + }; + let mut ihdr = Vec::new(); + ihdr.extend_from_slice(&w.to_be_bytes()); + ihdr.extend_from_slice(&h.to_be_bytes()); + ihdr.extend_from_slice(&[8, 2, 0, 0, 0]); + chunk(b"IHDR", &ihdr); + chunk(b"IDAT", &z); + chunk(b"IEND", &[]); + out +} + +struct Bits { + out: Vec, + acc: u64, + n: u32, +} + +impl Bits { + fn new() -> Self { + Bits { out: vec![], acc: 0, n: 0 } + } + fn put(&mut self, v: u64, bits: u32) { + for i in (0..bits).rev() { + self.acc = (self.acc << 1) | ((v >> i) & 1); + self.n += 1; + if self.n == 8 { + self.out.push(self.acc as u8); + self.acc = 0; + self.n = 0; + } + } + } + fn align(&mut self) { + while self.n != 0 { + self.put(0, 1); + } + } +} + +fn crc8(data: &[u8]) -> u8 { + let mut c = 0u8; + for &b in data { + c ^= b; + for _ in 0..8 { + c = if c & 0x80 != 0 { (c << 1) ^ 0x07 } else { c << 1 }; + } + } + c +} + +fn crc16(data: &[u8]) -> u16 { + let mut c = 0u16; + for &b in data { + c ^= (b as u16) << 8; + for _ in 0..8 { + c = if c & 0x8000 != 0 { (c << 1) ^ 0x8005 } else { c << 1 }; + } + } + c +} + +/// A FLAC stream of 16-bit PCM, `ch` independent channels, VERBATIM subframes, fixed +/// blocks of 256 samples (the last shorter), an unset MD5. +fn flac(samples: &[i16], ch: u32, rate: u32) -> Vec { + let n = samples.len() as u32 / ch; + let bs = 256u32; + let mut out = b"fLaC".to_vec(); + let mut si = Bits::new(); + si.put(bs as u64, 16); + si.put(bs as u64, 16); + si.put(0, 24); + si.put(0, 24); + si.put(rate as u64, 20); + si.put((ch - 1) as u64, 3); + si.put(15, 5); + si.put(n as u64, 36); + for _ in 0..16 { + si.put(0, 8); + } + out.push(0x80); // last block, STREAMINFO + out.extend_from_slice(&(si.out.len() as u32).to_be_bytes()[1..]); + out.extend_from_slice(&si.out); + let mut frame_no = 0u32; + let mut at = 0u32; + while at < n { + let len = bs.min(n - at); + let mut b = Bits::new(); + b.put(0b1111_1111_1111_10, 14); + b.put(0, 1); + b.put(0, 1); // fixed block size + b.put(0b0111, 4); // 16-bit (block size − 1) at the end of the header + b.put(0, 4); // sample rate from STREAMINFO + b.put((ch - 1) as u64, 4); // independent channels + b.put(0b100, 3); // 16 bits per sample + b.put(0, 1); + assert!(frame_no < 128, "the UTF-8 frame number is one byte here"); + b.put(frame_no as u64, 8); + b.put((len - 1) as u64, 16); + let crc = crc8(&b.out); + b.put(crc as u64, 8); + for c in 0..ch { + b.put(0, 1); + b.put(0b000001, 6); // VERBATIM + b.put(0, 1); + for i in 0..len { + b.put(samples[((at + i) * ch + c) as usize] as u16 as u64, 16); + } + } + b.align(); + let crc = crc16(&b.out); + b.put(crc as u64, 16); + out.extend_from_slice(&b.out); + at += len; + frame_no += 1; + } + out +} + +// ───────────────────────── bases ───────────────────────── + +/// How a base's stage 0 is checked. +#[derive(Clone)] +enum Decode { + /// `int_payload::decode_int` against the set (member → path below `wai/`). + Int { set: Vec<(&'static str, String)> }, + Png, + Flac, +} + +#[derive(Clone)] +struct Base { + envelope: Vec, + decoded_via: String, + via_fallback: bool, + attested: bool, + pin_resolved: bool, + tier: Option, + units: Vec, + decode: Decode, +} + +impl Base { + fn hash(&self) -> [u8; 32] { + chash(&self.envelope) + } + fn info(&self) -> BaseInfo { + BaseInfo { content_hash: self.hash(), capability: self.decoded_via.clone(), via_fallback: self.via_fallback, tier: self.tier, attested: self.attested } + } + fn unit(&self, u: usize) -> Vec { + match &self.units[u].samples { + Samples::U8(v) => v.clone(), + Samples::I16(_) => panic!("u8 unit expected"), + } + } + fn stage0(&self) -> Vec { + self.units.iter().flat_map(|u| u.samples.canonical_bytes()).collect() + } +} + +struct Ctx { + wai: PathBuf, +} + +impl Ctx { + fn read(&self, rel: &str) -> Vec { + std::fs::read(self.wai.join(rel)).unwrap_or_else(|e| panic!("read {rel}: {e}")) + } +} + +fn units_of(d: IntDecoded) -> Vec { + match d { + IntDecoded::Video { frames, width, height } => frames.into_iter().map(|f| UnitBuf { shape: (height, width, 3), samples: Samples::U8(f) }).collect(), + IntDecoded::Image { rgb, width, height } => vec![UnitBuf { shape: (height, width, 3), samples: Samples::U8(rgb) }], + IntDecoded::Pcm { samples, channels, .. } => { + let n = samples.len() as u32 / channels; + vec![UnitBuf { shape: (n, 1, channels), samples: Samples::I16(samples) }] + } + IntDecoded::Attr { bytes, n_attr, s, .. } => vec![UnitBuf { shape: (n_attr, s, s), samples: Samples::U8(bytes) }], + _ => panic!("an output kind the corpus does not use"), + } +} + +/// Decode `payload` through `cap` with `set` (member → path), pinned by `prior` when +/// given: the units, and whether a pin resolved. +fn decode_with>(ctx: &Ctx, cap: &str, payload: &[u8], set: &[(S, String)], prior: Option<&Prior>) -> Vec { + let bytes: Vec<(String, Vec)> = set.iter().map(|(m, p)| (m.as_ref().to_string(), ctx.read(p))).collect(); + let files: BTreeMap = bytes.iter().map(|(m, b)| (m.clone(), b.as_slice())).collect(); + let listing = if files.is_empty() { vec![] } else { wai::pin::listing_from(&files).expect("listing") }; + let held: Vec = if set.len() == 1 && prior.is_some_and(|p| p.form().is_none()) { + vec![HeldBytes::File(files.values().next().copied().unwrap())] + } else if set.is_empty() { + vec![] + } else { + vec![HeldBytes::Set { listing: &listing, files: &files }] + }; + let params = resolve(cap, prior, &held, None).unwrap_or_else(|e| panic!("resolve {cap}: {e}")); + assert_eq!(params.is_pinned(), prior.is_some(), "{cap}: the pin resolved"); + units_of(decode_int(cap, payload, ¶ms).unwrap_or_else(|e| panic!("decode {cap}: {e}"))) +} + +fn tiny_set() -> Vec<(&'static str, String)> { + vec![ + ("entropy.json", format!("{VEC}/int_hyper/tiny_entropy.json")), + ("g_s.whm", format!("{VEC}/int_hyper/tiny_g_s.bin")), + ("h_s.whm", format!("{VEC}/int_hyper/tiny_h_s.bin")), + ] +} + +fn set_pin(ctx: &Ctx, id: &str, set: &[(&'static str, String)], det: &str) -> Prior { + let bytes: Vec<(String, Vec)> = set.iter().map(|(m, p)| (m.to_string(), ctx.read(p))).collect(); + let files: BTreeMap = bytes.iter().map(|(m, b)| (m.clone(), b.as_slice())).collect(); + let listing = wai::pin::listing_from(&files).expect("listing"); + Prior::new(id, sha(&listing), det).with_form(CAP_PRIOR_SET).with_tier(DeclaredTier::DecodeEquivalence) +} + +fn file_pin(ctx: &Ctx, id: &str, path: &str, det: &str) -> Prior { + Prior::new(id, sha(&ctx.read(path)), det).with_tier(DeclaredTier::DecodeEquivalence) +} + +fn tiny_base(ctx: &Ctx, pinned: bool) -> Base { + let payload = ctx.read(&format!("{VEC}/int_hyper/tiny_bitstream.wih1")); + let set = tiny_set(); + let prior = pinned.then(|| set_pin(ctx, "int_hyper/tiny", &set, "int-rans/p16+int-hyper/i64")); + let units = decode_with(ctx, CAP_NEURAL_INT_HYPER, &payload, &set, prior.as_ref()); + let tier = tier::base_tier(CAP_NEURAL_INT_HYPER, prior.as_ref()); + Base { + envelope: envelope("image", CAP_NEURAL_INT_HYPER, None, "int_hyper", prior, None, payload), + decoded_via: CAP_NEURAL_INT_HYPER.into(), + via_fallback: false, + attested: false, + pin_resolved: pinned, + tier, + units, + decode: Decode::Int { set }, + } +} + +fn nk_base(ctx: &Ctx) -> Base { + let payload = ctx.read(&format!("{VEC}/int_video/video_nk_bitstream.wiv1")); + let set = tiny_set(); + let prior = Some(set_pin(ctx, "int_hyper/tiny", &set, "int-rans/p16+int-hyper/i64")); + let units = decode_with(ctx, CAP_VIDEO_INT_HYPER, &payload, &set, prior.as_ref()); + Base { + tier: tier::base_tier(CAP_VIDEO_INT_HYPER, prior.as_ref()), + envelope: envelope("video", CAP_VIDEO_INT_HYPER, None, "int_hyper", prior, None, payload), + decoded_via: CAP_VIDEO_INT_HYPER.into(), + via_fallback: false, + attested: false, + pin_resolved: true, + units, + decode: Decode::Int { set }, + } +} + +fn audio_base(ctx: &Ctx) -> Base { + let payload = ctx.read(&format!("{VEC}/int_audio/audio_bitstream.wia1")); + let model = format!("{VEC}/int_audio/audio_model.bin"); + let set = vec![("model.wam", model.clone())]; + let prior = Some(file_pin(ctx, "int_audio/vector", &model, "int-rans/p16+int-audio/i64")); + let units = decode_with(ctx, CAP_AUDIO_INT_CODEC, &payload, &set, prior.as_ref()); + Base { + tier: tier::base_tier(CAP_AUDIO_INT_CODEC, prior.as_ref()), + envelope: envelope("audio", CAP_AUDIO_INT_CODEC, None, "int_codec", prior, None, payload), + decoded_via: CAP_AUDIO_INT_CODEC.into(), + via_fallback: false, + attested: false, + pin_resolved: true, + units, + decode: Decode::Int { set }, + } +} + +fn splat_base(ctx: &Ctx) -> Base { + let payload = ctx.read(&format!("{VEC}/int_splat/splat.wis1")); + let model = format!("{VEC}/int_splat/splat_gs.bin"); + let set = vec![("g_s.whm", model.clone())]; + let prior = Some(file_pin(ctx, "int_splat/vector", &model, "int-rans/p16+int-splat/i64")); + let units = decode_with(ctx, CAP_SPLAT_INT_CODEC, &payload, &set, prior.as_ref()); + Base { + tier: tier::base_tier(CAP_SPLAT_INT_CODEC, prior.as_ref()), + envelope: envelope("volumetric", CAP_SPLAT_INT_CODEC, None, "int_codec", prior, None, payload), + decoded_via: CAP_SPLAT_INT_CODEC.into(), + via_fallback: false, + attested: false, + pin_resolved: true, + units, + decode: Decode::Int { set }, + } +} + +/// A `wai.video.int_motion` GOP of `n` frames of `h × w`, from the moving picture +/// `seed`, starting at time `t0`. +fn gop_base(ctx: &Ctx, h: u32, w: u32, n: u32, seed: u32, t0: u32, q: u32) -> Base { + let frames: Vec> = (0..n).map(|f| picture(h, w, seed, t0 + f)).collect(); + let (payload, recon) = encode_wiv1(&frames, h, w, q); + let units = decode_with::<&str>(ctx, CAP_VIDEO_INT_MOTION, &payload, &[], None); + let mine: Vec = recon.into_iter().map(|f| UnitBuf { shape: (h, w, 3), samples: Samples::U8(f) }).collect(); + assert_eq!(units, mine, "the WIV1 decode is the encoder's reconstruction"); + Base { + tier: tier::base_tier(CAP_VIDEO_INT_MOTION, None), + envelope: envelope("video", CAP_VIDEO_INT_MOTION, None, "int_motion", None, None, payload), + decoded_via: CAP_VIDEO_INT_MOTION.into(), + via_fallback: false, + attested: false, + pin_resolved: false, + units, + decode: Decode::Int { set: vec![] }, + } +} + +fn png_base(h: u32, w: u32, seed: u32) -> Base { + let rgb = picture(h, w, seed, 0); + Base { + tier: tier::base_tier(CAP_IMAGE_PNG, None), + envelope: envelope("image", CAP_IMAGE_PNG, None, "png", None, None, png(&rgb, h, w)), + decoded_via: CAP_IMAGE_PNG.into(), + via_fallback: false, + attested: false, + pin_resolved: false, + units: vec![UnitBuf { shape: (h, w, 3), samples: Samples::U8(rgb) }], + decode: Decode::Png, + } +} + +fn flac_base(n: u32, ch: u32, rate: u32) -> Base { + let s = pcm(n, ch, rate, 1); + Base { + tier: tier::base_tier(CAP_AUDIO_FLAC, None), + envelope: envelope("audio", CAP_AUDIO_FLAC, None, "flac", None, None, flac(&s, ch, rate)), + decoded_via: CAP_AUDIO_FLAC.into(), + via_fallback: false, + attested: false, + pin_resolved: false, + units: vec![UnitBuf { shape: (n, 1, ch), samples: Samples::I16(s) }], + decode: Decode::Flac, + } +} + +// ───────────────────────── layers ───────────────────────── + +fn below(s: &Samples) -> Below<'_> { + match s { + Samples::U8(v) => Below::U8(v), + Samples::I16(v) => Below::I16(v), + } +} + +/// A residual layer of `cap` over `base`: `prev`, `layer`, `span`; the units below +/// (`belows`, of shape `below_shape`) refined toward `targets` (of shape `dims`). +#[allow(clippy::too_many_arguments)] +fn layer(cap: &str, base: &[u8; 32], prev: Prev, layer: u8, span: Option, belows: &[Samples], below_shape: (u32, u32, u32), targets: &[Samples], dims: (u32, u32, u32), p: RefineParams) -> (Vec, Vec) { + let kind = wai::staged::unit_kind(cap).expect("a refinement"); + let b: Vec = belows.iter().map(below).collect(); + let t: Vec = targets.iter().map(below).collect(); + let (payload, recon) = wir1::encode(kind, &b, below_shape, &t, dims, &p).unwrap_or_else(|e| panic!("encode: {e}")); + let r = Refines { base: hex(base), prev, layer, span }; + let media = match kind { + UnitKind::Rgb8 => if cap == CAP_VIDEO_INT_REFINE { "video" } else { "image" }, + UnitKind::Pcm => "audio", + UnitKind::Attr => "volumetric", + }; + let env = envelope(media, cap, None, "wir1", None, Some(&r), payload); + (env, recon.into_iter().map(|n| match n { Next::U8(v) => Samples::U8(v), Next::I16(v) => Samples::I16(v) }).collect()) +} + +fn params(q: u32, predictor: Predictor, max_symbols: u32) -> RefineParams { + RefineParams { q, predictor, rate: (0, 0), table_per_unit: false, max_symbols } +} + +fn one(h: &[u8; 32]) -> Prev { + Prev::One(hex(h)) +} + +fn runs(hs: &[[u8; 32]]) -> Prev { + let mut r: Vec = Vec::new(); + for h in hs { + match r.last_mut() { + Some(l) if l.producer == hex(h) => l.count += 1, + _ => r.push(PrevRun { producer: hex(h), count: 1 }), + } + } + if r.len() == 1 { Prev::One(r.remove(0).producer) } else { Prev::Runs(r) } +} + +fn u8s(v: Vec) -> Samples { + Samples::U8(v) +} + +/// Rewrite a layer envelope's manifest or payload (for refusal cases). +fn edit(env: &[u8], f: impl FnOnce(&mut Wai)) -> Vec { + let mut w = Wai::from_bytes(env).expect("an envelope"); + f(&mut w); + w.to_bytes().expect("serialise") +} + +// ───────────────────────── cases ───────────────────────── + +#[derive(Clone)] +enum Step { + Offer { file: String, at_ms: u64, evidence: ReceiptEvidence }, + Base { index: usize, at_ms: u64 }, + Expire { at_ms: u64 }, +} + +struct Case { + name: &'static str, + about: &'static str, + bases: Vec, + files: Vec<(String, Vec)>, + cfg: ChainConfig, + steps: Vec, +} + +fn cfg() -> ChainConfig { + ChainConfig { bound: HoldBound { max_envelopes: 16, max_age_ms: 1000 }, cost_limit: None, live: None } +} + +fn offer(file: &str, at_ms: u64) -> Step { + Step::Offer { file: file.into(), at_ms, evidence: ReceiptEvidence::None } +} + +fn base_at(index: usize, at_ms: u64) -> Step { + Step::Base { index, at_ms } +} + +fn rgb_unit(b: &Base, u: usize) -> Samples { + u8s(b.unit(u)) +} + +fn cases(ctx: &Ctx) -> Vec { + let mut out = Vec::new(); + let pid = Predictor::Identity; + + // ---- image ---- + { + let b = tiny_base(ctx, true); + let bh = b.hash(); + let t = u8s(picture(16, 16, 1, 0)); + let (l1, r1) = layer(CAP_IMAGE_INT_REFINE, &bh, one(&bh), 1, None, &[rgb_unit(&b, 0)], (16, 16, 3), &[t.clone()], (16, 16, 3), params(12, pid, 4095)); + let (l2, _) = layer(CAP_IMAGE_INT_REFINE, &bh, one(&chash(&l1)), 2, None, &r1, (16, 16, 3), &[t], (16, 16, 3), params(3, pid, 4095)); + out.push(Case { name: "image_int_hyper_2l", about: "a pinned wai.neural.int_hyper base, then two identity layers", bases: vec![b], + files: vec![("L1.wai".into(), l1), ("L2.wai".into(), l2)], cfg: cfg(), steps: vec![base_at(0, 0), offer("L1.wai", 1), offer("L2.wai", 2)] }); + } + { + let b = tiny_base(ctx, false); + let bh = b.hash(); + let t = u8s(picture(16, 16, 1, 0)); + let (l1, _) = layer(CAP_IMAGE_INT_REFINE, &bh, one(&bh), 1, None, &[rgb_unit(&b, 0)], (16, 16, 3), &[t], (16, 16, 3), params(12, pid, 4095)); + out.push(Case { name: "image_int_hyper_unpinned", about: "an unpinned wai.neural.int_hyper base: the layer applies, and no unit holds a tier, so no stage hash is stated", bases: vec![b], + files: vec![("L1.wai".into(), l1)], cfg: cfg(), steps: vec![base_at(0, 0), offer("L1.wai", 1)] }); + } + { + let b = tiny_base(ctx, true); + let bh = b.hash(); + let t = u8s(picture(32, 32, 2, 0)); + let (l1, r1) = layer(CAP_IMAGE_INT_REFINE, &bh, one(&bh), 1, None, &[rgb_unit(&b, 0)], (16, 16, 3), &[t.clone()], (32, 32, 3), params(6, Predictor::Up2, 4095)); + let (l2, _) = layer(CAP_IMAGE_INT_REFINE, &bh, one(&chash(&l1)), 2, None, &r1, (32, 32, 3), &[t], (32, 32, 3), params(2, pid, 4095)); + out.push(Case { name: "image_up2_from_int_hyper", about: "a 16×16 base, an up2 layer to 32×32, then an identity layer", bases: vec![b], + files: vec![("L1.wai".into(), l1), ("L2.wai".into(), l2)], cfg: cfg(), steps: vec![base_at(0, 0), offer("L1.wai", 1), offer("L2.wai", 2)] }); + } + { + let b = tiny_base(ctx, true); + let bh = b.hash(); + let (l1, r1) = layer(CAP_IMAGE_INT_REFINE, &bh, one(&bh), 1, None, &[rgb_unit(&b, 0)], (16, 16, 3), &[u8s(picture(31, 31, 3, 0))], (31, 31, 3), params(4, Predictor::Up2, 4095)); + let (l2, _) = layer(CAP_IMAGE_INT_REFINE, &bh, one(&chash(&l1)), 2, None, &r1, (31, 31, 3), &[u8s(picture(61, 61, 3, 0))], (61, 61, 3), params(4, Predictor::Up2, 4095)); + out.push(Case { name: "image_up2_odd", about: "up2 to an odd size, then up2 from it: 16×16 → 31×31 → 61×61", bases: vec![b], + files: vec![("L1.wai".into(), l1), ("L2.wai".into(), l2)], cfg: cfg(), steps: vec![base_at(0, 0), offer("L1.wai", 1), offer("L2.wai", 2)] }); + } + { + let b = png_base(16, 16, 7); + let bh = b.hash(); + let (l1, _) = layer(CAP_IMAGE_INT_REFINE, &bh, one(&bh), 1, None, &[rgb_unit(&b, 0)], (16, 16, 3), &[u8s(picture(32, 32, 7, 1))], (32, 32, 3), params(1, Predictor::Up2, 4095)); + out.push(Case { name: "image_png_up2", about: "a PNG base (a stage-0 fixture here), an up2 layer at q = 1", bases: vec![b], + files: vec![("L1.wai".into(), l1)], cfg: cfg(), steps: vec![base_at(0, 0), offer("L1.wai", 1)] }); + } + + // ---- video ---- + { + let gops: Vec = (0..3).map(|g| gop_base(ctx, 16, 16, 4, 9, 4 * g, 10)).collect(); + let mut files = Vec::new(); + let mut steps = Vec::new(); + for (g, b) in gops.iter().enumerate() { + steps.push(base_at(g, 0)); + let bh = b.hash(); + let mut hs = Vec::new(); + let mut mid = Vec::new(); + for f in 0..4u32 { + let t = u8s(picture(16, 16, 9, 4 * g as u32 + f)); + let (l, r) = layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, Some(Span { from: f, count: 1 }), &[rgb_unit(b, f as usize)], (16, 16, 3), &[t], (16, 16, 3), params(4, pid, 4095)); + hs.push(chash(&l)); + mid.push(r.into_iter().next().unwrap()); + let name = format!("g{g}_L1_f{f}.wai"); + steps.push(offer(&name, 1)); + files.push((name, l)); + } + let targets: Vec = (0..4).map(|f| u8s(picture(16, 16, 9, 4 * g as u32 + f))).collect(); + let (l2, _) = layer(CAP_VIDEO_INT_REFINE, &bh, runs(&hs), 2, None, &mid, (16, 16, 3), &targets, (16, 16, 3), params(1, pid, 4095)); + let name = format!("g{g}_L2.wai"); + steps.push(offer(&name, 2)); + files.push((name, l2)); + } + out.push(Case { name: "video_int_motion_gop_perframe", about: "three closed-GOP wai.video.int_motion bases of four frames; a layer 1 per frame; a layer 2 per GOP whose prev is the run of its frames' producers", bases: gops, files, cfg: cfg(), steps }); + } + { + let b = gop_base(ctx, 16, 16, 4, 11, 0, 10); + let bh = b.hash(); + let mut files = Vec::new(); + let mut hs = Vec::new(); + let mut mid = Vec::new(); + let mut steps = vec![base_at(0, 0)]; + for f in 0..4u32 { + let t = u8s(picture(16, 16, 11, f)); + let (l, r) = layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, Some(Span { from: f, count: 1 }), &[rgb_unit(&b, f as usize)], (16, 16, 3), &[t], (16, 16, 3), params(4, pid, 4095)); + hs.push(chash(&l)); + mid.push(r.into_iter().next().unwrap()); + let name = format!("L1_f{f}.wai"); + if f != 2 { + steps.push(offer(&name, 1)); + } + files.push((name, l)); + } + let targets: Vec = (0..4).map(|f| u8s(picture(16, 16, 11, f))).collect(); + let (l2, _) = layer(CAP_VIDEO_INT_REFINE, &bh, runs(&hs), 2, None, &mid, (16, 16, 3), &targets, (16, 16, 3), params(1, pid, 4095)); + files.push(("L2.wai".into(), l2)); + steps.push(offer("L2.wai", 2)); + steps.push(Step::Expire { at_ms: 1003 }); + out.push(Case { name: "video_mixed_stage_l2", about: "frame 2's layer 1 is withheld: the run-form layer 2 applies to frames 0, 1 and 3, and frame 2 holds it to the bound (layer-gap)", bases: vec![b], files, cfg: cfg(), steps }); + } + { + let b = nk_base(ctx); + let bh = b.hash(); + let belows: Vec = (0..5).map(|f| rgb_unit(&b, f)).collect(); + let targets: Vec = (0..5).map(|f| u8s(picture(16, 16, 13, f))).collect(); + let (l1, _) = layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, None, &belows, (16, 16, 3), &targets, (16, 16, 3), RefineParams { table_per_unit: true, ..params(4, pid, 4095) }); + out.push(Case { name: "video_int_hyper_keyframe", about: "a pinned wai.video.int_hyper clip (keyframe 1), then one layer over its five frames with a table per unit", bases: vec![b], + files: vec![("L1.wai".into(), l1)], cfg: cfg(), steps: vec![base_at(0, 0), offer("L1.wai", 1)] }); + } + + // ---- audio ---- + { + let b = audio_base(ctx); + let bh = b.hash(); + let n = b.units[0].shape.0; + let (l1, _) = layer(CAP_AUDIO_INT_REFINE, &bh, one(&bh), 1, None, &[b.units[0].samples.clone()], (n, 1, 1), &[Samples::I16(pcm(n, 1, 16_000, 2))], (n, 1, 1), params(16, pid, 4095)); + out.push(Case { name: "audio_int_codec_1l", about: "a pinned wai.audio.int_codec base, then an identity layer", bases: vec![b], + files: vec![("L1.wai".into(), l1)], cfg: cfg(), steps: vec![base_at(0, 0), offer("L1.wai", 1)] }); + } + { + let b = flac_base(400, 2, 8000); + let bh = b.hash(); + let (l1, r1) = layer(CAP_AUDIO_INT_REFINE, &bh, one(&bh), 1, None, &[b.units[0].samples.clone()], (400, 1, 2), &[Samples::I16(pcm(800, 2, 16_000, 1))], (800, 1, 2), + RefineParams { rate: (2, 1), ..params(8, Predictor::Rate, 4095) }); + let (l2, _) = layer(CAP_AUDIO_INT_REFINE, &bh, one(&chash(&l1)), 2, None, &r1, (800, 1, 2), &[Samples::I16(pcm(2400, 2, 48_000, 1))], (2400, 1, 2), + RefineParams { rate: (3, 1), ..params(2, Predictor::Rate, 4095) }); + out.push(Case { name: "audio_flac_rate_ladder", about: "a stereo FLAC base at 8 kHz (a stage-0 fixture here), a rate layer to 16 kHz, then one to 48 kHz", bases: vec![b], + files: vec![("L1.wai".into(), l1), ("L2.wai".into(), l2)], cfg: cfg(), steps: vec![base_at(0, 0), offer("L1.wai", 1), offer("L2.wai", 2)] }); + } + + // ---- splat ---- + { + let b = splat_base(ctx); + let bh = b.hash(); + let sh = b.units[0].shape; + let target: Vec = b.unit(0).iter().enumerate().map(|(i, &v)| (v as i32 + ((i as i32 * 37) % 23) - 11).clamp(0, 255) as u8).collect(); + let (l1, _) = layer(CAP_SPLAT_INT_REFINE, &bh, one(&bh), 1, None, &[rgb_unit(&b, 0)], sh, &[u8s(target)], sh, params(3, pid, 4095)); + out.push(Case { name: "splat_attr_1l", about: "a pinned wai.splat.int_codec base, then an identity layer over its attribute tensor", bases: vec![b], + files: vec![("L1.wai".into(), l1)], cfg: cfg(), steps: vec![base_at(0, 0), offer("L1.wai", 1)] }); + } + + // ---- refusals, on a two-frame 8×8 GOP ---- + let small = |seed: u32| gop_base(ctx, 8, 8, 2, seed, 0, 8); + let l1_of = |b: &Base, seed: u32, q: u32| -> (Vec, Vec) { + let bh = b.hash(); + let belows: Vec = (0..2).map(|f| rgb_unit(b, f)).collect(); + let targets: Vec = (0..2).map(|f| u8s(picture(8, 8, seed, f))).collect(); + layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, None, &belows, (8, 8, 3), &targets, (8, 8, 3), params(q, pid, 4095)) + }; + let l2_of = |b: &Base, l1: &[u8], mid: &[Samples], seed: u32| -> Vec { + let targets: Vec = (0..2).map(|f| u8s(picture(8, 8, seed, f + 7))).collect(); + layer(CAP_VIDEO_INT_REFINE, &b.hash(), one(&chash(l1)), 2, None, mid, (8, 8, 3), &targets, (8, 8, 3), params(2, pid, 4095)).0 + }; + let simple = |name: &'static str, about: &'static str, b: Base, files: Vec<(String, Vec)>, steps: Vec| Case { name, about, bases: vec![b], files, cfg: cfg(), steps }; + { + let (a, other) = (small(20), small(21)); + let (lo, _) = l1_of(&other, 22, 4); + out.push(simple("wrong_base", "a layer of another stage set, whose base this sink never decodes: held for its base, then base-absent at the bound", a, vec![("L1_other.wai".into(), lo)], + vec![base_at(0, 0), offer("L1_other.wai", 1), Step::Expire { at_ms: 1002 }])); + } + { + let a = small(23); + let (l1, _) = l1_of(&a, 24, 4); + out.push(Case { name: "refinement_without_base", about: "a layer whose base never arrives: held, then base-absent at the bound", bases: vec![a], files: vec![("L1.wai".into(), l1)], cfg: cfg(), + steps: vec![offer("L1.wai", 0), Step::Expire { at_ms: 1001 }] }); + } + { + let a = small(25); + let (l1, mid) = l1_of(&a, 26, 4); + let l2 = l2_of(&a, &l1, &mid, 26); + let wrong = edit(&l2, |w| w.manifest.refines = Some(Refines { base: hex(&a.hash()), prev: one(&a.hash()), layer: 2, span: None }.to_value())); + out.push(simple("wrong_prev", "a layer 2 whose prev names the base, not the layer 1 the units were refined by: prev-mismatch", a, + vec![("L1.wai".into(), l1), ("L2_wrong_prev.wai".into(), wrong)], vec![base_at(0, 0), offer("L1.wai", 1), offer("L2_wrong_prev.wai", 2)])); + } + { + let a = small(27); + let (l1, mid) = l1_of(&a, 28, 4); + let l2 = l2_of(&a, &l1, &mid, 28); + out.push(simple("missing_layer", "a layer 2 whose layer 1 never arrives: held for each unit, then layer-gap at the bound", a, vec![("L2.wai".into(), l2)], + vec![base_at(0, 0), offer("L2.wai", 1), Step::Expire { at_ms: 1002 }])); + } + { + let a = small(29); + let (la, _) = l1_of(&a, 30, 4); + let (lb, _) = l1_of(&a, 31, 4); + out.push(Case { name: "conflict_held", about: "two different layers 1 held for the base and released together: neither applies, and both units are frozen", bases: vec![a], + files: vec![("L1a.wai".into(), la), ("L1b.wai".into(), lb)], cfg: cfg(), steps: vec![offer("L1a.wai", 0), offer("L1b.wai", 1), base_at(0, 2)] }); + } + { + let a = small(32); + let (la, mid) = l1_of(&a, 33, 4); + let (lb, _) = l1_of(&a, 34, 4); + let l2 = l2_of(&a, &la, &mid, 33); + out.push(simple("conflict_after_apply", "layer 1a applies; a different layer 1b conflicts and freezes the units at stage 1; a layer 2 over 1a is then refused", a, + vec![("L1a.wai".into(), la), ("L1b.wai".into(), lb), ("L2.wai".into(), l2)], vec![base_at(0, 0), offer("L1a.wai", 1), offer("L1b.wai", 2), offer("L2.wai", 3)])); + } + { + let a = small(73); + let (l1, mid) = l1_of(&a, 74, 4); + let l2 = l2_of(&a, &l1, &mid, 74); + out.push(Case { name: "pending_l1_l2", about: "layers 2 and 1 both arrive before their base: released together, layer 1 applies and releases layer 2, which applies", bases: vec![a], + files: vec![("L1.wai".into(), l1), ("L2.wai".into(), l2)], cfg: cfg(), steps: vec![offer("L2.wai", 0), offer("L1.wai", 1), base_at(0, 2)] }); + } + { + let a = small(35); + let (l1, mid) = l1_of(&a, 36, 4); + let l2 = l2_of(&a, &l1, &mid, 36); + out.push(simple("out_of_order", "layer 2 arrives before layer 1: held, and applied when layer 1 releases it; the stages equal an in-order arrival", a, + vec![("L1.wai".into(), l1), ("L2.wai".into(), l2)], vec![base_at(0, 0), offer("L2.wai", 1), offer("L1.wai", 2)])); + } + { + let mut a = small(37); + a.envelope = edit(&a.envelope, |w| { + w.manifest.model_requirement.capability = wai::codecs::CAP_VIDEO_VVC.into(); + w.manifest.model_requirement.fallback = Some(CAP_VIDEO_INT_MOTION.into()); + }); + a.via_fallback = true; + let (l1, _) = l1_of(&a, 38, 4); + out.push(simple("base_via_fallback", "the base was decoded through its manifest's fallback: base-via-fallback", a, vec![("L1.wai".into(), l1)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let a = small(39); + let small_units: Vec = (0..2).map(|f| u8s(picture(4, 4, 40, f))).collect(); + let (l1, _) = layer(CAP_VIDEO_INT_REFINE, &a.hash(), one(&a.hash()), 1, None, &small_units, (4, 4, 3), &small_units, (4, 4, 3), params(4, pid, 4095)); + out.push(simple("geometry_mismatch", "a layer coded for 4×4 frames over 8×8 frames: geometry-mismatch for each unit", a, vec![("L1.wai".into(), l1)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let a = small(41); + let (l1, _) = l1_of(&a, 42, 4); + let cut = edit(&l1, |w| { + let n = w.payload.len(); + w.payload.truncate(n - 3); + }); + out.push(simple("payload_truncated", "a WIR1 payload three bytes short: payload-malformed (stream_length)", a, vec![("L1.wai".into(), cut)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let a = small(43); + let (l1, _) = l1_of(&a, 44, 4); + let bad = edit(&l1, |w| { + let len = u16::from_le_bytes([w.payload[40], w.payload[41]]) as usize; + let at = 44 + 4 * (len / 2); + w.payload[at..at + 4].copy_from_slice(&0u32.to_le_bytes()); + }); + out.push(simple("cdf_malformed", "a table entry set to 0 in mid-table: payload-malformed (cdf_malformed)", a, vec![("L1.wai".into(), bad)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let a = small(45); + let (l1, _) = l1_of(&a, 46, 4); + let bad = edit(&l1, |w| w.payload[32..36].copy_from_slice(&0u32.to_le_bytes())); + out.push(simple("q_zero", "a step q of 0: payload-malformed (field_range)", a, vec![("L1.wai".into(), bad)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let a = small(47); + let (l1, _) = l1_of(&a, 48, 4); + let bad = edit(&l1, |w| { + let len = u16::from_le_bytes([w.payload[40], w.payload[41]]) as usize; + let lens_at = 44 + 4 * len; + let first = u32::from_le_bytes(w.payload[lens_at..lens_at + 4].try_into().unwrap()); + w.payload[lens_at..lens_at + 4].copy_from_slice(&(first + 4).to_le_bytes()); + let streams_at = lens_at + 8; + let end = streams_at + first as usize; + w.payload.splice(end..end, [0u8; 4]); + }); + out.push(simple("stream_noncanonical", "a word after unit 0's last symbol: payload-malformed (stream_noncanonical)", a, vec![("L1.wai".into(), bad)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let a = small(49); + let (l1, _) = l1_of(&a, 50, 4); + let bad = edit(&l1, |w| { + let len = u16::from_le_bytes([w.payload[40], w.payload[41]]) as usize; + let lens_at = 44 + 4 * len; + let first = u32::from_le_bytes(w.payload[lens_at..lens_at + 4].try_into().unwrap()) as usize; + let streams_at = lens_at + 8; + // Unit 0's stream becomes the state 2^31 alone: it codes no symbol. + w.payload.splice(streams_at..streams_at + first, [0, 0, 0, 0x80, 0, 0, 0, 0]); + w.payload[lens_at..lens_at + 4].copy_from_slice(&8u32.to_le_bytes()); + }); + out.push(simple("stream_exhausted", "unit 0's stream codes no symbol: it runs out at the first, payload-malformed (stream_exhausted)", a, vec![("L1.wai".into(), bad)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let a = small(51); + let belows: Vec = (0..2).map(|f| rgb_unit(&a, f)).collect(); + let targets: Vec = (0..2).map(|f| u8s(picture(8, 8, 52, f).iter().enumerate().map(|(i, _)| if i % 2 == 0 { 0 } else { 255 }).collect())).collect(); + let (l1, _) = layer(CAP_VIDEO_INT_REFINE, &a.hash(), one(&a.hash()), 1, None, &belows, (8, 8, 3), &targets, (8, 8, 3), params(200, pid, 4095)); + out.push(simple("clamp_extremes", "targets at 0 and 255 with q = 200: residuals that overshoot are clamped to the u8 range", a, vec![("L1.wai".into(), l1)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let a = small(53); + let belows: Vec = (0..2).map(|f| rgb_unit(&a, f)).collect(); + let targets: Vec = (0..2).map(|f| u8s(picture(8, 8, 54, f).iter().map(|&v| 255 - v).collect())).collect(); + let (l1, _) = layer(CAP_VIDEO_INT_REFINE, &a.hash(), one(&a.hash()), 1, None, &belows, (8, 8, 3), &targets, (8, 8, 3), params(1, pid, 1)); + out.push(simple("bypass_extremes", "a table of one symbol at q = 1: nearly every residual takes the bypass escape, and the layer is exact", a, vec![("L1.wai".into(), l1)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let a = small(55); + let (l1, _) = l1_of(&a, 56, 4); + let bad = edit(&l1, |w| w.manifest.refines = Some(json!({"base": hex(&a.hash()), "prev": hex(&a.hash()), "layer": 0}))); + out.push(simple("layer_zero", "refines.layer 0: refines-malformed", a, vec![("L1.wai".into(), bad)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let a = small(57); + let (l1, _) = l1_of(&a, 58, 4); + let bad = edit(&l1, |w| w.manifest.refines = Some(json!({"base": hex(&a.hash()).to_uppercase(), "prev": hex(&a.hash()).to_uppercase(), "layer": 1}))); + out.push(simple("refines_malformed", "a base hash in upper case: refines-malformed", a, vec![("L1.wai".into(), bad)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let a = small(59); + let (l1, _) = l1_of(&a, 60, 4); + let bad = edit(&l1, |w| w.manifest.model_requirement.capability = CAP_VIDEO_INT_MOTION.into()); + out.push(simple("capability_not_refinement", "refines on a wai.video.int_motion envelope: capability-not-refinement", a, vec![("L1.wai".into(), bad)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let a = small(61); + let belows: Vec = (0..2).map(|f| rgb_unit(&a, f)).collect(); + let (l1, _) = layer(CAP_IMAGE_INT_REFINE, &a.hash(), one(&a.hash()), 1, None, &belows, (8, 8, 3), &belows, (8, 8, 3), params(4, pid, 4095)); + out.push(simple("base_not_admitted", "a wai.image.int_refine layer over a wai.video.int_motion base: base-not-admitted", a, vec![("L1.wai".into(), l1)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let mut a = small(63); + a.attested = true; + let (l1, _) = l1_of(&a, 64, 4); + out.push(simple("inclusion_only_evidence", "an attested base: the layer verified by inclusion only is refinement-unattested; the same layer delivered-verified applies", a, + vec![("L1.wai".into(), l1)], + vec![base_at(0, 0), Step::Offer { file: "L1.wai".into(), at_ms: 1, evidence: ReceiptEvidence::InclusionVerified }, + Step::Offer { file: "L1.wai".into(), at_ms: 2, evidence: ReceiptEvidence::DeliveredVerified }])); + } + { + let a = small(65); + let (l1, _) = l1_of(&a, 66, 4); + out.push(Case { name: "cost_over_limit", about: "a sink whose cost limit is 100 output bytes: the 384-byte layer is cost-over-limit", bases: vec![a], files: vec![("L1.wai".into(), l1)], + cfg: ChainConfig { cost_limit: Some(Wir1Cost { output_bytes: 100, symbols: u64::MAX, working_bytes: u64::MAX }), ..cfg() }, steps: vec![base_at(0, 0), offer("L1.wai", 1)] }); + } + { + let a = small(67); + let bh = a.hash(); + let mut files = Vec::new(); + for f in 0..2u32 { + let (l, _) = layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, Some(Span { from: f, count: 1 }), &[rgb_unit(&a, f as usize)], (8, 8, 3), &[u8s(picture(8, 8, 68, f))], (8, 8, 3), params(4, pid, 4095)); + files.push((format!("L1_f{f}.wai"), l)); + } + out.push(Case { name: "deadline_missed", about: "units presented at 140 and 180 with a 30 ms lead: frame 0's layer arrives at 120 (late), frame 1's at 120 (in time)", bases: vec![a], files, + cfg: ChainConfig { live: Some(Live { clock: UnitClock { t0_ms: 100, unit_ms: 40, target_latency_ms: 40 }, lead_ms: vec![30] }), ..cfg() }, + steps: vec![base_at(0, 0), offer("L1_f0.wai", 120), offer("L1_f1.wai", 120)] }); + } + { + let a = small(69); + let (la, _) = l1_of(&a, 70, 4); + let (lb, _) = l1_of(&a, 71, 4); + out.push(Case { name: "hold_bound_count", about: "a bound of one held envelope: the first layer is held for its base, the second is base-absent on arrival", bases: vec![a], + files: vec![("L1a.wai".into(), la), ("L1b.wai".into(), lb)], cfg: ChainConfig { bound: HoldBound { max_envelopes: 1, max_age_ms: 1000 }, ..cfg() }, + steps: vec![offer("L1a.wai", 0), offer("L1b.wai", 1), base_at(0, 2)] }); + } + out +} + +// ───────────────────────── replay ───────────────────────── + +fn report_json(r: &OfferReport) -> Value { + json!({ + "object": hex(&r.content_hash), + "envelope": r.envelope.map(|x| x.code()), + "detail": r.detail, + "held_for_base": r.held_for_base, + "applied": r.applied, + "held": r.held, + "refused": r.refused.iter().map(|(u, w)| json!([u, w.code()])).collect::>(), + }) +} + +fn evidence_token(e: ReceiptEvidence) -> &'static str { + e.token() +} + +/// The inputs of a replay: the bases' infos and units, the files, the config, the steps. +struct Replay { + bases: Vec<(BaseInfo, Vec)>, + files: BTreeMap>, + cfg: ChainConfig, + steps: Vec, +} + +/// Run a replay: the reports of every step, and every base's final units. +fn run(r: &Replay) -> (Vec, Vec) { + let mut sink = StageSink::new(r.cfg.clone()); + let mut reports = Vec::new(); + for s in &r.steps { + let got = match s { + Step::Offer { file, at_ms, evidence } => sink.offer(&r.files[file], *at_ms, *evidence).expect("a WAI1 envelope"), + Step::Base { index, .. } => { + let (info, units) = r.bases[*index].clone(); + sink.add_base(info, units) + } + Step::Expire { at_ms } => sink.expire(*at_ms), + }; + reports.push(Value::Array(got.iter().map(report_json).collect())); + } + let finals = r + .bases + .iter() + .map(|(info, units)| match sink.chain(&info.content_hash) { + None => json!(null), + Some(c) => json!({ + "units": c.units().iter().enumerate().map(|(u, s)| json!({ + "stage": s.stage, + "producer": hex(&s.producer), + "frozen": s.frozen, + "tier": tier::token(s.tier), + "shape": [s.buf.shape.0, s.buf.shape.1, s.buf.shape.2], + "stage_sha256": c.unit_sha256(u as u32).map(|h| hex(&h)), + })).collect::>(), + "clip_sha256": c.clip_sha256().map(|h| hex(&h)), + "n_units": units.len(), + }), + }) + .collect(); + (reports, finals) +} + +fn step_json(s: &Step) -> Value { + match s { + Step::Offer { file, at_ms, evidence } => json!({"op": "offer", "file": file, "at_ms": at_ms, "evidence": evidence_token(*evidence)}), + Step::Base { index, at_ms } => json!({"op": "base", "index": index, "at_ms": at_ms}), + Step::Expire { at_ms } => json!({"op": "expire", "at_ms": at_ms}), + } +} + +fn cfg_json(c: &ChainConfig) -> Value { + json!({ + "bound": {"max_envelopes": c.bound.max_envelopes, "max_age_ms": c.bound.max_age_ms}, + "cost_limit": c.cost_limit.map(|l| json!({"output_bytes": l.output_bytes, "symbols": l.symbols, "working_bytes": l.working_bytes})), + "live": c.live.as_ref().map(|l| json!({"t0_ms": l.clock.t0_ms, "unit_ms": l.clock.unit_ms, "target_latency_ms": l.clock.target_latency_ms, "lead_ms": l.lead_ms})), + }) +} + +fn cfg_from(v: &Value) -> ChainConfig { + let u = |x: &Value| x.as_u64().expect("an integer"); + ChainConfig { + bound: HoldBound { max_envelopes: u(&v["bound"]["max_envelopes"]) as usize, max_age_ms: u(&v["bound"]["max_age_ms"]) }, + cost_limit: (!v["cost_limit"].is_null()).then(|| Wir1Cost { output_bytes: u(&v["cost_limit"]["output_bytes"]), symbols: u(&v["cost_limit"]["symbols"]), working_bytes: u(&v["cost_limit"]["working_bytes"]) }), + live: (!v["live"].is_null()).then(|| Live { + clock: UnitClock { t0_ms: u(&v["live"]["t0_ms"]), unit_ms: u(&v["live"]["unit_ms"]), target_latency_ms: u(&v["live"]["target_latency_ms"]) }, + lead_ms: v["live"]["lead_ms"].as_array().expect("leads").iter().map(u).collect(), + }), + } +} + +fn step_from(v: &Value) -> Step { + let at = v["at_ms"].as_u64().expect("at_ms"); + match v["op"].as_str().expect("op") { + "offer" => Step::Offer { file: v["file"].as_str().expect("file").into(), at_ms: at, evidence: ReceiptEvidence::from_token(v["evidence"].as_str().expect("evidence")).expect("an evidence token") }, + "base" => Step::Base { index: v["index"].as_u64().expect("index") as usize, at_ms: at }, + "expire" => Step::Expire { at_ms: at }, + o => panic!("unknown op {o}"), + } +} + +fn json_bytes(v: &Value) -> Vec { + let mut s = serde_json::to_string_pretty(v).expect("serialise"); + s.push('\n'); + s.into_bytes() +} + +/// Every file of the corpus, by path below the corpus directory. +fn generate(ctx: &Ctx) -> BTreeMap> { + let mut out = BTreeMap::new(); + let mut index = Vec::new(); + for c in cases(ctx) { + let dir = format!("cases/{}", c.name); + let mut bases_json = Vec::new(); + for (i, b) in c.bases.iter().enumerate() { + let suffix = if c.bases.len() == 1 { String::new() } else { format!("_{i}") }; + let file = format!("base{suffix}.wai"); + let s0 = format!("stage0{suffix}.bin"); + out.insert(format!("{dir}/{file}"), b.envelope.clone()); + let stage0 = b.stage0(); + let decode = match &b.decode { + Decode::Int { set } => json!({"kind": "int", "set": set.iter().map(|(m, p)| (m.to_string(), Value::from(p.clone()))).collect::>()}), + Decode::Png => json!({"kind": "png"}), + Decode::Flac => json!({"kind": "flac"}), + }; + bases_json.push(json!({ + "file": file, + "object": hex(&b.hash()), + "decoded_via": b.decoded_via, + "via_fallback": b.via_fallback, + "attested": b.attested, + "pin_resolved": b.pin_resolved, + "tier": tier::token(b.tier), + "units": b.units.iter().map(|u| json!({"shape": [u.shape.0, u.shape.1, u.shape.2], "elem": match u.samples { Samples::U8(_) => "u8", Samples::I16(_) => "i16" }})).collect::>(), + "stage0": s0, + "stage0_sha256": sha(&stage0), + "decode": decode, + })); + out.insert(format!("{dir}/{s0}"), stage0); + } + for (name, bytes) in &c.files { + out.insert(format!("{dir}/{name}"), bytes.clone()); + } + let replay = Replay { + bases: c.bases.iter().map(|b| (b.info(), b.units.clone())).collect(), + files: c.files.iter().cloned().collect(), + cfg: c.cfg.clone(), + steps: c.steps.clone(), + }; + let (reports, finals) = run(&replay); + let case = json!({ + "format": "wai-staged-case/1", + "name": c.name, + "about": c.about, + "bases": bases_json, + "files": c.files.iter().map(|(n, b)| json!({"file": n, "object": hex(&chash(b)), "bytes": b.len()})).collect::>(), + "config": cfg_json(&c.cfg), + "steps": c.steps.iter().map(step_json).collect::>(), + "expect": {"reports": reports, "bases": finals}, + }); + index.push(json!({"name": c.name, "about": c.about, "clip_sha256": finals.iter().map(|f| f.get("clip_sha256").cloned().unwrap_or(Value::Null)).collect::>()})); + out.insert(format!("{dir}/case.json"), json_bytes(&case)); + } + out.insert("schedule.json".into(), json_bytes(&schedule_json())); + out.insert( + "index.json".into(), + json_bytes(&json!({ + "format": "wai-staged-vectors/1", + "about": "The staged-delivery corpus (extensions/staged-delivery.md §12). Each case's case.json states its bases, layers, sink configuration and steps, and what a conforming sink reports and holds. Written by wai_staged_vectors gen.", + "cases": index, + })), + ); + out +} + +/// Arrival traces and what a sink presents for each (§8). +fn schedule_json() -> Value { + let c = UnitClock { t0_ms: 0, unit_ms: 40, target_latency_ms: 120 }; + let traces: Vec<(Vec, Vec>>, Vec)> = vec![ + (vec![0, 0, 0, 0], vec![vec![Some(10), Some(10), Some(10), Some(10)], vec![Some(20), Some(20), Some(20), Some(20)]], vec![20, 20]), + (vec![0, 0, 0, 0], vec![vec![Some(110), Some(110), Some(110), Some(110)], vec![Some(130), Some(130), Some(170), Some(240)]], vec![20, 20]), + (vec![0, 50, 200, 210], vec![vec![Some(0), None, Some(0), Some(0)], vec![Some(0), Some(0), None, Some(300)]], vec![10, 10]), + (vec![130, 130, 130], vec![vec![Some(100), Some(140), Some(190)]], vec![0]), + ]; + let out: Vec = traces + .iter() + .map(|(b, l, lead)| { + let p = present(&c, b, l, lead); + json!({ + "base_ms": b, + "layers_ms": l, + "lead_ms": lead, + "presented": p.iter().map(|x| json!({"at_ms": x.at_ms, "stage": x.stage, "expected": x.expected})).collect::>(), + "deadline_misses": deadline_misses(&p), + }) + }) + .collect(); + json!({"clock": {"t0_ms": c.t0_ms, "unit_ms": c.unit_ms, "target_latency_ms": c.target_latency_ms}, "traces": out}) +} + +fn wai_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("..") +} + +fn units_from_stage0(base: &Value, stage0: &[u8]) -> Vec { + let mut at = 0usize; + base["units"] + .as_array() + .expect("units") + .iter() + .map(|u| { + let s: Vec = u["shape"].as_array().expect("shape").iter().map(|x| x.as_u64().expect("dim") as u32).collect(); + let n = s[0] as usize * s[1] as usize * s[2] as usize; + let samples = match u["elem"].as_str().expect("elem") { + "u8" => { + let v = stage0[at..at + n].to_vec(); + at += n; + Samples::U8(v) + } + _ => { + let v = stage0[at..at + 2 * n].chunks_exact(2).map(|c| i16::from_le_bytes([c[0], c[1]])).collect(); + at += 2 * n; + Samples::I16(v) + } + }; + UnitBuf { shape: (s[0], s[1], s[2]), samples } + }) + .collect() +} + +fn verify(dir: &Path) -> Result<(), Vec> { + let ctx = Ctx { wai: wai_dir() }; + let mut fails = Vec::new(); + let mut fail = |m: String| { + eprintln!("FAIL {m}"); + fails.push(m); + }; + // 1. Byte-identical regeneration, and no committed file the generator does not write. + let regen = generate(&ctx); + for (rel, bytes) in ®en { + match std::fs::read(dir.join(rel)) { + Ok(b) if &b == bytes => {} + Ok(_) => fail(format!("{rel}: differs from its regeneration")), + Err(e) => fail(format!("{rel}: {e}")), + } + } + let mut committed = Vec::new(); + for sub in ["cases"] { + let root = dir.join(sub); + if let Ok(rd) = std::fs::read_dir(&root) { + for case in rd.flatten() { + for f in std::fs::read_dir(case.path()).into_iter().flatten().flatten() { + committed.push(format!("{sub}/{}/{}", case.file_name().to_string_lossy(), f.file_name().to_string_lossy())); + } + } + } + } + for rel in committed { + if !regen.contains_key(&rel) { + fail(format!("{rel}: committed but not generated")); + } + } + // 2. Replay every case from its committed files. + let mut n_cases = 0; + for rel in regen.keys().filter(|k| k.ends_with("/case.json")) { + n_cases += 1; + let cdir = dir.join(rel).parent().unwrap().to_path_buf(); + let case: Value = match std::fs::read(dir.join(rel)).ok().and_then(|b| serde_json::from_slice(&b).ok()) { + Some(v) => v, + None => { + fail(format!("{rel}: unreadable")); + continue; + } + }; + let mut bases = Vec::new(); + for b in case["bases"].as_array().expect("bases") { + let env = std::fs::read(cdir.join(b["file"].as_str().unwrap())).unwrap_or_default(); + let stage0 = std::fs::read(cdir.join(b["stage0"].as_str().unwrap())).unwrap_or_default(); + if sha(&stage0) != b["stage0_sha256"] { + fail(format!("{rel}: {} does not have its stated SHA-256", b["stage0"])); + } + if hex(&chash(&env)) != b["object"] { + fail(format!("{rel}: {} does not have its stated content hash", b["file"])); + } + let units = units_from_stage0(b, &stage0); + let via = b["decoded_via"].as_str().unwrap().to_string(); + // The base's own decode, where this build has the decoder. + match b["decode"]["kind"].as_str() { + Some("int") => { + let set: Vec<(String, String)> = b["decode"]["set"] + .as_object() + .unwrap() + .iter() + .map(|(m, p)| (m.clone(), p.as_str().unwrap().to_string())) + .collect(); + let w = Wai::from_bytes(&env).expect("a base envelope"); + let pin = w.prior().cloned(); + let decoded = decode_with(&ctx, &via, &w.payload, &set, pin.as_ref()); + if decoded != units { + fail(format!("{rel}: the base's decode is not stage0")); + } + let t = tier::base_tier(&via, if b["pin_resolved"] == true { pin.as_ref() } else { None }); + if tier::token(t) != b["tier"] { + fail(format!("{rel}: the base tier is {}, the case states {}", tier::token(t), b["tier"])); + } + } + Some(k) => println!(" {rel}: the {k} base is a stage-0 fixture in this build (staged_verify.py decodes it)"), + None => fail(format!("{rel}: no decode kind")), + } + let info = BaseInfo { + content_hash: chash(&env), + capability: via, + via_fallback: b["via_fallback"] == true, + tier: tier::from_token(b["tier"].as_str().unwrap()), + attested: b["attested"] == true, + }; + bases.push((info, units)); + } + let files: BTreeMap> = case["files"] + .as_array() + .unwrap() + .iter() + .map(|f| { + let n = f["file"].as_str().unwrap().to_string(); + (n.clone(), std::fs::read(cdir.join(&n)).unwrap_or_default()) + }) + .collect(); + let replay = Replay { bases, files, cfg: cfg_from(&case["config"]), steps: case["steps"].as_array().unwrap().iter().map(step_from).collect() }; + let (reports, finals) = run(&replay); + if Value::Array(reports) != case["expect"]["reports"] { + fail(format!("{rel}: the reports differ from the case's")); + } + if Value::Array(finals) != case["expect"]["bases"] { + fail(format!("{rel}: the final units differ from the case's")); + } + } + // 3. The schedule. + let sched: Value = std::fs::read(dir.join("schedule.json")).ok().and_then(|b| serde_json::from_slice(&b).ok()).unwrap_or(Value::Null); + if sched != schedule_json() { + fail("schedule.json: the traces do not replay to the stated presentation".into()); + } + println!("{n_cases} cases replayed"); + if fails.is_empty() { Ok(()) } else { Err(fails) } +} + +fn main() -> ExitCode { + let args: Vec = std::env::args().collect(); + let (Some(cmd), Some(dir)) = (args.get(1), args.get(2)) else { + eprintln!("usage: wai_staged_vectors gen|verify "); + return ExitCode::from(2); + }; + let dir = PathBuf::from(dir); + match cmd.as_str() { + "gen" => { + let ctx = Ctx { wai: wai_dir() }; + for (rel, bytes) in generate(&ctx) { + let p = dir.join(&rel); + std::fs::create_dir_all(p.parent().unwrap()).expect("mkdir"); + std::fs::write(&p, bytes).expect("write"); + } + println!("wrote {}", dir.display()); + ExitCode::SUCCESS + } + "verify" => match verify(&dir) { + Ok(()) => { + println!("staged-conformance: OK"); + ExitCode::SUCCESS + } + Err(f) => { + eprintln!("staged-conformance: {} failure(s)", f.len()); + ExitCode::from(1) + } + }, + _ => { + eprintln!("usage: wai_staged_vectors gen|verify "); + ExitCode::from(2) + } + } +} diff --git a/wai/wai-rs/src/staged/mod.rs b/wai/wai-rs/src/staged/mod.rs index 16f041b75..720fe6ba7 100644 --- a/wai/wai-rs/src/staged/mod.rs +++ b/wai/wai-rs/src/staged/mod.rs @@ -229,10 +229,15 @@ impl OfferReport { fn refuse(content_hash: [u8; 32], why: StageRefusal, detail: Option<&'static str>) -> Self { OfferReport { envelope: Some(why), detail, ..Self::new(content_hash) } } + /// Units in order; a unit held and then decided within the same step is reported + /// by its decision alone. fn sort(&mut self) { self.applied.sort_unstable(); - self.held.sort_unstable(); self.refused.sort_unstable(); + let (applied, refused) = (&self.applied, &self.refused); + self.held.retain(|u| !applied.contains(u) && !refused.iter().any(|(x, _)| x == u)); + self.held.sort_unstable(); + self.held.dedup(); } } -- 2.54.0 (Apple Git-157) From 24fcf4db3c2ab930f2092623049dfaabbdbf58a7 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 2 Oct 2026 00:34:05 -0400 Subject: [PATCH 07/36] wai: WAI2 refinement components, explode and pack, on the component identity A WAI2 entry with role "refinement" carries refines whose base and producers are the ids of entries in the same envelope, and is placed after every selectable entry. check_component_roles holds it to SPEC 7.1's rules in a stated order: refinement-only capability, id and sha256 on it and on what it names, well-formed refines, a selectable base, admission, producers that are selectable or earlier refinements, and placement; refines on any other entry, and a refinement-only capability in any other role, are reported. A refinement is never selectable; refinements_of gives a picked entry's refinements whose payloads match their sha256. WaiMulti::explode_all and explode write each component's WAI1 envelope: the entry's envelope_manifest verbatim when it has one, or a manifest built from the WAI2 manifest's members as written, read as raw text so nothing is re-serialised, with the ids of refines replaced by the content hashes of the named entries' exploded envelopes. So an exploded refinement is the object a separately carried layer is. WaiMulti::pack packs WAI1 envelopes with their manifests verbatim, and they explode back to the same bytes; check_envelope_manifests reports an envelope_manifest that does not match its entry. No separate component digest is defined: the component identity is the entry's id, sha256 and prior. SPEC 7.1 states exploding, envelope_manifest and refinement components; SPEC 8 lists the new optional members and states that a malformed one does not make the manifest unreadable. serde_json gains raw_value. Co-Authored-By: Claude Opus 5.5 --- wai/SPEC.md | 61 +++++++ wai/wai-rs/Cargo.toml | 2 +- wai/wai-rs/src/container.rs | 129 ++++++++++++- wai/wai-rs/src/container_explode.rs | 271 ++++++++++++++++++++++++++++ wai/wai-rs/src/lib.rs | 5 + wai/wai-rs/src/staged/tests.rs | 148 +++++++++++++++ 6 files changed, 613 insertions(+), 3 deletions(-) create mode 100644 wai/wai-rs/src/container_explode.rs diff --git a/wai/SPEC.md b/wai/SPEC.md index 2ffc0f0db..90da97872 100644 --- a/wai/SPEC.md +++ b/wai/SPEC.md @@ -1680,6 +1680,29 @@ by which capability happens to be installed. `companion_of_id` is not a string (`null` is the same as absent) makes the manifest malformed, as a mistyped pin member does (§3.1), and the reader rejects the envelope. +- **Exploding a component.** The exploded envelope of entry *i* is the `WAI1` + envelope (§2) whose payload is entry *i*'s rendition bytes. Its manifest is the + entry's `envelope_manifest` when it carries one, written exactly as it is in the + `WAI2` manifest; otherwise it is + `{"wai":W,"media":M,"intent":I,"model_requirement":{"capability":C,"fallback":null},"conditioning":{"kind":K[,"prior":P]}[,"security":S][,"refines":R][,"target":T]}`, + with no other bytes, where `W`, `M`, `I`, `S` and `T` are the `WAI2` manifest's + `wai`, `media`, `intent`, `security` and `target`, and `C`, `K` and `P` the entry's + `capability`, `kind` and `prior`, each written exactly as it is in the `WAI2` + manifest; `R` is the entry's `refines` (*Refinement components*, below) with each + component id replaced by the content hash of that entry's exploded envelope, written + as [staged-delivery](extensions/staged-delivery.md) §3 orders its members (`base`, + `prev`, `layer`, `span`; a run's `producer`, then `count`) with no whitespace. A + bracketed part is present exactly when its member is. A component and its exploded + envelope are the same object for every rule that names an object by content hash + ([jwp-receipts](extensions/jwp-receipts.md) §2.1). An entry MAY carry + `envelope_manifest`, a JSON object: the exact manifest of the entry as a `WAI1` + envelope. Its `model_requirement.capability`, `conditioning.kind` and + `conditioning.prior` MUST equal the entry's, and its `refines` MUST be the entry's + with each id replaced as above, or both absent (`component-manifest-mismatch`). A + `WAI1` envelope packed into a `WAI2` with its manifest copied verbatim into + `envelope_manifest` explodes back to the same bytes, and so keeps its content hash. + A tool that re-serialises a `WAI2` manifest may change these bytes, and with them + the content hashes of the exploded envelopes. - Rendition table: `u16 n_renditions | n × (u32 offset, u32 length)`, offsets relative to the start of the payloads block. The table's i-th entry corresponds to `renditions[i]` in the manifest. @@ -1793,6 +1816,37 @@ does not recognise; a companion that breaks rule 3 binds to nothing, or, for an enhancement layer, is handled as §5 states. The sink MAY still present the rendition it picked. +### Refinement components + +A component with `role: "refinement"` is a layer of a stage set +([staged-delivery](extensions/staged-delivery.md)). It carries `refines` (§3), whose +`base` and producers are the `id`s of entries in the same envelope (§7.1, *Component +identity*); its exploded envelope carries the same `refines` with those ids replaced +by the content hashes of the named entries' exploded envelopes (*Exploding a +component*, above), so it is the same object as the layer carried on its own. + +- **Placement.** A refinement component MUST follow every selectable entry in + `renditions`. +- **Selection.** Selection never picks one (step 2: it is not selectable). A sink + picks a rendition as above, then offers to staged-delivery §4, in `renditions` + order, the exploded envelopes of the refinements whose `refines.base` is the picked + entry's `id`, up to the stage its deployer policy chooses. A refinement whose + payload does not match its `sha256` is not offered. +- **Conformance.** A refinement component conforms only if, in this order: its + capability is refinement-only (§5); it carries `id` and `sha256`; its `refines` + is well formed (staged-delivery §3, with ids in place of hashes); `refines.base` + names a selectable entry, which carries `id` and `sha256`; its capability admits + that entry's (§5, *Base admission*); every producer in `prev` names a selectable + entry or a refinement before it, each carrying `id` and `sha256`; and no selectable + entry follows it. No other component carries `refines`, and a refinement-only + capability is carried with no other role. The reference + `WaiMulti::check_component_roles` reports the first break. + +A sink of an earlier revision does not advertise a refinement-only capability, so it +never picks a refinement by capability; and since refinements follow every selectable +entry, a sink that picks the first entry it can decode picks a base. If it picks one +by another policy, it finds the capability unsupported and the component inert. + ### v1.0 compatibility v1.0 envelopes (`WAI1`) remain valid and their format is unchanged, except that @@ -1883,6 +1937,13 @@ envelope that uses any string a later revision registers. then, does not parse now; §3.1 lists it. Nothing in the envelope signals the change: its `wai` value is the same, so a reader of this revision rejects the whole envelope where an earlier reader ignored the member. +- This revision also defines the manifest's `refines` (§3) and a rendition entry's + `refines` and `envelope_manifest` (§7.1) as OPTIONAL members under the MINOR rule + above; envelopes that use them keep their `wai` value. The exception does not cover + them: an envelope whose `refines` is malformed still parses, and + [staged-delivery](extensions/staged-delivery.md) §4 reports it + (`refines-malformed`); one whose `envelope_manifest` does not match its entry still + parses, and §7.1 reports it (`component-manifest-mismatch`). - New capabilities (§5) MAY be added in any MINOR revision. Existing capabilities MUST NOT be redefined — once a capability string is registered for a codec, its payload format is fixed forever; for a registration diff --git a/wai/wai-rs/Cargo.toml b/wai/wai-rs/Cargo.toml index 058c9e68c..3c33d4bc0 100644 --- a/wai/wai-rs/Cargo.toml +++ b/wai/wai-rs/Cargo.toml @@ -30,7 +30,7 @@ unexpected_cfgs = { level = "warn", check-cfg = ["cfg(kani)"] } [dependencies] # Container manifest. preserve_order so JSON round-trips byte-identical. serde = { version = "1", features = ["derive"] } -serde_json = { version = "1", features = ["preserve_order"] } +serde_json = { version = "1", features = ["preserve_order", "raw_value"] } # ---- zeroth codec wrappers: lossy + lossless across all media ---- # All optional behind the (default-on) `codecs` feature: native sinks get diff --git a/wai/wai-rs/src/container.rs b/wai/wai-rs/src/container.rs index f069be124..885faca8a 100644 --- a/wai/wai-rs/src/container.rs +++ b/wai/wai-rs/src/container.rs @@ -1425,6 +1425,18 @@ pub struct RenditionMeta { /// binds by capability. #[serde(default, skip_serializing_if = "is_none")] pub companion_of_id: Option, + /// For a refinement (`role: "refinement"`): what it refines + /// (`extensions/staged-delivery.md` §3), its base and producers named by the `id` + /// of entries in this envelope (SPEC §7.1, *Refinement components*). Kept as the + /// JSON it is, so a malformed value still parses ([`Refines::from_value`] with + /// [`RefForm::ComponentId`] reports it). + #[serde(default, skip_serializing_if = "is_none")] + pub refines: Option, + /// The exact manifest of this entry as a `WAI1` envelope (SPEC §7.1, *Exploding a + /// component*): what [`WaiMulti::explode`] writes as the exploded envelope's + /// manifest, verbatim from the envelope bytes. + #[serde(default, skip_serializing_if = "is_none")] + pub envelope_manifest: Option, /// Members this revision does not define, kept so they survive a round trip. #[serde(flatten)] pub extra: Map, @@ -1443,6 +1455,8 @@ impl RenditionMeta { role: None, companion_of: None, companion_of_id: None, + refines: None, + envelope_manifest: None, extra: Map::new(), } } @@ -1484,6 +1498,11 @@ impl RenditionMeta { self.role.as_deref() == Some("companion") } + /// Whether this component is a refinement (`role: "refinement"`, SPEC §7.1). + pub fn is_refinement(&self) -> bool { + self.role.as_deref() == Some("refinement") + } + /// Whether `pick`/`primary` may select this component: it is not marked a /// companion **and** [`crate::codecs::is_dispatchable`] accepts its /// capability — so not a companion-only capability, a candidate, a record or @@ -1492,7 +1511,7 @@ impl RenditionMeta { /// own. [`WaiMulti::check_component_roles`] reports the mislabelling, and /// [`WaiMulti::check_candidates`] a candidate. pub fn is_selectable(&self) -> bool { - !self.is_companion() && crate::codecs::is_dispatchable(&self.capability) + !self.is_companion() && !self.is_refinement() && crate::codecs::is_dispatchable(&self.capability) } /// Whether `payload` matches this entry's stated `sha256` (true when none is @@ -1561,6 +1580,22 @@ pub enum RoleError { RefinesMalformed { index: Option, detail: &'static str }, /// v1.1: a refinement-only capability carried as a rendition or a companion. RefinementOnlyAsComponent { index: usize, capability: String }, + /// v1.1: `refines` on an entry whose role is not `"refinement"`. + RefinesOnNonRefinement { index: usize }, + /// v1.1: a `role: "refinement"` entry whose capability is not refinement-only. + RefinementNotRefinementOnly { index: usize, capability: String }, + /// v1.1: a refinement, or an entry it names, without an `id` or a `sha256`. + RefinementIdentityMissing { index: usize }, + /// v1.1: a refinement whose `refines.base` names no selectable entry. + RefinementBaseMissing { index: usize, base: String }, + /// v1.1: a refinement whose capability does not admit its base entry's (SPEC §5, + /// *Base admission*). + RefinementBaseNotAdmitted { index: usize, capability: String, base: String }, + /// v1.1: a producer in a refinement's `prev` that names neither a selectable entry + /// nor a refinement before it. + RefinementPrevMissing { index: usize, producer: String }, + /// v1.1: a refinement placed before a selectable entry. + RefinementBeforeSelectable { index: usize, selectable: usize }, } impl std::fmt::Display for RoleError { @@ -1611,6 +1646,20 @@ impl std::fmt::Display for RoleError { RoleError::RefinesMalformed { index: Some(i), detail } => write!(f, "component {i}: refines-malformed: {detail}"), RoleError::RefinementOnlyAsComponent { index, capability } => write!( f, "component {index} ({capability}) is refinement-only and is not carried with role=\"refinement\""), + RoleError::RefinesOnNonRefinement { index } => write!( + f, "component {index} carries refines but its role is not \"refinement\""), + RoleError::RefinementNotRefinementOnly { index, capability } => write!( + f, "component {index} has role=\"refinement\" but {capability} is not refinement-only"), + RoleError::RefinementIdentityMissing { index } => write!( + f, "refinement {index}, or an entry it names, carries no id or no sha256 (SPEC \u{a7}7.1)"), + RoleError::RefinementBaseMissing { index, base } => write!( + f, "refinement {index} names base '{base}', which is no selectable entry"), + RoleError::RefinementBaseNotAdmitted { index, capability, base } => write!( + f, "refinement {index} ({capability}) does not apply over {base} (SPEC \u{a7}5, Base admission)"), + RoleError::RefinementPrevMissing { index, producer } => write!( + f, "refinement {index} names producer '{producer}', which is neither a selectable entry nor a refinement before it"), + RoleError::RefinementBeforeSelectable { index, selectable } => write!( + f, "refinement {index} precedes selectable entry {selectable}; refinements follow every selectable entry"), } } } @@ -2288,6 +2337,71 @@ impl WaiMulti { } } + /// SPEC §7.1, *Refinement components*, for the entry at `index`, in this order: + /// its capability is refinement-only; it carries `id` and `sha256`; its `refines` + /// is well formed with component ids; `refines.base` names a selectable entry with + /// an `id` and a `sha256`; its capability admits that entry's (SPEC §5, *Base + /// admission*); every producer of `prev` names a selectable entry or a refinement + /// before it, with an `id` and a `sha256`; and it follows every selectable entry. + fn check_refinement(&self, index: usize) -> Result<(), RoleError> { + let metas = &self.manifest.renditions; + let m = &metas[index]; + if !crate::codecs::is_refinement_only(&m.capability) { + return Err(RoleError::RefinementNotRefinementOnly { index, capability: m.capability.clone() }); + } + if m.id.is_none() || m.sha256.is_none() { + return Err(RoleError::RefinementIdentityMissing { index }); + } + let refines = match m.refines.as_ref().map(|v| Refines::from_value(v, RefForm::ComponentId)) { + Some(Ok(r)) => r, + Some(Err(e)) => return Err(RoleError::RefinesMalformed { index: Some(index), detail: e.0 }), + None => return Err(RoleError::RefinesMalformed { index: Some(index), detail: "a refinement carries no refines" }), + }; + let by_id = |id: &str| metas.iter().position(|e| e.id.as_deref() == Some(id)); + let Some(b) = by_id(&refines.base).filter(|&b| metas[b].is_selectable()) else { + return Err(RoleError::RefinementBaseMissing { index, base: refines.base.clone() }); + }; + if metas[b].sha256.is_none() { + return Err(RoleError::RefinementIdentityMissing { index }); + } + if !crate::codecs::admits(&m.capability, &metas[b].capability) { + return Err(RoleError::RefinementBaseNotAdmitted { index, capability: m.capability.clone(), base: metas[b].capability.clone() }); + } + for p in refines.producers() { + let ok = by_id(p).filter(|&e| metas[e].is_selectable() || (e < index && metas[e].is_refinement())); + match ok { + None => return Err(RoleError::RefinementPrevMissing { index, producer: p.to_string() }), + Some(e) if metas[e].sha256.is_none() => return Err(RoleError::RefinementIdentityMissing { index }), + Some(_) => {} + } + } + if let Some(selectable) = (index + 1..metas.len()).find(|&j| metas[j].is_selectable()) { + return Err(RoleError::RefinementBeforeSelectable { index, selectable }); + } + Ok(()) + } + + /// The refinements of the entry at `index`, in table order: each entry with + /// `role: "refinement"` whose `refines.base` names that entry's `id`, and whose + /// payload matches its `sha256` (a refinement that does not match is not applied, + /// SPEC §7.1). A sink offers their exploded envelopes to staged-delivery §4 + /// ([`WaiMulti::explode`]) after [`Self::check_component_roles`] passes. + pub fn refinements_of(&self, index: usize) -> Vec<(usize, &RenditionMeta, &[u8])> { + let metas = &self.manifest.renditions; + let Some(base_id) = metas.get(index).and_then(|b| b.id.as_deref()) else { return Vec::new() }; + metas + .iter() + .zip(self.renditions.iter()) + .enumerate() + .filter(|(_, (m, p))| { + m.is_refinement() + && m.payload_digest_matches(p) + && m.refines.as_ref().and_then(|v| Refines::from_value(v, RefForm::ComponentId).ok()).is_some_and(|r| r.base == base_id) + }) + .map(|(i, (m, p))| (i, m, p.as_slice())) + .collect() + } + /// Check the component table against SPEC §5 / §7.1, using the capability /// registry, in component order (the first violation is returned): /// @@ -2295,6 +2409,10 @@ impl WaiMulti { /// ([`RoleError::ComponentIdMalformed`], [`RoleError::DuplicateComponentId`]); /// - a companion-only capability is carried with `role = "companion"` /// ([`RoleError::CompanionOnlyAsRendition`]); + /// - a `role = "refinement"` entry obeys *Refinement components* + /// ([`Self::check_refinement`]'s rules); `refines` sits on no other entry, and a + /// refinement-only capability on no other role + /// ([`RoleError::RefinesOnNonRefinement`], [`RoleError::RefinementOnlyAsComponent`]); /// /// and, for each `role = "companion"` component, SPEC §7.1's three rules: /// @@ -2329,7 +2447,14 @@ impl WaiMulti { } for (index, m) in metas.iter().enumerate() { let capability = m.capability.clone(); - if crate::codecs::is_refinement_only(&m.capability) && m.role.as_deref() != Some("refinement") { + if m.is_refinement() { + self.check_refinement(index)?; + continue; + } + if m.refines.is_some() { + return Err(RoleError::RefinesOnNonRefinement { index }); + } + if crate::codecs::is_refinement_only(&m.capability) { return Err(RoleError::RefinementOnlyAsComponent { index, capability }); } let companion_only = crate::codecs::is_companion_only(&m.capability); diff --git a/wai/wai-rs/src/container_explode.rs b/wai/wai-rs/src/container_explode.rs new file mode 100644 index 000000000..70d6bbff4 --- /dev/null +++ b/wai/wai-rs/src/container_explode.rs @@ -0,0 +1,271 @@ +//! Exploding a `WAI2` component into its `WAI1` envelope, and packing `WAI1` envelopes +//! into a `WAI2` (SPEC §7.1, *Exploding a component*; *Refinement components*). +//! +//! A component and its exploded envelope are the same object for every rule that +//! names an object by content hash, so a refinement carried in a `WAI2` and the same +//! layer carried alone reach the same stages (`extensions/staged-delivery.md` §9). +//! Explode reads the `WAI2` manifest's members as the exact text they are in the +//! envelope (`serde_json::value::RawValue`), never re-serialised, so a component packed +//! with its manifest in `envelope_manifest` explodes back to the bytes it was packed +//! from. Identity is the entry's `id`, `sha256` and `prior` (SPEC §7.1, *Component +//! identity*): a refinement entry names its base and producers by `id`, and its +//! exploded envelope names them by the content hashes of their exploded envelopes. + +use std::collections::BTreeMap; + +use serde::Deserialize; +use serde_json::value::RawValue; +use serde_json::Value; + +use crate::container::{is_component_id, manifest_bytes, Prev, PrevRun, RefForm, Refines, Wai, WaiMulti, MAGIC_V2}; + +fn bad(m: impl Into) -> std::io::Error { + std::io::Error::new(std::io::ErrorKind::InvalidData, m.into()) +} + +#[derive(Deserialize)] +struct RawV2<'a> { + #[serde(borrow)] + wai: &'a RawValue, + #[serde(borrow)] + media: &'a RawValue, + #[serde(borrow)] + intent: &'a RawValue, + #[serde(borrow)] + renditions: Vec>, + #[serde(default, borrow)] + target: Option<&'a RawValue>, + #[serde(default, borrow)] + security: Option<&'a RawValue>, +} + +#[derive(Deserialize)] +struct RawEntry<'a> { + #[serde(borrow)] + capability: &'a RawValue, + #[serde(borrow)] + kind: &'a RawValue, + #[serde(default, borrow)] + prior: Option<&'a RawValue>, + #[serde(default, borrow)] + envelope_manifest: Option<&'a RawValue>, +} + +/// A `WAI1` envelope with manifest text `manifest` and `payload`. +fn wai1(manifest: &[u8], payload: &[u8]) -> Vec { + let mut out = Vec::with_capacity(12 + manifest.len() + payload.len()); + out.extend_from_slice(crate::container::MAGIC); + out.extend_from_slice(&(manifest.len() as u32).to_le_bytes()); + out.extend_from_slice(manifest); + out.extend_from_slice(&(payload.len() as u32).to_le_bytes()); + out.extend_from_slice(payload); + out +} + +/// `refines` with each reference translated by `map`, written in the form +/// [`Refines::to_value`] gives it, without whitespace. +fn translate(r: &Refines, map: &dyn Fn(&str) -> Option) -> std::io::Result { + let tr = |s: &str| map(s).ok_or_else(|| bad(format!("refines names '{s}', which no entry before it is"))); + Ok(Refines { + base: tr(&r.base)?, + prev: match &r.prev { + Prev::One(p) => Prev::One(tr(p)?), + Prev::Runs(runs) => Prev::Runs(runs.iter().map(|x| Ok(PrevRun { producer: tr(&x.producer)?, count: x.count })).collect::>()?), + }, + layer: r.layer, + span: r.span, + }) +} + +fn hex(h: &[u8; 32]) -> String { + h.iter().map(|b| format!("{b:02x}")).collect() +} + +impl WaiMulti { + /// Every component of the `WAI2` envelope `envelope` as its exploded `WAI1` + /// envelope, in table order (SPEC §7.1, *Exploding a component*): the payload is the + /// entry's rendition bytes; the manifest is the entry's `envelope_manifest` exactly + /// as written, or else + /// `{"wai":W,"media":M,"intent":I,"model_requirement":{"capability":C,"fallback":null},` + /// `"conditioning":{"kind":K[,"prior":P]}[,"security":S][,"refines":R][,"target":T]}`, + /// where `W`, `M`, `I`, `S` and `T` are the `WAI2` manifest's members as written, `C`, + /// `K` and `P` the entry's, and `R` the entry's `refines` with each component id + /// replaced by the content hash of that entry's exploded envelope. An entry a + /// refinement names must come before it. + pub fn explode_all(envelope: &[u8]) -> std::io::Result>> { + let w = WaiMulti::from_bytes(envelope)?; + let mtext = manifest_bytes(envelope)?; + let raw: RawV2 = serde_json::from_slice(mtext).map_err(|e| bad(format!("WAI2 manifest: {e}")))?; + if raw.renditions.len() != w.manifest.renditions.len() { + return Err(bad("the manifest's entries do not read alike")); + } + let mut ids: BTreeMap = BTreeMap::new(); + let mut out = Vec::with_capacity(raw.renditions.len()); + for (i, (e, meta)) in raw.renditions.iter().zip(&w.manifest.renditions).enumerate() { + let payload = &w.renditions[i]; + let manifest: Vec = match e.envelope_manifest { + Some(m) => m.get().as_bytes().to_vec(), + None => { + let mut s = String::new(); + s.push_str("{\"wai\":"); + s.push_str(raw.wai.get()); + s.push_str(",\"media\":"); + s.push_str(raw.media.get()); + s.push_str(",\"intent\":"); + s.push_str(raw.intent.get()); + s.push_str(",\"model_requirement\":{\"capability\":"); + s.push_str(e.capability.get()); + s.push_str(",\"fallback\":null},\"conditioning\":{\"kind\":"); + s.push_str(e.kind.get()); + if let Some(p) = e.prior { + s.push_str(",\"prior\":"); + s.push_str(p.get()); + } + s.push('}'); + if let Some(sec) = raw.security { + s.push_str(",\"security\":"); + s.push_str(sec.get()); + } + if let Some(v) = meta.refines.as_ref() { + let r = Refines::from_value(v, RefForm::ComponentId).map_err(|e| bad(format!("entry {i}: {e}")))?; + let t = translate(&r, &|id| ids.get(id).cloned())?; + s.push_str(",\"refines\":"); + s.push_str(&serde_json::to_string(&t.to_value()).map_err(|e| bad(e.to_string()))?); + } + if let Some(t) = raw.target { + s.push_str(",\"target\":"); + s.push_str(t.get()); + } + s.push('}'); + s.into_bytes() + } + }; + let env = wai1(&manifest, payload); + if let Some(id) = meta.id.as_deref() { + ids.insert(id.to_string(), hex(&crate::jwp_hash::content_hash(&env))); + } + out.push(env); + } + Ok(out) + } + + /// Component `index` of the `WAI2` envelope `envelope` as its exploded `WAI1` + /// envelope ([`Self::explode_all`]). + pub fn explode(envelope: &[u8], index: usize) -> std::io::Result> { + Self::explode_all(envelope)?.into_iter().nth(index).ok_or_else(|| bad(format!("no component {index}"))) + } + + /// Pack `WAI1` envelopes into one `WAI2`, in order: each entry carries the + /// envelope's capability, kind and pin, the id `c`, its payload's `sha256`, and + /// its manifest verbatim as `envelope_manifest`; an entry whose capability is + /// refinement-only takes `role: "refinement"` and its `refines` with each content + /// hash replaced by the id of the entry whose envelope has it. `wai`, `media`, + /// `intent` and `target` come from the first envelope. So every envelope explodes + /// back to its own bytes ([`Self::explode_all`]). An encrypted envelope, or a + /// refinement whose base or producers are not among the envelopes before it, is + /// refused. + pub fn pack(w1s: &[&[u8]]) -> std::io::Result> { + if w1s.is_empty() { + return Err(bad("nothing to pack")); + } + let mut entries = Vec::new(); + let mut payloads: Vec> = Vec::new(); + let mut by_hash: BTreeMap = BTreeMap::new(); + let mut first: Option<(String, String, String)> = None; + for (i, b) in w1s.iter().enumerate() { + let w = Wai::from_bytes(b)?; + if w.manifest.security.as_ref().is_some_and(|s| !s.is_plaintext()) { + return Err(bad(format!("envelope {i} is encrypted"))); + } + let mtext = std::str::from_utf8(manifest_bytes(b)?).map_err(|e| bad(e.to_string()))?; + let id = format!("c{i}"); + debug_assert!(is_component_id(&id)); + let mut e = serde_json::Map::new(); + e.insert("capability".into(), Value::from(w.capability())); + e.insert("kind".into(), Value::from(w.kind())); + e.insert("id".into(), Value::from(id.clone())); + e.insert("sha256".into(), Value::from(crate::pin::sha256_hex(&w.payload))); + if let Some(p) = &w.manifest.conditioning.prior { + e.insert("prior".into(), serde_json::to_value(p).map_err(|x| bad(x.to_string()))?); + } + if crate::codecs::is_refinement_only(w.capability()) { + e.insert("role".into(), Value::from("refinement")); + let r = w.refines().ok_or_else(|| bad(format!("envelope {i} is a refinement with no refines")))?.map_err(|x| bad(x.to_string()))?; + let t = translate(&r, &|h| by_hash.get(h).cloned())?; + e.insert("refines".into(), t.to_value()); + } + let mut entry = serde_json::to_string(&Value::Object(e)).map_err(|x| bad(x.to_string()))?; + entry.pop(); + entry.push_str(",\"envelope_manifest\":"); + entry.push_str(mtext); + entry.push('}'); + entries.push(entry); + if first.is_none() { + let tv = |v: &Value| serde_json::to_string(v).unwrap_or_else(|_| "null".into()); + first = Some((tv(&Value::from(w.media())), tv(&Value::from(w.intent())), tv(&w.manifest.target))); + } + by_hash.insert(hex(&crate::jwp_hash::content_hash(b)), id); + payloads.push(w.payload); + } + let (media, intent, target) = first.expect("one envelope"); + let manifest = format!("{{\"wai\":\"1.1\",\"media\":{media},\"intent\":{intent},\"renditions\":[{}],\"target\":{target}}}", entries.join(",")); + if payloads.len() > u16::MAX as usize { + return Err(bad("too many components")); + } + let mut out = Vec::new(); + out.extend_from_slice(MAGIC_V2); + out.extend_from_slice(&(manifest.len() as u32).to_le_bytes()); + out.extend_from_slice(manifest.as_bytes()); + out.extend_from_slice(&(payloads.len() as u16).to_le_bytes()); + let mut off = 0u32; + for p in &payloads { + out.extend_from_slice(&off.to_le_bytes()); + out.extend_from_slice(&(p.len() as u32).to_le_bytes()); + off = off.checked_add(p.len() as u32).ok_or_else(|| bad("payloads past u32"))?; + } + for p in &payloads { + out.extend_from_slice(p); + } + Ok(out) + } + + /// Check every entry's `envelope_manifest` against the entry (SPEC §7.1): it is a + /// `WAI1` manifest whose `model_requirement.capability`, `conditioning.kind` and + /// `conditioning.prior` are the entry's, and whose `refines` is the entry's with each + /// id replaced by the content hash of that entry's exploded envelope (or both are + /// absent). The first entry that fails, with `component-manifest-mismatch`. + pub fn check_envelope_manifests(envelope: &[u8]) -> Result<(), (usize, &'static str)> { + const CODE: &str = "component-manifest-mismatch"; + let w = WaiMulti::from_bytes(envelope).map_err(|_| (0, CODE))?; + let exploded = Self::explode_all(envelope).map_err(|_| (0, CODE))?; + let mut ids: BTreeMap = BTreeMap::new(); + for (i, (meta, env)) in w.manifest.renditions.iter().zip(&exploded).enumerate() { + if let Some(m) = &meta.envelope_manifest { + let mm: crate::container::Manifest = serde_json::from_value(m.clone()).map_err(|_| (i, CODE))?; + let as_json = |p: Option<&crate::container::Prior>| p.map(|p| serde_json::to_value(p).ok()); + let prior_eq = as_json(mm.conditioning.prior.as_ref()) == as_json(meta.prior.as_ref()); + if mm.model_requirement.capability != meta.capability || mm.conditioning.kind != meta.kind || !prior_eq { + return Err((i, CODE)); + } + let entry_refines = match &meta.refines { + None => None, + Some(v) => { + let r = Refines::from_value(v, RefForm::ComponentId).map_err(|_| (i, CODE))?; + Some(translate(&r, &|id| ids.get(id).cloned()).map_err(|_| (i, CODE))?) + } + }; + let wai1_refines = match &mm.refines { + None => None, + Some(v) => Some(Refines::from_value(v, RefForm::ContentHash).map_err(|_| (i, CODE))?), + }; + if entry_refines != wai1_refines { + return Err((i, CODE)); + } + } + if let Some(id) = meta.id.as_deref() { + ids.insert(id.to_string(), hex(&crate::jwp_hash::content_hash(env))); + } + } + Ok(()) + } +} diff --git a/wai/wai-rs/src/lib.rs b/wai/wai-rs/src/lib.rs index 8b3bbec37..5f67d439b 100644 --- a/wai/wai-rs/src/lib.rs +++ b/wai/wai-rs/src/lib.rs @@ -299,6 +299,11 @@ pub mod c2pa_emit; #[cfg(feature = "jwp_hash")] pub mod jwp_hash; +// SPEC §7.1: a WAI2 component exploded into its WAI1 envelope, WAI1 envelopes packed +// into a WAI2, and the check of an entry's envelope_manifest. Content hashes need BLAKE3. +#[cfg(feature = "jwp_hash")] +mod container_explode; + // Staged delivery (extensions/staged-delivery.md): a base first, then refinements as // their own objects, bound to what they refine by content hash; the residual payload // WIR1, its predictors, the per-unit application algorithm and the live schedule. diff --git a/wai/wai-rs/src/staged/tests.rs b/wai/wai-rs/src/staged/tests.rs index c89cddde5..5d833f661 100644 --- a/wai/wai-rs/src/staged/tests.rs +++ b/wai/wai-rs/src/staged/tests.rs @@ -740,3 +740,151 @@ fn base_never_waits() { } } } + +// ───────────────────────── WAI2 identity (SPEC §7.1) ───────────────────────── + +use crate::container::{Rendition, RenditionMeta, RoleError, WaiMulti}; + +/// A small chain: a base envelope, its frames, layer 1 and layer 2. +fn chain3() -> (Vec, Vec, Vec, Vec) { + let base = envelope(CAP_VIDEO_INT_MOTION, None, b"base payload".to_vec()); + let b = h(&base); + let units: Vec = (0..4).map(|f| UnitBuf { shape: (H, W, 3), samples: Samples::U8(frame(f)) }).collect(); + let below: Vec> = (0..4).map(frame).collect(); + let l1 = layer(&b, one(&b), 1, None, &below, 50); + let mut c = StageChain::new(BaseInfo { content_hash: b, capability: CAP_VIDEO_INT_MOTION.into(), via_fallback: false, tier: Some(DeterminismTier::DecodeEquivalence), attested: false }, units.clone(), cfg()); + c.offer(&l1, 0, ReceiptEvidence::None).unwrap(); + let mid: Vec> = (0..4).map(|f| unit_frame(&c, f)).collect(); + let l2 = layer(&b, Prev::One(hexs(&h(&l1))), 2, None, &mid, 60); + (base, units, l1, l2) +} + +/// Test 19: a WAI1 chain packed into a WAI2 explodes back to the same bytes and +/// hashes; the packed WAI2's roles and envelope manifests check; and an explode +/// without `envelope_manifest` writes the manifest's members exactly as they are in +/// the WAI2 (a prior, and a target holding `1.0e0`), with ids turned into the content +/// hashes of the exploded envelopes, which a stage chain then applies. +#[test] +fn explode_pack_roundtrip() { + let (base, units, l1, l2) = chain3(); + let packed = WaiMulti::pack(&[&base, &l1, &l2]).unwrap(); + let back = WaiMulti::explode_all(&packed).unwrap(); + assert_eq!(back, vec![base.clone(), l1.clone(), l2.clone()], "WAI1 → WAI2 → WAI1 keeps every byte"); + let w = WaiMulti::from_bytes(&packed).unwrap(); + assert_eq!(w.check_component_roles(), Ok(())); + assert_eq!(WaiMulti::check_envelope_manifests(&packed), Ok(())); + assert_eq!(w.refinements_of(0).iter().map(|(i, ..)| *i).collect::>(), vec![1, 2]); + assert_eq!(w.manifest.renditions[2].refines.as_ref().unwrap()["base"], "c0"); + assert_eq!(w.manifest.renditions[2].refines.as_ref().unwrap()["prev"], "c1"); + // An envelope_manifest that does not match its entry. + let mut w2 = WaiMulti::from_bytes(&packed).unwrap(); + w2.manifest.renditions[1].capability = CAP_VIDEO_INT_MOTION.into(); + w2.manifest.renditions[1].role = None; + w2.manifest.renditions[1].refines = None; + let mismatched = w2.to_bytes().unwrap(); + assert_eq!(WaiMulti::check_envelope_manifests(&mismatched), Err((1, "component-manifest-mismatch"))); + + // The synthesised form, with raw member text kept. + let pin = crate::container::Prior::new("p", "a".repeat(64), "int-rans/p16+int-motion/i64"); + let wm = WaiMulti::new("video", "replicate", json!({"dur": 1.0}), vec![ + Rendition::from_meta(RenditionMeta::new(CAP_VIDEO_INT_MOTION, "int_motion").with_id("base").with_prior(pin), b"base payload".to_vec()).with_payload_digest(), + ]); + let bytes = wm.to_bytes().unwrap(); + let text = String::from_utf8_lossy(&bytes[8..8 + u32::from_le_bytes(bytes[4..8].try_into().unwrap()) as usize]).to_string(); + let edited = text.replace("\"dur\":1.0", "\"dur\":1.0e0"); + assert_ne!(edited, text); + let mut e = b"WAI2".to_vec(); + e.extend_from_slice(&(edited.len() as u32).to_le_bytes()); + e.extend_from_slice(edited.as_bytes()); + e.extend_from_slice(&bytes[8 + text.len()..]); + let ex = WaiMulti::explode(&e, 0).unwrap(); + let exm = String::from_utf8_lossy(&ex[8..8 + u32::from_le_bytes(ex[4..8].try_into().unwrap()) as usize]).to_string(); + assert!(exm.contains("\"target\":{\"dur\":1.0e0}"), "{exm}"); + assert!(exm.starts_with("{\"wai\":\"1.1\",\"media\":\"video\",\"intent\":\"replicate\",\"model_requirement\":{\"capability\":\"wai.video.int_motion\",\"fallback\":null},\"conditioning\":{\"kind\":\"int_motion\",\"prior\":{"), "{exm}"); + // A refinement synthesised from ids explodes to hashes a chain applies. + let base_ex = WaiMulti::explode(&e, 0).unwrap(); + let bh = h(&base_ex); + let below: Vec> = (0..4).map(frame).collect(); + let layer_env = layer(&bh, one(&bh), 1, None, &below, 50); + let payload = Wai::from_bytes(&layer_env).unwrap().payload; + let mut meta = RenditionMeta::new(CAP_VIDEO_INT_REFINE, "wir1").with_id("l1").with_role("refinement"); + meta.refines = Some(json!({"base": "base", "prev": "base", "layer": 1})); + let wm = WaiMulti::new("video", "replicate", json!(null), vec![ + Rendition::from_meta(RenditionMeta::new(CAP_VIDEO_INT_MOTION, "int_motion").with_id("base"), b"base payload".to_vec()).with_payload_digest(), + Rendition::from_meta(meta, payload).with_payload_digest(), + ]); + assert_eq!(wm.check_component_roles(), Ok(())); + let bytes = wm.to_bytes().unwrap(); + let all = WaiMulti::explode_all(&bytes).unwrap(); + let r = Wai::from_bytes(&all[1]).unwrap().refines().unwrap().unwrap(); + assert_eq!(r.base, hexs(&h(&all[0]))); + let mut c = StageChain::new(BaseInfo { content_hash: h(&all[0]), capability: CAP_VIDEO_INT_MOTION.into(), via_fallback: false, tier: Some(DeterminismTier::DecodeEquivalence), attested: false }, units, cfg()); + assert_eq!(c.offer(&all[1], 0, ReceiptEvidence::None).unwrap()[0].applied, vec![0, 1, 2, 3]); +} + +/// The selection of the revision before refinement components: the first entry that +/// is not a companion and whose capability the sink advertises (that revision did not +/// register the refinement strings, so they were dispatchable). +fn primary_v1_1(w: &WaiMulti, advertises: impl Fn(&str) -> bool) -> Option { + w.manifest.renditions.iter().position(|m| m.role.as_deref() != Some("companion") && advertises(&m.capability)) +} + +/// Test 20: an earlier sink, picking by its own selection, picks the base of a +/// conforming WAI2 whose refinements follow it, whether it advertises only what it +/// knows or every capability; a WAI2 with a refinement first is non-conforming, and +/// there the earlier rule would pick the refinement. +#[test] +fn older_primary_returns_base() { + let (base, _, l1, l2) = chain3(); + let packed = WaiMulti::pack(&[&base, &l1, &l2]).unwrap(); + let w = WaiMulti::from_bytes(&packed).unwrap(); + assert_eq!(primary_v1_1(&w, |c| c != CAP_VIDEO_INT_REFINE), Some(0)); + assert_eq!(primary_v1_1(&w, |_| true), Some(0)); + assert_eq!(w.primary(crate::container::no_parameter_sets()).map(|p| p.index).ok(), Some(0)); + let mut swapped = WaiMulti::from_bytes(&packed).unwrap(); + swapped.manifest.renditions.swap(0, 1); + swapped.renditions.swap(0, 1); + assert!(matches!(swapped.check_component_roles(), Err(RoleError::RefinementBaseMissing { .. }) | Err(RoleError::RefinementBeforeSelectable { .. }))); + assert_eq!(primary_v1_1(&swapped, |_| true), Some(0), "the earlier rule picks the misplaced refinement"); + assert_eq!(swapped.manifest.renditions[0].capability, CAP_VIDEO_INT_REFINE); +} + +/// Test 21: every refinement role error, each from a conforming WAI2 changed in one +/// place. +#[test] +fn refinement_roles() { + let (base, _, l1, l2) = chain3(); + let packed = WaiMulti::pack(&[&base, &l1, &l2]).unwrap(); + let ok = || WaiMulti::from_bytes(&packed).unwrap(); + let err = |f: &dyn Fn(&mut WaiMulti)| { + let mut w = ok(); + f(&mut w); + w.check_component_roles().unwrap_err() + }; + assert!(matches!(err(&|w| w.manifest.renditions[0].refines = Some(json!({}))), RoleError::RefinesOnNonRefinement { index: 0 })); + assert!(matches!(err(&|w| w.manifest.renditions[1].capability = CAP_VIDEO_INT_MOTION.into()), RoleError::RefinementNotRefinementOnly { index: 1, .. })); + assert!(matches!(err(&|w| w.manifest.renditions[1].id = None), RoleError::RefinementIdentityMissing { index: 1 })); + assert!(matches!(err(&|w| w.manifest.renditions[1].sha256 = None), RoleError::RefinementIdentityMissing { index: 1 })); + assert!(matches!(err(&|w| w.manifest.renditions[0].sha256 = None), RoleError::RefinementIdentityMissing { index: 1 })); + assert!(matches!(err(&|w| w.manifest.renditions[1].refines = Some(json!({"base": "c0", "prev": "c0", "layer": 0}))), RoleError::RefinesMalformed { index: Some(1), .. })); + assert!(matches!(err(&|w| w.manifest.renditions[1].refines = None), RoleError::RefinesMalformed { index: Some(1), .. })); + assert!(matches!(err(&|w| w.manifest.renditions[1].refines = Some(json!({"base": "nope", "prev": "nope", "layer": 1}))), RoleError::RefinementBaseMissing { index: 1, .. })); + assert!(matches!(err(&|w| w.manifest.renditions[0].capability = crate::codecs::CAP_IMAGE_PNG.into()), RoleError::RefinementBaseNotAdmitted { index: 1, .. })); + assert!(matches!(err(&|w| w.manifest.renditions[2].refines = Some(json!({"base": "c0", "prev": "c9", "layer": 2}))), RoleError::RefinementPrevMissing { index: 2, .. })); + assert!(matches!(err(&|w| { + w.manifest.renditions.push(RenditionMeta::new(CAP_VIDEO_INT_MOTION, "int_motion")); + w.renditions.push(vec![1]); + }), RoleError::RefinementBeforeSelectable { index: 1, selectable: 3 })); + assert!(matches!(err(&|w| w.manifest.renditions[2].role = None), RoleError::RefinesOnNonRefinement { index: 2 })); + assert!(matches!(err(&|w| { + w.manifest.renditions[2].role = None; + w.manifest.renditions[2].refines = None; + }), RoleError::RefinementOnlyAsComponent { index: 2, .. })); + // A refinement is never selected, and a refinement whose payload does not match + // its sha256 is not offered. + let w = ok(); + assert!(!w.manifest.renditions[1].is_selectable()); + let mut tampered = ok(); + tampered.renditions[2][50] ^= 1; + assert_eq!(tampered.refinements_of(0).iter().map(|(i, ..)| *i).collect::>(), vec![1]); +} -- 2.54.0 (Apple Git-157) From 880099fec0ba266b219f214a4e789ce838ddacab Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 2 Oct 2026 00:38:14 -0400 Subject: [PATCH 08/36] wai: the corpus checks WAI2 refinement components and exploding staged-conformance/wai2/ holds WAI2 envelopes with refinement components and an index of what a sink decides about each: the role check, whether each envelope_manifest matches its entry, the content hash of every exploded component, and entry 0's refinements. A live-style chain packed with its manifests verbatim explodes back to its sources byte for byte. Both verifiers check every file; staged_verify.py explodes components by reading the manifest's members as the text they are. Explode now decides entry by entry, so an entry whose refines cannot be translated does not stop the others, and the envelope_manifest check reads only what each entry names. RoleError gains a code. SPEC 7.1 states that a member written as null is absent when a component is exploded. Co-Authored-By: Claude Opus 5.5 --- wai/SPEC.md | 3 +- wai/staged-conformance/README.md | 11 +- wai/staged-conformance/staged_verify.py | 277 +++++++++++++++++- .../wai2/component_manifest_mismatch.wai | Bin 0 -> 3555 bytes .../wai2/digest_mismatch.wai | Bin 0 -> 1839 bytes wai/staged-conformance/wai2/index.json | 114 +++++++ wai/staged-conformance/wai2/ok_explode.wai | Bin 0 -> 1839 bytes .../wai2/pack_live_chain.src0.wai | Bin 0 -> 555 bytes .../wai2/pack_live_chain.src1.wai | Bin 0 -> 531 bytes .../wai2/pack_live_chain.src2.wai | Bin 0 -> 523 bytes .../wai2/pack_live_chain.src3.wai | Bin 0 -> 1018 bytes .../wai2/pack_live_chain.wai | Bin 0 -> 3649 bytes wai/staged-conformance/wai2/prev_missing.wai | Bin 0 -> 1839 bytes .../wai2/refinement_before_selectable.wai | Bin 0 -> 2377 bytes wai/wai-rs/src/bin/wai_staged_vectors.rs | 114 ++++++- wai/wai-rs/src/container.rs | 33 +++ wai/wai-rs/src/container_explode.rs | 21 +- 17 files changed, 561 insertions(+), 12 deletions(-) create mode 100644 wai/staged-conformance/wai2/component_manifest_mismatch.wai create mode 100644 wai/staged-conformance/wai2/digest_mismatch.wai create mode 100644 wai/staged-conformance/wai2/index.json create mode 100644 wai/staged-conformance/wai2/ok_explode.wai create mode 100644 wai/staged-conformance/wai2/pack_live_chain.src0.wai create mode 100644 wai/staged-conformance/wai2/pack_live_chain.src1.wai create mode 100644 wai/staged-conformance/wai2/pack_live_chain.src2.wai create mode 100644 wai/staged-conformance/wai2/pack_live_chain.src3.wai create mode 100644 wai/staged-conformance/wai2/pack_live_chain.wai create mode 100644 wai/staged-conformance/wai2/prev_missing.wai create mode 100644 wai/staged-conformance/wai2/refinement_before_selectable.wai diff --git a/wai/SPEC.md b/wai/SPEC.md index 90da97872..65ecd90de 100644 --- a/wai/SPEC.md +++ b/wai/SPEC.md @@ -1692,7 +1692,8 @@ by which capability happens to be installed. component id replaced by the content hash of that entry's exploded envelope, written as [staged-delivery](extensions/staged-delivery.md) §3 orders its members (`base`, `prev`, `layer`, `span`; a run's `producer`, then `count`) with no whitespace. A - bracketed part is present exactly when its member is. A component and its exploded + bracketed part is present exactly when its member is present and not `null`. A + component and its exploded envelope are the same object for every rule that names an object by content hash ([jwp-receipts](extensions/jwp-receipts.md) §2.1). An entry MAY carry `envelope_manifest`, a JSON object: the exact manifest of the entry as a `WAI1` diff --git a/wai/staged-conformance/README.md b/wai/staged-conformance/README.md index 3c2ec9920..a9c2cfc49 100644 --- a/wai/staged-conformance/README.md +++ b/wai/staged-conformance/README.md @@ -17,6 +17,14 @@ report names the `WIR1` code (Appendix A.6). decoded through, whether through a fallback, whether the sink verified a delivery receipt for it, whether its pin resolved, its tier and its units; the sink's hold bound, cost limit and live schedule; the steps; and what the sink reports and holds. +- `wai2/` — `WAI2` envelopes with refinement components (SPEC §7.1) and + `index.json`, which states for each what `check_component_roles` reports, whether + each `envelope_manifest` matches its entry, the content hash of every component's + exploded envelope, and entry 0's refinements: a live-style chain packed with its + manifests verbatim, which explodes back to its sources (`*.src.wai`) byte for + byte; an envelope written with ids and no `envelope_manifest`; a refinement whose + payload does not match its `sha256`; an `envelope_manifest` that does not match its + entry; a producer that names nothing; a refinement before a selectable entry. - `schedule.json` — arrival traces, and the unit-by-unit presentation and stage a sink gives each (§8), with the count of units presented below the stage their layers would have reached on time. @@ -52,7 +60,8 @@ Every layer application, in every case, is checked by both. `wai-rs`. It reimplements the envelope and the content hash, `refines`, `WIR1` (its parse in order, its cost, its streams, the three predictors and `rdiv`), the sink's algorithm with its holds, batches, conflicts and deadlines, tiers and stage hashes, - the PNG and FLAC decodes, and the schedule. It takes BLAKE3 from + the PNG and FLAC decodes, the schedule, and exploding a `WAI2` component (reading + the manifest's members as the text they are) with the refinement role rules. It takes BLAKE3 from [`../claim-conformance/_blake3.py`](../claim-conformance/_blake3.py) and the rANS decoder and the `WIV1` keyframe-0 decode from [`../int-payload-conformance/int_payload_verify.py`](../int-payload-conformance/int_payload_verify.py), diff --git a/wai/staged-conformance/staged_verify.py b/wai/staged-conformance/staged_verify.py index 1a9748b66..e8f915ae1 100644 --- a/wai/staged-conformance/staged_verify.py +++ b/wai/staged-conformance/staged_verify.py @@ -815,6 +815,280 @@ def present(clock, base_ms, layers, lead): return out +# ---------------------------------------------------------------- WAI2 identity (SPEC §7.1) + +WS = " \t\n\r" + + +def skip(t, i): + while i < len(t) and t[i] in WS: + i += 1 + return i + + +def node(t, i): + """The JSON value at `i`: (start, end, members) with members a dict for an object, + a list for an array, None otherwise. Spans index `t`.""" + i = skip(t, i) + s, c = i, t[i] + if c == "{": + kids, i = {}, skip(t, i + 1) + if t[i] == "}": + return (s, i + 1, kids) + while True: + k = node(t, i) + i = skip(t, k[1]) + assert t[i] == ":" + v = node(t, i + 1) + kids[json.loads(t[k[0]:k[1]])] = v + i = skip(t, v[1]) + if t[i] == ",": + i += 1 + continue + assert t[i] == "}" + return (s, i + 1, kids) + if c == "[": + kids, i = [], skip(t, i + 1) + if t[i] == "]": + return (s, i + 1, kids) + while True: + v = node(t, i) + kids.append(v) + i = skip(t, v[1]) + if t[i] == ",": + i += 1 + continue + assert t[i] == "]" + return (s, i + 1, kids) + if c == '"': + j = i + 1 + while t[j] != '"': + j += 2 if t[j] == "\\" else 1 + return (s, j + 1, None) + j = i + while j < len(t) and t[j] not in ",}] \t\n\r": + j += 1 + return (s, j, None) + + +def parse_wai2(b): + if b[:4] != b"WAI2": + raise ValueError("not WAI2") + ml = struct.unpack_from("}lZ3=PNf^x0ZPnJe&2+_bRE<0M z;awt9A&R3hQCPwvz9&jy{M9W6AM4i^OQKv97skc%az8Fgx+Un(MyoPIZnQ@nKe9CV z^uO;Mi^^DhseomX^5WvEs?7ep`ih1t{CME0OX>x!tLn)%Gi;SRvS31S97|)8CR~l1 zwxg(?&4jC)%;bio$fm5?F4KhUI85<=+JxHU&Ypc6Q%tW`HP$^pBr0h@7Ut9Ey=3vIfm@&x?r+vITklLHyAg# zZCj4SWku&)(`;2`y5nj(cU|T%Ls2be{3o%6n2dBSe>bj)5hN3IDM?KfGJXhwfDICL444j&-ZP~i# z3Ff#SJZe~GZ-x7+kGokj@wl!9hmH`EqjJskG}E>`#g;V95vGSPdM<8t)xf3BO~=8_ z&F~dd?c>+JzUv#ynCml;NI_YhaEV&9Cl4Uni25jM3*^mSC?{YZO~OB&K77>G)in`g zLD?QKm}t=uqC_lAOd%R{9nq4psMClV($Stmv`Zo?P>F_Ecvm*j{kMS}qKg}dI=6rw zMDw3V|0SZWO{fop<3zQ;Bg*|V(UuQT{|)_rVBJ~JhIQ>kU0s0{Fcjq|P-*;%hU)Nu znqdQq!l|b!e>=5x!8-NWrk{+vuFRfXGwp_~6zfkjPT2D^E;MGH^4?o_$Dn6dKDVqX z_o)_ZYFlLJ*k^`LXnC%+djB5t$=@HZitVe-nYO9nrRBR1`ZMl+w@%I5dCTHMN4}X< zwrTp@Kkh!+F!JE=7jsY7-~Q0vV|Lqbe%8I1~Y`g8qd*8XSvN<=kc4fG(YWm>m z!!qX{ov}1w$@c5pXB9q`^m^jRgad!bEWAnGylmC*i*KFVnI!h@{Qk)k&dqB_?47u% z_1e7Ix!w8)JG-p^z+jU-CGcPe{BTWAKLG>Vq5F0PY7`OO`v|nz4~_mpw4n2-ZYgz_QcxC3I%I1f2-d;WWS7SbSC#~VJ zif`@eYCOF1!osyplSVh5I5zM76#PBCHSLMl)B9s*C+tk>v6GTvn@m)f5C7J{kFUU| zu<_z2S8beywND=`sXddLLDO4bxUboq^NccS<|7X#CFeJt-fZ8#v87JyJR5zu-mE_N z_m(+g4L_Iq_z_P_dCksD9mX-vBp(2_OU} zf;2D*q=TEmWN-_}08>CFmKn;lZv`+cIWH@wGFi&oK?1%c2w1)BL^NrB1Fonk~|5y!g_!qHVODq5Y literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/wai2/digest_mismatch.wai b/wai/staged-conformance/wai2/digest_mismatch.wai new file mode 100644 index 0000000000000000000000000000000000000000..be5a5c45053c71bec231c0a6b3423a700733374c GIT binary patch literal 1839 zcmbu9dr(w$6vq#Uf+!+rn)yf;L`B5=*w>}h)KSD76Hq}R;dl4mb+5wix~w=zn~xc( zsYz?B$j6lSP*h%0DkwgZDILj4oh;F`*Nh`*OqLHobo*Y=wEpOi`ptgkch0%L-|wFD z{hhn%lahtC3QXiRM)B^lvdukH{=vKQiCJ1d|!Q z-LBZJX2x-f-E3k;naNgFG+HbcRpg8U&oLY0RF2s>)y!m*BuYj_VkW^Vi$+Dp&)|#y z*Ni->U32Phe2~#{&7;@dnMk4uW`bR0vLJ9c%j3#bQB&7D^K>C~@$>|sTv1o;e@Wo>s0EJ%$z_GA z$B@YT{!cx^ap2r6u4wFLu!Eb)QKN>N8lijiD<`F%)i3_9qVp^HmqwW^858HkSN9pAkvWKRmG0LlAeJ%j{Z%B-;8aUvsOI0 z?VUjbywa$W2}2Vitv^maE2YJ?)+RLBFRz`{{gV~j7uTh3X||4SbA}B3FeIvZdrR@b zy_QYiHTc{IN|PpRtJ=S``iM4p#-&|idd2AZM~^=f>fJVR)(_PetNI-2y)X58<#Q`* zPD*WGf7N{L`uj0c+QuKh{KBxp#?=0$D;&Fg6MIbT89(d9?si3}dZ6O9i)ZDLYr<-xGg_WVPfqRBU$5+;{$01FR|=g$2kxO) zrw0WPD1+{00_+^3xo<$5gV5+_qB-{-=`<7ToAx%`o`JdYaQ^8!*R{%v^KvTJ)vc70 zO6In%ZtfQK`Lv9}##fWhuZsAx-_;8-RqG3$-Q%yVU(q^mP2Gry+OsE9u0-OmXk5(3 zQ?U>8e7sK{&r^@rBhLkx^|2cPJ=b!O(Vb`k=z;zT1|eV}`Y0RZfPE;ucPQ)#qR*bh zJcx(^8Pq&QR13m~5cLHSAQBwbZSh>#DEbX7K{X=z-SN$#(;P*791-;|L-Px7rj~n-UA!K zCNT9b(KL_*rh`AgP4Gbn(N^$0?rkQx16n~FXa{#e2iS2R-vda)H!uf0{1#?IXZ@ZV zR^JKJ`(uO4AAU4tZ?9K;*@IuN*t+xBvZ?m0z_(6Z>HD_3W9_Y%z94tw!0-#@0WbTT zcE=?<)BA*-x6Y4Czm#@#&FOntiOpFX#x%BfyIKEH%542^3o;H?yC>9~Sydc-AgFqZ za_*O1j@Z6u4z;XHoYfI^BR@K9XZYsO>BTXzgR)Ziw)JnCGGnTS+*x2Y>~WBz-_jeR GWAmTRkSB`( literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/wai2/index.json b/wai/staged-conformance/wai2/index.json new file mode 100644 index 000000000..dd59fbb72 --- /dev/null +++ b/wai/staged-conformance/wai2/index.json @@ -0,0 +1,114 @@ +{ + "about": "WAI2 envelopes with refinement components (SPEC §7.1): what check_component_roles reports, whether each envelope_manifest matches its entry, the content hash of each component's exploded envelope, and entry 0's refinements. Where `sources` is given, the components explode to those WAI1 envelopes byte for byte.", + "files": [ + { + "file": "pack_live_chain.wai", + "sources": [ + "pack_live_chain.src0.wai", + "pack_live_chain.src1.wai", + "pack_live_chain.src2.wai", + "pack_live_chain.src3.wai" + ], + "expect": { + "roles": null, + "manifests": null, + "exploded": [ + "20488045874a49bf9680c7c635b5db88dcdf1aefad0f48acaacd6f7c286e07df", + "7c2879ab5b602075503c62526abfcf8fc1dc7eb6626b88c9895ffeabcf493687", + "7e347c1d544e77ee0b3d47f4798f29044be7f075fc0df364bad7bb94dac98731", + "b8b998dac5a0e74511eaeb9692414ede5447cb4b280ee6311c084a31084988ee" + ], + "refinements_of_0": [ + 1, + 2, + 3 + ] + } + }, + { + "file": "ok_explode.wai", + "sources": [], + "expect": { + "roles": null, + "manifests": null, + "exploded": [ + "548f6a1b589a1b74fac21eaa7226b8bdc6be0360b8d47886cac964b1f081a5d4", + "53d7a0e6db5d6b6a0f2788f60d86717d3619b3dd5c834fd65ee18456eab7c5f1", + "2ae6c786594f2752428614de4ac6adba78a6e1614896ab3b185d014e0599dc54" + ], + "refinements_of_0": [ + 1, + 2 + ] + } + }, + { + "file": "digest_mismatch.wai", + "sources": [], + "expect": { + "roles": null, + "manifests": null, + "exploded": [ + "548f6a1b589a1b74fac21eaa7226b8bdc6be0360b8d47886cac964b1f081a5d4", + "53d7a0e6db5d6b6a0f2788f60d86717d3619b3dd5c834fd65ee18456eab7c5f1", + "6d45d931e5e0b7d7bbf5c858819b7507f5204adf08a056dd31644e6b0c8d9799" + ], + "refinements_of_0": [ + 1 + ] + } + }, + { + "file": "component_manifest_mismatch.wai", + "sources": [], + "expect": { + "roles": null, + "manifests": [ + 1, + "component-manifest-mismatch" + ], + "exploded": [ + "20488045874a49bf9680c7c635b5db88dcdf1aefad0f48acaacd6f7c286e07df", + "7c2879ab5b602075503c62526abfcf8fc1dc7eb6626b88c9895ffeabcf493687", + "7e347c1d544e77ee0b3d47f4798f29044be7f075fc0df364bad7bb94dac98731", + "b8b998dac5a0e74511eaeb9692414ede5447cb4b280ee6311c084a31084988ee" + ], + "refinements_of_0": [ + 2, + 3 + ] + } + }, + { + "file": "prev_missing.wai", + "sources": [], + "expect": { + "roles": "refinement-prev-missing", + "manifests": null, + "exploded": null, + "refinements_of_0": [ + 1, + 2 + ] + } + }, + { + "file": "refinement_before_selectable.wai", + "sources": [], + "expect": { + "roles": "refinement-before-selectable", + "manifests": null, + "exploded": [ + "548f6a1b589a1b74fac21eaa7226b8bdc6be0360b8d47886cac964b1f081a5d4", + "53d7a0e6db5d6b6a0f2788f60d86717d3619b3dd5c834fd65ee18456eab7c5f1", + "2ae6c786594f2752428614de4ac6adba78a6e1614896ab3b185d014e0599dc54", + "548f6a1b589a1b74fac21eaa7226b8bdc6be0360b8d47886cac964b1f081a5d4" + ], + "refinements_of_0": [ + 1, + 2 + ] + } + } + ] +} diff --git a/wai/staged-conformance/wai2/ok_explode.wai b/wai/staged-conformance/wai2/ok_explode.wai new file mode 100644 index 0000000000000000000000000000000000000000..42da3e6c3ce829519b5e0c7c7a855154a2b0ef3a GIT binary patch literal 1839 zcmbu9dr(w$6vq#Ug7`qtH1m-xh>D2!v9C+1siTNFCZK{s!td_A>t2Q3by;zcHXk!m zQdw(@%&Q{kQJsqB z)m+XzLqbNO!N%N7)^ap&0oLKL_|DDo*qW8=(wAY&Qq8I0>EB|S9+6p|eq^452_`dq zyIrwc&5Yv|yV=ByGLx;UXtY=?s>m4yo?|w~sT{L&s+q|qNtBF=#7u%!7LAIGpTQUR zuNiq%yXMs2_#mSdnn$m@GnX9QdXxz-F|#BpGOsdKkY%fpaYofWDUF=B3?YTKSj-M~^=f>fJtR_77DTEBhYly+7r8#dE8w zPfBfHf7N{L`uovS+a?^p{KD{p#*_giD;>LilX^_*88`dHl*IuVr9;|hWNi&S71TH2 z(D!j!BgBuF6!yModa(Spi)ZCgYx`73rMEnhmXy+|zh2ox{kv{UuM|3i4%|bp zP7ewoPzv2k1=!g{^WK0qwb1BiqPh1T=`;)LoAx!_o{72QaNg-U*R_g^^Rp|~*R7Hg zi|4hjY3>&J`SkRH##a;1uMYpR|J4i8l^gP(-RrNZU)ef;ZQaQ5nzJX9uSDRlXngdh zQ!x+oe7sK{&r^@rBhQ7H^|1#5J=Zd@pgYk*&;$Jw3_`#n^idYb2K!NX?@-v`M4vs0 zc`y+JGN^uvs0M@$CF%#lK?FFg+vqtvdagE}Xz_TWL+CvRPo#kX(B}wPf?iw-azHL{ z0vB+DWxxZ58Ht92r@;si2@D_#M1zqa28;ru!59z=#)3F74jjuz|L-As7rj~x-UFM! zW-#q8(R7dqW`IAyP4Gbn(Khfr?rj#h16n~FXa{#e2iSQZ-vda+H!v4G{1)awXZ@ZV z*4*i%_s2$;KkR7ozFx2RvWC1~zHQgB<yHN HjxB!z(2OUH literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/wai2/pack_live_chain.src0.wai b/wai/staged-conformance/wai2/pack_live_chain.src0.wai new file mode 100644 index 0000000000000000000000000000000000000000..d7413225c836f4b62275731c444d6ee82f8c7aa4 GIT binary patch literal 555 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1M7#SGCJ;Mwcfix4)xg2Qt z!~g&Pm4RX)um*(L7#Jeh85mfA3R#307+A#^7^202v1v( zehcJ(0;>B8#6N-Relalo{||Bn(4jzZ0*H+nWXdIqpqaJpm}to383-pjOmt&XfqtCnT$37YfDLicALhxj56#aGMTwe8+& zKI{7F+Japx-0c=_*j~74f2Nby-Brfn>og+|9Jl5wS!nNff79&^eEWH}1-)7CP`~4p z<bh7`;aNmq+{@uCpwBjC=3s h#48vt$*Si0clFykuGC%Yx^7=eQg7njp&ao}6aZnzzNP>G literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/wai2/pack_live_chain.src1.wai b/wai/staged-conformance/wai2/pack_live_chain.src1.wai new file mode 100644 index 0000000000000000000000000000000000000000..fd9e5d435224fed38046fc776041605f9b4453fa GIT binary patch literal 531 zcmb_YJ4*vW5Z<7LV5Ne+i&}+v7ca>r*w`2%Hi}8?lD*wMS-kAU%Ow&+@KM>QrT7mN zglJ(A41x$2iXc`#It%g9i-mH|`S=GeJ3Ei>+mD&aj;7RH0HGRQAOa__8d7i&7dR6H zvWj5bhFnOOOBV_bFIs{Um-{4}aVzKW`Lb|$;Tx}EN{Yk~mT;@U2FcL3LAIpt^q5ApxM9Ylxuu>rt-6mW z*AeoL5ZZk}X!;Xk&nE`Z1ia6O7jNU>8^@)qmi^ke&*U2$t)+N!EB(Im($;q}HdDP_ pOWrJZo%KB3_3v+1Mh?8@>Ee54wKdSyygr|N?9LVJq5kd5!7q}Cs15)C literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/wai2/pack_live_chain.src2.wai b/wai/staged-conformance/wai2/pack_live_chain.src2.wai new file mode 100644 index 0000000000000000000000000000000000000000..320ae80e42e829af50db5d01d0c9df9c1af67aaf GIT binary patch literal 523 zcmb_YyGjE=6um(UO%>EH7_gBLcH<_yNuxHRkV=tYKxB3vGPs$In@vPQ1RJrjvQQ93 z#6oOrv=c-S{03V~5w%bYMc2Dg|G;4$_dGbAgUd`MHsD7S9vO2K{rNbIuRKRK&Mn%kan6d zUvm{IUn{*piY>CJG>`2nFk|`0SdSH#)&(kA!*U$M$~m@0t%55HdB7EU@(r4Rq%i;h literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/wai2/pack_live_chain.src3.wai b/wai/staged-conformance/wai2/pack_live_chain.src3.wai new file mode 100644 index 0000000000000000000000000000000000000000..7d8bb84ba913055f6e62d977f020bb53ba90aa7a GIT binary patch literal 1018 zcmY+>TWl0n7zgmL+)9Ab7EJ&}k)f>GF3a36`(Q#yl?Gy3YO>H)>zQ-u4s2()+uahf zl!%G&KmwW?FZ6*XK4@()N`N2)ArKQ2U*y3=Onfpi(FYSErc#Q_f4C$%$#2d%-}&bI zPR``Eb{Xdzh-%t?7HL_{$mm*H8B5*ZXgrix;zji)tUj73z5ig6SVY1x{#FN#Hy9+iz` z`(ff}vJNXjUW;fS6kr%)C_)KFpbXF0 zM9;!{cn&sz1{QXon6+!)CZPM$|t;^uZj_hwu@63@4%UF44>I3T%hJ z;5M9^Cz^sC_lS1FEc^p^U=HrWJbd~u(Pz+GCwdL)fZL^Rym!L)hi99XW1J|~o3Hkq zTYhwW@abb;Oif>_cB+8|?|t`M%loDIcm6*3EtPJrZvOdn!=d``Gwoe*Ze`O?{+{;S zFTGda{^8z0_K$&yj+=7}Z(skUr#tz#W4kY%DZO;z##=`kFD^Q>P5k)l=lN92jmv)? v&%Qdp;nwKJrs?L%W!sOmrPdDg7-4vMxo)=4)xOG8{)v%WR(|q6MDiYB literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/wai2/pack_live_chain.wai b/wai/staged-conformance/wai2/pack_live_chain.wai new file mode 100644 index 0000000000000000000000000000000000000000..1a541dec7649c6ebeb2b0f6e7eaf734f36a6bfd2 GIT binary patch literal 3649 zcmeH}dvF!i9mjux@QOS_h*BzAxrB%z#C<=~3W_5E%TNhOAVib1d(LtfZ|=R^JeWYE zzznHV$47^1RICL{C4@%;it-48(h37QRi?H^s(sBk5^1U}uf&A(yCF!T(+<<2(?8ms z`gw!&IdwvKel(E1XQpL-BFNN5^@6+0>d7`UY?V8*U_x;m zOJkBIT#cKyqo|(EgsYp(UlF$UUq^mH=FR%OpIWLMV(lU>WPxXHP}xWR4P zawIM*I_H{Zt18nSSJS!cGKU$8YBA&A@fO}&@kESQxWTyKyOzh>g^x)vNdLP(CvHPh2f+wv4!(lkey9-ipA_-0TId@^v;aqwNh z@Dx++lh?kv>+5Co^=U|^;98k_@a7b&#G|0kyxkUHh2J(n5tRw2&1hx^) zeF^nfi8eQ(JOqvr)%}Jj|BpnQK0)~x)c=NYXFwaqwG(x91yLYSTt|RPW0y77g!|PF z=~og?KUwwb$*uF&s7E(EIp(@@dt&XB8*s~lZPkm|6O+RP3p!aD~4Wp@9g#zv3L6qPaJn{UOjC0_yw)k z7R=1=)<0O;CH?yci|o0A40a$7*Ywnr(6AM{Z&jd13DLceLYsZi=ubrRIxp)q3*%4j zI(GJMlnt+yz0(x?wBf}3l7`2c9)b&rT@6sr!S~#emL*_l_TFA^~nbrjgMD; zXGhnbL(9+4U)?letK= zUijj2j59Fy`2(eOr_-}&YU|7QHJh_uP$o=&^ud(W!lqLj?c3M4)N7q*Vh=Z%HD~|Y zGE1!DXVafP>}hGgu(MM~ZQPZ+@F(q`ts7EUz4r8__*q1}J_*SC**c)MJ;)8VL?*LeWz48EvfKm_zF%Sn!K?2;M5#0#B1#SZ4KnRQn z8DIj)1UG|;;1-YtCV^})8N5-6{ketcsdl1T@HBV^JPYnZPNsuAFa!J@oCZJbz*zyl z3t!Cw=fHW;2HL?FpaX2WfZT$)*rW5nrM-F&ln#7&Y}L78fq&P=x<(wFv+J54R2N_W z$oBP{->A6DE9&>7BOj;!INq`5%=dpu@#fJZKG>G@VAshlSu>*r!-u_ZEzBzTaPGm? z?{pUBwiK@~ literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/wai2/prev_missing.wai b/wai/staged-conformance/wai2/prev_missing.wai new file mode 100644 index 0000000000000000000000000000000000000000..d1176dfc56f55b0e7252aebd49d02da45ca6d91e GIT binary patch literal 1839 zcmbu9dr(w$6vq#Uf+!+rn)yf;L`B5=*w@`sQ%4bVOh5&Ngx}qJ*Ik9(by;zMHXk!m zQU9b!p46WtqdN;OXCD4lN?HJnhIlI};2> z_%@qjGaDJlDK?{l>18IHRZ(v;nN*R}3p~fHj8i#g<5VM)4U#D76^R)Hvn=Wr89$vb z{$Dfls5XaFd*g$QRysUd-CeolXx5`lc!?P$QIUC-se&w<^^DW2R=r}9WI?n^Otl(J z%%m7Ofj9A@WMz7lmt`i{L?#OYhqFAc92GTnzcW`8QWH;00Lm40*Z!9TZjYM(NRS*> zpn7x`-uHj%5gxBc;U$h2d7EtHtpyYPWe@xjG9Mud}*VXbpx!eJ{rAyv(yig{v)K zxtdJaqpZ-BNZ@?F9;A)c0o|0s9)lJ(gS4atd;sDwDdZpg=J)%f5z|JQMlYiDV4@%_ z42mS`Ify7@Fye}HILJ>~|` zNL2bQQOZw5JFjB?75?v7cMG&*T?dihuVsNmF-`!HhOMkBwg;B<4$QU>Y%cg_eCxb* z;>qpr3>oB=#*|DNo)BsNaq3wqHLk5Tq1kqM-ISi66z*7BpR%>ZJigr-GWf%gsFoeA z#dZ5ko4;%Hxet~mPTF2|V0rZs$JAMuc8h5hV;3Gh{!FNM`{X%4R9~#>cckzBlMAUBMO>Q29_4ucl##yn%p~n&WWi@0y4^mcFfG$8hR?I zU%;X73Y|-%8LuKE7#Yr zk`hbix2i@mDk< zX49$Ihj~8UCy(c;#T$_4BFx&@gMgN6Iat_}Xc6dz{s{&lU@`hA3uJ@+D7)49vw#>k{PF(5#w!3rPt(U$achlhT3*`YX z`n(FSqi*CyhwTdA5<0UuCU!_>GT*-8O+!XZ)v!B@jJmycvJY5( IL$q)C6L112;{X5v literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/wai2/refinement_before_selectable.wai b/wai/staged-conformance/wai2/refinement_before_selectable.wai new file mode 100644 index 0000000000000000000000000000000000000000..ba4848199d4f296ef377bf128a45c3e145f442a6 GIT binary patch literal 2377 zcmeH{dr(w$6vq#Uf+!+rn)yf;L`B5C_wMUbO6n+LjtQusknp>E@48oEcU@K-B=Ru> zH8p9C75SLb9*T;9lnR26WJ*VJQYT9^?KR^F8k6M%5Z%5PRIER8CjaQ4ezTuBzu&pP z``dHA=iFb)!~{OB8&SS)8B=w!IxdFOMeDK^S!Gz|suF3%%i~R zRvfbGQJsz)U2Iyu&dOX&QnOS~9`@m|n2wiY@M=c3Q`?4D7OM^!PygamHIKw{v?Fuu zj5nIFw%KHx#l-Y_*=8~_gTy3@A{fkOvm)pXJf~+?rdRaLrdLc%GKzv|kVR(XEs|i6 zCH!;+(f=5cTd}DQ?H}(&v`lqtd3VH;rOmn`;Y4N<1zF-0rtp$vF)+PBu^MEvDDi?# zWQx^jW@g!>=Q%Sch*oA$I7wo>O<)qw>v5LbnWZ47&i~BTgw({-0)TS)ow+}dz~xqQ zy9CK%d5T*X%N0CMKK#S^$egI>1kNUzIIGd9FwSa`EV5aaO-we)qG*xyGRGTb*)lPwFX0?kI3}Vy_D72 zo?R7u>|S~?TIXTzg$e@HLT1@o;gE&ARI5#lzM#O5w6W5slM>8F`4U+`CLU@2l!UpQ z4=h|1@Hc+*dcEQBX`@u52T`g&ksmhtg%WiiK$JERHiD=k67w-cyY)or0+GK3=fx5& z7!Tr!?rtQyvkmMbO8yk2^x_M{@*0x*mn^sM zD2VG3*E4$Nu}KSk(n<%nP0QF6c*3ub&w=lwGlmNvFUs$I_sq?*KxKc~Yv)f(BUcAi zg{L+@k&=+qp}&^def@FoJs4UA+fj#J_dGsultTAX9%^I~&3Oab)Ig)3iDuvF(rE_v zH}0vwIUTnAV9v=}=ausFb2H1=)UFWYi|4egYU&pD`P9_BhF9ayt_=CI-{o@=6>D>! z-R-TeTi!Bvb?u0d>eI&)FNNZg92c?vMCAQAAMTTf|H#kvd3>rONu z^g#dkg8;AqeUt$*!CoZZI}q~_qR*a$9Yn-{1gf4Qss_PBi28yM5DE@zbM%}IJy#P$ zv~V2J0rZ}oBT_+s=yM1xLN6``Ss)uYfD^dD65s|y4MfAh(_lCV13C~6BESd`2}XiZ zU^IvVV?Z<*3y$QX|929-i(V}T?}7DT1DJA~Xex*Y)4(6#I{2WSXft>ob(;ZhffmpT z+Q4nl4z}IJ_W+Xd4a^4jzlB-QS*vsXs#`%?f2?zQgAXU}>GeuM#^Be>Hg7+&bc!wA z_pM`>`o8UIFTC;67vyRf7<_J*&&%G%olyynls-XcE%TyME+ijbeezCvY*YHWF%50q zuGf8(I7_Q-ZfZ@XYeLnjl|}yh{VFHRXMWjXkL-KuK=Yc|neAcMa>9eQ2X73VRumCA kC_RyDUHhgnEuvz`t@$S1ZadlgEx9JxH~xRW{I~bZpDDoFF#rGn literal 0 HcmV?d00001 diff --git a/wai/wai-rs/src/bin/wai_staged_vectors.rs b/wai/wai-rs/src/bin/wai_staged_vectors.rs index 7481aefdf..e5da9386d 100644 --- a/wai/wai-rs/src/bin/wai_staged_vectors.rs +++ b/wai/wai-rs/src/bin/wai_staged_vectors.rs @@ -38,7 +38,7 @@ use wai::codecs::{ CAP_NEURAL_INT_HYPER, 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, Span, Wai}; +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::schedule::{deadline_misses, present, UnitClock}; @@ -1181,6 +1181,24 @@ fn generate(ctx: &Ctx) -> BTreeMap> { index.push(json!({"name": c.name, "about": c.about, "clip_sha256": finals.iter().map(|f| f.get("clip_sha256").cloned().unwrap_or(Value::Null)).collect::>()})); out.insert(format!("{dir}/case.json"), json_bytes(&case)); } + let mut w2 = Vec::new(); + for (name, bytes, sources) in wai2_files(ctx) { + let mut src_names = Vec::new(); + for (i, s) in sources.iter().enumerate() { + let n = format!("wai2/{name}.src{i}.wai"); + out.insert(n.clone(), s.clone()); + src_names.push(n.trim_start_matches("wai2/").to_string()); + } + w2.push(json!({"file": format!("{name}.wai"), "sources": src_names, "expect": wai2_expect(&bytes)})); + out.insert(format!("wai2/{name}.wai"), bytes); + } + out.insert( + "wai2/index.json".into(), + json_bytes(&json!({ + "about": "WAI2 envelopes with refinement components (SPEC §7.1): what check_component_roles reports, whether each envelope_manifest matches its entry, the content hash of each component's exploded envelope, and entry 0's refinements. Where `sources` is given, the components explode to those WAI1 envelopes byte for byte.", + "files": w2, + })), + ); out.insert("schedule.json".into(), json_bytes(&schedule_json())); out.insert( "index.json".into(), @@ -1218,6 +1236,80 @@ fn schedule_json() -> Value { json!({"clock": {"t0_ms": c.t0_ms, "unit_ms": c.unit_ms, "target_latency_ms": c.target_latency_ms}, "traces": out}) } +// ───────────────────────── WAI2 identity (SPEC §7.1) ───────────────────────── + +/// The `wai2/` files: name → (bytes, the WAI1 sources it packs, if any). +fn wai2_files(ctx: &Ctx) -> Vec<(&'static str, Vec, Vec>)> { + let pid = Predictor::Identity; + let a = gop_base(ctx, 8, 8, 2, 80, 0, 8); + let bh = a.hash(); + // A live-style chain: a layer 1 per frame, then a layer 2 over their run. + let mut l1s = Vec::new(); + let mut mid = Vec::new(); + for f in 0..2u32 { + let (l, r) = layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, Some(Span { from: f, count: 1 }), &[rgb_unit(&a, f as usize)], (8, 8, 3), &[u8s(picture(8, 8, 81, f))], (8, 8, 3), params(4, pid, 4095)); + l1s.push(l); + mid.push(r.into_iter().next().unwrap()); + } + let hs: Vec<[u8; 32]> = l1s.iter().map(|l| chash(l)).collect(); + let targets: Vec = (0..2).map(|f| u8s(picture(8, 8, 81, f + 3))).collect(); + let (l2, _) = layer(CAP_VIDEO_INT_REFINE, &bh, runs(&hs), 2, None, &mid, (8, 8, 3), &targets, (8, 8, 3), params(2, pid, 4095)); + let sources = vec![a.envelope.clone(), l1s[0].clone(), l1s[1].clone(), l2.clone()]; + let refs: Vec<&[u8]> = sources.iter().map(|v| v.as_slice()).collect(); + let packed = WaiMulti::pack(&refs).expect("pack"); + // The synthesised form: ids, no envelope_manifest. + let payload_of = |e: &[u8]| Wai::from_bytes(e).expect("WAI1").payload; + let refinement = |id: &str, refines: Value, payload: Vec| { + let mut m = RenditionMeta::new(CAP_VIDEO_INT_REFINE, "wir1").with_id(id).with_role("refinement"); + m.refines = Some(refines); + Rendition::from_meta(m, payload).with_payload_digest() + }; + let synth = |l2_prev: &str, extra: bool, tamper: bool| -> Vec { + let mut rs = vec![ + Rendition::from_meta(RenditionMeta::new(CAP_VIDEO_INT_MOTION, "int_motion").with_id("base"), payload_of(&a.envelope)).with_payload_digest(), + refinement("l1", json!({"base": "base", "prev": "base", "layer": 1}), payload_of(&l1s[0])), + ]; + let mut l2p = payload_of(&l2); + let mut two = refinement("l2", json!({"base": "base", "prev": l2_prev, "layer": 2, "span": {"from": 0, "count": 1}}), l2p.clone()); + if tamper { + l2p[60] ^= 1; + two.payload = l2p; + } + rs.push(two); + if extra { + rs.push(Rendition::from_meta(RenditionMeta::new(CAP_VIDEO_INT_MOTION, "int_motion").with_id("late"), payload_of(&a.envelope)).with_payload_digest()); + } + WaiMulti::new("video", "replicate", json!({"frames": 2, "dur": 1.0}), rs).to_bytes().expect("WAI2") + }; + // The packed chain with entry 1 turned into a selectable entry: its envelope_manifest + // no longer matches it. + let mut w = WaiMulti::from_bytes(&packed).expect("WAI2"); + w.manifest.renditions[1].capability = CAP_VIDEO_INT_MOTION.into(); + w.manifest.renditions[1].role = None; + w.manifest.renditions[1].refines = None; + let mismatch = w.to_bytes().expect("WAI2"); + vec![ + ("pack_live_chain", packed, sources), + ("ok_explode", synth("l1", false, false), vec![]), + ("digest_mismatch", synth("l1", false, true), vec![]), + ("component_manifest_mismatch", mismatch, vec![]), + ("prev_missing", synth("zz", false, false), vec![]), + ("refinement_before_selectable", synth("l1", true, false), vec![]), + ] +} + +/// What a sink and a verifier decide about a `WAI2`: its role check, its envelope +/// manifests, its exploded envelopes' content hashes, and entry 0's refinements. +fn wai2_expect(bytes: &[u8]) -> Value { + let w = WaiMulti::from_bytes(bytes).expect("WAI2"); + json!({ + "roles": w.check_component_roles().err().map(|e| e.code()), + "manifests": WaiMulti::check_envelope_manifests(bytes).err().map(|(i, c)| json!([i, c])), + "exploded": WaiMulti::explode_all(bytes).ok().map(|v| v.iter().map(|e| hex(&chash(e))).collect::>()), + "refinements_of_0": w.refinements_of(0).iter().map(|(i, ..)| *i).collect::>(), + }) +} + fn wai_dir() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("..") } @@ -1265,6 +1357,11 @@ fn verify(dir: &Path) -> Result<(), Vec> { } } let mut committed = Vec::new(); + if let Ok(rd) = std::fs::read_dir(dir.join("wai2")) { + for f in rd.flatten() { + committed.push(format!("wai2/{}", f.file_name().to_string_lossy())); + } + } for sub in ["cases"] { let root = dir.join(sub); if let Ok(rd) = std::fs::read_dir(&root) { @@ -1354,7 +1451,20 @@ fn verify(dir: &Path) -> Result<(), Vec> { fail(format!("{rel}: the final units differ from the case's")); } } - // 3. The schedule. + // 3. The WAI2 files. + let w2: Value = std::fs::read(dir.join("wai2/index.json")).ok().and_then(|b| serde_json::from_slice(&b).ok()).unwrap_or(Value::Null); + for f in w2["files"].as_array().into_iter().flatten() { + let name = f["file"].as_str().unwrap_or_default(); + let bytes = std::fs::read(dir.join("wai2").join(name)).unwrap_or_default(); + if wai2_expect(&bytes) != f["expect"] { + fail(format!("wai2/{name}: the checks differ from the index's")); + } + let srcs: Vec> = f["sources"].as_array().unwrap().iter().map(|s| std::fs::read(dir.join("wai2").join(s.as_str().unwrap())).unwrap_or_default()).collect(); + if !srcs.is_empty() && WaiMulti::explode_all(&bytes).ok() != Some(srcs) { + fail(format!("wai2/{name}: does not explode to its sources")); + } + } + // 4. The schedule. let sched: Value = std::fs::read(dir.join("schedule.json")).ok().and_then(|b| serde_json::from_slice(&b).ok()).unwrap_or(Value::Null); if sched != schedule_json() { fail("schedule.json: the traces do not replay to the stated presentation".into()); diff --git a/wai/wai-rs/src/container.rs b/wai/wai-rs/src/container.rs index 885faca8a..f97f04bb0 100644 --- a/wai/wai-rs/src/container.rs +++ b/wai/wai-rs/src/container.rs @@ -1598,6 +1598,39 @@ pub enum RoleError { RefinementBeforeSelectable { index: usize, selectable: usize }, } +impl RoleError { + /// A token for the error, for tools and corpora. + pub fn code(&self) -> &'static str { + match self { + RoleError::CompanionOnlyAsCapability { .. } => "companion-only-as-capability", + RoleError::CompanionOnlyAsFallback { .. } => "companion-only-as-fallback", + RoleError::CompanionOnlyAsRendition { .. } => "companion-only-as-rendition", + RoleError::StandaloneAsCompanion { .. } => "standalone-as-companion", + RoleError::UnboundCompanion { .. } => "unbound-companion", + RoleError::IncompatibleBase { .. } => "incompatible-base", + RoleError::EnhancementBaseNotUnique { .. } => "enhancement-base-not-unique", + RoleError::DuplicateEnhancementLayer { .. } => "duplicate-enhancement-layer", + RoleError::ComponentIdMalformed { .. } => "component-id-malformed", + RoleError::DuplicateComponentId { .. } => "duplicate-component-id", + RoleError::CompanionIdUnresolved { .. } => "companion-id-unresolved", + RoleError::CompanionIdCapabilityMismatch { .. } => "companion-id-capability-mismatch", + RoleError::RefinementOnlyAsFallback { .. } => "refinement-only-as-fallback", + RoleError::RefinementWithoutRefines { .. } => "refinement-without-refines", + RoleError::RefinementWithFallback { .. } => "refinement-with-fallback", + RoleError::RefinesWithoutRefinement { .. } => "refines-without-refinement", + RoleError::RefinesMalformed { .. } => "refines-malformed", + RoleError::RefinementOnlyAsComponent { .. } => "refinement-only-as-component", + RoleError::RefinesOnNonRefinement { .. } => "refines-on-non-refinement", + RoleError::RefinementNotRefinementOnly { .. } => "refinement-not-refinement-only", + RoleError::RefinementIdentityMissing { .. } => "refinement-identity-missing", + RoleError::RefinementBaseMissing { .. } => "refinement-base-missing", + RoleError::RefinementBaseNotAdmitted { .. } => "refinement-base-not-admitted", + RoleError::RefinementPrevMissing { .. } => "refinement-prev-missing", + RoleError::RefinementBeforeSelectable { .. } => "refinement-before-selectable", + } + } +} + impl std::fmt::Display for RoleError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/wai/wai-rs/src/container_explode.rs b/wai/wai-rs/src/container_explode.rs index 70d6bbff4..59dcebc27 100644 --- a/wai/wai-rs/src/container_explode.rs +++ b/wai/wai-rs/src/container_explode.rs @@ -93,6 +93,13 @@ impl WaiMulti { /// replaced by the content hash of that entry's exploded envelope. An entry a /// refinement names must come before it. pub fn explode_all(envelope: &[u8]) -> std::io::Result>> { + Self::explode_each(envelope)?.into_iter().collect() + } + + /// [`Self::explode_all`], entry by entry: an entry whose `refines` cannot be + /// translated (it is malformed, or names an entry that is not before it, or one + /// that could not be exploded) has its own error, and the others their envelopes. + fn explode_each(envelope: &[u8]) -> std::io::Result>>> { let w = WaiMulti::from_bytes(envelope)?; let mtext = manifest_bytes(envelope)?; let raw: RawV2 = serde_json::from_slice(mtext).map_err(|e| bad(format!("WAI2 manifest: {e}")))?; @@ -103,7 +110,7 @@ impl WaiMulti { let mut out = Vec::with_capacity(raw.renditions.len()); for (i, (e, meta)) in raw.renditions.iter().zip(&w.manifest.renditions).enumerate() { let payload = &w.renditions[i]; - let manifest: Vec = match e.envelope_manifest { + let manifest = (|| -> std::io::Result> { Ok(match e.envelope_manifest { Some(m) => m.get().as_bytes().to_vec(), None => { let mut s = String::new(); @@ -139,10 +146,10 @@ impl WaiMulti { s.push('}'); s.into_bytes() } - }; - let env = wai1(&manifest, payload); - if let Some(id) = meta.id.as_deref() { - ids.insert(id.to_string(), hex(&crate::jwp_hash::content_hash(&env))); + }) })(); + let env = manifest.map(|m| wai1(&m, payload)); + if let (Some(id), Ok(env)) = (meta.id.as_deref(), env.as_ref()) { + ids.insert(id.to_string(), hex(&crate::jwp_hash::content_hash(env))); } out.push(env); } @@ -237,7 +244,7 @@ impl WaiMulti { pub fn check_envelope_manifests(envelope: &[u8]) -> Result<(), (usize, &'static str)> { const CODE: &str = "component-manifest-mismatch"; let w = WaiMulti::from_bytes(envelope).map_err(|_| (0, CODE))?; - let exploded = Self::explode_all(envelope).map_err(|_| (0, CODE))?; + let exploded = Self::explode_each(envelope).map_err(|_| (0, CODE))?; let mut ids: BTreeMap = BTreeMap::new(); for (i, (meta, env)) in w.manifest.renditions.iter().zip(&exploded).enumerate() { if let Some(m) = &meta.envelope_manifest { @@ -262,7 +269,7 @@ impl WaiMulti { return Err((i, CODE)); } } - if let Some(id) = meta.id.as_deref() { + if let (Some(id), Ok(env)) = (meta.id.as_deref(), env.as_ref()) { ids.insert(id.to_string(), hex(&crate::jwp_hash::content_hash(env))); } } -- 2.54.0 (Apple Git-157) From e370e7840d942678e26de6ac64e7edffe866efdf Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 2 Oct 2026 00:41:42 -0400 Subject: [PATCH 09/36] wai: wai stage writes, applies, inspects, packs and explodes layers wai stage refine writes a residual layer toward a target over the stage the given layers bring a base to, with prev naming the producers its span holds (a string, or runs); apply offers layers to a base in order and prints each report and the units' stages, producers, tiers and stage hashes; inspect prints a layer's refines, role check, WIR1 header, declared cost and stream check; pack and explode convert between WAI1 envelopes and a WAI2. A base of an integer codec is decoded with its parameter set and its pin; any other base is read from its stage-0 buffers. The subcommand builds with staged and the integer codecs' features. wai wrap refuses a refinement capability, since it writes no refines, and points to wai stage refine; wai inspect prints a WAI1's refines. staged::units_from_decoded gives a base's units from its integer decode. Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/Cargo.toml | 5 + wai/wai-rs/src/bin/wai.rs | 24 ++ wai/wai-rs/src/bin/wai_cli/stage.rs | 375 +++++++++++++++++++++++ wai/wai-rs/src/bin/wai_staged_vectors.rs | 11 +- wai/wai-rs/src/staged/mod.rs | 15 + wai/wai-rs/tests/cli_stage.rs | 120 ++++++++ 6 files changed, 540 insertions(+), 10 deletions(-) create mode 100644 wai/wai-rs/src/bin/wai_cli/stage.rs create mode 100644 wai/wai-rs/tests/cli_stage.rs diff --git a/wai/wai-rs/Cargo.toml b/wai/wai-rs/Cargo.toml index 3c33d4bc0..9728cde61 100644 --- a/wai/wai-rs/Cargo.toml +++ b/wai/wai-rs/Cargo.toml @@ -648,6 +648,11 @@ name = "int_mlicpp_preflight" path = "tests/int_mlicpp_preflight.rs" required-features = ["neural_int"] +[[test]] +name = "cli_stage" +path = "tests/cli_stage.rs" +required-features = ["staged", "video_int", "audio_int", "splat_int"] + [[test]] name = "int_payload" path = "tests/int_payload.rs" diff --git a/wai/wai-rs/src/bin/wai.rs b/wai/wai-rs/src/bin/wai.rs index ec7ce19e5..3d1b6e722 100644 --- a/wai/wai-rs/src/bin/wai.rs +++ b/wai/wai-rs/src/bin/wai.rs @@ -18,6 +18,10 @@ use wai::{Conditioning, Manifest, ModelRequirement, Wai}; #[cfg(feature = "codecs")] use wai::*; +#[cfg(all(feature = "staged", feature = "video_int", feature = "audio_int", feature = "splat_int"))] +#[path = "wai_cli/stage.rs"] +mod stage; + fn die(msg: impl std::fmt::Display) -> ExitCode { eprintln!("error: {msg}"); ExitCode::from(2) @@ -35,6 +39,8 @@ fn usage() { eprintln!(" write (or, with --check, compare) registry/capabilities.json, its schema,"); eprintln!(" vocab/v2.* and the generated table of REGISTRY.md"); eprintln!("wai prior-set print the listing of a parameter set (wai.prior.set) and its pin"); + eprintln!("wai stage refine|apply|inspect|pack|explode … staged-delivery layers (a build with `staged`,"); + eprintln!(" `video_int`, `audio_int` and `splat_int`); `wai stage` alone prints its usage"); eprintln!(); eprintln!("encode and decode need a build with the `codecs` feature."); eprintln!("codecs: png jpeg avif jxl jxl-lossless opus flac av1 av1-lossless zstd xz"); @@ -327,6 +333,12 @@ fn inspect(path: &str) -> Result<(), String> { Ok(()) => println!(" pin check: \u{2713} no static finding (SPEC \u{a7}3.1, \u{a7}7)"), Err(f) => println!(" pin check: \u{2717} {f}"), } + match w.refines() { + None => {} + Some(Ok(r)) => println!(" refines: base {}… layer {}{}", &r.base[..16], r.layer, + r.span.map(|s| format!(" units {}..{}", s.from, s.from + s.count)).unwrap_or_default()), + Some(Err(e)) => println!(" refines: \u{2717} {e}"), + } println!(); println!(" capability: {cap}"); println!(" resolution: {}", @@ -506,6 +518,12 @@ fn wrap(cap: &str, input: &str, out: &str, fallback: Option<&str>, if !wai::codecs::is_known_capability(cap) { return Err(format!("'{cap}' is not a registered capability (see `wai capabilities`)")); } + // A layer names, in `refines`, what it refines, and `wai wrap` writes no + // `refines` (SPEC §5, Refinement capabilities). + if wai::codecs::is_refinement_only(cap) { + return Err(format!("{cap} is refinement-only: a layer names what it refines, which `wai wrap` \ + does not write; `wai stage refine` writes one (extensions/staged-delivery.md)")); + } // A candidate is refused before any other rule is applied, in every place the // envelope would name it (SPEC §5, Candidate capabilities). if companions.is_empty() { @@ -548,6 +566,8 @@ fn wrap(cap: &str, input: &str, out: &str, fallback: Option<&str>, envelope.check_component_roles().map_err(|e| match e { wai::container::RoleError::CompanionOnlyAsCapability { .. } => format!( "{e} (SPEC \u{a7}5): `wai wrap --companion {cap} `"), + wai::container::RoleError::RefinementWithoutRefines { .. } => format!( + "{e}: a layer names what it refines; `wai stage refine` writes one (extensions/staged-delivery.md)"), _ => format!("{e} (SPEC \u{a7}5)"), })?; let n = envelope.write(out).map_err(|e| e.to_string())?; @@ -792,6 +812,10 @@ fn main() -> ExitCode { Err(e) => Err(e), }, "prior-set" if args.len() >= 2 => prior_set(&args[1]), + #[cfg(all(feature = "staged", feature = "video_int", feature = "audio_int", feature = "splat_int"))] + "stage" => stage::main(&args[1..]), + #[cfg(not(all(feature = "staged", feature = "video_int", feature = "audio_int", feature = "splat_int")))] + "stage" => Err("`wai stage` needs a build with the features staged, video_int, audio_int and splat_int".into()), _ => { usage(); return ExitCode::from(2); } }; match r { diff --git a/wai/wai-rs/src/bin/wai_cli/stage.rs b/wai/wai-rs/src/bin/wai_cli/stage.rs new file mode 100644 index 000000000..31c6fdf4c --- /dev/null +++ b/wai/wai-rs/src/bin/wai_cli/stage.rs @@ -0,0 +1,375 @@ +//! `wai stage`: write, apply, inspect, pack and explode staged-delivery layers +//! (`extensions/staged-delivery.md`; SPEC §7.1). Built with the `staged` feature and +//! the integer codecs' features, which decode the bases. +//! +//! A base of an integer capability is decoded through `int_payload::decode_int`: with +//! no parameter set for `wai.video.int_motion`, otherwise against the set in the +//! directory `--set` names (its files by the names SPEC §5 gives them), with the base's +//! pin verified when it carries one. Any other base (PNG, FLAC: this tool has no +//! `codecs` decoder for them) is read from `--stage0`, the units' canonical buffers +//! concatenated, with `--units N --dims D0,D1,D2 --elem u8|i16`. + +use std::collections::BTreeMap; + +use wai::container::{Prev, PrevRun, Refines, Span, Wai, WaiMulti}; +use wai::int_payload::{decode_int, resolve}; +use wai::pin::HeldBytes; +use wai::staged::wir1::{self, Below, Next, Predictor, RefineParams}; +use wai::staged::{tier, BaseInfo, ChainConfig, HoldBound, ReceiptEvidence, Samples, StageChain, UnitBuf}; + +fn hex(b: &[u8]) -> String { + b.iter().map(|x| format!("{x:02x}")).collect() +} + +fn opt<'a>(args: &'a [String], flag: &str) -> Option<&'a str> { + args.iter().position(|a| a == flag).and_then(|i| args.get(i + 1)).map(String::as_str) +} + +fn opts<'a>(args: &'a [String], flag: &str) -> Vec<&'a str> { + args.iter().enumerate().filter(|(_, a)| *a == flag).filter_map(|(i, _)| args.get(i + 1)).map(String::as_str).collect() +} + +/// The positional arguments: those that are neither a flag nor a flag's value. +fn positional(args: &[String]) -> Vec<&str> { + let mut out = Vec::new(); + let mut skip = false; + for a in args { + if skip { + skip = false; + } else if a.starts_with("--") || a == "-o" { + skip = true; + } else { + out.push(a.as_str()); + } + } + out +} + +fn read(path: &str) -> Result, String> { + std::fs::read(path).map_err(|e| format!("read {path}: {e}")) +} + +fn dims(s: &str) -> Result<(u32, u32, u32), String> { + let v: Vec = s.split(',').map(|x| x.trim().parse::().map_err(|e| format!("--dims {s}: {e}"))).collect::>()?; + match v[..] { + [a, b, c] => Ok((a, b, c)), + _ => Err(format!("--dims {s}: three dimensions, D0,D1,D2")), + } +} + +/// The base's info and units, decoded as the module doc states. +fn base(path: &str, args: &[String]) -> Result<(Vec, BaseInfo, Vec), String> { + let env = read(path)?; + let w = Wai::from_bytes(&env).map_err(|e| format!("{path}: {e}"))?; + let cap = w.capability().to_string(); + let hash = wai::jwp_hash::content_hash(&env); + let (units, pin_resolved) = if let Some(s0) = opt(args, "--stage0") { + let bytes = read(s0)?; + let n: usize = opt(args, "--units").unwrap_or("1").parse().map_err(|e| format!("--units: {e}"))?; + let d = dims(opt(args, "--dims").ok_or("--stage0 needs --dims")?)?; + let per = d.0 as usize * d.1 as usize * d.2 as usize; + let i16 = opt(args, "--elem") == Some("i16"); + let need = n * per * if i16 { 2 } else { 1 }; + if bytes.len() != need { + return Err(format!("{s0}: {} bytes, the units need {need}", bytes.len())); + } + let units = (0..n) + .map(|u| { + let samples = if i16 { + Samples::I16(bytes[2 * u * per..2 * (u + 1) * per].chunks_exact(2).map(|c| i16::from_le_bytes([c[0], c[1]])).collect()) + } else { + Samples::U8(bytes[u * per..(u + 1) * per].to_vec()) + }; + UnitBuf { shape: d, samples } + }) + .collect(); + (units, false) + } else { + let set: BTreeMap> = match opt(args, "--set") { + Some(dir) => wai::pin::read_dir_set(std::path::Path::new(dir)).map_err(|e| format!("--set {dir}: {e}"))?, + None => BTreeMap::new(), + }; + let files: BTreeMap = set.iter().map(|(k, v)| (k.clone(), v.as_slice())).collect(); + let listing = if files.is_empty() { vec![] } else { wai::pin::listing_from(&files).map_err(|e| e.to_string())? }; + let one_file = wai::container::pin_shape(&cap) == wai::codecs::ParameterSetShape::OneFile; + let held: Vec = if files.is_empty() { + vec![] + } else if one_file && w.prior().is_some_and(|p| p.form().is_none()) { + vec![HeldBytes::File(files.values().next().copied().unwrap_or_default())] + } else { + vec![HeldBytes::Set { listing: &listing, files: &files }] + }; + let params = resolve(&cap, w.prior(), &held, None).map_err(|e| format!("{path}: {e}"))?; + let pinned = params.is_pinned(); + let d = decode_int(&cap, &w.payload, ¶ms).map_err(|e| format!("{path}: {e}"))?; + (wai::staged::units_from_decoded(d).ok_or("an output kind with no units")?, pinned) + }; + let info = BaseInfo { + content_hash: hash, + tier: tier::base_tier(&cap, if pin_resolved { w.prior() } else { None }), + capability: cap, + via_fallback: false, + attested: false, + }; + Ok((env, info, units)) +} + +fn below(s: &Samples) -> Below<'_> { + match s { + Samples::U8(v) => Below::U8(v), + Samples::I16(v) => Below::I16(v), + } +} + +fn cfg() -> ChainConfig { + ChainConfig { bound: HoldBound { max_envelopes: 1024, max_age_ms: u64::MAX }, cost_limit: None, live: None } +} + +fn print_units(c: &StageChain) { + for (u, s) in c.units().iter().enumerate() { + let sha = c.unit_sha256(u as u32).map(|h| hex(&h)).unwrap_or_else(|| "(none: not decode-equivalence)".into()); + println!(" unit {u}: stage {} {}x{}x{} producer {} tier {}{} sha256 {sha}", + s.stage, s.buf.shape.0, s.buf.shape.1, s.buf.shape.2, &hex(&s.producer)[..16], tier::token(s.tier), + if s.frozen { " FROZEN" } else { "" }); + } + match c.clip_sha256() { + Some(h) => println!(" clip sha256 {}", hex(&h)), + None => println!(" clip sha256 (none: not every unit holds decode-equivalence)"), + } +} + +/// `wai stage apply …`: offer each layer in order; print every +/// report and the units at the end. +fn apply(args: &[String]) -> Result<(), String> { + let pos = positional(args); + let (_, info, units) = base(pos.first().ok_or("wai stage apply …")?, args)?; + let mut c = StageChain::new(info, units, cfg()); + for (i, l) in pos[1..].iter().enumerate() { + let bytes = read(l)?; + for r in c.offer(&bytes, i as u64, ReceiptEvidence::None).map_err(|e| format!("{l}: {e}"))? { + print!("{l}: {}", &hex(&r.content_hash)[..16]); + if let Some(e) = r.envelope { + print!(" refused {}", e.code()); + if let Some(d) = r.detail { + print!(" ({d})"); + } + } + if !r.applied.is_empty() { + print!(" applied {:?}", r.applied); + } + if !r.held.is_empty() { + print!(" held {:?}", r.held); + } + for (u, w) in &r.refused { + print!(" unit {u}: {}", w.code()); + } + println!(); + } + } + print_units(&c); + Ok(()) +} + +/// `wai stage refine --base [--on ]… --target --q N +/// [--predictor identity|up2|rate:NUM/DEN] [--span F:C] [--dims D0,D1,D2] +/// [--max-symbols N] [--table-per-unit] -o L.wai`: a residual layer toward `--target` +/// (the span's units' canonical buffers concatenated, at `--dims`, by default the +/// predictor's output shape), over the stage the `--on` layers give; `prev` names the +/// producers the span holds. +fn refine(args: &[String]) -> Result<(), String> { + let base_path = opt(args, "--base").ok_or("--base ")?; + let out = opt(args, "-o").ok_or("-o ")?; + let (_, info, units) = base(base_path, args)?; + let cap = match units.first().map(|u| &u.samples) { + Some(Samples::I16(_)) => wai::codecs::CAP_AUDIO_INT_REFINE, + _ if wai::codecs::capability_media(&info.capability) == Some(wai::codecs::Media::Video) => wai::codecs::CAP_VIDEO_INT_REFINE, + _ if wai::codecs::capability_media(&info.capability) == Some(wai::codecs::Media::Volumetric) => wai::codecs::CAP_SPLAT_INT_REFINE, + _ => wai::codecs::CAP_IMAGE_INT_REFINE, + }; + let base_hash = info.content_hash; + let n_units = units.len() as u32; + let mut c = StageChain::new(info, units, cfg()); + for l in opts(args, "--on") { + let bytes = read(l)?; + c.offer(&bytes, 0, ReceiptEvidence::None).map_err(|e| format!("{l}: {e}"))?; + } + let (from, count) = match opt(args, "--span") { + Some(s) => { + let (a, b) = s.split_once(':').ok_or("--span F:C")?; + (a.parse::().map_err(|e| e.to_string())?, b.parse::().map_err(|e| e.to_string())?) + } + None => (0, n_units), + }; + if count == 0 || from as u64 + count as u64 > n_units as u64 { + return Err(format!("--span {from}:{count} is outside the base's {n_units} units")); + } + let span_units: Vec<&wai::staged::UnitState> = (from..from + count).map(|u| c.unit(u).expect("in range")).collect(); + let layer = span_units[0].stage.checked_add(1).ok_or("a unit is at stage 255")?; + if span_units.iter().any(|s| s.stage + 1 != layer) { + return Err("the span's units are at different stages; refine them in spans of one stage".into()); + } + let below_shape = span_units[0].buf.shape; + if span_units.iter().any(|s| s.buf.shape != below_shape) { + return Err("the span's units have different shapes".into()); + } + let q: u32 = opt(args, "--q").ok_or("--q N")?.parse().map_err(|e| format!("--q: {e}"))?; + let (predictor, rate) = match opt(args, "--predictor").unwrap_or("identity") { + "identity" => (Predictor::Identity, (0, 0)), + "up2" => (Predictor::Up2, (0, 0)), + r if r.starts_with("rate:") => { + let (a, b) = r[5..].split_once('/').ok_or("--predictor rate:NUM/DEN")?; + (Predictor::Rate, (a.parse::().map_err(|e| e.to_string())?, b.parse::().map_err(|e| e.to_string())?)) + } + p => return Err(format!("--predictor {p}: identity, up2 or rate:NUM/DEN")), + }; + let out_dims = match opt(args, "--dims") { + Some(d) if opt(args, "--stage0").is_none() => dims(d)?, + _ => match predictor { + Predictor::Identity => below_shape, + Predictor::Up2 => (below_shape.0 * 2, below_shape.1 * 2, below_shape.2), + Predictor::Rate => (wai::staged::predict::rate_len(below_shape.0 as u64, rate.0, rate.1).ok_or("rate")? as u32, 1, below_shape.2), + }, + }; + let target = read(opt(args, "--target").ok_or("--target ")?)?; + let per = out_dims.0 as usize * out_dims.1 as usize * out_dims.2 as usize; + let pcm = matches!(span_units[0].buf.samples, Samples::I16(_)); + if target.len() != count as usize * per * if pcm { 2 } else { 1 } { + return Err(format!("--target holds {} bytes; {count} units of {}x{}x{} need {}", target.len(), out_dims.0, out_dims.1, out_dims.2, count as usize * per * if pcm { 2 } else { 1 })); + } + let targets: Vec = (0..count as usize) + .map(|u| if pcm { + Samples::I16(target[2 * u * per..2 * (u + 1) * per].chunks_exact(2).map(|x| i16::from_le_bytes([x[0], x[1]])).collect()) + } else { + Samples::U8(target[u * per..(u + 1) * per].to_vec()) + }) + .collect(); + let belows: Vec = span_units.iter().map(|s| below(&s.buf.samples)).collect(); + let tb: Vec = targets.iter().map(below).collect(); + let p = RefineParams { + q, + predictor, + rate, + table_per_unit: args.iter().any(|a| a == "--table-per-unit"), + max_symbols: opt(args, "--max-symbols").unwrap_or("4095").parse().map_err(|e| format!("--max-symbols: {e}"))?, + }; + let kind = wai::staged::unit_kind(cap).expect("a refinement"); + let (payload, recon) = wir1::encode(kind, &belows, below_shape, &tb, out_dims, &p).map_err(|e| e.to_string())?; + let mut runs: Vec = Vec::new(); + for s in &span_units { + let h = hex(&s.producer); + match runs.last_mut() { + Some(r) if r.producer == h => r.count += 1, + _ => runs.push(PrevRun { producer: h, count: 1 }), + } + } + let prev = if runs.len() == 1 { Prev::One(runs.remove(0).producer) } else { Prev::Runs(runs) }; + let span = (from != 0 || count != n_units).then_some(Span { from, count }); + let refines = Refines { base: hex(&base_hash), prev, layer, span }; + let media = wai::codecs::capability_media(cap).map(|m| m.label()).unwrap_or("binary"); + let m = wai::Manifest::new(media, "replicate", wai::ModelRequirement::new(cap), wai::Conditioning::new("wir1")).with_refines(&refines); + let env = Wai::new(m, payload); + let bytes = env.to_emit_bytes().map_err(|e| e.to_string())?; + env.check_component_roles().map_err(|e| e.to_string())?; + std::fs::write(out, &bytes).map_err(|e| format!("write {out}: {e}"))?; + let err: u64 = recon.iter().zip(&targets).map(|(r, t)| match (r, t) { + (Next::U8(a), Samples::U8(b)) => a.iter().zip(b).map(|(x, y)| (*x as i64 - *y as i64).unsigned_abs()).max().unwrap_or(0), + (Next::I16(a), Samples::I16(b)) => a.iter().zip(b).map(|(x, y)| (*x as i64 - *y as i64).unsigned_abs()).max().unwrap_or(0), + _ => 0, + }).max().unwrap_or(0); + println!("refined → {out} ({} B) {cap} layer {layer} units {from}..{} q {q} {} largest error {err}", + bytes.len(), from + count, predictor.token(), ); + println!(" content hash {}", hex(&wai::jwp_hash::content_hash(&bytes))); + Ok(()) +} + +/// `wai stage inspect `: the layer's `refines`, its payload's header and its +/// declared cost. +fn inspect(args: &[String]) -> Result<(), String> { + let path = *positional(args).first().ok_or("wai stage inspect ")?; + let bytes = read(path)?; + let w = Wai::from_bytes(&bytes).map_err(|e| e.to_string())?; + println!("{path}: {} B content hash {}", bytes.len(), hex(&wai::jwp_hash::content_hash(&bytes))); + println!(" capability {} role {}", w.capability(), wai::codecs::capability_role(w.capability()).unwrap_or("unregistered")); + match w.refines() { + None => println!(" refines: (none)"), + Some(Err(e)) => println!(" refines: {e}"), + Some(Ok(r)) => { + println!(" refines: base {} layer {}", r.base, r.layer); + match &r.prev { + Prev::One(p) => println!(" prev {p}"), + Prev::Runs(runs) => runs.iter().for_each(|x| println!(" prev {} × {}", x.producer, x.count)), + } + if let Some(s) = r.span { + println!(" span {}..{}", s.from, s.from + s.count); + } + } + } + match w.check_component_roles() { + Ok(()) => println!(" roles: ✓"), + Err(e) => println!(" roles: ✗ {} ({e})", e.code()), + } + if let Some(kind) = wai::staged::unit_kind(w.capability()) { + match wir1::parse(&w.payload, kind) { + Ok(p) => { + let c = p.declared_cost(); + println!(" WIR1: {} units of {}x{}x{} predictor {} q {} tables {} ", + p.n_units, p.d0, p.d1, p.d2, p.predictor.token(), p.q, p.tables.len()); + println!(" declared cost: {} output bytes, {} symbols, {} working bytes", c.output_bytes, c.symbols, c.working_bytes); + match p.check_streams() { + Ok(()) => println!(" streams: ✓ every unit's stream decodes to its end"), + Err((u, e)) => println!(" streams: ✗ unit {u}: {e}"), + } + } + Err(e) => println!(" WIR1: ✗ {e}"), + } + } + Ok(()) +} + +/// `wai stage pack … -o x.wai` and `wai stage explode +/// [-o out.wai]`. +fn pack(args: &[String]) -> Result<(), String> { + let out = opt(args, "-o").ok_or("-o ")?; + let files: Vec> = positional(args).iter().map(|p| read(p)).collect::>()?; + let refs: Vec<&[u8]> = files.iter().map(Vec::as_slice).collect(); + let bytes = WaiMulti::pack(&refs).map_err(|e| e.to_string())?; + let w = WaiMulti::from_bytes(&bytes).map_err(|e| e.to_string())?; + w.check_component_roles().map_err(|e| e.to_string())?; + std::fs::write(out, &bytes).map_err(|e| format!("write {out}: {e}"))?; + println!("packed {} envelopes → {out} ({} B)", files.len(), bytes.len()); + Ok(()) +} + +fn explode(args: &[String]) -> Result<(), String> { + let pos = positional(args); + let (path, i) = match pos[..] { + [p, i] => (p, i.parse::().map_err(|e| e.to_string())?), + _ => return Err("wai stage explode [-o out.wai]".into()), + }; + let bytes = WaiMulti::explode(&read(path)?, i).map_err(|e| e.to_string())?; + println!("component {i}: {} B content hash {}", bytes.len(), hex(&wai::jwp_hash::content_hash(&bytes))); + if let Some(out) = opt(args, "-o") { + std::fs::write(out, &bytes).map_err(|e| format!("write {out}: {e}"))?; + } + Ok(()) +} + +pub fn main(args: &[String]) -> Result<(), String> { + match args.first().map(String::as_str) { + Some("apply") => apply(&args[1..]), + Some("refine") => refine(&args[1..]), + Some("inspect") => inspect(&args[1..]), + Some("pack") => pack(&args[1..]), + Some("explode") => explode(&args[1..]), + _ => Err(USAGE.into()), + } +} + +pub const USAGE: &str = "wai stage refine --base [--on ]… --target --q N \ +[--predictor identity|up2|rate:NUM/DEN] [--span F:C] [--max-symbols N] [--table-per-unit] -o L.wai +wai stage apply … offer the layers in order; print each report and the units +wai stage inspect a layer's refines, WIR1 header and declared cost +wai stage pack … -o x.wai pack WAI1 envelopes into a WAI2 (manifests verbatim) +wai stage explode [-o out.wai] a WAI2 component as its WAI1 envelope + a base of an integer codec decodes with --set (its parameter files by name); any + other base is read from --stage0 --units N --dims D0,D1,D2 [--elem u8|i16]"; diff --git a/wai/wai-rs/src/bin/wai_staged_vectors.rs b/wai/wai-rs/src/bin/wai_staged_vectors.rs index e5da9386d..4a1061b44 100644 --- a/wai/wai-rs/src/bin/wai_staged_vectors.rs +++ b/wai/wai-rs/src/bin/wai_staged_vectors.rs @@ -405,16 +405,7 @@ impl Ctx { } fn units_of(d: IntDecoded) -> Vec { - match d { - IntDecoded::Video { frames, width, height } => frames.into_iter().map(|f| UnitBuf { shape: (height, width, 3), samples: Samples::U8(f) }).collect(), - IntDecoded::Image { rgb, width, height } => vec![UnitBuf { shape: (height, width, 3), samples: Samples::U8(rgb) }], - IntDecoded::Pcm { samples, channels, .. } => { - let n = samples.len() as u32 / channels; - vec![UnitBuf { shape: (n, 1, channels), samples: Samples::I16(samples) }] - } - IntDecoded::Attr { bytes, n_attr, s, .. } => vec![UnitBuf { shape: (n_attr, s, s), samples: Samples::U8(bytes) }], - _ => panic!("an output kind the corpus does not use"), - } + wai::staged::units_from_decoded(d).expect("an output kind the corpus uses") } /// Decode `payload` through `cap` with `set` (member → path), pinned by `prior` when diff --git a/wai/wai-rs/src/staged/mod.rs b/wai/wai-rs/src/staged/mod.rs index 720fe6ba7..418c44ca1 100644 --- a/wai/wai-rs/src/staged/mod.rs +++ b/wai/wai-rs/src/staged/mod.rs @@ -35,6 +35,21 @@ pub fn unit_kind(cap: &str) -> Option { } } +/// A base's units from its integer decode (§2): every frame of a clip, the whole +/// picture, the whole clip of PCM (samples, 1, channels), the whole attribute tensor. +pub fn units_from_decoded(d: crate::int_payload::IntDecoded) -> Option> { + use crate::int_payload::IntDecoded; + Some(match d { + IntDecoded::Video { frames, width, height } => frames.into_iter().map(|f| UnitBuf { shape: (height, width, 3), samples: Samples::U8(f) }).collect(), + IntDecoded::Image { rgb, width, height } => vec![UnitBuf { shape: (height, width, 3), samples: Samples::U8(rgb) }], + IntDecoded::Pcm { samples, channels, .. } => { + let n = (samples.len() as u32).checked_div(channels)?; + vec![UnitBuf { shape: (n, 1, channels), samples: Samples::I16(samples) }] + } + IntDecoded::Attr { bytes, n_attr, s, .. } => vec![UnitBuf { shape: (n_attr, s, s), samples: Samples::U8(bytes) }], + }) +} + /// Why a layer, or a layer for one unit, was not applied: the closed list of /// staged-delivery §4.2, in the order of §4.1's steps. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] diff --git a/wai/wai-rs/tests/cli_stage.rs b/wai/wai-rs/tests/cli_stage.rs new file mode 100644 index 000000000..0086e6b53 --- /dev/null +++ b/wai/wai-rs/tests/cli_stage.rs @@ -0,0 +1,120 @@ +//! `wai stage`: the tool writes, applies, inspects, packs and explodes +//! staged-delivery layers (`extensions/staged-delivery.md`; SPEC §7.1), and `wai wrap` +//! refuses a refinement capability. These run the binary on the committed corpus +//! (`staged-conformance/`), whose expectations the corpus verifiers hold. + +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; + +fn wai_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("..") +} + +fn corpus(rel: &str) -> PathBuf { + wai_dir().join("staged-conformance").join(rel) +} + +fn scratch(name: &str) -> PathBuf { + let dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join("cli_stage"); + std::fs::create_dir_all(&dir).unwrap(); + let p = dir.join(name); + let _ = std::fs::remove_file(&p); + p +} + +fn wai(args: &[&std::ffi::OsStr]) -> Output { + Command::new(env!("CARGO_BIN_EXE_wai")).args(args).output().expect("run wai") +} + +fn os(s: &str) -> &std::ffi::OsStr { + std::ffi::OsStr::new(s) +} + +fn text(o: &Output) -> String { + format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr)) +} + +fn case(name: &str) -> serde_json::Value { + serde_json::from_slice(&std::fs::read(corpus(&format!("cases/{name}/case.json"))).unwrap()).unwrap() +} + +/// The tiny conditional codec's parameter set, by the names SPEC §5 gives its files. +fn tiny_set() -> PathBuf { + let dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join("cli_stage/tiny_set"); + std::fs::create_dir_all(&dir).unwrap(); + let v = wai_dir().join("wai-rs/tests/vectors/int_hyper"); + for (from, to) in [("tiny_entropy.json", "entropy.json"), ("tiny_g_s.bin", "g_s.whm"), ("tiny_h_s.bin", "h_s.whm")] { + std::fs::copy(v.join(from), dir.join(to)).unwrap(); + } + dir +} + +/// `wai stage apply` over a pinned integer base and two layers reaches the clip hash +/// the corpus states. +#[test] +fn apply_reaches_the_corpus_clip_hash() { + let c = case("image_int_hyper_2l"); + let d = corpus("cases/image_int_hyper_2l"); + let set = tiny_set(); + let o = wai(&[os("stage"), os("apply"), d.join("base.wai").as_os_str(), d.join("L1.wai").as_os_str(), d.join("L2.wai").as_os_str(), os("--set"), set.as_os_str()]); + let t = text(&o); + assert!(o.status.success(), "{t}"); + let want = c["expect"]["bases"][0]["clip_sha256"].as_str().unwrap(); + assert!(t.contains(&format!("clip sha256 {want}")), "{t}"); + assert!(t.contains("unit 0: stage 2"), "{t}"); +} + +/// `wai stage refine` at q = 1 with the identity predictor writes a layer that +/// `wai stage apply` brings exactly to the target, whose frames' SHA-256 it prints; +/// `wai stage inspect` reads the layer back. +#[test] +fn refine_then_apply_reaches_the_target() { + let d = corpus("cases/out_of_order"); + let target: Vec = (0..2 * 8 * 8 * 3).map(|i| ((i * 37) % 251) as u8).collect(); + let tpath = scratch("target.rgb"); + std::fs::write(&tpath, &target).unwrap(); + let lpath = scratch("L1.wai"); + let o = wai(&[os("stage"), os("refine"), os("--base"), d.join("base.wai").as_os_str(), os("--target"), tpath.as_os_str(), os("--q"), os("1"), os("-o"), lpath.as_os_str()]); + assert!(o.status.success(), "{}", text(&o)); + let o = wai(&[os("stage"), os("apply"), d.join("base.wai").as_os_str(), lpath.as_os_str()]); + let t = text(&o); + assert!(o.status.success(), "{t}"); + for f in 0..2 { + let sha = wai::pin::sha256_hex(&target[f * 192..(f + 1) * 192]); + assert!(t.contains(&format!("unit {f}: stage 1")) && t.contains(&sha), "frame {f}: {t}"); + } + let o = wai(&[os("stage"), os("inspect"), lpath.as_os_str()]); + let t = text(&o); + assert!(o.status.success() && t.contains("roles: ✓") && t.contains("layer 1") && t.contains("streams: ✓"), "{t}"); +} + +/// `wai stage pack` packs the corpus's live-style chain into a WAI2 that `wai stage +/// explode` turns back into each source, byte for byte. +#[test] +fn pack_then_explode_is_the_identity() { + let srcs: Vec = (0..4).map(|i| corpus(&format!("wai2/pack_live_chain.src{i}.wai"))).collect(); + let packed = scratch("packed.wai"); + let mut args: Vec<&std::ffi::OsStr> = vec![os("stage"), os("pack")]; + args.extend(srcs.iter().map(|p| p.as_os_str())); + args.extend([os("-o"), packed.as_os_str()]); + let o = wai(&args); + assert!(o.status.success(), "{}", text(&o)); + assert_eq!(std::fs::read(&packed).unwrap(), std::fs::read(corpus("wai2/pack_live_chain.wai")).unwrap(), "the corpus's packing"); + for (i, s) in srcs.iter().enumerate() { + let out = scratch(&format!("exploded{i}.wai")); + let o = wai(&[os("stage"), os("explode"), packed.as_os_str(), os(&i.to_string()), os("-o"), out.as_os_str()]); + assert!(o.status.success(), "{}", text(&o)); + assert_eq!(std::fs::read(out).unwrap(), std::fs::read(s).unwrap(), "component {i}"); + } +} + +/// `wai wrap` writes no `refines`, so it refuses a refinement capability and writes +/// nothing. +#[test] +fn wrap_refuses_a_refinement_capability() { + let out = scratch("wrapped.wai"); + let o = wai(&[os("wrap"), os("wai.video.int_refine"), corpus("cases/out_of_order/L1.wai").as_os_str(), os("-o"), out.as_os_str()]); + assert!(!o.status.success()); + assert!(text(&o).contains("refinement-only"), "{}", text(&o)); + assert!(!out.exists()); +} -- 2.54.0 (Apple Git-157) From 091d1c8a6da51cfbbac6c38ca3dfc355d2d40b59 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 2 Oct 2026 00:42:28 -0400 Subject: [PATCH 10/36] wai: the carriage tests hold a refinement to its own role A refinement-only capability is neither standalone media nor a companion: the carriage tests carry it as a WAI1 with refines, byte-exact, and report it carried as a companion as a refinement-only capability in another role. Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/tests/carriage.rs | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/wai/wai-rs/tests/carriage.rs b/wai/wai-rs/tests/carriage.rs index bedfd13db..1a026d36a 100644 --- a/wai/wai-rs/tests/carriage.rs +++ b/wai/wai-rs/tests/carriage.rs @@ -25,7 +25,7 @@ use wai::codecs::{ capability_license, capability_media, capability_tier, is_candidate, is_companion_only, - is_known_capability, known_capabilities, Media, + is_known_capability, is_refinement_only, known_capabilities, Media, }; use wai::container::{Rendition, RoleError, WaiMulti}; use wai::{Conditioning, Manifest, ModelRequirement, Wai}; @@ -62,6 +62,15 @@ fn wrap(cap: &str, payload: Vec) -> Wai { ) } +/// A refinement-only capability in the form SPEC §5 allows: a `WAI1` with `refines` +/// and no fallback (`extensions/staged-delivery.md` §3). +fn wrap_refinement(cap: &str, payload: Vec) -> Wai { + let h = "a".repeat(64); + let mut m = Manifest::new(media_for(cap), "replicate", ModelRequirement::new(cap), Conditioning::new("wir1")); + m.refines = Some(serde_json::json!({"base": h, "prev": h, "layer": 1})); + Wai::new(m, payload) +} + /// A companion-only capability in the form SPEC §5 allows: a `WAI2` companion /// bound to a base video rendition. Returns the parsed envelope. fn wrap_companion(cap: &str, payload: Vec) -> WaiMulti { @@ -93,6 +102,14 @@ fn every_registered_capability_carries_bytes_byte_exact() { assert!(capability_license(cap).is_some(), "{cap}: no license class"); continue; } + if is_refinement_only(cap) { + let back = Wai::from_bytes(&wrap_refinement(cap, payload.clone()).to_bytes().expect("serialize")).expect("parse"); + assert_eq!(back.check_component_roles(), Ok(()), "{cap}: refinement carriage must conform"); + assert_eq!(back.payload, payload, "{cap}: payload not carried byte-identically at {len} B"); + assert_eq!(back.refines().map(|r| r.map(|r| r.layer)), Some(Ok(1)), "{cap}: refines not preserved"); + assert!(capability_tier(cap).is_some() && capability_license(cap).is_some(), "{cap}: unclassified"); + continue; + } let env = wrap(cap, payload.clone()); let bytes = env.to_bytes().expect("serialize envelope"); let back = Wai::from_bytes(&bytes).expect("parse envelope"); @@ -151,6 +168,18 @@ fn no_standalone_capability_is_carried_as_a_companion() { continue; } let payload = payload_for(cap, 257); + if is_refinement_only(cap) { + // A refinement is not standalone media either: carried as a companion, the + // role check reports it as a refinement-only capability in another role. + let env = WaiMulti::new("video", "replicate", serde_json::Value::Null, vec![ + Rendition::media("wai.video.hevc", "hevc", payload_for("wai.video.hevc", 64)), + Rendition::companion(cap, payload.clone(), "wai.video.hevc"), + ]); + let back = WaiMulti::from_bytes(&env.to_bytes().expect("serialize envelope")).expect("parse envelope"); + assert_eq!(back.check_component_roles(), + Err(RoleError::RefinementOnlyAsComponent { index: 1, capability: cap.into() }), "{cap}"); + continue; + } let env = WaiMulti::new("video", "replicate", serde_json::Value::Null, vec![ Rendition::media("wai.video.hevc", "hevc", payload_for("wai.video.hevc", 64)), Rendition::companion(cap, payload.clone(), "wai.video.hevc"), -- 2.54.0 (Apple Git-157) From 48b41f21d38271a50f24def7dcf1e2d7a4985292 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Fri, 2 Oct 2026 00:45:31 -0400 Subject: [PATCH 11/36] wai: cross-architecture goldens for staged delivery byte-exact-conformance includes the WIR1 parser, predictors and arithmetic step and pins every stage of three corpus sets: a WIV1 GOP with per-frame and per-GOP layers, up2 to odd sizes, and a stereo rate ladder. The last stage of each must also be the clip hash its case states. Co-Authored-By: Claude Opus 5.5 --- wai/byte-exact-conformance/src/lib.rs | 7 + wai/byte-exact-conformance/src/staged.rs | 9 + .../src/staged_golden.rs | 166 ++++++++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 wai/byte-exact-conformance/src/staged.rs create mode 100644 wai/byte-exact-conformance/src/staged_golden.rs diff --git a/wai/byte-exact-conformance/src/lib.rs b/wai/byte-exact-conformance/src/lib.rs index b442bccc9..3ff907d42 100644 --- a/wai/byte-exact-conformance/src/lib.rs +++ b/wai/byte-exact-conformance/src/lib.rs @@ -75,9 +75,16 @@ pub mod int_mlicpp; #[path = "../../wai-rs/src/int_payload/wire.rs"] pub mod int_payload_wire; +// The staged-delivery residual (`WIR1`, extensions/staged-delivery.md Appendix A): its +// parser, predictors and arithmetic step. Self-contained beside the rANS coder and the +// histogram table already included here; the encoder is not used by the goldens. +#[allow(dead_code)] +pub mod staged; + mod golden; mod int_payload_golden; mod int_video_golden; mod neural_keyframe_golden; mod jpegai_golden; mod int_mlicpp_golden; +mod staged_golden; diff --git a/wai/byte-exact-conformance/src/staged.rs b/wai/byte-exact-conformance/src/staged.rs new file mode 100644 index 000000000..7d2832099 --- /dev/null +++ b/wai/byte-exact-conformance/src/staged.rs @@ -0,0 +1,9 @@ +//! The staged-delivery residual's sources, `#[path]`-included from wai-rs as siblings, +//! so `wir1.rs` finds `super::predict` and `super::wir1_arith` here as it does there. + +#[path = "../../wai-rs/src/staged/predict.rs"] +pub mod predict; +#[path = "../../wai-rs/src/staged/wir1_arith.rs"] +pub mod wir1_arith; +#[path = "../../wai-rs/src/staged/wir1.rs"] +pub mod wir1; diff --git a/wai/byte-exact-conformance/src/staged_golden.rs b/wai/byte-exact-conformance/src/staged_golden.rs new file mode 100644 index 000000000..6c0ff73d3 --- /dev/null +++ b/wai/byte-exact-conformance/src/staged_golden.rs @@ -0,0 +1,166 @@ +//! Cross-architecture byte-equality for staged delivery +//! (`extensions/staged-delivery.md`): the `WIR1` residual layers of three sets of the +//! corpus (`staged-conformance/`), decoded with the parser, predictors and arithmetic +//! step a sink runs (`staged/{wir1,predict,wir1_arith}.rs`, `#[path]`-included). +//! +//! - video: a `wai.video.int_motion` GOP (`WIV1`, decoded here by `int_video.rs`), a +//! layer 1 per frame, then a layer 2 over the GOP; +//! - image: the pinned 16×16 integer image's stage 0, up2 to 31×31, then up2 to 61×61 +//! (odd sizes, clamped neighbours); +//! - audio: a stereo stage 0 at 8 kHz, the rate predictor to 16 kHz, then to 48 kHz +//! (`rdiv`'s halves). +//! +//! Each stage's SHA-256 (the clip digest over its units, SPEC §7 *Digests*) is pinned +//! below, and the last stage's must also be the clip hash the case states. + +#![cfg(test)] + +use std::path::PathBuf; + +use sha2::{Digest, Sha256}; + +use crate::int_payload_wire::parse_wiv1; +use crate::int_video::{decode_typed, VideoStream}; +use crate::staged::wir1::{apply_unit, parse, Below, Next, UnitKind}; + +const VIDEO_STAGES: [&str; 3] = [ + "877fda1b49177f47d55298f28a280560555d8cace1dc6e9e6d370f2ce834b471", + "63ec8c064fcb36c3ab6e73854da46eace11eabf1aa010c0990b36d911ee408a4", + "4423cde72c6a314df503f5698532daa9917673ab871867b0ab5aa07c6d933694", +]; +const IMAGE_STAGES: [&str; 3] = [ + "4a501b0ee2468e4db07eb5f20e3408330604a231a1cd375a794adc5309197463", + "4695991ba30ab7d1129726f2c2cf743e4d892013b173a21c80fd4d67165e0239", + "64dba25135e5965fd511247477f911b85aa5a9b2fae263b64e791b72c9c43397", +]; +const AUDIO_STAGES: [&str; 3] = [ + "aab0d06a5d8fc29f47bd107940bd88dfd7d588980bbb72df54d5b8e9a3222006", + "e36b0ab5ed1f874edf0670cffd961133e7d9602cbc610fc2edf766a0d620c877", + "0408de38e23d42bd6af8064e109c73ab90582eaad588c68fbcf8b25fe9cf4c29", +]; + +fn case_dir(name: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../staged-conformance/cases").join(name) +} + +fn read(name: &str, file: &str) -> Vec { + let p = case_dir(name).join(file); + std::fs::read(&p).unwrap_or_else(|e| panic!("read {}: {e}", p.display())) +} + +/// A `WAI1` envelope's payload. +fn payload(env: &[u8]) -> Vec { + let ml = u32::from_le_bytes(env[4..8].try_into().unwrap()) as usize; + let at = 8 + ml; + let pl = u32::from_le_bytes(env[at..at + 4].try_into().unwrap()) as usize; + env[at + 4..at + 4 + pl].to_vec() +} + +fn hex(b: &[u8]) -> String { + b.iter().map(|x| format!("{x:02x}")).collect() +} + +#[derive(Clone)] +enum Buf { + U8(Vec), + I16(Vec), +} + +impl Buf { + fn bytes(&self) -> Vec { + match self { + Buf::U8(v) => v.clone(), + Buf::I16(v) => v.iter().flat_map(|s| s.to_le_bytes()).collect(), + } + } + fn below(&self) -> Below<'_> { + match self { + Buf::U8(v) => Below::U8(v), + Buf::I16(v) => Below::I16(v), + } + } +} + +/// The clip digest of `units`: SHA-256 of their SHA-256 digests in order. +fn clip(units: &[Buf]) -> String { + let mut all = Vec::new(); + for u in units { + all.extend_from_slice(&Sha256::digest(u.bytes())); + } + hex(&Sha256::digest(&all)) +} + +/// Apply `file` (a `WIR1` layer of `kind`) to `units[from..]`, unit by unit. +fn apply(name: &str, file: &str, kind: UnitKind, units: &mut [Buf], shapes: &mut [(u32, u32, u32)], from: usize) { + let p = payload(&read(name, file)); + let w = parse(&p, kind).unwrap_or_else(|e| panic!("{file}: {e}")); + for i in 0..w.n_units as usize { + let next = apply_unit(&w, i as u32, units[from + i].below(), shapes[from + i]).unwrap_or_else(|e| panic!("{file} unit {i}: {e}")); + units[from + i] = match next { + Next::U8(v) => Buf::U8(v), + Next::I16(v) => Buf::I16(v), + }; + shapes[from + i] = (w.d0, w.d1, w.d2); + } +} + +fn case_clip(name: &str, base: usize) -> String { + let c: serde_json::Value = serde_json::from_slice(&read(name, "case.json")).unwrap(); + c["expect"]["bases"][base]["clip_sha256"].as_str().unwrap().to_string() +} + +#[test] +fn staged_video_gop_is_byte_exact() { + let name = "video_int_motion_gop_perframe"; + let base = payload(&read(name, "base_0.wai")); + let v = parse_wiv1(&base, Some(0)).expect("WIV1"); + let s = VideoStream { + n_frames: v.n_frames as usize, h: v.h as usize, w: v.w as usize, block: v.block as usize, + q: v.q as i64, offset: v.offset, mv_precision: v.mv_precision, cdf: &v.cdf, mvs: &v.mvs, rans: v.rans, + }; + let mut units: Vec = decode_typed(&s).expect("decode").frames.into_iter().map(Buf::U8).collect(); + let mut shapes = vec![(v.h, v.w, 3); units.len()]; + let mut got = vec![clip(&units)]; + for f in 0..4 { + apply(name, &format!("g0_L1_f{f}.wai"), UnitKind::Rgb8, &mut units, &mut shapes, f); + } + got.push(clip(&units)); + apply(name, "g0_L2.wai", UnitKind::Rgb8, &mut units, &mut shapes, 0); + got.push(clip(&units)); + println!("video stages {got:?}"); + assert_eq!(got[2], case_clip(name, 0), "the final stage is the case's"); + assert_eq!(got, VIDEO_STAGES, "a stage of the video set diverged"); +} + +#[test] +fn staged_image_up2_odd_is_byte_exact() { + let name = "image_up2_odd"; + let mut units = vec![Buf::U8(read(name, "stage0.bin"))]; + let mut shapes = vec![(16, 16, 3)]; + let mut got = vec![clip(&units)]; + apply(name, "L1.wai", UnitKind::Rgb8, &mut units, &mut shapes, 0); + got.push(clip(&units)); + apply(name, "L2.wai", UnitKind::Rgb8, &mut units, &mut shapes, 0); + got.push(clip(&units)); + println!("image stages {got:?}"); + assert_eq!(shapes[0], (61, 61, 3)); + assert_eq!(got[2], case_clip(name, 0), "the final stage is the case's"); + assert_eq!(got, IMAGE_STAGES, "a stage of the image set diverged"); +} + +#[test] +fn staged_audio_rate_ladder_is_byte_exact() { + let name = "audio_flac_rate_ladder"; + let s0 = read(name, "stage0.bin"); + let mut units = vec![Buf::I16(s0.chunks_exact(2).map(|c| i16::from_le_bytes([c[0], c[1]])).collect())]; + let mut shapes = vec![(400, 1, 2)]; + let mut got = vec![clip(&units)]; + apply(name, "L1.wai", UnitKind::Pcm, &mut units, &mut shapes, 0); + got.push(clip(&units)); + apply(name, "L2.wai", UnitKind::Pcm, &mut units, &mut shapes, 0); + got.push(clip(&units)); + println!("audio stages {got:?}"); + assert_eq!(shapes[0], (2400, 1, 2)); + assert_eq!(got[2], case_clip(name, 0), "the final stage is the case's"); + assert_eq!(got, AUDIO_STAGES, "a stage of the audio set diverged"); +} -- 2.54.0 (Apple Git-157) From 5bd1cb9470f9fe48cbaa5b0d56ba390bbb8196b0 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 4 Oct 2026 14:10:20 -0400 Subject: [PATCH 12/36] ci(wai): staged delivery in the conformance, byte-equality and proof jobs The cargo-class build and the library tests add staged and the integer media features; the fast-kernel steps (overflow checks on and off) add staged and run the wai stage tests; staged is checked alone and for wasm32; the corpus loop verifies staged-conformance on every leg; staged_verify.py runs beside the other independent verifiers. The byte-equality job fires on the staged sources it includes and on the corpus, and the proof job on the two staged sources its harnesses cover. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-byte-equality.yml | 12 ++++++- .github/workflows/wai-conformance.yml | 35 +++++++++++++------ .github/workflows/wai-formal-verification.yml | 4 +++ 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/.github/workflows/wai-byte-equality.yml b/.github/workflows/wai-byte-equality.yml index 6f0f8ec05..34ef56ee6 100644 --- a/.github/workflows/wai-byte-equality.yml +++ b/.github/workflows/wai-byte-equality.yml @@ -15,7 +15,9 @@ name: wai cross-architecture byte-equality # and the MLIC++-topology codec (`wai.neural.int_mlicpp`), whose entropy network runs # before every checkerboard pass and must give every platform the encoder's scale # bucket for every element. The binary payloads SPEC §5 pins (WIV1, WIH1) are parsed -# with the wire parser a sink runs and decoded to the same digests. Video is the strictest leg — inter-frame decode is a +# with the wire parser a sink runs and decoded to the same digests, and the staged-delivery +# residual layers (WIR1) of the staged corpus are applied stage by stage to pinned digests. +# Video is the strictest leg — inter-frame decode is a # feedback loop, so one divergent half-pel rounding does not stay one byte, it # predicts the next frame and compounds for the rest of the clip. # @@ -40,6 +42,10 @@ on: - 'wai/wai-rs/src/int_hyper_synth.rs' - 'wai/wai-rs/src/int_mlicpp.rs' - 'wai/wai-rs/src/int_payload/wire.rs' + - 'wai/wai-rs/src/staged/predict.rs' + - 'wai/wai-rs/src/staged/wir1.rs' + - 'wai/wai-rs/src/staged/wir1_arith.rs' + - 'wai/staged-conformance/**' # The fast kernels int_synth, int_transform and int_hyper_synth dispatch to # by default: the goldens here decode through them. - 'wai/wai-rs/src/int_kernels.rs' @@ -65,6 +71,10 @@ on: - 'wai/wai-rs/src/int_hyper_synth.rs' - 'wai/wai-rs/src/int_mlicpp.rs' - 'wai/wai-rs/src/int_payload/wire.rs' + - 'wai/wai-rs/src/staged/predict.rs' + - 'wai/wai-rs/src/staged/wir1.rs' + - 'wai/wai-rs/src/staged/wir1_arith.rs' + - 'wai/staged-conformance/**' # The fast kernels int_synth, int_transform and int_hyper_synth dispatch to # by default: the goldens here decode through them. - 'wai/wai-rs/src/int_kernels.rs' diff --git a/.github/workflows/wai-conformance.yml b/.github/workflows/wai-conformance.yml index 4dbad796a..6c5c1786f 100644 --- a/.github/workflows/wai-conformance.yml +++ b/.github/workflows/wai-conformance.yml @@ -33,6 +33,7 @@ on: # A library test checks SPEC §5's pinned integer payloads against this # extension's sections, so an edit of it is checked here too. - 'wai/extensions/integer-payloads.md' + - 'wai/extensions/staged-delivery.md' - 'wai/wai-web/**' - '.github/workflows/wai-conformance.yml' pull_request: @@ -55,6 +56,7 @@ on: # A library test checks SPEC §5's pinned integer payloads against this # extension's sections, so an edit of it is checked here too. - 'wai/extensions/integer-payloads.md' + - 'wai/extensions/staged-delivery.md' - 'wai/wai-web/**' - '.github/workflows/wai-conformance.yml' @@ -114,11 +116,12 @@ jobs: # and is checkable on this matrix with no ONNX Runtime present. receipt_claims # is in the set because the reference meters (wai_meter, wai_world_meter) write a # measurement report by default and require it: without it they would drop out - # of this build on every leg of this matrix, without a word. - run: cargo build --no-default-features --features world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,receipt_claims + # of this build on every leg of this matrix, without a word. staged and the integer + # media features build the staged-delivery core, its corpus tool and `wai stage`. + run: cargo build --no-default-features --features world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,receipt_claims,staged,video_int,audio_int,splat_int - name: Library unit tests (the fixed-point determinism floors) working-directory: wai/wai-rs - run: cargo test --lib --no-default-features --features world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,oer,receipt_claims,key_discovery + run: cargo test --lib --no-default-features --features world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,oer,receipt_claims,key_discovery,staged,video_int,audio_int,splat_int # The JPEG AI exact decoder reproduces across platforms only if no platform math # library is on its path. Two guards, each failing the step: clippy's # disallowed-methods (wai-rs/clippy.toml), which each module on the decode path @@ -178,10 +181,10 @@ jobs: # wai.generate.spectral vector, which `generate` enables. - name: Fast integer kernels ≡ reference kernels (overflow checks on) working-directory: wai/wai-rs - run: cargo test --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate --lib --test int_kernels_vectors --test int_kernels_geometry --test int_mlicpp --test int_mlicpp_preflight --test int_audio --test int_splat --test generate --test int_payload --test int_payload_preflight --test int_payload_cost + run: cargo test --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate,staged --lib --test int_kernels_vectors --test int_kernels_geometry --test int_mlicpp --test int_mlicpp_preflight --test int_audio --test int_splat --test generate --test int_payload --test int_payload_preflight --test int_payload_cost --test cli_stage - name: Fast integer kernels ≡ reference kernels (release, overflow checks off) working-directory: wai/wai-rs - run: cargo test --release --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate --lib --test int_kernels_vectors --test int_kernels_geometry --test int_mlicpp --test int_mlicpp_preflight --test int_audio --test int_splat --test generate --test int_payload --test int_payload_preflight --test int_payload_cost + run: cargo test --release --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate,staged --lib --test int_kernels_vectors --test int_kernels_geometry --test int_mlicpp --test int_mlicpp_preflight --test int_audio --test int_splat --test generate --test int_payload --test int_payload_preflight --test int_payload_cost --test cli_stage # The integer decode path builds under each of its features alone, so a sink can take # one medium without the others, and for wasm32 (a browser sink); `int_payload` arms a # medium only under its feature, and none needs `neural`. @@ -191,11 +194,11 @@ jobs: working-directory: wai/wai-rs run: | set -euo pipefail - for f in neural_int video_int audio_int splat_int; do + for f in neural_int video_int audio_int splat_int staged; do cargo check --lib --no-default-features --features "$f" done rustup target add --toolchain 1.98.0 wasm32-unknown-unknown - cargo check --lib --target wasm32-unknown-unknown --no-default-features --features video_int,audio_int,splat_int + cargo check --lib --target wasm32-unknown-unknown --no-default-features --features video_int,audio_int,splat_int,staged # carriage: every registered capability round-trips byte-exact. cli_wrap_intent and # cli_registry run the `wai` binary, which builds without the codecs feature: the # intent rule of `wai wrap`, its refusal of candidate capabilities (SPEC §5), its media @@ -266,7 +269,7 @@ jobs: working-directory: wai/wai-rs run: | set -euo pipefail - F=world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,quantum,quantum_ops,oer,receipt_claims,key_discovery,video_int,audio_int,splat_int + F=world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,quantum,quantum_ops,oer,receipt_claims,key_discovery,video_int,audio_int,splat_int,staged v() { echo "::group::$1"; cargo run -q --no-default-features --features "$F" --bin "$2" -- verify "$3"; echo "::endgroup::"; } v worlds wai_world_vectors ../world-conformance v spatial-audio wai_audio_vectors ../audio-conformance @@ -285,6 +288,7 @@ jobs: v pin wai_pin_vectors ../pin-conformance v signers wai_signer_vectors ../signer-conformance v int-payload wai_int_payload_vectors ../int-payload-conformance + v staged wai_staged_vectors ../staged-conformance # The pin corpus's second implementation: Python standard library only, sharing no # code with wai-rs. One leg is enough — it checks digests and listing rules, which # do not depend on the architecture — and the macOS host has python3. @@ -313,7 +317,7 @@ jobs: shell: pwsh working-directory: wai/wai-rs run: | - $F = "world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,quantum,quantum_ops,oer,receipt_claims,key_discovery,video_int,audio_int,splat_int" + $F = "world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,quantum,quantum_ops,oer,receipt_claims,key_discovery,video_int,audio_int,splat_int,staged" function v($grp, $bin, $path) { Write-Output "::group::$grp" cargo run -q --no-default-features --features $F --bin $bin -- verify $path @@ -337,6 +341,7 @@ jobs: v pin wai_pin_vectors ../pin-conformance v signers wai_signer_vectors ../signer-conformance v int-payload wai_int_payload_vectors ../int-payload-conformance + v staged wai_staged_vectors ../staged-conformance # The generated v2 vocabulary (wai/vocab/v2.*) is checked as a consumer uses it: # sample manifests, typed and untyped, WAI1 and WAI2, are expanded through the @@ -367,7 +372,8 @@ jobs: # under a fixed seed, so a run is reproducible), jwp_verify.py (every JWP vector), and # signer_verify.py (every key-discovery vector and the canonical fixture, with its # verdicts required to equal wai_signer_vectors' line for line), and - # int_payload_verify.py (the integer payloads and their refusal fixtures). + # int_payload_verify.py (the integer payloads and their refusal fixtures), and + # staged_verify.py (the staged-delivery corpus). # Each exits non-zero on any failure or any difference, and the step fails with it. # One platform: what they check is the reading of the text, not the architecture. independent-verifiers: @@ -412,6 +418,15 @@ jobs: working-directory: wai shell: bash run: python3 int-payload-conformance/int_payload_verify.py int-payload-conformance + # The staged-delivery corpus's second implementation (staged-conformance/README): it + # replays every case from its stage-0 buffers through its own reading of the + # extension (refines, WIR1, the sink's steps, holds, batches, conflicts, deadlines, + # tiers, stage hashes), decodes the WIV1 keyframe-0, PNG and FLAC bases, explodes the + # WAI2 files, and replays the schedule. Standard library only. + - name: staged_verify.py + working-directory: wai + shell: bash + run: python3 staged-conformance/staged_verify.py staged-conformance - name: claim_verify.py, differential.py, jwp_verify.py, signer_verify.py working-directory: wai shell: bash diff --git a/.github/workflows/wai-formal-verification.yml b/.github/workflows/wai-formal-verification.yml index cc95ff47d..dfb7cadbb 100644 --- a/.github/workflows/wai-formal-verification.yml +++ b/.github/workflows/wai-formal-verification.yml @@ -33,6 +33,8 @@ on: - 'wai/wai-rs/src/int_entropy.rs' - 'wai/wai-rs/src/int_transform.rs' - 'wai/wai-rs/src/wai_video_policy.rs' + - 'wai/wai-rs/src/staged/predict.rs' + - 'wai/wai-rs/src/staged/wir1_arith.rs' # Not verified by any harness, but #[path]-included because int_transform's # IGDN dispatches to it, so it must still compile under the model checker # (its cfg(kani) branches are compiled nowhere else). @@ -47,6 +49,8 @@ on: - 'wai/wai-rs/src/int_entropy.rs' - 'wai/wai-rs/src/int_transform.rs' - 'wai/wai-rs/src/wai_video_policy.rs' + - 'wai/wai-rs/src/staged/predict.rs' + - 'wai/wai-rs/src/staged/wir1_arith.rs' - 'wai/wai-rs/src/int_kernels.rs' - '.github/workflows/wai-formal-verification.yml' -- 2.54.0 (Apple Git-157) From ea174e7a6f52e0b1cc171df432a6d2fe360b1e57 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 4 Oct 2026 14:51:53 -0400 Subject: [PATCH 13/36] wai: Kani proofs of the staged arithmetic, predictors and indices wai-verify includes staged/predict.rs and staged/wir1_arith.rs, whose harnesses prove: step_in_range (for every residual the rANS decoder returns, every step q and both element ranges, pred + r*q fits i64 and the element is that sum clamped), up2_bounded (the half-pel prediction lies between the neighbours it reads, odd output sizes included), rdiv_matches_spec (rdiv is the rounding half away from zero, stated by inequalities rather than a second division) and rate_index_in_bounds (every output sample's indices are source samples, for every channel). The last two are nonlinear and hold over the bounded windows their docs state; each harness's cover is reached. rdiv and the rate indices move from i128/u128 to checked 64-bit arithmetic, which is total for every input and gives the same values for every shape a WIR1 payload allows; the corpus and the goldens are unchanged. The CHANGELOG gains the staged-delivery subsection. Co-Authored-By: Claude Opus 5.5 --- wai/CHANGELOG.md | 71 +++++++++++++++++ wai/wai-rs/src/staged/predict.rs | 118 ++++++++++++++++++++++++---- wai/wai-rs/src/staged/wir1_arith.rs | 24 ++++++ wai/wai-verify/src/lib.rs | 8 ++ 4 files changed, 207 insertions(+), 14 deletions(-) diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index bd77e07a8..72ba4558f 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -6,6 +6,77 @@ wire format may change and such changes are recorded here. ## v0.4.0 — current +### Staged delivery: refinement layers, base admission, and WAI2 refinement components + +- **The extension** [staged-delivery](extensions/staged-delivery.md): a stage set is a + base and layers, each layer its own `WAI1` envelope naming, by content hash, the + base and the producer each unit it covers is expected to have (`refines`: `base`, + `prev` as one producer or runs, `layer`, `span`). A sink holds per unit a stage, a + producer and a tier, and applies a layer per unit by one algorithm with a closed + list of reasons (§4.1, §4.2): every envelope-scoped step (well-formed `refines`, a + refinement capability, the base present, not through a fallback, admitted, attested, + a well-formed payload within the sink's cost limit) before the unit steps + (conflict, readiness, geometry, deadline). Layers a unit is not ready for are held + to a declared bound and released when its stage advances; layers released together + form a batch, so two conflicting layers released together are both refused and the + unit frozen, whatever their order. A layer that differs from one that already passed + for the same unit freezes it at its stage. A unit's tier is the weakest of its base + and layers, `entropy-consistency` does not pass through a layer, and a stage hash is + stated only at `decode-equivalence`. Deferral, live deadlines (a sink never delays a + unit for a layer, and counts the units presented below the stage their layers would + have reached on time), attestation (a receipt verified without the layer's bytes + never authorizes an application) and byte counting (envelope bytes; receipts, + framing and parameter sets separately; never JSON or base64) are stated. +- **The residual payload `WIR1`** (Appendix A): one rANS stream per unit; + `clamp(pred + r·q)` with the identity, up2 (the half-pel arithmetic, odd sizes + included) and rate (linear interpolation, `rdiv`'s halves away from zero, channels + interleaved) predictors. It follows the integer payloads' rules: a parse in a + stated order with the closed list of their codes, caps, a declared cost a sink reads + before decoding, refusal at the first symbol a stream runs out on, canonical stream + ends; and it reads no parameter set. +- **Refinement capabilities** (SPEC §5, *Refinement capabilities*): + `wai.image.int_refine`, `wai.video.int_refine`, `wai.audio.int_refine` and + `wai.splat.int_refine`, each refinement-only, royalty-free, `StandardDefined` and + exact given the base it refines. SPEC §8 adds the refinement-only role; SPEC §4 never + dispatches one, and a sink that does not implement the extension finds it + unsupported. The registry gains the `refinement` kind and role, and the export is + regenerated. *Envelopes this makes non-conforming* (they still parse): a `WAI1` + carrying a refinement capability without `refines`, with a fallback, or as a + fallback; `refines` on any other capability; a refinement capability as a `WAI2` + rendition or companion; a companion bound to one. +- **Base admission is one table** (SPEC §5, *Base admission*; + `codecs::BASE_ADMISSION`): the bases of each refinement and of the LCEVC enhancement + layer, whose bases are unchanged. `enhancement_bases`, `companion_applies_to` and + `admits` read it. +- **WAI2 refinement components** (SPEC §7.1), on the component identity (`id`, + `sha256`, `prior`; no other digest is defined): an entry with + `role: "refinement"` carries `refines` naming entries by `id`, follows every + selectable entry, is never selected, and is checked by `check_component_roles` in a + stated order. *Exploding a component* writes its `WAI1` envelope, from + `envelope_manifest` verbatim or from the `WAI2` manifest's members as written, with + ids replaced by the content hashes of the named entries' exploded envelopes, so a + component and the layer carried alone are the same object; `WaiMulti::pack` packs + `WAI1` envelopes with their manifests verbatim, and they explode back to the same + bytes. A malformed `refines` or a mismatched `envelope_manifest` does not make a + manifest unreadable. +- **Reference implementation**: `staged::{StageChain, StageSink, wir1, predict, + wir1_arith, tier, schedule}` (feature `staged`); `container::{Refines, Prev}`; + `WaiMulti::{explode, explode_all, pack, check_envelope_manifests, refinements_of}`; + `wai stage refine|apply|inspect|pack|explode`; `wai wrap` refuses a refinement + capability, and `wai inspect` shows `refines`. `RansEncoder` writes the bypass + escape, and `int_entropy::hist_to_cdf` builds a table from a histogram in integer + arithmetic. In `ternary::rans_encode_trits`, the encode call sat inside + `debug_assert!`, so a release build coded no symbol; it now runs in every build. +- **Conformance**: the corpus [staged-conformance](staged-conformance/) (image, video, + audio and splat sets; one case per refusal and order rule; `WAI2` identity cases; the + live schedule), written and verified by `wai_staged_vectors` and verified + independently by `staged_verify.py`, which shares no code with `wai-rs`; the pure- + Python BLAKE3 moves to `claim-conformance/_blake3.py`, shared by both Python + verifiers. Cross-architecture goldens pin every stage of an image, a video and an + audio set; Kani proves, over bounded windows, the arithmetic step, the up2 bounds, + `rdiv` and the rate indices. `rdiv` and the rate indices are computed in checked + 64-bit arithmetic. + ### The integer codecs' payloads pinned in place: `WIV1`, `WIH1`, `WIA1`, `WIS1` - **Pinned in place** (SPEC §5, *Payloads of the WAI-native integer codecs*, and the diff --git a/wai/wai-rs/src/staged/predict.rs b/wai/wai-rs/src/staged/predict.rs index 2d135b87f..278e4235e 100644 --- a/wai/wai-rs/src/staged/predict.rs +++ b/wai/wai-rs/src/staged/predict.rs @@ -8,21 +8,25 @@ //! from zero (`rdiv`), channels interleaved. //! //! Every value is an integer of at most 64 bits, and every index is in bounds for the -//! shapes the caller checked (the Kani harnesses of `wai-verify` check `up2_at`'s -//! bounds, `rdiv` against its definition and `rate_at`'s indices). This file is +//! shapes the caller checked (the Kani harnesses below, run by `wai-verify`, check +//! `up2_at`'s bounds, `rdiv` against its definition and `rate_index`'s indices). This file is //! self-contained (no `crate::` or `super::` path, `core` only) so the model checker //! and the cross-architecture byte-equality crate include it as it is. /// `n / d` rounded to the nearest integer, halves away from zero: -/// `sign(n)·⌊(2|n| + d) / 2d⌋`, for `d ≥ 1`. Formed in `i128`, so it is total for -/// every `n` and every `d ≥ 1`; `d < 1` gives 0. +/// `sign(n)·⌊(2|n| + d) / 2d⌋`, for `d ≥ 1`, computed as the quotient `q` and remainder +/// `r` of `|n|` by `d`, plus one where `2r ≥ d`. In `u64`, where no step can overflow +/// (`r < d`, and `q < 2^62` whenever `d ≥ 2`), so it is total for every `n` and every +/// `d ≥ 1`; `d < 1` gives 0. pub fn rdiv(n: i64, d: i64) -> i64 { if d < 1 { return 0; } - let (n, d) = (n as i128, d as i128); - let q = (2 * n.abs() + d) / (2 * d); - (if n < 0 { -q } else { q }) as i64 + let (m, d) = (n.unsigned_abs(), d as u64); + let (q, r) = (m / d, m % d); + let mag = q + (r >= d - r) as u64; + // |result| ≤ 2^63, and 2^63 only for n = i64::MIN, d = 1, whose result is n. + if n < 0 { (mag as i64).wrapping_neg() } else { mag as i64 } } /// The up2 prediction of element `(y, x, ch)` of an `out_h × out_w` picture from @@ -58,13 +62,13 @@ pub fn rate_index(i: u64, n: u64, num: u64, den: u64) -> Option<(u64, u64, u64)> if num == 0 || den == 0 || n == 0 { return None; } - let s = (i as u128) * (den as u128); - let lo = s / num as u128; - let rho = (s % num as u128) as u64; - if lo >= n as u128 { + // Below the length `rate_len` gives, i·den < n·num, which is under 2^58 for the + // shapes a WIR1 payload allows (n ≤ 2^26, num < 2^32): checked, and None past u64. + let s = i.checked_mul(den)?; + let (lo, rho) = (s / num, s % num); + if lo >= n { return None; } - let lo = lo as u64; Some((lo, (lo + 1).min(n - 1), rho)) } @@ -86,12 +90,98 @@ pub fn rate_at(prev: &[i16], c: usize, num: u32, den: u32, i: usize, ch: usize) } /// The number of output samples per channel the rate predictor gives from `n` source -/// samples: `⌊n·num/den⌋`, or `None` for `den` 0 or a count past `u64`. +/// samples: `⌊n·num/den⌋`, or `None` for `den` 0 or `n·num` past `u64` (a WIR1 unit +/// holds at most 2^26 samples, so `n·num` is under 2^58 there). pub fn rate_len(n: u64, num: u32, den: u32) -> Option { if den == 0 { return None; } - u64::try_from((n as u128) * (num as u128) / (den as u128)).ok() + Some(n.checked_mul(num as u64)? / den as u64) +} + +// ───────────────────────────────────────────────────────────────────── +// Kani proof harnesses (wai-verify includes this file) +// ───────────────────────────────────────────────────────────────────── + +/// `up2_bounded`: on every picture of up to 2×2 pixels and at every output position +/// of the 2× picture (odd output sizes, which leave the last row or column unused, +/// included), the prediction lies between the least and the greatest of the +/// neighbours it reads, so the half-pel rounding never leaves their range. +#[cfg(kani)] +#[kani::proof] +#[kani::unwind(5)] +fn up2_bounded() { + let h2: usize = kani::any(); + let w2: usize = kani::any(); + kani::assume((1..=2).contains(&h2) && (1..=2).contains(&w2)); + let prev: [u8; 4] = kani::any(); + let y: usize = kani::any(); + let x: usize = kani::any(); + kani::assume(y < 2 * h2 && x < 2 * w2); + let v = up2_at(&prev[..h2 * w2], h2, w2, 1, y, x, 0); + let p = |yy: usize, xx: usize| prev[yy.min(h2 - 1) * w2 + xx.min(w2 - 1)] as i64; + let (iy, ix) = (y >> 1, x >> 1); + let ns = [p(iy, ix), p(iy, ix + 1), p(iy + 1, ix), p(iy + 1, ix + 1)]; + let (mut lo, mut hi) = (ns[0], ns[0]); + for n in ns { + lo = lo.min(n); + hi = hi.max(n); + } + kani::assert(lo <= v && v <= hi, "the prediction lies between its neighbours"); + kani::cover!(y & 1 == 1 && x & 1 == 1, "the four-neighbour case is reached"); +} + +/// `rdiv_matches_spec`: `rdiv(n, d)` is `|n|/d` rounded half away from zero with the +/// sign of `n`, stated without division: its magnitude `k` is the one integer with +/// `(2k − 1)·d ≤ 2|n| < (2k + 1)·d` (for `k = 0`, `2|n| < d`). For `|n| < 2^12` and `d` +/// in `1..=2^6`: the obligation is nonlinear, which the solver discharges only over a +/// window; the unit tests cover `i64`'s extremes. +#[cfg(kani)] +#[kani::proof] +fn rdiv_matches_spec() { + let n: i64 = kani::any(); + let d: i64 = kani::any(); + kani::assume(n > -(1i64 << 12) && n < (1i64 << 12)); + kani::assume(d >= 1 && d <= (1i64 << 6)); + let v = rdiv(n, d); + kani::assert(v == 0 || (v < 0) == (n < 0), "the sign is n's"); + let (k, m, d) = (v.unsigned_abs(), n.unsigned_abs(), d as u64); + kani::assert(2 * m < (2 * k + 1) * d, "below the next half"); + kani::assert(k == 0 || (2 * k - 1) * d <= 2 * m, "at or above the half below"); + kani::cover!(k >= 1 && (2 * k - 1) * d == 2 * m, "a half is reached"); +} + +/// `rate_index_in_bounds`: for every output sample `i` the rate predictor forms +/// (`i < ⌊n·num/den⌋`), with `n` source samples of `c` channels, both indices it reads +/// are samples of the source (`lo < n`, `hi < n`), so `lo·c + ch < n·c` for every +/// channel `ch < c`; `hi` is `min(lo + 1, n − 1)` and the phase is below `num`. For +/// `n`, `num` and `den` up to 32 and up to 4 channels: the obligation +/// (`i < ⌊n·num/den⌋ ⇒ ⌊i·den/num⌋ < n`) is nonlinear, which the solver discharges +/// only over a narrow window; the unit tests and the corpus cover larger rates. +#[cfg(kani)] +#[kani::proof] +fn rate_index_in_bounds() { + let n: u64 = kani::any(); + let num: u32 = kani::any(); + let den: u32 = kani::any(); + let i: u64 = kani::any(); + kani::assume((1..=32).contains(&n) && (1..=32).contains(&num) && (1..=32).contains(&den)); + let len = rate_len(n, num, den); + kani::assert(len.is_some(), "the output length fits u64"); + kani::assume(i < len.unwrap_or(0)); + let ix = rate_index(i, n, num as u64, den as u64); + kani::assert(ix.is_some(), "every output sample has its indices"); + if let Some((lo, hi, rho)) = ix { + kani::assert(lo < n && hi < n, "both indices are source samples"); + kani::assert(hi == (lo + 1).min(n - 1), "hi is the next sample, held at the end"); + kani::assert(rho < num as u64, "the phase is below num"); + let c: u64 = kani::any(); + let ch: u64 = kani::any(); + kani::assume((1..=4).contains(&c) && ch < c); + kani::assert(lo * c + ch < n * c && hi * c + ch < n * c, "every channel's index is in the buffer"); + } + kani::cover!(num > den, "an upsampling is reached"); + kani::cover!(num < den, "a downsampling is reached"); } #[cfg(test)] diff --git a/wai/wai-rs/src/staged/wir1_arith.rs b/wai/wai-rs/src/staged/wir1_arith.rs index 9fd051ef0..3d5712440 100644 --- a/wai/wai-rs/src/staged/wir1_arith.rs +++ b/wai/wai-rs/src/staged/wir1_arith.rs @@ -18,3 +18,27 @@ pub const I16_RANGE: (i64, i64) = (-32768, 32767); pub fn step(pred: i64, r: i64, q: i64, lo: i64, hi: i64) -> i64 { pred.saturating_add(r.saturating_mul(q)).clamp(lo, hi.max(lo)) } + +/// `step_in_range`: for every `r` the rANS decoder returns, every step `q` in +/// `1..=65535`, both element ranges and every prediction in the range, `pred + r·q` +/// fits `i64` without saturating, and the step is that sum clamped to the range. So a +/// build with overflow checks and one without compute the same element. +#[cfg(kani)] +#[kani::proof] +fn step_in_range() { + let r: i64 = kani::any(); + kani::assume(r >= -(1i64 << 31) && r < (1i64 << 31)); + let q: i64 = kani::any(); + kani::assume((1..=65_535).contains(&q)); + let wide: bool = kani::any(); + let (lo, hi) = if wide { I16_RANGE } else { U8_RANGE }; + let pred: i64 = kani::any(); + kani::assume(pred >= lo && pred <= hi); + let exact = r.checked_mul(q).and_then(|m| pred.checked_add(m)); + kani::assert(exact.is_some(), "pred + r·q fits i64"); + let v = step(pred, r, q, lo, hi); + kani::assert(v >= lo && v <= hi, "the element is in its range"); + kani::assert(Some(v) == exact.map(|e| e.clamp(lo, hi)), "the step is the exact sum, clamped"); + kani::cover!(v == lo, "the clamp's floor is reached"); + kani::cover!(v == hi, "the clamp's ceiling is reached"); +} diff --git a/wai/wai-verify/src/lib.rs b/wai/wai-verify/src/lib.rs index 9085b73f5..a31cdbb8c 100644 --- a/wai/wai-verify/src/lib.rs +++ b/wai/wai-verify/src/lib.rs @@ -42,3 +42,11 @@ pub mod int_kernels; /// crypto layer — a stand-in would close the goal without deciding it. #[path = "../../wai-rs/src/wai_video_policy.rs"] pub mod wai_video_policy; + +/// The staged-delivery residual (`extensions/staged-delivery.md` Appendix A): its +/// predictors (`up2_bounded`, `rdiv_matches_spec`, `rate_index_in_bounds`) and its one +/// arithmetic step (`step_in_range`). Self-contained, `core` only. +#[path = "../../wai-rs/src/staged/predict.rs"] +pub mod staged_predict; +#[path = "../../wai-rs/src/staged/wir1_arith.rs"] +pub mod staged_wir1_arith; -- 2.54.0 (Apple Git-157) From e4b46cbd4c8391d61fc5300cefc01bd6b771469b Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 4 Oct 2026 15:54:49 -0400 Subject: [PATCH 14/36] wai: the sidecar coverage test names where the staged corpus states its digests every_decoded_output_states_a_sha256 requires every conformance directory to carry a sha256.json sidecar or a stated reason; the staged corpus states the SHA-256 of every stage-0 buffer and every unit's stage in its case.json. Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/tests/int_kernels_vectors.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/wai/wai-rs/tests/int_kernels_vectors.rs b/wai/wai-rs/tests/int_kernels_vectors.rs index 586c4ba4a..fcccbec42 100644 --- a/wai/wai-rs/tests/int_kernels_vectors.rs +++ b/wai/wai-rs/tests/int_kernels_vectors.rs @@ -223,6 +223,7 @@ fn every_decoded_output_states_a_sha256() { ("signer-conformance", "key logs, grants and statuses: signed documents, no decoded output"), ("byte-exact-conformance", "goldens over tests/vectors, whose sidecars state the SHA-256"), ("int-payload-conformance", "refusal fixtures and the index of the payloads under tests/vectors, whose sidecars state the SHA-256"), + ("staged-conformance", "each case.json states the SHA-256 of every stage-0 buffer and of every unit's stage (SPEC §7 Digests)"), ("vectors/int_mlicpp", "expected.json states rgb_sha256 (RGB8)"), ("vectors/jpegai", "inputs only; the decoded pixels' SHA-256 is pinned by the JPEG AI e2e job"), ]; -- 2.54.0 (Apple Git-157) From 3d4f6ca03d37cbf6ddb253ac50de50e1fe1be156 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 4 Oct 2026 17:21:24 -0400 Subject: [PATCH 15/36] wai: the stage chain parses a layer once, keeps a chain budget, drops what no unit holds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - A layer's WIR1 payload is parsed once, at §4.1 step 6. Wir1 keeps its tables flat and its streams as end offsets beside the payload, so a parse holds its declared working bytes (which now count 8 bytes per unit for the stream index), and a unit's output is allocated at its exact size. - Step 7 compares a unit's key with the producers of its stages (a unit's stage rises by one per application) and a batch is grouped by unit; held units are kept per unit and counted per layer; prev's runs are indexed once and bisected. An offer's work and memory grow linearly with the layer's units (tests/staged_resources.rs, a counting allocator, in both CI profiles). - The chain budget (§4.5; chain-over-limit at step 6, before any stream is decoded) bounds the units' output, 32 bytes per stage, and every retained layer's payload, declared output and declared working bytes. A layer no unit is held for is dropped at the end of the offer, release or expiry. - Decoding again a base the sink holds keeps its chain and reports nothing; StageSink::remove_base drops a chain. - Shape products are checked: one past u64 is geometry-mismatch. - A layer whose manifest names a fallback is refused at step 1. - A layer refused when its base arrives has one report. - BaseInfo::first_unit places a base's units on the clock a set split across bases shares. - A layer held for its base and offered again with stronger receipt evidence keeps it; over an attested base it counts as arrived when its receipt was verified. - A payload whose unit count is not the span's is payload-malformed with the code field_range; A.6 states how step 6 and step 9 split A.4. - span: null is absent; SPEC §2 states what a JSON integer is. - base_never_waits plays random arrival traces through a live StageSink. The corpus is regenerated through its generator (the config's budget, each base's first_unit); every case keeps its outcome. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-conformance.yml | 10 +- wai/SPEC.md | 6 + .../src/staged_golden.rs | 2 +- wai/extensions/staged-delivery.md | 113 ++- .../cases/audio_flac_rate_ladder/case.json | 2 + .../cases/audio_int_codec_1l/case.json | 2 + .../cases/base_not_admitted/case.json | 2 + .../cases/base_via_fallback/case.json | 2 + .../cases/bypass_extremes/case.json | 2 + .../cases/capability_not_refinement/case.json | 2 + .../cases/cdf_malformed/case.json | 2 + .../cases/clamp_extremes/case.json | 2 + .../cases/conflict_after_apply/case.json | 2 + .../cases/conflict_held/case.json | 2 + .../cases/cost_over_limit/case.json | 2 + .../cases/deadline_missed/case.json | 2 + .../cases/geometry_mismatch/case.json | 2 + .../cases/hold_bound_count/case.json | 2 + .../cases/image_int_hyper_2l/case.json | 2 + .../cases/image_int_hyper_unpinned/case.json | 2 + .../cases/image_png_up2/case.json | 2 + .../cases/image_up2_from_int_hyper/case.json | 2 + .../cases/image_up2_odd/case.json | 2 + .../cases/inclusion_only_evidence/case.json | 2 + .../cases/layer_zero/case.json | 2 + .../cases/missing_layer/case.json | 2 + .../cases/out_of_order/case.json | 2 + .../cases/payload_truncated/case.json | 2 + .../cases/pending_l1_l2/case.json | 2 + wai/staged-conformance/cases/q_zero/case.json | 2 + .../cases/refinement_without_base/case.json | 2 + .../cases/refines_malformed/case.json | 2 + .../cases/splat_attr_1l/case.json | 2 + .../cases/stream_exhausted/case.json | 2 + .../cases/stream_noncanonical/case.json | 2 + .../cases/video_int_hyper_keyframe/case.json | 2 + .../video_int_motion_gop_perframe/case.json | 4 + .../cases/video_mixed_stage_l2/case.json | 2 + .../cases/wrong_base/case.json | 2 + .../cases/wrong_prev/case.json | 2 + wai/wai-rs/Cargo.toml | 5 + wai/wai-rs/src/bin/wai_cli/stage.rs | 7 +- wai/wai-rs/src/bin/wai_staged_vectors.rs | 17 +- wai/wai-rs/src/container.rs | 13 +- wai/wai-rs/src/staged/mod.rs | 730 ++++++++++++------ wai/wai-rs/src/staged/schedule.rs | 18 +- wai/wai-rs/src/staged/tests.rs | 319 ++++++-- wai/wai-rs/src/staged/wir1.rs | 133 ++-- wai/wai-rs/tests/staged_resources.rs | 325 ++++++++ 49 files changed, 1376 insertions(+), 396 deletions(-) create mode 100644 wai/wai-rs/tests/staged_resources.rs diff --git a/.github/workflows/wai-conformance.yml b/.github/workflows/wai-conformance.yml index 6c5c1786f..1ece4fb70 100644 --- a/.github/workflows/wai-conformance.yml +++ b/.github/workflows/wai-conformance.yml @@ -171,7 +171,11 @@ jobs: # allocator) checks that a header past the caps is refused before anything sized from it # is allocated; tests/int_payload_cost.rs (its own binary, a live-bytes allocator) checks # that int_payload::declared_cost bounds what each decode holds, and, in the release - # step only, decodes a 2^30-byte clip a 3 KB payload codes. Cargo.toml declares them, and the other + # step only, decodes a 2^30-byte clip a 3 KB payload codes. tests/staged_resources.rs (its + # own binary, a counting allocator) checks that the staged sink's work and memory per + # layer grow linearly in the layer's units, that a layer past the chain budget is + # refused before any stream is decoded, that what no unit holds is dropped, and that a + # WIR1 parse holds no more than its declared working bytes. Cargo.toml declares them, and the other # feature-gated targets named here, with required-features, so if this step ever loses a # feature, cargo stops with an error instead of compiling an empty binary that reports ok. # Each committed vector's decoded output is also checked against the SHA-256 in @@ -181,10 +185,10 @@ jobs: # wai.generate.spectral vector, which `generate` enables. - name: Fast integer kernels ≡ reference kernels (overflow checks on) working-directory: wai/wai-rs - run: cargo test --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate,staged --lib --test int_kernels_vectors --test int_kernels_geometry --test int_mlicpp --test int_mlicpp_preflight --test int_audio --test int_splat --test generate --test int_payload --test int_payload_preflight --test int_payload_cost --test cli_stage + run: cargo test --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate,staged --lib --test int_kernels_vectors --test int_kernels_geometry --test int_mlicpp --test int_mlicpp_preflight --test int_audio --test int_splat --test generate --test int_payload --test int_payload_preflight --test int_payload_cost --test cli_stage --test staged_resources - name: Fast integer kernels ≡ reference kernels (release, overflow checks off) working-directory: wai/wai-rs - run: cargo test --release --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate,staged --lib --test int_kernels_vectors --test int_kernels_geometry --test int_mlicpp --test int_mlicpp_preflight --test int_audio --test int_splat --test generate --test int_payload --test int_payload_preflight --test int_payload_cost --test cli_stage + run: cargo test --release --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate,staged --lib --test int_kernels_vectors --test int_kernels_geometry --test int_mlicpp --test int_mlicpp_preflight --test int_audio --test int_splat --test generate --test int_payload --test int_payload_preflight --test int_payload_cost --test cli_stage --test staged_resources # The integer decode path builds under each of its features alone, so a sink can take # one medium without the others, and for wasm32 (a browser sink); `int_payload` arms a # medium only under its feature, and none needs `neural`. diff --git a/wai/SPEC.md b/wai/SPEC.md index 65ecd90de..86444110b 100644 --- a/wai/SPEC.md +++ b/wai/SPEC.md @@ -69,6 +69,12 @@ OPTIONAL; §5 says which are RECOMMENDED. whitespace, preserving key insertion order. Readers MUST NOT depend on key order for *semantics*; encoders SHOULD preserve order so files round-trip byte-identically. +- An integer member of a manifest, or of any JSON this specification or + its extensions define, is a JSON number written as an integer: digits, + with a minus sign only before a non-zero value, and no fraction or + exponent. `-0`, `1.0` and `1e0` are not integers, whatever their value, + and a member that must be an integer and is written so has the wrong + type. - The payload is opaque codec bytes whose interpretation is fully determined by the manifest's `model_requirement.capability` and, for a capability that decodes against a parameter set, by that set, which the diff --git a/wai/byte-exact-conformance/src/staged_golden.rs b/wai/byte-exact-conformance/src/staged_golden.rs index 6c0ff73d3..62b73a939 100644 --- a/wai/byte-exact-conformance/src/staged_golden.rs +++ b/wai/byte-exact-conformance/src/staged_golden.rs @@ -95,7 +95,7 @@ fn apply(name: &str, file: &str, kind: UnitKind, units: &mut [Buf], shapes: &mut let p = payload(&read(name, file)); let w = parse(&p, kind).unwrap_or_else(|e| panic!("{file}: {e}")); for i in 0..w.n_units as usize { - let next = apply_unit(&w, i as u32, units[from + i].below(), shapes[from + i]).unwrap_or_else(|e| panic!("{file} unit {i}: {e}")); + let next = apply_unit(&w, &p, i as u32, units[from + i].below(), shapes[from + i]).unwrap_or_else(|e| panic!("{file} unit {i}: {e}")); units[from + i] = match next { Next::U8(v) => Buf::U8(v), Next::I16(v) => Buf::I16(v), diff --git a/wai/extensions/staged-delivery.md b/wai/extensions/staged-delivery.md index dd748ff13..b935a6c5a 100644 --- a/wai/extensions/staged-delivery.md +++ b/wai/extensions/staged-delivery.md @@ -54,7 +54,7 @@ whose capability is not refinement-only **MUST NOT** carry it. | `base` | REQUIRED | Content hash of the stage set's base. | | `prev` | REQUIRED | The producer each unit the layer covers is expected to have. A string means every such unit. An array of runs gives, in unit order, `count` consecutive units whose producer is `producer`. | | `layer` | REQUIRED | Integer 1–255: the stage this layer brings each unit to. | -| `span` | OPTIONAL | `from` (integer ≥ 0) and `count` (integer ≥ 1): the units the layer covers. Absent: every unit of the base. | +| `span` | OPTIONAL | `from` (integer ≥ 0) and `count` (integer ≥ 1): the units the layer covers. Absent, or `null` (which is the same as absent, as SPEC §3.1 reads an OPTIONAL member): every unit of the base. | Rules: @@ -69,8 +69,9 @@ Rules: unit count; a sink checks these once it holds the base (§4.1 step 2). - `from + count` **MUST** fit in 32 bits, and so **MUST** each `count`. -Any other JSON type, or a value outside these rules, makes `refines` malformed -(`refines-malformed`); the envelope still parses. A member of `refines` this +Any other JSON type (an integer is written as SPEC §2 defines one), or a value outside +these rules, makes `refines` malformed (`refines-malformed`); the envelope still +parses. A member of `refines` this revision does not define is ignored (SPEC §8). ## 4. Applying layers @@ -83,7 +84,8 @@ envelope; steps 7–10 decide for each unit *u* the layer covers. The first step fails is the reason for the envelope (1–6) or for the unit (7–10). 1. `refines` is present and well formed (§3) (`refines-malformed`). *R*'s capability - is refinement-only (`capability-not-refinement`). + is refinement-only, and its `model_requirement.fallback` is `null` + (`capability-not-refinement`). 2. The sink has decoded a base whose content hash is `refines.base`. Otherwise it **MAY** hold *R* until its bound (§4.3), and then discards it (`base-absent`). With the base, `refines` is checked against the base's unit count (§3) @@ -98,9 +100,11 @@ fails is the reason for the envelope (1–6) or for the unit (7–10). 6. The payload is well formed for *R*'s capability, in this order: it parses (Appendix A) (`payload-malformed`); its unit count is the number of units the layer covers (`payload-malformed`); its declared cost (Appendix A.5) is within the - limit the sink applies, if it applies one (`cost-over-limit`); and every unit's - stream decodes to its last symbol and ends canonically (`payload-malformed`). A - payload malformed for one unit is malformed for all. + limit the sink applies, if it applies one (`cost-over-limit`); retaining *R* keeps + the chain within its budget (§4.5) (`chain-over-limit`); and every unit's stream + decodes to its last symbol and ends canonically (`payload-malformed`). A payload + malformed for one unit is malformed for all. No unit's shape is read before step + 9. 7. Let *P(u)* be the producer `prev` gives for *u*. *u* is **conflicted** when it is frozen, or when an envelope with different content, the same `base` and the same `layer`, whose producer for *u* is also *P(u)*, has passed step 8 for *u*, earlier @@ -133,7 +137,7 @@ unit. | 3 | `base-via-fallback` | envelope | discarded | | 4 | `base-not-admitted` | envelope | discarded | | 5 | `refinement-unattested` | envelope | discarded | -| 6 | `payload-malformed`, `cost-over-limit` | envelope | discarded | +| 6 | `payload-malformed`, `cost-over-limit`, `chain-over-limit` | envelope | discarded | | 7 | `layer-conflict` | unit | not applied; the unit is frozen | | 8 | `prev-mismatch` | unit | not applied to the unit | | 8 | `layer-gap` | unit | held for the unit, then not applied | @@ -141,9 +145,11 @@ unit. | 10 | `deadline-missed` | unit | not applied to the unit | The list is closed. Two conforming sinks that receive the same envelopes in the same -order, at the same times, with the same receipts, hold bound and cost limit, report -the same reasons per envelope and per unit and reach the same stages and producers. -Order changes the outcome only through steps 7 and 10. +order, at the same times, with the same receipts, hold bound, cost limit, chain budget +and schedule, report the same reasons per envelope and per unit and reach the same +stages and producers. The order of arrival changes the outcome only through the chain +budget at step 6 (§4.5), step 7, the hold bound at step 8 (§4.3: what is refused on +arrival and what expires) and step 10. ### 4.3 Holding @@ -151,8 +157,15 @@ A sink **MUST** declare the bound to which it holds envelopes it cannot yet appl a count of envelopes and a duration from arrival. An envelope held past the duration is discarded with its hold reason (`base-absent`, or `layer-gap` for each unit it is held for). An envelope that would exceed the count is discarded on arrival with the -reason it would have been held for. A unit refined by a held envelope reaches the same -output as one refined in order. +reason it would have been held for. A unit refined by a held envelope that is +released within the bound, and that the chain budget admits, reaches the same output +as one refined in order. + +An envelope held for its base and received again is held once. If it comes with +stronger evidence of its delivery receipt (§10: a receipt verified over its bytes is +stronger than one verified by inclusion alone, which is stronger than none), the sink +keeps the stronger evidence for steps 5 and 10; its hold still runs from its first +arrival. ### 4.4 Batches @@ -168,6 +181,29 @@ step 8 earlier is refused, and the unit frozen at the stage it has. When a unit frozen, every envelope held for it is refused for it (`layer-conflict`). Applying a batch can release another, which is evaluated the same way. +A sink that decodes again a base whose chain it holds keeps the chain: re-decoding a +base does not reset its units' stages, producers or frozen state, and releases and +reports nothing. A sink **MAY** drop a base's chain when it no longer presents the +base; an envelope for that base is then held as for any base it has not decoded. + +### 4.5 Chain budget + +A sink **MUST** declare a budget, in bytes, for the chain of each base it decodes. The +chain's **state** is the sum of: + +- for every unit, the bytes of its current output (1 per u8 element, 2 per i16 + sample), and 32 bytes for each stage above 0 (the producers step 7 compares); +- for every layer the chain **retains**, its payload's bytes and its declared output + and working bytes (Appendix A.5). + +A chain retains a layer from the moment it passes step 6 to the end of the offer, +release or expiry after which no unit is held for it; the sink then drops it. At step +6, after the cost limit, a layer whose payload, declared output and declared working +bytes, added to the state, pass the budget is refused (`chain-over-limit`) before any +stream is decoded. An envelope the chain still retains, received again, adds +nothing. Applying a layer replaces a unit's output with one the layer's declared +output already counted, so the bytes a chain holds never pass its budget. + ## 5. The tier of a stage, and the stage hash - **The base step**: `decode-equivalence` when the capability the base was decoded @@ -238,7 +274,9 @@ the decode of a prefix. For the residual form that is every layer boundary. A layer **MAY** be published after its base, served only on request, or never. A sink **MUST** present the current stage of a unit without waiting for a layer it has not been told to expect. A later revision of a refinement is a new chain from the same -`base`, distinguished by `prev`. When two chains conflict, §4.1 step 7 applies. +`base`, distinguished by `prev` from layer 2 on. Every layer 1 names the base as its +producer, so `prev` cannot distinguish two revisions of layer 1: they are two +envelopes of the same `base`, `layer` and producer, and conflict (§4.1 step 7). ## 8. Live deadlines @@ -252,7 +290,12 @@ be applied to units of its span that are not yet due. A still image **MAY** be refined after it is presented. A sink **MUST** count every unit presented below the stage its expected layers would have reached on time. Verifying a layer's receipt (§10) adds the time to receive and verify that receipt to the layer's arrival, which -sets the least `lead_ms` that can be met. +sets the least `lead_ms` that can be met: over a base whose receipt the sink verified, +a layer counts as arrived when the sink verified its receipt (§4.3). + +A stage set split across bases (a base for each group of frames) has one clock: a base +whose units are the set's units from position `k` on presents its unit `i` at the +set's position `k + i`, and a sink gives each base its first position. ## 9. Carriage @@ -311,14 +354,23 @@ ignores the header. - Deferral lets anyone mint a chain for a public base hash. §10 and §4.1 step 7 bound what a sink presents. A conflict between two attested chains is evidence that an authorized signer signed both, and the sink reports it. -- A hold bound (§4.3) and a cost limit (Appendix A.5) are resource limits. A layer's - header declares its output and its symbols before any symbol is decoded, so a sink - refuses a layer past its limit without decoding it. +- A hold bound (§4.3), a cost limit (Appendix A.5) and a chain budget (§4.5) are + resource limits. A layer's header declares its output, its symbols and its working + bytes before any symbol is decoded, so a sink refuses a layer past its limit or its + budget without decoding it. A sink's own work for a layer is linear in the + layer's payload bytes and declared symbols, up to a factor logarithmic in its + units: it parses the payload once, at step 6; decides each unit the layer covers + once for each offer or release of the layer; finds a unit's producer in `prev` by + bisection over its runs; and compares a unit's key at step 7 with the producers of + that unit's stages. Beyond the chain's state, it keeps a fixed amount per unit of + the base and per unit held. - An encrypted layer ([confidentiality](confidentiality.md)) binds to the content hash of its base's envelope as delivered, ciphertext included, and is decrypted before §4.1 step 6. -- A frozen unit stays at its stage: a party that can sign conflicting layers can stop - a unit's refinement, and cannot change what it presents. +- Where the base is attested (§10), a frozen unit stays at its stage: a party that can + sign conflicting layers can stop a unit's refinement, and cannot change what it + presents. Over a base whose receipt the sink did not verify, anyone who learns the + base's hash can mint a layer, and the first one ready for a unit applies to it. ## Appendix A. `WIR1` @@ -424,16 +476,23 @@ For each unit *u* the layer is applied to, with the unit's current output of sha A payload's header declares, before any symbol is decoded: its output, `n_units·d0·d1·d2` elements of 1 byte (u8) or 2 bytes (i16); its symbols, `n_units·d0·d1·d2`, one entropy-decode step each; and the most bytes its decode holds -at once beyond the payload and the stages below, its parsed tables (`4·T·cdf_len`) -and one unit's output while it is built. It runs no network. A sink that applies a -limit compares this cost with it after the parse and before any stream is decoded -(§4.1 step 6, `cost-over-limit`). +at once beyond the payload and the stages below, its parsed tables (`4·T·cdf_len`), +the index of its streams (`8·n_units`) and one unit's output while it is built. It +runs no network. A sink that applies a limit compares this cost with it after the +parse and before any stream is decoded (§4.1 step 6, `cost-over-limit`), and counts +it in the chain's state while it retains the layer (§4.5). ### A.6 Refusal codes The list is closed, and its codes are those [integer-payloads](integer-payloads.md) §8 uses for the same faults: `bad_magic`, `version`, `reserved_nonzero`, `field_range`, `too_large`, `cdf_malformed`, `stream_length`, `trailing`, `stream_exhausted`, -`stream_noncanonical` and `geometry_mismatch`. A sink reports the first that applies, -in the order of A.3 and A.4. In §4.1, `geometry_mismatch` is the unit's -`geometry-mismatch`, and every other code is the envelope's `payload-malformed`. +`stream_noncanonical` and `geometry_mismatch`. A parser reports the first that +applies, in the order of A.3. §4.1 runs A.4 in two parts: step 6 decodes every unit's +stream to its end (A.4 steps 2 and 3, which read no prediction), so +`stream_exhausted` and `stream_noncanonical` are the envelope's `payload-malformed`, +reported before any unit's shape is read; step 9 then checks each unit's shape (A.4 +step 1), so `geometry_mismatch` is that unit's `geometry-mismatch`. A payload whose +`n_units` is not the number of units the layer covers is the envelope's +`payload-malformed` with the code `field_range`. Every other code is the envelope's +`payload-malformed`. diff --git a/wai/staged-conformance/cases/audio_flac_rate_ladder/case.json b/wai/staged-conformance/cases/audio_flac_rate_ladder/case.json index 30e07f3a9..d41064c0b 100644 --- a/wai/staged-conformance/cases/audio_flac_rate_ladder/case.json +++ b/wai/staged-conformance/cases/audio_flac_rate_ladder/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.audio.flac", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -46,6 +47,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/audio_int_codec_1l/case.json b/wai/staged-conformance/cases/audio_int_codec_1l/case.json index 0aef27755..310cc5178 100644 --- a/wai/staged-conformance/cases/audio_int_codec_1l/case.json +++ b/wai/staged-conformance/cases/audio_int_codec_1l/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.audio.int_codec", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": true, "tier": "decode-equivalence", "units": [ @@ -44,6 +45,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/base_not_admitted/case.json b/wai/staged-conformance/cases/base_not_admitted/case.json index 12ac0464f..d2ba2dfcf 100644 --- a/wai/staged-conformance/cases/base_not_admitted/case.json +++ b/wai/staged-conformance/cases/base_not_admitted/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -50,6 +51,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/base_via_fallback/case.json b/wai/staged-conformance/cases/base_via_fallback/case.json index e5a8d4f2c..ff347d441 100644 --- a/wai/staged-conformance/cases/base_via_fallback/case.json +++ b/wai/staged-conformance/cases/base_via_fallback/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": true, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -50,6 +51,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/bypass_extremes/case.json b/wai/staged-conformance/cases/bypass_extremes/case.json index b0d0301dd..0d4ad5663 100644 --- a/wai/staged-conformance/cases/bypass_extremes/case.json +++ b/wai/staged-conformance/cases/bypass_extremes/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -50,6 +51,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/capability_not_refinement/case.json b/wai/staged-conformance/cases/capability_not_refinement/case.json index f4da23f6c..4ed2c6a9a 100644 --- a/wai/staged-conformance/cases/capability_not_refinement/case.json +++ b/wai/staged-conformance/cases/capability_not_refinement/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -50,6 +51,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/cdf_malformed/case.json b/wai/staged-conformance/cases/cdf_malformed/case.json index 385c8ef5c..d74e3c255 100644 --- a/wai/staged-conformance/cases/cdf_malformed/case.json +++ b/wai/staged-conformance/cases/cdf_malformed/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -50,6 +51,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/clamp_extremes/case.json b/wai/staged-conformance/cases/clamp_extremes/case.json index 545b42f40..a1fda76c4 100644 --- a/wai/staged-conformance/cases/clamp_extremes/case.json +++ b/wai/staged-conformance/cases/clamp_extremes/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -50,6 +51,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/conflict_after_apply/case.json b/wai/staged-conformance/cases/conflict_after_apply/case.json index f747f0095..ad59b04ce 100644 --- a/wai/staged-conformance/cases/conflict_after_apply/case.json +++ b/wai/staged-conformance/cases/conflict_after_apply/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -60,6 +61,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/conflict_held/case.json b/wai/staged-conformance/cases/conflict_held/case.json index eb601e495..af36018a1 100644 --- a/wai/staged-conformance/cases/conflict_held/case.json +++ b/wai/staged-conformance/cases/conflict_held/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -55,6 +56,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/cost_over_limit/case.json b/wai/staged-conformance/cases/cost_over_limit/case.json index 6bcb1ddf9..c147ec0db 100644 --- a/wai/staged-conformance/cases/cost_over_limit/case.json +++ b/wai/staged-conformance/cases/cost_over_limit/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -54,6 +55,7 @@ "symbols": 18446744073709551615, "working_bytes": 18446744073709551615 }, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/deadline_missed/case.json b/wai/staged-conformance/cases/deadline_missed/case.json index fb9dd2ad4..41c7224f6 100644 --- a/wai/staged-conformance/cases/deadline_missed/case.json +++ b/wai/staged-conformance/cases/deadline_missed/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -55,6 +56,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": { "t0_ms": 100, "unit_ms": 40, diff --git a/wai/staged-conformance/cases/geometry_mismatch/case.json b/wai/staged-conformance/cases/geometry_mismatch/case.json index aaa1529e4..865bf6c5a 100644 --- a/wai/staged-conformance/cases/geometry_mismatch/case.json +++ b/wai/staged-conformance/cases/geometry_mismatch/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -50,6 +51,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/hold_bound_count/case.json b/wai/staged-conformance/cases/hold_bound_count/case.json index 32830ed3a..517d0a6be 100644 --- a/wai/staged-conformance/cases/hold_bound_count/case.json +++ b/wai/staged-conformance/cases/hold_bound_count/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -55,6 +56,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/image_int_hyper_2l/case.json b/wai/staged-conformance/cases/image_int_hyper_2l/case.json index 703575a8c..d2325191a 100644 --- a/wai/staged-conformance/cases/image_int_hyper_2l/case.json +++ b/wai/staged-conformance/cases/image_int_hyper_2l/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.neural.int_hyper", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": true, "tier": "decode-equivalence", "units": [ @@ -51,6 +52,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/image_int_hyper_unpinned/case.json b/wai/staged-conformance/cases/image_int_hyper_unpinned/case.json index e2a816544..721598f3e 100644 --- a/wai/staged-conformance/cases/image_int_hyper_unpinned/case.json +++ b/wai/staged-conformance/cases/image_int_hyper_unpinned/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.neural.int_hyper", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "none", "units": [ @@ -46,6 +47,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/image_png_up2/case.json b/wai/staged-conformance/cases/image_png_up2/case.json index 615daa77b..f4b4deb3a 100644 --- a/wai/staged-conformance/cases/image_png_up2/case.json +++ b/wai/staged-conformance/cases/image_png_up2/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.image.png", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -41,6 +42,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/image_up2_from_int_hyper/case.json b/wai/staged-conformance/cases/image_up2_from_int_hyper/case.json index f0aea95b2..745255239 100644 --- a/wai/staged-conformance/cases/image_up2_from_int_hyper/case.json +++ b/wai/staged-conformance/cases/image_up2_from_int_hyper/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.neural.int_hyper", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": true, "tier": "decode-equivalence", "units": [ @@ -51,6 +52,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/image_up2_odd/case.json b/wai/staged-conformance/cases/image_up2_odd/case.json index bf8574ddb..942cc7b5a 100644 --- a/wai/staged-conformance/cases/image_up2_odd/case.json +++ b/wai/staged-conformance/cases/image_up2_odd/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.neural.int_hyper", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": true, "tier": "decode-equivalence", "units": [ @@ -51,6 +52,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/inclusion_only_evidence/case.json b/wai/staged-conformance/cases/inclusion_only_evidence/case.json index 47cd8919d..1a024e11e 100644 --- a/wai/staged-conformance/cases/inclusion_only_evidence/case.json +++ b/wai/staged-conformance/cases/inclusion_only_evidence/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": true, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -50,6 +51,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/layer_zero/case.json b/wai/staged-conformance/cases/layer_zero/case.json index b6bc4f028..1c7e62b97 100644 --- a/wai/staged-conformance/cases/layer_zero/case.json +++ b/wai/staged-conformance/cases/layer_zero/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -50,6 +51,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/missing_layer/case.json b/wai/staged-conformance/cases/missing_layer/case.json index 75f56fedd..7a19787ae 100644 --- a/wai/staged-conformance/cases/missing_layer/case.json +++ b/wai/staged-conformance/cases/missing_layer/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -50,6 +51,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/out_of_order/case.json b/wai/staged-conformance/cases/out_of_order/case.json index 28899304c..d39e1aacd 100644 --- a/wai/staged-conformance/cases/out_of_order/case.json +++ b/wai/staged-conformance/cases/out_of_order/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -55,6 +56,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/payload_truncated/case.json b/wai/staged-conformance/cases/payload_truncated/case.json index e576db97d..67729e00e 100644 --- a/wai/staged-conformance/cases/payload_truncated/case.json +++ b/wai/staged-conformance/cases/payload_truncated/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -50,6 +51,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/pending_l1_l2/case.json b/wai/staged-conformance/cases/pending_l1_l2/case.json index 7f55645ff..660a0b803 100644 --- a/wai/staged-conformance/cases/pending_l1_l2/case.json +++ b/wai/staged-conformance/cases/pending_l1_l2/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -55,6 +56,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/q_zero/case.json b/wai/staged-conformance/cases/q_zero/case.json index 63ef736e2..6caf4969d 100644 --- a/wai/staged-conformance/cases/q_zero/case.json +++ b/wai/staged-conformance/cases/q_zero/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -50,6 +51,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/refinement_without_base/case.json b/wai/staged-conformance/cases/refinement_without_base/case.json index abc69b0e4..45f6d9a0f 100644 --- a/wai/staged-conformance/cases/refinement_without_base/case.json +++ b/wai/staged-conformance/cases/refinement_without_base/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -50,6 +51,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/refines_malformed/case.json b/wai/staged-conformance/cases/refines_malformed/case.json index 7d5f0e0fb..fbd1ac940 100644 --- a/wai/staged-conformance/cases/refines_malformed/case.json +++ b/wai/staged-conformance/cases/refines_malformed/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -50,6 +51,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/splat_attr_1l/case.json b/wai/staged-conformance/cases/splat_attr_1l/case.json index f18f8ff36..fb987e1f6 100644 --- a/wai/staged-conformance/cases/splat_attr_1l/case.json +++ b/wai/staged-conformance/cases/splat_attr_1l/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.splat.int_codec", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": true, "tier": "decode-equivalence", "units": [ @@ -44,6 +45,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/stream_exhausted/case.json b/wai/staged-conformance/cases/stream_exhausted/case.json index d2fcf189a..dd9912dcc 100644 --- a/wai/staged-conformance/cases/stream_exhausted/case.json +++ b/wai/staged-conformance/cases/stream_exhausted/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -50,6 +51,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/stream_noncanonical/case.json b/wai/staged-conformance/cases/stream_noncanonical/case.json index 060bf2fa6..ecad9653d 100644 --- a/wai/staged-conformance/cases/stream_noncanonical/case.json +++ b/wai/staged-conformance/cases/stream_noncanonical/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -50,6 +51,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/video_int_hyper_keyframe/case.json b/wai/staged-conformance/cases/video_int_hyper_keyframe/case.json index f3f5b8c4d..e2447cf14 100644 --- a/wai/staged-conformance/cases/video_int_hyper_keyframe/case.json +++ b/wai/staged-conformance/cases/video_int_hyper_keyframe/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_hyper", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": true, "tier": "decode-equivalence", "units": [ @@ -78,6 +79,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/case.json b/wai/staged-conformance/cases/video_int_motion_gop_perframe/case.json index a676faac2..fdb1b9b01 100644 --- a/wai/staged-conformance/cases/video_int_motion_gop_perframe/case.json +++ b/wai/staged-conformance/cases/video_int_motion_gop_perframe/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -58,6 +59,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -107,6 +109,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -234,6 +237,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/video_mixed_stage_l2/case.json b/wai/staged-conformance/cases/video_mixed_stage_l2/case.json index 7e5434ee3..36bdfcceb 100644 --- a/wai/staged-conformance/cases/video_mixed_stage_l2/case.json +++ b/wai/staged-conformance/cases/video_mixed_stage_l2/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -86,6 +87,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/wrong_base/case.json b/wai/staged-conformance/cases/wrong_base/case.json index f55209da1..ab621a178 100644 --- a/wai/staged-conformance/cases/wrong_base/case.json +++ b/wai/staged-conformance/cases/wrong_base/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -50,6 +51,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/wrong_prev/case.json b/wai/staged-conformance/cases/wrong_prev/case.json index 349c54ebe..71f887e30 100644 --- a/wai/staged-conformance/cases/wrong_prev/case.json +++ b/wai/staged-conformance/cases/wrong_prev/case.json @@ -9,6 +9,7 @@ "decoded_via": "wai.video.int_motion", "via_fallback": false, "attested": false, + "first_unit": 0, "pin_resolved": false, "tier": "decode-equivalence", "units": [ @@ -55,6 +56,7 @@ "max_age_ms": 1000 }, "cost_limit": null, + "budget": 1073741824, "live": null }, "steps": [ diff --git a/wai/wai-rs/Cargo.toml b/wai/wai-rs/Cargo.toml index 56a9d06e1..3139cbc0c 100644 --- a/wai/wai-rs/Cargo.toml +++ b/wai/wai-rs/Cargo.toml @@ -666,6 +666,11 @@ name = "cli_stage" path = "tests/cli_stage.rs" required-features = ["staged", "video_int", "audio_int", "splat_int"] +[[test]] +name = "staged_resources" +path = "tests/staged_resources.rs" +required-features = ["staged", "video_int"] + [[test]] name = "int_payload" path = "tests/int_payload.rs" diff --git a/wai/wai-rs/src/bin/wai_cli/stage.rs b/wai/wai-rs/src/bin/wai_cli/stage.rs index 31c6fdf4c..ab7012ea8 100644 --- a/wai/wai-rs/src/bin/wai_cli/stage.rs +++ b/wai/wai-rs/src/bin/wai_cli/stage.rs @@ -110,6 +110,7 @@ fn base(path: &str, args: &[String]) -> Result<(Vec, BaseInfo, Vec) capability: cap, via_fallback: false, attested: false, + first_unit: 0, }; Ok((env, info, units)) } @@ -122,7 +123,7 @@ fn below(s: &Samples) -> Below<'_> { } fn cfg() -> ChainConfig { - ChainConfig { bound: HoldBound { max_envelopes: 1024, max_age_ms: u64::MAX }, cost_limit: None, live: None } + ChainConfig { bound: HoldBound { max_envelopes: 1024, max_age_ms: u64::MAX }, cost_limit: None, budget: u64::MAX, live: None } } fn print_units(c: &StageChain) { @@ -313,9 +314,9 @@ fn inspect(args: &[String]) -> Result<(), String> { Ok(p) => { let c = p.declared_cost(); println!(" WIR1: {} units of {}x{}x{} predictor {} q {} tables {} ", - p.n_units, p.d0, p.d1, p.d2, p.predictor.token(), p.q, p.tables.len()); + p.n_units, p.d0, p.d1, p.d2, p.predictor.token(), p.q, p.n_tables()); println!(" declared cost: {} output bytes, {} symbols, {} working bytes", c.output_bytes, c.symbols, c.working_bytes); - match p.check_streams() { + match p.check_streams(&w.payload) { Ok(()) => println!(" streams: ✓ every unit's stream decodes to its end"), Err((u, e)) => println!(" streams: ✗ unit {u}: {e}"), } diff --git a/wai/wai-rs/src/bin/wai_staged_vectors.rs b/wai/wai-rs/src/bin/wai_staged_vectors.rs index 4a1061b44..acd454d11 100644 --- a/wai/wai-rs/src/bin/wai_staged_vectors.rs +++ b/wai/wai-rs/src/bin/wai_staged_vectors.rs @@ -374,6 +374,8 @@ struct Base { tier: Option, units: Vec, decode: Decode, + /// The position of its unit 0 on a live clock (§8). + first_unit: u64, } impl Base { @@ -381,7 +383,7 @@ impl Base { chash(&self.envelope) } fn info(&self) -> BaseInfo { - BaseInfo { content_hash: self.hash(), capability: self.decoded_via.clone(), via_fallback: self.via_fallback, tier: self.tier, attested: self.attested } + BaseInfo { content_hash: self.hash(), capability: self.decoded_via.clone(), via_fallback: self.via_fallback, tier: self.tier, attested: self.attested, first_unit: self.first_unit } } fn unit(&self, u: usize) -> Vec { match &self.units[u].samples { @@ -460,6 +462,7 @@ fn tiny_base(ctx: &Ctx, pinned: bool) -> Base { tier, units, decode: Decode::Int { set }, + first_unit: 0, } } @@ -477,6 +480,7 @@ fn nk_base(ctx: &Ctx) -> Base { pin_resolved: true, units, decode: Decode::Int { set }, + first_unit: 0, } } @@ -495,6 +499,7 @@ fn audio_base(ctx: &Ctx) -> Base { pin_resolved: true, units, decode: Decode::Int { set }, + first_unit: 0, } } @@ -513,6 +518,7 @@ fn splat_base(ctx: &Ctx) -> Base { pin_resolved: true, units, decode: Decode::Int { set }, + first_unit: 0, } } @@ -533,6 +539,7 @@ fn gop_base(ctx: &Ctx, h: u32, w: u32, n: u32, seed: u32, t0: u32, q: u32) -> Ba pin_resolved: false, units, decode: Decode::Int { set: vec![] }, + first_unit: 0, } } @@ -547,6 +554,7 @@ fn png_base(h: u32, w: u32, seed: u32) -> Base { pin_resolved: false, units: vec![UnitBuf { shape: (h, w, 3), samples: Samples::U8(rgb) }], decode: Decode::Png, + first_unit: 0, } } @@ -561,6 +569,7 @@ fn flac_base(n: u32, ch: u32, rate: u32) -> Base { pin_resolved: false, units: vec![UnitBuf { shape: (n, 1, ch), samples: Samples::I16(s) }], decode: Decode::Flac, + first_unit: 0, } } @@ -640,7 +649,7 @@ struct Case { } fn cfg() -> ChainConfig { - ChainConfig { bound: HoldBound { max_envelopes: 16, max_age_ms: 1000 }, cost_limit: None, live: None } + ChainConfig { bound: HoldBound { max_envelopes: 16, max_age_ms: 1000 }, cost_limit: None, budget: 1 << 30, live: None } } fn offer(file: &str, at_ms: u64) -> Step { @@ -1084,6 +1093,7 @@ fn cfg_json(c: &ChainConfig) -> Value { json!({ "bound": {"max_envelopes": c.bound.max_envelopes, "max_age_ms": c.bound.max_age_ms}, "cost_limit": c.cost_limit.map(|l| json!({"output_bytes": l.output_bytes, "symbols": l.symbols, "working_bytes": l.working_bytes})), + "budget": c.budget, "live": c.live.as_ref().map(|l| json!({"t0_ms": l.clock.t0_ms, "unit_ms": l.clock.unit_ms, "target_latency_ms": l.clock.target_latency_ms, "lead_ms": l.lead_ms})), }) } @@ -1093,6 +1103,7 @@ fn cfg_from(v: &Value) -> ChainConfig { ChainConfig { bound: HoldBound { max_envelopes: u(&v["bound"]["max_envelopes"]) as usize, max_age_ms: u(&v["bound"]["max_age_ms"]) }, cost_limit: (!v["cost_limit"].is_null()).then(|| Wir1Cost { output_bytes: u(&v["cost_limit"]["output_bytes"]), symbols: u(&v["cost_limit"]["symbols"]), working_bytes: u(&v["cost_limit"]["working_bytes"]) }), + budget: u(&v["budget"]), live: (!v["live"].is_null()).then(|| Live { clock: UnitClock { t0_ms: u(&v["live"]["t0_ms"]), unit_ms: u(&v["live"]["unit_ms"]), target_latency_ms: u(&v["live"]["target_latency_ms"]) }, lead_ms: v["live"]["lead_ms"].as_array().expect("leads").iter().map(u).collect(), @@ -1140,6 +1151,7 @@ fn generate(ctx: &Ctx) -> BTreeMap> { "decoded_via": b.decoded_via, "via_fallback": b.via_fallback, "attested": b.attested, + "first_unit": b.first_unit, "pin_resolved": b.pin_resolved, "tier": tier::token(b.tier), "units": b.units.iter().map(|u| json!({"shape": [u.shape.0, u.shape.1, u.shape.2], "elem": match u.samples { Samples::U8(_) => "u8", Samples::I16(_) => "i16" }})).collect::>(), @@ -1421,6 +1433,7 @@ fn verify(dir: &Path) -> Result<(), Vec> { via_fallback: b["via_fallback"] == true, tier: tier::from_token(b["tier"].as_str().unwrap()), attested: b["attested"] == true, + first_unit: b["first_unit"].as_u64().expect("first_unit"), }; bases.push((info, units)); } diff --git a/wai/wai-rs/src/container.rs b/wai/wai-rs/src/container.rs index f97f04bb0..c271f4539 100644 --- a/wai/wai-rs/src/container.rs +++ b/wai/wai-rs/src/container.rs @@ -878,8 +878,11 @@ impl Refines { /// object of `producer` and an integer `count` ≥ 1; two adjacent runs with the /// same producer, or an array of one run (a single producer is written as a /// string); `layer` not an integer in 1..=255; at layer 1 a producer other than - /// `base`; `span` not an object of integer `from` ≥ 0 and `count` ≥ 1, or past - /// `u32`; with a span, run counts that do not sum to its `count`. Whether the + /// `base`; `span` neither absent, `null`, nor an object of integer `from` ≥ 0 and + /// `count` ≥ 1, or past `u32`; with a span, run counts that do not sum to its + /// `count`. An integer is written as one (SPEC §2): `-0`, a fraction or an exponent + /// is not an integer, and `serde_json` reads each as a float, which no `as_u64` + /// accepts. Whether the /// span lies within the base's units, and, with no span, whether the runs sum to /// them, needs the base ([`Refines::check_units`]). pub fn from_value(v: &Value, form: RefForm) -> Result { @@ -892,7 +895,8 @@ impl Refines { .filter(|l| (1..=255).contains(l)) .ok_or(bad("layer is not an integer in 1..=255"))? as u8; let span = match o.get("span") { - None => None, + // `null` in an OPTIONAL member is the member absent (SPEC §3.1). + None | Some(Value::Null) => None, Some(sp) => { let so = sp.as_object().ok_or(bad("span is not an object"))?; let from = so.get("from").and_then(Value::as_u64).and_then(|f| u32::try_from(f).ok()).ok_or(bad("span.from is not an integer ≥ 0 within u32"))?; @@ -1010,7 +1014,8 @@ impl Refines { } /// The producer `prev` gives for the `i`-th unit of the span (0-based), or - /// `None` past the runs. + /// `None` past the runs. Linear in the runs; a sink resolving every unit of a span + /// indexes the runs once (`staged::StageChain` keeps their ends and bisects). pub fn producer_for(&self, i: u32) -> Option<&str> { match &self.prev { Prev::One(p) => Some(p), diff --git a/wai/wai-rs/src/staged/mod.rs b/wai/wai-rs/src/staged/mod.rs index 418c44ca1..f30b40f5c 100644 --- a/wai/wai-rs/src/staged/mod.rs +++ b/wai/wai-rs/src/staged/mod.rs @@ -21,7 +21,7 @@ mod tests; use std::collections::BTreeMap; use crate::codecs::{admits, is_refinement_only, DeterminismTier, CAP_AUDIO_INT_REFINE, CAP_IMAGE_INT_REFINE, CAP_SPLAT_INT_REFINE, CAP_VIDEO_INT_REFINE}; -use crate::container::{RefForm, Refines, Wai}; +use crate::container::{Prev, RefForm, Refines, Wai}; use wir1::{Below, Next, Wir1Cost, Wir1Refusal}; /// The unit kind of a residual refinement capability: what its `WIR1` payload's units @@ -64,6 +64,7 @@ pub enum StageRefusal { RefinementUnattested, PayloadMalformed, CostOverLimit, + ChainOverLimit, LayerConflict, PrevMismatch, LayerGap, @@ -75,10 +76,10 @@ impl StageRefusal { /// Every reason, in §4.2's order. /// /// cbindgen:ignore - pub const ALL: [StageRefusal; 13] = [ + pub const ALL: [StageRefusal; 14] = [ Self::RefinesMalformed, Self::CapabilityNotRefinement, Self::BaseAbsent, Self::BaseViaFallback, Self::BaseNotAdmitted, Self::RefinementUnattested, Self::PayloadMalformed, Self::CostOverLimit, - Self::LayerConflict, Self::PrevMismatch, Self::LayerGap, Self::GeometryMismatch, Self::DeadlineMissed, + Self::ChainOverLimit, Self::LayerConflict, Self::PrevMismatch, Self::LayerGap, Self::GeometryMismatch, Self::DeadlineMissed, ]; /// The token §4.2 names. @@ -92,6 +93,7 @@ impl StageRefusal { Self::RefinementUnattested => "refinement-unattested", Self::PayloadMalformed => "payload-malformed", Self::CostOverLimit => "cost-over-limit", + Self::ChainOverLimit => "chain-over-limit", Self::PrevMismatch => "prev-mismatch", Self::LayerGap => "layer-gap", Self::LayerConflict => "layer-conflict", @@ -169,6 +171,13 @@ impl Samples { Samples::I16(v) => v.len(), } } + /// The bytes the samples hold: 1 per u8 element, 2 per i16 sample. + pub fn byte_len(&self) -> u64 { + match self { + Samples::U8(v) => v.len() as u64, + Samples::I16(v) => 2 * v.len() as u64, + } + } } /// One unit of a base's output as decoded: its shape `(d0, d1, d2)` (rows, columns @@ -180,7 +189,7 @@ pub struct UnitBuf { pub samples: Samples, } -/// A unit's state (§1): its stage, its producer, whether it is frozen (§4.1 step 8), +/// A unit's state (§1): its stage, its producer, whether it is frozen (§4.1 step 7), /// its tier (§5) and its current output. #[derive(Debug, Clone, PartialEq, Eq)] pub struct UnitState { @@ -189,6 +198,16 @@ pub struct UnitState { pub frozen: bool, pub tier: Option, pub buf: UnitBuf, + /// The producers of stages 1 to `stage`, in order: the envelopes that passed step + /// 8 for the unit and were applied, which step 7 compares a later layer with. + producers: Vec<[u8; 32]>, +} + +impl UnitState { + /// The producers of the unit's stages 1 to `stage`, in order (§4.1 step 7). + pub fn producers(&self) -> &[[u8; 32]] { + &self.producers + } } /// How long and how much a sink holds (§4.3). @@ -209,11 +228,15 @@ pub struct Live { } /// What a sink applies to a base's layers besides §4.1: its hold bound, the cost limit -/// it applies to a layer's declared cost (Appendix A.5), and its live schedule. +/// it applies to a layer's declared cost (Appendix A.5), its chain budget (§4.5), and +/// its live schedule. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ChainConfig { pub bound: HoldBound, pub cost_limit: Option, + /// The most bytes a chain's state reaches (§4.5): a layer that would take it past + /// this is refused before any stream is decoded (`chain-over-limit`). + pub budget: u64, pub live: Option, } @@ -277,19 +300,80 @@ impl std::fmt::Display for OfferError { impl std::error::Error for OfferError {} -/// A layer that passed §4.1 steps 1–6 for a chain, kept for its conflict keys and -/// while units are held for it. +/// The producers `prev` gives the units of a layer's span: each run's producer and the +/// span offset it ends at, so one unit's producer is found by bisection. #[derive(Debug, Clone)] -struct Layer { - payload: Vec, - cap: String, - refines: Refines, - arrival_ms: u64, - /// The order in which layers passed step 6 (release order). - seq: u64, +struct Producers { + producers: Vec<[u8; 32]>, + ends: Vec, } -/// The base's identity and how it was decoded (§4.1 steps 2–4). +impl Producers { + /// From a `refines` whose runs sum to `count` (checked at step 2). + fn new(r: &Refines, count: u32) -> Option { + match &r.prev { + Prev::One(p) => Some(Producers { producers: vec![unhex32(p)?], ends: vec![count] }), + Prev::Runs(runs) => { + let mut producers = Vec::with_capacity(runs.len()); + let mut ends = Vec::with_capacity(runs.len()); + let mut at = 0u32; + for run in runs { + producers.push(unhex32(&run.producer)?); + at = at.checked_add(run.count)?; + ends.push(at); + } + Some(Producers { producers, ends }) + } + } + } + + /// The producer of the `i`-th unit of the span. + fn at(&self, i: u32) -> Option<&[u8; 32]> { + self.producers.get(self.ends.partition_point(|&e| e <= i)) + } +} + +fn unhex32(s: &str) -> Option<[u8; 32]> { + let b = s.as_bytes(); + if b.len() != 64 { + return None; + } + let nib = |c: u8| match c { + b'0'..=b'9' => Some(c - b'0'), + b'a'..=b'f' => Some(c - b'a' + 10), + _ => None, + }; + let mut out = [0u8; 32]; + for (i, o) in out.iter_mut().enumerate() { + *o = (nib(b[2 * i])? << 4) | nib(b[2 * i + 1])?; + } + Some(out) +} + +/// A layer the chain retains (§4.5): it passed §4.1 steps 1–6, and is being evaluated +/// or has units held for it. Its payload is parsed once, when it passes step 6. +#[derive(Debug, Clone)] +struct Layer { + hash: [u8; 32], + payload: Vec, + wir: wir1::Wir1, + layer: u8, + from: u32, + count: u32, + prev: Producers, + /// When it arrived: its hold runs from here (§4.3). + arrival_ms: u64, + /// When it counts as arrived for a live deadline (§8): its arrival, or, over a base + /// whose receipt the sink verified, when the sink verified the layer's (§10). + ready_ms: u64, + /// How many units are held for it. + held: u32, + /// What it adds to the chain's state while retained (§4.5). + reserve: u64, +} + +/// The base's identity, how it was decoded (§4.1 steps 2–4), and where its units sit +/// on a live clock (§8). #[derive(Debug, Clone, PartialEq, Eq)] pub struct BaseInfo { /// The base envelope's content hash. @@ -302,19 +386,31 @@ pub struct BaseInfo { pub tier: Option, /// Whether the sink verified a delivery receipt for the base (§10). pub attested: bool, + /// The position of the base's unit 0 on the live clock (§8): 0 for a base that + /// starts the schedule; `k` for a base whose units are the schedule's units + /// `k, k + 1, …` (a group of frames after the first). + pub first_unit: u64, } -/// One base and its units (§1), and the layers applied or held for them. +/// One base and its units (§1), and the layers it retains for them (§4.5). #[derive(Debug, Clone)] pub struct StageChain { base: BaseInfo, cfg: ChainConfig, units: Vec, - layers: BTreeMap<[u8; 32], Layer>, - /// Units held for a layer (§4.1 step 8): `(layer hash, unit)`. - held: Vec<([u8; 32], u32)>, - /// Pairs that passed step 8: `(layer, unit, P(u))` → the layer's content hash. - seen: BTreeMap<(u8, u32, String), [u8; 32]>, + /// Per unit, the envelope that passed step 8 for it at its current stage and was + /// not applied (step 9 or 10 refused it). + passed: BTreeMap, + /// The layers retained, by the order they passed step 6. + layers: BTreeMap, + /// The same layers by content hash. + ids: BTreeMap<[u8; 32], u64>, + /// Per unit, the layers held for it (§4.1 step 8). + held: BTreeMap>, + /// How many layers have a unit held for them. + held_layers: usize, + /// The chain's state (§4.5), in bytes. + state: u64, seq: u64, } @@ -326,11 +422,23 @@ impl StageChain { /// A chain over a decoded base: `units` are the base's units in order (§2), each /// at stage 0 with the base as its producer and the base step's tier. pub fn new(base: BaseInfo, units: Vec, cfg: ChainConfig) -> Self { + let state = units.iter().map(|u| u.samples.byte_len()).fold(0u64, u64::saturating_add); let units = units .into_iter() - .map(|buf| UnitState { stage: 0, producer: base.content_hash, frozen: false, tier: base.tier, buf }) + .map(|buf| UnitState { stage: 0, producer: base.content_hash, frozen: false, tier: base.tier, buf, producers: vec![] }) .collect(); - StageChain { base, cfg, units, layers: BTreeMap::new(), held: vec![], seen: BTreeMap::new(), seq: 0 } + StageChain { + base, + cfg, + units, + passed: BTreeMap::new(), + layers: BTreeMap::new(), + ids: BTreeMap::new(), + held: BTreeMap::new(), + held_layers: 0, + state, + seq: 0, + } } /// The base this chain refines. @@ -374,24 +482,36 @@ impl StageChain { Some(crate::pin::sha256(&cat)) } - /// The envelopes held, with the units each is held for. + /// The envelopes held, with the units each is held for, in the order the envelopes + /// passed step 6 and then by unit. pub fn held(&self) -> Vec<([u8; 32], u32)> { - self.held.clone() + let mut v: Vec<(u64, u32)> = self.held.iter().flat_map(|(u, ids)| ids.iter().map(move |id| (*id, *u))).collect(); + v.sort_unstable(); + v.into_iter().map(|(id, u)| (self.layers[&id].hash, u)).collect() } /// How many envelopes hold at least one unit. pub fn held_envelopes(&self) -> usize { - self.held.iter().map(|(h, _)| *h).collect::>().len() + self.held_layers + } + + /// The chain's state (§4.5): every unit's output bytes and 32 bytes per stage above + /// 0, and every retained layer's payload, declared output and declared working bytes. + pub fn state_bytes(&self) -> u64 { + self.state } /// §4.1 steps 2–6 for an envelope whose `refines` and capability passed step 1: - /// the envelope reason, or the layer, kept. - fn admit(&mut self, hash: [u8; 32], cap: &str, refines: Refines, payload: &[u8], arrival_ms: u64, evidence: ReceiptEvidence) -> Result<(), (StageRefusal, Option<&'static str>)> { + /// the envelope reason, or the layer, retained. + #[allow(clippy::too_many_arguments)] + fn admit(&mut self, hash: [u8; 32], cap: &str, refines: &Refines, payload: Vec, arrival_ms: u64, ready_ms: u64, evidence: ReceiptEvidence) -> Result)> { use StageRefusal::*; if refines.base != hex(&self.base.content_hash) { return Err((BaseAbsent, None)); } refines.check_units(self.n_units()).map_err(|_| (RefinesMalformed, None))?; + let (from, count) = refines.units(self.n_units()); + let prev = Producers::new(refines, count).ok_or((RefinesMalformed, None))?; if self.base.via_fallback { return Err((BaseViaFallback, None)); } @@ -402,13 +522,12 @@ impl StageChain { return Err((RefinementUnattested, None)); } let kind = unit_kind(cap).ok_or((PayloadMalformed, Some("unsupported_capability")))?; - let w = wir1::parse(payload, kind).map_err(|e| (PayloadMalformed, Some(e.code())))?; - let (_, count) = refines.units(self.n_units()); - if w.n_units != count { - return Err((PayloadMalformed, Some("geometry_mismatch"))); + let wir = wir1::parse(&payload, kind).map_err(|e| (PayloadMalformed, Some(e.code())))?; + if wir.n_units != count { + return Err((PayloadMalformed, Some("field_range"))); } + let c = wir.declared_cost(); if let Some(limit) = self.cfg.cost_limit { - let c = w.declared_cost(); let over = if c.output_bytes > limit.output_bytes { Some("output bytes") } else if c.symbols > limit.symbols { @@ -422,34 +541,55 @@ impl StageChain { return Err((CostOverLimit, Some(what))); } } - w.check_streams().map_err(|(_, e)| (PayloadMalformed, Some(e.code())))?; + let reserve = (payload.len() as u64).saturating_add(c.output_bytes).saturating_add(c.working_bytes); + if self.state.saturating_add(reserve) > self.cfg.budget { + return Err((ChainOverLimit, None)); + } + wir.check_streams(&payload).map_err(|(_, e)| (PayloadMalformed, Some(e.code())))?; self.seq += 1; - self.layers.insert(hash, Layer { payload: payload.to_vec(), cap: cap.to_string(), refines, arrival_ms, seq: self.seq }); - Ok(()) + self.state += reserve; + self.ids.insert(hash, self.seq); + self.layers.insert(self.seq, Layer { hash, payload, wir, layer: refines.layer, from, count, prev, arrival_ms, ready_ms, held: 0, reserve }); + Ok(self.seq) } - /// The conflict key of unit `u` under layer `hash` (§4.1 step 7): the layer, the - /// unit, and the producer the layer's `prev` gives for it. - fn key(&self, hash: &[u8; 32], u: u32) -> (u8, u32, String) { - let l = &self.layers[hash]; - let (from, _) = l.refines.units(self.n_units()); - (l.refines.layer, u, l.refines.producer_for(u - from).unwrap_or_default().to_string()) + /// The producer layer `id`'s `prev` gives for unit `u` (§4.1 step 7, *P(u)*). + fn p_of(&self, id: u64, u: u32) -> Option<[u8; 32]> { + let l = &self.layers[&id]; + l.prev.at(u.checked_sub(l.from)?).copied() } - /// §4.1 step 7 before the batch: unit `u` is frozen, or another envelope already - /// passed step 8 for `u` under the same key. - fn conflicted_before(&self, hash: &[u8; 32], u: u32) -> bool { - self.units[u as usize].frozen || self.seen.get(&self.key(hash, u)).is_some_and(|s| s != hash) - } - - /// §4.1 step 8 for unit `u` of layer `hash`: ready, held, or not ready. - fn readiness(&self, hash: &[u8; 32], u: u32) -> Readiness { - let l = &self.layers[hash]; - let (from, _) = l.refines.units(self.n_units()); - let Some(p) = l.refines.producer_for(u - from) else { return Readiness::Not }; + /// §4.1 step 7 against what passed step 8 before: unit `u` is frozen, or an + /// envelope other than layer `id`, of the same layer and with the same producer for + /// `u`, passed step 8 for it. A unit's stage rises by one per application, so the + /// envelope that passed step 8 for it at layer `k` is the producer of its stage `k` + /// (or, at the stage above its current one, the envelope that passed and was not + /// applied), and the producer it named is that of stage `k − 1`. + fn conflicted_before(&self, id: u64, u: u32) -> bool { let s = &self.units[u as usize]; - let want = l.refines.layer - 1; - if s.stage == want && hex(&s.producer) == p { + if s.frozen { + return true; + } + let l = &self.layers[&id]; + let Some(p) = self.p_of(id, u) else { return false }; + let (k, stage) = (l.layer as usize, s.stage as usize); + if k <= stage { + let before = if k == 1 { self.base.content_hash } else { s.producers[k - 2] }; + p == before && s.producers[k - 1] != l.hash + } else if k == stage + 1 { + self.passed.get(&u).is_some_and(|h| p == s.producer && *h != l.hash) + } else { + false + } + } + + /// §4.1 step 8 for unit `u` of layer `id`: ready, held, or not ready. + fn readiness(&self, id: u64, u: u32) -> Readiness { + let l = &self.layers[&id]; + let Some(p) = self.p_of(id, u) else { return Readiness::Not }; + let s = &self.units[u as usize]; + let want = l.layer - 1; + if s.stage == want && s.producer == p { Readiness::Ready } else if s.stage < want { Readiness::Held @@ -458,37 +598,81 @@ impl StageChain { } } - /// Evaluate a fresh layer's units (step 7), then run the batch of its ready units - /// and every batch that releases. - fn evaluate(&mut self, hash: [u8; 32], outside_held: usize, reports: &mut Reports) { - let l = &self.layers[&hash]; - let (from, count) = l.refines.units(self.n_units()); + /// Hold unit `u` for layer `id`. + fn hold(&mut self, id: u64, u: u32) { + let l = self.layers.get_mut(&id).expect("retained"); + if l.held == 0 { + self.held_layers += 1; + } + l.held += 1; + self.held.entry(u).or_default().push(id); + } + + /// Release every layer held for unit `u`. + fn unhold_all(&mut self, u: u32) -> Vec { + let ids = self.held.remove(&u).unwrap_or_default(); + for id in &ids { + let l = self.layers.get_mut(id).expect("retained"); + l.held -= 1; + if l.held == 0 { + self.held_layers -= 1; + } + } + ids + } + + /// Whether unit `u` is held for layer `id`. + fn is_held(&self, id: u64, u: u32) -> bool { + self.held.get(&u).is_some_and(|ids| ids.contains(&id)) + } + + /// Freeze unit `u`: no further layer applies to it, and every layer held for it is + /// refused for it (`layer-conflict`). + fn freeze(&mut self, u: u32, reports: &mut Reports) { + self.units[u as usize].frozen = true; + for id in self.unhold_all(u) { + let h = self.layers[&id].hash; + reports.get(id, h).refused.push((u, StageRefusal::LayerConflict)); + } + } + + /// Evaluate layer `id`'s units on its arrival (steps 7 and 8), then run the batch + /// of its ready units and every batch that releases. + fn evaluate(&mut self, id: u64, outside_held: usize, reports: &mut Reports) { + let (hash, from, count) = { + let l = &self.layers[&id]; + (l.hash, l.from, l.count) + }; + let already = self.layers[&id].held > 0; let mut ready = Vec::new(); let mut newly_held = Vec::new(); for u in from..from + count { - if self.conflicted_before(&hash, u) { + if self.conflicted_before(id, u) { + if self.is_held(id, u) { + // Frozen while held: freeze reports it below. + } else { + reports.get(id, hash).refused.push((u, StageRefusal::LayerConflict)); + } self.freeze(u, reports); - reports.get(hash).refused.push((u, StageRefusal::LayerConflict)); continue; } - match self.readiness(&hash, u) { - Readiness::Ready => ready.push((hash, u)), + match self.readiness(id, u) { + Readiness::Ready => ready.push((id, u)), Readiness::Held => newly_held.push(u), - Readiness::Not => reports.get(hash).refused.push((u, StageRefusal::PrevMismatch)), + Readiness::Not => reports.get(id, hash).refused.push((u, StageRefusal::PrevMismatch)), } } if !newly_held.is_empty() { - let already = self.held.iter().any(|(h, _)| *h == hash); - if !already && self.held_envelopes() + outside_held >= self.cfg.bound.max_envelopes { + if !already && self.held_layers + outside_held >= self.cfg.bound.max_envelopes { for u in newly_held { - reports.get(hash).refused.push((u, StageRefusal::LayerGap)); + reports.get(id, hash).refused.push((u, StageRefusal::LayerGap)); } } else { for u in newly_held { - if !self.held.contains(&(hash, u)) { - self.held.push((hash, u)); + if !self.is_held(id, u) { + self.hold(id, u); } - reports.get(hash).held.push(u); + reports.get(id, hash).held.push(u); } } } @@ -496,193 +680,207 @@ impl StageChain { } /// Run a batch (§4.4) and every batch it releases. - fn run_batches(&mut self, mut batch: Vec<([u8; 32], u32)>, reports: &mut Reports) { + fn run_batches(&mut self, mut batch: Vec<(u64, u32)>, reports: &mut Reports) { while !batch.is_empty() { - let advanced = self.run_batch(&batch, reports); + let advanced = self.run_batch(batch, reports); batch = self.release(&advanced, reports); } } - /// One batch: step 7 over every pair, then steps 9 and 10 and application. - /// Returns the units whose stage advanced. - fn run_batch(&mut self, batch: &[([u8; 32], u32)], reports: &mut Reports) -> Vec { - // Step 7: a pair conflicts with a pair passed earlier (`seen`) or with another - // pair of this batch under the same key with different content, or its unit is - // frozen. - let keys: Vec<(u8, u32, String)> = batch.iter().map(|(h, u)| self.key(h, *u)).collect(); - let mut conflicted = vec![false; batch.len()]; - for i in 0..batch.len() { - let (h, u) = batch[i]; - if self.units[u as usize].frozen { - conflicted[i] = true; - } - if self.seen.get(&keys[i]).is_some_and(|s| *s != h) { - conflicted[i] = true; - } - for j in 0..batch.len() { - if i != j && keys[j] == keys[i] && batch[j].0 != h { - conflicted[i] = true; - } - } - } - for (i, &(h, u)) in batch.iter().enumerate() { - if conflicted[i] { - self.freeze(u, reports); - reports.get(h).refused.push((u, StageRefusal::LayerConflict)); - } else { - self.seen.entry(keys[i].clone()).or_insert(h); - } - } + /// One batch: step 7 over every pair, grouped by key, then steps 9 and 10 and + /// application. Returns the units whose stage advanced. + fn run_batch(&mut self, mut batch: Vec<(u64, u32)>, reports: &mut Reports) -> Vec { + // A unit's ready pairs share its key (layer = stage + 1, P(u) = its producer), + // so grouping by unit groups by key. + batch.sort_unstable_by_key(|&(id, u)| (u, id)); + batch.dedup(); let mut advanced = Vec::new(); - for (i, &(h, u)) in batch.iter().enumerate() { - if conflicted[i] { - continue; - } - match self.apply(&h, u) { - Ok(()) => { - reports.get(h).applied.push(u); - advanced.push(u); + let mut i = 0; + while i < batch.len() { + let u = batch[i].1; + let j = i + batch[i..].iter().take_while(|p| p.1 == u).count(); + let group = &batch[i..j]; + let conflicted = group.len() > 1 || self.conflicted_before(group[0].0, u); + if conflicted { + self.freeze(u, reports); + for &(id, _) in group { + let h = self.layers[&id].hash; + reports.get(id, h).refused.push((u, StageRefusal::LayerConflict)); + } + } else { + let id = group[0].0; + let h = self.layers[&id].hash; + self.passed.insert(u, h); + match self.apply(id, u) { + Ok(()) => { + reports.get(id, h).applied.push(u); + advanced.push(u); + } + Err(why) => reports.get(id, h).refused.push((u, why)), } - Err(why) => reports.get(h).refused.push((u, why)), } + i = j; } advanced } /// Steps 9 and 10 and application for one pair. - fn apply(&mut self, hash: &[u8; 32], u: u32) -> Result<(), StageRefusal> { - let l = &self.layers[hash]; - let kind = unit_kind(&l.cap).ok_or(StageRefusal::PayloadMalformed)?; - let w = wir1::parse(&l.payload, kind).map_err(|_| StageRefusal::PayloadMalformed)?; - let (from, _) = l.refines.units(self.n_units()); + fn apply(&mut self, id: u64, u: u32) -> Result<(), StageRefusal> { + let l = &self.layers[&id]; + let kind = l.wir.kind; let s = &self.units[u as usize]; let below_ok = matches!((kind, &s.buf.samples), (wir1::UnitKind::Pcm, Samples::I16(_)) | (wir1::UnitKind::Rgb8 | wir1::UnitKind::Attr, Samples::U8(_))); let shape = s.buf.shape; - if !below_ok || s.buf.samples.len() as u64 != shape.0 as u64 * shape.1 as u64 * shape.2 as u64 || !w.accepts_below(shape) { + // Checked: a shape whose element count passes u64 is no shape a unit holds. + let elements = (shape.0 as u64).checked_mul(shape.1 as u64).and_then(|v| v.checked_mul(shape.2 as u64)); + if !below_ok || elements != Some(s.buf.samples.len() as u64) || !l.wir.accepts_below(shape) { return Err(StageRefusal::GeometryMismatch); } if let Some(live) = &self.cfg.live { - let lead = live.lead_ms.get(l.refines.layer as usize - 1).copied().unwrap_or(0); - if !schedule::on_time(&live.clock, u, l.arrival_ms, lead) { + let lead = live.lead_ms.get(l.layer as usize - 1).copied().unwrap_or(0); + if !schedule::on_time(&live.clock, self.base.first_unit.saturating_add(u as u64), l.ready_ms, lead) { return Err(StageRefusal::DeadlineMissed); } } - let next = wir1::apply_unit(&w, u - from, s.buf.samples.below(), shape).map_err(|e| match e.refusal { + let next = wir1::apply_unit(&l.wir, &l.payload, u - l.from, s.buf.samples.below(), shape).map_err(|e| match e.refusal { Wir1Refusal::GeometryMismatch => StageRefusal::GeometryMismatch, _ => StageRefusal::PayloadMalformed, })?; - let layer = l.refines.layer; - let shape = (w.d0, w.d1, w.d2); + let (layer, hash, shape) = (l.layer, l.hash, (l.wir.d0, l.wir.d1, l.wir.d2)); let s = &mut self.units[u as usize]; + let old = s.buf.samples.byte_len(); s.buf = UnitBuf { shape, samples: match next { Next::U8(v) => Samples::U8(v), Next::I16(v) => Samples::I16(v) } }; s.stage = layer; - s.producer = *hash; + s.producer = hash; + s.producers.push(hash); s.tier = tier::compose(s.tier, &[Some(DeterminismTier::DecodeEquivalence)], &[true]); + self.state = self.state.saturating_sub(old).saturating_add(s.buf.samples.byte_len()).saturating_add(32); + self.passed.remove(&u); Ok(()) } - /// Freeze unit `u`: no further layer applies to it, and every layer held for it is - /// refused for it (`layer-conflict`). - fn freeze(&mut self, u: u32, reports: &mut Reports) { - self.units[u as usize].frozen = true; - let (gone, keep): (Vec<_>, Vec<_>) = self.held.iter().copied().partition(|(_, x)| *x == u); - self.held = keep; - for (h, x) in gone { - reports.get(h).refused.push((x, StageRefusal::LayerConflict)); - } - } - /// The pairs held for `advanced` units that a stage change decides: those now ready - /// form the next batch, in the order their layers were admitted; those no longer - /// waiting are refused (`prev-mismatch`). - fn release(&mut self, advanced: &[u32], reports: &mut Reports) -> Vec<([u8; 32], u32)> { + /// form the next batch; those no longer waiting are refused (`prev-mismatch`). + fn release(&mut self, advanced: &[u32], reports: &mut Reports) -> Vec<(u64, u32)> { let mut batch = Vec::new(); - let mut keep = Vec::new(); - let held = std::mem::take(&mut self.held); - for (h, u) in held { - if !advanced.contains(&u) { - keep.push((h, u)); + for &u in advanced { + if !self.held.contains_key(&u) { continue; } - if self.conflicted_before(&h, u) { - self.held.extend(keep.drain(..)); - self.freeze(u, reports); - keep = std::mem::take(&mut self.held); - reports.get(h).refused.push((u, StageRefusal::LayerConflict)); - continue; + let mut keep = Vec::new(); + for id in self.unhold_all(u) { + let h = self.layers[&id].hash; + if self.conflicted_before(id, u) { + self.units[u as usize].frozen = true; + reports.get(id, h).refused.push((u, StageRefusal::LayerConflict)); + continue; + } + match self.readiness(id, u) { + Readiness::Ready => batch.push((id, u)), + Readiness::Held => keep.push(id), + Readiness::Not => reports.get(id, h).refused.push((u, StageRefusal::PrevMismatch)), + } } - match self.readiness(&h, u) { - Readiness::Ready => batch.push((h, u)), - Readiness::Held => keep.push((h, u)), - Readiness::Not => reports.get(h).refused.push((u, StageRefusal::PrevMismatch)), + for id in keep { + if self.units[u as usize].frozen { + let h = self.layers[&id].hash; + reports.get(id, h).refused.push((u, StageRefusal::LayerConflict)); + } else { + self.hold(id, u); + } } } - self.held = keep; - batch.sort_by_key(|(h, u)| (self.layers[h].seq, *u)); batch } - /// Offer a layer that passed step 1 to this chain at `now_ms` (§4.1 steps 2–10): - /// the envelope's report first, then a report for every held layer the offer - /// decided units of, in the order those layers were admitted. - #[allow(clippy::too_many_arguments)] - fn offer_checked(&mut self, hash: [u8; 32], cap: &str, refines: Refines, payload: &[u8], now_ms: u64, evidence: ReceiptEvidence, outside_held: usize) -> Vec { - let mut reports = Reports::new(hash); - if self.layers.contains_key(&hash) { - // The same envelope again: steps 2–6 passed before; step 7 decides again. - } else if let Err((why, detail)) = self.admit(hash, cap, refines, payload, now_ms, evidence) { - return vec![OfferReport::refuse(hash, why, detail)]; + /// Drop every layer no unit is held for (§4.5): the end of an offer, a release or + /// an expiry. + fn prune(&mut self) { + let gone: Vec = self.layers.iter().filter(|(_, l)| l.held == 0).map(|(id, _)| *id).collect(); + for id in gone { + let l = self.layers.remove(&id).expect("listed"); + self.ids.remove(&l.hash); + self.state = self.state.saturating_sub(l.reserve); } - self.evaluate(hash, outside_held, &mut reports); - reports.finish(&self.layers) + } + + /// Offer a layer that passed step 1 to this chain (§4.1 steps 2–10): the envelope's + /// report first, then a report for every held layer the offer decided units of, in + /// the order those layers passed step 6. + fn offer_checked(&mut self, p: Pending, outside_held: usize) -> Vec { + let id = match self.ids.get(&p.hash) { + // The same envelope again, still retained: steps 2–6 passed and it adds + // nothing to the state; step 7 decides again. + Some(&id) => id, + None => match self.admit(p.hash, &p.cap, &p.refines, p.payload, p.arrival_ms, p.arrival_ms, p.evidence) { + Ok(id) => id, + Err((why, detail)) => return vec![OfferReport::refuse(p.hash, why, detail)], + }, + }; + let mut reports = Reports::default(); + reports.get(id, p.hash); + self.evaluate(id, outside_held, &mut reports); + let out = reports.finish(); + self.prune(); + out } /// Offer the envelope `bytes` at `now_ms` with the receipt `evidence` (§4.1). An /// envelope that names another base is refused `base-absent`: [`StageSink`] holds /// such envelopes. pub fn offer(&mut self, bytes: &[u8], now_ms: u64, evidence: ReceiptEvidence) -> Result, OfferError> { - let (hash, cap, refines, payload) = match step1(bytes)? { - Ok(x) => x, - Err(r) => return Ok(vec![r]), - }; - Ok(self.offer_checked(hash, &cap, refines, &payload, now_ms, evidence, 0)) + match step1(bytes, now_ms, evidence)? { + Ok(p) => Ok(self.offer_checked(p, 0)), + Err(r) => Ok(vec![r]), + } } - /// Release a batch of layers that passed step 1 at once (§4.4): each passes steps - /// 2–7 alone, then their ready pairs run as one batch. - fn offer_batch(&mut self, items: Vec) -> Vec { - let order: Vec<[u8; 32]> = items.iter().map(|p| p.hash).collect(); - let mut reports = Reports::new(order.first().copied().unwrap_or([0; 32])); - let mut ready = Vec::new(); + /// Release the layers held for this chain's base, together (§4.4): each passes + /// steps 2–8 alone, in the order they arrived, then their ready pairs run as one + /// batch. Reports are in the order the layers arrived. + fn release_pending(&mut self, items: Vec) -> Vec { + let mut reports = Reports::default(); let mut refused = Vec::new(); + let mut ready = Vec::new(); + let order: Vec<[u8; 32]> = items.iter().map(|p| p.hash).collect(); for p in items { - if !self.layers.contains_key(&p.hash) { - if let Err((why, detail)) = self.admit(p.hash, &p.cap, p.refines.clone(), &p.payload, p.arrival_ms, p.evidence) { - refused.push(OfferReport::refuse(p.hash, why, detail)); - continue; - } - } - reports.get(p.hash); - let l = &self.layers[&p.hash]; - let (from, count) = l.refines.units(self.n_units()); - for u in from..from + count { - if self.conflicted_before(&p.hash, u) { - self.freeze(u, &mut reports); - reports.get(p.hash).refused.push((u, StageRefusal::LayerConflict)); - continue; - } - match self.readiness(&p.hash, u) { - Readiness::Ready => ready.push((p.hash, u)), - Readiness::Held => { - self.held.push((p.hash, u)); - reports.get(p.hash).held.push(u); + let ready_ms = if self.base.attested { p.verified_ms } else { p.arrival_ms }; + let id = match self.ids.get(&p.hash) { + Some(&id) => id, + None => match self.admit(p.hash, &p.cap, &p.refines, p.payload, p.arrival_ms, ready_ms, p.evidence) { + Ok(id) => id, + Err((why, detail)) => { + refused.push(OfferReport::refuse(p.hash, why, detail)); + continue; } - Readiness::Not => reports.get(p.hash).refused.push((u, StageRefusal::PrevMismatch)), + }, + }; + reports.get(id, p.hash); + let (from, count) = { + let l = &self.layers[&id]; + (l.from, l.count) + }; + for u in from..from + count { + if self.conflicted_before(id, u) { + reports.get(id, p.hash).refused.push((u, StageRefusal::LayerConflict)); + self.freeze(u, &mut reports); + continue; + } + match self.readiness(id, u) { + Readiness::Ready => ready.push((id, u)), + Readiness::Held => { + if !self.is_held(id, u) { + self.hold(id, u); + } + reports.get(id, p.hash).held.push(u); + } + Readiness::Not => reports.get(id, p.hash).refused.push((u, StageRefusal::PrevMismatch)), } } } self.run_batches(ready, &mut reports); - let mut out = reports.finish(&self.layers); + let mut out = reports.finish(); + self.prune(); out.extend(refused); let pos = |h: &[u8; 32]| order.iter().position(|x| x == h).unwrap_or(usize::MAX); out.sort_by_key(|r| pos(&r.content_hash)); @@ -692,19 +890,29 @@ impl StageChain { /// Discard what has been held past the bound at `now_ms` (§4.3): each unit a /// layer is held for, with `layer-gap`. pub fn expire(&mut self, now_ms: u64) -> Vec { - let mut reports = Reports::new([0; 32]); - let held = std::mem::take(&mut self.held); - let mut keep = Vec::new(); - for (h, u) in held { - let l = &self.layers[&h]; - if now_ms.saturating_sub(l.arrival_ms) > self.cfg.bound.max_age_ms { - reports.get(h).refused.push((u, StageRefusal::LayerGap)); - } else { - keep.push((h, u)); + let age = self.cfg.bound.max_age_ms; + let old: Vec = self.layers.iter().filter(|(_, l)| l.held > 0 && now_ms.saturating_sub(l.arrival_ms) > age).map(|(id, _)| *id).collect(); + let mut reports = Reports::default(); + for id in old { + let (hash, from, count) = { + let l = &self.layers[&id]; + (l.hash, l.from, l.count) + }; + for u in from..from + count { + let Some(ids) = self.held.get_mut(&u) else { continue }; + let Some(at) = ids.iter().position(|x| *x == id) else { continue }; + ids.remove(at); + if ids.is_empty() { + self.held.remove(&u); + } + reports.get(id, hash).refused.push((u, StageRefusal::LayerGap)); } + self.layers.get_mut(&id).expect("retained").held = 0; + self.held_layers -= 1; } - self.held = keep; - reports.finish(&self.layers).into_iter().filter(|r| !r.refused.is_empty()).collect() + let out = reports.finish(); + self.prune(); + out } } @@ -715,33 +923,27 @@ enum Readiness { Not, } -/// Reports under construction, by content hash, with the first-offered envelope's -/// report first. +/// Reports under construction, by layer (the order they passed step 6), with the +/// first one asked for first. +#[derive(Default)] struct Reports { - first: [u8; 32], - by: BTreeMap<[u8; 32], OfferReport>, + first: Option, + by: BTreeMap, } impl Reports { - fn new(first: [u8; 32]) -> Self { - let mut by = BTreeMap::new(); - by.insert(first, OfferReport::new(first)); - Reports { first, by } + fn get(&mut self, id: u64, hash: [u8; 32]) -> &mut OfferReport { + self.first.get_or_insert(id); + self.by.entry(id).or_insert_with(|| OfferReport::new(hash)) } - fn get(&mut self, h: [u8; 32]) -> &mut OfferReport { - self.by.entry(h).or_insert_with(|| OfferReport::new(h)) - } - /// The first envelope's report, then the others by the order their layers were - /// admitted. - fn finish(mut self, layers: &BTreeMap<[u8; 32], Layer>) -> Vec { + /// The first layer's report, then the others in the order they passed step 6. + fn finish(mut self) -> Vec { let mut out = Vec::new(); - if let Some(mut r) = self.by.remove(&self.first) { + if let Some(mut r) = self.first.and_then(|f| self.by.remove(&f)) { r.sort(); out.push(r); } - let mut rest: Vec = self.by.into_values().collect(); - rest.sort_by_key(|r| layers.get(&r.content_hash).map(|l| l.seq).unwrap_or(u64::MAX)); - for mut r in rest { + for mut r in self.by.into_values() { r.sort(); out.push(r); } @@ -749,10 +951,9 @@ impl Reports { } } -/// §4.1 step 1: the envelope's content hash, capability and `refines`, or its -/// report. `Err` when the bytes are not a `WAI1` this sink reads. -#[allow(clippy::type_complexity)] -fn step1(bytes: &[u8]) -> Result), OfferReport>, OfferError> { +/// §4.1 step 1: the envelope as a layer to offer, or its report. `Err` when the bytes +/// are not a `WAI1` this sink reads. +fn step1(bytes: &[u8], now_ms: u64, evidence: ReceiptEvidence) -> Result, OfferError> { let hash = crate::jwp_hash::content_hash(bytes); let w = Wai::from_bytes(bytes).map_err(|e| OfferError::NotWai1(e.to_string()))?; if w.manifest.security.as_ref().is_some_and(|s| !s.is_plaintext()) { @@ -762,14 +963,14 @@ fn step1(bytes: &[u8]) -> Result), Of Some(Ok(r)) => r, None | Some(Err(_)) => return Ok(Err(OfferReport::refuse(hash, StageRefusal::RefinesMalformed, None))), }; - if !is_refinement_only(w.capability()) { + if !is_refinement_only(w.capability()) || w.manifest.model_requirement.fallback.is_some() { return Ok(Err(OfferReport::refuse(hash, StageRefusal::CapabilityNotRefinement, None))); } let cap = w.capability().to_string(); - Ok(Ok((hash, cap, refines, w.payload))) + Ok(Ok(Pending { hash, cap, refines, payload: w.payload, arrival_ms: now_ms, verified_ms: now_ms, evidence })) } -/// An envelope held for its base (§4.1 step 2). +/// A layer that passed step 1: held for its base (§4.1 step 2), or being offered. #[derive(Debug, Clone)] struct Pending { hash: [u8; 32], @@ -777,6 +978,8 @@ struct Pending { refines: Refines, payload: Vec, arrival_ms: u64, + /// When the sink verified the evidence it holds (§8, §10). + verified_ms: u64, evidence: ReceiptEvidence, } @@ -790,8 +993,8 @@ pub struct StageSink { } impl StageSink { - /// A sink holding no base, with the bound, cost limit and schedule every chain it - /// creates uses. + /// A sink holding no base, with the bound, cost limit, budget and schedule every + /// chain it creates uses. pub fn new(cfg: ChainConfig) -> Self { StageSink { cfg, chains: BTreeMap::new(), pending: vec![] } } @@ -801,46 +1004,59 @@ impl StageSink { self.chains.get(base) } - /// Record a decoded base and release the layers held for it, together (§4.4). + /// Record a decoded base and release the layers held for it, together (§4.4). A + /// base the sink already holds is kept as it is: decoding it again does not reset + /// its units, and reports nothing. pub fn add_base(&mut self, base: BaseInfo, units: Vec) -> Vec { let hash = base.content_hash; + if self.chains.contains_key(&hash) { + return vec![]; + } let mut chain = StageChain::new(base, units, self.cfg.clone()); let hexed = hex(&hash); let (mine, rest): (Vec, Vec) = std::mem::take(&mut self.pending).into_iter().partition(|p| p.refines.base == hexed); self.pending = rest; - let reports = if mine.is_empty() { vec![] } else { chain.offer_batch(mine) }; + let reports = if mine.is_empty() { vec![] } else { chain.release_pending(mine) }; self.chains.insert(hash, chain); reports } + /// Forget a base the sink no longer presents, with its chain; a layer for it is + /// then held as for any base the sink has not decoded. Whether it held the base. + pub fn remove_base(&mut self, base: &[u8; 32]) -> bool { + self.chains.remove(base).is_some() + } + /// Offer the envelope `bytes` at `now_ms` with the receipt `evidence` (§4.1). A /// layer whose base this sink has not decoded is held for it, up to the bound - /// (§4.3). + /// (§4.3); offered again while held, it keeps the strongest evidence offered. pub fn offer(&mut self, bytes: &[u8], now_ms: u64, evidence: ReceiptEvidence) -> Result, OfferError> { - let (hash, cap, refines, payload) = match step1(bytes)? { - Ok(x) => x, + let p = match step1(bytes, now_ms, evidence)? { + Ok(p) => p, Err(r) => return Ok(vec![r]), }; - let base = self.chains.keys().copied().find(|k| hex(k) == refines.base); + let base = unhex32(&p.refines.base).filter(|b| self.chains.contains_key(b)); match base { Some(b) => { let outside = self.pending.len() + self.chains.iter().filter(|(k, _)| **k != b).map(|(_, c)| c.held_envelopes()).sum::(); - Ok(self.chains.get_mut(&b).expect("present").offer_checked(hash, &cap, refines, &payload, now_ms, evidence, outside)) + Ok(self.chains.get_mut(&b).expect("present").offer_checked(p, outside)) } None => { - if self.pending.iter().any(|p| p.hash == hash) { - let mut r = OfferReport::new(hash); - r.held_for_base = true; + let mut r = OfferReport::new(p.hash); + r.held_for_base = true; + if let Some(q) = self.pending.iter_mut().find(|q| q.hash == p.hash) { + if evidence_rank(p.evidence) > evidence_rank(q.evidence) { + q.evidence = p.evidence; + q.verified_ms = now_ms; + } return Ok(vec![r]); } let held_layers = self.pending.len() + self.chains.values().map(StageChain::held_envelopes).sum::(); if held_layers >= self.cfg.bound.max_envelopes { - return Ok(vec![OfferReport::refuse(hash, StageRefusal::BaseAbsent, None)]); + return Ok(vec![OfferReport::refuse(p.hash, StageRefusal::BaseAbsent, None)]); } - self.pending.push(Pending { hash, cap, refines, payload, arrival_ms: now_ms, evidence }); - let mut r = OfferReport::new(hash); - r.held_for_base = true; + self.pending.push(p); Ok(vec![r]) } } @@ -861,3 +1077,13 @@ impl StageSink { out } } + +/// The order of evidence (§10): none, then a receipt verified by inclusion alone, then +/// one verified over the envelope's bytes. +fn evidence_rank(e: ReceiptEvidence) -> u8 { + match e { + ReceiptEvidence::None => 0, + ReceiptEvidence::InclusionVerified => 1, + ReceiptEvidence::DeliveredVerified => 2, + } +} diff --git a/wai/wai-rs/src/staged/schedule.rs b/wai/wai-rs/src/staged/schedule.rs index c0a7ce636..bc54c6caf 100644 --- a/wai/wai-rs/src/staged/schedule.rs +++ b/wai/wai-rs/src/staged/schedule.rs @@ -4,8 +4,9 @@ //! of arrivals. Pure integer functions of their inputs, so the corpus's `schedule.json` //! is replayed exactly by the Rust and the Python verifiers. -/// The timing of a stage set's units: unit `u` is presented at -/// `t0_ms + u·unit_ms + target_latency_ms`. +/// The timing of a stage set's units: the unit at position `u` on the clock is +/// presented at `t0_ms + u·unit_ms + target_latency_ms`. A base's unit `i` sits at +/// position `first_unit + i` ([`super::BaseInfo::first_unit`]). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct UnitClock { pub t0_ms: u64, @@ -22,14 +23,15 @@ pub struct LayerPlan { pub bytes: u64, } -/// When unit `unit` is presented (saturating at `u64::MAX`). -pub fn presentation_ms(c: &UnitClock, unit: u32) -> u64 { - c.t0_ms.saturating_add((unit as u64).saturating_mul(c.unit_ms)).saturating_add(c.target_latency_ms) +/// When the unit at clock position `unit` is presented (saturating at `u64::MAX`). +pub fn presentation_ms(c: &UnitClock, unit: u64) -> u64 { + c.t0_ms.saturating_add(unit.saturating_mul(c.unit_ms)).saturating_add(c.target_latency_ms) } /// Whether a layer that arrived at `arrival_ms` with lead `lead_ms` is in time for -/// unit `unit`: it arrived at least `lead_ms` before the unit's presentation (§8). -pub fn on_time(c: &UnitClock, unit: u32, arrival_ms: u64, lead_ms: u64) -> bool { +/// the unit at clock position `unit`: it arrived at least `lead_ms` before the unit's +/// presentation (§8). +pub fn on_time(c: &UnitClock, unit: u64, arrival_ms: u64, lead_ms: u64) -> bool { arrival_ms.checked_add(lead_ms).is_some_and(|t| t <= presentation_ms(c, unit)) } @@ -83,7 +85,7 @@ pub fn present(c: &UnitClock, base_ms: &[u64], layers: &[Vec>], lead .iter() .enumerate() .map(|(u, &b)| { - let unit = u as u32; + let unit = u as u64; let at_ms = presentation_ms(c, unit).max(b); let mut stage = 0u8; let mut expected = 0u8; diff --git a/wai/wai-rs/src/staged/tests.rs b/wai/wai-rs/src/staged/tests.rs index 5d833f661..7b245466f 100644 --- a/wai/wai-rs/src/staged/tests.rs +++ b/wai/wai-rs/src/staged/tests.rs @@ -67,9 +67,9 @@ fn roundtrip_all_predictors() { for (max_symbols, per_unit) in [(4095, false), (5, true), (1, false)] { let (bytes, recon) = encode(UnitKind::Rgb8, &b, (9, 7, 3), &t, (9, 7, 3), ¶ms(1, Predictor::Identity, max_symbols, per_unit)).unwrap(); let w = parse(&bytes, UnitKind::Rgb8).unwrap(); - assert_eq!(w.tables.len(), if per_unit { 2 } else { 1 }); + assert_eq!(w.n_tables(), if per_unit { 2 } else { 1 }); for u in 0..2 { - let got = apply_unit(&w, u, b[u as usize], (9, 7, 3)).unwrap(); + let got = apply_unit(&w, &bytes, u, b[u as usize], (9, 7, 3)).unwrap(); assert_eq!(got, recon[u as usize]); assert_eq!(as_u8(&got), &target[u as usize][..], "q = 1 identity is lossless (max_symbols {max_symbols})"); } @@ -82,7 +82,7 @@ fn roundtrip_all_predictors() { let (bytes, recon) = encode(UnitKind::Rgb8, &[Below::U8(&below)], (h2 as u32, w2 as u32, 3), &[Below::U8(&target)], (h, w, 3), ¶ms(q, Predictor::Up2, 7, false)).unwrap(); let w1 = parse(&bytes, UnitKind::Rgb8).unwrap(); - let got = apply_unit(&w1, 0, Below::U8(&below), (h2 as u32, w2 as u32, 3)).unwrap(); + let got = apply_unit(&w1, &bytes, 0, Below::U8(&below), (h2 as u32, w2 as u32, 3)).unwrap(); assert_eq!(got, recon[0]); if q == 1 { assert_eq!(as_u8(&got), &target[..], "q = 1 up2 is lossless ({h2}x{w2} → {h}x{w})"); @@ -100,7 +100,7 @@ fn roundtrip_all_predictors() { let p = RefineParams { q: 1, predictor: Predictor::Rate, rate: (num, den), table_per_unit: false, max_symbols: 3 }; let (bytes, recon) = encode(UnitKind::Pcm, &[Below::I16(&below)], (n_src as u32, 1, ch as u32), &[Below::I16(&target)], (n_out, 1, ch as u32), &p).unwrap(); let w = parse(&bytes, UnitKind::Pcm).unwrap(); - let got = apply_unit(&w, 0, Below::I16(&below), (n_src as u32, 1, ch as u32)).unwrap(); + let got = apply_unit(&w, &bytes, 0, Below::I16(&below), (n_src as u32, 1, ch as u32)).unwrap(); assert_eq!(got, recon[0]); assert_eq!(as_i16(&got), &target[..], "q = 1 rate is lossless ({n_src}×{ch} at {num}/{den})"); } @@ -110,14 +110,14 @@ fn roundtrip_all_predictors() { let p = params(300, Predictor::Identity, 2, false); let (bytes, recon) = encode(UnitKind::Pcm, &[Below::I16(&below)], (6, 1, 1), &[Below::I16(&target)], (6, 1, 1), &p).unwrap(); let w = parse(&bytes, UnitKind::Pcm).unwrap(); - assert_eq!(apply_unit(&w, 0, Below::I16(&below), (6, 1, 1)).unwrap(), recon[0]); + assert_eq!(apply_unit(&w, &bytes, 0, Below::I16(&below), (6, 1, 1)).unwrap(), recon[0]); assert!(as_i16(&recon[0]).iter().zip(&target).all(|(a, b)| (*a as i32 - *b as i32).abs() <= 150)); // splat attributes, identity. let below = picture(5, 5, 8)[..14 * 4].to_vec(); let target = picture(5, 5, 9)[..14 * 4].to_vec(); let (bytes, recon) = encode(UnitKind::Attr, &[Below::U8(&below)], (14, 2, 2), &[Below::U8(&target)], (14, 2, 2), ¶ms(1, Predictor::Identity, 9, false)).unwrap(); let w = parse(&bytes, UnitKind::Attr).unwrap(); - assert_eq!(as_u8(&apply_unit(&w, 0, Below::U8(&below), (14, 2, 2)).unwrap()), &target[..]); + assert_eq!(as_u8(&apply_unit(&w, &bytes, 0, Below::U8(&below), (14, 2, 2)).unwrap()), &target[..]); assert_eq!(as_u8(&recon[0]), &target[..]); } @@ -208,7 +208,7 @@ fn wir1_refusal_order() { fn wir1_stream_ends() { let ok = fixture(); let w = parse(&ok, UnitKind::Rgb8).unwrap(); - assert!(w.check_streams().is_ok()); + assert!(w.check_streams(&ok).is_ok()); let cdf_len = u16::from_le_bytes([ok[40], ok[41]]) as usize; let lens_at = 44 + 4 * cdf_len; let len = u32::from_le_bytes(ok[lens_at..lens_at + 4].try_into().unwrap()); @@ -217,21 +217,21 @@ fn wir1_stream_ends() { extra[lens_at..lens_at + 4].copy_from_slice(&(len + 4).to_le_bytes()); extra.extend_from_slice(&[0, 0, 0, 0]); let we = parse(&extra, UnitKind::Rgb8).unwrap(); - assert_eq!(we.check_streams().unwrap_err().1.code(), "stream_noncanonical"); + assert_eq!(we.check_streams(&extra).unwrap_err().1.code(), "stream_noncanonical"); let below = picture(2, 2, 1); - assert_eq!(apply_unit(&we, 0, Below::U8(&below), (2, 2, 3)).unwrap_err().code(), "stream_noncanonical"); + assert_eq!(apply_unit(&we, &extra, 0, Below::U8(&below), (2, 2, 3)).unwrap_err().code(), "stream_noncanonical"); // A unit of four times the elements runs the stream out. let mut big = ok.clone(); big[12..16].copy_from_slice(&4u32.to_le_bytes()); big[16..20].copy_from_slice(&4u32.to_le_bytes()); let wb = parse(&big, UnitKind::Rgb8).unwrap(); - assert_eq!(wb.check_streams().unwrap_err().1.code(), "stream_exhausted"); + assert_eq!(wb.check_streams(&big).unwrap_err().1.code(), "stream_exhausted"); let below4 = picture(4, 4, 1); - assert_eq!(apply_unit(&wb, 0, Below::U8(&below4), (4, 4, 3)).unwrap_err().code(), "stream_exhausted"); + assert_eq!(apply_unit(&wb, &big, 0, Below::U8(&below4), (4, 4, 3)).unwrap_err().code(), "stream_exhausted"); // The wrong shape below is refused before any symbol. - assert_eq!(apply_unit(&w, 0, Below::U8(&below4), (4, 4, 3)).unwrap_err().code(), "geometry_mismatch"); - assert_eq!(apply_unit(&w, 0, Below::I16(&[0; 12]), (2, 2, 3)).unwrap_err().code(), "geometry_mismatch"); - assert_eq!(apply_unit(&w, 1, Below::U8(&below), (2, 2, 3)).unwrap_err().code(), "geometry_mismatch"); + assert_eq!(apply_unit(&w, &ok, 0, Below::U8(&below4), (4, 4, 3)).unwrap_err().code(), "geometry_mismatch"); + assert_eq!(apply_unit(&w, &ok, 0, Below::I16(&[0; 12]), (2, 2, 3)).unwrap_err().code(), "geometry_mismatch"); + assert_eq!(apply_unit(&w, &ok, 1, Below::U8(&below), (2, 2, 3)).unwrap_err().code(), "geometry_mismatch"); } // ───────────────────────── the stage chain (§4) ───────────────────────── @@ -269,13 +269,14 @@ fn base4() -> (Vec, BaseInfo, Vec) { via_fallback: false, tier: Some(DeterminismTier::DecodeEquivalence), attested: false, + first_unit: 0, }; let units = (0..4).map(|f| UnitBuf { shape: (H, W, 3), samples: Samples::U8(frame(f)) }).collect(); (env, info, units) } fn cfg() -> ChainConfig { - ChainConfig { bound: HoldBound { max_envelopes: 8, max_age_ms: 1000 }, cost_limit: None, live: None } + ChainConfig { bound: HoldBound { max_envelopes: 8, max_age_ms: 1000 }, cost_limit: None, budget: u64::MAX, live: None } } /// A residual layer over `belows` (units `from..from+belows.len()`) toward frames of @@ -319,6 +320,7 @@ fn refines_check_rejects_each_malformation() { assert_eq!(r.producer_for(3), None); assert!(read(json!({"base": a, "prev": a, "layer": 1})).is_ok()); assert!(read(json!({"base": a, "prev": a, "layer": 1, "unknown": [1, 2]})).is_ok(), "an undefined member is ignored"); + assert_eq!(read(json!({"base": a, "prev": a, "layer": 1, "span": null})).unwrap().span, None, "a null span is absent (SPEC §3.1)"); let bad = [ json!({"base": a.to_uppercase(), "prev": a, "layer": 1}), json!({"base": &a[..63], "prev": &a[..63], "layer": 1}), @@ -344,6 +346,19 @@ fn refines_check_rejects_each_malformation() { for (i, v) in bad.iter().enumerate() { assert!(read(v.clone()).is_err(), "case {i} is malformed: {v}"); } + // An integer member is written as an integer: no fraction, no exponent, no sign on + // zero (SPEC §3). + for (k, v) in [("layer", "-0"), ("layer", "1.0"), ("layer", "1e0"), ("layer", "-0.0"), ("from", "-0"), ("from", "0e0"), ("count", "1E0"), ("count", "2.5")] { + let text = match k { + "layer" => format!(r#"{{"base": "{a}", "prev": "{a}", "layer": {v}}}"#), + "from" => format!(r#"{{"base": "{a}", "prev": "{a}", "layer": 2, "span": {{"from": {v}, "count": 1}}}}"#), + _ => format!(r#"{{"base": "{a}", "prev": [{{"producer": "{a}", "count": {v}}}, {{"producer": "{b}", "count": 1}}], "layer": 2}}"#), + }; + let parsed: serde_json::Value = serde_json::from_str(&text).unwrap(); + assert!(read(parsed).is_err(), "{k}: {v} is not an integer"); + } + let zero: serde_json::Value = serde_json::from_str(&format!(r#"{{"base": "{a}", "prev": "{a}", "layer": 2, "span": {{"from": 0, "count": 1}}}}"#)).unwrap(); + assert!(read(zero).is_ok()); // The checks that need the base: a span past the base, runs not summing to it. let r = read(json!({"base": a, "prev": a, "layer": 2, "span": {"from": 3, "count": 2}})).unwrap(); assert!(r.check_units(4).is_err() && r.check_units(5).is_ok()); @@ -701,12 +716,14 @@ fn compose_table() { assert!(c.unit_sha256(0).is_none()); } -/// Test 22: for random arrival traces, a unit's stage-0 output is presented at its -/// presentation time whenever its base arrived by then, whatever its layers do, and -/// its stage is the run of layers that arrived in time. +/// Test 22: for random arrival traces, played through a live [`StageSink`], every +/// unit's stage at its presentation time is the run of layers 1, 2, … that arrived in +/// time, a layer that arrives after that is never applied, and the base is presented +/// whatever its layers do: the sink's stage for each unit is what [`present`] states +/// for the trace. #[test] fn base_never_waits() { - let c = UnitClock { t0_ms: 40, unit_ms: 33, target_latency_ms: 100 }; + let clock = UnitClock { t0_ms: 40, unit_ms: 33, target_latency_ms: 100 }; let mut x: u64 = 0x1234_5678_9abc_def1; let mut next = |m: u64| { x ^= x << 13; @@ -714,31 +731,245 @@ fn base_never_waits() { x ^= x << 17; x % m }; - for _ in 0..200 { - let n = 1 + next(12) as usize; - let base: Vec = (0..n).map(|u| presentation_ms(&c, u as u32).saturating_sub(next(150)) + next(60)).collect(); - let layers: Vec>> = - (0..3).map(|_| (0..n).map(|_| if next(4) == 0 { None } else { Some(next(800)) }).collect()).collect(); - let lead = vec![next(30), next(30), next(30)]; - let p = present(&c, &base, &layers, &lead); - for u in 0..n { - let due = presentation_ms(&c, u as u32); - if base[u] <= due { - assert_eq!(p[u].at_ms, due, "unit {u} presented on time"); - } else { - assert_eq!(p[u].at_ms, base[u], "a late base is presented when it arrives"); - } - let mut want = 0u8; - for l in 0..3 { - match layers[l][u] { - Some(t) if t + lead[l] <= due && want as usize == l => want += 1, - _ => break, - } - } - assert_eq!(p[u].stage, want, "unit {u}"); - assert!(p[u].stage <= p[u].expected); + let (_, info, units) = base4(); + let b = info.content_hash; + // Per unit, a chain of three single-unit layers (built in order on a chain with + // no schedule). + let mut scratch = StageChain::new(info.clone(), units.clone(), cfg()); + let mut envs: Vec>> = vec![vec![]; 4]; + for u in 0..4u32 { + let mut prev = b; + for l in 1..=3u8 { + let e = layer(&b, Prev::One(hexs(&prev)), l, Some(Span { from: u, count: 1 }), &[unit_frame(&scratch, u)], 100 * l as u32 + u); + assert_eq!(scratch.offer(&e, 0, ReceiptEvidence::None).unwrap()[0].applied, vec![u]); + prev = h(&e); + envs[u as usize].push(e); } } + let mut checked = 0; + let (mut stages, mut missed) = ([0u32; 4], 0u32); + for _ in 0..150 { + let arrivals: Vec>> = + (0..3).map(|_| (0..4).map(|_| if next(4) == 0 { None } else { Some(next(400)) }).collect()).collect(); + let lead = vec![next(30), next(30), next(30)]; + let cfg_live = ChainConfig { live: Some(Live { clock, lead_ms: lead.clone() }), bound: HoldBound { max_envelopes: 64, max_age_ms: 10_000 }, ..cfg() }; + let mut sink = StageSink::new(cfg_live); + sink.add_base(info.clone(), units.clone()); + let mut events: Vec<(u64, usize, usize)> = Vec::new(); + for (l, per_unit) in arrivals.iter().enumerate() { + for (u, t) in per_unit.iter().enumerate() { + if let Some(t) = t { + events.push((*t, l, u)); + } + } + } + events.sort_unstable(); + let p = present(&clock, &[0; 4], &arrivals, &lead); + let mut at_due = [None; 4]; + for (t, l, u) in events { + // What the sink presents for each unit due by now, before this arrival. + for v in 0..4 { + if at_due[v].is_none() && presentation_ms(&clock, v as u64) < t { + at_due[v] = Some(sink.chain(&b).unwrap().unit(v as u32).unwrap().stage); + } + } + for r in sink.offer(&envs[u][l], t, ReceiptEvidence::None).unwrap() { + for (v, why) in &r.refused { + assert!(matches!(why, StageRefusal::DeadlineMissed | StageRefusal::PrevMismatch), "unit {v}: {why:?}"); + missed += (*why == StageRefusal::DeadlineMissed) as u32; + } + } + } + let c = sink.chain(&b).unwrap(); + for u in 0..4 { + let now = c.unit(u as u32).unwrap().stage; + assert_eq!(now, p[u].stage, "unit {u}: the sink's stage is the run of layers in time"); + if let Some(s) = at_due[u] { + assert_eq!(s, now, "unit {u}: nothing arriving after its presentation applied"); + } + assert!(p[u].stage <= p[u].expected); + stages[now as usize] += 1; + checked += 1; + } + } + assert_eq!(checked, 600); + assert!(missed > 0 && stages.iter().all(|&n| n > 0), "the traces reach every stage and miss deadlines: {stages:?}, {missed} missed"); +} + +/// A live schedule over a set split into bases: each base's units sit on the one clock +/// from its `first_unit`, so a layer for the second base's unit 0 is due when the +/// set's unit 4 is (§8). +#[test] +fn live_clock_per_base() { + let (_, info, units) = base4(); + let env2 = envelope(CAP_VIDEO_INT_MOTION, None, b"the second group of frames".to_vec()); + let info2 = BaseInfo { content_hash: h(&env2), first_unit: 4, ..info.clone() }; + let b2 = info2.content_hash; + let live = ChainConfig { live: Some(Live { clock: UnitClock { t0_ms: 0, unit_ms: 100, target_latency_ms: 0 }, lead_ms: vec![50] }), ..cfg() }; + let l = layer(&b2, one(&b2), 1, Some(Span { from: 0, count: 1 }), &[frame(0)], 70); + let mut sink = StageSink::new(live.clone()); + sink.add_base(info.clone(), units.clone()); + sink.add_base(info2.clone(), units.clone()); + // Unit 0 of the second base is presented at 400: in time at 150 and at 350. + assert_eq!(sink.offer(&l, 150, ReceiptEvidence::None).unwrap()[0].applied, vec![0]); + let mut late = StageSink::new(live.clone()); + late.add_base(info2.clone(), units.clone()); + assert_eq!(codes(&late.offer(&l, 351, ReceiptEvidence::None).unwrap()[0]), vec![(0, "deadline-missed")]); + // At position 0 the same layer at 150 would be late. + let mut first = StageSink::new(live); + first.add_base(BaseInfo { first_unit: 0, ..info2 }, units); + assert_eq!(codes(&first.offer(&l, 150, ReceiptEvidence::None).unwrap()[0]), vec![(0, "deadline-missed")]); +} + +/// Decoding a base the sink holds again keeps its units: stages, producers, frozen +/// units and what passed step 8 stay, and nothing is reported. +#[test] +fn readding_a_base_keeps_its_units() { + let (_, info, units) = base4(); + let b = info.content_hash; + let la = layer(&b, one(&b), 1, Some(Span { from: 1, count: 1 }), &[frame(1)], 80); + let lb = layer(&b, one(&b), 1, Some(Span { from: 1, count: 1 }), &[frame(1)], 81); + let mut sink = StageSink::new(cfg()); + sink.add_base(info.clone(), units.clone()); + assert_eq!(sink.offer(&la, 0, ReceiptEvidence::None).unwrap()[0].applied, vec![1]); + assert_eq!(codes(&sink.offer(&lb, 1, ReceiptEvidence::None).unwrap()[0]), vec![(1, "layer-conflict")]); + let before = sink.chain(&b).unwrap().units().to_vec(); + assert!(sink.add_base(info.clone(), units.clone()).is_empty(), "no report"); + assert_eq!(sink.chain(&b).unwrap().units(), &before[..], "the units are kept"); + assert!(sink.chain(&b).unwrap().unit(1).unwrap().frozen); + assert_eq!(codes(&sink.offer(&lb, 2, ReceiptEvidence::None).unwrap()[0]), vec![(1, "layer-conflict")]); + assert_eq!(sink.chain(&b).unwrap().unit(1).unwrap().producer, h(&la)); + // Forgotten, then decoded again: a new chain. + assert!(sink.remove_base(&b)); + assert!(sink.chain(&b).is_none()); + assert!(sink.offer(&lb, 3, ReceiptEvidence::None).unwrap()[0].held_for_base); + let r = sink.add_base(info, units); + assert_eq!(r[0].applied, vec![1]); +} + +/// A unit or a stage below whose stated shape's element count passes u64 is refused +/// (`geometry_mismatch`, `geometry-mismatch`), not wrapped. +#[test] +fn shape_products_are_checked() { + let below = picture(2, 2, 1); + let ok = fixture(); + let w = parse(&ok, UnitKind::Rgb8).unwrap(); + let huge = (u32::MAX, u32::MAX, u32::MAX); + assert_eq!(apply_unit(&w, &ok, 0, Below::U8(&below), huge).unwrap_err().code(), "geometry_mismatch"); + let (_, info, _) = base4(); + let b = info.content_hash; + let l1 = layer(&b, one(&b), 1, Some(Span { from: 0, count: 1 }), &[frame(0)], 50); + let units = vec![ + UnitBuf { shape: huge, samples: Samples::U8(vec![0; 3]) }, + UnitBuf { shape: (H, W, 3), samples: Samples::U8(frame(1)) }, + ]; + let mut c = StageChain::new(info, units, cfg()); + assert_eq!(codes(&c.offer(&l1, 0, ReceiptEvidence::None).unwrap()[0]), vec![(0, "geometry-mismatch")]); +} + +/// A layer whose manifest names a fallback is refused at step 1 +/// (`capability-not-refinement`): §3 requires `fallback` to be null. +#[test] +fn fallback_refinement_refused_at_step_1() { + let (_, info, units) = base4(); + let b = info.content_hash; + let ok = layer(&b, one(&b), 1, None, &(0..4).map(frame).collect::>(), 50); + let mut w = Wai::from_bytes(&ok).unwrap(); + w.manifest.model_requirement = ModelRequirement::new(CAP_VIDEO_INT_REFINE).with_fallback(CAP_VIDEO_INT_MOTION); + let fb = w.to_bytes().unwrap(); + let mut c = StageChain::new(info.clone(), units.clone(), cfg()); + let r = &c.offer(&fb, 0, ReceiptEvidence::None).unwrap()[0]; + assert_eq!(r.envelope, Some(StageRefusal::CapabilityNotRefinement)); + assert!((0..4).all(|u| c.unit(u).unwrap().stage == 0)); + // Held for its base, it is refused at step 1 on arrival, not on release. + let mut sink = StageSink::new(cfg()); + assert_eq!(sink.offer(&fb, 0, ReceiptEvidence::None).unwrap()[0].envelope, Some(StageRefusal::CapabilityNotRefinement)); + assert!(sink.add_base(info, units).is_empty()); +} + +/// A layer held for its base and refused when the base arrives has one report, its +/// refusal; the reports of a release are in the order the layers arrived. +#[test] +fn refused_on_release_reports_once() { + let (_, info, units) = base4(); + let b = info.content_hash; + let ok = layer(&b, one(&b), 1, Some(Span { from: 0, count: 1 }), &[frame(0)], 50); + let mut w = Wai::from_bytes(&ok).unwrap(); + w.payload.truncate(w.payload.len() - 4); + let bad = w.to_bytes().unwrap(); + let ok2 = layer(&b, one(&b), 1, Some(Span { from: 2, count: 1 }), &[frame(2)], 52); + let mut sink = StageSink::new(cfg()); + for (t, e) in [&bad, &ok2].iter().enumerate() { + assert!(sink.offer(e, t as u64, ReceiptEvidence::None).unwrap()[0].held_for_base); + } + let r = sink.add_base(info, units); + assert_eq!(r.len(), 2, "one report per layer: {r:?}"); + assert_eq!((r[0].content_hash, r[0].envelope, r[0].detail), (h(&bad), Some(StageRefusal::PayloadMalformed), Some("stream_length"))); + assert_eq!((r[1].content_hash, r[1].applied.clone()), (h(&ok2), vec![2])); +} + +/// A layer held for its base and offered again with stronger evidence keeps the +/// stronger evidence, and counts as arrived for a live deadline when the sink verified +/// it (§8, §10). +#[test] +fn stronger_evidence_upgrades_a_held_layer() { + let (_, info, units) = base4(); + let b = info.content_hash; + let att = BaseInfo { attested: true, ..info }; + let l1 = layer(&b, one(&b), 1, Some(Span { from: 0, count: 1 }), &[frame(0)], 50); + let mut sink = StageSink::new(cfg()); + assert!(sink.offer(&l1, 0, ReceiptEvidence::None).unwrap()[0].held_for_base); + assert!(sink.offer(&l1, 3, ReceiptEvidence::DeliveredVerified).unwrap()[0].held_for_base); + assert!(sink.offer(&l1, 4, ReceiptEvidence::InclusionVerified).unwrap()[0].held_for_base, "weaker evidence keeps the stronger"); + assert_eq!(sink.add_base(att.clone(), units.clone())[0].applied, vec![0]); + // Without the upgrade: refinement-unattested. + let mut sink = StageSink::new(cfg()); + sink.offer(&l1, 0, ReceiptEvidence::None).unwrap(); + assert_eq!(sink.add_base(att.clone(), units.clone())[0].envelope, Some(StageRefusal::RefinementUnattested)); + // Unit 0 is presented at 100 with lead 96: arrived at 0 it was in time, but it was + // verified at 5, which is late for an attested base. + let live = ChainConfig { live: Some(Live { clock: UnitClock { t0_ms: 0, unit_ms: 100, target_latency_ms: 100 }, lead_ms: vec![96] }), ..cfg() }; + let mut sink = StageSink::new(live.clone()); + sink.offer(&l1, 0, ReceiptEvidence::None).unwrap(); + sink.offer(&l1, 5, ReceiptEvidence::DeliveredVerified).unwrap(); + assert_eq!(codes(&sink.add_base(att.clone(), units.clone())[0]), vec![(0, "deadline-missed")]); + let mut sink = StageSink::new(live); + sink.offer(&l1, 4, ReceiptEvidence::DeliveredVerified).unwrap(); + assert_eq!(sink.add_base(att, units)[0].applied, vec![0]); +} + +/// The chain budget (§4.5): a layer that would take the chain's state past it is +/// refused at step 6 (`chain-over-limit`), after the cost limit and before the +/// streams; what a chain retains is counted until no unit is held for it. +#[test] +fn chain_budget() { + let (_, info, units) = base4(); + let b = info.content_hash; + let below: Vec> = (0..4).map(frame).collect(); + let l1 = layer(&b, one(&b), 1, None, &below, 50); + let p = Wai::from_bytes(&l1).unwrap().payload; + let cost = parse(&p, UnitKind::Rgb8).unwrap().declared_cost(); + let units_bytes = 4 * (H * W * 3) as u64; + let need = units_bytes + p.len() as u64 + cost.output_bytes + cost.working_bytes; + let mut c = StageChain::new(info.clone(), units.clone(), ChainConfig { budget: need - 1, ..cfg() }); + assert_eq!(c.state_bytes(), units_bytes); + let r = &c.offer(&l1, 0, ReceiptEvidence::None).unwrap()[0]; + assert_eq!((r.envelope, r.detail), (Some(StageRefusal::ChainOverLimit), None)); + assert!(StageRefusal::ChainOverLimit.is_envelope_scoped()); + assert_eq!(StageRefusal::from_code("chain-over-limit"), Some(StageRefusal::ChainOverLimit)); + // The cost limit is checked first; a malformed stream after the budget. + let tight = ChainConfig { budget: need - 1, cost_limit: Some(super::wir1::Wir1Cost { output_bytes: 1, symbols: u64::MAX, working_bytes: u64::MAX }), ..cfg() }; + assert_eq!(StageChain::new(info.clone(), units.clone(), tight).offer(&l1, 0, ReceiptEvidence::None).unwrap()[0].envelope, Some(StageRefusal::CostOverLimit)); + let mut w = Wai::from_bytes(&l1).unwrap(); + let n = w.payload.len(); + w.payload[n - 4..].copy_from_slice(&[0xff; 4]); + let damaged = w.to_bytes().unwrap(); + assert_eq!(StageChain::new(info.clone(), units.clone(), ChainConfig { budget: need - 1, ..cfg() }).offer(&damaged, 0, ReceiptEvidence::None).unwrap()[0].envelope, Some(StageRefusal::ChainOverLimit)); + // At the budget it applies, and the state is the new output and 32 bytes a stage. + let mut c = StageChain::new(info, units, ChainConfig { budget: need, ..cfg() }); + assert_eq!(c.offer(&l1, 0, ReceiptEvidence::None).unwrap()[0].applied, vec![0, 1, 2, 3]); + assert_eq!(c.state_bytes(), units_bytes + 4 * 32); + assert_eq!(c.unit(0).unwrap().producers(), &[h(&l1)]); } // ───────────────────────── WAI2 identity (SPEC §7.1) ───────────────────────── @@ -752,7 +983,7 @@ fn chain3() -> (Vec, Vec, Vec, Vec) { let units: Vec = (0..4).map(|f| UnitBuf { shape: (H, W, 3), samples: Samples::U8(frame(f)) }).collect(); let below: Vec> = (0..4).map(frame).collect(); let l1 = layer(&b, one(&b), 1, None, &below, 50); - let mut c = StageChain::new(BaseInfo { content_hash: b, capability: CAP_VIDEO_INT_MOTION.into(), via_fallback: false, tier: Some(DeterminismTier::DecodeEquivalence), attested: false }, units.clone(), cfg()); + let mut c = StageChain::new(BaseInfo { content_hash: b, capability: CAP_VIDEO_INT_MOTION.into(), via_fallback: false, tier: Some(DeterminismTier::DecodeEquivalence), attested: false, first_unit: 0 }, units.clone(), cfg()); c.offer(&l1, 0, ReceiptEvidence::None).unwrap(); let mid: Vec> = (0..4).map(|f| unit_frame(&c, f)).collect(); let l2 = layer(&b, Prev::One(hexs(&h(&l1))), 2, None, &mid, 60); @@ -818,7 +1049,7 @@ fn explode_pack_roundtrip() { let all = WaiMulti::explode_all(&bytes).unwrap(); let r = Wai::from_bytes(&all[1]).unwrap().refines().unwrap().unwrap(); assert_eq!(r.base, hexs(&h(&all[0]))); - let mut c = StageChain::new(BaseInfo { content_hash: h(&all[0]), capability: CAP_VIDEO_INT_MOTION.into(), via_fallback: false, tier: Some(DeterminismTier::DecodeEquivalence), attested: false }, units, cfg()); + let mut c = StageChain::new(BaseInfo { content_hash: h(&all[0]), capability: CAP_VIDEO_INT_MOTION.into(), via_fallback: false, tier: Some(DeterminismTier::DecodeEquivalence), attested: false, first_unit: 0 }, units, cfg()); assert_eq!(c.offer(&all[1], 0, ReceiptEvidence::None).unwrap()[0].applied, vec![0, 1, 2, 3]); } diff --git a/wai/wai-rs/src/staged/wir1.rs b/wai/wai-rs/src/staged/wir1.rs index c0e22deaf..89cc93bdb 100644 --- a/wai/wai-rs/src/staged/wir1.rs +++ b/wai/wai-rs/src/staged/wir1.rs @@ -199,9 +199,12 @@ impl Predictor { } } -/// A parsed `WIR1` payload. +/// A parsed `WIR1` payload: its header, its tables (flat, one after another) and +/// where each unit's stream lies in the payload. It holds no reference to the payload, +/// so a sink keeps it beside the payload's bytes; the streams are read from them +/// ([`Wir1::stream`]). #[derive(Debug, Clone, PartialEq, Eq)] -pub struct Wir1<'a> { +pub struct Wir1 { pub kind: UnitKind, pub predictor: Predictor, /// One table for every unit (`false`), or one per unit (`true`). @@ -214,17 +217,23 @@ pub struct Wir1<'a> { pub rate_den: u32, pub q: u32, pub offset: i32, - /// One table, or one per unit. - pub tables: Vec>, - /// One stream per unit. - pub streams: Vec<&'a [u8]>, + /// Each table's length. + pub cdf_len: usize, + /// One table, or one per unit, `cdf_len` entries each, one after another. + tables: Vec, + /// Where the streams start in the payload. + streams_at: usize, + /// Where each unit's stream ends in the payload; unit `u`'s starts where unit + /// `u − 1`'s ends. + ends: Vec, } /// The cost a `WIR1` header declares, read before any symbol is decoded: the layer's /// output over its units, its symbols, and the most bytes its decode holds at once -/// beyond the payload and the stage below (the parsed tables, and one unit's output -/// while it is built, since a unit is applied whole). A residual runs no network, so -/// it has no multiply-accumulates. +/// beyond the payload and the stage below (the parsed tables, 4 bytes per entry; the +/// stream index, 8 bytes per unit; and one unit's output while it is built, since a +/// unit is applied whole). A residual runs no network, so it has no +/// multiply-accumulates. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Wir1Cost { pub output_bytes: u64, @@ -244,10 +253,6 @@ impl<'a> Cur<'a> { self.p += n; Ok(s) } - fn u32(&mut self, what: &'static str) -> Result { - let s = self.take(4, what)?; - Ok(u32::from_le_bytes([s[0], s[1], s[2], s[3]])) - } } fn le32(b: &[u8], at: usize) -> u32 { @@ -287,7 +292,7 @@ fn check_cdf(cdf: &[i32]) -> Result<(), Wir1Error> { /// (`cdf_malformed`); the stream lengths present (`stream_length`), each a multiple /// of 4 and at least 8 (`stream_length`); the streams present (`stream_length`); /// 8. nothing after the last stream (`trailing`). -pub fn parse(p: &[u8], kind: UnitKind) -> Result, Wir1Error> { +pub fn parse(p: &[u8], kind: UnitKind) -> Result { use Wir1Refusal::*; if p.get(0..4) != Some(&MAGIC[..]) { return Err(err(BadMagic, "the payload does not start with WIR1")); @@ -352,28 +357,32 @@ pub fn parse(p: &[u8], kind: UnitKind) -> Result, Wir1Error> { return Err(err(TooLarge, "the payload holds more than 2^30 elements")); } let mut c = Cur { b: p, p: HEADER }; - let n_tables = if table_mode == 1 { n_units } else { 1 }; - let mut tables = Vec::new(); + let n_tables = if table_mode == 1 { n_units as usize } else { 1 }; + // Sized by what the payload holds, so the parse allocates nothing its bytes do not + // justify; the tables are taken and checked one at a time (A.3 step 7). + let mut tables: Vec = Vec::with_capacity((n_tables * cdf_len).min((p.len() - c.p) / 4)); for _ in 0..n_tables { - let s = c.take(cdf_len as u64 * 4, "a table")?; - let t: Vec = s - .chunks_exact(4) - .map(|w| i32::try_from(u32::from_le_bytes([w[0], w[1], w[2], w[3]])).unwrap_or(i32::MAX)) - .collect(); - check_cdf(&t)?; - tables.push(t); + let t = c.take(cdf_len as u64 * 4, "a table")?; + let at = tables.len(); + tables.extend(t.chunks_exact(4).map(|w| i32::try_from(u32::from_le_bytes([w[0], w[1], w[2], w[3]])).unwrap_or(i32::MAX))); + check_cdf(&tables[at..])?; } - let mut lens = Vec::new(); - for _ in 0..n_units { - let l = c.u32("a stream length")?; + let lens = c.take(n_units as u64 * 4, "a stream length")?; + let streams_at = c.p; + let mut ends: Vec = Vec::with_capacity(n_units as usize); + let mut end = streams_at as u64; + for w in lens.chunks_exact(4) { + let l = u32::from_le_bytes([w[0], w[1], w[2], w[3]]); if l % 4 != 0 || l < 8 { return Err(err(StreamLength, "a stream length is not a multiple of 4, or is under 8")); } - lens.push(l); + end += l as u64; + // A stream past the payload's end is refused at the streams' presence below. + ends.push(usize::try_from(end).unwrap_or(usize::MAX)); } - let mut streams = Vec::new(); - for l in lens { - streams.push(c.take(l as u64, "a stream")?); + for (u, &e) in ends.iter().enumerate() { + let start = if u == 0 { streams_at } else { ends[u - 1] }; + c.take(e.saturating_sub(start) as u64, "a stream")?; } if c.p != p.len() { return Err(err(Trailing, "bytes follow the last stream")); @@ -390,12 +399,14 @@ pub fn parse(p: &[u8], kind: UnitKind) -> Result, Wir1Error> { rate_den, q, offset, + cdf_len, tables, - streams, + streams_at, + ends, }) } -impl Wir1<'_> { +impl Wir1 { /// Elements per unit. pub fn unit_elements(&self) -> u64 { self.d0 as u64 * self.d1 as u64 * self.d2 as u64 @@ -412,16 +423,31 @@ impl Wir1<'_> { /// The cost the header declares ([`Wir1Cost`]). pub fn declared_cost(&self) -> Wir1Cost { let unit_bytes = self.unit_elements() * self.elem_bytes(); - let tables: u64 = self.tables.iter().map(|t| 4 * t.len() as u64).sum(); + let tables = 4 * self.tables.len() as u64; Wir1Cost { output_bytes: unit_bytes * self.n_units as u64, symbols: self.unit_elements() * self.n_units as u64, - working_bytes: tables + unit_bytes, + working_bytes: tables + 8 * self.n_units as u64 + unit_bytes, } } - fn table(&self, unit: usize) -> &[i32] { - if self.table_per_unit { &self.tables[unit] } else { &self.tables[0] } + /// The number of tables: 1, or one per unit. + pub fn n_tables(&self) -> usize { + self.tables.len() / self.cdf_len.max(1) + } + + /// The table unit `unit` is decoded against. + pub fn table(&self, unit: usize) -> &[i32] { + let t = if self.table_per_unit { unit } else { 0 }; + &self.tables[t * self.cdf_len..(t + 1) * self.cdf_len] + } + + /// Unit `unit`'s stream in `payload`, the bytes this was parsed from; empty for a + /// unit outside the payload or a payload that is not the one parsed. + pub fn stream<'p>(&self, payload: &'p [u8], unit: usize) -> &'p [u8] { + let Some(&end) = self.ends.get(unit) else { return &[] }; + let start = if unit == 0 { self.streams_at } else { self.ends[unit - 1] }; + payload.get(start..end).unwrap_or(&[]) } /// The shape the stage below a unit must have for this payload's predictor: @@ -442,12 +468,12 @@ impl Wir1<'_> { /// runs to its last symbol and ends canonically: whether the payload is well /// formed (a payload malformed for one unit is malformed for all). Units in order; /// the first refusal. - pub fn check_streams(&self) -> Result<(), (u32, Wir1Error)> { + pub fn check_streams(&self, payload: &[u8]) -> Result<(), (u32, Wir1Error)> { let n = self.unit_elements(); - let cdf_len = self.tables[0].len(); - for (u, s) in self.streams.iter().enumerate() { + let cdf_len = self.cdf_len; + for u in 0..self.ends.len() { let fail = |e| (u as u32, e); - let mut dec = RansDecoder::new(s).map_err(|_| fail(err(Wir1Refusal::StreamLength, "a stream")))?; + let mut dec = RansDecoder::new(self.stream(payload, u)).map_err(|_| fail(err(Wir1Refusal::StreamLength, "a stream")))?; let t = self.table(u); for _ in 0..n { dec.decode(t, cdf_len, self.offset); @@ -478,7 +504,7 @@ pub enum Next { } /// The prediction of element `i` of a unit (raster order, `i = (a·d1 + b)·d2 + c`). -fn pred_at(w: &Wir1<'_>, below: Below<'_>, below_shape: (u32, u32, u32), i: u64) -> i64 { +fn pred_at(w: &Wir1, below: Below<'_>, below_shape: (u32, u32, u32), i: u64) -> i64 { let (d1, d2) = (w.d1 as u64, w.d2 as u64); let (a, b, c) = (i / (d1 * d2), (i / d2) % d1, i % d2); match (w.predictor, below) { @@ -499,13 +525,17 @@ fn pred_at(w: &Wir1<'_>, below: Below<'_>, below_shape: (u32, u32, u32), i: u64) /// then `stream_exhausted` at the first symbol whose decode runs the stream out, and /// `stream_noncanonical` at an end that is not canonical. Nothing is returned for a /// refused unit, so a caller never holds a partly decoded one. -pub fn apply_unit(w: &Wir1<'_>, unit: u32, below: Below<'_>, below_shape: (u32, u32, u32)) -> Result { +pub fn apply_unit(w: &Wir1, payload: &[u8], unit: u32, below: Below<'_>, below_shape: (u32, u32, u32)) -> Result { use Wir1Refusal::*; let u = unit as usize; - if u >= w.streams.len() { + if u >= w.ends.len() { return Err(err(GeometryMismatch, "the unit is outside the payload")); } - let below_len = below_shape.0 as u64 * below_shape.1 as u64 * below_shape.2 as u64; + // Checked: a shape whose element count passes u64 is no shape a payload reads. + let below_len = (below_shape.0 as u64) + .checked_mul(below_shape.1 as u64) + .and_then(|v| v.checked_mul(below_shape.2 as u64)) + .ok_or(err(GeometryMismatch, "the unit's current output is not the shape the predictor reads"))?; let type_ok = matches!((w.kind, below), (UnitKind::Pcm, Below::I16(_)) | (UnitKind::Rgb8 | UnitKind::Attr, Below::U8(_))); let len = match below { Below::U8(p) => p.len() as u64, @@ -516,13 +546,12 @@ pub fn apply_unit(w: &Wir1<'_>, unit: u32, below: Below<'_>, below_shape: (u32, } let n = w.unit_elements(); let t = w.table(u); - let mut dec = RansDecoder::new(w.streams[u]).map_err(|_| err(StreamLength, "a stream"))?; - // Built in the element type (the clamp puts every value in its range), so a unit - // holds the bytes its declared cost counts; grown as symbols arrive, so a stream - // that runs out stops before the declared size is reached. + let mut dec = RansDecoder::new(w.stream(payload, u)).map_err(|_| err(StreamLength, "a stream"))?; + // Built in the element type (the clamp puts every value in its range), sized + // exactly: a unit holds the bytes its declared cost counts, and no more. let mut out = match w.kind { - UnitKind::Pcm => Next::I16(Vec::new()), - _ => Next::U8(Vec::new()), + UnitKind::Pcm => Next::I16(Vec::with_capacity(n as usize)), + _ => Next::U8(Vec::with_capacity(n as usize)), }; for i in 0..n { let r = dec.decode(t, t.len(), w.offset); @@ -593,7 +622,7 @@ pub fn encode(kind: UnitKind, belows: &[Below<'_>], below_shape: (u32, u32, u32) let plan = Wir1 { kind, predictor: p.predictor, table_per_unit: p.table_per_unit, n_units: n_units as u32, d0: dims.0, d1: dims.1, d2: dims.2, rate_num, rate_den, q: p.q, offset: 0, - tables: vec![vec![0, 1, 1 << 16]], streams: vec![&[][..]; n_units], + cdf_len: 3, tables: vec![0, 1, 1 << 16], streams_at: 0, ends: vec![0; n_units], }; if !plan.accepts_below(below_shape) { return Err(enc_err("the stage below is not the shape the predictor reads")); @@ -680,7 +709,7 @@ pub fn encode(kind: UnitKind, belows: &[Below<'_>], below_shape: (u32, u32, u32) let w = parse(&out, kind).map_err(|e| enc_err(format!("the encoder wrote a payload the parser refuses: {e}")))?; let mut recon = Vec::with_capacity(n_units); for (u, b) in belows.iter().enumerate() { - let next = apply_unit(&w, u as u32, *b, below_shape).map_err(|e| enc_err(format!("unit {u}: {e}")))?; + let next = apply_unit(&w, &out, u as u32, *b, below_shape).map_err(|e| enc_err(format!("unit {u}: {e}")))?; // The reconstruction the encoder intended, element by element. for (i, &r) in residuals[u].iter().enumerate() { let want = step(pred_at(&w, *b, below_shape, i as u64), r, p.q as i64, lo, hi); diff --git a/wai/wai-rs/tests/staged_resources.rs b/wai/wai-rs/tests/staged_resources.rs new file mode 100644 index 000000000..0b88fce0e --- /dev/null +++ b/wai/wai-rs/tests/staged_resources.rs @@ -0,0 +1,325 @@ +//! The staged sink's own work and memory (`extensions/staged-delivery.md` §4.5 and +//! Appendix A.5), measured with a global allocator that counts the bytes allocated, +//! the allocations, the bytes live and their peak. +//! +//! - Work per layer is linear in its units: an offer of a layer over 4096, 16384 and +//! 65536 units (one layer alone; a layer whose `prev` is a run per unit, held; a +//! layer held and then released by the layer below it) allocates, holds and takes +//! time in proportion to the units, within a margin a quadratic sink is far outside. +//! - The chain budget: a layer that would take the chain's state past it is refused +//! (`chain-over-limit`) before any stream is decoded, and the state the chain +//! reports is the sum §4.5 defines. +//! - Retention: a layer no unit is held for is dropped with its payload, so offering +//! many layers that conflict, or that expire, leaves the sink holding what it held. +//! - The parse holds no more than the declared working bytes beyond the payload. +//! +//! Its own test binary with a single test, so that no other test's allocations land in +//! the windows it measures. +#![cfg(all(feature = "staged", feature = "video_int"))] + +use std::alloc::{GlobalAlloc, Layout, System}; +use std::sync::atomic::{AtomicUsize, Ordering::Relaxed}; +use std::time::{Duration, Instant}; + +use wai::codecs::{DeterminismTier, CAP_VIDEO_INT_MOTION, CAP_VIDEO_INT_REFINE}; +use wai::container::{Conditioning, Manifest, ModelRequirement, Prev, PrevRun, Refines, Wai}; +use wai::staged::wir1::{self, Below, Predictor, RefineParams, UnitKind}; +use wai::staged::{BaseInfo, ChainConfig, HoldBound, ReceiptEvidence, Samples, StageRefusal, StageSink, UnitBuf}; + +/// The system allocator, counting bytes allocated, allocations, bytes live and peak. +struct Count; +static ALLOCATED: AtomicUsize = AtomicUsize::new(0); +static CALLS: AtomicUsize = AtomicUsize::new(0); +static CUR: AtomicUsize = AtomicUsize::new(0); +static PEAK: AtomicUsize = AtomicUsize::new(0); + +fn grow(n: usize) { + ALLOCATED.fetch_add(n, Relaxed); + CALLS.fetch_add(1, Relaxed); + PEAK.fetch_max(CUR.fetch_add(n, Relaxed) + n, Relaxed); +} + +unsafe impl GlobalAlloc for Count { + unsafe fn alloc(&self, l: Layout) -> *mut u8 { + grow(l.size()); + unsafe { System.alloc(l) } + } + unsafe fn alloc_zeroed(&self, l: Layout) -> *mut u8 { + grow(l.size()); + unsafe { System.alloc_zeroed(l) } + } + unsafe fn realloc(&self, p: *mut u8, l: Layout, new: usize) -> *mut u8 { + if new >= l.size() { + grow(new - l.size()); + } else { + CUR.fetch_sub(l.size() - new, Relaxed); + } + unsafe { System.realloc(p, l, new) } + } + unsafe fn dealloc(&self, p: *mut u8, l: Layout) { + CUR.fetch_sub(l.size(), Relaxed); + unsafe { System.dealloc(p, l) } + } +} + +#[global_allocator] +static ALLOC: Count = Count; + +/// What a window measured: bytes allocated, allocations, the most bytes live at once +/// beyond the start, and the time. +#[derive(Debug, Clone, Copy)] +struct Window { + allocated: usize, + calls: usize, + peak: usize, + time: Duration, +} + +fn measure(f: impl FnOnce() -> T) -> (T, Window) { + let start = CUR.load(Relaxed); + PEAK.store(start, Relaxed); + let (a0, c0) = (ALLOCATED.load(Relaxed), CALLS.load(Relaxed)); + let t = Instant::now(); + let out = f(); + let time = t.elapsed(); + let w = Window { allocated: ALLOCATED.load(Relaxed) - a0, calls: CALLS.load(Relaxed) - c0, peak: PEAK.load(Relaxed) - start, time }; + (out, w) +} + +fn hex(h: &[u8; 32]) -> String { + h.iter().map(|b| format!("{b:02x}")).collect() +} + +fn cfg(max_envelopes: usize, budget: u64) -> ChainConfig { + ChainConfig { bound: HoldBound { max_envelopes, max_age_ms: 1000 }, cost_limit: None, budget, live: None } +} + +/// A base of `n` frames of `side × side` RGB8, named by `hash`. +fn base(n: u32, side: u32, hash: [u8; 32]) -> (BaseInfo, Vec) { + let info = BaseInfo { + content_hash: hash, + capability: CAP_VIDEO_INT_MOTION.into(), + via_fallback: false, + tier: Some(DeterminismTier::DecodeEquivalence), + attested: false, + first_unit: 0, + }; + let units = (0..n) + .map(|f| UnitBuf { shape: (side, side, 3), samples: Samples::U8((0..side * side * 3).map(|i| ((i * 7 + f * 3) % 251) as u8).collect()) }) + .collect(); + (info, units) +} + +/// A `wai.video.int_refine` envelope: an identity layer bringing each of `units` to +/// itself plus `delta`. +fn layer(base: &[u8; 32], prev: Prev, layer: u8, units: &[UnitBuf], delta: u8) -> Vec { + toward(base, prev, layer, units, |_, x| x.wrapping_add(delta)) +} + +/// A layer toward a target of noise seeded by `seed`, so its payload is about the +/// size of its output. +fn noisy(base: &[u8; 32], prev: Prev, layer: u8, units: &[UnitBuf], seed: u32) -> Vec { + toward(base, prev, layer, units, move |i, x| x ^ ((i as u32).wrapping_mul(2654435761).wrapping_add(seed.wrapping_mul(40503)) >> 13) as u8) +} + +fn toward(base: &[u8; 32], prev: Prev, layer: u8, units: &[UnitBuf], f: impl Fn(usize, u8) -> u8) -> Vec { + let shape = units[0].shape; + let b: Vec = units.iter().map(|u| match &u.samples { + Samples::U8(v) => Below::U8(v), + Samples::I16(v) => Below::I16(v), + }).collect(); + let targets: Vec> = units.iter().map(|u| match &u.samples { + Samples::U8(v) => v.iter().enumerate().map(|(i, x)| f(i, *x)).collect(), + Samples::I16(_) => unreachable!(), + }).collect(); + let t: Vec = targets.iter().map(|v| Below::U8(v)).collect(); + let p = RefineParams { q: 1, predictor: Predictor::Identity, rate: (0, 0), table_per_unit: false, max_symbols: 4095 }; + let (payload, _) = wir1::encode(UnitKind::Rgb8, &b, shape, &t, shape, &p).expect("encode"); + let r = Refines { base: hex(base), prev, layer, span: None }; + let m = Manifest::new("video", "replicate", ModelRequirement::new(CAP_VIDEO_INT_REFINE), Conditioning::new("wir1")).with_refines(&r); + Wai::new(m, payload).to_bytes().expect("serialise") +} + +/// The three shapes of offer, at `n` units: a layer 1 alone; a layer 2 whose `prev` is +/// one run per unit, held; a layer 2 held, then the layer 1 that releases it. +fn offers(n: u32) -> [Window; 3] { + let bh = [0x5a; 32]; + let (info, units) = base(n, 1, bh); + let l1 = layer(&bh, Prev::One(hex(&bh)), 1, &units, 1); + let h1 = wai::jwp_hash::content_hash(&l1); + let mut sink = StageSink::new(cfg(16, u64::MAX)); + sink.add_base(info.clone(), units.clone()); + let (r, single) = measure(|| sink.offer(&l1, 0, ReceiptEvidence::None).unwrap()); + assert_eq!(r[0].applied.len(), n as usize); + + let (pa, pb) = (hex(&[0xaa; 32]), hex(&[0xbb; 32])); + let runs: Vec = (0..n).map(|i| PrevRun { producer: if i % 2 == 0 { pa.clone() } else { pb.clone() }, count: 1 }).collect(); + let l2r = layer(&bh, Prev::Runs(runs), 2, &units, 2); + let mut sink = StageSink::new(cfg(16, u64::MAX)); + sink.add_base(info.clone(), units.clone()); + let (r, runs) = measure(|| sink.offer(&l2r, 0, ReceiptEvidence::None).unwrap()); + assert_eq!(r[0].held.len(), n as usize); + + let l1_out: Vec = units + .iter() + .map(|u| match &u.samples { + Samples::U8(v) => UnitBuf { shape: u.shape, samples: Samples::U8(v.iter().map(|x| x.wrapping_add(1)).collect()) }, + Samples::I16(_) => unreachable!(), + }) + .collect(); + let l2 = layer(&bh, Prev::One(hex(&h1)), 2, &l1_out, 1); + let mut sink = StageSink::new(cfg(16, u64::MAX)); + sink.add_base(info, units); + let (r, held) = measure(|| { + let a = sink.offer(&l2, 0, ReceiptEvidence::None).unwrap(); + let b = sink.offer(&l1, 1, ReceiptEvidence::None).unwrap(); + (a, b) + }); + assert_eq!(r.0[0].held.len(), n as usize); + assert_eq!((r.1[0].applied.len(), r.1[1].applied.len()), (n as usize, n as usize)); + [single, runs, held] +} + +#[test] +fn staged_sink_resources() { + // ── Work per layer, linear in its units ── + let sizes = [4096u32, 16384, 65536]; + // The fastest of three runs at each size, so a busy machine does not decide. + let mut at: Vec<[Window; 3]> = Vec::new(); + for &n in &sizes { + let mut best = offers(n); + for _ in 0..2 { + let w = offers(n); + for k in 0..3 { + if w[k].time < best[k].time { + best[k].time = w[k].time; + } + } + } + at.push(best); + } + for (k, name) in ["layer 1 alone", "layer 2, a run per unit, held", "layer 2 held, then layer 1"].iter().enumerate() { + for (i, &n) in sizes.iter().enumerate() { + let w = at[i][k]; + println!("{name:32} {n:6} units: {:9.3} ms {:10} bytes allocated {:7} allocations peak +{} bytes", + w.time.as_secs_f64() * 1e3, w.allocated, w.calls, w.peak); + } + // 16× the units: a linear sink stays near 16×, a quadratic one near 256×. + let (lo, hi) = (at[0][k], at[2][k]); + let ratio = |a: usize, b: usize| a as f64 / b.max(1) as f64; + assert!(ratio(hi.allocated, lo.allocated) <= 24.0, "{name}: bytes allocated grew {:.1}× for 16× the units", ratio(hi.allocated, lo.allocated)); + assert!(ratio(hi.calls, lo.calls) <= 24.0, "{name}: allocations grew {:.1}× for 16× the units", ratio(hi.calls, lo.calls)); + assert!(ratio(hi.peak, lo.peak) <= 24.0, "{name}: the peak grew {:.1}× for 16× the units", ratio(hi.peak, lo.peak)); + let t = hi.time.as_secs_f64() / lo.time.as_secs_f64().max(1e-4); + assert!(t <= 64.0 || hi.time < Duration::from_millis(50), "{name}: the offer's time grew {t:.1}× for 16× the units"); + } + + // ── The chain budget ── + let bh = [0x6b; 32]; + let (info, units) = base(4, 8, bh); + let l1 = layer(&bh, Prev::One(hex(&bh)), 1, &units, 1); + let payload_len = Wai::from_bytes(&l1).unwrap().payload.len() as u64; + let w = wir1::parse(&Wai::from_bytes(&l1).unwrap().payload, UnitKind::Rgb8).unwrap(); + let c = w.declared_cost(); + let unit_bytes: u64 = units.iter().map(|u| u.samples.byte_len()).sum(); + let reserve = payload_len + c.output_bytes + c.working_bytes; + // One byte short: refused before any stream is decoded, nothing changes. + let mut sink = StageSink::new(cfg(16, unit_bytes + reserve - 1)); + sink.add_base(info.clone(), units.clone()); + let (r, refused) = measure(|| sink.offer(&l1, 0, ReceiptEvidence::None).unwrap()); + assert_eq!(r[0].envelope, Some(StageRefusal::ChainOverLimit)); + assert!(r[0].applied.is_empty() && r[0].refused.is_empty()); + let chain = sink.chain(&bh).unwrap(); + assert!(chain.units().iter().all(|s| s.stage == 0)); + assert_eq!(chain.state_bytes(), unit_bytes); + // The refusal held the envelope as parsed and the payload's parse, and no unit's output. + assert!((refused.peak as u64) < 2 * l1.len() as u64 + c.working_bytes - c.output_bytes / 4, + "a refused layer held {} bytes", refused.peak); + // At the budget: applied; the state is the units' new output and 32 bytes a stage. + let mut sink = StageSink::new(cfg(16, unit_bytes + reserve)); + sink.add_base(info.clone(), units.clone()); + let r = sink.offer(&l1, 0, ReceiptEvidence::None).unwrap(); + assert_eq!(r[0].applied, vec![0, 1, 2, 3]); + assert_eq!(sink.chain(&bh).unwrap().state_bytes(), unit_bytes + 32 * 4); + + // A held layer is counted while it is retained, and released when it is not. + let h1 = wai::jwp_hash::content_hash(&l1); + let l2 = layer(&bh, Prev::One(hex(&h1)), 2, &units, 3); + let mut sink = StageSink::new(cfg(16, u64::MAX)); + sink.add_base(info.clone(), units.clone()); + let r = sink.offer(&l2, 0, ReceiptEvidence::None).unwrap(); + assert_eq!(r[0].held, vec![0, 1, 2, 3]); + let p2 = Wai::from_bytes(&l2).unwrap().payload; + let c2 = wir1::parse(&p2, UnitKind::Rgb8).unwrap().declared_cost(); + assert_eq!(sink.chain(&bh).unwrap().state_bytes(), unit_bytes + p2.len() as u64 + c2.output_bytes + c2.working_bytes); + let r = sink.expire(10_000); + assert_eq!(r[0].refused.len(), 4); + assert_eq!(sink.chain(&bh).unwrap().state_bytes(), unit_bytes); + + // ── Retention: what no unit holds is dropped ── + // Distinct layer-1 envelopes over one unit: the first applies, the second conflicts + // and freezes the unit, every later one is refused for it. None is held, so the sink + // keeps none of their payloads. + let bh = [0x7c; 32]; + let (info, units) = base(1, 64, bh); + let mut sink = StageSink::new(cfg(1, u64::MAX)); + sink.add_base(info.clone(), units.clone()); + let layers: Vec> = (0..48).map(|i| noisy(&bh, Prev::One(hex(&bh)), 1, &units, i)).collect(); + let after_first = { + sink.offer(&layers[0], 0, ReceiptEvidence::None).unwrap(); + CUR.load(Relaxed) + }; + for (i, l) in layers.iter().enumerate().skip(1) { + let r = sink.offer(l, i as u64, ReceiptEvidence::None).unwrap(); + assert_eq!(r[0].refused, vec![(0, StageRefusal::LayerConflict)]); + } + let grew = CUR.load(Relaxed).saturating_sub(after_first); + println!("retention: 47 refused layers of {} bytes each; the sink grew {grew} bytes", layers[1].len()); + assert!(grew < layers[1].len() / 2, "the sink kept {grew} bytes of layers no unit holds"); + // Held layers that expire are dropped too. + let mut sink = StageSink::new(cfg(64, u64::MAX)); + sink.add_base(info.clone(), units.clone()); + let before = CUR.load(Relaxed); + for (i, l) in layers.iter().enumerate().take(16) { + let mut l2 = Wai::from_bytes(l).unwrap(); + let r = Refines { base: hex(&bh), prev: Prev::One(hex(&[i as u8 + 1; 32])), layer: 2, span: None }; + l2.manifest = l2.manifest.clone().with_refines(&r); + let r = sink.offer(&l2.to_bytes().unwrap(), 0, ReceiptEvidence::None).unwrap(); + assert_eq!(r[0].held, vec![0]); + } + assert!(CUR.load(Relaxed) > before + 16 * layers[1].len() / 2, "held layers are kept"); + let r = sink.expire(10_000); + assert_eq!(r.len(), 16); + drop(r); + let grew = CUR.load(Relaxed).saturating_sub(before); + assert!(grew < layers[1].len() / 2, "the sink kept {grew} bytes after every held layer expired"); + + // ── The parse holds no more than the declared working bytes beyond the payload ── + // One table per unit, 65536 units of one attribute each. + let n = 65536u32; + let mut p = Vec::new(); + p.extend_from_slice(b"WIR1"); + p.extend_from_slice(&[1, 1, 0, 1]); + for v in [n, 1u32, 1, 1, 0, 0, 1] { + p.extend_from_slice(&v.to_le_bytes()); + } + p.extend_from_slice(&0i32.to_le_bytes()); + p.extend_from_slice(&3u16.to_le_bytes()); + p.extend_from_slice(&[0, 0]); + for _ in 0..n { + for v in [0u32, 65535, 65536] { + p.extend_from_slice(&v.to_le_bytes()); + } + } + for _ in 0..n { + p.extend_from_slice(&8u32.to_le_bytes()); + } + for _ in 0..n { + p.extend_from_slice(&(1u32 << 31).to_le_bytes()); + p.extend_from_slice(&0u32.to_le_bytes()); + } + let (w, parse) = measure(|| wir1::parse(&p, UnitKind::Attr).unwrap()); + let c = w.declared_cost(); + println!("parse: {} units, one table each: holds {} bytes, declares {} working bytes", n, parse.peak, c.working_bytes); + assert!(parse.peak as u64 <= c.working_bytes, "the parse held {} bytes, past the declared {}", parse.peak, c.working_bytes); +} -- 2.54.0 (Apple Git-157) From 8fa68207f6fc86b7ef44c8a9fad25bd72ce19dcf Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 4 Oct 2026 20:13:25 -0400 Subject: [PATCH 16/36] wai: WAI2 explodes entry by entry, pack checks its own round trip, names are unique MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WaiMulti::explode(i) returns entry i's own result (explode_each is public): an entry that cannot be exploded leaves every entry that does not name it intact. The corpus states each entry's hash, null only where that entry fails. - pack refuses a WAI1 with bytes around its manifest's JSON value or after its payload, and before returning parses its output, explodes it back to the sources byte for byte and runs check_envelope_manifests; a manifest nested near the depth limit, which a WAI1 reader takes, is refused because inside a WAI2 entry it passes the limit. SPEC §7.1 states the rule. - A refinement-only entry's envelope_manifest names no fallback (component-manifest-mismatch). - A manifest that names a member twice in one object, at any depth, is malformed (SPEC §2), in Wai::from_bytes and WaiMulti::from_bytes. - New WAI2 corpus files, one per refinement rule of SPEC §7.1 not covered before (not refinement-only, identity missing, base missing, base not admitted, refines on a non-refinement, a refinement-only capability as a plain component, malformed refines) and a refinement envelope_manifest with a fallback. - Test 20 models the earlier selection with its digest and pin skips. Co-Authored-By: Claude Opus 5.5 --- wai/SPEC.md | 20 ++- wai/staged-conformance/staged_verify.py | 5 +- wai/staged-conformance/wai2/index.json | 129 +++++++++++++- .../wai2/refinement_base_missing.wai | Bin 0 -> 1033 bytes .../wai2/refinement_base_not_admitted.wai | Bin 0 -> 1015 bytes .../wai2/refinement_identity_missing.wai | Bin 0 -> 953 bytes .../wai2/refinement_manifest_fallback.wai | Bin 0 -> 3683 bytes .../wai2/refinement_not_refinement_only.wai | Bin 0 -> 1035 bytes .../wai2/refinement_only_as_component.wai | Bin 0 -> 959 bytes .../wai2/refinement_refines_malformed.wai | Bin 0 -> 1029 bytes .../wai2/refines_on_non_refinement.wai | Bin 0 -> 1079 bytes wai/wai-rs/src/bin/wai_staged_vectors.rs | 45 ++++- wai/wai-rs/src/container.rs | 62 +++++++ wai/wai-rs/src/container_explode.rs | 49 +++++- wai/wai-rs/src/staged/tests.rs | 157 +++++++++++++++++- 15 files changed, 442 insertions(+), 25 deletions(-) create mode 100644 wai/staged-conformance/wai2/refinement_base_missing.wai create mode 100644 wai/staged-conformance/wai2/refinement_base_not_admitted.wai create mode 100644 wai/staged-conformance/wai2/refinement_identity_missing.wai create mode 100644 wai/staged-conformance/wai2/refinement_manifest_fallback.wai create mode 100644 wai/staged-conformance/wai2/refinement_not_refinement_only.wai create mode 100644 wai/staged-conformance/wai2/refinement_only_as_component.wai create mode 100644 wai/staged-conformance/wai2/refinement_refines_malformed.wai create mode 100644 wai/staged-conformance/wai2/refines_on_non_refinement.wai diff --git a/wai/SPEC.md b/wai/SPEC.md index 86444110b..7f0831d27 100644 --- a/wai/SPEC.md +++ b/wai/SPEC.md @@ -69,6 +69,10 @@ OPTIONAL; §5 says which are RECOMMENDED. whitespace, preserving key insertion order. Readers MUST NOT depend on key order for *semantics*; encoders SHOULD preserve order so files round-trip byte-identically. +- Member names are unique within each object of a manifest, at any + depth: a manifest that names a member twice in one object is malformed, + and a reader rejects it as it rejects any manifest that does not parse. + Names compare after their escapes are read. - An integer member of a manifest, or of any JSON this specification or its extensions define, is a JSON number written as an integer: digits, with a minus sign only before a non-zero value, and no fraction or @@ -1704,10 +1708,18 @@ by which capability happens to be installed. ([jwp-receipts](extensions/jwp-receipts.md) §2.1). An entry MAY carry `envelope_manifest`, a JSON object: the exact manifest of the entry as a `WAI1` envelope. Its `model_requirement.capability`, `conditioning.kind` and - `conditioning.prior` MUST equal the entry's, and its `refines` MUST be the entry's - with each id replaced as above, or both absent (`component-manifest-mismatch`). A - `WAI1` envelope packed into a `WAI2` with its manifest copied verbatim into - `envelope_manifest` explodes back to the same bytes, and so keeps its content hash. + `conditioning.prior` MUST equal the entry's, its `refines` MUST be the entry's + with each id replaced as above, or both absent, and, where the entry's capability is + refinement-only, its `model_requirement.fallback` MUST be `null` + (`component-manifest-mismatch`). A `WAI1` envelope packed into a `WAI2` with its + manifest copied verbatim into `envelope_manifest` explodes back to the same bytes, + and so keeps its content hash, when its manifest has no bytes before or after its + JSON value (whitespace around a member's value is not part of it, so an explode + cannot write it back) and nothing follows its payload; a packer refuses any other + envelope, and refuses an output that does not explode back to its sources (one + whose manifest, nested in the `WAI2` manifest, passes a reader's depth limit). Entry + *i*'s exploded envelope is defined by entry *i* and the entries its `refines` names, + so an entry that cannot be exploded leaves every entry that does not name it intact. A tool that re-serialises a `WAI2` manifest may change these bytes, and with them the content hashes of the exploded envelopes. - Rendition table: `u16 n_renditions | n × (u32 offset, u32 length)`, diff --git a/wai/staged-conformance/staged_verify.py b/wai/staged-conformance/staged_verify.py index e8f915ae1..9b3c5d04b 100644 --- a/wai/staged-conformance/staged_verify.py +++ b/wai/staged-conformance/staged_verify.py @@ -1033,7 +1033,8 @@ def manifests_check(b): ok = (isinstance(em, dict) and all(isinstance(em.get(k), str) for k in ("wai", "media", "intent")) and em["model_requirement"]["capability"] == m["capability"] and em["conditioning"]["kind"] == m["kind"] - and em["conditioning"].get("prior") == m.get("prior")) + and em["conditioning"].get("prior") == m.get("prior") + and (m["capability"] not in UNIT_KIND or em["model_requirement"].get("fallback") is None)) if ok: a = None if m.get("refines") is None else refines_json(parse_refines_ids(m["refines"]), lambda x: ids[x]) c = None if em.get("refines") is None else refines_json(parse_refines(em["refines"]), lambda x: x) @@ -1076,7 +1077,7 @@ def verify_wai2(d, fails): got = { "roles": role_check(entries), "manifests": manifests_check(b), - "exploded": None if any(e is None for e in ex) else [content_hash(e) for e in ex], + "exploded": [None if e is None else content_hash(e) for e in ex], "refinements_of_0": refinements_of_0(b), } if got != f["expect"]: diff --git a/wai/staged-conformance/wai2/index.json b/wai/staged-conformance/wai2/index.json index dd59fbb72..8a72a2db9 100644 --- a/wai/staged-conformance/wai2/index.json +++ b/wai/staged-conformance/wai2/index.json @@ -1,5 +1,5 @@ { - "about": "WAI2 envelopes with refinement components (SPEC §7.1): what check_component_roles reports, whether each envelope_manifest matches its entry, the content hash of each component's exploded envelope, and entry 0's refinements. Where `sources` is given, the components explode to those WAI1 envelopes byte for byte.", + "about": "WAI2 envelopes with refinement components (SPEC §7.1): what check_component_roles reports, whether each envelope_manifest matches its entry, the content hash of each component's exploded envelope (null for an entry that cannot be exploded; the others are exploded all the same), and entry 0's refinements. Where `sources` is given, the components explode to those WAI1 envelopes byte for byte.", "files": [ { "file": "pack_live_chain.wai", @@ -85,7 +85,11 @@ "expect": { "roles": "refinement-prev-missing", "manifests": null, - "exploded": null, + "exploded": [ + "548f6a1b589a1b74fac21eaa7226b8bdc6be0360b8d47886cac964b1f081a5d4", + "53d7a0e6db5d6b6a0f2788f60d86717d3619b3dd5c834fd65ee18456eab7c5f1", + null + ], "refinements_of_0": [ 1, 2 @@ -109,6 +113,127 @@ 2 ] } + }, + { + "file": "refinement_manifest_fallback.wai", + "sources": [], + "expect": { + "roles": null, + "manifests": [ + 1, + "component-manifest-mismatch" + ], + "exploded": [ + "20488045874a49bf9680c7c635b5db88dcdf1aefad0f48acaacd6f7c286e07df", + "9f29f42293547ca8a1c463acf427dd251e3b6324418f059113a2fb2cb5e504c8", + "7e347c1d544e77ee0b3d47f4798f29044be7f075fc0df364bad7bb94dac98731", + "b8b998dac5a0e74511eaeb9692414ede5447cb4b280ee6311c084a31084988ee" + ], + "refinements_of_0": [ + 1, + 2, + 3 + ] + } + }, + { + "file": "refinement_not_refinement_only.wai", + "sources": [], + "expect": { + "roles": "refinement-not-refinement-only", + "manifests": null, + "exploded": [ + "548f6a1b589a1b74fac21eaa7226b8bdc6be0360b8d47886cac964b1f081a5d4", + "e49911c2135e39829cc527ced20d9de8c78fe7629eb11b6c802cbe1f5afe64ec" + ], + "refinements_of_0": [ + 1 + ] + } + }, + { + "file": "refinement_identity_missing.wai", + "sources": [], + "expect": { + "roles": "refinement-identity-missing", + "manifests": null, + "exploded": [ + "548f6a1b589a1b74fac21eaa7226b8bdc6be0360b8d47886cac964b1f081a5d4", + "53d7a0e6db5d6b6a0f2788f60d86717d3619b3dd5c834fd65ee18456eab7c5f1" + ], + "refinements_of_0": [ + 1 + ] + } + }, + { + "file": "refinement_base_missing.wai", + "sources": [], + "expect": { + "roles": "refinement-base-missing", + "manifests": null, + "exploded": [ + "548f6a1b589a1b74fac21eaa7226b8bdc6be0360b8d47886cac964b1f081a5d4", + null + ], + "refinements_of_0": [] + } + }, + { + "file": "refinement_base_not_admitted.wai", + "sources": [], + "expect": { + "roles": "refinement-base-not-admitted", + "manifests": null, + "exploded": [ + "b8f86cdfdad10c074504661ae17e37567d57897b58d4731d2128025d33290308", + "78c02620a638b17fd4d04a8a83c4934a89966bbc0bdac4de1906158c63fefdb5" + ], + "refinements_of_0": [ + 1 + ] + } + }, + { + "file": "refines_on_non_refinement.wai", + "sources": [], + "expect": { + "roles": "refines-on-non-refinement", + "manifests": null, + "exploded": [ + null, + null + ], + "refinements_of_0": [ + 1 + ] + } + }, + { + "file": "refinement_only_as_component.wai", + "sources": [], + "expect": { + "roles": "refinement-only-as-component", + "manifests": null, + "exploded": [ + "548f6a1b589a1b74fac21eaa7226b8bdc6be0360b8d47886cac964b1f081a5d4", + "68b51fa0c19eea9aaac266bc686c06d2d2204220d5bd3c9d5c1a30a7fe4f7565" + ], + "refinements_of_0": [] + } + }, + { + "file": "refinement_refines_malformed.wai", + "sources": [], + "expect": { + "roles": "refines-malformed", + "manifests": null, + "exploded": [ + "548f6a1b589a1b74fac21eaa7226b8bdc6be0360b8d47886cac964b1f081a5d4", + null + ], + "refinements_of_0": [] + } } ] } diff --git a/wai/staged-conformance/wai2/refinement_base_missing.wai b/wai/staged-conformance/wai2/refinement_base_missing.wai new file mode 100644 index 0000000000000000000000000000000000000000..215ac78ccd5d3688bf29986bcf23a00d93122dc4 GIT binary patch literal 1033 zcmaJ=X>3eU6h2c$)smSg-|3zd{-SD#;yAYKcCfmCci;sDK zkO&HKD3YcMlu%JsT{7Wi>XkL8qKcBP5bCu%iBog%qTm!H#YA6H@@X z4+h8`f;JT(Iu8BG04I2W6%s&C7tV77teSzE0q}hvz_%l)#{rg{!uTw}(HPomsEvr_ zK7i*Xz>zm--(&m{`#zy|VBZ&jc$}d?peQ#`B^X{B-CB@P-YX%$U|>t=?v%ErTcn1{ z-9rZll?mlDN7zzaFVdToC6n6gY%Th;Ewj5HDm%O;=Ba3PP3iC_4LOiBy7h3|)(fYd zdml7Liq2PM%&d%_U3c=bkv{+FF)6!d;_@r?(~^UgX$zm9Y>xK3+~kU*_uQDAwn5pMo=*PRX21CF#+K}oM6_S=9eQ`g7EDwl z@6{r*kq@wJ2Xb=(IeG=K^xGev7GQtN>Bi6V(birnz7q?)t!>WAuiY8jpk$OUYv0t` zZS>W-IiW|}G9GU1e|^B4$End>CDTvD>#mixXKjv+?O)f_u=sTf{+v@&_uLxy19HYq Ai2wiq literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/wai2/refinement_base_not_admitted.wai b/wai/staged-conformance/wai2/refinement_base_not_admitted.wai new file mode 100644 index 0000000000000000000000000000000000000000..976228b1476395e77e1e805f9401e1ec6a09d8d9 GIT binary patch literal 1015 zcmZuueN0tl7=JDxASs(fvew7qKxTEhov-_GC_r!%agF8$fkp-Ix#vA_?0$8)!7g`q z5Sz}qsed`9GB;c)IfAQ=6uE`YEgi&i%sz;+^p7TJ%(=yYrMKq=immnR{Pujj&w1Y8 z^LuMcE5soSK!j@}23N=lHi5HpKB^f6vsOc+0n815i26fVFlo?hcu0t1(WHLO2pIu? z3s+bl;XEWrRKsh8!q|tyY=)0CQCra8guU+@ehtejiNQjZw6Gy9%|x_2FxPcWcR2{p zYr4ZuWR<8cD#=c#lS;fS3Ow-;p7KQJse`C?MN(ul)D};LNPU6%YyeK#Y zN%0Vw3aUy(T_UO|@;J*3cq#6*a;J|;$K+xJP^-ww?*B>OaAYKxtCRZp#qwiX| z5HXu5>K@^Alla)rqLtG=WalV48!@6o3>hm@_|#n0KYT)&k~Dpjv64BKY(Tl-4aebRnnd3Er{|nX z&z(Izw|`{FdHjd*Xz)^Bd2w&-!UyL^jj~<8oRw+^UVi)XwbwI3y(N{uo}Z3oj&AtE zeSdIU$Hg&a@!M}^@7+I{zkRXj+O4;q4^O$X``Vk&MoUsl)>|vDmo-`H`=43b+0c_Q zk&X7z+ITUqZtkht3iq=9Y_os#Pi9N5S_=BV z@Ex95%`I5yN8S5HRHG4K&mq)i1U0$?Q2l64r#G>GW@vnV7y7}kT5iMx_XekH8V3)@ zJCyS7JqsVrCg)z+Q5T*%Q2t}*rmvs8`%`}GNb4Kt62n*97izlVFK!y19NYbSR)a6_ KTK=(#Eq?`mHmgQ+qXT%DOCBU_?;; z71BXqlvhIzDI1Swh+(rwxCN2r-ZP`FWU?^7%4j~?^m9p8K3{N8Vv?36TJsRF>m zloE+aWV9MBqh@RZFA>a2Bwlb}F47t8#`iaEr!18GS;9)(g3Kgpz1|&a#zo{p0hRMG zqD*XbSf^(Az4Tnedc7e)tsFl9MJ>kJX<1qY#`GloAAAZ1gA1@mt&U;>K+Z$}B{nLf z0K#Gba%ZAf0r(bSyd2;%3$UF7m}tQJ5&?E5Axr=xrvQG`AT9x-X53sBRC{v+m}vF{5afPLQpftKQ`|pB^x*2-w4Co(+#3s2)0attsK; z(=M;Gt=6=r%GbQ7@urlz@zq5xyMF1mTkWeNT~*1OUpMynrrw-<)jZg+c3(@UG4SBt zz=y#zs&#>+_TDY?+|SL?wPi&Yy~z`jBh;HaQVSKib+f;1$U7Z*UpZBA{iQl@K6h@H zXY$D7ul12aTm9jlZp-2WQ(6|}49&<+Gmq*|H~Xc3C|z>gl<2?XcbGJuE3i<9y4UGY zjeLO2BdEstR$mUb-YFZ-I^bV8t+l&z>-#ADnO3S!++Xw)^iMWD literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/wai2/refinement_manifest_fallback.wai b/wai/staged-conformance/wai2/refinement_manifest_fallback.wai new file mode 100644 index 0000000000000000000000000000000000000000..5fdeac10d79382e4882777ee3df6f798261db572 GIT binary patch literal 3683 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 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/wai2/refinement_only_as_component.wai b/wai/staged-conformance/wai2/refinement_only_as_component.wai new file mode 100644 index 0000000000000000000000000000000000000000..7e903702a8f2e68cc6b54e5601e1232b4b1701fd GIT binary patch literal 959 zcmaJ=X-HI26h7lpW(#DNL<{vY+s5f_vpmZzgUpDc$<(mY`{v!3&vdkLQdEX4C?cqS zg;Ww4EheT@QKn8=2q{_=(q^{*2sM)k5|wm1PmHJ^9p1-T?m72;_q*Jbb&0&k08qjd z5{-#rI1|ShnH;KU1hWE7p}ClAPB(SBvEZV4*_wm6DHdJSsc3F3*O|}6q?IrZl1F4M zTXPp-A8uwE8#du+dQPt1h9mnlr-G}0)ik{#lYD*4{7k}|EtspSqS`EkWfj$8CZbGa z8x=&W)k+0cRdecp9A{Lhx%6|7 zamm)BKTJ4@SR_G_IZ7xm%Qlg)B6Wz0Rg!r@l?Zj1t;DKWSe~R z{zX{Rty;$ffYgZqLD(1+1`s?8AT0uYB!D*x;}rncS%6&vz(gDF7Xz>-7LfojdJbUZ z3gQ~T_M4d30bKQ=Z$`8umlpu`j{sM?(SODKJNErR3}N3efZwmPK%!`^2m$66dXHuX zluZiA$PDi(dKTTEbVO*YI6ZroTUuJSW}Yd`_94DQ+P-|S-qfSMJF+hLQt{<1pS`@- z7Cqz)iMSB5u=jHR(Z*ZWvoG5{c@32bYbv~T`)@R9@tfXO2`M$pcHVDU73!{t+w%TK zmv>szl-u^c+O-Gov`Irx9`%0dJ0H1zD7NL@*7-%9_VCK$%qmaZgt$q@Ev@l;4QbVL zhBu~{hdvIPX1M#tn7%-`m{l@m^!Y$dC~c@Y-qj&5IXv~wqSXGGDT($m{q>Xm);~}` zE<%!Ny2T|>cq{x~=?4nDRWKG473tRt-$fUaAV>fmoDl<&BcPADv z^SY4;3UMfsrV5l$QB_?s;brQPHK(GAlCBWyu{()VbMT_z6ePt%WGbjC5p{{EqR8Vc z)9nfliTZTE&B5wKe8t3-rwq)+TlwWb}(UV>yfst7tc8NK5PsZ zU#QBMRT(+A?$i|{J?q(VDW_)A@~bzdCk86h7QQ&u92sz>-&yzj+Swby_5Wy6|TDYD-@Ah_CbW_b1+tA7Huk)S5p=I&GzOdb;9QU=f~x?4av!Lck<6R`^|q3wq%#Yqy2{O(6=kLV4@m%uNIMw z0)S;ZkeiFh(QAOE-@AEQfb}hB8oy+rt-V}wH|l>^+nilcyED2$$*5S?zNxjx*lY80 vgO9gmJlZ($`k=Q@k|VoHXPk`H)t9$tZ;p;1Sl86B_)QZ2nA4K?+)nuk(}+u? literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/wai2/refines_on_non_refinement.wai b/wai/staged-conformance/wai2/refines_on_non_refinement.wai new file mode 100644 index 0000000000000000000000000000000000000000..1c7501bc4785ec73fde92313a7f8ccc2f63a031f GIT binary patch literal 1079 zcmb7DX-HI26h32CW(#B+(L%j!AE&p?@@%mTG9!v6Q^QK{n|EKn8fR>rlyMkZpopOQ z6;erHv{;xw&OU?;$E>cKT z!)^EjSclCl-G?oBG`GOZ%J9f~!=vHuA31|%R8quR7UdJsYQtF9HQiw&Jg@0CE0I;A zI;bSu?RF~hvMBJxMR>{+ou@XUS`|r=HHBD3hbqaMieEQ9-SAN47W~ou_a%isTHJG` zn*^wjOBO;Q^FL?sQP!?!h7!ZalhW~(0Vf{Eswnb|yVp&T;*UEEnDk8U|D0#Fne!9hT8i%$#X!l!GvqJ6ZaS1N2AdRm}6GO&<7y9FF-67#wGyt z9uAN*0(~MtcoMFs1DxUkR!IPT9oR1!V9jhq8o;*$0AG(GP68}HgK;gu@d)~>h-Tz+ z55V~X;Mi;Q?=b#=bsrI3Soawq8f7eyC|V;zg3%@6?fEff{bKU+hqeXoBz0tLlbR~_ zjvVe+rj*SYV@Ys4Pi;|_PwT9+wCPW`&Fg)n^ys>Xv%KAr)a8jAaVTzl`_Yc=4QK89 z?>C1E>nqddRD^3coW5kFE_!l8%Br5a^78eW@&1aGCC^T`h6i05aL)O*X6~l*O-k3T z8|`o29!#9yHT(M0rDFpRokJ^2^G}3Q`lR$TFKJ9&Ys#q_^?6}#dHl`TL8c3j&AH>G z!|Q?rzTN#)9Z&14ceb{uleZ2$KOwtgSXR2TTYom$Fa3M5A%9vd`Y-qn{eQ+LOjM!n zRU)d92e4unYSVxky#&bk+N0BAtZzHp{Am&Tnu|rZBi=VPt(kc>yCa*Fw6YbQTiSb# zzp@}Z@Nh@kgUy4l4SD@2F}$aE)~RUS)zZ$)t&xd?>sp$Yy-L7ec}C*Co0Gl+YLi== literal 0 HcmV?d00001 diff --git a/wai/wai-rs/src/bin/wai_staged_vectors.rs b/wai/wai-rs/src/bin/wai_staged_vectors.rs index acd454d11..ea6b3db5c 100644 --- a/wai/wai-rs/src/bin/wai_staged_vectors.rs +++ b/wai/wai-rs/src/bin/wai_staged_vectors.rs @@ -1198,7 +1198,7 @@ fn generate(ctx: &Ctx) -> BTreeMap> { out.insert( "wai2/index.json".into(), json_bytes(&json!({ - "about": "WAI2 envelopes with refinement components (SPEC §7.1): what check_component_roles reports, whether each envelope_manifest matches its entry, the content hash of each component's exploded envelope, and entry 0's refinements. Where `sources` is given, the components explode to those WAI1 envelopes byte for byte.", + "about": "WAI2 envelopes with refinement components (SPEC §7.1): what check_component_roles reports, whether each envelope_manifest matches its entry, the content hash of each component's exploded envelope (null for an entry that cannot be exploded; the others are exploded all the same), and entry 0's refinements. Where `sources` is given, the components explode to those WAI1 envelopes byte for byte.", "files": w2, })), ); @@ -1291,6 +1291,39 @@ fn wai2_files(ctx: &Ctx) -> Vec<(&'static str, Vec, Vec>)> { w.manifest.renditions[1].role = None; w.manifest.renditions[1].refines = None; let mismatch = w.to_bytes().expect("WAI2"); + // The packed chain with a fallback in a refinement's envelope_manifest. + let mut w = WaiMulti::from_bytes(&packed).expect("WAI2"); + w.manifest.renditions[1].envelope_manifest.as_mut().expect("packed")["model_requirement"]["fallback"] = json!(CAP_VIDEO_INT_MOTION); + let fallback = w.to_bytes().expect("WAI2"); + // One break of SPEC §7.1's refinement rules each, on the synthesised form. + let base_entry = || Rendition::from_meta(RenditionMeta::new(CAP_VIDEO_INT_MOTION, "int_motion").with_id("base"), payload_of(&a.envelope)).with_payload_digest(); + let l1_refines = json!({"base": "base", "prev": "base", "layer": 1}); + let wai2 = |rs: Vec| WaiMulti::new("video", "replicate", json!({"frames": 2, "dur": 1.0}), rs).to_bytes().expect("WAI2"); + let not_refinement_only = { + let mut m = RenditionMeta::new(CAP_VIDEO_INT_MOTION, "int_motion").with_id("l1").with_role("refinement"); + m.refines = Some(l1_refines.clone()); + wai2(vec![base_entry(), Rendition::from_meta(m, payload_of(&l1s[0])).with_payload_digest()]) + }; + let identity_missing = { + let mut m = RenditionMeta::new(CAP_VIDEO_INT_REFINE, "wir1").with_id("l1").with_role("refinement"); + m.refines = Some(l1_refines.clone()); + wai2(vec![base_entry(), Rendition::from_meta(m, payload_of(&l1s[0]))]) + }; + let base_missing = wai2(vec![base_entry(), refinement("l1", json!({"base": "nobody", "prev": "nobody", "layer": 1}), payload_of(&l1s[0]))]); + let base_not_admitted = wai2(vec![ + Rendition::from_meta(RenditionMeta::new(CAP_IMAGE_PNG, "png").with_id("base"), payload_of(&a.envelope)).with_payload_digest(), + refinement("l1", l1_refines.clone(), payload_of(&l1s[0])), + ]); + let refines_on_base = { + let mut m = RenditionMeta::new(CAP_VIDEO_INT_MOTION, "int_motion").with_id("base"); + m.refines = Some(l1_refines.clone()); + wai2(vec![Rendition::from_meta(m, payload_of(&a.envelope)).with_payload_digest(), refinement("l1", l1_refines.clone(), payload_of(&l1s[0]))]) + }; + let as_component = wai2(vec![ + base_entry(), + Rendition::from_meta(RenditionMeta::new(CAP_VIDEO_INT_REFINE, "wir1").with_id("l1"), payload_of(&l1s[0])).with_payload_digest(), + ]); + let malformed = wai2(vec![base_entry(), refinement("l1", json!({"base": "base", "prev": "base", "layer": 0}), payload_of(&l1s[0]))]); vec![ ("pack_live_chain", packed, sources), ("ok_explode", synth("l1", false, false), vec![]), @@ -1298,6 +1331,14 @@ fn wai2_files(ctx: &Ctx) -> Vec<(&'static str, Vec, Vec>)> { ("component_manifest_mismatch", mismatch, vec![]), ("prev_missing", synth("zz", false, false), vec![]), ("refinement_before_selectable", synth("l1", true, false), vec![]), + ("refinement_manifest_fallback", fallback, vec![]), + ("refinement_not_refinement_only", not_refinement_only, vec![]), + ("refinement_identity_missing", identity_missing, vec![]), + ("refinement_base_missing", base_missing, vec![]), + ("refinement_base_not_admitted", base_not_admitted, vec![]), + ("refines_on_non_refinement", refines_on_base, vec![]), + ("refinement_only_as_component", as_component, vec![]), + ("refinement_refines_malformed", malformed, vec![]), ] } @@ -1308,7 +1349,7 @@ fn wai2_expect(bytes: &[u8]) -> Value { json!({ "roles": w.check_component_roles().err().map(|e| e.code()), "manifests": WaiMulti::check_envelope_manifests(bytes).err().map(|(i, c)| json!([i, c])), - "exploded": WaiMulti::explode_all(bytes).ok().map(|v| v.iter().map(|e| hex(&chash(e))).collect::>()), + "exploded": WaiMulti::explode_each(bytes).ok().map(|v| v.iter().map(|e| e.as_ref().ok().map(|e| hex(&chash(e)))).collect::>()), "refinements_of_0": w.refinements_of(0).iter().map(|(i, ..)| *i).collect::>(), }) } diff --git a/wai/wai-rs/src/container.rs b/wai/wai-rs/src/container.rs index c271f4539..4be7bdc96 100644 --- a/wai/wai-rs/src/container.rs +++ b/wai/wai-rs/src/container.rs @@ -1141,6 +1141,8 @@ impl Wai { need(4, 4, "manifest length")?; let ml = u32::from_le_bytes(bytes[4..8].try_into().unwrap()) as usize; need(8, ml, "manifest body")?; + check_unique_members(&bytes[8..8 + ml]) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; let m: Manifest = serde_json::from_slice(&bytes[8..8 + ml]) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; let p_off = 8 + ml; @@ -1367,6 +1369,64 @@ impl Wai { } } +/// Whether the JSON text `text` names a member twice in one object, at any depth +/// (SPEC §2: member names are unique within an object). `serde_json` keeps the last of +/// two equal names in a `Value` and refuses them only in a struct's own fields, so a +/// manifest is read once more by this check, which refuses any repeat. Names compare +/// after escapes are read, so `"a"` and `"\u0061"` are one name. The text's own JSON +/// errors are reported as they are. +pub fn check_unique_members(text: &[u8]) -> Result<(), serde_json::Error> { + use serde::de::{DeserializeSeed, Deserializer, Error as _, MapAccess, SeqAccess, Visitor}; + struct Unique; + impl<'de> DeserializeSeed<'de> for Unique { + type Value = (); + fn deserialize>(self, d: D) -> Result<(), D::Error> { + d.deserialize_any(Unique) + } + } + impl<'de> Visitor<'de> for Unique { + type Value = (); + fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("a JSON value") + } + fn visit_bool(self, _: bool) -> Result<(), E> { + Ok(()) + } + fn visit_i64(self, _: i64) -> Result<(), E> { + Ok(()) + } + fn visit_u64(self, _: u64) -> Result<(), E> { + Ok(()) + } + fn visit_f64(self, _: f64) -> Result<(), E> { + Ok(()) + } + fn visit_str(self, _: &str) -> Result<(), E> { + Ok(()) + } + fn visit_unit(self) -> Result<(), E> { + Ok(()) + } + fn visit_seq>(self, mut seq: A) -> Result<(), A::Error> { + while seq.next_element_seed(Unique)?.is_some() {} + Ok(()) + } + fn visit_map>(self, mut map: A) -> Result<(), A::Error> { + let mut seen = std::collections::HashSet::new(); + while let Some(k) = map.next_key::()? { + if !seen.insert(k) { + return Err(A::Error::custom("an object names a member twice")); + } + map.next_value_seed(Unique)?; + } + Ok(()) + } + } + let mut de = serde_json::Deserializer::from_slice(text); + Unique.deserialize(&mut de)?; + de.end() +} + /// The manifest bytes of a `WAI1` or `WAI2` envelope, exactly as they were /// written, bounds-checked as the parsers check them. A tool that hands a manifest /// on (the C FFI's unpack calls) returns these, so nothing the Rust types do not @@ -2170,6 +2230,8 @@ impl WaiMulti { need(4, 4, "manifest length")?; let ml = u32::from_le_bytes(bytes[4..8].try_into().unwrap()) as usize; need(8, ml, "manifest body")?; + check_unique_members(&bytes[8..8 + ml]) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; let manifest: ManifestV2 = serde_json::from_slice(&bytes[8..8 + ml]) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; diff --git a/wai/wai-rs/src/container_explode.rs b/wai/wai-rs/src/container_explode.rs index 59dcebc27..6b3f283bf 100644 --- a/wai/wai-rs/src/container_explode.rs +++ b/wai/wai-rs/src/container_explode.rs @@ -99,7 +99,9 @@ impl WaiMulti { /// [`Self::explode_all`], entry by entry: an entry whose `refines` cannot be /// translated (it is malformed, or names an entry that is not before it, or one /// that could not be exploded) has its own error, and the others their envelopes. - fn explode_each(envelope: &[u8]) -> std::io::Result>>> { + /// Entry `i`'s envelope is defined by entry `i` and the entries it names, so one + /// entry that cannot be exploded leaves every entry that does not name it intact. + pub fn explode_each(envelope: &[u8]) -> std::io::Result>>> { let w = WaiMulti::from_bytes(envelope)?; let mtext = manifest_bytes(envelope)?; let raw: RawV2 = serde_json::from_slice(mtext).map_err(|e| bad(format!("WAI2 manifest: {e}")))?; @@ -157,9 +159,9 @@ impl WaiMulti { } /// Component `index` of the `WAI2` envelope `envelope` as its exploded `WAI1` - /// envelope ([`Self::explode_all`]). + /// envelope ([`Self::explode_each`]): its own result, whatever the other entries'. pub fn explode(envelope: &[u8], index: usize) -> std::io::Result> { - Self::explode_all(envelope)?.into_iter().nth(index).ok_or_else(|| bad(format!("no component {index}"))) + Self::explode_each(envelope)?.into_iter().nth(index).ok_or_else(|| bad(format!("no component {index}")))? } /// Pack `WAI1` envelopes into one `WAI2`, in order: each entry carries the @@ -168,10 +170,26 @@ impl WaiMulti { /// refinement-only takes `role: "refinement"` and its `refines` with each content /// hash replaced by the id of the entry whose envelope has it. `wai`, `media`, /// `intent` and `target` come from the first envelope. So every envelope explodes - /// back to its own bytes ([`Self::explode_all`]). An encrypted envelope, or a - /// refinement whose base or producers are not among the envelopes before it, is - /// refused. + /// back to its own bytes ([`Self::explode_all`]). Refused: an encrypted envelope; + /// a refinement whose base or producers are not among the envelopes before it; an + /// envelope with bytes around its manifest's JSON value or after its payload, which + /// no explode writes back (SPEC §7.1); and an output that does not parse, explode + /// back to every source byte for byte, and pass + /// [`Self::check_envelope_manifests`], which pack checks before it returns (a + /// manifest nested near the JSON depth limit as a `WAI1` passes it as a member of + /// a `WAI2` manifest). pub fn pack(w1s: &[&[u8]]) -> std::io::Result> { + let out = Self::pack_unchecked(w1s)?; + WaiMulti::from_bytes(&out).map_err(|e| bad(format!("the packed WAI2 does not parse: {e}")))?; + let back = Self::explode_all(&out).map_err(|e| bad(format!("the packed WAI2 does not explode: {e}")))?; + if back.len() != w1s.len() || back.iter().zip(w1s).any(|(a, b)| a.as_slice() != *b) { + return Err(bad("the packed WAI2 does not explode back to its sources")); + } + Self::check_envelope_manifests(&out).map_err(|(i, c)| bad(format!("the packed WAI2's entry {i}: {c}")))?; + Ok(out) + } + + fn pack_unchecked(w1s: &[&[u8]]) -> std::io::Result> { if w1s.is_empty() { return Err(bad("nothing to pack")); } @@ -185,6 +203,15 @@ impl WaiMulti { return Err(bad(format!("envelope {i} is encrypted"))); } let mtext = std::str::from_utf8(manifest_bytes(b)?).map_err(|e| bad(e.to_string()))?; + // The manifest is copied as the value of `envelope_manifest`, where bytes + // around a JSON value are not part of it: such a manifest cannot come back. + let value: &RawValue = serde_json::from_str(mtext).map_err(|e| bad(e.to_string()))?; + if value.get().len() != mtext.len() { + return Err(bad(format!("envelope {i}'s manifest has bytes around its JSON value"))); + } + if b.len() != 12 + mtext.len() + w.payload.len() { + return Err(bad(format!("envelope {i} has bytes after its payload"))); + } let id = format!("c{i}"); debug_assert!(is_component_id(&id)); let mut e = serde_json::Map::new(); @@ -238,9 +265,10 @@ impl WaiMulti { /// Check every entry's `envelope_manifest` against the entry (SPEC §7.1): it is a /// `WAI1` manifest whose `model_requirement.capability`, `conditioning.kind` and - /// `conditioning.prior` are the entry's, and whose `refines` is the entry's with each + /// `conditioning.prior` are the entry's, whose `refines` is the entry's with each /// id replaced by the content hash of that entry's exploded envelope (or both are - /// absent). The first entry that fails, with `component-manifest-mismatch`. + /// absent), and, for a refinement-only capability, whose `fallback` is `null`. The + /// first entry that fails, with `component-manifest-mismatch`. pub fn check_envelope_manifests(envelope: &[u8]) -> Result<(), (usize, &'static str)> { const CODE: &str = "component-manifest-mismatch"; let w = WaiMulti::from_bytes(envelope).map_err(|_| (0, CODE))?; @@ -254,6 +282,11 @@ impl WaiMulti { if mm.model_requirement.capability != meta.capability || mm.conditioning.kind != meta.kind || !prior_eq { return Err((i, CODE)); } + // A refinement-only capability's manifest names no fallback + // (staged-delivery §3). + if crate::codecs::is_refinement_only(&meta.capability) && mm.model_requirement.fallback.is_some() { + return Err((i, CODE)); + } let entry_refines = match &meta.refines { None => None, Some(v) => { diff --git a/wai/wai-rs/src/staged/tests.rs b/wai/wai-rs/src/staged/tests.rs index 7b245466f..4ff2ab51b 100644 --- a/wai/wai-rs/src/staged/tests.rs +++ b/wai/wai-rs/src/staged/tests.rs @@ -1053,11 +1053,137 @@ fn explode_pack_roundtrip() { assert_eq!(c.offer(&all[1], 0, ReceiptEvidence::None).unwrap()[0].applied, vec![0, 1, 2, 3]); } +/// Entry `i`'s exploded envelope is its own result: a refinement whose `prev` names +/// no entry fails alone, and the base and the other refinement explode as they would +/// without it. +#[test] +fn explode_each_entry_alone() { + let (base, _, l1, _) = chain3(); + let payload = |e: &[u8]| Wai::from_bytes(e).unwrap().payload; + let refinement = |id: &str, refines: serde_json::Value, p: Vec| { + let mut m = RenditionMeta::new(CAP_VIDEO_INT_REFINE, "wir1").with_id(id).with_role("refinement"); + m.refines = Some(refines); + Rendition::from_meta(m, p).with_payload_digest() + }; + let wm = WaiMulti::new("video", "replicate", json!(null), vec![ + Rendition::from_meta(RenditionMeta::new(CAP_VIDEO_INT_MOTION, "int_motion").with_id("base"), payload(&base)).with_payload_digest(), + refinement("l2", json!({"base": "base", "prev": "zz", "layer": 2}), payload(&l1)), + refinement("l1", json!({"base": "base", "prev": "base", "layer": 1}), payload(&l1)), + ]); + let bytes = wm.to_bytes().unwrap(); + assert!(WaiMulti::explode_all(&bytes).is_err()); + let each = WaiMulti::explode_each(&bytes).unwrap(); + assert!(each[0].is_ok() && each[1].is_err() && each[2].is_ok()); + assert_eq!(WaiMulti::explode(&bytes, 0).unwrap(), *each[0].as_ref().unwrap()); + assert!(WaiMulti::explode(&bytes, 1).is_err()); + assert_eq!(WaiMulti::explode(&bytes, 2).unwrap(), *each[2].as_ref().unwrap()); + let r = Wai::from_bytes(each[2].as_ref().unwrap()).unwrap().refines().unwrap().unwrap(); + assert_eq!(r.base, hexs(&h(each[0].as_ref().unwrap()))); +} + +/// Pack refuses what it cannot give back: a manifest with bytes around its JSON value, +/// bytes after a payload, and a manifest nested so deep that, as a member of the WAI2 +/// manifest, it passes the reader's depth limit; what it returns explodes back to its +/// sources and passes the manifest check. +#[test] +fn pack_refuses_what_it_cannot_give_back() { + let (base, _, l1, l2) = chain3(); + let parts = |e: &[u8]| { + let ml = u32::from_le_bytes(e[4..8].try_into().unwrap()) as usize; + (e[8..8 + ml].to_vec(), e[8 + ml..].to_vec()) + }; + let rebuild = |m: &[u8], rest: &[u8]| { + let mut out = b"WAI1".to_vec(); + out.extend_from_slice(&(m.len() as u32).to_le_bytes()); + out.extend_from_slice(m); + out.extend_from_slice(rest); + out + }; + let (m, rest) = parts(&base); + assert!(WaiMulti::pack(&[&base, &l1, &l2]).is_ok()); + for (what, env) in [ + ("a trailing newline", rebuild(&[&m[..], b"\n"].concat(), &rest)), + ("a leading space", rebuild(&[b" ", &m[..]].concat(), &rest)), + ("bytes after the payload", [&base[..], b"xyz"].concat()), + ] { + assert!(Wai::from_bytes(&env).is_ok(), "{what}: a WAI1 reader takes it"); + let e = WaiMulti::pack(&[&env]).unwrap_err().to_string(); + assert!(e.contains("bytes around") || e.contains("bytes after"), "{what}: {e}"); + } + // Depth: the WAI1 reader takes a manifest of depth 127; inside a WAI2 entry's + // envelope_manifest it sits three levels deeper. + for (depth, packs) in [(120usize, true), (124, false), (126, false)] { + let text = String::from_utf8(m.clone()).unwrap(); + let deep = format!("{},\"zz_deep\":{}{}}}", &text[..text.len() - 1], "[".repeat(depth), "]".repeat(depth)); + let env = rebuild(deep.as_bytes(), &rest); + assert!(Wai::from_bytes(&env).is_ok(), "depth {depth} is a WAI1"); + let got = WaiMulti::pack(&[&env]); + assert_eq!(got.is_ok(), packs, "depth {depth}: {:?}", got.as_ref().err()); + if let Ok(p) = got { + assert_eq!(WaiMulti::explode_all(&p).unwrap(), vec![env]); + } + } +} + +/// A refinement's envelope_manifest names no fallback (SPEC §7.1, staged-delivery §3). +#[test] +fn refinement_envelope_manifest_has_no_fallback() { + let (base, _, l1, l2) = chain3(); + let packed = WaiMulti::pack(&[&base, &l1, &l2]).unwrap(); + let mut w = WaiMulti::from_bytes(&packed).unwrap(); + w.manifest.renditions[1].envelope_manifest.as_mut().unwrap()["model_requirement"]["fallback"] = json!(CAP_VIDEO_INT_MOTION); + assert_eq!(WaiMulti::check_envelope_manifests(&w.to_bytes().unwrap()), Err((1, "component-manifest-mismatch"))); + let mut w = WaiMulti::from_bytes(&WaiMulti::pack(&[&base]).unwrap()).unwrap(); + w.manifest.renditions[0].envelope_manifest.as_mut().unwrap()["model_requirement"]["fallback"] = json!(crate::codecs::CAP_IMAGE_PNG); + assert_eq!(WaiMulti::check_envelope_manifests(&w.to_bytes().unwrap()), Ok(()), "a base may name one"); +} + +/// A manifest that names a member twice in one object, at any depth, is malformed +/// (SPEC §2), in a WAI1 and in a WAI2. +#[test] +fn repeated_member_names_are_malformed() { + let (base, _, l1, l2) = chain3(); + let (ml, text) = { + let ml = u32::from_le_bytes(base[4..8].try_into().unwrap()) as usize; + (ml, String::from_utf8(base[8..8 + ml].to_vec()).unwrap()) + }; + let with = |t: &str| { + let mut out = b"WAI1".to_vec(); + out.extend_from_slice(&(t.len() as u32).to_le_bytes()); + out.extend_from_slice(t.as_bytes()); + out.extend_from_slice(&base[8 + ml..]); + out + }; + assert!(Wai::from_bytes(&with(&text)).is_ok()); + let inner = text.replacen("\"capability\":", "\"q\":1,\"q\":2,\"capability\":", 1); + assert_ne!(inner, text); + let deep = format!("{},\"zz\":[{{\"a\":1,\"b\":{{\"c\":1,\"\\u0063\":2}}}}]}}", &text[..text.len() - 1]); + let top = format!("{},\"zz\":1,\"zz\":2}}", &text[..text.len() - 1]); + for (what, t) in [("in model_requirement", inner), ("nested, one name escaped", deep), ("an undefined member", top)] { + assert!(Wai::from_bytes(&with(&t)).is_err(), "{what}"); + } + let packed = WaiMulti::pack(&[&base, &l1, &l2]).unwrap(); + let pml = u32::from_le_bytes(packed[4..8].try_into().unwrap()) as usize; + let ptext = String::from_utf8(packed[8..8 + pml].to_vec()).unwrap(); + let dup = ptext.replacen("\"kind\":", "\"zz\":1,\"zz\":1,\"kind\":", 1); + let mut p2 = b"WAI2".to_vec(); + p2.extend_from_slice(&(dup.len() as u32).to_le_bytes()); + p2.extend_from_slice(dup.as_bytes()); + p2.extend_from_slice(&packed[8 + pml..]); + assert!(WaiMulti::from_bytes(&p2).is_err()); +} + /// The selection of the revision before refinement components: the first entry that -/// is not a companion and whose capability the sink advertises (that revision did not -/// register the refinement strings, so they were dispatchable). -fn primary_v1_1(w: &WaiMulti, advertises: impl Fn(&str) -> bool) -> Option { - w.manifest.renditions.iter().position(|m| m.role.as_deref() != Some("companion") && advertises(&m.capability)) +/// is not a companion, whose capability the sink advertises (that revision did not +/// register the refinement strings, so they were dispatchable), whose payload matches +/// its `sha256` when it states one, and whose pin, when it has one, the sink accepts. +fn primary_v1_1(w: &WaiMulti, advertises: impl Fn(&str) -> bool, pin_ok: impl Fn(&crate::container::Prior) -> bool) -> Option { + w.manifest.renditions.iter().enumerate().position(|(i, m)| { + m.role.as_deref() != Some("companion") + && advertises(&m.capability) + && m.sha256.as_deref().is_none_or(|d| d == crate::pin::sha256_hex(&w.renditions[i])) + && (!crate::container::pin_applies_to(&m.capability) || m.prior.as_ref().is_none_or(&pin_ok)) + }) } /// Test 20: an earlier sink, picking by its own selection, picks the base of a @@ -1069,14 +1195,31 @@ fn older_primary_returns_base() { let (base, _, l1, l2) = chain3(); let packed = WaiMulti::pack(&[&base, &l1, &l2]).unwrap(); let w = WaiMulti::from_bytes(&packed).unwrap(); - assert_eq!(primary_v1_1(&w, |c| c != CAP_VIDEO_INT_REFINE), Some(0)); - assert_eq!(primary_v1_1(&w, |_| true), Some(0)); + let knows = |c: &str| c != CAP_VIDEO_INT_REFINE; + assert_eq!(primary_v1_1(&w, knows, |_| true), Some(0)); + assert_eq!(primary_v1_1(&w, |_| true, |_| true), Some(0)); assert_eq!(w.primary(crate::container::no_parameter_sets()).map(|p| p.index).ok(), Some(0)); + // The base skipped for its digest, or for its pin: an earlier sink that advertises + // what it knows picks nothing, as the current selection does; one that advertised + // every string would reach a refinement, find it undecodable, and leave it inert. + let mut bad_digest = WaiMulti::from_bytes(&packed).unwrap(); + bad_digest.renditions[0][0] ^= 1; + assert_eq!(primary_v1_1(&bad_digest, knows, |_| true), None); + assert!(bad_digest.primary(crate::container::no_parameter_sets()).is_err()); + assert_eq!(primary_v1_1(&bad_digest, |_| true, |_| true), Some(1)); + // A pin applies to a base decoded against a parameter set. + let mut pinned = WaiMulti::from_bytes(&packed).unwrap(); + pinned.manifest.renditions[0].capability = crate::codecs::CAP_VIDEO_INT_HYPER.into(); + pinned.manifest.renditions[0].prior = Some(crate::container::Prior::new("p", "a".repeat(64), "int-rans/p16+int-hyper/i64")); + assert_eq!(primary_v1_1(&pinned, knows, |_| true), Some(0)); + assert_eq!(primary_v1_1(&pinned, knows, |_| false), None, "a refused pin skips the base"); + let refused = pinned.pick(|c| knows(c), |_, _| Err::<(), _>(crate::container::PinRefusal::Malformed { why: "refused".into() })).map(|p| p.index); + assert!(refused.is_err(), "the current selection skips it too, and no refinement is selectable"); let mut swapped = WaiMulti::from_bytes(&packed).unwrap(); swapped.manifest.renditions.swap(0, 1); swapped.renditions.swap(0, 1); assert!(matches!(swapped.check_component_roles(), Err(RoleError::RefinementBaseMissing { .. }) | Err(RoleError::RefinementBeforeSelectable { .. }))); - assert_eq!(primary_v1_1(&swapped, |_| true), Some(0), "the earlier rule picks the misplaced refinement"); + assert_eq!(primary_v1_1(&swapped, |_| true, |_| true), Some(0), "the earlier rule picks the misplaced refinement"); assert_eq!(swapped.manifest.renditions[0].capability, CAP_VIDEO_INT_REFINE); } -- 2.54.0 (Apple Git-157) From adc05fdb888c02054c8285f6cc494af497a5e866 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 4 Oct 2026 20:17:56 -0400 Subject: [PATCH 17/36] wai: the staged corpus covers each rule it was missing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New cases, written by wai_staged_vectors gen and replayed by its verify: - §3 and step 1: no refines; a fallback; a layer 1 naming another producer; prev as one run; adjacent runs with one producer; runs not summing to the span; -0, 1.0, 1e0 and -0.0 in integer members; span null as absent. - Step 2: a span past the base; runs past the base's units. - Step 6: a unit count not the span's (field_range); bad_magic, version, reserved_nonzero, an RGB8 element of 2 bytes, too_large, trailing; the symbol and working-byte cost limits; chain-over-limit; a retained held layer counted in the budget until it expires. - Steps 7 and 8: a freeze refusing a held layer; a conflict inside a release batch; layer-gap on arrival at the bound; a held layer turned prev-mismatch. - Steps 9 and 10: up2 and rate geometry; rate downsampling; a held layer late on release; a clip split into two bases on one live clock. - A base decoded again; a layer refused on release with one report; receipt evidence upgraded while held, and its effect on a deadline. - wai2/index.json lists pack attempts: bytes after a manifest, before it, after a payload, and a manifest too deep inside a WAI2 are refused. The CHANGELOG states what the corpus covers in place of "one case per refusal and order rule". Co-Authored-By: Claude Opus 5.5 --- wai/CHANGELOG.md | 6 +- .../cases/base_readded/L1a.wai | Bin 0 -> 595 bytes .../cases/base_readded/L1b.wai | Bin 0 -> 587 bytes .../cases/base_readded/base.wai | Bin 0 -> 551 bytes .../cases/base_readded/case.json | 184 +++++++++++ .../cases/base_readded/stage0.bin | 1 + .../cases/batch_conflict_release/L1.wai | Bin 0 -> 587 bytes .../cases/batch_conflict_release/L2a.wai | Bin 0 -> 943 bytes .../cases/batch_conflict_release/L2b.wai | Bin 0 -> 935 bytes .../cases/batch_conflict_release/base.wai | Bin 0 -> 559 bytes .../cases/batch_conflict_release/case.json | 207 ++++++++++++ .../cases/batch_conflict_release/stage0.bin | Bin 0 -> 384 bytes .../cases/chain_budget_retained/L1.wai | Bin 0 -> 587 bytes .../cases/chain_budget_retained/L2.wai | Bin 0 -> 795 bytes .../cases/chain_budget_retained/base.wai | Bin 0 -> 551 bytes .../cases/chain_budget_retained/case.json | 187 +++++++++++ .../cases/chain_budget_retained/stage0.bin | 1 + .../cases/chain_over_limit/L1.wai | Bin 0 -> 583 bytes .../cases/chain_over_limit/base.wai | Bin 0 -> 551 bytes .../cases/chain_over_limit/case.json | 118 +++++++ .../cases/chain_over_limit/stage0.bin | 1 + .../cases/cost_over_limit_symbols/L1.wai | Bin 0 -> 595 bytes .../cases/cost_over_limit_symbols/base.wai | Bin 0 -> 551 bytes .../cases/cost_over_limit_symbols/case.json | 122 +++++++ .../cases/cost_over_limit_symbols/stage0.bin | 1 + .../cases/cost_over_limit_working/L1.wai | Bin 0 -> 587 bytes .../cases/cost_over_limit_working/base.wai | Bin 0 -> 551 bytes .../cases/cost_over_limit_working/case.json | 122 +++++++ .../cases/cost_over_limit_working/stage0.bin | 1 + .../cases/deadline_held_layer/L1.wai | Bin 0 -> 579 bytes .../cases/deadline_held_layer/L2.wai | Bin 0 -> 795 bytes .../cases/deadline_held_layer/base.wai | Bin 0 -> 555 bytes .../cases/deadline_held_layer/case.json | 170 ++++++++++ .../cases/deadline_held_layer/stage0.bin | 1 + .../cases/evidence_upgrade/L1.wai | Bin 0 -> 591 bytes .../cases/evidence_upgrade/base.wai | Bin 0 -> 547 bytes .../cases/evidence_upgrade/case.json | 148 +++++++++ .../cases/evidence_upgrade/stage0.bin | 1 + .../cases/evidence_upgrade_deadline/L1.wai | Bin 0 -> 591 bytes .../cases/evidence_upgrade_deadline/base.wai | Bin 0 -> 559 bytes .../cases/evidence_upgrade_deadline/case.json | 159 ++++++++++ .../evidence_upgrade_deadline/stage0.bin | 1 + .../cases/frozen_refuses_held/L1a.wai | Bin 0 -> 627 bytes .../cases/frozen_refuses_held/L1b.wai | Bin 0 -> 631 bytes .../cases/frozen_refuses_held/L3.wai | Bin 0 -> 791 bytes .../cases/frozen_refuses_held/base.wai | Bin 0 -> 555 bytes .../cases/frozen_refuses_held/case.json | 195 ++++++++++++ .../cases/frozen_refuses_held/stage0.bin | 1 + .../cases/held_then_prev_mismatch/L1b.wai | Bin 0 -> 591 bytes .../cases/held_then_prev_mismatch/L2.wai | Bin 0 -> 795 bytes .../cases/held_then_prev_mismatch/base.wai | Bin 0 -> 551 bytes .../cases/held_then_prev_mismatch/case.json | 164 ++++++++++ .../cases/held_then_prev_mismatch/stage0.bin | 1 + .../cases/integer_spellings/L1_count_2.0.wai | Bin 0 -> 621 bytes .../integer_spellings/L1_from_minus0.wai | Bin 0 -> 620 bytes .../cases/integer_spellings/L1_layer_1.0.wai | Bin 0 -> 593 bytes .../cases/integer_spellings/L1_layer_1e0.wai | Bin 0 -> 593 bytes .../integer_spellings/L1_layer_minus0.wai | Bin 0 -> 592 bytes .../cases/integer_spellings/L1_span_null.wai | Bin 0 -> 603 bytes .../cases/integer_spellings/base.wai | Bin 0 -> 551 bytes .../cases/integer_spellings/case.json | 231 ++++++++++++++ .../cases/integer_spellings/stage0.bin | 1 + .../cases/layer_gap_on_arrival/L2a.wai | Bin 0 -> 931 bytes .../cases/layer_gap_on_arrival/L2b.wai | Bin 0 -> 935 bytes .../cases/layer_gap_on_arrival/base.wai | Bin 0 -> 555 bytes .../cases/layer_gap_on_arrival/case.json | 152 +++++++++ .../cases/layer_gap_on_arrival/stage0.bin | 1 + .../cases/live_multi_base/L1_second.wai | Bin 0 -> 535 bytes .../cases/live_multi_base/base_0.wai | Bin 0 -> 551 bytes .../cases/live_multi_base/base_1.wai | Bin 0 -> 523 bytes .../cases/live_multi_base/case.json | 197 ++++++++++++ .../cases/live_multi_base/stage0_0.bin | 1 + .../cases/live_multi_base/stage0_1.bin | 1 + .../cases/rate_downsampling/L1.wai | Bin 0 -> 16931 bytes .../cases/rate_downsampling/base.wai | Bin 0 -> 1183 bytes .../cases/rate_downsampling/case.json | 99 ++++++ .../cases/rate_downsampling/stage0.bin | Bin 0 -> 960 bytes .../cases/rate_geometry_mismatch/L1.wai | Bin 0 -> 12639 bytes .../cases/rate_geometry_mismatch/base.wai | Bin 0 -> 1825 bytes .../cases/rate_geometry_mismatch/case.json | 102 ++++++ .../cases/rate_geometry_mismatch/stage0.bin | Bin 0 -> 1600 bytes .../refines_against_base/L1_span_past.wai | Bin 0 -> 619 bytes .../refines_against_base/L2_runs_past.wai | Bin 0 -> 706 bytes .../cases/refines_against_base/base.wai | Bin 0 -> 555 bytes .../cases/refines_against_base/case.json | 140 ++++++++ .../cases/refines_against_base/stage0.bin | 1 + .../cases/refines_rules/L1_one_run.wai | Bin 0 -> 612 bytes .../cases/refines_rules/L1_other_producer.wai | Bin 0 -> 587 bytes .../cases/refines_rules/L2_adjacent_runs.wai | Bin 0 -> 702 bytes .../cases/refines_rules/L2_runs_not_span.wai | Bin 0 -> 730 bytes .../cases/refines_rules/base.wai | Bin 0 -> 555 bytes .../cases/refines_rules/case.json | 184 +++++++++++ .../cases/refines_rules/stage0.bin | 1 + .../cases/refused_on_release/L1_cut.wai | Bin 0 -> 527 bytes .../cases/refused_on_release/L1_f1.wai | Bin 0 -> 535 bytes .../cases/refused_on_release/base.wai | Bin 0 -> 551 bytes .../cases/refused_on_release/case.json | 161 ++++++++++ .../cases/refused_on_release/stage0.bin | 1 + .../cases/step1_refusals/L1_fallback.wai | Bin 0 -> 625 bytes .../cases/step1_refusals/L1_no_refines.wai | Bin 0 -> 421 bytes .../cases/step1_refusals/base.wai | Bin 0 -> 555 bytes .../cases/step1_refusals/case.json | 140 ++++++++ .../cases/step1_refusals/stage0.bin | 1 + .../cases/up2_geometry_mismatch/L1.wai | Bin 0 -> 807 bytes .../cases/up2_geometry_mismatch/base.wai | Bin 0 -> 555 bytes .../cases/up2_geometry_mismatch/case.json | 127 ++++++++ .../cases/up2_geometry_mismatch/stage0.bin | 1 + .../cases/wir1_faults/L1_bad_magic.wai | Bin 0 -> 587 bytes .../cases/wir1_faults/L1_elem.wai | Bin 0 -> 587 bytes .../cases/wir1_faults/L1_reserved.wai | Bin 0 -> 587 bytes .../cases/wir1_faults/L1_too_large.wai | Bin 0 -> 587 bytes .../cases/wir1_faults/L1_trailing.wai | Bin 0 -> 588 bytes .../cases/wir1_faults/L1_unit_count.wai | Bin 0 -> 615 bytes .../cases/wir1_faults/L1_version.wai | Bin 0 -> 587 bytes .../cases/wir1_faults/base.wai | Bin 0 -> 551 bytes .../cases/wir1_faults/case.json | 250 +++++++++++++++ .../cases/wir1_faults/stage0.bin | 1 + wai/staged-conformance/index.json | 155 +++++++++ wai/staged-conformance/wai2/index.json | 39 ++- .../wai2/pack_after_payload.src0.wai | Bin 0 -> 558 bytes .../wai2/pack_deep_120.src0.wai | Bin 0 -> 806 bytes .../wai2/pack_deep_124.src0.wai | Bin 0 -> 814 bytes .../wai2/pack_leading_space.src0.wai | Bin 0 -> 556 bytes .../wai2/pack_trailing_newline.src0.wai | Bin 0 -> 556 bytes wai/wai-rs/src/bin/wai_staged_vectors.rs | 298 +++++++++++++++++- 125 files changed, 4073 insertions(+), 4 deletions(-) create mode 100644 wai/staged-conformance/cases/base_readded/L1a.wai create mode 100644 wai/staged-conformance/cases/base_readded/L1b.wai create mode 100644 wai/staged-conformance/cases/base_readded/base.wai create mode 100644 wai/staged-conformance/cases/base_readded/case.json create mode 100644 wai/staged-conformance/cases/base_readded/stage0.bin create mode 100644 wai/staged-conformance/cases/batch_conflict_release/L1.wai create mode 100644 wai/staged-conformance/cases/batch_conflict_release/L2a.wai create mode 100644 wai/staged-conformance/cases/batch_conflict_release/L2b.wai create mode 100644 wai/staged-conformance/cases/batch_conflict_release/base.wai create mode 100644 wai/staged-conformance/cases/batch_conflict_release/case.json create mode 100644 wai/staged-conformance/cases/batch_conflict_release/stage0.bin create mode 100644 wai/staged-conformance/cases/chain_budget_retained/L1.wai create mode 100644 wai/staged-conformance/cases/chain_budget_retained/L2.wai create mode 100644 wai/staged-conformance/cases/chain_budget_retained/base.wai create mode 100644 wai/staged-conformance/cases/chain_budget_retained/case.json create mode 100644 wai/staged-conformance/cases/chain_budget_retained/stage0.bin create mode 100644 wai/staged-conformance/cases/chain_over_limit/L1.wai create mode 100644 wai/staged-conformance/cases/chain_over_limit/base.wai create mode 100644 wai/staged-conformance/cases/chain_over_limit/case.json create mode 100644 wai/staged-conformance/cases/chain_over_limit/stage0.bin create mode 100644 wai/staged-conformance/cases/cost_over_limit_symbols/L1.wai create mode 100644 wai/staged-conformance/cases/cost_over_limit_symbols/base.wai create mode 100644 wai/staged-conformance/cases/cost_over_limit_symbols/case.json create mode 100644 wai/staged-conformance/cases/cost_over_limit_symbols/stage0.bin create mode 100644 wai/staged-conformance/cases/cost_over_limit_working/L1.wai create mode 100644 wai/staged-conformance/cases/cost_over_limit_working/base.wai create mode 100644 wai/staged-conformance/cases/cost_over_limit_working/case.json create mode 100644 wai/staged-conformance/cases/cost_over_limit_working/stage0.bin create mode 100644 wai/staged-conformance/cases/deadline_held_layer/L1.wai create mode 100644 wai/staged-conformance/cases/deadline_held_layer/L2.wai create mode 100644 wai/staged-conformance/cases/deadline_held_layer/base.wai create mode 100644 wai/staged-conformance/cases/deadline_held_layer/case.json create mode 100644 wai/staged-conformance/cases/deadline_held_layer/stage0.bin create mode 100644 wai/staged-conformance/cases/evidence_upgrade/L1.wai create mode 100644 wai/staged-conformance/cases/evidence_upgrade/base.wai create mode 100644 wai/staged-conformance/cases/evidence_upgrade/case.json create mode 100644 wai/staged-conformance/cases/evidence_upgrade/stage0.bin create mode 100644 wai/staged-conformance/cases/evidence_upgrade_deadline/L1.wai create mode 100644 wai/staged-conformance/cases/evidence_upgrade_deadline/base.wai create mode 100644 wai/staged-conformance/cases/evidence_upgrade_deadline/case.json create mode 100644 wai/staged-conformance/cases/evidence_upgrade_deadline/stage0.bin create mode 100644 wai/staged-conformance/cases/frozen_refuses_held/L1a.wai create mode 100644 wai/staged-conformance/cases/frozen_refuses_held/L1b.wai create mode 100644 wai/staged-conformance/cases/frozen_refuses_held/L3.wai create mode 100644 wai/staged-conformance/cases/frozen_refuses_held/base.wai create mode 100644 wai/staged-conformance/cases/frozen_refuses_held/case.json create mode 100644 wai/staged-conformance/cases/frozen_refuses_held/stage0.bin create mode 100644 wai/staged-conformance/cases/held_then_prev_mismatch/L1b.wai create mode 100644 wai/staged-conformance/cases/held_then_prev_mismatch/L2.wai create mode 100644 wai/staged-conformance/cases/held_then_prev_mismatch/base.wai create mode 100644 wai/staged-conformance/cases/held_then_prev_mismatch/case.json create mode 100644 wai/staged-conformance/cases/held_then_prev_mismatch/stage0.bin create mode 100644 wai/staged-conformance/cases/integer_spellings/L1_count_2.0.wai create mode 100644 wai/staged-conformance/cases/integer_spellings/L1_from_minus0.wai create mode 100644 wai/staged-conformance/cases/integer_spellings/L1_layer_1.0.wai create mode 100644 wai/staged-conformance/cases/integer_spellings/L1_layer_1e0.wai create mode 100644 wai/staged-conformance/cases/integer_spellings/L1_layer_minus0.wai create mode 100644 wai/staged-conformance/cases/integer_spellings/L1_span_null.wai create mode 100644 wai/staged-conformance/cases/integer_spellings/base.wai create mode 100644 wai/staged-conformance/cases/integer_spellings/case.json create mode 100644 wai/staged-conformance/cases/integer_spellings/stage0.bin create mode 100644 wai/staged-conformance/cases/layer_gap_on_arrival/L2a.wai create mode 100644 wai/staged-conformance/cases/layer_gap_on_arrival/L2b.wai create mode 100644 wai/staged-conformance/cases/layer_gap_on_arrival/base.wai create mode 100644 wai/staged-conformance/cases/layer_gap_on_arrival/case.json create mode 100644 wai/staged-conformance/cases/layer_gap_on_arrival/stage0.bin create mode 100644 wai/staged-conformance/cases/live_multi_base/L1_second.wai create mode 100644 wai/staged-conformance/cases/live_multi_base/base_0.wai create mode 100644 wai/staged-conformance/cases/live_multi_base/base_1.wai create mode 100644 wai/staged-conformance/cases/live_multi_base/case.json create mode 100644 wai/staged-conformance/cases/live_multi_base/stage0_0.bin create mode 100644 wai/staged-conformance/cases/live_multi_base/stage0_1.bin create mode 100644 wai/staged-conformance/cases/rate_downsampling/L1.wai create mode 100644 wai/staged-conformance/cases/rate_downsampling/base.wai create mode 100644 wai/staged-conformance/cases/rate_downsampling/case.json create mode 100644 wai/staged-conformance/cases/rate_downsampling/stage0.bin create mode 100644 wai/staged-conformance/cases/rate_geometry_mismatch/L1.wai create mode 100644 wai/staged-conformance/cases/rate_geometry_mismatch/base.wai create mode 100644 wai/staged-conformance/cases/rate_geometry_mismatch/case.json create mode 100644 wai/staged-conformance/cases/rate_geometry_mismatch/stage0.bin create mode 100644 wai/staged-conformance/cases/refines_against_base/L1_span_past.wai create mode 100644 wai/staged-conformance/cases/refines_against_base/L2_runs_past.wai create mode 100644 wai/staged-conformance/cases/refines_against_base/base.wai create mode 100644 wai/staged-conformance/cases/refines_against_base/case.json create mode 100644 wai/staged-conformance/cases/refines_against_base/stage0.bin create mode 100644 wai/staged-conformance/cases/refines_rules/L1_one_run.wai create mode 100644 wai/staged-conformance/cases/refines_rules/L1_other_producer.wai create mode 100644 wai/staged-conformance/cases/refines_rules/L2_adjacent_runs.wai create mode 100644 wai/staged-conformance/cases/refines_rules/L2_runs_not_span.wai create mode 100644 wai/staged-conformance/cases/refines_rules/base.wai create mode 100644 wai/staged-conformance/cases/refines_rules/case.json create mode 100644 wai/staged-conformance/cases/refines_rules/stage0.bin create mode 100644 wai/staged-conformance/cases/refused_on_release/L1_cut.wai create mode 100644 wai/staged-conformance/cases/refused_on_release/L1_f1.wai create mode 100644 wai/staged-conformance/cases/refused_on_release/base.wai create mode 100644 wai/staged-conformance/cases/refused_on_release/case.json create mode 100644 wai/staged-conformance/cases/refused_on_release/stage0.bin create mode 100644 wai/staged-conformance/cases/step1_refusals/L1_fallback.wai create mode 100644 wai/staged-conformance/cases/step1_refusals/L1_no_refines.wai create mode 100644 wai/staged-conformance/cases/step1_refusals/base.wai create mode 100644 wai/staged-conformance/cases/step1_refusals/case.json create mode 100644 wai/staged-conformance/cases/step1_refusals/stage0.bin create mode 100644 wai/staged-conformance/cases/up2_geometry_mismatch/L1.wai create mode 100644 wai/staged-conformance/cases/up2_geometry_mismatch/base.wai create mode 100644 wai/staged-conformance/cases/up2_geometry_mismatch/case.json create mode 100644 wai/staged-conformance/cases/up2_geometry_mismatch/stage0.bin create mode 100644 wai/staged-conformance/cases/wir1_faults/L1_bad_magic.wai create mode 100644 wai/staged-conformance/cases/wir1_faults/L1_elem.wai create mode 100644 wai/staged-conformance/cases/wir1_faults/L1_reserved.wai create mode 100644 wai/staged-conformance/cases/wir1_faults/L1_too_large.wai create mode 100644 wai/staged-conformance/cases/wir1_faults/L1_trailing.wai create mode 100644 wai/staged-conformance/cases/wir1_faults/L1_unit_count.wai create mode 100644 wai/staged-conformance/cases/wir1_faults/L1_version.wai create mode 100644 wai/staged-conformance/cases/wir1_faults/base.wai create mode 100644 wai/staged-conformance/cases/wir1_faults/case.json create mode 100644 wai/staged-conformance/cases/wir1_faults/stage0.bin create mode 100644 wai/staged-conformance/wai2/pack_after_payload.src0.wai create mode 100644 wai/staged-conformance/wai2/pack_deep_120.src0.wai create mode 100644 wai/staged-conformance/wai2/pack_deep_124.src0.wai create mode 100644 wai/staged-conformance/wai2/pack_leading_space.src0.wai create mode 100644 wai/staged-conformance/wai2/pack_trailing_newline.src0.wai diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 72ba4558f..a612eb777 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -68,7 +68,11 @@ wire format may change and such changes are recorded here. arithmetic. In `ternary::rans_encode_trits`, the encode call sat inside `debug_assert!`, so a release build coded no symbol; it now runs in every build. - **Conformance**: the corpus [staged-conformance](staged-conformance/) (image, video, - audio and splat sets; one case per refusal and order rule; `WAI2` identity cases; the + audio and splat sets; cases for every reason of §4.2 and every `WIR1` refusal code, + in the order the steps check them, and for §3's rules, holds and their bound, + batches, frozen units, the chain budget, a base decoded again, receipt evidence and + live deadlines, one clock across bases included; `WAI2` cases for each refinement + rule of SPEC §7.1, for exploding entry by entry and for what a packer refuses; the live schedule), written and verified by `wai_staged_vectors` and verified independently by `staged_verify.py`, which shares no code with `wai-rs`; the pure- Python BLAKE3 moves to `claim-conformance/_blake3.py`, shared by both Python diff --git a/wai/staged-conformance/cases/base_readded/L1a.wai b/wai/staged-conformance/cases/base_readded/L1a.wai new file mode 100644 index 0000000000000000000000000000000000000000..7406fa2ef7b2ee5e1dd615b07a3fd3b60677d3d1 GIT binary patch literal 595 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_(cC=A z!Yswo5=bN(C0V4JS(=$7nJ1bXSfrYy8YLzg85o!tn53Fo0F|0p8XFrJ7#Jr4U0YC; zT1Fj{auO?3ixtjrzUDq-7*GD=U(5L_~J^?3r>~T->)sd)(73)J6ZFN zWZ^cB9}nc*pDmx`@#fXjSHX4(pRHG>J@N9)|McWv`r=zF1TtT;GHg!O-EZ>eeAV%+ z29uWf&iINSZ)^B=2K}FKOgqKZ#jt7L^>>wjCDdl0)jTE0u%bg}N#%;Yr588-GOgHh NIL@&0a6?+k3IG$@#q9t9 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/base_readded/L1b.wai b/wai/staged-conformance/cases/base_readded/L1b.wai new file mode 100644 index 0000000000000000000000000000000000000000..55dc6764f517771a3c5333f965a4bae73d5be93f GIT binary patch literal 587 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_(cC=A z!Yswo5=bN(C0V4JS(=$7nJ1bXSfrYy8YLzg85o!tn53Fo0F|0p8XFrJ7#Jr4U0YC; zT1Fj{auO?3i7)vi~wL{QnP9%*fyaBtf`l zc2M8`Ri~L3|GWE(T}yZCgAXUGSSJ(}m2qZ?KJZX0G4r@m(zxW(8Ci=qjR!&V__j`B zTy&*niz=gs*+vDW4>>Qolvm2%niTZNB68i^31@YmMlmq#zqWVkpFJY;xY+)3yR;?7 zuJ-x=S@QIvIo&5W@`uO^CrAIebyxKMP2X2CY>V73T4yc$Ws_AhA?^cT7>Da|zBSx@ Ic^B^i0BOa)&j0`b literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/base_readded/base.wai b/wai/staged-conformance/cases/base_readded/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..f32687d64ce5cb87b8c7b247049df3fbea85f52c GIT binary patch literal 551 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp2H7#SGCJ;Mwcfix4)xg2Qt z*l87XTdz1Sfzvfa%PH+yg*^WmlVjMbwGEp#u6JGiIhR@D zjvdqNe9D7YajXw&x;Peyotk+6M>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_}}5Xdu5HZe;!2fDVP zD7B0_CgmhnrWPq#83J8el30|U3JjOL(wv;yKMV{E;hsT;jKJVz0EdJ01J@u z=l}ozJWv5fpa7!?1H)=F28Pgh28NJ+28Q6R3=DtoGB5;v1+xD$F#HE%pehC*APK@> zJN$E|-_qc_+qsr+uf-)v*>{ZYu~4J-nF8dw;mx4%6@{ H`L-Vb@j}0b literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/batch_conflict_release/L2a.wai b/wai/staged-conformance/cases/batch_conflict_release/L2a.wai new file mode 100644 index 0000000000000000000000000000000000000000..8473f8d9affadec750ec3164e8c785e7504e5fd5 GIT binary patch literal 943 zcmXxgU2GIp6bJBsX{naBq;$1_MN`KHN?FRx&d$zG;Q=aD3MK_1RD_1!ow-{Ew!6!8 zyKUJNn=~;*P*a*<`ce|2PZkW34!5V4|AF}=jagyKMIrp47 zbLO^fXwhCQ0CGyNaFs?yYfzP-(j^^NFv_})Ofhzo8JWy*!;|TRYm1EZi&BnE?Dgcq z9@mpykMcRi7HJW46K{QZmGj1yBCOehJoN~ZTI)1t9 zX@2^@!frnk6Wx*-U7Bhv9?_$=wA8R3m9}aaQR#&AP{@i!EX@$QG@_;+5mCz&dOU0h z$I=+-b~s|2oHp&rEIXMZ92RC&>bh`@kPy<2=$hq(4aYDosk4P{#VuQn$AwgNRg0^( zBb}(ql9(McG=_BzqExfsT1VTg$6j0gsc9c=EAAuK!>D{zN{X zZ+*Nm&h<2%q1XA|ZrVfbRKnuDeEED4P+AOJq)&Wo?34f%3xIejl>?tH1Qt~SodF>5 z6mZE`1?*o+)j-uU;3_S7j%tAG^o#<0UJFz&r#c|LidNGa;0qNvr~w{58v-V&TL&`K zLs{yjKI*4Kl%wTgUQXIMX|vbBzCW4(PZ6 z6y4^b%+ef?zfb>Ao-qp0$Gfj;dUEcEv+afBQ)ABeqt}{xKdWz!HCCplK4=}e)NamP zdHLw|g-1ibezU%!bn5ob;XP%8M;2q?_sWSs#=2@F`!eM-8)siAoqJeuMC+S6+w@^g z>-1cuyZc((Uw1alPETyQd*SlOU%zszsAaO|wey>cihnK}E&uN1GW}LV^V?4YqYtwm z9Uoj%{@WWPYv1x_+Ol6gcz4S$P2cDUA+;h&H zxpP`KG%EFZL|Lhi+fq_eBC=E?bqUkv*!9|`$Y5`~zHog6Jkjmg2KPl!lre?V>4}{^ zwkNvY=ChK)yLrlXY`-7XXc>9iB7z2}v}{*Mduk*j|Ocgdub}#uCDiRW%{ZD2qn*R9sh7&V-uKSez$xjWa8za#L5Z zV@51)XgF=RCwkFI6UKDmc@n4WW|yVQUC{F5gFD)<^x5shzwB-}EHf~Y!Fit#D;X4Ib-(Nv|ESJk|dKWm0 zwGH*{Fc-;TxBwTS6E7CeA-W6;=MjbA1Gs|5B}GJ5G11ip5F%Psg1)f0l*nE}bnQc; z9Ul>0UrOYBOwM57b< z|38S%Ph&VQh;nZr7YrxCyu6A#VrX-IbJ?K&`TXJXP0MCG?k#$Gc=UeLM0NPZ z(3gdk4PUi%T}K*9 z55C+9|;li6`zcBN!v-;M7sYqhi2UOhZkx7j)K`qF2$Q%xQ0 z?5&lB6|IXGgvM^S_fHlV6!6l=GuzAR?`~rkn(L?2Vyb*_&fReHkL<*!*yCjbBd literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/batch_conflict_release/base.wai b/wai/staged-conformance/cases/batch_conflict_release/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..2c93f3f3635903848e035cfb64fed395d7f286ac GIT binary patch literal 559 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp217#SGCJ;Mwcfix4)xg2Qt z!~g&Pm4RX)um*^k*ccd?fr?nT7#O1XfmoD*VS@yaR%BpcRbya?)@EQ}(_>(WF$Q9D z28K-*3=HfJKBzb}qW~h3STf`r$ i#(@1#`>x9hkH5LTmz5NWNU}Zui$f?QDop&)wQ~T@^}sy< literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/batch_conflict_release/case.json b/wai/staged-conformance/cases/batch_conflict_release/case.json new file mode 100644 index 000000000..a19f17b3d --- /dev/null +++ b/wai/staged-conformance/cases/batch_conflict_release/case.json @@ -0,0 +1,207 @@ +{ + "format": "wai-staged-case/1", + "name": "batch_conflict_release", + "about": "layers 2a and 2b, both over layer 1, held; layer 1 applies and releases them together: they conflict in the batch, and the units freeze at stage 1", + "bases": [ + { + "file": "base.wai", + "object": "3e70bf638ce90438ec0558ed23229b6915a3e58736a897a3f45ad916a83c46c7", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "61cae63ae78b67bfe98aed1387a7781120f5a16f1aa3605633810b30a1710977", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L2a.wai", + "object": "eb051506ca9764570729f78c96cd5498cc53880f2e26566e8c3e6d9c185dcf1a", + "bytes": 943 + }, + { + "file": "L2b.wai", + "object": "9c361336e24cbbf8d7e91489226f285d63697c24f25078018ecaf2fc99e57aa1", + "bytes": 935 + }, + { + "file": "L1.wai", + "object": "7a44a78e33ad52aaec6319d45d579e387339f9c0ffae0301f0cded809d4bcb51", + "bytes": 587 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L2a.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2b.wai", + "at_ms": 2, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 3, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "eb051506ca9764570729f78c96cd5498cc53880f2e26566e8c3e6d9c185dcf1a", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [ + 0, + 1 + ], + "refused": [] + } + ], + [ + { + "object": "9c361336e24cbbf8d7e91489226f285d63697c24f25078018ecaf2fc99e57aa1", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [ + 0, + 1 + ], + "refused": [] + } + ], + [ + { + "object": "7a44a78e33ad52aaec6319d45d579e387339f9c0ffae0301f0cded809d4bcb51", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + }, + { + "object": "eb051506ca9764570729f78c96cd5498cc53880f2e26566e8c3e6d9c185dcf1a", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + }, + { + "object": "9c361336e24cbbf8d7e91489226f285d63697c24f25078018ecaf2fc99e57aa1", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "7a44a78e33ad52aaec6319d45d579e387339f9c0ffae0301f0cded809d4bcb51", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "3b34aea57788ede6bffae46cfc385087b3d88e2ca4343b6f1263c55588a0d053" + }, + { + "stage": 1, + "producer": "7a44a78e33ad52aaec6319d45d579e387339f9c0ffae0301f0cded809d4bcb51", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "d88b339d03e35a326944a921552088c8072c6094bba4b96dfdd064f97f254010" + } + ], + "clip_sha256": "6620d5f7e2d50dc37635c3881cd29a06cf6de2444320e0159a203fc600cc8aae", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/batch_conflict_release/stage0.bin b/wai/staged-conformance/cases/batch_conflict_release/stage0.bin new file mode 100644 index 0000000000000000000000000000000000000000..bbd335b532e6f8aac8beb104777ed2c7375c8994 GIT binary patch literal 384 zcmaLSAC$x}5XbSw!^PrZvA9?a7K6oLFc`bq1qWjr3l*gom%#h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZYX2}WFob&s88QNclL^S?fMRAK4FfDd z#-IQH|MNfvRs#iAivc-i3=FG67#RLdW?%?9$iT4t6_EbPz!3bOf#E+8GcYhR_y9=| zew4Vef(K~mNB4`bFDzSj#Mkdk(~iQ_?Q%yq=C+<^a+i^H(^@-6{aVI$)i2*N`0Hk0 zkeatVXOUU?E+2FCXT2-^9+j+5*lFeN@lR}*a!WkW76yi5>se~=B|733|FCP(Jf<>n z?`Ff@aRHOe3(oA_6|`qcOW&HqF4FeVMthsTNNm6M=ZtGtYv!EWT~>#}W+uEiAoDt@ Had{{JWQxK2 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/chain_budget_retained/L2.wai b/wai/staged-conformance/cases/chain_budget_retained/L2.wai new file mode 100644 index 0000000000000000000000000000000000000000..c293b4ac51447c3874014bbeea420990cfe7b24e GIT binary patch literal 795 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 ejKsL}B(q&n3=y-iEPkR26%SQnV$GJTK literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/cost_over_limit_symbols/case.json b/wai/staged-conformance/cases/cost_over_limit_symbols/case.json new file mode 100644 index 000000000..2d9a41781 --- /dev/null +++ b/wai/staged-conformance/cases/cost_over_limit_symbols/case.json @@ -0,0 +1,122 @@ +{ + "format": "wai-staged-case/1", + "name": "cost_over_limit_symbols", + "about": "a sink whose cost limit is 64 symbols: the 384-symbol layer is cost-over-limit (symbols)", + "bases": [ + { + "file": "base.wai", + "object": "17a4bbf83638fddab4404a156f5eaee382933d438fc8fe92bc5ffddaa891e93a", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "90505a23d051df0d5ed6bf7b6baaa8e14ab5a940e51b44c10e0cc47c6958bd4d", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "b60647ec8bfbab8ddccdea62b935851b32b4d2d15e02b62d8ddf029a54381924", + "bytes": 595 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": { + "output_bytes": 18446744073709551615, + "symbols": 64, + "working_bytes": 18446744073709551615 + }, + "budget": 1073741824, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "b60647ec8bfbab8ddccdea62b935851b32b4d2d15e02b62d8ddf029a54381924", + "envelope": "cost-over-limit", + "detail": "symbols", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "17a4bbf83638fddab4404a156f5eaee382933d438fc8fe92bc5ffddaa891e93a", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "d46a8c04a3efdcb0ea7ca67a174af553a33527b37136762ba7cbf6263c13b187" + }, + { + "stage": 0, + "producer": "17a4bbf83638fddab4404a156f5eaee382933d438fc8fe92bc5ffddaa891e93a", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "6cd64862accc34f777535d1924f4089f63f1895a9593588f00919f0a5a9caff9" + } + ], + "clip_sha256": "e4561786406a2ad52a46f3814fe516cd07745917fa5150b2b25b49f71218d2db", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/cost_over_limit_symbols/stage0.bin b/wai/staged-conformance/cases/cost_over_limit_symbols/stage0.bin new file mode 100644 index 000000000..9b9c2926f --- /dev/null +++ b/wai/staged-conformance/cases/cost_over_limit_symbols/stage0.bin @@ -0,0 +1 @@ +8@08@08XHP`PX`Ppxhpxhˆ�08X8P`P`hH`x`p€pxˆp€�x˜ HX`H`x¨Ðèpxˆp€ˆ€�˜€˜¨˜ °`hx°Ðÿ¸àÿÀðÿ€�¨˜ °˜¨À¨°Ð°àÿ¸èÿÐøÿÐÿÿàÿÿ¨ÀÀ¸ÀÐÀØà€�¨Ðøÿàÿÿèÿÿ¸ÈÐÀÀØÈÐàÈà𘠰¨°¸øÿÿ¸ÈàÀØàÐØèØèÿàøÿ°ÀÈ°ÈØÀØàÐØèÐàøèðÿðÿÿøÿÿ8@08@0@XHP`PX`Ppxhpxh€�08X8P`P`hPhx`x€p€ˆp€�€� HX`P`x`pppxˆx€ˆ€�˜€�¨˜ °`hxhp‡p€—x��Èðÿ˜ °˜¨À¨°Ðh€�pˆ—ˆ˜ŸÐÿÿàÿÿðÿÿ¸ÀÐÀÐØ€�¨�˜§àÿÿèÿÿøÿÿÿÿÿÿÿÿÈà𘠰¨°¸°¿¿øÿÿÿÿÿÿÿÿØèÿàøÿ°ÀȰÈиØàÐØèÿÿÿèèÿðÿÿøÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/cost_over_limit_working/L1.wai b/wai/staged-conformance/cases/cost_over_limit_working/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..dceb8541df3cadf6d5b9a0c82d82e8ec604aeea6 GIT binary patch literal 587 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X@G07;=GSSp1(G2L? zf}+$i>X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZYX2}WFob&s88QNclL^S?fMRAK4FfDd z#_#|C|MNlx)&OZHpkM?)5UT?9hchsQ&Sqd(wS$3Spq(Eeax_^Ig+1C4K_&6sMypvzrECTv^c<~%c#f=@f1sduav$i4lc+H1w<0)~(BH}ab#drZGrr!4GS z{XL=dO)uk3-(0^%Qu;SL8E4-RT(f0)QYJ^F*Y+@xf)b}ikN8qKujc)p{h~S4realb F9{?V(zwQ74 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/cost_over_limit_working/base.wai b/wai/staged-conformance/cases/cost_over_limit_working/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..105960af45f1c5a347a4457233fde86cf195424f GIT binary patch literal 551 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp2H7#SGCJ;Mwcfix4)xg2Qt z$n&gSonZg1c)UV7}m=$FtExoFhnaeFtBMbFtBR@ zu>k`^j5Pzp-(Db|&cLu@4g3gmU8SxSOKNrZZHuIVUtP+1#1Ed+Fkuk4MXbCmuHNUvhaK$IUMi$-lmOZC++k zuXp5=&Dr-c=^wt#Joa{9ZtTN1tU-5=Y>{4axHNe7)8CKgZa9`d@$BVQSvPN)&VQ^v zGsA|@CbiV6uitH{z;m~G$?-1yFQ>6h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_ zI4RXA+1S`1#mFqpD8<||&C<}sAjvG%(%2%&!aOnAB+b&y#MnI9!ooboJk`=L%>wA! zf}+$i>X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZYQHcrFob&s88QNclL^S?fMRAK4FfDd z#-IQH|8qkH!dMv?!h{(Z7>$59f`MVxBnF0%y$lR19|7?<28RFtK?)fee1N0}5bNsl zK25)9&bM-5`mC@h-AV=S`c;mRmskp;=f_Mu|N2rA^D4X0110JcxC13x7hY)->g{2l zR3R^I{q}<&%gOf{YH$3_4}My-^y&FXL2c2Mf9gIyv+T^fp|`B*ZDh-z&)iQHcIV%e z;ohZrv{h_*n)lMoKOKMH|Ik!enz=7U=k|)(8`HXHD2kl*3oH5gsCd?E>8PB(6#%X} BzSjT% literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/deadline_held_layer/L2.wai b/wai/staged-conformance/cases/deadline_held_layer/L2.wai new file mode 100644 index 0000000000000000000000000000000000000000..4649e7cd791e8bbf1244a24007b314cccdcc03e1 GIT binary patch literal 795 zcmXxgPe>GT6bJBUnr)^9`6n^UoXpH!*O{H&nH@WX)*&G%r1BET{Q3Q4a-7w1HB3EJ zgb_$kdFbH9Ll;ZyP(czXX|$%1LWlC8)hPe{WTP%UM?IxL^)7CPWuPQpp*EYf~Cera`Q^Y!|Lp6v!6Ci;4CV#ih35V=9J`n^ z_JiHMK5=!>J7;~!IxWUhb2p~4S7#f(Ef@bPS}*%l@HLUKbKf};zLokKzW2Ri L<-qxSWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1M7#SGCJ;Mwcfix4)xg2Qt zFH5eG8wHX-L^cWa6 znlLc1TQM-iI5IFSpT@xO2N)j97Xk4K28N)u3=Ata0r4InKElB8_dJlk!N3sw5Qv`x z@mmIlfA1L>LOuiWcLs+4|G_Q*yWFMryME8dsZc&G jrSm8L*GikH{;TG?Z=Ff5&;{K~Vr(n?J}~<4*4h97x>Uo7 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/deadline_held_layer/case.json b/wai/staged-conformance/cases/deadline_held_layer/case.json new file mode 100644 index 000000000..7cd60217e --- /dev/null +++ b/wai/staged-conformance/cases/deadline_held_layer/case.json @@ -0,0 +1,170 @@ +{ + "format": "wai-staged-case/1", + "name": "deadline_held_layer", + "about": "units presented at 200 and 240, leads 10 and 30: layer 2 arrives at 175 and is held; layer 1 at 180 applies to both and releases it; layer 2 is late for unit 0 (deadline-missed) and in time for unit 1", + "bases": [ + { + "file": "base.wai", + "object": "9d3fee0b3f8dc53be2c330d26f2d79f9140b6e938b87ac4f96437c887d7e91f8", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "ec6bbf86767acbb42ddf2e5a456a62b7ff62c7f629a8105ac0e8281f7d928fff", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "3b178bb976cf61d303a743766bcffb139dcafcb51e2d3e9bf6f109e2aff44bf0", + "bytes": 579 + }, + { + "file": "L2.wai", + "object": "82268f093ba4e9a08335d6218a823b79333c972883df2573a44300403e582651", + "bytes": 795 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "live": { + "t0_ms": 100, + "unit_ms": 40, + "target_latency_ms": 100, + "lead_ms": [ + 10, + 30 + ] + } + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 175, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 180, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "82268f093ba4e9a08335d6218a823b79333c972883df2573a44300403e582651", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [ + 0, + 1 + ], + "refused": [] + } + ], + [ + { + "object": "3b178bb976cf61d303a743766bcffb139dcafcb51e2d3e9bf6f109e2aff44bf0", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + }, + { + "object": "82268f093ba4e9a08335d6218a823b79333c972883df2573a44300403e582651", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 1 + ], + "held": [], + "refused": [ + [ + 0, + "deadline-missed" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "3b178bb976cf61d303a743766bcffb139dcafcb51e2d3e9bf6f109e2aff44bf0", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "fed27cc0f4641935ead64c558da6becb03dca32a7210df66bebf3aba543e8be8" + }, + { + "stage": 2, + "producer": "82268f093ba4e9a08335d6218a823b79333c972883df2573a44300403e582651", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "38942db7ef49b4787da8d6f264ffbb27271fef8d7fe9a7a577dfccb8baa2a6e3" + } + ], + "clip_sha256": "ee605579ccb95d90c626343ece2fd5e347647f588b73d48ccf01c7432b724bd2", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/deadline_held_layer/stage0.bin b/wai/staged-conformance/cases/deadline_held_layer/stage0.bin new file mode 100644 index 000000000..e8800b2c6 --- /dev/null +++ b/wai/staged-conformance/cases/deadline_held_layer/stage0.bin @@ -0,0 +1 @@ +(08(08(HP@HP@PhX`hXh€(080@H0@X@XXPhp`pxhp€h€�@HP@Ph˜Àà`hxhpxp€ˆpˆ˜ˆ� @`h¨Èð°Ðÿ°àøp€˜ˆ�˜ˆ˜°˜ À Øø¨ØÿÀàÿÀðÿÐðÿ˜°°¨¸À¸¸Ðpx˜ÀèÿÐøÿÐÿÿ¨¸À°¸È¸Àà¸Ðàˆ�°˜ ¸àÿÿ¨¸Ð°ÈÐÀØØÈØðÐèø ¨°°°À¸ÀØÀÈØÐÐàØØðàøøèÿÿ(08(0@0HP@HXHPhX`p`h€(080@H0@X@XXPhp`pxhp€p€�@HP@PhP`h`hxhpxp€ˆp€˜ˆ� H`h`hxhp‡h€€¸àÿˆ�˜ˆ˜°˜ ÀXx€`x‡x€�ÀèÿÐðÿàÿÿ¨°À°¸Ðpx˜xˆ—ÐøÿÐÿÿèÿÿøÿÿÿÿÿ¸Ðàˆ�¨˜ ¸˜¯¿èÿÿøÿÿÿÿÿÈØðÐèø ¨°¨°À¸ÀØÀÈØÿÿÿØØðàðøèÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/evidence_upgrade/L1.wai b/wai/staged-conformance/cases/evidence_upgrade/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..8032aee70e5e668d24ad68c746835fa54d6cf22e GIT binary patch literal 591 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZY8imR815Nl$OxpF7#J8hpqLp*!vG7A z@#p{l|2$9uMmC@xeg+0c1E4?z1H;N528NKO3=IEHGB5;)>|K^yhznl@(cTPtciqd4qIi2Jhli555_g3B|p>E3UJn+$`w) M!;>35>gP@d08_WVkN^Mx literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/evidence_upgrade/base.wai b/wai/staged-conformance/cases/evidence_upgrade/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..c6d87266d0d7d532d92260cce38c1a10d88b322d GIT binary patch literal 547 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1c7#SGCJ;Mwcfix4)xg2Qt z6Wn8Uyjyp)0A-%21}3&dL)7*_5B(kFr9*ManH28NJ_3=IFD z1Myo1hE*RJ7(%}S+5bT<06G!~jsUSetD=RBC6GQzVjitoPKWpwiTUEfoennYA9 zB=t|6;5PKNv=DwgEoFhLdr7qP`DJsnnSUCs%GLg~s6cjh*30jEShCk`+cVGaLB`_t zt>Rum=Or_y{f+s)_}A{`W(OW`Tr#Kqveg^SKfJg9&+mM*LhR6`4vX+hPs6kCR!Zi7 ziGH{zit$)UY`3&wd-%satPlDN5|%oMGtR2AV`mUHvUzHBneD*ZJGFN>zI%keo6^47 aea>O+$=&?h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_1KfWa8<8Dz)^q?s5P7&xGq8A!tb3y|^W z|Ns9yPyt3Z28J+628PvEK!F4XhLv*|7=m{*F#LTCq`xvSg#2Y-`2QcIn2{j>NcsS= zXX%3;kjUYG2Na#Y+8&y2eX)vNp!o3S1($0Sf)X}7Qp&Oab~yf8<=LHz-81VHODFcu zoUYs@wtU%<8Pe`*jXHUDr~dJ*4xi<{Hzi2)-89Djz`U!om>B9$ok@|4OO4pM>iQP5 zn+IpHhRiy3@6B1)y=MfIM0Yu~9y-GB;(q(z-JmRv6;kmZZs;d3T=n^5v6)4WxXX+= L6U?WTL_7olkc7oo literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/evidence_upgrade_deadline/base.wai b/wai/staged-conformance/cases/evidence_upgrade_deadline/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..c94dd7c4c4e665927cf6f3367293e93dd98d3fc4 GIT binary patch literal 559 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp217#SGCJ;Mwcfix4)xg2Qt z!~g&Pm4RX)um*_N0u@CvGcd5SFfc@MGcZK+GB9l9V_=98XJBBLVql1s2b!V8z_3|` zfgw(lfq_$pfnkdQkZ;4lu(F4N;qPn)h86RGco7gU1M=4c>1_-Q!TW*uB#?g*$iD%^ z_kj2r1H-?U3=ARf85mZ5Vqp0HALI(4LxJE75Fg<>yWl&J_Lr#FXcK$(uuJ9g<@LH8 zqI*Skt_w;p?OtrN@AC4gYa+HuSoeqOZwzMZ)hdhcE4w~d{+PG-u0^svw}NR01fsi~85-J?A6iXx*=Z?gY8@xpXprwL5Uk7rz9PUg!GdQd0e o{@{d-p0BC+idR;Ls%FnxexYvR@uM;Qo6V1HH`-l!WZv=@03CzK{Qv*} literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/evidence_upgrade_deadline/case.json b/wai/staged-conformance/cases/evidence_upgrade_deadline/case.json new file mode 100644 index 000000000..e2ebb889c --- /dev/null +++ b/wai/staged-conformance/cases/evidence_upgrade_deadline/case.json @@ -0,0 +1,159 @@ +{ + "format": "wai-staged-case/1", + "name": "evidence_upgrade_deadline", + "about": "units presented at 100 and 140 with a 96 ms lead: the layer arrives at 0 with no evidence and is verified at 5, so over the attested base it counts as arriving at 5: late for unit 0, in time for unit 1", + "bases": [ + { + "file": "base.wai", + "object": "e59fdd4e7c37a73cb52cfe892da45ee145ca2adf0bc5e59dcf6d12c8d0396e7e", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": true, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "fbef590b43726dec81cc945d337b760f96605ac7e1dfc550b559c36e3f63f166", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "3c41fb403d64bd906ed33337da8ea31951e0bb300bb4dcbd5355266e28474e94", + "bytes": 591 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "live": { + "t0_ms": 0, + "unit_ms": 40, + "target_latency_ms": 100, + "lead_ms": [ + 96 + ] + } + }, + "steps": [ + { + "op": "offer", + "file": "L1.wai", + "at_ms": 0, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 5, + "evidence": "delivered-verified" + }, + { + "op": "base", + "index": 0, + "at_ms": 6 + } + ], + "expect": { + "reports": [ + [ + { + "object": "3c41fb403d64bd906ed33337da8ea31951e0bb300bb4dcbd5355266e28474e94", + "envelope": null, + "detail": null, + "held_for_base": true, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "3c41fb403d64bd906ed33337da8ea31951e0bb300bb4dcbd5355266e28474e94", + "envelope": null, + "detail": null, + "held_for_base": true, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "3c41fb403d64bd906ed33337da8ea31951e0bb300bb4dcbd5355266e28474e94", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 1 + ], + "held": [], + "refused": [ + [ + 0, + "deadline-missed" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "e59fdd4e7c37a73cb52cfe892da45ee145ca2adf0bc5e59dcf6d12c8d0396e7e", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "e644f1ac16fa13e720e65273eb0877cb6316655c7339eb23a87912940469cfbe" + }, + { + "stage": 1, + "producer": "3c41fb403d64bd906ed33337da8ea31951e0bb300bb4dcbd5355266e28474e94", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "ef15f3ec3f859551f8c4ec5fdc81a327739644428ba39bee65309a533178e484" + } + ], + "clip_sha256": "f9a7d9010bd6d8b13be120607274ca4ea0f5c57dee5a2fd7c177d293d92918f6", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/evidence_upgrade_deadline/stage0.bin b/wai/staged-conformance/cases/evidence_upgrade_deadline/stage0.bin new file mode 100644 index 000000000..209bb5806 --- /dev/null +++ b/wai/staged-conformance/cases/evidence_upgrade_deadline/stage0.bin @@ -0,0 +1 @@ + @(8H(8H8H`HX`HXp`pppxˆ08P0H`HX`HhxPpxhpˆxˆˆxˆ @XhP`p¨Ðð`xˆxˆˆxˆ˜ˆ�¨� ¸Phx¨Èð¸àøÀàÿˆ�˜ˆ °˜¨¸¨¸À¨ØÿÀàÿÈðÿÐÿÿàÿÿ¨°À¨ÀиÀà€ˆ Øøÿàÿÿðÿÿ°¸À¸ÈÐÀØØÈà蘘¨ °Àøÿÿ°ÈØÈÈØÐÐàØàøàðø¨¸È°ÀظÐàÈÐèÐèðØðÿèÿÿðÿÿ @(8H(@P8P`HX`P`p`xppxˆ08P8H`HX`HhxPpxhpˆxˆˆxˆ HXhPhp`pp`xˆxˆˆxˆ˜ˆ�¨ˆ °Xhx`hxp€€€€—Ððÿˆ˜°˜¨¸¨¸À`x‡x€�€�§ÐÿÿØÿÿðÿÿ¨ÀиÀà€ˆ �˜ŸàÿÿðÿÿøÿÿøÿÿÿÿÿÈà蘘¨ °À°·¿ðÿÿÿÿÿÿÿÿØàøàèø °È°ÀظÈàÈÐàÿÿÿØðÿèÿÿðÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/frozen_refuses_held/L1a.wai b/wai/staged-conformance/cases/frozen_refuses_held/L1a.wai new file mode 100644 index 0000000000000000000000000000000000000000..4ef67d78c0bd63fe09f1a340ff0a02ebd8c331f4 GIT binary patch literal 627 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZYE^*2815Nl$OxpF7#J8hpqLp*!vG7A z@#+8n|AJ5fMxekN5e9}ZW1v6~1H-Bk28Q7I3=GTnFfjbS3B(T>82&s4O1%V%zh+RyYt=m7igVExgxapLF!}1Rga`yh>SZ*p`puKW)2*>Xq zf`tzgbZhP$VI)|g8Jf`;9 sT~=53gnY1z-Pk7+;qs0@wCS7hmKV({P78ePKlUm&x?HUJelEyd0J2omWdHyG literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/frozen_refuses_held/L1b.wai b/wai/staged-conformance/cases/frozen_refuses_held/L1b.wai new file mode 100644 index 0000000000000000000000000000000000000000..207389267389f9de50f3918b01388e3e70ac99c9 GIT binary patch literal 631 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZYBhkt815Nl$OxpF7#J8hpqLp*!vG7A z@#+8n|AJ6~H9&=H1Q-~?G=Ty>3=As^7#LPeV_*o}%fPVg8W7)OVEFTtfg$KQQ2Z4G z!@su-46EKTF#P`yGJugG0!V_eauHiIE0EsXeYe}Cys#`Uc!Cj&&-*5ZI=NNP9H)HQ zwW{9knsai$O=Io!rTXV?-d*#A{rbnQCiP&mkEh=BZN7c-&NF$>aON6*?dTnou7^Bz z5)rzxzoYv?d(k2Zpt--ZF6!@{D&h7g=3=U}-b=r#bJwEtW&W8oDtp_~`Md%@ucW2&NUma)Hadt~Y zsDr1F2r9w|mOMxhrO<~^bjS`t(V>ITLI*)G5DGeIm)f2&8~Dw``#cYSUf%wAFG-XD zObJ&g7rF!z5rwcY#wBF%=Sd0HQUIvjRfhHd7xY((#bSK7@fd4qIl#akfG2wa zo}xu`sRCdST}IC^=s9|UYN`Ne9YA#pz)Q4@_O}8YK(**Ksy__y2Jtq45yaa88ae@n zqgX|NMl?bInk0aOs2R1OLnti+WY9SB&;;^P7Uj?p1wcUU=qNgdj-wN(1D!;jD1^f3 z6pElI649q&fVXo1#bWRPCD4QaKz_VxDd-GkN55CQt6OUymTtYe{+XogwU0*hMel|3 zKdU`;@%%#jyPIX<#lrIGwjcA&cfagAvlcH5#W!>J>c4Jph93UDo!H1%*1}n}=|SSx z{qFq0a$&x&@!U#>^QdCL*cc4mnAvFATA!(#+a3;Gthy6jUs$@DnoG3S^nDArbSWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1M7#SGCJ;Mwcfix4)xg2Qt z>vk6X_$eU!0#N)4kbfUY zKLXnM9Ejfn_5TOTF#sI^1ZRMFK1bU{5c@uN$OLVnsp5TVuNW_?nzE@)Wu9cG{9nvL zYjdXL9BsF`l7~0U({_(oIxkgUc-9qn1BL5XGat{Wc)Ofu@{3iqDMJ4>k6pR*Lixox z!3i^N@^x1H<KGydi*RlBSMIk@qnlYQKbreJx+}%v1pvJPxu*aC literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/frozen_refuses_held/case.json b/wai/staged-conformance/cases/frozen_refuses_held/case.json new file mode 100644 index 000000000..e00a02c6d --- /dev/null +++ b/wai/staged-conformance/cases/frozen_refuses_held/case.json @@ -0,0 +1,195 @@ +{ + "format": "wai-staged-case/1", + "name": "frozen_refuses_held", + "about": "a layer 3 held for both units; layer 1a applies, then layer 1b conflicts: the units freeze, and the held layer 3 is refused for them (layer-conflict)", + "bases": [ + { + "file": "base.wai", + "object": "3b22769b3655d2d40e6088c198acf1c621aef4fc03a87cd3059e76594f7817e4", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "fca9fd5587687b760e9cf7404ec9b31728c35893cf5473c9697a1e0a696a418e", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L3.wai", + "object": "0f0220cdae1ca73c04708465a2f09932704a86202d377e0ac9845ac2f134a79c", + "bytes": 791 + }, + { + "file": "L1a.wai", + "object": "e067e3140e46cf850faa7fbc2b4640408e47c091905fe6b49c96651541a0fd58", + "bytes": 627 + }, + { + "file": "L1b.wai", + "object": "60a22722db1888adb6276ec3d000afdd22a48780a538f0e56d0fcd95a9a08094", + "bytes": 631 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L3.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1a.wai", + "at_ms": 2, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1b.wai", + "at_ms": 3, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "0f0220cdae1ca73c04708465a2f09932704a86202d377e0ac9845ac2f134a79c", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [ + 0, + 1 + ], + "refused": [] + } + ], + [ + { + "object": "e067e3140e46cf850faa7fbc2b4640408e47c091905fe6b49c96651541a0fd58", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "60a22722db1888adb6276ec3d000afdd22a48780a538f0e56d0fcd95a9a08094", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + }, + { + "object": "0f0220cdae1ca73c04708465a2f09932704a86202d377e0ac9845ac2f134a79c", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "e067e3140e46cf850faa7fbc2b4640408e47c091905fe6b49c96651541a0fd58", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "02e96440c5b5c89c4f4a640743c0ee168e90614bed124db87cf92c982d77b144" + }, + { + "stage": 1, + "producer": "e067e3140e46cf850faa7fbc2b4640408e47c091905fe6b49c96651541a0fd58", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "cda9839d1d0a4092fc8b1fe55bcf766e3441268ec2166da0d6b20d08daf68641" + } + ], + "clip_sha256": "98e342cc102fba975a75e08ca3e3fdfb83c271443b66516eb7b6c3ad35579c2c", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/frozen_refuses_held/stage0.bin b/wai/staged-conformance/cases/frozen_refuses_held/stage0.bin new file mode 100644 index 000000000..825309ccb --- /dev/null +++ b/wai/staged-conformance/cases/frozen_refuses_held/stage0.bin @@ -0,0 +1 @@ +(0@0HP@HXHPhXhp`hˆ`€ˆx€�@HXHX`H`pXpxpp�px˜€�˜ˆ�°H`pXpp°Ðøhˆ˜€�˜€˜¨˜¨¸˜¸À`xˆ°àÿ¸ðÿÀðÿ�¨¨  À°°È¸ÈÐÀàÿÈðÿÐÿÿàÿÿàÿÿ°ÀÐÀÈàÀØà�˜ àÿÿàÿÿøÿÿ¸ÈÐÀØèÈàðØàø¨°À¨ÀÈðÿÿÈÈàÐØààèðàðÿøøÿ°ÈØÀÈàÈÐèÐàøèøøèÿÿøÿÿÿÿÿ(0@0HP@PXHPhXhp`h€h€ˆx€ˆ@HXH``H`xXpxppˆpx˜€ˆ˜ˆ�°H`pXpppp€hˆ�€�˜€�¨�¨¸˜°À`x€h€‡p��€�§Øÿÿ˜ À°°À¸ÈÐx€Ÿ€�§ˆŸ¯àÿÿàÿÿðÿÿÀÀàÀÐà�˜ ˜§¯àÿÿøÿÿÿÿÿÿÿÿÿÿÿØàø¨°¸¨ÀȰ¿ÏÿÿÿÿÿÿÿÿÿØðÿðøÿ°ÈظÈàÈÐèÐØøÿÿÿèÿÿøÿÿÿÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/held_then_prev_mismatch/L1b.wai b/wai/staged-conformance/cases/held_then_prev_mismatch/L1b.wai new file mode 100644 index 0000000000000000000000000000000000000000..dbdf29c8f48cc86589dfbcdd20df44ef5d9dfb6d GIT binary patch literal 591 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZY8imR815Nl$OxpF7#J8hpqLp*!vG7A z@#p{l|GZFvH9&=oVhjwc4Hy_!g)lJus|Vuw3=GR}Ffast1mgb;48cJBABce}8GL{w z2*26ZG+V2&h2f#$?V_C_ku$#WG?~fHlvMm_{r%>>-MyD1B|kF$HJe`O_}ldpYj(51 z4@F+r4aNLFxAuEIKYRL_fbLBbp3F2em1{B|UOr!ub&27=DMLwcWI)o-#Ggj$eFtM_ z-E2J<_}clwPByjm;s+*740_35xaF(8;iWelt7h`Am>75YS21J$+WE7UGZvINp8gfH K=4whdiva-5ZN&%x literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/held_then_prev_mismatch/L2.wai b/wai/staged-conformance/cases/held_then_prev_mismatch/L2.wai new file mode 100644 index 0000000000000000000000000000000000000000..2c4479062dd4554f5fa240ed8e7c7925076324b0 GIT binary patch literal 795 zcmXxgPe>F|90%~t8z#-tbP>cE#_XI8=4+;>}Yt z9YZCVyA!3Ko13eDS9#xl@fSWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp2H7#SGCJ;Mwcfix4)xg2Qt zehM>hjyqtky#VQ7d;LQvSD|aw3{M!%Yp8)dD1Lf`l)ja{yFBusA zz6FYXW?%^U%E0jdI}rZ|xd7-$AOI?7xWT-G_pvi0!%tS@l{D*?8!k*)q?_!g!YDn%-WbS5v3S*^9=lS=T&s?dPt= z7k>WOy7SMJUWY?l7p*?Eh|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WM!zMR9ujl2XbXvQGTwHl>x|OrJ!Ik(le+9`lBSVC_NR(%`45x zsbv5La=2%ZAtR7xVqjq4fMRAK4FfDd#-IQH|MNfv!dMv?!bE`_69$IRSO$ib(-;{3 z?Pg$Daf^W=_#+ViWnlRKAEcO(Apl7F0P)6qd zWao1(k7cE&-Clm@SrH_!V(_R^>uEbj!y--9V~Rb`LscR%9s#TKjby!`+DU3l@58SoM9$hrmw$A2tgv*#+3jDzNShomtgXS+7}YbfuuC kSW0qk_$#i@Pt{%q_8gVDtK@#)v$v{Yzo?s%T3Y^50D=+96951J literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/integer_spellings/L1_from_minus0.wai b/wai/staged-conformance/cases/integer_spellings/L1_from_minus0.wai new file mode 100644 index 0000000000000000000000000000000000000000..7a3efc8aaabdbcb2f0760c4b574d612a70a9197f GIT binary patch literal 620 zcmWG{^fXLhWMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WM!zMR9ujl2XbXvQGTwHm97EEXQiNUGO7i7qa?8?Jr&5wE6vHN zWdMe8xMz?dBamidU|`^YVrC!>11vzspa1{=^FRf{SQ!|?M1dR=28PgB28NZ>7#RNT zW?)!xi-95dBM|>(VEF$Zq?nN*07&`(@y2_63oU{43kj)@KOXYi`h=@XoLeeWU+?{7 z=W{NPWu>RxUVi6U5hSl-@TgMjX*);5B2Cp}iapOmRU$cxwko-={`#bCx>;7prksGE{nTK-W0-mS^u literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/integer_spellings/L1_layer_1.0.wai b/wai/staged-conformance/cases/integer_spellings/L1_layer_1.0.wai new file mode 100644 index 0000000000000000000000000000000000000000..061a3c93ce87b2580a2cdfbcd4559e5c9c0f1041 GIT binary patch literal 593 zcmWG{^fYv3WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WCaW#Ad|364EBSQd? z^a0|H_xKiC0_hhLQXhXjc+2o`h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WM!CY0Ca0fVo`c3Fl6#db8>1KfB_lq8Dz)^q?s5P7&xGq8A!tb z3y|^W|Ns9yP=PR328J+EAjgD(AvBhOVdXRihJU*m7*^b3Uhy1oa;p!6Smde!Edq3IvoXcZb>1nr@-+5L9$*UMVs?>Vg z&e5<)Q}vi)&+|}~NRFbdO75$_K53h7mKE}8rt+~xob7tAf{oUG-NtY?;{1Yz9vfDD zU-BWalmCa!f=hM*wz3MWJ40tyHC5JYmKt3ts413`oE!d%>+@5!mw`P;W$r4u-}mgT OYS=I8rlgjZe-r@A=)rRU literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/integer_spellings/L1_layer_minus0.wai b/wai/staged-conformance/cases/integer_spellings/L1_layer_minus0.wai new file mode 100644 index 0000000000000000000000000000000000000000..7420755ead8798091cd9f154b5ed08393a365635 GIT binary patch literal 592 zcmWG{^fYu~WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WTk5WbZSXrQFee$Ap0)G?sy32>uAfe;FA5{|6~%WC#F~ zK0v(j9^XPsApJr@>f?`x{I)*f>JsOc%GB3;KiT=5%VSyTX}6c(c~%6;s~9}0)Oy;^ z(XdEU^_XJM^H7yYj-stf?yJ8(X`61A74m7O^07sn?Ru|*jn;nM#&9>{{DOrZ8&-W^ z@*%L3|A)h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WM!zMR9ujlr(~5^nv(-`bxC4TdMYrCz+47kkcN8(88QNCCI$uu z4k%^@(lEdRWc>O6|342@AdHoPAxsp=F=1c`jb&h1IgNqg-);tm6}K1|f$G6ZDNWYMf`uO7^zpYQWy2QDqGWGS|Pj)`%@>o`S+U@0co)tmz zDh7`#wVt+fG%V6oJ*L?6JX9r;qiCy=`|7Vx+NPUjg?yT+d~6YCyWXo{qqSeRG2D$f zzhI%qhE?B}dl3jqUtOD!K(3w?DmGzpXMpp`IilrpyhQH$a{8a5_V9!yR VyGri&J$tJf_KUhHsiox~1pt;+#Qy*Q literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/integer_spellings/base.wai b/wai/staged-conformance/cases/integer_spellings/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..455e453b3d98d7eaaf6f0e30f5da67c53366816c GIT binary patch literal 551 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp2H7#SGCJ;Mwcfix4)xg2Qt zTy#fORs|o`{v?>s5Gcd&H zGcd3lF)*;%F);k;2jZCw3_-v!S+RtH;oouwhL!7pcpH$tA4s2IUP{F{tt2i(2+m@RLbyx`JwNTdv*+!62i+=6<#Vfs2bUCRJ*t38_x!h zO)^ujPBy%`(kN`L!3C-OV;b69B;@D18QW|=VrO*bd8W|2hOY*Tl)gVZwqV!2ugB)~ z-rqIj&hAH!hI4-yF8JSAw(>wpknZINZCB@exNbkWw_N}3wzX!@Rtg?EdfaK@^@^^m zFD`MO{HE@`H>Ke#zr%B`X*m}PUAX`1-}4f<$H?l|oBg0lNiR6RbF1WX`!}JbESg?T fJRDaKu^-rzFe`$8>(bWMkuH`|Prb^P3-AH}z;?uP literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/integer_spellings/case.json b/wai/staged-conformance/cases/integer_spellings/case.json new file mode 100644 index 000000000..0c7337fbc --- /dev/null +++ b/wai/staged-conformance/cases/integer_spellings/case.json @@ -0,0 +1,231 @@ +{ + "format": "wai-staged-case/1", + "name": "integer_spellings", + "about": "an integer member of refines written as -0, 1.0, 1e0 or -0.0 is not an integer (SPEC §2): refines-malformed; span null is span absent, and that layer applies", + "bases": [ + { + "file": "base.wai", + "object": "82cdc06e05a3ed9d9856e5d22ebce9567f759adda34ce24900aff98e50cef5e9", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "27d2f728364854312c97a8d99702d522c6fc7a547ad87f3bcfc51ed687f1e21f", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1_layer_minus0.wai", + "object": "a0c9b9ea94e44ba08fe9bca0f90736b2289136309388a3fc4510430ce8c227ff", + "bytes": 592 + }, + { + "file": "L1_layer_1.0.wai", + "object": "27baed2e59910aa7549f88bb9dd43002202880325a1c3d250246edd10a57e4a0", + "bytes": 593 + }, + { + "file": "L1_layer_1e0.wai", + "object": "d76f6e0fe9858408003e11cf2bc3e60a3ada91998714af03737a57476fef7220", + "bytes": 593 + }, + { + "file": "L1_from_minus0.wai", + "object": "cfa01ee27764473b22f701609110f8658e1970bc2da14593189c3d3f917268ce", + "bytes": 620 + }, + { + "file": "L1_count_2.0.wai", + "object": "ae3aa692f44a11fc832395e2455fceadf33660652d61d81bf01a973a803158db", + "bytes": 621 + }, + { + "file": "L1_span_null.wai", + "object": "2070d413cac0b4c5a23aac07a59702dc0d6ba19495009fbabf10cb6324160db5", + "bytes": 603 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1_layer_minus0.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_layer_1.0.wai", + "at_ms": 2, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_layer_1e0.wai", + "at_ms": 3, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_from_minus0.wai", + "at_ms": 4, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_count_2.0.wai", + "at_ms": 5, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_span_null.wai", + "at_ms": 6, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "a0c9b9ea94e44ba08fe9bca0f90736b2289136309388a3fc4510430ce8c227ff", + "envelope": "refines-malformed", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "27baed2e59910aa7549f88bb9dd43002202880325a1c3d250246edd10a57e4a0", + "envelope": "refines-malformed", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "d76f6e0fe9858408003e11cf2bc3e60a3ada91998714af03737a57476fef7220", + "envelope": "refines-malformed", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "cfa01ee27764473b22f701609110f8658e1970bc2da14593189c3d3f917268ce", + "envelope": "refines-malformed", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "ae3aa692f44a11fc832395e2455fceadf33660652d61d81bf01a973a803158db", + "envelope": "refines-malformed", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "2070d413cac0b4c5a23aac07a59702dc0d6ba19495009fbabf10cb6324160db5", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "2070d413cac0b4c5a23aac07a59702dc0d6ba19495009fbabf10cb6324160db5", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "e6cac22a7aa29381012e329ed828f793540034477072603c147ded204e3f3b41" + }, + { + "stage": 1, + "producer": "2070d413cac0b4c5a23aac07a59702dc0d6ba19495009fbabf10cb6324160db5", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "fd114942eff4b23f04c193742486e1a08aafc4d1a78ada722d0bd9ed5a770860" + } + ], + "clip_sha256": "029bb6e4dd92a6ee2f6a4a52ab01beaa2e6959b43833915d40dd808a251aad6b", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/integer_spellings/stage0.bin b/wai/staged-conformance/cases/integer_spellings/stage0.bin new file mode 100644 index 000000000..5f5ade247 --- /dev/null +++ b/wai/staged-conformance/cases/integer_spellings/stage0.bin @@ -0,0 +1 @@ +(8(8888P8HPHPhP`h`hx`x�08H0HPHHhH`pX`p`xˆx€ˆ€�˜@H`H`p¨Àø`x€hx�€��ˆ�¨ˆ¨°Xhp Èÿ¸Øÿ¸ðÿˆ�¨˜˜°˜°À¨°À°àøÀàÿÈèÿÈÿÿØÿÿ˜¸¸°¸È¸ÈÐx�˜Øøÿàÿÿèÿÿ¨¸À°ÈÈÀÈàÈØàˆ °˜°¸èÿÿ°ÈÐÀÈØÀØèÐèðèèÿ °È¨ÀиÈàÈÐðØàøàèøèøÿðÿÿ(8(8@88P8HPHPhP`h`hx`xˆ88P0HPHHhHhp`hp`xˆx€ˆ€�˜@H`H`p``€`x€h€�€��ˆ�¨ˆ¨°XhpXh‡px‡p��Ððÿ�˜¨˜¨À¨°Àh€€x€—ˆˆŸÈÿÿØÿÿàÿÿ°¸È¸ÀЀ���˜ŸàÿÿèÿÿðÿÿøÿÿÿÿÿÈØàˆ °�°¸ ·ÇøÿÿÿÿÿÿÿÿÐèðèèÿ °È¨¸Ð¸ÀàÈÐèÿÿÿàèøèøÿèÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/layer_gap_on_arrival/L2a.wai b/wai/staged-conformance/cases/layer_gap_on_arrival/L2a.wai new file mode 100644 index 0000000000000000000000000000000000000000..f9d657b42760eb5f193dd00b4829dc4e3648dc15 GIT binary patch literal 931 zcmXxgd29?(90%}k6`_tUs)wVPXt&yGW@hKwwsBvT5@{L@Le0*+Vc1T$)9u=-CgO-I z{%}@EkRbkXM5|hYQ1|hNP$CHaK_sF@NCa8GbxiV^H^29O@ArH2Dkhf*Qxkv&Zk29x zg`7~pbNO5qGi{xsh;6cJift#%oG?weEEKd2J7-%ZUB>8MoVsAJvAhRRQBo!(&~?%$`uvkfSo&+?O0IMr#F9T}pfY5sCw;33E5MV9zulIr8&*(1R0`tEB zu^;3YiP6O%0a1F#jVaoFYsuGMi791so*!hDc?+s`Hm&%$J(TfddvS65x8^g?_=iei zT1EPkd&Ns$XWkp`UA^d4V$;O#*1TDwCwg@5c;&$fd=sS9^Ez+N;fh%~hq=+i21F*{!pN_3&(Pr_?w%rXRK{ zOFKWc@)y4M**f9!vF6C@{%bO}l!X|Wob#*yL~G>M*hHwi@nK%$3)J5o_BjhdvSi&^LXFi D8xs#G literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/layer_gap_on_arrival/L2b.wai b/wai/staged-conformance/cases/layer_gap_on_arrival/L2b.wai new file mode 100644 index 0000000000000000000000000000000000000000..7ade8c437b605c1c989ba2663371f62160e074d5 GIT binary patch literal 935 zcmXxgTWl0n7zgnGQc~!JvV@{QfhL1uTHAGQJ2P7nF2#g^4+LW@v06R5bEXVzXP4== z*wj8Kgpz;{n)m<@6idKpR}9f=)W(L=mIQD~_404M$cm(&_H#YVX35yS5-}EFrsoGz>1$9@nZWgPekSmPcBQFT+3p8!G2IWf zV)%YyXEBl#ostnVk?@2}3fs0FJ#M*{V+zX_re-8krqoipB|XQsQ<6i+HjKEfNyD&I zU8t6(doE9#4P`egxfa)0suS0Qt0h!ZH$>8ML_#wiFJW0J(=)hJ+)ksTQbK*gipZz-T=+0NWaY1uF0eousq|bkIT_I7Q=>F@P-XppZH#M_trS zJ4rEt<@6XmPAllF!*Mg?n}FIEfe8<&3AxX1?)xUscL4bO1Kw|hcm4+W=mIcw4Jh0M zp1TdaKh1uj_)rnN#(&ZJz0skot$eS^>>l;T&z~;OIn8$lzpq=EZ@u)?FOh%V+53vT zUSEN&t+S3a9yz{pG&Z0Qm)6JkMV1ZxaHVwPk26~SUR__=S6Mz*qOX6o^Vny$Qn!z8Y&-bJ^qjT}-R5u3s;~dtvgg(rd1@xUwEq3| zzcY3JN3UK!@m}`hS(~S(^IcD$9UtkRfBxc)ljjaTf5K4z`mWR#5e z4=022ZJ$goe*N_M&0kC`$&TsY#X9ms^RLvdsg0hlnjUW6|JT6HnkVO7Tc_=NqwK9o QSWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1M7#SGCJ;Mwcfix4)xg2Qt z2>Hpt@c%!^1wcmv!5JX#V?F8b2&5AQOm&y@U${0wuH`qMV$M=8n=l7A z6+H=4{q`UGOp;!_vY%D*RO!B}tzi%MA*%wtKMJ+$wYU3dP59wIXWFutf3}^j%NKjG z^wHF>d;D8=lqxhj9_8Tp^Z(e+{c*oij`{Z9ULj-i`maG))-5^K=^wXQi5*x_;&kRS zuf_eWz!%@lPd{jBeV>y3B%gbBnuX`|Sr%VE^lvRIAKlnVjcH>JFva4DWTZ6KdUuUhocGS>J0r_-5hgO8bb7jkJxh5bD4)Qbalx`AU0@Ph=vkAW{UPX*?74QH_@Q8;d*| zpt5DGCc_+&zEvhNzBP7*lm}$hSe436$Wr>hu$wYX(bdS5w4*hrf-+X1j%2h&dC_gU z(k-==a+ngFNtdhA3>DLYJIdu$axM#;EAF_{*liH%-u*avqOU_zW;xOggiX6_g?^iq zX*yV(u89qCib#Yj8i9>HZ@2>h%kx#1!Byjh$G4Hi_wGEv&qb0XPj3tLhXD0wsLugx zR{@qj0DSw7u?KK+1mT+R{Z-*fA7bm$N)@bpOwJeR6K&)88SJ!RX_7{O@4hfAQovDiu#Re=WF&6Sd0jHS86n7XSbN literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/live_multi_base/base_0.wai b/wai/staged-conformance/cases/live_multi_base/base_0.wai new file mode 100644 index 0000000000000000000000000000000000000000..238530e33cb5c543565fc722eaa40c9dcb877b57 GIT binary patch literal 551 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp2H7#SGCJ;Mwcfix4)xg2Qt zyY#Iy<8?}JgfPrC? z5zss{28LK?28Q72Knx6ze@ht{R<2-R_`ecJZvpc60>zI5@o5Hz6*qwNU7*-gAbt&$ ze+R^07#Kpn1J(Zrxd7-$AUFZUJuK_j7%pUF*vgt`xLfjH^)CHrReim<%2e^vw7bel z^SSWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1E7#SGCJ;Mwcfix4)xg2Qt z)Bpef6@g+Pum*^kfQr@u@p={p239r(hA4g@EyTdEK>|oC0Qu@bIRhZujDaEA8p!u% zVDQ_@z_9oL5T9UR_<0(L&jZD-1NrxW_z{r*n1Nx*GX{o$=L`(XUNA5Oy<}kc2gIvh zF)*xt!@%(WKhR2`h`cQ2>N>sD8?Z&~<@uk-RR z{WgK485U=ZROi>;GLw-hmcg74Cz4?OqjmLh@P+3=@Q2l2wpN7Q*RUOx_0c|S)OKG=I literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/live_multi_base/case.json b/wai/staged-conformance/cases/live_multi_base/case.json new file mode 100644 index 000000000..1d5557f8a --- /dev/null +++ b/wai/staged-conformance/cases/live_multi_base/case.json @@ -0,0 +1,197 @@ +{ + "format": "wai-staged-case/1", + "name": "live_multi_base", + "about": "a clip split into two bases of two frames on one clock (the second's first unit is the clip's unit 2, presented at 200): its layer for that unit arriving at 150 with a 30 ms lead is in time", + "bases": [ + { + "file": "base_0.wai", + "object": "8f3a701e2f904ad90663466c2c12cafed3410eeb84ad41db82acdc8f9a10adf7", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0_0.bin", + "stage0_sha256": "e978b07a3d5f6210391269b8dbd31f9febb236fd4a707984b99b54d79a684083", + "decode": { + "kind": "int", + "set": {} + } + }, + { + "file": "base_1.wai", + "object": "9e4ee50a011204c10f0538b8c89fdd41da51c85d970a6a54d850c55c255d5486", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 2, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0_1.bin", + "stage0_sha256": "192742f69c3ee15dda4e25602a57ff43de8beb06d0c49b3374b5750938b561bb", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1_second.wai", + "object": "8332309f88f11b520341c80edae6626e5c61ef2b332b12c6fdba4d7c79871504", + "bytes": 535 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "live": { + "t0_ms": 0, + "unit_ms": 100, + "target_latency_ms": 0, + "lead_ms": [ + 30 + ] + } + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "base", + "index": 1, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1_second.wai", + "at_ms": 150, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [], + [ + { + "object": "8332309f88f11b520341c80edae6626e5c61ef2b332b12c6fdba4d7c79871504", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "8f3a701e2f904ad90663466c2c12cafed3410eeb84ad41db82acdc8f9a10adf7", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "bf96922af8a85885f0a6f2f1b7ed952f1b0e42df04674d974b6b6ddcb0e1a5ae" + }, + { + "stage": 0, + "producer": "8f3a701e2f904ad90663466c2c12cafed3410eeb84ad41db82acdc8f9a10adf7", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "cbad99676ac303b94115b0ab3759eae1e77417326c49ab440d3a7455c8b773b8" + } + ], + "clip_sha256": "aebc5f02725a6b51a50ed4f8eb29793bc14548f0e5bb79069aa1338c6eada40a", + "n_units": 2 + }, + { + "units": [ + { + "stage": 1, + "producer": "8332309f88f11b520341c80edae6626e5c61ef2b332b12c6fdba4d7c79871504", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "610c949f127f349e273a3f986f0b723f1091e40d72a866dd82e8687c5d54e4dc" + }, + { + "stage": 0, + "producer": "9e4ee50a011204c10f0538b8c89fdd41da51c85d970a6a54d850c55c255d5486", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "4660d70e9c4b1b14c317c496ea606437e2299dc9cf30b36c59b4825889990b48" + } + ], + "clip_sha256": "892935ee985334efbada8760d05d321b5f64e651ddd872af614885a21f487cab", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/live_multi_base/stage0_0.bin b/wai/staged-conformance/cases/live_multi_base/stage0_0.bin new file mode 100644 index 000000000..9df24ab99 --- /dev/null +++ b/wai/staged-conformance/cases/live_multi_base/stage0_0.bin @@ -0,0 +1 @@ +((@(8@88P8P`PPhP`x`px 8808P8HPHP`X`p`hp`€ˆxx�0HXHH`�ÀàXhp`xˆxxˆ€ˆ�€�¨HXp Èè¨Øø¸Øÿ€ˆ�€�¨�¨¨˜¨¸¨Èð¨àøÈàÿÈèÿÐÿÿ˜°°¨¸À°¸Øxˆ�ÀðÿØðÿàÿÿ ¨¸°¸Ð°ÈظØèˆ˜°�¨°àÿÿ¨¸È°ÈØÈÐØÐØèØðø˜¨À°°È°¸ØÀÈàÈÐðØàðàðÿðøÿ((@(8@88P8P`PPhP`x`xx(8@08P8HPHP`X`p`hp`€ˆxx�0HXHH`H`h`hphxˆxxˆ€ˆ�€�¨HXpXhp`x€px‡Èèÿ€�¨�¨¨�¨¸`hx`€€€€—ÀèÿÐÿÿØÿÿ¨¸¸¨¸Ðxˆ�x�ŸØðÿàÿÿèÿÿøÿÿøÿÿ¸Øà€˜°�¨° ¯·ðÿÿøÿÿÿÿÿÐØàØèø˜¨À°°È°¸ØÀÈàÿÿÿÐàèØðÿðøÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/live_multi_base/stage0_1.bin b/wai/staged-conformance/cases/live_multi_base/stage0_1.bin new file mode 100644 index 000000000..19b1b4a99 --- /dev/null +++ b/wai/staged-conformance/cases/live_multi_base/stage0_1.bin @@ -0,0 +1 @@ +((@(8@88P8P`PPhP`x`px 8808P8HPHP`X`p`hp`€ˆxx�0HXHH`H`hXhp`xˆxxˆ€ˆ�€�¨HXpXhp`x€pxˆ€ˆ�€�¨�¨¨˜¨¸`hx`€€€ˆ˜€ˆ¨� ¨˜°°èÿÿ°¸Øxˆ�x� ��°  ° ¨¸øÿÿøÿÿÿÿÿˆ˜°�¨° °¸¨¸Èøÿÿÿÿÿÿÿÿÿÿÿ˜¨À°°È°¸ØÀÈàÈÐðÿÿÿÿÿÿÿÿÿ((@(8@88P8P`PPhP`x`xx(8@08P8HPHP`X`p`hp`€ˆxx�0HXHH`H`h`hphxˆxxˆ€ˆ�€�¨HXpXhp`x€pxˆ€ˆ�€�¨�¨¨�¨¸hhxh€€€€˜€ˆ¨ˆ  �°°¨·¿¨¸Ðxˆ�x�˜��¨˜ ° ¨¸°·Ï°Ç×·×瀘°�¨° °¸¨¸È°Ç×ÇÏ×Ï×çÿÿÿ˜¨À°°È°¸ØÀÈàÈÐè×ßïÿÿÿÿÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/rate_downsampling/L1.wai b/wai/staged-conformance/cases/rate_downsampling/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..67f67c5e4eeef0453ab3c6887f9bc338432e7cda GIT binary patch literal 16931 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 b?|0K+l}{wp5s=282r3BSp>={SS=IjnbbUs& literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/rate_downsampling/case.json b/wai/staged-conformance/cases/rate_downsampling/case.json new file mode 100644 index 000000000..69d041329 --- /dev/null +++ b/wai/staged-conformance/cases/rate_downsampling/case.json @@ -0,0 +1,99 @@ +{ + "format": "wai-staged-case/1", + "name": "rate_downsampling", + "about": "a mono FLAC base at 48 kHz and a rate layer to 16 kHz (rate 1/3), the predictor reading every third sample", + "bases": [ + { + "file": "base.wai", + "object": "2499eed2f0e6b6b25873cc52c269c93962d485a5d54c860e280a86e6fd2b9248", + "decoded_via": "wai.audio.flac", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 480, + 1, + 1 + ], + "elem": "i16" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "acdf62f8c667f5a5fe32a592d610af6a1f4fb52b9d8958dcc753809f8d43fdb3", + "decode": { + "kind": "flac" + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "bba602e3e4512eda8c140010a0b044f8ae05f4a616426c76cf249a230e0afe79", + "bytes": 16931 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "bba602e3e4512eda8c140010a0b044f8ae05f4a616426c76cf249a230e0afe79", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "bba602e3e4512eda8c140010a0b044f8ae05f4a616426c76cf249a230e0afe79", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 160, + 1, + 1 + ], + "stage_sha256": "4c867e1b112dc3efdeaea8282058d2f5dd72faea0024c89164169327ef05db86" + } + ], + "clip_sha256": "c4edba1107169547cc6876a1b46a8a7526a29cc1ddac6698a92b5db51d0a902a", + "n_units": 1 + } + ] + } +} diff --git a/wai/staged-conformance/cases/rate_downsampling/stage0.bin b/wai/staged-conformance/cases/rate_downsampling/stage0.bin new file mode 100644 index 0000000000000000000000000000000000000000..dfa66518b491320b137723a5e2788fde0162afa3 GIT binary patch literal 960 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 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/rate_geometry_mismatch/base.wai b/wai/staged-conformance/cases/rate_geometry_mismatch/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..5ee396a2374541db51672f589487ffa9d86eaf61 GIT binary patch literal 1825 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 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/refines_against_base/base.wai b/wai/staged-conformance/cases/refines_against_base/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..9d9e0ec89d9d8bf227b9a7fdf2e3a74cf7903b95 GIT binary patch literal 555 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1M7#SGCJ;Mwcfix4)xg2Qt zjAM51H(oe28KTqfp{hZLl7`b{w`!-2wu*>@NYc>!^#~%d;rKk2^6~sq^|?gu~Q$-7Q8UK{_riQRP5EcEN9;` zFE0MKrKj)b5%&9Tud9okpG?^~VMWk=A1+%>&a0pH@UPLDn{BGFfsJ8N9J}^~rb{|s iI|Ulgaa>EvdAH=C%re1sD?V;liuZP^zO!Tg;hg{@^}s3s literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/refines_against_base/case.json b/wai/staged-conformance/cases/refines_against_base/case.json new file mode 100644 index 000000000..7344e0a51 --- /dev/null +++ b/wai/staged-conformance/cases/refines_against_base/case.json @@ -0,0 +1,140 @@ +{ + "format": "wai-staged-case/1", + "name": "refines_against_base", + "about": "refines well formed alone and malformed once the base is known (step 2): a span past the base's two units; runs summing to three units with no span", + "bases": [ + { + "file": "base.wai", + "object": "fe839f7653007183b35585d43b919af2cdfa0a3c1110aef73ddd15d36a869a5a", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "f50556c9e08ba2dda6d2c916489cc5743002f312dfddea52289eb453a71248ab", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1_span_past.wai", + "object": "36a67289284c64e45a2756b0738443742d4bcfad352e6578b7ab199fc195d41c", + "bytes": 619 + }, + { + "file": "L2_runs_past.wai", + "object": "4b4b6a7559c238929a00fcaae6128b076a2c4cc353a996262bfd4bec5465d12b", + "bytes": 706 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1_span_past.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2_runs_past.wai", + "at_ms": 2, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "36a67289284c64e45a2756b0738443742d4bcfad352e6578b7ab199fc195d41c", + "envelope": "refines-malformed", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "4b4b6a7559c238929a00fcaae6128b076a2c4cc353a996262bfd4bec5465d12b", + "envelope": "refines-malformed", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "fe839f7653007183b35585d43b919af2cdfa0a3c1110aef73ddd15d36a869a5a", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "59061f33d463b93cff6bf3545709b95b189a3fc9b445e1c251a0d2e9122eeca7" + }, + { + "stage": 0, + "producer": "fe839f7653007183b35585d43b919af2cdfa0a3c1110aef73ddd15d36a869a5a", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "41bd3a0dca28cec04468df0253c78c18e97c437bb37afd08f3148802da09ae06" + } + ], + "clip_sha256": "b1338ed308113d939238b0e97c91901908731b1c2a599bf010b688d4e30b383b", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/refines_against_base/stage0.bin b/wai/staged-conformance/cases/refines_against_base/stage0.bin new file mode 100644 index 000000000..c9cc32420 --- /dev/null +++ b/wai/staged-conformance/cases/refines_against_base/stage0.bin @@ -0,0 +1 @@ +00 0H0HH@P`HXhX`pXppp€ˆ(@H8HP@XhX`hXh€`xˆxˆ�ˆ� HXhX`h Àø`pˆpxˆ€ˆ�€�¨�¨¨Phx Øø°àÿÀèÿ€�¨ˆ¨¨˜°¸°°À¨Ðÿ¸ðÿÀøÿÈÿÿèÿÿ¨°¸¨ÀظÀØ€˜ ØðÿØÿÿàÿÿ¨°Ð¸ÀÐÈØàÈàè� ° ¸¸øÿÿ¸ÈÐÈÐàÀØèØàðàðø¨ÀÀ°¸ÐÀÐØÈØðÐèðàøÿèðÿðÿÿ00 0H0HH@P`HXhX`pXpxp€ˆ0@H@HP@XhX`hXh€`xˆxˆ�ˆˆ HXhX`hX`€`pˆpxˆ€ˆ�€�¨� ¨Xhx`x€p€‡xˆ�Èèÿˆ ¨�°¸¨°À`p‡p�—x˜—Èÿÿàÿÿðÿÿ¨ÀиÀØ€� ��§ØÿÿàÿÿðÿÿøÿÿÿÿÿÈàè� ° °¸°·¿ÿÿÿÿÿÿÿÿÿØàèàðø¨¸À°¸ÐÀÐØÈØðÿÿÿØøÿèðÿðÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/refines_rules/L1_one_run.wai b/wai/staged-conformance/cases/refines_rules/L1_one_run.wai new file mode 100644 index 0000000000000000000000000000000000000000..0c4854bb55977c6c5614c042ee8db6a31a36ebd6 GIT binary patch literal 612 zcmWG{^fZiNWMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_(wEy>t2%`nX@EzLO1$T%s{#5m17HO;`lz{Jqp+{D1b$k@Wn#N5on)WF0d#S-Y+ zf}+$iC9CLaAR|AeG&!}1I>sjFmx2Pus5VwdDJQWKWUL|33nhs~>8U{RywaSU+CK~o z4B?(ZhK#^)W&*M~pqLp*!vG7A@#p{l|2$BEFrefb0R{#}0|tiGVGIl_ConLq*u}uG z`~d?)&`$=2;QtH^|A82&g24w!g76Xk*tat|85lxZ&sX_vJ^Z0qO0=ftp?$;FxsB`c zvR^N5zFPkAyu(cg##8J6HBHZ0t-7mVi{=%RplzQ`l;!1)P5QF<fI|ghfB*Zxwq`} ex1P2qELI&7{NX>`t|qw2ConBjU9_%{?=t|w7|W6X literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/refines_rules/L1_other_producer.wai b/wai/staged-conformance/cases/refines_rules/L1_other_producer.wai new file mode 100644 index 0000000000000000000000000000000000000000..a4d3c092994bffabf9754467f160fc66f0f3ef20 GIT binary patch literal 587 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_(wEy>t2%`nX@EzLO1$T%s{#5m17HO;`lz{Jqp+{D1b$k@Wn#N5on)WF0d#S-Y+ zf}+$iph>9|0-#AbiIu5EN>+wImzE?JrKbYJC9gCmr}hs614FoHkRc;5IGKQK4k%^@ z(lEdRWc>O6|342@APgwEMu35V(SU(rbr=J~$_We%D|Rt3EPuej5cHFQA^1N7!+#(K zs$lQ|k|2D9KlbfRP6mdM*7H?e1W0Srt{`lndqKCzGOP&4Z@bzvp1e&>H{>Jtm=k0#M zA@=U#r?<>_wlZ-3SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&LppiH89l7Xp(S!#+=l3}u; zrAdlWT9UD4nqit*TAFd1k#SO@iE)~FYMOz8fr+8Hxru>=k+FrDiMg4Dsey?_iY3ss z1x2Z4N>w{2(AZQ80npgw{8CWB7}o00%;Z=drJTe{kb{kYfm4!Ll%5K- zF|RZyr}hs614FoHkRc;52AP0t4k%^@(lEdRWc>O6|342@APgwEMu35V(SU(rbr=J~ z$_We%D|Rt3EPuej5cHFQA^1N7!+#(Ks$lQ|k|2D9KlbfRP6mdM*7H?e1W0Srt{`lnd zqKCzGOP&4Z@bzvp1e&>H{>Jtm=k0#MA@=U#r?<>_wlZ-3SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&LppiH89l7Xp(S!#+=l3}u; zrAdlWT9UD4nqit*TAFd1k#SO@iE)~FYMOz8fr+8Hxru>=k+FrDiMg4Dsey?_iY3ss z1x2Z4N>w{2(AZQ80npgw{8CWB7}n~bm`nu+LrpfSjnz@gNvs4p*hojI zxF9hP6fkK;`MF9~25<)$)dB;qB(W$x73iA0(wv;yKMV{E;hsT;jKJ7s0EdJ z01J@u=l}ozJWzo!pyV0>1_nk028PvP3=AtLFfgpx#lW!q0RuzOPX>nI{|pTOff%TQ z!3Ri!@Dcvlw=+2z7(!alSNUx{{GnJ%w5H~veZ$tdjqCEVUoURHTK@68!%YXqQ|tdV zP0v`Zx~pJ|<`t8mZJ$k)<>ih|`m*@rlhcbH7S}Cx_M5}kyUh@2=8pLr+k2e1`vr&C zyNjRRGUM6G!1SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1M7#SGCJ;Mwcfix4)xg2Qt zI@7Ubr=}f zjTjgr>{OF05-0Tb4nm2u0vd`?!=F;!4RAc(Li!*TEO4OYF zVW+0gufOkKO*s(U(;D;l zrm#uG!a@j&krOKkgh2cYL@S#ViT^-)L3o)p=0C`?!^}6|$H&anpHK7o0zjWF3d3rQ zmjYH~E!0L3>KTnGVPiA288sZX6Qe|iUX*G~<{iH28i%bLzR#3s3uzLQEs>fmOE(rJ zG9YEcSY(4DQ>oP^GpRMU$!dMJV666XvfyV;qH{5+V$>tf(MTK^e;bZYuAb{nIV|Ghg@T<3W{aoZS<1H66+Fbn}^ z=K%6GfP*gpuP!OB2|~?>wlV{62d8>Egp9c3R1|FDKKZ q&#SW8U;g?mdi(4C!sgD#`=8&Go7%q`ZhZgoZvEcj^~K@dS@;Lsccu3L literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/refused_on_release/L1_f1.wai b/wai/staged-conformance/cases/refused_on_release/L1_f1.wai new file mode 100644 index 0000000000000000000000000000000000000000..817f2bf82cf98407913d60a0d20cad16f5defa4c GIT binary patch literal 535 zcmb_Yy-yTD6n_^nq0od{V<1ysE<4L**F7sbFF_L^go1+1$Ggkm-t67p-obHRWhLZj zNN7y7u|QE9V?n})g%ubR?Nw?L3JMAm!us}<|G>$7yx&KDdGqEb%6uUQ&}B=)uoB}1 zhviufwGo8cMq@5?EOO}tIIbiYfpECiK~ejT z8&rhG!1p{Qgy(5Ft{h)EF1p&6GLW9D94sp8wjN{q|0IQ?gE1@dJZsj4&AP0_QEfn| zsFjV0_vjaih^t7zw!*OY4gluL(>zC`rVYdY&JB96*ANw#X`0@;CbD7-VA}ix~bSM3kCH-EoATHihYQvUICZDfD@v+y65?&V*O x_BT7#mB!b{clgxl_d;W@A3ZSWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp2H7#SGCJ;Mwcfix4)xg2Qt z)Am<;obvM^`z#Hn<#Lbn5tiyT8+#4*g|3 zVX(IJ@w{1^-Hv@0VcT*kSxo6(g3FS1`RNmXyL9h5*Y5Y{!Rgk7HRXIwch`K{@RqA7 zh|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_v4CDk}3CCxm^D9PB+*vQb*G%+Q`G}$;MDJ|K;*w`@D z#LzO)BFV@Ehz!kv&MGKMEu)S}If<32MM_qNK$n&z7Nw^GLoTm0C#RMH7|`LKL57S# znu&pdfdh(}fiw)T02zP&|NqYe6<}mzU|1sq#1=pt!N3sO%fRq|H3P%Hs|*amUxD~P zkPpNRK*Ixoqz@23Jg%cZ0Z6m^+SGNN`8P{%-}i|}e+K<-vOX&@rN;el(yB5As}Ihm zK0juo$5)~-yZ9Fl-Rjp`ggyL0sO(q+kHy4_f-A8c;koZssD$*&TD6= z3tn^4bt&(K!d>^}IM-+J2i^^DeY#gA&7%SU8;8>P literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/step1_refusals/L1_no_refines.wai b/wai/staged-conformance/cases/step1_refusals/L1_no_refines.wai new file mode 100644 index 0000000000000000000000000000000000000000..7cdd60f5d288a4ad96fa96f547b588f8f2df34dc GIT binary patch literal 421 zcmWG{^fa8wz`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1K7#SGCJ%bDxfix4) zp&U@m45VRz1<3gG|Nnm;r~o4y1H&2_AhrPF2nL4GUIvE$s~H&nU1eYh{tCqZfqWol z02&?uBz=JR;c*@P2|$|N*QT!H%)eQB`@T;+`ZMTvll57NDK+kYlU9`}SbcCd_4zT= zHF-+twb=s4?o>~@{`Oecqr}b))4%&|4B!t=-tJSXy{GE$#T!3NPyIjibzVC|UGSQV zu1k3@6z;k&$GJX(Kk#mN>(jj=JGr(jkk@}6F0lRWUA^r*Ar`U)3y$q9Gw+|0(VFPU T_h33h78iHeM-OM0Y918;;9Qjx literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/step1_refusals/base.wai b/wai/staged-conformance/cases/step1_refusals/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..cd6fedce9fced01fa6765c486e4a07f9f36c0753 GIT binary patch literal 555 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1M7#SGCJ;Mwcfix4)xg2Qt z48eE@&Feq>pZC!=M)&YDp%)(N*QUo-oL(CKRN(HsJH3D3 zu>Jnkmiqm&fzbbzyY}mUv!6S4w_)t3qp$u6F8?yGb@L z2WJx`E`{S&YA%*5XQCv~tVR-919avCKw<7#8M(h9RES)~fIWG>@U9NclqEc)z{%=_fO#wwp; z5ULm{TD7R*lUbsXC95@Rf$^FIQP62wd_2?gNxCGBQJo-aMI&R01`Xq>p))262H&eS zBqjS!Ymr&a|4A|lb|!NvJe|}k$YqS~#awMN)lSeg7G@vj6$%CX0IXX>N%i03PlGwH z*UPUpZcyzcZqkA-5|0D`-GRW-AfP7%I2KNI6VnniX3wQ}Y0SkS7oO4tc(eK09bimctTv&f-EL%UbJ<9oJ8_Vh3 z_x{YnmVo|@kcli)`OwTK)W4`4IGsx^QdRZJQpRx;y@CW2)$i zds4S6Z^NG3c8}q;=#F%Z^m%sp#q~vdbnP#POCGN{HLBrJ1yXY}MowIORUkKp#WW8z zTp1ej-f_AYm75E`JW$@Z=UjNyk*HKT=Q<->Jg$PS@t);-w$bUZv~)b2Dh#yQiSWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1M7#SGCJ;Mwcfix4)xg2Qt z#D!7^2mISeJoeqX7d0 zy9v-d3n1;nzz{s0fnmiw28O>&f%FO>y&j0S0@-_j_!vZ^KZcb(V@q1i&L zT8~xNNo}_A@|!TrslHD&S9*b|*sFO<>%D?EZSc78E}JLlr+&(>^~oz1?$nv8{&VB} zlK*c7-1ceL@ZMlI>AAN@R$yiJ!!xJWT+Ucg;ZptQufFD)@4+{p*?o}Rqu)?@S#p8V zn;)MOzWn?4+vn+;YX!{QHX*(&RX&WSvm&E3Ps*RWdf-ma$%fh;3}@sU1caVi8|;^u fANngS@082=X(!D1j(5E{Ze-kN{e059HLCgm)#t$@ literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/up2_geometry_mismatch/case.json b/wai/staged-conformance/cases/up2_geometry_mismatch/case.json new file mode 100644 index 000000000..b75885678 --- /dev/null +++ b/wai/staged-conformance/cases/up2_geometry_mismatch/case.json @@ -0,0 +1,127 @@ +{ + "format": "wai-staged-case/1", + "name": "up2_geometry_mismatch", + "about": "an up2 layer to 8×8 reads 4×4 frames; the units are 8×8: geometry-mismatch", + "bases": [ + { + "file": "base.wai", + "object": "40d319e7fe9030ae136da54e22e418583f06d4dfee7b3e15835f9ab9ab94b89e", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "01fb56ae4042c285da793fa207694e3e1fe9a3b79b3904ebb0fb20ac95744267", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "b07672a2d4466e54d919865cce12d8fa5d5aeca2625491f1408e47ef8945698e", + "bytes": 807 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "b07672a2d4466e54d919865cce12d8fa5d5aeca2625491f1408e47ef8945698e", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "geometry-mismatch" + ], + [ + 1, + "geometry-mismatch" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "40d319e7fe9030ae136da54e22e418583f06d4dfee7b3e15835f9ab9ab94b89e", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "09259e3aa9a258bb6b236c91ef21fa633699fcc88651e2030f4a48e438a092bf" + }, + { + "stage": 0, + "producer": "40d319e7fe9030ae136da54e22e418583f06d4dfee7b3e15835f9ab9ab94b89e", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "fe52ce42109cdaf3aed41625a482dd31869b5125eb7a13fe85b6338fcf5a5445" + } + ], + "clip_sha256": "ef4e67d1cc2b756fb5221906fa60f561072ecc606f9b0c1f7002672814d22519", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/up2_geometry_mismatch/stage0.bin b/wai/staged-conformance/cases/up2_geometry_mismatch/stage0.bin new file mode 100644 index 000000000..3d451e832 --- /dev/null +++ b/wai/staged-conformance/cases/up2_geometry_mismatch/stage0.bin @@ -0,0 +1 @@ + ( (88@08H8PXHXXHXp`hx8@08H0@X8P`P`h`hx`pxpxˆ0@XHPX�ÀèXpxhx€px�x��ˆˆ¨HXh ¸è Ðø°Øøxˆ�x˜ �˜¨�¨°¨Èø°Øø°ØÿÈèÿØøÿ�¨¸ °¸¨ÀÈp€�ÀðÿÐðÿØÿÿ ¸¸¨¸ÈÀÀØÈÈà�˜ � °Øÿÿ¨°È°ÀÈÀÐàÈØðÐØð˜°¸ ¸È¸¸È¸ÀØÈààÐèèØèðððÿ ( (@ 8H08H8PXPXXHXp`hx8@08H8@X8P`P`h`hx`pxpxˆ0@XHPXH`pXpxhx€px�x��ˆˆ¨PXhXXp`p€px€Àèÿx˜ �˜¨�¨°`h€hx€px—ÈèÿØøÿØÿÿ ¨¸¨ÀÈx€ˆx��ÈðÿØÿÿèÿÿèÿÿÿÿÿÈÈàˆ˜ � °�§·ðÿÿøÿÿÿÿÿÀØèÐØð˜°¸ ¸È¸¸È¸ÀØÿÿÿÐèèÐèðèðÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/wir1_faults/L1_bad_magic.wai b/wai/staged-conformance/cases/wir1_faults/L1_bad_magic.wai new file mode 100644 index 0000000000000000000000000000000000000000..fccb296880c19822e01ea90a0579021400bb18b9 GIT binary patch literal 587 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_4w8k!+M^2y|^h zQEC}=Ov*{DOf6EfG6cG`B(W$x6&Nmgr8zmZe;61TB0Pf(8G*sc1Y~nSF*A^c0Tv+R z&;S4bd7uKTfdXOTK#m0i!~b{&hJP~|7*_0HVEB8Vfg$)a1H;OHK=J<|#f%I-KoW%A zHr14Ksc-$-c}hU?lT_9F zf>(9L=3aPq;+9_dwiioGR~gr69@-zI;I#a%h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_4w8k!+M^2y|^h zQEC}=Ov*{DOf6EfG6cG`B(W$x6&Nmgr8zmZe;61T!aaiw8JQRufQW$uikX2l46pzh zfBygf&jS@$4HO6y2XZVJ82-mIF#Mazz_4Nm1H<3@3=F}a85man1B(9#DQ0Bw0g@o> zwyCC^ONEOu@~D+dwAyKo<3h7GzPY#JW%RDqM~?Eah|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_4w8k!+M^2y|^h zQEC}=Ov*{DOf6EfG6cG`B(W$x6&Nmgr8zmZe;61T!aaiw8G*sc1Y~nSF*A^c0Tv+R z&;S4bc^H6-fM7LHAWR&{uwY>LAJ4$>Zzco7iX99LfA2Fe1b=2=Soseq{vV{0k--N@ zg0S1BnsP1`F2=~CRxZ(Mr#X%b&D!|p-inveyH+1L%EOj7X|_zG-u=s^N`Ed-2}pjD zs(N4Ws;=1F3(rp6(ktKgVu|T0;~LFF`-2pmmfw}!y8p^*-(`&R^HvrZ$*yYkxy~7U zz@w?{0*n2$bU)!0h>3tAjK4*hYOkGEcncu}QtrozjXD@6}R^ LGdJy=Ue*o((q_X< literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/wir1_faults/L1_too_large.wai b/wai/staged-conformance/cases/wir1_faults/L1_too_large.wai new file mode 100644 index 0000000000000000000000000000000000000000..b89a8252a8e25b03fbbfb23cafb10ad7bb18ae04 GIT binary patch literal 587 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_4w8k!+M^2y|^h zQEC}=Ov*{DOf6EfG6cG`B(W$x6&Nmgr8zmZe;61T!aaiw8G*sc1Y|QPKrl0i4FxPf z`p^IW|9PPN)j)wTaUjQnf#H8V1H-?W3=AuFFfjbR&%hA;nSo*DKcM)3kYYv#A0P?B zZkuY#xm36qBad3SM5~?VI4(46h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_4w8k!+M^2y|^h zQEC}=Ov*{DOf6EfG6cG`B(W$x6&Nmgr8zmZe;F7U!aaiw8G*sc1Y~nSF*A^c0Tv+R z&;S4bd7uKTfdXOTK#m0i!~b{&hJP~|7*_0HVEB8Vfg$)a1H;OHK=J<|#f%I-KoW%A zHr14Ksc-$-c}hU?lT_9F zf>(9L=3aPq;+9_dwiioGR~gr69@-zI;I#a%h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_4w8k!+M^2y|^h zQEC}=Ov*{DOf6EfGSpEjE=bG+xiYONKUc}h0OYY!P%s(R0)0`ESd^X$l*CKpF;EfQ&!?|NrNK3akbSgoy(=77Psk;~5zK&17I$v4er( z?|lY_;Li*UEB^t-|AQ1WGWY;V5O&*CQ_iKr#Ta?i$|YLuG{CfdU0m)BNRqqR4)fJn2;n|5>dga?*EHPbWT%&nte~^OH^1G5- z_g`7+LV6mT;?kBvW{6fUesHcHx7u@H|KRL90b#TTW e+o+F6=85+_HmNtdQ(95ty;^E<=BAy~%h~}w=gc7h literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/wir1_faults/L1_version.wai b/wai/staged-conformance/cases/wir1_faults/L1_version.wai new file mode 100644 index 0000000000000000000000000000000000000000..9f24d637b9a92a2a9769e50ff06ff41083eade65 GIT binary patch literal 587 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_4w8k!+M^2y|^h zQEC}=Ov*{DOf6EfG6cG`B(W$x6&Nmgr8zmZe;61T!aaiwnSjB`1Y~nSF*A^c0Tv+R z&;S4bd7uKTfdXOTK#m0i!~b{&hJP~|7*_0HVEB8Vfg$)a1H;OHK=J<|#f%I-KoW%A zHr14Ksc-$-c}hU?lT_9F zf>(9L=3aPq;+9_dwiioGR~gr69@-zI;I#a%SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp2H7#SGCJ;Mwcfix4)xg2Qt z!~g&Pm4RX)um*@Dfa)Td85p8~cs&mT!v-K`<6~fm76sB$3=Hft3=FaIK&;BZ5T^;m zx(p1Q20(1Zz_4-x5YJ*@STzTT7Xk4KAYRA75W1Ct;s1UHhLGb74F4_y>FYpzA1L-1 zh+hKLya8JL3CR8ras|)>P?%>_;9(#gV|^0 z?q@4*lVZ6o6z-nzU9VAmeE#eNzPIYmw}0bY`hM|= zHHVL^i8c9@HY?{(!tB|nSOl~dxC?)LQdd}YEu;R(lNCx$8-@OVJ9)S6*OTS@-2RvrVS8y^mhl Yju!n7!uHN94|@4>HZDw2S@S*}08YKZ{{R30 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/wir1_faults/case.json b/wai/staged-conformance/cases/wir1_faults/case.json new file mode 100644 index 000000000..c3bf4eb69 --- /dev/null +++ b/wai/staged-conformance/cases/wir1_faults/case.json @@ -0,0 +1,250 @@ +{ + "format": "wai-staged-case/1", + "name": "wir1_faults", + "about": "step 6, each payload fault once: a two-unit payload over a one-unit span (field_range); bad_magic; version; reserved_nonzero; an element of 2 bytes for RGB8 (field_range); a frame past 2^26 elements (too_large); a byte after the last stream (trailing). Each is payload-malformed with its code", + "bases": [ + { + "file": "base.wai", + "object": "ed5412690ea1fb0774f634a2272f34c95b07b54e5bbebc6b83e801693778c2f1", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "de810c5d3cb16230bbe543b17bfbe39b33d45fd60b3d43ee7c323980a347396b", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1_unit_count.wai", + "object": "640aa8c0b9908c32945648da6f0485c7e0da11519fe9307792222610a521c280", + "bytes": 615 + }, + { + "file": "L1_bad_magic.wai", + "object": "234645d4b51e53af57bc62bb2c67122792c578b1e96f30e1ab5bad042ee7711f", + "bytes": 587 + }, + { + "file": "L1_version.wai", + "object": "ee6bfe4eb4de3d8332d318b766edfa559f2bd1b6f92f8c8b9d80c62071fe9445", + "bytes": 587 + }, + { + "file": "L1_reserved.wai", + "object": "f2ab56f3d89717d2becadbb448e17c4731c7988dbf51405de8a1061bd36a5db1", + "bytes": 587 + }, + { + "file": "L1_elem.wai", + "object": "5a5ac824bc750d07504b78717c76bb688ae00d1aee2e10c07746ead5656f6657", + "bytes": 587 + }, + { + "file": "L1_too_large.wai", + "object": "16abad5a9070fd7be41edb8ed74a22de1693e43e5e4019a9be49a127cf3a2aef", + "bytes": 587 + }, + { + "file": "L1_trailing.wai", + "object": "5a74b598df4caa7010df5b8e64ba96f366225e65fb2588f6bea971b85d7e10b3", + "bytes": 588 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1_unit_count.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_bad_magic.wai", + "at_ms": 2, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_version.wai", + "at_ms": 3, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_reserved.wai", + "at_ms": 4, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_elem.wai", + "at_ms": 5, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_too_large.wai", + "at_ms": 6, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_trailing.wai", + "at_ms": 7, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "640aa8c0b9908c32945648da6f0485c7e0da11519fe9307792222610a521c280", + "envelope": "payload-malformed", + "detail": "field_range", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "234645d4b51e53af57bc62bb2c67122792c578b1e96f30e1ab5bad042ee7711f", + "envelope": "payload-malformed", + "detail": "bad_magic", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "ee6bfe4eb4de3d8332d318b766edfa559f2bd1b6f92f8c8b9d80c62071fe9445", + "envelope": "payload-malformed", + "detail": "version", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "f2ab56f3d89717d2becadbb448e17c4731c7988dbf51405de8a1061bd36a5db1", + "envelope": "payload-malformed", + "detail": "reserved_nonzero", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "5a5ac824bc750d07504b78717c76bb688ae00d1aee2e10c07746ead5656f6657", + "envelope": "payload-malformed", + "detail": "field_range", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "16abad5a9070fd7be41edb8ed74a22de1693e43e5e4019a9be49a127cf3a2aef", + "envelope": "payload-malformed", + "detail": "too_large", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "5a74b598df4caa7010df5b8e64ba96f366225e65fb2588f6bea971b85d7e10b3", + "envelope": "payload-malformed", + "detail": "trailing", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "ed5412690ea1fb0774f634a2272f34c95b07b54e5bbebc6b83e801693778c2f1", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "7142cdba98ba22a116ce4487ffdacc69631c71f9824304efa84f3b5af9927261" + }, + { + "stage": 0, + "producer": "ed5412690ea1fb0774f634a2272f34c95b07b54e5bbebc6b83e801693778c2f1", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "7a0805ea8887f8f73e5fa8725c8c089ec9131cd54b88d41626ae44f7c7cc3dd3" + } + ], + "clip_sha256": "1d1f19c330ee7162da72c4af9c6f543d5faa4592e1afa441fe669e2f8ff348aa", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/wir1_faults/stage0.bin b/wai/staged-conformance/cases/wir1_faults/stage0.bin new file mode 100644 index 000000000..692f7a959 --- /dev/null +++ b/wai/staged-conformance/cases/wir1_faults/stage0.bin @@ -0,0 +1 @@ + @ @H0@P8P`@X`X`xhp€hxˆ0@H8@`PX`XXpXh€hpˆh€˜xˆ¨HX`PXx˜Èøhx€x€�xˆ ˆ  ˜˜¸Xpx¨Øø°ØÿÀàÿˆ˜ ˆ¨¸ ¨¸ ¸Ð¸ØÿÀèÿÀøÿØøÿèÿÿ °È°¸ØÀÐØ€ˆ˜ÈÿÿØÿÿèÿÿ°ÀÈ°ÈØÈÈèÐØè˜¨¸ °Àèÿÿ¸ÈÐÀÐØÈàðÐèøàøÿ¨°È°ÀظÈàÈÐèØèðàøøðÿÿðÿÿ (@ @H0@P8X`@X`X`xhx€px€0@P8@`PX`XXpXh€hpˆp€˜xˆ¨PX`PXxPh€hx€x€�xˆ ˆ  ˜˜¸Xpx`xxhx�x€ŸÈøÿˆ¨° ¨¸ ¸Ðpx�xˆ�x˜ŸØðÿèÿÿèÿÿ°¸Ø¸ÐЀˆ˜ˆŸŸØÿÿèÿÿøÿÿøÿÿÿÿÿÐØè˜¨° °À ·ÏÿÿÿÿÿÿÿÿÿÐèøàøÿ¨°È°¸Ø¸ÈàÈÐèÿÿÿàøøðÿÿðÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/index.json b/wai/staged-conformance/index.json index 84374a036..cb51915a5 100644 --- a/wai/staged-conformance/index.json +++ b/wai/staged-conformance/index.json @@ -255,6 +255,161 @@ "clip_sha256": [ "ac1fa9507d1004bcc1eaaf366da0013acd8c196dd5fdc2448232847348d75058" ] + }, + { + "name": "step1_refusals", + "about": "a refinement-only envelope with no refines (refines-malformed), and one whose manifest names a fallback (capability-not-refinement)", + "clip_sha256": [ + "636def3e3165e6f3ee4d812c793c5c86cb5f10763a37cf86fcf8ed472a9c3299" + ] + }, + { + "name": "refines_rules", + "about": "§3's rules, each broken once: a layer 1 naming a producer other than the base; prev an array of one run; two adjacent runs naming one producer; runs not summing to the span's count. Each is refines-malformed", + "clip_sha256": [ + "27b600a505e95bb2bb0097854d98a42a0c77af1641c9090046412e52dd9fe7b2" + ] + }, + { + "name": "integer_spellings", + "about": "an integer member of refines written as -0, 1.0, 1e0 or -0.0 is not an integer (SPEC §2): refines-malformed; span null is span absent, and that layer applies", + "clip_sha256": [ + "029bb6e4dd92a6ee2f6a4a52ab01beaa2e6959b43833915d40dd808a251aad6b" + ] + }, + { + "name": "refines_against_base", + "about": "refines well formed alone and malformed once the base is known (step 2): a span past the base's two units; runs summing to three units with no span", + "clip_sha256": [ + "b1338ed308113d939238b0e97c91901908731b1c2a599bf010b688d4e30b383b" + ] + }, + { + "name": "wir1_faults", + "about": "step 6, each payload fault once: a two-unit payload over a one-unit span (field_range); bad_magic; version; reserved_nonzero; an element of 2 bytes for RGB8 (field_range); a frame past 2^26 elements (too_large); a byte after the last stream (trailing). Each is payload-malformed with its code", + "clip_sha256": [ + "1d1f19c330ee7162da72c4af9c6f543d5faa4592e1afa441fe669e2f8ff348aa" + ] + }, + { + "name": "cost_over_limit_symbols", + "about": "a sink whose cost limit is 64 symbols: the 384-symbol layer is cost-over-limit (symbols)", + "clip_sha256": [ + "e4561786406a2ad52a46f3814fe516cd07745917fa5150b2b25b49f71218d2db" + ] + }, + { + "name": "cost_over_limit_working", + "about": "a sink whose cost limit is 100 working bytes: a layer that holds its tables, its stream index and one 192-byte frame is cost-over-limit (working bytes)", + "clip_sha256": [ + "08b88ef3b0bd8d6cd033ffe1dc09cb6e95bf6bd592ef7ac497fbf01b99d71fae" + ] + }, + { + "name": "chain_over_limit", + "about": "a chain budget one byte short of the base's units and the layer's payload, declared output and declared working bytes: chain-over-limit, before any stream is decoded", + "clip_sha256": [ + "b139902d21228247e2a7901db2077ca0e346c6f34c398b62be71d4516a1dcdf5" + ] + }, + { + "name": "chain_budget_retained", + "about": "a budget that holds the base and either layer, not both: layer 2, held, is retained, so layer 1 is chain-over-limit; once layer 2 expires and is dropped, layer 1 applies", + "clip_sha256": [ + "9b359fd5c3a4e4212911fdd0c0cbec31d00d46474656b49a655b4ef122bbded2" + ] + }, + { + "name": "frozen_refuses_held", + "about": "a layer 3 held for both units; layer 1a applies, then layer 1b conflicts: the units freeze, and the held layer 3 is refused for them (layer-conflict)", + "clip_sha256": [ + "98e342cc102fba975a75e08ca3e3fdfb83c271443b66516eb7b6c3ad35579c2c" + ] + }, + { + "name": "batch_conflict_release", + "about": "layers 2a and 2b, both over layer 1, held; layer 1 applies and releases them together: they conflict in the batch, and the units freeze at stage 1", + "clip_sha256": [ + "6620d5f7e2d50dc37635c3881cd29a06cf6de2444320e0159a203fc600cc8aae" + ] + }, + { + "name": "layer_gap_on_arrival", + "about": "a bound of one held envelope: layer 2a is held for both units, and layer 2b, which would be held too, is refused on arrival (layer-gap)", + "clip_sha256": [ + "b3b18bbe90363440a4a88eeb7d89efe60201069bc91d67dc9c304f90e5e4cc4e" + ] + }, + { + "name": "held_then_prev_mismatch", + "about": "a layer 2 over layer 1a is held; layer 1b applies instead and releases it: the units' producer is 1b, so layer 2 is prev-mismatch", + "clip_sha256": [ + "83265622ac3f3f335c5d4df9231ba2c4ba27617722a2a9b446084740cee13fb2" + ] + }, + { + "name": "up2_geometry_mismatch", + "about": "an up2 layer to 8×8 reads 4×4 frames; the units are 8×8: geometry-mismatch", + "clip_sha256": [ + "ef4e67d1cc2b756fb5221906fa60f561072ecc606f9b0c1f7002672814d22519" + ] + }, + { + "name": "rate_geometry_mismatch", + "about": "a rate layer to 600 samples reads 300; the base holds 400: geometry-mismatch", + "clip_sha256": [ + "aab0d06a5d8fc29f47bd107940bd88dfd7d588980bbb72df54d5b8e9a3222006" + ] + }, + { + "name": "rate_downsampling", + "about": "a mono FLAC base at 48 kHz and a rate layer to 16 kHz (rate 1/3), the predictor reading every third sample", + "clip_sha256": [ + "c4edba1107169547cc6876a1b46a8a7526a29cc1ddac6698a92b5db51d0a902a" + ] + }, + { + "name": "deadline_held_layer", + "about": "units presented at 200 and 240, leads 10 and 30: layer 2 arrives at 175 and is held; layer 1 at 180 applies to both and releases it; layer 2 is late for unit 0 (deadline-missed) and in time for unit 1", + "clip_sha256": [ + "ee605579ccb95d90c626343ece2fd5e347647f588b73d48ccf01c7432b724bd2" + ] + }, + { + "name": "live_multi_base", + "about": "a clip split into two bases of two frames on one clock (the second's first unit is the clip's unit 2, presented at 200): its layer for that unit arriving at 150 with a 30 ms lead is in time", + "clip_sha256": [ + "aebc5f02725a6b51a50ed4f8eb29793bc14548f0e5bb79069aa1338c6eada40a", + "892935ee985334efbada8760d05d321b5f64e651ddd872af614885a21f487cab" + ] + }, + { + "name": "base_readded", + "about": "layer 1a applies and 1b conflicts, freezing the units; the base decoded again keeps them as they are and reports nothing; 1b offered again is still refused", + "clip_sha256": [ + "11513b7b7c495cc504f4d6deb308d15c95c08aaca41e85479f2359764f49046d" + ] + }, + { + "name": "refused_on_release", + "about": "two layers held for their base; when it arrives, the first is payload-malformed and has that one report, and the second applies", + "clip_sha256": [ + "f7efabb2f5641fb662d3e5fcbcd77c1a1b0106dd91d0f42c2d7a722f455dd1c1" + ] + }, + { + "name": "evidence_upgrade", + "about": "an attested base: a layer held for it with no receipt evidence is offered again delivered-verified, keeps that evidence, and applies when the base arrives", + "clip_sha256": [ + "97edd7b43a75c09b47bed54626f511065c1abba7e3dbfffe7aa3398eca890a7b" + ] + }, + { + "name": "evidence_upgrade_deadline", + "about": "units presented at 100 and 140 with a 96 ms lead: the layer arrives at 0 with no evidence and is verified at 5, so over the attested base it counts as arriving at 5: late for unit 0, in time for unit 1", + "clip_sha256": [ + "f9a7d9010bd6d8b13be120607274ca4ea0f5c57dee5a2fd7c177d293d92918f6" + ] } ] } diff --git a/wai/staged-conformance/wai2/index.json b/wai/staged-conformance/wai2/index.json index 8a72a2db9..9a5c9f400 100644 --- a/wai/staged-conformance/wai2/index.json +++ b/wai/staged-conformance/wai2/index.json @@ -1,5 +1,5 @@ { - "about": "WAI2 envelopes with refinement components (SPEC §7.1): what check_component_roles reports, whether each envelope_manifest matches its entry, the content hash of each component's exploded envelope (null for an entry that cannot be exploded; the others are exploded all the same), and entry 0's refinements. Where `sources` is given, the components explode to those WAI1 envelopes byte for byte.", + "about": "WAI2 envelopes with refinement components (SPEC §7.1): what check_component_roles reports, whether each envelope_manifest matches its entry, the content hash of each component's exploded envelope (null for an entry that cannot be exploded; the others are exploded all the same), and entry 0's refinements. Where `sources` is given, the components explode to those WAI1 envelopes byte for byte. `packs` lists WAI1 envelopes and whether a packer packs them: it refuses bytes around a manifest's JSON value or after a payload, and a manifest that inside a WAI2 entry passes the JSON depth limit.", "files": [ { "file": "pack_live_chain.wai", @@ -235,5 +235,42 @@ "refinements_of_0": [] } } + ], + "packs": [ + { + "name": "pack_trailing_newline", + "sources": [ + "pack_trailing_newline.src0.wai" + ], + "packs": false + }, + { + "name": "pack_leading_space", + "sources": [ + "pack_leading_space.src0.wai" + ], + "packs": false + }, + { + "name": "pack_after_payload", + "sources": [ + "pack_after_payload.src0.wai" + ], + "packs": false + }, + { + "name": "pack_deep_120", + "sources": [ + "pack_deep_120.src0.wai" + ], + "packs": true + }, + { + "name": "pack_deep_124", + "sources": [ + "pack_deep_124.src0.wai" + ], + "packs": false + } ] } diff --git a/wai/staged-conformance/wai2/pack_after_payload.src0.wai b/wai/staged-conformance/wai2/pack_after_payload.src0.wai new file mode 100644 index 0000000000000000000000000000000000000000..548725bc921aa6cfbf8d77b532fe315b712e94e9 GIT binary patch literal 558 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1M7#SGCJ;Mwcfix4)xg2Qt z!~g&Pm4RX)um*(L7#Jeh85mfA3R#307+A#^7^202v1v( zehcJ(0;>B8#6N-Relalo{||Bn(4jzZ0*H+nWXdIqpqaJpm}to383-pjOmt&XfqtCnT$37YfDLicALhxj56#aGMTwe8+& zKI{7F+Japx-0c=_*j~74f2Nby-Brfn>og+|9Jl5wS!nNff79&^eEWH}1-)7CP`~4p z<bh7`;aNmq+{@uCpwBjC=3s k#48vt$*Si0clFykuGC%Yx^7=eQg7njp&ao}w4$;K0HCYD5dZ)H literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/wai2/pack_deep_120.src0.wai b/wai/staged-conformance/wai2/pack_deep_120.src0.wai new file mode 100644 index 0000000000000000000000000000000000000000..dfa4068f335587fd2a02ade31ea217e27298feaa GIT binary patch literal 806 zcmWG{^fa8s$iPsoRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&LpW)s;c5sQd0|*tfB`9#10Us zZ2-njxM!FlBamidU|`@t!yo?t|E~-b1A#Rl%*MbF!OpizDq~fxJ)1qJj^7EV ybjIj4(!V_7=Wv~6;bYu;M<-svcu7_@&%dkR)^VloTGw^^Qj&TT?+)dNccK7$h!~6j literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/wai2/pack_deep_124.src0.wai b/wai/staged-conformance/wai2/pack_deep_124.src0.wai new file mode 100644 index 0000000000000000000000000000000000000000..b600eebb4be4b2dfbbcc05f1c718245e053b7c03 GIT binary patch literal 814 zcmWG{^fX+=$iPsoRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&LpW)s;c5sQd0|*tfB`H#10^+ zZ2-nnxM!FlBamidU|`@t!yo?t|E~-b1A#Rl%*MbF!OpizDq~fxJ)1qJj^7EV ybjIj4(!V_7=Wv~6;bYu;M<-svcu7_@&%dkR)^VloTGw^^Qj&TT?+)dNccK82FB^0K literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/wai2/pack_leading_space.src0.wai b/wai/staged-conformance/wai2/pack_leading_space.src0.wai new file mode 100644 index 0000000000000000000000000000000000000000..26c84998064977a4134dfacda9ca810f761463f2 GIT binary patch literal 556 zcmWG{^fX++z`&qTtyG?vsbr;OsAr(0qm-MPl9>qPm1U-+<^%bec_pcNB|w3q)PkJM z{FB~UfcNIkF-dO!o>bMs3w^YfHy zb(E6x^HMTFoXos*C97(s?999rpmJ12C5c7psX#mPN^^2*8yFcF!ac(b8G$qt(7haJ z_{0DI|CNDaAg~66*%%li*cljDfC^cJ7#LW^7#O0(fwT$(!v-}VTbqGlqX7d$j4=ZP zy9LlZD+Y!*TOf94VE8wif#L6RAYRMB5WE@4-_F3Wau1L`48*4y7*V7dW{QnPf1<;{DZ~};p8Dz>gv}G_&=VVIA5P4a7MfY7utMRFY zQzgYpEH$Rv$?6GNJaD>X8LahVr{2r7d##SFORJV;?FpLm%0l;N9*6iM4#ii?-nH%C zYCh}w>Dq!_E8OiCZrEP9X@91Z*WFdd;p;Rb4;;7VDp_dncYo9E4Sf4~wgtUe?@+(v zl;zL!XI{O2Gf&0&r|t23{&JN!gG5$TWvr^TXS3(j@jKy^&KSK$`j$+}VN>Xp)-Ju-uP80yDA-=}| literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/wai2/pack_trailing_newline.src0.wai b/wai/staged-conformance/wai2/pack_trailing_newline.src0.wai new file mode 100644 index 0000000000000000000000000000000000000000..1bc948cb27aad4744917331c35eb38a9818d23d2 GIT binary patch literal 556 zcmWG{^fX++z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp3A8WV7dW{QnPf1<;{DZ~};p8Dz>gv}G_&=VVIA5P4a7MfY7utMRFY zQzgYpEH$Rv$?6GNJaD>X8LahVr{2r7d##SFORJV;?FpLm%0l;N9*6iM4#ii?-nH%C zYCh}w>Dq!_E8OiCZrEP9X@91Z*WFdd;p;Rb4;;7VDp_dncYo9E4Sf4~wgtUe?@+(v zl;zL!XI{O2Gf&0&r|t23{&JN!gG5$TWvr^TXS3(j@jKy^&KSK$`j$+}VN>Xp)-Ju-uP80yFalWwt literal 0 HcmV?d00001 diff --git a/wai/wai-rs/src/bin/wai_staged_vectors.rs b/wai/wai-rs/src/bin/wai_staged_vectors.rs index ea6b3db5c..c45dc2806 100644 --- a/wai/wai-rs/src/bin/wai_staged_vectors.rs +++ b/wai/wai-rs/src/bin/wai_staged_vectors.rs @@ -1015,9 +1015,254 @@ fn cases(ctx: &Ctx) -> Vec { files: vec![("L1a.wai".into(), la), ("L1b.wai".into(), lb)], cfg: ChainConfig { bound: HoldBound { max_envelopes: 1, max_age_ms: 1000 }, ..cfg() }, steps: vec![offer("L1a.wai", 0), offer("L1b.wai", 1), base_at(0, 2)] }); } + + // ---- §3 and step 1 ---- + let refusals = |name: &'static str, about: &'static str, a: Base, files: Vec<(String, Vec)>| { + let mut steps = vec![base_at(0, 0)]; + steps.extend(files.iter().enumerate().map(|(i, (f, _))| offer(f, i as u64 + 1))); + Case { name, about, bases: vec![a], files, cfg: cfg(), steps } + }; + { + let a = small(101); + let (l1, _) = l1_of(&a, 102, 4); + let none = edit(&l1, |w| w.manifest.refines = None); + let fb = edit(&l1, |w| w.manifest.model_requirement.fallback = Some(CAP_VIDEO_INT_MOTION.into())); + out.push(refusals("step1_refusals", "a refinement-only envelope with no refines (refines-malformed), and one whose manifest names a fallback (capability-not-refinement)", a, + vec![("L1_no_refines.wai".into(), none), ("L1_fallback.wai".into(), fb)])); + } + { + let a = small(103); + let bh = hex(&a.hash()); + let (l1, _) = l1_of(&a, 104, 4); + let other = "e".repeat(64); + let with = |r: Value| edit(&l1, |w| w.manifest.refines = Some(r)); + out.push(refusals("refines_rules", "§3's rules, each broken once: a layer 1 naming a producer other than the base; prev an array of one run; two adjacent runs naming one producer; runs not summing to the span's count. Each is refines-malformed", a, vec![ + ("L1_other_producer.wai".into(), with(json!({"base": bh, "prev": other, "layer": 1}))), + ("L1_one_run.wai".into(), with(json!({"base": bh, "prev": [{"producer": bh, "count": 2}], "layer": 1}))), + ("L2_adjacent_runs.wai".into(), with(json!({"base": bh, "prev": [{"producer": other, "count": 1}, {"producer": other, "count": 1}], "layer": 2}))), + ("L2_runs_not_span.wai".into(), with(json!({"base": bh, "prev": [{"producer": other, "count": 1}, {"producer": bh, "count": 2}], "layer": 2, "span": {"from": 0, "count": 2}}))), + ])); + } + { + let a = small(105); + let (l1, _) = l1_of(&a, 106, 4); + let text = |e: &[u8]| String::from_utf8(wai::container::manifest_bytes(e).expect("WAI1").to_vec()).expect("UTF-8"); + let t = text(&l1); + let spelled = |from: &str, to: &str| { + let u = t.replacen(from, to, 1); + assert_ne!(u, t, "{from}"); + raw_manifest(&l1, &u) + }; + let with_span = raw_manifest(&l1, &t.replacen("\"layer\":1", "\"layer\":1,\"span\":{\"from\":0,\"count\":2}", 1)); + let ts = text(&with_span); + let span_null = raw_manifest(&l1, &t.replacen("\"layer\":1", "\"layer\":1,\"span\":null", 1)); + out.push(refusals("integer_spellings", "an integer member of refines written as -0, 1.0, 1e0 or -0.0 is not an integer (SPEC §2): refines-malformed; span null is span absent, and that layer applies", a, vec![ + ("L1_layer_minus0.wai".into(), spelled("\"layer\":1", "\"layer\":-0")), + ("L1_layer_1.0.wai".into(), spelled("\"layer\":1", "\"layer\":1.0")), + ("L1_layer_1e0.wai".into(), spelled("\"layer\":1", "\"layer\":1e0")), + ("L1_from_minus0.wai".into(), raw_manifest(&l1, &ts.replacen("\"from\":0", "\"from\":-0", 1))), + ("L1_count_2.0.wai".into(), raw_manifest(&l1, &ts.replacen("\"count\":2", "\"count\":2.0", 1))), + ("L1_span_null.wai".into(), span_null), + ])); + } + // ---- step 2 ---- + { + let a = small(107); + let bh = hex(&a.hash()); + let (l1, _) = l1_of(&a, 108, 4); + let other = "e".repeat(64); + let with = |r: Value| edit(&l1, |w| w.manifest.refines = Some(r)); + out.push(refusals("refines_against_base", "refines well formed alone and malformed once the base is known (step 2): a span past the base's two units; runs summing to three units with no span", a, vec![ + ("L1_span_past.wai".into(), with(json!({"base": bh, "prev": bh, "layer": 1, "span": {"from": 1, "count": 2}}))), + ("L2_runs_past.wai".into(), with(json!({"base": bh, "prev": [{"producer": other, "count": 2}, {"producer": bh, "count": 1}], "layer": 2}))), + ])); + } + // ---- step 6 ---- + { + let a = small(109); + let (l1, _) = l1_of(&a, 110, 4); + let bh = hex(&a.hash()); + let one_unit = edit(&l1, |w| w.manifest.refines = Some(json!({"base": bh, "prev": bh, "layer": 1, "span": {"from": 0, "count": 1}}))); + let pay = |f: &dyn Fn(&mut Vec)| edit(&l1, |w| f(&mut w.payload)); + out.push(refusals("wir1_faults", "step 6, each payload fault once: a two-unit payload over a one-unit span (field_range); bad_magic; version; reserved_nonzero; an element of 2 bytes for RGB8 (field_range); a frame past 2^26 elements (too_large); a byte after the last stream (trailing). Each is payload-malformed with its code", a, vec![ + ("L1_unit_count.wai".into(), one_unit), + ("L1_bad_magic.wai".into(), pay(&|p| p[0] = b'X')), + ("L1_version.wai".into(), pay(&|p| p[4] = 2)), + ("L1_reserved.wai".into(), pay(&|p| p[42] = 1)), + ("L1_elem.wai".into(), pay(&|p| p[5] = 2)), + ("L1_too_large.wai".into(), pay(&|p| { + p[12..16].copy_from_slice(&8192u32.to_le_bytes()); + p[16..20].copy_from_slice(&8192u32.to_le_bytes()); + })), + ("L1_trailing.wai".into(), pay(&|p| p.push(0))), + ])); + } + { + let a = small(111); + let (l1, _) = l1_of(&a, 112, 4); + out.push(Case { name: "cost_over_limit_symbols", about: "a sink whose cost limit is 64 symbols: the 384-symbol layer is cost-over-limit (symbols)", bases: vec![a], files: vec![("L1.wai".into(), l1)], + cfg: ChainConfig { cost_limit: Some(Wir1Cost { output_bytes: u64::MAX, symbols: 64, working_bytes: u64::MAX }), ..cfg() }, steps: vec![base_at(0, 0), offer("L1.wai", 1)] }); + } + { + let a = small(113); + let (l1, _) = l1_of(&a, 114, 4); + out.push(Case { name: "cost_over_limit_working", about: "a sink whose cost limit is 100 working bytes: a layer that holds its tables, its stream index and one 192-byte frame is cost-over-limit (working bytes)", bases: vec![a], files: vec![("L1.wai".into(), l1)], + cfg: ChainConfig { cost_limit: Some(Wir1Cost { output_bytes: u64::MAX, symbols: u64::MAX, working_bytes: 100 }), ..cfg() }, steps: vec![base_at(0, 0), offer("L1.wai", 1)] }); + } + { + let a = small(115); + let (l1, _) = l1_of(&a, 116, 4); + let state0: u64 = a.units.iter().map(|u| u.samples.byte_len()).sum(); + out.push(Case { name: "chain_over_limit", about: "a chain budget one byte short of the base's units and the layer's payload, declared output and declared working bytes: chain-over-limit, before any stream is decoded", bases: vec![a], + files: vec![("L1.wai".into(), l1.clone())], cfg: ChainConfig { budget: state0 + reserve(&l1) - 1, ..cfg() }, steps: vec![base_at(0, 0), offer("L1.wai", 1)] }); + } + { + let a = small(117); + let (l1, mid) = l1_of(&a, 118, 4); + let l2 = l2_of(&a, &l1, &mid, 118); + let state0: u64 = a.units.iter().map(|u| u.samples.byte_len()).sum(); + out.push(Case { name: "chain_budget_retained", about: "a budget that holds the base and either layer, not both: layer 2, held, is retained, so layer 1 is chain-over-limit; once layer 2 expires and is dropped, layer 1 applies", bases: vec![a], + files: vec![("L1.wai".into(), l1.clone()), ("L2.wai".into(), l2.clone())], cfg: ChainConfig { budget: state0 + reserve(&l1).max(reserve(&l2)) + 2 * 64, ..cfg() }, + steps: vec![base_at(0, 0), offer("L2.wai", 1), offer("L1.wai", 2), Step::Expire { at_ms: 1002 }, offer("L1.wai", 1003)] }); + } + // ---- steps 7 and 8 ---- + { + let a = small(119); + let (la, mid) = l1_of(&a, 120, 4); + let (lb, _) = l1_of(&a, 121, 4); + let l2 = l2_of(&a, &la, &mid, 120); + let l3 = edit(&l2, |w| w.manifest.refines = Some(Refines { base: hex(&a.hash()), prev: one(&chash(&l2)), layer: 3, span: None }.to_value())); + out.push(simple("frozen_refuses_held", "a layer 3 held for both units; layer 1a applies, then layer 1b conflicts: the units freeze, and the held layer 3 is refused for them (layer-conflict)", a, + vec![("L3.wai".into(), l3), ("L1a.wai".into(), la), ("L1b.wai".into(), lb)], vec![base_at(0, 0), offer("L3.wai", 1), offer("L1a.wai", 2), offer("L1b.wai", 3)])); + } + { + let a = small(122); + let (l1, mid) = l1_of(&a, 123, 4); + let l2a = l2_of(&a, &l1, &mid, 124); + let l2b = l2_of(&a, &l1, &mid, 125); + out.push(simple("batch_conflict_release", "layers 2a and 2b, both over layer 1, held; layer 1 applies and releases them together: they conflict in the batch, and the units freeze at stage 1", a, + vec![("L2a.wai".into(), l2a), ("L2b.wai".into(), l2b), ("L1.wai".into(), l1)], vec![base_at(0, 0), offer("L2a.wai", 1), offer("L2b.wai", 2), offer("L1.wai", 3)])); + } + { + let a = small(126); + let (l1, mid) = l1_of(&a, 127, 4); + let l2a = l2_of(&a, &l1, &mid, 128); + let l2b = l2_of(&a, &l1, &mid, 129); + out.push(Case { name: "layer_gap_on_arrival", about: "a bound of one held envelope: layer 2a is held for both units, and layer 2b, which would be held too, is refused on arrival (layer-gap)", bases: vec![a], + files: vec![("L2a.wai".into(), l2a), ("L2b.wai".into(), l2b)], cfg: ChainConfig { bound: HoldBound { max_envelopes: 1, max_age_ms: 1000 }, ..cfg() }, + steps: vec![base_at(0, 0), offer("L2a.wai", 1), offer("L2b.wai", 2)] }); + } + { + let a = small(130); + let (la, mid) = l1_of(&a, 131, 4); + let (lb, _) = l1_of(&a, 132, 4); + let l2 = l2_of(&a, &la, &mid, 131); + out.push(simple("held_then_prev_mismatch", "a layer 2 over layer 1a is held; layer 1b applies instead and releases it: the units' producer is 1b, so layer 2 is prev-mismatch", a, + vec![("L2.wai".into(), l2), ("L1b.wai".into(), lb)], vec![base_at(0, 0), offer("L2.wai", 1), offer("L1b.wai", 2)])); + } + // ---- steps 9 and 10 ---- + { + let a = small(133); + let quarter: Vec = (0..2).map(|f| u8s(picture(4, 4, 134, f))).collect(); + let targets: Vec = (0..2).map(|f| u8s(picture(8, 8, 134, f))).collect(); + let (l1, _) = layer(CAP_VIDEO_INT_REFINE, &a.hash(), one(&a.hash()), 1, None, &quarter, (4, 4, 3), &targets, (8, 8, 3), params(4, Predictor::Up2, 4095)); + out.push(simple("up2_geometry_mismatch", "an up2 layer to 8×8 reads 4×4 frames; the units are 8×8: geometry-mismatch", a, vec![("L1.wai".into(), l1)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let b = flac_base(400, 2, 8000); + let bh = b.hash(); + let (l1, _) = layer(CAP_AUDIO_INT_REFINE, &bh, one(&bh), 1, None, &[Samples::I16(pcm(300, 2, 8000, 2))], (300, 1, 2), &[Samples::I16(pcm(600, 2, 16_000, 2))], (600, 1, 2), + RefineParams { rate: (2, 1), ..params(8, Predictor::Rate, 4095) }); + out.push(Case { name: "rate_geometry_mismatch", about: "a rate layer to 600 samples reads 300; the base holds 400: geometry-mismatch", bases: vec![b], files: vec![("L1.wai".into(), l1)], cfg: cfg(), + steps: vec![base_at(0, 0), offer("L1.wai", 1)] }); + } + { + let b = flac_base(480, 1, 48_000); + let bh = b.hash(); + let (l1, _) = layer(CAP_AUDIO_INT_REFINE, &bh, one(&bh), 1, None, &[b.units[0].samples.clone()], (480, 1, 1), &[Samples::I16(pcm(160, 1, 16_000, 3))], (160, 1, 1), + RefineParams { rate: (1, 3), ..params(4, Predictor::Rate, 4095) }); + out.push(Case { name: "rate_downsampling", about: "a mono FLAC base at 48 kHz and a rate layer to 16 kHz (rate 1/3), the predictor reading every third sample", bases: vec![b], files: vec![("L1.wai".into(), l1)], cfg: cfg(), + steps: vec![base_at(0, 0), offer("L1.wai", 1)] }); + } + { + let a = small(135); + let (l1, mid) = l1_of(&a, 136, 4); + let l2 = l2_of(&a, &l1, &mid, 136); + out.push(Case { name: "deadline_held_layer", about: "units presented at 200 and 240, leads 10 and 30: layer 2 arrives at 175 and is held; layer 1 at 180 applies to both and releases it; layer 2 is late for unit 0 (deadline-missed) and in time for unit 1", bases: vec![a], + files: vec![("L1.wai".into(), l1), ("L2.wai".into(), l2)], + cfg: ChainConfig { live: Some(Live { clock: UnitClock { t0_ms: 100, unit_ms: 40, target_latency_ms: 100 }, lead_ms: vec![10, 30] }), ..cfg() }, + steps: vec![base_at(0, 0), offer("L2.wai", 175), offer("L1.wai", 180)] }); + } + { + let mut a = gop_base(ctx, 8, 8, 2, 137, 0, 8); + let mut b = gop_base(ctx, 8, 8, 2, 137, 2, 8); + a.first_unit = 0; + b.first_unit = 2; + let bh = b.hash(); + let (lb, _) = layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, Some(Span { from: 0, count: 1 }), &[rgb_unit(&b, 0)], (8, 8, 3), &[u8s(picture(8, 8, 138, 2))], (8, 8, 3), params(4, pid, 4095)); + out.push(Case { name: "live_multi_base", about: "a clip split into two bases of two frames on one clock (the second's first unit is the clip's unit 2, presented at 200): its layer for that unit arriving at 150 with a 30 ms lead is in time", bases: vec![a, b], + files: vec![("L1_second.wai".into(), lb)], + cfg: ChainConfig { live: Some(Live { clock: UnitClock { t0_ms: 0, unit_ms: 100, target_latency_ms: 0 }, lead_ms: vec![30] }), ..cfg() }, + steps: vec![base_at(0, 0), base_at(1, 0), offer("L1_second.wai", 150)] }); + } + // ---- bases and releases ---- + { + let a = small(139); + let (la, _) = l1_of(&a, 140, 4); + let (lb, _) = l1_of(&a, 141, 4); + out.push(simple("base_readded", "layer 1a applies and 1b conflicts, freezing the units; the base decoded again keeps them as they are and reports nothing; 1b offered again is still refused", a, + vec![("L1a.wai".into(), la), ("L1b.wai".into(), lb)], vec![base_at(0, 0), offer("L1a.wai", 1), offer("L1b.wai", 2), base_at(0, 3), offer("L1b.wai", 4)])); + } + { + let a = small(142); + let bh = a.hash(); + let (l0, _) = layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, Some(Span { from: 0, count: 1 }), &[rgb_unit(&a, 0)], (8, 8, 3), &[u8s(picture(8, 8, 143, 0))], (8, 8, 3), params(4, pid, 4095)); + let (l1, _) = layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, Some(Span { from: 1, count: 1 }), &[rgb_unit(&a, 1)], (8, 8, 3), &[u8s(picture(8, 8, 143, 1))], (8, 8, 3), params(4, pid, 4095)); + let cut = edit(&l0, |w| { + let n = w.payload.len(); + w.payload.truncate(n - 4); + }); + out.push(Case { name: "refused_on_release", about: "two layers held for their base; when it arrives, the first is payload-malformed and has that one report, and the second applies", bases: vec![a], + files: vec![("L1_cut.wai".into(), cut), ("L1_f1.wai".into(), l1)], cfg: cfg(), steps: vec![offer("L1_cut.wai", 0), offer("L1_f1.wai", 1), base_at(0, 2)] }); + } + { + let mut a = small(144); + a.attested = true; + let (l1, _) = l1_of(&a, 145, 4); + let dv = |at| Step::Offer { file: "L1.wai".into(), at_ms: at, evidence: ReceiptEvidence::DeliveredVerified }; + out.push(Case { name: "evidence_upgrade", about: "an attested base: a layer held for it with no receipt evidence is offered again delivered-verified, keeps that evidence, and applies when the base arrives", bases: vec![a], + files: vec![("L1.wai".into(), l1)], cfg: cfg(), steps: vec![offer("L1.wai", 0), dv(5), base_at(0, 6)] }); + } + { + let mut a = small(146); + a.attested = true; + let (l1, _) = l1_of(&a, 147, 4); + let dv = |at| Step::Offer { file: "L1.wai".into(), at_ms: at, evidence: ReceiptEvidence::DeliveredVerified }; + out.push(Case { name: "evidence_upgrade_deadline", about: "units presented at 100 and 140 with a 96 ms lead: the layer arrives at 0 with no evidence and is verified at 5, so over the attested base it counts as arriving at 5: late for unit 0, in time for unit 1", bases: vec![a], + files: vec![("L1.wai".into(), l1)], + cfg: ChainConfig { live: Some(Live { clock: UnitClock { t0_ms: 0, unit_ms: 40, target_latency_ms: 100 }, lead_ms: vec![96] }), ..cfg() }, + steps: vec![offer("L1.wai", 0), dv(5), base_at(0, 6)] }); + } out } +/// A copy of the `WAI1` envelope `env` with the manifest text `text`, written as it is. +fn raw_manifest(env: &[u8], text: &str) -> Vec { + let ml = u32::from_le_bytes(env[4..8].try_into().expect("a length")) as usize; + let mut out = b"WAI1".to_vec(); + out.extend_from_slice(&(text.len() as u32).to_le_bytes()); + out.extend_from_slice(text.as_bytes()); + out.extend_from_slice(&env[8 + ml..]); + out +} + +/// What a layer adds to its chain's state while retained (staged-delivery §4.5). +fn reserve(env: &[u8]) -> u64 { + let w = Wai::from_bytes(env).expect("WAI1"); + let c = wir1::parse(&w.payload, wai::staged::unit_kind(w.capability()).expect("a refinement")).expect("WIR1").declared_cost(); + w.payload.len() as u64 + c.output_bytes + c.working_bytes +} + // ───────────────────────── replay ───────────────────────── fn report_json(r: &OfferReport) -> Value { @@ -1185,7 +1430,18 @@ fn generate(ctx: &Ctx) -> BTreeMap> { out.insert(format!("{dir}/case.json"), json_bytes(&case)); } let mut w2 = Vec::new(); - for (name, bytes, sources) in wai2_files(ctx) { + let files = wai2_files(ctx); + let packs: Vec = pack_attempts(&files[0].2) + .into_iter() + .map(|(name, sources, packs)| { + let names: Vec = sources.iter().enumerate().map(|(i, _)| format!("{name}.src{i}.wai")).collect(); + for (n, b) in names.iter().zip(&sources) { + out.insert(format!("wai2/{n}"), b.clone()); + } + json!({"name": name, "sources": names, "packs": packs}) + }) + .collect(); + for (name, bytes, sources) in files { let mut src_names = Vec::new(); for (i, s) in sources.iter().enumerate() { let n = format!("wai2/{name}.src{i}.wai"); @@ -1198,8 +1454,9 @@ fn generate(ctx: &Ctx) -> BTreeMap> { out.insert( "wai2/index.json".into(), json_bytes(&json!({ - "about": "WAI2 envelopes with refinement components (SPEC §7.1): what check_component_roles reports, whether each envelope_manifest matches its entry, the content hash of each component's exploded envelope (null for an entry that cannot be exploded; the others are exploded all the same), and entry 0's refinements. Where `sources` is given, the components explode to those WAI1 envelopes byte for byte.", + "about": "WAI2 envelopes with refinement components (SPEC §7.1): what check_component_roles reports, whether each envelope_manifest matches its entry, the content hash of each component's exploded envelope (null for an entry that cannot be exploded; the others are exploded all the same), and entry 0's refinements. Where `sources` is given, the components explode to those WAI1 envelopes byte for byte. `packs` lists WAI1 envelopes and whether a packer packs them: it refuses bytes around a manifest's JSON value or after a payload, and a manifest that inside a WAI2 entry passes the JSON depth limit.", "files": w2, + "packs": packs, })), ); out.insert("schedule.json".into(), json_bytes(&schedule_json())); @@ -1342,6 +1599,30 @@ fn wai2_files(ctx: &Ctx) -> Vec<(&'static str, Vec, Vec>)> { ] } +/// WAI1 envelopes and whether `WaiMulti::pack` packs them (SPEC §7.1): it refuses an +/// envelope with bytes around its manifest's JSON value or after its payload, and a +/// manifest nested so deep that inside a `WAI2` entry it passes the JSON depth limit. +fn pack_attempts(chain: &[Vec]) -> Vec<(&'static str, Vec>, bool)> { + let base = &chain[0]; + let text = String::from_utf8(wai::container::manifest_bytes(base).expect("WAI1").to_vec()).expect("UTF-8"); + let deep = |d: usize| raw_manifest(base, &format!("{},\"zz_deep\":{}{}}}", &text[..text.len() - 1], "[".repeat(d), "]".repeat(d))); + let attempts = vec![ + ("pack_trailing_newline", vec![raw_manifest(base, &format!("{text}\n"))]), + ("pack_leading_space", vec![raw_manifest(base, &format!(" {text}"))]), + ("pack_after_payload", vec![[&base[..], b"xyz"].concat()]), + ("pack_deep_120", vec![deep(120)]), + ("pack_deep_124", vec![deep(124)]), + ]; + attempts + .into_iter() + .map(|(name, srcs)| { + let refs: Vec<&[u8]> = srcs.iter().map(|v| v.as_slice()).collect(); + let packs = WaiMulti::pack(&refs).is_ok(); + (name, srcs, packs) + }) + .collect() +} + /// What a sink and a verifier decide about a `WAI2`: its role check, its envelope /// manifests, its exploded envelopes' content hashes, and entry 0's refinements. fn wai2_expect(bytes: &[u8]) -> Value { @@ -1509,6 +1790,19 @@ fn verify(dir: &Path) -> Result<(), Vec> { fail(format!("wai2/{name}: does not explode to its sources")); } } + for p in w2["packs"].as_array().into_iter().flatten() { + let srcs: Vec> = p["sources"].as_array().unwrap().iter().map(|s| std::fs::read(dir.join("wai2").join(s.as_str().unwrap())).unwrap_or_default()).collect(); + let refs: Vec<&[u8]> = srcs.iter().map(|v| v.as_slice()).collect(); + let got = WaiMulti::pack(&refs); + if got.is_ok() != (p["packs"] == true) { + fail(format!("wai2 {}: pack {}", p["name"], if got.is_ok() { "packs it" } else { "refuses it" })); + } + if let Ok(packed) = got { + if WaiMulti::explode_all(&packed).ok() != Some(srcs) { + fail(format!("wai2 {}: the packed envelope does not explode to its sources", p["name"])); + } + } + } // 4. The schedule. let sched: Value = std::fs::read(dir.join("schedule.json")).ok().and_then(|b| serde_json::from_slice(&b).ok()).unwrap_or(Value::Null); if sched != schedule_json() { -- 2.54.0 (Apple Git-157) From c1ec1e9d647bbe79cc9a5dfebdecf6850a80c336 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 4 Oct 2026 20:29:23 -0400 Subject: [PATCH 18/36] =?UTF-8?q?wai:=20staged=5Fverify.py=20implements=20?= =?UTF-8?q?=C2=A74=20from=20the=20text,=20with=20a=20strict=20envelope=20r?= =?UTF-8?q?eader?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Python verifier is rewritten as a direct implementation of the extension, not of the reference sink's structure: - a strict reader of envelope JSON (SPEC §2): UTF-8 with no byte-order mark, no member named twice, no NaN or Infinity, no number past the float range, no lone surrogate, at most 127 nested containers, and -0, a fraction or an exponent as no integer; the members SPEC §3 and §7.1 require, with their types. A WAI1 or WAI2 it cannot read is the step's error, never a crash; - each envelope goes through §4.1's numbered steps in turn: step 1, a hold for an absent base with the strongest evidence kept, steps 2 to 6 with the chain budget, then steps 7 and 8 per unit against a literal record of what passed step 8; batches as §4.4 forms them; holds, their bound and expiry; frozen units; dropping what no unit holds; - live deadlines on one clock across bases (first_unit), saturating at 2^64-1, and over an attested base from when the layer's receipt was verified; - a step's reports are compared as a set, one per envelope; - WAI2: per-entry explode, the fallback rule for a refinement's envelope_manifest, the WAI1 schema of an envelope_manifest, and what a packer refuses; - a base whose payload is a committed integer-codec vector has its stage 0 checked against that vector's sha256.json. The README defines the evidence tokens and the report fields, and states which digests check each pinned base's stage 0: the vectors' sidecars, and the cross-architecture goldens for the int_hyper image. Co-Authored-By: Claude Opus 5.5 --- wai/staged-conformance/README.md | 48 +- wai/staged-conformance/staged_verify.py | 1129 +++++++++++++++-------- 2 files changed, 783 insertions(+), 394 deletions(-) diff --git a/wai/staged-conformance/README.md b/wai/staged-conformance/README.md index a9c2cfc49..fc6a590f2 100644 --- a/wai/staged-conformance/README.md +++ b/wai/staged-conformance/README.md @@ -9,22 +9,40 @@ reports, at every step, the reasons the case states per envelope and per unit, a ends with every unit at the stated stage, producer, frozen flag, tier, shape and stage hash (SHA-256 of the unit's canonical buffer, SPEC §7 *Digests*; stated only where the unit holds `decode-equivalence`). The reasons are §4.2's; a `payload-malformed` -report names the `WIR1` code (Appendix A.6). +report names the `WIR1` code (Appendix A.6), and a `cost-over-limit` report the figure +past the limit (`output bytes`, `symbols` or `working bytes`). A step reports once per +envelope it decided: what was refused for the whole envelope, or per unit what was +applied, held and refused (a unit held and decided within the same step is reported by +its decision); `held_for_base` marks an envelope held for a base the sink has not +decoded. The order of a step's reports is not part of the outcome. + +**Evidence.** Each offer states what the sink verified of the layer's delivery receipt +(§10), as a token: `delivered-verified`, a receipt verified through jwp-receipts §3 +step 6 over the envelope's bytes as received; `inclusion-verified`, a receipt verified +without those bytes (§3 steps 1, 2, 3 and 5), which never authorizes an application; +`none`, no receipt verified. A base's `attested` states whether the sink verified a +receipt for it, so that step 5 requires `delivered-verified` of its layers. - `cases//` — one case: `case.json`, each base's envelope (`base*.wai`) and its stage-0 output (`stage0*.bin`: the units' canonical buffers concatenated, in unit order), and the layers (`*.wai`). `case.json` states each base's capability it was decoded through, whether through a fallback, whether the sink verified a delivery - receipt for it, whether its pin resolved, its tier and its units; the sink's hold - bound, cost limit and live schedule; the steps; and what the sink reports and holds. + receipt for it, whether its pin resolved, its tier, its units and the live clock + position of its first unit (`first_unit`, §8); the sink's hold bound, cost limit, + chain budget (§4.5) and live schedule; the steps; and what the sink reports and + holds. - `wai2/` — `WAI2` envelopes with refinement components (SPEC §7.1) and `index.json`, which states for each what `check_component_roles` reports, whether - each `envelope_manifest` matches its entry, the content hash of every component's - exploded envelope, and entry 0's refinements: a live-style chain packed with its + each `envelope_manifest` matches its entry, the content hash of each component's + exploded envelope (`null` for an entry that cannot be exploded, the others exploded + all the same), and entry 0's refinements: a live-style chain packed with its manifests verbatim, which explodes back to its sources (`*.src.wai`) byte for byte; an envelope written with ids and no `envelope_manifest`; a refinement whose payload does not match its `sha256`; an `envelope_manifest` that does not match its - entry; a producer that names nothing; a refinement before a selectable entry. + entry, and one that names a fallback; each refinement rule of SPEC §7.1 broken once. + Its `packs` list gives `WAI1` envelopes and whether a packer packs them: bytes before + or after a manifest's JSON value, bytes after a payload, and a manifest that inside a + `WAI2` entry passes the JSON depth limit are refused. - `schedule.json` — arrival traces, and the unit-by-unit presentation and stage a sink gives each (§8), with the count of units presented below the stage their layers would have reached on time. @@ -45,7 +63,7 @@ the five row filters; VERBATIM subframes). | `wai.video.int_motion` (`WIV1`, keyframe 0) | `wai_staged_vectors verify` (the reference decode) and `staged_verify.py` (an independent decode) | | `wai.image.png` | `staged_verify.py` (zlib and the row filters); a fixture to `wai_staged_vectors`, which builds without `codecs` | | `wai.audio.flac` | `staged_verify.py` (VERBATIM subframes, both CRCs); a fixture to `wai_staged_vectors` | -| `wai.neural.int_hyper`, `wai.video.int_hyper` (keyframe 1), `wai.audio.int_codec`, `wai.splat.int_codec` | `wai_staged_vectors verify` (the reference decode, pinned); no independent decoder here, so `staged_verify.py` starts from `stage0.bin` and its SHA-256, which the cross-architecture goldens of [`../byte-exact-conformance/`](../byte-exact-conformance/) also hold | +| `wai.neural.int_hyper`, `wai.video.int_hyper` (keyframe 1), `wai.audio.int_codec`, `wai.splat.int_codec` | `wai_staged_vectors verify` (the reference decode, pinned). Each base's payload is a committed vector's bitstream under [`../wai-rs/tests/vectors/`](../wai-rs/tests/vectors/), and `staged_verify.py`, which has no decoder for these, checks `stage0.bin` against the decoded output that vector's `sha256.json` states (`int_hyper/tiny`, `int_video/video_nk` frame by frame, `int_audio/audio`, `int_splat/splat`); `int_kernels_vectors` checks those sidecars against the decoders. The cross-architecture goldens of [`../byte-exact-conformance/`](../byte-exact-conformance/) also hold the `int_hyper` image's stage 0 | Every layer application, in every case, is checked by both. @@ -57,11 +75,17 @@ Every layer application, in every case, is checked by both. can and checks their tiers, and replays the schedule. `wai_staged_vectors gen .` writes the corpus. - `staged_verify.py .`: pure Python with the standard library, sharing no code with - `wai-rs`. It reimplements the envelope and the content hash, `refines`, `WIR1` (its - parse in order, its cost, its streams, the three predictors and `rdiv`), the sink's - algorithm with its holds, batches, conflicts and deadlines, tiers and stage hashes, - the PNG and FLAC decodes, the schedule, and exploding a `WAI2` component (reading - the manifest's members as the text they are) with the refinement role rules. It takes BLAKE3 from + `wai-rs`. It reimplements, from the extension and SPEC: a strict reader of envelope + JSON (UTF-8 with no byte-order mark, no member named twice, no NaN or Infinity, no + number past the float range, no lone surrogate, the depth limit, `-0`, a fraction or + an exponent as no integer) with the members SPEC §3 and §7.1 require; the content + hash; `refines`; `WIR1` (its parse in order, its cost, its streams, the three + predictors and `rdiv`); the sink as §4 states it, each envelope through §4.1's + numbered steps in turn, batches as §4.4 forms them, holds and their bound, frozen + units, the chain budget, live deadlines on one saturating clock across bases, and + receipt evidence; tiers and stage hashes; the PNG and FLAC decodes; the schedule; + exploding a `WAI2` component (reading the manifest's members as the text they are) + with the refinement role rules; and what a packer refuses. It takes BLAKE3 from [`../claim-conformance/_blake3.py`](../claim-conformance/_blake3.py) and the rANS decoder and the `WIV1` keyframe-0 decode from [`../int-payload-conformance/int_payload_verify.py`](../int-payload-conformance/int_payload_verify.py), diff --git a/wai/staged-conformance/staged_verify.py b/wai/staged-conformance/staged_verify.py index 9b3c5d04b..cdddcc5a0 100644 --- a/wai/staged-conformance/staged_verify.py +++ b/wai/staged-conformance/staged_verify.py @@ -1,32 +1,44 @@ #!/usr/bin/env python3 """Independent verifier of the staged-delivery corpus (extensions/staged-delivery.md §12). -Written from the extension text. It shares no code with wai-rs, which generates the -corpus. It takes BLAKE3 from ../claim-conformance/_blake3.py and the rANS decoder and the -WIV1 keyframe-0 decode from ../int-payload-conformance/int_payload_verify.py, both written -from their specifications, and reimplements the rest: +Written from the extension text and SPEC §2, §3 and §7.1. It shares no code with +wai-rs, which generates the corpus. It takes BLAKE3 from ../claim-conformance/_blake3.py +and the rANS decoder and the WIV1 keyframe-0 decode from +../int-payload-conformance/int_payload_verify.py, both written from their +specifications, and implements the rest: -- the WAI1 envelope and the content hash (jwp-receipts §2.1); +- a strict reader of envelope JSON (SPEC §2): UTF-8 with no byte-order mark, no member + named twice in an object, no NaN or Infinity, no number past the 64-bit float range, + no lone surrogate, at most 127 nested containers; `-0`, a fraction or an exponent is + not an integer; and the members SPEC §3 (WAI1) and §7.1 (WAI2) require, with their + types; - `refines` and its rules (§3); - WIR1: the parse in its stated order, the declared cost, the stream checks, the three predictors and rdiv (Appendix A); -- the sink: §4.1's steps, holds and their bound (§4.3), batches (§4.4), conflicts and - frozen units, live deadlines (§8), tiers and stage hashes (§5); +- the sink, as §4 states it: each envelope through §4.1's numbered steps in turn + (step 1, then the base or a hold for it, then steps 2–6, then steps 7 and 8 for each + unit), batches as §4.4 forms them, holds and their bound (§4.3), the chain budget + (§4.5), live deadlines on one clock across bases (§8) with a saturating clock, and + receipt evidence (§10); tiers and stage hashes (§5); - base decodes: WIV1 keyframe 0, PNG (zlib and the five filters), FLAC (VERBATIM subframes, with both CRCs); +- WAI2 refinement components (SPEC §7.1): the role checks, envelope manifests, each + entry's exploded envelope, and the envelopes a packer must refuse; - the live schedule's presentation rule (schedule.json). 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 -tier, replays every step, and requires every report and every unit's final stage, -producer, frozen flag, tier, shape and stage hash to be the case's. +tier, replays every step, and requires every step's reports (one per envelope; the +order of the reports within a step is not part of the outcome) and every unit's final +stage, producer, frozen flag, tier, shape and stage hash to be the case's. python3 staged_verify.py -Exit status 0 only if every case and the schedule verify. +Exit status 0 only if every case, every WAI2 file and the schedule verify. """ import hashlib import json +import math import os import struct import sys @@ -40,25 +52,219 @@ from _blake3 import blake3 # noqa: E402 import int_payload_verify as IPV # noqa: E402 DE, EC, NONE = "decode-equivalence", "entropy-consistency", "none" +U64 = (1 << 64) - 1 -# ---------------------------------------------------------------- envelope and registry +# ---------------------------------------------------------------- strict JSON (SPEC §2) + +# The most containers a reader nests: the reference reader's limit, which an envelope +# that two readers must agree on stays within. +DEPTH_LIMIT = 127 + + +class NotJSON(ValueError): + pass + + +def strict_json(b): + """The JSON value of the bytes `b`, read strictly, or NotJSON.""" + if b[:3] == b"\xef\xbb\xbf": + raise NotJSON("a byte-order mark") + try: + text = b.decode("utf-8") + except UnicodeDecodeError as e: + raise NotJSON(f"not UTF-8: {e}") + + def members(pairs): + out = {} + for k, v in pairs: + if k in out: + raise NotJSON(f"the member {k!r} is named twice") + out[k] = v + return out + + def constant(name): + raise NotJSON(f"{name} is not a JSON number") + + def real(s): + f = float(s) + if math.isinf(f): + raise NotJSON(f"{s} is past the 64-bit float range") + return f + + def integer(s): + # `-0` is a number and not an integer; an integer past 64 bits is read as the + # float it rounds to, and is then no integer either. + if s == "-0": + return -0.0 + n = int(s) + if -(1 << 63) <= n <= U64: + return n + try: + return float(n) + except OverflowError: + raise NotJSON(f"{s[:20]}… is past the 64-bit float range") + + try: + v = json.loads(text, object_pairs_hook=members, parse_constant=constant, parse_float=real, parse_int=integer) + except NotJSON: + raise + except (ValueError, RecursionError) as e: + raise NotJSON(str(e)) + stack = [(v, 1)] + while stack: + x, depth = stack.pop() + if isinstance(x, (dict, list)): + if depth > DEPTH_LIMIT: + raise NotJSON("nested past the depth limit") + kids = list(x.items()) if isinstance(x, dict) else [(None, y) for y in x] + for k, y in kids: + if k is not None and any(0xD800 <= ord(c) <= 0xDFFF for c in k): + raise NotJSON("a lone surrogate") + stack.append((y, depth + 1)) + elif isinstance(x, str) and any(0xD800 <= ord(c) <= 0xDFFF for c in x): + raise NotJSON("a lone surrogate") + return v + + +def is_int(v): + return isinstance(v, int) and not isinstance(v, bool) + + +def is_num(v): + return isinstance(v, (int, float)) and not isinstance(v, bool) + + +class Schema(ValueError): + pass + + +def need(o, k, ok, what, optional=False): + """Member `k` of the object `o`: present and `ok` (or, if `optional`, absent or null).""" + if k not in o or o[k] is None: + if optional: + return None + raise Schema(f"{k} is required ({what})") + if not ok(o[k]): + raise Schema(f"{k} is not {what}") + return o[k] + + +def a_str(v): + return isinstance(v, str) + + +def an_obj(v): + return isinstance(v, dict) + + +def a_u64(v): + return is_int(v) and 0 <= v <= U64 + + +def an_f32(v): + if not is_num(v): + return False + try: + struct.pack(" len(b): - raise ValueError("truncated manifest") - m = json.loads(b[8:8 + ml]) + raise NotWai1("truncated manifest") + text = b[8:8 + ml] + try: + m = strict_json(text) + check_manifest_v1(m) + except (NotJSON, Schema) as e: + raise NotWai1(str(e)) pl = struct.unpack_from(" len(b): - raise ValueError("truncated payload") - return m, b[12 + ml:12 + ml + pl] + raise NotWai1("truncated payload") + return m, text, b[12 + ml:12 + ml + pl] + + +def plaintext(m): + s = m.get("security") + return s is None or s.get("encryption") in (None, "none") + + +# ---------------------------------------------------------------- the registry (SPEC §5, §7) # SPEC §5, Refinement capabilities and Base admission (the refinement rows). @@ -96,6 +302,8 @@ def base_tier(cap, manifest, pin_resolved): return NONE + + # ---------------------------------------------------------------- refines (§3) @@ -103,10 +311,6 @@ class Malformed(Exception): pass -def is_int(v): - return isinstance(v, int) and not isinstance(v, bool) - - def is_hash(v): return isinstance(v, str) and len(v) == 64 and all(c in "0123456789abcdef" for c in v) @@ -116,6 +320,8 @@ def u32_int(v, least): def parse_refines(r): + """§3, the rules a sink checks without the base. `None` (absent or null) is + malformed: a refinement-only capability carries `refines`.""" if not isinstance(r, dict): raise Malformed("not an object") base = r.get("base") @@ -125,7 +331,7 @@ def parse_refines(r): if not (is_int(layer) and 1 <= layer <= 255): raise Malformed("layer") span = None - if "span" in r: + if r.get("span") is not None: # null is absent (SPEC §3.1) s = r["span"] if not isinstance(s, dict): raise Malformed("span") @@ -161,6 +367,7 @@ def units_of(ref, n): def check_units(ref, n): + """§3's checks that need the base's unit count (§4.1 step 2).""" frm, count = units_of(ref, n) if frm + count > n or count == 0: raise Malformed("the span lies outside the base's units") @@ -169,6 +376,7 @@ def check_units(ref, n): def producer_for(ref, i): + """P(u) for the i-th unit the layer covers.""" if ref["runs"][0][1] is None: return ref["runs"][0][0] at = 0 @@ -269,7 +477,9 @@ def check_streams(w): def cost(w): unit = w["d"][0] * w["d"][1] * w["d"][2] eb = 2 if w["kind"] == "pcm" else 1 - return (unit * eb * w["n_units"], unit * w["n_units"], sum(4 * len(t) for t in w["tables"]) + unit * eb) + # Working bytes: the parsed tables, the index of the streams (8 bytes a unit) and one + # unit's output (A.5). + return (unit * eb * w["n_units"], unit * w["n_units"], sum(4 * len(t) for t in w["tables"]) + 8 * w["n_units"] + unit * eb) def rdiv(n, d): @@ -333,276 +543,264 @@ def apply_unit(w, u, prev, shape): return out -# ---------------------------------------------------------------- the sink (§4) +# ---------------------------------------------------------------- the sink (§4, §5, §8, §10) + +# §4.2, in its order. REASONS = ["refines-malformed", "capability-not-refinement", "base-absent", "base-via-fallback", "base-not-admitted", - "refinement-unattested", "payload-malformed", "cost-over-limit", "layer-conflict", "prev-mismatch", - "layer-gap", "geometry-mismatch", "deadline-missed"] + "refinement-unattested", "payload-malformed", "cost-over-limit", "chain-over-limit", "layer-conflict", + "prev-mismatch", "layer-gap", "geometry-mismatch", "deadline-missed"] +EVIDENCE = {"none": 0, "inclusion-verified": 1, "delivered-verified": 2} # §10, weakest first -class Report: - def __init__(self, h): - self.h, self.envelope, self.detail, self.held_for_base = h, None, None, False - self.applied, self.held, self.refused = [], [], [] +class Outcome: + """What one offer, base decode or expiry decided, for each envelope it touched.""" - def json(self): - applied = sorted(self.applied) - refused = sorted(self.refused, key=lambda x: (x[0], REASONS.index(x[1]))) - decided = set(applied) | {u for u, _ in refused} - held = sorted({u for u in self.held if u not in decided}) - return {"object": self.h, "envelope": self.envelope, "detail": self.detail, "held_for_base": self.held_for_base, - "applied": applied, "held": held, "refused": [[u, r] for u, r in refused]} + def __init__(self): + self.by = {} - -def refuse(h, why, detail=None): - r = Report(h) - r.envelope, r.detail = why, detail - return r - - -class Reports: - def __init__(self, first): - self.first, self.by = first, {first: Report(first)} - - def get(self, h): + def of(self, h): if h not in self.by: - self.by[h] = Report(h) + self.by[h] = {"envelope": None, "detail": None, "held_for_base": False, "applied": [], "held": [], "refused": []} return self.by[h] - def finish(self, layers): - out = [self.by.pop(self.first)] - rest = sorted(self.by.values(), key=lambda r: layers[r.h]["seq"] if r.h in layers else float("inf")) - return out + rest + def refuse(self, h, why, detail=None): + r = self.of(h) + r["envelope"], r["detail"] = why, detail + + def reports(self): + """One report per envelope: its units in order; a unit held and then decided in + the same step is reported by its decision.""" + out = [] + for h, r in self.by.items(): + decided = set(r["applied"]) | {u for u, _ in r["refused"]} + out.append({"object": h, "envelope": r["envelope"], "detail": r["detail"], "held_for_base": r["held_for_base"], + "applied": sorted(r["applied"]), "held": sorted({u for u in r["held"] if u not in decided}), + "refused": [[u, w] for u, w in sorted(r["refused"], key=lambda x: (x[0], REASONS.index(x[1])))]}) + return out -class Chain: - def __init__(self, base, units, cfg): - self.base, self.cfg = base, cfg - self.units = [dict(stage=0, producer=base["hash"], frozen=False, tier=base["tier"], shape=s, elem=e, data=d) +class Envelope: + """A received envelope that passed §4.1 step 1.""" + + def __init__(self, h, cap, ref, payload, at, evidence): + self.h, self.cap, self.ref, self.payload = h, cap, ref, payload + self.arrival, self.evidence, self.verified = at, evidence, at + self.w = None # its payload, parsed at step 6 + + def p(self, u, n): + """P(u): the producer `prev` gives for unit u of a base of n units.""" + return producer_for(self.ref, u - units_of(self.ref, n)[0]) + + +def unit_bytes(s): + return len(s["data"]) * (2 if s["elem"] == "i16" else 1) + + +class Base: + """A decoded base and its units (§1), with what §4 keeps for them.""" + + def __init__(self, info, units, cfg): + self.info, self.cfg = info, cfg + self.units = [dict(stage=0, producer=info["hash"], frozen=False, tier=info["tier"], shape=s, elem=e, data=list(d)) for (s, e, d) in units] - self.layers, self.held, self.seen, self.seq = {}, [], {}, 0 + self.passed = {} # step 7's record: (u, layer, P(u)) -> the envelope that passed step 8 for u + self.held = set() # (envelope hash, u): u held for the envelope (step 8) + self.retained = {} # envelope hash -> Envelope, from step 6 to when no unit is held for it (§4.5) def n(self): return len(self.units) def held_envelopes(self): - return len({h for h, _ in self.held}) + return {h for h, _ in self.held} - def admit(self, h, cap, ref, payload, arrival, evidence): - if ref["base"] != self.base["hash"]: - return ("base-absent", None) + def state(self): + """§4.5: units' output bytes and 32 bytes per stage above 0; retained layers' + payload bytes, declared output and declared working bytes.""" + s = sum(unit_bytes(x) + 32 * x["stage"] for x in self.units) + return s + sum(len(e.payload) + e.cost[0] + e.cost[2] for e in self.retained.values()) + + # -- §4.1 steps 2 to 6, for an envelope whose base this is + + def steps_2_to_6(self, e): + """The envelope's reason and detail, or None and the envelope retained.""" try: - check_units(ref, self.n()) + check_units(e.ref, self.n()) # step 2 except Malformed: return ("refines-malformed", None) - if self.base["via_fallback"]: + if self.info["via_fallback"]: # step 3 return ("base-via-fallback", None) - if self.base["capability"] not in ADMIT[cap]: + if self.info["capability"] not in ADMIT[e.cap]: # step 4 return ("base-not-admitted", None) - if self.base["attested"] and evidence != "delivered-verified": + if self.info["attested"] and e.evidence != "delivered-verified": # step 5 return ("refinement-unattested", None) - try: - w = parse_wir1(payload, UNIT_KIND[cap]) - except Refused as e: - return ("payload-malformed", e.code) - if w["n_units"] != units_of(ref, self.n())[1]: - return ("payload-malformed", "geometry_mismatch") + try: # step 6 + w = parse_wir1(e.payload, UNIT_KIND[e.cap]) + except Refused as x: + return ("payload-malformed", x.code) + if w["n_units"] != units_of(e.ref, self.n())[1]: + return ("payload-malformed", "field_range") + c = cost(w) lim = self.cfg["cost_limit"] if lim is not None: - c = cost(w) for got, key, what in zip(c, ("output_bytes", "symbols", "working_bytes"), ("output bytes", "symbols", "working bytes")): if got > lim[key]: return ("cost-over-limit", what) + if self.state() + len(e.payload) + c[0] + c[2] > self.cfg["budget"]: + return ("chain-over-limit", None) try: check_streams(w) - except Refused as e: - return ("payload-malformed", e.code) - self.seq += 1 - self.layers[h] = dict(w=w, ref=ref, arrival=arrival, seq=self.seq) + except Refused as x: + return ("payload-malformed", x.code) + e.w, e.cost = w, c + # §8, §10: over an attested base a layer counts as arrived when its receipt was verified. + e.when = e.verified if self.info["attested"] else e.arrival + self.retained[e.h] = e return None - def key(self, h, u): - ref = self.layers[h]["ref"] - frm = units_of(ref, self.n())[0] - return (ref["layer"], u, producer_for(ref, u - frm) or "") + # -- steps 7 and 8, for unit u - def conflicted_before(self, h, u): - k = self.key(h, u) - return self.units[u]["frozen"] or (k in self.seen and self.seen[k] != h) + def conflicted(self, e, u): + """Step 7 against what passed step 8 before.""" + if self.units[u]["frozen"]: + return True + other = self.passed.get((u, e.ref["layer"], e.p(u, self.n()))) + return other is not None and other != e.h - def readiness(self, h, u): - ref = self.layers[h]["ref"] - frm = units_of(ref, self.n())[0] - p = producer_for(ref, u - frm) - if p is None: - return "not" - s = self.units[u] - want = ref["layer"] - 1 - if s["stage"] == want and s["producer"] == p: + def readiness(self, e, u): + """Step 8.""" + s, want = self.units[u], e.ref["layer"] - 1 + if s["stage"] == want and s["producer"] == e.p(u, self.n()): return "ready" return "held" if s["stage"] < want else "not" - def freeze(self, u, reps): + def freeze(self, u, out): + """Step 7: no further layer for u; every envelope held for it is refused for it.""" self.units[u]["frozen"] = True - gone = [x for x in self.held if x[1] == u] - self.held = [x for x in self.held if x[1] != u] - for h, x in gone: - reps.get(h).refused.append((x, "layer-conflict")) + for h, x in sorted(self.held): + if x == u: + self.held.discard((h, x)) + out.of(h)["refused"].append((u, "layer-conflict")) - def evaluate(self, h, outside, reps): - ref = self.layers[h]["ref"] - frm, count = units_of(ref, self.n()) - ready, newly = [], [] - for u in range(frm, frm + count): - if self.conflicted_before(h, u): - self.freeze(u, reps) - reps.get(h).refused.append((u, "layer-conflict")) - continue - r = self.readiness(h, u) - if r == "ready": - ready.append((h, u)) - elif r == "held": - newly.append(u) - else: - reps.get(h).refused.append((u, "prev-mismatch")) - if newly: - already = any(x == h for x, _ in self.held) - if not already and self.held_envelopes() + outside >= self.cfg["bound"]["max_envelopes"]: - for u in newly: - reps.get(h).refused.append((u, "layer-gap")) - else: - for u in newly: - if (h, u) not in self.held: - self.held.append((h, u)) - reps.get(h).held.append(u) - self.run_batches(ready, reps) + # -- steps 9 and 10, and the next stage - def run_batches(self, batch, reps): - while batch: - adv = self.run_batch(batch, reps) - batch = self.release(adv, reps) - - def run_batch(self, batch, reps): - keys = [self.key(h, u) for h, u in batch] - conflicted = [] - for i, (h, u) in enumerate(batch): - c = self.units[u]["frozen"] or (keys[i] in self.seen and self.seen[keys[i]] != h) - c = c or any(j != i and keys[j] == keys[i] and batch[j][0] != h for j in range(len(batch))) - conflicted.append(c) - for i, (h, u) in enumerate(batch): - if conflicted[i]: - self.freeze(u, reps) - reps.get(h).refused.append((u, "layer-conflict")) - else: - self.seen.setdefault(keys[i], h) - adv = [] - for i, (h, u) in enumerate(batch): - if conflicted[i]: - continue - why = self.apply(h, u) - if why is None: - reps.get(h).applied.append(u) - adv.append(u) - else: - reps.get(h).refused.append((u, why)) - return adv - - def apply(self, h, u): - L = self.layers[h] - w, ref = L["w"], L["ref"] - frm = units_of(ref, self.n())[0] - s = self.units[u] - elem_ok = (s["elem"] == "i16") == (w["kind"] == "pcm") - if not elem_ok or len(s["data"]) != s["shape"][0] * s["shape"][1] * s["shape"][2] or not accepts_below(w, s["shape"]): + def steps_9_10_apply(self, e, u): + s, w = self.units[u], e.w + if (s["elem"] == "i16") != (w["kind"] == "pcm") or len(s["data"]) != s["shape"][0] * s["shape"][1] * s["shape"][2] \ + or not accepts_below(w, s["shape"]): return "geometry-mismatch" live = self.cfg["live"] if live is not None: leads = live["lead_ms"] - lead = leads[ref["layer"] - 1] if ref["layer"] - 1 < len(leads) else 0 - due = live["t0_ms"] + u * live["unit_ms"] + live["target_latency_ms"] - if L["arrival"] + lead > due: + lead = leads[e.ref["layer"] - 1] if e.ref["layer"] - 1 < len(leads) else 0 + position = self.info["first_unit"] + u + due = min(live["t0_ms"] + position * live["unit_ms"] + live["target_latency_ms"], U64) + if e.when + lead > min(due, U64): return "deadline-missed" - s["data"] = apply_unit(w, u - frm, s["data"], s["shape"]) + s["data"] = apply_unit(w, u - units_of(e.ref, self.n())[0], s["data"], s["shape"]) s["shape"] = w["d"] - s["stage"] = ref["layer"] - s["producer"] = h - s["tier"] = DE if s["tier"] == DE else NONE + s["stage"] = e.ref["layer"] + s["producer"] = e.h + s["tier"] = DE if s["tier"] == DE else NONE # a layer acts on reconstructed output (§5) return None - def release(self, adv, reps): - batch, keep = [], [] - held, self.held = self.held, [] - for h, u in held: - if u not in adv: - keep.append((h, u)) - continue - if self.conflicted_before(h, u): - self.held = keep - self.freeze(u, reps) - keep, self.held = self.held, [] - reps.get(h).refused.append((u, "layer-conflict")) - continue - r = self.readiness(h, u) - if r == "ready": - batch.append((h, u)) - elif r == "held": - keep.append((h, u)) + # -- §4.4 + + def receive(self, envelopes, out, held_elsewhere, on_arrival): + """Envelopes offered on arrival (one) or released when this base was decoded + (in arrival order): each passes steps 2 to 6, and for each unit step 7 against + what passed step 8 earlier and step 8, alone; the ready pairs then form a batch.""" + ready = [] + for e in envelopes: + if e.h in self.retained: + e = self.retained[e.h] # §4.5: steps 2 to 6 passed, and it adds nothing else: - reps.get(h).refused.append((u, "prev-mismatch")) - self.held = keep - batch.sort(key=lambda x: (self.layers[x[0]]["seq"], x[1])) - return batch - - def offer_checked(self, h, cap, ref, payload, at, ev, outside): - reps = Reports(h) - if h not in self.layers: - why = self.admit(h, cap, ref, payload, at, ev) - if why is not None: - return [refuse(h, *why)] - self.evaluate(h, outside, reps) - return reps.finish(self.layers) - - def offer_batch(self, items): - order = [p["hash"] for p in items] - reps = Reports(order[0]) - ready, refused = [], [] - for p in items: - h = p["hash"] - if h not in self.layers: - why = self.admit(h, p["cap"], p["ref"], p["payload"], p["arrival"], p["evidence"]) + why = self.steps_2_to_6(e) if why is not None: - refused.append(refuse(h, *why)) + out.refuse(e.h, *why) continue - reps.get(h) - frm, count = units_of(self.layers[h]["ref"], self.n()) + r = out.of(e.h) + already_held = e.h in self.held_envelopes() + waiting = [] + frm, count = units_of(e.ref, self.n()) for u in range(frm, frm + count): - if self.conflicted_before(h, u): - self.freeze(u, reps) - reps.get(h).refused.append((u, "layer-conflict")) + if self.conflicted(e, u): + r["refused"].append((u, "layer-conflict")) + self.freeze(u, out) continue - r = self.readiness(h, u) - if r == "ready": - ready.append((h, u)) - elif r == "held": - self.held.append((h, u)) - reps.get(h).held.append(u) + k = self.readiness(e, u) + if k == "ready": + ready.append((e, u)) + elif k == "held": + waiting.append(u) else: - reps.get(h).refused.append((u, "prev-mismatch")) - self.run_batches(ready, reps) - out = reps.finish(self.layers) + refused - out.sort(key=lambda r: order.index(r.h) if r.h in order else len(order)) - return out - - def expire(self, now): - reps = {} - keep = [] - for h, u in self.held: - if now - self.layers[h]["arrival"] > self.cfg["bound"]["max_age_ms"]: - reps.setdefault(h, Report(h)).refused.append((u, "layer-gap")) + r["refused"].append((u, "prev-mismatch")) + if not waiting: + continue + # §4.3: an envelope that would pass the bound's count is refused on arrival. + if on_arrival and not already_held and len(self.held_envelopes()) + held_elsewhere >= self.cfg["bound"]["max_envelopes"]: + r["refused"] += [(u, "layer-gap") for u in waiting] else: - keep.append((h, u)) - self.held = keep - return sorted(reps.values(), key=lambda r: self.layers[r.h]["seq"]) + for u in waiting: + self.held.add((e.h, u)) + r["held"].append(u) + self.batches(ready, out) + self.drop_unheld() + + def batches(self, batch, out): + while batch: + n = self.n() + keys = {} + for e, u in batch: + keys.setdefault((u, e.ref["layer"], e.p(u, n)), set()).add(e.h) + advanced = [] + go = [] + for e, u in batch: + # Step 7 over the batch: another pair of it with the same key and other content. + if self.conflicted(e, u) or len(keys[(u, e.ref["layer"], e.p(u, n))]) > 1: + out.of(e.h)["refused"].append((u, "layer-conflict")) + self.freeze(u, out) + else: + go.append((e, u)) + for e, u in go: + self.passed[(u, e.ref["layer"], e.p(u, n))] = e.h + why = self.steps_9_10_apply(e, u) + if why is None: + out.of(e.h)["applied"].append(u) + advanced.append(u) + else: + out.of(e.h)["refused"].append((u, why)) + # The envelopes held for a unit whose stage advanced are released together. + batch = [] + for u in advanced: + for h, x in sorted(self.held): + if x != u or (h, x) not in self.held: + continue + self.held.discard((h, x)) + e = self.retained[h] + if self.conflicted(e, u): + out.of(h)["refused"].append((u, "layer-conflict")) + self.freeze(u, out) + continue + k = self.readiness(e, u) + if k == "ready": + batch.append((e, u)) + elif k == "held": + self.held.add((h, u)) + else: + out.of(h)["refused"].append((u, "prev-mismatch")) + + def drop_unheld(self): + """§4.5: the end of an offer, a release or an expiry drops what no unit holds.""" + keep = self.held_envelopes() + self.retained = {h: e for h, e in self.retained.items() if h in keep} + + def expire(self, now, out): + for h, u in sorted(self.held): + if max(now - self.retained[h].arrival, 0) > self.cfg["bound"]["max_age_ms"]: + self.held.discard((h, u)) + out.of(h)["refused"].append((u, "layer-gap")) + self.drop_unheld() def unit_sha(self, u): s = self.units[u] @@ -621,62 +819,74 @@ def canonical(elem, data): return struct.pack(f"<{len(data)}h", *data) if elem == "i16" else bytes(data) -def step1(env): - h = content_hash(env) - m, payload = parse_wai1(env) - sec = m.get("security") - if isinstance(sec, dict) and sec.get("encryption") not in (None, "none"): - raise ValueError("encrypted") - if "refines" not in m or m["refines"] is None: - return refuse(h, "refines-malformed") - try: - ref = parse_refines(m["refines"]) - except Malformed: - return refuse(h, "refines-malformed") - cap = m["model_requirement"]["capability"] - if cap not in UNIT_KIND: - return refuse(h, "capability-not-refinement") - return (h, cap, ref, payload) - - class Sink: + """The bases a sink decoded, and the envelopes it holds for bases it has not.""" + def __init__(self, cfg): - self.cfg, self.chains, self.pending = cfg, {}, [] + self.cfg, self.bases, self.waiting = cfg, {}, [] - def offer(self, env, at, ev): - s = step1(env) - if isinstance(s, Report): - return [s] - h, cap, ref, payload = s - if ref["base"] in self.chains: - outside = len(self.pending) + sum(c.held_envelopes() for b, c in self.chains.items() if b != ref["base"]) - return self.chains[ref["base"]].offer_checked(h, cap, ref, payload, at, ev, outside) - r = Report(h) - if any(p["hash"] == h for p in self.pending): - r.held_for_base = True - return [r] - if len(self.pending) + sum(c.held_envelopes() for c in self.chains.values()) >= self.cfg["bound"]["max_envelopes"]: - return [refuse(h, "base-absent")] - self.pending.append(dict(hash=h, cap=cap, ref=ref, payload=payload, arrival=at, evidence=ev)) - r.held_for_base = True - return [r] + def held_elsewhere(self, base): + return len(self.waiting) + sum(len(b.held_envelopes()) for k, b in self.bases.items() if k != base) - def add_base(self, base, units): - chain = Chain(base, units, self.cfg) - mine = [p for p in self.pending if p["ref"]["base"] == base["hash"]] - self.pending = [p for p in self.pending if p["ref"]["base"] != base["hash"]] - reps = chain.offer_batch(mine) if mine else [] - self.chains[base["hash"]] = chain - return reps + def offer(self, env, at, evidence): + """§4.1 for one received envelope. NotWai1 or Encrypted when the bytes are not a + WAI1 this sink reads (an encrypted layer is decrypted before step 6, §14).""" + h = content_hash(env) + m, _, payload = read_wai1(env) + if not plaintext(m): + raise Encrypted("encrypted") + out = Outcome() + try: # step 1 + ref = parse_refines(m.get("refines")) + except Malformed: + out.refuse(h, "refines-malformed") + return out.reports() + mr = m["model_requirement"] + if mr["capability"] not in UNIT_KIND or mr.get("fallback") is not None: + out.refuse(h, "capability-not-refinement") + return out.reports() + e = Envelope(h, mr["capability"], ref, payload, at, evidence) + base = self.bases.get(ref["base"]) + if base is not None: # step 2 + base.receive([e], out, self.held_elsewhere(ref["base"]), True) + return out.reports() + # Held for its base (§4.3), once; stronger receipt evidence is kept. + same = next((x for x in self.waiting if x.h == h), None) + if same is not None: + if EVIDENCE[evidence] > EVIDENCE[same.evidence]: + same.evidence, same.verified = evidence, at + out.of(h)["held_for_base"] = True + return out.reports() + if self.held_elsewhere(None) >= self.cfg["bound"]["max_envelopes"]: + out.refuse(h, "base-absent") + return out.reports() + self.waiting.append(e) + out.of(h)["held_for_base"] = True + return out.reports() + + def add_base(self, info, units): + """A decoded base: the envelopes held for it are released together (§4.4). A + base this sink holds already is kept as it is (§4.4).""" + if info["hash"] in self.bases: + return [] + b = self.bases[info["hash"]] = Base(info, units, self.cfg) + mine = [e for e in self.waiting if e.ref["base"] == info["hash"]] + self.waiting = [e for e in self.waiting if e.ref["base"] != info["hash"]] + out = Outcome() + if mine: + b.receive(mine, out, 0, False) + return out.reports() def expire(self, now): + """§4.3: what was held past the bound's duration.""" + out = Outcome() age = self.cfg["bound"]["max_age_ms"] - gone = [p for p in self.pending if now - p["arrival"] > age] - self.pending = [p for p in self.pending if not now - p["arrival"] > age] - out = [refuse(p["hash"], "base-absent") for p in gone] - for b in sorted(self.chains): - out += self.chains[b].expire(now) - return out + for e in [x for x in self.waiting if max(now - x.arrival, 0) > age]: + out.refuse(e.h, "base-absent") + self.waiting = [x for x in self.waiting if not max(now - x.arrival, 0) > age] + for k in sorted(self.bases): + self.bases[k].expire(now, out) + return out.reports() # ---------------------------------------------------------------- base decodes @@ -794,26 +1004,6 @@ def decode_flac(b): return (info["total"], 1, ch), out -# ---------------------------------------------------------------- the schedule (§8) - - -def present(clock, base_ms, layers, lead): - out = [] - for u, b in enumerate(base_ms): - due = clock["t0_ms"] + u * clock["unit_ms"] + clock["target_latency_ms"] - stage = expected = 0 - in_time = True - for li, arr in enumerate(layers): - t = arr[u] if u < len(arr) else None - if t is None: - break - expected += 1 - in_time = in_time and t + (lead[li] if li < len(lead) else 0) <= due - if in_time: - stage += 1 - out.append({"at_ms": max(due, b), "stage": stage, "expected": expected}) - return out - # ---------------------------------------------------------------- WAI2 identity (SPEC §7.1) @@ -871,17 +1061,47 @@ def node(t, i): return (s, j, None) -def parse_wai2(b): - if b[:4] != b"WAI2": - raise ValueError("not WAI2") +class NotWai2(ValueError): + pass + + +def read_wai2(b): + """A WAI2 envelope (SPEC §7.1): its manifest text, its entries and their payloads. + The manifest is read strictly (SPEC §2) and its members checked; the table has one + entry per rendition, contiguous from offset 0, within the bytes.""" + if len(b) < 8 or b[:4] != b"WAI2": + raise NotWai2("not WAI2") ml = struct.unpack_from(" len(b): + raise NotWai2("truncated manifest") + try: + m = strict_json(b[8:8 + ml]) + check_manifest_v2(m) + except (NotJSON, Schema) as e: + raise NotWai2(str(e)) text = b[8:8 + ml].decode("utf-8") at = 8 + ml + if at + 2 > len(b): + raise NotWai2("truncated table") n = struct.unpack_from(" len(b): + raise NotWai2("truncated table") + start, expect, payloads = at + 8 * n, 0, [] + for i in range(n): + off, ln = struct.unpack_from(" len(b): + raise NotWai2("a rendition outside the payload block") + payloads.append(b[start + off:start + off + ln]) + expect += ln + return text, m["renditions"], payloads + + +def parse_wai2(b): + text, _, payloads = read_wai2(b) + return text, payloads def is_component_id(s): @@ -938,7 +1158,7 @@ def explode_each(b): raw = lambda n: text[n[0]:n[1]] # noqa: E731 ids, out = {}, [] for i, e in enumerate(root["renditions"][2]): - k, meta = e[2], json.loads(raw(e)) + k, meta = e[2], strict_json(raw(e).encode("utf-8")) try: if has_member(k.get("envelope_manifest"), text): man = raw(k["envelope_manifest"]) @@ -1022,24 +1242,23 @@ def check_refinement(entries, i): def manifests_check(b): - text, payloads = parse_wai2(b) - entries = json.loads(text)["renditions"] + _, entries, _ = read_wai2(b) ex = explode_each(b) ids = {} for i, (m, env) in enumerate(zip(entries, ex)): em = m.get("envelope_manifest") if em is not None: try: - ok = (isinstance(em, dict) and all(isinstance(em.get(k), str) for k in ("wai", "media", "intent")) - and em["model_requirement"]["capability"] == m["capability"] + check_manifest_v1(em) # a WAI1 manifest (SPEC §3) + ok = (em["model_requirement"]["capability"] == m["capability"] and em["conditioning"]["kind"] == m["kind"] - and em["conditioning"].get("prior") == m.get("prior") + and pin_of(em["conditioning"].get("prior")) == pin_of(m.get("prior")) and (m["capability"] not in UNIT_KIND or em["model_requirement"].get("fallback") is None)) if ok: a = None if m.get("refines") is None else refines_json(parse_refines_ids(m["refines"]), lambda x: ids[x]) c = None if em.get("refines") is None else refines_json(parse_refines(em["refines"]), lambda x: x) ok = a == c - except (Malformed, KeyError, TypeError): + except (Malformed, Schema, KeyError, TypeError): ok = False if not ok: return [i, "component-manifest-mismatch"] @@ -1048,9 +1267,18 @@ def manifests_check(b): return None +def pin_of(p): + """A pin as SPEC §3.1 reads it: an optional member that is null is absent.""" + if p is None: + return None + out = {k: v for k, v in p.items() if not (k in ("form", "tier", "supersedes") and v is None)} + if isinstance(out.get("supersedes"), dict): + out["supersedes"] = {k: v for k, v in out["supersedes"].items() if not (k == "form" and v is None)} + return out + + def refinements_of_0(b): - text, payloads = parse_wai2(b) - entries = json.loads(text)["renditions"] + _, entries, payloads = read_wai2(b) base_id = entries[0].get("id") out = [] for i, (m, p) in enumerate(zip(entries, payloads)): @@ -1067,48 +1295,110 @@ def refinements_of_0(b): return out + + +def depth(v): + if isinstance(v, dict): + return 1 + max([depth(x) for x in v.values()] + [0]) + if isinstance(v, list): + return 1 + max([depth(x) for x in v] + [0]) + return 0 + + +def packs(sources): + """Whether WAI1 envelopes can be packed into a WAI2 that explodes back to them byte + for byte (SPEC §7.1): each a WAI1 in plaintext, with no bytes before or after its + manifest's JSON value and none after its payload, its manifest nested so that, as an + entry's envelope_manifest three levels down, it stays within the depth limit; a + refinement naming earlier envelopes only, and naming no fallback.""" + if not sources: + return False + hashes = [] + for env in sources: + try: + m, text, payload = read_wai1(env) + except NotWai1: + return False + if not plaintext(m) or text[:1] != b"{" or text[-1:] != b"}" or len(env) != 12 + len(text) + len(payload): + return False + if depth(m) + 3 > DEPTH_LIMIT: + return False + if m["model_requirement"]["capability"] in UNIT_KIND: + try: + ref = parse_refines(m.get("refines")) + except Malformed: + return False + if ref["base"] not in hashes or any(p not in hashes for p, _ in ref["runs"]): + return False + if m["model_requirement"].get("fallback") is not None: + return False + hashes.append(content_hash(env)) + return True + + +def wai2_expect(b): + """The four checks wai2/index.json states for a WAI2.""" + _, entries, _ = read_wai2(b) + ex = explode_each(b) + return {"roles": role_check(entries), "manifests": manifests_check(b), + "exploded": [None if e is None else content_hash(e) for e in ex], "refinements_of_0": refinements_of_0(b)} + + def verify_wai2(d, fails): idx = json.load(open(os.path.join(d, "wai2", "index.json"))) for f in idx["files"]: b = open(os.path.join(d, "wai2", f["file"]), "rb").read() - text, _ = parse_wai2(b) - entries = json.loads(text)["renditions"] - ex = explode_each(b) - got = { - "roles": role_check(entries), - "manifests": manifests_check(b), - "exploded": [None if e is None else content_hash(e) for e in ex], - "refinements_of_0": refinements_of_0(b), - } + got = wai2_expect(b) if got != f["expect"]: fails.append(f"wai2/{f['file']}") print(f"FAIL wai2/{f['file']}: {got} != {f['expect']}") srcs = [open(os.path.join(d, "wai2", s), "rb").read() for s in f["sources"]] - if srcs and ex != srcs: + if srcs and explode_each(b) != srcs: fails.append(f"wai2/{f['file']} sources") print(f"FAIL wai2/{f['file']}: does not explode to its sources") - return len(idx["files"]) + for p in idx["packs"]: + srcs = [open(os.path.join(d, "wai2", s), "rb").read() for s in p["sources"]] + if packs(srcs) != p["packs"]: + fails.append(f"wai2 pack {p['name']}") + print(f"FAIL wai2 pack {p['name']}: packs is {packs(srcs)}, the index states {p['packs']}") + return len(idx["files"]) + len(idx["packs"]) -# ---------------------------------------------------------------- main +# ---------------------------------------------------------------- the schedule (§8) -def verify_case(cdir, fails): +def present(clock, base_ms, layers, lead): + """What a sink presents for a trace: each unit at its presentation time (or when its + base arrives, if later), at the stage of the run of layers that arrived in time. The + clock saturates at 2^64 − 1.""" + out = [] + for u, b in enumerate(base_ms): + due = min(clock["t0_ms"] + u * clock["unit_ms"] + clock["target_latency_ms"], U64) + stage = expected = 0 + in_time = True + for li, arr in enumerate(layers): + t = arr[u] if u < len(arr) else None + if t is None: + break + expected += 1 + t_lead = t + (lead[li] if li < len(lead) else 0) + in_time = in_time and t_lead <= U64 and t_lead <= due + if in_time: + stage += 1 + out.append({"at_ms": max(due, b), "stage": stage, "expected": expected}) + return out + + +# ---------------------------------------------------------------- cases + + +def load_case(cdir): + """A case's bases (info and stage-0 units), its envelopes, config and steps.""" case = json.load(open(os.path.join(cdir, "case.json"))) - name = case["name"] - - def fail(m): - fails.append(f"{name}: {m}") - print(f"FAIL {name}: {m}") bases = [] for b in case["bases"]: env = open(os.path.join(cdir, b["file"]), "rb").read() s0 = open(os.path.join(cdir, b["stage0"]), "rb").read() - if hashlib.sha256(s0).hexdigest() != b["stage0_sha256"]: - fail(f"{b['stage0']} does not have its SHA-256") - if content_hash(env) != b["object"]: - fail(f"{b['file']} does not have its content hash") - m, payload = parse_wai1(env) units, at = [], 0 for u in b["units"]: shape, elem = tuple(u["shape"]), u["elem"] @@ -1120,8 +1410,89 @@ def verify_case(cdir, fails): data = list(struct.unpack_from(f"<{n}h", s0, at)) at += 2 * n units.append((shape, elem, data)) - via = b["decoded_via"] - kind = b["decode"]["kind"] + bases.append({"hash": content_hash(env), "capability": b["decoded_via"], "via_fallback": b["via_fallback"], + "attested": b["attested"], "tier": b["tier"], "first_unit": b["first_unit"], "units": units, + "env": env, "s0": s0, "spec": b}) + files = {f["file"]: open(os.path.join(cdir, f["file"]), "rb").read() for f in case["files"]} + return case, bases, files + + +def replay(case, bases, files): + """Every step's reports and every base's final units, as a sink of this verifier + reaches them. An envelope that is not a WAI1 this sink reads is the step's error.""" + sink = Sink(case["config"]) + got = [] + for st in case["steps"]: + if st["op"] == "offer": + try: + got.append(sink.offer(files[st["file"]], st["at_ms"], st["evidence"])) + except Encrypted: + got.append({"error": "encrypted"}) + except NotWai1: + got.append({"error": "not-wai1"}) + elif st["op"] == "base": + b = bases[st["index"]] + got.append(sink.add_base(b, b["units"])) + else: + got.append(sink.expire(st["at_ms"])) + finals = [] + for b in bases: + c = sink.bases.get(b["hash"]) + if c is None: + finals.append(None) + continue + finals.append({"units": [{"stage": s["stage"], "producer": s["producer"], "frozen": s["frozen"], "tier": s["tier"], + "shape": list(s["shape"]), "stage_sha256": c.unit_sha(u)} for u, s in enumerate(c.units)], + "clip_sha256": c.clip_sha()}) + return got, finals + + +def replay_dir(cdir): + """A case directory replayed: {"reports": [...], "finals": [...]}, each step's + reports in object order.""" + case, bases, files = load_case(cdir) + got, finals = replay(case, bases, files) + return {"reports": [sorted(g, key=lambda r: r["object"]) if isinstance(g, list) else g for g in got], "finals": finals} + + +VECTORS = os.path.join(WAI, "wai-rs", "tests", "vectors") +VECTOR_DIGESTS = {} + + +def vector_digests(): + """The committed integer-codec vectors' bitstreams, by SHA-256, with the decoded + output's digests their sidecar (sha256.json) states: `X_bitstream.ext` or `X.ext` + under entry `X`.""" + out = {} + for d in sorted(os.listdir(VECTORS)): + side = os.path.join(VECTORS, d, "sha256.json") + if not os.path.exists(side): + continue + entries = json.load(open(side)) + for f in os.listdir(os.path.join(VECTORS, d)): + stem, ext = os.path.splitext(f) + key = stem[:-len("_bitstream")] if stem.endswith("_bitstream") else stem + if ext in (".wih1", ".wia1", ".wis1", ".wiv1") and key in entries: + b = open(os.path.join(VECTORS, d, f), "rb").read() + out[hashlib.sha256(b).hexdigest()] = (f"{d}/{f}", entries[key]) + return out + + +def verify_case(cdir, fails): + case, bases, files = load_case(cdir) + name = case["name"] + + def fail(m): + fails.append(f"{name}: {m}") + print(f"FAIL {name}: {m}") + for b in bases: + spec = b["spec"] + if hashlib.sha256(b["s0"]).hexdigest() != spec["stage0_sha256"]: + fail(f"{spec['stage0']} does not have its SHA-256") + if b["hash"] != spec["object"]: + fail(f"{spec['file']} does not have its content hash") + m, _, payload = read_wai1(b["env"]) + via, kind = spec["decoded_via"], spec["decode"]["kind"] independent = None if kind == "png": independent = [decode_png(payload)] @@ -1130,51 +1501,45 @@ def verify_case(cdir, fails): elif via == "wai.video.int_motion": v = IPV.parse_wiv1(payload, 0) independent = [((v["h"], v["w"], 3), list(f)) for f in IPV.decode_wiv1_k0(v)] - if independent is not None: - if [(s, d) for s, _, d in units] != independent: - fail(f"the independent decode of {b['file']} is not its stage0") - if base_tier(via, m, b["pin_resolved"]) != b["tier"]: - fail(f"the tier of {b['file']} is not {b['tier']}") - bases.append(dict(hash=content_hash(env), capability=via, via_fallback=b["via_fallback"], - attested=b["attested"], tier=b["tier"]), ) - bases[-1]["units"] = units - files = {f["file"]: open(os.path.join(cdir, f["file"]), "rb").read() for f in case["files"]} + if independent is not None and [(s, d) for s, _, d in b["units"]] != independent: + fail(f"the independent decode of {spec['file']} is not its stage0") + # A base whose payload is a committed vector's bitstream: its stage 0 is the + # decoded output that vector's sidecar states. + known = VECTOR_DIGESTS.get(hashlib.sha256(payload).hexdigest()) + if kind == "int" and known is not None: + name_v, digests = known + if "sha256" in digests: + ok = hashlib.sha256(b["s0"]).hexdigest() == digests["sha256"] + else: + ok = [hashlib.sha256(canonical(e, d)).hexdigest() for _, e, d in b["units"]] == digests["frames_sha256"] + if not ok: + fail(f"the stage0 of {spec['file']} is not the decoded output {name_v}'s sidecar states") + if base_tier(via, m, spec["pin_resolved"]) != spec["tier"]: + fail(f"the tier of {spec['file']} is not {spec['tier']}") for f in case["files"]: if content_hash(files[f["file"]]) != f["object"] or len(files[f["file"]]) != f["bytes"]: fail(f"{f['file']} is not the object the case names") - cfg = case["config"] - sink = Sink(cfg) - got = [] - for st in case["steps"]: - if st["op"] == "offer": - reps = sink.offer(files[st["file"]], st["at_ms"], st["evidence"]) - elif st["op"] == "base": - b = bases[st["index"]] - reps = sink.add_base(b, b["units"]) - else: - reps = sink.expire(st["at_ms"]) - got.append([r.json() for r in reps]) - for i, (g, e) in enumerate(zip(got, case["expect"]["reports"])): - if g != e: - fail(f"step {i}: reports {g} != {e}") - if len(got) != len(case["expect"]["reports"]): + got, finals = replay(case, bases, files) + expect = case["expect"]["reports"] + if len(got) != len(expect): fail("a different number of steps") - for bi, b in enumerate(bases): - exp = case["expect"]["bases"][bi] - c = sink.chains.get(b["hash"]) - if c is None: - if exp is not None: - fail(f"base {bi} was never decoded") + for i, (g, e) in enumerate(zip(got, expect)): + if not isinstance(g, list) or sorted(g, key=lambda r: r["object"]) != sorted(e, key=lambda r: r["object"]): + fail(f"step {i}: reports {g} != {e}") + for bi, (got_b, exp) in enumerate(zip(finals, case["expect"]["bases"])): + if got_b is None or exp is None: + if got_b != exp: + fail(f"base {bi}: decoded {got_b is not None}, the case states {exp is not None}") continue - units = [{"stage": s["stage"], "producer": s["producer"], "frozen": s["frozen"], "tier": s["tier"], - "shape": list(s["shape"]), "stage_sha256": c.unit_sha(u)} for u, s in enumerate(c.units)] - if units != exp["units"]: + if got_b["units"] != exp["units"]: fail(f"base {bi}: final units differ") - if c.clip_sha() != exp["clip_sha256"]: + if got_b["clip_sha256"] != exp["clip_sha256"]: fail(f"base {bi}: the clip hash differs") def main(d): + global VECTOR_DIGESTS + VECTOR_DIGESTS = vector_digests() fails = [] cases = sorted(os.listdir(os.path.join(d, "cases"))) for name in cases: @@ -1187,7 +1552,7 @@ def main(d): if p != t["presented"] or misses != t["deadline_misses"]: fails.append(f"schedule trace {i}") print(f"FAIL schedule trace {i}") - print(f"{len(cases)} cases, {n_wai2} WAI2 files, schedule: {len(sched['traces'])} traces, {len(fails)} failure(s)") + print(f"{len(cases)} cases, {n_wai2} WAI2 checks, schedule: {len(sched['traces'])} traces, {len(fails)} failure(s)") return 1 if fails else 0 -- 2.54.0 (Apple Git-157) From e4cf48c3e079ef4b37cc4b3ca25ab621bc9df608 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 4 Oct 2026 20:29:32 -0400 Subject: [PATCH 19/36] =?UTF-8?q?wai:=20is=5Fenhancement=5Flayer=20reads?= =?UTF-8?q?=20the=20admission=20table;=20a=20test=20reads=20=C2=A74.2=20an?= =?UTF-8?q?d=20A.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - codecs::is_enhancement_layer is read from BASE_ADMISSION (the capabilities applied to a base as an enhancement) rather than naming one capability. - staged::tests::staged_delivery_text_lists_the_codes reads the extension and requires §4.2's table to list StageRefusal's reasons and A.6 to list Wir1Refusal's codes, each in its order; the CI paths comment for staged-delivery.md now names that test. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-conformance.yml | 4 ++++ wai/wai-rs/src/codecs/mod.rs | 5 +++-- wai/wai-rs/src/staged/tests.rs | 26 ++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wai-conformance.yml b/.github/workflows/wai-conformance.yml index 1ece4fb70..823da2153 100644 --- a/.github/workflows/wai-conformance.yml +++ b/.github/workflows/wai-conformance.yml @@ -33,6 +33,8 @@ on: # A library test checks SPEC §5's pinned integer payloads against this # extension's sections, so an edit of it is checked here too. - 'wai/extensions/integer-payloads.md' + # A library test (staged::tests::staged_delivery_text_lists_the_codes) checks + # that this extension's §4.2 and A.6 list the codes the sink reports. - 'wai/extensions/staged-delivery.md' - 'wai/wai-web/**' - '.github/workflows/wai-conformance.yml' @@ -56,6 +58,8 @@ on: # A library test checks SPEC §5's pinned integer payloads against this # extension's sections, so an edit of it is checked here too. - 'wai/extensions/integer-payloads.md' + # A library test (staged::tests::staged_delivery_text_lists_the_codes) checks + # that this extension's §4.2 and A.6 list the codes the sink reports. - 'wai/extensions/staged-delivery.md' - 'wai/wai-web/**' - '.github/workflows/wai-conformance.yml' diff --git a/wai/wai-rs/src/codecs/mod.rs b/wai/wai-rs/src/codecs/mod.rs index cf96bf6b6..1edc0f3da 100644 --- a/wai/wai-rs/src/codecs/mod.rs +++ b/wai/wai-rs/src/codecs/mod.rs @@ -647,9 +647,10 @@ pub fn is_companion_only(cap: &str) -> bool { /// whose payload was encoded against one specific base encode, so it binds to /// exactly one selectable rendition of one of its [`enhancement_bases`], never /// to "every rendition of that codec" (SPEC §5, §7.1). Every enhancement layer -/// is also companion-only. +/// is also companion-only. Read from [`BASE_ADMISSION`]: the capabilities applied +/// to a base as an enhancement. pub fn is_enhancement_layer(cap: &str) -> bool { - cap == CAP_VIDEO_LCEVC + applied_form(cap) == Some(AppliedForm::Enhancement) } /// The base codecs WAI registers `wai.video.lcevc` over (SPEC §5): AVC, HEVC, diff --git a/wai/wai-rs/src/staged/tests.rs b/wai/wai-rs/src/staged/tests.rs index 4ff2ab51b..0676d88fc 100644 --- a/wai/wai-rs/src/staged/tests.rs +++ b/wai/wai-rs/src/staged/tests.rs @@ -201,6 +201,32 @@ fn wir1_refusal_order() { } } +/// The extension's text lists the codes this module reports, in its order: §4.2's +/// table the reasons of [`super::StageRefusal::ALL`] (two rows at step 2 and one row +/// listing three at step 6 included), and A.6 the codes of [`Wir1Refusal::ALL`]. +#[test] +fn staged_delivery_text_lists_the_codes() { + let text = include_str!("../../../extensions/staged-delivery.md"); + let at = text.find("### 4.2 Reasons").expect("§4.2"); + let table = &text[at..at + text[at..].find("The list is closed").expect("the table's end")]; + let mut reasons: Vec<&str> = Vec::new(); + for row in table.lines().filter(|l| l.starts_with("| ") && !l.starts_with("| step") ) { + let cell = row.split('|').nth(2).unwrap_or(""); + for code in cell.split('`').skip(1).step_by(2) { + if !reasons.contains(&code) { + reasons.push(code); + } + } + } + let want: Vec<&str> = super::StageRefusal::ALL.iter().map(|r| r.code()).collect(); + assert_eq!(reasons, want, "§4.2 lists the reasons StageRefusal reports, in its order"); + let a6 = &text[text.find("### A.6 Refusal codes").expect("A.6")..]; + let list = &a6[..a6.find(". A parser").expect("the list's end")]; + let codes: Vec<&str> = list.split('`').skip(1).step_by(2).filter(|c| !c.contains('.')).collect(); + let want: Vec<&str> = Wir1Refusal::ALL.iter().map(|r| r.code()).collect(); + assert_eq!(codes, want, "A.6 lists the codes Wir1Refusal reports, in its order"); +} + /// A stream that runs out is refused at its first missing symbol, and a word after /// the last symbol is not canonical; both before any unit is applied, by /// `check_streams`, and by the per-unit decode. -- 2.54.0 (Apple Git-157) From 3b769ee4bedae5c2051d44649eae30a7ab9c7792 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 4 Oct 2026 20:29:32 -0400 Subject: [PATCH 20/36] wai: wai stage refine takes the layer's shape from --out-dims --dims is the stage-0 shape a --stage0 base is read at; with --stage0 the layer's own shape could not be given. --out-dims gives it in every case, and tests/cli_stage.rs refines a --stage0 base by up2 to an odd 31x31. Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/src/bin/wai_cli/stage.rs | 15 ++++++++------- wai/wai-rs/tests/cli_stage.rs | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/wai/wai-rs/src/bin/wai_cli/stage.rs b/wai/wai-rs/src/bin/wai_cli/stage.rs index ab7012ea8..a6a8084b7 100644 --- a/wai/wai-rs/src/bin/wai_cli/stage.rs +++ b/wai/wai-rs/src/bin/wai_cli/stage.rs @@ -172,11 +172,12 @@ fn apply(args: &[String]) -> Result<(), String> { } /// `wai stage refine --base [--on ]… --target --q N -/// [--predictor identity|up2|rate:NUM/DEN] [--span F:C] [--dims D0,D1,D2] +/// [--predictor identity|up2|rate:NUM/DEN] [--span F:C] [--out-dims D0,D1,D2] /// [--max-symbols N] [--table-per-unit] -o L.wai`: a residual layer toward `--target` -/// (the span's units' canonical buffers concatenated, at `--dims`, by default the +/// (the span's units' canonical buffers concatenated, at `--out-dims`, by default the /// predictor's output shape), over the stage the `--on` layers give; `prev` names the -/// producers the span holds. +/// producers the span holds. `--dims` is the stage-0 shape a `--stage0` base is read +/// at, never the layer's. fn refine(args: &[String]) -> Result<(), String> { let base_path = opt(args, "--base").ok_or("--base ")?; let out = opt(args, "-o").ok_or("-o ")?; @@ -223,9 +224,9 @@ fn refine(args: &[String]) -> Result<(), String> { } p => return Err(format!("--predictor {p}: identity, up2 or rate:NUM/DEN")), }; - let out_dims = match opt(args, "--dims") { - Some(d) if opt(args, "--stage0").is_none() => dims(d)?, - _ => match predictor { + let out_dims = match opt(args, "--out-dims") { + Some(d) => dims(d)?, + None => match predictor { Predictor::Identity => below_shape, Predictor::Up2 => (below_shape.0 * 2, below_shape.1 * 2, below_shape.2), Predictor::Rate => (wai::staged::predict::rate_len(below_shape.0 as u64, rate.0, rate.1).ok_or("rate")? as u32, 1, below_shape.2), @@ -367,7 +368,7 @@ pub fn main(args: &[String]) -> Result<(), String> { } pub const USAGE: &str = "wai stage refine --base [--on ]… --target --q N \ -[--predictor identity|up2|rate:NUM/DEN] [--span F:C] [--max-symbols N] [--table-per-unit] -o L.wai +[--predictor identity|up2|rate:NUM/DEN] [--span F:C] [--out-dims D0,D1,D2] [--max-symbols N] [--table-per-unit] -o L.wai wai stage apply … offer the layers in order; print each report and the units wai stage inspect a layer's refines, WIR1 header and declared cost wai stage pack … -o x.wai pack WAI1 envelopes into a WAI2 (manifests verbatim) diff --git a/wai/wai-rs/tests/cli_stage.rs b/wai/wai-rs/tests/cli_stage.rs index 0086e6b53..a2a2dcc49 100644 --- a/wai/wai-rs/tests/cli_stage.rs +++ b/wai/wai-rs/tests/cli_stage.rs @@ -88,6 +88,25 @@ fn refine_then_apply_reaches_the_target() { assert!(o.status.success() && t.contains("roles: ✓") && t.contains("layer 1") && t.contains("streams: ✓"), "{t}"); } +/// `wai stage refine` over a base read from `--stage0` at `--dims` writes a layer at +/// `--out-dims`: up2 from 16×16 to an odd 31×31 (the predictor's default would be +/// 32×32), which `wai stage inspect` reads back. +#[test] +fn refine_honours_out_dims_over_stage0() { + let d = corpus("cases/image_up2_odd"); + let target: Vec = (0..31 * 31 * 3).map(|i| ((i * 29) % 253) as u8).collect(); + let tpath = scratch("target31.rgb"); + std::fs::write(&tpath, &target).unwrap(); + let lpath = scratch("L1_31.wai"); + let o = wai(&[os("stage"), os("refine"), os("--base"), d.join("base.wai").as_os_str(), os("--stage0"), d.join("stage0.bin").as_os_str(), + os("--units"), os("1"), os("--dims"), os("16,16,3"), os("--predictor"), os("up2"), os("--out-dims"), os("31,31,3"), + os("--target"), tpath.as_os_str(), os("--q"), os("1"), os("-o"), lpath.as_os_str()]); + assert!(o.status.success(), "{}", text(&o)); + let o = wai(&[os("stage"), os("inspect"), lpath.as_os_str()]); + let t = text(&o); + assert!(o.status.success() && t.contains("1 units of 31x31x3") && t.contains("predictor up2"), "{t}"); +} + /// `wai stage pack` packs the corpus's live-style chain into a WAI2 that `wai stage /// explode` turns back into each source, byte for byte. #[test] -- 2.54.0 (Apple Git-157) From 9b61af63ed67fb01c15ecf4a3f6ac25b740c4e86 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 4 Oct 2026 20:32:09 -0400 Subject: [PATCH 21/36] wai: a staged case of envelopes a strict reader refuses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The corpus replay records an offer that is not a WAI1 the sink reads as the step's error (not-wai1, encrypted). The new case unreadable_envelopes offers a member named twice (in refines and at the top), NaN, a number past the float range, a lone surrogate, a byte-order mark, a manifest nested 128 deep, a manifest with no media and an encrypted layer, each refused before §4.1; then a layer nested 127 deep, which is read and applies. staged_verify.py compares an error step as it is. Co-Authored-By: Claude Opus 5.5 --- .../cases/unreadable_envelopes/1e400.wai | Bin 0 -> 594 bytes .../cases/unreadable_envelopes/base.wai | Bin 0 -> 547 bytes .../cases/unreadable_envelopes/bom.wai | Bin 0 -> 586 bytes .../cases/unreadable_envelopes/case.json | 247 ++++++++++++++++++ .../cases/unreadable_envelopes/deep_127.wai | Bin 0 -> 841 bytes .../cases/unreadable_envelopes/deep_128.wai | Bin 0 -> 843 bytes .../unreadable_envelopes/dup_in_refines.wai | Bin 0 -> 593 bytes .../cases/unreadable_envelopes/dup_top.wai | Bin 0 -> 597 bytes .../cases/unreadable_envelopes/encrypted.wai | Bin 0 -> 631 bytes .../unreadable_envelopes/lone_surrogate.wai | Bin 0 -> 597 bytes .../cases/unreadable_envelopes/nan.wai | Bin 0 -> 592 bytes .../cases/unreadable_envelopes/no_media.wai | Bin 0 -> 567 bytes .../cases/unreadable_envelopes/stage0.bin | 1 + wai/staged-conformance/index.json | 7 + wai/staged-conformance/staged_verify.py | 4 +- wai/wai-rs/src/bin/wai_staged_vectors.rs | 34 ++- 16 files changed, 290 insertions(+), 3 deletions(-) create mode 100644 wai/staged-conformance/cases/unreadable_envelopes/1e400.wai create mode 100644 wai/staged-conformance/cases/unreadable_envelopes/base.wai create mode 100644 wai/staged-conformance/cases/unreadable_envelopes/bom.wai create mode 100644 wai/staged-conformance/cases/unreadable_envelopes/case.json create mode 100644 wai/staged-conformance/cases/unreadable_envelopes/deep_127.wai create mode 100644 wai/staged-conformance/cases/unreadable_envelopes/deep_128.wai create mode 100644 wai/staged-conformance/cases/unreadable_envelopes/dup_in_refines.wai create mode 100644 wai/staged-conformance/cases/unreadable_envelopes/dup_top.wai create mode 100644 wai/staged-conformance/cases/unreadable_envelopes/encrypted.wai create mode 100644 wai/staged-conformance/cases/unreadable_envelopes/lone_surrogate.wai create mode 100644 wai/staged-conformance/cases/unreadable_envelopes/nan.wai create mode 100644 wai/staged-conformance/cases/unreadable_envelopes/no_media.wai create mode 100644 wai/staged-conformance/cases/unreadable_envelopes/stage0.bin diff --git a/wai/staged-conformance/cases/unreadable_envelopes/1e400.wai b/wai/staged-conformance/cases/unreadable_envelopes/1e400.wai new file mode 100644 index 0000000000000000000000000000000000000000..30454f5e27ba0310d0206dab1bc491bb94801f11 GIT binary patch literal 594 zcmWG{^fdHiWMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_}h6D9y;y zJjKX3CE3_8+0Zc6G&MOX#mqR#IN3NQF~!WlG|Al1G}+L=Fxe!X?+1SeaU+WMv3+X-Q&HdMYqn@=9}Zbd;*9fTF1;1_rf17#JABJ%bDxfg#HT zWOG079iu#|NsAapaNk)$uM39hSdf@fd~eMmD3m)Rvcnr2zt!G@b@PJL-2nF zhW|j!z`)4h10+3wIQ0UXsW_0Xc=UStu|@BUmc^}DoO|4}UWWhDXPs$}Ej6T77Hobp z@#4m}voEGp1~ocI70Bi19dH$$w@J}RNX6`tRe|c7<})7`ysJ;~zs1@bu4u~g#NM^A z=7Rc{$1ChRE1z%u%y>F*g5T~osn9FOZG^2Z>*Vqs7mT#3h%K#{xOa7?yxTGM-(8H8 P>_6R7z3dt9apoof?ytdL literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/unreadable_envelopes/base.wai b/wai/staged-conformance/cases/unreadable_envelopes/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..0c56a09ad447b2c72f3ff9563f63211ce234d0fa GIT binary patch literal 547 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1c7#SGCJ;Mwcfix4)xg2Qt zDdR_p`f6F~NPAbpL2A^1K6!@tK2 z3?VNW82-OyU|97DNdE`90O&{{I0D21EPY?u=W|xx<@Gc;qGF#Dta*K7V!ZM3<5HZx zYt;l6dpQZeRF^xYd~F`X)|64^(ERy;F##TfLU61FMe#DqWR{ul5xlt(z}~y?p&5UFY^& z2V0r=HidhQDV5WMQskC0ypZz?)nUtLI(gridCei4Zq@7sEL9p>E;p2w1QieXxZd^o Ze6+kq#OnUpzeiU~^w}-rGY*ay0|3T|weJ7` literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/unreadable_envelopes/bom.wai b/wai/staged-conformance/cases/unreadable_envelopes/bom.wai new file mode 100644 index 0000000000000000000000000000000000000000..179b62b661573298326b53afcd1badb715703cc6 GIT binary patch literal 586 zcmWG{^fYv1WMFu|dw;c3d19uLm6D;Jfs&3=ZfZ(qB9K>>nUb0hOHx6K@>5cC;)_xXOEZg7bHVb}O38@@iAk9`nI)A#)j%Wlz((i+4FIZ4%gjqv zs?||S&d*E9EXmBz%gjqxvZ_|f&df^zDbFl21W7{_7K3Dx5{pxTG6u;ehNgyw7Rkv* zX-1ajDMrRA$;O7shK8x8smVzxX2waz$;K&(DP{(yN#=&8$%Y1o$tEc#NtQ{eX{kw; zMnKmV6s49?$E2LZ%G4qyD?^}5OA?FHQ-R@1>_kP#T7Oh7gV6f*;9 z7+?W1{`~*{p9d-s29ylrWnfrs02GK|U|2bgfnmiV28N)=3=DsNGB5=HXJGgb#0(6K z3_d{81Bg>Eu$hVj>550Mmmgd7&S+WOip9CdJ?mxoFMZaT_SjNGT4llJClfDjd^`JM zN@Y-^b5wy`e%=9B(RrH`jf7Op9$6Kru4z8=alyO#6#rYSt>KEMEKlrR3u`W@e|fyZ zzO(ZA*3XQm11I?HZj%bVa@atEQ-*Lf6tBTmtiivwyXUe-BWB=X7ILZFgE!E4O J@g8Sx0st4A!1w?F literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/unreadable_envelopes/case.json b/wai/staged-conformance/cases/unreadable_envelopes/case.json new file mode 100644 index 000000000..e42ce799b --- /dev/null +++ b/wai/staged-conformance/cases/unreadable_envelopes/case.json @@ -0,0 +1,247 @@ +{ + "format": "wai-staged-case/1", + "name": "unreadable_envelopes", + "about": "envelopes a reader of SPEC §2 and §3 refuses, so a sink never reaches §4.1: a member named twice (in refines, and at the top), NaN, a number past the float range, a lone surrogate, a byte-order mark, a manifest nested 128 deep, a manifest with no media; an encrypted layer; then the layer with a manifest nested 127 deep, which is read and applies", + "bases": [ + { + "file": "base.wai", + "object": "0c415118cc2f297d23dc31c11e5ecbd63b3c3dad605b715c101c4d4b9befeb92", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "4aecd199feb88f0107733bf13c8e38d76199270e5c89322a19ecc73d1553bd3d", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "dup_in_refines.wai", + "object": "0f0a75e14ff4e7bd4d557e586605ab79c24070423eac11ac82ccf4208cdfaf3b", + "bytes": 593 + }, + { + "file": "dup_top.wai", + "object": "53c771a4d9087abd78b9d2abd6b2d2c690d1e5a7413c76f1351f3db792485122", + "bytes": 597 + }, + { + "file": "nan.wai", + "object": "077eb3328f4f8bd46df6bd6c921ed8aa46a2df02acd89e3f93a050bb6d046909", + "bytes": 592 + }, + { + "file": "1e400.wai", + "object": "f71902b19339376abcbb47bd2f5e3df517a4f93d8fc8358dc4892e94a4b9b735", + "bytes": 594 + }, + { + "file": "lone_surrogate.wai", + "object": "0d7078f22f67f990b46f626de36cb2280085789166e7d02e9e21eff2ac78e748", + "bytes": 597 + }, + { + "file": "bom.wai", + "object": "c3886765584e2bdbd705b8032ca11025ae787e012ea4e1ad01682bfd9382e9b9", + "bytes": 586 + }, + { + "file": "deep_128.wai", + "object": "332f77f14b28aaf2a04a85098c5f2658ca4c00d29d1e7d8a60301eeff86869f0", + "bytes": 843 + }, + { + "file": "no_media.wai", + "object": "7e2dd695603969dc50ebf6f23c98885bc0a991aa43dd71e266ce895d702b22d0", + "bytes": 567 + }, + { + "file": "encrypted.wai", + "object": "a314b84d317cdcc745396dbf26026e98e3db24233e84b0fd6066c85a3dc3e6ca", + "bytes": 631 + }, + { + "file": "deep_127.wai", + "object": "e00515d669f9cc14511d771638c6f976689832a450f747854f5f94b6901d2056", + "bytes": 841 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "dup_in_refines.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "dup_top.wai", + "at_ms": 2, + "evidence": "none" + }, + { + "op": "offer", + "file": "nan.wai", + "at_ms": 3, + "evidence": "none" + }, + { + "op": "offer", + "file": "1e400.wai", + "at_ms": 4, + "evidence": "none" + }, + { + "op": "offer", + "file": "lone_surrogate.wai", + "at_ms": 5, + "evidence": "none" + }, + { + "op": "offer", + "file": "bom.wai", + "at_ms": 6, + "evidence": "none" + }, + { + "op": "offer", + "file": "deep_128.wai", + "at_ms": 7, + "evidence": "none" + }, + { + "op": "offer", + "file": "no_media.wai", + "at_ms": 8, + "evidence": "none" + }, + { + "op": "offer", + "file": "encrypted.wai", + "at_ms": 9, + "evidence": "none" + }, + { + "op": "offer", + "file": "deep_127.wai", + "at_ms": 10, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + { + "error": "not-wai1" + }, + { + "error": "not-wai1" + }, + { + "error": "not-wai1" + }, + { + "error": "not-wai1" + }, + { + "error": "not-wai1" + }, + { + "error": "not-wai1" + }, + { + "error": "not-wai1" + }, + { + "error": "not-wai1" + }, + { + "error": "encrypted" + }, + [ + { + "object": "e00515d669f9cc14511d771638c6f976689832a450f747854f5f94b6901d2056", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "e00515d669f9cc14511d771638c6f976689832a450f747854f5f94b6901d2056", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "beaeff60e70afe20466916ce1802b0a889be287762795f7e5ca2a500b806ff06" + }, + { + "stage": 1, + "producer": "e00515d669f9cc14511d771638c6f976689832a450f747854f5f94b6901d2056", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "e9ec935d0a73543fd7d28c4cea664e7bbddb9f13bdecb87559c7bba5ac3249c9" + } + ], + "clip_sha256": "f0b1480ddabbfb085acd12f2697ce2f033169c8e34e1c044298c03ce975573a2", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/unreadable_envelopes/deep_127.wai b/wai/staged-conformance/cases/unreadable_envelopes/deep_127.wai new file mode 100644 index 0000000000000000000000000000000000000000..5f4fc4e7775151b3067c2b5e2b27619fc0bd6d17 GIT binary patch literal 841 zcmWG{^fYv3VqmCNDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_}h6D9y;y zJjKX3CE3_8+0Zc6G&MOX#mqR#IN3NQF~!WlG|Al1G}+L=Fxe!X?+1SeaU+WMv3+X-Q&HdMYqn@=9}Zbd;*9l&qo$6vPfFsQtmfz!2^kWXQ;9*45VRz1<3gG|Nnm;s6ZG{GK`mjVYLBJAcBEmFFfcOs07(xZPQAcpDh{M89=%?EY|%TTWpOJO=N|X0m*KzkS!dc~OATq2 z1)HBtytwi0?29RtL568)2)WJ?{h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_}h6D9y;y zJjKX3CE3_8+0Zc6G&MOX#mqR#IN3NQF~!WlG|Al1G}+L=Fxe!X?+1SeaU+WMv3+X-Q&HdMYqn@=9}Zbd;*9l&qo$7Q_xLsQtmfz!2^kWXQ;9*45VRz1<3gG|Nnm;s6ZG{GK`mjVYLBJAcBEmFFfcOs07(xZPQAcpDh{M89=%?EY|%TTWpOJO=N|X0m*KzkS!dc~OATq2 z1)HBtytwi0?29RtL568)2)WJ?{h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_}h6D9y;y zJjKX3CE3_8+0Zc6G&MOX#mqR#IN3NQF~!WlG|Al1G}+L=Fxe!X?+1SeaU+WM!nI1Z5cl-CUAbl%5IudV$SU97tC@dcFMEqIX8i;#MrqJ?>dA!++_s&a}st8qz8YHb0qoapT+B z7gH*O8l9sGmqG-Y{W?^;-MLH*0) z751H#&$oVNJRLZ}Z+Dwi=#}F(!d91ca`}!6Mp{+GmR3yMyE;?e?HK#-F2+grpKhsM K_Kf#9a}xlpD8Y{a literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/unreadable_envelopes/dup_top.wai b/wai/staged-conformance/cases/unreadable_envelopes/dup_top.wai new file mode 100644 index 0000000000000000000000000000000000000000..02d97e4b8f1e388c242159857cdb0b7ea211b222 GIT binary patch literal 597 zcmWG{^fU})WMHUPDo@N*vQjeCGf>h|s;W}5GK5pPsVSL>K)JHal+=8nY-V0bYF-IY zpeVHu12R1?vXaG=U zT4r9VQmu|sa(-S)W=Up#US?jpl2x@*c4l4*NO@+FAxIjcuoxthlvtb!lrcy)F*G$a zv`9`iN;9%FPcbr1Nj5f2HZ)8%O-)WpF*8mwPBuh|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_pol?5QRKn01Z#kz(@ z7P{%lxj_4~Gl7~6f$AX!7lTYnN-RzVDl|woF*G$av`9`iN;9%FPcbr1Nj5f2HZ)8% zO-)WpF*8mwPBu2rbV_s=aPVEl{28M9YAVWrA@G}9~98k;*q+x&s$oTXB|9>8+Kp0RmjF*97 zwE<8df`MV>GzNwhhZq=w9y2if{mH-({GWm0KM*r8Ff#Z6Ne>`Sy})KF4x}p{yQnr0v9^XQnzB5xcP*^Bp#J6Y3j5B==UYEBo(`Pgx4TU$^vZD? qVXMnJxqQb3Bdsc8ODiVsU7acKc8vXZ7vm)RPq$Prd&Ya5xd{MsfX-V0 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/unreadable_envelopes/lone_surrogate.wai b/wai/staged-conformance/cases/unreadable_envelopes/lone_surrogate.wai new file mode 100644 index 0000000000000000000000000000000000000000..a97bc8411d8745d1c842dc275273d2d15af43cd0 GIT binary patch literal 597 zcmWG{^fU})WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_}h6D9y;y zJjKX3CE3_8+0Zc6G&MOX#mqR#IN3NQF~!WlG|Al1G}+L=Fxe!X?+1SeaU+WMv3+X-Q&HdMYqn@=9}Zbd;*9fEL7*rdSvlDAoR8U|l*CKpF;EfQ&!?|NrNK3WNbA!+04ORvQ2XA{ZD}PGewLafpE-=rIGs-=7Q& z!T%W;{sS=s10#bEkn{lJ)C+8;;y}9M(d*^M7QHiC7Pn$??s3n08U9P3b*4SG)R0zL zu=&ZviyPn0zL-)O)aV>lAeWzaz*ThKCPgD56|+ZH1*&VB&wO0)u0F;87Hey`qAAN0 zd)LC63+i7Uudwf|e7^NFh|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_}h6D9y;y zJjKX3CE3_8+0Zc6G&MOX#mqR#IN3NQF~!WlG|Al1G}+L=Fxe!X?+1SeaU+WMv3+X-Q&HdMYqn@=9}Zbd;*9l&t&`{c3+OFffFB1{pE}!<7lh z=73^mAPoa7K*pc{|Nrwq1;T)mVZ00ss||nx5ey6~r!g?BIK;pZ^q7I+?@tDX;QtH^ z|ACl+fsw%nNO}Nq>IF7aaUfms==Jhri{2S6i(9cc_qb=h4F9FiI@2CoYDlXr*!*PT z#f@)gUrebCYIKe&kju|I;3_(AlcJH3irFKp0@XFmXFe`?SD)g4i?uae(Uj$hy=!63 z1@$kFSJ-z}KHvJ8@pRw>zuj$8p;wOE2wPp&$>lpP7->}zTUs%3@9Io>w`1(TyBH_g Of4Zf5*)!hb%uN96w!ts} literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/unreadable_envelopes/no_media.wai b/wai/staged-conformance/cases/unreadable_envelopes/no_media.wai new file mode 100644 index 0000000000000000000000000000000000000000..f51260dcc9be550dd5681360dede001d36409c6a GIT binary patch literal 567 zcmWG{^fWYPWMHUPDo@N*vQjeCGf>h|%FHWC%_{+Ni&6`6GLsWaQh|cG`6;P6@kObH zrI|&kxnTKfrR2nd#H7re%#uo=YM>E%Wtl0d`FcPDfGX26^HP;+b(E6x^HMTPGV}8? z^U{^9s+F=c^HM;{Gm8vC(h!BkAep4Z;#8oFL9&UVsiC1oa?6V7TO!=H%4=U|?Vf_Y5**1O^`ykj(+b%s?6jSb&T_|NsBzfeM5H zCBt|b7*-nq1tJ(2R!(DJSaFDfA?Ps!!{46_48i{y82$q>0|O(250LZ#;?xUlrs6=l z;?e8n#}>UaS{Aoraqe-?dKvyppLM1^w$zYTS+M!Z#ETo>&c2vZ8Pw<;RUnt2cfeJ2 z-X=vOAr-SnRt2hSn$LV(@UA|^{}yX&xS}b`6MNUfnhWY*9 String { b.iter().map(|x| format!("{x:02x}")).collect() @@ -1065,6 +1065,29 @@ fn cases(ctx: &Ctx) -> Vec { ("L1_span_null.wai".into(), span_null), ])); } + { + let a = small(148); + let (l1, _) = l1_of(&a, 149, 4); + let t = String::from_utf8(wai::container::manifest_bytes(&l1).expect("WAI1").to_vec()).expect("UTF-8"); + let with = |u: String| { + assert_ne!(u, t); + raw_manifest(&l1, &u) + }; + let deep = format!("{},\"zz\":{}{}}}", &t[..t.len() - 1], "[".repeat(127), "]".repeat(127)); + let ok_deep = format!("{},\"zz\":{}{}}}", &t[..t.len() - 1], "[".repeat(126), "]".repeat(126)); + out.push(refusals("unreadable_envelopes", "envelopes a reader of SPEC §2 and §3 refuses, so a sink never reaches §4.1: a member named twice (in refines, and at the top), NaN, a number past the float range, a lone surrogate, a byte-order mark, a manifest nested 128 deep, a manifest with no media; an encrypted layer; then the layer with a manifest nested 127 deep, which is read and applies", a, vec![ + ("dup_in_refines.wai".into(), with(t.replacen("\"layer\":1", "\"layer\":2,\"layer\":1", 1))), + ("dup_top.wai".into(), with(t.replacen("\"media\":", "\"zz\":1,\"zz\":1,\"media\":", 1))), + ("nan.wai".into(), with(format!("{},\"zz\":NaN}}", &t[..t.len() - 1]))), + ("1e400.wai".into(), with(format!("{},\"zz\":1e400}}", &t[..t.len() - 1]))), + ("lone_surrogate.wai".into(), with(format!("{},\"zz\":\"\\ud800\"}}", &t[..t.len() - 1]))), + ("bom.wai".into(), with(format!("\u{feff}{t}"))), + ("deep_128.wai".into(), with(deep)), + ("no_media.wai".into(), with(t.replacen("\"media\":\"video\",", "", 1))), + ("encrypted.wai".into(), edit(&l1, |w| w.manifest.security = Some(serde_json::from_value(json!({"encryption": "aes-128-gcm", "kid": 1})).expect("security")))), + ("deep_127.wai".into(), with(ok_deep)), + ])); + } // ---- step 2 ---- { let a = small(107); @@ -1295,7 +1318,14 @@ fn run(r: &Replay) -> (Vec, Vec) { let mut reports = Vec::new(); for s in &r.steps { let got = match s { - Step::Offer { file, at_ms, evidence } => sink.offer(&r.files[file], *at_ms, *evidence).expect("a WAI1 envelope"), + Step::Offer { file, at_ms, evidence } => match sink.offer(&r.files[file], *at_ms, *evidence) { + Ok(got) => got, + Err(e) => { + // Not a WAI1 this sink reads: the step's outcome is the error. + reports.push(json!({"error": match e { OfferError::NotWai1(_) => "not-wai1", OfferError::Encrypted => "encrypted" }})); + continue; + } + }, Step::Base { index, .. } => { let (info, units) = r.bases[*index].clone(); sink.add_base(info, units) -- 2.54.0 (Apple Git-157) From 854a339950e05c64c1528f24236828326fceedbe Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 4 Oct 2026 20:32:56 -0400 Subject: [PATCH 22/36] wai: the CHANGELOG states the staged fix round's rules and interfaces Co-Authored-By: Claude Opus 5.5 --- wai/CHANGELOG.md | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index a612eb777..f1eb96ce9 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -14,9 +14,9 @@ wire format may change and such changes are recorded here. `prev` as one producer or runs, `layer`, `span`). A sink holds per unit a stage, a producer and a tier, and applies a layer per unit by one algorithm with a closed list of reasons (§4.1, §4.2): every envelope-scoped step (well-formed `refines`, a - refinement capability, the base present, not through a fallback, admitted, attested, - a well-formed payload within the sink's cost limit) before the unit steps - (conflict, readiness, geometry, deadline). Layers a unit is not ready for are held + refinement capability with no fallback, the base present, not through a fallback, + admitted, attested, a well-formed payload within the sink's cost limit and its chain + budget) before the unit steps (conflict, readiness, geometry, deadline). Layers a unit is not ready for are held to a declared bound and released when its stage advances; layers released together form a batch, so two conflicting layers released together are both refused and the unit frozen, whatever their order. A layer that differs from one that already passed @@ -27,6 +27,16 @@ wire format may change and such changes are recorded here. have reached on time), attestation (a receipt verified without the layer's bytes never authorizes an application) and byte counting (envelope bytes; receipts, framing and parameter sets separately; never JSON or base64) are stated. +- **Resource bounds** (§4.5, §14, A.5): a sink declares a chain budget per base, over + its units' output, 32 bytes per stage, and the payload, declared output and declared + working bytes of every layer it retains; a layer that would pass it is refused + (`chain-over-limit`) before any stream is decoded, and a layer no unit is held for is + dropped. The declared working bytes count the stream index. A sink's work per layer + is linear in its payload and declared symbols up to a logarithmic factor in its + units, and §14 states how. Decoding a base again keeps its chain; a layer held for + its base and offered again keeps the strongest receipt evidence, and over an + attested base counts as arriving when its receipt was verified; a set split across + bases shares one live clock (`first_unit`). - **The residual payload `WIR1`** (Appendix A): one rANS stream per unit; `clamp(pred + r·q)` with the identity, up2 (the half-pel arithmetic, odd sizes included) and rate (linear interpolation, `rdiv`'s halves away from zero, channels @@ -57,12 +67,20 @@ wire format may change and such changes are recorded here. ids replaced by the content hashes of the named entries' exploded envelopes, so a component and the layer carried alone are the same object; `WaiMulti::pack` packs `WAI1` envelopes with their manifests verbatim, and they explode back to the same - bytes. A malformed `refines` or a mismatched `envelope_manifest` does not make a + bytes; it refuses an envelope it could not give back (bytes around a manifest's JSON + value or after a payload, or a manifest that inside a `WAI2` passes the depth limit) + and checks its output before returning it. Each entry explodes on its own, so one + that cannot leaves the others intact. A refinement's `envelope_manifest` names no + fallback. A malformed `refines` or a mismatched `envelope_manifest` does not make a manifest unreadable. +- **SPEC §2**: member names are unique within each object of a manifest, at any depth, + and an integer member is written as an integer (`-0`, `1.0` and `1e0` are not); both + readers refuse a manifest that breaks either. `span: null` is `span` absent. - **Reference implementation**: `staged::{StageChain, StageSink, wir1, predict, wir1_arith, tier, schedule}` (feature `staged`); `container::{Refines, Prev}`; - `WaiMulti::{explode, explode_all, pack, check_envelope_manifests, refinements_of}`; - `wai stage refine|apply|inspect|pack|explode`; `wai wrap` refuses a refinement + `WaiMulti::{explode, explode_each, explode_all, pack, check_envelope_manifests, + refinements_of}`; `container::check_unique_members`; `wai stage + refine|apply|inspect|pack|explode` (`refine --out-dims` gives a layer's shape); `wai wrap` refuses a refinement capability, and `wai inspect` shows `refines`. `RansEncoder` writes the bypass escape, and `int_entropy::hist_to_cdf` builds a table from a histogram in integer arithmetic. In `ternary::rans_encode_trits`, the encode call sat inside -- 2.54.0 (Apple Git-157) From 77df55cca8e696b19ef33155c4b3fece75ff7393 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 4 Oct 2026 20:50:01 -0400 Subject: [PATCH 23/36] wai: a retained layer reserves the stage each covered unit gains MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The chain budget counted a retained layer's payload, declared output and declared working bytes, but not the 32 bytes of the stage each covered unit gains when the layer applies; for units smaller than that, applying a layer admitted at the budget could take the chain past it. The reserve now counts 32 bytes per covered unit, in the sink, the spec (§4.5), staged_verify.py, the generator and the tests; chain_budget applies a layer over 64 one-pixel frames at its budget and requires the state to stay within it. The two budget cases are regenerated with their outcomes unchanged. Co-Authored-By: Claude Opus 5.5 --- wai/CHANGELOG.md | 2 +- wai/extensions/staged-delivery.md | 16 ++++++++------- .../cases/chain_budget_retained/case.json | 2 +- .../cases/chain_over_limit/case.json | 4 ++-- wai/staged-conformance/index.json | 2 +- wai/staged-conformance/staged_verify.py | 6 +++--- wai/wai-rs/src/bin/wai_staged_vectors.rs | 7 ++++--- wai/wai-rs/src/staged/mod.rs | 10 ++++++++-- wai/wai-rs/src/staged/tests.rs | 20 ++++++++++++++++++- wai/wai-rs/tests/staged_resources.rs | 4 ++-- 10 files changed, 50 insertions(+), 23 deletions(-) diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index f1eb96ce9..c367a4962 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -29,7 +29,7 @@ wire format may change and such changes are recorded here. framing and parameter sets separately; never JSON or base64) are stated. - **Resource bounds** (§4.5, §14, A.5): a sink declares a chain budget per base, over its units' output, 32 bytes per stage, and the payload, declared output and declared - working bytes of every layer it retains; a layer that would pass it is refused + working bytes of every layer it retains with 32 bytes per unit it covers; a layer that would pass it is refused (`chain-over-limit`) before any stream is decoded, and a layer no unit is held for is dropped. The declared working bytes count the stream index. A sink's work per layer is linear in its payload and declared symbols up to a logarithmic factor in its diff --git a/wai/extensions/staged-delivery.md b/wai/extensions/staged-delivery.md index b935a6c5a..bc83f3006 100644 --- a/wai/extensions/staged-delivery.md +++ b/wai/extensions/staged-delivery.md @@ -193,16 +193,18 @@ chain's **state** is the sum of: - for every unit, the bytes of its current output (1 per u8 element, 2 per i16 sample), and 32 bytes for each stage above 0 (the producers step 7 compares); -- for every layer the chain **retains**, its payload's bytes and its declared output - and working bytes (Appendix A.5). +- for every layer the chain **retains**, its payload's bytes, its declared output and + working bytes (Appendix A.5), and 32 bytes for each unit it covers (the stage it + would add). A chain retains a layer from the moment it passes step 6 to the end of the offer, release or expiry after which no unit is held for it; the sink then drops it. At step -6, after the cost limit, a layer whose payload, declared output and declared working -bytes, added to the state, pass the budget is refused (`chain-over-limit`) before any -stream is decoded. An envelope the chain still retains, received again, adds -nothing. Applying a layer replaces a unit's output with one the layer's declared -output already counted, so the bytes a chain holds never pass its budget. +6, after the cost limit, a layer whose payload, declared output and working bytes and +32 bytes for each unit it covers, added to the state, pass the budget is refused +(`chain-over-limit`) before any stream is decoded. An envelope the chain still +retains, received again, adds nothing. Applying a layer to a unit replaces its output +with one, and adds a stage, that the layer's retention already counted, so the bytes +a chain holds never pass its budget. ## 5. The tier of a stage, and the stage hash diff --git a/wai/staged-conformance/cases/chain_budget_retained/case.json b/wai/staged-conformance/cases/chain_budget_retained/case.json index ddae7bdb3..d672a73b1 100644 --- a/wai/staged-conformance/cases/chain_budget_retained/case.json +++ b/wai/staged-conformance/cases/chain_budget_retained/case.json @@ -56,7 +56,7 @@ "max_age_ms": 1000 }, "cost_limit": null, - "budget": 1820, + "budget": 1884, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/chain_over_limit/case.json b/wai/staged-conformance/cases/chain_over_limit/case.json index 8349c3559..f09e88ae3 100644 --- a/wai/staged-conformance/cases/chain_over_limit/case.json +++ b/wai/staged-conformance/cases/chain_over_limit/case.json @@ -1,7 +1,7 @@ { "format": "wai-staged-case/1", "name": "chain_over_limit", - "about": "a chain budget one byte short of the base's units and the layer's payload, declared output and declared working bytes: chain-over-limit, before any stream is decoded", + "about": "a chain budget one byte short of the base's units and the layer's payload, declared output and working bytes and 32 bytes a unit: chain-over-limit, before any stream is decoded", "bases": [ { "file": "base.wai", @@ -51,7 +51,7 @@ "max_age_ms": 1000 }, "cost_limit": null, - "budget": 1271, + "budget": 1335, "live": null }, "steps": [ diff --git a/wai/staged-conformance/index.json b/wai/staged-conformance/index.json index cdcca6955..7e32fc0f5 100644 --- a/wai/staged-conformance/index.json +++ b/wai/staged-conformance/index.json @@ -314,7 +314,7 @@ }, { "name": "chain_over_limit", - "about": "a chain budget one byte short of the base's units and the layer's payload, declared output and declared working bytes: chain-over-limit, before any stream is decoded", + "about": "a chain budget one byte short of the base's units and the layer's payload, declared output and working bytes and 32 bytes a unit: chain-over-limit, before any stream is decoded", "clip_sha256": [ "b139902d21228247e2a7901db2077ca0e346c6f34c398b62be71d4516a1dcdf5" ] diff --git a/wai/staged-conformance/staged_verify.py b/wai/staged-conformance/staged_verify.py index 43ecdd1f1..fd7092708 100644 --- a/wai/staged-conformance/staged_verify.py +++ b/wai/staged-conformance/staged_verify.py @@ -616,9 +616,9 @@ class Base: def state(self): """§4.5: units' output bytes and 32 bytes per stage above 0; retained layers' - payload bytes, declared output and declared working bytes.""" + payload bytes, declared output and working bytes, and 32 bytes a unit covered.""" s = sum(unit_bytes(x) + 32 * x["stage"] for x in self.units) - return s + sum(len(e.payload) + e.cost[0] + e.cost[2] for e in self.retained.values()) + return s + sum(len(e.payload) + e.cost[0] + e.cost[2] + 32 * e.w["n_units"] for e in self.retained.values()) # -- §4.1 steps 2 to 6, for an envelope whose base this is @@ -646,7 +646,7 @@ class Base: for got, key, what in zip(c, ("output_bytes", "symbols", "working_bytes"), ("output bytes", "symbols", "working bytes")): if got > lim[key]: return ("cost-over-limit", what) - if self.state() + len(e.payload) + c[0] + c[2] > self.cfg["budget"]: + if self.state() + len(e.payload) + c[0] + c[2] + 32 * w["n_units"] > self.cfg["budget"]: return ("chain-over-limit", None) try: check_streams(w) diff --git a/wai/wai-rs/src/bin/wai_staged_vectors.rs b/wai/wai-rs/src/bin/wai_staged_vectors.rs index 3069d1391..82f4d81e9 100644 --- a/wai/wai-rs/src/bin/wai_staged_vectors.rs +++ b/wai/wai-rs/src/bin/wai_staged_vectors.rs @@ -1136,7 +1136,7 @@ fn cases(ctx: &Ctx) -> Vec { let a = small(115); let (l1, _) = l1_of(&a, 116, 4); let state0: u64 = a.units.iter().map(|u| u.samples.byte_len()).sum(); - out.push(Case { name: "chain_over_limit", about: "a chain budget one byte short of the base's units and the layer's payload, declared output and declared working bytes: chain-over-limit, before any stream is decoded", bases: vec![a], + out.push(Case { name: "chain_over_limit", about: "a chain budget one byte short of the base's units and the layer's payload, declared output and working bytes and 32 bytes a unit: chain-over-limit, before any stream is decoded", bases: vec![a], files: vec![("L1.wai".into(), l1.clone())], cfg: ChainConfig { budget: state0 + reserve(&l1) - 1, ..cfg() }, steps: vec![base_at(0, 0), offer("L1.wai", 1)] }); } { @@ -1282,8 +1282,9 @@ fn raw_manifest(env: &[u8], text: &str) -> Vec { /// What a layer adds to its chain's state while retained (staged-delivery §4.5). fn reserve(env: &[u8]) -> u64 { let w = Wai::from_bytes(env).expect("WAI1"); - let c = wir1::parse(&w.payload, wai::staged::unit_kind(w.capability()).expect("a refinement")).expect("WIR1").declared_cost(); - w.payload.len() as u64 + c.output_bytes + c.working_bytes + let p = wir1::parse(&w.payload, wai::staged::unit_kind(w.capability()).expect("a refinement")).expect("WIR1"); + let c = p.declared_cost(); + w.payload.len() as u64 + c.output_bytes + c.working_bytes + 32 * p.n_units as u64 } // ───────────────────────── replay ───────────────────────── diff --git a/wai/wai-rs/src/staged/mod.rs b/wai/wai-rs/src/staged/mod.rs index f30b40f5c..eb95acbc2 100644 --- a/wai/wai-rs/src/staged/mod.rs +++ b/wai/wai-rs/src/staged/mod.rs @@ -496,7 +496,8 @@ impl StageChain { } /// The chain's state (§4.5): every unit's output bytes and 32 bytes per stage above - /// 0, and every retained layer's payload, declared output and declared working bytes. + /// 0, and every retained layer's payload, declared output and declared working + /// bytes, and 32 bytes for each unit it covers. pub fn state_bytes(&self) -> u64 { self.state } @@ -541,7 +542,12 @@ impl StageChain { return Err((CostOverLimit, Some(what))); } } - let reserve = (payload.len() as u64).saturating_add(c.output_bytes).saturating_add(c.working_bytes); + // The stage each covered unit gains when the layer applies (32 bytes) is counted + // with its output, so what an application adds was counted at admission. + let reserve = (payload.len() as u64) + .saturating_add(c.output_bytes) + .saturating_add(c.working_bytes) + .saturating_add(32 * count as u64); if self.state.saturating_add(reserve) > self.cfg.budget { return Err((ChainOverLimit, None)); } diff --git a/wai/wai-rs/src/staged/tests.rs b/wai/wai-rs/src/staged/tests.rs index 0676d88fc..33ca2def2 100644 --- a/wai/wai-rs/src/staged/tests.rs +++ b/wai/wai-rs/src/staged/tests.rs @@ -976,7 +976,7 @@ fn chain_budget() { let p = Wai::from_bytes(&l1).unwrap().payload; let cost = parse(&p, UnitKind::Rgb8).unwrap().declared_cost(); let units_bytes = 4 * (H * W * 3) as u64; - let need = units_bytes + p.len() as u64 + cost.output_bytes + cost.working_bytes; + let need = units_bytes + p.len() as u64 + cost.output_bytes + cost.working_bytes + 4 * 32; let mut c = StageChain::new(info.clone(), units.clone(), ChainConfig { budget: need - 1, ..cfg() }); assert_eq!(c.state_bytes(), units_bytes); let r = &c.offer(&l1, 0, ReceiptEvidence::None).unwrap()[0]; @@ -996,6 +996,24 @@ fn chain_budget() { assert_eq!(c.offer(&l1, 0, ReceiptEvidence::None).unwrap()[0].applied, vec![0, 1, 2, 3]); assert_eq!(c.state_bytes(), units_bytes + 4 * 32); assert_eq!(c.unit(0).unwrap().producers(), &[h(&l1)]); + // Tiny units: a 1×1 frame holds 3 bytes and each stage 32. Admitted at the budget, + // the layer leaves the chain within it once applied. + let (_, info, _) = base4(); + let b = info.content_hash; + let tiny: Vec = (0..64u32).map(|f| UnitBuf { shape: (1, 1, 3), samples: Samples::U8(vec![f as u8; 3]) }).collect(); + let belows: Vec> = (0..64u32).map(|f| vec![f as u8; 3]).collect(); + let targets: Vec> = (0..64u32).map(|f| vec![(f * 3) as u8, 7, 200]).collect(); + let bb: Vec = belows.iter().map(|v| Below::U8(v)).collect(); + let tt: Vec = targets.iter().map(|v| Below::U8(v)).collect(); + let (payload, _) = encode(UnitKind::Rgb8, &bb, (1, 1, 3), &tt, (1, 1, 3), ¶ms(1, Predictor::Identity, 31, false)).unwrap(); + let cost = parse(&payload, UnitKind::Rgb8).unwrap().declared_cost(); + let need = 64 * 3 + payload.len() as u64 + cost.output_bytes + cost.working_bytes + 64 * 32; + let l = envelope(CAP_VIDEO_INT_REFINE, Some(Refines { base: hexs(&b), prev: one(&b), layer: 1, span: None }.to_value()), payload); + let mut c = StageChain::new(info.clone(), tiny.clone(), ChainConfig { budget: need - 1, ..cfg() }); + assert_eq!(c.offer(&l, 0, ReceiptEvidence::None).unwrap()[0].envelope, Some(StageRefusal::ChainOverLimit)); + let mut c = StageChain::new(info, tiny, ChainConfig { budget: need, ..cfg() }); + assert_eq!(c.offer(&l, 0, ReceiptEvidence::None).unwrap()[0].applied.len(), 64); + assert!(c.state_bytes() <= need, "the state {} passed the budget {need}", c.state_bytes()); } // ───────────────────────── WAI2 identity (SPEC §7.1) ───────────────────────── diff --git a/wai/wai-rs/tests/staged_resources.rs b/wai/wai-rs/tests/staged_resources.rs index 0b88fce0e..f59ad8230 100644 --- a/wai/wai-rs/tests/staged_resources.rs +++ b/wai/wai-rs/tests/staged_resources.rs @@ -222,7 +222,7 @@ fn staged_sink_resources() { let w = wir1::parse(&Wai::from_bytes(&l1).unwrap().payload, UnitKind::Rgb8).unwrap(); let c = w.declared_cost(); let unit_bytes: u64 = units.iter().map(|u| u.samples.byte_len()).sum(); - let reserve = payload_len + c.output_bytes + c.working_bytes; + let reserve = payload_len + c.output_bytes + c.working_bytes + 4 * 32; // One byte short: refused before any stream is decoded, nothing changes. let mut sink = StageSink::new(cfg(16, unit_bytes + reserve - 1)); sink.add_base(info.clone(), units.clone()); @@ -251,7 +251,7 @@ fn staged_sink_resources() { assert_eq!(r[0].held, vec![0, 1, 2, 3]); let p2 = Wai::from_bytes(&l2).unwrap().payload; let c2 = wir1::parse(&p2, UnitKind::Rgb8).unwrap().declared_cost(); - assert_eq!(sink.chain(&bh).unwrap().state_bytes(), unit_bytes + p2.len() as u64 + c2.output_bytes + c2.working_bytes); + assert_eq!(sink.chain(&bh).unwrap().state_bytes(), unit_bytes + p2.len() as u64 + c2.output_bytes + c2.working_bytes + 4 * 32); let r = sink.expire(10_000); assert_eq!(r[0].refused.len(), 4); assert_eq!(sink.chain(&bh).unwrap().state_bytes(), unit_bytes); -- 2.54.0 (Apple Git-157) From 9d1006e45612f67d86020448571f037501001399 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 4 Oct 2026 22:19:58 -0400 Subject: [PATCH 24/36] wai: a report's held units are filtered by binary search, not by scans OfferReport::sort dropped from `held` each unit also applied or refused with a linear scan of those lists per held unit, so a layer whose report mixed held units with applied or refused ones took time quadratic in its units. The lists are sorted first, and membership is a binary search. A unit's held layers are kept in id order, so is_held and an expiry find a layer by binary search too. tests/staged_resources.rs adds the two mixed shapes (half held and half applied; half held and half prev-mismatch) to its linearity checks. Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/src/staged/mod.rs | 13 +++++--- wai/wai-rs/tests/staged_resources.rs | 50 ++++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/wai/wai-rs/src/staged/mod.rs b/wai/wai-rs/src/staged/mod.rs index eb95acbc2..93759659a 100644 --- a/wai/wai-rs/src/staged/mod.rs +++ b/wai/wai-rs/src/staged/mod.rs @@ -269,11 +269,13 @@ impl OfferReport { } /// Units in order; a unit held and then decided within the same step is reported /// by its decision alone. + /// Linear in the units up to a log factor: membership is a binary search in the + /// sorted lists. fn sort(&mut self) { self.applied.sort_unstable(); self.refused.sort_unstable(); let (applied, refused) = (&self.applied, &self.refused); - self.held.retain(|u| !applied.contains(u) && !refused.iter().any(|(x, _)| x == u)); + self.held.retain(|u| applied.binary_search(u).is_err() && refused.binary_search_by_key(u, |&(x, _)| x).is_err()); self.held.sort_unstable(); self.held.dedup(); } @@ -611,7 +613,10 @@ impl StageChain { self.held_layers += 1; } l.held += 1; - self.held.entry(u).or_default().push(id); + // Kept in id order, so membership is a binary search. + let ids = self.held.entry(u).or_default(); + let at = ids.partition_point(|&x| x < id); + ids.insert(at, id); } /// Release every layer held for unit `u`. @@ -629,7 +634,7 @@ impl StageChain { /// Whether unit `u` is held for layer `id`. fn is_held(&self, id: u64, u: u32) -> bool { - self.held.get(&u).is_some_and(|ids| ids.contains(&id)) + self.layers[&id].held > 0 && self.held.get(&u).is_some_and(|ids| ids.binary_search(&id).is_ok()) } /// Freeze unit `u`: no further layer applies to it, and every layer held for it is @@ -906,7 +911,7 @@ impl StageChain { }; for u in from..from + count { let Some(ids) = self.held.get_mut(&u) else { continue }; - let Some(at) = ids.iter().position(|x| *x == id) else { continue }; + let Ok(at) = ids.binary_search(&id) else { continue }; ids.remove(at); if ids.is_empty() { self.held.remove(&u); diff --git a/wai/wai-rs/tests/staged_resources.rs b/wai/wai-rs/tests/staged_resources.rs index f59ad8230..886edb708 100644 --- a/wai/wai-rs/tests/staged_resources.rs +++ b/wai/wai-rs/tests/staged_resources.rs @@ -140,9 +140,11 @@ fn toward(base: &[u8; 32], prev: Prev, layer: u8, units: &[UnitBuf], f: impl Fn( Wai::new(m, payload).to_bytes().expect("serialise") } -/// The three shapes of offer, at `n` units: a layer 1 alone; a layer 2 whose `prev` is -/// one run per unit, held; a layer 2 held, then the layer 1 that releases it. -fn offers(n: u32) -> [Window; 3] { +/// The shapes of offer, at `n` units: a layer 1 alone; a layer 2 whose `prev` is one +/// run per unit, held; a layer 2 held, then the layer 1 that releases it; and a layer 2 +/// whose report mixes outcomes, with layer 1 applied to half the units: half held and +/// half applied, then half held and half prev-mismatch. +fn offers(n: u32) -> [Window; 5] { let bh = [0x5a; 32]; let (info, units) = base(n, 1, bh); let l1 = layer(&bh, Prev::One(hex(&bh)), 1, &units, 1); @@ -177,7 +179,38 @@ fn offers(n: u32) -> [Window; 3] { }); assert_eq!(r.0[0].held.len(), n as usize); assert_eq!((r.1[0].applied.len(), r.1[1].applied.len()), (n as usize, n as usize)); - [single, runs, held] + + let half = n / 2; + let upper: Vec = l1_out[half as usize..].to_vec(); + let base_upper: Vec = { + let (_, all) = base(n, 1, bh); + all[half as usize..].to_vec() + }; + let l1_upper = { + let e = layer(&bh, Prev::One(hex(&bh)), 1, &base_upper, 1); + let mut w = Wai::from_bytes(&e).unwrap(); + w.manifest = w.manifest.clone().with_refines(&Refines { base: hex(&bh), prev: Prev::One(hex(&bh)), layer: 1, span: Some(wai::container::Span { from: half, count: n - half }) }); + w.to_bytes().unwrap() + }; + let h1u = wai::jwp_hash::content_hash(&l1_upper); + let all_mid: Vec = (0..n as usize).map(|i| if i < half as usize { base_upper[0].clone() } else { upper[i - half as usize].clone() }).collect(); + let mut mixed = Vec::new(); + for prev in [h1u, [0x11; 32]] { + let l2m = layer(&bh, Prev::One(hex(&prev)), 2, &all_mid, 3); + let (info, units) = base(n, 1, bh); + let mut sink = StageSink::new(cfg(16, u64::MAX)); + sink.add_base(info, units); + assert_eq!(sink.offer(&l1_upper, 0, ReceiptEvidence::None).unwrap()[0].applied.len(), (n - half) as usize); + let (r, w) = measure(|| sink.offer(&l2m, 1, ReceiptEvidence::None).unwrap()); + assert_eq!(r[0].held.len(), half as usize); + if prev == h1u { + assert_eq!(r[0].applied.len(), (n - half) as usize); + } else { + assert_eq!(r[0].refused.len(), (n - half) as usize); + } + mixed.push(w); + } + [single, runs, held, mixed[0], mixed[1]] } #[test] @@ -185,12 +218,12 @@ fn staged_sink_resources() { // ── Work per layer, linear in its units ── let sizes = [4096u32, 16384, 65536]; // The fastest of three runs at each size, so a busy machine does not decide. - let mut at: Vec<[Window; 3]> = Vec::new(); + let mut at: Vec<[Window; 5]> = Vec::new(); for &n in &sizes { let mut best = offers(n); for _ in 0..2 { let w = offers(n); - for k in 0..3 { + for k in 0..5 { if w[k].time < best[k].time { best[k].time = w[k].time; } @@ -198,7 +231,10 @@ fn staged_sink_resources() { } at.push(best); } - for (k, name) in ["layer 1 alone", "layer 2, a run per unit, held", "layer 2 held, then layer 1"].iter().enumerate() { + for (k, name) in ["layer 1 alone", "layer 2, a run per unit, held", "layer 2 held, then layer 1", "layer 2: half held, half applied", "layer 2: half held, half prev-mismatch"] + .iter() + .enumerate() + { for (i, &n) in sizes.iter().enumerate() { let w = at[i][k]; println!("{name:32} {n:6} units: {:9.3} ms {:10} bytes allocated {:7} allocations peak +{} bytes", -- 2.54.0 (Apple Git-157) From d6a6d7164e27425abad12bd155b2f8a180fa97c5 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 4 Oct 2026 22:23:04 -0400 Subject: [PATCH 25/36] wai: WIR1's table capacity is formed in u64 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parse sized its table vector by n_tables·cdf_len in usize, which passes 2^32 for a header of one table per unit, 2^20 units and tables of 4097 entries: a 32-bit debug build aborted on a 44-byte payload. The product is formed in u64, saturating, and capped by the bytes the payload holds. No other usize product in wir1.rs or staged/mod.rs is formed from header values: the predictors' indices are bounded by the stage below, whose length was checked. wir1_header_products_fit_any_usize parses such headers and requires stream_length; it also passes under wasm32-wasip1. Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/src/staged/tests.rs | 19 +++++++++++++++++++ wai/wai-rs/src/staged/wir1.rs | 6 ++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/wai/wai-rs/src/staged/tests.rs b/wai/wai-rs/src/staged/tests.rs index 33ca2def2..a3d62cd4c 100644 --- a/wai/wai-rs/src/staged/tests.rs +++ b/wai/wai-rs/src/staged/tests.rs @@ -227,6 +227,25 @@ fn staged_delivery_text_lists_the_codes() { assert_eq!(codes, want, "A.6 lists the codes Wir1Refusal reports, in its order"); } +/// A header whose sizes pass 32 bits when multiplied (one table per unit, 2^20 units, +/// tables of 4097 entries) is refused for the bytes it lacks, on a 32-bit target as on +/// a 64-bit one: no size is formed in a 32-bit `usize`. +#[test] +fn wir1_header_products_fit_any_usize() { + for (n_units, cdf_len) in [(1u32 << 20, 4097u16), (1 << 20, 4096), (1 << 19, 4097)] { + let mut p = Vec::new(); + p.extend_from_slice(b"WIR1"); + p.extend_from_slice(&[1, 1, 0, 1]); + for v in [n_units, 1u32, 1, 1, 0, 0, 1] { + p.extend_from_slice(&v.to_le_bytes()); + } + p.extend_from_slice(&0i32.to_le_bytes()); + p.extend_from_slice(&cdf_len.to_le_bytes()); + p.extend_from_slice(&[0, 0]); + assert_eq!(code(&p, UnitKind::Attr), "stream_length", "{n_units} units, tables of {cdf_len}"); + } +} + /// A stream that runs out is refused at its first missing symbol, and a word after /// the last symbol is not canonical; both before any unit is applied, by /// `check_streams`, and by the per-unit decode. diff --git a/wai/wai-rs/src/staged/wir1.rs b/wai/wai-rs/src/staged/wir1.rs index 89cc93bdb..4b3a04aa1 100644 --- a/wai/wai-rs/src/staged/wir1.rs +++ b/wai/wai-rs/src/staged/wir1.rs @@ -359,8 +359,10 @@ pub fn parse(p: &[u8], kind: UnitKind) -> Result { let mut c = Cur { b: p, p: HEADER }; let n_tables = if table_mode == 1 { n_units as usize } else { 1 }; // Sized by what the payload holds, so the parse allocates nothing its bytes do not - // justify; the tables are taken and checked one at a time (A.3 step 7). - let mut tables: Vec = Vec::with_capacity((n_tables * cdf_len).min((p.len() - c.p) / 4)); + // justify; the tables are taken and checked one at a time (A.3 step 7). The product + // is formed in u64: n_tables·cdf_len reaches 2^32 + 2^20, past a 32-bit usize. + let room = ((p.len() - c.p) / 4) as u64; + let mut tables: Vec = Vec::with_capacity((n_tables as u64).saturating_mul(cdf_len as u64).min(room) as usize); for _ in 0..n_tables { let t = c.take(cdf_len as u64 * 4, "a table")?; let at = tables.len(); -- 2.54.0 (Apple Git-157) From cc44ec303016aa80911a6b8bffcc0be5b754f821 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 4 Oct 2026 22:25:14 -0400 Subject: [PATCH 26/36] wai: an applied unit takes its share out of its layer's reserve MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A retained layer reserved one unit's output and 32 bytes for every unit it covers, and an application added the unit's new output and stage on top, so while a layer was applied to some units and held for others, state_bytes() counted those units twice and could pass the budget. Each application now releases the unit's share from the layer's reserve: the state rises only when a layer is admitted, and §4.5 states it. staged_verify.py counts the same. chain_budget offers a layer applied to half its units and held for the rest at a budget with no byte to spare and requires the state within it; the new case budget_partly_applied needs the released share to admit a further layer. Co-Authored-By: Claude Opus 5.5 --- wai/CHANGELOG.md | 5 +- wai/extensions/staged-delivery.md | 13 +- .../cases/budget_partly_applied/L1.wai | Bin 0 -> 535 bytes .../cases/budget_partly_applied/L1x.wai | Bin 0 -> 535 bytes .../cases/budget_partly_applied/L2.wai | Bin 0 -> 782 bytes .../cases/budget_partly_applied/base.wai | Bin 0 -> 547 bytes .../cases/budget_partly_applied/case.json | 181 ++++++++++++++++++ .../cases/budget_partly_applied/stage0.bin | 1 + wai/staged-conformance/index.json | 7 + wai/staged-conformance/staged_verify.py | 10 +- wai/wai-rs/src/bin/wai_staged_vectors.rs | 20 ++ wai/wai-rs/src/staged/mod.rs | 18 +- wai/wai-rs/src/staged/tests.rs | 20 ++ 13 files changed, 262 insertions(+), 13 deletions(-) create mode 100644 wai/staged-conformance/cases/budget_partly_applied/L1.wai create mode 100644 wai/staged-conformance/cases/budget_partly_applied/L1x.wai create mode 100644 wai/staged-conformance/cases/budget_partly_applied/L2.wai create mode 100644 wai/staged-conformance/cases/budget_partly_applied/base.wai create mode 100644 wai/staged-conformance/cases/budget_partly_applied/case.json create mode 100644 wai/staged-conformance/cases/budget_partly_applied/stage0.bin diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index c367a4962..231507a9d 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -28,8 +28,9 @@ wire format may change and such changes are recorded here. never authorizes an application) and byte counting (envelope bytes; receipts, framing and parameter sets separately; never JSON or base64) are stated. - **Resource bounds** (§4.5, §14, A.5): a sink declares a chain budget per base, over - its units' output, 32 bytes per stage, and the payload, declared output and declared - working bytes of every layer it retains with 32 bytes per unit it covers; a layer that would pass it is refused + its units' output, 32 bytes per stage, and the payload and declared working bytes of + every layer it retains with one unit's output and 32 bytes for each unit it covers + and has not been applied to; a layer that would pass it is refused (`chain-over-limit`) before any stream is decoded, and a layer no unit is held for is dropped. The declared working bytes count the stream index. A sink's work per layer is linear in its payload and declared symbols up to a logarithmic factor in its diff --git a/wai/extensions/staged-delivery.md b/wai/extensions/staged-delivery.md index bc83f3006..193c87e0b 100644 --- a/wai/extensions/staged-delivery.md +++ b/wai/extensions/staged-delivery.md @@ -193,9 +193,10 @@ chain's **state** is the sum of: - for every unit, the bytes of its current output (1 per u8 element, 2 per i16 sample), and 32 bytes for each stage above 0 (the producers step 7 compares); -- for every layer the chain **retains**, its payload's bytes, its declared output and - working bytes (Appendix A.5), and 32 bytes for each unit it covers (the stage it - would add). +- for every layer the chain **retains**, its payload's bytes and its declared working + bytes (Appendix A.5), and, for each unit it covers and has not been applied to, one + unit's output (its declared output over its units) and 32 bytes (the stage it would + add). A chain retains a layer from the moment it passes step 6 to the end of the offer, release or expiry after which no unit is held for it; the sink then drops it. At step @@ -203,8 +204,10 @@ release or expiry after which no unit is held for it; the sink then drops it. At 32 bytes for each unit it covers, added to the state, pass the budget is refused (`chain-over-limit`) before any stream is decoded. An envelope the chain still retains, received again, adds nothing. Applying a layer to a unit replaces its output -with one, and adds a stage, that the layer's retention already counted, so the bytes -a chain holds never pass its budget. +with one, and adds a stage, that the layer's retention counted, and that unit's share +leaves the layer's: an application lowers the state by the unit's old output. The +state rises only when a layer is admitted, so it never passes the budget, and the +bytes a chain holds never pass the state. ## 5. The tier of a stage, and the stage hash diff --git a/wai/staged-conformance/cases/budget_partly_applied/L1.wai b/wai/staged-conformance/cases/budget_partly_applied/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..2898b91febe84b4f0dbef72364eadfbbfa203ed5 GIT binary patch literal 535 zcmb_YO-ma=7=E{UQ1H-$7kjWU=MXa8x*s_xEwm6?5L?tkz%V;An+|Sf<78tAX-)xq z@}Px!5HBL)M-c=)i6;yGg@Tlx3gQnal&b5ysr?5X=AD`6gNJwC*=HqMu>jiSJyWDe zXud#lq$Y)8h!&NQF=C|?sS_lOtVfEoL}sQ~$f#oELQ@%8>*;O6S)BzcQb`Nl7@6-y z{wsBy!*vQ=J+Eckr(y!`RI`aL}L(hNR2nj83Y zJaV{xdj4+n_~6yr%0~6(+$vl>94S{uFP0upZhr`dUX*@z-(GfCm-_~D6Bpmir#q_- Br4;}G literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/budget_partly_applied/L1x.wai b/wai/staged-conformance/cases/budget_partly_applied/L1x.wai new file mode 100644 index 0000000000000000000000000000000000000000..f5d430b6aa2bcea949fd486f73500f4c0b74bca9 GIT binary patch literal 535 zcmWG{^fZiTWMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_l}+#)T- z(%3TDA~D$@+0fF|Ffqm0+#)H-z#=8lEYZR=+1$*?*wQ4)%+lPX?+1SeaU+WM!zMR9ujl2XbXvQGTwHl>x|OrJ!IktOfd_B(W$x70Age&B>`< z!@$50?ipmr2n=cAI CIH|S( literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/budget_partly_applied/L2.wai b/wai/staged-conformance/cases/budget_partly_applied/L2.wai new file mode 100644 index 0000000000000000000000000000000000000000..2dcaa1d4e4e2456dc05ac804ce047a277b7a5ff4 GIT binary patch literal 782 zcmY*WT}V_x7@f^T!4Qdj%Z%F(J*;x){>>dlBSb+6vWV77t+_LE<>q>KyWU-sWh1Bu zDaj)1Aqp|_kL|~o!Y7NghG9oOfsF9?svqYfoL4@@z}qh~yRKH+%0eM3)4l*EbU zL><3NxJ6|7Mnt&?5Kh~1xs(n|H0CnLcVe#Nwn`N#sm*a&V0Szi$bXC-fnt#kE)+zd zt=deb5Scc^DyAl(GKE-&gsI51EZJnlAf`@{p{Q8142%qeFa)`+5+q(5_jppOIGqxQ z7)wy@iC9V$PGp2-1<4p16miQyOjQ`hq9cZcTbS9Vf-I(4&?Xu;8Dpx6WYZ!@4<o@1z>1V;0gf4tAbw>`Z@sFY!Da% zOzdYx4L)n^s1c}r^&dRBp6;$GEaUWkb!G`&njG14@b%2z(D>UO^OvWmN|!&E-MqJd zU*bo@jR||fPfldKrTn3i2CsDZ>ge3Z?zaA&TgQ6O^~}<3CDo0Qcvaz-zRH}r;=IT> zyfRqxBJ^(L?zM;0y>i`BlevDRIqy`)%RNv~tuB6_{}j44(*(!*TKZq@&N&`=d3#{t rsj>2Yy0fU@jh-xA9_Z_ws{CbbubTVN61hNU*`3;NUR`dqDaii~ww(VE literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/budget_partly_applied/base.wai b/wai/staged-conformance/cases/budget_partly_applied/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..1e38e3f538dfc62cd2acab27787b2b67d57269a9 GIT binary patch literal 547 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1c7#SGCJ;Mwcfix4)xg2Qt zAT0h9f|{nMLW;HA!v;KAxlMapLJJqH3K5#YRWD10s?CU(HV*-<-T1sI{Tt;&bgr$ z*FVA%eof=7PBjo(TD|YPQM+{NgodxL14JC8SBfdv9#1XYxSZ)}SPxrKI%8Yw^tZ7o bn*TO!>sN?U{@8lxYUfJ7M{ifGQ5FOMoR`3- literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/budget_partly_applied/case.json b/wai/staged-conformance/cases/budget_partly_applied/case.json new file mode 100644 index 000000000..05902da71 --- /dev/null +++ b/wai/staged-conformance/cases/budget_partly_applied/case.json @@ -0,0 +1,181 @@ +{ + "format": "wai-staged-case/1", + "name": "budget_partly_applied", + "about": "layer 2 is applied to unit 1 and held for unit 0, so it stays retained; unit 1's share has left its reserve, which leaves room for layer 1x over unit 0 at a budget with no byte to spare; 1x applies and releases layer 2 for unit 0", + "bases": [ + { + "file": "base.wai", + "object": "7a1f3fd3c178fd939c8ac0c1951ad378bb08da6a85c762394b69766ad71ef3a7", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "19ac5dd5c595b4def507ec3aa0238718fc3d8972d44bb44cc5f9195e93abfd47", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "831449088f97460da5d77cab959903425e529f8a0670b80fcc47046b7da41753", + "bytes": 535 + }, + { + "file": "L2.wai", + "object": "a2681570d66d8e136ff0f3d30c0d72b901a2920b2721b288136c32e9826da15b", + "bytes": 782 + }, + { + "file": "L1x.wai", + "object": "ca2ea0790270916c7eb67d32d997a191aeb9df827bd4b1fa4e8ddd38708ba750", + "bytes": 535 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1940, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 2, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1x.wai", + "at_ms": 3, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "831449088f97460da5d77cab959903425e529f8a0670b80fcc47046b7da41753", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 1 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "a2681570d66d8e136ff0f3d30c0d72b901a2920b2721b288136c32e9826da15b", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 1 + ], + "held": [ + 0 + ], + "refused": [] + } + ], + [ + { + "object": "ca2ea0790270916c7eb67d32d997a191aeb9df827bd4b1fa4e8ddd38708ba750", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + }, + { + "object": "a2681570d66d8e136ff0f3d30c0d72b901a2920b2721b288136c32e9826da15b", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 2, + "producer": "a2681570d66d8e136ff0f3d30c0d72b901a2920b2721b288136c32e9826da15b", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "ab8af1bca23c023336e602a6d5b5418792976b8033d33c6c99b6c42a5e9c6894" + }, + { + "stage": 2, + "producer": "a2681570d66d8e136ff0f3d30c0d72b901a2920b2721b288136c32e9826da15b", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "1dde3ca447b566cc50731df85b84992f5ced961630c051adb80b5efb593145dd" + } + ], + "clip_sha256": "b74036d5d3befc48255321629b144b058dd516a33adb48bd65762b789ccad475", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/budget_partly_applied/stage0.bin b/wai/staged-conformance/cases/budget_partly_applied/stage0.bin new file mode 100644 index 000000000..610872968 --- /dev/null +++ b/wai/staged-conformance/cases/budget_partly_applied/stage0.bin @@ -0,0 +1 @@ + 08(0H8@H@X`P``X`xhpxhp�(@H8@`H``P`x`pxp€ˆx€�x� PX`H`p Ðèpxˆp€˜x� ˆ  ˜ °Xpx°ØøÀØÿÀðÿˆ˜ ˜ ¨ ¨¸ °Ð°èÿÀèÿÐøÿØøÿàÿÿ °È¸ÀÈ¸ÐØ€ˆ¨ÐøÿØÿÿðÿÿ°ÀȸÈÐÈÐàØØð˜ ¸¨¨Èøÿÿ¸ÀÐÀÈàÐàðÐèÿàøÿ°¸À¸ÀÐÈÐØÐàèØèðèøÿðÿÿÿÿÿ 08(0H8@H@X`P`hX`xhpxpp�(@H8H`H``P`x`pxp€ˆx€�x� PX`H`pXpppxˆx€˜x� ˆ  ˜ °Xpxhx€xx‡x�ŸÐøÿ˜ ¨  ¸ °Èhˆ�xˆ�ˆ˜ŸØøÿàÿÿèÿÿ¸ÀÈ¸ÐØ€ˆ¨ˆ˜¯ØÿÿðÿÿðÿÿÿÿÿÿÿÿØØð˜ ¸¨¨È°¯ÇÿÿÿÿÿÿÿÿÿÐèÿàðÿ°¸À¸ÀÐÈÐØÐàèÿÿÿèøÿðÿÿÿÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/index.json b/wai/staged-conformance/index.json index 7e32fc0f5..7b59fb7e2 100644 --- a/wai/staged-conformance/index.json +++ b/wai/staged-conformance/index.json @@ -326,6 +326,13 @@ "9b359fd5c3a4e4212911fdd0c0cbec31d00d46474656b49a655b4ef122bbded2" ] }, + { + "name": "budget_partly_applied", + "about": "layer 2 is applied to unit 1 and held for unit 0, so it stays retained; unit 1's share has left its reserve, which leaves room for layer 1x over unit 0 at a budget with no byte to spare; 1x applies and releases layer 2 for unit 0", + "clip_sha256": [ + "b74036d5d3befc48255321629b144b058dd516a33adb48bd65762b789ccad475" + ] + }, { "name": "frozen_refuses_held", "about": "a layer 3 held for both units; layer 1a applies, then layer 1b conflicts: the units freeze, and the held layer 3 is refused for them (layer-conflict)", diff --git a/wai/staged-conformance/staged_verify.py b/wai/staged-conformance/staged_verify.py index fd7092708..100642077 100644 --- a/wai/staged-conformance/staged_verify.py +++ b/wai/staged-conformance/staged_verify.py @@ -587,6 +587,7 @@ class Envelope: self.h, self.cap, self.ref, self.payload = h, cap, ref, payload self.arrival, self.evidence, self.verified = at, evidence, at self.w = None # its payload, parsed at step 6 + self.applied = 0 # the units it has been applied to (§4.5) def p(self, u, n): """P(u): the producer `prev` gives for unit u of a base of n units.""" @@ -616,9 +617,13 @@ class Base: def state(self): """§4.5: units' output bytes and 32 bytes per stage above 0; retained layers' - payload bytes, declared output and working bytes, and 32 bytes a unit covered.""" + payload bytes and declared working bytes, and, for each unit a layer covers and + has not been applied to, one unit's output and 32 bytes.""" s = sum(unit_bytes(x) + 32 * x["stage"] for x in self.units) - return s + sum(len(e.payload) + e.cost[0] + e.cost[2] + 32 * e.w["n_units"] for e in self.retained.values()) + for e in self.retained.values(): + n = e.w["n_units"] + s += len(e.payload) + e.cost[2] + (n - e.applied) * (e.cost[0] // n + 32) + return s # -- §4.1 steps 2 to 6, for an envelope whose base this is @@ -702,6 +707,7 @@ class Base: s["stage"] = e.ref["layer"] s["producer"] = e.h s["tier"] = DE if s["tier"] == DE else NONE # a layer acts on reconstructed output (§5) + e.applied += 1 return None # -- §4.4 diff --git a/wai/wai-rs/src/bin/wai_staged_vectors.rs b/wai/wai-rs/src/bin/wai_staged_vectors.rs index 82f4d81e9..cf1a47b4b 100644 --- a/wai/wai-rs/src/bin/wai_staged_vectors.rs +++ b/wai/wai-rs/src/bin/wai_staged_vectors.rs @@ -1148,6 +1148,26 @@ fn cases(ctx: &Ctx) -> Vec { files: vec![("L1.wai".into(), l1.clone()), ("L2.wai".into(), l2.clone())], cfg: ChainConfig { budget: state0 + reserve(&l1).max(reserve(&l2)) + 2 * 64, ..cfg() }, steps: vec![base_at(0, 0), offer("L2.wai", 1), offer("L1.wai", 2), Step::Expire { at_ms: 1002 }, offer("L1.wai", 1003)] }); } + { + let a = small(150); + let bh = a.hash(); + let (l1, r1) = layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, Some(Span { from: 1, count: 1 }), &[rgb_unit(&a, 1)], (8, 8, 3), &[u8s(picture(8, 8, 151, 1))], (8, 8, 3), params(4, pid, 4095)); + let (l1x, r1x) = layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, Some(Span { from: 0, count: 1 }), &[rgb_unit(&a, 0)], (8, 8, 3), &[u8s(picture(8, 8, 151, 0))], (8, 8, 3), params(4, pid, 4095)); + let mid = vec![r1x[0].clone(), r1[0].clone()]; + let targets: Vec = (0..2).map(|f| u8s(picture(8, 8, 152, f))).collect(); + let (l2, _) = layer(CAP_VIDEO_INT_REFINE, &bh, runs(&[chash(&l1x), chash(&l1)]), 2, None, &mid, (8, 8, 3), &targets, (8, 8, 3), params(2, pid, 4095)); + // The state once layer 2 is applied to unit 1 and held for unit 0, then room for + // layer 1x and no more. + let mut probe = StageSink::new(cfg()); + probe.add_base(a.info(), a.units.clone()); + for e in [&l1, &l2] { + probe.offer(e, 0, ReceiptEvidence::None).expect("WAI1"); + } + let budget = probe.chain(&bh).expect("decoded").state_bytes() + reserve(&l1x); + out.push(Case { name: "budget_partly_applied", about: "layer 2 is applied to unit 1 and held for unit 0, so it stays retained; unit 1's share has left its reserve, which leaves room for layer 1x over unit 0 at a budget with no byte to spare; 1x applies and releases layer 2 for unit 0", bases: vec![a], + files: vec![("L1.wai".into(), l1), ("L2.wai".into(), l2), ("L1x.wai".into(), l1x)], cfg: ChainConfig { budget, ..cfg() }, + steps: vec![base_at(0, 0), offer("L1.wai", 1), offer("L2.wai", 2), offer("L1x.wai", 3)] }); + } // ---- steps 7 and 8 ---- { let a = small(119); diff --git a/wai/wai-rs/src/staged/mod.rs b/wai/wai-rs/src/staged/mod.rs index 93759659a..fc445e49c 100644 --- a/wai/wai-rs/src/staged/mod.rs +++ b/wai/wai-rs/src/staged/mod.rs @@ -372,6 +372,9 @@ struct Layer { held: u32, /// What it adds to the chain's state while retained (§4.5). reserve: u64, + /// What one covered unit's application releases from `reserve`: its output and its + /// stage's 32 bytes. + share: u64, } /// The base's identity, how it was decoded (§4.1 steps 2–4), and where its units sit @@ -498,8 +501,8 @@ impl StageChain { } /// The chain's state (§4.5): every unit's output bytes and 32 bytes per stage above - /// 0, and every retained layer's payload, declared output and declared working - /// bytes, and 32 bytes for each unit it covers. + /// 0, and every retained layer's payload and declared working bytes, with one unit's + /// output and 32 bytes for each unit it covers and has not been applied to. pub fn state_bytes(&self) -> u64 { self.state } @@ -557,7 +560,8 @@ impl StageChain { self.seq += 1; self.state += reserve; self.ids.insert(hash, self.seq); - self.layers.insert(self.seq, Layer { hash, payload, wir, layer: refines.layer, from, count, prev, arrival_ms, ready_ms, held: 0, reserve }); + let share = (c.output_bytes / count as u64).saturating_add(32); + self.layers.insert(self.seq, Layer { hash, payload, wir, layer: refines.layer, from, count, prev, arrival_ms, ready_ms, held: 0, reserve, share }); Ok(self.seq) } @@ -765,7 +769,13 @@ impl StageChain { s.producer = hash; s.producers.push(hash); s.tier = tier::compose(s.tier, &[Some(DeterminismTier::DecodeEquivalence)], &[true]); - self.state = self.state.saturating_sub(old).saturating_add(s.buf.samples.byte_len()).saturating_add(32); + let new = s.buf.samples.byte_len(); + // The unit's share of the layer's reserve (its output and its stage) is now held + // by the unit, so it leaves the reserve: an application never raises the state. + let l = self.layers.get_mut(&id).expect("retained"); + let share = l.share.min(l.reserve); + l.reserve -= share; + self.state = self.state.saturating_sub(old).saturating_add(new).saturating_add(32).saturating_sub(share); self.passed.remove(&u); Ok(()) } diff --git a/wai/wai-rs/src/staged/tests.rs b/wai/wai-rs/src/staged/tests.rs index a3d62cd4c..4d279a936 100644 --- a/wai/wai-rs/src/staged/tests.rs +++ b/wai/wai-rs/src/staged/tests.rs @@ -1033,6 +1033,26 @@ fn chain_budget() { let mut c = StageChain::new(info, tiny, ChainConfig { budget: need, ..cfg() }); assert_eq!(c.offer(&l, 0, ReceiptEvidence::None).unwrap()[0].applied.len(), 64); assert!(c.state_bytes() <= need, "the state {} passed the budget {need}", c.state_bytes()); + // A layer applied to some of its units and held for the others stays retained: the + // units it was applied to take their share out of its reserve, so the state stays + // within the budget it was admitted at, to the byte. + let (_, info, units) = base4(); + let b = info.content_hash; + let below: Vec> = (0..4).map(frame).collect(); + let l1 = layer(&b, one(&b), 1, Some(Span { from: 2, count: 2 }), &below[2..], 50); + let l2 = layer(&b, Prev::One(hexs(&h(&l1))), 2, None, &below, 60); + let mut probe = StageChain::new(info.clone(), units.clone(), cfg()); + probe.offer(&l1, 0, ReceiptEvidence::None).unwrap(); + let p2 = Wai::from_bytes(&l2).unwrap().payload; + let c2 = parse(&p2, UnitKind::Rgb8).unwrap().declared_cost(); + let budget = probe.state_bytes() + p2.len() as u64 + c2.output_bytes + c2.working_bytes + 4 * 32; + let mut c = StageChain::new(info, units, ChainConfig { budget, ..cfg() }); + c.offer(&l1, 0, ReceiptEvidence::None).unwrap(); + let r = &c.offer(&l2, 1, ReceiptEvidence::None).unwrap()[0]; + assert_eq!((r.applied.clone(), r.held.clone()), (vec![2, 3], vec![0, 1])); + assert!(c.state_bytes() <= budget, "half applied: the state {} passed the budget {budget}", c.state_bytes()); + let unit = (H * W * 3) as u64; + assert_eq!(c.state_bytes(), budget - 2 * unit, "each applied unit's old output left the state"); } // ───────────────────────── WAI2 identity (SPEC §7.1) ───────────────────────── -- 2.54.0 (Apple Git-157) From f29536afc75b9a29378158e135303c8d595dcf1d Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 4 Oct 2026 22:27:18 -0400 Subject: [PATCH 27/36] wai: a base's receipt verified after it was decoded makes it attested MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit add_base on a base the sink held ignored the new BaseInfo, so a sink that verified the base's receipt after decoding it never set attested, and a layer with no receipt still applied. Decoding the base again with attested=true now marks it attested, and never the reverse; StageSink::mark_attested and StageChain::mark_attested do the same without a decode. Step 5 applies to every envelope that arrives after; those admitted before keep their decision (§4.4, §10). staged_verify.py does the same, and the corpus case attested_after_decode decodes, attests, decodes unattested again and offers layers between. Co-Authored-By: Claude Opus 5.5 --- wai/CHANGELOG.md | 3 +- wai/extensions/staged-delivery.md | 7 +- .../cases/attested_after_decode/L1_f0.wai | Bin 0 -> 535 bytes .../cases/attested_after_decode/L1_f1.wai | Bin 0 -> 527 bytes .../cases/attested_after_decode/base_0.wai | Bin 0 -> 551 bytes .../cases/attested_after_decode/base_1.wai | Bin 0 -> 551 bytes .../cases/attested_after_decode/case.json | 254 ++++++++++++++++++ .../cases/attested_after_decode/stage0_0.bin | 1 + .../cases/attested_after_decode/stage0_1.bin | 1 + wai/staged-conformance/index.json | 8 + wai/staged-conformance/staged_verify.py | 7 +- wai/wai-rs/src/bin/wai_staged_vectors.rs | 15 ++ wai/wai-rs/src/staged/mod.rs | 22 +- wai/wai-rs/src/staged/tests.rs | 27 ++ 14 files changed, 338 insertions(+), 7 deletions(-) create mode 100644 wai/staged-conformance/cases/attested_after_decode/L1_f0.wai create mode 100644 wai/staged-conformance/cases/attested_after_decode/L1_f1.wai create mode 100644 wai/staged-conformance/cases/attested_after_decode/base_0.wai create mode 100644 wai/staged-conformance/cases/attested_after_decode/base_1.wai create mode 100644 wai/staged-conformance/cases/attested_after_decode/case.json create mode 100644 wai/staged-conformance/cases/attested_after_decode/stage0_0.bin create mode 100644 wai/staged-conformance/cases/attested_after_decode/stage0_1.bin diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 231507a9d..f70edea02 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -34,7 +34,8 @@ wire format may change and such changes are recorded here. (`chain-over-limit`) before any stream is decoded, and a layer no unit is held for is dropped. The declared working bytes count the stream index. A sink's work per layer is linear in its payload and declared symbols up to a logarithmic factor in its - units, and §14 states how. Decoding a base again keeps its chain; a layer held for + units, and §14 states how. Decoding a base again keeps its chain, and a receipt + verified for the base after it was decoded makes it attested from then on; a layer held for its base and offered again keeps the strongest receipt evidence, and over an attested base counts as arriving when its receipt was verified; a set split across bases shares one live clock (`first_unit`). diff --git a/wai/extensions/staged-delivery.md b/wai/extensions/staged-delivery.md index 193c87e0b..f2da2a5cb 100644 --- a/wai/extensions/staged-delivery.md +++ b/wai/extensions/staged-delivery.md @@ -183,7 +183,8 @@ batch can release another, which is evaluated the same way. A sink that decodes again a base whose chain it holds keeps the chain: re-decoding a base does not reset its units' stages, producers or frozen state, and releases and -reports nothing. A sink **MAY** drop a base's chain when it no longer presents the +reports nothing. If the sink has verified the base's delivery receipt since, the base +is attested from then on (§10); a base once attested stays attested. A sink **MAY** drop a base's chain when it no longer presents the base; an envelope for that base is then held as for any base it has not decoded. ### 4.5 Chain budget @@ -320,7 +321,9 @@ key the sink holds with the authority `delivery` under the same origin (jwp-receipts §7.5). A receipt verified without the layer's envelope bytes (§3 steps 1, 2, 3 and 5, step 4 not run) shows that a publisher signed an object with that content hash; it never authorizes an application. A content hash in `refines` binds a layer -to a base, not to its publisher. +to a base, not to its publisher. A sink that verifies a base's delivery receipt after +it decoded the base applies step 5 to every envelope that arrives after it; envelopes +it admitted before keep the decision they had. ## 11. Counting bytes diff --git a/wai/staged-conformance/cases/attested_after_decode/L1_f0.wai b/wai/staged-conformance/cases/attested_after_decode/L1_f0.wai new file mode 100644 index 0000000000000000000000000000000000000000..c8f26e670e419aea14e22a9f441d1dddd1964dd4 GIT binary patch literal 535 zcmWG{^fZiTWMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_+BCVT(sXc{;=0=abGW#z6b#28livy literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/attested_after_decode/L1_f1.wai b/wai/staged-conformance/cases/attested_after_decode/L1_f1.wai new file mode 100644 index 0000000000000000000000000000000000000000..e3e86e1aa70751663493a33f67721c956685e719 GIT binary patch literal 527 zcmb_YJxc>Y5S^f)qF_2fv{|t-ggcYFTvGYbsQ7^*S_tai-kvO8_TuKENDLMt7J{u{ zCyF50Y9k1OMX(nIv9btgZzY~{QU8IN6Un>d4^L4&V7%fm+cp}(DHCiU?7D+Tfr7I*S zl>}kFbVMM0DdY;tmC34*P9zsfkLVv^F=A|5pKwHnW*&qz&x0p!oX#IC*rFE0WU_q*$Ob>e5wyz782`RQi( kWhh>SfwqgqSWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp2H7#SGCJ;Mwcfix4)xg2Qt zVqShWjC9|qEAfcO%SeG@4E5Qv`x z`LBQ$zXOW>2e|;~03bL4#9mxy^Rsw?c8IS!86?-d`i;V?;70~$z1~ZDf4-m+@PtK$ zS-su7g=Mnxel^wghyUC$>(v&UYG%%@cwqI82R9EZE-8$;ziRU1^iNAJJpTA<0o!(l zh==DFC%$>VY=75JyLH!2eTxa0J5j6Y_3I0c7pt;f-%K~VvG<(S#3ou+WV~_#00#ZQs{jB1 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/attested_after_decode/base_1.wai b/wai/staged-conformance/cases/attested_after_decode/base_1.wai new file mode 100644 index 0000000000000000000000000000000000000000..562a89da71bda5ed68ad231cd38df9aa521e91eb GIT binary patch literal 551 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp2H7#SGCJ;Mwcfix4)xg2Qt zVqShWjC9|qEAfcO%SeG@4E5Qv`x z`LBQ$zXOW>2e|;~03bL4#9mxy^Rsw?c8IS!86?-d`i;V?;70~$z1~ZDf4-m+@PtK$ zS-su7g=Mnxel^wghyUC$>(v&UYG%%@cwqI82R9EZE-8$;ziRU1^iNAJJpTA<0o!(l zh==DFC%$>VY=75JyLH!2eTxa0J5j6Y_3I0c7pt;f-%K~VvG<(S#3ou+WV~_#00#ZQs{jB1 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/attested_after_decode/case.json b/wai/staged-conformance/cases/attested_after_decode/case.json new file mode 100644 index 000000000..cb6da612b --- /dev/null +++ b/wai/staged-conformance/cases/attested_after_decode/case.json @@ -0,0 +1,254 @@ +{ + "format": "wai-staged-case/1", + "name": "attested_after_decode", + "about": "a base decoded before its receipt is verified: frame 0's layer with no receipt applies; the base decoded again with its receipt verified is attested from then on, so frame 1's layer with no receipt is refinement-unattested; decoding it again without the receipt leaves it attested; delivered-verified, the layer applies", + "bases": [ + { + "file": "base_0.wai", + "object": "ca89ed081688f3f5370fc6d96e471d085b7606850f9d3e55c2a876f3bcbc6545", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0_0.bin", + "stage0_sha256": "f6a56010bb71a9e7848c764e499e81f834664224e35ec9c978f794ff0e3c8848", + "decode": { + "kind": "int", + "set": {} + } + }, + { + "file": "base_1.wai", + "object": "ca89ed081688f3f5370fc6d96e471d085b7606850f9d3e55c2a876f3bcbc6545", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": true, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0_1.bin", + "stage0_sha256": "f6a56010bb71a9e7848c764e499e81f834664224e35ec9c978f794ff0e3c8848", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1_f0.wai", + "object": "25eac95b83d649a906696dae8e163fd4ac24570371602c10cbb3e2a8e1c54d2a", + "bytes": 535 + }, + { + "file": "L1_f1.wai", + "object": "c0ab531af4d89041d976b16c74a5d7d2595f6d2972d91a065d4e4114699e3a02", + "bytes": 527 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1_f0.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "base", + "index": 1, + "at_ms": 2 + }, + { + "op": "offer", + "file": "L1_f1.wai", + "at_ms": 3, + "evidence": "none" + }, + { + "op": "base", + "index": 0, + "at_ms": 4 + }, + { + "op": "offer", + "file": "L1_f1.wai", + "at_ms": 5, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_f1.wai", + "at_ms": 6, + "evidence": "delivered-verified" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "25eac95b83d649a906696dae8e163fd4ac24570371602c10cbb3e2a8e1c54d2a", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ], + [], + [ + { + "object": "c0ab531af4d89041d976b16c74a5d7d2595f6d2972d91a065d4e4114699e3a02", + "envelope": "refinement-unattested", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [], + [ + { + "object": "c0ab531af4d89041d976b16c74a5d7d2595f6d2972d91a065d4e4114699e3a02", + "envelope": "refinement-unattested", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "c0ab531af4d89041d976b16c74a5d7d2595f6d2972d91a065d4e4114699e3a02", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "25eac95b83d649a906696dae8e163fd4ac24570371602c10cbb3e2a8e1c54d2a", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "c164503f2d738b8729252a24bf6e94300979aa7aa11427195f1221cfffd81e98" + }, + { + "stage": 1, + "producer": "c0ab531af4d89041d976b16c74a5d7d2595f6d2972d91a065d4e4114699e3a02", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "f1029000aa1799224e89bc1b00021dad8cc869e9708b8b7bb94a7a63aea27d80" + } + ], + "clip_sha256": "4008894e56d04cd74635889e6f4ac81b787bd0b35bf6d7681648457caa0ff177", + "n_units": 2 + }, + { + "units": [ + { + "stage": 1, + "producer": "25eac95b83d649a906696dae8e163fd4ac24570371602c10cbb3e2a8e1c54d2a", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "c164503f2d738b8729252a24bf6e94300979aa7aa11427195f1221cfffd81e98" + }, + { + "stage": 1, + "producer": "c0ab531af4d89041d976b16c74a5d7d2595f6d2972d91a065d4e4114699e3a02", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "f1029000aa1799224e89bc1b00021dad8cc869e9708b8b7bb94a7a63aea27d80" + } + ], + "clip_sha256": "4008894e56d04cd74635889e6f4ac81b787bd0b35bf6d7681648457caa0ff177", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/attested_after_decode/stage0_0.bin b/wai/staged-conformance/cases/attested_after_decode/stage0_0.bin new file mode 100644 index 000000000..7e998917b --- /dev/null +++ b/wai/staged-conformance/cases/attested_after_decode/stage0_0.bin @@ -0,0 +1 @@ +(0@0@P8@P@PhPXhPhx`xˆxx�8HPHPhH`hXhp`xˆpxˆx€˜x� H``P`p¨Ðøpxˆx��x� ˆ˜¨˜¨°Xp€¸Øÿ¸àÿÈèÿˆ ¨�¨¨˜¨À¨ÀÐÀàÿÀðÿÈøÿØÿÿèÿÿ¨¸È°ÈØÈÐØ��¨Øÿÿàÿÿèÿÿ¸ÀÐÀØàÈÐèÐàø˜°° °ÈøÿÿÀÀØÐààØàðØðøèÿÿ¨ÀȸÈàÈÐèÐØèàèÿèðÿðÿÿÿÿÿ(0@0@P8@P@PhPXhPhxhxˆxx�8HPHPhH`hXhp`x€pxˆx€˜x� H``P`p`p€px€xˆ�x� ˆ˜¨˜ °`p€px‡p€—€ˆŸÈÿÿ�¨¨˜¨À¨ÀÐx€�x�—€˜§Øÿÿèÿÿðÿÿ°ÀØÈÐØˆ� �§¯ØÿÿèÿÿÿÿÿÿÿÿÿÿÿÐØø˜°° ¨È°·ÏÿÿÿÿÿÿÿÿÿØðøèÿÿ¨ÀÀ¸ÈàÈÐèÐØàÿÿÿèðÿðÿÿÿÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/attested_after_decode/stage0_1.bin b/wai/staged-conformance/cases/attested_after_decode/stage0_1.bin new file mode 100644 index 000000000..7e998917b --- /dev/null +++ b/wai/staged-conformance/cases/attested_after_decode/stage0_1.bin @@ -0,0 +1 @@ +(0@0@P8@P@PhPXhPhx`xˆxx�8HPHPhH`hXhp`xˆpxˆx€˜x� H``P`p¨Ðøpxˆx��x� ˆ˜¨˜¨°Xp€¸Øÿ¸àÿÈèÿˆ ¨�¨¨˜¨À¨ÀÐÀàÿÀðÿÈøÿØÿÿèÿÿ¨¸È°ÈØÈÐØ��¨Øÿÿàÿÿèÿÿ¸ÀÐÀØàÈÐèÐàø˜°° °ÈøÿÿÀÀØÐààØàðØðøèÿÿ¨ÀȸÈàÈÐèÐØèàèÿèðÿðÿÿÿÿÿ(0@0@P8@P@PhPXhPhxhxˆxx�8HPHPhH`hXhp`x€pxˆx€˜x� H``P`p`p€px€xˆ�x� ˆ˜¨˜ °`p€px‡p€—€ˆŸÈÿÿ�¨¨˜¨À¨ÀÐx€�x�—€˜§Øÿÿèÿÿðÿÿ°ÀØÈÐØˆ� �§¯ØÿÿèÿÿÿÿÿÿÿÿÿÿÿÐØø˜°° ¨È°·ÏÿÿÿÿÿÿÿÿÿØðøèÿÿ¨ÀÀ¸ÈàÈÐèÐØàÿÿÿèðÿðÿÿÿÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/index.json b/wai/staged-conformance/index.json index 7b59fb7e2..50db58b1e 100644 --- a/wai/staged-conformance/index.json +++ b/wai/staged-conformance/index.json @@ -411,6 +411,14 @@ "f7efabb2f5641fb662d3e5fcbcd77c1a1b0106dd91d0f42c2d7a722f455dd1c1" ] }, + { + "name": "attested_after_decode", + "about": "a base decoded before its receipt is verified: frame 0's layer with no receipt applies; the base decoded again with its receipt verified is attested from then on, so frame 1's layer with no receipt is refinement-unattested; decoding it again without the receipt leaves it attested; delivered-verified, the layer applies", + "clip_sha256": [ + "4008894e56d04cd74635889e6f4ac81b787bd0b35bf6d7681648457caa0ff177", + "4008894e56d04cd74635889e6f4ac81b787bd0b35bf6d7681648457caa0ff177" + ] + }, { "name": "evidence_upgrade", "about": "an attested base: a layer held for it with no receipt evidence is offered again delivered-verified, keeps that evidence, and applies when the base arrives", diff --git a/wai/staged-conformance/staged_verify.py b/wai/staged-conformance/staged_verify.py index 100642077..f128c06f5 100644 --- a/wai/staged-conformance/staged_verify.py +++ b/wai/staged-conformance/staged_verify.py @@ -602,7 +602,7 @@ class Base: """A decoded base and its units (§1), with what §4 keeps for them.""" def __init__(self, info, units, cfg): - self.info, self.cfg = info, cfg + self.info, self.cfg = dict(info), cfg self.units = [dict(stage=0, producer=info["hash"], frozen=False, tier=info["tier"], shape=s, elem=e, data=list(d)) for (s, e, d) in units] self.passed = {} # step 7's record: (u, layer, P(u)) -> the envelope that passed step 8 for u @@ -872,8 +872,11 @@ class Sink: def add_base(self, info, units): """A decoded base: the envelopes held for it are released together (§4.4). A - base this sink holds already is kept as it is (§4.4).""" + base this sink holds already is kept as it is (§4.4), except that a receipt + verified for it since makes it attested from then on (§10), never the reverse.""" if info["hash"] in self.bases: + if info["attested"]: + self.bases[info["hash"]].info["attested"] = True return [] b = self.bases[info["hash"]] = Base(info, units, self.cfg) mine = [e for e in self.waiting if e.ref["base"] == info["hash"]] diff --git a/wai/wai-rs/src/bin/wai_staged_vectors.rs b/wai/wai-rs/src/bin/wai_staged_vectors.rs index cf1a47b4b..6d6eade77 100644 --- a/wai/wai-rs/src/bin/wai_staged_vectors.rs +++ b/wai/wai-rs/src/bin/wai_staged_vectors.rs @@ -1268,6 +1268,21 @@ fn cases(ctx: &Ctx) -> Vec { out.push(Case { name: "refused_on_release", about: "two layers held for their base; when it arrives, the first is payload-malformed and has that one report, and the second applies", bases: vec![a], files: vec![("L1_cut.wai".into(), cut), ("L1_f1.wai".into(), l1)], cfg: cfg(), steps: vec![offer("L1_cut.wai", 0), offer("L1_f1.wai", 1), base_at(0, 2)] }); } + { + let a = small(153); + let bh = a.hash(); + let mut att = a.clone(); + att.attested = true; + let mut files = Vec::new(); + for f in 0..2u32 { + let (l, _) = layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, Some(Span { from: f, count: 1 }), &[rgb_unit(&a, f as usize)], (8, 8, 3), &[u8s(picture(8, 8, 154, f))], (8, 8, 3), params(4, pid, 4095)); + files.push((format!("L1_f{f}.wai"), l)); + } + let dv = |at| Step::Offer { file: "L1_f1.wai".into(), at_ms: at, evidence: ReceiptEvidence::DeliveredVerified }; + out.push(Case { name: "attested_after_decode", about: "a base decoded before its receipt is verified: frame 0's layer with no receipt applies; the base decoded again with its receipt verified is attested from then on, so frame 1's layer with no receipt is refinement-unattested; decoding it again without the receipt leaves it attested; delivered-verified, the layer applies", + bases: vec![a, att], files, cfg: cfg(), + steps: vec![base_at(0, 0), offer("L1_f0.wai", 1), base_at(1, 2), offer("L1_f1.wai", 3), base_at(0, 4), offer("L1_f1.wai", 5), dv(6)] }); + } { let mut a = small(144); a.attested = true; diff --git a/wai/wai-rs/src/staged/mod.rs b/wai/wai-rs/src/staged/mod.rs index fc445e49c..c9b3eb5ce 100644 --- a/wai/wai-rs/src/staged/mod.rs +++ b/wai/wai-rs/src/staged/mod.rs @@ -451,6 +451,13 @@ impl StageChain { &self.base } + /// Record that the sink verified a delivery receipt for the base (§10): from now + /// on step 5 applies to every envelope that arrives. A verified receipt is never + /// unverified, so there is no converse. + pub fn mark_attested(&mut self) { + self.base.attested = true; + } + /// The number of units. pub fn n_units(&self) -> u32 { self.units.len() as u32 @@ -1027,10 +1034,14 @@ impl StageSink { /// Record a decoded base and release the layers held for it, together (§4.4). A /// base the sink already holds is kept as it is: decoding it again does not reset - /// its units, and reports nothing. + /// its units, and reports nothing; if this decode comes with the base's delivery + /// receipt verified, the base is attested from now on (§10), and never the reverse. pub fn add_base(&mut self, base: BaseInfo, units: Vec) -> Vec { let hash = base.content_hash; - if self.chains.contains_key(&hash) { + if let Some(c) = self.chains.get_mut(&hash) { + if base.attested { + c.mark_attested(); + } return vec![]; } let mut chain = StageChain::new(base, units, self.cfg.clone()); @@ -1042,6 +1053,13 @@ impl StageSink { reports } + /// Record that the sink verified a delivery receipt for the base `base` it holds + /// (§10): step 5 applies to every envelope that arrives after it. Whether it holds + /// the base. + pub fn mark_attested(&mut self, base: &[u8; 32]) -> bool { + self.chains.get_mut(base).map(StageChain::mark_attested).is_some() + } + /// Forget a base the sink no longer presents, with its chain; a layer for it is /// then held as for any base the sink has not decoded. Whether it held the base. pub fn remove_base(&mut self, base: &[u8; 32]) -> bool { diff --git a/wai/wai-rs/src/staged/tests.rs b/wai/wai-rs/src/staged/tests.rs index 4d279a936..6c456fca3 100644 --- a/wai/wai-rs/src/staged/tests.rs +++ b/wai/wai-rs/src/staged/tests.rs @@ -892,6 +892,33 @@ fn readding_a_base_keeps_its_units() { assert_eq!(r[0].applied, vec![1]); } +/// A base decoded before its delivery receipt is verified becomes attested when the +/// receipt is: decoded again with it, or marked. Step 5 then applies to every layer +/// that arrives after, and nothing makes an attested base unattested again (§10). +#[test] +fn late_attestation_applies_step_5() { + let (_, info, units) = base4(); + let b = info.content_hash; + let l0 = layer(&b, one(&b), 1, Some(Span { from: 0, count: 1 }), &[frame(0)], 50); + let l1 = layer(&b, one(&b), 1, Some(Span { from: 1, count: 1 }), &[frame(1)], 51); + let l2 = layer(&b, one(&b), 1, Some(Span { from: 2, count: 1 }), &[frame(2)], 52); + let mut sink = StageSink::new(cfg()); + sink.add_base(info.clone(), units.clone()); + assert_eq!(sink.offer(&l0, 0, ReceiptEvidence::None).unwrap()[0].applied, vec![0], "unattested: no receipt needed"); + assert!(sink.add_base(BaseInfo { attested: true, ..info.clone() }, units.clone()).is_empty()); + assert!(sink.chain(&b).unwrap().base().attested); + assert_eq!(sink.offer(&l1, 1, ReceiptEvidence::None).unwrap()[0].envelope, Some(StageRefusal::RefinementUnattested)); + assert!(sink.add_base(info.clone(), units.clone()).is_empty()); + assert!(sink.chain(&b).unwrap().base().attested, "never back to unattested"); + assert_eq!(sink.offer(&l1, 2, ReceiptEvidence::DeliveredVerified).unwrap()[0].applied, vec![1]); + // Marked, without decoding again. + let mut sink = StageSink::new(cfg()); + sink.add_base(info.clone(), units.clone()); + assert!(sink.mark_attested(&b)); + assert!(!sink.mark_attested(&[9; 32]), "a base the sink does not hold"); + assert_eq!(sink.offer(&l2, 0, ReceiptEvidence::InclusionVerified).unwrap()[0].envelope, Some(StageRefusal::RefinementUnattested)); +} + /// A unit or a stage below whose stated shape's element count passes u64 is refused /// (`geometry_mismatch`, `geometry-mismatch`), not wrapped. #[test] -- 2.54.0 (Apple Git-157) From 360ebd6178b5fb230e5e6c8e98abe8646df28d7c Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 4 Oct 2026 22:31:46 -0400 Subject: [PATCH 28/36] wai: a dropped base keeps each unit's record; its held layers are base-absent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit remove_base followed by add_base started the base afresh, so a layer that had conflicted, or one that differed from the producers a unit had, applied on the second decode; and the layers held for the dropped chain vanished with no reason reported. - StageSink::remove_base discards the layers held for the chain, reporting each base-absent, and keeps each unit's record: the producers of the stages it reached, the envelope that passed step 8 at its last stage without being applied, and its frozen flag (32 bytes a recorded stage, one more producer and a flag). - The base decoded again starts each unit at stage 0 with its record (StageChain::with_records). Step 7 compares a layer with the recorded producers, so a unit advances only along them; other content under a recorded key conflicts, and a frozen unit stays frozen. The record's stages count in the chain's state; replaying one adds nothing. - StageSink::forget_base drops a record; §14 states what that gives up. - §4.1 step 7, §4.4, §4.5 and §14 state it; staged_verify.py keeps the same record, and the corpus gains a remove step and three cases (replay, frozen, diverging producer). Co-Authored-By: Claude Opus 5.5 --- wai/CHANGELOG.md | 16 +- wai/extensions/staged-delivery.md | 27 +- .../cases/remove_readd_diverge/L1a.wai | Bin 0 -> 591 bytes .../cases/remove_readd_diverge/L1b.wai | Bin 0 -> 591 bytes .../cases/remove_readd_diverge/base.wai | Bin 0 -> 559 bytes .../cases/remove_readd_diverge/case.json | 200 +++++++++++++++ .../cases/remove_readd_diverge/stage0.bin | Bin 0 -> 384 bytes .../cases/remove_readd_frozen/L1a.wai | Bin 0 -> 583 bytes .../cases/remove_readd_frozen/L1b.wai | Bin 0 -> 587 bytes .../cases/remove_readd_frozen/base.wai | Bin 0 -> 551 bytes .../cases/remove_readd_frozen/case.json | 226 +++++++++++++++++ .../cases/remove_readd_frozen/stage0.bin | 1 + .../cases/remove_readd_replay/L1.wai | Bin 0 -> 579 bytes .../cases/remove_readd_replay/L2.wai | Bin 0 -> 795 bytes .../cases/remove_readd_replay/L4.wai | Bin 0 -> 795 bytes .../cases/remove_readd_replay/base.wai | Bin 0 -> 551 bytes .../cases/remove_readd_replay/case.json | 233 ++++++++++++++++++ .../cases/remove_readd_replay/stage0.bin | 1 + wai/staged-conformance/index.json | 21 ++ wai/staged-conformance/staged_verify.py | 35 ++- wai/wai-rs/src/bin/wai_staged_vectors.rs | 30 +++ wai/wai-rs/src/staged/mod.rs | 119 +++++++-- wai/wai-rs/src/staged/tests.rs | 68 ++++- 23 files changed, 930 insertions(+), 47 deletions(-) create mode 100644 wai/staged-conformance/cases/remove_readd_diverge/L1a.wai create mode 100644 wai/staged-conformance/cases/remove_readd_diverge/L1b.wai create mode 100644 wai/staged-conformance/cases/remove_readd_diverge/base.wai create mode 100644 wai/staged-conformance/cases/remove_readd_diverge/case.json create mode 100644 wai/staged-conformance/cases/remove_readd_diverge/stage0.bin create mode 100644 wai/staged-conformance/cases/remove_readd_frozen/L1a.wai create mode 100644 wai/staged-conformance/cases/remove_readd_frozen/L1b.wai create mode 100644 wai/staged-conformance/cases/remove_readd_frozen/base.wai create mode 100644 wai/staged-conformance/cases/remove_readd_frozen/case.json create mode 100644 wai/staged-conformance/cases/remove_readd_frozen/stage0.bin create mode 100644 wai/staged-conformance/cases/remove_readd_replay/L1.wai create mode 100644 wai/staged-conformance/cases/remove_readd_replay/L2.wai create mode 100644 wai/staged-conformance/cases/remove_readd_replay/L4.wai create mode 100644 wai/staged-conformance/cases/remove_readd_replay/base.wai create mode 100644 wai/staged-conformance/cases/remove_readd_replay/case.json create mode 100644 wai/staged-conformance/cases/remove_readd_replay/stage0.bin diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index f70edea02..140d0b28c 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -34,11 +34,14 @@ wire format may change and such changes are recorded here. (`chain-over-limit`) before any stream is decoded, and a layer no unit is held for is dropped. The declared working bytes count the stream index. A sink's work per layer is linear in its payload and declared symbols up to a logarithmic factor in its - units, and §14 states how. Decoding a base again keeps its chain, and a receipt - verified for the base after it was decoded makes it attested from then on; a layer held for - its base and offered again keeps the strongest receipt evidence, and over an - attested base counts as arriving when its receipt was verified; a set split across - bases shares one live clock (`first_unit`). + units, and §14 states how. A sink that drops a base's chain discards the layers held + for it (`base-absent`) and keeps each unit's record (its producers, a pending + producer, its frozen flag); the base decoded again advances only along that record. + Decoding a base again keeps its chain, and a receipt verified for the base after it + was decoded makes it attested from then on; a layer held for its base and offered + again keeps the strongest receipt evidence, and over an attested base counts as + arriving when its receipt was verified; a set split across bases shares one live + clock (`first_unit`). - **The residual payload `WIR1`** (Appendix A): one rANS stream per unit; `clamp(pred + r·q)` with the identity, up2 (the half-pel arithmetic, odd sizes included) and rate (linear interpolation, `rdiv`'s halves away from zero, channels @@ -82,7 +85,8 @@ wire format may change and such changes are recorded here. wir1_arith, tier, schedule}` (feature `staged`); `container::{Refines, Prev}`; `WaiMulti::{explode, explode_each, explode_all, pack, check_envelope_manifests, refinements_of}`; `container::check_unique_members`; `wai stage - refine|apply|inspect|pack|explode` (`refine --out-dims` gives a layer's shape); `wai wrap` refuses a refinement + refine|apply|inspect|pack|explode` (`refine --out-dims` gives a layer's shape); + `StageSink::{remove_base, forget_base, mark_attested}`; `wai wrap` refuses a refinement capability, and `wai inspect` shows `refines`. `RansEncoder` writes the bypass escape, and `int_entropy::hist_to_cdf` builds a table from a histogram in integer arithmetic. In `ternary::rans_encode_trits`, the encode call sat inside diff --git a/wai/extensions/staged-delivery.md b/wai/extensions/staged-delivery.md index f2da2a5cb..7c159be41 100644 --- a/wai/extensions/staged-delivery.md +++ b/wai/extensions/staged-delivery.md @@ -108,7 +108,8 @@ fails is the reason for the envelope (1–6) or for the unit (7–10). 7. Let *P(u)* be the producer `prev` gives for *u*. *u* is **conflicted** when it is frozen, or when an envelope with different content, the same `base` and the same `layer`, whose producer for *u* is also *P(u)*, has passed step 8 for *u*, earlier - or in the same batch (§4.4) (`layer-conflict`). A conflicted unit is **frozen**: + (before the base's chain was dropped included, §4.4) or in the same batch (§4.4) + (`layer-conflict`). A conflicted unit is **frozen**: the sink **MUST** apply no further layer to it, it keeps the output it had (a layer applied to it before the conflict stays applied), and the sink **MUST** report the conflict. @@ -185,7 +186,16 @@ A sink that decodes again a base whose chain it holds keeps the chain: re-decodi base does not reset its units' stages, producers or frozen state, and releases and reports nothing. If the sink has verified the base's delivery receipt since, the base is attested from then on (§10); a base once attested stays attested. A sink **MAY** drop a base's chain when it no longer presents the -base; an envelope for that base is then held as for any base it has not decoded. +base. Every envelope held for the chain's units is then discarded (`base-absent`, +reported once for each), and an envelope for that base is held as for any base the +sink has not decoded. The sink keeps each unit's **record**: the producers of the +stages it reached, the envelope that passed step 8 at its last stage without being +applied, and whether it is frozen; 32 bytes for each recorded stage, at most one +further 32-byte producer, and a flag. When it decodes the base again, each unit +starts at stage 0 with its record. Step 7 compares a layer with the recorded +producers, so a unit advances again only along them: a layer whose key a recorded +stage holds with other content conflicts, and the unit freezes; a frozen unit stays +frozen, at stage 0. ### 4.5 Chain budget @@ -193,7 +203,9 @@ A sink **MUST** declare a budget, in bytes, for the chain of each base it decode chain's **state** is the sum of: - for every unit, the bytes of its current output (1 per u8 element, 2 per i16 - sample), and 32 bytes for each stage above 0 (the producers step 7 compares); + sample), and 32 bytes for each stage its record holds (the producers step 7 + compares: its stages above 0, or, for a base decoded again, the stages it reached + before, §4.4); - for every layer the chain **retains**, its payload's bytes and its declared working bytes (Appendix A.5), and, for each unit it covers and has not been applied to, one unit's output (its declared output over its units) and 32 bytes (the stage it would @@ -206,7 +218,8 @@ release or expiry after which no unit is held for it; the sink then drops it. At (`chain-over-limit`) before any stream is decoded. An envelope the chain still retains, received again, adds nothing. Applying a layer to a unit replaces its output with one, and adds a stage, that the layer's retention counted, and that unit's share -leaves the layer's: an application lowers the state by the unit's old output. The +leaves the layer's: an application lowers the state by the unit's old output (and by +32 bytes more where it replays a recorded stage). The state rises only when a layer is admitted, so it never passes the budget, and the bytes a chain holds never pass the state. @@ -375,6 +388,12 @@ ignores the header. - An encrypted layer ([confidentiality](confidentiality.md)) binds to the content hash of its base's envelope as delivered, ciphertext included, and is decrypted before §4.1 step 6. +- A sink keeps the record of a base whose chain it dropped (§4.4), so a base decoded + again cannot be refined along other producers than it had; the record counts in the + chain's state when the base is decoded again (§4.5). A sink that forgets a record + (the reference `StageSink::forget_base`) treats the base as never decoded, so a layer + that conflicted with the forgotten history can apply; it forgets only a base it will + not decode again. - Where the base is attested (§10), a frozen unit stays at its stage: a party that can sign conflicting layers can stop a unit's refinement, and cannot change what it presents. Over a base whose receipt the sink did not verify, anyone who learns the diff --git a/wai/staged-conformance/cases/remove_readd_diverge/L1a.wai b/wai/staged-conformance/cases/remove_readd_diverge/L1a.wai new file mode 100644 index 0000000000000000000000000000000000000000..5139e067e4a772087e04b9dc3c72b771d1f2967e GIT binary patch literal 591 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZY8imR815Nl$OxpF7#J8hpqLp*!vG7A z@#p{l|2$9uMm7cpMllA4)piUFtFjmv{!eCL__v*bA^0v3e`H`-`41@eAEcO(Apl7F z0P*hC33+WG5lg{qIyc{4^Xg@;S6=OXb@dHD)`G)3f)8fs_k=1k^IqTOE?dh!XTj>f zmjt=LZSTr_RLNqps;PZJsk7ept6AGVde6xg^@!XMu^_Q+%j!h#N$Lmcek;_?U_IID zE+%kRWpAd{pU*4KZ4V7z=zj9vqP-IA>+>JqR$X7@&^sX_KGgk>s_Zu2{vNBch|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZY8imR815Nl$OxpF7#J8hpqLp*!vG7A z@#p{l|GZFvFreBnK_Ewmf#JU|14D2l1H<373=DyHf$BapFf9KA6#Ea917ZfC1wKF$ zgx3}Qd{*};ozebo&#|X2QTpL+8uKc7&T`lMvk#36KN?eT(dh03`L*{Au54jC=Hq%k z?|}C6Yd+qSlBz}1kF&MuRKEIW{*3c;T|>^kExL)`vxL+b71(-v>Oul*>yaaq)inq|k^zkXT#a)-i?O^3@KZqAhTzgBQResy_jP%q!( JK(~$OcmdSWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp217#SGCJ;Mwcfix4)xg2Qt z!~g&Pm4RX)5C&AUhK+$Cf}MeZ1*kAm7)XmTFhofLu_6Nls|EuDn+^j5yB-jm0I>xF zL##CrJ2EhA@?u~Jp3T4zw48zA?`k042;^^PVEDHihz|qtX(0bPQ2sFk!-{7>`L{s& z6A*u6U|9Ksf#LseApQ?>1<(;da0ZBd7}sklr)zOC36-r55c4y3(r5FMvbMG~_Q4@dp>UXJ^%Wy8fIa><-e?(LgW^{ib}aVX;0(H jy|vE!-ru}=!;ZU@&&YU~n*494rQlvDxYFUEk(~{;$pZ zeYdXv>g5dmLp?^hFak_qzWaxi`#26#CQ#aHxn_}aAEss9lr~^2SZ8KumoO;p^B%Fr z(yW!@=Oy}(e6DM$iCQ8qm7SEBLPoGl-Wd-r2m3XJgeharzDr}qvLVssPbhEj>EFeFJOO#c)Aj%W literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/remove_readd_frozen/L1a.wai b/wai/staged-conformance/cases/remove_readd_frozen/L1a.wai new file mode 100644 index 0000000000000000000000000000000000000000..c2adbb1ad6f2fa298d7f357723e27f39758015b9 GIT binary patch literal 583 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZYJV^=Fob&s88QNclL^S?fMRAK4FfDd z#-IQH|8qkH7}*#Y7{wSE!i<49kbxm&Dg(pHeL(yK$o|Q|@c%zZAtQqikObi?>-Id< z7GPiyXUl(ZLt$a{DV{a`i3TfHTkT%{-oaUXr-UC%o?7OosNot0m5YOwUs=I+!)*>rEZQI{$@y{#rgUx-EZK zmMiuwNe_FwPkgz)*n=gW8o7^uUFh0#da35@yw8(nlmzG|_}A53jjzf+eeRk)0|1v{ Bx)T5Z literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/remove_readd_frozen/L1b.wai b/wai/staged-conformance/cases/remove_readd_frozen/L1b.wai new file mode 100644 index 0000000000000000000000000000000000000000..c0c3fe39dd16c0d3290a4cada73878e62b3c7295 GIT binary patch literal 587 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZYX2}WFob&s88QNclL^S?fMRAK4FfDd z#-IQH|MNfv7=Z#zd_aB53=C_0fPy^?454co7*<_kU~^R28fYr)6lk?GS}c@y zEVXJ{#!1_Vu2X#_2~*E4oPX*pOR9j$sq;KnSN}Jh%gDfau`v4Ax06M_`B(mZ+maXN z^vdbVj(Pvg1(*};pRW|H^?tWlyYa>ihslYuQ%}x1V6ieeGP1a9)03ulm(1vWHe0%N HICcX7@Y=vL literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/remove_readd_frozen/base.wai b/wai/staged-conformance/cases/remove_readd_frozen/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..67e86163f14d949609221a622444d991a42e66de GIT binary patch literal 551 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp2H7#SGCJ;Mwcfix4)xg2Qt zwUW&pAI$-@&x2@J7>#pN>oaa@^nhDm&!O z(O+sT3np;7{yU#&vwxe`va2tI=4~zGW;nIDxaP6IMTg(@Uqo){SbTdGn|thY1do2E zl1_fh(h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_B^er{H?lbGrQd9$$P&f?`FF6wmE@=!Ds&0oaa+5drFdCGM|=P z$oJeklmE<~zi(`6b6&pkShD($ZjFm_sO`g^b&3Kp3{yhpmRx$7_pyX==XJFS>wV8w zFF)YCtoz7i`Gm+F*Ee076H+|LT(`-% z8cqoDTqV~=ZD!)Gi`g`cd*;Qc7sEu9MqEbB81El|NXZ!6eD){ApjU9f)GCL?%EZq_{ z%h7a6B-9agU6F8A9ZQf@$Iw*SRzzL0i0a5ZQB9jtV#$)IDWa?!nn3WjktprLNxUsv zvcNk$<{gU{s7zFYn7m>ds%8)>5mT0IOA}>Dk`%*GETRdHWlNeSO1k7va?M^E_6?X$!{xpP!$L$KwY#6Q}$$=rl^iViMYll2HnJ ziYCxAl$Ho!B?FLc08^;|FHlArK<0LUEHsUFW&oVa1ZYJrn#}>2DFn#j0D|Z>n%e^q zE&<3Z1=zh8puHR*e;+^rDnv!7vjSkg5}>OJpg07ufV!&zdeA!_U~ zzyKOV7tj#8h%TYas5k;ZrvTz{|K|hHfUB==JijR&7u&YN-q>l{!tnPXAz38N>JXw1+T|c|4cV<19d!L)kEEwLg8Y`)I*t}k{ a{PoJBG3N548IP+z&)>YEU#k^=eEJJJ-O=v= literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/remove_readd_replay/L4.wai b/wai/staged-conformance/cases/remove_readd_replay/L4.wai new file mode 100644 index 0000000000000000000000000000000000000000..a187fc122916ebb248db5bade37dd4c85157c277 GIT binary patch literal 795 zcmbu*&r1|x7zgn0nx=_{TDoTbz#%nL*WKCO+0h^}A(A{qRCX|Oe!MGhjIIA_`DDA^E~tL&f8E^=c{!A zy+W75g=)d)@e1WaE7Q3`yH2jN2<^FPGt;Jo#i9n+6q`9s5uF)lE!Nh-E!MhG?-ev9 zs;J!Hb`N#aNYBOy4-KHsFgKadS1xD~Q|C61nA~g@s(XbNZt6}uw|q|dZ)3YtQI&Q^ z6;)QnkQxrjeo;#TBWe+^Go5XkQC5^qU={SDID->lA<%FsR6$j3W@=_D&*7X zv{8$7(j;%_|6ZD8C_T&)ssnUVTd|rM-HX{_7<~(LjSY3jeJ+Y|0FpLsq4@7{@^i6R z?BHhNl=BTaP2#DTK(>-Zl0=@63G$Sr!~<#~plt)Dl7Z(WEd@y54rGvNvNH`hmkzWL zgUn_DGxAKlnJu~a>?EAuG ndfw2ERlBI{LDPEC@|P=%RilP?B<)efr}>*VSWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp2H7#SGCJ;Mwcfix4)xg2Qt z=_Wh0?NNx(=9N-Mw- literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/remove_readd_replay/case.json b/wai/staged-conformance/cases/remove_readd_replay/case.json new file mode 100644 index 000000000..d352b0831 --- /dev/null +++ b/wai/staged-conformance/cases/remove_readd_replay/case.json @@ -0,0 +1,233 @@ +{ + "format": "wai-staged-case/1", + "name": "remove_readd_replay", + "about": "layers 1 and 2 apply and a layer 4 is held; the sink drops the base's chain, discarding layer 4 (base-absent), and decodes the base again: each unit starts at stage 0 with its record, and layers 1 and 2 offered again replay it", + "bases": [ + { + "file": "base.wai", + "object": "b8b27bf7832cef28853f286fb136f9764d5283bc6f40c6adeecb43275248971c", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "e2b162dbeb0b85c28c386c43164fba9c3b08af92a5258a543361e44019e23fd7", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "0d4b410f0897fb01e4c69ca05a9679ce3ca43db7243335995bc71fbd3772383c", + "bytes": 579 + }, + { + "file": "L2.wai", + "object": "62c1b72aaee563d60027a4be9554c61de2d7035068fb5239869aed8a75485469", + "bytes": 795 + }, + { + "file": "L4.wai", + "object": "97faccae6d0a0cb9c9a5d0126541fee463126fd3a1b5c2026d3338a34cca48d7", + "bytes": 795 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 2, + "evidence": "none" + }, + { + "op": "offer", + "file": "L4.wai", + "at_ms": 3, + "evidence": "none" + }, + { + "op": "remove", + "index": 0, + "at_ms": 4 + }, + { + "op": "base", + "index": 0, + "at_ms": 5 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 6, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 7, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "0d4b410f0897fb01e4c69ca05a9679ce3ca43db7243335995bc71fbd3772383c", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "62c1b72aaee563d60027a4be9554c61de2d7035068fb5239869aed8a75485469", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "97faccae6d0a0cb9c9a5d0126541fee463126fd3a1b5c2026d3338a34cca48d7", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [ + 0, + 1 + ], + "refused": [] + } + ], + [ + { + "object": "97faccae6d0a0cb9c9a5d0126541fee463126fd3a1b5c2026d3338a34cca48d7", + "envelope": "base-absent", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [], + [ + { + "object": "0d4b410f0897fb01e4c69ca05a9679ce3ca43db7243335995bc71fbd3772383c", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "62c1b72aaee563d60027a4be9554c61de2d7035068fb5239869aed8a75485469", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 2, + "producer": "62c1b72aaee563d60027a4be9554c61de2d7035068fb5239869aed8a75485469", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "47a7b508abab4691faaf1552d6ce426d5146b4de94ad9d7ad76a0ddf3e160029" + }, + { + "stage": 2, + "producer": "62c1b72aaee563d60027a4be9554c61de2d7035068fb5239869aed8a75485469", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "132d8b8ca6b0c1061b2d809b42de3fe64c1acd97a61ebb97ac0eadf7b8667186" + } + ], + "clip_sha256": "fd2937f18c3732ca0fa24424bfaf5feedb1fbd7e95732fe2bbc32c41b71e62d0", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/remove_readd_replay/stage0.bin b/wai/staged-conformance/cases/remove_readd_replay/stage0.bin new file mode 100644 index 000000000..7658c82b6 --- /dev/null +++ b/wai/staged-conformance/cases/remove_readd_replay/stage0.bin @@ -0,0 +1 @@ + 0H88H@HXHP`XXhXp€hpˆh€˜0@X@X`PXhXpxhx€p€ˆxˆ ˆ  HXhX`x°Ðøh€ˆxˆ˜€˜ �˜¨  ¸`xx°ØÿÀàÿÀøÿ�˜¨˜ ¸ °À°¸È¸àÿÈèÿÐðÿØøÿàÿÿ°¸À°ÈàÀÈà€�°Ðÿÿèÿÿðÿÿ°ÀØÀÈØÐàèÐèø ¨¸°ÀÀÿÿÿÀÈØÐØèÈàøàðøèðÿ¸¸È¸ÐÐÈØàÐàèØàÿèðÿðÿÿøÿÿ 0H88H@HXHX`XXhXp€hp€p€˜0@X@XhPXhXpxhx€p€ˆxˆ €˜ PXhX`xhp€p€ˆxˆ˜€˜ �˜¨  ¸hxxpx�x€—x˜ŸØøÿ˜ ¸ ¨À°¸Èp€—€ˆ—ˆ�§Øøÿàÿÿøÿÿ°ÈØÀÈà€�¨ˆŸ·èÿÿèÿÿøÿÿÿÿÿÿÿÿÐè𠨸°ÀÀ·¿Ïÿÿÿÿÿÿÿÿÿàèøèðÿ°¸È¸ÐÐÀØØÐàèÿÿÿèðÿðÿÿøÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/index.json b/wai/staged-conformance/index.json index 50db58b1e..fd26766e1 100644 --- a/wai/staged-conformance/index.json +++ b/wai/staged-conformance/index.json @@ -397,6 +397,27 @@ "892935ee985334efbada8760d05d321b5f64e651ddd872af614885a21f487cab" ] }, + { + "name": "remove_readd_replay", + "about": "layers 1 and 2 apply and a layer 4 is held; the sink drops the base's chain, discarding layer 4 (base-absent), and decodes the base again: each unit starts at stage 0 with its record, and layers 1 and 2 offered again replay it", + "clip_sha256": [ + "fd2937f18c3732ca0fa24424bfaf5feedb1fbd7e95732fe2bbc32c41b71e62d0" + ] + }, + { + "name": "remove_readd_frozen", + "about": "layer 1a applies and 1b conflicts, freezing the units; the chain is dropped, 1b arrives and is held for the base, and the base is decoded again: the record keeps the units frozen at stage 0, so 1b is refused for them, and 1a too", + "clip_sha256": [ + "1b0dbc764ee287d74f78a77bf79ea2267133643301bb842882aaa62adcbd1776" + ] + }, + { + "name": "remove_readd_diverge", + "about": "layer 1a applies; the chain is dropped, a different layer 1b arrives and is held for the base, and the base is decoded again: 1b differs from the producer each unit's record holds at stage 1, so it conflicts and freezes the units", + "clip_sha256": [ + "3a0b5f2bc2fcbec02c714c18a06576c9bdc49334f5a76e0e3d4b28c12658774c" + ] + }, { "name": "base_readded", "about": "layer 1a applies and 1b conflicts, freezing the units; the base decoded again keeps them as they are and reports nothing; 1b offered again is still refused", diff --git a/wai/staged-conformance/staged_verify.py b/wai/staged-conformance/staged_verify.py index f128c06f5..69dda8b75 100644 --- a/wai/staged-conformance/staged_verify.py +++ b/wai/staged-conformance/staged_verify.py @@ -601,11 +601,15 @@ def unit_bytes(s): class Base: """A decoded base and its units (§1), with what §4 keeps for them.""" - def __init__(self, info, units, cfg): + def __init__(self, info, units, cfg, record=None): self.info, self.cfg = dict(info), cfg - self.units = [dict(stage=0, producer=info["hash"], frozen=False, tier=info["tier"], shape=s, elem=e, data=list(d)) + self.units = [dict(stage=0, producer=info["hash"], frozen=False, tier=info["tier"], shape=s, elem=e, data=list(d), reached=0) for (s, e, d) in units] self.passed = {} # step 7's record: (u, layer, P(u)) -> the envelope that passed step 8 for u + if record is not None: # §4.4: a base decoded again starts at stage 0 with its record + self.passed = dict(record["passed"]) + for x, frozen, reached in zip(self.units, record["frozen"], record["reached"]): + x["frozen"], x["reached"] = frozen, reached self.held = set() # (envelope hash, u): u held for the envelope (step 8) self.retained = {} # envelope hash -> Envelope, from step 6 to when no unit is held for it (§4.5) @@ -616,10 +620,11 @@ class Base: return {h for h, _ in self.held} def state(self): - """§4.5: units' output bytes and 32 bytes per stage above 0; retained layers' - payload bytes and declared working bytes, and, for each unit a layer covers and - has not been applied to, one unit's output and 32 bytes.""" - s = sum(unit_bytes(x) + 32 * x["stage"] for x in self.units) + """§4.5: units' output bytes and 32 bytes per stage their record holds (the + highest stage each reached); retained layers' payload bytes and declared working + bytes, and, for each unit a layer covers and has not been applied to, one unit's + output and 32 bytes.""" + s = sum(unit_bytes(x) + 32 * x["reached"] for x in self.units) for e in self.retained.values(): n = e.w["n_units"] s += len(e.payload) + e.cost[2] + (n - e.applied) * (e.cost[0] // n + 32) @@ -705,6 +710,7 @@ class Base: s["data"] = apply_unit(w, u - units_of(e.ref, self.n())[0], s["data"], s["shape"]) s["shape"] = w["d"] s["stage"] = e.ref["layer"] + s["reached"] = max(s["reached"], s["stage"]) s["producer"] = e.h s["tier"] = DE if s["tier"] == DE else NONE # a layer acts on reconstructed output (§5) e.applied += 1 @@ -830,6 +836,7 @@ class Sink: def __init__(self, cfg): self.cfg, self.bases, self.waiting = cfg, {}, [] + self.records = {} # §4.4: what a dropped base's units keep def held_elsewhere(self, base): return len(self.waiting) + sum(len(b.held_envelopes()) for k, b in self.bases.items() if k != base) @@ -878,7 +885,7 @@ class Sink: if info["attested"]: self.bases[info["hash"]].info["attested"] = True return [] - b = self.bases[info["hash"]] = Base(info, units, self.cfg) + b = self.bases[info["hash"]] = Base(info, units, self.cfg, self.records.pop(info["hash"], None)) mine = [e for e in self.waiting if e.ref["base"] == info["hash"]] self.waiting = [e for e in self.waiting if e.ref["base"] != info["hash"]] out = Outcome() @@ -886,6 +893,18 @@ class Sink: b.receive(mine, out, 0, False) return out.reports() + def remove_base(self, h): + """§4.4: the sink drops a base's chain. Every envelope held for its units is + discarded (base-absent); each unit's record is kept for the base decoded again.""" + b = self.bases.pop(h, None) + if b is None: + return [] + out = Outcome() + for e in sorted(b.held_envelopes()): + out.refuse(e, "base-absent") + self.records[h] = {"passed": b.passed, "frozen": [x["frozen"] for x in b.units], "reached": [x["reached"] for x in b.units]} + return out.reports() + def expire(self, now): """§4.3: what was held past the bound's duration.""" out = Outcome() @@ -1442,6 +1461,8 @@ def replay(case, bases, files): elif st["op"] == "base": b = bases[st["index"]] got.append(sink.add_base(b, b["units"])) + elif st["op"] == "remove": + got.append(sink.remove_base(bases[st["index"]]["hash"])) else: got.append(sink.expire(st["at_ms"])) finals = [] diff --git a/wai/wai-rs/src/bin/wai_staged_vectors.rs b/wai/wai-rs/src/bin/wai_staged_vectors.rs index 6d6eade77..daf2146d3 100644 --- a/wai/wai-rs/src/bin/wai_staged_vectors.rs +++ b/wai/wai-rs/src/bin/wai_staged_vectors.rs @@ -637,6 +637,8 @@ enum Step { Offer { file: String, at_ms: u64, evidence: ReceiptEvidence }, Base { index: usize, at_ms: u64 }, Expire { at_ms: u64 }, + /// The sink drops the base's chain (§4.4). + Remove { index: usize, at_ms: u64 }, } struct Case { @@ -1249,6 +1251,31 @@ fn cases(ctx: &Ctx) -> Vec { steps: vec![base_at(0, 0), base_at(1, 0), offer("L1_second.wai", 150)] }); } // ---- bases and releases ---- + { + let a = small(155); + let (l1, mid) = l1_of(&a, 156, 4); + let l2 = l2_of(&a, &l1, &mid, 156); + let l4 = edit(&l2, |w| w.manifest.refines = Some(Refines { base: hex(&a.hash()), prev: one(&[7; 32]), layer: 4, span: None }.to_value())); + out.push(simple("remove_readd_replay", "layers 1 and 2 apply and a layer 4 is held; the sink drops the base's chain, discarding layer 4 (base-absent), and decodes the base again: each unit starts at stage 0 with its record, and layers 1 and 2 offered again replay it", a, + vec![("L1.wai".into(), l1), ("L2.wai".into(), l2), ("L4.wai".into(), l4)], + vec![base_at(0, 0), offer("L1.wai", 1), offer("L2.wai", 2), offer("L4.wai", 3), Step::Remove { index: 0, at_ms: 4 }, base_at(0, 5), offer("L1.wai", 6), offer("L2.wai", 7)])); + } + { + let a = small(157); + let (la, _) = l1_of(&a, 158, 4); + let (lb, _) = l1_of(&a, 159, 4); + out.push(simple("remove_readd_frozen", "layer 1a applies and 1b conflicts, freezing the units; the chain is dropped, 1b arrives and is held for the base, and the base is decoded again: the record keeps the units frozen at stage 0, so 1b is refused for them, and 1a too", a, + vec![("L1a.wai".into(), la), ("L1b.wai".into(), lb)], + vec![base_at(0, 0), offer("L1a.wai", 1), offer("L1b.wai", 2), Step::Remove { index: 0, at_ms: 3 }, offer("L1b.wai", 4), base_at(0, 5), offer("L1a.wai", 6)])); + } + { + let a = small(160); + let (la, _) = l1_of(&a, 161, 4); + let (lb, _) = l1_of(&a, 162, 4); + out.push(simple("remove_readd_diverge", "layer 1a applies; the chain is dropped, a different layer 1b arrives and is held for the base, and the base is decoded again: 1b differs from the producer each unit's record holds at stage 1, so it conflicts and freezes the units", a, + vec![("L1a.wai".into(), la), ("L1b.wai".into(), lb)], + vec![base_at(0, 0), offer("L1a.wai", 1), Step::Remove { index: 0, at_ms: 2 }, offer("L1b.wai", 3), base_at(0, 4), offer("L1a.wai", 5)])); + } { let a = small(139); let (la, _) = l1_of(&a, 140, 4); @@ -1367,6 +1394,7 @@ fn run(r: &Replay) -> (Vec, Vec) { sink.add_base(info, units) } Step::Expire { at_ms } => sink.expire(*at_ms), + Step::Remove { index, .. } => sink.remove_base(&r.bases[*index].0.content_hash).unwrap_or_default(), }; reports.push(Value::Array(got.iter().map(report_json).collect())); } @@ -1397,6 +1425,7 @@ fn step_json(s: &Step) -> Value { Step::Offer { file, at_ms, evidence } => json!({"op": "offer", "file": file, "at_ms": at_ms, "evidence": evidence_token(*evidence)}), Step::Base { index, at_ms } => json!({"op": "base", "index": index, "at_ms": at_ms}), Step::Expire { at_ms } => json!({"op": "expire", "at_ms": at_ms}), + Step::Remove { index, at_ms } => json!({"op": "remove", "index": index, "at_ms": at_ms}), } } @@ -1428,6 +1457,7 @@ fn step_from(v: &Value) -> Step { "offer" => Step::Offer { file: v["file"].as_str().expect("file").into(), at_ms: at, evidence: ReceiptEvidence::from_token(v["evidence"].as_str().expect("evidence")).expect("an evidence token") }, "base" => Step::Base { index: v["index"].as_u64().expect("index") as usize, at_ms: at }, "expire" => Step::Expire { at_ms: at }, + "remove" => Step::Remove { index: v["index"].as_u64().expect("index") as usize, at_ms: at }, o => panic!("unknown op {o}"), } } diff --git a/wai/wai-rs/src/staged/mod.rs b/wai/wai-rs/src/staged/mod.rs index c9b3eb5ce..ec122f502 100644 --- a/wai/wai-rs/src/staged/mod.rs +++ b/wai/wai-rs/src/staged/mod.rs @@ -198,18 +198,32 @@ pub struct UnitState { pub frozen: bool, pub tier: Option, pub buf: UnitBuf, - /// The producers of stages 1 to `stage`, in order: the envelopes that passed step - /// 8 for the unit and were applied, which step 7 compares a later layer with. + /// The producers of the stages the unit's record holds, in order: the envelopes + /// that passed step 8 for it and were applied, which step 7 compares a later layer + /// with. As long as `stage` at least; longer while a base decoded again (§4.4) + /// replays its record. producers: Vec<[u8; 32]>, } impl UnitState { - /// The producers of the unit's stages 1 to `stage`, in order (§4.1 step 7). + /// The producers of the stages the unit's record holds, in order (§4.1 step 7, + /// §4.4): stages 1 to `stage`, and past it while the unit replays its record. pub fn producers(&self) -> &[[u8; 32]] { &self.producers } } +/// What a sink keeps of a unit when it drops its base's chain (§4.4): the producers of +/// the stages it reached, the envelope that passed step 8 at its last stage without +/// being applied, and whether it is frozen. 32 bytes for each recorded stage, at most +/// one more 32-byte producer, and a flag. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnitRecord { + pub producers: Vec<[u8; 32]>, + pub passed: Option<[u8; 32]>, + pub frozen: bool, +} + /// How long and how much a sink holds (§4.3). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct HoldBound { @@ -427,16 +441,33 @@ impl StageChain { /// A chain over a decoded base: `units` are the base's units in order (§2), each /// at stage 0 with the base as its producer and the base step's tier. pub fn new(base: BaseInfo, units: Vec, cfg: ChainConfig) -> Self { - let state = units.iter().map(|u| u.samples.byte_len()).fold(0u64, u64::saturating_add); - let units = units + Self::with_records(base, units, cfg, vec![]) + } + + /// A chain over a base decoded again after its chain was dropped (§4.4): each unit + /// at stage 0 with its record, so step 7 compares a layer with the producers the + /// unit had, and a frozen unit stays frozen. A unit with no record starts afresh. + pub fn with_records(base: BaseInfo, units: Vec, cfg: ChainConfig, records: Vec) -> Self { + let mut records = records.into_iter(); + let mut passed = BTreeMap::new(); + let mut state = 0u64; + let units: Vec = units .into_iter() - .map(|buf| UnitState { stage: 0, producer: base.content_hash, frozen: false, tier: base.tier, buf, producers: vec![] }) + .enumerate() + .map(|(u, buf)| { + let r = records.next().unwrap_or(UnitRecord { producers: vec![], passed: None, frozen: false }); + if let Some(h) = r.passed { + passed.insert(u as u32, h); + } + state = state.saturating_add(buf.samples.byte_len()).saturating_add(32 * r.producers.len() as u64); + UnitState { stage: 0, producer: base.content_hash, frozen: r.frozen, tier: base.tier, buf, producers: r.producers } + }) .collect(); StageChain { base, cfg, units, - passed: BTreeMap::new(), + passed, layers: BTreeMap::new(), ids: BTreeMap::new(), held: BTreeMap::new(), @@ -446,6 +477,15 @@ impl StageChain { } } + /// Each unit's record (§4.4), what [`StageSink::remove_base`] keeps. + pub fn records(&self) -> Vec { + self.units + .iter() + .enumerate() + .map(|(u, s)| UnitRecord { producers: s.producers.clone(), passed: self.passed.get(&(u as u32)).copied(), frozen: s.frozen }) + .collect() + } + /// The base this chain refines. pub fn base(&self) -> &BaseInfo { &self.base @@ -581,9 +621,9 @@ impl StageChain { /// §4.1 step 7 against what passed step 8 before: unit `u` is frozen, or an /// envelope other than layer `id`, of the same layer and with the same producer for /// `u`, passed step 8 for it. A unit's stage rises by one per application, so the - /// envelope that passed step 8 for it at layer `k` is the producer of its stage `k` - /// (or, at the stage above its current one, the envelope that passed and was not - /// applied), and the producer it named is that of stage `k − 1`. + /// envelope that passed step 8 for it at layer `k` is the producer of its recorded + /// stage `k` (or, at the stage above its record, the envelope that passed and was + /// not applied), and the producer it named is that of stage `k − 1`. fn conflicted_before(&self, id: u64, u: u32) -> bool { let s = &self.units[u as usize]; if s.frozen { @@ -591,12 +631,12 @@ impl StageChain { } let l = &self.layers[&id]; let Some(p) = self.p_of(id, u) else { return false }; - let (k, stage) = (l.layer as usize, s.stage as usize); - if k <= stage { - let before = if k == 1 { self.base.content_hash } else { s.producers[k - 2] }; - p == before && s.producers[k - 1] != l.hash - } else if k == stage + 1 { - self.passed.get(&u).is_some_and(|h| p == s.producer && *h != l.hash) + let (k, r) = (l.layer as usize, s.producers.len()); + let producer_of = |stage: usize| if stage == 0 { self.base.content_hash } else { s.producers[stage - 1] }; + if k <= r { + p == producer_of(k - 1) && s.producers[k - 1] != l.hash + } else if k == r + 1 { + self.passed.get(&u).is_some_and(|h| p == producer_of(r) && *h != l.hash) } else { false } @@ -732,7 +772,12 @@ impl StageChain { } else { let id = group[0].0; let h = self.layers[&id].hash; - self.passed.insert(u, h); + // A pair at the stage above the unit's record is recorded as passing step + // 8; one replaying the record is that record (step 7 let it through). + let s = &self.units[u as usize]; + if s.stage as usize == s.producers.len() { + self.passed.insert(u, h); + } match self.apply(id, u) { Ok(()) => { reports.get(id, h).applied.push(u); @@ -774,7 +819,12 @@ impl StageChain { s.buf = UnitBuf { shape, samples: match next { Next::U8(v) => Samples::U8(v), Next::I16(v) => Samples::I16(v) } }; s.stage = layer; s.producer = hash; - s.producers.push(hash); + // A new stage is recorded; a stage the record holds is replayed (step 7 let + // only its recorded producer through). + let recorded = (layer as usize) <= s.producers.len(); + if !recorded { + s.producers.push(hash); + } s.tier = tier::compose(s.tier, &[Some(DeterminismTier::DecodeEquivalence)], &[true]); let new = s.buf.samples.byte_len(); // The unit's share of the layer's reserve (its output and its stage) is now held @@ -782,8 +832,10 @@ impl StageChain { let l = self.layers.get_mut(&id).expect("retained"); let share = l.share.min(l.reserve); l.reserve -= share; - self.state = self.state.saturating_sub(old).saturating_add(new).saturating_add(32).saturating_sub(share); - self.passed.remove(&u); + self.state = self.state.saturating_sub(old).saturating_add(new).saturating_add(if recorded { 0 } else { 32 }).saturating_sub(share); + if !recorded { + self.passed.remove(&u); + } Ok(()) } @@ -1018,13 +1070,15 @@ pub struct StageSink { cfg: ChainConfig, chains: BTreeMap<[u8; 32], StageChain>, pending: Vec, + /// The unit records of bases whose chains were dropped (§4.4). + records: BTreeMap<[u8; 32], Vec>, } impl StageSink { /// A sink holding no base, with the bound, cost limit, budget and schedule every /// chain it creates uses. pub fn new(cfg: ChainConfig) -> Self { - StageSink { cfg, chains: BTreeMap::new(), pending: vec![] } + StageSink { cfg, chains: BTreeMap::new(), pending: vec![], records: BTreeMap::new() } } /// The chain of the base whose content hash is `base`. @@ -1044,7 +1098,8 @@ impl StageSink { } return vec![]; } - let mut chain = StageChain::new(base, units, self.cfg.clone()); + let records = self.records.remove(&hash).unwrap_or_default(); + let mut chain = StageChain::with_records(base, units, self.cfg.clone(), records); let hexed = hex(&hash); let (mine, rest): (Vec, Vec) = std::mem::take(&mut self.pending).into_iter().partition(|p| p.refines.base == hexed); self.pending = rest; @@ -1060,10 +1115,22 @@ impl StageSink { self.chains.get_mut(base).map(StageChain::mark_attested).is_some() } - /// Forget a base the sink no longer presents, with its chain; a layer for it is - /// then held as for any base the sink has not decoded. Whether it held the base. - pub fn remove_base(&mut self, base: &[u8; 32]) -> bool { - self.chains.remove(base).is_some() + /// Drop the chain of a base the sink no longer presents (§4.4): every envelope held + /// for its units is discarded (`base-absent`, one report each, in the order they + /// passed step 6), and each unit's record is kept, so the base decoded again + /// advances only along the producers it had. A layer for the base is then held as + /// for any base the sink has not decoded. `None` when the sink does not hold it. + pub fn remove_base(&mut self, base: &[u8; 32]) -> Option> { + let chain = self.chains.remove(base)?; + let reports = chain.layers.values().filter(|l| l.held > 0).map(|l| OfferReport::refuse(l.hash, StageRefusal::BaseAbsent, None)).collect(); + self.records.insert(*base, chain.records()); + Some(reports) + } + + /// Discard the record of a base whose chain was dropped (§14): decoded again, the + /// base starts with no history, as one never decoded. Whether there was a record. + pub fn forget_base(&mut self, base: &[u8; 32]) -> bool { + self.records.remove(base).is_some() } /// Offer the envelope `bytes` at `now_ms` with the receipt `evidence` (§4.1). A diff --git a/wai/wai-rs/src/staged/tests.rs b/wai/wai-rs/src/staged/tests.rs index 6c456fca3..ed8f90d4e 100644 --- a/wai/wai-rs/src/staged/tests.rs +++ b/wai/wai-rs/src/staged/tests.rs @@ -884,12 +884,72 @@ fn readding_a_base_keeps_its_units() { assert!(sink.chain(&b).unwrap().unit(1).unwrap().frozen); assert_eq!(codes(&sink.offer(&lb, 2, ReceiptEvidence::None).unwrap()[0]), vec![(1, "layer-conflict")]); assert_eq!(sink.chain(&b).unwrap().unit(1).unwrap().producer, h(&la)); - // Forgotten, then decoded again: a new chain. - assert!(sink.remove_base(&b)); + // Dropped and decoded again: the record keeps unit 1 frozen, so the layer that + // conflicted, held for the base meanwhile, is refused again on release. + assert_eq!(sink.remove_base(&b), Some(vec![])); assert!(sink.chain(&b).is_none()); assert!(sink.offer(&lb, 3, ReceiptEvidence::None).unwrap()[0].held_for_base); - let r = sink.add_base(info, units); - assert_eq!(r[0].applied, vec![1]); + let r = sink.add_base(info.clone(), units.clone()); + assert_eq!(codes(&r[0]), vec![(1, "layer-conflict")]); + let u1 = sink.chain(&b).unwrap().unit(1).unwrap().clone(); + assert!(u1.frozen && u1.stage == 0 && u1.producers() == [h(&la)]); + // Forgotten as well: a base never decoded. + sink.remove_base(&b); + assert!(sink.forget_base(&b)); + assert!(sink.offer(&lb, 4, ReceiptEvidence::None).unwrap()[0].held_for_base); + assert_eq!(sink.add_base(info, units)[0].applied, vec![1]); +} + +/// A base whose chain was dropped and that is decoded again replays its record (§4.4): +/// each unit starts at stage 0 and advances only along the producers it had, a layer +/// whose key a recorded stage holds with other content conflicts, and a layer held for +/// the dropped chain is reported `base-absent`. +#[test] +fn removed_base_keeps_its_record() { + let (_, info, units) = base4(); + let b = info.content_hash; + let below: Vec> = (0..4).map(frame).collect(); + let l1 = layer(&b, one(&b), 1, None, &below, 50); + let mut probe = StageChain::new(info.clone(), units.clone(), cfg()); + probe.offer(&l1, 0, ReceiptEvidence::None).unwrap(); + let mid: Vec> = (0..4).map(|f| unit_frame(&probe, f)).collect(); + let l2 = layer(&b, Prev::One(hexs(&h(&l1))), 2, None, &mid, 60); + let l1x = layer(&b, one(&b), 1, None, &below, 51); + let l3 = edit_refines(&l2, Refines { base: hexs(&b), prev: Prev::One(hexs(&[7; 32])), layer: 4, span: None }); + let mut sink = StageSink::new(cfg()); + sink.add_base(info.clone(), units.clone()); + sink.offer(&l1, 0, ReceiptEvidence::None).unwrap(); + sink.offer(&l2, 1, ReceiptEvidence::None).unwrap(); + assert_eq!(sink.offer(&l3, 2, ReceiptEvidence::None).unwrap()[0].held, vec![0, 1, 2, 3]); + let before = sink.chain(&b).unwrap().units().to_vec(); + let state_before = sink.chain(&b).unwrap().state_bytes(); + // The held layer 4 is discarded with the chain. + let r = sink.remove_base(&b).unwrap(); + assert_eq!(r.len(), 1); + assert_eq!((r[0].content_hash, r[0].envelope), (h(&l3), Some(StageRefusal::BaseAbsent))); + assert_eq!(sink.remove_base(&b), None); + // Decoded again: stage 0, the record's producers counted in the state. + assert!(sink.add_base(info.clone(), units.clone()).is_empty()); + let c = sink.chain(&b).unwrap(); + assert!(c.units().iter().all(|u| u.stage == 0 && u.producers() == [h(&l1), h(&l2)])); + let unit_bytes = (H * W * 3) as u64; + assert_eq!(c.state_bytes(), 4 * unit_bytes + 4 * 2 * 32); + // Another layer 1 conflicts with the record and freezes the units. + let mut other = sink.clone(); + assert_eq!(codes(&other.offer(&l1x, 3, ReceiptEvidence::None).unwrap()[0]), vec![(0, "layer-conflict"), (1, "layer-conflict"), (2, "layer-conflict"), (3, "layer-conflict")]); + assert!(other.chain(&b).unwrap().units().iter().all(|u| u.frozen && u.stage == 0)); + // The recorded layers replay to the same stage, and the state does not grow. + assert_eq!(sink.offer(&l1, 4, ReceiptEvidence::None).unwrap()[0].applied, vec![0, 1, 2, 3]); + assert_eq!(sink.offer(&l2, 5, ReceiptEvidence::None).unwrap()[0].applied, vec![0, 1, 2, 3]); + assert_eq!(sink.chain(&b).unwrap().units(), &before[..]); + assert_eq!(sink.chain(&b).unwrap().state_bytes(), 4 * unit_bytes + 4 * 2 * 32, "replayed stages add nothing"); + assert!(state_before > 4 * unit_bytes + 4 * 2 * 32, "layer 4 was retained before"); +} + +fn edit_refines(env: &[u8], r: Refines) -> Vec { + let mut w = Wai::from_bytes(env).unwrap(); + w.manifest.refines = Some(r.to_value()); + w.to_bytes().unwrap() } /// A base decoded before its delivery receipt is verified becomes attested when the -- 2.54.0 (Apple Git-157) From 9b5e19f83edb483f7dff301b3660e991ce4ce02f Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 4 Oct 2026 22:32:33 -0400 Subject: [PATCH 29/36] wai: a test that a unit frozen with nothing recorded stays frozen when its base is decoded again Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/src/staged/tests.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/wai/wai-rs/src/staged/tests.rs b/wai/wai-rs/src/staged/tests.rs index ed8f90d4e..f6f6615fe 100644 --- a/wai/wai-rs/src/staged/tests.rs +++ b/wai/wai-rs/src/staged/tests.rs @@ -944,6 +944,19 @@ fn removed_base_keeps_its_record() { assert_eq!(sink.chain(&b).unwrap().units(), &before[..]); assert_eq!(sink.chain(&b).unwrap().state_bytes(), 4 * unit_bytes + 4 * 2 * 32, "replayed stages add nothing"); assert!(state_before > 4 * unit_bytes + 4 * 2 * 32, "layer 4 was retained before"); + // A unit frozen with nothing recorded (two layers 1 conflicting in one batch) stays + // frozen when the base is decoded again: its flag is part of its record. + let la = layer(&b, one(&b), 1, Some(Span { from: 1, count: 1 }), &[frame(1)], 80); + let lb = layer(&b, one(&b), 1, Some(Span { from: 1, count: 1 }), &[frame(1)], 81); + let mut sink = StageSink::new(cfg()); + sink.offer(&la, 0, ReceiptEvidence::None).unwrap(); + sink.offer(&lb, 1, ReceiptEvidence::None).unwrap(); + sink.add_base(info.clone(), units.clone()); + let u1 = sink.chain(&b).unwrap().unit(1).unwrap().clone(); + assert!(u1.frozen && u1.producers().is_empty()); + sink.remove_base(&b); + sink.add_base(info, units); + assert_eq!(codes(&sink.offer(&la, 2, ReceiptEvidence::None).unwrap()[0]), vec![(1, "layer-conflict")]); } fn edit_refines(env: &[u8], r: Refines) -> Vec { -- 2.54.0 (Apple Git-157) From ab0e1981824b734ceba48efb450753b8ee1217bc Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 4 Oct 2026 22:33:24 -0400 Subject: [PATCH 30/36] wai: a staged case of a unit frozen in a batch keeping its flag across a dropped chain Co-Authored-By: Claude Opus 5.5 --- .../cases/remove_readd_batch_frozen/L1a.wai | Bin 0 -> 587 bytes .../cases/remove_readd_batch_frozen/L1b.wai | Bin 0 -> 587 bytes .../cases/remove_readd_batch_frozen/base.wai | Bin 0 -> 559 bytes .../cases/remove_readd_batch_frozen/case.json | 215 ++++++++++++++++++ .../remove_readd_batch_frozen/stage0.bin | 1 + wai/staged-conformance/index.json | 7 + wai/wai-rs/src/bin/wai_staged_vectors.rs | 8 + 7 files changed, 231 insertions(+) create mode 100644 wai/staged-conformance/cases/remove_readd_batch_frozen/L1a.wai create mode 100644 wai/staged-conformance/cases/remove_readd_batch_frozen/L1b.wai create mode 100644 wai/staged-conformance/cases/remove_readd_batch_frozen/base.wai create mode 100644 wai/staged-conformance/cases/remove_readd_batch_frozen/case.json create mode 100644 wai/staged-conformance/cases/remove_readd_batch_frozen/stage0.bin diff --git a/wai/staged-conformance/cases/remove_readd_batch_frozen/L1a.wai b/wai/staged-conformance/cases/remove_readd_batch_frozen/L1a.wai new file mode 100644 index 0000000000000000000000000000000000000000..a019b0a9710c4485a546acec74e6a71bae3fcc42 GIT binary patch literal 587 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZYX2}WFob&s88QNclL^S?fMRAK4FfDd z#-IQH|MNfv!hn*D0zhoUz_2=of#H8Q14GDq28Q6U$`VdX!d*nf~>Mg|`s3Bnwr zUj-N5(luvU@P13e?*q@zAANCNGHzq`?u+{j3l6mO`G;`L^3k-GEOT7h;N7M##iHr> zK(bs7tI%RFgnS#fOn*$;o&q?-7uo%!;Z_dlC(?@wxVO}%AU#{W8&{+^FaY3&)D FrvO#{z0Uvu literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/remove_readd_batch_frozen/L1b.wai b/wai/staged-conformance/cases/remove_readd_batch_frozen/L1b.wai new file mode 100644 index 0000000000000000000000000000000000000000..d42a616c583e015099ad303d1d4a956d033afec2 GIT binary patch literal 587 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZYX2}WFob&s88QNclL^S?fMRAK4FfDd z#-IQH|MNfv7=a2IfgDB!28J+428LA?3=ID^GB5<+1kxXY_%{Q?|NkJxj0`?N5`-sp zMJ~T{9H{<>%a#j!T+U6K7-14sVfXsbsY~t)FS~tIh;e@)b?tErK<9aGw^;OEM?0U~P*oA#Rp>wio`+8QOnd(Xb>dRisi`7qNzOtNm!PP_Y z8%(sir-+4})@JeFBvLxP!D_=~hxPv!iz=*Rob~5)!n75x`?oKDP>?afyl#PTlz;F8 FIRMG|z!U%g literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/remove_readd_batch_frozen/base.wai b/wai/staged-conformance/cases/remove_readd_batch_frozen/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..d692037b112a390f18af9c3937cbe90667204d58 GIT binary patch literal 559 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp217#SGCJ;Mwcfix4)xg2Qt zk}1H+%i3=Dy*f%JMH-V9{#2GWOt^l2cz3dHw;{HF{I%il0C1bqbJ zuM7;q-x(NI{$gPG{~zQ6pd*3c0uVoB)~;L))bp74vHmTA8AlXltus&Roe{2)c=jw( zNx1xlw)e$I)PHl_`e>{A9G!-{@M`6Eb%?%C+; Vec { vec![("L1a.wai".into(), la), ("L1b.wai".into(), lb)], vec![base_at(0, 0), offer("L1a.wai", 1), offer("L1b.wai", 2), Step::Remove { index: 0, at_ms: 3 }, offer("L1b.wai", 4), base_at(0, 5), offer("L1a.wai", 6)])); } + { + let a = small(163); + let (la, _) = l1_of(&a, 164, 4); + let (lb, _) = l1_of(&a, 165, 4); + out.push(simple("remove_readd_batch_frozen", "layers 1a and 1b, held for the base, conflict in one batch and freeze the units with nothing applied; the chain is dropped and the base decoded again: the frozen flags are part of the record, so 1a is refused", a, + vec![("L1a.wai".into(), la), ("L1b.wai".into(), lb)], + vec![offer("L1a.wai", 0), offer("L1b.wai", 1), base_at(0, 2), Step::Remove { index: 0, at_ms: 3 }, base_at(0, 4), offer("L1a.wai", 5)])); + } { let a = small(160); let (la, _) = l1_of(&a, 161, 4); -- 2.54.0 (Apple Git-157) From 10c54973514b5d932a3aa26ef458d7f8607aba53 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 4 Oct 2026 22:38:15 -0400 Subject: [PATCH 31/36] wai: the browser writers refuse a manifest that names a member twice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wai-web's pack and packMulti parsed the manifest with serde_json, which keeps the last of two equal names outside a struct's own fields, and then wrote the text as given, so they could emit an envelope every reader refuses (SPEC §2). Both now read the manifest through check_unique_members first. A native test covers a repeat at the top, in target, in model_requirement, nested with an escape, and in a WAI2 entry; verify_pack.mjs checks the built wasm writers refuse it; the CI native test count is 9. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-web-wasm.yml | 7 +++--- wai/wai-web/demo/verify_pack.mjs | 22 ++++++++++++++-- wai/wai-web/src/lib.rs | 40 ++++++++++++++++++++++++++---- 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/.github/workflows/wai-web-wasm.yml b/.github/workflows/wai-web-wasm.yml index cd648b6c9..6470617c8 100644 --- a/.github/workflows/wai-web-wasm.yml +++ b/.github/workflows/wai-web-wasm.yml @@ -11,7 +11,8 @@ name: wai-web wasm32 # capability, none is a candidate (SPEC §5), and the wasm build's own # registry agrees with the export on every candidate; # - demo/verify_pack.mjs: the browser writers `pack` and `packMulti` refuse -# every candidate the export lists, in every place an envelope can name one; +# every candidate the export lists, in every place an envelope can name one, +# and a manifest that names a member twice in one object; # - a byte comparison of the demo's staged int_synth copies with the corpus; # - the crate's wasm_tests under wasm-bindgen-test-runner, and its native unit # tests on the host. @@ -90,7 +91,7 @@ jobs: # bindings, which wasm_tests does not run. They decode the committed integer # payloads to their sidecars' SHA-256 (int_payload::tests) and check that the # writers `pack` and `packMulti` refuse a payload that is not the format SPEC §5 - # pins its integer capability to (tests). The count check turns a test that + # pins its integer capability to, and a manifest that names a member twice (tests). The count check turns a test that # stopped being compiled into a failure instead of a silent "0 passed". - name: wai-web native unit tests shell: bash @@ -98,7 +99,7 @@ jobs: run: | set -euo pipefail cargo test --locked --lib 2>&1 | tee "$RUNNER_TEMP/wai-web-native.log" - grep -q "test result: ok. 8 passed; 0 failed" "$RUNNER_TEMP/wai-web-native.log" + grep -q "test result: ok. 9 passed; 0 failed" "$RUNNER_TEMP/wai-web-native.log" - name: wasm32 int_synth decode equals the frozen native corpus shell: bash working-directory: wai/wai-web diff --git a/wai/wai-web/demo/verify_pack.mjs b/wai/wai-web/demo/verify_pack.mjs index e7cfce242..10bc191b6 100644 --- a/wai/wai-web/demo/verify_pack.mjs +++ b/wai/wai-web/demo/verify_pack.mjs @@ -3,7 +3,8 @@ // envelope can name one (the WAI1 capability or fallback; a WAI2 component in // either role), and still write an envelope that names none. The candidates are // read from the committed export wai/registry/capabilities.json, so a newly -// registered candidate is checked without editing this script. +// registered candidate is checked without editing this script. They also refuse a +// manifest that names a member twice in one object (SPEC §2), which no reader reads. // // node verify_pack.mjs [path/to/wai_web.js] [path/to/capabilities.json] // The package path is relative to this directory (as in the other verify scripts); @@ -55,6 +56,23 @@ for (const cand of candidates) { refuses(`packMulti, ${cand} as a companion`, () => wai.packMulti(v2([['wai.video.av1'], [cand, 'wai.video.av1']]), [bytes, bytes])); } +// A manifest that names a member twice in one object is refused (SPEC §2): every +// reader refuses it, so a writer never emits it. +function refusesTwice(what, write) { + try { + write(); + fail(`${what}: written, but no reader reads it`); + } catch (e) { + if (!String(e.message ?? e).includes('twice')) fail(`${what}: refused for another reason: ${e.message ?? e}`); + } +} +const v1Text = v1('wai.image.png'); +refusesTwice('pack, a member twice at the top', () => wai.pack(v1Text.slice(0, -1) + ',"zz":1,"zz":2}', bytes)); +refusesTwice('pack, a member twice in target', () => wai.pack(v1Text.replace('"target":null', '"target":{"w":1,"w":2}'), bytes)); +refusesTwice('pack, a member twice in model_requirement', + () => wai.pack(v1Text.replace('"capability":"wai.image.png"', '"capability":"wai.image.png","q":1,"q":2'), bytes)); +refusesTwice('packMulti, a member twice in an entry', + () => wai.packMulti(v2([['wai.video.av1']]).replace('"kind":"bytes"', '"kind":"bytes","zz":1,"zz":2'), [bytes])); // An envelope that names no candidate is still written, and parses back. try { const one = wai.pack(v1('wai.image.png', 'wai.image.jpeg'), bytes); @@ -64,6 +82,6 @@ try { } catch (e) { fail(`an envelope with no candidate was refused: ${e.message ?? e}`); } -console.log(pass ? `PASS pack and packMulti refuse every candidate (${candidates.join(', ')})` +console.log(pass ? `PASS pack and packMulti refuse every candidate (${candidates.join(', ')}) and a member named twice` : '\n✗ a browser writer emits a candidate'); process.exit(pass ? 0 : 1); diff --git a/wai/wai-web/src/lib.rs b/wai/wai-web/src/lib.rs index c7c5f5a5e..11819025a 100644 --- a/wai/wai-web/src/lib.rs +++ b/wai/wai-web/src/lib.rs @@ -479,6 +479,14 @@ fn pack_refusal(m: &wai::container::Manifest, payload: &[u8]) -> Result<(), Stri Ok(()) } +/// A manifest a writer emits as given, read as every reader reads it (SPEC §2): a +/// member named twice in one object, at any depth, is refused, so a writer never emits +/// an envelope the reader refuses. +fn read_manifest(json: &str) -> Result { + wai::container::check_unique_members(json.as_bytes()).map_err(|e| format!("manifest parse: {e}"))?; + serde_json::from_str(json).map_err(|e| format!("manifest parse: {e}")) +} + /// What [`pack_multi`] refuses before it writes, once it holds the payloads: a /// `security` member that is not a security object (SPEC §6.1), and a component whose /// payload is not the format SPEC §5 pins its integer capability to. @@ -499,11 +507,11 @@ fn pack_multi_refusal(manifest: &wai::container::ManifestV2, payloads: &[Vec /// that names a candidate capability as its capability or fallback, which an /// encoder MUST NOT emit (SPEC §5, *Candidate capabilities*), and a payload that is /// not the format SPEC §5 pins its integer capability or fallback to. The manifest is -/// written as given. +/// written as given, so a manifest that names a member twice in one object is refused +/// (SPEC §2), as every reader refuses it. #[wasm_bindgen(js_name = pack)] pub fn pack(manifest_json: &str, payload: &[u8]) -> Result, JsError> { - let m: wai::container::Manifest = serde_json::from_str(manifest_json) - .map_err(|e| JsError::new(&format!("manifest parse: {e}")))?; + let m: wai::container::Manifest = read_manifest(manifest_json).map_err(|e| JsError::new(&e))?; pack_refusal(&m, payload).map_err(|e| JsError::new(&e))?; let mb = manifest_json.as_bytes(); let mut out = Vec::with_capacity(12 + mb.len() + payload.len()); @@ -653,8 +661,7 @@ pub fn parse_multi(bytes: &[u8]) -> Result { /// to (SPEC §5). #[wasm_bindgen(js_name = packMulti)] pub fn pack_multi(manifest_json: &str, payloads: js_sys::Array) -> Result, JsError> { - let manifest: wai::container::ManifestV2 = serde_json::from_str(manifest_json) - .map_err(|e| JsError::new(&format!("manifest parse: {e}")))?; + let manifest: wai::container::ManifestV2 = read_manifest(manifest_json).map_err(|e| JsError::new(&e))?; wai::container::check_component_candidates(manifest.renditions.iter().map(|r| r.capability.as_str())) .map_err(|e| JsError::new(&e.to_string()))?; let n = payloads.length() as usize; @@ -714,6 +721,29 @@ mod tests { /// `WIV1` cut short, and a keyframe-0 `WIV1` under `wai.video.int_hyper`, as the /// capability or the fallback of a `WAI1` and as a `WAI2` component. The committed /// payloads pass. + /// The writers read a manifest as every reader does (SPEC §2): a member named twice + /// in one object, at the top, in `target`, in `model_requirement`, nested, or in a + /// `WAI2` entry, is refused; the same manifest without the repeat is read. + #[test] + fn the_writers_refuse_a_member_named_twice() { + use wai::container::{Manifest, ManifestV2}; + let ok = r#"{"wai":"1.0","media":"binary","intent":"improve","model_requirement":{"capability":"wai.image.png"},"conditioning":{"kind":"png"},"target":null}"#; + assert!(super::read_manifest::(ok).is_ok()); + for twice in [ + r#"{"wai":"1.0","media":"binary","intent":"improve","model_requirement":{"capability":"wai.image.png"},"conditioning":{"kind":"png"},"target":null,"zz":1,"zz":2}"#, + r#"{"wai":"1.0","media":"binary","intent":"improve","model_requirement":{"capability":"wai.image.png"},"conditioning":{"kind":"png"},"target":{"w":1,"w":2}}"#, + r#"{"wai":"1.0","media":"binary","intent":"improve","model_requirement":{"capability":"wai.image.png","q":1,"q":2},"conditioning":{"kind":"png"},"target":null}"#, + r#"{"wai":"1.0","media":"image","intent":"replicate","model_requirement":{"capability":"wai.image.png"},"conditioning":{"kind":"png"},"target":{"a":[{"b":1,"\u0062":1}]}}"#, + ] { + let e = super::read_manifest::(twice).unwrap_err(); + assert!(e.contains("twice"), "{twice}: {e}"); + } + let v2 = r#"{"wai":"1.1","media":"video","intent":"improve","target":null,"renditions":[{"capability":"wai.image.png","kind":"png"}]}"#; + assert!(super::read_manifest::(v2).is_ok()); + let e = super::read_manifest::(&v2.replace(r#""kind":"png""#, r#""kind":"png","zz":1,"zz":2"#)).unwrap_err(); + assert!(e.contains("twice"), "{e}"); + } + #[test] fn the_writers_refuse_a_payload_that_is_not_the_pinned_format() { use wai::container::{Conditioning, Manifest, ManifestV2, ModelRequirement}; -- 2.54.0 (Apple Git-157) From fa1b60fa2dded3a93efe2644356b16201e477251 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 4 Oct 2026 22:39:27 -0400 Subject: [PATCH 32/36] wai: verify_pack.mjs names its failure for both rules Co-Authored-By: Claude Opus 5.5 --- wai/wai-web/demo/verify_pack.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wai/wai-web/demo/verify_pack.mjs b/wai/wai-web/demo/verify_pack.mjs index 10bc191b6..1b0524844 100644 --- a/wai/wai-web/demo/verify_pack.mjs +++ b/wai/wai-web/demo/verify_pack.mjs @@ -83,5 +83,5 @@ try { fail(`an envelope with no candidate was refused: ${e.message ?? e}`); } console.log(pass ? `PASS pack and packMulti refuse every candidate (${candidates.join(', ')}) and a member named twice` - : '\n✗ a browser writer emits a candidate'); + : '\n✗ a browser writer emits what no reader may take'); process.exit(pass ? 0 : 1); -- 2.54.0 (Apple Git-157) From 61edd17971a5775f8261aa9ca7feaf7c823482b4 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 4 Oct 2026 23:49:22 -0400 Subject: [PATCH 33/36] wai: attestation belongs to the base hash and survives a dropped chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit remove_base kept unit records but not that the base was attested, so a base dropped and decoded again by a decode that carried no receipt came back unattested and layers with no receipt applied; and mark_attested only reached a held chain. The sink now keeps the attestation of every base it does not hold (dropped while attested, or marked before it was decoded). mark_attested(hash) records it whether the base is held, dropped or not yet decoded; add_base attests a base it marked before, whatever the decode says; forget_base discards it with the record. §4.4 and §10 state it. staged_verify.py does the same; the corpus gains an attest step and the cases attested_across_drop and attested_before_decode. Co-Authored-By: Claude Opus 5.5 --- wai/CHANGELOG.md | 4 +- wai/extensions/staged-delivery.md | 9 +- .../cases/attested_across_drop/L1.wai | Bin 0 -> 583 bytes .../cases/attested_across_drop/base_0.wai | Bin 0 -> 559 bytes .../cases/attested_across_drop/base_1.wai | Bin 0 -> 559 bytes .../cases/attested_across_drop/case.json | 234 ++++++++++++++++++ .../cases/attested_across_drop/stage0_0.bin | Bin 0 -> 384 bytes .../cases/attested_across_drop/stage0_1.bin | Bin 0 -> 384 bytes .../cases/attested_before_decode/L1.wai | Bin 0 -> 587 bytes .../cases/attested_before_decode/base.wai | Bin 0 -> 555 bytes .../cases/attested_before_decode/case.json | 144 +++++++++++ .../cases/attested_before_decode/stage0.bin | 1 + wai/staged-conformance/index.json | 15 ++ wai/staged-conformance/staged_verify.py | 17 ++ wai/wai-rs/src/bin/wai_staged_vectors.rs | 27 ++ wai/wai-rs/src/staged/mod.rs | 38 ++- wai/wai-rs/src/staged/tests.rs | 20 +- 17 files changed, 493 insertions(+), 16 deletions(-) create mode 100644 wai/staged-conformance/cases/attested_across_drop/L1.wai create mode 100644 wai/staged-conformance/cases/attested_across_drop/base_0.wai create mode 100644 wai/staged-conformance/cases/attested_across_drop/base_1.wai create mode 100644 wai/staged-conformance/cases/attested_across_drop/case.json create mode 100644 wai/staged-conformance/cases/attested_across_drop/stage0_0.bin create mode 100644 wai/staged-conformance/cases/attested_across_drop/stage0_1.bin create mode 100644 wai/staged-conformance/cases/attested_before_decode/L1.wai create mode 100644 wai/staged-conformance/cases/attested_before_decode/base.wai create mode 100644 wai/staged-conformance/cases/attested_before_decode/case.json create mode 100644 wai/staged-conformance/cases/attested_before_decode/stage0.bin diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 140d0b28c..d52d887aa 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -37,8 +37,8 @@ wire format may change and such changes are recorded here. units, and §14 states how. A sink that drops a base's chain discards the layers held for it (`base-absent`) and keeps each unit's record (its producers, a pending producer, its frozen flag); the base decoded again advances only along that record. - Decoding a base again keeps its chain, and a receipt verified for the base after it - was decoded makes it attested from then on; a layer held for its base and offered + Decoding a base again keeps its chain, and a receipt verified for the base, before or + after it was decoded, makes it attested from then on, across dropped chains; a layer held for its base and offered again keeps the strongest receipt evidence, and over an attested base counts as arriving when its receipt was verified; a set split across bases shares one live clock (`first_unit`). diff --git a/wai/extensions/staged-delivery.md b/wai/extensions/staged-delivery.md index 7c159be41..7fa74c2d2 100644 --- a/wai/extensions/staged-delivery.md +++ b/wai/extensions/staged-delivery.md @@ -185,7 +185,10 @@ batch can release another, which is evaluated the same way. A sink that decodes again a base whose chain it holds keeps the chain: re-decoding a base does not reset its units' stages, producers or frozen state, and releases and reports nothing. If the sink has verified the base's delivery receipt since, the base -is attested from then on (§10); a base once attested stays attested. A sink **MAY** drop a base's chain when it no longer presents the +is attested from then on (§10). Attestation belongs to the base's content hash: a base +once attested stays attested, held or dropped, and a decode that carries no receipt +does not undo it; a receipt the sink verifies before it decodes the base makes the base +attested when it is decoded. Only forgetting the base (§14) ends it. A sink **MAY** drop a base's chain when it no longer presents the base. Every envelope held for the chain's units is then discarded (`base-absent`, reported once for each), and an envelope for that base is held as for any base the sink has not decoded. The sink keeps each unit's **record**: the producers of the @@ -336,7 +339,9 @@ key the sink holds with the authority `delivery` under the same origin hash; it never authorizes an application. A content hash in `refines` binds a layer to a base, not to its publisher. A sink that verifies a base's delivery receipt after it decoded the base applies step 5 to every envelope that arrives after it; envelopes -it admitted before keep the decision they had. +it admitted before keep the decision they had. The verification belongs to the base's +content hash and holds across the base's chain being dropped and the base decoded +again (§4.4). ## 11. Counting bytes diff --git a/wai/staged-conformance/cases/attested_across_drop/L1.wai b/wai/staged-conformance/cases/attested_across_drop/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..224a5ba456d5ff36eb423a4a0d8ea03f28ce5682 GIT binary patch literal 583 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_cYMyGA1axgd zQEC}=Ov*{DOf6EfG6cG`B(W$x6&Nmgr8zmZKNuJo!aaiw8G*sc1Y~nSF*A^c0Tv+R z&;S4bxuF89fdXOTKy1puusVr>A+(c$A!IuP!@tKs`YQv&|NkI`j0`?N5`--eu57z5 zZN>00S<16eX_h|It0yPly`O2{XJmU^`-=XT-}@y3KmJQwns>Rgq(D4USt5{{( zl~5yh)^x+SV+$6t`ddy?%{2cLY%Qv*li$?y)3;Ab*Y4f|SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp217#SGCJ;Mwcfix4)xg2Qt z!~g&Pm4RX)um*_NvM?|(voSEN11el6z`(Fxh=CzW6i6!ou?hnNs}=)8v@Qcfj6MUy zCKI4}77PrVtr!?$-5D7E&Sqd(v50};-*O;c%fJw_nSo*DE+Bmn$UhBaUjoYC1L7w@ zwXcEnJD~VyApXw4@c$BFBnHeb&w8?M`Okb8aJ z0l$OleeRFz3T+N7mYsY_US##%$-lk+N(*M6D792|=`dLR>h~7O?Z?*hpEQ#(+^p2> zbjqex>eW-0r1$rKJc+I5OfJ8|C9&x0KHm5%yrJH*4ZFAxOm5{&$XZoh8NL!oN98#) iB}`{+iJ!dhAOE!@X|H*kgLhnws+LZ>!JW4=xDfzOaKifl literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/attested_across_drop/base_1.wai b/wai/staged-conformance/cases/attested_across_drop/base_1.wai new file mode 100644 index 0000000000000000000000000000000000000000..44ce7cec665419036dca56402ac6ae3ea73c430d GIT binary patch literal 559 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp217#SGCJ;Mwcfix4)xg2Qt z!~g&Pm4RX)um*_NvM?|(voSEN11el6z`(Fxh=CzW6i6!ou?hnNs}=)8v@Qcfj6MUy zCKI4}77PrVtr!?$-5D7E&Sqd(v50};-*O;c%fJw_nSo*DE+Bmn$UhBaUjoYC1L7w@ zwXcEnJD~VyApXw4@c$BFBnHeb&w8?M`Okb8aJ z0l$OleeRFz3T+N7mYsY_US##%$-lk+N(*M6D792|=`dLR>h~7O?Z?*hpEQ#(+^p2> zbjqex>eW-0r1$rKJc+I5OfJ8|C9&x0KHm5%yrJH*4ZFAxOm5{&$XZoh8NL!oN98#) iB}`{+iJ!dhAOE!@X|H*kgLhnws+LZ>!JW4=xDfzOaKifl literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/attested_across_drop/case.json b/wai/staged-conformance/cases/attested_across_drop/case.json new file mode 100644 index 000000000..4f3d85abd --- /dev/null +++ b/wai/staged-conformance/cases/attested_across_drop/case.json @@ -0,0 +1,234 @@ +{ + "format": "wai-staged-case/1", + "name": "attested_across_drop", + "about": "an attested base's layer 1 applies with its receipt; the chain is dropped and the base decoded again by a decode that carries no receipt: it is still attested, so layer 1 with no receipt is refinement-unattested, and with its receipt it replays", + "bases": [ + { + "file": "base_0.wai", + "object": "6fd89fffe05e4ef6defa6726fd1bcac8a578e85210a15cdae65b4654d5fe7e6b", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": true, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0_0.bin", + "stage0_sha256": "84a4afb831e2d2c98b261aaa08b1f7c1e49a7df9c3a1f7327bf87ec140cda4ae", + "decode": { + "kind": "int", + "set": {} + } + }, + { + "file": "base_1.wai", + "object": "6fd89fffe05e4ef6defa6726fd1bcac8a578e85210a15cdae65b4654d5fe7e6b", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0_1.bin", + "stage0_sha256": "84a4afb831e2d2c98b261aaa08b1f7c1e49a7df9c3a1f7327bf87ec140cda4ae", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "0ccfe9af9e31cf9014699a20610168f81692920e29adebd6ea6d277da3e8ca3e", + "bytes": 583 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "delivered-verified" + }, + { + "op": "remove", + "index": 0, + "at_ms": 2 + }, + { + "op": "base", + "index": 1, + "at_ms": 3 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 4, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 5, + "evidence": "delivered-verified" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "0ccfe9af9e31cf9014699a20610168f81692920e29adebd6ea6d277da3e8ca3e", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ], + [], + [], + [ + { + "object": "0ccfe9af9e31cf9014699a20610168f81692920e29adebd6ea6d277da3e8ca3e", + "envelope": "refinement-unattested", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "0ccfe9af9e31cf9014699a20610168f81692920e29adebd6ea6d277da3e8ca3e", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "0ccfe9af9e31cf9014699a20610168f81692920e29adebd6ea6d277da3e8ca3e", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "20c254f70a8d4de095e3064f92bc01bd93cbd7fbb2a3266bcec7e9a8902b1bc2" + }, + { + "stage": 1, + "producer": "0ccfe9af9e31cf9014699a20610168f81692920e29adebd6ea6d277da3e8ca3e", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "2724b6f993f4946ecfaa99aa18dffcb0ba2125592be2bb146a8b754fe74fefab" + } + ], + "clip_sha256": "7990406f958ddaefc5e91f8e0cd9ca1d265cd3c81a681eec4bb7dbb9ba04c92e", + "n_units": 2 + }, + { + "units": [ + { + "stage": 1, + "producer": "0ccfe9af9e31cf9014699a20610168f81692920e29adebd6ea6d277da3e8ca3e", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "20c254f70a8d4de095e3064f92bc01bd93cbd7fbb2a3266bcec7e9a8902b1bc2" + }, + { + "stage": 1, + "producer": "0ccfe9af9e31cf9014699a20610168f81692920e29adebd6ea6d277da3e8ca3e", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "2724b6f993f4946ecfaa99aa18dffcb0ba2125592be2bb146a8b754fe74fefab" + } + ], + "clip_sha256": "7990406f958ddaefc5e91f8e0cd9ca1d265cd3c81a681eec4bb7dbb9ba04c92e", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/attested_across_drop/stage0_0.bin b/wai/staged-conformance/cases/attested_across_drop/stage0_0.bin new file mode 100644 index 0000000000000000000000000000000000000000..a931ec801ee4692b09f43788f1b5865958c3684e GIT binary patch literal 384 zcmZ9|A#=na5QgFV36&>Lo{U6BB9W0uWaLU@Tyez}iNtj!5*dk%L?Yw1o1}9yy_rwF z>Y4BM@w`8HVi4mX)hMS?n@P|2$2bV_6iP@Xm6jUxY|U(kqiBa!PF%o%1@N$hQ5uKM zmmb${z<~?admm%SEtNf0!ba;(J|>qi#;jqrQ+I3 z?JlH{FknHfrPg(8we_vj?eafa`{$Q$e#L(C=9iOJm*1X#d-&xvD+6W**1r6Fg)!E^ o=jCS@`@`vnR@qW}KPevW0^R`xgg1-9GV3;+NC literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/attested_across_drop/stage0_1.bin b/wai/staged-conformance/cases/attested_across_drop/stage0_1.bin new file mode 100644 index 0000000000000000000000000000000000000000..a931ec801ee4692b09f43788f1b5865958c3684e GIT binary patch literal 384 zcmZ9|A#=na5QgFV36&>Lo{U6BB9W0uWaLU@Tyez}iNtj!5*dk%L?Yw1o1}9yy_rwF z>Y4BM@w`8HVi4mX)hMS?n@P|2$2bV_6iP@Xm6jUxY|U(kqiBa!PF%o%1@N$hQ5uKM zmmb${z<~?admm%SEtNf0!ba;(J|>qi#;jqrQ+I3 z?JlH{FknHfrPg(8we_vj?eafa`{$Q$e#L(C=9iOJm*1X#d-&xvD+6W**1r6Fg)!E^ o=jCS@`@`vnR@qW}KPevW0^R`xgg1-9GV3;+NC literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/attested_before_decode/L1.wai b/wai/staged-conformance/cases/attested_before_decode/L1.wai new file mode 100644 index 0000000000000000000000000000000000000000..38e234510cf61e7877655d9d571665bf1166bd49 GIT binary patch literal 587 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_Nn%lYDllB~N^^2*|1dBxgnI@VG6I8>3CQMvVrC!>11vzs zpa1{=b3+BffRby(7#PB=85mY4Ffja|48&U*7*<|mU#WMl{cl0HCO%ya)H z(0B%pTi2eH#mIdAel=i8cgC*+RhJ?omYTeDjd^o6Ju6b@=fSDxRNjbP>%N-3!K7_d zvHA6+zxs2Q+>g2Nl;OX}QI6wXe{G&|8D2`M-XV5~j%I`(9qtO~d@o^Pj68^2UapoiW=(WYf}1xj6*|-)Aka IF}INe0My{bi2wiq literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/attested_before_decode/base.wai b/wai/staged-conformance/cases/attested_before_decode/base.wai new file mode 100644 index 0000000000000000000000000000000000000000..9e990fd8f8055d66fcc20238ec3853cfe47b935d GIT binary patch literal 555 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1M7#SGCJ;Mwcfix4)xg2Qt zG z3=Esh7#L!0fwT()L-0%nhQEt}^l}Dv5&&H%9$n}wGjD+5D2Ps4=&;#!XqbTn#aTe3f|WBWg>>I+(TVprc8 z+uTo2LKoV9D@ZS!zVZ%xNgaP&r$yFh&II|M8Deq~Tn$Thy*;VGBs0UyN?Pe(MyulG iDS_p$TAqJ@=W|rdY!P=duiySuj+IU Vec { bases: vec![a, att], files, cfg: cfg(), steps: vec![base_at(0, 0), offer("L1_f0.wai", 1), base_at(1, 2), offer("L1_f1.wai", 3), base_at(0, 4), offer("L1_f1.wai", 5), dv(6)] }); } + { + let mut a = small(166); + a.attested = true; + let mut plain = a.clone(); + plain.attested = false; + let (l1, _) = l1_of(&a, 167, 4); + let dv = |at| Step::Offer { file: "L1.wai".into(), at_ms: at, evidence: ReceiptEvidence::DeliveredVerified }; + out.push(Case { name: "attested_across_drop", about: "an attested base's layer 1 applies with its receipt; the chain is dropped and the base decoded again by a decode that carries no receipt: it is still attested, so layer 1 with no receipt is refinement-unattested, and with its receipt it replays", + bases: vec![a, plain], files: vec![("L1.wai".into(), l1)], cfg: cfg(), + steps: vec![base_at(0, 0), dv(1), Step::Remove { index: 0, at_ms: 2 }, base_at(1, 3), offer("L1.wai", 4), dv(5)] }); + } + { + let a = small(168); + let (l1, _) = l1_of(&a, 169, 4); + let dv = |at| Step::Offer { file: "L1.wai".into(), at_ms: at, evidence: ReceiptEvidence::DeliveredVerified }; + out.push(Case { name: "attested_before_decode", about: "the sink verifies the base's receipt before it decodes the base: the decode, which carries no receipt, gives an attested base, so layer 1 with no receipt is refinement-unattested and applies with its receipt", + bases: vec![a], files: vec![("L1.wai".into(), l1)], cfg: cfg(), + steps: vec![Step::Attest { index: 0, at_ms: 0 }, base_at(0, 1), offer("L1.wai", 2), dv(3)] }); + } { let mut a = small(144); a.attested = true; @@ -1403,6 +1424,10 @@ fn run(r: &Replay) -> (Vec, Vec) { } Step::Expire { at_ms } => sink.expire(*at_ms), Step::Remove { index, .. } => sink.remove_base(&r.bases[*index].0.content_hash).unwrap_or_default(), + Step::Attest { index, .. } => { + sink.mark_attested(&r.bases[*index].0.content_hash); + vec![] + } }; reports.push(Value::Array(got.iter().map(report_json).collect())); } @@ -1434,6 +1459,7 @@ fn step_json(s: &Step) -> Value { Step::Base { index, at_ms } => json!({"op": "base", "index": index, "at_ms": at_ms}), Step::Expire { at_ms } => json!({"op": "expire", "at_ms": at_ms}), Step::Remove { index, at_ms } => json!({"op": "remove", "index": index, "at_ms": at_ms}), + Step::Attest { index, at_ms } => json!({"op": "attest", "index": index, "at_ms": at_ms}), } } @@ -1466,6 +1492,7 @@ fn step_from(v: &Value) -> Step { "base" => Step::Base { index: v["index"].as_u64().expect("index") as usize, at_ms: at }, "expire" => Step::Expire { at_ms: at }, "remove" => Step::Remove { index: v["index"].as_u64().expect("index") as usize, at_ms: at }, + "attest" => Step::Attest { index: v["index"].as_u64().expect("index") as usize, at_ms: at }, o => panic!("unknown op {o}"), } } diff --git a/wai/wai-rs/src/staged/mod.rs b/wai/wai-rs/src/staged/mod.rs index ec122f502..70f525eee 100644 --- a/wai/wai-rs/src/staged/mod.rs +++ b/wai/wai-rs/src/staged/mod.rs @@ -1072,13 +1072,17 @@ pub struct StageSink { pending: Vec, /// The unit records of bases whose chains were dropped (§4.4). records: BTreeMap<[u8; 32], Vec>, + /// Bases the sink does not hold whose delivery receipt it verified (§10): dropped + /// while attested, or marked before they were decoded. A held base's attestation is + /// its chain's. + attested: std::collections::BTreeSet<[u8; 32]>, } impl StageSink { /// A sink holding no base, with the bound, cost limit, budget and schedule every /// chain it creates uses. pub fn new(cfg: ChainConfig) -> Self { - StageSink { cfg, chains: BTreeMap::new(), pending: vec![], records: BTreeMap::new() } + StageSink { cfg, chains: BTreeMap::new(), pending: vec![], records: BTreeMap::new(), attested: Default::default() } } /// The chain of the base whose content hash is `base`. @@ -1090,7 +1094,9 @@ impl StageSink { /// base the sink already holds is kept as it is: decoding it again does not reset /// its units, and reports nothing; if this decode comes with the base's delivery /// receipt verified, the base is attested from now on (§10), and never the reverse. - pub fn add_base(&mut self, base: BaseInfo, units: Vec) -> Vec { + /// A base the sink attested before, held, dropped or not yet decoded, is attested + /// whatever `base.attested` says. + pub fn add_base(&mut self, mut base: BaseInfo, units: Vec) -> Vec { let hash = base.content_hash; if let Some(c) = self.chains.get_mut(&hash) { if base.attested { @@ -1098,6 +1104,7 @@ impl StageSink { } return vec![]; } + base.attested |= self.attested.remove(&hash); let records = self.records.remove(&hash).unwrap_or_default(); let mut chain = StageChain::with_records(base, units, self.cfg.clone(), records); let hexed = hex(&hash); @@ -1108,11 +1115,17 @@ impl StageSink { reports } - /// Record that the sink verified a delivery receipt for the base `base` it holds - /// (§10): step 5 applies to every envelope that arrives after it. Whether it holds - /// the base. - pub fn mark_attested(&mut self, base: &[u8; 32]) -> bool { - self.chains.get_mut(base).map(StageChain::mark_attested).is_some() + /// Record that the sink verified a delivery receipt for the base `base` (§10), + /// whether it holds the base, dropped its chain or has not decoded it yet: step 5 + /// applies to every envelope for it that arrives after, and the base stays attested + /// across drops and decodes until the sink forgets it. + pub fn mark_attested(&mut self, base: &[u8; 32]) { + match self.chains.get_mut(base) { + Some(c) => c.mark_attested(), + None => { + self.attested.insert(*base); + } + } } /// Drop the chain of a base the sink no longer presents (§4.4): every envelope held @@ -1124,13 +1137,18 @@ impl StageSink { let chain = self.chains.remove(base)?; let reports = chain.layers.values().filter(|l| l.held > 0).map(|l| OfferReport::refuse(l.hash, StageRefusal::BaseAbsent, None)).collect(); self.records.insert(*base, chain.records()); + if chain.base.attested { + self.attested.insert(*base); + } Some(reports) } - /// Discard the record of a base whose chain was dropped (§14): decoded again, the - /// base starts with no history, as one never decoded. Whether there was a record. + /// Discard what the sink keeps of a base it does not hold (§14): its record and its + /// attestation. Decoded again, the base starts with no history and unattested, as + /// one never decoded. Whether there was anything to discard. pub fn forget_base(&mut self, base: &[u8; 32]) -> bool { - self.records.remove(base).is_some() + let r = self.records.remove(base).is_some(); + self.attested.remove(base) || r } /// Offer the envelope `bytes` at `now_ms` with the receipt `evidence` (§4.1). A diff --git a/wai/wai-rs/src/staged/tests.rs b/wai/wai-rs/src/staged/tests.rs index f6f6615fe..d9db2650b 100644 --- a/wai/wai-rs/src/staged/tests.rs +++ b/wai/wai-rs/src/staged/tests.rs @@ -987,9 +987,25 @@ fn late_attestation_applies_step_5() { // Marked, without decoding again. let mut sink = StageSink::new(cfg()); sink.add_base(info.clone(), units.clone()); - assert!(sink.mark_attested(&b)); - assert!(!sink.mark_attested(&[9; 32]), "a base the sink does not hold"); + sink.mark_attested(&b); assert_eq!(sink.offer(&l2, 0, ReceiptEvidence::InclusionVerified).unwrap()[0].envelope, Some(StageRefusal::RefinementUnattested)); + // Attestation belongs to the base hash: it survives a dropped chain and a decode + // that does not carry it, and a mark before the first decode holds. + let mut sink = StageSink::new(cfg()); + sink.add_base(BaseInfo { attested: true, ..info.clone() }, units.clone()); + sink.remove_base(&b); + sink.add_base(info.clone(), units.clone()); + assert!(sink.chain(&b).unwrap().base().attested, "dropped and decoded again"); + assert_eq!(sink.offer(&l0, 0, ReceiptEvidence::None).unwrap()[0].envelope, Some(StageRefusal::RefinementUnattested)); + let mut sink = StageSink::new(cfg()); + sink.mark_attested(&b); + sink.add_base(info.clone(), units.clone()); + assert!(sink.chain(&b).unwrap().base().attested, "marked before it was decoded"); + // Forgotten: as never decoded, so unattested. + sink.remove_base(&b); + assert!(sink.forget_base(&b)); + sink.add_base(info, units); + assert!(!sink.chain(&b).unwrap().base().attested); } /// A unit or a stage below whose stated shape's element count passes u64 is refused -- 2.54.0 (Apple Git-157) From 8d89785c342b5016ec7cee9a4ba17b33202392a7 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 4 Oct 2026 23:51:16 -0400 Subject: [PATCH 34/36] wai: a frozen unit of a base decoded again replays its record in order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After a dropped chain and a re-decode, a frozen unit sat at stage 0 and presented its base, while §14 said a frozen unit stays at its stage. A frozen unit may now replay its own recorded producers, in order, up to its recorded stage and never past it; every other layer is refused (layer-conflict) and the unit stays frozen. It presents its base or a recorded producer's output, never any other. §4.1 step 7, §4.4 and §14 say so; staged_verify.py does the same. remove_readd_frozen now replays layer 1a and refuses a layer 2 before and after; removed_base_keeps_its_record checks order, the frozen stage presented again, and nothing past the record. Co-Authored-By: Claude Opus 5.5 --- wai/extensions/staged-delivery.md | 21 ++++-- .../cases/remove_readd_diverge/case.json | 32 ++++---- .../cases/remove_readd_frozen/L2.wai | Bin 0 -> 791 bytes .../cases/remove_readd_frozen/case.json | 69 +++++++++++++++--- wai/staged-conformance/index.json | 8 +- wai/staged-conformance/staged_verify.py | 8 +- wai/wai-rs/src/bin/wai_staged_vectors.rs | 11 +-- wai/wai-rs/src/staged/mod.rs | 12 ++- wai/wai-rs/src/staged/tests.rs | 19 ++++- 9 files changed, 128 insertions(+), 52 deletions(-) create mode 100644 wai/staged-conformance/cases/remove_readd_frozen/L2.wai diff --git a/wai/extensions/staged-delivery.md b/wai/extensions/staged-delivery.md index 7fa74c2d2..d5275490b 100644 --- a/wai/extensions/staged-delivery.md +++ b/wai/extensions/staged-delivery.md @@ -110,9 +110,10 @@ fails is the reason for the envelope (1–6) or for the unit (7–10). `layer`, whose producer for *u* is also *P(u)*, has passed step 8 for *u*, earlier (before the base's chain was dropped included, §4.4) or in the same batch (§4.4) (`layer-conflict`). A conflicted unit is **frozen**: - the sink **MUST** apply no further layer to it, it keeps the output it had (a layer - applied to it before the conflict stays applied), and the sink **MUST** report the - conflict. + the sink **MUST** apply no further layer to it, except the replay of its own record + after its base's chain is dropped and the base decoded again (§4.4); it keeps the + output it had (a layer applied to it before the conflict stays applied), and the + sink **MUST** report the conflict. 8. *u* is **ready** when its stage is `layer − 1` and its producer is *P(u)*. A unit whose stage is below `layer − 1` keeps *R* held for it, and is evaluated again when its stage advances; at the bound *R* is discarded for that unit (`layer-gap`). Any @@ -197,8 +198,11 @@ applied, and whether it is frozen; 32 bytes for each recorded stage, at most one further 32-byte producer, and a flag. When it decodes the base again, each unit starts at stage 0 with its record. Step 7 compares a layer with the recorded producers, so a unit advances again only along them: a layer whose key a recorded -stage holds with other content conflicts, and the unit freezes; a frozen unit stays -frozen, at stage 0. +stage holds with other content conflicts, and the unit freezes. A frozen unit stays +frozen: it may replay its own recorded producers, in order, up to its recorded stage +and never past it, and every other layer is refused for it (`layer-conflict`). So it +presents its base's output or the output of one of its recorded producers, never any +other. ### 4.5 Chain budget @@ -399,9 +403,10 @@ ignores the header. (the reference `StageSink::forget_base`) treats the base as never decoded, so a layer that conflicted with the forgotten history can apply; it forgets only a base it will not decode again. -- Where the base is attested (§10), a frozen unit stays at its stage: a party that can - sign conflicting layers can stop a unit's refinement, and cannot change what it - presents. Over a base whose receipt the sink did not verify, anyone who learns the +- Where the base is attested (§10), a frozen unit presents only its base's output or + the output of one of the producers it recorded, replayed in order (§4.4): a party + that can sign conflicting layers can stop a unit's refinement, and cannot make it + present anything else. Over a base whose receipt the sink did not verify, anyone who learns the base's hash can mint a layer, and the first one ready for a unit applies to it. ## Appendix A. `WIR1` diff --git a/wai/staged-conformance/cases/remove_readd_diverge/case.json b/wai/staged-conformance/cases/remove_readd_diverge/case.json index 716481470..4cd0b11e7 100644 --- a/wai/staged-conformance/cases/remove_readd_diverge/case.json +++ b/wai/staged-conformance/cases/remove_readd_diverge/case.json @@ -1,7 +1,7 @@ { "format": "wai-staged-case/1", "name": "remove_readd_diverge", - "about": "layer 1a applies; the chain is dropped, a different layer 1b arrives and is held for the base, and the base is decoded again: 1b differs from the producer each unit's record holds at stage 1, so it conflicts and freezes the units", + "about": "layer 1a applies; the chain is dropped, a different layer 1b arrives and is held for the base, and the base is decoded again: 1b differs from the producer each unit's record holds at stage 1, so it conflicts and freezes the units; 1a, the recorded producer, still replays", "bases": [ { "file": "base.wai", @@ -149,18 +149,12 @@ "envelope": null, "detail": null, "held_for_base": false, - "applied": [], + "applied": [ + 0, + 1 + ], "held": [], - "refused": [ - [ - 0, - "layer-conflict" - ], - [ - 1, - "layer-conflict" - ] - ] + "refused": [] } ] ], @@ -168,8 +162,8 @@ { "units": [ { - "stage": 0, - "producer": "28280c24e00d831db61d63e95fbb8133a76b94942083496685078f4d3f8ec536", + "stage": 1, + "producer": "4f5ffeb91bb801a82c79d43268eee3ae2ca54bcbcf1f69b5e2e844866aa060da", "frozen": true, "tier": "decode-equivalence", "shape": [ @@ -177,11 +171,11 @@ 8, 3 ], - "stage_sha256": "19c8c863d0060573c229eff9d8354f9735e34f6c3db94a79b05c4ac0d80712b2" + "stage_sha256": "65683884222d31f89d1b13d5cd178158071354f1477d537c6389fe93d4343cc0" }, { - "stage": 0, - "producer": "28280c24e00d831db61d63e95fbb8133a76b94942083496685078f4d3f8ec536", + "stage": 1, + "producer": "4f5ffeb91bb801a82c79d43268eee3ae2ca54bcbcf1f69b5e2e844866aa060da", "frozen": true, "tier": "decode-equivalence", "shape": [ @@ -189,10 +183,10 @@ 8, 3 ], - "stage_sha256": "e89e20b9db5d6420126b3d74c19183cd115d3632ad6fbc24d542a280b0a59584" + "stage_sha256": "ef184ab186741db82845c9d1f50d6aa4756603bd548dfe106e9892ad19cde9dc" } ], - "clip_sha256": "3a0b5f2bc2fcbec02c714c18a06576c9bdc49334f5a76e0e3d4b28c12658774c", + "clip_sha256": "0ffaf3f861041a87d543be9f70f90d805aacfe0a072f74deb7c16ae8415f96e0", "n_units": 2 } ] diff --git a/wai/staged-conformance/cases/remove_readd_frozen/L2.wai b/wai/staged-conformance/cases/remove_readd_frozen/L2.wai new file mode 100644 index 0000000000000000000000000000000000000000..6004ed9dbac0062c0f8359f54d17920649599537 GIT binary patch literal 791 zcmXxgTS!zv7zglgGtA8{t4U<7rh_-k)^qk;c8^6Vg(e6_FIFINF0(edp4D|#Y*90% z^d%yDD9|_UrHF!x0*MG8v@E(j6cGfy8yS)W(e_`n1Hbbz^L_l8nU=an`G|)o$@)ye zYM305SQYEwjxe$66%LPKE!+fm6WECJ&Zw}>1Wyme93DLr=UqJ_&O6rnNoJd!rX`{x z(T{0F2G$}12*6}SxSS1CF+1iuA|Ya~aNAf-lC=xhNso)ToNoUc>`r$qvzwz64k?kK zWh*vUwUBK)s;-1(DI^7zpdwj{smmeNl4XbMN=WCSh-1h$msCSFbjdP!2&e6g^Ijy$ zLC0})BgDCF*jhNEXqxWmx*iUzhNgxiGPexdloZWzY(M zJkDx@IBCL+w{g6S+Y^lr%;Onb8c)a`)D1+GwJrno|CipMOQlk^>l=4aPh$QQoQ6yc zY=SKC!b5lj7DBdd1cYwiOyq+c$c3}nL;}ujC2HSJ6on4R_dy=f^FpkPiCmatM6pt$ zJ!M3GD5xNshr&vt1xV~8+FM2R0$#!*umI63cn$s4L?seYDI{g0vLMl0cn1Rt(I5=L z1sH~lFao1cFo^TsBTA*xm-o;y{F`8*#~aQpe(alm|1GPcx%~6d)q<|>%+v?so5P9zTxkeihjC3*!=y;1IzVeEc3?rOv$gjqPo7@D;{q~to3?M>uPx|?pryf zEcut3r>~8k&u(y@@;57D@yX>Yx%pehe-HVa8mzJUse^g!&qvm@J9qyV@A6&$%&pR! SJ7=pW&GxJ1xpl$ABYy!g@5_1s literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/remove_readd_frozen/case.json b/wai/staged-conformance/cases/remove_readd_frozen/case.json index 0fa0514a0..243eb835e 100644 --- a/wai/staged-conformance/cases/remove_readd_frozen/case.json +++ b/wai/staged-conformance/cases/remove_readd_frozen/case.json @@ -1,7 +1,7 @@ { "format": "wai-staged-case/1", "name": "remove_readd_frozen", - "about": "layer 1a applies and 1b conflicts, freezing the units; the chain is dropped, 1b arrives and is held for the base, and the base is decoded again: the record keeps the units frozen at stage 0, so 1b is refused for them, and 1a too", + "about": "layer 1a applies and 1b conflicts, freezing the units at stage 1; the chain is dropped, 1b arrives and is held for the base, and the base is decoded again: the units start frozen at stage 0 and may only replay their record, in order and no further, so 1b and a layer 2 are refused, and 1a replays and presents the frozen stage again; then layer 2 is still refused", "bases": [ { "file": "base.wai", @@ -48,6 +48,11 @@ "file": "L1b.wai", "object": "882c09668e4f8f0189c3c5ae75ef795fb43fc4d8589c3ec7ed752f0c76eac663", "bytes": 587 + }, + { + "file": "L2.wai", + "object": "3abecc787ecd987c604975c1dae9d2b035905974e845d1d630120141b19124c1", + "bytes": 791 } ], "config": { @@ -95,9 +100,21 @@ }, { "op": "offer", - "file": "L1a.wai", + "file": "L2.wai", "at_ms": 6, "evidence": "none" + }, + { + "op": "offer", + "file": "L1a.wai", + "at_ms": 7, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 8, + "evidence": "none" } ], "expect": { @@ -169,12 +186,46 @@ ] } ], + [ + { + "object": "3abecc787ecd987c604975c1dae9d2b035905974e845d1d630120141b19124c1", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + } + ], [ { "object": "12ddd697eec9c58f3556d6668849548f1eb9ca035ddc31f44c0699599a903bbb", "envelope": null, "detail": null, "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "3abecc787ecd987c604975c1dae9d2b035905974e845d1d630120141b19124c1", + "envelope": null, + "detail": null, + "held_for_base": false, "applied": [], "held": [], "refused": [ @@ -194,8 +245,8 @@ { "units": [ { - "stage": 0, - "producer": "873f2bc3ce457ccd4638107023230b3a6174b11de6376e7fd91ce0494960b9e7", + "stage": 1, + "producer": "12ddd697eec9c58f3556d6668849548f1eb9ca035ddc31f44c0699599a903bbb", "frozen": true, "tier": "decode-equivalence", "shape": [ @@ -203,11 +254,11 @@ 8, 3 ], - "stage_sha256": "910258aabeaafe97b66d8766421ddb94e2c65cd0e47504087843e7da41e813c9" + "stage_sha256": "91348dfeff7a5954435db4ced092bd7bb2a95b641b4eb91baa264d83db6547d4" }, { - "stage": 0, - "producer": "873f2bc3ce457ccd4638107023230b3a6174b11de6376e7fd91ce0494960b9e7", + "stage": 1, + "producer": "12ddd697eec9c58f3556d6668849548f1eb9ca035ddc31f44c0699599a903bbb", "frozen": true, "tier": "decode-equivalence", "shape": [ @@ -215,10 +266,10 @@ 8, 3 ], - "stage_sha256": "750076ca81eeb45d0e1c7b37749dc443d3d3bf7d1d975e160e4fc7035ef0d581" + "stage_sha256": "57d6b4afb7d33af0f3866b6b1525619ade8b944ba7873bd94d517c5a83b93acc" } ], - "clip_sha256": "1b0dbc764ee287d74f78a77bf79ea2267133643301bb842882aaa62adcbd1776", + "clip_sha256": "30836e3ff57924a86260a38c7031f85283cb6a93d581b535c790ad3b6797e75f", "n_units": 2 } ] diff --git a/wai/staged-conformance/index.json b/wai/staged-conformance/index.json index 96beba16e..c143c5fda 100644 --- a/wai/staged-conformance/index.json +++ b/wai/staged-conformance/index.json @@ -406,9 +406,9 @@ }, { "name": "remove_readd_frozen", - "about": "layer 1a applies and 1b conflicts, freezing the units; the chain is dropped, 1b arrives and is held for the base, and the base is decoded again: the record keeps the units frozen at stage 0, so 1b is refused for them, and 1a too", + "about": "layer 1a applies and 1b conflicts, freezing the units at stage 1; the chain is dropped, 1b arrives and is held for the base, and the base is decoded again: the units start frozen at stage 0 and may only replay their record, in order and no further, so 1b and a layer 2 are refused, and 1a replays and presents the frozen stage again; then layer 2 is still refused", "clip_sha256": [ - "1b0dbc764ee287d74f78a77bf79ea2267133643301bb842882aaa62adcbd1776" + "30836e3ff57924a86260a38c7031f85283cb6a93d581b535c790ad3b6797e75f" ] }, { @@ -420,9 +420,9 @@ }, { "name": "remove_readd_diverge", - "about": "layer 1a applies; the chain is dropped, a different layer 1b arrives and is held for the base, and the base is decoded again: 1b differs from the producer each unit's record holds at stage 1, so it conflicts and freezes the units", + "about": "layer 1a applies; the chain is dropped, a different layer 1b arrives and is held for the base, and the base is decoded again: 1b differs from the producer each unit's record holds at stage 1, so it conflicts and freezes the units; 1a, the recorded producer, still replays", "clip_sha256": [ - "3a0b5f2bc2fcbec02c714c18a06576c9bdc49334f5a76e0e3d4b28c12658774c" + "0ffaf3f861041a87d543be9f70f90d805aacfe0a072f74deb7c16ae8415f96e0" ] }, { diff --git a/wai/staged-conformance/staged_verify.py b/wai/staged-conformance/staged_verify.py index 6a715c15b..284cd710f 100644 --- a/wai/staged-conformance/staged_verify.py +++ b/wai/staged-conformance/staged_verify.py @@ -672,8 +672,12 @@ class Base: def conflicted(self, e, u): """Step 7 against what passed step 8 before.""" - if self.units[u]["frozen"]: - return True + s = self.units[u] + if s["frozen"]: + # §4.4: a frozen unit replays its own record, in order, never past it. + k, p = e.ref["layer"], e.p(u, self.n()) + replay = k == s["stage"] + 1 and k <= s["reached"] and p == s["producer"] and self.passed.get((u, k, p)) == e.h + return not replay other = self.passed.get((u, e.ref["layer"], e.p(u, self.n()))) return other is not None and other != e.h diff --git a/wai/wai-rs/src/bin/wai_staged_vectors.rs b/wai/wai-rs/src/bin/wai_staged_vectors.rs index dfb14c2c4..169c16057 100644 --- a/wai/wai-rs/src/bin/wai_staged_vectors.rs +++ b/wai/wai-rs/src/bin/wai_staged_vectors.rs @@ -1264,11 +1264,12 @@ fn cases(ctx: &Ctx) -> Vec { } { let a = small(157); - let (la, _) = l1_of(&a, 158, 4); + let (la, mid) = l1_of(&a, 158, 4); let (lb, _) = l1_of(&a, 159, 4); - out.push(simple("remove_readd_frozen", "layer 1a applies and 1b conflicts, freezing the units; the chain is dropped, 1b arrives and is held for the base, and the base is decoded again: the record keeps the units frozen at stage 0, so 1b is refused for them, and 1a too", a, - vec![("L1a.wai".into(), la), ("L1b.wai".into(), lb)], - vec![base_at(0, 0), offer("L1a.wai", 1), offer("L1b.wai", 2), Step::Remove { index: 0, at_ms: 3 }, offer("L1b.wai", 4), base_at(0, 5), offer("L1a.wai", 6)])); + let l2 = l2_of(&a, &la, &mid, 158); + out.push(simple("remove_readd_frozen", "layer 1a applies and 1b conflicts, freezing the units at stage 1; the chain is dropped, 1b arrives and is held for the base, and the base is decoded again: the units start frozen at stage 0 and may only replay their record, in order and no further, so 1b and a layer 2 are refused, and 1a replays and presents the frozen stage again; then layer 2 is still refused", a, + vec![("L1a.wai".into(), la), ("L1b.wai".into(), lb), ("L2.wai".into(), l2)], + vec![base_at(0, 0), offer("L1a.wai", 1), offer("L1b.wai", 2), Step::Remove { index: 0, at_ms: 3 }, offer("L1b.wai", 4), base_at(0, 5), offer("L2.wai", 6), offer("L1a.wai", 7), offer("L2.wai", 8)])); } { let a = small(163); @@ -1282,7 +1283,7 @@ fn cases(ctx: &Ctx) -> Vec { let a = small(160); let (la, _) = l1_of(&a, 161, 4); let (lb, _) = l1_of(&a, 162, 4); - out.push(simple("remove_readd_diverge", "layer 1a applies; the chain is dropped, a different layer 1b arrives and is held for the base, and the base is decoded again: 1b differs from the producer each unit's record holds at stage 1, so it conflicts and freezes the units", a, + out.push(simple("remove_readd_diverge", "layer 1a applies; the chain is dropped, a different layer 1b arrives and is held for the base, and the base is decoded again: 1b differs from the producer each unit's record holds at stage 1, so it conflicts and freezes the units; 1a, the recorded producer, still replays", a, vec![("L1a.wai".into(), la), ("L1b.wai".into(), lb)], vec![base_at(0, 0), offer("L1a.wai", 1), Step::Remove { index: 0, at_ms: 2 }, offer("L1b.wai", 3), base_at(0, 4), offer("L1a.wai", 5)])); } diff --git a/wai/wai-rs/src/staged/mod.rs b/wai/wai-rs/src/staged/mod.rs index 70f525eee..1df433766 100644 --- a/wai/wai-rs/src/staged/mod.rs +++ b/wai/wai-rs/src/staged/mod.rs @@ -626,13 +626,17 @@ impl StageChain { /// not applied), and the producer it named is that of stage `k − 1`. fn conflicted_before(&self, id: u64, u: u32) -> bool { let s = &self.units[u as usize]; - if s.frozen { - return true; - } let l = &self.layers[&id]; - let Some(p) = self.p_of(id, u) else { return false }; + let p = self.p_of(id, u); let (k, r) = (l.layer as usize, s.producers.len()); let producer_of = |stage: usize| if stage == 0 { self.base.content_hash } else { s.producers[stage - 1] }; + if s.frozen { + // A frozen unit replays its own record, in order, and nothing else (§4.4): + // the recorded producer of the stage above its current one, never past it. + let replay = k == s.stage as usize + 1 && k <= r && s.producers[k - 1] == l.hash && p == Some(producer_of(k - 1)); + return !replay; + } + let Some(p) = p else { return false }; if k <= r { p == producer_of(k - 1) && s.producers[k - 1] != l.hash } else if k == r + 1 { diff --git a/wai/wai-rs/src/staged/tests.rs b/wai/wai-rs/src/staged/tests.rs index d9db2650b..1b87abd20 100644 --- a/wai/wai-rs/src/staged/tests.rs +++ b/wai/wai-rs/src/staged/tests.rs @@ -955,8 +955,25 @@ fn removed_base_keeps_its_record() { let u1 = sink.chain(&b).unwrap().unit(1).unwrap().clone(); assert!(u1.frozen && u1.producers().is_empty()); sink.remove_base(&b); - sink.add_base(info, units); + sink.add_base(info.clone(), units.clone()); assert_eq!(codes(&sink.offer(&la, 2, ReceiptEvidence::None).unwrap()[0]), vec![(1, "layer-conflict")]); + // A frozen unit with a record replays it, in order and no further, and stays frozen: + // it presents its base or a recorded producer's output, nothing else. + let mut sink = StageSink::new(cfg()); + sink.add_base(info.clone(), units.clone()); + sink.offer(&l1, 0, ReceiptEvidence::None).unwrap(); + let frozen_by = layer(&b, one(&b), 1, None, &below, 52); + assert_eq!(codes(&sink.offer(&frozen_by, 1, ReceiptEvidence::None).unwrap()[0]).len(), 4); + let at_stage_1 = sink.chain(&b).unwrap().units().to_vec(); + sink.remove_base(&b); + sink.add_base(info.clone(), units.clone()); + assert!(sink.chain(&b).unwrap().units().iter().all(|u| u.frozen && u.stage == 0)); + assert_eq!(codes(&sink.offer(&l2, 2, ReceiptEvidence::None).unwrap()[0]).len(), 4, "not in order"); + assert_eq!(sink.offer(&l1, 3, ReceiptEvidence::None).unwrap()[0].applied, vec![0, 1, 2, 3], "its recorded producer"); + assert_eq!(sink.chain(&b).unwrap().units(), &at_stage_1[..], "the frozen stage, presented again"); + assert_eq!(codes(&sink.offer(&l2, 4, ReceiptEvidence::None).unwrap()[0]), vec![(0, "layer-conflict"), (1, "layer-conflict"), (2, "layer-conflict"), (3, "layer-conflict")], "never past the record"); + assert_eq!(codes(&sink.offer(&frozen_by, 5, ReceiptEvidence::None).unwrap()[0]).len(), 4); + assert_eq!(codes(&sink.offer(&l1, 6, ReceiptEvidence::None).unwrap()[0]).len(), 4, "a frozen unit takes no layer it already holds"); } fn edit_refines(env: &[u8], r: Refines) -> Vec { -- 2.54.0 (Apple Git-157) From 83e7fe6b2088c79ddb46933f55e1cd9866bd142f Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sun, 4 Oct 2026 23:57:00 -0400 Subject: [PATCH 35/36] wai: a sink keeps only non-empty records, within a declared record budget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit remove_base kept a record for every unit of every dropped base, outside any budget: twenty dropped bases of 4096 units with no layer held 5 MB. - A unit record that holds nothing (no stage, nothing passed step 8, not frozen) is not kept, nor a base all of whose records are empty and that is not attested. - ChainConfig::record_budget bounds what the sink keeps of the bases it does not hold, records and attestations together, counted as §4.4 states (32 bytes a base; 8 a kept record, 32 a recorded stage, 32 an envelope that passed step 8 unapplied). Past it, the bases written longest ago are forgotten whole, with forget_base's meaning: as never decoded and never attested. StageSink::kept_bytes reports the count. - §4.4 and §14 state the budget and the eviction; staged_verify.py keeps the same records and evicts the same bases; every case states its record budget, and the case record_budget evicts one base and keeps another. - records_within_their_budget and tests/staged_resources.rs check that bases with nothing recorded keep nothing, that distinct dropped bases stay within the budget, and that an evicted base decoded again starts with no history. Co-Authored-By: Claude Opus 5.5 --- wai/CHANGELOG.md | 6 +- wai/extensions/staged-delivery.md | 25 +- wai/staged-conformance/README.md | 3 +- .../cases/attested_across_drop/case.json | 1 + .../cases/attested_after_decode/case.json | 1 + .../cases/attested_before_decode/case.json | 1 + .../cases/audio_flac_rate_ladder/case.json | 1 + .../cases/audio_int_codec_1l/case.json | 1 + .../cases/base_not_admitted/case.json | 1 + .../cases/base_readded/case.json | 1 + .../cases/base_via_fallback/case.json | 1 + .../cases/batch_conflict_release/case.json | 1 + .../cases/budget_partly_applied/case.json | 1 + .../cases/bypass_extremes/case.json | 1 + .../cases/capability_not_refinement/case.json | 1 + .../cases/cdf_malformed/case.json | 1 + .../cases/chain_budget_retained/case.json | 1 + .../cases/chain_over_limit/case.json | 1 + .../cases/clamp_extremes/case.json | 1 + .../cases/conflict_after_apply/case.json | 1 + .../cases/conflict_held/case.json | 1 + .../cases/cost_over_limit/case.json | 1 + .../cases/cost_over_limit_symbols/case.json | 1 + .../cases/cost_over_limit_working/case.json | 1 + .../cases/deadline_held_layer/case.json | 1 + .../cases/deadline_missed/case.json | 1 + .../cases/evidence_upgrade/case.json | 1 + .../cases/evidence_upgrade_deadline/case.json | 1 + .../cases/frozen_refuses_held/case.json | 1 + .../cases/geometry_mismatch/case.json | 1 + .../cases/held_then_prev_mismatch/case.json | 1 + .../cases/hold_bound_count/case.json | 1 + .../cases/image_int_hyper_2l/case.json | 1 + .../cases/image_int_hyper_unpinned/case.json | 1 + .../cases/image_png_up2/case.json | 1 + .../cases/image_up2_from_int_hyper/case.json | 1 + .../cases/image_up2_odd/case.json | 1 + .../cases/inclusion_only_evidence/case.json | 1 + .../cases/integer_spellings/case.json | 1 + .../cases/layer_gap_on_arrival/case.json | 1 + .../cases/layer_zero/case.json | 1 + .../cases/live_multi_base/case.json | 1 + .../cases/missing_layer/case.json | 1 + .../cases/out_of_order/case.json | 1 + .../cases/payload_truncated/case.json | 1 + .../cases/pending_l1_l2/case.json | 1 + wai/staged-conformance/cases/q_zero/case.json | 1 + .../cases/rate_downsampling/case.json | 1 + .../cases/rate_geometry_mismatch/case.json | 1 + .../cases/record_budget/L1a_0.wai | Bin 0 -> 587 bytes .../cases/record_budget/L1a_1.wai | Bin 0 -> 587 bytes .../cases/record_budget/L1a_2.wai | Bin 0 -> 587 bytes .../cases/record_budget/L1b_0.wai | Bin 0 -> 595 bytes .../cases/record_budget/L1b_1.wai | Bin 0 -> 599 bytes .../cases/record_budget/L1b_2.wai | Bin 0 -> 595 bytes .../cases/record_budget/base_0.wai | Bin 0 -> 555 bytes .../cases/record_budget/base_1.wai | Bin 0 -> 551 bytes .../cases/record_budget/base_2.wai | Bin 0 -> 551 bytes .../cases/record_budget/case.json | 354 ++++++++++++++++++ .../cases/record_budget/stage0_0.bin | 1 + .../cases/record_budget/stage0_1.bin | 1 + .../cases/record_budget/stage0_2.bin | 1 + .../cases/refinement_without_base/case.json | 1 + .../cases/refines_against_base/case.json | 1 + .../cases/refines_malformed/case.json | 1 + .../cases/refines_rules/case.json | 1 + .../cases/refused_on_release/case.json | 1 + .../cases/remove_readd_batch_frozen/case.json | 1 + .../cases/remove_readd_diverge/case.json | 1 + .../cases/remove_readd_frozen/case.json | 1 + .../cases/remove_readd_replay/case.json | 1 + .../cases/splat_attr_1l/case.json | 1 + .../cases/step1_refusals/case.json | 1 + .../cases/stream_exhausted/case.json | 1 + .../cases/stream_noncanonical/case.json | 1 + .../cases/unreadable_envelopes/case.json | 1 + .../cases/up2_geometry_mismatch/case.json | 1 + .../cases/video_int_hyper_keyframe/case.json | 1 + .../video_int_motion_gop_perframe/case.json | 1 + .../cases/video_mixed_stage_l2/case.json | 1 + .../cases/wir1_faults/case.json | 1 + .../cases/wrong_base/case.json | 1 + .../cases/wrong_prev/case.json | 1 + wai/staged-conformance/index.json | 9 + wai/staged-conformance/staged_verify.py | 54 ++- wai/wai-rs/src/bin/wai_cli/stage.rs | 2 +- wai/wai-rs/src/bin/wai_staged_vectors.rs | 29 +- wai/wai-rs/src/staged/mod.rs | 116 ++++-- wai/wai-rs/src/staged/tests.rs | 58 ++- wai/wai-rs/tests/staged_resources.rs | 36 +- 90 files changed, 714 insertions(+), 48 deletions(-) create mode 100644 wai/staged-conformance/cases/record_budget/L1a_0.wai create mode 100644 wai/staged-conformance/cases/record_budget/L1a_1.wai create mode 100644 wai/staged-conformance/cases/record_budget/L1a_2.wai create mode 100644 wai/staged-conformance/cases/record_budget/L1b_0.wai create mode 100644 wai/staged-conformance/cases/record_budget/L1b_1.wai create mode 100644 wai/staged-conformance/cases/record_budget/L1b_2.wai create mode 100644 wai/staged-conformance/cases/record_budget/base_0.wai create mode 100644 wai/staged-conformance/cases/record_budget/base_1.wai create mode 100644 wai/staged-conformance/cases/record_budget/base_2.wai create mode 100644 wai/staged-conformance/cases/record_budget/case.json create mode 100644 wai/staged-conformance/cases/record_budget/stage0_0.bin create mode 100644 wai/staged-conformance/cases/record_budget/stage0_1.bin create mode 100644 wai/staged-conformance/cases/record_budget/stage0_2.bin diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index d52d887aa..c7bea8e52 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -35,8 +35,10 @@ wire format may change and such changes are recorded here. dropped. The declared working bytes count the stream index. A sink's work per layer is linear in its payload and declared symbols up to a logarithmic factor in its units, and §14 states how. A sink that drops a base's chain discards the layers held - for it (`base-absent`) and keeps each unit's record (its producers, a pending - producer, its frozen flag); the base decoded again advances only along that record. + for it (`base-absent`) and keeps each unit's record that is not empty (its + producers, a pending producer, its frozen flag), within a declared record budget + whose oldest bases are forgotten first; the base decoded again advances only along + that record, and a frozen unit replays only its own record, in order. Decoding a base again keeps its chain, and a receipt verified for the base, before or after it was decoded, makes it attested from then on, across dropped chains; a layer held for its base and offered again keeps the strongest receipt evidence, and over an attested base counts as diff --git a/wai/extensions/staged-delivery.md b/wai/extensions/staged-delivery.md index d5275490b..0002dde25 100644 --- a/wai/extensions/staged-delivery.md +++ b/wai/extensions/staged-delivery.md @@ -194,8 +194,10 @@ base. Every envelope held for the chain's units is then discarded (`base-absent` reported once for each), and an envelope for that base is held as for any base the sink has not decoded. The sink keeps each unit's **record**: the producers of the stages it reached, the envelope that passed step 8 at its last stage without being -applied, and whether it is frozen; 32 bytes for each recorded stage, at most one -further 32-byte producer, and a flag. When it decodes the base again, each unit +applied, and whether it is frozen. A record is empty when the unit reached no stage, +nothing passed step 8 for it and it is not frozen; the sink keeps only the others, and +nothing of a base all of whose records are empty and that is not attested. When it +decodes the base again, each unit starts at stage 0 with its record. Step 7 compares a layer with the recorded producers, so a unit advances again only along them: a layer whose key a recorded stage holds with other content conflicts, and the unit freezes. A frozen unit stays @@ -204,6 +206,15 @@ and never past it, and every other layer is refused for it (`layer-conflict`). S presents its base's output or the output of one of its recorded producers, never any other. +A sink **MUST** declare a **record budget**, in bytes, for what it keeps of the bases it +does not hold: their records, and the attestation of a base dropped while attested or +attested before it was decoded (§10). A kept base counts 32 bytes, and each record it +keeps 8 bytes, 32 for each recorded stage and 32 for an envelope that passed step 8 +without being applied. When what it keeps passes the budget, the sink forgets whole +bases, the one whose record or attestation it wrote longest ago first, until it is +within it. A forgotten base is as one the sink never decoded and never attested +(§14). + ### 4.5 Chain budget A sink **MUST** declare a budget, in bytes, for the chain of each base it decodes. The @@ -399,10 +410,12 @@ ignores the header. §4.1 step 6. - A sink keeps the record of a base whose chain it dropped (§4.4), so a base decoded again cannot be refined along other producers than it had; the record counts in the - chain's state when the base is decoded again (§4.5). A sink that forgets a record - (the reference `StageSink::forget_base`) treats the base as never decoded, so a layer - that conflicted with the forgotten history can apply; it forgets only a base it will - not decode again. + chain's state when the base is decoded again (§4.5), and, while the base is not + held, in the sink's record budget (§4.4). A sink that forgets a base, by + `StageSink::forget_base` in the reference or because its record budget is full, + treats it as never decoded and never attested: a layer that conflicted with the + forgotten history can apply, and step 5 no longer requires a receipt for it. A sink + sizes its record budget for the bases it may decode again. - Where the base is attested (§10), a frozen unit presents only its base's output or the output of one of the producers it recorded, replayed in order (§4.4): a party that can sign conflicting layers can stop a unit's refinement, and cannot make it diff --git a/wai/staged-conformance/README.md b/wai/staged-conformance/README.md index fc6a590f2..112002b05 100644 --- a/wai/staged-conformance/README.md +++ b/wai/staged-conformance/README.md @@ -29,7 +29,8 @@ receipt for it, so that step 5 requires `delivered-verified` of its layers. decoded through, whether through a fallback, whether the sink verified a delivery receipt for it, whether its pin resolved, its tier, its units and the live clock position of its first unit (`first_unit`, §8); the sink's hold bound, cost limit, - chain budget (§4.5) and live schedule; the steps; and what the sink reports and + chain budget (§4.5), record budget (§4.4) and live schedule; the steps (offer, decode + a base, expire, drop a base's chain, attest a base); and what the sink reports and holds. - `wai2/` — `WAI2` envelopes with refinement components (SPEC §7.1) and `index.json`, which states for each what `check_component_roles` reports, whether diff --git a/wai/staged-conformance/cases/attested_across_drop/case.json b/wai/staged-conformance/cases/attested_across_drop/case.json index 4f3d85abd..18756086d 100644 --- a/wai/staged-conformance/cases/attested_across_drop/case.json +++ b/wai/staged-conformance/cases/attested_across_drop/case.json @@ -86,6 +86,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/attested_after_decode/case.json b/wai/staged-conformance/cases/attested_after_decode/case.json index cb6da612b..9c1c68be5 100644 --- a/wai/staged-conformance/cases/attested_after_decode/case.json +++ b/wai/staged-conformance/cases/attested_after_decode/case.json @@ -91,6 +91,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/attested_before_decode/case.json b/wai/staged-conformance/cases/attested_before_decode/case.json index 8a0e57c76..420b327b4 100644 --- a/wai/staged-conformance/cases/attested_before_decode/case.json +++ b/wai/staged-conformance/cases/attested_before_decode/case.json @@ -52,6 +52,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/audio_flac_rate_ladder/case.json b/wai/staged-conformance/cases/audio_flac_rate_ladder/case.json index d41064c0b..7b6c988a3 100644 --- a/wai/staged-conformance/cases/audio_flac_rate_ladder/case.json +++ b/wai/staged-conformance/cases/audio_flac_rate_ladder/case.json @@ -48,6 +48,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/audio_int_codec_1l/case.json b/wai/staged-conformance/cases/audio_int_codec_1l/case.json index 310cc5178..ce9fd47cf 100644 --- a/wai/staged-conformance/cases/audio_int_codec_1l/case.json +++ b/wai/staged-conformance/cases/audio_int_codec_1l/case.json @@ -46,6 +46,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/base_not_admitted/case.json b/wai/staged-conformance/cases/base_not_admitted/case.json index d2ba2dfcf..a25614378 100644 --- a/wai/staged-conformance/cases/base_not_admitted/case.json +++ b/wai/staged-conformance/cases/base_not_admitted/case.json @@ -52,6 +52,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/base_readded/case.json b/wai/staged-conformance/cases/base_readded/case.json index a167e5314..9ae8b939e 100644 --- a/wai/staged-conformance/cases/base_readded/case.json +++ b/wai/staged-conformance/cases/base_readded/case.json @@ -57,6 +57,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/base_via_fallback/case.json b/wai/staged-conformance/cases/base_via_fallback/case.json index ff347d441..c1a7b6469 100644 --- a/wai/staged-conformance/cases/base_via_fallback/case.json +++ b/wai/staged-conformance/cases/base_via_fallback/case.json @@ -52,6 +52,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/batch_conflict_release/case.json b/wai/staged-conformance/cases/batch_conflict_release/case.json index a19f17b3d..0bf98c77e 100644 --- a/wai/staged-conformance/cases/batch_conflict_release/case.json +++ b/wai/staged-conformance/cases/batch_conflict_release/case.json @@ -62,6 +62,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/budget_partly_applied/case.json b/wai/staged-conformance/cases/budget_partly_applied/case.json index 05902da71..5d2eb849c 100644 --- a/wai/staged-conformance/cases/budget_partly_applied/case.json +++ b/wai/staged-conformance/cases/budget_partly_applied/case.json @@ -62,6 +62,7 @@ }, "cost_limit": null, "budget": 1940, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/bypass_extremes/case.json b/wai/staged-conformance/cases/bypass_extremes/case.json index 0d4ad5663..cb0074b2d 100644 --- a/wai/staged-conformance/cases/bypass_extremes/case.json +++ b/wai/staged-conformance/cases/bypass_extremes/case.json @@ -52,6 +52,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/capability_not_refinement/case.json b/wai/staged-conformance/cases/capability_not_refinement/case.json index 4ed2c6a9a..89a0b794c 100644 --- a/wai/staged-conformance/cases/capability_not_refinement/case.json +++ b/wai/staged-conformance/cases/capability_not_refinement/case.json @@ -52,6 +52,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/cdf_malformed/case.json b/wai/staged-conformance/cases/cdf_malformed/case.json index d74e3c255..5c9314130 100644 --- a/wai/staged-conformance/cases/cdf_malformed/case.json +++ b/wai/staged-conformance/cases/cdf_malformed/case.json @@ -52,6 +52,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/chain_budget_retained/case.json b/wai/staged-conformance/cases/chain_budget_retained/case.json index d672a73b1..93d05646e 100644 --- a/wai/staged-conformance/cases/chain_budget_retained/case.json +++ b/wai/staged-conformance/cases/chain_budget_retained/case.json @@ -57,6 +57,7 @@ }, "cost_limit": null, "budget": 1884, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/chain_over_limit/case.json b/wai/staged-conformance/cases/chain_over_limit/case.json index f09e88ae3..0ad6fa587 100644 --- a/wai/staged-conformance/cases/chain_over_limit/case.json +++ b/wai/staged-conformance/cases/chain_over_limit/case.json @@ -52,6 +52,7 @@ }, "cost_limit": null, "budget": 1335, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/clamp_extremes/case.json b/wai/staged-conformance/cases/clamp_extremes/case.json index a1fda76c4..3abf34e67 100644 --- a/wai/staged-conformance/cases/clamp_extremes/case.json +++ b/wai/staged-conformance/cases/clamp_extremes/case.json @@ -52,6 +52,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/conflict_after_apply/case.json b/wai/staged-conformance/cases/conflict_after_apply/case.json index ad59b04ce..5ca04c097 100644 --- a/wai/staged-conformance/cases/conflict_after_apply/case.json +++ b/wai/staged-conformance/cases/conflict_after_apply/case.json @@ -62,6 +62,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/conflict_held/case.json b/wai/staged-conformance/cases/conflict_held/case.json index af36018a1..0752689ae 100644 --- a/wai/staged-conformance/cases/conflict_held/case.json +++ b/wai/staged-conformance/cases/conflict_held/case.json @@ -57,6 +57,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/cost_over_limit/case.json b/wai/staged-conformance/cases/cost_over_limit/case.json index c147ec0db..034595402 100644 --- a/wai/staged-conformance/cases/cost_over_limit/case.json +++ b/wai/staged-conformance/cases/cost_over_limit/case.json @@ -56,6 +56,7 @@ "working_bytes": 18446744073709551615 }, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/cost_over_limit_symbols/case.json b/wai/staged-conformance/cases/cost_over_limit_symbols/case.json index 2d9a41781..3a972851b 100644 --- a/wai/staged-conformance/cases/cost_over_limit_symbols/case.json +++ b/wai/staged-conformance/cases/cost_over_limit_symbols/case.json @@ -56,6 +56,7 @@ "working_bytes": 18446744073709551615 }, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/cost_over_limit_working/case.json b/wai/staged-conformance/cases/cost_over_limit_working/case.json index 04a464f6d..f856985b0 100644 --- a/wai/staged-conformance/cases/cost_over_limit_working/case.json +++ b/wai/staged-conformance/cases/cost_over_limit_working/case.json @@ -56,6 +56,7 @@ "working_bytes": 100 }, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/deadline_held_layer/case.json b/wai/staged-conformance/cases/deadline_held_layer/case.json index 7cd60217e..a5c3a81a3 100644 --- a/wai/staged-conformance/cases/deadline_held_layer/case.json +++ b/wai/staged-conformance/cases/deadline_held_layer/case.json @@ -57,6 +57,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": { "t0_ms": 100, "unit_ms": 40, diff --git a/wai/staged-conformance/cases/deadline_missed/case.json b/wai/staged-conformance/cases/deadline_missed/case.json index 41c7224f6..523f17a07 100644 --- a/wai/staged-conformance/cases/deadline_missed/case.json +++ b/wai/staged-conformance/cases/deadline_missed/case.json @@ -57,6 +57,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": { "t0_ms": 100, "unit_ms": 40, diff --git a/wai/staged-conformance/cases/evidence_upgrade/case.json b/wai/staged-conformance/cases/evidence_upgrade/case.json index f7e8b2402..2c73b0cbf 100644 --- a/wai/staged-conformance/cases/evidence_upgrade/case.json +++ b/wai/staged-conformance/cases/evidence_upgrade/case.json @@ -52,6 +52,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/evidence_upgrade_deadline/case.json b/wai/staged-conformance/cases/evidence_upgrade_deadline/case.json index e2ebb889c..29eb58655 100644 --- a/wai/staged-conformance/cases/evidence_upgrade_deadline/case.json +++ b/wai/staged-conformance/cases/evidence_upgrade_deadline/case.json @@ -52,6 +52,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": { "t0_ms": 0, "unit_ms": 40, diff --git a/wai/staged-conformance/cases/frozen_refuses_held/case.json b/wai/staged-conformance/cases/frozen_refuses_held/case.json index e00a02c6d..c23565832 100644 --- a/wai/staged-conformance/cases/frozen_refuses_held/case.json +++ b/wai/staged-conformance/cases/frozen_refuses_held/case.json @@ -62,6 +62,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/geometry_mismatch/case.json b/wai/staged-conformance/cases/geometry_mismatch/case.json index 865bf6c5a..a8d6b93ba 100644 --- a/wai/staged-conformance/cases/geometry_mismatch/case.json +++ b/wai/staged-conformance/cases/geometry_mismatch/case.json @@ -52,6 +52,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/held_then_prev_mismatch/case.json b/wai/staged-conformance/cases/held_then_prev_mismatch/case.json index 03335d1b8..623e532e2 100644 --- a/wai/staged-conformance/cases/held_then_prev_mismatch/case.json +++ b/wai/staged-conformance/cases/held_then_prev_mismatch/case.json @@ -57,6 +57,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/hold_bound_count/case.json b/wai/staged-conformance/cases/hold_bound_count/case.json index 517d0a6be..81db11b04 100644 --- a/wai/staged-conformance/cases/hold_bound_count/case.json +++ b/wai/staged-conformance/cases/hold_bound_count/case.json @@ -57,6 +57,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/image_int_hyper_2l/case.json b/wai/staged-conformance/cases/image_int_hyper_2l/case.json index d2325191a..3f3a17b38 100644 --- a/wai/staged-conformance/cases/image_int_hyper_2l/case.json +++ b/wai/staged-conformance/cases/image_int_hyper_2l/case.json @@ -53,6 +53,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/image_int_hyper_unpinned/case.json b/wai/staged-conformance/cases/image_int_hyper_unpinned/case.json index 721598f3e..4862747c7 100644 --- a/wai/staged-conformance/cases/image_int_hyper_unpinned/case.json +++ b/wai/staged-conformance/cases/image_int_hyper_unpinned/case.json @@ -48,6 +48,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/image_png_up2/case.json b/wai/staged-conformance/cases/image_png_up2/case.json index f4b4deb3a..931e4bc31 100644 --- a/wai/staged-conformance/cases/image_png_up2/case.json +++ b/wai/staged-conformance/cases/image_png_up2/case.json @@ -43,6 +43,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/image_up2_from_int_hyper/case.json b/wai/staged-conformance/cases/image_up2_from_int_hyper/case.json index 745255239..625d44ca4 100644 --- a/wai/staged-conformance/cases/image_up2_from_int_hyper/case.json +++ b/wai/staged-conformance/cases/image_up2_from_int_hyper/case.json @@ -53,6 +53,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/image_up2_odd/case.json b/wai/staged-conformance/cases/image_up2_odd/case.json index 942cc7b5a..b1b94147a 100644 --- a/wai/staged-conformance/cases/image_up2_odd/case.json +++ b/wai/staged-conformance/cases/image_up2_odd/case.json @@ -53,6 +53,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/inclusion_only_evidence/case.json b/wai/staged-conformance/cases/inclusion_only_evidence/case.json index 1a024e11e..1f6b636a3 100644 --- a/wai/staged-conformance/cases/inclusion_only_evidence/case.json +++ b/wai/staged-conformance/cases/inclusion_only_evidence/case.json @@ -52,6 +52,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/integer_spellings/case.json b/wai/staged-conformance/cases/integer_spellings/case.json index 0c7337fbc..6a59c2f85 100644 --- a/wai/staged-conformance/cases/integer_spellings/case.json +++ b/wai/staged-conformance/cases/integer_spellings/case.json @@ -77,6 +77,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/layer_gap_on_arrival/case.json b/wai/staged-conformance/cases/layer_gap_on_arrival/case.json index b5a1aaa88..6b92c1a51 100644 --- a/wai/staged-conformance/cases/layer_gap_on_arrival/case.json +++ b/wai/staged-conformance/cases/layer_gap_on_arrival/case.json @@ -57,6 +57,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/layer_zero/case.json b/wai/staged-conformance/cases/layer_zero/case.json index 1c7e62b97..9056d4d46 100644 --- a/wai/staged-conformance/cases/layer_zero/case.json +++ b/wai/staged-conformance/cases/layer_zero/case.json @@ -52,6 +52,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/live_multi_base/case.json b/wai/staged-conformance/cases/live_multi_base/case.json index 1d5557f8a..194a5c5a1 100644 --- a/wai/staged-conformance/cases/live_multi_base/case.json +++ b/wai/staged-conformance/cases/live_multi_base/case.json @@ -86,6 +86,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": { "t0_ms": 0, "unit_ms": 100, diff --git a/wai/staged-conformance/cases/missing_layer/case.json b/wai/staged-conformance/cases/missing_layer/case.json index 7a19787ae..132602a51 100644 --- a/wai/staged-conformance/cases/missing_layer/case.json +++ b/wai/staged-conformance/cases/missing_layer/case.json @@ -52,6 +52,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/out_of_order/case.json b/wai/staged-conformance/cases/out_of_order/case.json index d39e1aacd..c67d557d0 100644 --- a/wai/staged-conformance/cases/out_of_order/case.json +++ b/wai/staged-conformance/cases/out_of_order/case.json @@ -57,6 +57,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/payload_truncated/case.json b/wai/staged-conformance/cases/payload_truncated/case.json index 67729e00e..4d06fcddf 100644 --- a/wai/staged-conformance/cases/payload_truncated/case.json +++ b/wai/staged-conformance/cases/payload_truncated/case.json @@ -52,6 +52,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/pending_l1_l2/case.json b/wai/staged-conformance/cases/pending_l1_l2/case.json index 660a0b803..99a894e94 100644 --- a/wai/staged-conformance/cases/pending_l1_l2/case.json +++ b/wai/staged-conformance/cases/pending_l1_l2/case.json @@ -57,6 +57,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/q_zero/case.json b/wai/staged-conformance/cases/q_zero/case.json index 6caf4969d..d0eeacd01 100644 --- a/wai/staged-conformance/cases/q_zero/case.json +++ b/wai/staged-conformance/cases/q_zero/case.json @@ -52,6 +52,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/rate_downsampling/case.json b/wai/staged-conformance/cases/rate_downsampling/case.json index 69d041329..c7e6a4907 100644 --- a/wai/staged-conformance/cases/rate_downsampling/case.json +++ b/wai/staged-conformance/cases/rate_downsampling/case.json @@ -43,6 +43,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/rate_geometry_mismatch/case.json b/wai/staged-conformance/cases/rate_geometry_mismatch/case.json index 29349c3bc..1e1848bcf 100644 --- a/wai/staged-conformance/cases/rate_geometry_mismatch/case.json +++ b/wai/staged-conformance/cases/rate_geometry_mismatch/case.json @@ -43,6 +43,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/record_budget/L1a_0.wai b/wai/staged-conformance/cases/record_budget/L1a_0.wai new file mode 100644 index 0000000000000000000000000000000000000000..7dd6786df022fca813f738f269edac99c11e8ce2 GIT binary patch literal 587 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_F1iG{&u_!$i7%q9GIXSg|7#JABJ%bDxfx*cHWOG079iu_ z|NsAapaQFb0*pY;Y6}L2RWU%hsSFH32Z8t@1H<3XK>lwAhX4OTiWwPvfFua(?VP>l z>9i*d#-3xbwY<<=6RVlJh<2(dKn#cZ@`9+@wsGNYjd<}gx;EYHO@0rDcsAv`or?}jXhsjfoATtnYRA@Tk$DtF8#UTV5rY=RN_+i zzZi?lw(Xxd55B7?Rh|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZYX2}WFob&s88QNclL^S?fMRAK4FfDd z#=rmn|MNfvRs#iA3jjG*3=FHn7#LP|F);i+%)qex83RMmR|baQzYGlj|AQ1WGWY;V z5bid;$F(7dnPJWUv=-Ejqc z%xU5O+%BE>_y4uF?ct4gX4*DKibQLx&8?gra<_3$SCA1r?-jErVX0G_epuO6ivVE{ H!J9V$W`4U< literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/record_budget/L1a_2.wai b/wai/staged-conformance/cases/record_budget/L1a_2.wai new file mode 100644 index 0000000000000000000000000000000000000000..548a838c91b10285ff6d9768b27e22631f3bbe26 GIT binary patch literal 587 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_E!or} zHPtZHD9I=#*~r4!&^*y3EeQxxlFUs_lZ{f0QVk4}lZ`D>fif1RCMgysi3XNH*A^6| zmQlx~oW#o1A|)$Bpi4^G5M(Z}|6!pya8PggJA)7h)Q zBk!%YJm~fA4AU<^&&J%Je`BKks-5D~KRo*J>71GFB8G2~uX;td^EaH{w4{C)pT@CB E041rzy#N3J literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/record_budget/L1b_0.wai b/wai/staged-conformance/cases/record_budget/L1b_0.wai new file mode 100644 index 0000000000000000000000000000000000000000..662a88b924828aa50141e4d6ff426f7c1a56bbf6 GIT binary patch literal 595 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_F1iG{&u_!$i7%q9GIXSf~z+epb3^HT{(o75t3>;9*45VRz1AtcUgJN1# zBUi@MlT*Vw?@bJN&9>_cyW-Mr*>tI;pE5Sj4xeX#+%;ln^x@M#XSg&i+iY?7YN6Vf z9j0geGr80R?(V&I@x-EHhdVp|&kj)E72;*b%pj8^6xHabeDO(-gTX()-Li&XOj@`3 zb9by<`R!-crn`}kk8hDv`IYAD$vtn+p^N6CkHt32i+|Carn+^()IVa}cT-oj@ChH8 GpauY<62LP6 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/record_budget/L1b_1.wai b/wai/staged-conformance/cases/record_budget/L1b_1.wai new file mode 100644 index 0000000000000000000000000000000000000000..5294c254ab54f5207657287c2cfcb5bd3b67b6fd GIT binary patch literal 599 zcmWG{^fYv4WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_X?+1SeaU+WMv3+X-Q&HdMYqn@=9}ZYB_+x815Nl$OxpF7#J8hpqLp*!vG7E z!3z@*V_;Y<1H>i_3}LHVE`FE6_tcxJZTZGq38RErx`-Z!^JbQd&USYzaB*2gED zxF`GIft^)+mtLE!{NEbD6tQ$q*LU}ff2U7=ePUxj)1TR;pHGzG@ca4~zo+Mg|!5o?eC&~e?M`*PmX9gLX)>RYedG)*nGVY@5pdb{FEz@|6P Ri)!vLh|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_E!or} zHPtZHD9I=#*~r4!&^*y3EeQxxlFUs_lZ{f0QVk4}lZ`D>fif1RCMgysi3XNH*A^6| zmQlx~oW#o1A|)$Bpi4^IMdeRm&I{R$gFW`1hKDA>=m$!~g#v#f%IAKoW$NVn04* zRAXRxmS`iVx+KT?=_cjc_y~Z4TFc%oAk+U&+T%K>M?db39WYc%*nv8DCKgemw5iE?&Xb7;|vaG>TNX3_L-$ijm|n>b9TKKv?kq2F=}%84<7gH M*XMXY{d1`w0Htxnwg3PC literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/record_budget/base_0.wai b/wai/staged-conformance/cases/record_budget/base_0.wai new file mode 100644 index 0000000000000000000000000000000000000000..772bd17915ddd5443fcc18ca3a5fe468f8615184 GIT binary patch literal 555 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp1M7#SGCJ;Mwcfix4)xg2Qt z>V+5>B7_+j*2(~BWgu2(U|`l_V2IQOVp9eNRx<_$ zHVX!Z4K54}%ce0f1T1D?_`Q^YA#gPV!}1Lb3_&{?82;{OU|4Yih|dApH-YpcAb!EX z@b4`Ue`H_?`O3iX|2q)>2e|;~NFX=^#Cj}qHEV!$pWwawrb0P4eC2(M;x+vQIeB#$ zb_qsIx~_G3nzfCBjJKp@-_cofUS}IFUz_T(+1y9VnbGgg&xVVB&0U;3{Qge*T=c;5 z=S}^tzmxSg9P+N?n#I-q@c--Le$Cq&+naXdzNqq+q}TV) j&E>jw^7M0m?!*HdPjE~B`ZjN;tLUA;Z=aL*+DQWd=v=}~ literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/record_budget/base_1.wai b/wai/staged-conformance/cases/record_budget/base_1.wai new file mode 100644 index 0000000000000000000000000000000000000000..2a66f6786e596ec0f4db4876c074083835e85999 GIT binary patch literal 551 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp2H7#SGCJ;Mwcfix4)xg2Qt zRlhU|a$28NYKfb>P6_+18u6;FWd z=Ro`xh(7}5zA!L^{A6JG{~zQ6pd*3c1Q4^bY_E*nv0pBV|KCPG3Hkk%sxJAf&2Bn< zS6p2GK!IhgsG*K^rTv$`VR9|g`i$SMzhEMz_j9TG*R$tWy}0?HOLP9RUFij@x@NOJ zzZjMMe!uFzl_tIJ2eqtk{wZ#X_%5;V##KSdbN9TPJ>JxRNZ%Fnjpb$S%4tj1C!9Pc zXYTMau($K0P1^b5bw?~)n~zpCdaF95>h6_kSXbf9Y4+kR$AZex$gOn(?BexDlWy2= hvAG<((*F;u>6sfPm!z)P>E4((%Q!)E_rLuNN&pa%###UX literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/record_budget/base_2.wai b/wai/staged-conformance/cases/record_budget/base_2.wai new file mode 100644 index 0000000000000000000000000000000000000000..241c849d79bbb26cb04846ef7f510943dfd23427 GIT binary patch literal 551 zcmWG{^fa8$z`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&Lp2H7#SGCJ;Mwcfix4)xg2Qt z`ZD=;vuS7Bg?(gb3C28Im=3=C{0 z3=A=r3=A8c7#M=4Gcf#F$iT3C36Q-KNN)hr+Zh;E>;d9q3=DtI0>$nC#U2CsFM#+R z1H-?M3=AP(85mamU|{(FAM64U!EgeIUoz!+ex7qsxKZfqT5grIKhyNu*cTWwUVkU& z?)X*N%IT}_U4wtKOiKKeXMg5XTUNK*G)*gFNt8zU;+Eisofl6Q*0B8Dy6nr|#m@^q zv%gts7`*0ByxbX`!sGAwb$0&iPup?UW7nf6@AL1x`d@Ht^Q^422ez!wmK9yUadw+* zz!TfDv!9rgj%S}+e&46C(ad)$FU!<{9 literal 0 HcmV?d00001 diff --git a/wai/staged-conformance/cases/record_budget/case.json b/wai/staged-conformance/cases/record_budget/case.json new file mode 100644 index 000000000..b4f2dc289 --- /dev/null +++ b/wai/staged-conformance/cases/record_budget/case.json @@ -0,0 +1,354 @@ +{ + "format": "wai-staged-case/1", + "name": "record_budget", + "about": "a record budget with room for one dropped base's records: bases 0 and 1 each apply a layer 1 and are dropped, so base 0, dropped first, is forgotten; decoded again it starts with no history and another layer 1 applies, while base 1 keeps its record and the other layer conflicts. Base 2, dropped with nothing recorded, keeps nothing and costs no room", + "bases": [ + { + "file": "base_0.wai", + "object": "af699aa62e0c73746f529abac02aa628ce7413349b97d6b554af7fadd81f4cbf", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0_0.bin", + "stage0_sha256": "c1ac8d671c3d32f6351ca7d04ef779a735488b5c858eb835cccd5918c0286bc7", + "decode": { + "kind": "int", + "set": {} + } + }, + { + "file": "base_1.wai", + "object": "8c13e6e22a3f7ff0a4876d6c46273000edbe70111dbf389c34a5dc5483860541", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0_1.bin", + "stage0_sha256": "c92a6d59d8499b8aeabff1282319af5beb0bd96c6e8165b3765eca434c560ff2", + "decode": { + "kind": "int", + "set": {} + } + }, + { + "file": "base_2.wai", + "object": "63f8b55093fc58ee1e2b2dc28317a4fbbbbdb755c2d2e00cc38e17a854d84a09", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0_2.bin", + "stage0_sha256": "f0200358dfc0a2932366db121a6555f06dafa8bcbe2c06bdd1752da5e593b5c0", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1a_0.wai", + "object": "729200a3f8b525f4699768200762fc53a43c57a4a11c5b91a5adb52bec5d2300", + "bytes": 587 + }, + { + "file": "L1b_0.wai", + "object": "99dbfe63b5a2aed2e544b1a7318ac11e4189d8638078cef682bc8d5e0dc3e935", + "bytes": 595 + }, + { + "file": "L1a_1.wai", + "object": "26e5016c1a1bb0ae25cd80e99efd9a9ba1f8e4191544024eace94276fb95b2c5", + "bytes": 587 + }, + { + "file": "L1b_1.wai", + "object": "1989c68717ad8fe42777c35dcb52f2057e3b435308f94a3a8d59eeeb1fa33f64", + "bytes": 599 + }, + { + "file": "L1a_2.wai", + "object": "5310224c7997c28cb3e77e7db220ecea13da4512a08686f6183703cbc84690a0", + "bytes": 587 + }, + { + "file": "L1b_2.wai", + "object": "5a8b7053d9489eb4ec7afecd00d51a821dbf71bccf80291ccb0e6c9d54eb84a3", + "bytes": 595 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 112, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 2, + "at_ms": 0 + }, + { + "op": "remove", + "index": 2, + "at_ms": 1 + }, + { + "op": "base", + "index": 0, + "at_ms": 2 + }, + { + "op": "offer", + "file": "L1a_0.wai", + "at_ms": 3, + "evidence": "none" + }, + { + "op": "base", + "index": 1, + "at_ms": 4 + }, + { + "op": "offer", + "file": "L1a_1.wai", + "at_ms": 5, + "evidence": "none" + }, + { + "op": "remove", + "index": 0, + "at_ms": 6 + }, + { + "op": "remove", + "index": 1, + "at_ms": 7 + }, + { + "op": "base", + "index": 0, + "at_ms": 8 + }, + { + "op": "offer", + "file": "L1b_0.wai", + "at_ms": 9, + "evidence": "none" + }, + { + "op": "base", + "index": 1, + "at_ms": 10 + }, + { + "op": "offer", + "file": "L1b_1.wai", + "at_ms": 11, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [], + [], + [ + { + "object": "729200a3f8b525f4699768200762fc53a43c57a4a11c5b91a5adb52bec5d2300", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ], + [], + [ + { + "object": "26e5016c1a1bb0ae25cd80e99efd9a9ba1f8e4191544024eace94276fb95b2c5", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ], + [], + [], + [], + [ + { + "object": "99dbfe63b5a2aed2e544b1a7318ac11e4189d8638078cef682bc8d5e0dc3e935", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ], + [], + [ + { + "object": "1989c68717ad8fe42777c35dcb52f2057e3b435308f94a3a8d59eeeb1fa33f64", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "99dbfe63b5a2aed2e544b1a7318ac11e4189d8638078cef682bc8d5e0dc3e935", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "ffc3dcceeef14fc0975399adc5138ab09f3e29cced7ec9e30cc9fdaeae0fcb34" + }, + { + "stage": 1, + "producer": "99dbfe63b5a2aed2e544b1a7318ac11e4189d8638078cef682bc8d5e0dc3e935", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "b0ed3da1b8e4ec2f679b7dee723458c445c0afbbdfe9a37ceda38fb78c7e764a" + } + ], + "clip_sha256": "e46d8b6cce68ded46f9d5a32739d0b2e1edb39c9b744e6761e33e509afb9308f", + "n_units": 2 + }, + { + "units": [ + { + "stage": 0, + "producer": "8c13e6e22a3f7ff0a4876d6c46273000edbe70111dbf389c34a5dc5483860541", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "62ffa82b6ccba510e4506f4824654e7d10db31f67e218e1463bc08e09590f3ce" + }, + { + "stage": 0, + "producer": "8c13e6e22a3f7ff0a4876d6c46273000edbe70111dbf389c34a5dc5483860541", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "89b674417dc9f28e3872c925de74a142c22ff56cc12da59280de1a509307ddc0" + } + ], + "clip_sha256": "7afc585abb5a585bc805778f132bd3267b28a40b8f2adceacaa82e9e37df0861", + "n_units": 2 + }, + null + ] + } +} diff --git a/wai/staged-conformance/cases/record_budget/stage0_0.bin b/wai/staged-conformance/cases/record_budget/stage0_0.bin new file mode 100644 index 000000000..4eee05424 --- /dev/null +++ b/wai/staged-conformance/cases/record_budget/stage0_0.bin @@ -0,0 +1 @@ +((8(0@(@@8@X@P`PXp``x (@(8@(HP@HXH``X`x`xx`x�88H@H`ˆÀØP`p`xˆ`xˆx€˜€�˜HXh˜¸ð Ðè°àÿx€ˆx�˜ˆ ¨˜¨°¨Àð°Ðð¸àÿÀðÿÐÿÿˆ °¨¨¸¨ÀÈpxˆÈèÿÐøÿÐøÿ ¨° °È¨ÀȸÐàˆˆ˜� ¨Øÿÿ ¨È°ÈиÀØÐØàØè𘨸 °¸¨ÀÈ¸ÈØÈÐèÐØèààÿèøÿ((8(0@(@@8@X@PhPXp``x (@(8@0HP@HXH`hX`x`xxhx�88HHH`@``P`x`xˆhxˆx€˜€�˜HXpXXxXpph€‡Ààÿ€��ˆ ¨˜¨°``xhpxp€�ÀðÿÐÿÿÐÿÿ ¨¸¨ÀÈpxˆ€ˆ—ÐøÿÐøÿèÿÿèÿÿðÿÿ¸Ðà€ˆ˜� ¨�¯·èÿÿøÿÿÿÿÿÈØàØà𘨸 °¸¨ÀȸÈÐÿÿÿÐØàààÿàøÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/record_budget/stage0_1.bin b/wai/staged-conformance/cases/record_budget/stage0_1.bin new file mode 100644 index 000000000..eddf2d511 --- /dev/null +++ b/wai/staged-conformance/cases/record_budget/stage0_1.bin @@ -0,0 +1 @@ + (8 (8 8P8H`8XXP`pXhp 0808H0HXHHhH`hX`x`pˆpx�0HH8P`ˆÀàXhphpˆhp�€ˆ�x�¨PP` Àà Èø°Ðÿxˆ�ˆ� �˜°� ¸¨Ðø°ØøÀàÿÈèÿÐøÿ�˜¸˜¸À¨¸Èh€ˆÀðÿÈøÿØøÿ¨°À¨¸È°ÀØÀÈЀ˜¨ˆ ¸àÿÿ¨¸È°¸ÈÀÐÐÀàèÐèð�¨¸¨¨À°ÀÈÀÀØÈÐàØàèÐèðèøÿ ((8 (8(8P8H`@XXP`pXhp 0808H0HXHHhH`h``x`pˆpxˆ0HH8P`@`hXhphpˆhxˆ€ˆ�x� PP`X`hXh€hp�Àèÿˆ� �˜°� ¸`p€px€x€�ÈèÿÐøÿØøÿ˜°À¨¸Àp€ˆx��ÈøÿØøÿèÿÿðÿÿðÿÿÀÈЀ˜¨ˆ °˜Ÿ¿èÿÿøÿÿÿÿÿÀØèÈèð�¨¸¨¨À°ÀÈÀÀØÿÿÿØàèÐèðèøÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/record_budget/stage0_2.bin b/wai/staged-conformance/cases/record_budget/stage0_2.bin new file mode 100644 index 000000000..8f107525c --- /dev/null +++ b/wai/staged-conformance/cases/record_budget/stage0_2.bin @@ -0,0 +1 @@ +0 0 0@8@H0PXHPXPXp`hx (80@@8HXHPXHXpXhphpˆp€ˆ8@PHPX˜¸àPhpXx€pxˆxˆ�ˆ� H`h�Èè°Èð°Øøpˆ˜€˜ ˆ ¨˜¨¸ Èð¨Øÿ¸àÿÈàÿØøÿ˜ ¸¨°È¨ÀÀxˆ�¸èÿÐðÿØÿÿ °¸ ÀȸÀÐÈÐØ€ˆ ˆ˜¨àÿÿ¨°È¨ÈȸÈàÀÐèÐØø˜¨¸ °À°ÀÐÀÐÐÈØàÈèðØðøàøÿ0(0 0@8@H0PXPPXPXp`hx (80@H8PXHPXHXpXpphpˆp€ˆ8@PHPXPXhPhpXx€px€xˆ�ˆ� P`hHhphhxhx€¸èÿ€˜ € ¨˜ ¸Xhx`x�p€�ÈàÿØøÿàÿÿ °À¨ÀÀx€�xˆŸÐðÿØÿÿàÿÿèÿÿÿÿÿÈÐØ€ˆ ˆ˜¨˜¯¯ðÿÿðÿÿÿÿÿÀÐèÐØø˜¨° °À°ÀиÐÐÿÿÿÈàðØðøàøÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/refinement_without_base/case.json b/wai/staged-conformance/cases/refinement_without_base/case.json index 45f6d9a0f..a87edfc79 100644 --- a/wai/staged-conformance/cases/refinement_without_base/case.json +++ b/wai/staged-conformance/cases/refinement_without_base/case.json @@ -52,6 +52,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/refines_against_base/case.json b/wai/staged-conformance/cases/refines_against_base/case.json index 7344e0a51..c03957005 100644 --- a/wai/staged-conformance/cases/refines_against_base/case.json +++ b/wai/staged-conformance/cases/refines_against_base/case.json @@ -57,6 +57,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/refines_malformed/case.json b/wai/staged-conformance/cases/refines_malformed/case.json index fbd1ac940..bd0403d2e 100644 --- a/wai/staged-conformance/cases/refines_malformed/case.json +++ b/wai/staged-conformance/cases/refines_malformed/case.json @@ -52,6 +52,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/refines_rules/case.json b/wai/staged-conformance/cases/refines_rules/case.json index 9b62560e4..eb7df09ef 100644 --- a/wai/staged-conformance/cases/refines_rules/case.json +++ b/wai/staged-conformance/cases/refines_rules/case.json @@ -67,6 +67,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/refused_on_release/case.json b/wai/staged-conformance/cases/refused_on_release/case.json index 53517dd2c..66ea51546 100644 --- a/wai/staged-conformance/cases/refused_on_release/case.json +++ b/wai/staged-conformance/cases/refused_on_release/case.json @@ -57,6 +57,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/remove_readd_batch_frozen/case.json b/wai/staged-conformance/cases/remove_readd_batch_frozen/case.json index 59622efce..c0631fc1a 100644 --- a/wai/staged-conformance/cases/remove_readd_batch_frozen/case.json +++ b/wai/staged-conformance/cases/remove_readd_batch_frozen/case.json @@ -57,6 +57,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/remove_readd_diverge/case.json b/wai/staged-conformance/cases/remove_readd_diverge/case.json index 4cd0b11e7..dbe18658b 100644 --- a/wai/staged-conformance/cases/remove_readd_diverge/case.json +++ b/wai/staged-conformance/cases/remove_readd_diverge/case.json @@ -57,6 +57,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/remove_readd_frozen/case.json b/wai/staged-conformance/cases/remove_readd_frozen/case.json index 243eb835e..572af4465 100644 --- a/wai/staged-conformance/cases/remove_readd_frozen/case.json +++ b/wai/staged-conformance/cases/remove_readd_frozen/case.json @@ -62,6 +62,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/remove_readd_replay/case.json b/wai/staged-conformance/cases/remove_readd_replay/case.json index d352b0831..eadebd04b 100644 --- a/wai/staged-conformance/cases/remove_readd_replay/case.json +++ b/wai/staged-conformance/cases/remove_readd_replay/case.json @@ -62,6 +62,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/splat_attr_1l/case.json b/wai/staged-conformance/cases/splat_attr_1l/case.json index fb987e1f6..a969e0123 100644 --- a/wai/staged-conformance/cases/splat_attr_1l/case.json +++ b/wai/staged-conformance/cases/splat_attr_1l/case.json @@ -46,6 +46,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/step1_refusals/case.json b/wai/staged-conformance/cases/step1_refusals/case.json index 1e02dd236..26ac2af7d 100644 --- a/wai/staged-conformance/cases/step1_refusals/case.json +++ b/wai/staged-conformance/cases/step1_refusals/case.json @@ -57,6 +57,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/stream_exhausted/case.json b/wai/staged-conformance/cases/stream_exhausted/case.json index dd9912dcc..b443c4f9e 100644 --- a/wai/staged-conformance/cases/stream_exhausted/case.json +++ b/wai/staged-conformance/cases/stream_exhausted/case.json @@ -52,6 +52,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/stream_noncanonical/case.json b/wai/staged-conformance/cases/stream_noncanonical/case.json index ecad9653d..c068e3cf9 100644 --- a/wai/staged-conformance/cases/stream_noncanonical/case.json +++ b/wai/staged-conformance/cases/stream_noncanonical/case.json @@ -52,6 +52,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/unreadable_envelopes/case.json b/wai/staged-conformance/cases/unreadable_envelopes/case.json index e42ce799b..f27f6f43f 100644 --- a/wai/staged-conformance/cases/unreadable_envelopes/case.json +++ b/wai/staged-conformance/cases/unreadable_envelopes/case.json @@ -97,6 +97,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/up2_geometry_mismatch/case.json b/wai/staged-conformance/cases/up2_geometry_mismatch/case.json index b75885678..063dcaf08 100644 --- a/wai/staged-conformance/cases/up2_geometry_mismatch/case.json +++ b/wai/staged-conformance/cases/up2_geometry_mismatch/case.json @@ -52,6 +52,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/video_int_hyper_keyframe/case.json b/wai/staged-conformance/cases/video_int_hyper_keyframe/case.json index e2447cf14..b29de415c 100644 --- a/wai/staged-conformance/cases/video_int_hyper_keyframe/case.json +++ b/wai/staged-conformance/cases/video_int_hyper_keyframe/case.json @@ -80,6 +80,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/case.json b/wai/staged-conformance/cases/video_int_motion_gop_perframe/case.json index fdb1b9b01..3197f5ac1 100644 --- a/wai/staged-conformance/cases/video_int_motion_gop_perframe/case.json +++ b/wai/staged-conformance/cases/video_int_motion_gop_perframe/case.json @@ -238,6 +238,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/video_mixed_stage_l2/case.json b/wai/staged-conformance/cases/video_mixed_stage_l2/case.json index 36bdfcceb..747861ff0 100644 --- a/wai/staged-conformance/cases/video_mixed_stage_l2/case.json +++ b/wai/staged-conformance/cases/video_mixed_stage_l2/case.json @@ -88,6 +88,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/wir1_faults/case.json b/wai/staged-conformance/cases/wir1_faults/case.json index c3bf4eb69..f1d29f609 100644 --- a/wai/staged-conformance/cases/wir1_faults/case.json +++ b/wai/staged-conformance/cases/wir1_faults/case.json @@ -82,6 +82,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/wrong_base/case.json b/wai/staged-conformance/cases/wrong_base/case.json index ab621a178..2fc1d4f5e 100644 --- a/wai/staged-conformance/cases/wrong_base/case.json +++ b/wai/staged-conformance/cases/wrong_base/case.json @@ -52,6 +52,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/cases/wrong_prev/case.json b/wai/staged-conformance/cases/wrong_prev/case.json index 71f887e30..ca188a2d5 100644 --- a/wai/staged-conformance/cases/wrong_prev/case.json +++ b/wai/staged-conformance/cases/wrong_prev/case.json @@ -57,6 +57,7 @@ }, "cost_limit": null, "budget": 1073741824, + "record_budget": 1048576, "live": null }, "steps": [ diff --git a/wai/staged-conformance/index.json b/wai/staged-conformance/index.json index c143c5fda..a73311241 100644 --- a/wai/staged-conformance/index.json +++ b/wai/staged-conformance/index.json @@ -418,6 +418,15 @@ "059c584015e4d63f5adbe512f820431e611ccb8ca31e661c66498bf5ee9047ce" ] }, + { + "name": "record_budget", + "about": "a record budget with room for one dropped base's records: bases 0 and 1 each apply a layer 1 and are dropped, so base 0, dropped first, is forgotten; decoded again it starts with no history and another layer 1 applies, while base 1 keeps its record and the other layer conflicts. Base 2, dropped with nothing recorded, keeps nothing and costs no room", + "clip_sha256": [ + "e46d8b6cce68ded46f9d5a32739d0b2e1edb39c9b744e6761e33e509afb9308f", + "7afc585abb5a585bc805778f132bd3267b28a40b8f2adceacaa82e9e37df0861", + null + ] + }, { "name": "remove_readd_diverge", "about": "layer 1a applies; the chain is dropped, a different layer 1b arrives and is held for the base, and the base is decoded again: 1b differs from the producer each unit's record holds at stage 1, so it conflicts and freezes the units; 1a, the recorded producer, still replays", diff --git a/wai/staged-conformance/staged_verify.py b/wai/staged-conformance/staged_verify.py index 284cd710f..350903489 100644 --- a/wai/staged-conformance/staged_verify.py +++ b/wai/staged-conformance/staged_verify.py @@ -608,8 +608,9 @@ class Base: self.passed = {} # step 7's record: (u, layer, P(u)) -> the envelope that passed step 8 for u if record is not None: # §4.4: a base decoded again starts at stage 0 with its record self.passed = dict(record["passed"]) - for x, frozen, reached in zip(self.units, record["frozen"], record["reached"]): - x["frozen"], x["reached"] = frozen, reached + for u, (frozen, reached) in record["units"].items(): + if u < len(self.units): + self.units[u]["frozen"], self.units[u]["reached"] = frozen, reached self.held = set() # (envelope hash, u): u held for the envelope (step 8) self.retained = {} # envelope hash -> Envelope, from step 6 to when no unit is held for it (§4.5) @@ -840,8 +841,10 @@ class Sink: def __init__(self, cfg): self.cfg, self.bases, self.waiting = cfg, {}, [] - self.records = {} # §4.4: what a dropped base's units keep - self.attested = set() # §10: bases attested and not held (dropped, or not yet decoded) + # §4.4: what the sink keeps of each base it does not hold (its units' records and + # its attestation), with when it was last written, within the record budget. + self.kept = {} + self.clock = 0 def held_elsewhere(self, base): return len(self.waiting) + sum(len(b.held_envelopes()) for k, b in self.bases.items() if k != base) @@ -891,9 +894,9 @@ class Sink: self.bases[info["hash"]].info["attested"] = True return [] # Attestation belongs to the base hash: a base attested before stays attested. - info = dict(info, attested=info["attested"] or info["hash"] in self.attested) - self.attested.discard(info["hash"]) - b = self.bases[info["hash"]] = Base(info, units, self.cfg, self.records.pop(info["hash"], None)) + k = self.kept.pop(info["hash"], None) + info = dict(info, attested=info["attested"] or (k is not None and k["attested"])) + b = self.bases[info["hash"]] = Base(info, units, self.cfg, k) mine = [e for e in self.waiting if e.ref["base"] == info["hash"]] self.waiting = [e for e in self.waiting if e.ref["base"] != info["hash"]] out = Outcome() @@ -910,9 +913,15 @@ class Sink: out = Outcome() for e in sorted(b.held_envelopes()): out.refuse(e, "base-absent") - self.records[h] = {"passed": b.passed, "frozen": [x["frozen"] for x in b.units], "reached": [x["reached"] for x in b.units]} - if b.info["attested"]: - self.attested.add(h) + # A unit's record is empty when it reached no stage, nothing passed step 8 for + # it and it is not frozen; only the others are kept. + units = {} + for u, x in enumerate(b.units): + passed_here = any(k[0] == u for k in b.passed) + if x["reached"] or x["frozen"] or passed_here: + units[u] = (x["frozen"], x["reached"]) + passed = {k: v for k, v in b.passed.items() if k[0] in units} + self.keep(h, {"units": units, "passed": passed, "attested": b.info["attested"]}) return out.reports() def mark_attested(self, h): @@ -921,9 +930,32 @@ class Sink: if h in self.bases: self.bases[h].info["attested"] = True else: - self.attested.add(h) + k = self.kept.pop(h, None) or {"units": {}, "passed": {}, "attested": False} + k["attested"] = True + self.keep(h, k) return [] + @staticmethod + def kept_size(k): + """§4.4: 32 bytes for the base, and for each kept unit 8, 32 a recorded stage and + 32 for a producer that passed step 8 and was not applied.""" + n = 32 + for u, (_, reached) in k["units"].items(): + n += 8 + 32 * reached + (32 if any(key[0] == u and key[1] == reached + 1 for key in k["passed"]) else 0) + return n + + def keep(self, h, k): + """Keep what is not empty, written now; then, past the record budget, forget the + bases written longest ago, whole (§4.4, §14).""" + if not k["units"] and not k["attested"]: + return + self.clock += 1 + k["written"] = self.clock + self.kept[h] = k + while sum(self.kept_size(x) for x in self.kept.values()) > self.cfg["record_budget"]: + oldest = min(self.kept, key=lambda x: self.kept[x]["written"]) + del self.kept[oldest] + def expire(self, now): """§4.3: what was held past the bound's duration.""" out = Outcome() diff --git a/wai/wai-rs/src/bin/wai_cli/stage.rs b/wai/wai-rs/src/bin/wai_cli/stage.rs index a6a8084b7..c8cdd2da2 100644 --- a/wai/wai-rs/src/bin/wai_cli/stage.rs +++ b/wai/wai-rs/src/bin/wai_cli/stage.rs @@ -123,7 +123,7 @@ fn below(s: &Samples) -> Below<'_> { } fn cfg() -> ChainConfig { - ChainConfig { bound: HoldBound { max_envelopes: 1024, max_age_ms: u64::MAX }, cost_limit: None, budget: u64::MAX, live: None } + ChainConfig { bound: HoldBound { max_envelopes: 1024, max_age_ms: u64::MAX }, cost_limit: None, budget: u64::MAX, record_budget: u64::MAX, live: None } } fn print_units(c: &StageChain) { diff --git a/wai/wai-rs/src/bin/wai_staged_vectors.rs b/wai/wai-rs/src/bin/wai_staged_vectors.rs index 169c16057..1edae28d1 100644 --- a/wai/wai-rs/src/bin/wai_staged_vectors.rs +++ b/wai/wai-rs/src/bin/wai_staged_vectors.rs @@ -653,7 +653,7 @@ struct Case { } fn cfg() -> ChainConfig { - ChainConfig { bound: HoldBound { max_envelopes: 16, max_age_ms: 1000 }, cost_limit: None, budget: 1 << 30, live: None } + ChainConfig { bound: HoldBound { max_envelopes: 16, max_age_ms: 1000 }, cost_limit: None, budget: 1 << 30, record_budget: 1 << 20, live: None } } fn offer(file: &str, at_ms: u64) -> Step { @@ -1279,6 +1279,31 @@ fn cases(ctx: &Ctx) -> Vec { vec![("L1a.wai".into(), la), ("L1b.wai".into(), lb)], vec![offer("L1a.wai", 0), offer("L1b.wai", 1), base_at(0, 2), Step::Remove { index: 0, at_ms: 3 }, base_at(0, 4), offer("L1a.wai", 5)])); } + { + // Three bases; a record budget with room for one base's records (32 bytes and, + // for each of its two units, 8 + 32). + let bases: Vec = (0..3).map(|i| gop_base(ctx, 8, 8, 2, 170 + i, 0, 8)).collect(); + let mut files = Vec::new(); + for (i, b) in bases.iter().enumerate() { + let bh = b.hash(); + for (tag, seed) in [("a", 180), ("b", 190)] { + let belows: Vec = (0..2).map(|f| rgb_unit(b, f)).collect(); + let targets: Vec = (0..2).map(|f| u8s(picture(8, 8, seed + i as u32, f))).collect(); + let (l, _) = layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, None, &belows, (8, 8, 3), &targets, (8, 8, 3), params(4, pid, 4095)); + files.push((format!("L1{tag}_{i}.wai"), l)); + } + } + let one_base = 32 + 2 * (8 + 32); + let o = |f: &str, at| offer(f, at); + out.push(Case { name: "record_budget", about: "a record budget with room for one dropped base's records: bases 0 and 1 each apply a layer 1 and are dropped, so base 0, dropped first, is forgotten; decoded again it starts with no history and another layer 1 applies, while base 1 keeps its record and the other layer conflicts. Base 2, dropped with nothing recorded, keeps nothing and costs no room", + bases, files, cfg: ChainConfig { record_budget: one_base, ..cfg() }, + steps: vec![ + base_at(2, 0), Step::Remove { index: 2, at_ms: 1 }, + base_at(0, 2), o("L1a_0.wai", 3), base_at(1, 4), o("L1a_1.wai", 5), + Step::Remove { index: 0, at_ms: 6 }, Step::Remove { index: 1, at_ms: 7 }, + base_at(0, 8), o("L1b_0.wai", 9), base_at(1, 10), o("L1b_1.wai", 11), + ] }); + } { let a = small(160); let (la, _) = l1_of(&a, 161, 4); @@ -1469,6 +1494,7 @@ fn cfg_json(c: &ChainConfig) -> Value { "bound": {"max_envelopes": c.bound.max_envelopes, "max_age_ms": c.bound.max_age_ms}, "cost_limit": c.cost_limit.map(|l| json!({"output_bytes": l.output_bytes, "symbols": l.symbols, "working_bytes": l.working_bytes})), "budget": c.budget, + "record_budget": c.record_budget, "live": c.live.as_ref().map(|l| json!({"t0_ms": l.clock.t0_ms, "unit_ms": l.clock.unit_ms, "target_latency_ms": l.clock.target_latency_ms, "lead_ms": l.lead_ms})), }) } @@ -1479,6 +1505,7 @@ fn cfg_from(v: &Value) -> ChainConfig { bound: HoldBound { max_envelopes: u(&v["bound"]["max_envelopes"]) as usize, max_age_ms: u(&v["bound"]["max_age_ms"]) }, cost_limit: (!v["cost_limit"].is_null()).then(|| Wir1Cost { output_bytes: u(&v["cost_limit"]["output_bytes"]), symbols: u(&v["cost_limit"]["symbols"]), working_bytes: u(&v["cost_limit"]["working_bytes"]) }), budget: u(&v["budget"]), + record_budget: u(&v["record_budget"]), live: (!v["live"].is_null()).then(|| Live { clock: UnitClock { t0_ms: u(&v["live"]["t0_ms"]), unit_ms: u(&v["live"]["unit_ms"]), target_latency_ms: u(&v["live"]["target_latency_ms"]) }, lead_ms: v["live"]["lead_ms"].as_array().expect("leads").iter().map(u).collect(), diff --git a/wai/wai-rs/src/staged/mod.rs b/wai/wai-rs/src/staged/mod.rs index 1df433766..a1fbc97b7 100644 --- a/wai/wai-rs/src/staged/mod.rs +++ b/wai/wai-rs/src/staged/mod.rs @@ -215,8 +215,9 @@ impl UnitState { /// What a sink keeps of a unit when it drops its base's chain (§4.4): the producers of /// the stages it reached, the envelope that passed step 8 at its last stage without -/// being applied, and whether it is frozen. 32 bytes for each recorded stage, at most -/// one more 32-byte producer, and a flag. +/// being applied, and whether it is frozen. Counted as 8 bytes (its index and flag), +/// 32 for each recorded stage and 32 for a producer that passed; an empty record (no +/// stage, nothing passed, not frozen) is not kept. #[derive(Debug, Clone, PartialEq, Eq)] pub struct UnitRecord { pub producers: Vec<[u8; 32]>, @@ -224,6 +225,18 @@ pub struct UnitRecord { pub frozen: bool, } +impl UnitRecord { + /// Whether the record holds nothing a re-decoded base would differ by. + pub fn is_empty(&self) -> bool { + self.producers.is_empty() && self.passed.is_none() && !self.frozen + } + + /// The bytes §4.4 counts for it. + pub fn counted_bytes(&self) -> u64 { + 8 + 32 * self.producers.len() as u64 + if self.passed.is_some() { 32 } else { 0 } + } +} + /// How long and how much a sink holds (§4.3). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct HoldBound { @@ -251,6 +264,10 @@ pub struct ChainConfig { /// The most bytes a chain's state reaches (§4.5): a layer that would take it past /// this is refused before any stream is decoded (`chain-over-limit`). pub budget: u64, + /// The most bytes a sink keeps of the bases it does not hold (§4.4): their unit + /// records and their attestation ([`StageSink::kept_bytes`]). Past it, the bases + /// dropped or marked longest ago are forgotten, whole. + pub record_budget: u64, pub live: Option, } @@ -447,15 +464,16 @@ impl StageChain { /// A chain over a base decoded again after its chain was dropped (§4.4): each unit /// at stage 0 with its record, so step 7 compares a layer with the producers the /// unit had, and a frozen unit stays frozen. A unit with no record starts afresh. - pub fn with_records(base: BaseInfo, units: Vec, cfg: ChainConfig, records: Vec) -> Self { - let mut records = records.into_iter(); + /// `records` gives, by unit index, the units whose record is not empty. + pub fn with_records(base: BaseInfo, units: Vec, cfg: ChainConfig, records: Vec<(u32, UnitRecord)>) -> Self { + let mut records: BTreeMap = records.into_iter().collect(); let mut passed = BTreeMap::new(); let mut state = 0u64; let units: Vec = units .into_iter() .enumerate() .map(|(u, buf)| { - let r = records.next().unwrap_or(UnitRecord { producers: vec![], passed: None, frozen: false }); + let r = records.remove(&(u as u32)).unwrap_or(UnitRecord { producers: vec![], passed: None, frozen: false }); if let Some(h) = r.passed { passed.insert(u as u32, h); } @@ -477,12 +495,14 @@ impl StageChain { } } - /// Each unit's record (§4.4), what [`StageSink::remove_base`] keeps. - pub fn records(&self) -> Vec { + /// The record of every unit whose record is not empty, by unit index (§4.4): what + /// [`StageSink::remove_base`] keeps. + pub fn records(&self) -> Vec<(u32, UnitRecord)> { self.units .iter() .enumerate() - .map(|(u, s)| UnitRecord { producers: s.producers.clone(), passed: self.passed.get(&(u as u32)).copied(), frozen: s.frozen }) + .map(|(u, s)| (u as u32, UnitRecord { producers: s.producers.clone(), passed: self.passed.get(&(u as u32)).copied(), frozen: s.frozen })) + .filter(|(_, r)| !r.is_empty()) .collect() } @@ -1074,19 +1094,36 @@ pub struct StageSink { cfg: ChainConfig, chains: BTreeMap<[u8; 32], StageChain>, pending: Vec, - /// The unit records of bases whose chains were dropped (§4.4). - records: BTreeMap<[u8; 32], Vec>, - /// Bases the sink does not hold whose delivery receipt it verified (§10): dropped - /// while attested, or marked before they were decoded. A held base's attestation is - /// its chain's. - attested: std::collections::BTreeSet<[u8; 32]>, + /// What the sink keeps of the bases it does not hold (§4.4): the records of dropped + /// chains and the attestation of bases dropped while attested or marked before + /// they were decoded. A held base's are its chain's. + kept: BTreeMap<[u8; 32], Kept>, + /// The same bases by when they were last written, oldest first. + kept_order: BTreeMap, + kept_bytes: u64, + tick: u64, +} + +/// What a sink keeps of one base it does not hold. +#[derive(Debug, Clone, Default)] +struct Kept { + records: Vec<(u32, UnitRecord)>, + attested: bool, + written: u64, +} + +impl Kept { + /// §4.4: 32 bytes for the base's hash and attestation, and each kept unit record. + fn counted_bytes(&self) -> u64 { + 32 + self.records.iter().map(|(_, r)| r.counted_bytes()).sum::() + } } impl StageSink { /// A sink holding no base, with the bound, cost limit, budget and schedule every /// chain it creates uses. pub fn new(cfg: ChainConfig) -> Self { - StageSink { cfg, chains: BTreeMap::new(), pending: vec![], records: BTreeMap::new(), attested: Default::default() } + StageSink { cfg, chains: BTreeMap::new(), pending: vec![], kept: BTreeMap::new(), kept_order: BTreeMap::new(), kept_bytes: 0, tick: 0 } } /// The chain of the base whose content hash is `base`. @@ -1108,8 +1145,9 @@ impl StageSink { } return vec![]; } - base.attested |= self.attested.remove(&hash); - let records = self.records.remove(&hash).unwrap_or_default(); + let kept = self.take_kept(&hash).unwrap_or_default(); + base.attested |= kept.attested; + let records = kept.records; let mut chain = StageChain::with_records(base, units, self.cfg.clone(), records); let hexed = hex(&hash); let (mine, rest): (Vec, Vec) = std::mem::take(&mut self.pending).into_iter().partition(|p| p.refines.base == hexed); @@ -1127,11 +1165,45 @@ impl StageSink { match self.chains.get_mut(base) { Some(c) => c.mark_attested(), None => { - self.attested.insert(*base); + let mut k = self.take_kept(base).unwrap_or_default(); + k.attested = true; + self.put_kept(*base, k); } } } + /// The bytes the sink keeps of the bases it does not hold (§4.4), within + /// `record_budget`. + pub fn kept_bytes(&self) -> u64 { + self.kept_bytes + } + + /// Remove what the sink keeps of `base`. + fn take_kept(&mut self, base: &[u8; 32]) -> Option { + let k = self.kept.remove(base)?; + self.kept_order.remove(&k.written); + self.kept_bytes -= k.counted_bytes(); + Some(k) + } + + /// Keep `k` for `base`, written now, unless it holds nothing; then forget the + /// bases written longest ago until what is kept is within the record budget (§4.4). + fn put_kept(&mut self, base: [u8; 32], mut k: Kept) { + if k.records.is_empty() && !k.attested { + return; + } + self.tick += 1; + k.written = self.tick; + self.kept_bytes += k.counted_bytes(); + self.kept_order.insert(k.written, base); + self.kept.insert(base, k); + while self.kept_bytes > self.cfg.record_budget { + let Some((_, oldest)) = self.kept_order.pop_first() else { break }; + let gone = self.kept.remove(&oldest).expect("kept"); + self.kept_bytes -= gone.counted_bytes(); + } + } + /// Drop the chain of a base the sink no longer presents (§4.4): every envelope held /// for its units is discarded (`base-absent`, one report each, in the order they /// passed step 6), and each unit's record is kept, so the base decoded again @@ -1140,10 +1212,7 @@ impl StageSink { pub fn remove_base(&mut self, base: &[u8; 32]) -> Option> { let chain = self.chains.remove(base)?; let reports = chain.layers.values().filter(|l| l.held > 0).map(|l| OfferReport::refuse(l.hash, StageRefusal::BaseAbsent, None)).collect(); - self.records.insert(*base, chain.records()); - if chain.base.attested { - self.attested.insert(*base); - } + self.put_kept(*base, Kept { records: chain.records(), attested: chain.base.attested, written: 0 }); Some(reports) } @@ -1151,8 +1220,7 @@ impl StageSink { /// attestation. Decoded again, the base starts with no history and unattested, as /// one never decoded. Whether there was anything to discard. pub fn forget_base(&mut self, base: &[u8; 32]) -> bool { - let r = self.records.remove(base).is_some(); - self.attested.remove(base) || r + self.take_kept(base).is_some() } /// Offer the envelope `bytes` at `now_ms` with the receipt `evidence` (§4.1). A diff --git a/wai/wai-rs/src/staged/tests.rs b/wai/wai-rs/src/staged/tests.rs index 1b87abd20..9e2baf05d 100644 --- a/wai/wai-rs/src/staged/tests.rs +++ b/wai/wai-rs/src/staged/tests.rs @@ -321,7 +321,7 @@ fn base4() -> (Vec, BaseInfo, Vec) { } fn cfg() -> ChainConfig { - ChainConfig { bound: HoldBound { max_envelopes: 8, max_age_ms: 1000 }, cost_limit: None, budget: u64::MAX, live: None } + ChainConfig { bound: HoldBound { max_envelopes: 8, max_age_ms: 1000 }, cost_limit: None, budget: u64::MAX, record_budget: u64::MAX, live: None } } /// A residual layer over `belows` (units `from..from+belows.len()`) toward frames of @@ -976,6 +976,62 @@ fn removed_base_keeps_its_record() { assert_eq!(codes(&sink.offer(&l1, 6, ReceiptEvidence::None).unwrap()[0]).len(), 4, "a frozen unit takes no layer it already holds"); } +/// What a sink keeps of the bases it does not hold stays within its record budget +/// (§4.4): a base with nothing recorded keeps nothing; past the budget the base dropped +/// longest ago is forgotten whole, and decoded again it starts with no history and +/// unattested, as after forget_base; an attestation mark is counted too. +#[test] +fn records_within_their_budget() { + let (_, info, units) = base4(); + let below: Vec> = (0..4).map(frame).collect(); + let base_i = |i: u8| { + let mut b = info.clone(); + b.content_hash[0] ^= i; + b + }; + // Nothing recorded: nothing kept. + let mut sink = StageSink::new(cfg()); + for i in 1..=20u8 { + let b = base_i(i); + sink.add_base(b.clone(), units.clone()); + sink.remove_base(&b.content_hash); + } + assert_eq!(sink.kept_bytes(), 0); + // One layer applied to every unit: 32 + 4·(8 + 32) bytes a base; room for two. + let one_base = 32 + 4 * (8 + 32); + let mut sink = StageSink::new(ChainConfig { record_budget: 2 * one_base, ..cfg() }); + for i in 1..=4u8 { + let b = base_i(i); + let h = b.content_hash; + sink.add_base(b, units.clone()); + assert_eq!(sink.offer(&layer(&h, one(&h), 1, None, &below, 50), 0, ReceiptEvidence::None).unwrap()[0].applied, vec![0, 1, 2, 3]); + sink.remove_base(&h); + assert!(sink.kept_bytes() <= 2 * one_base, "{}", sink.kept_bytes()); + } + assert_eq!(sink.kept_bytes(), 2 * one_base, "bases 3 and 4 kept"); + // Base 1 was forgotten: a layer other than its old producer applies. + let b1 = base_i(1); + let h1 = b1.content_hash; + sink.add_base(b1, units.clone()); + assert!(sink.chain(&h1).unwrap().units().iter().all(|u| u.producers().is_empty())); + assert_eq!(sink.offer(&layer(&h1, one(&h1), 1, None, &below, 51), 1, ReceiptEvidence::None).unwrap()[0].applied, vec![0, 1, 2, 3]); + // Base 4 kept its record: the same other layer conflicts. + let b4 = base_i(4); + let h4 = b4.content_hash; + sink.add_base(b4, units.clone()); + assert_eq!(codes(&sink.offer(&layer(&h4, one(&h4), 1, None, &below, 51), 2, ReceiptEvidence::None).unwrap()[0]).len(), 4); + // Attestation marks are kept within the budget too, and a forgotten one is gone. + let mut sink = StageSink::new(ChainConfig { record_budget: 3 * 32, ..cfg() }); + for i in 1..=5u8 { + sink.mark_attested(&base_i(i).content_hash); + assert!(sink.kept_bytes() <= 3 * 32); + } + sink.add_base(base_i(1), units.clone()); + assert!(!sink.chain(&base_i(1).content_hash).unwrap().base().attested, "marked longest ago: forgotten"); + sink.add_base(base_i(5), units.clone()); + assert!(sink.chain(&base_i(5).content_hash).unwrap().base().attested); +} + fn edit_refines(env: &[u8], r: Refines) -> Vec { let mut w = Wai::from_bytes(env).unwrap(); w.manifest.refines = Some(r.to_value()); diff --git a/wai/wai-rs/tests/staged_resources.rs b/wai/wai-rs/tests/staged_resources.rs index 886edb708..b89c75913 100644 --- a/wai/wai-rs/tests/staged_resources.rs +++ b/wai/wai-rs/tests/staged_resources.rs @@ -91,7 +91,7 @@ fn hex(h: &[u8; 32]) -> String { } fn cfg(max_envelopes: usize, budget: u64) -> ChainConfig { - ChainConfig { bound: HoldBound { max_envelopes, max_age_ms: 1000 }, cost_limit: None, budget, live: None } + ChainConfig { bound: HoldBound { max_envelopes, max_age_ms: 1000 }, cost_limit: None, budget, record_budget: u64::MAX, live: None } } /// A base of `n` frames of `side × side` RGB8, named by `hash`. @@ -330,6 +330,40 @@ fn staged_sink_resources() { let grew = CUR.load(Relaxed).saturating_sub(before); assert!(grew < layers[1].len() / 2, "the sink kept {grew} bytes after every held layer expired"); + // ── Records of dropped chains (§4.4) ── + // Dropped bases with nothing recorded leave nothing behind; with a layer applied to + // every unit, what the sink keeps stays within its record budget, and the heap it + // holds for them within a small multiple of it. + let units: Vec = base(4096, 1, [0; 32]).1; + let mut sink = StageSink::new(cfg(16, u64::MAX)); + let before = CUR.load(Relaxed); + for i in 0..20u8 { + let mut info = base(1, 1, [0x40 + i; 32]).0; + info.content_hash = [0x40 + i; 32]; + sink.add_base(info, units.clone()); + sink.remove_base(&[0x40 + i; 32]); + } + let grew = CUR.load(Relaxed).saturating_sub(before); + println!("records: 20 dropped bases of 4096 units with nothing recorded keep {} bytes; the sink grew {grew} bytes", sink.kept_bytes()); + assert_eq!(sink.kept_bytes(), 0); + assert!(grew < 16 << 10, "the sink kept {grew} bytes for bases with nothing recorded"); + let record_budget = 1 << 20; + let mut sink = StageSink::new(ChainConfig { record_budget, ..cfg(16, u64::MAX) }); + let before = CUR.load(Relaxed); + for i in 0..20u8 { + let h = [0x60 + i; 32]; + let mut info = base(1, 1, h).0; + info.content_hash = h; + sink.add_base(info, units.clone()); + let r = sink.offer(&layer(&h, Prev::One(hex(&h)), 1, &units, 1), 0, ReceiptEvidence::None).unwrap(); + assert_eq!(r[0].applied.len(), 4096); + sink.remove_base(&h); + assert!(sink.kept_bytes() <= record_budget); + } + let grew = CUR.load(Relaxed).saturating_sub(before); + println!("records: 20 dropped bases of 4096 refined units keep {} counted bytes within {record_budget}; the sink grew {grew} bytes", sink.kept_bytes()); + assert!(grew as u64 <= 4 * record_budget, "the sink holds {grew} bytes for a record budget of {record_budget}"); + // ── The parse holds no more than the declared working bytes beyond the payload ── // One table per unit, 65536 units of one attribute each. let n = 65536u32; -- 2.54.0 (Apple Git-157) From 5f991e87e834161d94322b8c4906cc60c78e1d7f Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Mon, 5 Oct 2026 00:50:50 -0400 Subject: [PATCH 36/36] wai: the record budget is counted alike by every sink MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit staged-delivery §4.4: the counted bytes are the interoperable measure, so two sinks with the same record budget keep and forget the same bases; what an implementation allocates per counted byte is its own, stated beside its budget. The reference states its ratio (at most four allocated bytes per counted byte), which tests/staged_resources.rs asserts. Co-Authored-By: Claude Opus 5.5 --- wai/extensions/staged-delivery.md | 5 +++++ wai/wai-rs/src/staged/mod.rs | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/wai/extensions/staged-delivery.md b/wai/extensions/staged-delivery.md index 0002dde25..3b5feda14 100644 --- a/wai/extensions/staged-delivery.md +++ b/wai/extensions/staged-delivery.md @@ -215,6 +215,11 @@ bases, the one whose record or attestation it wrote longest ago first, until it within it. A forgotten base is as one the sink never decoded and never attested (§14). +These counts are the interoperable measure: every conforming sink counts the same +bytes, so two sinks with the same record budget keep and forget the same bases. What an +implementation allocates for each counted byte is its own, and it states that ratio +beside its record budget. + ### 4.5 Chain budget A sink **MUST** declare a budget, in bytes, for the chain of each base it decodes. The diff --git a/wai/wai-rs/src/staged/mod.rs b/wai/wai-rs/src/staged/mod.rs index a1fbc97b7..d91fc1857 100644 --- a/wai/wai-rs/src/staged/mod.rs +++ b/wai/wai-rs/src/staged/mod.rs @@ -266,7 +266,9 @@ pub struct ChainConfig { pub budget: u64, /// The most bytes a sink keeps of the bases it does not hold (§4.4): their unit /// records and their attestation ([`StageSink::kept_bytes`]). Past it, the bases - /// dropped or marked longest ago are forgotten, whole. + /// dropped or marked longest ago are forgotten, whole. The budget is in §4.4's + /// counted bytes, which every sink counts alike; this implementation allocates at + /// most four bytes for each counted byte (`tests/staged_resources.rs` checks it). pub record_budget: u64, pub live: Option, } -- 2.54.0 (Apple Git-157)