- video_receipt::decode_wiv1 reports a decode refusal by its code (stream_exhausted, stream_noncanonical, field_range), as it already did for a parse refusal, not by a sentence. - int_payload_verify.py grows each frame as its symbols decode instead of allocating it before the stream is read. - The receipts workflow runs when the refusal fixtures its test reads change. Test: video_receipt::decode_wiv1_reports_refusal_codes (the run-out and unread-word fixtures, and a JSON form). Co-Authored-By: Claude Opus 5.5 <[email protected]>
350 lines
15 KiB
Rust
350 lines
15 KiB
Rust
//! `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")]
|
|
// Most tests here seal over the tool's JSON form through the deprecated entry points:
|
|
// they are the legacy receipts, whose pinned bytes must keep verifying. The tests of
|
|
// the WIV1 payload, the identity a receipt has now, are at the end.
|
|
#![allow(deprecated)]
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use ed25519_dalek::SigningKey;
|
|
use wai::energy_class::{CounterFiltering, Uncertainty};
|
|
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])
|
|
}
|
|
|
|
/// An on-chip counter reading as the native meter emits it: a declared 3 %
|
|
/// standard uncertainty over a 2.8 s bracket, filtering state not determinable
|
|
/// by an unprivileged reader.
|
|
fn chip() -> Provenance {
|
|
Provenance::on_chip_counter(Uncertainty::new(30_000, 2_800_000).unwrap(), CounterFiltering::Unknown)
|
|
.unwrap()
|
|
}
|
|
|
|
fn seal_as(name: &str, uj: u64, prov: Option<Provenance>) -> (VideoReceipt, Vec<u8>, 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, prov, None)
|
|
.expect("seal");
|
|
(r, bytes, bs)
|
|
}
|
|
|
|
fn seal(name: &str, uj: u64) -> (VideoReceipt, Vec<u8>, serde_json::Value) {
|
|
seal_as(name, uj, (uj > 0).then(chip))
|
|
}
|
|
|
|
#[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, chip());
|
|
}
|
|
|
|
/// 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(chip()),
|
|
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(chip());
|
|
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");
|
|
}
|
|
|
|
fn hex(b: &[u8]) -> String {
|
|
b.iter().map(|x| format!("{x:02x}")).collect()
|
|
}
|
|
|
|
/// A receipt sealed under the three-rung ladder keeps its signed bytes and its
|
|
/// JSON rendering. The digests were captured from the legacy encoding (tag
|
|
/// byte, no trailing evidence) and pin it: a verifier holding such a receipt
|
|
/// must reach the same verdict after the acquisition-class split as before.
|
|
#[test]
|
|
#[allow(deprecated)] // sealing a legacy HwShunt receipt is the point
|
|
fn legacy_labels_keep_their_signed_bytes() {
|
|
let (hw, _, _) = seal_as("int_video_bitstream.json", 4_200, Some(Provenance::HwShunt));
|
|
let (none, _, _) = seal("int_video_bitstream.json", 0);
|
|
assert!(hw.verify() && none.verify());
|
|
assert_eq!(
|
|
[
|
|
hex(&hw.receipt_hash()),
|
|
hex(blake3::hash(hw.to_json().as_bytes()).as_bytes()),
|
|
hex(&none.receipt_hash()),
|
|
],
|
|
[
|
|
"0dc851f603666496b1358ab427d8a84163c1841eade8970d537bcdc0567b1745",
|
|
"1cdcf1ebd778c39aca8614c4aa4d470dc9c5ad7ff680eaf3e1ee7302c39db15b",
|
|
"03a88242e999887d0ad9e515d9980755e6aad00989bed8e0ccba35b4bb2dd710",
|
|
]
|
|
);
|
|
}
|
|
|
|
/// The refined hardware class carries its declared uncertainty into the
|
|
/// signature and into the rendered receipt; the same class with no uncertainty
|
|
/// does not verify.
|
|
#[test]
|
|
fn a_refined_class_carries_its_uncertainty_and_cannot_omit_it() {
|
|
let (r, _, _) = seal("int_video_bitstream.json", 4_200);
|
|
assert!(r.verify());
|
|
let j = r.to_json();
|
|
assert!(j.contains("\"energy_provenance\":\"OnChipCounter\""), "{j}");
|
|
assert!(j.contains("\"energy_uncertainty\":{\"relative_ppm\":30000,\"window_us\":2800000}"), "{j}");
|
|
assert!(j.contains("\"energy_counter_filtering\":\"Unknown\""), "{j}");
|
|
serde_json::from_str::<serde_json::Value>(&j).expect("the receipt renders as valid JSON");
|
|
|
|
// Sealed without an uncertainty (by hand, bypassing the checked
|
|
// constructor): the signature is valid, the receipt is not.
|
|
let undeclared = Provenance::OnChipCounter {
|
|
uncertainty: Uncertainty { relative_ppm: 0, window_us: 2_800_000 },
|
|
filtering: CounterFiltering::Unknown,
|
|
};
|
|
let (bad, _, _) = seal_as("int_video_bitstream.json", 4_200, Some(undeclared));
|
|
assert!(!bad.verify(), "a hardware class with no declared uncertainty must not verify");
|
|
|
|
// And a declared uncertainty cannot be tightened after sealing.
|
|
let mut t = r.clone();
|
|
t.energy_provenance = Some(
|
|
Provenance::on_chip_counter(Uncertainty::new(1_000, 2_800_000).unwrap(), CounterFiltering::Unknown)
|
|
.unwrap(),
|
|
);
|
|
assert!(!t.verify(), "restated uncertainty");
|
|
}
|
|
|
|
/// The refined classes' signed bytes, pinned as they were when the hardware
|
|
/// class was split. `legacy_labels_keep_their_signed_bytes` holds the legacy
|
|
/// encodings; this holds the refined ones, so a byte that leaks into the
|
|
/// evidence encoding (`write_signed`) fails here. The digests were captured
|
|
/// from the code as the refined classes were introduced and must never be
|
|
/// edited to make a test pass.
|
|
#[test]
|
|
fn refined_labels_keep_their_signed_bytes() {
|
|
let (chip_r, _, _) = seal("int_video_bitstream.json", 4_200);
|
|
let cal = Provenance::calibrated_instrument(
|
|
Uncertainty::new(5_000, 60_000_000).unwrap(),
|
|
"https://calibration.example.org/certificates/0001",
|
|
)
|
|
.unwrap();
|
|
let (cal_r, _, _) = seal_as("int_video_bitstream.json", 4_200, Some(cal));
|
|
assert!(chip_r.verify() && cal_r.verify());
|
|
assert_eq!(
|
|
[
|
|
hex(&chip_r.receipt_hash()),
|
|
hex(blake3::hash(chip_r.to_json().as_bytes()).as_bytes()),
|
|
hex(&cal_r.receipt_hash()),
|
|
hex(blake3::hash(cal_r.to_json().as_bytes()).as_bytes()),
|
|
],
|
|
[
|
|
"23b6f548184a543fa324066427648152de6bb367f64aaddb9ff608d8af41543d",
|
|
"7ece767eb1428faff0d4dd035d1d85e285461bc4610c5af30223a5c0fc20dcc3",
|
|
"c6aad993760ff0b23c57a49e64a4a24c568db2af1c3f18b3c11e659c4ffb4151",
|
|
"2d5529def21fdbf30524a53c1638f06c6311bd3d0d50e125ba4d3a4b2fcf69bb",
|
|
]
|
|
);
|
|
}
|
|
|
|
/// A receipt sealed over the `WIV1` payload (`extensions/integer-payloads.md` §3):
|
|
/// its identity is BLAKE3 over the payload, it re-derives from the payload alone,
|
|
/// and every reconstruction field — frames, clip commitment, geometry, work — is the
|
|
/// one the JSON form's receipt carries, since both decode to the same frames.
|
|
#[test]
|
|
fn a_wiv1_receipt_identifies_the_payload_and_reconstructs_the_same_clip() {
|
|
for (bin, json) in [("int_video_bitstream.wiv1", "int_video_bitstream.json"), ("video_real_bitstream.wiv1", "video_real_bitstream.json")] {
|
|
let payload = raw(bin);
|
|
let r = VideoReceipt::seal_decode_wiv1(&key(7), "did:example:meter", &payload, 4_200, Some(chip()), None).expect("seal");
|
|
assert!(r.verify(), "{bin}");
|
|
assert!(r.verify_reconstruction_wiv1(&payload), "{bin}: re-decode must reproduce every frame");
|
|
assert_eq!(r.codestream_hash, *blake3::hash(&payload).as_bytes(), "{bin}: the identity is the payload's");
|
|
let (legacy, json_bytes, _) = seal(json, 4_200);
|
|
assert_eq!((&r.frames, r.clip_hash, r.h, r.w, r.work_pixel_ops), (&legacy.frames, legacy.clip_hash, legacy.h, legacy.w, legacy.work_pixel_ops),
|
|
"{bin}: the same reconstruction as the JSON form's receipt");
|
|
assert_ne!(r.codestream_hash, legacy.codestream_hash);
|
|
// Neither receipt vouches for the other's codestream.
|
|
assert!(!r.verify_reconstruction_wiv1(&json_bytes), "{bin}: a JSON form is not a WIV1 payload");
|
|
assert!(!legacy.verify_reconstruction_wiv1(&payload), "{bin}: the legacy receipt names the JSON bytes");
|
|
let mut other = payload.clone();
|
|
let at = other.len() - 5;
|
|
other[at] ^= 1;
|
|
assert!(!r.verify_reconstruction_wiv1(&other), "{bin}: one changed byte");
|
|
}
|
|
}
|
|
|
|
/// The signed bytes of receipts sealed over the `WIV1` payload, captured from the
|
|
/// code as this identity was introduced: a metered one with the on-chip class, an
|
|
/// unmetered one, and one chained to the first. Never edited to make a test pass.
|
|
#[test]
|
|
fn wiv1_receipts_keep_their_signed_bytes() {
|
|
let payload = raw("int_video_bitstream.wiv1");
|
|
let metered = VideoReceipt::seal_decode_wiv1(&key(7), "did:example:meter", &payload, 4_200, Some(chip()), None).unwrap();
|
|
let unmetered = VideoReceipt::seal_decode_wiv1(&key(7), "did:example:meter", &payload, 0, None, None).unwrap();
|
|
let chained = VideoReceipt::seal_decode_wiv1(&key(7), "did:example:meter", &payload, 0, None, Some(metered.receipt_hash())).unwrap();
|
|
assert!(metered.verify() && unmetered.verify() && chained.verify());
|
|
let got = [
|
|
hex(&metered.codestream_hash),
|
|
hex(&metered.receipt_hash()),
|
|
hex(blake3::hash(metered.to_json().as_bytes()).as_bytes()),
|
|
hex(&unmetered.receipt_hash()),
|
|
hex(&chained.receipt_hash()),
|
|
];
|
|
assert_eq!(
|
|
got,
|
|
[
|
|
"277865b574029f700f4fac023de3ab06ae598ee6f8ed726444e91442a6bd59e9",
|
|
"52c0ca1c9b383b9ad83226d423b36ecafbef92bf431432c7426a484c0406a742",
|
|
"97a7cb3b2598f609d205159e2ef1b2d7fb2d35e429ee161a6a8fbbd8ffdcae52",
|
|
"f19f86c9106981096bdbb5ea06926e3543112c26a5f737732938feb51c2b6119",
|
|
"58ca49ab785116aec8d2f3b990722e962967cbca6b4d6fe41087343872bd1628",
|
|
]
|
|
);
|
|
}
|
|
|
|
/// `video_receipt::decode_wiv1` reports a refusal by its code
|
|
/// (`extensions/integer-payloads.md` §8), the decode's as well as the parse's: the
|
|
/// refusal fixtures of a run-out stream and an unread word, and a JSON form.
|
|
#[test]
|
|
fn decode_wiv1_reports_refusal_codes() {
|
|
let fixture = |name: &str| std::fs::read(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../int-payload-conformance/refusals").join(name)).unwrap();
|
|
let code = |p: &[u8]| wai::video_receipt::decode_wiv1(p).map(|_| ()).unwrap_err();
|
|
assert_eq!(code(&fixture("wiv1_exhausted.bin")), "stream_exhausted");
|
|
assert_eq!(code(&fixture("wiv1_unread_word.bin")), "stream_noncanonical");
|
|
assert_eq!(code(&raw("int_video_bitstream.json")), "bad_magic");
|
|
}
|