wai: prove integer video byte-identical, meter it, and publish the method #60
12 changed files with 1319 additions and 1 deletions
29
.github/workflows/wai-byte-equality.yml
vendored
29
.github/workflows/wai-byte-equality.yml
vendored
|
|
@ -8,6 +8,13 @@ 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, 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.
|
||||
#
|
||||
# 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 +25,17 @@ 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/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:
|
||||
|
|
@ -26,6 +44,17 @@ 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/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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
119
wai/byte-exact-conformance/src/int_video_golden.rs
Normal file
119
wai/byte-exact-conformance/src/int_video_golden.rs
Normal file
|
|
@ -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.
|
||||
|
|
@ -23,5 +23,37 @@ 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;
|
||||
|
||||
// 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;
|
||||
|
|
|
|||
108
wai/byte-exact-conformance/src/neural_keyframe_golden.rs
Normal file
108
wai/byte-exact-conformance/src/neural_keyframe_golden.rs
Normal file
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
216
wai/extensions/energy-measurement.md
Normal file
216
wai/extensions/energy-measurement.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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. |
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
236
wai/wai-rs/src/bin/wai_video_meter.rs
Normal file
236
wai/wai-rs/src/bin/wai_video_meter.rs
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
//! `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 <bitstream.json> [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, Provenance, VideoReceipt};
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let Some(path) = args.get(1) else {
|
||||
eprintln!("usage: wai_video_meter <bitstream.json> [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."
|
||||
);
|
||||
}
|
||||
|
||||
// `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<u64> = 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::<f64>() / v.len() as f64;
|
||||
let sd = |v: &[f64]| {
|
||||
let m = mean(v);
|
||||
(v.iter().map(|x| (x - m).powi(2)).sum::<f64>() / 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 (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));
|
||||
// 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: 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;
|
||||
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 {
|
||||
None
|
||||
};
|
||||
|
||||
// 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,
|
||||
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");
|
||||
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 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 — 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",
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<f64> {
|
||||
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": <W>` out of one macmon JSON line.
|
||||
fn cpu_power_w(line: &str) -> Option<f64> {
|
||||
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()
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
382
wai/wai-rs/src/video_receipt.rs
Normal file
382
wai/wai-rs/src/video_receipt.rs
Normal file
|
|
@ -0,0 +1,382 @@
|
|||
//! 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";
|
||||
|
||||
/// 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
|
||||
/// 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<FrameHash>,
|
||||
/// 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,
|
||||
/// 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<Provenance>,
|
||||
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<u8> {
|
||||
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());
|
||||
o.push(self.energy_provenance.map_or(0, Provenance::tag));
|
||||
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<String>,
|
||||
codestream_hash: [u8; 32],
|
||||
clip: &DecodedClip,
|
||||
joules_micro: u64,
|
||||
energy_provenance: Option<Provenance>,
|
||||
parent_receipt_hash: Option<[u8; 32]>,
|
||||
) -> VideoReceipt {
|
||||
let frames: Vec<FrameHash> = 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,
|
||||
energy_provenance,
|
||||
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<String>,
|
||||
raw: &[u8],
|
||||
codestream: &Value,
|
||||
joules_micro: u64,
|
||||
energy_provenance: Option<Provenance>,
|
||||
parent_receipt_hash: Option<[u8; 32]>,
|
||||
) -> Result<VideoReceipt, &'static str> {
|
||||
let clip = decode(codestream)?;
|
||||
let codestream_hash = *blake3::hash(raw).as_bytes();
|
||||
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
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
// 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()
|
||||
}
|
||||
|
||||
/// 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, **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<(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 {
|
||||
let hx = |b: &[u8]| b.iter().map(|x| format!("{x:02x}")).collect::<String>();
|
||||
let objs: Vec<String> =
|
||||
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, 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!(
|
||||
"{{\"profile\":\"wai.video.int_motion/receipt\",",
|
||||
"\"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\":\"{}\"}}"
|
||||
),
|
||||
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,
|
||||
prov,
|
||||
parent,
|
||||
hx(&self.signer_pubkey),
|
||||
self.signer_id,
|
||||
hx(&self.sig),
|
||||
hx(&self.receipt_hash())
|
||||
)
|
||||
}
|
||||
}
|
||||
176
wai/wai-rs/tests/video_receipt.rs
Normal file
176
wai/wai-rs/tests/video_receipt.rs
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
//! `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, Provenance, VideoReceipt};
|
||||
|
||||
fn dir() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/vectors/int_video")
|
||||
}
|
||||
fn raw(name: &str) -> Vec<u8> {
|
||||
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<u8>, serde_json::Value) {
|
||||
let bytes = raw(name);
|
||||
let bs: serde_json::Value = serde_json::from_slice(&bytes).expect("parse");
|
||||
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)
|
||||
}
|
||||
|
||||
#[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, 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
|
||||
/// 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!(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
|
||||
/// 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(Provenance::HwShunt),
|
||||
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");
|
||||
}
|
||||
|
||||
/// 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");
|
||||
}
|
||||
Loading…
Reference in a new issue