From c9be2adb57acba97e934f5264b3eb19247febe6a Mon Sep 17 00:00:00 2001 From: David Charlot Date: Tue, 25 Aug 2026 22:48:10 -0400 Subject: [PATCH 1/5] wai: prove integer VIDEO decode byte-identical across architectures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cross-arch byte-equality matrix covered the still-image and synthesis paths — rANS, int_synth, the JPEG-AI entropy stage — but not the inter-frame video decoder. `wai.video.int_motion` was verified only by `wai-rs/tests/int_video.rs`, which is `#![cfg(feature = "video_int")]`, and no CI job passes `--features`. Measured, not assumed: `cargo test --test int_video` reports 0 tests without the feature and 1 with it. So the temporal leg of the determinism contract was asserted on four platforms and checked on none. Video is the strictest leg of that contract, not merely another one. Still-image decode is a single shot: one divergent byte is one wrong byte. Inter-frame decode is a feedback loop — each P-frame predicts from the previously *reconstructed* frame — so one architecture rounding a half-pel sample differently produces a frame that then predicts the next frame, and the error compounds for the rest of the clip. That is why per-frame hashes are asserted and not only the clip hash: they say where divergence began, which a single terminal hash cannot. - `int_video.rs` is `#[path]`-included into `byte-exact-conformance`. It needs only `serde_json` and `crate::rans`, both already present, so all four legs still run in about a minute with no codec libraries and no ONNX runtime. - `rans` is re-exported rather than included a second time. Two `#[path]` includes would compile two structurally identical but distinct types, and one-source-of-truth is the whole reason this crate exists. - Two clips: the synthetic 5x48x48 (whole-pel) and the real-footage 8x96x96, which the encoder chose half-pel for (27_960 bytes against 38_232). The real clip is the load-bearing one — half-pel bilinear rounding is the arithmetic most at risk of differing between architectures — and the test asserts the vector still *prefers* half-pel, so it cannot silently degrade into covering only the whole-pel path. The pair also separates the failure modes: a synthetic failure implicates the entropy/residual stage, a real-footage-only failure implicates the predictor. Also fixes a pre-existing hole in the workflow's own path triggers. The crate compiles five sources; only one of them (`neural/jpegai_mans.rs`) could fire this workflow. `rans.rs`, `int_entropy.rs`, crate-root `int_synth.rs` and crate-root `jpegai_mans.rs` were unwatched — the trigger named `neural/int_synth.rs`, which is a different file from the one actually included. Any of them could have broken cross-arch equality and merged green. All five are now watched, plus `int_video.rs` and its vectors. Not covered, and deliberately not papered over: `video_nk` (`wai.video.int_hyper`, the neural-keyframe variant). Its I-frame comes from `int_hyper_synth::decode_conditional`, so it cannot be driven by `decode()` — an earlier draft tried and got a slice overrun out of the rANS reader. Covering it means including `int_hyper_synth.rs` too; until that lands, the neural-keyframe leg is proven only where someone runs the feature locally. The source comment says so rather than leaving a reader to assume full coverage. Verified: 17 tests pass in the conformance crate (15 existing + 2 new), both clips reproducing their committed golden hashes; workflow YAML parses with all four matrix legs intact. --- .github/workflows/wai-byte-equality.yml | 22 ++++ .../src/int_video_golden.rs | 119 ++++++++++++++++++ wai/byte-exact-conformance/src/lib.rs | 19 +++ 3 files changed, 160 insertions(+) create mode 100644 wai/byte-exact-conformance/src/int_video_golden.rs diff --git a/.github/workflows/wai-byte-equality.yml b/.github/workflows/wai-byte-equality.yml index d1a403c87..e193332a4 100644 --- a/.github/workflows/wai-byte-equality.yml +++ b/.github/workflows/wai-byte-equality.yml @@ -8,6 +8,12 @@ name: wai cross-architecture byte-equality # int_synth output hash produced on the reference machine (macOS arm64). One # diverging byte anywhere fails the suite. # +# Coverage is still-image AND temporal: the rANS coder, integer fixed-point +# synthesis, the JPEG-AI entropy stage, and the inter-frame video decoder +# (`wai.video.int_motion`). 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. +# # The conformance crate (`wai/byte-exact-conformance`) #[path]-includes the # pure-integer sources from wai-rs — no system codec libraries, no ONNX runtime # — so every leg runs in about a minute. @@ -18,6 +24,14 @@ on: - 'wai/byte-exact-conformance/**' - 'wai/wai-rs/src/neural/int_synth.rs' - 'wai/wai-rs/src/neural/jpegai_mans.rs' + # Every source the crate #[path]-includes must be able to fire this + # workflow, or a change can break cross-arch equality and merge green. + - 'wai/wai-rs/src/int_synth.rs' + - 'wai/wai-rs/src/int_entropy.rs' + - 'wai/wai-rs/src/jpegai_mans.rs' + - 'wai/wai-rs/src/rans.rs' + - 'wai/wai-rs/src/int_video.rs' + - 'wai/wai-rs/tests/vectors/int_video/**' - '.github/workflows/wai-byte-equality.yml' push: branches: @@ -26,6 +40,14 @@ on: - 'wai/byte-exact-conformance/**' - 'wai/wai-rs/src/neural/int_synth.rs' - 'wai/wai-rs/src/neural/jpegai_mans.rs' + # Every source the crate #[path]-includes must be able to fire this + # workflow, or a change can break cross-arch equality and merge green. + - 'wai/wai-rs/src/int_synth.rs' + - 'wai/wai-rs/src/int_entropy.rs' + - 'wai/wai-rs/src/jpegai_mans.rs' + - 'wai/wai-rs/src/rans.rs' + - 'wai/wai-rs/src/int_video.rs' + - 'wai/wai-rs/tests/vectors/int_video/**' - '.github/workflows/wai-byte-equality.yml' jobs: diff --git a/wai/byte-exact-conformance/src/int_video_golden.rs b/wai/byte-exact-conformance/src/int_video_golden.rs new file mode 100644 index 000000000..ec5272f67 --- /dev/null +++ b/wai/byte-exact-conformance/src/int_video_golden.rs @@ -0,0 +1,119 @@ +//! Cross-architecture byte-equality for `wai.video.int_motion` — the temporal +//! leg of the determinism contract. +//! +//! The still-image goldens next door prove a *single* decode is portable. Video +//! is a strictly harder claim, because inter-frame decode is a feedback loop: +//! every P-frame is predicted from the previously **reconstructed** frame, so an +//! architecture that rounds one half-pel sample differently does not produce one +//! wrong byte — it produces a frame that is then used to predict the next frame, +//! and the error compounds for the rest of the clip. Per-frame hashes are +//! asserted rather than only the clip hash for exactly that reason: they say +//! *where* divergence began, which a single terminal hash cannot. +//! +//! The vectors are the ones `wai-rs/tests/int_video.rs` already commits. That +//! test only ever ran where someone passed `--features video_int`; these run on +//! every leg of the matrix, which is the difference between a determinism claim +//! and a determinism proof. + +#![cfg(test)] + +use std::path::PathBuf; + +use serde_json::Value; + +use crate::int_video::{decode, fnv1a}; + +fn vectors() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../wai-rs/tests/vectors/int_video") +} + +fn jload(name: &str) -> Value { + let path = vectors().join(name); + let bytes = std::fs::read(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display())); + serde_json::from_slice(&bytes).unwrap_or_else(|e| panic!("parse {}: {e}", path.display())) +} + +/// The 8 raw bytes behind a 16-hex string. The clip hash is taken over the +/// concatenated frame-hash *bytes*, not their hex text. +fn hex16(s: &str) -> [u8; 8] { + let mut o = [0u8; 8]; + for (i, b) in o.iter_mut().enumerate() { + *b = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).expect("hex"); + } + o +} + +/// Decode a committed clip and assert every frame hash and the clip hash. +/// Returns the clip hash so callers can pin it verbatim. +fn assert_clip(bitstream: &str, conf: &str) -> String { + let bs = jload(bitstream); + let conf = jload(conf); + let clip = decode(&bs).expect("decode must succeed on every architecture"); + + let h = conf["h"].as_u64().expect("h") as usize; + let w = conf["w"].as_u64().expect("w") as usize; + let want = conf["frame_hashes"].as_array().expect("frame_hashes"); + + assert_eq!((clip.h, clip.w), (h, w), "frame geometry"); + assert_eq!(clip.frames.len(), want.len(), "frame count"); + + let mut all = Vec::with_capacity(want.len() * 8); + for (i, frame) in clip.frames.iter().enumerate() { + assert_eq!(frame.len(), h * w * 3, "frame {i} size"); + let got = format!("{:016x}", fnv1a(frame)); + assert_eq!( + got, + want[i].as_str().expect("frame hash"), + "frame {i} diverged — inter-frame decode is a feedback loop, so this \ + is also the last frame that can be trusted" + ); + all.extend_from_slice(&hex16(&got)); + } + let clip_hash = format!("{:016x}", fnv1a(&all)); + assert_eq!(clip_hash, conf["clip_hash"].as_str().expect("clip_hash"), "clip hash"); + clip_hash +} + +/// The synthetic clip: 5 frames of 48×48 RGB, whole-pel motion. +#[test] +fn int_video_synthetic_clip_is_byte_identical() { + let clip = assert_clip("int_video_bitstream.json", "int_video.json"); + assert_eq!(clip, "27573cdba1ad6628"); + println!("int_video (synthetic, 5x48x48): clip {clip}"); +} + +/// Real footage, 8 frames of 96×96. This is the load-bearing case: the encoder +/// chose half-pel motion for it (27_960 bytes against 38_232 whole-pel), so the +/// bilinear predictor — the integer rounding most at risk of differing between +/// architectures — is actually exercised rather than merely present. +#[test] +fn int_video_real_footage_exercises_the_half_pel_predictor() { + let conf = jload("video_real.json"); + let half = conf["half_pel_bytes"].as_u64().expect("half_pel_bytes"); + let whole = conf["whole_pel_bytes"].as_u64().expect("whole_pel_bytes"); + assert!( + half < whole, + "vector no longer prefers half-pel ({half} >= {whole}); this test would \ + still pass while silently covering only the whole-pel path" + ); + + let clip = assert_clip("video_real_bitstream.json", "video_real.json"); + assert_eq!(clip, "1ec1f0fa7da0d70f"); + println!("int_video (real footage, 8x96x96, half-pel): clip {clip}"); +} + +// NOT covered here: `video_nk` (`wai.video.int_hyper`, the NEURAL-keyframe +// variant). Its I-frame is produced by `int_hyper_synth::decode_conditional` +// and only its P-frames go through `int_video::decode_pframes`, so it cannot be +// driven by `decode()` — an earlier draft of this file tried and got a slice +// overrun out of the rANS reader, which is what feeding one codec another +// codec'"'"'s bitstream looks like. Covering it means `#[path]`-including +// `int_hyper_synth.rs` as well; that is a real follow-on, not a line to add +// here, and until it lands the neural-keyframe leg of the temporal contract +// remains proven only where someone runs `--features neural_int,video_int` +// locally. +// +// The synthetic/real pair above already separates the two failure modes this +// file needs to distinguish: the synthetic clip is whole-pel only, so a failure +// there implicates the entropy or residual stage, while a failure confined to +// the real-footage clip implicates the half-pel predictor. diff --git a/wai/byte-exact-conformance/src/lib.rs b/wai/byte-exact-conformance/src/lib.rs index 7baa9886e..27b45ca2b 100644 --- a/wai/byte-exact-conformance/src/lib.rs +++ b/wai/byte-exact-conformance/src/lib.rs @@ -23,5 +23,24 @@ pub mod int_entropy; #[path = "../../wai-rs/src/jpegai_mans.rs"] pub mod jpegai_mans_exact; +// `int_video.rs` names the rANS decoder as `crate::rans` (its path in wai-rs, +// where rans is a crate-root module). Here rans is included under `neural`, so +// re-export rather than `#[path]`-include a second copy: two includes would +// compile two structurally identical but distinct types, and the whole point of +// this crate is that there is one source of truth per decode path. +pub use neural::rans; + +// The deterministic integer INTER-FRAME video decoder (`wai.video.int_motion`). +// Included so the cross-arch matrix covers temporal decode, not only the +// still-image and synthesis paths. This one matters disproportionately: its +// half-pel predictor is the only place in the integer contract where a +// *rounding* decision (`(a + b + 1) >> 1` over clamped neighbours) sits inside a +// feedback loop — each P-frame predicts from the previously reconstructed +// frame, so a single divergent byte on one architecture does not stay one byte. +// It propagates through every subsequent frame. +#[path = "../../wai-rs/src/int_video.rs"] +pub mod int_video; + mod golden; +mod int_video_golden; mod jpegai_golden; -- 2.54.0 (Apple Git-157) From 256e5dd118e5ea4d9e9e0f7780a0a4612492e8be Mon Sep 17 00:00:00 2001 From: David Charlot Date: Wed, 26 Aug 2026 00:02:07 -0400 Subject: [PATCH 2/5] =?UTF-8?q?wai:=20meter=20the=20integer=20video=20clas?= =?UTF-8?q?s=20=E2=80=94=20the=20last=20cargo=20class=20with=20no=20meter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Haptics, 4D splats, scores and film are metered by `wai_meter`; worlds, feeds and avatars by `wai_world_meter`; quantum circuits by `wai_quantum_meter`. Video had none, so `wai.video.int_motion` could be decoded but never accounted. This adds the JWP receipt profile and the meter binary. `VideoReceipt` follows `QuantumReceipt` exactly — codestream hash as identity, per-frame decoded-pixel hashes as the group's objects, Merkle root over them, exact recomputable work, measured joules, `parent_receipt_hash` chaining, one Ed25519 signature over the lot. What makes this profile stronger than its siblings is the preceding commit's matrix. In every other class the reconstruction is deterministic *by construction* and taken on trust; here it is deterministic *and proven on real silicon* across four architectures. So a verifier holding only the codestream can re-derive `clip_hash`, every frame hash and `work_pixel_ops` themselves — `verify_reconstruction` does exactly that — and the receipt collapses to a single field that rests on trusting the signer: `joules_micro`. The docs say which fields are verifiable and which is attested, in those words, so a reader cannot conflate them. Deliberate choices: - The receipt lives in `video_receipt.rs`, NOT in `int_video.rs`. That file is `#[path]`-included by `byte-exact-conformance`, whose three-dependency set is what lets the cross-arch matrix run everywhere in about a minute; pulling blake3 and ed25519 into it would break the very proof this profile leans on. `video_receipt = ["video_int", "world"]` mirrors `quantum_receipt`. - Work counts decoded *subpixels*, not motion-vector or residual operations. Only the output is fixed by the format — a future encoder may spend fewer residual ops on the same clip — so counting output keeps the figure comparable across encoders, and recomputation stops it being inflated to flatter an energy number. - The frame index is inside each leaf hash, not merely beside it. Inter-frame decode is order-dependent, so a receipt whose frames could be permuted without changing a leaf would vouch for a clip nobody can play. - `uj_per_kilopixel_op()` returns `None` when unmetered, never `0`. Zero would read as "this decode was free", a stronger and false claim than "nobody measured". `to_json` emits `null` to match, and without `macmon` the meter emits an UNMETERED receipt rather than an estimate. Measured on this machine (release build, real footage vector, 8 frames 96x96): idle 20.45 W, busy 54.89 W, marginal 34.45 W over 11,548 decodes in 3.54 s → 10,565 µJ per decode, 47.77 µJ per kilo-subpixel. A debug build reports 4.9x higher, which is why the meter is documented as a release-build tool. Verified: 6 receipt tests (round-trip on both committed clips, chaining, and tamper cases for every checkable field — flipped frame hash, permuted frames, inflated work, restated geometry, wrong codestream, restated energy); the byte-exact conformance crate still passes 17/17 unchanged; no new clippy warnings; default build unaffected since the feature is off by default. --- wai/wai-rs/Cargo.toml | 12 + wai/wai-rs/src/bin/wai_video_meter.rs | 174 +++++++++++++++ wai/wai-rs/src/lib.rs | 3 + wai/wai-rs/src/video_receipt.rs | 308 ++++++++++++++++++++++++++ wai/wai-rs/tests/video_receipt.rs | 145 ++++++++++++ 5 files changed, 642 insertions(+) create mode 100644 wai/wai-rs/src/bin/wai_video_meter.rs create mode 100644 wai/wai-rs/src/video_receipt.rs create mode 100644 wai/wai-rs/tests/video_receipt.rs diff --git a/wai/wai-rs/Cargo.toml b/wai/wai-rs/Cargo.toml index e34d3c494..84b68d0eb 100644 --- a/wai/wai-rs/Cargo.toml +++ b/wai/wai-rs/Cargo.toml @@ -155,6 +155,13 @@ quantum = ["dep:blake3"] # from the sink's meter), both bound to a re-simulatable statevector hash. Reuses # the JWP Merkle + Ed25519 from `world` — reuse, not reinvention. quantum_receipt = ["quantum", "world"] +# The JWP receipt profile for the integer inter-frame video codec. Follows the +# same shape as `quantum_receipt`: the class feature plus `world`, which is what +# supplies blake3, ed25519 and the shared `merkle_root`. Kept OUT of `video_int` +# on purpose — `int_video.rs` is #[path]-included by the byte-exact conformance +# crate, and that crate must keep its three-dependency set to stay runnable on +# every leg of the cross-arch matrix. +video_receipt = ["video_int", "world"] # Balanced-ternary matrix-product-state transform (`wai.ternary.tt`, # extensions/ternary-tt): quantum-INSPIRED (tensor-network) compression whose # novel delta is an INTEGER core scale — the whole decode is pure i64/i128, so a @@ -287,6 +294,11 @@ name = "wai_quantum_meter" path = "src/bin/wai_quantum_meter.rs" required-features = ["quantum_receipt"] +[[bin]] +name = "wai_video_meter" +path = "src/bin/wai_video_meter.rs" +required-features = ["video_receipt"] + [[bin]] name = "wai_meter" path = "src/bin/wai_meter.rs" diff --git a/wai/wai-rs/src/bin/wai_video_meter.rs b/wai/wai-rs/src/bin/wai_video_meter.rs new file mode 100644 index 000000000..37ae5811a --- /dev/null +++ b/wai/wai-rs/src/bin/wai_video_meter.rs @@ -0,0 +1,174 @@ +//! `wai_video_meter` — the native sink-metering path for `wai.video.int_motion`, +//! the integer inter-frame video codec. +//! +//! Measures the **real CPU energy** a sink spends decoding an integer video +//! codestream to frames, with no elevated privileges. On Apple Silicon the SoC +//! energy counters are readable through IOReport — the same source `macmon`/ +//! `asitop` use — so this meter samples power via `macmon`, brackets a tight +//! decode burst against an idle baseline, and reports the **marginal** CPU +//! energy attributable to the decode. +//! +//! wai_video_meter [receipt_out.json] +//! +//! Video was the last cargo class with no meter: haptics, 4D splats, scores and +//! film are metered by `wai_meter`, worlds/feeds/avatars by `wai_world_meter`, +//! quantum circuits by `wai_quantum_meter`. It also has the strongest claim of +//! any of them, because its decode is not merely deterministic by construction +//! but **proven byte-identical on four architectures** by the cross-arch +//! matrix. So of everything the emitted receipt says, only `joules_micro` rests +//! on trusting this machine — the result and the work can be re-derived from the +//! codestream by anyone, anywhere. +//! +//! This is a *measurement* with stated bounds, exactly as the sibling meters: +//! IOReport CPU power is SoC-wide, so the meter reports the DELTA over an +//! otherwise-idle machine (busy − idle mean power × wall-time ÷ decodes). Run it +//! on a quiet box for a clean number; it never fabricates one. Without `macmon` +//! it emits an UNMETERED receipt (`joules_micro = 0`) rather than an estimate, +//! and that receipt reports no rate at all — an absent measurement must never be +//! readable as "this decode was free". + +use std::process::Command; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::Arc; +use std::time::Instant; + +use ed25519_dalek::SigningKey; +use wai::int_video::decode; +use wai::video_receipt::{work_pixel_ops, VideoReceipt}; + +fn main() { + let args: Vec = std::env::args().collect(); + let Some(path) = args.get(1) else { + eprintln!("usage: wai_video_meter [receipt_out.json]"); + std::process::exit(2); + }; + + let bytes = std::fs::read(path).expect("read bitstream"); + let bs: serde_json::Value = serde_json::from_slice(&bytes).expect("parse bitstream JSON"); + // A decode that does not even run is not meterable. + let clip = decode(&bs).expect("codestream decodes"); + let (n, h, w) = (clip.frames.len() as u64, clip.h as u64, clip.w as u64); + let work = work_pixel_ops(n, h, w); + + eprintln!( + "metering {path} ({n} frames {h}x{w} RGB, work {work} subpixel-ops), \ + no sudo — IOReport via macmon" + ); + + // Phase A: idle baseline (no decode running). + eprintln!(" idle baseline …"); + let idle = sample_cpu_power(10); + let metered = idle.len() >= 3; + if !metered { + eprintln!( + "macmon unavailable or too few samples; emitting an UNMETERED receipt \ + (joules_micro = 0). Install `brew install macmon` (the non-sudo \ + IOReport reader) for a measured figure." + ); + } + + let joules_micro = if metered { + // Phase B: saturate several cores with real decodes while sampling. + // Many threads lift the marginal draw above the box's idle fluctuation; + // energy per decode is total measured energy ÷ total decodes regardless + // of how many cores ran them. + let workers = std::thread::available_parallelism() + .map(|n| n.get().saturating_sub(1).clamp(1, 10)) + .unwrap_or(6); + eprintln!(" decode burst on {workers} threads …"); + let stop = Arc::new(AtomicBool::new(false)); + let iters = Arc::new(AtomicU64::new(0)); + let handles: Vec<_> = (0..workers) + .map(|_| { + let (stop, iters, b) = (stop.clone(), iters.clone(), bs.clone()); + std::thread::spawn(move || { + // Increment per decode, not per fixed batch: a short clip + // runs many times and a long one only a few, and `runs` must + // track whichever actually happened. + while !stop.load(Ordering::Relaxed) { + let _ = decode(&b); + iters.fetch_add(1, Ordering::Relaxed); + } + }) + }) + .collect(); + let i0 = iters.load(Ordering::Relaxed); + let t0 = Instant::now(); + let busy = sample_cpu_power(14); + let window = t0.elapsed().as_secs_f64(); + let i1 = iters.load(Ordering::Relaxed); + stop.store(true, Ordering::Relaxed); + for hd in handles { + hd.join().ok(); + } + + let runs = (i1 - i0).max(1); + let mean = |v: &[f64]| v.iter().sum::() / v.len() as f64; + let idle_w = mean(&idle); + let busy_w = mean(&busy); + let marginal_w = (busy_w - idle_w).max(0.0); + let energy_j = marginal_w * window; + let per_run_uj = energy_j / runs as f64 * 1e6; + + println!("== wai_video_meter — measured CPU energy (IOReport via macmon, no sudo) =="); + println!("decodes : {runs} over {window:.2}s"); + println!("cpu power idle : {idle_w:.2} W ({} samples)", idle.len()); + println!("cpu power busy : {busy_w:.2} W ({} samples)", busy.len()); + println!("marginal (busy-idle): {marginal_w:.2} W"); + println!("→ per decode : {per_run_uj:.1} µJ ({n} frames {h}x{w}, work {work})"); + if work > 0 { + println!("→ per kilo-subpixel : {:.4} µJ", per_run_uj * 1000.0 / work as f64); + } + let uj = per_run_uj.round() as u64; + println!("joules_micro (receipt, MEASURED): {uj}"); + uj + } else { + 0 + }; + + // Optional: emit a receipt carrying the measured (or unmetered) energy. + if let Some(out) = args.get(2) { + // A native sink signs with its own identity key; this metering tool + // uses a fixed, clearly-labeled demo signer. + let signer = SigningKey::from_bytes(&[0x76u8; 32]); // 'v' for video-meter + let receipt = VideoReceipt::seal_decode( + &signer, + "did:web:wai.transaction.science#wai_video_meter", + &bytes, + &bs, + joules_micro, + None, + ) + .expect("seal receipt"); + assert!(receipt.verify(), "emitted receipt must verify"); + assert!( + receipt.verify_reconstruction(&bytes, &bs), + "emitted receipt must re-decode to the same frames" + ); + std::fs::write(out, receipt.to_json()).expect("write receipt"); + let tag = if metered { "MEASURED" } else { "UNMETERED" }; + println!("receipt : {out} (verified, joules_micro {tag}, work {work})"); + } +} + +/// One IOReport sample burst via `macmon pipe`, returning the CPU-power readings +/// (W). Same non-sudo reader and parse as the sibling meters. +fn sample_cpu_power(n: u32) -> Vec { + let out = Command::new("macmon") + .args(["pipe", "-s", &n.to_string(), "-i", "200"]) + .output(); + let Ok(out) = out else { return vec![] }; + String::from_utf8_lossy(&out.stdout) + .lines() + .filter_map(cpu_power_w) + .collect() +} + +/// Pull `"cpu_power": ` out of one macmon JSON line. +fn cpu_power_w(line: &str) -> Option { + let key = "\"cpu_power\":"; + let i = line.find(key)? + key.len(); + let rest = line[i..].trim_start(); + let end = rest.find([',', '}']).unwrap_or(rest.len()); + rest[..end].trim().parse().ok() +} diff --git a/wai/wai-rs/src/lib.rs b/wai/wai-rs/src/lib.rs index f20c4c86c..b3ed5b32f 100644 --- a/wai/wai-rs/src/lib.rs +++ b/wai/wai-rs/src/lib.rs @@ -230,6 +230,9 @@ pub mod quantum; #[cfg(feature = "quantum_receipt")] pub mod quantum_receipt; +#[cfg(feature = "video_receipt")] +pub mod video_receipt; + // Balanced-ternary matrix-product-state transform (extensions/ternary-tt, // `wai.ternary.tt`): quantum-INSPIRED (tensor-network) compression. The ternary // weight fabric (BitNet-b1.58, 5 trits/byte) + MPS are ported, not reinvented; diff --git a/wai/wai-rs/src/video_receipt.rs b/wai/wai-rs/src/video_receipt.rs new file mode 100644 index 000000000..7d0be214e --- /dev/null +++ b/wai/wai-rs/src/video_receipt.rs @@ -0,0 +1,308 @@ +//! Video receipt — a JWP profile for `wai.video.int_motion`, the integer +//! inter-frame codec. The **codestream hash** is the identity; the group's +//! objects are the per-frame decoded-pixel hashes; one Ed25519 signature covers +//! codestream hash + frame hashes + exact reconstruction work + measured joules. +//! Chains via `parent_receipt_hash` exactly as the other classes — reuse, not +//! reinvention. +//! +//! **What makes this profile different from its siblings.** In every other +//! cargo class the reconstruction is deterministic *by construction* and taken +//! on trust. Here it is deterministic *and proven on real silicon*: the +//! cross-architecture byte-equality matrix decodes these same vectors on +//! linux-x86_64, linux-aarch64, macos-arm64 and windows-x86_64 and asserts +//! identical bytes. So a verifier holding only the codestream can independently +//! recompute both `clip_hash` and `work_pixel_ops` — see +//! [`VideoReceipt::verify_reconstruction`] — and every field except the energy +//! stops being a claim and becomes a checkable fact. +//! +//! That leaves exactly one attested-not-verifiable field, `joules_micro`, and +//! the receipt is shaped so a reader cannot mistake which is which: +//! +//! - `codestream_hash`, `clip_hash`, `frames`, `work_pixel_ops` — **verifiable** +//! by anyone with the bitstream, on any of the four proven architectures. +//! - `joules_micro` — **measured-attested**: the real marginal CPU energy the +//! metering sink spent, vouched for by the signature and by nothing else. A +//! different machine will legitimately measure a different number for a +//! byte-identical decode. +//! +//! This module deliberately lives outside `int_video.rs`. That file is +//! `#[path]`-included by `wai/byte-exact-conformance`, whose whole purpose is to +//! compile the decode path with no codec libraries, no ONNX and a three-crate +//! dependency set; pulling blake3 and ed25519 into it would break the matrix +//! that makes the paragraph above true. + +use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; +use serde_json::Value; + +use crate::int_video::{decode, DecodedClip}; +use crate::world::receipt::merkle_root; + +const DOMAIN_RECEIPT: &[u8] = b"wai:video-receipt\x01"; +const DOMAIN_RECEIPT_ID: &[u8] = b"wai:video-receipt-id\x01"; +const DOMAIN_FRAME: &[u8] = b"wai:video-rframe\x01"; +const DOMAIN_CLIP: &[u8] = b"wai:video-clip\x01"; + +/// `(frame_index, BLAKE3 over the frame's decoded RGB bytes)`. +/// +/// The index is inside the hash, not merely beside it: inter-frame decode is +/// order-dependent, so a receipt whose frames could be permuted without +/// changing a leaf would vouch for a clip nobody can play. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FrameHash { + pub index: u64, + pub hash: [u8; 32], +} + +/// The exact, portable reconstruction work: one op per decoded subpixel. +/// +/// Verified by recomputation from `(frames, h, w)`, so it cannot be inflated to +/// make an energy figure look efficient. It counts *output* subpixels rather +/// than motion-vector or residual operations because only the output is fixed +/// by the format — a future encoder may spend fewer residual ops on the same +/// clip, and the receipt should stay comparable across encoders. +pub fn work_pixel_ops(n_frames: u64, h: u64, w: u64) -> u64 { + n_frames.saturating_mul(h).saturating_mul(w).saturating_mul(3) +} + +/// BLAKE3 over one frame's decoded RGB bytes, bound to its index. +pub fn frame_hash(index: u64, rgb: &[u8]) -> FrameHash { + let mut h = blake3::Hasher::new(); + h.update(DOMAIN_FRAME); + h.update(&index.to_le_bytes()); + h.update(rgb); + FrameHash { index, hash: *h.finalize().as_bytes() } +} + +/// The portable whole-clip commitment: geometry, then every frame hash in order. +pub fn clip_hash(h: u32, w: u32, frames: &[FrameHash]) -> [u8; 32] { + let mut hs = blake3::Hasher::new(); + hs.update(DOMAIN_CLIP); + hs.update(&h.to_le_bytes()); + hs.update(&w.to_le_bytes()); + hs.update(&(frames.len() as u64).to_le_bytes()); + for f in frames { + hs.update(&f.index.to_le_bytes()); + hs.update(&f.hash); + } + *hs.finalize().as_bytes() +} + +#[derive(Clone, Debug, PartialEq)] +pub struct VideoReceipt { + /// Identity: BLAKE3 over the canonical codestream bytes as they were read. + pub codestream_hash: [u8; 32], + /// The reconstruction result: the portable whole-clip commitment. + pub clip_hash: [u8; 32], + pub h: u32, + pub w: u32, + /// Per-frame decoded-pixel hashes, in decode order. + pub frames: Vec, + /// Merkle root over the frame hashes — JWP's exact domains, reused. + pub merkle_root: [u8; 32], + /// Exact, portable reconstruction work (see [`work_pixel_ops`]). + /// Verified by recomputation. + pub work_pixel_ops: u64, + /// Measured microjoules the sink spent reconstructing (`0` = unmetered). + /// Attested by the signature, not independently verifiable. + pub joules_micro: u64, + pub parent_receipt_hash: Option<[u8; 32]>, + pub signer_pubkey: [u8; 32], + pub signer_id: String, + pub sig: [u8; 64], +} + +impl VideoReceipt { + fn signing_payload(&self) -> Vec { + let mut o = DOMAIN_RECEIPT.to_vec(); + o.extend_from_slice(&self.codestream_hash); + o.extend_from_slice(&self.clip_hash); + o.extend_from_slice(&self.h.to_be_bytes()); + o.extend_from_slice(&self.w.to_be_bytes()); + o.extend_from_slice(&self.merkle_root); + o.extend_from_slice(&(self.frames.len() as u64).to_be_bytes()); + o.extend_from_slice(&self.work_pixel_ops.to_be_bytes()); + o.extend_from_slice(&self.joules_micro.to_be_bytes()); + match self.parent_receipt_hash { + Some(h) => { + o.push(1); + o.extend_from_slice(&h); + } + None => o.push(0), + } + for f in &self.frames { + o.extend_from_slice(&f.index.to_be_bytes()); + o.extend_from_slice(&f.hash); + } + o.extend_from_slice(&self.signer_pubkey); + let id = self.signer_id.as_bytes(); + o.extend_from_slice(&(id.len() as u32).to_be_bytes()); + o.extend_from_slice(id); + o + } + + /// Seal a receipt over an already-decoded clip. + /// + /// Prefer [`VideoReceipt::seal_decode`], which decodes the codestream here + /// and so cannot be handed a clip that is not what the codestream produces. + pub fn build( + signer: &SigningKey, + signer_id: impl Into, + codestream_hash: [u8; 32], + clip: &DecodedClip, + joules_micro: u64, + parent_receipt_hash: Option<[u8; 32]>, + ) -> VideoReceipt { + let frames: Vec = clip + .frames + .iter() + .enumerate() + .map(|(i, f)| frame_hash(i as u64, f)) + .collect(); + let (h, w) = (clip.h as u32, clip.w as u32); + let leaves: Vec<[u8; 32]> = frames.iter().map(|f| f.hash).collect(); + let mut r = VideoReceipt { + codestream_hash, + clip_hash: clip_hash(h, w, &frames), + h, + w, + merkle_root: merkle_root(&leaves), + work_pixel_ops: work_pixel_ops(frames.len() as u64, h as u64, w as u64), + frames, + joules_micro, + parent_receipt_hash, + signer_pubkey: signer.verifying_key().to_bytes(), + signer_id: signer_id.into(), + sig: [0u8; 64], + }; + r.sig = signer.sign(&r.signing_payload()).to_bytes(); + r + } + + /// Decode `codestream` and seal a receipt over what it actually produces. + /// + /// The identity is hashed from `raw` — the codestream bytes exactly as they + /// arrived — rather than from a re-serialisation of the parsed value, so the + /// hash identifies the artifact a verifier will be handed rather than this + /// process's idea of it. + pub fn seal_decode( + signer: &SigningKey, + signer_id: impl Into, + raw: &[u8], + codestream: &Value, + joules_micro: u64, + parent_receipt_hash: Option<[u8; 32]>, + ) -> Result { + let clip = decode(codestream)?; + let codestream_hash = *blake3::hash(raw).as_bytes(); + Ok(Self::build(signer, signer_id, codestream_hash, &clip, joules_micro, parent_receipt_hash)) + } + + /// Signature + internal consistency: the Merkle root, the clip commitment + /// and the work figure must all follow from the frame hashes carried here. + /// + /// This does **not** prove the frames are what the codestream decodes to — + /// only [`VideoReceipt::verify_reconstruction`] does that. + pub fn verify(&self) -> bool { + let leaves: Vec<[u8; 32]> = self.frames.iter().map(|f| f.hash).collect(); + if merkle_root(&leaves) != self.merkle_root { + return false; + } + if clip_hash(self.h, self.w, &self.frames) != self.clip_hash { + return false; + } + if work_pixel_ops(self.frames.len() as u64, self.h as u64, self.w as u64) + != self.work_pixel_ops + { + return false; + } + for (i, f) in self.frames.iter().enumerate() { + if f.index != i as u64 { + return false; + } + } + let Ok(k) = VerifyingKey::from_bytes(&self.signer_pubkey) else { return false }; + k.verify(&self.signing_payload(), &Signature::from_bytes(&self.sig)).is_ok() + } + + /// The independent check: re-decode the codestream and confirm this receipt + /// describes it — identity, geometry, every frame, the clip commitment and + /// the work figure. + /// + /// Byte-equality across architectures is what makes this meaningful rather + /// than circular: the caller need not trust the machine that sealed the + /// receipt, because the same bitstream reconstructs the same bytes on any of + /// the proven architectures. `joules_micro` is untouched here — it is not + /// reproducible and is not meant to be. + pub fn verify_reconstruction(&self, raw: &[u8], codestream: &Value) -> bool { + if *blake3::hash(raw).as_bytes() != self.codestream_hash { + return false; + } + let Ok(clip) = decode(codestream) else { return false }; + if clip.h as u32 != self.h || clip.w as u32 != self.w { + return false; + } + if clip.frames.len() != self.frames.len() { + return false; + } + for (i, f) in clip.frames.iter().enumerate() { + if frame_hash(i as u64, f) != self.frames[i] { + return false; + } + } + self.verify() + } + + pub fn receipt_hash(&self) -> [u8; 32] { + let mut h = blake3::Hasher::new(); + h.update(DOMAIN_RECEIPT_ID); + h.update(&self.signing_payload()); + h.update(&self.sig); + *h.finalize().as_bytes() + } + + /// Microjoules per thousand decoded subpixels — the comparable figure. + /// `None` when unmetered, so an absent measurement can never be read as + /// "zero energy" (SPEC: state what you did, never claim what you did not). + pub fn uj_per_kilopixel_op(&self) -> Option { + (self.joules_micro > 0 && self.work_pixel_ops > 0) + .then(|| self.joules_micro as f64 * 1000.0 / self.work_pixel_ops as f64) + } + + pub fn to_json(&self) -> String { + let hx = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::(); + let objs: Vec = + self.frames.iter().map(|f| format!("\"{}\"", hx(&f.hash))).collect(); + let parent = match self.parent_receipt_hash { + Some(h) => format!("\"{}\"", hx(&h)), + None => "null".to_string(), + }; + let rate = match self.uj_per_kilopixel_op() { + Some(r) => format!("{r:.6}"), + None => "null".to_string(), + }; + format!( + concat!( + "{{\"profile\":\"wai.video.int_motion/receipt\",", + "\"codestream_hash\":\"{}\",\"clip_hash\":\"{}\",", + "\"h\":{},\"w\":{},\"frames\":[{}],\"merkle_root\":\"{}\",", + "\"work_pixel_ops\":{},\"joules_micro\":{},\"uj_per_kilopixel_op\":{},", + "\"parent_receipt_hash\":{},\"signer_pubkey\":\"{}\",", + "\"signer_id\":\"{}\",\"sig\":\"{}\",\"receipt_hash\":\"{}\"}}" + ), + hx(&self.codestream_hash), + hx(&self.clip_hash), + self.h, + self.w, + objs.join(","), + hx(&self.merkle_root), + self.work_pixel_ops, + self.joules_micro, + rate, + parent, + hx(&self.signer_pubkey), + self.signer_id, + hx(&self.sig), + hx(&self.receipt_hash()) + ) + } +} diff --git a/wai/wai-rs/tests/video_receipt.rs b/wai/wai-rs/tests/video_receipt.rs new file mode 100644 index 000000000..6f99959f6 --- /dev/null +++ b/wai/wai-rs/tests/video_receipt.rs @@ -0,0 +1,145 @@ +//! `wai.video.int_motion` receipt — the metered leg of the integer video class. +//! +//! The point of these tests is the split the profile rests on: everything except +//! the energy is independently checkable from the codestream, and the energy is +//! attested by the signature alone. Each half is tested for what it actually +//! promises, and the tamper cases confirm the checkable half cannot be bent. +#![cfg(feature = "video_receipt")] + +use std::path::PathBuf; + +use ed25519_dalek::SigningKey; +use wai::video_receipt::{work_pixel_ops, VideoReceipt}; + +fn dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/vectors/int_video") +} +fn raw(name: &str) -> Vec { + std::fs::read(dir().join(name)).expect("read vector") +} +fn key(seed: u8) -> SigningKey { + SigningKey::from_bytes(&[seed; 32]) +} + +fn seal(name: &str, uj: u64) -> (VideoReceipt, Vec, serde_json::Value) { + let bytes = raw(name); + let bs: serde_json::Value = serde_json::from_slice(&bytes).expect("parse"); + let r = VideoReceipt::seal_decode(&key(7), "did:example:meter", &bytes, &bs, uj, None) + .expect("seal"); + (r, bytes, bs) +} + +#[test] +fn seals_and_verifies_against_the_committed_clip() { + let (r, bytes, bs) = seal("int_video_bitstream.json", 4_200); + + assert!(r.verify(), "signature + internal consistency"); + assert!(r.verify_reconstruction(&bytes, &bs), "re-decode must reproduce every frame"); + + // The work figure follows from the geometry and is recomputable, so it + // cannot be inflated to flatter an energy number. + assert_eq!(r.h, 48); + assert_eq!(r.w, 48); + assert_eq!(r.frames.len(), 5); + assert_eq!(r.work_pixel_ops, work_pixel_ops(5, 48, 48)); + assert_eq!(r.work_pixel_ops, 5 * 48 * 48 * 3); + + let rate = r.uj_per_kilopixel_op().expect("metered"); + assert!((rate - 4_200.0 * 1000.0 / 34_560.0).abs() < 1e-9); +} + +/// An unmetered receipt reports **no** rate. A `0` would read as "this decode +/// was free", which is a stronger and false claim than "nobody measured". +#[test] +fn unmetered_reports_no_rate_rather_than_zero() { + let (r, _, _) = seal("int_video_bitstream.json", 0); + assert!(r.verify()); + assert_eq!(r.joules_micro, 0); + assert_eq!(r.uj_per_kilopixel_op(), None); + assert!(r.to_json().contains("\"uj_per_kilopixel_op\":null")); +} + +/// Real footage, the half-pel path — the clip whose decode is most at risk of +/// differing between architectures, and therefore the one whose receipt is +/// worth the most. +#[test] +fn real_footage_receipt_round_trips() { + let (r, bytes, bs) = seal("video_real_bitstream.json", 91_000); + assert!(r.verify()); + assert!(r.verify_reconstruction(&bytes, &bs)); + assert_eq!((r.h, r.w, r.frames.len()), (96, 96, 8)); + assert_eq!(r.work_pixel_ops, 8 * 96 * 96 * 3); +} + +/// A receipt sealed over one clip must not verify against another. +#[test] +fn a_receipt_does_not_vouch_for_a_different_clip() { + let (r, _, _) = seal("int_video_bitstream.json", 10); + let other = raw("video_real_bitstream.json"); + let other_bs: serde_json::Value = serde_json::from_slice(&other).unwrap(); + assert!(!r.verify_reconstruction(&other, &other_bs)); +} + +/// Tampering with any checkable field must break the receipt. +#[test] +fn every_checkable_field_is_bound() { + let (base, bytes, bs) = seal("int_video_bitstream.json", 4_200); + + // A flipped frame hash breaks the Merkle root. + let mut t = base.clone(); + t.frames[2].hash[0] ^= 1; + assert!(!t.verify(), "flipped frame hash"); + + // Reordering frames breaks the index binding — inter-frame decode is + // order-dependent, so a permuted clip is a different clip. + let mut t = base.clone(); + t.frames.swap(1, 3); + assert!(!t.verify(), "permuted frames"); + + // Inflating the work figure (the denominator of any efficiency claim) + // breaks recomputation. + let mut t = base.clone(); + t.work_pixel_ops *= 10; + assert!(!t.verify(), "inflated work"); + + // Restating the geometry breaks the clip commitment. + let mut t = base.clone(); + t.w += 1; + assert!(!t.verify(), "restated geometry"); + + // A different identity no longer describes this codestream. + let mut t = base.clone(); + t.codestream_hash[0] ^= 1; + assert!(!t.verify_reconstruction(&bytes, &bs), "wrong codestream hash"); + + // And the energy is inside the signature even though it is not + // independently checkable — an intermediary cannot restate it. + let mut t = base.clone(); + t.joules_micro = 1; + assert!(!t.verify(), "restated energy"); +} + +/// Receipts chain like every other class, and the chain link is covered by the +/// signature. +#[test] +fn receipts_chain_by_parent_hash() { + let (first, _, _) = seal("int_video_bitstream.json", 4_200); + let bytes = raw("video_real_bitstream.json"); + let bs: serde_json::Value = serde_json::from_slice(&bytes).unwrap(); + let second = VideoReceipt::seal_decode( + &key(7), + "did:example:meter", + &bytes, + &bs, + 91_000, + Some(first.receipt_hash()), + ) + .expect("seal"); + + assert!(second.verify()); + assert_eq!(second.parent_receipt_hash, Some(first.receipt_hash())); + + let mut t = second.clone(); + t.parent_receipt_hash = None; + assert!(!t.verify(), "the chain link is signed"); +} -- 2.54.0 (Apple Git-157) From 7dfca26c663e0d64c61bfcddd9c37da9dba34d54 Mon Sep 17 00:00:00 2001 From: David Charlot Date: Wed, 26 Aug 2026 06:25:43 -0400 Subject: [PATCH 3/5] wai: seal how the energy was acquired, and refuse to publish a bad reading MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gaps found by running the meter rather than reasoning about it. **The number had no acquisition class.** `joules_micro` was a bare integer, so two conformant implementations could put a counter reading and a modelled constant in the same field and no reader could tell. `VideoReceipt` now carries `energy_provenance` using the family's existing ladder — `HwShunt | ModelBased | Estimator`, the same three names `mesh-schema` and `proof-schema` already define — rather than a new vocabulary invented for this class. The coupling is enforced in `verify()`, not merely documented: an unlabelled non-zero figure is rejected (a number nobody can compare), and so is a labelled zero (a measurement that did not happen). The class is inside the signing payload, so an intermediary cannot promote a modelled figure to a measured one. **The meter would publish a badly wrong number without complaint.** The same clip, same binary, minutes apart: 10,565 uJ/decode on a quiet box, 497 uJ while a build was running — a 21x error, in the flattering direction, caused entirely by an inflated idle baseline collapsing the marginal delta. Nothing in the output distinguished the two. The meter now computes the idle standard deviation and reports the marginal-to-noise ratio with a plain verdict (clean / usable / NOISY - do not publish / UNUSABLE - the box was not idle), warns when the burst barely rises above the machine's own fluctuation, and — the part that matters — emits an UNMETERED receipt when the figure rounds to zero or sits below the noise. A decode that measured 0 uJ did not consume no energy; it failed to measure, and saying so is the only honest report. That path was found by the coupling rule above firing on the meter's own output: it tried to seal `HwShunt` on a zero and its own receipt refused to verify. Verified: 7 receipt tests (the new one pins both halves of the coupling and that the class is signed); meter re-run end to end, sealing `HwShunt` on a good reading and an unmetered receipt on a noisy one. --- wai/wai-rs/src/bin/wai_video_meter.rs | 70 ++++++++++++++++++--- wai/wai-rs/src/video_receipt.rs | 90 ++++++++++++++++++++++++--- wai/wai-rs/tests/video_receipt.rs | 39 ++++++++++-- 3 files changed, 179 insertions(+), 20 deletions(-) diff --git a/wai/wai-rs/src/bin/wai_video_meter.rs b/wai/wai-rs/src/bin/wai_video_meter.rs index 37ae5811a..efa99dbbe 100644 --- a/wai/wai-rs/src/bin/wai_video_meter.rs +++ b/wai/wai-rs/src/bin/wai_video_meter.rs @@ -34,7 +34,7 @@ use std::time::Instant; use ed25519_dalek::SigningKey; use wai::int_video::decode; -use wai::video_receipt::{work_pixel_ops, VideoReceipt}; +use wai::video_receipt::{work_pixel_ops, Provenance, VideoReceipt}; fn main() { let args: Vec = std::env::args().collect(); @@ -67,7 +67,10 @@ fn main() { ); } - let joules_micro = if metered { + // `Some(uj)` only when a figure was genuinely obtained. A measurement that + // rounds to zero is not a measurement of zero energy — the decode plainly + // spent some — it is a failure to measure, and it must be reported as one. + let measured: Option = if metered { // Phase B: saturate several cores with real decodes while sampling. // Many threads lift the marginal draw above the box's idle fluctuation; // energy per decode is total measured energy ÷ total decodes regardless @@ -104,26 +107,59 @@ fn main() { let runs = (i1 - i0).max(1); let mean = |v: &[f64]| v.iter().sum::() / v.len() as f64; + let sd = |v: &[f64]| { + let m = mean(v); + (v.iter().map(|x| (x - m).powi(2)).sum::() / v.len() as f64).sqrt() + }; let idle_w = mean(&idle); let busy_w = mean(&busy); + let idle_sd = sd(&idle); let marginal_w = (busy_w - idle_w).max(0.0); let energy_j = marginal_w * window; let per_run_uj = energy_j / runs as f64 * 1e6; + // Attribution quality, stated rather than assumed. The marginal method + // is only meaningful when the burst lifts power clearly above the box's + // own idle fluctuation. On a machine that was already busy the baseline + // is inflated, the delta collapses toward zero, and the meter would + // otherwise report a confidently-formatted number that is far too low — + // measured on this very tool, the same clip read 10,565 uJ on a quiet + // box and 497 uJ while a build was running, a 21x error in the + // flattering direction. Silence about that is how a bad number travels. + let snr = if idle_sd > 0.0 { marginal_w / idle_sd } else { f64::INFINITY }; + println!("== wai_video_meter — measured CPU energy (IOReport via macmon, no sudo) =="); println!("decodes : {runs} over {window:.2}s"); - println!("cpu power idle : {idle_w:.2} W ({} samples)", idle.len()); + println!("cpu power idle : {idle_w:.2} W (sd {idle_sd:.2} W, {} samples)", idle.len()); println!("cpu power busy : {busy_w:.2} W ({} samples)", busy.len()); println!("marginal (busy-idle): {marginal_w:.2} W"); println!("→ per decode : {per_run_uj:.1} µJ ({n} frames {h}x{w}, work {work})"); if work > 0 { println!("→ per kilo-subpixel : {:.4} µJ", per_run_uj * 1000.0 / work as f64); } + println!("marginal / idle sd : {snr:.1}x ({})", quality(snr)); + if snr < 3.0 { + eprintln!( + "WARNING: the decode burst barely rose above this machine's idle \n\ + fluctuation (marginal {marginal_w:.2} W vs idle sd {idle_sd:.2} W). The \n\ + figure below is an UNDER-estimate and must not be published. Close \n\ + other work and re-run on a quiet box." + ); + } let uj = per_run_uj.round() as u64; - println!("joules_micro (receipt, MEASURED): {uj}"); - uj + if uj == 0 || snr < 1.0 { + eprintln!( + "no usable figure obtained (per-decode {uj} uJ at {snr:.1}x noise); \n\ + emitting an UNMETERED receipt rather than sealing a number the \n\ + measurement does not support." + ); + None + } else { + println!("joules_micro (receipt, MEASURED): {uj}"); + Some(uj) + } } else { - 0 + None }; // Optional: emit a receipt carrying the measured (or unmetered) energy. @@ -136,7 +172,14 @@ fn main() { "did:web:wai.transaction.science#wai_video_meter", &bytes, &bs, - joules_micro, + measured.unwrap_or(0), + // IOReport is a hardware energy interface, so a reading from it is + // `HwShunt` — a sensor value, not an estimate. Its limitation is + // scope (SoC-wide, hence the marginal-over-idle attribution), not + // accuracy. With no reader present there is no measurement at all, + // and the receipt says so by carrying no class rather than + // downgrading to a label it also did not earn. + measured.map(|_| Provenance::HwShunt), None, ) .expect("seal receipt"); @@ -146,11 +189,22 @@ fn main() { "emitted receipt must re-decode to the same frames" ); std::fs::write(out, receipt.to_json()).expect("write receipt"); - let tag = if metered { "MEASURED" } else { "UNMETERED" }; + let tag = if measured.is_some() { "MEASURED" } else { "UNMETERED" }; println!("receipt : {out} (verified, joules_micro {tag}, work {work})"); } } +/// A plain-language verdict on attribution quality: how far the burst's +/// marginal draw rose above the machine's own idle fluctuation. +fn quality(snr: f64) -> &'static str { + match snr { + s if s >= 10.0 => "clean", + s if s >= 3.0 => "usable", + s if s >= 1.0 => "NOISY — do not publish", + _ => "UNUSABLE — the box was not idle", + } +} + /// One IOReport sample burst via `macmon pipe`, returning the CPU-power readings /// (W). Same non-sudo reader and parse as the sibling meters. fn sample_cpu_power(n: u32) -> Vec { diff --git a/wai/wai-rs/src/video_receipt.rs b/wai/wai-rs/src/video_receipt.rs index 7d0be214e..b8c4ba91e 100644 --- a/wai/wai-rs/src/video_receipt.rs +++ b/wai/wai-rs/src/video_receipt.rs @@ -42,6 +42,45 @@ const DOMAIN_RECEIPT_ID: &[u8] = b"wai:video-receipt-id\x01"; const DOMAIN_FRAME: &[u8] = b"wai:video-rframe\x01"; const DOMAIN_CLIP: &[u8] = b"wai:video-clip\x01"; +/// How an energy figure was acquired — the family's ladder, reused verbatim +/// from `mesh-schema` / `proof-schema` rather than reinvented for this class. +/// +/// A joule figure without this label is not comparable to another: the same +/// number can mean a counter reading, an arithmetic model, or a constant. +/// Sealing the label beside the number is the whole point — the number is not +/// the contribution, the number *plus its acquisition class* is. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Provenance { + /// Read from a hardware energy interface (RAPL / NVML / SMC / IOReport / + /// shunt). Requires a present, non-zero measurement. + HwShunt, + /// Computed from a calibrated per-operation model (e.g. wall time x a power + /// constant). A real number, honestly labelled as derived. + ModelBased, + /// A coarse constant or load estimator. + Estimator, +} + +impl Provenance { + /// The receipt wire label. + pub fn label(self) -> &'static str { + match self { + Provenance::HwShunt => "HwShunt", + Provenance::ModelBased => "ModelBased", + Provenance::Estimator => "Estimator", + } + } + + /// The byte written into the signing payload. + fn tag(self) -> u8 { + match self { + Provenance::HwShunt => 1, + Provenance::ModelBased => 2, + Provenance::Estimator => 3, + } + } +} + /// `(frame_index, BLAKE3 over the frame's decoded RGB bytes)`. /// /// The index is inside the hash, not merely beside it: inter-frame decode is @@ -105,6 +144,13 @@ pub struct VideoReceipt { /// Measured microjoules the sink spent reconstructing (`0` = unmetered). /// Attested by the signature, not independently verifiable. pub joules_micro: u64, + /// How `joules_micro` was acquired. `None` iff the receipt is unmetered. + /// + /// The coupling is enforced by [`VideoReceipt::verify`], not merely + /// documented: a labelled figure of zero, or an unlabelled figure above + /// zero, is rejected. Either would let a reader take a number at a + /// confidence the producer never claimed. + pub energy_provenance: Option, pub parent_receipt_hash: Option<[u8; 32]>, pub signer_pubkey: [u8; 32], pub signer_id: String, @@ -122,6 +168,7 @@ impl VideoReceipt { o.extend_from_slice(&(self.frames.len() as u64).to_be_bytes()); o.extend_from_slice(&self.work_pixel_ops.to_be_bytes()); o.extend_from_slice(&self.joules_micro.to_be_bytes()); + o.push(self.energy_provenance.map_or(0, Provenance::tag)); match self.parent_receipt_hash { Some(h) => { o.push(1); @@ -150,6 +197,7 @@ impl VideoReceipt { codestream_hash: [u8; 32], clip: &DecodedClip, joules_micro: u64, + energy_provenance: Option, parent_receipt_hash: Option<[u8; 32]>, ) -> VideoReceipt { let frames: Vec = clip @@ -169,6 +217,7 @@ impl VideoReceipt { work_pixel_ops: work_pixel_ops(frames.len() as u64, h as u64, w as u64), frames, joules_micro, + energy_provenance, parent_receipt_hash, signer_pubkey: signer.verifying_key().to_bytes(), signer_id: signer_id.into(), @@ -190,11 +239,20 @@ impl VideoReceipt { raw: &[u8], codestream: &Value, joules_micro: u64, + energy_provenance: Option, parent_receipt_hash: Option<[u8; 32]>, ) -> Result { let clip = decode(codestream)?; let codestream_hash = *blake3::hash(raw).as_bytes(); - Ok(Self::build(signer, signer_id, codestream_hash, &clip, joules_micro, parent_receipt_hash)) + Ok(Self::build( + signer, + signer_id, + codestream_hash, + &clip, + joules_micro, + energy_provenance, + parent_receipt_hash, + )) } /// Signature + internal consistency: the Merkle root, the clip commitment @@ -220,6 +278,15 @@ impl VideoReceipt { return false; } } + // A figure and its acquisition class stand or fall together. An + // unlabelled non-zero figure is a number nobody can compare; a labelled + // zero claims a measurement that did not happen. Both are rejected here + // rather than left to a reader's charity. + match (self.energy_provenance, self.joules_micro) { + (None, 0) => {} + (Some(_), n) if n > 0 => {} + _ => return false, + } let Ok(k) = VerifyingKey::from_bytes(&self.signer_pubkey) else { return false }; k.verify(&self.signing_payload(), &Signature::from_bytes(&self.sig)).is_ok() } @@ -260,12 +327,17 @@ impl VideoReceipt { *h.finalize().as_bytes() } - /// Microjoules per thousand decoded subpixels — the comparable figure. + /// Microjoules per thousand decoded subpixels, **with** its acquisition + /// class — the only form in which the figure is comparable to another. /// `None` when unmetered, so an absent measurement can never be read as /// "zero energy" (SPEC: state what you did, never claim what you did not). - pub fn uj_per_kilopixel_op(&self) -> Option { - (self.joules_micro > 0 && self.work_pixel_ops > 0) - .then(|| self.joules_micro as f64 * 1000.0 / self.work_pixel_ops as f64) + pub fn uj_per_kilopixel_op(&self) -> Option<(f64, Provenance)> { + match (self.energy_provenance, self.work_pixel_ops) { + (Some(p), w) if self.joules_micro > 0 && w > 0 => { + Some((self.joules_micro as f64 * 1000.0 / w as f64, p)) + } + _ => None, + } } pub fn to_json(&self) -> String { @@ -276,9 +348,9 @@ impl VideoReceipt { Some(h) => format!("\"{}\"", hx(&h)), None => "null".to_string(), }; - let rate = match self.uj_per_kilopixel_op() { - Some(r) => format!("{r:.6}"), - None => "null".to_string(), + let (rate, prov) = match self.uj_per_kilopixel_op() { + Some((r, p)) => (format!("{r:.6}"), format!("\"{}\"", p.label())), + None => ("null".to_string(), "null".to_string()), }; format!( concat!( @@ -286,6 +358,7 @@ impl VideoReceipt { "\"codestream_hash\":\"{}\",\"clip_hash\":\"{}\",", "\"h\":{},\"w\":{},\"frames\":[{}],\"merkle_root\":\"{}\",", "\"work_pixel_ops\":{},\"joules_micro\":{},\"uj_per_kilopixel_op\":{},", + "\"energy_provenance\":{},", "\"parent_receipt_hash\":{},\"signer_pubkey\":\"{}\",", "\"signer_id\":\"{}\",\"sig\":\"{}\",\"receipt_hash\":\"{}\"}}" ), @@ -298,6 +371,7 @@ impl VideoReceipt { self.work_pixel_ops, self.joules_micro, rate, + prov, parent, hx(&self.signer_pubkey), self.signer_id, diff --git a/wai/wai-rs/tests/video_receipt.rs b/wai/wai-rs/tests/video_receipt.rs index 6f99959f6..a2d6c00b0 100644 --- a/wai/wai-rs/tests/video_receipt.rs +++ b/wai/wai-rs/tests/video_receipt.rs @@ -9,7 +9,7 @@ use std::path::PathBuf; use ed25519_dalek::SigningKey; -use wai::video_receipt::{work_pixel_ops, VideoReceipt}; +use wai::video_receipt::{work_pixel_ops, Provenance, VideoReceipt}; fn dir() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/vectors/int_video") @@ -24,7 +24,8 @@ fn key(seed: u8) -> SigningKey { fn seal(name: &str, uj: u64) -> (VideoReceipt, Vec, serde_json::Value) { let bytes = raw(name); let bs: serde_json::Value = serde_json::from_slice(&bytes).expect("parse"); - let r = VideoReceipt::seal_decode(&key(7), "did:example:meter", &bytes, &bs, uj, None) + let prov = (uj > 0).then_some(Provenance::HwShunt); + let r = VideoReceipt::seal_decode(&key(7), "did:example:meter", &bytes, &bs, uj, prov, None) .expect("seal"); (r, bytes, bs) } @@ -44,8 +45,9 @@ fn seals_and_verifies_against_the_committed_clip() { assert_eq!(r.work_pixel_ops, work_pixel_ops(5, 48, 48)); assert_eq!(r.work_pixel_ops, 5 * 48 * 48 * 3); - let rate = r.uj_per_kilopixel_op().expect("metered"); + let (rate, prov) = r.uj_per_kilopixel_op().expect("metered"); assert!((rate - 4_200.0 * 1000.0 / 34_560.0).abs() < 1e-9); + assert_eq!(prov, Provenance::HwShunt); } /// An unmetered receipt reports **no** rate. A `0` would read as "this decode @@ -55,8 +57,10 @@ fn unmetered_reports_no_rate_rather_than_zero() { let (r, _, _) = seal("int_video_bitstream.json", 0); assert!(r.verify()); assert_eq!(r.joules_micro, 0); - assert_eq!(r.uj_per_kilopixel_op(), None); + assert!(r.uj_per_kilopixel_op().is_none()); + assert_eq!(r.energy_provenance, None); assert!(r.to_json().contains("\"uj_per_kilopixel_op\":null")); + assert!(r.to_json().contains("\"energy_provenance\":null")); } /// Real footage, the half-pel path — the clip whose decode is most at risk of @@ -132,6 +136,7 @@ fn receipts_chain_by_parent_hash() { &bytes, &bs, 91_000, + Some(Provenance::HwShunt), Some(first.receipt_hash()), ) .expect("seal"); @@ -143,3 +148,29 @@ fn receipts_chain_by_parent_hash() { t.parent_receipt_hash = None; assert!(!t.verify(), "the chain link is signed"); } + +/// A number and its acquisition class stand or fall together — the doctrine +/// this class exists to enforce, checked rather than merely documented. +#[test] +fn an_energy_figure_and_its_class_are_inseparable() { + let (metered, _, _) = seal("int_video_bitstream.json", 4_200); + let (unmetered, _, _) = seal("int_video_bitstream.json", 0); + assert!(metered.verify() && unmetered.verify()); + + // Stripping the class off a real figure leaves a number nobody can compare. + let mut t = metered.clone(); + t.energy_provenance = None; + assert!(!t.verify(), "unlabelled non-zero figure"); + + // Labelling a measurement that never happened is the more dangerous half: + // it claims a sensor was in the loop when none was. + let mut t = unmetered.clone(); + t.energy_provenance = Some(Provenance::HwShunt); + assert!(!t.verify(), "labelled zero"); + + // The class is inside the signature, so an intermediary cannot promote a + // modelled figure to a measured one. + let mut t = metered.clone(); + t.energy_provenance = Some(Provenance::ModelBased); + assert!(!t.verify(), "restated class"); +} -- 2.54.0 (Apple Git-157) From 3d3486069f89087a2104795e7ba0966afe1af147 Mon Sep 17 00:00:00 2001 From: David Charlot Date: Wed, 26 Aug 2026 08:14:53 -0400 Subject: [PATCH 4/5] wai: publish the energy-measurement methodology (extensions/energy-measurement.md) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `jwp-receipts.md` defines `joules_micro` as "microjoules consumed producing this object". That fixes the unit and the scope of attribution and nothing else, so the field is a number-shaped hole: one implementation can write a counter reading, another a model, a third a constant, and all three are conformant, incomparable, and — worse — indistinguishable. This extension closes it. Any receipt carrying energy must declare how the figure was acquired (`HwShunt` / `ModelBased` / `Estimator`, the vocabulary the sibling receipt families already use rather than a new one for media), and must declare whether the measurement was good enough to publish. Also specified: the bracketed marginal protocol (idle bracket with standard deviation, saturating multi-thread burst, busy bracket, marginal delta divided by completions), the five bracket parameters that must accompany any published figure, and what the number is not — machine-specific, marginal over idle, scoped to what the interface reports, build-dependent, and never verifiable by re-execution. Two corrections this document forced on the implementation: **The publication bar moves from 3x to 10x, on evidence.** A later reading of the same clip at a 3.8x marginal-to-noise ratio — inside what the previous rule called "usable" — reported 2,469 uJ against 10,565 uJ from a quieter run. A 4.3x disagreement from a reading the rule had passed. The 3-10 band is sound for comparing two workloads back-to-back on one machine, where the bias applies to both; it is not sound for an absolute figure. The bias has a direction, too: a busy machine inflates the idle baseline that is then subtracted, so a poor bracket always reads LOW. An implementation reporting its own energy errs toward flattering itself, which is why the check has to be automatic. **The worked example is a failing one, deliberately.** The draft of this file carried a clean-looking bracket with an idle standard deviation and a quality ratio for the 10,565 uJ run — numbers that run never produced, because it predates the standard-deviation reporting. Inventing them in a document about measurement honesty is the exact failure the document exists to prevent, so the example is now the real 3.8x bracket, labelled as one this spec forbids publishing, with a note saying why a clean bracket is not currently obtainable on the authoring machine. The quiet row in the comparison table is marked as predating the rule rather than given a ratio it never had. Linked from SPEC.md beside the receipt sub-spec, and from the `joules_micro` row of jwp-receipts.md that it constrains. --- wai/SPEC.md | 5 + wai/extensions/energy-measurement.md | 216 ++++++++++++++++++++++++++ wai/extensions/jwp-receipts.md | 2 +- wai/wai-rs/src/bin/wai_video_meter.rs | 22 ++- 4 files changed, 237 insertions(+), 8 deletions(-) create mode 100644 wai/extensions/energy-measurement.md diff --git a/wai/SPEC.md b/wai/SPEC.md index fe512bf4e..4810b7583 100644 --- a/wai/SPEC.md +++ b/wai/SPEC.md @@ -241,6 +241,11 @@ capabilities are added by registration; the WAI envelope is unchanged. See [`extensions/jwp-receipts.md`](extensions/jwp-receipts.md) for the receipt-chain sub-spec covering per-object content hashes, per-group Merkle roots, Ed25519 group signatures, and cross-hop preservation. +See [`extensions/energy-measurement.md`](extensions/energy-measurement.md) for +how a receipt's `joules_micro` is acquired: the required acquisition class +(`HwShunt` / `ModelBased` / `Estimator`), the bracketed marginal protocol, and +the attribution-quality rule that decides whether a figure may be published at +all. See [`extensions/confidentiality.md`](extensions/confidentiality.md) for the private-channel sub-spec: MLS (RFC 9420) group keys + SFrame (RFC 9605) object encryption, the byte-level framing behind §6. diff --git a/wai/extensions/energy-measurement.md b/wai/extensions/energy-measurement.md new file mode 100644 index 000000000..e5205aa30 --- /dev/null +++ b/wai/extensions/energy-measurement.md @@ -0,0 +1,216 @@ +# WAI Extension: Energy Measurement + +> Status: Draft. Specifies **how a joule figure is acquired**, so that the +> `joules_micro` field the receipt profiles already carry means the same thing +> in two independent implementations. No new wire format; an acquisition +> protocol, a reporting contract, and a quality rule over the existing receipt +> fields. +> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174. + +## 1. Why an acquisition protocol + +[`jwp-receipts.md`](jwp-receipts.md) defines `joules_micro` as "microjoules +consumed producing this object". That fixes the *unit* and the *scope of +attribution*. It does not fix how a number gets into the field, and without that +the field is a number-shaped hole: one implementation can write a counter +reading, another an arithmetic model, a third a constant, and every one of them +is conformant. The figures are then incomparable, and — worse — indistinguishable, +because nothing in the receipt says which kind it is. + +This extension closes that. It requires two things of any receipt carrying +energy: that the figure declare **how it was acquired**, and that the +measurement declare **whether it was good enough to publish**. + +The contribution is not the number. It is the number *plus its acquisition +class*, and the discipline of withholding both when the measurement fails. + +## 2. The acquisition class (REQUIRED) + +A receipt carrying a non-zero energy figure **MUST** carry an acquisition class +alongside it, from the shared vocabulary: + +| class | meaning | requires | +|---|---|---| +| `HwShunt` | Read from a hardware energy interface — RAPL, NVML, SMC, IOReport, or an external shunt. | a present, non-zero measurement | +| `ModelBased` | Computed from a calibrated per-operation model (e.g. wall time × a power constant). A real number, honestly labelled as derived. | — | +| `Estimator` | A coarse constant or load estimator. | — | + +These are the same three names the receipt families elsewhere in the ecosystem +already use; this extension deliberately reuses the vocabulary rather than +minting a parallel one for media. + +A counter reading is **not** an estimate. RAPL, NVML and IOReport are sensor +values, and `HwShunt` says so. What separates it from the rungs below is +provenance and scope, not accuracy — see §5. + +Normative: + +- An implementation **MUST NOT** emit a non-zero energy figure without a class. + An unlabelled number cannot be compared to any other number. +- An implementation **MUST NOT** emit a class with a zero or absent figure. That + claims a measurement which did not occur. +- The class **MUST** be inside the signature, so an intermediary cannot promote + a `ModelBased` figure to `HwShunt` in transit. +- A verifier **MUST** treat a receipt violating either coupling as invalid, not + merely as unlabelled. + +The reference implementation enforces both couplings in `verify()` rather than +documenting them, and returns *no rate at all* — never `0` — when unmetered, +because a zero reads as "this work was free", which is a stronger and false +claim than "nobody measured". + +## 3. The reference protocol: bracketed marginal measurement + +On-die energy counters report the draw of a whole package or SoC, not of one +process. A decode cannot be isolated on such an interface, so the reference +protocol measures the **marginal** draw the work adds to an otherwise-idle +machine: + +1. **Idle bracket.** Sample power for a fixed count with no work running. + Record the mean and the **standard deviation**. +2. **Saturating burst.** Run the reconstruction continuously on *N* threads, + counting completions. Many threads are used deliberately: a single-threaded + burst on a modern SoC often fails to lift total draw clear of idle drift. +3. **Busy bracket.** Sample power again for a fixed count while the burst runs, + recording the wall-clock window it spans. +4. **Attribute.** `energy = max(0, mean_busy − mean_idle) × window`, then divide + by the number of completions in that window. + +Per-unit energy is total marginal energy ÷ total completions, so the thread +count does not enter the result. Completions **MUST** be counted per +reconstruction, not per fixed-size batch: a cheap payload runs millions of times +and an expensive one a handful, and a coarse batch collapses the count. + +An implementation **MUST** report the idle mean, the busy mean, the sample +counts, the window, and the completion count alongside any figure it publishes. +Those five numbers are what make a figure auditable; the figure alone is not. + +## 4. Attribution quality (REQUIRED) + +The marginal method is only meaningful when the burst lifts power **clearly +above the machine's own idle fluctuation**. When it does not, the method does +not degrade gracefully — it returns a confidently-formatted number that is far +too low, because an inflated idle baseline is subtracted from a busy mean that +never rose. + +This is not a theoretical concern. Measuring one fixed clip with one binary, +minutes apart on the same machine: + +| condition | idle | busy | marginal | reported | +|---|---|---|---|---| +| quiet | 20.45 W | 54.89 W | 34.45 W | **10,565 µJ/decode** | +| *(the quiet row predates this rule, so its ratio was not recorded)* | | | | | +| build running | ~23.9 W | 24.26 W | 0.36 W | **497 µJ/decode** | + +A **21×** error, in the flattering direction, with nothing in the output to +distinguish the two. + +So an implementation **MUST** compute the ratio of the marginal draw to the idle +standard deviation and report it: + +| ratio | verdict | +|---|---| +| ≥ 10 | clean — publishable | +| ≥ 3 | usable for relative comparison — **not** publishable | +| ≥ 1 | **noisy — do not publish** | +| < 1 | **unusable — the machine was not idle** | + +The publication bar is 10, not 3, and that is an empirical result rather than a +round number. A later reading of the same clip at a ratio of **3.8** — inside +what an earlier draft of this rule called "usable" — reported 2,469 µJ against +the 10,565 µJ above: a **4.3× disagreement from a reading the rule had passed.** +A ratio in the 3–10 band is sound for comparing two workloads measured +back-to-back on one machine, because the bias applies to both. It is not sound +for an absolute figure. + +The bias has a direction. A busy machine inflates the idle baseline, which is +then subtracted from a busy mean that never rose as far, so a poor bracket +always reads **low**. An implementation reporting energy therefore errs toward +flattering itself, which is precisely why the check must be automatic rather +than left to the operator's judgement. + +- An implementation **MUST NOT** seal a `HwShunt` figure when the ratio is below + 1, or when the per-unit figure rounds to zero. Neither is a measurement of low + energy; both are a failure to measure, and the receipt **MUST** be emitted + unmetered instead. +- An implementation **MUST NOT** publish an absolute figure taken below a ratio + of 10, and **SHOULD** warn whenever it produces one. + +Emitting an unmetered receipt is the correct outcome of a failed measurement, +not an error state. A tool that says "I did not measure this" is more useful +than one that says "this decode cost 267 µJ" when the true figure is forty times +higher. + +## 5. What the figure is, and is not + +- **It is machine-specific.** The same codestream on different silicon + legitimately measures differently. Energy is a property of an execution, not + of an artifact. +- **It is a marginal figure over an idle baseline**, so it excludes the fixed + cost of having a machine powered on, and attributes shared platform overhead + to the work only insofar as the work raised it. +- **It is scoped to what the interface reports** — a package or an SoC, not a + process. Provenance and scope are the honest limitations, not sensor accuracy. +- **It depends on the build.** A debug build of the reference decoder measured + **4.9×** a release build of the same code. An implementation **MUST** state + the build profile alongside any published figure. +- **It is not verifiable by re-execution.** A third party re-running the decode + reproduces the *result* and the *work*, never the joules. + +That last point is the reason the receipt profiles are shaped as they are. Where +a class's reconstruction is byte-exact and proven across architectures, every +field except the energy is independently checkable, and the energy is the single +attested variable. Pairing a meter with a reproducible outcome is what turns an +energy receipt from a claim into a measurement with one unverifiable term — so +an implementation **SHOULD** report energy against a work unit that a verifier +can recompute from the artifact, and **MUST NOT** report it against a work unit +only the producer can compute. + +## 6. Reporting contract + +A published figure **MUST** be accompanied by: the acquisition class (§2); the +idle mean and standard deviation, busy mean, sample counts, window and +completion count (§3); the attribution-quality ratio (§4); the build profile +(§5); and the recomputable work unit the rate is expressed against. + +A figure published without these is not wrong so much as unusable: nothing about +it can be checked, reproduced, or compared. + +## 7. Worked example + +`wai.video.int_motion`, real-footage vector, 8 frames of 96×96 RGB, release +build, IOReport via a non-privileged reader: + +``` +work : 221,184 subpixel-ops (8 x 96 x 96 x 3, recomputable) +cpu power idle : 24.95 W (sd 0.23 W, 10 samples) +cpu power busy : 25.84 W (14 samples) +marginal (busy-idle): 0.89 W +decodes : 1,525 over 4.23 s +marginal / idle sd : 3.8x (usable for relative comparison — NOT publishable) +-> per decode : 2,468.8 uJ +-> per kilo-subpixel : 11.16 uJ +acquisition class : HwShunt +``` + +**This example is deliberately a failing one.** It is a complete, honestly +reported bracket that this extension nonetheless forbids publishing as an +absolute figure, because 3.8 is below the bar of §4 — and the earlier reading of +the same clip on a quieter machine differed by 4.3×. It is shown rather than a +clean bracket because a clean bracket is not currently reproducible on the +authoring machine, and substituting an invented one would be the exact failure +this document exists to prevent. An implementation quoting a figure for this +vector **MUST** first obtain a bracket at ≥ 10. + +The receipt seals the codestream hash, the per-frame decoded-pixel hashes, the +work figure, the energy and its class under one signature. A verifier re-derives +everything but the energy from the codestream alone. + +## 8. Reproducing a figure + +An implementation claiming conformance **SHOULD** publish, with any figure: the +vector decoded, the build profile and toolchain, the reader used for the power +interface, and the five bracket parameters of §3. A figure whose bracket is not +published cannot be reproduced, and an unreproducible energy number is an +assertion, not a measurement — which is the failure this extension exists to +prevent. diff --git a/wai/extensions/jwp-receipts.md b/wai/extensions/jwp-receipts.md index 44aea6854..0aeac8fc5 100644 --- a/wai/extensions/jwp-receipts.md +++ b/wai/extensions/jwp-receipts.md @@ -150,7 +150,7 @@ Required fields: | `group_id` | u64 | Mirrors the parent group receipt. | | `object_id` | u64 | Monotonic from 0 within the group. | | `content_hash` | 32 B hex | §2.1 BLAKE3 over the WAI envelope bytes. | -| `joules_micro` | u64 | Microjoules consumed producing this object. | +| `joules_micro` | u64 | Microjoules consumed producing this object. This field fixes the unit and the scope of attribution, not how the number is obtained — see [energy-measurement](energy-measurement.md), which requires an acquisition class beside it and forbids publishing a figure from a measurement that failed. | | `origin` | string | Datacenter / origin label. | | `merkle_proof` | object | Inclusion proof: `{ "siblings": [32 B hex, …], "direction_bits": u64, "leaf_index": u64 }`. `direction_bits` bit `i` is `1` iff the current node at level `i` is the right child. | diff --git a/wai/wai-rs/src/bin/wai_video_meter.rs b/wai/wai-rs/src/bin/wai_video_meter.rs index efa99dbbe..1c1ca811a 100644 --- a/wai/wai-rs/src/bin/wai_video_meter.rs +++ b/wai/wai-rs/src/bin/wai_video_meter.rs @@ -138,12 +138,20 @@ fn main() { println!("→ per kilo-subpixel : {:.4} µJ", per_run_uj * 1000.0 / work as f64); } println!("marginal / idle sd : {snr:.1}x ({})", quality(snr)); - if snr < 3.0 { + // The publication bar is 10x, not 3x, and that is an empirical finding + // rather than a round number: on this tool the same clip read 2,469 uJ + // at a 3.8x ratio and 10,565 uJ on a quiet box — a 4.3x disagreement + // from a reading an earlier version of this check called "usable". A + // ratio in the 3-10 band is good enough to compare two workloads + // measured back-to-back on the same machine; it is not good enough to + // publish an absolute figure. + if snr < 10.0 { eprintln!( - "WARNING: the decode burst barely rose above this machine's idle \n\ - fluctuation (marginal {marginal_w:.2} W vs idle sd {idle_sd:.2} W). The \n\ - figure below is an UNDER-estimate and must not be published. Close \n\ - other work and re-run on a quiet box." + "WARNING: marginal draw {marginal_w:.2} W against idle sd {idle_sd:.2} W \n\ + ({snr:.1}x). Absolute figures are only publishable at >= 10x; below \n\ + that the inflated idle baseline biases the result LOW by a factor \n\ + that has been observed as large as 21x. Close other work and \n\ + re-run on a quiet box before quoting this number." ); } let uj = per_run_uj.round() as u64; @@ -198,8 +206,8 @@ fn main() { /// marginal draw rose above the machine's own idle fluctuation. fn quality(snr: f64) -> &'static str { match snr { - s if s >= 10.0 => "clean", - s if s >= 3.0 => "usable", + s if s >= 10.0 => "clean — publishable", + s if s >= 3.0 => "usable for relative comparison — NOT publishable", s if s >= 1.0 => "NOISY — do not publish", _ => "UNUSABLE — the box was not idle", } -- 2.54.0 (Apple Git-157) From d4c922bebec176ad3daf72f458e204d202803d4f Mon Sep 17 00:00:00 2001 From: David Charlot Date: Fri, 28 Aug 2026 08:45:21 -0400 Subject: [PATCH 5/5] wai: bring the neural-keyframe clip into the cross-arch matrix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit named this gap and left it open: `wai.video.int_hyper` — the variant whose I-frame is decoded by the *learned* conditional codec rather than as an ordinary integer keyframe — could not be driven by `int_video::decode` and so sat outside the four-platform matrix. Covering it meant pulling `int_hyper_synth.rs` in rather than adding a line, which is why it was deferred rather than bodged. It needed one more source than expected: `int_hyper_synth` depends on `int_transform::igdn`, which was not included either. `int_transform` turns out to have no crate-internal dependencies at all, so both drop in cleanly and the legs still run in about a minute with no codec libraries and no ONNX. This is the strictest composition the container has — a learned decode feeding a feedback loop. The keyframe comes out of a quantised transform network, and every later frame predicts from the previously reconstructed one, so a divergent byte in the learned synthesis does not stay in the keyframe; it propagates through the clip. Frame 0 is therefore pinned in its own test as well as inside the clip, so a regression in the conditional codec is distinguishable from one in the motion predictor rather than leaving both suspect. A side effect worth naming: including a source brings its `#[cfg(test)]` module with it, so `int_transform`'s 13 unit tests now run on linux-x86_64, linux-aarch64, macos-arm64 and windows-x86_64 instead of only where someone passes `--features`. The crate goes from 17 tests to 32. Both new sources and the `int_hyper` vectors are added to the workflow's path triggers, on the same rule as before: every source the crate compiles must be able to fire the matrix, or a change can break cross-arch equality and merge green. Verified: 32 tests pass in the conformance crate, the clip reproducing its committed hash `a5f53d477999d159`; workflow YAML parses with all four legs intact and 13 triggers. --- .github/workflows/wai-byte-equality.yml | 13 ++- wai/byte-exact-conformance/src/lib.rs | 13 +++ .../src/neural_keyframe_golden.rs | 108 ++++++++++++++++++ 3 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 wai/byte-exact-conformance/src/neural_keyframe_golden.rs diff --git a/.github/workflows/wai-byte-equality.yml b/.github/workflows/wai-byte-equality.yml index e193332a4..b7cce5ab9 100644 --- a/.github/workflows/wai-byte-equality.yml +++ b/.github/workflows/wai-byte-equality.yml @@ -8,9 +8,10 @@ name: wai cross-architecture byte-equality # int_synth output hash produced on the reference machine (macOS arm64). One # diverging byte anywhere fails the suite. # -# Coverage is still-image AND temporal: the rANS coder, integer fixed-point -# synthesis, the JPEG-AI entropy stage, and the inter-frame video decoder -# (`wai.video.int_motion`). Video is the strictest leg — inter-frame decode is a +# Coverage is still-image AND temporal, classical AND learned: the rANS coder, +# integer fixed-point synthesis, the JPEG-AI entropy stage, the inter-frame video +# decoder (`wai.video.int_motion`), and the neural-keyframe variant +# (`wai.video.int_hyper`) whose I-frame comes out of a learned conditional codec. 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. # @@ -31,7 +32,10 @@ on: - 'wai/wai-rs/src/jpegai_mans.rs' - 'wai/wai-rs/src/rans.rs' - 'wai/wai-rs/src/int_video.rs' + - 'wai/wai-rs/src/int_transform.rs' + - 'wai/wai-rs/src/int_hyper_synth.rs' - 'wai/wai-rs/tests/vectors/int_video/**' + - 'wai/wai-rs/tests/vectors/int_hyper/**' - '.github/workflows/wai-byte-equality.yml' push: branches: @@ -47,7 +51,10 @@ on: - 'wai/wai-rs/src/jpegai_mans.rs' - 'wai/wai-rs/src/rans.rs' - 'wai/wai-rs/src/int_video.rs' + - 'wai/wai-rs/src/int_transform.rs' + - 'wai/wai-rs/src/int_hyper_synth.rs' - 'wai/wai-rs/tests/vectors/int_video/**' + - 'wai/wai-rs/tests/vectors/int_hyper/**' - '.github/workflows/wai-byte-equality.yml' jobs: diff --git a/wai/byte-exact-conformance/src/lib.rs b/wai/byte-exact-conformance/src/lib.rs index 27b45ca2b..60da55ebd 100644 --- a/wai/byte-exact-conformance/src/lib.rs +++ b/wai/byte-exact-conformance/src/lib.rs @@ -41,6 +41,19 @@ pub use neural::rans; #[path = "../../wai-rs/src/int_video.rs"] pub mod int_video; +// The integer transform kernels (`igdn` and friends) the learned codecs use. +// Self-contained — no crate-internal dependencies of its own. +#[path = "../../wai-rs/src/int_transform.rs"] +pub mod int_transform; + +// The learned integer conditional codec that decodes a NEURAL keyframe. Needed +// for `wai.video.int_hyper`, whose I-frame comes from here rather than from +// `int_video::decode` — the gap the previous commit named and left open, since +// covering it meant pulling this source in rather than adding a line. +#[path = "../../wai-rs/src/int_hyper_synth.rs"] +pub mod int_hyper_synth; + mod golden; mod int_video_golden; +mod neural_keyframe_golden; mod jpegai_golden; diff --git a/wai/byte-exact-conformance/src/neural_keyframe_golden.rs b/wai/byte-exact-conformance/src/neural_keyframe_golden.rs new file mode 100644 index 000000000..1f4d6934b --- /dev/null +++ b/wai/byte-exact-conformance/src/neural_keyframe_golden.rs @@ -0,0 +1,108 @@ +//! Cross-architecture byte-equality for `wai.video.int_hyper` — the +//! **neural-keyframe** inter-frame codec. +//! +//! This closes the gap the video goldens next door named and left open. That +//! file covers `wai.video.int_motion`, whose I-frame is an ordinary integer +//! keyframe; this one covers the variant whose I-frame is decoded by the +//! *learned* conditional codec (`int_hyper_synth`) and whose P-frames are +//! integer motion-compensated residuals on top of it. +//! +//! It is the strictest composition in the container: a learned decode feeding a +//! feedback loop. The keyframe comes out of a quantised transform network, and +//! every subsequent frame predicts from the previously reconstructed one — so a +//! single divergent byte anywhere in the learned synthesis does not stay in the +//! keyframe, it propagates through the whole clip. A learned keyframe that +//! reproduces bit-for-bit on four architectures is the claim that makes +//! "a learned codec you can put in a receipt" mean anything. + +#![cfg(test)] + +use std::path::PathBuf; + +use serde_json::Value; + +use crate::int_hyper_synth::{decode_conditional, CondBitstream, CondEntropy, IntModel}; +use crate::int_video::{decode_pframes, fnv1a}; + +fn vectors() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../wai-rs/tests/vectors") +} + +fn jload(rel: &str) -> Value { + let path = vectors().join(rel); + let bytes = std::fs::read(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display())); + serde_json::from_slice(&bytes).unwrap_or_else(|e| panic!("parse {}: {e}", path.display())) +} + +/// The 8 raw bytes behind a 16-hex string. +fn hex16(s: &str) -> [u8; 8] { + let mut o = [0u8; 8]; + for (i, b) in o.iter_mut().enumerate() { + *b = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).expect("hex"); + } + o +} + +/// Frame 0 from the learned conditional codec, frames 1..N from integer motion +/// compensation on top — every one asserted against its committed hash. +#[test] +fn the_neural_keyframe_clip_is_byte_identical() { + // The learned I-frame: analysis/synthesis models, the conditional entropy + // parameters, and the committed bitstream. + let g_s = IntModel::from_json(&jload("int_hyper/tiny_g_s.json")); + let h_s = IntModel::from_json(&jload("int_hyper/tiny_h_s.json")); + let ent = CondEntropy::from_value(&jload("int_hyper/tiny_entropy.json")); + let bs = CondBitstream::from_value(&jload("int_hyper/tiny_bitstream.json")); + let (frame0, h, w) = decode_conditional(&g_s, &h_s, &ent, &bs).expect("learned I-frame"); + + let conf = jload("int_video/video_nk.json"); + let vbs = jload("int_video/video_nk_bitstream.json"); + assert_eq!(h, conf["h"].as_u64().expect("h") as usize, "keyframe height"); + assert_eq!(w, conf["w"].as_u64().expect("w") as usize, "keyframe width"); + + let clip = decode_pframes(&frame0, h, w, &vbs).expect("integer P-frames"); + let want = conf["frame_hashes"].as_array().expect("frame_hashes"); + assert_eq!(clip.frames.len(), want.len(), "frame count"); + + let mut all = Vec::with_capacity(want.len() * 8); + for (i, frame) in clip.frames.iter().enumerate() { + assert_eq!(frame.len(), h * w * 3, "frame {i} size"); + let got = format!("{:016x}", fnv1a(frame)); + assert_eq!( + got, + want[i].as_str().expect("frame hash"), + "frame {i} diverged — frame 0 is the learned keyframe, so a mismatch there \ + implicates the conditional codec, while a later-only mismatch implicates \ + the motion predictor" + ); + all.extend_from_slice(&hex16(&got)); + } + + let clip_hash = format!("{:016x}", fnv1a(&all)); + assert_eq!(clip_hash, conf["clip_hash"].as_str().expect("clip_hash")); + assert_eq!(clip_hash, "a5f53d477999d159"); + println!("int_hyper keyframe clip ({} frames {h}x{w}): {clip_hash}", clip.frames.len()); +} + +/// Frame 0 of the clip must be exactly what the learned codec produced, with no +/// motion compensation applied. Pinned separately so a regression in the +/// conditional codec is distinguishable from one in the temporal stage. +#[test] +fn the_learned_keyframe_alone_is_byte_identical() { + let g_s = IntModel::from_json(&jload("int_hyper/tiny_g_s.json")); + let h_s = IntModel::from_json(&jload("int_hyper/tiny_h_s.json")); + let ent = CondEntropy::from_value(&jload("int_hyper/tiny_entropy.json")); + let bs = CondBitstream::from_value(&jload("int_hyper/tiny_bitstream.json")); + let (frame0, h, w) = decode_conditional(&g_s, &h_s, &ent, &bs).expect("learned I-frame"); + + let conf = jload("int_video/video_nk.json"); + let want0 = conf["frame_hashes"].as_array().expect("frame_hashes")[0] + .as_str() + .expect("frame 0 hash"); + assert_eq!(frame0.len(), h * w * 3); + assert_eq!( + format!("{:016x}", fnv1a(&frame0)), + want0, + "the learned keyframe itself diverged, before any temporal decode" + ); +} -- 2.54.0 (Apple Git-157)