wai: energy reports, labelled JWP leaves and what signatures cover #133

Merged
dcharlot merged 6 commits from wai/land-14-energy-reporting into main 2026-10-01 14:54:28 -04:00
71 changed files with 16050 additions and 592 deletions

View file

@ -9,12 +9,20 @@ on:
pull_request:
paths:
- 'joule-code/**'
# joule-moq's receipt tests pin leaves, roots and signatures copied from WAI's
# jwp-conformance corpus, and run every leaf, group and object vector in it:
# a change there must re-run them.
- 'wai/jwp-conformance/**'
- '.github/workflows/joule-code-conformance.yml'
push:
branches:
- main
paths:
- 'joule-code/**'
# joule-moq's receipt tests pin leaves, roots and signatures copied from WAI's
# jwp-conformance corpus, and run every leaf, group and object vector in it:
# a change there must re-run them.
- 'wai/jwp-conformance/**'
- '.github/workflows/joule-code-conformance.yml'
jobs:
@ -49,6 +57,14 @@ jobs:
working-directory: joule-code/joule-code-rs
run: cargo test --workspace
# joule-moq's QUIC transport and its example compile only with `quic`. Named
# here so they stay built and tested whatever the workspace's other members
# happen to enable; this also runs tests/wai_jwp_corpus.rs, every leaf, group
# and object vector of wai/jwp-conformance through the crate.
- name: joule-moq with the QUIC transport, every target
working-directory: joule-code/joule-code-rs
run: cargo test -p joule-moq --features quic --all-targets
- name: Round-trip the v1 conformance pack (C1-C4)
working-directory: joule-code/joule-code-rs
run: |

View file

@ -105,8 +105,11 @@ jobs:
working-directory: wai/wai-rs
# neural_int joins the pure-Rust set: the integer-synthesis decoder is
# i64-only (no ONNX, no float), so it carries the same bit-exact promise
# and is checkable on this matrix with no ONNX Runtime present.
run: cargo build --no-default-features --features world,audio_scene,haptic,splat4d,score,provenance,neural_int,film
# and is checkable on this matrix with no ONNX Runtime present. receipt_claims
# is in the set because the reference meters (wai_meter, wai_world_meter) write a
# measurement report by default and require it: without it they would drop out
# of this build on every leg of this matrix, without a word.
run: cargo build --no-default-features --features world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,receipt_claims
- name: Library unit tests (the fixed-point determinism floors)
working-directory: wai/wai-rs
run: cargo test --lib --no-default-features --features world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,oer,receipt_claims
@ -223,6 +226,7 @@ jobs:
v quantum wai_quantum_vectors ../quantum-conformance
v quantum-ops wai_quantum_ops_vectors ../quantum-ops-conformance
v oer wai_oer_vectors ../oer-conformance
v jwp wai_jwp_vectors ../jwp-conformance
v claims wai_claim_vectors ../claim-conformance
v pin wai_pin_vectors ../pin-conformance
# The pin corpus's second implementation: Python standard library only, sharing no
@ -272,6 +276,7 @@ jobs:
v quantum wai_quantum_vectors ../quantum-conformance
v quantum-ops wai_quantum_ops_vectors ../quantum-ops-conformance
v oer wai_oer_vectors ../oer-conformance
v jwp wai_jwp_vectors ../jwp-conformance
v claims wai_claim_vectors ../claim-conformance
v pin wai_pin_vectors ../pin-conformance
@ -296,6 +301,55 @@ jobs:
shell: bash
run: python wai/vocab/check_shapes.py
# The corpora above are generated and verified by wai-rs, one implementation. These are
# the second implementations, written from the extension text in Python and sharing no
# code with wai-rs: claim_verify.py (every claim vector, the canonical fixture, and the
# bracket arithmetic of energy-measurement §6.2 recomputed with Python's integers),
# differential.py (both verifiers' verdicts on generated inputs the corpus does not hold,
# under a fixed seed, so a run is reproducible), and jwp_verify.py (every JWP vector).
# Each exits non-zero on any failure or any difference, and the step fails with it.
# One platform: what they check is the reading of the text, not the architecture.
independent-verifiers:
name: independent verifiers · claims and JWP (Python)
runs-on: linux
steps:
- uses: actions/checkout@v5
- name: Install Rust 1.98.0
shell: bash
run: |
export PATH="$HOME/.cargo/bin:$PATH"
command -v rustup >/dev/null 2>&1 || \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0
rustup toolchain install 1.98.0 --profile minimal
rustup default 1.98.0
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- uses: actions/setup-python@v5
with:
python-version: '3.11'
# The verifiers check Ed25519's equation with the `cryptography` package and make the
# rest of jwp-receipts §3.1 step 3 themselves.
- name: Install the Ed25519 library
shell: bash
run: python -m pip install cryptography
# differential.py drives wai_claim_vectors' `verdicts` mode.
- name: Build the corpus verifier wai-rs ships
working-directory: wai/wai-rs
shell: bash
run: cargo build --no-default-features --features receipt_claims --bin wai_claim_vectors --bin wai_jwp_vectors
- name: claim_verify.py, differential.py, jwp_verify.py
working-directory: wai
shell: bash
run: |
set -euo pipefail
python3 claim-conformance/claim_verify.py claim-conformance/vectors.json
python3 claim-conformance/differential.py "$GITHUB_WORKSPACE/wai/wai-rs/target/debug/wai_claim_vectors" --random 2000 --seed 1
python3 jwp-conformance/jwp_verify.py jwp-conformance/vectors.json
byte-exact:
name: cross-arch byte-equality · ${{ matrix.os }}
runs-on: ${{ matrix.os }}

View file

@ -1,16 +1,21 @@
name: WAI energy receipts
# Tests of the energy acquisition class (extensions/energy-measurement.md §2):
# the class rules, the optional labels, the resolution floor, and the pins that
# hold legacy receipts (unlabelled, or labelled with the legacy HwShunt) to the
# signed bytes they already had. Three steps, on linux (x86_64), linux-arm64
# and macos (arm64):
# the class rules, the optional labels, the resolution floor, the pins that
# hold legacy receipts (unlabelled, or labelled with the legacy HwShunt) and
# labelled ones to the signed bytes they have; the measurement report's
# arithmetic and the liveness rule (§3.1, §6.2) with their fault-injection
# tests; the figure views a report is checked against, for every receipt
# family the set compiles; and the reference meters' report path. Three
# steps, on linux (x86_64), linux-arm64 and macos (arm64):
#
# 1. wai-rs: `cargo test --locked` with the feature set in the step, over
# --lib, --bins and --test video_receipt. That runs the library's unit
# tests under those features (energy_class, energy_burst, energy_meter,
# energy_binding, the label and pin tests of the wai-rs receipt families,
# the video and OTT receipt tests); the unit tests of every bin target
# tests under those features (energy_class, energy_report, energy_burst,
# energy_meter, energy_binding, energy_figure, the label and pin tests of
# the wai-rs receipt families, the video and OTT receipt tests, and — with
# receipt_claims — the claim classes and jwp_delivery); the unit tests of
# every bin target
# the set can build (one whose required features are all on), which also
# proves those bins compile; and the one integration test file
# tests/video_receipt.rs.
@ -18,7 +23,9 @@ name: WAI energy receipts
# unit tests, among them the pins and label tests of every quantum
# receipt. The tests already marked #[ignore] stay ignored.
# 3. wai-quantum-serve: `cargo test --locked --lib`, native: the verdict
# logic behind POST /verify, for a receipt in either form.
# logic behind POST /verify, for a receipt in either form, and that an
# unmetered receipt's 0 is rendered as the unmetered marker (null), never
# as zero joules.
#
# It does NOT:
# - check that a newly sealed receipt in a family with an optional label
@ -27,8 +34,11 @@ name: WAI energy receipts
# labelling path could, and none is written; those families'
# constructors take no class, and a label is added afterwards. It checks
# the label rules for the receipts that carry one, and that four of the
# five reference meter bins still build (wai_video_meter, wai_meter,
# wai_world_meter, wai_quantum_meter).
# five reference meter bins still build with their report path
# (wai_video_meter, wai_meter, wai_world_meter, wai_quantum_meter).
# - run a meter against a power reader. The report path is tested in the
# library (energy_figure::meter_report), and the protocol against
# scripted readers (energy_burst, energy_meter).
# - build wai_prov (needs `codecs`, which links system libjxl) or the
# wai-quantum CLI bin (needs quantum_toolchain, quantum_qir,
# quantum_channel, quantum_comm and quantum_qudit). Cargo skips a bin whose
@ -39,8 +49,8 @@ name: WAI energy receipts
# manifest; the c2pa-emitter job of wai-conformance.yml tests it. Nor
# `oer`: an OER receipt grades its figure and carries no acquisition
# class, and energy-measurement.md §1 leaves it out of scope.
# - test the JWP group and object receipts. joule-code's joule-moq seals
# them with no class, and energy-measurement.md §1 records that as open.
# - verify the JWP corpus; wai-conformance.yml does that, and joule-code's
# workflow tests joule-moq against literals copied from it.
# - build wai-web, or the wasm32-wasip2 component of wai-quantum-serve.
# - verify the conformance corpora; wai-conformance.yml does that.
# - run on Windows. The pinned bytes are integers and hashes and make no
@ -102,7 +112,8 @@ jobs:
# re-exports with the cross-crate encoding test. --bins compiles every
# bin whose required features this set enables — among them four of the five
# reference meters (wai_video_meter, wai_meter, wai_world_meter,
# wai_quantum_meter) — so one of those that stops building fails here.
# wai_quantum_meter), whose report path needs receipt_claims — so one of
# those that stops building fails here.
# The fifth, wai_prov, is NOT built by this or any workflow: it requires
# the `codecs` feature, which links system libjxl (>= 0.11), and no
# workflow enables `codecs` or installs libjxl. Cargo skips a bin whose
@ -114,7 +125,7 @@ jobs:
shell: bash
run: |
cargo test --locked --no-default-features \
--features world,audio_scene,haptic,splat4d,score,provenance,film,ternary_receipt,world_model,video_receipt,video_workflow,energy_meter,quantum_receipt,quantum_ops,quantum_qbom,quantum_phasor_meter \
--features world,audio_scene,haptic,splat4d,score,provenance,film,ternary_receipt,world_model,video_receipt,video_workflow,energy_meter,quantum_receipt,quantum_ops,quantum_qbom,quantum_phasor_meter,receipt_claims \
--lib --bins --test video_receipt
# wai-quantum is published separately; its receipts' legacy pins and the

View file

@ -18,6 +18,48 @@ All notable changes to the Joule Code standard are recorded here. The format fol
- **§6 / `spec/receipt.md` §2 — a vendor counter is a hardware reading only while it is demonstrably live.** A reading from a vendor energy counter (RAPL, NVML, Apple SMC/IOReport) MAY be tagged `HwShunt`, and supply `measured_uj`, only while the counter has been observed advancing under load for the domain the figure claims (for IOReport: the CPU energy rail itself, in at least 5 sample intervals, at least 4 of every 5, over at least 300 ms of CPU burn). A counter that is present but not live — frozen, or republished in lumps — MUST NOT produce either; the reading falls back to the estimator. A figure MUST NOT claim coverage of a domain its counter does not read. No wire or conformance-vector change.
### Changed — `joule-moq` 0.2.0: WAI's labelled JWP forms, and a strict verifier
`joule-moq` reads and writes the forms of WAI's `extensions/jwp-receipts.md`
that sign an object's energy figure, its class and its origin, and a group
total's class. Every receipt in the legacy forms keeps its bytes and verifies as
before; a verifier that predates the labelled forms refuses them. The crate's
version moves to 0.2.0 because its public API changes:
- `ObjectReceipt` gains `energy_class: Option<Vec<u8>>` (the class in WAI
energy-measurement §2.7's signed encoding). Present, the object's leaf is
`BLAKE3("moq-jwp:merkle-leaf\x02" ‖ content_hash ‖ u64_be(joules_micro) ‖
class ‖ u32_be(len(origin)) ‖ origin)`; absent, the legacy leaf. New:
`ObjectReceipt::leaf`, `ObjectReceipt::check_label`,
`ReceiptBuilder::add_labelled_object`, `ReceiptBuilder::class_for`.
- `GroupReceipt` gains `energy_class`, `energy_coverage` and
`parent_receipt_hash`. With a class, the group is signed under
`moq-jwp:group-sign\x02`, which covers the class, the coverage and the
parent link. `energy_coverage` is written only when it is non-zero, and a
legacy group carrying a non-zero coverage is refused: its signature does not
cover one. New: `ReceiptBuilder::finalize_labelled`, `GroupReceipt::link_hash`
and `compose_group_class`, the rule a labelled group's class is composed by.
- `ReceiptVerifier::verify_group` returns a `VerifiedGroup` (the root with the
group's leaf count and coordinates) instead of the bare root, and
`verify_object` and `verify_payload` take `&VerifiedGroup`. An object is
accepted only at the position its proof walks: its coordinates are the
group's, `object_id` is its leaf index, the proof has the group's depth and
its direction bits are the index's.
- `MerkleProof::verify` is replaced by `MerkleProof::verify_leaf`, which takes
the leaf in its form and the group's leaf count, and
`MerkleProof::walks_its_position`.
- `verify_group` holds the key and the signature to WAI jwp-receipts §3.1
step 3's rule: the publisher key must be the canonical encoding of a point
not of small order (a `y` coordinate ≥ p is refused), `S` must be below L
(checked by the crate as well as by `verify_strict`), `R` must not be of
small order, and the cofactorless equation must hold.
- `JWP_MOQ_EXTENSION_VERSION` is 2.
- `tests/wai_jwp_corpus.rs` runs every leaf, group and object vector of
`wai/jwp-conformance/vectors.json` through the crate.
External JWP implementations need the same update to verify labelled leaves and
groups, and to refuse what this verifier refuses.
## [0.2.0] — draft
### Changed

View file

@ -654,7 +654,7 @@ dependencies = [
[[package]]
name = "joule-moq"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"async-trait",
"blake3",

View file

@ -1367,7 +1367,7 @@ dependencies = [
[[package]]
name = "joule-moq"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"async-trait",
"blake3",

View file

@ -1,6 +1,6 @@
[package]
name = "joule-moq"
version = "0.1.0"
version = "0.2.0"
description = "MoQT (Media over QUIC Transport) wire protocol + JWP receipt extension for CommunicationOS broadcast media. Trait surface lets the Realtime stack publish, subscribe, and relay tracks with object-level signed provenance and joule receipts."
edition = "2024"
rust-version = "1.97"

View file

@ -120,6 +120,7 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
joules_micro: joules,
origin: origin.to_string(),
merkle_proof: proof,
energy_class: None,
};
write_object_to_uni(&mut uni, &header, Some(&receipt), &payload).await?;
}
@ -153,7 +154,7 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
"subscriber: group receipt verified vs pinned key — {} objects, {} µJ, root {}",
group_receipt.leaf_count,
group_receipt.total_joules_micro,
hex_encode(&root[..8]),
hex_encode(&root.root[..8]),
);
for i in 0..group_receipt.leaf_count {

View file

@ -62,8 +62,8 @@ pub use messages::{
SubscribeMessage, SubscribeOk,
};
pub use receipt::{
GroupReceipt, ObjectReceipt, ReceiptVerifier, ReceiptBuilder,
MerkleProof,
compose_group_class, GroupReceipt, ObjectReceipt, ReceiptVerifier, ReceiptBuilder,
MerkleProof, VerifiedGroup,
};
pub use session::{MoqSession, SessionRole, SessionState};
pub use track::{
@ -85,8 +85,9 @@ pub const MOQT_DRAFT_VERSION: u32 = 0xff00_0008; // matches draft-08-style versi
/// JWP receipt extension version.
///
/// Independent of the MoQT draft version. Bumped when the receipt header
/// schema changes.
pub const JWP_MOQ_EXTENSION_VERSION: u8 = 1;
/// schema changes. 2: the labelled object leaf and the labelled group payload
/// (optional members; a receipt without them keeps its bytes).
pub const JWP_MOQ_EXTENSION_VERSION: u8 = 2;
/// Crate version, exposed for receipt-emitting tooling.
pub const VERSION: &str = env!("CARGO_PKG_VERSION");

View file

@ -216,6 +216,7 @@ impl GroupHandle for MockGroupHandle {
joules_micro: joules,
origin: origin.clone(),
merkle_proof: proof,
energy_class: None,
};
fabric.deliver(
&key,

View file

@ -20,20 +20,50 @@
//! signed root.
//!
//! O(log n) verification per object, O(1) signature operations per group.
//!
//! # What the signature covers
//!
//! The wire forms are those of WAI's `extensions/jwp-receipts.md`. An object's
//! leaf takes one of two forms (§2.2): the legacy
//! `BLAKE3("moq-jwp:merkle-leaf\x01" ‖ content_hash)`, which binds the payload
//! only, and the labelled
//! `BLAKE3("moq-jwp:merkle-leaf\x02" ‖ content_hash ‖ u64_be(joules_micro) ‖ class
//! ‖ u32_be(len(origin)) ‖ origin)`, which also binds the object's joule figure,
//! the figure's acquisition class with its evidence, and the origin. A group's
//! signing payload takes one of two forms too (§2.3): the legacy one, which signs
//! the total and no class, and the labelled one (`moq-jwp:group-sign\x02`), which
//! also signs the total's class (composed from the objects' classes, §2.4.1), its
//! coverage and the parent receipt hash. Both new forms are opt-in, and every
//! receipt in a legacy form keeps its bytes. A verifier that predates them
//! refuses a labelled object and a labelled group.
//!
//! The class bytes are WAI's `energy-measurement.md` §2.7 encoding: a tag (0
//! unmetered, 2 ModelBased, 3 Estimator, 4 OnChipCounter, 5 CalibratedInstrument;
//! 1, the legacy HwShunt, is refused here) and the class's evidence.
use crate::error::{MoqError, MoqResult};
use crate::track::{GroupId, ObjectId, TrackAlias};
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
use serde::{Deserialize, Serialize};
/// Domain separator for Merkle internal nodes.
const MERKLE_NODE_DOMAIN: &[u8] = b"moq-jwp:merkle-node\x01";
/// Domain separator for Merkle leaves (object content hashes).
const MERKLE_LEAF_DOMAIN: &[u8] = b"moq-jwp:merkle-leaf\x01";
/// Domain separator for labelled Merkle leaves (content hash, figure, class,
/// origin).
const MERKLE_LEAF_LABELLED_DOMAIN: &[u8] = b"moq-jwp:merkle-leaf\x02";
/// Domain separator for object payload hashing.
pub const OBJECT_HASH_DOMAIN: &[u8] = b"moq-jwp:object\x01";
/// Domain separator for the group payload that gets signed.
const GROUP_SIGNING_DOMAIN: &[u8] = b"moq-jwp:group-sign\x01";
/// Domain separator for the labelled group payload.
const GROUP_SIGNING_LABELLED_DOMAIN: &[u8] = b"moq-jwp:group-sign\x02";
/// Domain of a group receipt's link hash (its identity for `parent_receipt_hash`).
const RECEIPT_LINK_DOMAIN: &[u8] = b"moq-jwp:receipt-link\x01";
/// `joules_micro × relative_ppm` may not be below this: the 0.5/√3 µJ every
/// whole-microjoule figure carries from its rounding (WAI energy-measurement §2.3).
const RESOLUTION_FLOOR_UJ_PPM: u128 = 288_676;
/// Per-object receipt header that travels with the object payload.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
@ -49,9 +79,33 @@ pub struct ObjectReceipt {
pub origin: String,
/// Inclusion proof binding `content_hash` to the group's Merkle root.
pub merkle_proof: MerkleProof,
/// The figure's acquisition class, in its signed encoding (tag and
/// evidence). Present, the object's leaf is the labelled one, which binds
/// `joules_micro`, this class and `origin`; absent, the legacy one.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub energy_class: Option<Vec<u8>>,
}
impl ObjectReceipt {
/// The object's Merkle leaf, in the form its label gives.
pub fn leaf(&self) -> [u8; 32] {
match &self.energy_class {
None => hash_leaf(&self.content_hash),
Some(c) => hash_labelled_leaf(&self.content_hash, self.joules_micro, c, &self.origin),
}
}
/// The label's structural checks: a class this crate can read, tag 0 (the
/// unmetered marker) exactly beside a zero figure, never the legacy
/// HwShunt, and a declared uncertainty at the resolution floor.
pub fn check_label(&self) -> MoqResult<()> {
match &self.energy_class {
None => Ok(()),
Some(c) => check_class(c, self.joules_micro)
.map_err(|e| MoqError::ReceiptVerification(format!("object label: {e}"))),
}
}
/// Compute a properly-domained content hash for an object payload.
pub fn hash_payload(payload: &[u8]) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
@ -81,15 +135,36 @@ pub struct GroupReceipt {
pub publisher_id: String,
/// Ed25519 signature over [`GroupReceipt::signing_payload`] (must be 64 bytes).
pub signature: Vec<u8>,
/// The total's acquisition class, in its signed encoding. Present, the
/// group is signed in the labelled form, which covers this class,
/// `energy_coverage` and `parent_receipt_hash`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub energy_class: Option<Vec<u8>>,
/// The total's coverage in the labelled form: 0 the whole of the work, 1
/// only the measured parts. Signed only in the labelled form, so a legacy
/// group carries 0 and does not write it; a legacy group read with any
/// other value is refused.
#[serde(default, skip_serializing_if = "is_zero")]
pub energy_coverage: u8,
/// The link hash of the previous group receipt under this publisher key.
/// Signed in the labelled form only.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_receipt_hash: Option<[u8; 32]>,
}
impl GroupReceipt {
/// Canonical serialization that gets signed (and re-derived for verification).
/// Order: domain || track_alias || group_id || merkle_root || leaf_count
/// || total_joules_micro || group_start || group_end || publisher_key || publisher_id.
/// || total_joules_micro || group_start || group_end || publisher_key || publisher_id,
/// and in the labelled form, under its own domain, then
/// || class || coverage || has_parent || parent_receipt_hash (zeros when none).
pub fn signing_payload(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(256);
buf.extend_from_slice(GROUP_SIGNING_DOMAIN);
buf.extend_from_slice(if self.energy_class.is_some() {
GROUP_SIGNING_LABELLED_DOMAIN
} else {
GROUP_SIGNING_DOMAIN
});
buf.extend_from_slice(&self.track_alias.0.to_be_bytes());
buf.extend_from_slice(&self.group_id.0.to_be_bytes());
buf.extend_from_slice(&self.merkle_root);
@ -102,8 +177,204 @@ impl GroupReceipt {
buf.extend_from_slice(&self.publisher_key);
buf.extend_from_slice(&(self.publisher_id.len() as u32).to_be_bytes());
buf.extend_from_slice(self.publisher_id.as_bytes());
if let Some(c) = &self.energy_class {
buf.extend_from_slice(c);
buf.push(self.energy_coverage);
match self.parent_receipt_hash {
Some(h) => {
buf.push(1);
buf.extend_from_slice(&h);
}
None => {
buf.push(0);
buf.extend_from_slice(&[0u8; 32]);
}
}
}
buf
}
/// The group's link hash: `BLAKE3("moq-jwp:receipt-link\x01" ‖
/// signing_payload ‖ signature)`, its identity for `parent_receipt_hash`.
pub fn link_hash(&self) -> [u8; 32] {
let mut h = blake3::Hasher::new();
h.update(RECEIPT_LINK_DOMAIN);
h.update(&self.signing_payload());
h.update(&self.signature);
h.finalize().into()
}
/// A labelled group's own checks: a class this crate can read, tag 0
/// exactly beside a zero total and then coverage 0, coverage 0 or 1, no
/// legacy HwShunt, at the resolution floor. A legacy group carries no
/// coverage: its signature does not cover one, so a non-zero value is an
/// unsigned member and is refused.
fn check_label(&self) -> MoqResult<()> {
let e = |m: &str| MoqError::ReceiptVerification(format!("group label: {m}"));
let Some(c) = &self.energy_class else {
return if self.energy_coverage == 0 { Ok(()) } else { Err(e("coverage on a legacy group")) };
};
check_class(c, self.total_joules_micro).map_err(|m| e(&m))?;
if self.energy_coverage > 1 || (c[0] == 0 && self.energy_coverage != 0) {
return Err(e("coverage"));
}
Ok(())
}
}
fn is_zero(v: &u8) -> bool {
*v == 0
}
/// The order of the Ed25519 base point, L, little-endian (RFC 8032 §5.1).
const ED25519_L: [u8; 32] = [
0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0x10,
];
/// Whether the little-endian scalar `s` is below L: RFC 8032 §5.1.7's range
/// check on a signature's `S`.
fn scalar_is_canonical(s: &[u8]) -> bool {
for (a, l) in s.iter().zip(ED25519_L.iter()).rev() {
if a != l {
return a < l;
}
}
false
}
/// A publisher key under WAI jwp-receipts §3.1 step 3's key rule: it decodes
/// under RFC 8032 §5.1.3 — which refuses a `y` coordinate ≥ p, so the bytes are
/// the point's one encoding — and is not of small order.
fn publisher_key(bytes: &[u8; 32]) -> MoqResult<VerifyingKey> {
let e = |m: &str| MoqError::ReceiptVerification(format!("publisher key: {m}"));
let key = VerifyingKey::from_bytes(bytes).map_err(|x| e(&x.to_string()))?;
if key.to_edwards().compress().to_bytes() != *bytes {
return Err(e("not the canonical encoding of its point"));
}
if key.is_weak() {
return Err(e("of small order"));
}
Ok(key)
}
/// A class's signed encoding, read: `(tag, relative_ppm, window_us, filtering
/// byte or calibration reference)`. `None` where the bytes are not one class.
fn read_class(c: &[u8]) -> Option<(u8, Option<(u32, u64)>, ClassEvidence)> {
let (&tag, rest) = c.split_first()?;
let be32 = |b: &[u8]| u32::from_be_bytes(b.try_into().expect("4"));
let be64 = |b: &[u8]| u64::from_be_bytes(b.try_into().expect("8"));
match tag {
0 | 2 | 3 if rest.is_empty() => Some((tag, None, ClassEvidence::None)),
4 if rest.len() == 13 && rest[12] <= 2 => {
Some((tag, Some((be32(&rest[0..4]), be64(&rest[4..12]))), ClassEvidence::Filtering(rest[12])))
}
5 if rest.len() >= 16 && be32(&rest[12..16]) as usize == rest.len() - 16 => {
let r = std::str::from_utf8(&rest[16..]).ok()?;
Some((tag, Some((be32(&rest[0..4]), be64(&rest[4..12]))), ClassEvidence::CalibrationRef(r.to_owned())))
}
_ => None,
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum ClassEvidence {
None,
Filtering(u8),
CalibrationRef(String),
}
/// The structural checks a class's signed encoding must pass beside `figure`.
fn check_class(c: &[u8], figure: u64) -> Result<(), String> {
let (tag, u, ev) = read_class(c).ok_or("unreadable class")?;
match tag {
0 if figure == 0 => return Ok(()),
0 => return Err("the unmetered marker beside a figure".into()),
_ if figure == 0 => return Err("a class beside a zero figure".into()),
_ => {}
}
if let Some((ppm, window)) = u {
if ppm == 0 || window == 0 {
return Err("an undeclared uncertainty".into());
}
if (figure as u128) * (ppm as u128) < RESOLUTION_FLOOR_UJ_PPM {
return Err("a declaration below the resolution floor".into());
}
}
if let ClassEvidence::CalibrationRef(r) = ev {
if r.trim().is_empty() {
return Err("a calibration reference that is blank".into());
}
}
Ok(())
}
/// The class a group's parts compose to (WAI jwp-receipts §2.4.1), as its
/// signed encoding and coverage byte, or `None` where the parts compose to no
/// class: a non-zero part unlabelled, or calibrated instruments under different
/// certificates. Every non-zero part zero gives the unmetered marker. Otherwise
/// the weakest class; for a hardware class the least declaration
/// `⌈Σ F·p / Σ F⌉` over the longest window, an on-chip counter's filtering the
/// parts' common one or Unknown; coverage 1 exactly when a part is zero.
pub fn compose_group_class(parts: &[(u64, Option<&[u8]>)]) -> Option<(Vec<u8>, u8)> {
let any_zero = parts.iter().any(|(f, _)| *f == 0);
let mut classes = Vec::new();
for (f, c) in parts {
if *f == 0 {
continue;
}
let (tag, u, ev) = read_class((*c)?)?;
if tag == 0 || tag == 1 {
return None;
}
classes.push((*f, tag, u, ev));
}
if classes.is_empty() {
return Some((vec![0], 0));
}
let coverage = u8::from(any_zero);
// Rank: 3 Estimator < 2 ModelBased < 4 OnChipCounter < 5 CalibratedInstrument.
let rank = |t: u8| match t {
3 => 1,
2 => 2,
4 => 4,
_ => 5,
};
let weakest = classes.iter().map(|c| c.1).min_by_key(|t| rank(*t))?;
if weakest == 2 || weakest == 3 {
return Some((vec![weakest], coverage));
}
let (mut num, mut den, mut window) = (0u128, 0u128, 0u64);
for (f, _, u, _) in &classes {
let (ppm, w) = (*u)?;
num = num.checked_add((*f as u128).checked_mul(ppm as u128)?)?;
den = den.checked_add(*f as u128)?;
window = window.max(w);
}
let ppm = u32::try_from(num.div_ceil(den)).ok()?;
let mut out = vec![weakest];
out.extend_from_slice(&ppm.to_be_bytes());
out.extend_from_slice(&window.to_be_bytes());
if weakest == 4 {
let mut fs = classes.iter().filter_map(|c| match c.3 {
ClassEvidence::Filtering(f) => Some(f),
_ => None,
});
let first = fs.next()?;
out.push(if fs.all(|f| f == first) { first } else { 0 });
} else {
let mut refs = classes.iter().map(|c| match &c.3 {
ClassEvidence::CalibrationRef(r) => r.as_str(),
_ => "",
});
let first = refs.next()?;
if !refs.all(|r| r == first) {
return None;
}
out.extend_from_slice(&(first.len() as u32).to_be_bytes());
out.extend_from_slice(first.as_bytes());
}
Some((out, coverage))
}
/// Merkle inclusion proof: list of sibling hashes from leaf to root,
@ -117,9 +388,26 @@ pub struct MerkleProof {
}
impl MerkleProof {
/// Verify that `leaf_hash` hashes up to `expected_root` along this proof.
pub fn verify(&self, leaf_hash: &[u8; 32], expected_root: &[u8; 32]) -> bool {
let mut current = hash_leaf(leaf_hash);
/// Whether the proof walks position `leaf_index` of a group of `leaf_count`
/// leaves: the index is inside the group, the proof has the group's depth
/// (⌈log₂ leaf_count⌉), and the direction bits are the index's bits and no
/// others. Without it the duplicated last leaf of an odd level walks two
/// positions, and an object can be presented at another's index.
pub fn walks_its_position(&self, leaf_count: u64) -> bool {
let depth = if leaf_count <= 1 { 0 } else { 64 - (leaf_count - 1).leading_zeros() };
self.leaf_index < leaf_count
&& self.siblings.len() as u64 == depth as u64
&& self.direction_bits == self.leaf_index
}
/// Verify that `leaf` — an object's leaf, in its form ([`ObjectReceipt::leaf`])
/// — hashes up to `expected_root` at this proof's position in a group of
/// `leaf_count` leaves.
pub fn verify_leaf(&self, leaf: &[u8; 32], expected_root: &[u8; 32], leaf_count: u64) -> bool {
if !self.walks_its_position(leaf_count) {
return false;
}
let mut current = *leaf;
for (level, sibling) in self.siblings.iter().enumerate() {
let is_right = (self.direction_bits >> level) & 1 == 1;
current = if is_right {
@ -140,6 +428,18 @@ fn hash_leaf(content_hash: &[u8; 32]) -> [u8; 32] {
hasher.finalize().into()
}
/// Hash an object's labelled leaf: its content hash, figure, class and origin.
fn hash_labelled_leaf(content_hash: &[u8; 32], joules_micro: u64, class: &[u8], origin: &str) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
hasher.update(MERKLE_LEAF_LABELLED_DOMAIN);
hasher.update(content_hash);
hasher.update(&joules_micro.to_be_bytes());
hasher.update(class);
hasher.update(&(origin.len() as u32).to_be_bytes());
hasher.update(origin.as_bytes());
hasher.finalize().into()
}
/// Hash an internal Merkle node (left, right children).
fn hash_node(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
let mut hasher = blake3::Hasher::new();
@ -154,9 +454,11 @@ fn hash_node(left: &[u8; 32], right: &[u8; 32]) -> [u8; 32] {
pub struct ReceiptBuilder {
track_alias: TrackAlias,
group_id: GroupId,
/// Each object's leaf, already in its form.
leaves: Vec<[u8; 32]>,
joules_per_object: Vec<u64>,
origins: Vec<String>,
classes: Vec<Option<Vec<u8>>>,
started_at: Option<String>,
}
@ -168,6 +470,7 @@ impl ReceiptBuilder {
leaves: Vec::new(),
joules_per_object: Vec::new(),
origins: Vec::new(),
classes: Vec::new(),
started_at: None,
}
}
@ -185,12 +488,42 @@ impl ReceiptBuilder {
if self.started_at.is_none() {
self.started_at = Some(timestamp.into());
}
self.leaves.push(content_hash);
self.leaves.push(hash_leaf(&content_hash));
self.joules_per_object.push(joules_micro);
self.origins.push(origin.into());
self.classes.push(None);
id
}
/// Record an object with its figure's acquisition class, in its signed
/// encoding: the object's leaf is the labelled one, binding the figure, the
/// class and the origin. Refuses a class the verifier would refuse.
pub fn add_labelled_object(
&mut self,
content_hash: [u8; 32],
joules_micro: u64,
origin: impl Into<String>,
timestamp: impl Into<String>,
class: Vec<u8>,
) -> MoqResult<ObjectId> {
check_class(&class, joules_micro).map_err(MoqError::ReceiptVerification)?;
let origin = origin.into();
let id = ObjectId(self.leaves.len() as u64);
if self.started_at.is_none() {
self.started_at = Some(timestamp.into());
}
self.leaves.push(hash_labelled_leaf(&content_hash, joules_micro, &class, &origin));
self.joules_per_object.push(joules_micro);
self.origins.push(origin);
self.classes.push(Some(class));
Ok(id)
}
/// The class object `index` was recorded with, for its receipt.
pub fn class_for(&self, index: usize) -> Option<Vec<u8>> {
self.classes.get(index).cloned().flatten()
}
/// Number of objects accumulated so far.
pub fn len(&self) -> usize {
self.leaves.len()
@ -222,6 +555,28 @@ impl ReceiptBuilder {
Ok(compute_merkle_proof(&self.leaves, index))
}
/// Finalize and sign the group in the labelled form: the total's class is
/// the one the objects compose to ([`compose_group_class`]), signed with its
/// coverage and `parent`. Refuses a group whose objects compose to no class.
pub fn finalize_labelled(
self,
signing_key: &SigningKey,
publisher_id: impl Into<String>,
group_end: impl Into<String>,
parent: Option<[u8; 32]>,
) -> MoqResult<GroupReceipt> {
let parts: Vec<(u64, Option<&[u8]>)> =
self.joules_per_object.iter().zip(&self.classes).map(|(f, c)| (*f, c.as_deref())).collect();
let (class, coverage) = compose_group_class(&parts)
.ok_or_else(|| MoqError::ReceiptVerification("the objects compose to no class".into()))?;
let mut receipt = self.finalize(signing_key, publisher_id, group_end);
receipt.energy_class = Some(class);
receipt.energy_coverage = coverage;
receipt.parent_receipt_hash = parent;
receipt.signature = signing_key.sign(&receipt.signing_payload()).to_bytes().to_vec();
Ok(receipt)
}
/// Finalize and sign the group with the publisher's Ed25519 key.
pub fn finalize(
self,
@ -245,6 +600,9 @@ impl ReceiptBuilder {
publisher_key,
publisher_id: publisher_id.into(),
signature: Vec::new(),
energy_class: None,
energy_coverage: 0,
parent_receipt_hash: None,
};
let payload = receipt.signing_payload();
let sig: Signature = signing_key.sign(&payload);
@ -286,11 +644,18 @@ impl ReceiptVerifier {
}
}
/// Verify a group receipt's signature. Returns the verified Merkle root
/// on success.
pub fn verify_group(&self, receipt: &GroupReceipt) -> MoqResult<[u8; 32]> {
let publisher_key = VerifyingKey::from_bytes(&receipt.publisher_key)
.map_err(|e| MoqError::ReceiptVerification(format!("publisher key: {}", e)))?;
/// Verify a group receipt's signature, in either form, and a labelled
/// group's own label. Returns the verified root with the position facts an
/// object is checked against.
///
/// The key and signature are held to the rule WAI's jwp-receipts §3.1
/// step 3 holds every receipt signature to: the key is the canonical
/// encoding of a point not of small order; `S` < L, checked here as well
/// as inside `verify_strict`, whose own range check weakens when any crate
/// in a build enables ed25519-dalek's `legacy_compatibility`; `R` not of
/// small order; and the cofactorless equation.
pub fn verify_group(&self, receipt: &GroupReceipt) -> MoqResult<VerifiedGroup> {
let publisher_key = publisher_key(&receipt.publisher_key)?;
if self.require_known_publisher {
if !self.trust_anchors.contains(&publisher_key) {
return Err(MoqError::ReceiptVerification(
@ -306,33 +671,48 @@ impl ReceiptVerifier {
}
let mut sig_bytes = [0u8; 64];
sig_bytes.copy_from_slice(&receipt.signature);
if !scalar_is_canonical(&sig_bytes[32..]) {
return Err(MoqError::ReceiptVerification("signature: S is not below L".into()));
}
let payload = receipt.signing_payload();
let signature = Signature::from_bytes(&sig_bytes);
publisher_key
.verify(&payload, &signature)
.verify_strict(&payload, &signature)
.map_err(|e| MoqError::ReceiptVerification(format!("signature: {}", e)))?;
Ok(receipt.merkle_root)
receipt.check_label()?;
Ok(VerifiedGroup {
root: receipt.merkle_root,
leaf_count: receipt.leaf_count,
track_alias: receipt.track_alias,
group_id: receipt.group_id,
})
}
/// Verify a per-object receipt against a previously-verified group root.
pub fn verify_object(
&self,
receipt: &ObjectReceipt,
verified_root: &[u8; 32],
) -> MoqResult<()> {
if !receipt.merkle_proof.verify(&receipt.content_hash, verified_root) {
/// Verify a per-object receipt against a verified group: its label, its
/// position (its coordinates are the group's, `object_id` is its leaf
/// index, and its proof walks that index), and its leaf — in its form —
/// up to the verified root.
pub fn verify_object(&self, receipt: &ObjectReceipt, group: &VerifiedGroup) -> MoqResult<()> {
receipt.check_label()?;
if receipt.track_alias != group.track_alias
|| receipt.group_id != group.group_id
|| receipt.object_id.0 != receipt.merkle_proof.leaf_index
{
return Err(MoqError::MerkleProofInvalid);
}
if !receipt.merkle_proof.verify_leaf(&receipt.leaf(), &group.root, group.leaf_count) {
return Err(MoqError::MerkleProofInvalid);
}
Ok(())
}
/// Convenience: verify a payload directly against an object receipt and
/// a verified group root. Recomputes the content hash from `payload`.
/// a verified group. Recomputes the content hash from `payload`.
pub fn verify_payload(
&self,
payload: &[u8],
receipt: &ObjectReceipt,
verified_root: &[u8; 32],
verified_root: &VerifiedGroup,
) -> MoqResult<()> {
let computed = ObjectReceipt::hash_payload(payload);
if computed != receipt.content_hash {
@ -344,6 +724,16 @@ impl ReceiptVerifier {
}
}
/// What [`ReceiptVerifier::verify_group`] establishes: the signed root, and the
/// group's size and coordinates an object's position is checked against.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct VerifiedGroup {
pub root: [u8; 32],
pub leaf_count: u64,
pub track_alias: TrackAlias,
pub group_id: GroupId,
}
impl Default for ReceiptVerifier {
fn default() -> Self {
Self::new()
@ -352,11 +742,12 @@ impl Default for ReceiptVerifier {
// ── Merkle tree helpers ──
/// The root over leaves already hashed (in their forms).
fn compute_merkle_root(leaves: &[[u8; 32]]) -> [u8; 32] {
if leaves.is_empty() {
return [0u8; 32];
}
let mut level: Vec<[u8; 32]> = leaves.iter().map(hash_leaf).collect();
let mut level: Vec<[u8; 32]> = leaves.to_vec();
while level.len() > 1 {
if level.len() % 2 == 1 {
level.push(*level.last().unwrap());
@ -370,10 +761,11 @@ fn compute_merkle_root(leaves: &[[u8; 32]]) -> [u8; 32] {
level[0]
}
/// The proof for leaf `leaf_index` over leaves already hashed.
fn compute_merkle_proof(leaves: &[[u8; 32]], leaf_index: usize) -> MerkleProof {
let mut siblings = Vec::new();
let mut direction_bits: u64 = 0;
let mut level: Vec<[u8; 32]> = leaves.iter().map(hash_leaf).collect();
let mut level: Vec<[u8; 32]> = leaves.to_vec();
let mut idx = leaf_index;
let mut bit_pos = 0;
while level.len() > 1 {
@ -414,24 +806,20 @@ mod tests {
#[test]
fn merkle_proof_verifies_for_each_leaf() {
let leaves = vec![h(1), h(2), h(3), h(4), h(5)];
let leaves: Vec<[u8; 32]> = [h(1), h(2), h(3), h(4), h(5)].iter().map(hash_leaf).collect();
let root = compute_merkle_root(&leaves);
for (i, leaf) in leaves.iter().enumerate() {
let proof = compute_merkle_proof(&leaves, i);
assert!(
proof.verify(leaf, &root),
"proof for leaf {} should verify",
i
);
assert!(proof.verify_leaf(leaf, &root, 5), "proof for leaf {} should verify", i);
}
}
#[test]
fn merkle_proof_rejects_wrong_leaf() {
let leaves = vec![h(1), h(2), h(3), h(4)];
let leaves: Vec<[u8; 32]> = [h(1), h(2), h(3), h(4)].iter().map(hash_leaf).collect();
let root = compute_merkle_root(&leaves);
let proof = compute_merkle_proof(&leaves, 0);
assert!(!proof.verify(&h(99), &root));
assert!(!proof.verify_leaf(&hash_leaf(&h(99)), &root, 4));
}
#[test]
@ -462,6 +850,7 @@ mod tests {
joules_micro: 150,
origin: "joule-cloud-us-west-2".into(),
merkle_proof: proof_for_1,
energy_class: None,
};
verifier.verify_payload(b"frame-2", &object_receipt, &root).unwrap();
}
@ -513,6 +902,7 @@ mod tests {
joules_micro: 1,
origin: "dc1".into(),
merkle_proof: proof,
energy_class: None,
};
// Verify against a *different* payload — must fail.
assert!(matches!(
@ -520,4 +910,251 @@ mod tests {
Err(MoqError::ReceiptVerification(_))
));
}
// ── the labelled forms, pinned to WAI's jwp-conformance corpus ──
//
// The literals are copied from `wai/jwp-conformance/vectors.json`, which
// WAI's reference implementation generates and an independent script
// verifies. Both implementations must give the same leaves, root and
// signatures for the same inputs.
const T0: &str = "2026-09-30T12:00:00Z";
const T1: &str = "2026-09-30T12:00:01Z";
const CAL_REF: &str = "https://calibration.example.org/certificates/0001";
fn hx(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
fn envelope(n: u8) -> Vec<u8> {
format!("WAI1 jwp-conformance object {n}").into_bytes()
}
/// An OnChipCounter class: ppm, window, filtering Unknown.
fn chip(ppm: u32, window_us: u64) -> Vec<u8> {
let mut c = vec![4];
c.extend_from_slice(&ppm.to_be_bytes());
c.extend_from_slice(&window_us.to_be_bytes());
c.push(0);
c
}
fn cal(ppm: u32, window_us: u64, r: &str) -> Vec<u8> {
let mut c = vec![5];
c.extend_from_slice(&ppm.to_be_bytes());
c.extend_from_slice(&window_us.to_be_bytes());
c.extend_from_slice(&(r.len() as u32).to_be_bytes());
c.extend_from_slice(r.as_bytes());
c
}
/// The corpus's mixed group: a counter, an instrument, a legacy object, an
/// object explicitly unmetered and a model.
fn mixed() -> (ReceiptBuilder, Vec<ObjectReceipt>) {
let parts: [(u64, Option<Vec<u8>>); 5] =
[(1_200, Some(chip(20_000, 1_000_000))), (800, Some(cal(5_000, 2_000_000, CAL_REF))), (500, None), (0, Some(vec![0])), (300, Some(vec![2]))];
build(0, 3, &parts)
}
fn build(first: u8, group: u64, parts: &[(u64, Option<Vec<u8>>)]) -> (ReceiptBuilder, Vec<ObjectReceipt>) {
let mut b = ReceiptBuilder::new(TrackAlias(7), GroupId(group));
for (i, (j, c)) in parts.iter().enumerate() {
let n = first + i as u8;
let h = ObjectReceipt::hash_payload(&envelope(n));
let origin = format!("dc-{n}.example");
match c {
None => {
b.add_object(h, *j, origin, T0);
}
Some(c) => {
b.add_labelled_object(h, *j, origin, T0, c.clone()).unwrap();
}
}
}
let objects = parts
.iter()
.enumerate()
.map(|(i, (j, c))| {
let n = first + i as u8;
ObjectReceipt {
track_alias: TrackAlias(7),
group_id: GroupId(group),
object_id: ObjectId(i as u64),
content_hash: ObjectReceipt::hash_payload(&envelope(n)),
joules_micro: *j,
origin: format!("dc-{n}.example"),
merkle_proof: b.proof_for(i).unwrap(),
energy_class: c.clone(),
}
})
.collect();
(b, objects)
}
#[test]
fn leaves_match_the_wai_corpus() {
let (_, o) = mixed();
assert_eq!(hx(&o[2].leaf()), "65dbe4a732e01bd69de3104e03d9822107318551714d055c8a7acc45313796e8", "legacy");
assert_eq!(hx(&o[3].leaf()), "339ea521d795afe4a4ec26042a2827fc9e8052b581fdbb1636b2315ee9953e93", "tag 0");
assert_eq!(hx(&o[0].leaf()), "eeb5d0cf357a3bca9437eff04cf82570a6338652c842d9a20a7670f409a80760", "tag 4");
assert_eq!(hx(&o[1].leaf()), "281de97b5d3b5f872298b30812bf65f6a14d2cb223c135d17034e19e20988c9f", "tag 5");
assert_eq!(hx(&o[4].leaf()), "3049a2823be995b9ef123c82675c47e2df96a77b50be6a71d676246325d46a76", "tag 2");
}
#[test]
fn groups_match_the_wai_corpus() {
let key = test_signing_key(0x11);
let (b, objects) = mixed();
let legacy = b.finalize(&key, "did:example:publisher", T1);
assert_eq!(hx(&legacy.merkle_root), "62ceef6eff690aa495e0744b8f17dc530e69ed587f3de45fc3107992ee531f8d");
assert_eq!(
hx(&legacy.signature),
"83a54aac08476e4af1ccdf8b87db5f1685fe20e428a1ab008e596749d08bf877cb9968e1f92a6650c4f7390ff4f207fb38f3fe91cb6f3c25621991914f7e340a"
);
assert_eq!(hx(&legacy.link_hash()), "2e58ff648d74e0ad023c4efe65f69fba6103655f0be027c8cba5f4767548fea5");
let verifier = ReceiptVerifier::new();
let g = verifier.verify_group(&legacy).unwrap();
for (i, o) in objects.iter().enumerate() {
verifier.verify_payload(&envelope(i as u8), o, &g).unwrap();
}
let parts = [(1_200, Some(chip(20_000, 1_000_000))), (800, Some(cal(5_000, 2_000_000, CAL_REF))), (0, Some(vec![0]))];
let (b, objects) = build(10, 4, &parts);
let labelled = b.finalize_labelled(&key, "did:example:publisher", T1, Some(legacy.link_hash())).unwrap();
// OnChipCounter, ⌈(1200·20000 + 800·5000) / 2000⌉ = 14000 ppm over 2 s; Partial.
assert_eq!(labelled.energy_class.as_deref(), Some(chip(14_000, 2_000_000).as_slice()));
assert_eq!(labelled.energy_coverage, 1);
assert_eq!(hx(&labelled.merkle_root), "5a8870c49a7ce232b1a4cd4bced3b0ee70b33535bcb8f9f563f617279ae9ce29");
assert_eq!(
hx(&labelled.signature),
"cd89ea4722e276b4458604faaf7d92a2fb1286c69a62dde021dfaaed9d6fc2975150865e71dffa91526ca52e2844a74197dc87102a5f489dcf32dc3394fd6509"
);
assert_eq!(hx(&labelled.link_hash()), "9c49baff6e40a5d24222e49679dad204f036e356b402449a4047e9bafcdaa7b7");
let g = verifier.verify_group(&labelled).unwrap();
for (i, o) in objects.iter().enumerate() {
verifier.verify_payload(&envelope(10 + i as u8), o, &g).unwrap();
}
}
#[test]
fn a_label_or_the_origin_cannot_be_restated_stripped_or_added() {
let (b, objects) = mixed();
let receipt = b.finalize(&test_signing_key(0x11), "p", T1);
let verifier = ReceiptVerifier::new();
let g = verifier.verify_group(&receipt).unwrap();
let fails = |o: &ObjectReceipt, n: u8| verifier.verify_payload(&envelope(n), o, &g).is_err();
let mut t = objects[0].clone();
t.energy_class = Some(chip(19_000, 1_000_000));
assert!(fails(&t, 0), "restated");
let mut t = objects[0].clone();
t.energy_class = None;
assert!(fails(&t, 0), "stripped");
let mut t = objects[2].clone();
t.energy_class = Some(vec![2]);
assert!(fails(&t, 2), "added");
let mut t = objects[3].clone();
t.origin = "elsewhere.example".into();
assert!(fails(&t, 3), "origin altered");
let mut t = objects[1].clone();
t.joules_micro += 1;
assert!(fails(&t, 1), "figure altered");
// The legacy leaf binds neither.
let mut t = objects[2].clone();
t.joules_micro = 9_999;
t.origin = "elsewhere.example".into();
assert!(!fails(&t, 2));
}
#[test]
fn an_object_must_be_at_its_position() {
let mut b = ReceiptBuilder::new(TrackAlias(0), GroupId(0));
for i in 0..3u8 {
b.add_object(h(i), 1, "dc1", "t");
}
let proofs: Vec<_> = (0..3).map(|i| b.proof_for(i).unwrap()).collect();
let receipt = b.finalize(&test_signing_key(0x05), "p", "t");
let verifier = ReceiptVerifier::new();
let g = verifier.verify_group(&receipt).unwrap();
let obj = |i: u64, proof: MerkleProof, content: [u8; 32]| ObjectReceipt {
track_alias: TrackAlias(0),
group_id: GroupId(0),
object_id: ObjectId(i),
content_hash: content,
joules_micro: 1,
origin: "dc1".into(),
merkle_proof: proof,
energy_class: None,
};
verifier.verify_object(&obj(2, proofs[2].clone(), h(2)), &g).unwrap();
// The duplicated last leaf of the odd level, presented past the end.
let mut past = proofs[2].clone();
past.leaf_index = 3;
past.direction_bits = 3;
assert!(verifier.verify_object(&obj(3, past, h(2)), &g).is_err());
// An object at its neighbour's index, with its own proof.
let mut moved = proofs[0].clone();
moved.leaf_index = 1;
assert!(verifier.verify_object(&obj(1, moved, h(0)), &g).is_err());
// object_id other than the index.
assert!(verifier.verify_object(&obj(0, proofs[1].clone(), h(1)), &g).is_err());
}
#[test]
fn labels_that_break_the_rules_are_refused() {
let h0 = h(1);
let mut b = ReceiptBuilder::new(TrackAlias(0), GroupId(0));
assert!(b.add_labelled_object(h0, 100, "o", "t", vec![1]).is_err(), "legacy HwShunt");
assert!(b.add_labelled_object(h0, 0, "o", "t", vec![2]).is_err(), "a class beside zero");
assert!(b.add_labelled_object(h0, 5, "o", "t", vec![0]).is_err(), "the unmetered marker beside a figure");
assert!(b.add_labelled_object(h0, 1, "o", "t", chip(288_675, 1)).is_err(), "below the resolution floor");
assert!(b.add_labelled_object(h0, 1, "o", "t", vec![4, 0]).is_err(), "unreadable evidence");
assert!(b.add_labelled_object(h0, 1, "o", "t", chip(288_676, 1)).is_ok());
// A labelled group whose parts compose to no class is refused.
let mut b = ReceiptBuilder::new(TrackAlias(0), GroupId(0));
b.add_labelled_object(h(1), 10, "o", "t", vec![2]).unwrap();
b.add_object(h(2), 10, "o", "t");
assert!(b.finalize_labelled(&test_signing_key(0x05), "p", "t", None).is_err());
assert_eq!(compose_group_class(&[(1, Some(&cal(300_000, 1, "a")[..])), (1, Some(&cal(300_000, 1, "b")[..]))]), None);
}
#[test]
fn a_labelled_group_signs_its_class_coverage_and_parent() {
let key = test_signing_key(0x11);
let mut b = ReceiptBuilder::new(TrackAlias(0), GroupId(0));
b.add_labelled_object(h(1), 100, "o", "t", vec![2]).unwrap();
let g = b.finalize_labelled(&key, "p", "t", Some([3; 32])).unwrap();
let verifier = ReceiptVerifier::new();
verifier.verify_group(&g).unwrap();
let mut t = g.clone();
t.energy_class = Some(vec![3]);
assert!(verifier.verify_group(&t).is_err(), "class");
let mut t = g.clone();
t.energy_coverage = 1;
assert!(verifier.verify_group(&t).is_err(), "coverage");
let mut t = g.clone();
t.parent_receipt_hash = None;
assert!(verifier.verify_group(&t).is_err(), "parent");
// A verifier that predates the labelled form computes the legacy
// payload, and refuses the signature.
let mut legacy = g.clone();
legacy.energy_class = None;
assert!(verifier.verify_group(&legacy).is_err());
// The JSON of a legacy receipt is unchanged: the new members are omitted.
let (b, _) = build(0, 3, &[(5, None)]);
let r = b.finalize(&key, "p", T1);
let j = serde_json::to_string(&r).unwrap();
assert!(
!j.contains("energy_class") && !j.contains("energy_coverage") && !j.contains("parent_receipt_hash"),
"{j}"
);
assert_eq!(serde_json::from_str::<GroupReceipt>(&j).unwrap(), r, "and reads back");
// A legacy group's signature covers no coverage: a value beside it is refused.
let mut t = r.clone();
t.energy_coverage = 1;
assert!(verifier.verify_group(&r).is_ok());
assert!(verifier.verify_group(&t).is_err(), "coverage on a legacy group");
let jt = serde_json::to_string(&t).unwrap();
assert!(jt.contains("\"energy_coverage\":1"), "{jt}");
assert!(verifier.verify_group(&serde_json::from_str(&jt).unwrap()).is_err());
}
}

View file

@ -684,6 +684,7 @@ mod tests {
joules_micro: joules,
origin: origin.to_string(),
merkle_proof: proof,
energy_class: None,
};
write_object_to_uni(&mut uni, &header, Some(&receipt), &payload)
.await

View file

@ -0,0 +1,317 @@
//! joule-moq against every leaf, group and object vector of WAI's JWP corpus
//! (`wai/jwp-conformance/vectors.json`, jwp-receipts §5.7).
//!
//! The corpus is generated by WAI's reference implementation and checked by an
//! independent script; this file reads it as a third implementation. Each
//! vector's receipt is read from the corpus's JSON form into this crate's
//! types, and the outcome compared:
//!
//! - a `leaf` vector: the leaf this crate computes;
//! - a `group` vector: accepted with the corpus's signing payload and link hash,
//! or refused where the corpus refuses it at §3 step 1 or 2. Step 3 (the
//! parent chain) is the holder's, not this crate's, so a group the corpus
//! refuses there is one this crate accepts;
//! - an `object` vector: accepted against its verified group with its envelope,
//! or refused at any step.
//!
//! A refusal while reading the JSON counts as a refusal: it is where the corpus
//! refuses an unreadable member (§3 step 1, step 4 for an object).
//!
//! The corpus lives beside this crate in the monorepo. If it cannot be read the
//! test fails; it never passes on nothing.
use joule_moq::{GroupId, GroupReceipt, MerkleProof, ObjectId, ObjectReceipt, ReceiptVerifier, TrackAlias};
use serde_json::{Map, Value};
const CORPUS: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../../../wai/jwp-conformance/vectors.json");
const CLASS_KEYS: [&str; 4] = ["energy_provenance", "energy_uncertainty", "energy_counter_filtering", "energy_calibration_ref"];
const GROUP_MEMBERS: [&str; 12] = [
"kind",
"track_alias",
"group_id",
"root_hash",
"leaf_count",
"joules_total",
"group_start",
"group_end",
"signer_pubkey",
"signer_id",
"sig",
"parent_receipt_hash",
];
const OBJECT_MEMBERS: [&str; 8] =
["kind", "track_alias", "group_id", "object_id", "content_hash", "joules_micro", "origin", "merkle_proof"];
fn unhex(v: &Value) -> Option<Vec<u8>> {
let s = v.as_str()?;
if s.len() % 2 != 0 || !s.bytes().all(|c| matches!(c, b'0'..=b'9' | b'a'..=b'f')) {
return None;
}
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok()).collect()
}
fn hash(v: &Value) -> Option<[u8; 32]> {
unhex(v)?.try_into().ok()
}
fn hx(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
fn present(m: &Map<String, Value>, k: &str) -> bool {
!matches!(m.get(k), None | Some(Value::Null))
}
/// A class in energy-measurement §2.7's JSON form, as its signed encoding: the
/// tag and its evidence. Tag 0 (the unmetered marker) where no class is
/// named. `None` where the members are not one class.
fn class_bytes(m: &Map<String, Value>) -> Option<Vec<u8>> {
if !present(m, "energy_provenance") {
return (!CLASS_KEYS.iter().any(|k| present(m, k))).then(|| vec![0]);
}
let uncertainty = |out: &mut Vec<u8>| -> Option<()> {
let u = m.get("energy_uncertainty")?.as_object()?;
if u.len() != 2 {
return None;
}
let ppm = u32::try_from(u.get("relative_ppm")?.as_u64()?).ok()?;
let window = u.get("window_us")?.as_u64()?;
out.extend_from_slice(&ppm.to_be_bytes());
out.extend_from_slice(&window.to_be_bytes());
Some(())
};
match m.get("energy_provenance")?.as_str()? {
label @ ("HwShunt" | "ModelBased" | "Estimator") => {
if ["energy_uncertainty", "energy_counter_filtering", "energy_calibration_ref"].iter().any(|k| present(m, k)) {
return None;
}
Some(vec![match label {
"HwShunt" => 1,
"ModelBased" => 2,
_ => 3,
}])
}
"OnChipCounter" => {
if present(m, "energy_calibration_ref") {
return None;
}
let mut out = vec![4];
uncertainty(&mut out)?;
out.push(match m.get("energy_counter_filtering") {
None | Some(Value::Null) => 0,
Some(f) => match f.as_str()? {
"Unknown" => 0,
"Off" => 1,
"On" => 2,
_ => return None,
},
});
Some(out)
}
"CalibratedInstrument" => {
if present(m, "energy_counter_filtering") {
return None;
}
let mut out = vec![5];
uncertainty(&mut out)?;
let r = m.get("energy_calibration_ref")?.as_str()?;
out.extend_from_slice(&(r.len() as u32).to_be_bytes());
out.extend_from_slice(r.as_bytes());
Some(out)
}
_ => None,
}
}
/// The corpus's group JSON as a [`GroupReceipt`], or `None` where its reading
/// refuses it (§3 step 1).
fn group_of(v: &Value) -> Option<GroupReceipt> {
let m = v.as_object()?;
if m.get("kind")?.as_str()? != "group" {
return None;
}
let label_members: Vec<&str> = std::iter::once("sign_version").chain(CLASS_KEYS).chain(["energy_coverage"]).collect();
if GROUP_MEMBERS.iter().any(|k| !m.contains_key(*k))
|| m.keys().any(|k| !GROUP_MEMBERS.contains(&k.as_str()) && !label_members.contains(&k.as_str()))
{
return None;
}
let s = |k: &str| m.get(k).and_then(Value::as_str).map(str::to_owned);
let u = |k: &str| m.get(k).and_then(Value::as_u64);
let signature = unhex(m.get("sig")?)?;
if signature.len() != 64 {
return None;
}
let (energy_class, energy_coverage) = match m.get("sign_version") {
None | Some(Value::Null) => {
if label_members.iter().any(|k| present(m, k)) {
return None;
}
(None, 0)
}
Some(Value::Number(n)) if n.as_u64() == Some(2) => {
let c = class_bytes(m)?;
let coverage = match (c[0], m.get("energy_coverage")) {
(0, None | Some(Value::Null)) => 0,
(0, _) => return None,
(_, Some(Value::String(c))) if c == "Whole" => 0,
(_, Some(Value::String(c))) if c == "Partial" => 1,
_ => return None,
};
(Some(c), coverage)
}
Some(_) => return None,
};
Some(GroupReceipt {
track_alias: TrackAlias(u("track_alias")?),
group_id: GroupId(u("group_id")?),
merkle_root: hash(m.get("root_hash")?)?,
leaf_count: u("leaf_count")?,
total_joules_micro: u("joules_total")?,
group_start: s("group_start")?,
group_end: s("group_end")?,
publisher_key: hash(m.get("signer_pubkey")?)?,
publisher_id: s("signer_id")?,
signature,
energy_class,
energy_coverage,
parent_receipt_hash: match m.get("parent_receipt_hash")? {
Value::Null => None,
h => Some(hash(h)?),
},
})
}
/// The corpus's object JSON as an [`ObjectReceipt`], or `None` where its
/// reading refuses it (§3 steps 4–5).
fn object_of(v: &Value) -> Option<ObjectReceipt> {
let m = v.as_object()?;
if m.get("kind")?.as_str()? != "object" {
return None;
}
let label_members: Vec<&str> = std::iter::once("leaf_version").chain(CLASS_KEYS).collect();
if OBJECT_MEMBERS.iter().any(|k| !m.contains_key(*k))
|| m.keys().any(|k| !OBJECT_MEMBERS.contains(&k.as_str()) && !label_members.contains(&k.as_str()))
{
return None;
}
let energy_class = match m.get("leaf_version") {
None | Some(Value::Null) => {
if CLASS_KEYS.iter().any(|k| present(m, k)) {
return None;
}
None
}
Some(Value::Number(n)) if n.as_u64() == Some(2) => Some(class_bytes(m)?),
Some(_) => return None,
};
let p = m.get("merkle_proof")?.as_object()?;
if p.len() != 3 {
return None;
}
Some(ObjectReceipt {
track_alias: TrackAlias(m.get("track_alias")?.as_u64()?),
group_id: GroupId(m.get("group_id")?.as_u64()?),
object_id: ObjectId(m.get("object_id")?.as_u64()?),
content_hash: hash(m.get("content_hash")?)?,
joules_micro: m.get("joules_micro")?.as_u64()?,
origin: m.get("origin")?.as_str()?.to_owned(),
merkle_proof: MerkleProof {
siblings: p.get("siblings")?.as_array()?.iter().map(hash).collect::<Option<Vec<_>>>()?,
direction_bits: p.get("direction_bits")?.as_u64()?,
leaf_index: p.get("leaf_index")?.as_u64()?,
},
energy_class,
})
}
#[test]
fn every_leaf_group_and_object_vector_of_the_wai_corpus() {
let text = std::fs::read_to_string(CORPUS).unwrap_or_else(|e| panic!("read {CORPUS}: {e}"));
let doc: Value = serde_json::from_str(&text).expect("the corpus is JSON");
let vectors = doc["vectors"].as_array().expect("vectors");
let verifier = ReceiptVerifier::new();
let (mut ran, mut failures) = (std::collections::BTreeMap::<&str, usize>::new(), Vec::new());
for v in vectors {
let name = v["name"].as_str().expect("name");
let procedure = v["procedure"].as_str().expect("procedure");
let input = &v["input"];
let expected = &v["expected"];
let accepts = expected["outcome"] == "accept";
let outcome: Result<(), String> = match procedure {
"leaf" => {
let got = object_of(&input["object"]).map(|o| hx(&o.leaf()));
let want = expected["leaf"].as_str().map(str::to_owned);
if got == want { Ok(()) } else { Err(format!("leaf {got:?}, the corpus {want:?}")) }
}
"group" => {
let step = expected["step"].as_u64();
let verified = group_of(&input["group"]).ok_or("unreadable".to_string()).and_then(|g| {
verifier.verify_group(&g).map(|_| g).map_err(|e| e.to_string())
});
match (accepts || step == Some(3), verified) {
(true, Ok(g)) if accepts => {
let pl = hx(&g.signing_payload());
let lh = hx(&g.link_hash());
if Some(pl.as_str()) != expected["signing_payload"].as_str() {
Err(format!("signing payload {pl}"))
} else if Some(lh.as_str()) != expected["link_hash"].as_str() {
Err(format!("link hash {lh}"))
} else {
Ok(())
}
}
(true, Ok(_)) => Ok(()),
(true, Err(e)) => Err(format!("refused ({e}); the corpus passes steps 1-2")),
(false, Err(_)) => Ok(()),
(false, Ok(_)) => Err(format!("accepted; the corpus refuses at step {step:?}")),
}
}
"object" => {
let envelope = unhex(&input["envelope"]).expect("envelope hex");
let r = group_of(&input["group"])
.ok_or("group unreadable".to_string())
.and_then(|g| verifier.verify_group(&g).map_err(|e| e.to_string()))
.and_then(|vg| {
let o = object_of(&input["object"]).ok_or("object unreadable".to_string())?;
verifier.verify_payload(&envelope, &o, &vg).map_err(|e| e.to_string())
});
match (accepts, r) {
(true, Ok(())) | (false, Err(_)) => Ok(()),
(true, Err(e)) => Err(format!("refused ({e}); the corpus accepts")),
(false, Ok(())) => Err(format!("accepted; the corpus refuses: {expected}")),
}
}
// The group total and the WAI_RECEIPT property are readings this crate
// does not give.
_ => continue,
};
*ran.entry(procedure).or_default() += 1;
if let Err(e) = outcome {
failures.push(format!("{name}: {e}"));
}
}
assert!(failures.is_empty(), "{} vector(s) disagree:\n{}", failures.len(), failures.join("\n"));
for p in ["leaf", "group", "object"] {
assert!(ran.get(p).copied().unwrap_or(0) > 0, "no {p} vector ran: {ran:?}");
}
}
/// The corpus's non-canonical key vector, pinned: a key whose `y` is p + 3 is
/// refused as a key, not reached as a signature.
#[test]
fn a_non_canonical_publisher_key_is_refused_as_a_key() {
let text = std::fs::read_to_string(CORPUS).unwrap_or_else(|e| panic!("read {CORPUS}: {e}"));
let doc: Value = serde_json::from_str(&text).expect("the corpus is JSON");
let v = doc["vectors"]
.as_array()
.expect("vectors")
.iter()
.find(|v| v["name"] == "group_signer_key_non_canonical")
.expect("the corpus holds group_signer_key_non_canonical");
assert_eq!(v["expected"]["reason"], "bad_signer_key");
let g = group_of(&v["input"]["group"]).expect("its JSON reads");
let e = ReceiptVerifier::new().verify_group(&g).unwrap_err().to_string();
assert!(e.contains("publisher key"), "{e}");
}

View file

@ -264,6 +264,130 @@ wire format may change and such changes are recorded here.
second model-identity construct; it is to fold into the pin once that renderer
is registered as decoding against a parameter set.
### Energy reports, labelled object leaves, and what the signatures cover
- **Reports about a figure.** Two claim classes, [`extensions/jwp-receipts.md`](extensions/jwp-receipts.md)
§2.4.8–§2.4.9, report on one energy figure without changing the receipt that seals
it. A claim names that receipt by a **figure profile** — the receipt's identity domain
less its version byte, so the registry is the code's own
([`extensions/energy-measurement.md`](extensions/energy-measurement.md) §6.1) — and its
identity, and restates the figure with its class. `measurement` carries the bracket's
values as integers with each bracket's liveness facts, the build (profile, toolchain,
target) and the power reader; only the key that signed the figure may sign it, so a
figure no Ed25519 key signs (an energy binding's) has no `measurement` claim. A
hardware figure's report must name its power reader (`measurement_no_power_reader`),
with or without a bracket, and a `null` bracket says the figure was not obtained under
the bracketed protocol. Because only §3.1 step 6 shows the signer is the figure's, a
`measurement` claim is accepted only against the receipt that carries its figure:
checked without it, it is `figure_not_held`, and `ClaimReceipt::attribution` gives a
verdict only for a claim accepted against that figure.
`operating-point` records the requested, committed and delivered operating points,
from a vocabulary named by URI, and a prediction. energy-measurement §6 makes the
`measurement` claim the carrier of a class-bearing figure's report and lists the
producers that fall short.
- **Exact report arithmetic.** energy-measurement §6.2 gives the figure and the least
declaration a report's integers support as exact integer formulas, at whatever width
their products need. Producers seal from the same integers; computed from unrounded
samples, a declaration could come out one ppm below what the published integers
support. `wai-rs` `energy_report` implements it with no floating-point arithmetic, and
`claim-conformance/claim_verify.py` recomputes the pinned brackets with Python's
integers.
- **Derived, never stated.** The §4 ratio and verdict, whether a commitment was met,
and a prediction's residual are derived by every reader from what a claim carries. A
report carrying its ratio is refused (`unknown_member`).
- **A live power reader.** energy-measurement §3.1: a rail is live in a bracket only if
it advanced in two consecutive intervals, in at least five and in at least four of
every five, over at least 300 ms, in both brackets; a figure from a bracket that is not
live is never sealed under a hardware class. A power sample advances only when it is
above zero and differs from the sample before it, so a reader that holds its last
value never advances; a reader of a report refuses, under a hardware class, a bracket
whose `sd_uw` is 0 whatever liveness facts it carries. A frozen rail, a stale value
held, one batch refresh, and one refresh published in two steps all used to give a
flat idle bracket and an infinite ratio that sealed as `OnChipCounter`; `energy_burst`
and `energy_meter` now refuse them, with fault-injection tests, and a counter meter reads its counter during a span
rather than only at its ends. On a host whose reader reports a rail that does not
advance, the reference meters seal unmetered and say why.
- **The unmetered marker.** energy-measurement §2: an unmetered figure is not a figure
of zero joules. A `0` that marks unmetered is never presented, totalled or compared as
0 J; a total over parts that include one covers only the measured parts; new forms
mark unmetered with class tag `0`. quantum-sim, provenance, interactive-worlds,
moq-streaming-format and SPEC read `0` this way. `ModelMeter` returns no reading rather
than a labelled zero or a saturated product, and `wai-quantum-serve`'s `POST /verify`
renders an unmetered receipt as `"joules_micro": null, "metered": false`.
- **The labelled JWP leaf.** An object's class rides in its Merkle leaf,
`BLAKE3("moq-jwp:merkle-leaf\x02" ‖ content_hash ‖ u64_be(joules_micro) ‖ class ‖
u32_be(len(origin)) ‖ origin)`, so the group signature covers the object's figure, its
class with its evidence, and its origin (jwp-receipts §2.2, §2.4.2). A label, or the
origin, can be neither added, stripped nor altered in transit.
- **The labelled group payload.** `moq-jwp:group-sign\x02` signs the legacy fields and
then the total's class, its coverage and the parent link (§2.3, §2.4.1); the legacy
payload never signed the parent. The class is composed from the objects' classes by an
integer rule — the weakest class, and for a hardware class `⌈Σ F·p / Σ F⌉` ppm over the
longest window — and a verifier holding every object checks it (§3, *The group
total*), alongside the sum.
- **Position.** An object must sit at the position its inclusion proof walks (§3 step
5, `proof_position_mismatch`), which closes the duplicated last leaf of an odd level
and an object presented at another object's index — the latter could hide a weaker
class from the group total. Group signatures are held to the strict Ed25519 rule
claims use. §3.3 lists the delivery reasons; a group's link hash is defined over its
signing payload and signature.
- **`WAI_RECEIPT`** ([`extensions/moq-streaming-format.md`](extensions/moq-streaming-format.md)
§5): `0` is the unmetered marker, `group_receipt` is the group's link hash, and the
property's class reads as signed only where it equals the object's leaf label or an
accepted `energy` claim; otherwise it is discarded (`property_disagrees`) or read as
unlabelled. A class read as signed names the key that signed it — the group's for a
leaf, the claim's for a claim — so a class a relay signs into the property under its
own key is never read as the publisher's.
- **What stays unverifiable.** energy-measurement §5 and energy-binding §3 now say it is
the measurement — the joules and every field describing how they were obtained — not
one number, and that a report's values can be checked for consistency, which is not
truth. energy-binding §7.4 no longer says green metadata is registered; it is not yet.
- **Implementation.** `wai-rs` (feature `receipt_claims`): `jwp_delivery` reads and
verifies both forms of each JWP receipt, the group total and the property;
`energy_figure` holds the profile registry and views every figure-bearing receipt
family, the quantum receipts through `MaybeLabelled`; the reference meters write a
measurement report beside the receipt by default (`--no-report` to opt out;
`--previous-report` with no file is a usage error), and `wai_prov` presents its total as
`Partial` over the measured steps, or `UNMETERED`, never as 0 µJ. joule-code's
`joule-moq` (0.2.0) builds and verifies both labelled forms, checks position, holds
publisher keys and group signatures to the strict rule (a non-canonical key encoding is
refused as a key), refuses an unsigned coverage on a legacy group, and runs every
leaf, group and object vector of the new `jwp-conformance/` corpus.
- **Conformance and CI.** `claim-conformance/` gains vectors for every new reason, the
arithmetic at its boundaries, liveness faults (a held value among them) and the
labelled leaf; the earlier vectors are unchanged and pinned. `jwp-conformance/` holds the JWP corpus
(`wai_jwp_vectors`) and `jwp_verify.py`, an independent verifier. CI now runs
`claim_verify.py`, `differential.py` (fixed seed) and `jwp_verify.py` on the linux
runner, and the JWP corpus on every platform of the conformance matrix. The
conformance build includes `receipt_claims`, so `wai_meter` and `wai_world_meter`
are built on every platform of the conformance matrix; `wai-energy-receipts.yml`
builds those two, `wai_video_meter` and `wai_quantum_meter` on linux, linux-arm64
and macos; no workflow builds `wai_prov`, which needs `codecs`. joule-code's CI runs
`joule-moq` with its QUIC transport.
- **Build.** Every reference meter bin (`wai_meter`, `wai_world_meter`,
`wai_video_meter`, `wai_quantum_meter`, `wai_prov`) now also requires the
`receipt_claims` feature, because it writes a measurement report. Cargo skips a bin
whose required features are off without an error, so a build that names only the
older features (`--features video_receipt`, say) no longer produces that meter; add
`receipt_claims`.
**Wire.** Every receipt issued in a legacy form keeps its bytes and verifies as before:
v1 groups, `\x01` leaves, every claim and corpus vector already pinned, `wai-quantum`'s
receipts (no `wai-quantum` code changes). The new forms are refused by software that
predates them: a verifier or relay refuses a labelled group at §3 step 2 and a labelled
object at §3 step 5, and a claim verifier refuses `measurement` and `operating-point` as
`unknown_class`. A group's `sign_version` other than 2 is `group_label_invalid`.
**External JWP implementations must adopt the `\x02` leaf and the labelled group payload
before verifying objects or groups that use them, and hold publisher keys to the
canonical-encoding rule**; the in-tree `joule-moq` does, and its public API changes are
listed in joule-code's CHANGELOG. `wai-quantum-serve` (0.4.0) changes the `POST /verify`
response: an unmetered receipt renders as `"joules_micro": null, "metered": false`
where it rendered its marker as the integer `0`, and every other receipt adds
`"metered": true`; a client that reads `joules_micro` as an integer must accept
`null`. New meter receipts can declare one ppm more than before, because they
seal from the published integers, and a meter now refuses a rail that is not live where
it used to seal a figure.
### Typed receipt classes, and a second (soft) binding class
- [`extensions/jwp-receipts.md`](extensions/jwp-receipts.md) §2.4.3–§2.4.7: every

View file

@ -512,11 +512,11 @@ registration and makes no conforming envelope non-conforming (§8).
| `wai.neural.dcvc_fm` | DCVC-FM (feature modulation), wide single-model quality range (RGB + YUV) | a wide single-model quality range; shares the inter-frame route | research code, `neural/video_dcvc.rs` |
| `wai.video.int_motion` | DETERMINISTIC INTEGER inter-frame — I-frame + integer block motion-compensated P-frames + integer-quantized residual over rANS, no float/ONNX; motion is whole-pel or **half-pel** (`mv_precision`), the latter predicting at the half-grid by integer bilinear interpolation (`(a+b+1)>>1`, `(a+b+c+d+2)>>2`) so real footage — which pans a non-integer number of pixels per frame — costs fewer residual bits while staying byte-exact | byte-identical frame-by-frame on every machine (*frame-state-equivalence*, a per-frame portable hash); the temporal extension of the integer contract, where a float inter-frame decode desyncs down the GOP | `int_video.rs` (whole-pel `tools/wai_int_video_encode.py`; half-pel from real footage `tools/wai_int_video_real_encode.py`) |
| `wai.video.int_hyper` | NEURAL-KEYFRAME inter-frame — the I-frame is the LEARNED integer codec `int_hyper` decoded byte-exact, the P-frames are integer block-MC residuals on top | a learned keyframe you can put in a receipt + integer inter-frames; whole decode integer, byte-identical everywhere | `int_video::decode_pframes` ∘ `int_hyper_synth::decode_conditional` |
| `wai.video.manifest` | ATTESTED PER-SESSION MANIFEST — the OTT delivery layer, not a codec: a personalised HLS manifest (ads stitched at `EXT-X-DISCONTINUITY`, blackouts + geo enforced) rendered as a **pure function of the session *class*** `(content, entitlement, geo, blackout set, ad pod, window)` rather than the viewer, and sealed in an Ed25519 receipt that binds the JCP grant (capability, joule ceiling, `funds_ceiling` = ad budget), the composition, the stitched ads and the policy applied | **manifest-equivalence**: `manifest_hash == hash(render(segments))` and `session_class_hash` are recomputable by anyone holding the bytes, so the ads, blackouts and compose-work stop being dashboard claims; class-determinism means one generation serves every viewer of the class, and `uj_per_viewer` is the amortised energy that a 1:1-unique generator cannot report below its per-viewer cost. Only `joules_micro` is measured-attested | `wai_video_manifest.rs` |
| `wai.video.ssai` | ATTESTED SERVER-SIDE AD INSERTION — a **deterministic** ad decision (greedy by price within the avail duration and the funds remaining) plus a per-impression Ed25519 receipt binding the candidate pod, the manifest it belongs to, the served `creative_hash`es and the VAST quartile each reached | **spend-equivalence**: `budget_spent_micros` is recomputed from the *billable* impressions and rejected if it exceeds the grant's `funds_ceiling` — an overspent receipt fails verification; every creative and quartile is checkable, so a buyer and seller reconcile impressions from the same bytes instead of two logs that only a dashboard joins. Only `joules_micro` is measured-attested | `wai_video_ssai.rs` |
| `wai.video.keys` | ATTESTED CONTENT-KEY RELEASE — the DRM licensing decision as a record: every key release, and every refusal, is an Ed25519 receipt binding the KID, the DRM system, the entitlement demanded and the window enforced, with a BLAKE3 `key_commit` proving *which* key was released without carrying it | **decision-equivalence**: `verify()` re-runs the release policy over the receipt's own `(grant capability, entitlement, issued, expiry)` and rejects any misreported outcome, and enforces granted-iff-committed — so "keys went only to entitled, unexpired sessions" is checkable against the signed log, not asserted. Only `joules_micro` is measured-attested | `wai_video_keys.rs` |
| `wai.video.package` | DETERMINISTIC PACKAGING + ATTESTED SEGMENTMAP — a source sliced into content-hashed segments on GOP boundaries; the ordered `(index → content_hash)` map is a signed Merkle commitment, and identical content (ads, bumpers, replays, simulcast overlap) dedups to one stored/encoded copy | **map-equivalence + verifiable dedup**: `segment_map_root` lets a `wai.video.manifest` reference be checked against the package that produced it, and `verify()` recomputes `unique_segments`/`stored_bytes` from the segments so the storage/encode saving cannot be overstated; `uj_per_unique_segment` is the encode-once energy. Only `joules_micro` is measured-attested | `wai_video_package.rs` |
| `wai.video.channel` | VIRTUAL LINEAR / FAST ASSEMBLY — a continuous schedule of assets over wall-clock time, sliced into attested playback windows; windows chain via `parent_receipt_hash`, so a channel's history is a hash chain of program-as-run records | **schedule-continuity + recomputable program-as-run**: `verify()` rejects a schedule with a gap or overlap (no dead air, no double-booking) and recomputes `aired` from the schedule and the window, so what actually aired is checkable by a licensor, advertiser or regulator against the signed chain rather than an operator-held play-out log. Only `joules_micro` is measured-attested | `wai_video_channel.rs` |
| `wai.video.manifest` | ATTESTED PER-SESSION MANIFEST — the OTT delivery layer, not a codec: a personalised HLS manifest (ads stitched at `EXT-X-DISCONTINUITY`, blackouts + geo enforced) rendered as a **pure function of the session *class*** `(content, entitlement, geo, blackout set, ad pod, window)` rather than the viewer, and sealed in an Ed25519 receipt that binds the JCP grant (capability, joule ceiling, `funds_ceiling` = ad budget), the composition, the stitched ads and the policy applied | **manifest-equivalence**: `manifest_hash == hash(render(segments))` and `session_class_hash` are recomputable by anyone holding the bytes, so the ads, blackouts and compose-work stop being dashboard claims; class-determinism means one generation serves every viewer of the class, and `uj_per_viewer` is the amortised energy that a 1:1-unique generator cannot report below its per-viewer cost. Only the measurement — `joules_micro`, its acquisition class and the class's evidence — is measured-attested: signed, and not re-measurable | `wai_video_manifest.rs` |
| `wai.video.ssai` | ATTESTED SERVER-SIDE AD INSERTION — a **deterministic** ad decision (greedy by price within the avail duration and the funds remaining) plus a per-impression Ed25519 receipt binding the candidate pod, the manifest it belongs to, the served `creative_hash`es and the VAST quartile each reached | **spend-equivalence**: `budget_spent_micros` is recomputed from the *billable* impressions and rejected if it exceeds the grant's `funds_ceiling` — an overspent receipt fails verification; every creative and quartile is checkable, so a buyer and seller reconcile impressions from the same bytes instead of two logs that only a dashboard joins. Only the measurement — `joules_micro`, its acquisition class and the class's evidence — is measured-attested: signed, and not re-measurable | `wai_video_ssai.rs` |
| `wai.video.keys` | ATTESTED CONTENT-KEY RELEASE — the DRM licensing decision as a record: every key release, and every refusal, is an Ed25519 receipt binding the KID, the DRM system, the entitlement demanded and the window enforced, with a BLAKE3 `key_commit` proving *which* key was released without carrying it | **decision-equivalence**: `verify()` re-runs the release policy over the receipt's own `(grant capability, entitlement, issued, expiry)` and rejects any misreported outcome, and enforces granted-iff-committed — so "keys went only to entitled, unexpired sessions" is checkable against the signed log, not asserted. Only the measurement — `joules_micro`, its acquisition class and the class's evidence — is measured-attested: signed, and not re-measurable | `wai_video_keys.rs` |
| `wai.video.package` | DETERMINISTIC PACKAGING + ATTESTED SEGMENTMAP — a source sliced into content-hashed segments on GOP boundaries; the ordered `(index → content_hash)` map is a signed Merkle commitment, and identical content (ads, bumpers, replays, simulcast overlap) dedups to one stored/encoded copy | **map-equivalence + verifiable dedup**: `segment_map_root` lets a `wai.video.manifest` reference be checked against the package that produced it, and `verify()` recomputes `unique_segments`/`stored_bytes` from the segments so the storage/encode saving cannot be overstated; `uj_per_unique_segment` is the encode-once energy. Only the measurement — `joules_micro`, its acquisition class and the class's evidence — is measured-attested: signed, and not re-measurable | `wai_video_package.rs` |
| `wai.video.channel` | VIRTUAL LINEAR / FAST ASSEMBLY — a continuous schedule of assets over wall-clock time, sliced into attested playback windows; windows chain via `parent_receipt_hash`, so a channel's history is a hash chain of program-as-run records | **schedule-continuity + recomputable program-as-run**: `verify()` rejects a schedule with a gap or overlap (no dead air, no double-booking) and recomputes `aired` from the schedule and the window, so what actually aired is checkable by a licensor, advertiser or regulator against the signed chain rather than an operator-held play-out log. Only the measurement — `joules_micro`, its acquisition class and the class's evidence — is measured-attested: signed, and not re-measurable | `wai_video_channel.rs` |
**Sink-architecture note:** the inter-frame neural video codecs (DCVC-RT,
DCVC-FM) decode through `ort` and are **CPU-capable** for native sinks —
@ -538,11 +538,14 @@ 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; the
Merkle roots, Ed25519 group signatures, and cross-hop preservation; an
object's figure, acquisition class and origin bound into its Merkle leaf,
and a group total's class signed under a labelled group payload; the
typed receipt classes beside delivery (`energy`, `lawful-basis`,
`log-inclusion`) carried as `claim` receipts; and a second, weaker
binding class (a registry-named soft binding with a resolution endpoint)
whose recovered receipts attest the ancestor, never the object in hand.
`log-inclusion`, `measurement`, `operating-point`) carried as `claim`
receipts; and a second, weaker binding class (a registry-named soft
binding with a resolution endpoint) whose recovered receipts attest the
ancestor, never the object in hand.
See [`extensions/energy-measurement.md`](extensions/energy-measurement.md) for
how a receipt's `joules_micro` is acquired: the required acquisition class
(`CalibratedInstrument` / `OnChipCounter` / `ModelBased` / `Estimator`, with the
@ -551,10 +554,13 @@ standard relative uncertainty and its integration window, never less than the
`0.5/√3` µJ (≈ 0.289 µJ) that rounding the figure to whole microjoules adds, so
`joules_micro × relative_ppm ≥ 288 676` — that both hardware classes must carry, the counter filtering state and calibration reference that
distinguish them, the bracketed marginal protocol (a burst whose window opens
from rest, divided by the work the window paid for), the attribution-quality
rule that decides whether a figure may be published at all, and how the receipt
families that sealed a figure before the class existed carry it as an optional
signed label that leaves every issued receipt's bytes unchanged.
from rest, divided by the work the window paid for, from brackets in which the
power reader's rail was live), the attribution-quality rule that decides whether
a figure may be published at all, the signed measurement report that publishes a
figure's bracket, build and power reader beside the receipt it names, and how the
receipt families that sealed a figure before the class existed carry it as an
optional signed label that leaves every issued receipt's bytes unchanged. A
`joules_micro` of `0` is the unmetered marker, never a figure of zero joules.
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.
@ -912,9 +918,14 @@ A WAI relay or multiplexer that carries an encrypted payload MUST treat
it as opaque: it MUST NOT require the plaintext to route, meter, or
attest, and MUST NOT attempt capability dispatch on ciphertext. The JWP
receipt chain ([`extensions/jwp-receipts.md`](extensions/jwp-receipts.md))
commits over the **ciphertext** content hash, so per-object delivery,
energy, and integrity are attested end-to-end without any relay holding
the key. Content secrecy (SFrame/MLS) and delivery attestation (JWP) are
commits over the **ciphertext** content hash, so per-object delivery and
integrity are attested end-to-end without any relay holding the key. Each
object's figure, class and origin are signed where its leaf is labelled, and
the group total, its class, its coverage and the parent link are signed under
the labelled group payload (jwp-receipts §2.2–§2.3). An object's figure and
origin beside a legacy leaf are the publisher's statement, unsigned beside the
signed bytes; a legacy group payload signs the total, but no class, coverage or
parent link. Content secrecy (SFrame/MLS) and delivery attestation (JWP) are
independent layers over the same bytes and compose: a relay proves it
forwarded the exact sealed bytes it was given while never learning what
they say.

View file

@ -1,9 +1,9 @@
# claim-conformance
Conformance for [typed receipt classes and the soft binding class](../extensions/jwp-receipts.md)
(§2.4.3–§2.4.7, §2.6, §3.1–§3.2): `claim` receipts of class `energy`,
`lawful-basis`, `log-inclusion`, `soft-binding` and `binding-recovery`, and recovery
through a soft binding under the ancestor rule.
(§2.4.3–§2.4.9, §2.6, §3.1–§3.2): `claim` receipts of class `energy`,
`lawful-basis`, `log-inclusion`, `soft-binding`, `binding-recovery`, `measurement` and
`operating-point`, and recovery through a soft binding under the ancestor rule.
Each vector declares **one outcome**: `accept` — with the claim's `link_hash`, which
pins the canonical form, or for a recovery the ancestor it attests — or `reject` at a
@ -14,7 +14,7 @@ another step, does not conform.
| file | what it is |
|---|---|
| `vectors.json` | 154 vectors: 27 accepted claims, 119 rejected claims, and 8 recoveries (2 accepted, 6 rejected). Every reason of §3.1 is exercised at least once. |
| `vectors.json` | The corpus. Every vector declares one outcome, and every reason of §3.1 is exercised at least once. |
| `claim_verify.py` | A second verifier, written from the extension text, that shares no code with `wai-rs`. |
| `differential.py` | Generates inputs the corpus does not hold, runs both verifiers on them, and reports every input on which their verdicts differ. |
@ -26,7 +26,14 @@ signer keys), `declaration` (the claim a decision or a recovery record names),
declaration's subject — §2.6.3 makes the declaration context only together with it, so
a vector that gives a recovery record one without the other is malformed), and
`exact_match_algs` (the soft-binding algorithms the verifier has a matcher for; the
corpus's matcher is byte equality). `recover` runs §3.2 from a `declaration`, the
corpus's matcher is byte equality), and `figure` (the receipt that carries the figure a
`measurement` or `operating-point` claim names). An `object` context may also carry the
class its leaf is labelled with (jwp-receipts §2.2), in energy-measurement §2.7's JSON
members, read as that section reads them. A `figure` context is one of two forms:
`claim`, an `energy` claim's wire form, viewed under the `moq-jwp:claim-link` profile;
or `view`, a receipt the verifier has already verified, given as `{profile, identity,
group, figures: [{index, joules_micro, energy_* members}], signer}`, so that neither
verifier needs to parse every receipt family a figure profile names. `recover` runs §3.2 from a `declaration`, the
`publisher` key taken from a verified delivery receipt, the `observed_value`, and the
object in hand's `observed_hash` (or `null`). Both procedures report
`recovery_signer_mismatch` for a declaration the ancestor's publisher did not sign:
@ -44,7 +51,7 @@ illustrative and no verifier here checks them against a vocabulary.
An `energy` claim carries its class as energy-measurement §2.7 writes a class in JSON:
`energy_provenance`, with `energy_uncertainty`, `energy_counter_filtering` and
`energy_calibration_ref` present and `null` where the class has no such evidence.
Twenty-eight vectors are about the class and its evidence:
These vectors are about the class and its evidence:
- **Accepted:** an `OnChipCounter` figure with a declared uncertainty (2 % over 1 s) and
its filtering state `Unknown`, and with `Off`; a `CalibratedInstrument` figure with a
@ -67,14 +74,56 @@ Twenty-eight vectors are about the class and its evidence:
- **Rejected at step 3:** a `ModelBased` claim rewritten in transit to `OnChipCounter`
with an uncertainty and a filtering state: the class and its evidence are signed.
## Reports about a figure: `measurement` and `operating-point`
A `measurement` claim publishes how one figure was measured — its bracket's values as
integers, with the liveness facts of each bracket, the build and the power reader — and
an `operating-point` claim records the operating point it was spent at. Both name the
receipt that seals the figure by a figure profile (energy-measurement §6.1) and restate
the figure with its class. The vectors cover:
- **The arithmetic of energy-measurement §6.2:** declarations at the least a bracket
supports and one ppm below it; a figure at a half microjoule, which rounds up; brackets
whose products exceed 128 and 200 bits; a calibrated instrument declaring more than the
least; and the declaration a sealing path computes from unrounded samples, which the
rule refuses. Those values are checked by `claim_verify.py` with Python's integers, and
the bracket table `wai-rs` pins in `energy_report` is recomputed by it too.
- **Liveness (§3.1):** a bracket whose rail advanced in too few samples; a frozen rail
refreshed once, which gives a flat idle bracket and an infinite ratio; a reader holding
a stale non-zero value, stepped once by a batch refresh, with its honest facts and with
facts claiming every sample live; a flat idle bracket whose facts claim the rail live
(a repeated sample is not an advance, so a bracket with `sd_uw` 0 is never live); a
refresh published in two steps in each bracket, which passes a rule of two consecutive
advances; facts that describe no sequence; and, accepted, a `ModelBased` figure whose
bracket shows a dead rail, since liveness binds the hardware classes only.
- **The power reader (§2.4.8 rule 4):** a hardware figure's report that names no reader
is refused, with a bracket and without one; accepted, an `OnChipCounter` report with no
bracket that names its reader.
- **Every other rule of §2.4.8 rule 5 and §2.4.9,** each with its reason, and two defects
at once.
- **Step 2:** derived quantities are never carried (a bracket with its ratio is
`unknown_member`); the opening and build-profile enumerations; integers, terms and
absolute URIs where the tables require them.
- **Step 6:** against an `energy` claim and against views of a provenance receipt, a
delivered object (named by its leaf index and its group), and an energy binding, which
no Ed25519 key signs — a `measurement` claim about it is `figure_signer_unknown`, an
`operating-point` claim about it is accepted; a report by a key other than the figure's
signer; and an operating point recorded against an unmetered or unlabelled figure. A
`measurement` claim checked without the receipt that carries its figure is
`figure_not_held`, whether a stranger or the figure's own signer signed it: only step 6
shows a report is the figure signer's.
- **The labelled delivery leaf:** an `energy` claim against an object whose leaf carries
a class, agreeing, and disagreeing in figure, class or evidence; an `OnChipCounter`
label with no filtering state reads as `Unknown`.
## Steps 1 and 2: types, and the order a body is read in
§3.1 step 2 checks exactly the type a member table gives, member by member in the
table's order; whatever a class's rules add to a type is step 4's, with the rule's own
reason. Four step-4 vectors pin that boundary from the rules' side: a zero figure
reason. Step-4 vectors pin that boundary from the rules' side: a zero figure
(`energy_zero_figure`), a zero work count (`energy_zero_work`), an empty basis
(`lawful_basis_purpose_without_basis`) and an `http` endpoint
(`soft_binding_endpoint_not_https`). Eight more pin what the text settles on the other
(`soft_binding_endpoint_not_https`). Others pin what the text settles on the other
side, each signed again after its edit so the edit is its only defect:
- **Step 1:** a byte-order mark before the text is `malformed` (RFC 8259 lets a parser
@ -158,8 +207,8 @@ cargo build --no-default-features --features receipt_claims --bin wai_claim_vect
python3 differential.py <target-dir>/debug/wai_claim_vectors [--random N] [--seed S] [--keep FILE]
```
CI runs the first on the conformance matrix (Linux x86_64, Linux arm64, macOS arm64). The second and third are
run by hand; they are not CI steps.
CI runs the first on the conformance matrix (Linux x86_64, Linux arm64, macOS arm64), and the second
and third on the linux runner, with a fixed seed, in `.github/workflows/wai-conformance.yml`.
`differential.py` writes its inputs in the corpus's format with no declared outcome
and runs each verifier's `verdicts` mode (`wai_claim_vectors verdicts <file>`,

View file

@ -1,7 +1,8 @@
#!/usr/bin/env python3
"""Second verifier for claim-conformance/ — typed claim receipts, the soft binding class,
and recovery under the ancestor rule (extensions/jwp-receipts.md §2.4.3-§2.4.7, §2.6,
§3.1-§3.2).
and recovery under the ancestor rule (extensions/jwp-receipts.md §2.4.3-§2.4.9, §2.6,
§3.1-§3.2), and the arithmetic of a measurement report (energy-measurement.md §3.1, §6.2),
computed with Python's integers.
Written from the extension text. It shares no code with wai-rs (`crate::claim_receipt`),
which generates the corpus: the canonical form is Python's JSON serialiser with members
@ -14,12 +15,14 @@ reference verifier (`../oer-conformance/oer2_verify.py`), under oer-receipts' ow
python3 claim_verify.py [vectors.json] verify every vector and the canonical fixture
python3 claim_verify.py fixture print the fixture's canonical bytes,
signature and link hash
python3 claim_verify.py brackets print the figure and least declaration of
each bracket wai-rs pins
python3 claim_verify.py verdicts <file> print `name<TAB>verdict` for every vector of a
corpus-format file, ignoring any declared
outcome (the input to differential.py)
Exit status 0 only if every vector reports its declared outcome and the fixture this
script computes equals the literals pinned in wai-rs.
Exit status 0 only if every vector reports its declared outcome, and the fixture and the
bracket arithmetic this script computes equal the literals pinned in wai-rs.
A corpus-format file is read as the OER reference reads a vector document (`OER.load`,
oer-receipts §4 "The document"), since it holds OER receipts: a file that reading refuses
@ -279,14 +282,35 @@ def parse_strict(text):
ENVELOPE = ["kind", "class", "subject_type", "subject", "seq", "issued_at", "body",
"signer_pubkey", "signer_id", "parent_receipt_hash", "sig"]
ADMITS = {"energy": {"object"}, "lawful-basis": {"object", "receipt"}, "log-inclusion": {"receipt"},
"soft-binding": {"object"}, "binding-recovery": {"object"}}
"soft-binding": {"object"}, "binding-recovery": {"object"},
"measurement": {"object", "receipt"}, "operating-point": {"object", "receipt"}}
FIGURE = ["figure_profile", "figure_index", "group", "joules_micro", "energy_provenance", "energy_uncertainty",
"energy_counter_filtering", "energy_calibration_ref"]
BODY = {
"energy": ["joules_micro", "energy_provenance", "energy_uncertainty", "energy_counter_filtering",
"energy_calibration_ref", "coverage", "grade", "method", "work_units", "work_unit", "oer_receipt"],
"log-inclusion": ["log", "profile", "entry", "log_receipt"],
"soft-binding": ["alg", "registry", "binding_type", "value", "resolution_endpoint"],
"binding-recovery": ["alg", "binding_type", "value", "declaration", "resolution_endpoint", "observed", "attests"],
"measurement": FIGURE + ["bracket", "build", "power_reader"],
"operating-point": FIGURE + ["vocabulary", "op_requested", "op_committed", "op_delivered", "prediction"],
}
BRACKET = ["idle", "busy", "window_us", "completions", "in_flight", "opening"]
SUMMARY = ["n", "mean_uw", "sd_uw", "live_intervals", "longest_live_run"]
BUILD = ["profile", "toolchain", "target"]
PREDICTION = ["predictor", "descriptor_hash", "predicted_joules_micro"]
# energy-measurement §6.1: profile -> (subject_type, carries several figures, an Ed25519
# key signs the figure, names a group).
PROFILES = {"moq-jwp:object": ("object", True, True, True), "moq-jwp:claim-link": ("receipt", False, True, False),
"wai:energy-binding-id": ("receipt", False, False, False)}
for _n in ["video-receipt-id", "video-manifest-receipt-id", "video-ssai-receipt-id", "video-keys-receipt-id",
"video-package-receipt-id", "video-channel-receipt-id", "world-receipt-id", "film-receipt-id",
"haptic-receipt-id", "score-receipt-id", "splat4d-receipt-id", "audio-receipt-id", "ternary-receipt-id",
"quantum-receipt-id", "quantum-calibration-id", "quantum-job-id", "quantum-mitigate-id",
"quantum-decode-id", "quantum-compile-id", "quantum-rearrange-id", "phasor-receipt-id"]:
PROFILES["wai:" + _n] = ("receipt", False, True, False)
for _n in ["asset-receipt-id", "quantum-qbom-id"]:
PROFILES["wai:" + _n] = ("receipt", True, True, False)
DECLARATION = ["record", "vocabulary", "fragment", "purposes"]
PURPOSE = ["purpose", "basis", "data"]
DECISION = ["record", "vocabulary", "declaration", "purpose", "outcome"]
@ -423,6 +447,42 @@ def check_body(cls, b):
raise Reject(2, "field_type")
return
members(b, BODY[cls], 2)
if cls in ("measurement", "operating-point"):
check_figure_header(b)
if cls == "measurement":
br = b["bracket"]
if br is not None:
need(isinstance(br, dict), 2)
members(br, BRACKET, 2)
for side in ("idle", "busy"):
need(isinstance(br[side], dict), 2)
members(br[side], SUMMARY, 2)
need(all(is_int(br[side][k]) for k in SUMMARY), 2)
need(all(is_int(br[k]) for k in ("window_us", "completions", "in_flight")), 2)
need(br["opening"] in ("from-rest", "running"), 2)
bd = b["build"]
need(isinstance(bd, dict), 2)
members(bd, BUILD, 2)
need(bd["profile"] in ("release", "debug"), 2)
need(bd["toolchain"] is None or isinstance(bd["toolchain"], str), 2)
need(bd["target"] is None or is_token(bd["target"]), 2)
need(b["power_reader"] is None or isinstance(b["power_reader"], str), 2)
else:
need(is_uri(b["vocabulary"]), 2)
for k in ("op_requested", "op_committed", "op_delivered"):
pt = b[k]
if pt is not None:
need(isinstance(pt, dict), 2)
for name, val in pt.items():
need(is_token(name) and is_int(val), 2)
pr = b["prediction"]
if pr is not None:
need(isinstance(pr, dict), 2)
members(pr, PREDICTION, 2)
need(is_uri(pr["predictor"]), 2)
need(is_hex(pr["descriptor_hash"], 32), 2)
need(is_int(pr["predicted_joules_micro"]), 2)
return
if cls == "energy":
# In the member table's order: a nested object's member check can report another
# reason than a type check, so the order is the one every verifier uses.
@ -458,6 +518,105 @@ def check_body(cls, b):
need(is_token(b["alg"]) and is_uri(b["resolution_endpoint"]), 2)
def check_figure_header(b):
"""§2.4.8's header at step 2, in its table's order: `figure_profile` a term, as it is
read; the index an integer or null; the group a hash or null; then the figure and the
class's members as §2.4.5 types them."""
need(is_token(b["figure_profile"]), 2)
need(b["figure_index"] is None or is_int(b["figure_index"]), 2)
need(b["group"] is None or is_hex(b["group"], 32), 2)
need(is_int(b["joules_micro"]), 2)
need(isinstance(b["energy_provenance"], str), 2)
u = b["energy_uncertainty"]
if u is not None:
need(isinstance(u, dict), 2)
members(u, UNCERTAINTY, 2)
need(is_int(u["relative_ppm"]) and is_int(u["window_us"]), 2)
need(b["energy_counter_filtering"] is None or isinstance(b["energy_counter_filtering"], str), 2)
need(b["energy_calibration_ref"] is None or isinstance(b["energy_calibration_ref"], str), 2)
# ------------------------------------------- a measurement report's arithmetic (§3.1, §6.2)
def summary_live(s):
"""energy-measurement §3.1: a rail is live over a bracket only if it advanced in two
consecutive intervals, in at least five, and in at least four of every five — facts
that describe one sequence of `n` intervals. A power sample advances only where it
differs from the one before it, so a bracket whose samples are all equal (`sd_uw` 0)
advanced once at most, and is never live whatever facts it carries."""
n, live, run = s["n"], s["live_intervals"], s["longest_live_run"]
return s["sd_uw"] > 0 and run <= live <= n and run >= 2 and live >= 5 and live * 5 >= n * 4
def work(br):
c, n = br["completions"], br["in_flight"]
return (2 * c + n, 2) if br["opening"] == "from-rest" else (c, 1)
def bracket_figure(br):
"""F = floor((2·M·T·Wd + 10^6·Wn) / (2·10^6·Wn)), or None."""
m = br["busy"]["mean_uw"] - br["idle"]["mean_uw"]
t, c, n = br["window_us"], br["completions"], br["in_flight"]
if m <= 0 or t == 0 or c == 0 or n == 0:
return None
wn, wd = work(br)
f = (2 * m * t * wd + 10 ** 6 * wn) // (2 * 10 ** 6 * wn)
return f if 0 < f <= MAX_INT else None
def k_and_p(br, f):
m = br["busy"]["mean_uw"] - br["idle"]["mean_uw"]
t, n = br["window_us"], br["in_flight"]
wn, wd = work(br)
ni, nb, si, sb = br["idle"]["n"], br["busy"]["n"], br["idle"]["sd_uw"], br["busy"]["sd_uw"]
k = 12 * ni * nb * wn ** 4 * f * f
p = 12 * wn ** 2 * t ** 2 * wd ** 2 * (si * si * nb + sb * sb * ni) + 4 * ni * nb * m * m * t * t * wd * wd * n * n \
+ 10 ** 12 * ni * nb * wn ** 4
return k, p
def least_ppm(br, f):
"""The least p with p^2·K >= P, or None above 2^32 - 1."""
k, p = k_and_p(br, f)
if k == 0:
return None
q = -(-p // k) # p^2 >= ceil(P / K)
r = __import__("math").isqrt(q)
while r * r * k < p:
r += 1
while r > 1 and (r - 1) * (r - 1) * k >= p:
r -= 1
return r if 1 <= r <= 2 ** 32 - 1 else None
def bracket_rules(br, joules, label, u):
"""§2.4.8 rule 5, in its order."""
hw = label in ("OnChipCounter", "CalibratedInstrument")
need(br["idle"]["n"] >= 2 and br["busy"]["n"] >= 2, 4, "measurement_too_few_samples")
need(not hw or (summary_live(br["idle"]) and summary_live(br["busy"])), 4, "measurement_rail_not_live")
need(br["window_us"] != 0 and br["completions"] != 0 and br["in_flight"] != 0, 4, "measurement_empty_burst")
m = br["busy"]["mean_uw"] - br["idle"]["mean_uw"]
need(m > 0, 4, "measurement_no_marginal")
need(not hw or m >= br["idle"]["sd_uw"], 4, "measurement_below_attribution_floor")
need(bracket_figure(br) == joules, 4, "measurement_figure_mismatch")
if hw:
need(u["window_us"] == br["window_us"], 4, "measurement_window_mismatch")
k, p = k_and_p(br, joules)
need(u["relative_ppm"] ** 2 * k >= p, 4, "measurement_declaration_below_bracket")
def figure_rules(c):
"""§2.4.8 rules 1-3: a non-zero figure, a profile for the subject type with an index
and a group exactly where it has them, and the class."""
b = c["body"]
need(b["joules_micro"] != 0, 4, "energy_zero_figure")
prof = PROFILES.get(b["figure_profile"])
need(prof is not None and prof[0] == c["subject_type"], 4, "figure_unknown_profile")
need((b["figure_index"] is not None) == prof[1], 4, "figure_index_mismatch")
need((b["group"] is not None) == prof[3], 4, "figure_group_mismatch")
acquisition(b)
def acquisition(b):
"""§2.4.5 rule 2: the class is one energy-measurement.md §2 names, not the legacy
HwShunt, and carries exactly the evidence §2.3-§2.5 give it: a refined hardware class
@ -499,6 +658,21 @@ def class_rules(c):
need(p["basis"] != "", 4, "lawful_basis_purpose_without_basis")
need(p["purpose"] not in seen, 4, "lawful_basis_duplicate_purpose")
seen.add(p["purpose"])
elif cls == "measurement":
figure_rules(c)
# Rule 4: a hardware figure is a reading, and its report names the reader.
need(b["energy_provenance"] not in ("OnChipCounter", "CalibratedInstrument") or b["power_reader"] is not None,
4, "measurement_no_power_reader")
if b["bracket"] is not None:
bracket_rules(b["bracket"], b["joules_micro"], b["energy_provenance"], b["energy_uncertainty"])
elif cls == "operating-point":
figure_rules(c)
pts = [b[k] for k in ("op_requested", "op_committed", "op_delivered")]
need(all(p is None or len(p) > 0 for p in pts), 4, "operating_point_empty_point")
req, com, dlv = pts
need(not (dlv is not None and com is None), 4, "operating_point_delivered_alone")
need(not ((req is not None or com is not None) and dlv is None), 4, "operating_point_incomplete")
need(dlv is not None or b["prediction"] is not None, 4, "operating_point_empty")
elif cls == "log-inclusion":
need(b["log_receipt"] != "", 4, "log_inclusion_no_evidence")
elif cls in ("soft-binding", "binding-recovery"):
@ -592,6 +766,14 @@ def check_context(c, ctx):
raise ValueError("bad object.joules_micro")
need(c["class"] == "energy" and o["content_hash"] == c["subject"], 6, "context_mismatch")
need(o["joules_micro"] == b["joules_micro"], 6, "energy_disagrees_with_delivery")
label = class_of_json(o) # the label in the object's leaf, where its leaf is labelled
need(label is None or label == class_of_body(b), 6, "energy_class_disagrees_with_delivery")
if "figure" in ctx:
check_figure(c, figure_view(ctx["figure"]))
elif c["class"] == "measurement":
# §2.4.8: a report is its figure signer's only once step 6 has run against the
# figure (check 5); checked without it, it is not accepted.
raise Reject(6, "figure_not_held")
if "oer" in ctx:
r, keys = ctx["oer"]["receipt"], ctx["oer"]["keys"]
need(c["class"] == "energy" and b["oer_receipt"] is not None
@ -636,6 +818,71 @@ def check_context(c, ctx):
need(any(p["purpose"] == b["purpose"] for p in db["purposes"]), 6, "decision_purpose_not_declared")
def class_of_json(m):
"""A class as energy-measurement §2.7 reads one from a receipt's JSON members: None
for no class; (label, ppm, window, filtering, reference) otherwise, an on-chip
counter's absent filtering state read as Unknown. A label that cannot be read is a
malformed corpus (ValueError), not a verdict."""
label = m.get("energy_provenance")
u, f, r = m.get("energy_uncertainty"), m.get("energy_counter_filtering"), m.get("energy_calibration_ref")
if label is None:
if u is not None or f is not None or r is not None:
raise ValueError("evidence without a class")
return None
if label in ("OnChipCounter", "CalibratedInstrument"):
if not isinstance(u, dict):
raise ValueError("a refined class without its uncertainty")
ppm, win = u["relative_ppm"], u["window_us"]
if label == "OnChipCounter":
return (label, ppm, win, "Unknown" if f is None else f, None)
return (label, ppm, win, None, r)
if label not in STRONGEST:
raise ValueError("unknown class " + str(label))
return (label, None, None, None, None)
def class_of_body(b):
"""The class a claim's body restates, in class_of_json's form (step 4 has admitted it)."""
return class_of_json(b)
def figure_view(f):
"""A `figure` context as (profile, identity, group, {index: (joules, class)}, signer),
or None for a receipt that does not verify. A `claim` is an energy claim's wire form,
viewed under `moq-jwp:claim-link`; a `view` is given as the verifier holds it."""
if "claim" in f:
if not isinstance(f["claim"], str):
raise TypeError("a figure claim is given as its wire form")
try:
e = verify_claim(f["claim"])
except Reject:
return None
figs = {}
if e["class"] == "energy":
figs[None] = (e["body"]["joules_micro"], class_of_body(e["body"]))
return ("moq-jwp:claim-link", link_hash(e), None, figs, e["signer_pubkey"])
v = f["view"]
figs = {x["index"]: (x["joules_micro"], class_of_json(x)) for x in v["figures"]}
return (v["profile"], v["identity"], v["group"], figs, v["signer"])
def check_figure(c, view):
"""§2.4.8 / §2.4.9 against the figure, in order."""
b = c["body"]
need(c["class"] in ("measurement", "operating-point"), 6, "context_mismatch")
need(view is not None, 6, "context_invalid")
profile, identity, group, figs, signer = view
need(profile == b["figure_profile"] and identity == c["subject"] and group == b["group"]
and b["figure_index"] in figs, 6, "context_mismatch")
joules, cls = figs[b["figure_index"]]
need(joules == b["joules_micro"], 6, "figure_disagrees")
need(cls is not None, 6, "figure_unclassified")
need(cls == class_of_body(b), 6, "figure_class_disagrees")
if c["class"] == "measurement":
need(PROFILES[b["figure_profile"]][2], 6, "figure_signer_unknown")
need(signer == c["signer_pubkey"], 6, "figure_signer_mismatch")
def held_previous(ctx):
"""§3.1 step 5's previous claim: one that does not itself pass steps 1-4 is not held —
failing steps 1-3 it is not known to be its signer's, failing step 4 it breaks its
@ -759,7 +1006,47 @@ def fixture_matches_wai_rs():
return fails
def bracket_fixture():
"""The brackets wai-rs pins in `energy_report::pinned_brackets`, read from its source,
with the figure and least declaration this script computes for each:
[(name, values, F, p)]."""
src_path = os.path.join(HERE, "..", "wai-rs", "src", "energy_report.rs")
src = open(src_path, encoding="utf-8").read()
body = src[src.index("fn pinned_brackets()"):]
body = body[:body.index("\n}\n")]
out = []
num = r"(x|[0-9_]+)"
pat = (r'\("(\w+)", v\(s\(' + num + r', ' + num + r', ' + num + r'\), s\(' + num + r', ' + num + r', ' + num
+ r'\), ' + num + r', ' + num + r', ' + num + r', (FromRest|Running)\), ' + num + r', ' + num + r'\)')
for m in re.finditer(pat, body):
g = [MAX_INT if x == "x" else int(x.replace("_", "")) if x not in ("FromRest", "Running") else x for x in m.groups()[1:]]
br = {"idle": {"n": g[0], "mean_uw": g[1], "sd_uw": g[2]}, "busy": {"n": g[3], "mean_uw": g[4], "sd_uw": g[5]},
"window_us": g[6], "completions": g[7], "in_flight": g[8],
"opening": "from-rest" if g[9] == "FromRest" else "running"}
out.append((m.group(1), br, g[10], g[11]))
return out
def brackets_match_wai_rs():
"""The figures and least declarations wai-rs pins equal what this script computes, and
one ppm less is not supported."""
fails, cases = [], bracket_fixture()
if len(cases) < 7:
return ["fewer pinned brackets than expected were read from energy_report.rs"]
for name, br, f, p in cases:
got_f = bracket_figure(br)
got_p = least_ppm(br, f) if got_f is not None else None
if got_f != f or got_p != p:
fails.append(f"{name}: computed F={got_f} p={got_p}, pinned F={f} p={p}")
return fails
def main(argv):
if argv[1:2] == ["brackets"]:
for name, br, _, _ in bracket_fixture():
f = bracket_figure(br)
print(name, f, least_ppm(br, f))
return 0
if argv[1:2] == ["verdicts"] and len(argv) == 3:
return verdicts(argv[2])
if argv[1:2] == ["fixture"]:
@ -777,6 +1064,9 @@ def main(argv):
ff = fixture_matches_wai_rs()
print(("PASS" if not ff else "FAIL") + " canonical fixture equals the literals pinned in wai-rs " + ",".join(ff))
fail += bool(ff)
bm = brackets_match_wai_rs()
print(("PASS" if not bm else "FAIL") + " bracket arithmetic equals the literals pinned in wai-rs " + ",".join(bm))
fail += bool(bm)
rd = OER.check_canonical_form() # the OER reference's own fixtures, its reading of -0 among them
print(("PASS" if not rd else "FAIL") + " the OER reference's canonical form and number reading hold " + ",".join(rd))
fail += bool(rd)

View file

@ -11,7 +11,12 @@ half-microjoule and at the edges of oer-receipts §4's quantities, OER receipts
and floors sit at §6's tolerances, OER receipts holding numbers §4 forbids in a body
(`-0`, fractions, integers beyond I-JSON's range) in members of every kind, context
receipts that do not themselves verify, and bodies of every class with one defect, or two
at different positions, where the order §3.1 step 2 reads a body in decides the reason —
at different positions, where the order §3.1 step 2 reads a body in decides the reason;
measurement reports whose brackets sit at the least declaration and one ppm below it, at a
half microjoule, at the ratio thresholds, with a flat idle bracket, with products far past
128 bits and with every pattern of liveness facts; operating-point claims with every
presence combination of their points and prediction; reports checked against figure views
that agree and disagree in each member; and energy claims against labelled objects —
writes them in the corpus's format with no declared outcome, runs both verifiers'
`verdicts` mode, and reports every input on which they differ.
@ -630,6 +635,177 @@ def documents(corpus):
return out
def reports(ins, rng, n):
"""measurement and operating-point claims (§2.4.8-§2.4.9) around the edges of their
rules: brackets at and one ppm below the least declaration, figures one off, ties at a
half microjoule, the ratio thresholds, flat brackets and a held value, products past 128 bits,
every pattern of liveness facts; operating points in every presence combination; and
reports and energy claims against figure views and labelled objects."""
m0 = ins.receipt("measurement_accept")
fig0 = ins.by_name["measurement_accept"]["context"]["figure"]
op0 = ins.receipt("operating_point_accept")
hw = ["OnChipCounter", "CalibratedInstrument"]
def summary(n_, mean, sd, live=None, run=None):
live = n_ if live is None else live
return {"n": n_, "mean_uw": mean, "sd_uw": sd, "live_intervals": live, "longest_live_run": n_ if run is None else run}
def class_fields(label, ppm, window, figure):
b = {"energy_provenance": label, "energy_uncertainty": None, "energy_counter_filtering": None,
"energy_calibration_ref": None}
if label == "OnChipCounter":
b.update(energy_uncertainty={"relative_ppm": ppm, "window_us": window}, energy_counter_filtering="Unknown")
elif label == "CalibratedInstrument":
b.update(energy_uncertainty={"relative_ppm": ppm, "window_us": window},
energy_calibration_ref="https://calibration.example.org/certificates/0001")
return dict(b, joules_micro=figure)
def report(br, label, ppm, window, figure, name, ctx=None):
body = dict(m0["body"], bracket=br, **class_fields(label, ppm, window, figure))
ins.add(name, CV.jcs(sign(dict(m0, body=body), METER)), ctx)
def no_reader(br, label, ppm, window, figure, name, ctx=None):
body = dict(m0["body"], bracket=br, power_reader=None, **class_fields(label, ppm, window, figure))
ins.add(name, CV.jcs(sign(dict(m0, body=body), METER)), ctx)
def around(name, br, labels=("OnChipCounter", "CalibratedInstrument", "ModelBased")):
f = CV.bracket_figure(br)
for label in labels:
if f is None:
report(br, label, 300_000, br["window_us"], 1, f"{name}_nofig_{label}")
continue
p = CV.least_ppm(br, f) or 2 ** 32 - 1
for d_ppm, d_fig, d_win in [(0, 0, 0), (-1, 0, 0), (1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1)]:
ppm = max(1, min(2 ** 32 - 1, p + d_ppm))
fig = f + d_fig
if fig <= 0 or fig > CV.MAX_INT:
continue
report(br, label, ppm, br["window_us"] + d_win, fig, f"{name}_{label}_{d_ppm}_{d_fig}_{d_win}")
def bracket(idle, busy, t, c, nn, opening):
return {"idle": idle, "busy": busy, "window_us": t, "completions": c, "in_flight": nn, "opening": opening}
# Fixed edges.
around("half_tie", bracket(summary(10, 20_000_000, 1_000), summary(14, 21_000_000, 1_000), 2_500_000, 1_000_000, 1, "running"))
around("flat_idle", bracket(summary(10, 20_000_000, 0), summary(14, 30_000_000, 115_471), 3_000_000, 60, 10, "from-rest"))
around("flat_busy", bracket(summary(10, 20_000_000, 115_471), summary(14, 30_000_000, 0), 3_000_000, 60, 10, "from-rest"))
around("held_value", bracket(summary(10, 5_000_000, 0, 1, 1), summary(14, 10_500_000, 2_980_708, 2, 1), 3_000_000, 60, 10, "from-rest"))
# A report that names no power reader, with and without a bracket, under each class.
for label in ("OnChipCounter", "CalibratedInstrument", "ModelBased", "Estimator"):
unit_br = m0["body"]["bracket"]
f = CV.bracket_figure(unit_br)
p = CV.least_ppm(unit_br, f)
for br in (unit_br, None):
no_reader(br, label, p, unit_br["window_us"], f, f"no_reader_{label}_{br is None}", fig0)
around("wide", bracket(summary(10, 20_000_000, 115_471), summary(14, 30_000_000, 115_471), 10 ** 11, 10 ** 13, 64, "from-rest"))
for k in (10, 3, 1):
for d in (0, -1):
around(f"ratio_{k}_{d}", bracket(summary(10, 20_000_000, 100_000), summary(14, 20_000_000 + k * 100_000 + d, 100_000),
3_000_000, 60, 10, "from-rest"), labels=("OnChipCounter", "ModelBased"))
# Liveness facts: every pattern around the thresholds, in each bracket.
for n_ in (4, 5, 6, 10, 14):
for live in sorted({0, 1, 2, 4, 5, n_ * 4 // 5, n_ * 4 // 5 + 1, n_ - 1, n_, n_ + 1}):
for run in sorted({0, 1, 2, 3, live, live + 1}):
idle = summary(10, 20_000_000, 115_471)
busy = summary(n_, 30_000_000, 115_471, live, run)
br = bracket(idle, busy, 3_000_000, 60, 10, "from-rest")
f = CV.bracket_figure(br)
p = CV.least_ppm(br, f)
report(br, "OnChipCounter", p, 3_000_000, f, f"live_busy_{n_}_{live}_{run}")
br2 = bracket(summary(n_, 20_000_000, 115_471, live, run), summary(14, 30_000_000, 115_471), 3_000_000, 60, 10, "from-rest")
report(br2, "OnChipCounter", CV.least_ppm(br2, f), 3_000_000, f, f"live_idle_{n_}_{live}_{run}")
# Random brackets.
for i in range(n):
ni, nb = rng.choice([1, 2, 3, 5, 10, 14, 50]), rng.choice([1, 2, 3, 5, 10, 14, 50])
idle_mean = rng.randrange(0, 50_000_000)
busy_mean = idle_mean + rng.choice([-1, 0, 1, rng.randrange(1, 40_000_000)])
sd = lambda: rng.choice([0, 1, rng.randrange(1, 5_000_000)]) # noqa: E731
live = lambda m: rng.choice([m, m - 1, 0, 2, 5]) # noqa: E731
idle = summary(ni, idle_mean, sd(), live(ni))
busy = summary(nb, max(0, busy_mean), sd(), live(nb))
t = rng.choice([0, 1, rng.randrange(1, 10 ** 7), rng.randrange(1, 10 ** 12)])
c = rng.choice([0, 1, rng.randrange(1, 10 ** 6), rng.randrange(1, 10 ** 13)])
nn = rng.choice([0, 1, rng.randrange(1, 65)])
br = bracket(idle, busy, t, c, nn, rng.choice(["from-rest", "running"]))
f = CV.bracket_figure(br)
label = rng.choice(hw + ["ModelBased", "Estimator"])
if f is None:
report(br, label, 300_000, max(1, t), rng.randrange(1, 10 ** 6), f"random_{i}_nofig")
continue
p = CV.least_ppm(br, f)
ppm = max(1, min(2 ** 32 - 1, (p or 2 ** 32 - 1) + rng.choice([0, 0, -1, 1, 1000])))
if f * ppm < 288_676:
ppm = 288_676 // f + 1
report(br, label, ppm, rng.choice([max(1, t), t + 1]), f + rng.choice([0, 0, 0, 1, -1]) or 1, f"random_{i}")
# Operating points: every presence combination, and points that are empty, not terms,
# or not integers.
pts = [None, {}, {"height": 1080}, {"height": 720}, {"frame rate": 1}, {"height": "1080"}, {"height": -1}]
pred = op0["body"]["prediction"]
for ri, r in enumerate(pts):
for ci, c in enumerate(pts):
for di, d in enumerate(pts):
for pi, pr in enumerate([None, pred]):
body = dict(op0["body"], op_requested=r, op_committed=c, op_delivered=d, prediction=pr)
ins.add(f"op_{ri}{ci}{di}{pi}", CV.jcs(sign(dict(op0, body=body), METER)) if all(
not isinstance(x, dict) or all(isinstance(v, int) and v >= 0 for v in x.values()) for x in (r, c, d))
else text_of(sign_raw(dict(op0, body=body), METER)), {"figure": fig0})
# Figure views that agree and disagree in each member.
e = json.loads(fig0["claim"])
eb = e["body"]
base = {"profile": "moq-jwp:claim-link", "identity": CV.link_hash(e), "group": None,
"figures": [{"index": None, "joules_micro": eb["joules_micro"], "energy_provenance": eb["energy_provenance"],
"energy_uncertainty": eb["energy_uncertainty"],
"energy_counter_filtering": eb["energy_counter_filtering"]}],
"signer": pk(METER)}
variants = [("same", {}), ("profile", {"profile": "wai:asset-receipt-id"}), ("identity", {"identity": "ab" * 32}),
("group", {"group": "cd" * 32}), ("signer", {"signer": pk(OPERATOR)}), ("no_signer", {"signer": None})]
fig_variants = [("figure_plus", {"joules_micro": eb["joules_micro"] + 1}), ("unlabelled", {"energy_provenance": None,
"energy_uncertainty": None, "energy_counter_filtering": None}),
("filtering_absent", {"energy_counter_filtering": None}), ("filtering_off", {"energy_counter_filtering": "Off"}),
("ppm", {"energy_uncertainty": dict(eb["energy_uncertainty"], relative_ppm=eb["energy_uncertainty"]["relative_ppm"] + 1)}),
("index", {"index": 0})]
for name, edit in variants:
ins.add("view_" + name, CV.jcs(m0), {"figure": {"view": dict(base, **edit)}})
ins.add("view_op_" + name, CV.jcs(op0), {"figure": {"view": dict(base, **edit)}})
for name, edit in fig_variants:
fv = [{k: v for k, v in dict(base["figures"][0], **edit).items() if v is not None or k == "index"}]
ins.add("view_fig_" + name, CV.jcs(m0), {"figure": {"view": dict(base, figures=fv)}})
ins.add("view_none_given_claim_not_json", CV.jcs(m0), {"figure": {"claim": "not json"}})
# Energy claims against labelled objects.
ec = ins.receipt("energy_on_chip_counter_accept")
b = ec["body"]
lab = {"energy_provenance": "OnChipCounter", "energy_uncertainty": b["energy_uncertainty"], "energy_counter_filtering": "Unknown"}
for name, j, extra in [("same", b["joules_micro"], lab), ("no_label", b["joules_micro"], {}),
("filtering_absent", b["joules_micro"], {k: v for k, v in lab.items() if k != "energy_counter_filtering"}),
("model", b["joules_micro"], {"energy_provenance": "ModelBased"}),
("ppm", b["joules_micro"], dict(lab, energy_uncertainty={"relative_ppm": 20_001, "window_us": 1_000_000})),
("figure", b["joules_micro"] + 1, lab)]:
ins.add("labelled_object_" + name, CV.jcs(ec), {"object": dict({"content_hash": ec["subject"], "joules_micro": j}, **extra)})
def sign_raw(c, k):
"""`c` signed over its JCS where JCS can serialise it, with a member JCS refuses (a
string or negative point value) left for the verifier to refuse at step 2. The
signature covers a canonical form computed with that member as written."""
c = copy.deepcopy(c)
c.pop("sig", None)
c["signer_pubkey"] = pk(k)
def canon(v):
if isinstance(v, int) and not isinstance(v, bool) and v < 0:
return str(v)
if isinstance(v, dict):
keys = sorted(v, key=lambda x: x.encode("utf-16-be"))
return "{" + ",".join(json.dumps(x, ensure_ascii=False) + ":" + canon(v[x]) for x in keys) + "}"
if isinstance(v, list):
return "[" + ",".join(canon(x) for x in v) + "]"
return CV.jcs(v)
c["sig"] = k.sign(b"moq-jwp:claim\x01" + canon(c).encode()).hex()
return c
def contexts(ins):
decl = ins.receipt("soft_binding_accept")
record = ins.receipt("binding_recovery_accept")
@ -735,6 +911,7 @@ def main(argv):
readings(ins)
contexts(ins)
bodies(ins)
reports(ins, random.Random(f"reports {a.seed}"), a.random) # its own stream, as classes()
doc = text_of({"about": "differential inputs; no outcome is declared", "vectors": ins.out})
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as f:
f.write(doc)

File diff suppressed because it is too large Load diff

View file

@ -74,12 +74,20 @@ under the conditions in [determinism-tiers](determinism-tiers.md) §5
- recompute `workUnits` from the descriptor rather than trusting it,
- and re-derive the rate.
What remains is **exactly one** unverifiable term: the joules. That is the
point of the construction, and the honest limit of it. An energy claim about an
output nobody can reproduce is not checkable at all. An energy claim about an
output anyone can reproduce is checkable except for one number — and knowing
precisely which number is the difference between a measurement and an
assertion.
What remains unverifiable is the measurement: the joules, and every field that
describes how they were obtained — `acquisition` and its evidence, `coverage`,
`method`, and the values of any report about the figure
([energy-measurement](energy-measurement.md) §6). No verifier can re-measure any
of them; each is its signer's statement. Some can be checked for consistency
without re-measuring anything: the declared uncertainty against the resolution
floor (§4), a calibration reference against the certificate it names
(energy-measurement §2.5), and a report's bracket against the figure and the
declared uncertainty it must give, and against the liveness rule
(energy-measurement §6.2). Consistency is not truth. That is the point of the
construction, and the honest limit of it. An energy claim about an output nobody
can reproduce is not checkable at all. An energy claim about an output anyone can
reproduce is checkable except for its measurement — and knowing precisely which
fields those are is the difference between a measurement and an assertion.
An implementation **MUST NOT** describe an energy binding as proving the energy
figure. It binds the figure to a reproducible outcome; it does not attest the
@ -124,6 +132,15 @@ measurement.
joule figure, in the same assertion, with no per-field provenance, is how a
model comes to be read as a measurement.
- A `Partial` figure **MUST NOT** be presented as the reconstruction's total.
- A binding's identity is `BLAKE3("wai:energy-binding-id\x01" ‖ JCS(body))`,
where `body` is the companion assertion's body as a JSON value and JCS is
RFC 8785. A body holding an integer above 2⁵³ − 1 has no identity. A report
about the binding's figure ([jwp-receipts](jwp-receipts.md) §2.4.8–§2.4.9)
names it by this identity under the figure profile `wai:energy-binding-id`
([energy-measurement](energy-measurement.md) §6.1). No member is added to
the body, so a body written before this rule has the same bytes and an
identity. No Ed25519 key signs a binding's figure, so an `operating-point`
claim may name it and a `measurement` claim may not (jwp-receipts §2.4.8).
## 5. Relationship to the standard assertion
@ -224,8 +241,12 @@ is scored against something rather than merely reported.
### 7.2 Request, commitment, delivery
Where one party requests an operating point and another acts on it, the receipt
**MUST** seal all three of:
Where one party requests an operating point and another acts on it, all three
**MUST** be sealed, as `op_requested`, `op_committed` and `op_delivered`, in an
`operating-point` claim about the figure spent at that operating point
([jwp-receipts](jwp-receipts.md) §2.4.9). The claim names that figure by its
figure profile ([energy-measurement](energy-measurement.md) §6.1) and leaves the
receipt that seals it unchanged:
| field | meaning |
|---|---|
@ -238,17 +259,31 @@ receiving device sends a DOR-Req, and the remote encoder answers with a DOR-Resp
saying how it decided to answer.
An operating point is a class-scoped descriptor — frame rate, spatial resolution,
a complexity target — and this extension does not define its vocabulary. What it
a complexity target — and this extension does not define its vocabulary. The
claim names the vocabulary its parameters are drawn from by URI, and writes each
operating point as an object from parameter to unsigned integer, so whether two
operating points are equal is a comparison of canonical bytes. What this extension
fixes is that the three are **distinct fields**, because the interesting cases are
exactly the ones where they differ.
exactly the ones where they differ, and that whether the commitment was met is
derived from them, never stated.
- A receipt **MUST NOT** carry `op_delivered` alone. A delivered figure with
nothing to score it against is the degradation-blind case §7.1 describes.
- A claim **MUST NOT** carry `op_delivered` without `op_committed`
(`operating_point_delivered_alone`). A delivered figure with nothing to score it
against is the degradation-blind case §7.1 describes.
- `op_committed` **MUST** be recorded as what the acting party stated, even where
it differs from the request. A commitment that silently becomes the request is not
a commitment.
- Where `op_delivered` differs from `op_committed`, the receipt **MUST** record
both and **MUST NOT** claim the commitment was met.
a commitment. No verifier can check what was stated; the claim is its signer's
account of it.
- Where `op_delivered` differs from `op_committed`, the claim records both and
states nothing about whether the commitment was met. A reader **MUST NOT** report
the commitment as met unless it derives that `op_delivered` equals
`op_committed` (jwp-receipts §2.4.9).
- An operating point **MUST NOT** be recorded against an unmetered figure or a
figure labelled with the legacy `HwShunt`. An `operating-point` claim restates a
figure with a refined or model class: a claim restating `HwShunt` is refused
(`energy_legacy_acquisition`), and one naming a figure that carries the unmetered
marker or no class is refused against it (`figure_disagrees`,
`figure_unclassified`).
- A receipt over a payload that an encoder altered to save energy, whether at a
receiver's request (such as a decoding-operation-reduction or attenuated-video
request) or on its own account, covers that payload, and **MUST NOT** be
@ -263,19 +298,28 @@ against the first.
### 7.3 The residual
When a declared complexity or operating-point descriptor and a measured energy
binding both cover the same reconstruction, the receipt **MUST** carry the
**residual** — the signed difference between the energy the descriptor implies
and the energy measured.
figure both cover the same reconstruction, the claim about that figure **MUST**
carry the prediction the descriptor gives: the `prediction` member of an
`operating-point` claim ([jwp-receipts](jwp-receipts.md) §2.4.9), holding the
energy the descriptor implies for that work, the model that computed it, and the
hash of the descriptor. The **residual** — the signed difference between the
energy measured and the energy the descriptor implies — is not written. Every
reader derives the same one, `joules_micro − predicted_joules_micro`, so it can be
neither stated inconsistently with the two figures nor dropped once the
prediction is sealed.
- The residual **MUST** be computed over the same work unit as the measurement
(`energy-measurement.md` §5), or it compares two different things and means
nothing.
- `predicted_joules_micro` **MUST** be stated for the same work as the figure
(`energy-measurement.md` §5), or the residual compares two different things and
means nothing. A verifier checks that the claim names the figure; it cannot
check what work the prediction was made for.
- A residual **MUST NOT** be reported without the acquisition class of the
measured term. A residual against an `Estimator` figure is a difference between
two predictions.
- An implementation **MUST NOT** suppress a residual because it is large. The
large ones are the informative ones; suppression converts a falsifiable claim
into an unfalsifiable one.
measured term. An `operating-point` claim restates the figure's class and is
refused where the figure has none (`figure_unclassified`). A residual against an
`Estimator` figure is a difference between two predictions.
- An implementation **MUST NOT** omit a prediction because the residual is large.
The large ones are the informative ones; omission converts a falsifiable claim
into an unfalsifiable one. No verifier can tell an omitted prediction from one
never made, so this rule binds producers.
The residual is what makes the predictive half checkable. A complexity descriptor
is a prediction about energy, and until something records what actually happened
@ -284,9 +328,11 @@ rather than in a report.
### 7.4 Companion capability
Green-metadata semantics are registered the way
[`wai.meta.hdr10plus`](../research/media-codec-registry.md) and `wai.meta.dovi`
are: the capability names the descriptor class — complexity metrics, quality
recovery, display adaptation — and the sink supplies the decoder. **No carriage
is adopted**: ISOBMFF and DASH carriage of green metadata is somebody else's
layer, and WAI naming a descriptor does not make WAI responsible for moving it.
Green-metadata semantics are not yet registered as a capability. When they are,
they are registered the way [`wai.meta.hdr10plus`](../research/media-codec-registry.md)
and `wai.meta.dovi` are: the capability names the descriptor class — complexity
metrics, quality recovery, display adaptation — and the sink supplies the decoder.
**No carriage is adopted**: ISOBMFF and DASH carriage of green metadata belongs to
another layer, and WAI naming a descriptor does not make WAI responsible for moving
it. Until then, an `operating-point` claim names the vocabulary of its operating
points and the model behind its prediction by URI (jwp-receipts §2.4.9).

View file

@ -1,10 +1,13 @@
# 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.
> `joules_micro` field the receipt profiles carry means the same thing in two
> independent implementations. It defines an acquisition class and its signed
> encoding (§2, §2.7–§2.8), an acquisition protocol with a liveness rule for the
> power reader (§3, §3.1), a quality rule (§4), and a reporting contract (§6)
> whose values are published in a signed report that names the receipt it
> describes and leaves that receipt's bytes unchanged. A receipt that carries
> none of these additions keeps exactly the bytes it had.
> Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174.
## 1. Why an acquisition protocol
@ -49,57 +52,54 @@ One rule reaches past the governed receipts. §2's rule for carrying a WAI
figure into a vocabulary with only three class names binds whoever carries
it, although the receipt the figure lands in is not governed.
Two receipts WAI defines have no member for the class: the JWP group receipt
and the JWP object receipt ([jwp-receipts](jwp-receipts.md) §2.4.1 and
§2.4.2). The group receipt signs `joules_total`, the sum of its objects'
figures. The object receipt carries its own `joules_micro`. Neither meets §2,
however it is carried:
Two receipts WAI defines carry a figure that the group signature does not
cover in its legacy form: the JWP group receipt and the JWP object receipt
([jwp-receipts](jwp-receipts.md) §2.4.1 and §2.4.2). The group receipt signs
`joules_total`, the sum of its objects' figures. The object receipt carries
its own `joules_micro`, which the group signature covers only through the
object's Merkle leaf (jwp-receipts §2.2). Each meets §2 as follows:
- Where a `WAI_RECEIPT` property accompanies an object over MoQ, the
object's figure and its class travel together in it. The property is
optional (moq-streaming-format §5), and neither that document nor
jwp-receipts puts it under a signature. The group signature (jwp-receipts
§2.3) covers the Merkle root over the objects' content hashes and the
group total. It covers neither an object's figure nor its class, so the
class is not inside the signature as §2 requires.
- Without that property, over MoQ or any other carriage, an object receipt
has no place for the class.
- Nothing carries a class for the group total. `WAI_RECEIPT` is per object
and holds only a hash of the group receipt. §2.8's rule that a total
carries no class of its own does not cover it. That rule is for a total
whose parts are labelled inside the same receipt, and a group receipt's
parts are its objects, labelled, if at all, outside it.
- **An object's figure** carries its class as an optional label (§2.8), bound
into its Merkle leaf with the figure and the object's `origin`. Absent, the
leaf and every signed byte are the legacy ones and the figure is unlabelled.
Present, the leaf covers the figure, its class, the class's evidence and the
origin, and the group signature, which covers the Merkle root, covers them
too.
- **A group total** may carry a class of its own, signed under the labelled
group payload (jwp-receipts §2.3) together with its coverage and the parent
link. The class is the one the objects' classes compose to (jwp-receipts
§2.4.1), so a holder of the group receipt alone holds a signed class for the
total, and a holder of every object checks it (jwp-receipts §3, *The group
total*). A group in the legacy payload carries none; a verifier holding every
object reads its total from the objects' labels as §2.8 reads a total, and
otherwise reads it as unlabelled.
- **A `WAI_RECEIPT` property** ([moq-streaming-format](moq-streaming-format.md)
§5) has no signature of its own. Its class is inside a signature only where
it equals a signed statement of the same object's figure and class: the label
in the object's leaf, or an `energy` claim about the object. A sink reads it
as a class only after checking that it does (moq-streaming-format §5).
An `energy` claim ([jwp-receipts](jwp-receipts.md) §2.4.5) gives an
object's figure a signed place for its class without changing the object
receipt. It is a receipt of its own, signed by whoever acquired the
figure, that restates the object's `joules_micro` beside its class and the
class's evidence in §2.7's JSON fields, and a verifier holding both
rejects the pair if the two figures differ. The claim meets §2: its figure,
class and evidence are inside its signature. The object receipt still has
no member for the class, and on its own still falls short. Nothing like the
claim exists for a group total.
An `energy` claim ([jwp-receipts](jwp-receipts.md) §2.4.5) is a receipt of its
own, signed by whoever acquired the figure. It restates an object's
`joules_micro` beside its class and the class's evidence in §2.7's JSON
fields, and meets §2. A verifier holding the claim and the object receipt
rejects the pair if the two figures differ, or, where the object's leaf is
labelled, if the two classes or their evidence differ. It is the way for a
party other than the publisher to sign a class for an object's figure.
The interactive-worlds session receipt is a profile of the group receipt that
carries the class as an optional signed label (interactive-worlds §8, and
§2.8 here). It is signed under its own domain, `wai:world-receipt\x01`, not
the base group receipt's, so it is not among these. A JWP group or object
receipt that carries a non-zero figure falls short of §2. A `WAI_RECEIPT`
property falls short too, because its class is outside any signature. This
extension records that rather than exempting them. Giving the group total a
signed place for its class, and giving either receipt a member of its own for
it, remains open: it changes the jwp-receipts wire format and what its
producers seal.
carries the class as an optional signed label (interactive-worlds §8, and §2.8
here). It is signed under its own domain, `wai:world-receipt\x01`, not the
base group receipt's, and nothing above changes it.
## 2. The acquisition class (REQUIRED)
A receipt this extension governs (§1) that carries a non-zero energy figure
**MUST** carry an acquisition class alongside it. The one exception is the
unlabelled legacy form that §2.8 keeps for the receipt families that sealed a
figure before the class existed. The JWP group and object receipts, and the
`WAI_RECEIPT` property that carries an object's class over MoQ, cannot meet
this section as they are defined; §1 says where each falls short, and that it
is open.
figure before the class existed, the JWP object receipt and the JWP group
receipt's legacy payload among them. A `WAI_RECEIPT` property's class counts
only where §1 says it does.
| class | meaning | requires |
|---|---|---|
@ -132,6 +132,18 @@ applies", it means this order.
Normative:
- **An unmetered figure is not a figure of zero joules.** Where a field encodes
unmetered as `0` — `joules_micro` in every receipt profile WAI defines, and
the legacy fields that predate this section — that `0` is the **unmetered
marker**: a reader **MUST** present it as unmetered and **MUST NOT** present,
total, average or compare it as 0 J. A total over parts that include
unmetered parts covers only the measured parts (`Partial`, energy-binding
§4) and **MUST** say so. A field or form defined after this revision marks
unmetered explicitly — class tag `0` beside a zero figure, as the labelled
JWP leaf and group do (jwp-receipts §2.2, §2.3) — and never by a bare `0`
alone. A meter that cannot measure produces no reading at all, never a
reading of zero, and a model whose output is zero is not a measurement of
zero either.
- An implementation **MUST NOT** emit a non-zero energy figure without a class
in a receipt this extension governs (§1), except in the unlabelled form of
§2.8. An unlabelled number cannot be compared to any other number.
@ -170,8 +182,12 @@ reference implementation seals carries its figure in one of two ways:
quantum receipt of `wai-quantum` (circuit, calibration, job, mitigation,
decode, compile, rearrange, phasor, and the bill of materials per entry).
Neither crate seals a JWP group or object receipt or writes a `WAI_RECEIPT`
property, the carriers §1 records as falling short of §2.
`wai-rs` reads and verifies JWP group and object receipts in both forms of
each, computes an object's leaf in both forms of jwp-receipts §2.2, reads a
group total from its objects, and reads a `WAI_RECEIPT` property against a
signed statement of its object (`jwp_delivery`). Neither crate seals a JWP
group receipt for delivery — `wai-rs` builds them only for its conformance
corpus — or writes a `WAI_RECEIPT` property.
### 2.1 Relationship to network-layer energy identities
@ -391,7 +407,8 @@ rounds down declares it relative to the smaller number. At `E` = 0.563 µJ with
The term shrinks only as `C` grows, so it is large when an expensive payload
completes only a handful of times (§3). An implementation **MUST** state
which opening it used. An implementation that removes more of this term by
which opening it used; a measurement report states it in its bracket's
`opening` (§6). An implementation that removes more of this term by
construction **MUST** state how, and **MUST** still declare whatever part it
has not removed.
- The window is timed on a monotonic clock around the busy bracket. An
@ -473,8 +490,9 @@ Receipts sealed before this revision carry `HwShunt`. Its meaning is fixed as
sealed, so it accepts `HwShunt` there as above. The reference implementation
meets the rule in its own producers, none of which emits `HwShunt`, and
holds its callers to it in part: its meters cannot produce `HwShunt`, a new
energy binding, an optional label (§2.8) and an `energy` claim
([jwp-receipts](jwp-receipts.md) §2.4.5) refuse it, and the `wai-rs`
energy binding, an optional label (§2.8), the label of a JWP object's leaf or
group, and an `energy`, `measurement` or `operating-point` claim
([jwp-receipts](jwp-receipts.md) §2.4.5, §2.4.8–§2.4.9) refuse it, and the `wai-rs`
variant is deprecated, so code that names it draws a compiler warning. The
constructors of the `wai.video.int_motion` receipt and the `wai.video.*` OTT
receipts accept `HwShunt`, so that an issued receipt can be recreated byte
@ -494,7 +512,7 @@ by the class's evidence:
| tag | class | followed by |
|---|---|---|
| `0` | unmetered (no class) | — |
| `0` | no class: the unmetered marker beside a zero figure; in a receipt family of §2.8, the slot of an unlabelled legacy figure beside a non-zero one | — |
| `1` | `HwShunt` (legacy) | — |
| `2` | `ModelBased` | — |
| `3` | `Estimator` | — |
@ -586,10 +604,30 @@ Where a receipt carries several figures, each is labelled where it sits:
domain, every entry's figure is followed by its optional class, `0` when
unlabelled. An entry's class restates its stage receipt's class, and a
verifier binding the bill to its stage receipts checks that they agree.
- In both, the total carries no class of its own. A sum over figures of
different classes is only as strong as its weakest part. This covers a
total whose parts are labelled inside the same receipt. A JWP group
receipt's total is not one (§1).
- A JWP group receipt labels each object in that object's Merkle leaf
([jwp-receipts](jwp-receipts.md) §2.2). A labelled object's leaf uses the
`0x02` leaf domain, with the figure, its class and the object's origin after
the content hash; an unlabelled object keeps its legacy leaf. Its group may
sign the total's own class under the `0x02` group-signing domain (§1).
A provenance or quantum bill total carries no class of its own, and neither
does a JWP group total in the legacy payload. A sum over figures of different
classes is only as strong as its weakest part, and an unmetered part is not a
zero. A reader that holds every part reads such a total as follows:
- if every part's figure is the unmetered marker, the total is unmetered;
- otherwise, if any non-zero figure is unlabelled, the total is unlabelled;
- otherwise the total reads at the weakest class among the non-zero figures, in
§2's order, as a class name only. A total read this way carries no declared
uncertainty and **MUST NOT** be presented as carrying one. It covers the
whole of the work when no part is unmetered, and only the measured parts
otherwise ([energy-binding](energy-binding.md) §4's `Whole` and `Partial`).
A reader that does not hold every part reads such a total as unlabelled. A JWP
group total in the labelled payload carries its own class instead, composed by
jwp-receipts §2.4.1's rule, which adds a declared uncertainty for a hardware
class: the parts' figure-weighted declarations, rounded up, over the longest
window — linear in the parts, so it assumes their errors fully correlated.
In the reference implementation, each `wai-rs` family has an
`energy_provenance` field and `with_energy_class()`, which re-signs with the
@ -612,8 +650,9 @@ before the label existed.
What the reference implementation seals in these families:
- The reference meters that seal into them (`wai_meter`, `wai_world_meter`,
`wai_quantum_meter`, `wai_prov`) label every non-zero figure they seal, and
seal a figure they could not measure as zero, unlabelled.
`wai_quantum_meter`, `wai_prov`) label every non-zero figure they seal,
write a measurement report (§6) for it by default, and seal a figure they
could not measure with the unmetered marker, unlabelled.
- Two producers seal a new non-zero figure without a label, and so fall
short of the **SHOULD** above. `world_model::frame_receipt` is handed its
caller's figures without their class; a caller that knows the class builds
@ -696,7 +735,48 @@ busy mean and standard deviation (sample standard deviations, as §2.3 uses),
the sample counts, the window, the completion count, the thread count and how
the window opened alongside any figure it publishes. Those numbers are
what make a figure auditable, and they are what its declared uncertainty (§2.3)
is computed from; the figure alone is not auditable.
is computed from; the figure alone is not auditable. Where the figure is sealed
in a receipt, they are published as §6 says.
### 3.1 A live power reader
A power reader can return values for a rail that is not being refreshed. A
frozen rail reads as zero in every sample, or as the last value the reader
holds; a rail refreshed in batches reads the same value except where one batch
lands, and one refresh can be published in two steps in adjacent samples.
Bracketed, either gives an idle bracket whose samples are all equal and a busy
bracket with a lump or a step in it: a flat idle, a positive marginal, and a
ratio (§4) that reads as clean — from no measurement of the work at all.
So a figure is sealed under a hardware class only from brackets in which the
reader's rail was **live**. A rail is live in a bracket only if:
- it advanced in two consecutive intervals;
- it advanced in at least five intervals, and in at least four of every five of
the bracket's intervals; and
- the bracket spans at least 300 ms.
An interval is one sample of a power reader, in which the rail advanced if the
sample is strictly above zero and differs from the sample before it (the first
sample of a bracket has none before it, and advanced if it is above zero); or
one pair of successive reads of an energy counter, in which it advanced if the
second read exceeds the first, across the counter's wrap. A reader that holds
its last value repeats it, and a repeated value is not an advance, whatever the
value. Both brackets must be live. One advance is not a live rail, and neither
is one refresh published in two steps. An implementation reading a counter at
the ends of a span alone has one interval, which is never enough: it reads the
counter during the span as well.
A figure from a bracket that is not live **MUST NOT** be sealed under a hardware
class. The producer seals it with the unmetered marker, or under a weaker class
it can justify. A measurement report carries each bracket's liveness facts —
its live intervals and its longest run of them (§6.2) — so a verifier checks the
first two conditions from the report; the span is the producer's to check.
A bracket whose samples are all equal is not evidence of a quiet machine, and
never of a live rail: its samples advanced once at most. Its standard deviation
is zero, and under a hardware class a reader refuses a bracket whose `sd_uw` is
0 whatever liveness facts it carries.
## 4. Attribution quality (REQUIRED)
@ -719,7 +799,9 @@ 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:
standard deviation and report it. A measurement report reports it by carrying
the values it follows from, and every reader derives the same verdict from them
(§6.2):
| ratio | verdict |
|---|---|
@ -743,12 +825,16 @@ 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 figure under a hardware class
(`CalibratedInstrument`, `OnChipCounter`) 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.
(`CalibratedInstrument`, `OnChipCounter`) when the reader's rail was not live
(§3.1), when the ratio is below 1, or when the per-unit figure rounds to zero.
None is a measurement of low energy; each is 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.
- A reader **MUST NOT** present a figure as publishable unless the verdict its
measurement report gives is clean (§6.2). A reader holding no report for a
figure cannot tell whether the figure met this bar, and **MUST NOT** say that
it did.
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
@ -767,31 +853,163 @@ higher.
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.
the build profile alongside any published figure; a measurement report
states it in `build` (§6).
- **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.
a class's reconstruction is byte-exact and proven across architectures, a
verifier holding the artifact re-derives the output and the work unit. It cannot
re-measure the energy, and it cannot re-measure anything that describes how the
energy was obtained: the acquisition class and its evidence (§2.2–§2.5), the
bracket values of §3 and their liveness facts (§3.1), the ratio they give (§4),
the build profile, and the coverage and method a receipt states beside the
figure. Those are the measurer's statements, as the figure is, and a signature
makes them attributable, not true.
Some of them can still be checked without re-measuring anything: a declaration
against the resolution floor (§2.3), which needs nothing but the receipt; a
calibration reference against the certificate it names (§2.5); and a
measurement report's values against the figure and the declared uncertainty
they must give, and against the liveness rule (§6.2). Values that pass those
checks are consistent, not correct: a measurer who invents every value
consistently passes every one of them. Pairing a meter with a reproducible
outcome narrows what rests on the measurer to the measurement and its
description. 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), with
its declared uncertainty and window (§2.3) and its filtering state (§2.4) or
calibration reference (§2.5); the idle mean and standard deviation, busy mean
and standard deviation, sample counts, window, completion count, thread count
and how the window opened (§3); the attribution-quality ratio (§4); the build profile (§5); and the
recomputable work unit the rate is expressed against.
calibration reference (§2.5); the build profile (§5); the recomputable work unit
the rate is expressed against; and, for a figure obtained under the protocol of
§3, the idle mean and standard deviation, the busy mean and standard deviation,
the sample counts, each bracket's liveness facts (§3.1), the window, the
completion count, the thread count and how the window opened. The
attribution-quality ratio (§4) follows from those values (§6.2).
Where a figure is sealed in a receipt with a class, its producer **MUST**
publish these, except the work unit, which the receipt carries, as a
`measurement` claim about the figure ([jwp-receipts](jwp-receipts.md) §2.4.8).
The claim names the figure by its figure profile (§6.1), restates it with its
class, and leaves the receipt that seals it unchanged. Only the key that signed
the figure may sign the claim, so a figure that no Ed25519 key signs — an energy
binding's (§6.1) — has no `measurement` claim; carrying a binding's report in
its C2PA manifest is outside this revision. A report printed beside a receipt
but not signed does not meet this section. No verifier can tell a figure whose
report was withheld from one that never had one, so this rule binds producers,
and what a verifier checks is a report it holds (§6.2).
Producers that fall short of this section, recorded rather than exempted:
the generators that recreate a committed corpus with its unlabelled legacy
figures (§2.8); `world_model::frame_receipt`, which is handed its caller's
figures without their class or their bracket; and the in-browser demos of
`wai-web`, which seal illustrative figures. A figure sealed with no class
cannot meet this section, because a `measurement` claim restates a class.
A figure published without these is not wrong so much as unusable: nothing about
it can be checked, reproduced, or compared.
### 6.1 Figure profiles
A report about a figure ([jwp-receipts](jwp-receipts.md) §2.4.8–§2.4.9) names
it by a **figure profile**, an index, and the identity of the receipt that
carries the figure. A profile is named by the domain its receipt's identity is
computed under, without the domain's final version byte. It fixes the claim's
`subject_type`, how the identity is computed, which figure an index selects,
where the figure's class is, and which key signed it.
| figure profile | receipt | `subject_type` and identity | figure; its class | index | signed by |
|---|---|---|---|---|---|
| `moq-jwp:object` | JWP object receipt (jwp-receipts §2.4.2), accepted under its group (§3) | `object`: its §2.1 content hash; the claim's `group` is the group receipt's link hash | `joules_micro`; the label in its leaf (jwp-receipts §2.2) | its leaf index | the group receipt's `signer_pubkey` |
| `moq-jwp:claim-link` | `energy` claim (jwp-receipts §2.4.5) | `receipt`: its `link_hash` | `joules_micro`; its class | `null` | its `signer_pubkey` |
| `wai:energy-binding-id` | energy binding ([energy-binding](energy-binding.md) §2) | `receipt`: `BLAKE3("wai:energy-binding-id\x01" ‖ JCS(body))` (energy-binding §4) | `microjoules`; `acquisition` | `null` | no Ed25519 key |
| `wai:video-receipt-id`, `wai:video-manifest-receipt-id`, `wai:video-ssai-receipt-id`, `wai:video-keys-receipt-id`, `wai:video-package-receipt-id`, `wai:video-channel-receipt-id` | the `wai.video.int_motion` receipt and the `wai.video.*` OTT receipts | `receipt`: its receipt hash | `joules_micro`; its class | `null` | its `signer_pubkey` |
| `wai:world-receipt-id`, `wai:film-receipt-id`, `wai:haptic-receipt-id`, `wai:score-receipt-id`, `wai:splat4d-receipt-id`, `wai:audio-receipt-id`, `wai:ternary-receipt-id` | the world-session, film and reel, haptic, score, 4D-splat, audio-scene and ternary-reconstruction receipts | `receipt`: its receipt hash, over the payload in the form it was signed (§2.8) | `joules_micro`; its optional label | `null` | its `signer_pubkey` |
| `wai:asset-receipt-id` | provenance receipt | `receipt`: its receipt hash | step `i`'s `joules_micro`; that step's optional label | step, from 0 | its `signer_pubkey` |
| `wai:quantum-receipt-id`, `wai:quantum-calibration-id`, `wai:quantum-job-id`, `wai:quantum-mitigate-id`, `wai:quantum-decode-id`, `wai:quantum-compile-id`, `wai:quantum-rearrange-id`, `wai:phasor-receipt-id` | the quantum receipts | `receipt`: its receipt hash in the form it was sealed, labelled or not | `joules_micro`; its optional label | `null` | its `signer_pubkey` |
| `wai:quantum-qbom-id` | quantum bill of materials | `receipt`: its receipt hash | entry `i`'s `joules_micro`; that entry's optional label | entry, from 0 | its `signer_pubkey` |
A receipt hash here is `BLAKE3(identity domain ‖ signing payload ‖ signature)`,
as each family defines it. One content hash can be delivered in more than one
group, so a `moq-jwp:object` report also names the group, and the same object in
another group is another figure. A claim naming a profile not in this table is
refused (jwp-receipts §3.1, `figure_unknown_profile`). A receipt family that
seals a figure after this revision adds its row.
### 6.2 The measurement report
A measurement report ([jwp-receipts](jwp-receipts.md) §2.4.8) carries a figure's
§3 values as integers:
- each bracket's sample count `n`; its mean, in microwatts, rounded to the
nearest with a half rounding up; its sample standard deviation (divisor
`n − 1`, as §2.3 uses), in microwatts, rounded up, so a sealed dispersion never
states less than was evaluated; and its liveness facts (§3.1): the samples in
which the reader's rail advanced, and the longest run of consecutive ones;
- the window `T`, the busy bracket's, in microseconds rounded to the nearest:
the `window_us` of the figure's declared uncertainty (§2.3);
- the completion count `C`, the thread count `N`, and how the window opened.
A producer **MUST** compute the figure it seals, and the least uncertainty it
declares, from those integers as below and not from the samples they summarise,
so that every verifier reproduces both exactly.
Let `M` be the busy mean less the idle mean, and let the work the window paid
for (§2.3) be `Wn / Wd`: `Wn = 2C + N` and `Wd = 2` for a window opened from
rest, `Wn = C` and `Wd = 1` on running workers. The per-unit estimate is
`E = M·T·Wd / (10⁶·Wn)` µJ, and the figure is `E` rounded to the nearest
microjoule, a half rounding up:
```
F = ⌊(2·M·T·Wd + 10⁶·Wn) / (2·10⁶·Wn)⌋
```
With `n_i`, `s_i` the idle bracket's count and standard deviation and `n_b`,
`s_b` the busy bracket's, §2.3's `u_rel` over those integers, multiplied out, is
the least integer `p` for which:
```
p² · K ≥ P, where
K = 12 · n_i · n_b · Wn⁴ · F²
P = 12 · Wn² · T² · Wd² · (s_i² · n_b + s_b² · n_i)
+ 4 · n_i · n_b · M² · T² · Wd² · N²
+ 10¹² · n_i · n_b · Wn⁴
```
The three terms of `P` are `u_power`, `u_count` and the `0.5/√3` µJ of rounding,
each carried across in microjoules (`u_count` is `N / (√3 · Wn)` from rest and
on running workers alike). The least `p` always meets the resolution floor of
§2.3, `F · p ≥ 288 676`. Both computations are exact integer arithmetic. With
every value at most 2⁵³ − 1 (jwp-receipts §2.4.3), no product exceeds 2⁵¹² and
no floating-point operation is involved, so every verifier reaches the same `F`
and `p`.
A figure sealed under `OnChipCounter` or `CalibratedInstrument` **MUST** come
from brackets whose rail was live (§3.1), and **MUST** declare a `relative_ppm`
of at least `p` over `window_us = T`. It declares more wherever its declaration
covers more than the bracket does: an instrument's calibration (§2.3), the noise
a filtered counter's vendor states (§2.4), or a clock resolution that is not
negligible (§2.3). A figure whose `p` exceeds 2³² − 1 cannot be declared (§2.7)
and **MUST NOT** be sealed under a hardware class.
The attribution-quality ratio (§4) is `M / s_i`. Every reader derives the same
verdict from the report's values: clean when `M ≥ 10 · s_i`, usable for relative
comparison when `M ≥ 3 · s_i`, noisy when `M ≥ s_i`, and unusable otherwise. The
verdict is about the bracket's arithmetic; under a hardware class a report is
accepted only where the liveness facts also show the rail live and neither
bracket's `sd_uw` is zero, so a flat idle bracket from a frozen rail, or from a
reader holding a stale value, is refused, not read as clean.
What a verifier checks with a report is arithmetic: that the figure is the one
its values give, that the declared uncertainty is at least what they support,
and, under a hardware class, that the rail was live and the ratio is at least 1
(jwp-receipts §2.4.8). It does not check the values, which cannot be re-measured
(§5).
## 7. Worked example
`wai.video.int_motion`, real-footage vector, 8 frames of 96×96 RGB, release
@ -820,7 +1038,10 @@ microjoules adds. It also opens its window from rest and divides by
so its per-decode figure also carries the high-reading count bias of §2.3. None
of this can be reconstructed for that run: neither the busy bracket's standard
deviation nor the thread count was recorded, and substituting either would be
the invention this section refuses.
the invention this section refuses. The reference meter now also publishes
these values, with the build profile, in a measurement report (§6). The run
above printed them to its console, and its report cannot be written after the
fact.
**This example is deliberately a failing one.** It is a complete, honestly
reported bracket that this extension nonetheless forbids publishing as an
@ -833,7 +1054,10 @@ 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.
the codestream hash, the frame hashes and the work figure from the codestream
alone. The energy, its class and the class's evidence rest on the signer (§5); a
report about the figure lets a verifier check that they are consistent with the
bracket, and no more (§6.2).
## 8. Reproducing a figure
@ -843,4 +1067,7 @@ interface, the bracket parameters of §3, and the declared uncertainty with the
evidence of §2.3–§2.5. 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.
prevent. A measurement report (§6) publishes, signed, the build profile, the
toolchain and target, the power reader and the bracket with its liveness facts,
in cleartext: the measuring machine's power levels and build environment are
visible to whoever holds the report.

View file

@ -507,7 +507,9 @@ as `SessionReceipt::parent_receipt_hash` + `receipt_hash()` +
`wai_world_meter` brackets a replay burst against an idle baseline and
reports the marginal measured CPU energy. A browser, lacking those
counters, supplies a wall-clock estimate against a **declared** power
coefficient and labels it an estimate; an unmetered sink signs `0`.
coefficient and labels it an estimate; an unmetered sink signs the
unmetered marker `0`, which a reader presents as unmetered and never as a
figure of zero joules ([energy-measurement](energy-measurement.md) §2).
Joules are derived *from* measurement or a stated model, never
asserted in place of one. The figure's acquisition class rides beside
it as an optional signed label

View file

@ -31,9 +31,15 @@ In scope:
- **Typed receipt classes** (§2.4): every receipt belongs to exactly one
class. `group` and `object` receipts are the **delivery** class; a
`claim` receipt (§2.4.3) carries one of the others — `energy`,
`lawful-basis`, `log-inclusion` (§2.4.5–§2.4.7) — on one envelope, one
canonical form and one signature rule, so a statement other than
delivery does not need a field set of its own.
`lawful-basis`, `log-inclusion`, `measurement`, `operating-point`
(§2.4.5–§2.4.9) — on one envelope, one canonical form and one signature
rule, so a statement other than delivery does not need a field set of
its own.
- An optional acquisition-class label on the object receipt, bound with the
object's figure and origin into its Merkle leaf (§2.2, §2.4.2); an
optional class for the group total, signed under a labelled group payload
with its coverage and the parent link (§2.3, §2.4.1); and the group total
read from its objects (§3).
- **A second binding class** (§2.6): a soft binding, declared by the
publisher with a registry-named algorithm and a resolution endpoint,
for objects whose bytes changed in normal distribution — and the
@ -100,9 +106,25 @@ internal nodes use distinct domain separators:
```
leaf(content_hash) = BLAKE3("moq-jwp:merkle-leaf\x01" || content_hash)
leaf(content_hash, joules_micro, class, origin)
= BLAKE3("moq-jwp:merkle-leaf\x02" || content_hash
|| u64_be(joules_micro) || class
|| u32_be(len(origin)) || origin_utf8)
node(left, right) = BLAKE3("moq-jwp:merkle-node\x01" || left || right)
```
An object's leaf takes the first form when its receipt carries no class
label (§2.4.2) and the second when it does. `class` is the label's signed
encoding: the tag byte and the evidence that follows it
([energy-measurement](energy-measurement.md) §2.7), tag `0` marking an
object explicitly unmetered. The encoding is self-delimiting and `origin` is
length-prefixed, so no two inputs share a leaf. The version byte keeps the two
forms disjoint, so a label, or the origin, can be neither added, stripped nor
altered on a delivered object without changing the root the group signature
covers. A group may mix the forms. A verifier that predates the labelled form
computes the first form for every object: it refuses a labelled object at §3
step 5 and verifies an unlabelled one as before.
When the leaf count at any level is odd, the publisher MUST duplicate
the final leaf to obtain an even pair. The root is the single hash at
the top of the tree. The empty-group root is the 32-byte all-zero
@ -131,6 +153,23 @@ signing_payload =
The signature is exactly 64 bytes. Implementations MUST reject any
group receipt whose `signature` length is not 64.
A group whose total carries a class of its own (§2.4.1) is signed over the
**labelled** payload instead: the fields above, in that order, under the
domain `"moq-jwp:group-sign\x02"`, followed by
```
|| class // energy-measurement §2.7: tag, evidence
|| u8(coverage) // 0 Whole, 1 Partial
|| u8(has_parent) || parent[32] // 1 and parent_receipt_hash, or 0 and 32 zero bytes
```
`class` is tag `0` exactly when `total_joules_micro` is zero, and then
`coverage` is `0`. The labelled payload signs `parent_receipt_hash`, which the
legacy payload does not (§6.1). The two domains keep the payloads disjoint. A
verifier or relay that predates the labelled payload computes the legacy one
and refuses a labelled group at §3 step 2; every group signed over the legacy
payload verifies as before.
### 2.4. Receipt envelope (typed)
Three receipt kinds are defined: `group` (one per group), `object`
@ -154,7 +193,7 @@ Required fields:
| `group_id` | u64 | Monotonic group counter under `publisher_key`. |
| `root_hash` | 32 B hex | §2.2 Merkle root over object content hashes. |
| `leaf_count` | u64 | Number of objects covered by `root_hash`. |
| `joules_total` | u64 | Sum of microjoules across the group's objects. No member carries its acquisition class, which [energy-measurement](energy-measurement.md) §2 requires beside a non-zero figure, and no claim class carries one for a group total; §1 there records this as open. |
| `joules_total` | u64 | Sum of microjoules across the group's objects; `0` is the unmetered marker ([energy-measurement](energy-measurement.md) §2). Its class, where it carries one, is in the labelled members below; a verifier holding every object checks it, or, for a group without one, reads the total from the objects' labels (§3, *The group total*). |
| `group_start` | string | RFC 3339 timestamp of the first object. |
| `group_end` | string | RFC 3339 timestamp of the last object. |
| `signer_pubkey` | 32 B hex | Ed25519 verifying key per RFC 8032. |
@ -162,6 +201,38 @@ Required fields:
| `sig` | 64 B hex | Ed25519 signature over §2.3 payload. |
| `parent_receipt_hash` | 32 B hex or `null` | BLAKE3 of the previous group receipt's canonical bytes under this publisher key; `null` for the first group. |
**The total's class (labelled form).** A group whose total carries a class of
its own adds these members and is signed over the labelled payload (§2.3):
| field | type | meaning |
|---|---|---|
| `sign_version` | integer | `2`. Absent (or `null`), the group is in the legacy form; any other value is refused (`group_label_invalid`). |
| `energy_provenance`, `energy_uncertainty`, `energy_counter_filtering`, `energy_calibration_ref` | as energy-measurement §2.7 | The total's class and its evidence; all absent for the unmetered marker beside a zero total. |
| `energy_coverage` | string | `Whole` or `Partial`; absent with the unmetered marker. |
A legacy group carrying any of these members, a `sign_version` other than `2`,
or a labelled group whose class cannot be read, is refused
(`group_label_invalid`), never read as the legacy form. The class **MUST** be the one the group's objects compose to, by this
rule over the objects whose figure `F_i` is not zero, in integers:
- if any such object is unlabelled, the group **MUST NOT** carry a class (it is
signed in the legacy form);
- if there is none, the class is the unmetered marker and the coverage `Whole`;
- otherwise the class is the weakest of the objects' classes, in
energy-measurement §2's order, and the coverage is `Partial` exactly when some
object's figure is zero;
- for a hardware class, `relative_ppm` is at least `⌈Σ F_i·p_i / Σ F_i⌉` and
`window_us` is the longest `window_us_i`. The bound is linear in the parts, so
it assumes their errors fully correlated, and it is conservative;
- an `OnChipCounter` total's filtering state is the objects' common one, or
`Unknown`; a `CalibratedInstrument` total needs one calibration reference
shared by every object, or the group carries no class.
The resolution floor then holds for the total. With the group alone a verifier
checks that the class reads, is not `HwShunt`, is the unmetered marker exactly
beside a zero total, and reaches the resolution floor (§3 step 2); with every
object it checks the composition (§3, *The group total*).
#### 2.4.2. `kind: "object"`
Required fields:
@ -173,10 +244,29 @@ 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. 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. This receipt has no member for the class; an `energy` claim (§2.4.5) restates the figure with its class under a signature. |
| `joules_micro` | u64 | Microjoules consumed producing this object; `0` is the unmetered marker. 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. Its class rides as an optional label, below, bound into the object's leaf with the figure and the origin (§2.2). |
| `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. |
**Optional class label.** An object receipt **MAY** carry its figure's
acquisition class as an optional label
([energy-measurement](energy-measurement.md) §2.8): `"leaf_version": 2`, and
the class in energy-measurement §2.7's JSON fields — `energy_provenance`, and
for a refined hardware class `energy_uncertainty` with
`energy_counter_filtering` or `energy_calibration_ref`. With
`"leaf_version": 2` and no class the object is explicitly unmetered (tag `0`),
which is valid only beside a zero figure. Absent, the receipt is exactly as
above and its leaf takes the legacy form of §2.2. Present, its leaf takes the
labelled form, and the group signature covers the figure, the class, the
class's evidence and the origin through the root. A label **MUST NOT** be the
legacy `HwShunt` and **MUST NOT** sit beside a zero figure; a refined class
**MUST** carry its evidence and reach the resolution floor (energy-measurement
§2.3); class members without `"leaf_version": 2`, evidence without a class, or
a class that cannot be read, are refused rather than dropped
(energy-measurement §2.7). A verifier refuses an object that breaks any of
these at §3 step 5 (`object_label_invalid`). An `energy` claim (§2.4.5) remains
the way for a party other than the publisher to sign a class for the figure.
#### 2.4.3. `kind: "claim"`
`group` and `object` answer one question — *what was delivered, by
@ -197,10 +287,10 @@ is present and `null`, so one claim has one canonical form.
| `kind` | string | MUST equal `"claim"`. |
| `class` | string | A class of §2.4.4 other than `delivery`. |
| `subject_type` | string | `"object"` or `"receipt"`; which a class admits is fixed by §2.4.4. |
| `subject` | 32 B hex | For `object`: the §2.1 content hash of the object the claim is about. For `receipt`: the link hash of a receipt — §3 step 3's for a group receipt, `link_hash` below for a claim. |
| `subject` | 32 B hex | For `object`: the §2.1 content hash of the object the claim is about. For `receipt`: the link hash of a receipt — §3 step 3's for a group receipt, `link_hash` below for a claim — or, for a class that names a figure profile (§2.4.8–§2.4.9), the identity that profile defines ([energy-measurement](energy-measurement.md) §6.1). |
| `seq` | integer | Strictly increasing across the claims of one `signer_pubkey`. |
| `issued_at` | string | A date-time (below) at which the signer issued the claim. Self-asserted, like `group_end`. |
| `body` | object | Class-scoped; its members are fixed by `class` and by nothing else (§2.4.5–§2.4.7, §2.6). |
| `body` | object | Class-scoped; its members are fixed by `class` and by nothing else (§2.4.5–§2.4.9, §2.6). |
| `signer_pubkey` | 32 B hex | Ed25519 verifying key per RFC 8032. |
| `signer_id` | string | Bound identity string. |
| `parent_receipt_hash` | 32 B hex or `null` | `link_hash` of this signer's previous claim; `null` for its first. |
@ -267,6 +357,8 @@ key.
| `log-inclusion` | `claim` | `receipt` | §2.4.7 | whoever attaches the log's evidence |
| `soft-binding` | `claim` | `object` | §2.6.2 | the object's publisher |
| `binding-recovery` | `claim` | `object` (the ancestor) | §2.6.3 | the party that performed the recovery |
| `measurement` | `claim` | `object` or `receipt`, as its figure profile fixes | §2.4.8 | the key that signed the figure: the group's publisher for `moq-jwp:object`, the receipt's signer for every other profile |
| `operating-point` | `claim` | `object` or `receipt`, as its figure profile fixes | §2.4.9 | the party that records the request, the commitment and the delivery |
Rules:
@ -288,9 +380,17 @@ Rules:
not evidence about delivery, and a delivery signature is not
evidence about a claim.
- A `claim` that repeats a figure a delivery receipt carries for the
same subject — the `energy` class does, deliberately, to bind its
labels to the figure — **MUST** agree with it. Where they disagree a
verifier **MUST** reject rather than choose.
same subject **MUST** agree with it; the `energy` class does this
deliberately, to bind its labels to the figure. Where the delivery
receipt labels its figure (§2.4.2), the claim **MUST** also agree with
that label's class and evidence. Where they disagree a verifier **MUST**
reject rather than choose.
- A class that is about one figure — `measurement` or `operating-point` —
names it by a figure profile ([energy-measurement](energy-measurement.md)
§6.1) and the identity that profile defines, restates it with its class,
and is checked against the receipt that carries it at §3.1 step 6. It never
changes that receipt, and never supplies a class that receipt does not
carry.
#### 2.4.5. `class: "energy"`
@ -368,8 +468,12 @@ for a subject **MUST NOT** report that subject's `joules_micro` as the
whole cost of producing it.
**Against the delivery receipt** (§3.1 step 6): the claim's
`joules_micro` **MUST** equal the `object` receipt's for the same
subject.
`joules_micro` **MUST** equal the `object` receipt's for the same subject
(`energy_disagrees_with_delivery`). Where that receipt carries a class label
(§2.4.2), the claim's class and its evidence **MUST** also equal the label's
(`energy_class_disagrees_with_delivery`). Each is read as energy-measurement
§2.7 reads it, so an `OnChipCounter` label with no filtering state reads as
`Unknown`.
**Against the OER receipt it names** (§3.1 step 6): a verifier holding
that receipt checks, in this order. The receipt is read as oer-receipts
@ -496,6 +600,159 @@ the subject as logged. This claim's signature never stands in for the
log's, and [transparency](transparency.md) §6 applies: registration is
a witness, not a verification.
#### 2.4.8. `class: "measurement"`
[energy-measurement](energy-measurement.md) §6 requires a published figure
to be accompanied by how it was measured: for a figure obtained under that
extension's §3, the values its bracket gave, and for any figure, the build
profile. A `measurement` claim carries them for one figure. It restates the
figure and its acquisition class, so that it can be checked on its own, and
leaves the receipt that seals the figure unchanged. It names that receipt by a
figure profile (energy-measurement §6.1) and the receipt's identity under it.
Its `subject_type` and `subject` are the ones the profile gives: `object` and
the object's §2.1 content hash for `moq-jwp:object`, and `receipt` and the
receipt's identity for every other profile.
| member | type | meaning |
|---|---|---|
| `figure_profile` | term | A figure profile of energy-measurement §6.1. |
| `figure_index` | integer or `null` | Which figure, from 0, in a profile that carries several — for `moq-jwp:object`, the object's leaf index; `null` in one that carries one. |
| `group` | 32 B hex or `null` | For `moq-jwp:object`, the link hash of the group receipt the object was delivered under (§3 step 3); `null` for every other profile. |
| `joules_micro` | integer | The figure. |
| `energy_provenance` | string | Its acquisition class, as §2.4.5 writes it. |
| `energy_uncertainty` | object or `null` | As §2.4.5. |
| `energy_counter_filtering` | string or `null` | As §2.4.5. |
| `energy_calibration_ref` | string or `null` | As §2.4.5. |
| `bracket` | object or `null` | For a figure obtained under energy-measurement §3: `{ "idle": summary, "busy": summary, "window_us": integer, "completions": integer, "in_flight": integer, "opening": "from-rest" or "running" }`, a summary being `{ "n": integer, "mean_uw": integer, "sd_uw": integer, "live_intervals": integer, "longest_live_run": integer }` as energy-measurement §6.2 defines them. `null` otherwise. |
| `build` | object | `{ "profile": "release" or "debug", "toolchain": string or null, "target": term or null }`: the build of the code measured (energy-measurement §5, §8). `release` is a build optimised as shipped; any other build is `debug`. A finer vocabulary is a new registration. |
| `power_reader` | string or `null` | The reader of the power interface (energy-measurement §8). Required under a hardware class (rule 4). |
A report carries none of what follows from its values: neither the §4 ratio nor
its verdict, which every reader derives (energy-measurement §6.2). A bracket
carrying either has a member its table does not define (`unknown_member`).
A `null` bracket says the figure was not obtained under energy-measurement §3 —
under a hardware class, for example, a counter read through the span of the
work. Such a report carries no values to check, so the figure's liveness
(energy-measurement §3.1) rests on its producer's statement alone, and no reader
derives a §4 verdict for it.
Rules, checked at §3.1 step 4 in this order:
1. `joules_micro` **MUST NOT** be zero (`energy_zero_figure`): a zero is the
unmetered marker, or a failed measurement, and gets no report.
2. `figure_profile` **MUST** name a profile of energy-measurement §6.1 whose
subject type is the claim's `subject_type` (`figure_unknown_profile`);
`figure_index` **MUST** be an integer exactly when that profile carries
several figures (`figure_index_mismatch`); and `group` **MUST** be a hash
exactly when the profile is `moq-jwp:object` (`figure_group_mismatch`).
3. The acquisition class, as §2.4.5's rule 2 (`energy_unknown_acquisition`,
`energy_legacy_acquisition`, `energy_acquisition_evidence`).
4. Under `OnChipCounter` or `CalibratedInstrument`, `power_reader` **MUST NOT**
be `null` (`measurement_no_power_reader`): a hardware figure is a reading, and
its report names what read it, whether or not it carries a bracket.
5. Where `bracket` is not `null`, in this order, with `M` its busy mean less its
idle mean:
- each `n` **MUST** be at least 2 (`measurement_too_few_samples`);
- under `OnChipCounter` or `CalibratedInstrument`, each bracket's rail
**MUST** have been live (energy-measurement §3.1): `sd_uw` not zero (a
bracket whose samples are all equal advanced once at most),
`longest_live_run` at least 2, `live_intervals` at least 5 and at least
four fifths of `n` (`live_intervals · 5 ≥ n · 4`), and
`longest_live_run ≤ live_intervals ≤ n` (`measurement_rail_not_live`);
- `window_us`, `completions` and `in_flight` **MUST NOT** be zero
(`measurement_empty_burst`);
- `M` **MUST** be positive (`measurement_no_marginal`);
- under `OnChipCounter` or `CalibratedInstrument`, `M` **MUST** be at least
the idle `sd_uw`. A ratio below 1 is a failure to measure
(energy-measurement §4) (`measurement_below_attribution_floor`);
- `joules_micro` **MUST** be the figure `F` the bracket gives
(energy-measurement §6.2) (`measurement_figure_mismatch`);
- under `OnChipCounter` or `CalibratedInstrument`, the declared `window_us`
**MUST** be the bracket's (`measurement_window_mismatch`), and the declared
`relative_ppm` **MUST** be at least the least `p` the bracket supports
(energy-measurement §6.2) (`measurement_declaration_below_bracket`).
**Against the figure** (§3.1 step 6). The context is the receipt the claim
names, verified under its own profile's rules; for `moq-jwp:object`, that is
the object receipt accepted under §3 against the group receipt `group` names.
In this order:
1. the receipt verifies (`context_invalid`); it is of `figure_profile`, its
identity under that profile is `subject`, it is within the group `group`
names, and it carries a figure at `figure_index` (`context_mismatch`);
2. that figure is `joules_micro` (`figure_disagrees`);
3. that figure carries an acquisition class (`figure_unclassified`). An
unlabelled figure has none, and neither has the unmetered marker; a claim does
not supply one;
4. that class and its evidence are the claim's, each read as energy-measurement
§2.7 reads it (`figure_class_disagrees`);
5. an Ed25519 key signs the figure (`figure_signer_unknown`) — every profile but
`wai:energy-binding-id` has one — and it is `signer_pubkey`
(`figure_signer_mismatch`). For `moq-jwp:object` it is the group receipt's
`signer_pubkey`. Only the key that signed a figure signs its report.
Only check 5 shows that a `measurement` claim's signer is the figure's, so a
`measurement` claim is accepted only once step 6 has run against the receipt
it names. A verifier that does not hold that receipt reports
`figure_not_held` at step 6 — recoverable by fetching the receipt — and
**MUST NOT** present the claim as the figure signer's report, nor derive a §4
verdict for the figure from its values: without the context, any key can sign
any values about any figure.
An accepted `measurement` claim establishes that the figure's signer reported
these values for this figure, and that the figure and its declared uncertainty
are what the values give. It does not establish that the values were measured:
they cannot be re-measured (energy-measurement §5). A reader derives the §4
verdict from an accepted claim's bracket (energy-measurement §6.2).
#### 2.4.9. `class: "operating-point"`
[energy-binding](energy-binding.md) §7 makes the operating point a figure was
spent at part of the claim about it: what was requested, what was committed,
what was delivered, and what a declared descriptor predicted. An
`operating-point` claim records these for one figure, named and restated as
§2.4.8 names and restates it, without changing the receipt that seals the
figure. It is signed by the party that records them.
| member | type | meaning |
|---|---|---|
| `figure_profile`, `figure_index`, `group`, `joules_micro`, `energy_provenance`, `energy_uncertainty`, `energy_counter_filtering`, `energy_calibration_ref` | | As §2.4.8. |
| `vocabulary` | absolute URI | The vocabulary the operating points' parameters, and the prediction's descriptor, are drawn from. WAI defines none. |
| `op_requested` | point or `null` | What the requesting party asked for; `null` when nothing was requested. |
| `op_committed` | point or `null` | What the acting party said it would use. |
| `op_delivered` | point or `null` | What was used. |
| `prediction` | object or `null` | `{ "predictor": absolute URI, "descriptor_hash": 32 B hex, "predicted_joules_micro": integer }`: the model that turned the declared descriptor into energy, `BLAKE3("wai:energy-descriptor\x01" ‖ descriptor)` over the descriptor as `vocabulary` serialises it, and the energy it implies for the work the figure covers. |
A **point** is an object whose member names are terms and whose values are
integers of §2.4.3. JCS orders its members and §3.1 step 1 refuses a repeated
one, so two points are equal exactly when their canonical forms are.
Rules, checked at §3.1 step 4 in this order:
1. §2.4.8's rules 1–3. A claim restating `HwShunt` is refused there, so no
operating point is recorded against a figure labelled with the legacy class.
2. A point that is present **MUST NOT** be empty (`operating_point_empty_point`).
3. `op_delivered` **MUST NOT** be present without `op_committed`
(`operating_point_delivered_alone`).
4. `op_requested` and `op_committed` **MUST NOT** be present without
`op_delivered` (`operating_point_incomplete`).
5. A claim **MUST** carry `op_delivered` or `prediction` (`operating_point_empty`).
**Against the figure** (§3.1 step 6): §2.4.8's checks 1–4. The claim may be
signed by a party other than the figure's signer, so check 5 does not apply,
and it may name a figure no Ed25519 key signs. An operating point recorded
against a figure that carries the unmetered marker or no class is refused here
(`figure_disagrees`, `figure_unclassified`).
From an accepted claim a reader derives, and no claim states:
- whether the commitment was met: it was when `op_delivered` equals
`op_committed`, and there was none when `op_committed` is `null`;
- the **residual**, `joules_micro − predicted_joules_micro`, a signed integer,
whenever `prediction` is present.
A reader **MUST NOT** report the commitment as met unless it derives that it
was. An accepted claim establishes what its signer recorded about the request,
the commitment, the delivery and the prediction. An `op_requested` in a claim
the acting party signed is that party's account of the request.
### 2.5. Worked example
Manifest JSON (group receipt, formatted for readability — wire form
@ -579,6 +836,71 @@ nothing; the figure is invented:
Its `link_hash` is
`797a6d60712bb66270be3683546ed9d92c7134d3a3cfa34f304e6fdad886046f`.
A `measurement` claim (corpus vector `measurement_accept`, §5.6) reporting
on an `energy` claim's figure, by the key that signed it: ten idle and
fourteen busy samples, every one live, ten workers released from rest and
sixty completions over 3 s. Its figure and its declared uncertainty are the
least its bracket's integers give (energy-measurement §6.2). Formatted for
readability; the signature is real, under the corpus's published test seed,
and the values are invented:
```json
{
"body": {
"bracket": {
"busy": {
"live_intervals": 14,
"longest_live_run": 14,
"mean_uw": 30000000,
"n": 14,
"sd_uw": 115471
},
"completions": 60,
"idle": {
"live_intervals": 10,
"longest_live_run": 10,
"mean_uw": 20000000,
"n": 10,
"sd_uw": 115471
},
"in_flight": 10,
"opening": "from-rest",
"window_us": 3000000
},
"build": {
"profile": "release",
"target": "x86_64-unknown-linux-gnu",
"toolchain": "rustc 1.98.0 (example)"
},
"energy_calibration_ref": null,
"energy_counter_filtering": "Unknown",
"energy_provenance": "OnChipCounter",
"energy_uncertainty": {
"relative_ppm": 44669,
"window_us": 3000000
},
"figure_index": null,
"figure_profile": "moq-jwp:claim-link",
"group": null,
"joules_micro": 461538,
"power_reader": "reader:example"
},
"class": "measurement",
"issued_at": "2026-09-27T12:00:00Z",
"kind": "claim",
"parent_receipt_hash": null,
"seq": 1,
"sig": "1e3f79ad3571073e64b4ff97733c472c030d241377367a511e2a7be4f5975ea0fb34f2612fc78271941281fa185c3ba1252e8b8c96e3b3c61ee7854580e56e0f",
"signer_id": "did:example:meter",
"signer_pubkey": "a09aa5f47a6759802ff955f8dc2d2a14a5c99d23be97f864127ff9383455a4f0",
"subject": "c857bdf859edf4041541d2507611c4205435be54f47ec38fc6ab4350feaf6b4c",
"subject_type": "receipt"
}
```
Its `link_hash` is
`31cfac4ec8c104a7db59c4156cd7968f539c690c7b5df0c69e34cf2c48529db9`.
### 2.6. Binding classes
A receipt binds to an object in one of two ways.
@ -699,52 +1021,108 @@ for every group receipt and every object receipt it accepts. Steps
match the algorithm style of WAI SPEC.md §4 (numbered, terminating).
1. **Parse and shape-check.** Read the group receipt. If `kind !=
"group"`, reject. If `sig` is not 64 bytes, reject. If
`signer_pubkey` is not a valid 32-byte Ed25519 verifying key
(RFC 8032), reject. If any required field from §2.4.1 is absent,
reject.
"group"`, reject (`wrong_kind`). If `sig` is not 64 bytes, if any
required field from §2.4.1 is absent, or if a member §2.4.1 does not
define is present, reject (`malformed`). Labelled members on a group
without `"sign_version": 2`, a `sign_version` other than `2`, or a class
that cannot be read, reject (`group_label_invalid`). If `signer_pubkey` does not decode under
RFC 8032 §5.1.3 or is of small order — §3.1 step 3's key rule — reject
(`bad_signer_key`).
2. **Verify the Ed25519 group signature.** Construct the canonical
signing payload per §2.3 from the receipt's fields. Verify `sig`
against `signer_pubkey` per RFC 8032 Pure Ed25519. If verification
fails, reject. After this step, the verifier holds a **verified
root**: the receipt's `root_hash` field, now bound to
`signer_pubkey` under signature.
2. **Verify the Ed25519 group signature.** Construct the signing payload
per §2.3 from the receipt's fields: the labelled payload for a group
with `"sign_version": 2`, the legacy payload otherwise. Verify `sig`
against `signer_pubkey` under §3.1 step 3's rule — `S` < L, `R` not of
small order, the cofactorless equation — the same rule every claim
signature is held to, so that every conforming verifier accepts the same
group signatures. If verification fails, reject
(`signature_verify_failed`). For a labelled group, check its class with
the group alone (§2.4.1) and reject one that breaks it
(`group_label_invalid`). After this step, the verifier holds a
**verified root**: the receipt's `root_hash` field, now bound to
`signer_pubkey` under signature, and for a labelled group the total's
class.
3. **Check parent linkage.** If the verifier maintains receipt-chain
state for `signer_pubkey`, compute `BLAKE3("moq-jwp:receipt-link\x01"
|| canonical_bytes(previous_group_receipt))` and compare to
`parent_receipt_hash`. If they differ, reject (chain break). If
`parent_receipt_hash`. If they differ, reject (`chain_break`). If
no previous receipt is known and `parent_receipt_hash` is `null`,
accept as the chain head. A non-null `parent_receipt_hash` without
a corresponding previously-verified receipt is a recoverable
condition: the verifier MAY request the missing receipt before
accepting; it MUST NOT silently accept.
condition (`parent_not_held`): the verifier MAY request the missing
receipt before accepting; it MUST NOT silently accept.
`canonical_bytes(G)` is G's §2.3 signing payload, in the form G was
signed in, followed by its 64-byte `sig`. The resulting hash is G's
**link hash**: the identity a `claim` names G by (§2.4.3), and a
`WAI_RECEIPT` property's `group_receipt`
([moq-streaming-format](moq-streaming-format.md) §5). For a group in
the legacy form it does not cover `parent_receipt_hash`, which that
payload does not sign either; the labelled payload signs it.
4. **Verify the object's content hash.** For each accompanying
`kind: "object"` receipt: recompute `content_hash` per §2.1 over
the received WAI envelope bytes and compare to the receipt's
`content_hash` field. If they differ, reject the object (the
envelope was mutated in transit).
`content_hash` field. If they differ, reject the object
(`content_hash_mismatch`: the envelope was mutated in transit). An
object receipt missing a member of §2.4.2, or carrying one neither
§2.4.2 nor its label defines, is `malformed`.
5. **Verify the Merkle inclusion proof.** Compute the leaf as
`BLAKE3("moq-jwp:merkle-leaf\x01" || content_hash)`. Walk the
`merkle_proof.siblings` list from level 0 upward; at each level
`i`, combine the current value with the sibling using the
`direction_bits` bit `i` (1 = current is right child, 0 = current
is left child) and the `moq-jwp:merkle-node\x01` domain. After
walking all siblings, compare the resulting hash to the verified
root from step 2. If they differ, reject.
5. **Verify the Merkle inclusion proof.** In this order:
- If the object receipt carries a class label (§2.4.2), check it as
§2.4.2 requires and reject the object if it fails
(`object_label_invalid`).
- Check the object's position: `leaf_index` is below the group's
`leaf_count`; `merkle_proof.siblings` holds exactly
⌈log₂ `leaf_count`⌉ hashes (none for one leaf); `direction_bits`
equals `leaf_index`, so it holds the index's bits and no higher
ones; `object_id` equals `leaf_index`; and `track_alias` and
`group_id` are the group's. Otherwise reject
(`proof_position_mismatch`). Without this, the last leaf of an odd
level, duplicated, walks two positions, and an object can be
presented at another object's index.
- Compute the object's leaf in the labelled form of §2.2 where it
carries a label, and in the legacy form otherwise. Walk the
`merkle_proof.siblings` list from level 0 upward; at each level `i`,
combine the current value with the sibling using the
`direction_bits` bit `i` (1 = current is right child, 0 = current is
left child) and the `moq-jwp:merkle-node\x01` domain. After walking
all siblings, compare the resulting hash to the verified root from
step 2. If they differ, reject (`inclusion_proof_failed`).
6. **Accept.** The object's bytes are bound to `signer_pubkey` at
`(track_alias, group_id, object_id)`, with the publisher's
declared `joules_micro` and `origin`. The verifier MAY now persist
the group receipt as the new chain head for `signer_pubkey` and
dispatch the WAI envelope per WAI SPEC.md §4.
`(track_alias, group_id, object_id)`. Where the object's leaf is
labelled, its `joules_micro`, its class, the class's evidence and its
`origin` are bound with them. Where it is not, `joules_micro` and
`origin` are the publisher's unsigned statements. The verifier MAY now
persist the group receipt as the new chain head for `signer_pubkey`
and dispatch the WAI envelope per WAI SPEC.md §4.
A verifier MUST treat any rejection in steps 1–5 as a hard failure
for the object in question. A verifier MUST NOT accept a partial
verification (e.g. valid Merkle proof against an unverified root).
verification (e.g. valid Merkle proof against an unverified root): a
reading of an object's figure, a group total and a `WAI_RECEIPT`
property are taken only from objects accepted under steps 1–6.
**The group total.** A verifier that holds, for every `leaf_index` below
the group's `leaf_count`, exactly one object receipt it has accepted under
steps 1–6 **MUST** check that their `joules_micro` sum to `joules_total`;
if they do not, it reports `group_total_mismatch`, and the objects' own
verdicts stand. If they do:
- for a group in the legacy form, it reads the total from the objects'
labels as [energy-measurement](energy-measurement.md) §2.8 reads a
total: unmetered, unlabelled, or the weakest class as a class name only;
- for a labelled group, it checks that the group's class is the one the
objects compose to (§2.4.1) — the class and its evidence other than the
declared uncertainty (`group_class_disagrees`), the coverage
(`group_coverage_disagrees`), and a `relative_ppm` at least the
composed one over the composed window (`group_uncertainty_below_parts`)
— and reads the total at the group's signed class.
A verifier that does not hold exactly one accepted object for every index
reads a legacy group's total as unlabelled, and a labelled group's total
at its own signed class, which it has not checked against the parts.
### 3.1. Claim receipts
@ -783,10 +1161,12 @@ order listed.
a wrong JSON type; an integer that is not one of §2.4.3; a byte
string that is not lowercase hex, two digits a byte, or a hash that
is not 64 of them; a value outside an enumeration this extension
defines here (`record`, `binding_type`); a URI that is not absolute;
defines here (`record`, `binding_type`, a bracket's `opening`, a build's
`profile`); a URI that is not absolute;
a term that is not one of §2.4.3 (empty, longer than 2048 bytes, or
holding a White_Space or control character); or a malformed
`oer_receipt`. A member's type and encoding are checked together,
`oer_receipt`; or an operating point whose member names are not terms
or whose values are not integers of §2.4.3. A member's type and encoding are checked together,
before the next member is read. An object a class fixes the members
of — a lawful-basis purpose, an `energy_uncertainty` — is checked
the same way, its members and then their types, where the table
@ -799,8 +1179,14 @@ order listed.
zero; an empty `purposes`; an empty `basis`, which is why its type
is `term or ""`; an empty `log_receipt` or `value`; an `alg` that is
a term but not in registry form; a `resolution_endpoint` that is an
absolute URI but not an `https` URI with a host and no userinfo; and
an `attests` other than `"ancestor"`.
absolute URI but not an `https` URI with a host and no userinfo; an
`attests` other than `"ancestor"`; a figure profile, an index or a
group that does not fit the profile; a hardware figure's report that
names no power reader; a bracket with fewer than two
samples, liveness facts that do not show a live rail, a zero window,
completion or thread count, or a busy mean not above its idle mean;
the figure and least uncertainty a bracket gives; an empty point; and
which points and predictions are present together.
3. **Signature.** Ed25519 (RFC 8032). This step restates RFC 8032's
own decoding and range rules, and fixes the two choices on which
RFC 8032 verifiers differ — whether a key or `R` of small order is
@ -824,7 +1210,7 @@ order listed.
for a signature made without any private key — under the identity,
`R` = [s]B with `S` = s verifies for every message — so such a key
is refused before its signature is examined.
4. **Class rules**, per §2.4.5–§2.4.7 and §2.6, each with its own
4. **Class rules**, per §2.4.5–§2.4.9 and §2.6, each with its own
reason (below). They run after the signature because a rule about
what a signer said only means something once it is established who
said it.
@ -840,9 +1226,14 @@ order listed.
a `null` parent while a previous claim is held, is `chain_break`.
6. **Context**: checks against other receipts the verifier holds,
each verified first — an `energy` claim against the `object` receipt
for its subject, verified under §3, and against the OER receipt it
names, verified under oer-receipts as §2.4.5's check 2
(`energy_oer_invalid`); a decision against its declaration (§2.4.6);
for its subject, verified under §3, including the class label its leaf
carries (§2.4.5), and against the OER receipt it names, verified under
oer-receipts as §2.4.5's check 2 (`energy_oer_invalid`); a
`measurement` or `operating-point` claim against the receipt that
carries its figure, verified under that receipt's own profile (for
`moq-jwp:object`, the object receipt accepted under §3 against the
group receipt `group` names) (§2.4.8); a decision against its
declaration (§2.4.6);
a recovery record against its declaration together with the
publisher key of a verified delivery receipt for the declaration's
subject (§2.6.3), and never against the declaration alone. A
@ -864,12 +1255,14 @@ order listed.
publisher is then `recovery_signer_mismatch`, in §2.6.3's order.
A claim that passes every step it was checked at is **accepted**: its
signer made this statement about this subject. §2.4.4's rules bound
signer made this statement about this subject. A `measurement` claim is
checked at step 6 always: without the receipt carrying its figure it is
`figure_not_held`, not accepted (§2.4.8). §2.4.4's rules bound
what that means, and §2.4.7 bounds it further for `log-inclusion`.
| step | reason | raised when |
|---|---|---|
| 4 | `energy_zero_figure` | `joules_micro` is 0 |
| 4 | `energy_zero_figure` | `joules_micro` is 0 in an `energy`, `measurement` or `operating-point` claim |
| 4 | `energy_unknown_acquisition` | `energy_provenance` names no class of energy-measurement §2 |
| 4 | `energy_legacy_acquisition` | `energy_provenance` is the legacy `HwShunt` |
| 4 | `energy_acquisition_evidence` | the class's evidence is missing, carried by a class that has none, or below the resolution floor (§2.4.5 rule 2) |
@ -886,10 +1279,33 @@ what that means, and §2.4.7 bounds it further for `log-inclusion`.
| 4 | `soft_binding_endpoint_not_https` | `resolution_endpoint` is not an `https` URI with a host and no userinfo |
| 4 | `recovery_not_ancestor` | `attests` is not `"ancestor"` |
| 4 | `recovery_of_hard_bound_object` | `observed` equals `subject` |
| 4 | `figure_unknown_profile` | `figure_profile` names no profile of energy-measurement §6.1 for the claim's `subject_type` |
| 4 | `figure_index_mismatch` | `figure_index` is an integer under a profile that carries one figure, or `null` under one that carries several |
| 4 | `figure_group_mismatch` | `group` is a hash under a profile other than `moq-jwp:object`, or `null` under it |
| 4 | `measurement_no_power_reader` | under a hardware class, `power_reader` is `null` |
| 4 | `measurement_too_few_samples` | a bracket's `n` is below 2 |
| 4 | `measurement_rail_not_live` | under a hardware class, a bracket's liveness facts do not show a live rail, or its samples are all equal |
| 4 | `measurement_empty_burst` | a bracket's `window_us`, `completions` or `in_flight` is 0 |
| 4 | `measurement_no_marginal` | a bracket's busy mean is not above its idle mean |
| 4 | `measurement_below_attribution_floor` | under a hardware class, the marginal draw is below the idle standard deviation |
| 4 | `measurement_figure_mismatch` | `joules_micro` is not the figure the bracket gives |
| 4 | `measurement_window_mismatch` | under a hardware class, the declared window is not the bracket's |
| 4 | `measurement_declaration_below_bracket` | under a hardware class, `relative_ppm` is below the least the bracket supports |
| 4 | `operating_point_empty_point` | a point is present with no member |
| 4 | `operating_point_delivered_alone` | `op_delivered` is present without `op_committed` |
| 4 | `operating_point_incomplete` | `op_requested` or `op_committed` is present without `op_delivered` |
| 4 | `operating_point_empty` | neither `op_delivered` nor `prediction` is present |
| 6 | `energy_disagrees_with_delivery` | the figure differs from the `object` receipt's |
| 6 | `energy_class_disagrees_with_delivery` | the object's leaf is labelled, and the `energy` claim's class or evidence differs from the label |
| 6 | `figure_disagrees` | the receipt a `measurement` or `operating-point` claim names carries another figure at `figure_index` |
| 6 | `figure_unclassified` | that figure carries no acquisition class |
| 6 | `figure_class_disagrees` | its class or evidence differs from the claim's |
| 6 | `figure_signer_unknown` | a `measurement` claim names a figure no Ed25519 key signs |
| 6 | `figure_signer_mismatch` | a `measurement` claim's figure is signed by an Ed25519 key other than `signer_pubkey` |
| 6 | `figure_not_held` | a `measurement` claim is checked without the receipt that carries its figure (§2.4.8) |
| 6 | `energy_oer_invalid` | the named OER receipt reports any reason under oer-receipts |
| 6 | `energy_disagrees_with_oer` | the named OER receipt states another grade or figure |
| 6 | `context_invalid` | a declaration a decision or a recovery record is checked against does not pass steps 1–4 |
| 6 | `context_invalid` | a receipt a claim is checked against — a declaration a decision or a recovery record names, or the receipt that carries a figure — does not verify |
| 6 | `decision_declaration_mismatch` | a decision does not match the declaration it names |
| 6 | `decision_purpose_not_declared` | a decision is about a purpose its declaration does not list |
| 6 | `recovery_declaration_mismatch` | a recovery record does not match the declaration it names |
@ -955,6 +1371,26 @@ The reference implementation (`wai-rs`, feature `receipt_claims`,
ancestor; there is no path from it to a statement about the object in
hand.
### 3.3. Delivery reasons
| check | reason | raised when |
|---|---|---|
| §3 step 1 | `wrong_kind` | `kind` is not the receipt's kind |
| §3 step 1, 4 | `malformed` | a group receipt (step 1) or object receipt (step 4) lacks a member, carries one its kind does not define, or holds one of the wrong type or encoding |
| §3 step 1 | `bad_signer_key` | `signer_pubkey` does not decode under RFC 8032 §5.1.3, or is of small order |
| §3 step 1, 2 | `group_label_invalid` | a group's labelled members appear without `"sign_version": 2`, its `sign_version` is not `2`, or its labelled members cannot be read (step 1), or its class breaks §2.4.1 with the group alone (step 2) |
| §3 step 2 | `signature_verify_failed` | the group signature fails §3.1 step 3's rule over the payload of the group's form |
| §3 step 3 | `chain_break`, `parent_not_held` | as §3.1 step 5, for groups |
| §3 step 4 | `content_hash_mismatch` | the envelope does not hash to `content_hash` |
| §3 step 5 | `object_label_invalid` | an object receipt's class label cannot be read or breaks §2.4.2: legacy `HwShunt`, a class beside a zero figure, the unmetered marker beside a non-zero one, a refined class without its evidence or below the resolution floor, or evidence without a class |
| §3 step 5 | `proof_position_mismatch` | the object is not at the position its proof walks: `leaf_index` outside the group, a proof of the wrong length, direction bits other than the index's, an `object_id` other than the index, or another group's coordinates |
| §3 step 5 | `inclusion_proof_failed` | the object's leaf, walked up its proof, does not reach the verified root — among other causes, because the leaf was computed in the other form of §2.2 |
| the group total | `group_total_mismatch` | the accepted object receipts, one for every leaf, do not sum to `joules_total` |
| the group total | `group_class_disagrees` | a labelled group's class, or its evidence other than the declared uncertainty, is not the one its objects compose to, or its objects compose to none |
| the group total | `group_coverage_disagrees` | a labelled group's coverage is not the one its objects give |
| the group total | `group_uncertainty_below_parts` | a labelled group declares less than the composed `relative_ppm`, or another window |
| `WAI_RECEIPT` | `property_invalid`, `property_disagrees` | as [moq-streaming-format](moq-streaming-format.md) §5 |
---
## 4. Relay preservation requirements
@ -972,7 +1408,10 @@ chain across hops, a conforming relay:
- Verify the group receipt's Ed25519 signature per §3 step 2 before
the first object of the group is fanned out. A relay that cannot
verify the signature MUST drop the group and emit a structured
error to upstream operators.
error to upstream operators. A relay that predates the labelled
group payload (§2.3) cannot verify a labelled group and drops it, so
a publisher labels a group only once the relays on its path verify
both payloads.
- Forward the `parent_receipt_hash` field as received. A relay that
has previously forwarded the chain MAY additionally check the link
per §3 step 3 and refuse to fan out a broken chain.
@ -1079,7 +1518,7 @@ exercises all five vectors in its own test suite.
### 5.6. Claim receipts and recovery (`claim-conformance/`)
The typed classes of §2.4.3–§2.4.7, the binding classes of §2.6 and
The typed classes of §2.4.3–§2.4.9, the binding classes of §2.6 and
the procedures of §3.1–§3.2 are checked by the corpus in
[`claim-conformance/`](../claim-conformance/), generated and verified
by `wai_claim_vectors` (`wai-rs`, feature `receipt_claims`) and run by
@ -1108,7 +1547,18 @@ unpaired surrogate escape, deep nesting, numbers out of every range —
and dates that do not exist; the boundary between a member's type
(step 2) and what a class's rules add to it (step 4), from both sides,
and the order step 2 reads a body in, where a body has two defects;
and a claim whose canonical form spans three BLAKE3 chunks.
and a claim whose canonical form spans three BLAKE3 chunks. The corpus
also covers the `measurement` and `operating-point` classes: every reason
they add, the arithmetic of energy-measurement §6.2 at its boundaries — a
declaration at the least value a bracket supports and one below it, a
figure at a half microjoule, the ratio thresholds, a flat idle bracket, and
brackets whose products exceed 128 bits — the liveness rule of
energy-measurement §3.1 (a frozen rail refreshed once, a refresh published
in two steps, facts that describe no sequence), and the declaration a
sealing path computes from unrounded samples, which the rule refuses. That
arithmetic is pinned against values `claim_verify.py` computes with
Python's integers. It covers an `energy` claim checked against an object
whose leaf is labelled.
The generator and the verifier in `wai-rs` are one implementation, so
two checks are made against code that shares nothing with it. A
@ -1131,13 +1581,34 @@ evidence, figures near a half-microjoule and at the edges of
oer-receipts §4 and §6, OER receipts holding numbers §4 forbids in a
body, files §4 does not read at all, context receipts that do not
verify, bodies of every class with one defect and with two at
different positions — runs both verifiers' `verdicts` mode on them,
and reports every input on which their verdicts differ. Agreement
shows that the two read alike, not that either reads §3 as written;
only the vectors' declared outcomes, set from the text, speak to that,
and only for the inputs they hold. The fixture runs in CI;
the script and the differential need Python with an Ed25519 library
and are run by hand, not by CI.
different positions, measurement brackets and operating points around
every rule — runs both verifiers' `verdicts` mode on them, and reports
every input on which their verdicts differ. Agreement shows that the two
read alike, not that either reads §3 as written; only the vectors'
declared outcomes, set from the text, speak to that, and only for the
inputs they hold. The fixture and the bracket arithmetic run in CI on every
platform of the matrix; the script and the differential, which need Python
with an Ed25519 library, run in CI on the linux runner with a fixed seed.
### 5.7. Labelled leaves, labelled groups and the group total (`jwp-conformance/`)
`wai_jwp_vectors gen` in `wai-rs` writes `jwp-conformance/vectors.json`,
and `verify` requires a byte-identical regeneration and every declared
outcome. Every vector is synthetic: keys come from published fixed seeds
and attest nothing, and envelopes, figures and origins are invented. The
corpus holds leaf vectors in both forms of §2.2; a group in the legacy
payload whose objects mix the forms, and a group in the labelled payload
chained to it, each with its signing payload, signature and link hash;
each object's outcome (`accept`, or a rejection at its §3 step with its
§3.3 reason), including objects presented at another object's position;
group signatures the strict rule refuses; the group total's reading or its
reason; and `WAI_RECEIPT` properties with the reading
[moq-streaming-format](moq-streaming-format.md) §5 gives them.
`jwp-conformance/jwp_verify.py`, written from this text, reports the same
outcomes; CI runs it on the linux runner, and runs `wai_jwp_vectors
verify` on every platform of the conformance matrix. joule-code's
`joule-moq` pins leaves, roots, signatures and link hashes copied from the
corpus.
---
@ -1152,7 +1623,9 @@ later time. This extension mitigates replay with two mechanisms:
- `parent_receipt_hash` (§2.4.1) links each group receipt to its
predecessor under the same `signer_pubkey`, so a chain replay
requires forging every intermediate hash.
requires forging every intermediate hash. The labelled payload (§2.3)
signs it; the legacy payload does not, so in a legacy group the link
is the publisher's unsigned statement and a relay can rewrite it.
- `group_id` is monotonic per publisher key. Verifiers SHOULD track
the highest `group_id` seen per `signer_pubkey` and reject any
receipt with a non-increasing `group_id`. This binds the chain
@ -1186,9 +1659,11 @@ to re-present a group receipt under a different `(track_alias,
group_id)` tuple cannot do so without re-signing — the receipt's
fields are signature-covered, and any field mutation breaks
verification at §3 step 2. The Merkle proof is similarly bound:
`leaf_index` is verifier-checked during the proof walk, so an
object's proof cannot be transplanted to a different position within
the same group.
the verifier checks that the proof walks the position `leaf_index`
names (§3 step 5), so an object's proof cannot be transplanted to a
different position within the same group, and the duplicated last
leaf of an odd level cannot be presented at the position past the
group's end.
### 6.4. Envelope mutation in transit
@ -1226,7 +1701,9 @@ multi-signer container; the v1.0 wire format remains a strict subset.
[Confidentiality extension](confidentiality.md) seals the object
bytes a receipt attests, and this chain deliberately commits over
that ciphertext so a relay signs delivery + energy without ever
reading the content.
reading the content. A `measurement` claim (§2.4.8) also carries the
measuring machine's power levels, thread count, build and power reader
in cleartext.
- **Pricing semantics for joules.** `joules_micro` and
`joules_total` are unit-of-energy fields, not unit-of-account
fields. Conversion to currency, carbon, or grid-mix CO₂ is

View file

@ -101,14 +101,20 @@ commitment:
| field | type | meaning |
|---|---|---|
| `content_hash` | 32 B | `jwp-receipts` §2.1 BLAKE3 over the envelope bytes. |
| `joules_micro` | u64 | Microjoules producing this object. |
| `acquisition` | string | Acquisition class per [energy-measurement](energy-measurement.md) §2. |
| `joules_micro` | u64 | Microjoules producing this object; `0` is the unmetered marker, never 0 J ([energy-measurement](energy-measurement.md) §2). |
| `acquisition` | string | Acquisition class per [energy-measurement](energy-measurement.md) §2; empty when `joules_micro` is `0`. |
| `acquisition_evidence` | bytes | For `OnChipCounter` and `CalibratedInstrument`, the evidence that follows the tag byte in `energy-measurement.md` §2.7 (declared uncertainty, then filtering state or calibration reference). Empty for every other class. |
| `group_receipt` | 32 B | BLAKE3 of the group receipt covering this object. |
| `group_receipt` | 32 B | The link hash of the group receipt covering this object (`jwp-receipts` §3 step 3). |
- `acquisition` **MUST** accompany `joules_micro`. A joule figure without its
acquisition class is exactly what `energy-measurement.md` §2 forbids, and
putting it on the wire does not make it acceptable.
- A non-zero `joules_micro` **MUST** be accompanied by `acquisition`. A joule
figure without its acquisition class is exactly what `energy-measurement.md`
§2 forbids, and putting it on the wire does not make it acceptable.
- A zero `joules_micro` is the unmetered marker: `acquisition` **MUST** be
empty and `acquisition_evidence` empty, since a class beside a zero figure
claims a measurement that did not occur (`energy-measurement.md` §2). A sink
**MUST NOT** present the marker as a figure of zero joules, and **MUST**
discard a property that breaks this bullet or the one above
(`property_invalid`).
- `acquisition_evidence` **MUST** be present and non-empty for a refined
hardware class (`OnChipCounter`, `CalibratedInstrument`), and empty
otherwise — including for legacy `HwShunt`, which carries no evidence. A
@ -117,6 +123,26 @@ commitment:
property written before this field existed has no `acquisition_evidence`; a
reader treats its absence as empty, which is valid only for the classes that
carry none.
- No signature covers this property. A sink **MUST NOT** report `acquisition`
or `acquisition_evidence` as signed unless they and `joules_micro` equal a
signed statement it holds of the same object, which is one of:
- the label in the object's Merkle leaf, once the object receipt has been
accepted under `jwp-receipts` §3 against the group receipt
`group_receipt` names — `group_receipt` must then name that group; or
- an `energy` claim about the object that it has accepted under
`jwp-receipts` §3.1. A claim names no group, so `group_receipt` is outside
what it states.
Where such a statement gives another figure, class, evidence or group, the
sink **MUST** discard the property (`property_disagrees`). Where it holds
none, the class is outside every signature, and the sink reads the figure as
unlabelled.
- A class read as signed is the statement of the key that signed it: the group
receipt's `signer_pubkey` for a leaf, the claim's `signer_pubkey` for an
`energy` claim. A sink **MUST** report that key with the class, and **MUST
NOT** report a class a claim signs as the publisher's unless the claim's
signer is a key it holds as the publisher's. Anyone can sign an `energy`
claim, a relay that rewrites this property included.
- A relay **MUST** preserve this property unchanged, per `jwp-receipts` §4.1.
A relay that recomputes it is asserting something it did not measure.
- Absence of the property is not evidence of absence of a receipt; receipts MAY

View file

@ -43,7 +43,7 @@ A `ProvStep` is one transformation in the asset's lineage:
| `input_hash` | content hash of the input (all-zero for an originating step) |
| `output_hash` | content hash of the output |
| `work` | exact deterministic work (pixels, samples) |
| `joules_micro` | measured/carried microjoules (0 = unmetered) |
| `joules_micro` | measured/carried microjoules; `0` is the unmetered marker, never 0 J ([energy-measurement](energy-measurement.md) §2) |
Step content hash:
`BLAKE3("wai:prov-step\x01" || op_len | op | input | output | work | joules)`.
@ -74,7 +74,8 @@ There is no tolerance parameter: the content hash matches or it does not.
`joules_micro` per step is **carried** from a meter, exactly as the other
receipts carry it: `wai_prov` measures the real CPU energy of each encode
(IOReport via macmon, no sudo) and signs the figure. An unmetered step
carries `0`. The receipt never derives energy it did not measure.
carries the unmetered marker `0`, which a reader presents as unmetered and never
totals as zero joules. The receipt never derives energy it did not measure.
Each step's figure carries its acquisition class as an optional label
([energy-measurement](energy-measurement.md) §2.8), because the steps of one
@ -82,8 +83,11 @@ lineage can be metered by different parties in different ways. A labelled step
binds its class into its leaf hash; a step with no label keeps its legacy leaf
and its figure is unlabelled. `wai_prov` labels each encode it measures
`OnChipCounter`, with its declared uncertainty, and records an encode whose
measurement fails the §4 ratio as unmetered. `total_joules_micro` carries no
class of its own.
measurement fails the §4 ratio or the power reader's liveness (§3.1) as
unmetered, and writes a measurement report for each step it measured
(energy-measurement §6). `total_joules_micro` carries no class of its own, and a
total over steps that include an unmetered one covers only the measured steps
(`Partial`).
## Appendix

View file

@ -196,12 +196,14 @@ is trusted the way it honestly can be.**
| `statevector_hash` | the byte-exact reconstruction result | **re-checkable** (re-simulate) |
| `measurement` | optional `{seed, shots, histogram_hash}` | re-checkable |
| `work_amp_updates` | `n_ops · 2^n` — the exponential reconstruction cost | **verified by recomputation** |
| `joules_micro` | measured marginal CPU energy the sink spent | **attested by signature** |
| `joules_micro` | measured marginal CPU energy the sink spent; `0` is the unmetered marker | **attested by signature** |
| `energy_provenance` and its evidence (optional; [energy-measurement](energy-measurement.md) §2.8) | how `joules_micro` was acquired | **attested by signature**, not re-measurable |
`work_amp_updates` is *portable-exact*: anyone recomputes it from `(n_ops,
n_qubits)` and it grows exponentially in qubits, so a sink cannot inflate or
deflate the stated cost. `joules_micro` is *measured-attested*: only the signer
can vouch for what its own silicon drew. One Ed25519 signature covers all of it
can vouch for what its own silicon drew, and the same holds for the class that
labels it and for the values of any report about it (energy-measurement §6). One Ed25519 signature covers all of it
over a Merkle root of the leaf hashes (`circuit`, `statevector`, `[histogram]`) —
the worlds / provenance / film receipt shape reused, `parent_receipt_hash`
chaining a derivation exactly as they do.
@ -219,11 +221,17 @@ Two verification depths:
`to_json()` / `from_json()` give the portable, sorted-key JSON an out-of-band
verifier reads. `wai_quantum_meter <circuit.wqc> [receipt.json]` fills the
energy honestly: on arm64 macOS it samples CPU power via `macmon` (IOReport, no
sudo), brackets a reconstruction burst against an idle baseline, and writes the
energy honestly: on a host that exposes its SoC's power interface to
unprivileged readers it samples CPU power through the open-source `macmon`
reader (no elevated privileges), brackets a reconstruction burst against an idle baseline, and writes the
**measured** marginal microjoules — never a fabricated number; where no meter is
present it emits an UNMETERED receipt (`joules_micro = 0`) with the exact work
intact. A measured figure is sealed with its acquisition class as a
present, or the reader's rail is not live in both brackets
([energy-measurement](energy-measurement.md) §3.1), or the §4 ratio or the §2.3
uncertainty cannot support a figure, it emits an UNMETERED receipt, whose `joules_micro` carries the
unmetered marker `0` ([energy-measurement](energy-measurement.md) §2), never a
figure of zero joules, with the exact work intact; a reader presents it as
unmetered, and `POST /verify` renders it as `"joules_micro": null`. Beside a
measured figure the meter writes a measurement report (energy-measurement §6). A measured figure is sealed with its acquisition class as a
`quantum_energy::Labelled` receipt ([energy-measurement](energy-measurement.md)
§2.8): `OnChipCounter` with its declared uncertainty, inside the signature,
verified with `Labelled::verify` / `Labelled::verify_reconstruction`. The

3
wai/jwp-conformance/.gitattributes vendored Normal file
View file

@ -0,0 +1,3 @@
# The pinned corpus is compared byte-for-byte with what the generator writes (LF), on
# every platform of the CI matrix. Keep a CRLF checkout from turning that into a failure.
* text eol=lf

View file

@ -0,0 +1,64 @@
# jwp-conformance
Conformance for the labelled forms of the [JWP delivery receipts](../extensions/jwp-receipts.md)
(§2.2–§2.4.2, §3, §3.3 and §5.7) and for the `WAI_RECEIPT` property's readings
([moq-streaming-format](../extensions/moq-streaming-format.md) §5): an object's figure,
class and origin bound into its Merkle leaf; a group total's class signed under the
labelled group payload; the position an object's inclusion proof must walk; the group
total read from its objects; and a property read as signed only where a signed statement
restates it.
## Files
| file | what it is |
|---|---|
| `vectors.json` | The corpus, generated by `wai_jwp_vectors gen` in `wai-rs` (feature `receipt_claims`). `wai_jwp_vectors verify` requires a byte-identical regeneration and every declared outcome. |
| `jwp_verify.py` | A second verifier, written from the extension text, that shares no code with `wai-rs`. It takes BLAKE3, jwp-receipts §3.1 step 3's Ed25519 rule and the claim verifier from `../claim-conformance/claim_verify.py`, itself independent of `wai-rs`. |
## Procedures
Each vector names one procedure and declares one outcome.
| procedure | input | outcome |
|---|---|---|
| `leaf` | an object receipt | its leaf, in the form its label gives (§2.2) |
| `group` | a group receipt, and optionally the previous one held | `accept` with its link hash and signing payload (§2.3, §3 step 3), or a rejection at a §3 step |
| `object` | a group receipt, an object receipt and the envelope delivered | `accept` (§3 steps 1–5), or a rejection at a §3 step |
| `total` | a group receipt and the objects delivered with their envelopes | the total's reading — `unmetered`, `unlabelled`, `weakest` (a legacy group whose every non-zero part is labelled: a class name only), or `signed` (a labelled group's own class, `checked` against every object or `unchecked`) — or a rejection |
| `property` | a `WAI_RECEIPT` property, with the object, its group and its envelope, or with an `energy` claim | the property's reading — `unmetered`, `unsigned`, or `signed` with the class and the key that signed it (the group's for a leaf, the claim's for a claim) — or a rejection |
A rejection names its reason (§3.3), and its §3 step where it has one; the group total
and a property are not steps of §3.
## What the corpus holds
Leaves in both forms, the labelled one with tags 0, 2, 4 and 5. A group in the legacy
payload whose objects mix the forms, and a group in the labelled payload chained to it,
with their signing payloads, signatures and link hashes. Every way a label, a figure or
an origin can be restated, stripped or added, and which of them the legacy leaf leaves
unbound. Labels that break their rules in correctly built trees. Objects presented at
another object's position — including the duplicated last leaf of an odd level, and a
duplicate that would hide a weaker class from the total. Group labels that break their
rules, correctly signed; a `sign_version` other than 2; and group signatures the strict
Ed25519 rule refuses (`S` ≥ L, a key of small order, a key whose `y` is not reduced). Totals read from complete and incomplete holdings, a total that
does not match its parts, and labelled totals whose class, coverage, declared uncertainty
or window disagree with what their parts compose to. Properties that agree with, and
disagree with, a labelled leaf or an `energy` claim, including a claim signed by a key
other than the publisher's, which reads as signed by that key.
## Synthetic, and what it attests
Keys are Ed25519 from fixed 32-byte seeds — 0x11 the publisher, 0x22 the meter, 0x33 a
relay — and attest nothing. Envelopes, figures, origins and calibration references are invented, and
calibration references use a reserved example domain.
## Running
cargo run --no-default-features --features receipt_claims --bin wai_jwp_vectors -- verify ../jwp-conformance
python3 jwp_verify.py vectors.json
Both run in CI: the Rust verifier on every platform of the conformance matrix, the Python
one on the linux runner (`.github/workflows/wai-conformance.yml`). The Python verifier
needs the `cryptography` package. joule-code's `joule-moq` pins leaves, roots and
signatures from this corpus in its own tests, and its workflow re-runs when the corpus
changes.

View file

@ -0,0 +1,544 @@
#!/usr/bin/env python3
"""Second verifier for jwp-conformance/ — labelled leaves, labelled group receipts, the
group total and the WAI_RECEIPT property (extensions/jwp-receipts.md §2.2-§2.4.2, §3,
§3.3 and §5.7; moq-streaming-format.md §5).
Written from the extension text. It shares no code with wai-rs (`crate::jwp_delivery`),
which generates the corpus. It takes from ../claim-conformance/claim_verify.py — itself
independent of wai-rs — BLAKE3 written from its specification, jwp-receipts §3.1 step 3's
Ed25519 rule (key decoding, small order, S < L, the cofactorless equation through the
`cryptography` package), and the claim verifier an energy claim is checked with.
python3 jwp_verify.py [vectors.json] verify every vector against its declared outcome
python3 jwp_verify.py verdicts <file> print `name<TAB>verdict` for every vector,
ignoring any declared outcome
Exit status 0 only if every vector reports its declared outcome.
"""
import json, os, sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "claim-conformance"))
import claim_verify as CV # noqa: E402
LEAF1 = b"moq-jwp:merkle-leaf\x01"
LEAF2 = b"moq-jwp:merkle-leaf\x02"
NODE = b"moq-jwp:merkle-node\x01"
SIGN1 = b"moq-jwp:group-sign\x01"
SIGN2 = b"moq-jwp:group-sign\x02"
LINK = b"moq-jwp:receipt-link\x01"
OBJECT = b"moq-jwp:object\x01"
FLOOR = 288676 # energy-measurement §2.3: joules_micro x relative_ppm, at least
U32, U64 = 2 ** 32 - 1, 2 ** 64 - 1
RANK = {"Estimator": 1, "ModelBased": 2, "HwShunt": 3, "OnChipCounter": 4, "CalibratedInstrument": 5}
TAG = {"HwShunt": 1, "ModelBased": 2, "Estimator": 3, "OnChipCounter": 4, "CalibratedInstrument": 5}
FILTERING = {"Unknown": 0, "Off": 1, "On": 2}
CLASS_KEYS = ["energy_provenance", "energy_uncertainty", "energy_counter_filtering", "energy_calibration_ref"]
GROUP = ["kind", "track_alias", "group_id", "root_hash", "leaf_count", "joules_total", "group_start", "group_end",
"signer_pubkey", "signer_id", "sig", "parent_receipt_hash"]
GROUP_LABEL = ["sign_version"] + CLASS_KEYS + ["energy_coverage"]
OBJ = ["kind", "track_alias", "group_id", "object_id", "content_hash", "joules_micro", "origin", "merkle_proof"]
OBJ_LABEL = ["leaf_version"] + CLASS_KEYS
class Refuse(Exception):
def __init__(self, step, reason):
super().__init__(reason)
self.step, self.reason = step, reason
def label(self):
return f"reject reason={self.reason}" if self.step is None else f"reject step={self.step} reason={self.reason}"
class Unreadable(Exception):
"""A class whose JSON members do not make one class (energy-measurement §2.7)."""
def blake3(b):
return CV.blake3(b)
def u64(v):
return isinstance(v, int) and not isinstance(v, bool) and 0 <= v <= U64
def hex_bytes(v, n=None):
if not CV.is_hex(v, n):
return None
return bytes.fromhex(v)
def present(m, k):
return m.get(k) is not None
# ------------------------------------------------------------ classes (§2.7)
def read_class(m):
"""The class a receipt's JSON members name, as energy-measurement §2.7 reads it, or
None for no class. Evidence without a class, an unknown class, or evidence a class
does not carry make no class: Unreadable."""
label = m.get("energy_provenance")
u, f, r = m.get("energy_uncertainty"), m.get("energy_counter_filtering"), m.get("energy_calibration_ref")
if label is None:
if u is not None or f is not None or r is not None:
raise Unreadable()
return None
if not isinstance(label, str) or label not in TAG:
raise Unreadable()
if label in ("HwShunt", "ModelBased", "Estimator"):
if u is not None or f is not None or r is not None:
raise Unreadable()
return {"label": label}
if not isinstance(u, dict) or not u64(u.get("relative_ppm")) or not u64(u.get("window_us")):
raise Unreadable()
ppm, win = u["relative_ppm"], u["window_us"]
if ppm == 0 or ppm > U32 or win == 0:
raise Unreadable()
if label == "OnChipCounter":
if r is not None:
raise Unreadable()
if f is None:
f = "Unknown" # an unstated filtering state is Unknown, never Off (§2.4)
if f not in FILTERING:
raise Unreadable()
return {"label": label, "ppm": ppm, "window": win, "filtering": f}
if f is not None or not isinstance(r, str) or r.strip(CV.WHITE_SPACE) == "":
raise Unreadable()
return {"label": label, "ppm": ppm, "window": win, "ref": r}
def encode(c):
"""The signed encoding (§2.7); None is tag 0, the unmetered marker."""
if c is None:
return b"\x00"
b = bytes([TAG[c["label"]]])
if c["label"] == "OnChipCounter":
b += c["ppm"].to_bytes(4, "big") + c["window"].to_bytes(8, "big") + bytes([FILTERING[c["filtering"]]])
elif c["label"] == "CalibratedInstrument":
ref = c["ref"].encode()
b += c["ppm"].to_bytes(4, "big") + c["window"].to_bytes(8, "big") + len(ref).to_bytes(4, "big") + ref
return b
def label_ok(figure, c):
"""A class as an optional label (energy-measurement §2.8): never HwShunt, never
beside a zero figure, at the resolution floor where it declares an uncertainty."""
if c["label"] == "HwShunt" or figure == 0:
return False
return "ppm" not in c or figure * c["ppm"] >= FLOOR
# ------------------------------------------------------------ the tree (§2.2)
def node(a, b):
return blake3(NODE + a + b)
def leaf(o):
if not o["labelled"]:
return blake3(LEAF1 + o["content_hash"])
origin = o["origin"].encode()
return blake3(LEAF2 + o["content_hash"] + o["joules_micro"].to_bytes(8, "big") + encode(o["class"])
+ len(origin).to_bytes(4, "big") + origin)
def depth(n):
return 0 if n <= 1 else (n - 1).bit_length()
# ------------------------------------------------------------ group receipts (§2.4.1, §3 steps 1-3)
def read_group(g):
"""§3 step 1's reading of a group receipt."""
if not isinstance(g, dict) or "kind" not in g:
raise Refuse(1, "malformed")
if g["kind"] != "group":
raise Refuse(1, "wrong_kind")
if any(k not in g for k in GROUP) or any(k not in GROUP and k not in GROUP_LABEL for k in g):
raise Refuse(1, "malformed")
ok = all(u64(g[k]) for k in ("track_alias", "group_id", "leaf_count", "joules_total")) \
and all(isinstance(g[k], str) for k in ("group_start", "group_end", "signer_id")) \
and CV.is_hex(g["root_hash"], 32) and CV.is_hex(g["signer_pubkey"], 32) and CV.is_hex(g["sig"], 64) \
and (g["parent_receipt_hash"] is None or CV.is_hex(g["parent_receipt_hash"], 32))
if not ok:
raise Refuse(1, "malformed")
out = dict(g, labelled=False)
sv = g.get("sign_version")
if sv is None:
if any(present(g, k) for k in GROUP_LABEL):
raise Refuse(1, "group_label_invalid")
return out
# sign_version is a labelled member (§2.4.1): any value but 2 is not the labelled
# form, and is group_label_invalid (§3.3), as leaf_version is object_label_invalid.
if not (isinstance(sv, int) and not isinstance(sv, bool) and sv == 2):
raise Refuse(1, "group_label_invalid")
try:
c = read_class(g)
except Unreadable:
raise Refuse(1, "group_label_invalid")
cov = g.get("energy_coverage")
if c is None:
if cov is not None:
raise Refuse(1, "group_label_invalid")
cov = "Whole"
elif cov not in ("Whole", "Partial"):
raise Refuse(1, "group_label_invalid")
out.update(labelled=True, cls=c, coverage=cov)
return out
def payload(g):
"""§2.3: the legacy payload, or the labelled one — the same fields under the 0x02
domain, then the class, the coverage byte and the parent link."""
b = (SIGN2 if g["labelled"] else SIGN1) + g["track_alias"].to_bytes(8, "big") + g["group_id"].to_bytes(8, "big")
b += bytes.fromhex(g["root_hash"]) + g["leaf_count"].to_bytes(8, "big") + g["joules_total"].to_bytes(8, "big")
for s in (g["group_start"], g["group_end"]):
e = s.encode()
b += len(e).to_bytes(4, "big") + e
sid = g["signer_id"].encode()
b += bytes.fromhex(g["signer_pubkey"]) + len(sid).to_bytes(4, "big") + sid
if g["labelled"]:
b += encode(g["cls"]) + (b"\x01" if g["cls"] is not None and g["coverage"] == "Partial" else b"\x00")
parent = g["parent_receipt_hash"]
b += (b"\x01" + bytes.fromhex(parent)) if parent is not None else b"\x00" + bytes(32)
return b
def link(g):
return blake3(LINK + payload(g) + bytes.fromhex(g["sig"]))
def verify_group(g):
"""§3 steps 1-2: the key, the signature under §3.1 step 3's rule, then a labelled
group's own checks."""
try:
CV.check_signature(bytes.fromhex(g["signer_pubkey"]), bytes.fromhex(g["sig"]), payload(g))
except CV.Reject as r:
raise Refuse(1 if r.reason == "bad_signer_key" else 2, r.reason)
if g["labelled"]:
c, total = g["cls"], g["joules_total"]
if c is None:
ok = total == 0 and g["coverage"] == "Whole"
else:
ok = label_ok(total, c)
if not ok:
raise Refuse(2, "group_label_invalid")
def check_parent(g, previous):
"""§3 step 3. A previous group that does not verify is not held."""
if previous is not None:
try:
verify_group(previous)
except Refuse:
previous = None
parent = g["parent_receipt_hash"]
if previous is None:
if parent is not None:
raise Refuse(3, "parent_not_held")
return
if parent is None or previous["signer_pubkey"] != g["signer_pubkey"] or bytes.fromhex(parent) != link(previous):
raise Refuse(3, "chain_break")
# ------------------------------------------------------------ object receipts (§2.4.2, §3 steps 4-5)
def read_object(o):
"""The object receipt's members. Shape failures are `malformed` at step 4; a label
that cannot be read is kept, and refused at step 5."""
if not isinstance(o, dict) or "kind" not in o:
raise Refuse(4, "malformed")
if o["kind"] != "object":
raise Refuse(4, "wrong_kind")
if any(k not in o for k in OBJ) or any(k not in OBJ and k not in OBJ_LABEL for k in o):
raise Refuse(4, "malformed")
p = o["merkle_proof"]
ok = all(u64(o[k]) for k in ("track_alias", "group_id", "object_id", "joules_micro")) \
and CV.is_hex(o["content_hash"], 32) and isinstance(o["origin"], str) \
and isinstance(p, dict) and sorted(p) == ["direction_bits", "leaf_index", "siblings"] \
and u64(p["direction_bits"]) and u64(p["leaf_index"]) and isinstance(p["siblings"], list) \
and all(CV.is_hex(s, 32) for s in p["siblings"])
if not ok:
raise Refuse(4, "malformed")
r = {"track_alias": o["track_alias"], "group_id": o["group_id"], "object_id": o["object_id"],
"content_hash": bytes.fromhex(o["content_hash"]), "joules_micro": o["joules_micro"], "origin": o["origin"],
"siblings": [bytes.fromhex(s) for s in p["siblings"]], "bits": p["direction_bits"], "index": p["leaf_index"],
"labelled": False, "class": None, "unreadable": False}
lv = o.get("leaf_version")
if lv is None:
r["unreadable"] = any(present(o, k) for k in OBJ_LABEL)
elif isinstance(lv, int) and not isinstance(lv, bool) and lv == 2:
try:
r.update(labelled=True, **{"class": read_class(o)})
except Unreadable:
r["unreadable"] = True
else:
r["unreadable"] = True
return r
def inclusion(o, g):
"""§3 step 5: the label, the position, the walk."""
if o["unreadable"]:
raise Refuse(5, "object_label_invalid")
if o["labelled"]:
c = o["class"]
if (c is None and o["joules_micro"] != 0) or (c is not None and not label_ok(o["joules_micro"], c)):
raise Refuse(5, "object_label_invalid")
n = g["leaf_count"]
if (o["track_alias"] != g["track_alias"] or o["group_id"] != g["group_id"] or o["object_id"] != o["index"]
or not o["index"] < n or len(o["siblings"]) != depth(n) or o["bits"] != o["index"]):
raise Refuse(5, "proof_position_mismatch")
cur = leaf(o)
for i, s in enumerate(o["siblings"]):
cur = node(s, cur) if (o["bits"] >> i) & 1 else node(cur, s)
if cur != bytes.fromhex(g["root_hash"]):
raise Refuse(5, "inclusion_proof_failed")
def accept(o, g, envelope):
"""§3 steps 1-5."""
verify_group(g)
if blake3(OBJECT + envelope) != o["content_hash"]:
raise Refuse(4, "content_hash_mismatch")
inclusion(o, g)
# ------------------------------------------------------------ the group total (§3)
def compose(parts):
"""The composition rule over (figure, labelled, class) parts. None: no class may be
carried; otherwise (class or None for the unmetered marker, coverage)."""
any_zero = any(f == 0 for f, _, _ in parts)
live = [(f, c) for f, lab, c in parts if f != 0]
if any(c is None for _, c in live):
return None
if not live:
return (None, "Whole")
cov = "Partial" if any_zero else "Whole"
weakest = min(RANK[c["label"]] for _, c in live)
if weakest <= 2:
return ({"label": "ModelBased" if weakest == 2 else "Estimator"}, cov)
if any("ppm" not in c for _, c in live):
return None
num = sum(f * c["ppm"] for f, c in live)
den = sum(f for f, _ in live)
ppm = -(-num // den)
win = max(c["window"] for _, c in live)
if weakest == 4:
fs = {c["filtering"] for _, c in live if c["label"] == "OnChipCounter"}
return ({"label": "OnChipCounter", "ppm": ppm, "window": win, "filtering": fs.pop() if len(fs) == 1 else "Unknown"}, cov)
refs = {c["ref"] for _, c in live}
if len(refs) != 1:
return None
return ({"label": "CalibratedInstrument", "ppm": ppm, "window": win, "ref": refs.pop()}, cov)
def same_but_uncertainty(a, b):
if a["label"] != b["label"]:
return False
if a["label"] == "OnChipCounter":
return a["filtering"] == b["filtering"]
if a["label"] == "CalibratedInstrument":
return a["ref"] == b["ref"]
return True
def signed_label(c, cov, checked):
u = f" relative_ppm={c['ppm']} window_us={c['window']}" if "ppm" in c else ""
return f"read signed class={c['label']}{u} coverage={cov} parts={'checked' if checked else 'unchecked'}"
def total(inp):
g = read_group(inp["group"])
verify_group(g)
held = {}
for d in inp["delivered"]:
try:
o = read_object(d["object"])
accept(o, g, bytes.fromhex(d["envelope"]))
except Refuse:
continue
held.setdefault(o["index"], []).append(o)
complete = g["leaf_count"] > 0 and len(held) == g["leaf_count"] and all(len(v) == 1 for v in held.values())
def own(checked):
if not g["labelled"]:
return "read unlabelled"
if g["cls"] is None:
return "read unmetered"
return signed_label(g["cls"], g["coverage"], checked)
if not complete:
return own(False)
parts = [held[i][0] for i in sorted(held)]
if sum(o["joules_micro"] for o in parts) != g["joules_total"]:
raise Refuse(None, "group_total_mismatch")
comp = compose([(o["joules_micro"], o["labelled"], o["class"]) for o in parts])
if not g["labelled"]:
if all(o["joules_micro"] == 0 for o in parts):
return "read unmetered"
if comp is None or comp[0] is None:
return "read unlabelled"
return f"read weakest class={comp[0]['label']} coverage={comp[1]}"
if comp is None:
raise Refuse(None, "group_class_disagrees")
got, want = g["cls"], comp[0]
if (got is None) != (want is None) or (got is not None and not same_but_uncertainty(got, want)):
raise Refuse(None, "group_class_disagrees")
if g["coverage"] != comp[1]:
raise Refuse(None, "group_coverage_disagrees")
if got is not None and "ppm" in got and (got["ppm"] < want["ppm"] or got["window"] != want["window"]):
raise Refuse(None, "group_uncertainty_below_parts")
return own(True)
# ------------------------------------------------------------ the WAI_RECEIPT property
def property_class(p):
"""moq-streaming-format §5's rules on the property alone (`property_invalid`); the
class it names, or None for a zero figure."""
bad = Refuse(None, "property_invalid")
acq, ev = p["acquisition"], bytes.fromhex(p["acquisition_evidence"])
if p["joules_micro"] == 0:
if acq != "" or ev:
raise bad
return None
if acq in ("HwShunt", "ModelBased", "Estimator"):
if ev:
raise bad
return {"label": acq}
if acq == "OnChipCounter":
if len(ev) != 13 or ev[12] > 2:
raise bad
c = {"label": acq, "ppm": int.from_bytes(ev[0:4], "big"), "window": int.from_bytes(ev[4:12], "big"),
"filtering": {0: "Unknown", 1: "Off", 2: "On"}[ev[12]]}
elif acq == "CalibratedInstrument":
if len(ev) < 16 or int.from_bytes(ev[12:16], "big") != len(ev) - 16:
raise bad
try:
ref = ev[16:].decode()
except UnicodeDecodeError:
raise bad
if ref.strip(CV.WHITE_SPACE) == "":
raise bad
c = {"label": acq, "ppm": int.from_bytes(ev[0:4], "big"), "window": int.from_bytes(ev[4:12], "big"), "ref": ref}
else:
raise bad
if c["ppm"] == 0 or c["window"] == 0 or p["joules_micro"] * c["ppm"] < FLOOR:
raise bad
return c
def claim_class(b):
"""An energy claim's class in this file's form."""
label = b["energy_provenance"]
if label in ("OnChipCounter", "CalibratedInstrument"):
u = b["energy_uncertainty"]
c = {"label": label, "ppm": u["relative_ppm"], "window": u["window_us"]}
if label == "OnChipCounter":
c["filtering"] = b["energy_counter_filtering"]
else:
c["ref"] = b["energy_calibration_ref"]
return c
return {"label": label}
def prop(inp):
p = inp["property"]
c = property_class(p)
if "claim" in inp:
try:
claim = CV.verify_claim(inp["claim"])
except CV.Reject:
return "read unsigned" if c is not None else "read unmetered"
if claim["class"] != "energy" or claim["subject"] != p["content_hash"]:
return "read unsigned" if c is not None else "read unmetered"
b = claim["body"]
if c is not None and claim_class(b) == c and b["joules_micro"] == p["joules_micro"]:
# Signed by the claim's signer, whoever that is: never read as the publisher's.
return f"read signed class={c['label']} by={claim['signer_pubkey']}"
raise Refuse(None, "property_disagrees")
g = read_group(inp["group"])
o = read_object(inp["object"])
if bytes.fromhex(p["content_hash"]) != o["content_hash"] or bytes.fromhex(p["group_receipt"]) != link(g):
raise Refuse(None, "property_disagrees")
accept(o, g, bytes.fromhex(inp["envelope"]))
if not o["labelled"]:
return "read unmetered" if c is None else "read unsigned"
if o["class"] is None and c is None:
return "read unmetered"
if o["class"] is not None and c is not None and o["class"] == c and o["joules_micro"] == p["joules_micro"]:
return f"read signed class={c['label']} by={g['signer_pubkey']}"
raise Refuse(None, "property_disagrees")
# ------------------------------------------------------------ vectors
def verdict(v):
inp = v["input"]
try:
if v["procedure"] == "leaf":
return "leaf=" + leaf(read_object(inp["object"])).hex()
if v["procedure"] == "group":
g = read_group(inp["group"])
verify_group(g)
prev = None
if "previous" in inp:
try:
prev = read_group(inp["previous"])
except Refuse:
prev = None
check_parent(g, prev)
return f"accept link_hash={link(g).hex()} signing_payload={payload(g).hex()}"
if v["procedure"] == "object":
g = read_group(inp["group"])
o = read_object(inp["object"])
accept(o, g, bytes.fromhex(inp["envelope"]))
return "accept"
if v["procedure"] == "total":
return total(inp)
if v["procedure"] == "property":
return prop(inp)
except Refuse as r:
return r.label()
raise ValueError("unknown procedure " + str(v["procedure"]))
def expected_of(e):
if "leaf" in e:
return "leaf=" + e["leaf"]
if e["outcome"] == "accept":
return f"accept link_hash={e['link_hash']} signing_payload={e['signing_payload']}" if "link_hash" in e else "accept"
if e["outcome"] == "read":
return "read " + e["reading"]
return f"reject step={e['step']} reason={e['reason']}" if "step" in e else f"reject reason={e['reason']}"
def main(argv):
if argv[1:2] == ["verdicts"] and len(argv) == 3:
doc = json.load(open(argv[2], encoding="utf-8"))
for v in doc["vectors"]:
try:
got = verdict(v)
except (ValueError, KeyError, TypeError, AttributeError):
got = "error"
print(f"{v['name']}\t{got}")
return 0
path = argv[1] if len(argv) > 1 else os.path.join(HERE, "vectors.json")
doc = json.load(open(path, encoding="utf-8"))
fail = 0
b3 = blake3(b"abc").hex() == "6437b3ac38465133ffb63b75273a8db548c558465d79db03fd359c6cd5bd9d85"
print(("PASS" if b3 else "FAIL") + " BLAKE3 reproduces its published value")
fail += not b3
for v in doc["vectors"]:
want, got = expected_of(v["expected"]), verdict(v)
ok = want == got
fail += not ok
print(f"{'PASS' if ok else 'FAIL'} {v['name']} expected={want} got={got}")
print(f"{len(doc['vectors'])} vectors, {fail} failure(s)")
return 1 if fail else 0
if __name__ == "__main__":
sys.exit(main(sys.argv))

File diff suppressed because it is too large Load diff

View file

@ -689,7 +689,7 @@ review.
| G3 | **SyfAI's AI update-framework thread, opened at MPEG 155 jointly with WG 7 (S ⚠), covers parameter formats, manifests, repositories, integrity checking and bit exactness for NN decoders. The artifact a WAI `Prior` names has no normative wire form.** `Prior { id, sha256, determinism }` pins identity: `sha256` hashes the decoder artifact the sink runs. No wire format for that artifact is specified (2030 §6.4). | `wai-rs/src/container.rs` `struct Prior`; 2030 landscape §6.4; no SyfAI or Exploration 52 mention in the tree (grep) | **Addressed at link 05 (PR #106), with limits.** [`prior-carriage.md`](../extensions/prior-carriage.md) §2: a `Prior` MAY declare `form`; when it does, the digest covers the coded bitstream, and a sink that cannot read the form MUST refuse the prior. §3 registers `wai.prior.nnc` (ISO/IEC 15938-17:2024) as the one form, optional and not recommended, because its royalty status and cross-decoder exactness are not established. `Prior::is_cross_hardware_exact` requires a declared form to be established exact, none is, so a `replicate` resting on a declared form is refused (§5). The reference sink reads no form (`READABLE_PRIOR_FORMS` is empty), and `wai-rs` has no NNC reader or writer. SPEC's opening section now says a model's identity MAY be pinned by digest (a `Prior`). [`syfai-input.md`](syfai-input.md), committed with this record, maps WAI onto the update framework; its G1 records what SPEC still does not define |
| G4 | **WAI's `HwShunt` takes in IOReport, a reading the monorepo classes five ways; and neither WAI nor the family's receipt ladder separates platform-relayed counters from independently rooted instruments. Only `sandbox-tee` does.** Three layers. **(a) The family's receipt ladder.** Every standard that lists interfaces under `HwShunt` puts on-die counters and dedicated shunts in it together (JCR-1 §2.3, JCP §5.1, Sandbox §6.3, `mesh-schema`, `proof-schema`), and IOReport is classed inconsistently (§5.3). **(b) `sandbox-tee`'s `CounterClass`** reserves `HwShunt` for a dedicated shunt and gives RAPL, NVML, IOReport and Redfish classes of their own; its `AttestTrust` ladder is derived from evidence, and `efficiency/1` commits to the attestation (§5.1). **(c) WAI** has `HwShunt` cover "RAPL, NVML, SMC, IOReport, or an external shunt", and `wai_video_meter` stamps IOReport readings `HwShunt`. That places WAI with Joule Code's text and the Sandbox meter, and against JCR-1 §2.3. WAI's texts present the ladder as the family's own without noting the split: `video_receipt.rs` says it is "reused verbatim from `mesh-schema` / `proof-schema`", which list no IOReport; `energy_binding.rs` calls it "the family's ladder"; `energy-measurement.md` §2 says WAI reuses "the same three names the receipt families elsewhere in the ecosystem already use". At every layer but (b), a verifier cannot tell a platform-relayed counter, which may be deliberately filtered (§5.1), from an independently rooted instrument. | §5.3 (the per-standard survey); `wai/extensions/energy-measurement.md` §2; `wai-rs/src/energy_binding.rs`:30, :47–51 (`Acquisition`); `wai-rs/src/video_receipt.rs` `Provenance` doc comment; `wai-rs/src/bin/wai_video_meter.rs`:184–190; `jouleclaw/jouleclaw-rs/crates/jouleclaw-jcr1/docs/JCR-1-spec.md` §2.3; `jcp/SPEC.md` §5.1; `sandbox/SPEC.md` §6.3–§6.4; `mesh/mesh-rs/crates/mesh-schema/src/lib.rs` and `proof/proof-rs/crates/proof-schema/src/lib.rs` (`Provenance`); `sandbox/sandbox-rs/crates/sandbox-tee/src/energy.rs` (`AttestTrust`, `CounterClass`, `is_independently_rooted`); `jcp/jcp-rs/crates/jcp-efficiency/MAPPING.md` §4 and `src/training.rs` (`energy_attestation`). No mention of `sandbox-tee`, `CounterClass`, `AttestTrust`, `energy_attestation`, `PhysicallyRooted` or `IntegrityAttested` under `wai/` (grep) | **WAI's side closed at link 08 (PR #109); the family's side open.** energy-measurement §2 separates `CalibratedInstrument` (an external instrument with a current calibration) from `OnChipCounter` (a counter the measured silicon keeps, or any instrument whose uncertainty is not traceable). Each carries a declared uncertainty (§2.3); the counter carries its filtering state (§2.4), the instrument a calibration reference (§2.5). `HwShunt` is legacy, accepted and never emitted (§2.6), and IOReport is `OnChipCounter` (§2.9). `energy_class.rs` is the one definition, and the reference meters seal `OnChipCounter`. The "reused verbatim" sentence is gone; `video_receipt.rs` and `energy_binding.rs` still call the ladder the family's, and §2 says the three-name vocabulary is extended, not replaced, with both refinements mapping to `HwShunt`. §2.9's list of differing documents is partial (§5.3). No WAI text references `sandbox-tee`'s ladder (grep). The family's IOReport split and whether its receipt-level ladder should separate the two kinds of hardware evidence are unassigned (below) |
| G5 | **The codec registry has no AV2, VVC, HEVC, LCEVC, MPEG-H Audio, AVS3 or Audio Vivid capability.** Brazil mandates three of these (§3.3). Two are Chinese standards: AVS3 (a group standard, with its national standard pending) and Audio Vivid (GB/T 46271-2025). 2030 §6.1 (*Honest scope*) records that WAI registers no VSEI-family codec. **The tree's codec survey is a second surface to reconcile.** `wai/glossary/codecs.json`, which `wai/README.md` describes as the "Reference catalog of media codecs surveyed for capability registration", has entries for HEVC / H.265 and VVC / H.266 (both `niche`), LCEVC (`niche`) and ECM (`academic`, JVET's exploration model beyond VVC), and none for AV2, MPEG-H Audio, AVS3 or Audio Vivid. `glossary/SCHEMA.md` defines `success` as "carries non-trivial production traffic today" and `niche` as "alive in specific contexts". §4 reports HEVC in production use at 65% in a vendor survey (**S** ⚠), §3.3 reports VVC as a national broadcast system's main codec (**R** ⚠), and the HEVC entry's own description calls it universal in broadcast UHD. The grades are descriptive and the contrary evidence is unverified, so this is a reconciliation item, not a correction. | capability strings in `wai-rs/src/codecs/*.rs`: none for these codecs. Other mentions, from a grep over `*.rs`, `*.md`, `*.json`, `*.toml`, `*.yml`, `*.yaml`, `*.html`, `*.ts` and `*.js` under `wai/`: the `glossary/codecs.json` entries above; the H.264/H.265 royalty sentence in `SPEC.md` and `CHANGELOG.md`; the BD-rate note that names VVC in `wai-rs/src/codecs/mod.rs` and `wai-rs/src/neural/mlicpp.rs`; `HEVC` as an example name in `glossary/SCHEMA.md`; the 2030 landscape. Royalty class per codec **not established by this sweep**: only AV2's royalty-free patent policy was seen at the issuing body (the AOMedia press release, §2.4) | **Closed in the registry at link 04 (PR #105); the glossary is not reconciled.** SPEC §5 *Broadcast and streaming codecs* registers `wai.video.{av2,vvc,hevc,avc,vp9,evc,evc.baseline,avs3,lcevc}` and `wai.audio.{mpegh,avs3,aac}`, each with one payload framing and a licence class whose basis is recorded in [`media-codec-registry.md`](media-codec-registry.md). `wai.audio.avs3` names AVS3 audio / Audio Vivid (GB/T 46271-2025). All are sink-supplied, none is a default, and `wai.video.lcevc` is companion-only. SPEC §7 classes AVC, HEVC, VVC and VP9 `StandardDefined` on the conformance clauses read, AV2 `StandardTolerance`, and EVC, AVS3, LCEVC, MPEG-H 3D Audio and AAC `StandardUnclassified`. The licence classes rest on the registry's recorded bases, not on this sweep. `glossary/codecs.json` is unchanged, so its grades still stand against §3.3 and §4 |
| G6 | **No WAI receipt says who can check the energy figure, and the base JWP receipt does not carry even the acquisition class that `energy-measurement.md` requires.** The precedents in §5.1 differ exactly on who can check: instrument-signed (OCMF), peer-reviewed but unsigned, log-registered (SCITT), TEE-attested with a derived trust level (the family's `sandbox-tee` `AttestTrust`), or producer-asserted. **The base JWP receipt** has no acquisition-class field; its group signature covers `total_joules_micro` and no class, and the per-object `joules_micro` is in neither the Merkle leaf nor the signature. `energy-measurement.md` §2 requires a class inside the signature for any non-zero figure, while its header says it adds "no new wire format". **The video receipt** signs its class. **The energy binding** carries its class in a C2PA companion assertion signed as part of whatever manifest carries it. **The OER receipt** carries an evidential grade and a ledger identity, and no class. 2030 §8 item 4 queues typed receipt classes. | `extensions/energy-measurement.md` header, §1, §2 and §5; `extensions/jwp-receipts.md` §2.1–§2.4; `joule-code/joule-code-rs/crates/joule-moq/src/receipt.rs` (`ObjectReceipt`, `GroupReceipt::signing_payload`: no class); `wai-rs/src/energy_binding.rs`, `wai-rs/src/video_receipt.rs`; `extensions/oer-receipts.md` §2–§3, `wai-rs/src/oer.rs`; no reference to the family ladder under `wai/` (grep, as in G4) | **Partly closed at link 10 (PR #112); the rest open.** Every receipt within energy-measurement's scope that `wai-rs` and `wai-quantum` seal can carry the class (link 08, PR #109). jwp-receipts §2.4.3–§2.4.7 add a `claim` kind with typed classes. An `energy` claim (§2.4.5) restates a JWP object's figure beside its class, the class's evidence, a coverage statement and a grade; a verifier holding the object receipt rejects the pair if the figures differ, and one holding the OER receipt the claim names checks that grade and figure agree. `claim_receipt.rs` (feature `receipt_claims`) implements it, with vectors in `claim-conformance/`. Open, as energy-measurement §1 records: the JWP object receipt has no member of its own for the class; the group total has none, and nothing carries one for it; the `WAI_RECEIPT` property's class and evidence sit outside any signature; an OER/2 receipt has no member for the class. On who can check: a claim carries its signer's authority and no more (§2.4.4); key discovery and an authority class for who may sign which class are not done (CHANGELOG, v0.4.0 entry for typed receipt classes); no WAI receipt references `sandbox-tee`'s ladder or `efficiency/1`'s attestation (grep) |
| G6 | **No WAI receipt says who can check the energy figure, and the base JWP receipt does not carry even the acquisition class that `energy-measurement.md` requires.** The precedents in §5.1 differ exactly on who can check: instrument-signed (OCMF), peer-reviewed but unsigned, log-registered (SCITT), TEE-attested with a derived trust level (the family's `sandbox-tee` `AttestTrust`), or producer-asserted. **The base JWP receipt** has no acquisition-class field; its group signature covers `total_joules_micro` and no class, and the per-object `joules_micro` is in neither the Merkle leaf nor the signature. `energy-measurement.md` §2 requires a class inside the signature for any non-zero figure, while its header says it adds "no new wire format". **The video receipt** signs its class. **The energy binding** carries its class in a C2PA companion assertion signed as part of whatever manifest carries it. **The OER receipt** carries an evidential grade and a ledger identity, and no class. 2030 §8 item 4 queues typed receipt classes. | `extensions/energy-measurement.md` header, §1, §2 and §5; `extensions/jwp-receipts.md` §2.1–§2.4; `joule-code/joule-code-rs/crates/joule-moq/src/receipt.rs` (`ObjectReceipt`, `GroupReceipt::signing_payload`: no class); `wai-rs/src/energy_binding.rs`, `wai-rs/src/video_receipt.rs`; `extensions/oer-receipts.md` §2–§3, `wai-rs/src/oer.rs`; no reference to the family ladder under `wai/` (grep, as in G4) | **Partly closed at link 10 (PR #112); the rest open.** Every receipt within energy-measurement's scope that `wai-rs` and `wai-quantum` seal can carry the class (link 08, PR #109). jwp-receipts §2.4.3–§2.4.7 add a `claim` kind with typed classes. An `energy` claim (§2.4.5) restates a JWP object's figure beside its class, the class's evidence, a coverage statement and a grade; a verifier holding the object receipt rejects the pair if the figures differ, and one holding the OER receipt the claim names checks that grade and figure agree. `claim_receipt.rs` (feature `receipt_claims`) implements it, with vectors in `claim-conformance/`. Open, as energy-measurement §1 records: the JWP object receipt has no member of its own for the class; the group total has none, and nothing carries one for it; the `WAI_RECEIPT` property's class and evidence sit outside any signature; an OER/2 receipt has no member for the class. On who can check: a claim carries its signer's authority and no more (§2.4.4); key discovery and an authority class for who may sign which class are not done (CHANGELOG, v0.4.0 entry for typed receipt classes); no WAI receipt references `sandbox-tee`'s ladder or `efficiency/1`'s attestation (grep) *Status after the v0.4.0 change "Energy reports, labelled object leaves, and what the signatures cover":* the object's class rides in its labelled leaf, a group total can carry a signed class of its own, and a `WAI_RECEIPT` property's class counts only where a signed statement restates it; joule-moq verifies both labelled forms. Who can check remains open. |
| G7 | **Explicit parameter transmission appears in `determinism-tiers.md` only as a generic route. Its first published cross-NPU instance (arXiv 2606.28027) and its cost are not recorded.** Also unrecorded: the mixed-precision entropy-synchronisation line (arXiv 2608.00483: FP16/FP32 across GPUs, no bit-exactness claim); the quantised learned-coding line (arXiv 2007.04684, 2111.09348); and IEEE 1857.11's use of *reproducibility cross-checks* rather than bit-exact decode (**R** ⚠), a reproducibility notion the tier vocabulary does not name. Both 2606.28027 and 2608.00483 target a synchronised entropy stage, so at most `entropy-consistency`, and 2608.00483 only over the GPUs it tested. On `main`, whether such a decode may back `replicate` depends on which of G10's conditions is read. | `extensions/determinism-tiers.md` §1–§3; `SPEC.md` §7 (read) | **Partly closed at link 02 (PR #103).** determinism-tiers §1 names arXiv 2606.28027 as the explicit-parameter instance and says the side information costs bitrate (no magnitude), and names a quantised decoder, arXiv 2312.11209, as the route to a reproducible reconstruction. Such a decode reaches `entropy-consistency` and does not back `replicate` (G10). Not in the tree at link 10 (grep): arXiv 2608.00483, the quantised line 2007.04684 / 2111.09348, and IEEE 1857.11's reproducibility cross-checks; the last is described in [`syfai-input.md`](syfai-input.md) §5, committed with this record |
| G8 | **Standards inputs not tracked in the tree:** SyfAI; 3GPP TR 26.942; DVB S100 and CM-EE; the beyond-VVC CfP; Gaussian Splat Coding Exploration 45; IEEE 1857.11 / P1857.11.1; UWA 042; India D2M; the JPEG AI core experiments on energy consumption and bit-exact reconstruction. | a case-insensitive, word-bounded `git grep -P` over tracked `*.md`, `*.rs`, `*.json`, `*.toml`, `*.yml`, `*.yaml`, `*.html`, `*.ts` and `*.js` files finds none of these designations outside this record. The one other hit for `uwa` is the Hausa word in `aac/aac-rs/crates/aac-core/src/translations_data.json` (`"ha":"uwa"`), which is not the organisation. MPEG's splat work *is* tracked, though not Exploration 45 by name: `media-codec-registry.md` and `codecs/mod.rs` register `wai.splat.sog` against "MPEG WG4" work in progress, `3d-neural-coding-landscape.md` records splat coding under standardisation, and 2030 §6.4 records an NNC amendment covering Gaussian splats (G11) | **Partly closed by the two notes committed with this record.** [`syfai-input.md`](syfai-input.md) tracks SyfAI, IEEE 1857.11 and UWA 042; [`energy-exposure-mapping.md`](energy-exposure-mapping.md) tracks TR 26.942, DVB S100 and CM-EE. The same grep over the tree at link 10 finds the beyond-VVC CfP, Exploration 45, India D2M and the JPEG AI core experiments in no document but this record. Those four stay open |
| G9 | **WAI's texts and code class `wai.image.jpegai` in ways that contradict one another, and SPEC contradicts itself on it.** SPEC §5, the capability's registration (`SPEC.md`:197), describes the decode as "integer entropy front end (mANS) + integer dequant/value path + fixed-order synthesis and post-filters" and declares "**decode-equivalence**: one pinned reconstruction … reproducible on every machine (a portable decoded-pixel hash)". `reconstruction-binding.md` §1 (:30–31) says the decode to RGB is "byte-identical on every machine", and §2 (:72) sets the binding's criterion at `decode-equivalence`; `determinism-tiers.md` §2–§3 list the capability there. SPEC §7 lets a neural capability back `replicate` only "with no float operation anywhere" (:445–448). `capability_replicate_class` returns `StandardDefined` for it through the default arm, and SPEC §7 defines that class as "reconstruction is fixed by the named codec/standard … the neural float hazard does not apply"; neither half holds for JPEG AI (§2.2; its decode runs the model). The strict `replicate` check reads only the envelope's prior (`neural/mod.rs`:282–292). **Loose ends:** (i) `jpegai_synthesis.rs` and `jpegai_mans.rs` say they back `wai.neural.jpeg_ai`, a string registered nowhere; (ii) the decode has f32 steps between its integer stages — its dequantisation among them, although SPEC §5 calls it integer; (iii) the byte-for-byte claim is made normatively, in SPEC §5 and `reconstruction-binding.md` §1, and rests on an `exp()` from the platform maths library and on no measurement on a second platform; (iv) the reference sink's dispatch (`neural/mod.rs`:205–216) uses the exact decoder only when the integer weight bundle is present, and otherwise falls back to a float ONNX Runtime decode. | `SPEC.md` §5 (:197) and §7 (:445–448, the `StandardDefined` bullet at :476–478, :492–495); `extensions/reconstruction-binding.md` §1 (:21–35) and §2 (:72); `extensions/determinism-tiers.md` §2–§3; `wai-rs/src/codecs/mod.rs` (the default arm and the test asserting `StandardDefined`); `wai-rs/src/jpegai_decode.rs`, `jpegai_synthesis.rs`, `jpegai_mans.rs`, `jpegai_pipeline.rs`; `wai-rs/src/neural/mod.rs`:205–216, :282–292; `git grep -n decode-equivalence` over `wai/` | **Closed at link 02 (PR #103), with the maths library removed at link 03 (PR #104).** SPEC §5's row registers `entropy-consistency` (registry class `NeuralEntropyExact`), describes dequantisation, latent refinement, post-filters and colour conversion as IEEE-754 float with a fixed evaluation order, and says "**Downgraded in v0.4.0:** this row previously claimed decode-equivalence". `reconstruction-binding.md` §1–§2 make the pinned reconstruction a property of the exact decoder, reproducible under that decoder's build and floating-point conditions. SPEC §7 states the new class and the reason. Loose ends: (i) fixed — the module docs name `wai.image.jpegai`, and `wai.neural.jpeg_ai` appears nowhere else in the tree (grep); (ii) the float steps remain and are now stated as float; (iii) link 03 builds `exp` and rounding from IEEE-754 basic operations (`float_repro`), guards the decode path with clippy's `disallowed_methods` and a linked-symbol check (`tests/jpegai_decode_symbols.rs`), and pins nine hashes for one codestream; determinism-tiers §6 records them identical on macOS arm64, wasm32-wasip2, Linux arm64 and Linux x86_64 under user-mode emulation, and not measured on Windows, macOS x86_64 or x86_64 hardware; (iv) the float fallback remains, and SPEC §5, determinism-tiers §5 and reconstruction-binding §1 now state it, and that the result does not record which path ran. Still true: a `norm.json` with a missing or mistyped key panics in the loaders instead of falling back, as the `try_jpegai_exact` doc comment says |
@ -730,6 +730,14 @@ review.
- **The open parts of G6.** A signed place for a JWP group total's class, a
member of its own for the JWP object receipt, a signature over
`WAI_RECEIPT`, and key discovery for claim signers.
*Status after the v0.4.0 change "Energy reports, labelled object leaves, and
what the signatures cover":* the JWP object receipt carries its class in its
Merkle leaf with its figure and origin (jwp-receipts §2.2, §2.4.2); a group
total can carry its own class under a labelled group payload that also signs
the parent link (§2.3, §2.4.1); a `WAI_RECEIPT` property's class reads as
signed only where it equals the leaf's label or an `energy` claim
(moq-streaming-format §5). Open: key discovery for claim signers, an OER/2
receipt's class, and who, other than the signer, stands behind a figure.
- **G7's other lines** (arXiv 2608.00483, the quantised learned-coding line,
IEEE 1857.11's reproducibility cross-checks) and **G8's other inputs** (the
beyond-VVC CfP, Exploration 45, India D2M, the JPEG AI core experiments) are

View file

@ -465,6 +465,12 @@ term") read as if the joules were the only such term. That is true only if the
measurement fields are counted as part of the joule figure. An input should use
the wider wording, and those two sections should be reworded to match.
*Status after the v0.4.0 change "Energy reports, labelled object leaves, and what the signatures cover":* energy-measurement §5 and energy-binding §3
now use the wider wording: what stays unverifiable is the measurement — the joules
and every field that describes how they were obtained — and a report's values can
be checked for consistency with the figure, which is not truth. The code docs that
called `joules_micro` the only attested field say the same.
**L9: The reporting contract has no receipt fields.** energy-measurement §6
requires a published figure to be accompanied by the idle mean and SD, busy
mean and SD, sample counts, window, completion count, thread count, how the
@ -477,6 +483,17 @@ meters print the idle and busy figures, the marginal-to-idle ratio and the
resolution term to their console. So the record DVB use case 1 would need (§2)
is a requirement in the text, not yet a format any receipt carries.
*Status after the v0.4.0 change "Energy reports, labelled object leaves, and what the signatures cover":* closed for figures sealed with a class. A
`measurement` claim (jwp-receipts §2.4.8) carries the bracket's values as integers,
each bracket's liveness facts, the build (profile, toolchain, target) and the power
reader, names the receipt that seals the figure by a figure profile
(energy-measurement §6.1), and is signed by the figure's own key. Verifiers check
that the figure and the declared uncertainty are what the integers give
(energy-measurement §6.2); the reference meters write one by default. Figures
sealed with no class, `world_model::frame_receipt` and the `wai-web` demos are
recorded as falling short (energy-measurement §6), and the energy binding's report
carriage is not specified.
**L10: The operating-point fields exist only as text.** energy-binding §7.2 and
§7.3 specify `op_requested`, `op_committed`, `op_delivered` and the residual
between a declared complexity descriptor and the measured energy. No receipt or
@ -485,6 +502,12 @@ carries the reconstruction hash, the microjoules, the acquisition class and its
evidence, coverage, work units and `method`. §2 use case 3 and §4 describe what
the text adds, not what a receipt carries today.
*Status after the v0.4.0 change "Energy reports, labelled object leaves, and what the signatures cover":* an `operating-point` claim (jwp-receipts
§2.4.9) carries `op_requested`, `op_committed`, `op_delivered` and a prediction for
one figure, from a vocabulary named by URI. Whether the commitment was met and the
residual are derived by every reader, never stated. No operating point is recorded
against an unmetered or legacy `HwShunt` figure (energy-binding §7.2).
**Unverified near-prior-art.** Search snippets seen during this work pointed to
public repositories describing signed receipts for AI inference with
NVML-measured joules. Their bodies were not read, so they are neither named nor

View file

@ -598,7 +598,7 @@ dependencies = [
[[package]]
name = "wai-quantum-serve"
version = "0.3.0"
version = "0.4.0"
dependencies = [
"ed25519-dalek",
"wai",

View file

@ -1,6 +1,6 @@
[package]
name = "wai-quantum-serve"
version = "0.3.0"
version = "0.4.0"
edition = "2021"
license = "Apache-2.0"
description = "The WAI quantum stack as a WASI-HTTP component — deterministic quantum computation as a serverless function. Runs on any wasi:http/proxy host (wasmtime serve, and other component-model runtimes); returns the same statevector hashes and the same signed receipt bytes as a native run."

View file

@ -43,12 +43,21 @@ struct Component;
/// (`quantum_energy::Labelled`, as the reference quantum meter emits). The
/// verdict restates the class and its evidence, or `"energy_provenance":null`
/// for a figure with no class.
///
/// A receipt's `joules_micro` of `0` is its unmetered marker, never a figure of
/// zero joules (energy-measurement §2), so the verdict renders it as
/// `"joules_micro":null` with `"metered":false`: a client totalling or comparing
/// the field cannot read an unmetered run as free work.
fn verify_response(src: &str) -> (u16, String) {
let Some(any) = MaybeLabelled::<QuantumReceipt>::from_json(src) else {
return (400, "{\"valid\":false,\"reason\":\"unparseable receipt\"}".to_string());
};
let ok = any.verify();
let r = any.receipt();
let (joules, metered) = match r.joules_micro {
0 => ("null".to_string(), false),
j => (j.to_string(), true),
};
let class = match any.energy_class() {
None => "\"energy_provenance\":null".to_string(),
Some(c) => c.json_fields().iter().map(|(k, v)| format!("\"{k}\":{v}")).collect::<Vec<_>>().join(","),
@ -57,14 +66,13 @@ fn verify_response(src: &str) -> (u16, String) {
if ok { 200 } else { 409 },
format!(
"{{\"valid\":{},\"circuit_hash\":\"{}\",\"statevector_hash\":\"{}\",\
\"n_qubits\":{},\"n_ops\":{},\"work_amp_updates\":{},\"joules_micro\":{},\"signer_id\":\"{}\",{class}}}",
\"n_qubits\":{},\"n_ops\":{},\"work_amp_updates\":{},\"joules_micro\":{joules},\"metered\":{metered},\"signer_id\":\"{}\",{class}}}",
ok,
hx(&r.circuit_hash),
hx(&r.statevector_hash),
r.n_qubits,
r.n_ops,
r.work_amp_updates,
r.joules_micro,
esc(&r.signer_id)
),
)
@ -348,4 +356,18 @@ mod tests {
assert_eq!(verify_response(&j.replace("288830", "288831")).0, 409);
assert_eq!(verify_response("not a receipt").0, 400);
}
/// An unmetered receipt's `0` is the unmetered marker: the verdict never
/// presents it as a figure of zero joules.
#[test]
fn an_unmetered_receipt_is_never_reported_as_zero_joules() {
let key = SigningKey::from_bytes(&[0x71u8; 32]);
let r0 = QuantumReceipt::seal(&key, "did:key:wai-quantum-cli", &ghz3(), None, 0, None).unwrap();
let (_, body) = verify_response(&r0.to_json());
assert!(body.contains("\"joules_micro\":null,\"metered\":false"), "{body}");
assert!(!body.contains("\"joules_micro\":0"), "{body}");
let r1 = QuantumReceipt::seal(&key, "did:web:meter", &ghz3(), None, 7, None).unwrap();
let (_, body) = verify_response(&r1.to_json());
assert!(body.contains("\"joules_micro\":7,\"metered\":true"), "{body}");
}
}

View file

@ -474,7 +474,7 @@ required-features = ["world"]
[[bin]]
name = "wai_world_meter"
path = "src/bin/wai_world_meter.rs"
required-features = ["world"]
required-features = ["world", "receipt_claims"]
[[bin]]
name = "wai_audio_vectors"
@ -520,6 +520,11 @@ name = "wai_claim_vectors"
path = "src/bin/wai_claim_vectors.rs"
required-features = ["receipt_claims"]
[[bin]]
name = "wai_jwp_vectors"
path = "src/bin/wai_jwp_vectors.rs"
required-features = ["receipt_claims"]
[[bin]]
name = "wai_quantum_vectors"
path = "src/bin/wai_quantum_vectors.rs"
@ -533,22 +538,22 @@ required-features = ["quantum_ops"]
[[bin]]
name = "wai_quantum_meter"
path = "src/bin/wai_quantum_meter.rs"
required-features = ["quantum_receipt"]
required-features = ["quantum_receipt", "receipt_claims"]
[[bin]]
name = "wai_video_meter"
path = "src/bin/wai_video_meter.rs"
required-features = ["video_receipt"]
required-features = ["video_receipt", "receipt_claims"]
[[bin]]
name = "wai_meter"
path = "src/bin/wai_meter.rs"
required-features = ["haptic", "splat4d", "score", "film"]
required-features = ["haptic", "splat4d", "score", "film", "receipt_claims"]
[[bin]]
name = "wai_prov"
path = "src/bin/wai_prov.rs"
required-features = ["provenance", "codecs"]
required-features = ["provenance", "codecs", "receipt_claims"]
[[bin]]
name = "wai_bench"

View file

@ -10,8 +10,12 @@
//!
//! This replaces the macOS-hardcoded `.cargo/config.toml` rustflags.
#[path = "src/build_info.rs"]
mod build_info;
fn main() {
generate_c_header();
record_build();
// Only the `codecs` feature compiles flac-bound — and pkg-config's
// probe() emits a `cargo:rustc-link-lib=flac` of its own, so running
// it unconditionally makes every consumer (e.g. the wasm32 world
@ -21,6 +25,33 @@ fn main() {
}
}
/// Record the build for a measurement report (`extensions/energy-measurement.md`
/// §5, §8): the profile, from cargo's `PROFILE` and `OPT_LEVEL`
/// (`src/build_info.rs`), the target triple, and the compiler's version line.
/// A part that cannot be determined is left unset and reads as `null`.
fn record_build() {
println!("cargo:rerun-if-env-changed=PROFILE");
println!("cargo:rerun-if-env-changed=OPT_LEVEL");
println!("cargo:rerun-if-env-changed=TARGET");
println!("cargo:rerun-if-env-changed=RUSTC");
let var = |k: &str| std::env::var(k).unwrap_or_default();
println!(
"cargo:rustc-env=WAI_BUILD_PROFILE={}",
build_info::profile_label(&var("PROFILE"), &var("OPT_LEVEL"))
);
let target = var("TARGET");
if !target.is_empty() {
println!("cargo:rustc-env=WAI_BUILD_TARGET={target}");
}
let rustc = std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into());
if let Ok(out) = std::process::Command::new(rustc).arg("-V").output() {
let line = String::from_utf8_lossy(&out.stdout).trim().to_owned();
if out.status.success() && !line.is_empty() && !line.contains('\n') {
println!("cargo:rustc-env=WAI_BUILD_TOOLCHAIN={line}");
}
}
}
/// Regenerate `include/wai.h` from the FFI surface so SDK consumers can
/// always grab a typed header that matches the shipped binary. Only
/// regenerates when source files actually change (cargo handles this

View file

@ -1,5 +1,5 @@
//! Conformance corpus for typed receipt classes and the soft binding class
//! (`claim-conformance/`, extensions/jwp-receipts.md §2.4.3–§2.4.7, §2.6, §3.1–§3.2).
//! (`claim-conformance/`, extensions/jwp-receipts.md §2.4.3–§2.4.9, §2.6, §3.1–§3.2).
//!
//! Every vector declares one outcome: `accept` (with the claim's link hash, which pins
//! the canonical form; or, for a recovery, the ancestor it attests), or `reject` at a
@ -16,11 +16,13 @@
//! `claim-conformance/differential.py`
//!
//! The generator and this verifier are one implementation, so the corpus alone cannot
//! show that this one reads the specification correctly. Three things are checked
//! show that this one reads the specification correctly. Four things are checked
//! against code that shares nothing with it: the canonical fixture (bytes, signature and
//! link hash computed by `claim-conformance/claim_verify.py`), and — run by hand, not in
//! CI — that script's own verification of every vector, and `differential.py`, which
//! compares the two verifiers' verdicts on generated inputs the corpus does not hold.
//! link hash computed by `claim-conformance/claim_verify.py`); the bracket arithmetic of
//! energy-measurement §6.2 (figures and least declarations that script computes with
//! Python's integers); and — in CI, on the linux runner — that script's own verification
//! of every vector, and `differential.py`, which compares the two verifiers' verdicts on
//! generated inputs the corpus does not hold.
use std::path::Path;
use wai::claim_receipt::{check_canonical_form, conformance_vectors, corpus_verdicts, corpus_vector_count, run_corpus};
@ -80,6 +82,14 @@ fn verify(dir: &Path) -> i32 {
);
fail += (!cf.is_empty()) as usize;
let ba = wai::energy_report::check_bracket_arithmetic();
println!(
"{} bracket arithmetic pinned to its independently computed values got={}",
if ba.is_empty() { "PASS" } else { "FAIL" },
ba.join(",")
);
fail += (!ba.is_empty()) as usize;
let path = dir.join("vectors.json");
// `.gitattributes` pins LF; a checkout that ignores it (a CRLF working tree) changes
// only the pretty-print line breaks, never a receipt, so it is undone rather than

View file

@ -0,0 +1,98 @@
//! Conformance corpus for labelled leaves, labelled group receipts, the group
//! total and the `WAI_RECEIPT` property (`jwp-conformance/`, extensions/jwp-receipts.md
//! §2.2–§2.4.2, §3, §3.3 and §5.7, moq-streaming-format §5).
//!
//! Every vector declares one outcome: a leaf hash; `accept` (for a group, with its
//! link hash and signing payload); a reading of a total or a property; or a
//! rejection with its reason, and its §3 step where it has one.
//!
//! wai_jwp_vectors gen <corpus-dir> write vectors.json
//! wai_jwp_vectors verify <corpus-dir> the pinned file regenerates byte-identically,
//! and every vector gets its declared outcome
//! wai_jwp_vectors verdicts <file> one `name<TAB>verdict` line per vector,
//! any declared outcome ignored
//!
//! `jwp-conformance/jwp_verify.py`, written from the extension text and sharing no
//! code with this crate, reports the same outcomes; CI runs it.
use std::path::Path;
use wai::jwp_delivery::corpus::{generate, run, vector_count, verdict};
fn main() {
let a: Vec<String> = std::env::args().collect();
match (a.get(1).map(String::as_str), a.get(2)) {
(Some("gen"), Some(d)) => {
let p = Path::new(d).join("vectors.json");
if let Err(e) = std::fs::create_dir_all(d).and_then(|_| std::fs::write(&p, generate())) {
eprintln!("write {}: {e}", p.display());
std::process::exit(1);
}
println!("wrote {}", p.display());
}
(Some("verify"), Some(d)) => std::process::exit(verify(Path::new(d))),
(Some("verdicts"), Some(f)) => std::process::exit(verdicts(Path::new(f))),
_ => {
eprintln!("usage: wai_jwp_vectors gen|verify <corpus-dir> | verdicts <file>");
std::process::exit(2);
}
}
}
fn verdicts(file: &Path) -> i32 {
let doc = match std::fs::read_to_string(file) {
Ok(s) => s,
Err(e) => {
eprintln!("read {}: {e}", file.display());
return 1;
}
};
let Ok(d) = serde_json::from_str::<serde_json::Value>(&doc) else {
eprintln!("{}: not JSON", file.display());
return 1;
};
for v in d.get("vectors").and_then(|v| v.as_array()).into_iter().flatten() {
let name = v.get("name").and_then(|n| n.as_str()).unwrap_or("?");
println!("{name}\t{}", verdict(v).unwrap_or_else(|_| "error".into()));
}
0
}
fn verify(dir: &Path) -> i32 {
let mut fail = 0usize;
let path = dir.join("vectors.json");
// `.gitattributes` pins LF; a CRLF working tree changes only line breaks.
let pinned = match std::fs::read_to_string(&path) {
Ok(s) => s.replace("\r\n", "\n"),
Err(e) => {
println!("FAIL {}: {e}", path.display());
return 1;
}
};
let same = pinned == generate();
println!("{} pinned corpus regenerates byte-identically", if same { "PASS" } else { "FAIL" });
fail += (!same) as usize;
match run(&pinned) {
Ok(results) => {
if results.len() != vector_count() {
println!("FAIL {}: {} vectors, expected {}", path.display(), results.len(), vector_count());
fail += 1;
}
for (name, want, got) in results {
let ok = want == got;
println!("{} {name} expected={want} got={got}", if ok { "PASS" } else { "FAIL" });
fail += (!ok) as usize;
}
}
Err(e) => {
println!("FAIL {}: {e}", path.display());
fail += 1;
}
}
if fail == 0 {
println!("all vectors pass");
0
} else {
println!("{fail} failure(s)");
1
}
}

View file

@ -6,7 +6,8 @@
//! idle baseline, and emits a signed receipt carrying that **measured**
//! energy — a portable receipt with real joules, end to end.
//!
//! wai_meter <file.whap | file.w4ds | file.wscr | file.wflm> [receipt_out.json]
//! wai_meter <file.whap | file.w4ds | file.wscr | file.wflm | file.wrel>
//! [receipt_out.json [report_out.json]] [--no-report] [--previous-report <report.json>]
//!
//! World classes (worlds / state-feeds / avatars) are metered by
//! `wai_world_meter`. A linear film embeds a sealed world but reconstructs a
@ -17,12 +18,22 @@
//! `wai::energy_burst`); it never fabricates a number — run it on a quiet box
//! for a clean one. A usable figure is sealed with its acquisition class,
//! `OnChipCounter` with a declared uncertainty (IOReport is the SoC's own
//! counter); without `macmon`, or when the §4 ratio or the §2.3 uncertainty
//! cannot support a figure, the receipt is sealed UNMETERED (`0`, no class).
//! counter); without `macmon`, or when the reader's rail was not live (§3.1),
//! or the §4 ratio or the §2.3 uncertainty cannot support a figure, the receipt
//! is sealed UNMETERED: `joules_micro` carries the unmetered marker `0`, with no
//! class, never a figure of zero joules.
//!
//! Beside a measured figure it writes a measurement report (energy-measurement
//! §6): a `measurement` claim, signed with the receipt's key, carrying the
//! bracket's values, the build and the power reader — beside the receipt
//! (`<receipt>.report.json`), at the path given, or to standard output when no
//! receipt path is given. `--no-report` turns it off, and says so.
use ed25519_dalek::SigningKey;
use wai::energy_burst::measure_with_macmon;
use wai::energy_burst::{measure_with_macmon, power_reader};
use wai::energy_class::{CounterFiltering, EnergyProvenance};
use wai::energy_figure::{meter_report, EnergyFigures};
use wai::energy_report::BracketValues;
use wai::film::{Film, Reel};
use wai::haptic::{HapticReceipt, HapticSignal};
use wai::score::{Score, ScoreReceipt};
@ -32,11 +43,16 @@ const SIGNER_SEED: u8 = 0x6d; // 'm' for meter
const SIGNER_ID: &str = "did:web:wai.transaction.science#wai_meter";
fn main() {
let args: Vec<String> = std::env::args().collect();
let report_args = meter_report::parse_args_or_exit(&std::env::args().collect::<Vec<_>>());
let args = &report_args.positional;
let Some(path) = args.get(1) else {
eprintln!("usage: wai_meter <file.whap | file.w4ds | file.wscr | file.wflm | file.wrel> [receipt_out.json]");
eprintln!(
"usage: wai_meter <file.whap | file.w4ds | file.wscr | file.wflm | file.wrel> \
[receipt_out.json [report_out.json]] [--no-report] [--previous-report <report.json>]"
);
std::process::exit(2);
};
let out = Out { args: &report_args };
let bytes = std::fs::read(path).expect("read file");
let signer = SigningKey::from_bytes(&[SIGNER_SEED; 32]);
@ -44,85 +60,85 @@ fn main() {
let sig = HapticSignal::from_bytes(&bytes).expect("parse whap");
let work = sig.contract.duration * sig.contract.channels as u64;
eprintln!("metering {path} (haptic: {} samples × {} ch)", sig.contract.duration, sig.contract.channels);
let (uj, class) = meter("reconstruct", &sig, work, "samples×ch", |s| {
let (uj, class, values) = meter("reconstruct", &sig, work, "samples×ch", |s| {
for c in 0..s.channels.len() {
std::hint::black_box(s.reconstruct_channel(c));
}
});
if let Some(out) = args.get(2) {
{
let r = HapticReceipt::build(&signer, SIGNER_ID, sig.signal_hash(), 0, sig.channel_hashes(), work, uj, None);
let r = match class {
Some(c) => r.with_energy_class(&signer, c).expect("a usable figure is a valid label"),
None => r,
};
assert!(r.verify(), "emitted receipt must verify");
emit(out, &r.to_json(), r.energy_provenance.is_some(), work);
out.emit(&r, &r.to_json(), r.energy_provenance.is_some(), work, values, &signer);
}
} else if path.ends_with(".w4ds") {
let clip = Splat4dClipData::from_bytes(&bytes).expect("parse w4ds");
let work = clip.contract.n_frames * clip.contract.n_gaussians as u64;
eprintln!("metering {path} (4D splat: {} frames × {} Gaussians)", clip.contract.n_frames, clip.contract.n_gaussians);
let (uj, class) = meter("reconstruct", &clip, work, "frames×gaussians", |c| {
let (uj, class, values) = meter("reconstruct", &clip, work, "frames×gaussians", |c| {
for f in 0..c.contract.n_frames {
std::hint::black_box(c.frame_hash(f));
}
});
if let Some(out) = args.get(2) {
{
let r = Splat4dReceipt::build(&signer, SIGNER_ID, clip.clip_hash(), 0, clip.frame_hashes(), work, uj, None);
let r = match class {
Some(c) => r.with_energy_class(&signer, c).expect("a usable figure is a valid label"),
None => r,
};
assert!(r.verify(), "emitted receipt must verify");
emit(out, &r.to_json(), r.energy_provenance.is_some(), work);
out.emit(&r, &r.to_json(), r.energy_provenance.is_some(), work, values, &signer);
}
} else if path.ends_with(".wscr") {
let score = Score::from_bytes(&bytes).expect("parse wscr");
let work = score.contract.duration * score.notes.len() as u64;
eprintln!("metering {path} (score: {} notes → {} samples)", score.notes.len(), score.contract.duration);
let (uj, class) = meter("reconstruct", &score, work, "samples×notes", |s| {
let (uj, class, values) = meter("reconstruct", &score, work, "samples×notes", |s| {
std::hint::black_box(s.mixdown());
});
if let Some(out) = args.get(2) {
{
let r = ScoreReceipt::build(&signer, SIGNER_ID, score.score_hash(), 0, score.segment_hashes(), work, uj, None);
let r = match class {
Some(c) => r.with_energy_class(&signer, c).expect("a usable figure is a valid label"),
None => r,
};
assert!(r.verify(), "emitted receipt must verify");
emit(out, &r.to_json(), r.energy_provenance.is_some(), work);
out.emit(&r, &r.to_json(), r.energy_provenance.is_some(), work, values, &signer);
}
} else if path.ends_with(".wflm") {
let film = Film::from_bytes(&bytes).expect("parse wflm");
let work = film.contract.frames;
eprintln!("metering {path} (film: {} frames × ticks_per_frame {})", film.contract.frames, film.contract.ticks_per_frame);
let (uj, class) = meter("reconstruct", &film, work, "frames", |f| {
let (uj, class, values) = meter("reconstruct", &film, work, "frames", |f| {
std::hint::black_box(f.frame_hashes().expect("frame hashes"));
});
if let Some(out) = args.get(2) {
{
let r = film.receipt(&signer, SIGNER_ID, 0, uj, None).expect("receipt");
let r = match class {
Some(c) => r.with_energy_class(&signer, c).expect("a usable figure is a valid label"),
None => r,
};
assert!(r.verify(), "emitted receipt must verify");
emit(out, &r.to_json(), r.energy_provenance.is_some(), work);
out.emit(&r, &r.to_json(), r.energy_provenance.is_some(), work, values, &signer);
}
} else if path.ends_with(".wrel") {
let reel = Reel::from_bytes(&bytes).expect("parse wrel");
let work = reel.total_frames();
eprintln!("metering {path} (reel: {} shots → {} frames)", reel.shots.len(), work);
let (uj, class) = meter("reconstruct", &reel, work, "frames", |r| {
let (uj, class, values) = meter("reconstruct", &reel, work, "frames", |r| {
std::hint::black_box(r.frame_hashes().expect("frame hashes"));
});
if let Some(out) = args.get(2) {
{
let r = reel.receipt(&signer, SIGNER_ID, 0, uj, None).expect("receipt");
let r = match class {
Some(c) => r.with_energy_class(&signer, c).expect("a usable figure is a valid label"),
None => r,
};
assert!(r.verify(), "emitted receipt must verify");
emit(out, &r.to_json(), r.energy_provenance.is_some(), work);
out.emit(&r, &r.to_json(), r.energy_provenance.is_some(), work, values, &signer);
}
} else {
eprintln!("unrecognized file (want .whap, .w4ds, .wscr, .wflm, or .wrel); world classes → wai_world_meter");
@ -132,19 +148,21 @@ fn main() {
/// Run the §3 protocol on `item` (`energy_burst`), print the report §6
/// requires, and return what the receipt seals: `(joules_micro, Some(class))`
/// for a usable on-chip figure, `(0, None)` — unmetered — otherwise.
fn meter<T, W>(verb: &str, item: &T, work: u64, unit: &str, one: W) -> (u64, Option<EnergyProvenance>)
/// for a usable on-chip figure, `(0, None)` — the unmetered marker — otherwise;
/// and the bracket's values for the measurement report.
fn meter<T, W>(verb: &str, item: &T, work: u64, unit: &str, one: W) -> (u64, Option<EnergyProvenance>, Option<BracketValues>)
where
T: Clone + Send + 'static,
W: Fn(&T) + Send + Sync + 'static,
{
let Some(b) = measure_with_macmon(verb, item, one) else {
eprintln!(
"macmon unavailable or too few samples; sealing UNMETERED (joules_micro = 0). \
"macmon unavailable or too few samples; sealing UNMETERED (joules_micro carries the unmetered marker 0). \
Install `brew install macmon` (the non-sudo IOReport reader) for a measured figure."
);
return (0, None);
return (0, None, None);
};
let values = b.values();
println!("== wai_meter — measured CPU energy (IOReport via macmon, no sudo) ==");
print!("{}", b.report(verb));
if let Some(per) = b.per_unit_uj() {
@ -154,7 +172,7 @@ where
match b.on_chip_figure(CounterFiltering::Unknown) {
Some((uj, class)) => {
println!("joules_micro (receipt, MEASURED): {uj}");
(uj, Some(class))
(uj, Some(class), values)
}
None => {
eprintln!(
@ -163,13 +181,35 @@ where
b.ratio(),
b.unsealed_reason().unwrap_or("refused")
);
(0, None)
(0, None, values)
}
}
}
fn emit(out: &str, json: &str, measured: bool, work: u64) {
std::fs::write(out, json).expect("write receipt");
let tag = if measured { "MEASURED, OnChipCounter" } else { "UNMETERED" };
println!("receipt : {out} (verified, joules_micro {tag}, work {work})");
/// Where a sealed receipt and its report go.
struct Out<'a> {
args: &'a meter_report::ReportArgs,
}
impl Out<'_> {
/// Write the receipt when a path was given, and its report by default.
fn emit<R: EnergyFigures>(&self, r: &R, json: &str, measured: bool, work: u64, values: Option<BracketValues>, signer: &SigningKey) {
let receipt_out = self.args.positional.get(2).map(String::as_str);
if let Some(out) = receipt_out {
std::fs::write(out, json).expect("write receipt");
let tag = if measured { "MEASURED, OnChipCounter" } else { "UNMETERED" };
println!("receipt : {out} (verified, joules_micro {tag}, work {work})");
}
meter_report::write(
self.args,
r,
None,
values,
power_reader(),
signer,
SIGNER_ID,
receipt_out,
self.args.positional.get(3).map(String::as_str),
);
}
}

View file

@ -4,13 +4,20 @@
//! and a signed, energy-accounted lineage: origin → PNG master → published
//! JPEG, with the **real measured CPU energy** of each encode (IOReport
//! via macmon, no sudo), each figure labelled `OnChipCounter` with its
//! declared uncertainty. An encode whose measurement the §4 ratio or the §2.3
//! uncertainty cannot support is recorded unmetered (`0`, no label).
//! declared uncertainty. An encode whose measurement the reader's liveness
//! (§3.1), the §4 ratio or the §2.3 uncertainty cannot support is recorded
//! unmetered: the step's `joules_micro` carries the unmetered marker `0`, with
//! no label, never a figure of zero joules.
//!
//! wai_prov <out_dir>
//! wai_prov <out_dir> [--no-report] [--previous-report <file>]
//!
//! Writes `<out_dir>/captured.jpeg.wai` (the content-addressed asset) and
//! `<out_dir>/captured.receipt.json` (the signed provenance). The asset is
//! Writes `<out_dir>/captured.jpeg.wai` (the content-addressed asset),
//! `<out_dir>/captured.receipt.json` (the signed provenance), and for each
//! measured step `<out_dir>/captured.step<i>.report.json`: its measurement
//! report (energy-measurement §6), a `measurement` claim signed with the
//! receipt's key naming the step by its index, the first chained to
//! `--previous-report` when it is given and the second to the first.
//! `--no-report` turns the reports off, and says so. The asset is
//! synthesized deterministically here — WAI is asset-agnostic; the lineage
//! mechanism is identical whether the origin is a camera or a synthesizer,
//! and that is stated plainly. The point is provenance for bytes WAI
@ -19,8 +26,10 @@
use ed25519_dalek::SigningKey;
use std::path::Path;
use wai::codecs::image::{jpeg_encode, png_encode};
use wai::energy_burst::measure_with_macmon;
use wai::energy_burst::{measure_with_macmon, power_reader};
use wai::energy_class::{CounterFiltering, EnergyProvenance};
use wai::energy_figure::meter_report;
use wai::energy_report::BracketValues;
use wai::provenance::{AssetReceipt, ProvStep};
use wai::world::asset_content_hash;
use wai::{Conditioning, Manifest, ModelRequirement, Wai};
@ -29,7 +38,8 @@ const W: u32 = 512;
const H: u32 = 512;
fn main() {
let args: Vec<String> = std::env::args().collect();
let report_args = meter_report::parse_args_or_exit(&std::env::args().collect::<Vec<_>>());
let args = &report_args.positional;
let dir = Path::new(args.get(1).map(String::as_str).unwrap_or("."));
std::fs::create_dir_all(dir).expect("mkdir");
@ -40,14 +50,14 @@ fn main() {
let raw_hash = blake3::hash(&rgb).into();
// step 1: lossless PNG master — measured.
let png_m = measure("encode.png", &rgb, |r| {
let (png_m, png_v) = measure("encode.png", &rgb, |r| {
let _ = std::hint::black_box(png_encode(r, H, W));
});
let png = png_encode(&rgb, H, W).expect("png");
let png_hash: [u8; 32] = blake3::hash(&png).into();
// step 2: published JPEG (from the master pixels) — measured.
let jpeg_m = measure("encode.jpeg", &rgb, |r| {
let (jpeg_m, jpeg_v) = measure("encode.jpeg", &rgb, |r| {
let _ = std::hint::black_box(jpeg_encode(r, H, W, 90));
});
let jpeg = jpeg_encode(&rgb, H, W, 90).expect("jpeg");
@ -74,9 +84,10 @@ fn main() {
];
let signer = SigningKey::from_bytes(&[0x70u8; 32]); // 'p' for prov
let signer_id = "did:web:wai.transaction.science#wai_prov";
let receipt = AssetReceipt::build(
&signer,
"did:web:wai.transaction.science#wai_prov",
signer_id,
asset_hash,
"image",
"wai.image.jpeg",
@ -91,12 +102,28 @@ fn main() {
std::fs::write(dir.join("captured.receipt.json"), receipt.to_json()).expect("write receipt");
println!("captured.jpeg.wai : {} B envelope (wai.image.jpeg), asset {}…", envelope.len(), &hex(&asset_hash)[..16]);
println!("captured.receipt.json : {} steps, total {} µJ, receipt {}…", receipt.steps.len(), receipt.total_joules_micro, &hex(&receipt.receipt_hash())[..16]);
// The origin step is never metered, so the total covers the measured steps
// only, and an all-unmetered lineage is UNMETERED, never 0 µJ.
println!("captured.receipt.json : {} steps, total {}, receipt {}…", receipt.steps.len(), receipt.total_label(), &hex(&receipt.receipt_hash())[..16]);
let shown = |m: &Option<(u64, EnergyProvenance)>| match m {
Some((uj, c)) => format!("{uj} µJ ({})", c.label()),
None => "UNMETERED".to_string(),
};
println!(" encode.png : {} encode.jpeg : {}", shown(&png_m), shown(&jpeg_m));
// One report per measured step, by its index: the first chained to the
// previous run's report when `--previous-report` names one, each later one
// to the report before it; a step recorded unmetered gets none, and says why.
let mut chain = report_args.clone();
for (index, values) in [(1u64, png_v), (2, jpeg_v)] {
let path = dir.join(format!("captured.step{index}.report.json"));
let path_s = path.to_string_lossy().into_owned();
if meter_report::write(&chain, &receipt, Some(index), values, power_reader(), &signer, signer_id, None, Some(&path_s))
.is_some()
{
chain.previous_report = Some(path_s);
}
}
}
/// Deterministic 512×512 RGB: radial rings over a diagonal gradient.
@ -121,14 +148,15 @@ fn synth_rgb() -> Vec<u8> {
/// One encode's figure and class, measured by the §3 protocol
/// (`energy_burst`): `Some((joules_micro, OnChipCounter))` for a usable
/// figure, `None` — unmetered — otherwise.
fn measure<F>(label: &str, rgb: &[u8], op: F) -> Option<(u64, EnergyProvenance)>
/// figure, `None` — the unmetered marker — otherwise; and the bracket's values
/// for the step's measurement report.
fn measure<F>(label: &str, rgb: &[u8], op: F) -> (Option<(u64, EnergyProvenance)>, Option<BracketValues>)
where
F: Fn(&Vec<u8>) + Send + Sync + 'static,
{
let Some(b) = measure_with_macmon(label, &rgb.to_vec(), op) else {
eprintln!("macmon unavailable; recording {label} UNMETERED. Install `brew install macmon` (non-sudo IOReport reader).");
return None;
return (None, None);
};
eprint!("{}", b.report(label));
b.warn_if_unpublishable();
@ -140,7 +168,7 @@ where
b.unsealed_reason().unwrap_or("refused")
);
}
m
(m, b.values())
}
fn hex(b: &[u8]) -> String {

View file

@ -9,7 +9,8 @@
//! idle baseline, and reports the **marginal** CPU energy attributable to the
//! simulation.
//!
//! wai_quantum_meter <circuit.wqc> [receipt_out.json]
//! wai_quantum_meter <circuit.wqc> [receipt_out.json [report_out.json]]
//! [--no-report] [--previous-report <report.json>]
//!
//! Output: measured microjoules per reconstruction — the honest figure that
//! fills a receipt's `joules_micro`. With a second argument the meter emits a
@ -25,23 +26,30 @@
//! quiet box for a clean number; it never fabricates one. A usable figure is
//! sealed with its acquisition class — `OnChipCounter` with a declared
//! uncertainty, as a `quantum_energy::Labelled` receipt; without `macmon`, or
//! when the §4 ratio or the §2.3 uncertainty cannot support a figure, the
//! receipt is sealed UNMETERED (`0`, unlabelled). Because a
//! when the reader's rail was not live (§3.1), or the §4 ratio or the §2.3
//! uncertainty cannot support a figure, the receipt is sealed UNMETERED:
//! `joules_micro` carries the unmetered marker `0`, unlabelled, never a figure
//! of zero joules. Beside a measured figure it writes a measurement report
//! (energy-measurement §6), a `measurement` claim signed with the receipt's key —
//! beside the receipt (`<receipt>.report.json`), at the path given, or to
//! standard output; `--no-report` turns it off, and says so. Because a
//! quantum computation carries no speedup here, this energy — and the
//! exponential `work_amp_updates = n_ops·2^n` beside it — is precisely what the
//! receipt makes auditable. No quantum hardware is involved.
use wai::energy_burst::measure_with_macmon;
use wai::energy_burst::{measure_with_macmon, power_reader};
use wai::energy_figure::meter_report;
use wai::energy_class::{to_quantum_class, CounterFiltering, EnergyProvenance};
use wai::quantum::from_wqc;
use wai::quantum_energy::Labelled;
use wai::quantum_energy::{Labelled, MaybeLabelled};
use wai::quantum_receipt::{work_amp_updates, QuantumReceipt};
use wai_quantum::ed25519_dalek::SigningKey;
fn main() {
let args: Vec<String> = std::env::args().collect();
let report_args = meter_report::parse_args_or_exit(&std::env::args().collect::<Vec<_>>());
let args = &report_args.positional;
let Some(path) = args.get(1) else {
eprintln!("usage: wai_quantum_meter <circuit.wqc> [receipt_out.json]");
eprintln!("usage: wai_quantum_meter <circuit.wqc> [receipt_out.json [report_out.json]] [--no-report] [--previous-report <report.json>]");
std::process::exit(2);
};
@ -61,18 +69,20 @@ fn main() {
// The §3 protocol (energy_burst): an idle bracket, then reconstructions on
// several threads released together from a start barrier, counted per
// reconstruction — never per batch — with a busy bracket over them.
let mut values = None;
let measured: Option<(u64, EnergyProvenance)> = match measure_with_macmon("reconstruction", &circuit, |c| {
let _ = c.simulate();
}) {
None => {
eprintln!(
"macmon unavailable or too few samples; emitting an UNMETERED receipt \
(joules_micro = 0). Install `brew install macmon` (the non-sudo \
(joules_micro carries the unmetered marker 0). Install `brew install macmon` (the non-sudo \
IOReport reader) for a measured figure."
);
None
}
Some(b) => {
values = b.values();
println!("== wai_quantum_meter — measured CPU energy (IOReport via macmon, no sudo) ==");
print!("{}", b.report("reconstruction"));
if let Some(per) = b.per_unit_uj() {
@ -95,27 +105,29 @@ fn main() {
};
let joules_micro = measured.as_ref().map_or(0, |(uj, _)| *uj);
// Optional: emit a receipt carrying the measured (or unmetered) energy.
if let Some(out) = args.get(2) {
// The receipt carrying the measured (or unmetered) energy, written when a
// path is given; its report is written whether or not it is.
{
// A native sink signs with its own identity key; this metering tool
// uses a fixed, clearly-labeled demo signer.
let signer = SigningKey::from_bytes(&[0x71u8; 32]); // 'q' for quantum-meter
let signer_id = "did:web:wai.transaction.science#wai_quantum_meter";
let receipt = QuantumReceipt::seal(
&signer,
"did:web:wai.transaction.science#wai_quantum_meter",
signer_id,
&circuit,
measure,
joules_micro,
None,
)
.expect("seal receipt");
let (json, tag) = match &measured {
let (sealed, tag) = match &measured {
Some((_, class)) => {
let class = to_quantum_class(class).expect("an on-chip class has a quantum counterpart");
let l = Labelled::seal(receipt, class, &signer).expect("a usable figure is a valid label");
assert!(l.verify(), "emitted receipt must verify");
assert!(l.verify_reconstruction(&circuit), "emitted receipt must re-simulate to the same statevector");
(l.to_json(), "MEASURED, OnChipCounter")
(MaybeLabelled::Labelled(l), "MEASURED, OnChipCounter")
}
None => {
assert!(receipt.verify(), "emitted receipt must verify");
@ -123,10 +135,23 @@ fn main() {
receipt.verify_reconstruction(&circuit),
"emitted receipt must re-simulate to the same statevector"
);
(receipt.to_json(), "UNMETERED")
(MaybeLabelled::Unlabelled(receipt), "UNMETERED")
}
};
std::fs::write(out, json).expect("write receipt");
println!("receipt : {out} (verified, joules_micro {tag}, work {work})");
if let Some(out) = args.get(2) {
std::fs::write(out, sealed.to_json()).expect("write receipt");
println!("receipt : {out} (verified, joules_micro {tag}, work {work})");
}
meter_report::write(
&report_args,
&sealed,
None,
values,
power_reader(),
&signer,
signer_id,
args.get(2).map(String::as_str),
args.get(3).map(String::as_str),
);
}
}

View file

@ -8,37 +8,48 @@
//! decode burst against an idle baseline, and reports the **marginal** CPU
//! energy attributable to the decode.
//!
//! wai_video_meter <bitstream.json> [receipt_out.json]
//! wai_video_meter <bitstream.json> [receipt_out.json [report_out.json]]
//! [--no-report] [--previous-report <report.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.
//! matrix. So of everything the emitted receipt says, only the measurement —
//! `joules_micro` and the record of how it was obtained — rests on trusting this
//! machine; the result and the work can be re-derived from the codestream by
//! anyone, anywhere.
//!
//! Beside a measured figure it writes a measurement report
//! (energy-measurement §6): a `measurement` claim, signed with the receipt's key,
//! carrying the bracket's values, the build and the power reader. It goes beside
//! the receipt (`<receipt>.report.json`), to the path given, or to standard output
//! when no receipt path is given; `--no-report` turns it off, and says so.
//!
//! 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 × window ÷ the decodes the
//! window paid for; see `wai::energy_burst`). Run it on a quiet box for a clean
//! number; it never fabricates one. Without `macmon`, or when the §4 ratio or
//! the §2.3 uncertainty cannot support a figure, 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".
//! number; it never fabricates one. Without `macmon`, or when the reader's rail
//! was not live (§3.1), or the §4 ratio or the §2.3 uncertainty cannot support a
//! figure, it emits an UNMETERED receipt — `joules_micro` carries the unmetered
//! marker `0`, never a figure of zero joules — and that receipt reports no rate
//! at all: an absent measurement must never be readable as "this decode was
//! free".
use ed25519_dalek::SigningKey;
use wai::energy_burst::measure_with_macmon;
use wai::energy_burst::{measure_with_macmon, power_reader};
use wai::energy_figure::meter_report;
use wai::energy_class::CounterFiltering;
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 report_args = meter_report::parse_args_or_exit(&std::env::args().collect::<Vec<_>>());
let args = &report_args.positional;
let Some(path) = args.get(1) else {
eprintln!("usage: wai_video_meter <bitstream.json> [receipt_out.json]");
eprintln!("usage: wai_video_meter <bitstream.json> [receipt_out.json [report_out.json]] [--no-report] [--previous-report <report.json>]");
std::process::exit(2);
};
@ -65,6 +76,7 @@ fn main() {
// measurement that rounds to zero, or that the §4 ratio or the §2.3
// uncertainty cannot support, is a failure to measure, and it is reported
// as one.
let mut values = None;
let measured: Option<(u64, Provenance)> =
match measure_with_macmon("decode", &bs, |b| {
let _ = decode(b);
@ -72,12 +84,13 @@ fn main() {
None => {
eprintln!(
"macmon unavailable or too few samples; emitting an UNMETERED receipt \
(joules_micro = 0). Install `brew install macmon` (the non-sudo \
(joules_micro carries the unmetered marker 0). Install `brew install macmon` (the non-sudo \
IOReport reader) for a measured figure."
);
None
}
Some(b) => {
values = b.values();
println!("== wai_video_meter — measured CPU energy (IOReport via macmon, no sudo) ==");
print!("{}", b.report("decode"));
match b.per_unit_uj() {
@ -116,11 +129,13 @@ fn main() {
}
};
// Optional: emit a receipt carrying the measured (or unmetered) energy.
if let Some(out) = args.get(2) {
// The receipt carrying the measured (or unmetered) energy, written when a
// path is given; its report is written whether or not it is.
{
// 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 signer_id = "did:web:wai.transaction.science#wai_video_meter";
let (uj, class) = match &measured {
Some((uj, class)) => (*uj, Some(class.clone())),
// With no reader present there is no measurement at all, and the
@ -130,7 +145,7 @@ fn main() {
};
let receipt = VideoReceipt::seal_decode(
&signer,
"did:web:wai.transaction.science#wai_video_meter",
signer_id,
&bytes,
&bs,
uj,
@ -143,8 +158,21 @@ fn main() {
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})");
if let Some(out) = args.get(2) {
std::fs::write(out, receipt.to_json()).expect("write receipt");
println!("receipt : {out} (verified, joules_micro {tag}, work {work})");
}
meter_report::write(
&report_args,
&receipt,
None,
values,
power_reader(),
&signer,
signer_id,
args.get(2).map(String::as_str),
args.get(3).map(String::as_str),
);
}
}

View file

@ -8,7 +8,8 @@
//! tight replay burst against an idle baseline, and reports the
//! **marginal** CPU energy attributable to the replay.
//!
//! wai_world_meter <sealed.wwld> [receipt_out.json]
//! wai_world_meter <sealed.wwld> [receipt_out.json [report_out.json]]
//! [--no-report] [--previous-report <report.json>]
//!
//! Output: measured microjoules per replay (and per tick) — the honest
//! figure that fills a receipt's `joules_micro`, replacing a browser's
@ -23,11 +24,20 @@
//! for; see `wai::energy_burst`). Run it on a quiet box for a clean number;
//! it never fabricates one. A usable figure is sealed with its acquisition
//! class, `OnChipCounter` with a declared uncertainty; without `macmon`, or
//! when the §4 ratio or the §2.3 uncertainty cannot support a figure, the
//! receipt is sealed UNMETERED (`0`, no class).
//! when the reader's rail was not live (§3.1), or the §4 ratio or the §2.3
//! uncertainty cannot support a figure, the receipt is sealed UNMETERED:
//! `joules_micro` carries the unmetered marker `0`, with no class, never a
//! figure of zero joules.
//!
//! Beside a measured figure it writes a measurement report (energy-measurement
//! §6): a `measurement` claim, signed with the receipt's key, carrying the
//! bracket's values, the build and the power reader — beside the receipt
//! (`<receipt>.report.json`), at the path given, or to standard output when no
//! receipt path is given. `--no-report` turns it off, and says so.
use ed25519_dalek::SigningKey;
use wai::energy_burst::measure_with_macmon;
use wai::energy_burst::{measure_with_macmon, power_reader};
use wai::energy_figure::meter_report;
use wai::energy_class::CounterFiltering;
use wai::world::feed::CAP_SIM_FEED0;
use wai::world::sim::{CAP_SIM_BALLISTIC0, CAP_SIM_KINEMATIC0};
@ -39,9 +49,10 @@ use wai::world::{
};
fn main() {
let args: Vec<String> = std::env::args().collect();
let report_args = meter_report::parse_args_or_exit(&std::env::args().collect::<Vec<_>>());
let args = &report_args.positional;
let Some(path) = args.get(1) else {
eprintln!("usage: wai_world_meter <sealed.wwld> [receipt_out.json]");
eprintln!("usage: wai_world_meter <sealed.wwld> [receipt_out.json [report_out.json]] [--no-report] [--previous-report <report.json>]");
std::process::exit(2);
};
@ -60,17 +71,19 @@ fn main() {
// threads released together from a start barrier, counted per replay —
// never per batch, which would collapse the count for a heavy world — with
// a busy bracket over them.
let mut values = None;
let (joules_micro, class) = match measure_with_macmon("replay", &container, |c| {
let _ = run_sealed_dispatch(c);
}) {
None => {
eprintln!(
"macmon unavailable or too few samples; sealing UNMETERED (joules_micro = 0). \
"macmon unavailable or too few samples; sealing UNMETERED (joules_micro carries the unmetered marker 0). \
Install `brew install macmon` (the non-sudo IOReport reader this meter samples)."
);
(0, None)
}
Some(b) => {
values = b.values();
println!("== wai_world_meter — measured CPU energy (IOReport via macmon, no sudo) ==");
print!("{}", b.report("replay"));
if let Some(per_replay_uj) = b.per_unit_uj() {
@ -96,16 +109,18 @@ fn main() {
}
};
// Optional: emit a §8 receipt carrying the measured energy.
if let Some(out) = args.get(2) {
// The §8 receipt carrying the measured energy, written when a path is
// given; its report is written whether or not it is.
{
let (checkpoints, work_steps) = checkpoints_and_work(&container, final_tick);
// A native sink signs with its own identity key; this metering
// tool uses a fixed, clearly-labeled demo signer.
let signer = SigningKey::from_bytes(&[0x6du8; 32]); // 'm' for meter
let wh = world_hash(&container.contract_bytes, &container.graph_bytes);
let signer_id = "did:web:wai.transaction.science#wai_world_meter";
let receipt = SessionReceipt::build(
&signer,
"did:web:wai.transaction.science#wai_world_meter",
signer_id,
wh, 0, &container.ops, &checkpoints, joules_micro, work_steps, None,
);
let receipt = match class {
@ -113,9 +128,22 @@ fn main() {
None => receipt,
};
assert!(receipt.verify(), "emitted receipt must verify");
std::fs::write(out, receipt.to_json()).expect("write receipt");
let tag = if receipt.energy_provenance.is_some() { "MEASURED, OnChipCounter" } else { "UNMETERED" };
println!("receipt : {out} (verified, joules_micro {tag}, work_steps {work_steps})");
if let Some(out) = args.get(2) {
std::fs::write(out, receipt.to_json()).expect("write receipt");
let tag = if receipt.energy_provenance.is_some() { "MEASURED, OnChipCounter" } else { "UNMETERED" };
println!("receipt : {out} (verified, joules_micro {tag}, work_steps {work_steps})");
}
meter_report::write(
&report_args,
&receipt,
None,
values,
power_reader(),
&signer,
signer_id,
args.get(2).map(String::as_str),
args.get(3).map(String::as_str),
);
}
}

View file

@ -0,0 +1,36 @@
//! What `build.rs` records about the build for a measurement report
//! (`extensions/energy-measurement.md` §5, §8). This file is compiled twice:
//! into the crate, and into `build.rs` through `#[path]`, so the mapping the
//! build applies is the one the crate's tests check. It uses nothing but
//! `core`.
/// The build-profile label for cargo's `PROFILE` and `OPT_LEVEL`: `release` for
/// a build optimised as shipped — cargo's `release` profile at `opt-level` 2
/// or 3 — and `debug` for any other, including a `release` profile turned down
/// to a lower level and the size levels `s` and `z`, which are not how this
/// crate ships. The label is a statement about optimisation, not about debug
/// assertions: a release build with assertions on is still `release`, and a
/// dev build with them off is still `debug`.
pub fn profile_label(profile: &str, opt_level: &str) -> &'static str {
match (profile, opt_level) {
("release", "2" | "3") => "release",
_ => "debug",
}
}
#[cfg(test)]
mod tests {
use super::profile_label;
#[test]
fn release_means_the_release_profile_optimised() {
assert_eq!(profile_label("release", "3"), "release");
assert_eq!(profile_label("release", "2"), "release");
assert_eq!(profile_label("release", "1"), "debug");
assert_eq!(profile_label("release", "0"), "debug");
assert_eq!(profile_label("release", "s"), "debug");
assert_eq!(profile_label("debug", "3"), "debug", "a dev profile turned up is not the shipped build");
assert_eq!(profile_label("debug", "0"), "debug");
assert_eq!(profile_label("", ""), "debug");
}
}

View file

@ -303,7 +303,9 @@ impl RecoveredBindings {
/// Does the sealed claim describe the pixels a verifier just decoded?
///
/// This is the whole construction in one call: re-decode the asset, pass the
/// canonical RGB8 here, and everything in the claim is confirmed except the joules.
/// canonical RGB8 here, and everything in the claim is confirmed except the
/// measurement: the joules and the fields that describe how they were obtained
/// (energy-binding §3).
pub fn describes(&self, rgb: &[u8]) -> bool {
use sha2::{Digest, Sha256};
let mut hex = String::with_capacity(64);

File diff suppressed because it is too large Load diff

View file

@ -23,10 +23,11 @@
//! [`crate::reconstruction_binding::ReconstructionBinding`] commits to. This is the
//! whole point: because a WAI reconstruction is byte-exact and reproducible on every
//! architecture, a verifier can re-decode, confirm it holds the same pixels the figure
//! is about, and be left with **exactly one** unverifiable term — the joules. An
//! is about, and be left with the measurement as the unverifiable part — the joules
//! and the fields that describe how they were obtained (energy-binding §3). An
//! energy claim about an output nobody can reproduce is not checkable at all; an
//! energy claim about an output anyone can reproduce is checkable except for one
//! number, and knowing precisely which number that is, is the difference.
//! energy claim about an output anyone can reproduce is checkable except for its
//! measurement, and knowing precisely which fields those are is the difference.
//! - **An acquisition class.** `OnChipCounter` / `CalibratedInstrument` /
//! `ModelBased` / `Estimator` (and, read-only, the legacy `HwShunt`) — the family's
//! ladder, stated rather than implied. A counter reading and a modelled constant are
@ -568,6 +569,11 @@ mod tests {
assert!(j2.contains("\"acquisition\":\"CalibratedInstrument\""), "{j2}");
assert!(j2.contains("\"calibrationRef\":\"https://lab.example/certificates/0412\""), "{j2}");
assert!(!j2.contains("counterFiltering"), "an instrument has no counter filter: {j2}");
// The whole bodies, pinned as the refined classes first emitted them, so a
// member that moves, or a byte of evidence that changes, fails here and not
// only a member that disappears. Never edit these to make a test pass.
assert_eq!(j, "{\"reconstructionHash\":\"4dbdc2b2b62cb00749785bc84202236dbc3777d74660611b8e58812f0cfde6c3\",\"alg\":\"sha256\",\"microjoules\":10565,\"acquisition\":\"OnChipCounter\",\"uncertainty\":{\"relativePpm\":30000,\"windowUs\":2000000},\"counterFiltering\":\"Unknown\",\"coverage\":\"Whole\",\"workUnits\":48,\"method\":\"https://transaction.science/wai/extensions/energy-measurement\"}");
assert_eq!(j2, "{\"reconstructionHash\":\"4dbdc2b2b62cb00749785bc84202236dbc3777d74660611b8e58812f0cfde6c3\",\"alg\":\"sha256\",\"microjoules\":10565,\"acquisition\":\"CalibratedInstrument\",\"uncertainty\":{\"relativePpm\":5000,\"windowUs\":60000000},\"calibrationRef\":\"https://lab.example/certificates/0412\",\"coverage\":\"Partial\",\"workUnits\":48,\"method\":\"https://transaction.science/wai/extensions/energy-measurement\"}");
for b in [chip_b, cal_b] {
let v: serde_json::Value = serde_json::from_str(&b.to_json()).expect("valid JSON");

View file

@ -35,16 +35,40 @@
//! declared uncertainty describes that sealed figure: the measurement's own
//! terms (`u_power`, `u_count`) carried across in microjoules, and the ±0.5 µJ
//! rounding (`u_res`, rectangular, `0.5/√3` µJ), divided by the figure sealed
//! ([`crate::energy_class::sealed_figure`]). At a figure of a few microjoules
//! (§2.3, §6.2). At a figure of a few microjoules
//! the rounding is the largest term — 29 % of a 1 µJ figure — and the
//! declaration says so rather than stating the measurement's precision alone.
//!
//! # Sealed from the integers a report carries
//!
//! The figure and its declaration are computed from the bracket's values as a
//! measurement report carries them (§6.2) — sample counts, means rounded to
//! the nearest microwatt, standard deviations rounded up to the next, the
//! window in microseconds — by the exact integer arithmetic of
//! [`crate::energy_report`], not from the unrounded samples. A verifier holding
//! the report then reproduces both numbers exactly. Computed from the samples,
//! a declaration can come out one ppm below what the published integers
//! support.
//!
//! # A live rail
//!
//! A power reader can report a rail that is not being refreshed: a frozen rail
//! reads as a flat zero or as its last value held, and a rail refreshed in
//! batches shows one lump — sometimes published in two steps — in a bracket
//! that otherwise does not move. Either gives a flat idle bracket and a
//! positive marginal, a ratio the §4 rule reads as clean, from no measurement
//! of the work. So both brackets must show the rail live (§3.1,
//! [`crate::energy_report::Liveness`]): advancing — a sample above zero that
//! differs from the one before it — in two consecutive samples, in at least
//! five, in at least four of every five, over at least 300 ms.
//!
//! # What this module refuses to produce
//!
//! A figure is sealed under a hardware class only when §4 and §2.3 allow it:
//! the marginal-to-idle-noise ratio is at least 1, the per-unit figure does
//! not round to zero, and a declared uncertainty can be evaluated. Anything
//! else is a failure to measure, and the caller seals the receipt unmetered
//! A figure is sealed under a hardware class only when §3.1, §4 and §2.3
//! allow it: the reader's rail was live in both brackets, the
//! marginal-to-idle-noise ratio is at least 1, the per-unit figure does not
//! round to zero, and a declared uncertainty can be evaluated. Anything else is
//! a failure to measure, and the caller seals the receipt unmetered
//! ([`Bracket::on_chip_figure`] returns `None`; [`Bracket::unsealed_reason`]
//! says why).
@ -54,9 +78,10 @@ use std::sync::{Arc, Barrier};
use std::time::Instant;
use crate::energy_class::{
bracket_relative_uncertainty, check_resolution, per_unit_relative_uncertainty, sealed_figure, CompletionCount,
CounterFiltering, EnergyProvenance, Uncertainty, WindowOpening, RESOLUTION_UNCERTAINTY_UJ,
bracket_relative_uncertainty, check_resolution, per_unit_relative_uncertainty, CompletionCount, CounterFiltering,
EnergyProvenance, Uncertainty, WindowOpening, RESOLUTION_UNCERTAINTY_UJ,
};
use crate::energy_report::{BracketSummary, BracketValues, BuildRecord, Liveness};
/// Idle-bracket sample count the reference meters use.
const IDLE_SAMPLES: u32 = 10;
@ -75,6 +100,9 @@ pub struct Bracket {
pub busy_w: Vec<f64>,
/// The timed window the busy bracket spans, in seconds.
pub window_s: f64,
/// How long the idle bracket took to sample, in seconds — what §3.1's span
/// rule is checked against for the idle bracket.
pub idle_span_s: f64,
/// Completions, workers, and how the window opened.
pub count: CompletionCount,
}
@ -118,10 +146,14 @@ impl Bracket {
if m.is_nan() { 0.0 } else { m.max(0.0) }
}
/// The §4 attribution-quality ratio: marginal draw over the idle bracket's
/// standard deviation. A flat idle bracket gives an infinite ratio only if
/// the burst actually rose above it; a reader returning nothing but zeros
/// has a flat idle *and* a zero marginal, and that is `0`, not "clean".
/// The §4 attribution-quality ratio, for display: marginal draw over the
/// idle bracket's standard deviation. A flat idle bracket gives an infinite
/// ratio only if the burst actually rose above it; a reader returning
/// nothing but zeros has a flat idle *and* a zero marginal, and that is
/// `0`, not "clean". An infinite ratio is not a clean measurement either:
/// a flat idle is what a frozen rail gives, and whether the rail was live
/// is [`Bracket::rail_live`]'s question. What a receipt seals follows the
/// report's integers ([`Bracket::values`]), not this.
pub fn ratio(&self) -> f64 {
let m = self.marginal_w();
match self.idle_sd() {
@ -170,10 +202,58 @@ impl Bracket {
}
/// The figure a receipt seals: the per-unit estimate rounded to whole
/// microjoules. `None` when it rounds to zero.
/// microjoules, computed from the report's integers ([`Bracket::values`],
/// §6.2). `None` when it rounds to zero or there is nothing to compute it
/// from.
pub fn sealed_uj(&self) -> Option<u64> {
let f = self.per_unit_uj()?.round();
(f.is_finite() && f >= 1.0 && f < u64::MAX as f64).then_some(f as u64)
self.values()?.figure()
}
/// The bracket as a measurement report carries it (§6.2): each bracket's
/// sample count, its mean in microwatts rounded to the nearest (a half up),
/// its sample standard deviation in microwatts rounded up, and the samples
/// in which the reader's rail advanced (§3.1); the window in microseconds
/// rounded to the nearest; the completion and thread counts and how the
/// window opened. `None` with fewer than two samples in a bracket, a
/// sample or window that is negative or not finite, or a value above
/// 2⁵³ − 1, which no report can carry.
pub fn values(&self) -> Option<BracketValues> {
const MAX: f64 = 9_007_199_254_740_991.0;
let to_int = |x: f64| (x.is_finite() && (0.0..=MAX).contains(&x)).then_some(x as u64);
let summary = |v: &[f64]| -> Option<BracketSummary> {
if v.iter().any(|x| !x.is_finite() || *x < 0.0) {
return None;
}
let live = Liveness::of_power_samples(v);
Some(BracketSummary {
n: v.len() as u64,
mean_uw: to_int((mean(v) * 1e6).round())?,
sd_uw: to_int((sample_sd(v)? * 1e6).ceil())?,
live_intervals: live.live_intervals,
longest_live_run: live.longest_live_run,
})
};
let (completions, in_flight) = (self.count.completions, self.count.in_flight);
if completions as f64 > MAX || in_flight as f64 > MAX {
return None;
}
Some(BracketValues {
idle: summary(&self.idle_w)?,
busy: summary(&self.busy_w)?,
window_us: to_int((self.window_s * 1e6).round())?,
completions,
in_flight,
opening: self.count.opening,
})
}
/// Whether the power reader's rail was live in both brackets (§3.1):
/// advancing in two consecutive samples, in at least five and in four of
/// every five, over at least 300 ms.
pub fn rail_live(&self) -> bool {
let span_us = |s: f64| if s.is_finite() && s > 0.0 { (s * 1e6) as u64 } else { 0 };
Liveness::of_power_samples(&self.idle_w).is_live_over(span_us(self.idle_span_s))
&& Liveness::of_power_samples(&self.busy_w).is_live_over(span_us(self.window_s))
}
/// `u_res`: the rounding to [`Bracket::sealed_uj`], relative to it
@ -182,15 +262,14 @@ impl Bracket {
Some(RESOLUTION_UNCERTAINTY_UJ / self.sealed_uj()? as f64)
}
/// The sealed figure and the declaration that describes it, computed
/// together so they cannot disagree ([`sealed_figure`]).
/// The sealed figure and the least declaration its report's integers
/// support, computed together from those integers so a verifier
/// reproduces both (§6.2).
fn sealed_declaration(&self) -> Option<(u64, Uncertainty)> {
let window_us = (self.window_s * 1e6).round();
if !window_us.is_finite() || window_us < 1.0 {
return None;
}
let (uj, rel) = sealed_figure(self.per_unit_uj()?, self.u_measurement()?)?;
Some((uj, Uncertainty::from_relative(rel, window_us as u64).ok()?))
let v = self.values()?;
let f = v.figure()?;
let p = v.least_relative_ppm(f)?;
Some((f, Uncertainty::new(p, v.window_us).ok()?))
}
/// The declared uncertainty a receipt carries: the measurement's own
@ -207,7 +286,9 @@ impl Bracket {
pub fn unsealed_reason(&self) -> Option<&'static str> {
if self.count.work().is_none() {
Some("no completion inside the window")
} else if self.ratio() < 1.0 {
} else if !self.rail_live() {
Some("the power reader's rail did not advance in enough intervals (§3.1): not a live reading")
} else if !self.values().is_some_and(|v| v.meets_attribution_floor()) {
Some("the marginal draw did not rise above the idle noise (ratio below 1)")
} else if self.sealed_uj().is_none() {
Some("the per-unit figure rounds to zero microjoules")
@ -222,12 +303,13 @@ impl Bracket {
/// What a receipt seals from this bracket when the power reader is the
/// measured SoC's own counter: `(joules_micro, OnChipCounter)`, or `None`
/// when §4 or §2.3 forbid sealing a hardware class — ratio below 1, a
/// per-unit figure that rounds to zero, or no evaluable uncertainty
/// ([`Bracket::unsealed_reason`]). The caller then seals the receipt
/// unmetered.
/// when §3.1, §4 or §2.3 forbid sealing a hardware class — a rail that was
/// not live, a ratio below 1, a per-unit figure that rounds to zero, or no
/// evaluable uncertainty ([`Bracket::unsealed_reason`]). The caller then
/// seals the receipt unmetered. The figure and declaration are the ones
/// the report's integers give ([`Bracket::values`]).
pub fn on_chip_figure(&self, filtering: CounterFiltering) -> Option<(u64, EnergyProvenance)> {
if self.ratio() < 1.0 {
if !self.rail_live() || !self.values()?.meets_attribution_floor() {
return None;
}
let (uj, uncertainty) = self.sealed_declaration()?;
@ -270,6 +352,19 @@ impl Bracket {
);
line("marginal (busy-idle)".into(), format!("{:.2} W", self.marginal_w()));
line("marginal / idle sd".into(), format!("{:.1}x ({})", self.ratio(), self.verdict()));
let live = |v: &[f64]| {
let l = Liveness::of_power_samples(v);
format!("{} of {} samples, longest run {}", l.live_intervals, l.intervals, l.longest_live_run)
};
line(
"rail live (§3.1)".into(),
format!(
"{} — idle {}; busy {}",
if self.rail_live() { "yes" } else { "NO" },
live(&self.idle_w),
live(&self.busy_w)
),
);
line("u_power (brackets)".into(), pct(self.u_power()));
line(format!("u_count ({n} in flight)"), pct(self.u_count()));
line(
@ -290,6 +385,31 @@ impl Bracket {
None => format!("none — {}", self.unsealed_reason().unwrap_or("not evaluable from this run")),
},
);
line(
"sealed values (§6.2)".into(),
match self.values() {
Some(v) => format!(
"idle n={} mean={} uW sd={} uW; busy n={} mean={} uW sd={} uW; T={} us; C={}; N={}",
v.idle.n, v.idle.mean_uw, v.idle.sd_uw, v.busy.n, v.busy.mean_uw, v.busy.sd_uw, v.window_us,
v.completions, v.in_flight
),
None => "none".into(),
},
);
line(
"derived verdict (§4)".into(),
self.values().and_then(|v| v.attribution()).map_or("none".into(), |a| a.label().to_string()),
);
let b = BuildRecord::of_this_build();
line(
"build".into(),
format!(
"{} ({}, {})",
b.profile.label(),
b.toolchain.as_deref().unwrap_or("toolchain unknown"),
b.target.as_deref().unwrap_or("target unknown")
),
);
o
}
@ -371,16 +491,41 @@ pub fn measure_with_macmon<T, W>(label: &str, item: &T, work: W) -> Option<Brack
where
T: Clone + Send + 'static,
W: Fn(&T) + Send + Sync + 'static,
{
measure_with(label, item, work, macmon_cpu_power)
}
/// The whole §3 protocol against any power reader: `reader(n)` returns `n`
/// samples of power in watts, one per sampling interval, or fewer when it
/// cannot. The idle bracket is read first, then a from-rest burst of `work` on
/// [`default_workers`] threads with a busy bracket over it.
///
/// The reader is a parameter so a reader that misbehaves — a frozen rail, a
/// rail refreshed in batches — can be put in front of the protocol and shown
/// to be refused.
pub fn measure_with<T, W, R>(label: &str, item: &T, work: W, mut reader: R) -> Option<Bracket>
where
T: Clone + Send + 'static,
W: Fn(&T) + Send + Sync + 'static,
R: FnMut(u32) -> Vec<f64>,
{
eprintln!(" {label}: idle baseline …");
let idle_w = macmon_cpu_power(IDLE_SAMPLES);
let t0 = Instant::now();
let idle_w = reader(IDLE_SAMPLES);
let idle_span_s = t0.elapsed().as_secs_f64();
if idle_w.len() < 3 {
return None;
}
let workers = default_workers();
eprintln!(" {label}: burst on {workers} threads, released together …");
let (busy_w, window_s, count) = burst_from_rest(workers, item, work, || macmon_cpu_power(BUSY_SAMPLES));
Some(Bracket { idle_w, busy_w, window_s, count })
let (busy_w, window_s, count) = burst_from_rest(workers, item, work, || reader(BUSY_SAMPLES));
Some(Bracket { idle_w, busy_w, window_s, idle_span_s, count })
}
/// The power reader the reference meters name in a measurement report: the
/// open-source reader and the reading it takes.
pub fn power_reader() -> &'static str {
"macmon cpu_power"
}
/// One sample burst from `macmon pipe` (the non-privileged IOReport reader),
@ -406,31 +551,71 @@ pub fn cpu_power_w(line: &str) -> Option<f64> {
mod tests {
use super::*;
/// A bracket whose window opened from rest, with the idle bracket sampled
/// over two seconds.
fn bracket(idle: &[f64], busy: &[f64], window_s: f64, n: u64, c: u64) -> Bracket {
Bracket {
idle_w: idle.to_vec(),
busy_w: busy.to_vec(),
window_s,
idle_span_s: 2.0,
count: CompletionCount { in_flight: n, completions: c, opening: WindowOpening::FromRest },
}
}
/// `pair` repeated to `n` samples: a tight, live bracket.
fn rep(pair: [f64; 2], n: usize) -> Vec<f64> {
(0..n).map(|i| pair[i % 2]).collect()
}
#[test]
fn the_figure_divides_by_the_work_the_window_paid_for() {
// 10 W marginal over 3 s = 30 J; 10 workers from rest, 60 completions:
// work 65, so 461,538 uJ per unit — not 500,000 (the raw count).
let b = bracket(&[19.9, 20.1, 19.9, 20.1], &[29.9, 30.1, 29.9, 30.1], 3.0, 10, 60);
let b = bracket(&rep([19.9, 20.1], 10), &rep([29.9, 30.1], 14), 3.0, 10, 60);
let per = b.per_unit_uj().unwrap();
assert!((per - 30.0 / 65.0 * 1e6).abs() < 1e-6, "{per}");
let (uj, class) = b.on_chip_figure(CounterFiltering::Unknown).unwrap();
assert_eq!(uj, 461_538);
let u = class.uncertainty().unwrap();
// The declaration is the least the report's integers support, and it
// is within a ppm of what the unrounded samples give.
let v = b.values().unwrap();
assert_eq!(Some(u.relative_ppm), v.least_relative_ppm(uj));
let u_meas = b.u_power().unwrap().hypot(10.0 / (3f64.sqrt() * 130.0));
let want = (per * u_meas).hypot(0.5 / 3f64.sqrt()) / 461_538.0;
assert_eq!(u.relative_ppm, (want * 1e6).ceil() as u32);
assert!((u.relative_ppm as f64 - want * 1e6).abs() <= 1.5, "{} vs {}", u.relative_ppm, want * 1e6);
assert_eq!(u.window_us, 3_000_000);
}
/// The failing-before case: sealed from the unrounded samples, this
/// bracket's declaration is 6,232 ppm, one below the 6,233 ppm the
/// integers its report publishes support — so the report would refuse the
/// receipt it describes. Sealed from the integers, it is 6,233.
#[test]
fn a_sealed_declaration_is_what_its_bracket_supports() {
let b = bracket(&rep([19.9, 20.1], 10), &rep([29.74, 30.08], 14), 2.007, 10, 1301);
let v = b.values().unwrap();
assert_eq!(
(v.idle.n, v.idle.mean_uw, v.idle.sd_uw, v.busy.n, v.busy.mean_uw, v.busy.sd_uw, v.window_us),
(10, 20_000_000, 105_410, 14, 29_910_000, 176_418, 2_007_000)
);
let (uj, class) = b.on_chip_figure(CounterFiltering::Unknown).unwrap();
assert_eq!(uj, 15_229);
assert_eq!(
class,
EnergyProvenance::OnChipCounter {
uncertainty: Uncertainty { relative_ppm: 6_233, window_us: 2_007_000 },
filtering: CounterFiltering::Unknown,
}
);
assert_eq!(v.check(uj, Some((6_233, 2_007_000))), Ok(()));
assert_eq!(
v.check(uj, Some((6_232, 2_007_000))),
Err(crate::energy_report::ReportDefect::DeclarationBelowBracket)
);
}
/// A bracket with a 10 W marginal whose per-unit estimate is `e` µJ:
/// the window and count are chosen so `10 W × window ÷ (C + N/2) = e`.
fn small(e: f64) -> Bracket {
@ -464,9 +649,26 @@ mod tests {
assert!(uj * u.relative_ppm as u64 >= crate::energy_class::RESOLUTION_FLOOR_UJ_PPM);
assert_eq!(check_resolution(uj, &class), Ok(()));
}
// The 1 µJ figure is dominated by its resolution: about 29 %.
let u = small(0.563).declared().unwrap();
assert!(u.relative_ppm >= 288_676 && u.relative_ppm < 290_000, "{}", u.relative_ppm);
// The 1 µJ figure is dominated by its resolution: 288,994 ppm, the
// value the independent script computes for these integers.
assert_eq!(small(0.563).declared().unwrap().relative_ppm, 288_994);
}
/// Every bracket these tests seal: the figure and declaration are what the
/// report's integers give, so a verifier holding the report reproduces both.
#[test]
fn every_sealed_figure_is_reproduced_by_its_values() {
let mut brackets: Vec<Bracket> = [0.563, 10.35, 1.49, 42.6, 461_538.46, 0.51].into_iter().map(small).collect();
brackets.push(bracket(&rep([19.9, 20.1], 10), &rep([29.9, 30.1], 14), 3.0, 10, 60));
brackets.push(bracket(&rep([19.9, 20.1], 10), &rep([29.74, 30.08], 14), 2.007, 10, 1301));
for b in brackets {
let (uj, class) = b.on_chip_figure(CounterFiltering::Unknown).expect("each of these seals");
let v = b.values().unwrap();
let u = class.uncertainty().unwrap();
assert_eq!(v.figure(), Some(uj));
assert_eq!(v.least_relative_ppm(uj), Some(u.relative_ppm));
assert_eq!(v.check(uj, Some((u.relative_ppm, u.window_us))), Ok(()));
}
}
#[test]
@ -478,6 +680,27 @@ mod tests {
assert_eq!(small(0.51).unsealed_reason(), None);
}
#[test]
fn unsealed_reasons_follow_the_integer_rules() {
// A marginal that clears the idle noise only before the noise is
// rounded up: the idle sd is 541,487.34 µW and the marginal 0.1 µW
// above it, a ratio just over 1 from the samples; the report carries
// the sd rounded up to 541,488 µW and the marginal rounded to
// 541,487 µW, a ratio below 1, so nothing is sealed.
let live = rep([20.0 - 0.5137, 20.0 + 0.5137], 10);
let sd_idle = sample_sd(&live).unwrap();
let busy = 20.0 + sd_idle + 1e-7;
let at_one = bracket(&live, &rep([busy - 1e-6, busy + 1e-6], 14), 3.0, 10, 5000);
assert!(at_one.ratio() > 1.0, "{}", at_one.ratio());
let v = at_one.values().unwrap();
assert_eq!((v.idle.sd_uw, v.marginal_uw()), (541_488, Some(541_487)), "{v:?}");
assert!(at_one.on_chip_figure(CounterFiltering::Unknown).is_none());
assert_eq!(at_one.unsealed_reason(), Some("the marginal draw did not rise above the idle noise (ratio below 1)"));
// No completion is reported before anything about the reader.
let none = bracket(&[0.0; 10], &[0.0; 14], 3.0, 10, 0);
assert_eq!(none.unsealed_reason(), Some("no completion inside the window"));
}
#[test]
fn reported_standard_deviations_reproduce_the_declared_uncertainty() {
let b = bracket(&[19.8, 20.2, 19.8, 20.2, 20.0], &[29.5, 30.5, 29.5, 30.5, 30.0], 2.0, 4, 1000);
@ -494,17 +717,166 @@ mod tests {
assert_eq!(zeros.ratio(), 0.0);
assert!(zeros.on_chip_figure(CounterFiltering::Unknown).is_none());
// Busy never rose above a noisy idle: ratio below 1.
let noisy = bracket(&[18.0, 22.0, 18.0, 22.0], &[20.1, 20.3, 20.1, 20.3], 3.0, 10, 5000);
let noisy = bracket(&rep([18.0, 22.0], 10), &rep([20.1, 20.3], 14), 3.0, 10, 5000);
assert!(noisy.ratio() < 1.0);
assert!(noisy.on_chip_figure(CounterFiltering::Unknown).is_none());
// No completion: no per-unit figure.
let none = bracket(&[19.9, 20.1, 19.9], &[29.9, 30.1, 29.9], 3.0, 10, 0);
let none = bracket(&rep([19.9, 20.1], 10), &rep([29.9, 30.1], 14), 3.0, 10, 0);
assert!(none.per_unit_uj().is_none() && none.on_chip_figure(CounterFiltering::Unknown).is_none());
// One sample in a bracket: no evaluable uncertainty, so no hardware class.
let one = bracket(&[20.0, 20.1, 19.9], &[30.0], 3.0, 10, 100);
assert!(one.declared().is_none() && one.on_chip_figure(CounterFiltering::Unknown).is_none());
}
// ── fault injection: a power reader whose rail is not live (§3.1) ──
/// The burst these tests run: nothing, as fast as it goes. What is under
/// test is the reader in front of the protocol.
fn measure(reader: impl FnMut(u32) -> Vec<f64>) -> Bracket {
measure_with("fault", &(), |_| std::thread::sleep(std::time::Duration::from_millis(1)), reader)
.expect("the reader returns samples")
}
/// A reader that returns `n` samples of `f(i)`, after sleeping long enough
/// that each bracket spans more than 300 ms.
fn reader(f: impl Fn(u32) -> f64) -> impl FnMut(u32) -> Vec<f64> {
move |n| {
std::thread::sleep(std::time::Duration::from_millis(320));
(0..n).map(&f).collect()
}
}
/// A frozen rail: the reader returns zeros in both brackets.
#[test]
fn a_frozen_rail_is_refused() {
let b = measure(reader(|_| 0.0));
assert!(!b.rail_live());
assert!(b.on_chip_figure(CounterFiltering::Unknown).is_none());
assert!(b.unsealed_reason().is_some());
}
/// A rail refreshed in batches: the idle bracket is flat zero and one busy
/// sample carries the whole refresh. The flat idle makes the ratio
/// infinite and the arithmetic reads it as clean; before the liveness rule
/// this sealed an `OnChipCounter` figure.
#[test]
fn a_single_refresh_lump_is_refused() {
let mut calls = 0;
let b = measure(move |n| {
calls += 1;
std::thread::sleep(std::time::Duration::from_millis(320));
let busy = calls == 2;
(0..n).map(|i| if busy && i == 6 { 140.0 } else { 0.0 }).collect()
});
assert_eq!(b.ratio(), f64::INFINITY, "the display ratio is infinite");
assert!(b.values().unwrap().attribution().is_some(), "the arithmetic gives a verdict");
assert!(!b.rail_live());
assert!(b.on_chip_figure(CounterFiltering::Unknown).is_none(), "one lump is not a live rail");
assert_eq!(
b.unsealed_reason(),
Some("the power reader's rail did not advance in enough intervals (§3.1): not a live reading")
);
}
/// A rail refreshed in batches, one lump in each bracket: a larger one
/// while the burst runs. The idle bracket is no longer flat, the ratio is
/// above 1, and a rule that counted one advance as a live rail would seal
/// it.
#[test]
fn a_rail_refreshed_in_batches_is_refused() {
let mut calls = 0;
let b = measure(move |n| {
calls += 1;
std::thread::sleep(std::time::Duration::from_millis(320));
let (at, lump) = if calls == 1 { (3, 20.0) } else { (6, 140.0) };
(0..n).map(|i| if i == at { lump } else { 0.0 }).collect()
});
assert!(b.ratio() >= 1.0, "{}", b.ratio());
assert!(!b.rail_live());
assert!(b.on_chip_figure(CounterFiltering::Unknown).is_none());
}
/// The same, with each refresh published in two steps in adjacent
/// samples. Every bracket shows two consecutive advances, so a rule of two
/// consecutive advances alone would seal it; the rest of §3.1 refuses it.
#[test]
fn a_two_step_refresh_is_refused() {
let mut calls = 0;
let b = measure(move |n| {
calls += 1;
std::thread::sleep(std::time::Duration::from_millis(320));
let (at, step) = if calls == 1 { (3, 10.0) } else { (6, 70.0) };
(0..n)
.map(|i| if i == at { step } else if i == at + 1 { step * 1.25 } else { 0.0 })
.collect()
});
for side in [&b.idle_w, &b.busy_w] {
let l = Liveness::of_power_samples(side);
assert_eq!((l.live_intervals, l.longest_live_run), (2, 2), "two consecutive advances");
}
assert!(b.ratio() >= 1.0, "{}", b.ratio());
assert!(!b.rail_live());
assert!(b.on_chip_figure(CounterFiltering::Unknown).is_none());
}
/// A reader that holds its last value between refreshes, on a rail that
/// is not being refreshed: it reads a stale 5 W through the idle bracket
/// and steps once to 12 W, where one batch lands, in the busy bracket.
/// Every sample is above zero, the idle bracket is flat (sd 0), the
/// marginal is positive and the ratio infinite: a rule that counted a
/// non-zero sample as an advance sealed this as `OnChipCounter`.
#[test]
fn a_rail_stuck_at_a_held_value_is_refused() {
let mut calls = 0;
let b = measure(move |n| {
calls += 1;
std::thread::sleep(std::time::Duration::from_millis(320));
let busy = calls == 2;
(0..n).map(|i| if busy && i >= 3 { 12.0 } else { 5.0 }).collect()
});
assert!(b.idle_w.iter().chain(&b.busy_w).all(|w| *w > 0.0), "no sample reads zero");
let v = b.values().unwrap();
assert_eq!(v.idle.sd_uw, 0, "a flat idle bracket");
assert_eq!(v.attribution(), Some(crate::energy_report::Attribution::Clean), "the arithmetic reads it as clean");
assert!(!b.rail_live());
assert!(b.on_chip_figure(CounterFiltering::Unknown).is_none(), "a held value is not a live rail");
assert_eq!(
b.unsealed_reason(),
Some("the power reader's rail did not advance in enough intervals (§3.1): not a live reading")
);
// And a reader of the report refuses it under a hardware class, whatever
// liveness facts the report carries.
let claimed = BracketValues {
idle: BracketSummary { live_intervals: v.idle.n, longest_live_run: v.idle.n, ..v.idle },
busy: BracketSummary { live_intervals: v.busy.n, longest_live_run: v.busy.n, ..v.busy },
..v
};
let f = claimed.figure().unwrap();
let p = claimed.least_relative_ppm(f).unwrap();
assert_eq!(
claimed.check(f, Some((p, claimed.window_us))),
Err(crate::energy_report::ReportDefect::RailNotLive)
);
}
/// A live reader seals. The busy samples are set high so that the burst's
/// own effect on the machine does not matter: the reader is scripted.
#[test]
fn a_live_rail_seals() {
let mut calls = 0;
let b = measure(move |n| {
calls += 1;
std::thread::sleep(std::time::Duration::from_millis(320));
let level = if calls == 1 { 20.0 } else { 30.0 };
(0..n).map(|i| level + if i % 2 == 0 { 0.1 } else { -0.1 }).collect()
});
assert!(b.rail_live());
assert!(b.on_chip_figure(CounterFiltering::Unknown).is_some());
// The same samples over a bracket shorter than 300 ms are not live.
let short = Bracket { idle_span_s: 0.2, ..b.clone() };
assert!(!short.rail_live() && short.on_chip_figure(CounterFiltering::Unknown).is_none());
}
#[test]
fn a_from_rest_burst_counts_only_work_begun_inside_the_window() {
// Each unit of work records that it started; the sampler records the

View file

@ -30,6 +30,15 @@
//! never reads it as any class — it is not upgraded to one — and checks a
//! present label with [`check_label`].
//!
//! # Reports about a figure
//!
//! A figure obtained under the bracketed protocol is published with the values its
//! bracket gave, the build and the power reader, in a `measurement` claim about it
//! (`extensions/energy-measurement.md` §6, jwp-receipts §2.4.8), which names the
//! receipt sealing the figure and leaves it unchanged; an `operating-point` claim
//! records the operating point it was spent at (energy-binding §7). The class here is
//! what such a claim restates.
//!
//! # Why hardware evidence is two classes, not one
//!
//! The first version of the ladder had a single hardware rung, `HwShunt`, for
@ -583,7 +592,8 @@ pub fn write_optional(p: Option<&EnergyProvenance>, o: &mut Vec<u8>) {
/// enforce in `verify()` (the `wai.video.int_motion` and `wai.video.*` OTT
/// receipts):
///
/// - unmetered (`0`, no class) is valid;
/// - the unmetered marker (`0`, no class) is valid — it marks a figure nobody
/// measured, never a figure of zero joules (energy-measurement §2);
/// - a non-zero figure must carry a class, the class must pass
/// [`EnergyProvenance::validate`], and a declared uncertainty must reach the
/// resolution floor ([`check_resolution`]);
@ -810,6 +820,31 @@ pub fn to_quantum_class(
Ok(c)
}
/// The same class in this crate's vocabulary, from `wai-quantum`'s
/// (`wai_quantum::quantum_energy::EnergyClass`): the inverse of
/// [`to_quantum_class`], with the same signed encoding. Every quantum class has a
/// counterpart here.
#[cfg(any(feature = "quantum_receipt", feature = "quantum_ops", feature = "quantum_phasor_meter"))]
pub fn from_quantum_class(c: &wai_quantum::quantum_energy::EnergyClass) -> EnergyProvenance {
use wai_quantum::quantum_energy as q;
let u = |u: &q::Uncertainty| Uncertainty { relative_ppm: u.relative_ppm, window_us: u.window_us };
match c {
q::EnergyClass::ModelBased => EnergyProvenance::ModelBased,
q::EnergyClass::Estimator => EnergyProvenance::Estimator,
q::EnergyClass::OnChipCounter { uncertainty, filtering } => EnergyProvenance::OnChipCounter {
uncertainty: u(uncertainty),
filtering: match filtering {
q::CounterFiltering::Unknown => CounterFiltering::Unknown,
q::CounterFiltering::Off => CounterFiltering::Off,
q::CounterFiltering::On => CounterFiltering::On,
},
},
q::EnergyClass::CalibratedInstrument { uncertainty, calibration_ref } => {
EnergyProvenance::CalibratedInstrument { uncertainty: u(uncertainty), calibration_ref: calibration_ref.clone() }
}
}
}
/// The statistical (Type A) relative standard uncertainty of a bracketed
/// marginal power draw — `u_power` in `extensions/energy-measurement.md` §2.3:
///
@ -1390,6 +1425,7 @@ mod tests {
let jp: Vec<_> = json_fields(&p);
let jq: Vec<_> = q.json_fields();
assert_eq!(jp, jq, "the two vocabularies render the same JSON fields");
assert_eq!(from_quantum_class(&q), p, "and the two conversions are inverse");
}
assert_eq!(to_quantum_class(&EnergyProvenance::HwShunt), Err(AcquisitionError::LegacyLabel));
}

View file

@ -0,0 +1,473 @@
//! Figure profiles (`extensions/energy-measurement.md` §6.1): how a report about one
//! energy figure — a `measurement` or `operating-point` claim (jwp-receipts §2.4.8–§2.4.9)
//! — names the receipt that seals the figure, and what it reads there.
//!
//! A profile is named by the domain its receipt's identity is computed under, without
//! the domain's final version byte, so the registry cannot drift from the code: a
//! receipt family's identity domain *is* its profile's name. A profile fixes the
//! claim's subject type, how the identity is computed, which figure an index selects,
//! where the figure's class is, and which key signed it.
//!
//! [`EnergyFigures::figure_view`] gives, for a receipt this crate has verified, what a
//! report is checked against at jwp-receipts §3.1 step 6: the profile, the identity,
//! the figures with their classes, and the Ed25519 key that signed them. It is `None`
//! for a receipt that does not verify. No receipt family is changed by being viewed.
//!
//! The quantum receipts are viewed through `wai-quantum`'s `MaybeLabelled` and
//! `LabelledQbom`. Their profile names are read from the receipt type's identity
//! domain through the crate's labelling trait, whose methods `wai-quantum` documents
//! as plumbing rather than a public API; a test pins each type's profile name, so a
//! change there fails loudly here.
use crate::claim_receipt::{jcs, ClaimBody, ClaimReceipt, SubjectType};
use crate::energy_class::EnergyProvenance;
/// One figure profile of energy-measurement §6.1.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Profile {
pub name: &'static str,
/// The `subject_type` a claim naming this profile carries.
pub subject_type: SubjectType,
/// Whether the receipt carries several figures, selected by `figure_index`.
pub indexed: bool,
/// Whether an Ed25519 key signs the figure. Only that key may sign a
/// `measurement` claim about it.
pub ed25519_signer: bool,
/// Whether the figure is named within a group: the `moq-jwp:object` profile, whose
/// subject is a content hash that more than one group can deliver.
pub grouped: bool,
}
const fn p(name: &'static str, subject_type: SubjectType, indexed: bool, ed25519_signer: bool, grouped: bool) -> Profile {
Profile { name, subject_type, indexed, ed25519_signer, grouped }
}
const PROFILES: &[Profile] = &[
p("moq-jwp:object", SubjectType::Object, true, true, true),
p("moq-jwp:claim-link", SubjectType::Receipt, false, true, false),
p("wai:energy-binding-id", SubjectType::Receipt, false, false, false),
p("wai:video-receipt-id", SubjectType::Receipt, false, true, false),
p("wai:video-manifest-receipt-id", SubjectType::Receipt, false, true, false),
p("wai:video-ssai-receipt-id", SubjectType::Receipt, false, true, false),
p("wai:video-keys-receipt-id", SubjectType::Receipt, false, true, false),
p("wai:video-package-receipt-id", SubjectType::Receipt, false, true, false),
p("wai:video-channel-receipt-id", SubjectType::Receipt, false, true, false),
p("wai:world-receipt-id", SubjectType::Receipt, false, true, false),
p("wai:film-receipt-id", SubjectType::Receipt, false, true, false),
p("wai:haptic-receipt-id", SubjectType::Receipt, false, true, false),
p("wai:score-receipt-id", SubjectType::Receipt, false, true, false),
p("wai:splat4d-receipt-id", SubjectType::Receipt, false, true, false),
p("wai:audio-receipt-id", SubjectType::Receipt, false, true, false),
p("wai:ternary-receipt-id", SubjectType::Receipt, false, true, false),
p("wai:asset-receipt-id", SubjectType::Receipt, true, true, false),
p("wai:quantum-receipt-id", SubjectType::Receipt, false, true, false),
p("wai:quantum-calibration-id", SubjectType::Receipt, false, true, false),
p("wai:quantum-job-id", SubjectType::Receipt, false, true, false),
p("wai:quantum-mitigate-id", SubjectType::Receipt, false, true, false),
p("wai:quantum-decode-id", SubjectType::Receipt, false, true, false),
p("wai:quantum-compile-id", SubjectType::Receipt, false, true, false),
p("wai:quantum-rearrange-id", SubjectType::Receipt, false, true, false),
p("wai:phasor-receipt-id", SubjectType::Receipt, false, true, false),
p("wai:quantum-qbom-id", SubjectType::Receipt, true, true, false),
];
/// The §6.1 table. A function rather than a public constant, so the C header does
/// not gain it.
pub fn profiles() -> &'static [Profile] {
PROFILES
}
/// The profile named `name`, if §6.1 has one.
pub fn profile(name: &str) -> Option<&'static Profile> {
PROFILES.iter().find(|p| p.name == name)
}
/// The profile name of an identity domain: the domain without its final version byte.
pub fn profile_name_of_domain(domain: &[u8]) -> Option<&'static str> {
let (_, name) = domain.split_last()?;
let name = std::str::from_utf8(name).ok()?;
profile(name).map(|p| p.name)
}
/// One figure a receipt carries.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Figure {
/// Its index, in a profile that carries several; `None` in one that carries one.
pub index: Option<u64>,
pub joules_micro: u64,
/// Its class; `None` for an unlabelled or unmetered figure.
pub class: Option<EnergyProvenance>,
}
/// What a report about a figure is checked against (jwp-receipts §2.4.8, step 6): a
/// receipt that has verified, by profile and identity.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FigureView {
pub profile: String,
pub identity: [u8; 32],
/// For `moq-jwp:object`, the link hash of the group the object was accepted under.
pub group: Option<[u8; 32]>,
pub figures: Vec<Figure>,
/// The Ed25519 key that signed the figures, where one does.
pub signer: Option<[u8; 32]>,
}
impl FigureView {
fn single(profile: &str, identity: [u8; 32], joules_micro: u64, class: Option<EnergyProvenance>, signer: [u8; 32]) -> FigureView {
FigureView {
profile: profile.into(),
identity,
group: None,
figures: vec![Figure { index: None, joules_micro, class }],
signer: Some(signer),
}
}
/// The figure at `index`.
pub fn figure(&self, index: Option<u64>) -> Option<&Figure> {
self.figures.iter().find(|f| f.index == index)
}
}
/// A receipt whose figures a report can name.
pub trait EnergyFigures {
/// The receipt's figures under its profile, or `None` when the receipt does not
/// verify.
fn figure_view(&self) -> Option<FigureView>;
}
/// An `energy` claim (`moq-jwp:claim-link`): its figure and class, identified by its
/// link hash. A claim of another class carries no figure.
impl EnergyFigures for ClaimReceipt {
fn figure_view(&self) -> Option<FigureView> {
self.verify().ok()?;
let ClaimBody::Energy(e) = &self.body else {
return Some(FigureView {
profile: "moq-jwp:claim-link".into(),
identity: self.link_hash(),
group: None,
figures: Vec::new(),
signer: Some(self.signer_pubkey),
});
};
let class = e.acquisition_class().ok()?;
Some(FigureView::single("moq-jwp:claim-link", self.link_hash(), e.joules_micro, Some(class), self.signer_pubkey))
}
}
/// An energy binding's identity: `BLAKE3("wai:energy-binding-id\x01" ‖ JCS(body))`
/// (energy-binding §4). `None` for a body holding a value outside JCS's subset here,
/// such as an integer above 2⁵³ − 1.
pub fn binding_identity(body: &serde_json::Value) -> Option<[u8; 32]> {
let canonical = jcs(body).ok()?;
let mut h = blake3::Hasher::new();
h.update(b"wai:energy-binding-id\x01");
h.update(canonical.as_bytes());
Some(*h.finalize().as_bytes())
}
/// An energy binding's body (energy-binding §2), viewed by its identity. No Ed25519
/// key signs it.
pub struct BindingBody<'a>(pub &'a serde_json::Value);
impl EnergyFigures for BindingBody<'_> {
fn figure_view(&self) -> Option<FigureView> {
let b = crate::energy_binding::EnergyBinding::from_json(self.0).ok()?;
Some(FigureView {
profile: "wai:energy-binding-id".into(),
identity: binding_identity(self.0)?,
group: None,
figures: vec![Figure { index: None, joules_micro: b.microjoules, class: Some(b.acquisition) }],
signer: None,
})
}
}
/// A JWP object (`moq-jwp:object`), accepted under its group with the envelope it was
/// delivered with: its figure at its leaf index, labelled where its leaf is, within
/// the group named by its link hash.
pub struct DeliveredObject<'a> {
pub object: &'a crate::jwp_delivery::ObjectReceipt,
pub group: &'a crate::jwp_delivery::GroupReceipt,
pub envelope: &'a [u8],
}
impl EnergyFigures for DeliveredObject<'_> {
fn figure_view(&self) -> Option<FigureView> {
self.object.accept(self.group, self.envelope).ok()?;
let class = self.object.label.as_ref().and_then(|l| l.class().cloned());
Some(FigureView {
profile: "moq-jwp:object".into(),
identity: self.object.content_hash,
group: Some(self.group.link_hash()),
figures: vec![Figure {
index: Some(self.object.merkle_proof.leaf_index),
joules_micro: self.object.joules_micro,
class,
}],
signer: Some(self.group.signer_pubkey),
})
}
}
/// The receipt families whose figure sits in `joules_micro` beside an optional or
/// always-present `energy_provenance`, signed by `signer_pubkey`, identified by
/// `receipt_hash()`.
macro_rules! single_figure {
($feature:literal, $t:ty, $profile:literal) => {
#[cfg(feature = $feature)]
impl EnergyFigures for $t {
fn figure_view(&self) -> Option<FigureView> {
if !self.verify() {
return None;
}
Some(FigureView::single($profile, self.receipt_hash(), self.joules_micro, self.energy_provenance.clone(), self.signer_pubkey))
}
}
};
}
single_figure!("video_receipt", crate::video_receipt::VideoReceipt, "wai:video-receipt-id");
single_figure!("video_manifest", crate::wai_video_manifest::ManifestReceipt, "wai:video-manifest-receipt-id");
single_figure!("video_ssai", crate::wai_video_ssai::SsaiReceipt, "wai:video-ssai-receipt-id");
single_figure!("video_keys", crate::wai_video_keys::KeyReleaseReceipt, "wai:video-keys-receipt-id");
single_figure!("video_package", crate::wai_video_package::PackageReceipt, "wai:video-package-receipt-id");
single_figure!("video_channel", crate::wai_video_channel::ChannelReceipt, "wai:video-channel-receipt-id");
single_figure!("world", crate::world::receipt::SessionReceipt, "wai:world-receipt-id");
single_figure!("film", crate::film::FilmReceipt, "wai:film-receipt-id");
single_figure!("haptic", crate::haptic::HapticReceipt, "wai:haptic-receipt-id");
single_figure!("score", crate::score::ScoreReceipt, "wai:score-receipt-id");
single_figure!("splat4d", crate::splat4d::Splat4dReceipt, "wai:splat4d-receipt-id");
single_figure!("audio_scene", crate::audio_scene::SceneReceipt, "wai:audio-receipt-id");
single_figure!("ternary_receipt", crate::ternary_receipt::TernaryReceipt, "wai:ternary-receipt-id");
/// A provenance receipt (`wai:asset-receipt-id`): one figure per step, indexed from 0.
impl EnergyFigures for crate::provenance::AssetReceipt {
fn figure_view(&self) -> Option<FigureView> {
if !self.verify() {
return None;
}
Some(FigureView {
profile: "wai:asset-receipt-id".into(),
identity: self.receipt_hash(),
group: None,
figures: self
.steps
.iter()
.enumerate()
.map(|(i, s)| Figure { index: Some(i as u64), joules_micro: s.joules_micro, class: s.energy_provenance.clone() })
.collect(),
signer: Some(self.signer_pubkey),
})
}
}
/// A quantum receipt of `wai-quantum`, in either form.
#[cfg(any(feature = "quantum_receipt", feature = "quantum_ops", feature = "quantum_phasor_meter"))]
impl<R: wai_quantum::quantum_energy::Labellable> EnergyFigures for wai_quantum::quantum_energy::MaybeLabelled<R> {
fn figure_view(&self) -> Option<FigureView> {
if !self.verify() {
return None;
}
let r = self.receipt();
let profile = profile_name_of_domain(r.id_domain())?;
let class = self.energy_class().map(crate::energy_class::from_quantum_class);
Some(FigureView::single(profile, self.receipt_hash(), r.figure(), class, r.signer_key()))
}
}
/// A quantum bill of materials, unlabelled: one unlabelled figure per entry.
#[cfg(feature = "quantum_qbom")]
impl EnergyFigures for wai_quantum::quantum_qbom::Qbom {
fn figure_view(&self) -> Option<FigureView> {
if !self.verify() {
return None;
}
Some(FigureView {
profile: "wai:quantum-qbom-id".into(),
identity: self.receipt_hash(),
group: None,
figures: self
.entries
.iter()
.enumerate()
.map(|(i, e)| Figure { index: Some(i as u64), joules_micro: e.joules_micro, class: None })
.collect(),
signer: Some(self.signer_pubkey),
})
}
}
/// A quantum bill of materials labelled per entry.
#[cfg(feature = "quantum_qbom")]
impl EnergyFigures for wai_quantum::quantum_energy::LabelledQbom {
fn figure_view(&self) -> Option<FigureView> {
if !self.verify() {
return None;
}
Some(FigureView {
profile: "wai:quantum-qbom-id".into(),
identity: self.receipt_hash(),
group: None,
figures: self
.qbom
.entries
.iter()
.zip(&self.classes)
.enumerate()
.map(|(i, (e, c))| Figure {
index: Some(i as u64),
joules_micro: e.joules_micro,
class: c.as_ref().map(crate::energy_class::from_quantum_class),
})
.collect(),
signer: Some(self.qbom.signer_pubkey),
})
}
}
/// What a reference meter writes beside the receipt it seals (energy-measurement §6): a
/// `measurement` claim about the receipt's figure, signed with the receipt's key and
/// checked against the receipt before it is written. Kept here, not in the meters, so
/// the whole path is tested in the library.
pub mod meter_report {
use super::{EnergyFigures, FigureView};
use crate::claim_receipt::{rfc3339_utc, ClaimBody, ClaimReceipt, FigureHeader, MeasurementClaim};
use crate::energy_report::{BracketValues, BuildRecord};
use ed25519_dalek::SigningKey;
use std::path::PathBuf;
/// A meter's command line with the report options taken out: `--no-report`, and
/// `--previous-report <file>`, the meter's previous report, which the new one chains
/// to (jwp-receipts §3.1 step 5).
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ReportArgs {
pub positional: Vec<String>,
pub no_report: bool,
pub previous_report: Option<String>,
}
/// The report options taken out of `args`. `--previous-report` with no file
/// after it — at the end, or followed by another option — is an error, never a
/// silent first report.
pub fn parse_args(args: &[String]) -> Result<ReportArgs, String> {
let mut out = ReportArgs::default();
let mut it = args.iter();
while let Some(a) = it.next() {
match a.as_str() {
"--no-report" => out.no_report = true,
"--previous-report" => match it.next() {
Some(p) if !p.starts_with("--") => out.previous_report = Some(p.clone()),
_ => return Err("--previous-report needs the previous report's file".into()),
},
_ => out.positional.push(a.clone()),
}
}
Ok(out)
}
/// [`parse_args`] for a meter's `main`: a usage error exits with status 2.
pub fn parse_args_or_exit(args: &[String]) -> ReportArgs {
parse_args(args).unwrap_or_else(|e| {
eprintln!("usage error: {e}");
std::process::exit(2)
})
}
/// Where the report goes by default: beside the receipt, its `.json` extension
/// replaced by `.report.json` (or `.report.json` appended), and to standard output
/// (`None`) when the receipt is not written to a file.
pub fn default_path(receipt_out: Option<&str>) -> Option<PathBuf> {
let r = receipt_out?;
Some(PathBuf::from(match r.strip_suffix(".json") {
Some(stem) => format!("{stem}.report.json"),
None => format!("{r}.report.json"),
}))
}
/// The report on the figure `view` holds at `index`: the figure restated with its
/// class, the bracket's values, the build and the reader, signed by `signer` — the
/// key that signed the figure — and checked against `view` (jwp-receipts §3.1 steps
/// 1–4 and 6). A figure with no class gets no report: `Err` says why.
#[allow(clippy::too_many_arguments)]
pub fn emit(
view: &FigureView,
index: Option<u64>,
bracket: Option<BracketValues>,
build: BuildRecord,
power_reader: &str,
signer: &SigningKey,
signer_id: &str,
previous: Option<&ClaimReceipt>,
issued_at: &str,
) -> Result<ClaimReceipt, String> {
let profile = super::profile(&view.profile).ok_or("the receipt's profile is not in energy-measurement section 6.1")?;
let f = view.figure(index).ok_or("the receipt carries no figure at that index")?;
let class = f.class.as_ref().ok_or("the figure carries no class (unmetered or unlabelled): it gets no report")?;
let body = ClaimBody::Measurement(MeasurementClaim {
figure: FigureHeader::new(profile.name, index, view.group, f.joules_micro, class),
bracket,
build,
power_reader: Some(power_reader.to_owned()),
});
let (seq, parent) = match previous {
Some(p) => (p.seq + 1, Some(p.link_hash())),
None => (0, None),
};
let r = ClaimReceipt::seal(signer, signer_id, profile.subject_type, view.identity, seq, issued_at, body, parent)
.map_err(|e| format!("the report is refused ({e})"))?;
r.check_figure_against(Some(view)).map_err(|e| format!("the report does not check against the receipt ({e})"))?;
r.check_parent(previous).map_err(|e| format!("the report does not chain to the previous one ({e})"))?;
Ok(r)
}
/// Everything a meter does with its report, after sealing `receipt`: unless
/// `--no-report` was given, build the report on the figure at `index` and write it to
/// `explicit` or the default path beside `receipt_out`, or print it; say where it
/// went, or why there is none. Returns the report written.
#[allow(clippy::too_many_arguments)]
pub fn write<R: EnergyFigures>(
args: &ReportArgs,
receipt: &R,
index: Option<u64>,
bracket: Option<BracketValues>,
power_reader: &str,
signer: &SigningKey,
signer_id: &str,
receipt_out: Option<&str>,
explicit: Option<&str>,
) -> Option<ClaimReceipt> {
if args.no_report {
println!("report : not written (--no-report); energy-measurement section 6 requires one beside a published figure");
return None;
}
let Some(view) = receipt.figure_view() else {
eprintln!("report : none — the receipt does not verify");
return None;
};
let previous = args.previous_report.as_deref().map(|p| {
let wire = std::fs::read_to_string(p).unwrap_or_else(|e| panic!("read {p}: {e}"));
crate::claim_receipt::verify_claim_json(&wire).unwrap_or_else(|e| panic!("{p}: {e}"))
});
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs());
match emit(&view, index, bracket, BuildRecord::of_this_build(), power_reader, signer, signer_id, previous.as_ref(), &rfc3339_utc(now)) {
Err(why) => {
println!("report : none — {why}");
None
}
Ok(r) => {
match explicit.map(PathBuf::from).or_else(|| default_path(receipt_out)) {
Some(path) => {
std::fs::write(&path, r.to_json()).unwrap_or_else(|e| panic!("write {}: {e}", path.display()));
println!("report : {} (measurement claim, checked against the receipt)", path.display());
}
None => println!("report : {}", r.to_json()),
}
Some(r)
}
}
}
}
#[cfg(test)]
mod tests;

View file

@ -0,0 +1,336 @@
use super::*;
use crate::energy_class::{CounterFiltering, Uncertainty};
use ed25519_dalek::SigningKey;
fn key(s: u8) -> SigningKey {
SigningKey::from_bytes(&[s; 32])
}
fn chip(ppm: u32) -> EnergyProvenance {
EnergyProvenance::OnChipCounter { uncertainty: Uncertainty { relative_ppm: ppm, window_us: 2_977_104 }, filtering: CounterFiltering::Unknown }
}
/// The registry is the set of identity domains the code hashes under, less their
/// version byte: one name each, no name twice.
#[test]
fn the_table_is_the_identity_domains_the_code_hashes_under() {
let domains: &[&[u8]] = &[
b"wai:video-receipt-id\x01",
b"wai:video-manifest-receipt-id\x01",
b"wai:video-ssai-receipt-id\x01",
b"wai:video-keys-receipt-id\x01",
b"wai:video-package-receipt-id\x01",
b"wai:video-channel-receipt-id\x01",
b"wai:world-receipt-id\x01",
b"wai:film-receipt-id\x01",
b"wai:haptic-receipt-id\x01",
b"wai:score-receipt-id\x01",
b"wai:splat4d-receipt-id\x01",
b"wai:audio-receipt-id\x01",
b"wai:ternary-receipt-id\x01",
b"wai:asset-receipt-id\x01",
b"wai:energy-binding-id\x01",
b"moq-jwp:claim-link\x01",
b"moq-jwp:object\x01",
];
for d in domains {
assert!(profile_name_of_domain(d).is_some(), "{}", String::from_utf8_lossy(d));
}
let mut names: Vec<&str> = profiles().iter().map(|p| p.name).collect();
names.sort();
names.dedup();
assert_eq!(names.len(), profiles().len());
assert!(profile_name_of_domain(b"wai:phasor-compute-id\x01").is_none(), "a domain that identifies no figure");
// Exactly one profile names a group, and only the energy binding has no signer.
assert_eq!(profiles().iter().filter(|p| p.grouped).map(|p| p.name).collect::<Vec<_>>(), ["moq-jwp:object"]);
assert_eq!(profiles().iter().filter(|p| !p.ed25519_signer).map(|p| p.name).collect::<Vec<_>>(), ["wai:energy-binding-id"]);
}
#[test]
fn an_energy_claim_is_viewed_by_its_link_hash() {
use crate::claim_receipt::{EnergyClaim, SubjectType};
use crate::energy_binding::Coverage;
let c = ClaimReceipt::seal(
&key(0x22),
"m",
SubjectType::Object,
[1; 32],
0,
"2026-09-30T12:00:00Z",
ClaimBody::Energy(EnergyClaim::new(500, &chip(20_000), Coverage::Whole, "measured", "https://example.org/m", 1, "u", None)),
None,
)
.unwrap();
let v = c.figure_view().unwrap();
assert_eq!(v.profile, "moq-jwp:claim-link");
assert_eq!(v.identity, c.link_hash());
assert_eq!(v.figures, vec![Figure { index: None, joules_micro: 500, class: Some(chip(20_000)) }]);
assert_eq!(v.signer, Some(key(0x22).verifying_key().to_bytes()));
let mut bad = c.clone();
bad.sig[0] ^= 1;
assert!(bad.figure_view().is_none(), "a claim that does not verify shows no figure");
}
#[test]
fn a_binding_is_viewed_by_its_identity_and_has_no_signer() {
let body: serde_json::Value = serde_json::from_str(
r#"{"reconstructionHash":"4dbdc2b2b62cb00749785bc84202236dbc3777d74660611b8e58812f0cfde6c3","alg":"sha256","microjoules":10565,"acquisition":"ModelBased","coverage":"Whole","workUnits":48,"method":"https://transaction.science/wai/extensions/energy-measurement"}"#,
)
.unwrap();
let v = BindingBody(&body).figure_view().unwrap();
assert_eq!(v.profile, "wai:energy-binding-id");
let mut h = blake3::Hasher::new();
h.update(b"wai:energy-binding-id\x01");
h.update(crate::claim_receipt::jcs(&body).unwrap().as_bytes());
assert_eq!(v.identity, *h.finalize().as_bytes());
assert_eq!(v.signer, None);
assert_eq!(v.figures[0].class, Some(EnergyProvenance::ModelBased));
// A body holding an integer no canonical form holds has no identity.
let mut big = body.clone();
big["workUnits"] = serde_json::Value::from(1u64 << 53);
assert!(binding_identity(&big).is_none());
}
#[test]
fn a_delivered_object_is_viewed_at_its_index_within_its_group() {
use crate::jwp_delivery::{GroupBuilder, ObjectLabel};
let env = |i: u8| vec![i; 9];
let mut b = GroupBuilder::default();
b.add(crate::claim_receipt::object_content_hash(&env(0)), 300, "o", Some(ObjectLabel::Class(chip(20_000))));
b.add(crate::claim_receipt::object_content_hash(&env(1)), 200, "o", None);
let (g, objs) = b.finish(&key(0x11), "p", 1, 2, "2026-09-30T12:00:00Z", "2026-09-30T12:00:01Z", None, false).unwrap();
let v = DeliveredObject { object: &objs[1], group: &g, envelope: &env(1) }.figure_view().unwrap();
assert_eq!(v.profile, "moq-jwp:object");
assert_eq!(v.identity, objs[1].content_hash);
assert_eq!(v.group, Some(g.link_hash()));
assert_eq!(v.figures, vec![Figure { index: Some(1), joules_micro: 200, class: None }]);
let v = DeliveredObject { object: &objs[0], group: &g, envelope: &env(0) }.figure_view().unwrap();
assert_eq!(v.figures[0].class, Some(chip(20_000)));
assert!(DeliveredObject { object: &objs[0], group: &g, envelope: &env(9) }.figure_view().is_none());
}
#[cfg(feature = "world")]
#[test]
fn a_world_session_is_viewed_by_its_receipt_hash() {
use crate::world::{receipt::SessionReceipt, Checkpoint};
let cps = vec![Checkpoint { tick: 100, hash: [0x11; 32] }];
let r = SessionReceipt::build(&key(9), "did:key:pin", [0xab; 32], 3, &[], &cps, 12_345, 999, None);
let v = r.figure_view().unwrap();
assert_eq!((v.profile.as_str(), v.identity), ("wai:world-receipt-id", r.receipt_hash()));
assert_eq!(v.figures[0], Figure { index: None, joules_micro: 12_345, class: None });
let l = r.with_energy_class(&key(9), chip(15_403)).unwrap();
let v = l.figure_view().unwrap();
assert_eq!((v.identity, v.figures[0].class.clone()), (l.receipt_hash(), Some(chip(15_403))));
let mut bad = l.clone();
bad.sig[0] ^= 1;
assert!(bad.figure_view().is_none());
}
#[test]
fn a_provenance_receipt_is_viewed_step_by_step() {
use crate::provenance::{AssetReceipt, ProvStep};
let steps = vec![
ProvStep::new("capture", [0; 32], [0x10; 32], 1, 0),
ProvStep::new("encode.png", [0x10; 32], [0x20; 32], 1, 4_200).with_energy_class(chip(15_403)).unwrap(),
];
let r = AssetReceipt::build(&key(9), "did:web:pin#prov", [0x20; 32], "image", "wai.image.png", 0, steps, None);
let v = r.figure_view().unwrap();
assert_eq!((v.profile.as_str(), v.identity), ("wai:asset-receipt-id", r.receipt_hash()));
assert_eq!(v.figure(Some(0)).unwrap().class, None);
assert_eq!(v.figure(Some(1)), Some(&Figure { index: Some(1), joules_micro: 4_200, class: Some(chip(15_403)) }));
assert_eq!(v.figure(Some(2)), None);
}
#[cfg(feature = "video_receipt")]
#[test]
fn the_video_receipt_is_viewed_by_its_receipt_hash() {
let p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/vectors/int_video/int_video_bitstream.json");
let bytes = std::fs::read(p).unwrap();
let bs: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
let r = crate::video_receipt::VideoReceipt::seal_decode(&key(7), "m", &bytes, &bs, 4_200, Some(chip(30_000)), None).unwrap();
let v = r.figure_view().unwrap();
assert_eq!((v.profile.as_str(), v.identity), ("wai:video-receipt-id", r.receipt_hash()));
assert_eq!(v.figures[0].class, Some(chip(30_000)));
}
#[cfg(feature = "video_manifest")]
#[test]
fn the_ott_receipts_are_viewed_under_their_profiles() {
use crate::wai_video_manifest::{ManifestReceipt, Segment, SegmentKind, SessionClass};
let class = SessionClass {
content_id: "asset".into(),
entitlement: "tier".into(),
geo: "US".into(),
blackouts: vec![],
ad_pod_id: "pod".into(),
window: 7,
};
let segments = vec![Segment { index: 0, kind: SegmentKind::Content, uri: "seg0.ts".into(), duration_ms: 6000, ad_id: None }];
let grant = wai_quantum::quantum_ops::GrantRef {
grant_hash: [1; 32],
capability: "video.manifest".into(),
joule_ceiling_micro: 1_000_000,
funds_ceiling: None,
};
let r = ManifestReceipt::seal(&key(3), "o", class, segments, 300, 9_000, Some(EnergyProvenance::ModelBased), grant, None);
let v = r.figure_view().unwrap();
assert_eq!((v.profile.as_str(), v.identity), ("wai:video-manifest-receipt-id", r.receipt_hash()));
assert_eq!(v.figures[0].class, Some(EnergyProvenance::ModelBased));
// The other OTT families' profiles are the receipt-id domains they hash under.
for (name, domain) in [
("wai:video-ssai-receipt-id", &b"wai:video-ssai-receipt-id\x01"[..]),
("wai:video-keys-receipt-id", b"wai:video-keys-receipt-id\x01"),
("wai:video-package-receipt-id", b"wai:video-package-receipt-id\x01"),
("wai:video-channel-receipt-id", b"wai:video-channel-receipt-id\x01"),
] {
assert_eq!(profile_name_of_domain(domain), Some(name));
}
}
#[cfg(feature = "quantum_receipt")]
#[test]
fn a_quantum_receipt_is_viewed_in_either_form() {
use wai_quantum::quantum::{Circuit, Measure};
use wai_quantum::quantum_energy::{Labelled, MaybeLabelled};
use wai_quantum::quantum_receipt::QuantumReceipt;
let mut c = Circuit::new(3);
c.h(0).cx(0, 1);
let r = QuantumReceipt::seal(&key(1), "did:key:pin", &c, Some(Measure { seed: 7, shots: 1024 }), 117_000, None).unwrap();
let u = MaybeLabelled::Unlabelled(r.clone());
let v = u.figure_view().unwrap();
// The profile name is pinned by literal: it is read through the quantum crate's
// labelling trait, so a change there must fail here.
assert_eq!(v.profile, "wai:quantum-receipt-id");
assert_eq!((v.identity, v.figures[0].class.clone()), (r.receipt_hash(), None));
let q = crate::energy_class::to_quantum_class(&chip(15_403)).unwrap();
let l = MaybeLabelled::Labelled(Labelled::seal(r, q, &key(1)).unwrap());
let v = l.figure_view().unwrap();
assert_eq!((v.identity, v.figures[0].class.clone()), (l.receipt_hash(), Some(chip(15_403))));
}
#[cfg(feature = "quantum_ops")]
#[test]
fn the_quantum_ops_receipts_name_their_profiles() {
use wai_quantum::quantum_energy::MaybeLabelled;
use wai_quantum::quantum_ops::*;
let grant = |cap: &str| GrantRef { grant_hash: content_hash(b"g"), capability: cap.into(), joule_ceiling_micro: 5_000_000, funds_ceiling: Some(100) };
fn view<R: wai_quantum::quantum_energy::Labellable>(r: R) -> String {
MaybeLabelled::Unlabelled(r).figure_view().expect("verifies").profile
}
let k = key(1);
let job = JobReceipt::seal(&k, "p", "wai.quantum.circuit", content_hash(b"c"), content_hash(b"r"), None, 1000, 250, grant("quantum.job.run"), None);
let mit = MitigationReceipt::seal(&k, "p", "wai.quantum.circuit", "zne.richardson", "ZZ", content_hash(b"raw"), None, 744_178, 912_680, 20_971, 48_000, 900_000, grant("quantum.mitigate"), None);
let dec = DecodeReceipt::seal(&k, "p", "toric:L7", "relay-bp.min-sum", content_hash(b"H"), content_hash(b"s"), content_hash(b"c"), true, 3, 46_656, 900_000, grant("quantum.decode"), None);
let cmp = CompileReceipt::seal(&k, "p", "clifford-tableau", content_hash(b"s"), content_hash(b"t"), content_hash(b"cpl"), true, 3, 4096, 300_000, grant("quantum.compile"), None);
let rea = RearrangeReceipt::seal(&k, "p", "hungarian-lsap", content_hash(b"i"), content_hash(b"t"), content_hash(b"p"), true, 41, 318, 450_000, grant("quantum.rearrange"), None);
let cal = CalibrationReceipt::seal(&k, "p", "lab:rig", "q3", content_hash(b"cfg"), vec![], 1_000, grant("quantum.calibrate"), None);
assert_eq!(view(job), "wai:quantum-job-id");
assert_eq!(view(mit), "wai:quantum-mitigate-id");
assert_eq!(view(dec), "wai:quantum-decode-id");
assert_eq!(view(cmp), "wai:quantum-compile-id");
assert_eq!(view(rea), "wai:quantum-rearrange-id");
assert_eq!(view(cal), "wai:quantum-calibration-id");
}
#[cfg(feature = "quantum_phasor_meter")]
#[test]
fn a_phasor_receipt_names_its_profile() {
use wai_quantum::quantum_energy::MaybeLabelled;
use wai_quantum::quantum_phasor_meter::{computation_id, result_fingerprint, PhasorReceipt, PhasorWork};
let work = PhasorWork { n_features: 200, input_dim: 2, n_samples: 300 };
let r = PhasorReceipt::seal(&key(1), "p", "wai.quantum.kernel", computation_id("wai.quantum.kernel", &[0.5]), result_fingerprint(&[0.1]), work, 117_000, None);
assert_eq!(MaybeLabelled::Unlabelled(r).figure_view().unwrap().profile, "wai:phasor-receipt-id");
}
#[cfg(feature = "quantum_qbom")]
#[test]
fn a_bill_of_materials_is_viewed_entry_by_entry() {
use wai_quantum::quantum_energy::LabelledQbom;
use wai_quantum::quantum_qbom::QbomBuilder;
let q = QbomBuilder::new("job", [0x51; 32], "sim").stage("compile", [0x52; 32], 120_000).stage("execute", [0x53; 32], 90_000).seal(&key(1), "p");
let v = q.figure_view().unwrap();
assert_eq!((v.profile.as_str(), v.identity), ("wai:quantum-qbom-id", q.receipt_hash()));
assert_eq!(v.figure(Some(1)), Some(&Figure { index: Some(1), joules_micro: 90_000, class: None }));
let c = crate::energy_class::to_quantum_class(&chip(15_403)).unwrap();
let l = LabelledQbom::seal(q, vec![Some(c), None], &key(1)).unwrap();
let v = l.figure_view().unwrap();
assert_eq!(v.identity, l.receipt_hash());
assert_eq!(v.figure(Some(0)).unwrap().class, Some(chip(15_403)));
assert_eq!(v.figure(Some(1)).unwrap().class, None);
}
mod meter_reports {
use super::*;
use crate::energy_figure::meter_report::{default_path, emit, parse_args, ReportArgs};
use crate::energy_report::{BracketSummary, BracketValues, BuildRecord};
fn live(n: u64, mean_uw: u64, sd_uw: u64) -> BracketSummary {
BracketSummary { n, mean_uw, sd_uw, live_intervals: n, longest_live_run: n }
}
fn bracket() -> BracketValues {
BracketValues {
idle: live(10, 20_000_000, 115_471),
busy: live(14, 30_000_000, 115_471),
window_us: 3_000_000,
completions: 60,
in_flight: 10,
opening: crate::energy_class::WindowOpening::FromRest,
}
}
#[test]
fn a_report_is_written_by_default_beside_the_receipt() {
assert_eq!(default_path(Some("out/r.json")), Some("out/r.report.json".into()));
assert_eq!(default_path(Some("receipt")), Some("receipt.report.json".into()));
assert_eq!(default_path(None), None, "no receipt file: the report goes to standard output");
let a = parse_args(&["x.wscr".into(), "--no-report".into(), "r.json".into(), "--previous-report".into(), "p.json".into()]);
assert_eq!(a, Ok(ReportArgs { positional: vec!["x.wscr".into(), "r.json".into()], no_report: true, previous_report: Some("p.json".into()) }));
assert!(!parse_args(&["x".into()]).unwrap().no_report, "on unless opted out");
// A missing value is an error, not a report with no parent.
assert!(parse_args(&["x".into(), "--previous-report".into()]).is_err(), "at the end");
assert!(parse_args(&["x".into(), "--previous-report".into(), "--no-report".into()]).is_err(), "followed by an option");
}
/// A report on a world session, sealed from a bracket: it checks against the
/// receipt, chains to the previous report, and leaves the receipt's bytes as they were.
#[cfg(feature = "world")]
#[test]
fn a_meter_report_checks_against_its_receipt_and_changes_nothing() {
use crate::world::{receipt::SessionReceipt, Checkpoint};
let b = bracket();
let f = b.figure().unwrap();
let class = EnergyProvenance::OnChipCounter {
uncertainty: Uncertainty { relative_ppm: b.least_relative_ppm(f).unwrap(), window_us: b.window_us },
filtering: CounterFiltering::Unknown,
};
let cps = vec![Checkpoint { tick: 1, hash: [1; 32] }];
let r = SessionReceipt::build(&key(9), "s", [2; 32], 0, &[], &cps, f, 1, None).with_energy_class(&key(9), class).unwrap();
let (hash, json) = (r.receipt_hash(), r.to_json());
let view = r.figure_view().unwrap();
let first = emit(&view, None, Some(b), BuildRecord::of_this_build(), "reader:example", &key(9), "s", None, "2026-09-30T12:00:00Z").unwrap();
first.verify().unwrap();
assert_eq!(first.check_figure_against(Some(&view)), Ok(()));
let second = emit(&view, None, Some(b), BuildRecord::of_this_build(), "reader:example", &key(9), "s", Some(&first), "2026-09-30T12:00:01Z").unwrap();
assert_eq!(second.check_parent(Some(&first)), Ok(()));
assert_eq!((r.receipt_hash(), r.to_json()), (hash, json), "the receipt is unchanged by a report about it");
// Another key's report is refused before it is written.
assert!(emit(&view, None, Some(b), BuildRecord::of_this_build(), "reader:example", &key(8), "s", None, "2026-09-30T12:00:00Z").is_err());
// An unmetered receipt gets none.
let u = SessionReceipt::build(&key(9), "s", [2; 32], 0, &[], &cps, 0, 1, None);
assert!(emit(&u.figure_view().unwrap(), None, None, BuildRecord::of_this_build(), "reader:example", &key(9), "s", None, "2026-09-30T12:00:00Z").is_err());
}
/// One report per labelled provenance step, by index.
#[test]
fn a_provenance_step_gets_its_own_report() {
use crate::provenance::{AssetReceipt, ProvStep};
let model = EnergyProvenance::ModelBased;
let steps = vec![ProvStep::new("capture", [0; 32], [1; 32], 1, 0), ProvStep::new("encode", [1; 32], [2; 32], 1, 77).with_energy_class(model.clone()).unwrap()];
let r = AssetReceipt::build(&key(9), "s", [2; 32], "image", "wai.image.png", 0, steps, None);
let view = r.figure_view().unwrap();
let rep = emit(&view, Some(1), None, BuildRecord::of_this_build(), "reader:example", &key(9), "s", None, "2026-09-30T12:00:00Z").unwrap();
assert_eq!(rep.check_figure_against(Some(&view)), Ok(()));
assert!(emit(&view, Some(0), None, BuildRecord::of_this_build(), "reader:example", &key(9), "s", None, "2026-09-30T12:00:00Z").is_err(), "an unmetered step gets none");
}
}

View file

@ -18,13 +18,27 @@
//! register readings, and the integration window it
//! actually measured over. It never yields the legacy `HwShunt`.
//! - A [`ModelMeter`] multiplies elapsed time by a calibrated power figure and
//! can only ever yield [`EnergyProvenance::ModelBased`] — it has no way to
//! claim a measurement.
//! - When a meter cannot measure — no hardware, no counter, no span started —
//! [`Meter::stop`] returns `None`. It never falls back to a fabricated figure,
//! because a plausible number with the wrong label is worse than no number:
//! the caller then either meters with a model and says so, or seals unmetered
//! (`0` / `None`), which the receipts already accept.
//! can only ever yield [`EnergyProvenance::ModelBased`]: its figure is a
//! model's output, labelled as one, and never presented as a measurement.
//! - When a meter cannot measure — no hardware, no counter, no span started, a
//! counter whose rail was not live, a model that gives nothing —
//! [`Meter::stop`] returns `None`. It never falls back to a fabricated
//! figure, because a plausible number with the wrong label is worse than no
//! number: the caller then either meters another way and says so, or seals
//! the receipt with the unmetered marker (`0` with no class,
//! `extensions/energy-measurement.md` §2). The marker is not a figure of zero
//! joules, and no meter here returns a reading of zero.
//!
//! # A live counter
//!
//! A counter that is not being refreshed, or is refreshed in batches, can show
//! a delta between two reads that is no measurement of the span: nothing at
//! all, or one batch's worth. So a counter meter reads the counter during the
//! span as well as at its ends ([`CounterMeter::sample`]), and refuses the span
//! unless the counter advanced over it as a live rail does
//! (energy-measurement §3.1, [`crate::energy_report::Liveness`]): in two
//! consecutive intervals, in at least five, in at least four of every five,
//! over at least 300 ms. Two reads alone are one interval, and never enough.
//!
//! Both meters take their source injected — a counter reader, a clock — so the
//! delta and wraparound arithmetic is testable without hardware, and so the
@ -49,9 +63,12 @@ use core::num::NonZeroU32;
use crate::energy_class::{
AcquisitionError, AcquisitionKind, CounterFiltering, Uncertainty, REGISTER_DELTA_UNCERTAINTY_UJ,
};
use crate::energy_report::Liveness;
use crate::wai_video_manifest::EnergyProvenance;
/// A measured span: the energy, and the class of the measurement.
/// A reading over a span: the energy, and its class — measured or modelled, as
/// the class states. Never zero: a span with no figure is `None` at
/// [`Meter::stop`], and the receipt carries the unmetered marker instead.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EnergyReading {
pub joules_micro: u64,
@ -65,7 +82,8 @@ impl EnergyReading {
}
/// What to seal when there was no measurement at all — `(0, None)`, the
/// only unmetered form the receipts accept.
/// unmetered marker (energy-measurement §2): a `0` that means "not
/// metered", never a figure of zero joules.
pub fn unmetered() -> (u64, Option<EnergyProvenance>) {
(0, None)
}
@ -101,8 +119,12 @@ pub struct CounterMeter {
wrap_at: u64,
relative_ppm: NonZeroU32,
source: CounterSource,
/// `(counter, clock)` at the start of the span.
start: Option<(u64, u64)>,
/// `(counter, clock)` at each read of the span: its start, every
/// [`CounterMeter::sample`], and nothing yet of its stop. Empty with no span
/// started.
reads: Vec<(u64, u64)>,
/// A read during the span failed, so the span cannot be measured.
broken: bool,
}
impl CounterMeter {
@ -132,10 +154,46 @@ impl CounterMeter {
wrap_at,
relative_ppm,
source: CounterSource::OnChip { filtering: CounterFiltering::Unknown },
start: None,
reads: Vec::new(),
broken: false,
}
}
/// Read the counter during a span, as one interval's boundary. A span
/// with too few of these is refused at [`Meter::stop`]: two reads are one
/// interval, and the liveness rule needs at least five advancing ones
/// (energy-measurement §3.1). Does nothing with no span started.
pub fn sample(&mut self) {
if self.reads.is_empty() || self.broken {
return;
}
match (self.read)() {
Some(c) => {
let t = (self.clock_micros)();
self.reads.push((c, t));
}
None => self.broken = true,
}
}
/// Probe whether the counter is live before trusting it: a span of
/// `intervals` intervals, `pause` between reads, that must pass the
/// liveness rule. The probe's reading is discarded.
pub fn probe(&mut self, intervals: u32, mut pause: impl FnMut()) -> bool {
self.start();
for _ in 1..intervals {
pause();
self.sample();
}
pause();
self.stop().is_some()
}
/// The counter delta between two reads, across one wrap at `wrap_at`.
fn delta(&self, from: u64, to: u64) -> u64 {
if to >= from { to - from } else { self.wrap_at.saturating_sub(from).saturating_add(to) }
}
/// An external instrument that exposes a cumulative energy register and
/// holds a current calibration. `calibration_ref` identifies the
/// certificate (a URI, or the issuing laboratory's certificate number);
@ -182,8 +240,25 @@ impl CounterMeter {
/// Where filtering may be active, the advisory's guidance states the added noise as typically
/// under 2 % standard deviation over 1 s and 5–15 % over 100 ms
/// (INTEL-SA-00389); `relative_ppm` should cover it at the windows used.
///
/// The counter is probed before the meter is returned: six intervals of
/// 60 ms, which must pass the liveness rule (energy-measurement §3.1). A
/// counter that is present but frozen, or refreshed in batches, gives no
/// meter either.
#[cfg(not(target_arch = "wasm32"))]
pub fn rapl(domain: &str, relative_ppm: NonZeroU32) -> Option<CounterMeter> {
CounterMeter::rapl_with_probe(domain, relative_ppm, |m| {
m.probe(6, || std::thread::sleep(std::time::Duration::from_millis(60)))
})
}
/// [`CounterMeter::rapl`] with the liveness probe supplied by the caller.
#[cfg(not(target_arch = "wasm32"))]
pub fn rapl_with_probe(
domain: &str,
relative_ppm: NonZeroU32,
probe: impl FnOnce(&mut CounterMeter) -> bool,
) -> Option<CounterMeter> {
let base = format!("/sys/class/powercap/{domain}");
let energy = format!("{base}/energy_uj");
let range = format!("{base}/max_energy_range_uj");
@ -191,12 +266,13 @@ impl CounterMeter {
let wrap_at = read_u64_file(&range)?.saturating_add(1);
read_u64_file(&energy)?;
let origin = std::time::Instant::now();
Some(CounterMeter::new(
let mut m = CounterMeter::new(
move || read_u64_file(&energy),
move || origin.elapsed().as_micros() as u64,
wrap_at,
relative_ppm,
))
);
probe(&mut m).then_some(m)
}
}
@ -207,18 +283,31 @@ fn read_u64_file(path: &str) -> Option<u64> {
impl Meter for CounterMeter {
fn start(&mut self) {
self.broken = false;
let counter = (self.read)();
let t = (self.clock_micros)();
self.start = counter.map(|c| (c, t));
self.reads = counter.map(|c| vec![(c, t)]).unwrap_or_default();
}
fn stop(&mut self) -> Option<EnergyReading> {
let (started, t0) = self.start.take()?;
let mut reads = std::mem::take(&mut self.reads);
if reads.is_empty() || std::mem::take(&mut self.broken) {
return None;
}
let now = (self.read)()?;
let window_us = (self.clock_micros)().saturating_sub(t0);
// Energy counters are monotonic until they wrap at their range.
let delta =
if now >= started { now - started } else { self.wrap_at - started + now };
let t = (self.clock_micros)();
reads.push((now, t));
let window_us = t.saturating_sub(reads[0].1);
// Energy counters are monotonic until they wrap at their range; each
// interval is read across at most one wrap.
let deltas: Vec<u64> = reads.windows(2).map(|w| self.delta(w[0].0, w[1].0)).collect();
// The counter must have advanced as a live rail does over the span
// (energy-measurement §3.1). One interval — a read at each end — never
// passes, nor does a counter that is frozen or moves in one batch.
if !Liveness::from_advances(deltas.iter().map(|d| *d > 0)).is_live_over(window_us) {
return None;
}
let delta = deltas.iter().try_fold(0u64, |a, d| a.checked_add(*d))?;
// A zero delta or a zero window is a failure to measure, not a
// measurement of nothing; sealing either would be refused downstream
// anyway, so it is refused here where the cause is known.
@ -254,9 +343,13 @@ impl Meter for CounterMeter {
}
}
/// Elapsed time multiplied by a calibrated power figure. A real number, and
/// honestly labelled as derived — it cannot claim a hardware class however it
/// is configured.
/// Elapsed time multiplied by a calibrated power figure: a modelled figure,
/// labelled `ModelBased`, never a measurement. It cannot claim a hardware class
/// however it is configured, and it never yields a figure of zero: a model
/// that gives nothing — a zero power, a span too short to reach a microjoule —
/// or a product too large to represent gives `None`, never a labelled zero
/// (which the receipts refuse) or a saturated maximum (which is not the
/// model's output).
pub struct ModelMeter {
clock_micros: Box<dyn FnMut() -> u64>,
milliwatts: u64,
@ -287,7 +380,10 @@ impl Meter for ModelMeter {
let t0 = self.t0.take()?;
let dt_micros = (self.clock_micros)().saturating_sub(t0);
// uJ = mW * us / 1000 (mW*us = nJ; /1000 -> uJ)
let joules_micro = self.milliwatts.saturating_mul(dt_micros) / 1000;
let joules_micro = self.milliwatts.checked_mul(dt_micros)? / 1000;
if joules_micro == 0 {
return None;
}
Some(EnergyReading { joules_micro, provenance: EnergyProvenance::ModelBased })
}
@ -317,16 +413,37 @@ mod tests {
NonZeroU32::new(n).unwrap()
}
/// A span over `n_reads` reads: the first at start, the last at stop, the
/// rest as samples.
fn span(m: &mut CounterMeter, n_reads: usize) -> Option<EnergyReading> {
m.start();
for _ in 2..n_reads {
m.sample();
}
m.stop()
}
/// A meter over the counter values `counter`, read every `step_us`.
fn meter(counter: &[u64], step_us: u64, wrap_at: u64, p: u32) -> CounterMeter {
let clock: Vec<u64> = (0..counter.len() as u64).map(|i| 10 + i * step_us).collect();
CounterMeter::new(scripted(counter.iter().copied().map(Some).collect()), scripted_clock(clock), wrap_at, ppm(p))
}
/// A span over `counter` read every `step_us`.
fn measured(counter: &[u64], step_us: u64, wrap_at: u64, p: u32) -> Option<EnergyReading> {
span(&mut meter(counter, step_us, wrap_at, p), counter.len())
}
/// Six reads 200 ms apart, advancing by `per` µJ each: five live intervals
/// over a second.
fn live(start: u64, per: u64) -> Vec<u64> {
(0..6).map(|i| start + i * per).collect()
}
#[test]
fn counter_measures_a_real_delta_as_an_on_chip_reading_with_its_uncertainty() {
let mut m = CounterMeter::new(
scripted(vec![Some(1_000), Some(4_500)]),
scripted_clock(vec![10, 1_000_010]),
1 << 32,
ppm(20_000),
);
m.start();
let r = m.stop().unwrap();
let mut m = meter(&live(1_000, 700), 200_000, 1 << 32, 20_000);
let r = span(&mut m, 6).unwrap();
assert_eq!(r.joules_micro, 3_500);
// 2 % of 3,500 µJ is 70 µJ; with the 1/√6 µJ resolution of a register
// delta in quadrature, 70.0012 µJ, which is 20,000.34 ppm, rounded up.
@ -344,13 +461,11 @@ mod tests {
#[test]
fn a_known_filtering_state_is_recorded_and_the_default_is_unknown() {
let mut m = CounterMeter::new(scripted(vec![Some(0), Some(9)]), scripted_clock(vec![0, 100_000]), 1 << 32, ppm(150_000))
.with_filtering(CounterFiltering::On);
m.start();
match m.stop().unwrap().provenance {
let mut m = meter(&live(0, 2), 80_000, 1 << 32, 150_000).with_filtering(CounterFiltering::On);
match span(&mut m, 6).unwrap().provenance {
EnergyProvenance::OnChipCounter { filtering, uncertainty } => {
assert_eq!(filtering, CounterFiltering::On);
assert_eq!(uncertainty.window_us, 100_000);
assert_eq!(uncertainty.window_us, 400_000);
}
other => panic!("unexpected class {other:?}"),
}
@ -362,9 +477,11 @@ mod tests {
CounterMeter::calibrated_instrument(scripted(vec![]), scripted_clock(vec![]), 1 << 32, ppm(5_000), " "),
Err(AcquisitionError::NoCalibrationRef)
));
let reads = live(100, 120);
let clock: Vec<u64> = (0..6).map(|i| i * 12_000_000).collect();
let mut m = CounterMeter::calibrated_instrument(
scripted(vec![Some(100), Some(700)]),
scripted_clock(vec![0, 60_000_000]),
scripted(reads.iter().copied().map(Some).collect()),
scripted_clock(clock),
1 << 32,
ppm(5_000),
"lab-cert-0412",
@ -372,8 +489,7 @@ mod tests {
.unwrap()
.with_filtering(CounterFiltering::On); // no effect on an instrument
assert_eq!(m.kind(), AcquisitionKind::CalibratedInstrument);
m.start();
let r = m.stop().unwrap();
let r = span(&mut m, 6).unwrap();
assert_eq!(
r.provenance,
EnergyProvenance::CalibratedInstrument {
@ -386,32 +502,28 @@ mod tests {
#[test]
fn a_short_span_declares_its_register_resolution() {
// A 3 µJ delta: the operator's 2 % is 0.06 µJ, far below the 1/√6 µJ
// the two whole-microjoule readings leave. Declaring 2 % alone would
// state less than the integer can carry, and the receipts refuse that.
let mut m = CounterMeter::new(scripted(vec![Some(10), Some(13)]), scripted_clock(vec![0, 500]), 1 << 32, ppm(20_000));
m.start();
let r = m.stop().unwrap();
assert_eq!(r.joules_micro, 3);
// A 5 µJ delta, the least a live span can show (one µJ in each of five
// intervals): the operator's 2 % is 0.1 µJ, below the 1/√6 µJ the two
// whole-microjoule end readings leave. Declaring 2 % alone would state
// less than the integer can carry, and the receipts refuse that.
let r = measured(&live(10, 1), 100_000, 1 << 32, 20_000).unwrap();
assert_eq!(r.joules_micro, 5);
let u = r.provenance.uncertainty().unwrap();
assert_eq!(u.relative_ppm, 137_545);
assert_eq!(u.relative_ppm, 84_064);
assert_eq!(crate::energy_class::check_resolution(r.joules_micro, &r.provenance), Ok(()));
assert!(crate::energy_class::figure_and_class_cohere(r.joules_micro, Some(&r.provenance)));
// Every span meets the floor, whatever the operator declared.
for delta in [1u64, 2, 5, 17, 288, 289, 1_000, 288_676, 10_000_000] {
let mut m = CounterMeter::new(scripted(vec![Some(0), Some(delta)]), scripted_clock(vec![0, 1]), u64::MAX, ppm(1));
m.start();
let r = m.stop().unwrap();
assert_eq!(crate::energy_class::check_resolution(r.joules_micro, &r.provenance), Ok(()), "delta {delta}");
for per in [1u64, 2, 5, 17, 288, 289, 1_000, 288_676, 10_000_000] {
let r = measured(&live(0, per), 100_000, u64::MAX, 1).unwrap();
assert_eq!(crate::energy_class::check_resolution(r.joules_micro, &r.provenance), Ok(()), "per {per}");
}
}
#[test]
fn counter_handles_wraparound() {
// Range 10_000 (wrap_at 10_000): 9_800 -> 200 is a 400 uJ span.
let mut m = CounterMeter::new(scripted(vec![Some(9_800), Some(200)]), scripted_clock(vec![0, 5]), 10_000, ppm(1));
m.start();
assert_eq!(m.stop().unwrap().joules_micro, 400);
// Range 10_000 (wrap_at 10_000): 9_800 -> 200 crosses the wrap in the
// third interval; every interval advances by 100 µJ.
assert_eq!(measured(&[9_700, 9_800, 9_900, 0, 100, 200], 100_000, 10_000, 1).unwrap().joules_micro, 500);
}
#[test]
@ -424,16 +536,66 @@ mod tests {
// And a stop with no start is likewise nothing.
let mut m2 = CounterMeter::new(scripted(vec![Some(5)]), scripted_clock(vec![0]), 1 << 32, ppm(1));
assert!(m2.stop().is_none());
// A read that fails during the span breaks the span.
let mut m3 = CounterMeter::new(
scripted(vec![Some(0), Some(1), None, Some(3), Some(4), Some(5)]),
scripted_clock((0..6).map(|i| i * 100_000).collect()),
1 << 32,
ppm(1),
);
assert!(span(&mut m3, 6).is_none());
}
#[test]
fn a_zero_delta_or_a_zero_window_is_not_a_measurement() {
let mut m = CounterMeter::new(scripted(vec![Some(7), Some(7)]), scripted_clock(vec![0, 1_000]), 1 << 32, ppm(1));
assert!(measured(&[7; 6], 200_000, 1 << 32, 1).is_none(), "no energy counted is a failure to measure");
assert!(measured(&live(7, 2), 0, 1 << 32, 1).is_none(), "no window is not an integration");
}
// ── fault injection: a counter whose rail is not live (§3.1) ──
/// Two reads are one interval: the delta a counter shows between them is
/// no evidence that it was refreshed in between. Before the liveness rule
/// this sealed a 3,500 µJ OnChipCounter reading.
#[test]
fn two_reads_are_never_a_live_span() {
let mut m = CounterMeter::new(scripted(vec![Some(1_000), Some(4_500)]), scripted_clock(vec![10, 1_000_010]), 1 << 32, ppm(20_000));
m.start();
assert!(m.stop().is_none(), "no energy counted is a failure to measure");
let mut m = CounterMeter::new(scripted(vec![Some(7), Some(9)]), scripted_clock(vec![50, 50]), 1 << 32, ppm(1));
m.start();
assert!(m.stop().is_none(), "no window is not an integration");
assert!(m.stop().is_none());
}
#[test]
fn a_frozen_counter_is_refused() {
assert!(measured(&[5_000; 11], 100_000, 1 << 32, 20_000).is_none());
}
#[test]
fn a_single_refresh_lump_is_refused() {
assert!(measured(&[5_000, 5_000, 5_000, 9_000, 9_000, 9_000, 9_000, 9_000, 9_000, 9_000, 9_000], 100_000, 1 << 32, 20_000).is_none());
}
#[test]
fn a_two_step_refresh_is_refused() {
// Two consecutive advances, and nothing else in ten intervals.
assert!(measured(&[5_000, 5_000, 5_000, 7_000, 9_000, 9_000, 9_000, 9_000, 9_000, 9_000, 9_000], 100_000, 1 << 32, 20_000).is_none());
}
#[test]
fn a_live_counter_over_too_short_a_span_is_refused() {
assert!(measured(&live(0, 100), 59_000, 1 << 32, 20_000).is_none(), "five live intervals over 295 ms");
assert!(measured(&live(0, 100), 60_000, 1 << 32, 20_000).is_some(), "over 300 ms");
}
#[test]
fn the_probe_refuses_a_counter_that_is_not_live() {
let mut frozen = meter(&[1; 6], 60_000, 1 << 32, 20_000);
assert!(!frozen.probe(5, || {}), "five intervals: six reads");
let mut ok = meter(&live(0, 3), 60_000, 1 << 32, 20_000);
assert!(ok.probe(5, || {}));
// The probe's span is discarded: a stop after it measures nothing.
assert!(ok.stop().is_none());
// An absent interface gives no meter, whatever the probe would say.
assert!(CounterMeter::rapl_with_probe("no-such-domain", ppm(20_000), |_| true).is_none());
}
#[test]
@ -448,12 +610,46 @@ mod tests {
assert_eq!(m.kind(), AcquisitionKind::ModelBased);
}
#[test]
fn model_meter_never_yields_a_labelled_zero() {
// A span too short to reach a microjoule, a zero power, a zero span:
// a model that gives nothing is None, never a ModelBased zero.
for (clock, mw) in [(vec![0, 400], 2), (vec![0, 1_000_000], 0), (vec![5, 5], 2_000)] {
let mut m = ModelMeter::new(scripted_clock(clock), mw);
m.start();
assert_eq!(m.stop(), None);
}
}
#[test]
fn model_meter_refuses_overflow() {
let mut m = ModelMeter::new(scripted_clock(vec![0, u64::MAX]), 2);
m.start();
assert_eq!(m.stop(), None, "a product past u64 is refused, not saturated to u64::MAX / 1000");
let mut m = ModelMeter::new(scripted_clock(vec![0, u64::MAX / 2]), 2);
m.start();
assert_eq!(m.stop().unwrap().joules_micro, (u64::MAX / 2 * 2) / 1000);
}
#[test]
fn model_meter_is_never_hardware() {
let meters = [ModelMeter::new(scripted_clock(vec![0, 1_000_000]), 1_000), ModelMeter::wall_clock(1_000)];
for mut m in meters {
assert_eq!(m.kind(), AcquisitionKind::ModelBased);
m.start();
if let Some(r) = m.stop() {
assert_eq!(r.provenance, EnergyProvenance::ModelBased);
assert!(!r.provenance.is_hardware());
}
}
}
#[test]
fn seal_args_round_trip_to_the_receipt_contract() {
let p = crate::energy_class::test_on_chip();
let r = EnergyReading { joules_micro: 42, provenance: p.clone() };
assert_eq!(r.as_seal_args(), (42, Some(p)));
// The only unmetered form the receipts accept.
// The unmetered marker the receipts accept.
assert_eq!(EnergyReading::unmetered(), (0, None));
}

View file

@ -0,0 +1,808 @@
//! The integers a measurement report carries, and the arithmetic every reader
//! applies to them (`extensions/energy-measurement.md` §3.1 and §6.2).
//!
//! A figure obtained under the bracketed protocol of §3 is published with the
//! values its bracket gave: each bracket's sample count, mean and standard
//! deviation, the window, the completion count, the thread count and how the
//! window opened, and the facts that show the power reader's rail was live
//! (§3.1). This module holds those values as integers and computes from them,
//! exactly, the figure they give and the least uncertainty they support. A
//! producer seals the figure and the declaration this module computes, so a
//! verifier holding the same integers reaches the same two numbers: no
//! floating-point operation is involved, and the products are evaluated at
//! whatever width they need.
//!
//! The values describe a measurement and are the measurer's statements. What
//! the arithmetic checks is that the figure and the declaration are what the
//! values give. It cannot check that the values were measured (§5).
//!
//! # Liveness
//!
//! A power reader can return a figure for a rail that is not being refreshed:
//! a frozen rail reads as a flat zero, or holds its last value, and a rail
//! refreshed in batches publishes one lump, sometimes in two steps, inside a
//! bracket that does not move. A marginal computed from such a bracket is not
//! a measurement of the work. A power sample counts as an advance only when it
//! is above zero and differs from the sample before it, so a held value never
//! advances. A rail is **live** over a bracket only if it advanced in two
//! consecutive intervals, in at least five intervals, and in at least four
//! intervals of every five (§3.1). [`Liveness`] computes those facts and the
//! rule; the report carries them per bracket, and a figure sealed under a
//! hardware class needs both brackets live. A bracket whose standard deviation
//! is zero has samples that are all equal, so at most one of them advanced;
//! its facts cannot show a live rail, and a reader refuses it whatever facts
//! it carries ([`BracketSummary::is_live`]).
#![deny(clippy::float_arithmetic)]
use crate::energy_class::WindowOpening;
/// The largest integer a report carries: I-JSON's exact-integer bound, as a
/// claim's (`jwp-receipts.md` §2.4.3). Crate-private so the C header does not
/// gain a generic name.
const MAX_INTEGER: u64 = (1 << 53) - 1;
/// The least number of intervals in which a rail must advance (§3.1).
const MIN_LIVE_INTERVALS: u64 = 5;
/// The least run of consecutive advancing intervals (§3.1).
const MIN_LIVE_RUN: u64 = 2;
/// The least span, in microseconds, of a bracket whose rail is called live
/// (§3.1). Checked by the producer, which holds the bracket's timing; a
/// report's idle bracket carries no span.
const MIN_LIVE_SPAN_US: u64 = 300_000;
/// Whether a rail advanced, interval by interval, and what that adds up to.
///
/// An interval is one sample of a power reader, or one pair of successive
/// reads of an energy counter. For a power reader a sample "advanced" when it
/// is strictly above zero and differs from the sample before it (the first
/// sample of a bracket has none before it, and advanced when it is above zero);
/// for a counter, when the second read exceeds the first (modulo the counter's
/// wrap).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Liveness {
/// The intervals the bracket spans.
pub intervals: u64,
/// The intervals in which the rail advanced.
pub live_intervals: u64,
/// The longest run of consecutive intervals in which it advanced.
pub longest_live_run: u64,
}
impl Liveness {
/// The facts of a sequence of intervals, each `true` where the rail
/// advanced.
pub fn from_advances<I: IntoIterator<Item = bool>>(advances: I) -> Liveness {
let mut l = Liveness::default();
let mut run = 0u64;
for a in advances {
l.intervals += 1;
if a {
l.live_intervals += 1;
run += 1;
l.longest_live_run = l.longest_live_run.max(run);
} else {
run = 0;
}
}
l
}
/// The facts of a bracket of power samples: an interval advanced where its
/// sample is strictly above zero and differs from the one before it. A
/// reader that holds its last value between refreshes repeats it, and a
/// repeated value is not an advance; nor is a sample that is not a number.
pub fn of_power_samples(samples_w: &[f64]) -> Liveness {
let mut previous: Option<f64> = None;
Liveness::from_advances(samples_w.iter().map(|w| {
let advanced = *w > 0.0 && previous.is_none_or(|p| p != *w);
previous = Some(*w);
advanced
}))
}
/// §3.1's rule over the facts alone: at least two consecutive advances, at
/// least five advances, and advances in at least four intervals of every
/// five. One advance is not a live rail, and neither is one refresh
/// published in two steps inside a bracket of zeros. Facts that cannot
/// describe one sequence — more live intervals than intervals, or a run
/// longer than the live intervals — are not live either.
pub fn is_live(&self) -> bool {
let (n, live, run) = (self.intervals as u128, self.live_intervals as u128, self.longest_live_run as u128);
run <= live
&& live <= n
&& run >= MIN_LIVE_RUN as u128
&& live >= MIN_LIVE_INTERVALS as u128
&& live * 5 >= n * 4
}
/// [`Liveness::is_live`] together with the producer's span rule: the
/// bracket lasted at least 300 ms (§3.1).
pub fn is_live_over(&self, span_us: u64) -> bool {
self.is_live() && span_us >= MIN_LIVE_SPAN_US
}
}
/// One bracket of a report, as integers (§6.2).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BracketSummary {
/// The sample count.
pub n: u64,
/// The mean, in microwatts, rounded to the nearest with a half rounding up.
pub mean_uw: u64,
/// The sample standard deviation (divisor `n − 1`), in microwatts, rounded
/// up.
pub sd_uw: u64,
/// The samples in which the reader's rail advanced (§3.1).
pub live_intervals: u64,
/// The longest run of consecutive samples in which it advanced (§3.1).
pub longest_live_run: u64,
}
impl BracketSummary {
/// This bracket's liveness facts, over `n` intervals.
pub fn liveness(&self) -> Liveness {
Liveness { intervals: self.n, live_intervals: self.live_intervals, longest_live_run: self.longest_live_run }
}
/// Whether these values show a live rail (§3.1): the facts meet
/// [`Liveness::is_live`], and the samples are not all equal. A standard
/// deviation of zero means every sample repeated the first, and a repeated
/// sample is not an advance, so facts that claim a live rail beside it
/// contradict the bracket they describe.
pub fn is_live(&self) -> bool {
self.sd_uw > 0 && self.liveness().is_live()
}
}
/// The values a report carries for a figure obtained under §3.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BracketValues {
pub idle: BracketSummary,
pub busy: BracketSummary,
/// `T`: the busy bracket's window, in microseconds rounded to the nearest.
pub window_us: u64,
/// `C`: completions counted inside the window.
pub completions: u64,
/// `N`: the thread count.
pub in_flight: u64,
pub opening: WindowOpening,
}
/// The first rule of energy-measurement §6.2 (jwp-receipts §2.4.8 rule 5) that
/// a report's bracket breaks, in the order the rules are checked.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ReportDefect {
/// A bracket with fewer than two samples.
TooFewSamples,
/// Under a hardware class, a bracket whose rail was not live (§3.1).
RailNotLive,
/// A zero window, completion count or thread count.
EmptyBurst,
/// A busy mean not above the idle mean.
NoMarginal,
/// Under a hardware class, a marginal below the idle standard deviation:
/// a ratio below 1, which §4 makes a failure to measure.
BelowAttributionFloor,
/// The figure is not the one the bracket gives.
FigureMismatch,
/// Under a hardware class, a declared window other than the bracket's.
WindowMismatch,
/// Under a hardware class, a declared uncertainty below the least the
/// bracket supports.
DeclarationBelowBracket,
}
impl ReportDefect {
/// The conformance reason (jwp-receipts §3.1).
pub fn reason(self) -> &'static str {
match self {
ReportDefect::TooFewSamples => "measurement_too_few_samples",
ReportDefect::RailNotLive => "measurement_rail_not_live",
ReportDefect::EmptyBurst => "measurement_empty_burst",
ReportDefect::NoMarginal => "measurement_no_marginal",
ReportDefect::BelowAttributionFloor => "measurement_below_attribution_floor",
ReportDefect::FigureMismatch => "measurement_figure_mismatch",
ReportDefect::WindowMismatch => "measurement_window_mismatch",
ReportDefect::DeclarationBelowBracket => "measurement_declaration_below_bracket",
}
}
}
/// §4's verdict, derived from a report's values (§6.2). No report states it.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Attribution {
/// `M ≥ 10 · s_idle`: publishable as an absolute figure.
Clean,
/// `M ≥ 3 · s_idle`: sound for comparing two workloads measured back to
/// back, not as an absolute figure.
RelativeOnly,
/// `M ≥ s_idle`: not publishable.
Noisy,
/// Below that: a failure to measure.
Unusable,
}
impl Attribution {
pub fn label(self) -> &'static str {
match self {
Attribution::Clean => "clean — publishable",
Attribution::RelativeOnly => "usable for relative comparison — NOT publishable",
Attribution::Noisy => "NOISY — do not publish",
Attribution::Unusable => "UNUSABLE — the box was not idle, or the reader saw no draw",
}
}
}
impl BracketValues {
fn in_range(&self) -> bool {
[
self.idle.n,
self.idle.mean_uw,
self.idle.sd_uw,
self.idle.live_intervals,
self.idle.longest_live_run,
self.busy.n,
self.busy.mean_uw,
self.busy.sd_uw,
self.busy.live_intervals,
self.busy.longest_live_run,
self.window_us,
self.completions,
self.in_flight,
]
.iter()
.all(|v| *v <= MAX_INTEGER)
}
/// `M`: the busy mean less the idle mean, in microwatts, when it is
/// positive.
pub fn marginal_uw(&self) -> Option<u64> {
self.busy.mean_uw.checked_sub(self.idle.mean_uw).filter(|m| *m > 0)
}
/// `(Wn, Wd)`: the work the window paid for is `Wn / Wd` (§2.3).
fn work(&self) -> (u128, u128) {
let (c, n) = (self.completions as u128, self.in_flight as u128);
match self.opening {
WindowOpening::FromRest => (2 * c + n, 2),
WindowOpening::Running => (c, 1),
}
}
/// Whether the marginal is at least the idle standard deviation: §4's
/// ratio of at least 1.
pub fn meets_attribution_floor(&self) -> bool {
self.marginal_uw().is_some_and(|m| m >= self.idle.sd_uw)
}
/// §4's verdict from these values, or `None` with no positive marginal.
/// A flat idle bracket (`s_idle = 0`) under a positive marginal is
/// `Clean` by the arithmetic; whether the reader's rail was live is a
/// separate rule ([`BracketValues::check`]), and a flat bracket never
/// passes it ([`BracketSummary::is_live`]).
pub fn attribution(&self) -> Option<Attribution> {
let m = self.marginal_uw()? as u128;
let s = self.idle.sd_uw as u128;
Some(if m >= 10 * s {
Attribution::Clean
} else if m >= 3 * s {
Attribution::RelativeOnly
} else if m >= s {
Attribution::Noisy
} else {
Attribution::Unusable
})
}
/// `F`: the per-unit estimate `M·T·Wd / (10⁶·Wn)` µJ rounded to the nearest
/// microjoule, a half rounding up (§6.2). `None` when the bracket has no
/// positive marginal, no window, no completion or no thread, a value above
/// 2⁵³ − 1, or a figure of zero or above 2⁵³ − 1.
pub fn figure(&self) -> Option<u64> {
if !self.in_range() || self.window_us == 0 || self.completions == 0 || self.in_flight == 0 {
return None;
}
let m = self.marginal_uw()? as u128;
let (wn, wd) = self.work();
let t = self.window_us as u128;
let num = 2 * m * t * wd + 1_000_000 * wn;
let den = 2_000_000 * wn;
let f = num / den;
(f > 0 && f <= MAX_INTEGER as u128).then_some(f as u64)
}
/// `K` and `P` of §6.2 for the figure `figure`: a declaration of `p` ppm is
/// supported exactly when `p²·K ≥ P`.
fn k_and_p(&self, figure: u64) -> Option<(Nat, Nat)> {
if !self.in_range() || figure == 0 {
return None;
}
let m = Nat::from(self.marginal_uw()? as u128);
let (wn, wd) = self.work();
let (wn, wd) = (Nat::from(wn), Nat::from(wd));
let t = Nat::from(self.window_us as u128);
let n = Nat::from(self.in_flight as u128);
let (ni, nb) = (Nat::from(self.idle.n as u128), Nat::from(self.busy.n as u128));
let (si, sb) = (Nat::from(self.idle.sd_uw as u128), Nat::from(self.busy.sd_uw as u128));
let f = Nat::from(figure as u128);
let ninb = ni.mul(&nb);
let wn2 = wn.mul(&wn);
let wn4 = wn2.mul(&wn2);
let t2wd2 = t.mul(&t).mul(&wd).mul(&wd);
let k = Nat::from(12).mul(&ninb).mul(&wn4).mul(&f).mul(&f);
let spread = si.mul(&si).mul(&nb).add(&sb.mul(&sb).mul(&ni));
let p_power = Nat::from(12).mul(&wn2).mul(&t2wd2).mul(&spread);
let p_count = Nat::from(4).mul(&ninb).mul(&m).mul(&m).mul(&t2wd2).mul(&n).mul(&n);
let p_res = Nat::from(1_000_000_000_000).mul(&ninb).mul(&wn4);
Some((k, p_power.add(&p_count).add(&p_res)))
}
/// Whether a declaration of `relative_ppm` on `figure` covers the
/// uncertainty the values give: `p²·K ≥ P` (§6.2). `None` when the values
/// give no figure to declare for.
pub fn supports(&self, figure: u64, relative_ppm: u32) -> Option<bool> {
let (k, p) = self.k_and_p(figure)?;
let pp = Nat::from(relative_ppm as u128).mul(&Nat::from(relative_ppm as u128));
Some(pp.mul(&k) >= p)
}
/// The least `p` the values support for `figure`, or `None` where no
/// `u32` declaration covers it (§2.7) or there is no figure. The least `p`
/// always meets the resolution floor, `figure · p ≥ 288 676` (§2.3).
pub fn least_relative_ppm(&self, figure: u64) -> Option<u32> {
let (k, p) = self.k_and_p(figure)?;
let holds = |x: u32| Nat::from(x as u128).mul(&Nat::from(x as u128)).mul(&k) >= p;
if !holds(u32::MAX) {
return None;
}
let (mut lo, mut hi) = (1u32, u32::MAX);
while lo < hi {
let mid = lo + (hi - lo) / 2;
if holds(mid) {
hi = mid;
} else {
lo = mid + 1;
}
}
Some(lo)
}
/// Every rule of jwp-receipts §2.4.8 rule 5, in its order, for a report of
/// `figure` declared under `hardware` — `Some((relative_ppm, window_us))`
/// for a hardware class, `None` for any other. The first rule broken is
/// returned.
pub fn check(&self, figure: u64, hardware: Option<(u32, u64)>) -> Result<(), ReportDefect> {
if self.idle.n < 2 || self.busy.n < 2 {
return Err(ReportDefect::TooFewSamples);
}
if hardware.is_some() && !(self.idle.is_live() && self.busy.is_live()) {
return Err(ReportDefect::RailNotLive);
}
if self.window_us == 0 || self.completions == 0 || self.in_flight == 0 {
return Err(ReportDefect::EmptyBurst);
}
if self.marginal_uw().is_none() {
return Err(ReportDefect::NoMarginal);
}
if hardware.is_some() && !self.meets_attribution_floor() {
return Err(ReportDefect::BelowAttributionFloor);
}
if self.figure() != Some(figure) {
return Err(ReportDefect::FigureMismatch);
}
if let Some((ppm, window_us)) = hardware {
if window_us != self.window_us {
return Err(ReportDefect::WindowMismatch);
}
if self.supports(figure, ppm) != Some(true) {
return Err(ReportDefect::DeclarationBelowBracket);
}
}
Ok(())
}
}
/// How the code a figure measured was built (energy-measurement §5).
///
/// The vocabulary is `release` and `debug`; a finer one is a new registration
/// (jwp-receipts §2.4.4).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BuildProfile {
/// Optimised as shipped: cargo's `release` profile at `opt-level` 2 or 3.
Release,
/// Any other build.
Debug,
}
impl BuildProfile {
pub fn label(self) -> &'static str {
match self {
BuildProfile::Release => "release",
BuildProfile::Debug => "debug",
}
}
pub fn from_label(s: &str) -> Option<BuildProfile> {
match s {
"release" => Some(BuildProfile::Release),
"debug" => Some(BuildProfile::Debug),
_ => None,
}
}
/// The profile this crate was built under, as `build.rs` recorded it from
/// cargo's `PROFILE` and `OPT_LEVEL` (`src/build_info.rs`).
pub fn of_this_build() -> BuildProfile {
BuildProfile::from_label(env!("WAI_BUILD_PROFILE")).unwrap_or(BuildProfile::Debug)
}
}
/// The build a figure measured: its profile, and where known the compiler
/// and the target (energy-measurement §5, §8).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BuildRecord {
pub profile: BuildProfile,
/// The compiler's version line, e.g. `rustc 1.98.0 (…)`.
pub toolchain: Option<String>,
/// The target triple.
pub target: Option<String>,
}
impl BuildRecord {
/// This crate's build, as `build.rs` recorded it. A part `build.rs` could
/// not determine is `None`.
pub fn of_this_build() -> BuildRecord {
let nonempty = |s: &str| (!s.is_empty()).then(|| s.to_owned());
BuildRecord {
profile: BuildProfile::of_this_build(),
toolchain: option_env!("WAI_BUILD_TOOLCHAIN").and_then(nonempty),
target: option_env!("WAI_BUILD_TARGET").and_then(nonempty),
}
}
}
/// An unsigned integer of any width, little-endian in 64-bit limbs with no
/// high zero limb — only what §6.2's products need: multiply, add, compare.
#[derive(Clone, Debug, PartialEq, Eq)]
struct Nat(Vec<u64>);
impl Nat {
fn from(v: u128) -> Nat {
let mut n = Nat(vec![v as u64, (v >> 64) as u64]);
n.trim();
n
}
fn trim(&mut self) {
while self.0.last() == Some(&0) {
self.0.pop();
}
}
fn mul(&self, o: &Nat) -> Nat {
if self.0.is_empty() || o.0.is_empty() {
return Nat(Vec::new());
}
let mut out = vec![0u64; self.0.len() + o.0.len()];
for (i, a) in self.0.iter().enumerate() {
let mut carry = 0u128;
for (j, b) in o.0.iter().enumerate() {
let t = (*a as u128) * (*b as u128) + out[i + j] as u128 + carry;
out[i + j] = t as u64;
carry = t >> 64;
}
let mut k = i + o.0.len();
while carry > 0 {
let t = out[k] as u128 + carry;
out[k] = t as u64;
carry = t >> 64;
k += 1;
}
}
let mut n = Nat(out);
n.trim();
n
}
fn add(&self, o: &Nat) -> Nat {
let len = self.0.len().max(o.0.len());
let mut out = Vec::with_capacity(len + 1);
let mut carry = 0u128;
for i in 0..len {
let t = *self.0.get(i).unwrap_or(&0) as u128 + *o.0.get(i).unwrap_or(&0) as u128 + carry;
out.push(t as u64);
carry = t >> 64;
}
if carry > 0 {
out.push(carry as u64);
}
let mut n = Nat(out);
n.trim();
n
}
#[cfg(test)]
fn bits(&self) -> u32 {
match self.0.last() {
None => 0,
Some(top) => (self.0.len() as u32 - 1) * 64 + (64 - top.leading_zeros()),
}
}
}
impl PartialOrd for Nat {
fn partial_cmp(&self, o: &Nat) -> Option<core::cmp::Ordering> {
Some(self.cmp(o))
}
}
impl Ord for Nat {
fn cmp(&self, o: &Nat) -> core::cmp::Ordering {
self.0.len().cmp(&o.0.len()).then_with(|| self.0.iter().rev().cmp(o.0.iter().rev()))
}
}
/// The §6.2 arithmetic, pinned against values `claim-conformance/claim_verify.py`
/// computes with Python's integers — code that shares nothing with this module.
/// Returns the checks that fail; empty means the two agree.
pub fn check_bracket_arithmetic() -> Vec<String> {
let mut fails = Vec::new();
for (name, v, f, p) in pinned_brackets() {
if v.figure() != Some(f) {
fails.push(format!("{name}: figure {:?}, pinned {f}", v.figure()));
}
if v.least_relative_ppm(f) != Some(p) {
fails.push(format!("{name}: least ppm {:?}, pinned {p}", v.least_relative_ppm(f)));
}
if v.supports(f, p) != Some(true) || v.supports(f, p - 1) != Some(false) {
fails.push(format!("{name}: {p} ppm is not the least supported"));
}
}
fails
}
/// The brackets [`check_bracket_arithmetic`] pins: `(name, values, F, p)`, as
/// `claim_verify.py`'s `bracket_fixture()` prints them.
fn pinned_brackets() -> Vec<(&'static str, BracketValues, u64, u32)> {
let s = |n: u64, mean_uw: u64, sd_uw: u64| BracketSummary { n, mean_uw, sd_uw, live_intervals: n, longest_live_run: n };
let v = |idle, busy, window_us, completions, in_flight, opening| BracketValues {
idle,
busy,
window_us,
completions,
in_flight,
opening,
};
use WindowOpening::{FromRest, Running};
let x = MAX_INTEGER;
vec![
("unit_from_rest", v(s(4, 20_000_000, 115_471), s(4, 30_000_000, 115_471), 3_000_000, 60, 10, FromRest), 461_538, 45_156),
("unit_running", v(s(4, 20_000_000, 115_471), s(4, 30_000_000, 115_471), 3_000_000, 60, 10, Running), 500_000, 96_571),
("sealed_from_samples", v(s(4, 20_000_000, 115_471), s(4, 29_930_000, 115_471), 2_007_000, 1_000, 10, FromRest), 19_830, 8_711),
("one_microjoule", v(s(5, 20_000_000, 200_000), s(5, 30_000_000, 500_000), 2_815_000, 50_000_000, 10, FromRest), 1, 288_994),
("half_up", v(s(4, 20_000_000, 1_000), s(4, 21_000_000, 1_000), 2_500_000, 1_000_000, 1, Running), 3, 96_227),
("wide", v(s(10, 20_000_000, 115_471), s(14, 30_000_000, 115_471), 100_000_000_000, 1_000_000_000_000, 10, FromRest), 1, 288_715),
("at_bounds", v(s(x, 0, x), s(x, x, x), x, x, x, FromRest), 6_004_799_503, 192_451),
]
}
#[cfg(test)]
mod tests {
use super::*;
fn s(n: u64, mean_uw: u64, sd_uw: u64) -> BracketSummary {
BracketSummary { n, mean_uw, sd_uw, live_intervals: n, longest_live_run: n }
}
fn unit(opening: WindowOpening) -> BracketValues {
BracketValues {
idle: s(10, 20_000_000, 115_471),
busy: s(14, 30_000_000, 115_471),
window_us: 3_000_000,
completions: 60,
in_flight: 10,
opening,
}
}
/// The arithmetic of §6.2 against the literals the independent script
/// computes: the figure, the least declaration, and that one ppm less is
/// not supported — including products of 151, 211 and 429 bits.
#[test]
fn pinned_brackets() {
assert_eq!(check_bracket_arithmetic(), Vec::<String>::new());
// Each case's widest product, so a narrower implementation is known to
// be out of its depth here, not merely wrong.
let bits: Vec<u32> = super::pinned_brackets()
.iter()
.map(|(_, v, f, p)| {
let (k, pp) = v.k_and_p(*f).unwrap();
Nat::from(*p as u128).mul(&Nat::from(*p as u128)).mul(&k).bits().max(pp.bits())
})
.collect();
assert_eq!(bits, vec![105, 103, 107, 151, 124, 211, 429]);
}
#[test]
fn half_rounds_up_and_from_rest_divides_by_the_midpoint() {
// E = 2.5 uJ exactly: a half rounds up, never to even.
let h = super::pinned_brackets().into_iter().find(|c| c.0 == "half_up").unwrap();
assert_eq!(h.1.figure(), Some(3));
// From rest the divisor is C + N/2 = 65, not C = 60.
assert_eq!(unit(WindowOpening::FromRest).figure(), Some(461_538));
assert_eq!(unit(WindowOpening::Running).figure(), Some(500_000));
}
#[test]
fn least_meets_the_resolution_floor() {
for c in [1u64, 2, 3, 7, 50, 1_000, 60_000, 1_000_000, 50_000_000] {
for t in [1_000u64, 200_000, 2_815_000, 30_000_000] {
for opening in [WindowOpening::FromRest, WindowOpening::Running] {
let v = BracketValues { completions: c, window_us: t, opening, ..unit(opening) };
let Some(f) = v.figure() else { continue };
let p = v.least_relative_ppm(f).unwrap();
assert!(f as u128 * p as u128 >= 288_676, "C={c} T={t}: F={f} p={p}");
assert_eq!(v.supports(f, p), Some(true));
if p > 1 {
assert_eq!(v.supports(f, p - 1), Some(false));
}
}
}
}
}
#[test]
fn defects_are_reported_in_order() {
let hw = Some((45_156u32, 3_000_000u64));
let ok = unit(WindowOpening::FromRest);
let f = ok.figure().unwrap();
let p = ok.least_relative_ppm(f).unwrap();
assert_eq!(ok.check(f, Some((p, 3_000_000))), Ok(()));
// Two defects at once: the earlier rule is reported.
let two = BracketValues { idle: s(1, 20_000_000, 0), completions: 0, ..ok };
assert_eq!(two.check(f, hw), Err(ReportDefect::TooFewSamples));
let frozen = BracketValues { busy: BracketSummary { live_intervals: 1, longest_live_run: 1, ..ok.busy }, completions: 0, ..ok };
assert_eq!(frozen.check(f, hw), Err(ReportDefect::RailNotLive));
assert_eq!(frozen.check(f, None), Err(ReportDefect::EmptyBurst), "liveness binds the hardware classes only");
let flat = BracketValues { busy: s(14, 20_000_000, 1), ..ok };
assert_eq!(flat.check(f, None), Err(ReportDefect::NoMarginal));
let noisy = BracketValues { idle: s(10, 20_000_000, 10_000_001), ..ok };
assert_eq!(noisy.check(f, hw), Err(ReportDefect::BelowAttributionFloor));
assert_eq!(ok.check(f + 1, Some((p, 3_000_000))), Err(ReportDefect::FigureMismatch));
assert_eq!(ok.check(f, Some((p, 3_000_001))), Err(ReportDefect::WindowMismatch));
assert_eq!(ok.check(f, Some((p - 1, 3_000_000))), Err(ReportDefect::DeclarationBelowBracket));
assert_eq!(ok.check(f, None), Ok(()), "a non-hardware class declares nothing to check");
}
#[test]
fn attribution_thresholds() {
let at = |m: u64, sd: u64| {
BracketValues { idle: s(10, 1_000, sd), busy: s(14, 1_000 + m, 5), ..unit(WindowOpening::FromRest) }.attribution()
};
assert_eq!(at(100, 10), Some(Attribution::Clean));
assert_eq!(at(99, 10), Some(Attribution::RelativeOnly));
assert_eq!(at(30, 10), Some(Attribution::RelativeOnly));
assert_eq!(at(29, 10), Some(Attribution::Noisy));
assert_eq!(at(10, 10), Some(Attribution::Noisy));
assert_eq!(at(9, 10), Some(Attribution::Unusable));
assert_eq!(at(1, 0), Some(Attribution::Clean), "a flat idle under a positive marginal, by the arithmetic");
assert_eq!(at(0, 0), None, "no marginal, no verdict");
}
/// §3.1's liveness rule: necessary conditions each, and the cases a
/// refreshed-in-batches rail produces.
#[test]
fn a_rail_is_live_only_over_enough_consecutive_advances() {
let l = |s: &str| Liveness::from_advances(s.chars().map(|c| c == '1'));
assert!(l("1111111111").is_live());
assert!(l("1111111110").is_live(), "9 of 10");
assert!(l("1111011110").is_live(), "8 of 10, runs of 4");
assert!(l("1110111011").is_live(), "8 of 10, longest run 3");
assert!(!l("0000000000").is_live(), "a frozen rail");
assert!(!l("0000100000").is_live(), "one refresh lump");
assert!(!l("0000110000").is_live(), "one refresh published in two steps: two consecutive advances alone are not enough");
assert!(!l("1010101010").is_live(), "no two consecutive advances");
assert!(!l("1111").is_live(), "fewer than five advances");
assert!(!l("1111100000").is_live(), "five of ten is below four in five");
assert!(!Liveness { intervals: 10, live_intervals: 11, longest_live_run: 2 }.is_live(), "more live than intervals");
assert!(!Liveness { intervals: 10, live_intervals: 8, longest_live_run: 9 }.is_live(), "a run longer than the live intervals");
assert!(l("11111").is_live_over(300_000) && !l("11111").is_live_over(299_999));
assert_eq!(Liveness::of_power_samples(&[0.0, 1.0, 2.0, f64::NAN, 3.0]), Liveness { intervals: 5, live_intervals: 3, longest_live_run: 2 });
}
/// A reader that holds its last non-zero value between refreshes: a rail
/// stuck at 5 W, refreshed once to 12 W while the burst runs. Every sample
/// is above zero, so a rule that counted a non-zero sample as an advance
/// would call both brackets live; a repeated value is not an advance.
#[test]
fn a_held_value_is_not_an_advance() {
let idle = [5.0; 10];
let busy = [5.0, 5.0, 5.0, 12.0, 12.0, 12.0, 12.0, 12.0, 12.0, 12.0, 12.0, 12.0, 12.0, 12.0];
assert_eq!(Liveness::of_power_samples(&idle), Liveness { intervals: 10, live_intervals: 1, longest_live_run: 1 });
assert_eq!(Liveness::of_power_samples(&busy), Liveness { intervals: 14, live_intervals: 2, longest_live_run: 1 });
assert!(!Liveness::of_power_samples(&idle).is_live() && !Liveness::of_power_samples(&busy).is_live());
// A live reader whose readings move every sample is unaffected.
assert!(Liveness::of_power_samples(&[5.0, 5.1, 5.0, 5.2, 4.9, 5.0, 5.1]).is_live());
// Two equal readings in a row break a run; one in five may repeat.
assert_eq!(Liveness::of_power_samples(&[5.0, 5.1, 5.1, 5.2, 5.3, 5.4]), Liveness { intervals: 6, live_intervals: 5, longest_live_run: 3 });
}
/// A reader of the report refuses a flat bracket under a hardware class,
/// whatever liveness facts it carries: equal samples mean at most one
/// advance, so facts that say otherwise are not the bracket's.
#[test]
fn a_flat_bracket_is_never_live() {
let hw = Some((45_156u32, 3_000_000u64));
let ok = unit(WindowOpening::FromRest);
let f = ok.figure().unwrap();
assert!(ok.idle.is_live() && ok.busy.is_live());
// The stuck-rail report: a flat idle at 5 W with full liveness facts, a
// busy bracket with one refresh in it. The arithmetic calls it clean.
let stuck = BracketValues { idle: s(10, 5_000_000, 0), busy: s(14, 10_000_000, 3_631_365), ..ok };
assert_eq!(stuck.attribution(), Some(Attribution::Clean));
let fs = stuck.figure().unwrap();
assert!(!stuck.idle.is_live(), "sd 0 is not a live rail");
assert_eq!(stuck.check(fs, hw), Err(ReportDefect::RailNotLive));
assert_eq!(stuck.check(fs, None), Ok(()), "liveness binds the hardware classes only");
let flat_busy = BracketValues { busy: s(14, 30_000_000, 0), ..ok };
assert_eq!(flat_busy.check(f, hw), Err(ReportDefect::RailNotLive));
}
#[test]
fn nat_products_agree_with_u128() {
let xs = [0u128, 1, 7, u64::MAX as u128, 1 << 64, (1 << 100) + 12345];
for a in xs {
for b in xs {
if let Some(p) = a.checked_mul(b) {
assert_eq!(Nat::from(a).mul(&Nat::from(b)), Nat::from(p));
}
if let Some(q) = a.checked_add(b) {
assert_eq!(Nat::from(a).add(&Nat::from(b)), Nat::from(q));
}
assert_eq!(Nat::from(a).cmp(&Nat::from(b)), a.cmp(&b));
}
}
}
/// The products against an independent big-integer library, where one is
/// in the build.
#[cfg(feature = "oer")]
#[test]
fn wide_matches_biguint() {
use num_bigint::BigUint;
let to_big = |n: &Nat| {
n.0.iter().rev().fold(BigUint::from(0u8), |acc, limb| (acc << 64u32) + BigUint::from(*limb))
};
for (_, v, f, p) in super::pinned_brackets() {
let (k, pp) = v.k_and_p(f).unwrap();
let ws = |x: u64| BigUint::from(x);
let (wn, wd) = v.work();
let (wn, wd) = (BigUint::from(wn), BigUint::from(wd));
let m = ws(v.marginal_uw().unwrap());
let t = ws(v.window_us);
let (ni, nb, si, sb) = (ws(v.idle.n), ws(v.busy.n), ws(v.idle.sd_uw), ws(v.busy.sd_uw));
let k_want = ws(12) * &ni * &nb * wn.pow(4) * ws(f) * ws(f);
let p_want = ws(12) * wn.pow(2) * t.pow(2) * wd.pow(2) * (&si * &si * &nb + &sb * &sb * &ni)
+ ws(4) * &ni * &nb * m.pow(2) * t.pow(2) * wd.pow(2) * ws(v.in_flight).pow(2)
+ ws(1_000_000_000_000) * &ni * &nb * wn.pow(4);
assert_eq!(to_big(&k), k_want);
assert_eq!(to_big(&pp), p_want);
assert!(ws(p as u64).pow(2) * &k_want >= p_want);
}
}
#[test]
fn the_build_profile_vocabulary() {
assert_eq!(BuildProfile::from_label("release"), Some(BuildProfile::Release));
assert_eq!(BuildProfile::from_label("debug"), Some(BuildProfile::Debug));
assert_eq!(BuildProfile::from_label("Release"), None);
// This test binary is built under the dev profile at opt-level 0.
if cfg!(debug_assertions) && crate::build_info::profile_label("debug", "0") == "debug" {
assert_eq!(BuildProfile::of_this_build().label(), env!("WAI_BUILD_PROFILE"));
}
let r = BuildRecord::of_this_build();
assert!(r.target.as_deref().is_some_and(|t| t.contains('-')), "{r:?}");
assert!(r.toolchain.as_deref().is_some_and(|t| t.starts_with("rustc ")), "{r:?}");
}
}

View file

@ -214,7 +214,8 @@ mod tests {
impl Film {
/// Build a film receipt over this film's per-frame reconstruction blocks.
/// `joules_micro` is supplied by the meter (0 = unmetered).
/// `joules_micro` is supplied by the meter; `0` is the unmetered marker,
/// never a figure of zero joules (energy-measurement §2).
pub fn receipt(
&self,
signer: &SigningKey,

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,684 @@
//! The conformance corpus for labelled leaves, labelled groups, the group total
//! and the `WAI_RECEIPT` property (`jwp-conformance/vectors.json`,
//! jwp-receipts §5.7), regenerated and run.
//!
//! Every vector is synthetic: keys come from fixed 32-byte seeds (0x11
//! publisher, 0x22 meter, 0x33 relay) and attest nothing; envelopes, figures, origins and
//! calibration references are invented. Expected outcomes are declared here,
//! per vector, not copied from a verifier.
use super::*;
use crate::claim_receipt::{ClaimBody, ClaimReceipt, EnergyClaim, SubjectType};
use crate::energy_class::{CounterFiltering, Uncertainty};
use serde_json::json;
const T0: &str = "2026-09-30T12:00:00Z";
const T1: &str = "2026-09-30T12:00:01Z";
const PUBLISHER_ID: &str = "did:example:publisher";
const CAL_REF: &str = "https://calibration.example.org/certificates/0001";
/// The encoding of `y = p + 3` with the sign bit clear: a point of the curve, not
/// of small order, written with a `y` coordinate RFC 8032 §5.1.3 refuses (≥ p).
/// Decoders that reduce `y` read it as the point with `y = 3`.
const NON_CANONICAL_KEY: [u8; 32] = {
let mut k = [0xff; 32];
k[0] = 0xf0;
k[31] = 0x7f;
k
};
fn key(seed: u8) -> SigningKey {
SigningKey::from_bytes(&[seed; 32])
}
/// The envelope of object `n`: invented bytes standing in for a WAI envelope.
pub fn envelope(n: u8) -> Vec<u8> {
format!("WAI1 jwp-conformance object {n}").into_bytes()
}
fn origin(n: u8) -> String {
format!("dc-{n}.example")
}
fn chip(ppm: u32, window_us: u64) -> EnergyProvenance {
EnergyProvenance::OnChipCounter { uncertainty: Uncertainty { relative_ppm: ppm, window_us }, filtering: CounterFiltering::Unknown }
}
fn cal(ppm: u32, window_us: u64, r: &str) -> EnergyProvenance {
EnergyProvenance::CalibratedInstrument { uncertainty: Uncertainty { relative_ppm: ppm, window_us }, calibration_ref: r.into() }
}
fn lab(p: EnergyProvenance) -> Option<ObjectLabel> {
Some(ObjectLabel::Class(p))
}
/// A group of `parts`, envelopes numbered from `first`.
fn build(
first: u8,
parts: &[(u64, Option<ObjectLabel>)],
group_id: u64,
parent: Option<[u8; 32]>,
labelled: bool,
) -> (GroupReceipt, Vec<ObjectReceipt>) {
let mut b = GroupBuilder::default();
for (i, (j, l)) in parts.iter().enumerate() {
let n = first + i as u8;
b.add(object_content_hash(&envelope(n)), *j, &origin(n), l.clone());
}
b.finish(&key(0x11), PUBLISHER_ID, 7, group_id, T0, T1, parent, labelled).expect("the corpus's groups compose")
}
/// The mixed group, legacy form: a counter, an instrument, a legacy object, an
/// object explicitly unmetered and a model — five, so the last leaf of the
/// first level is duplicated. Its objects use envelopes 0–4.
pub fn mixed() -> (GroupReceipt, Vec<ObjectReceipt>) {
build(
0,
&[
(1_200, lab(chip(20_000, 1_000_000))),
(800, lab(cal(5_000, 2_000_000, CAL_REF))),
(500, None),
(0, Some(ObjectLabel::Unmetered)),
(300, lab(EnergyProvenance::ModelBased)),
],
3,
None,
false,
)
}
/// The labelled group: a counter, an instrument and an object explicitly
/// unmetered, chained to the mixed group, with the class they compose to.
/// Envelopes 10–12.
pub fn labelled() -> (GroupReceipt, Vec<ObjectReceipt>) {
build(
10,
&[(1_200, lab(chip(20_000, 1_000_000))), (800, lab(cal(5_000, 2_000_000, CAL_REF))), (0, Some(ObjectLabel::Unmetered))],
4,
Some(mixed().0.link_hash()),
true,
)
}
fn hexs(b: &[u8]) -> String {
hex(b)
}
struct V {
name: &'static str,
about: &'static str,
procedure: &'static str,
input: Value,
expected: Value,
}
fn v(name: &'static str, about: &'static str, procedure: &'static str, input: Value, expected: Value) -> V {
V { name, about, procedure, input, expected }
}
fn reject(step: Option<u8>, reason: Reason) -> Value {
match step {
Some(s) => json!({ "outcome": "reject", "step": s, "reason": reason.label() }),
None => json!({ "outcome": "reject", "reason": reason.label() }),
}
}
fn read(reading: &str) -> Value {
json!({ "outcome": "read", "reading": reading })
}
fn delivered_json(objs: &[(ObjectReceipt, Vec<u8>)]) -> Value {
Value::Array(objs.iter().map(|(o, e)| json!({ "object": o.to_json(), "envelope": hexs(e) })).collect())
}
fn with_envelopes(objs: &[ObjectReceipt], first: u8) -> Vec<(ObjectReceipt, Vec<u8>)> {
objs.iter().enumerate().map(|(i, o)| (o.clone(), envelope(first + i as u8))).collect()
}
fn group_accept(g: &GroupReceipt) -> Value {
json!({ "outcome": "accept", "link_hash": hexs(&g.link_hash()), "signing_payload": hexs(&g.signing_payload()) })
}
/// The reading's verdict string.
pub fn total_label(r: &TotalReading) -> String {
let kind = |k: AcquisitionKind| match k {
AcquisitionKind::HwShunt => "HwShunt",
AcquisitionKind::ModelBased => "ModelBased",
AcquisitionKind::Estimator => "Estimator",
AcquisitionKind::OnChipCounter => "OnChipCounter",
AcquisitionKind::CalibratedInstrument => "CalibratedInstrument",
};
match r {
TotalReading::Unmetered => "unmetered".into(),
TotalReading::Unlabelled => "unlabelled".into(),
TotalReading::Weakest { kind: k, coverage } => format!("weakest class={} coverage={}", kind(*k), coverage.label()),
TotalReading::Signed { class, coverage, checked } => {
let u = class.uncertainty().map_or(String::new(), |u| format!(" relative_ppm={} window_us={}", u.relative_ppm, u.window_us));
format!(
"signed class={}{u} coverage={} parts={}",
class.label(),
coverage.label(),
if *checked { "checked" } else { "unchecked" }
)
}
}
}
pub fn property_label(r: &PropertyReading) -> String {
match r {
PropertyReading::Unmetered => "unmetered".into(),
PropertyReading::Unsigned => "unsigned".into(),
PropertyReading::Signed { class, by } => format!("signed class={} by={}", class.label(), hexs(by)),
}
}
fn vectors() -> Vec<V> {
let (gm, om) = mixed();
let (gl, ol) = labelled();
let mut out = Vec::new();
// ── leaves (§2.2) ──
let leaf = |o: &ObjectReceipt| json!({ "leaf": hexs(&o.leaf()) });
out.extend([
v("leaf_legacy", "an object with no label: the legacy leaf over the content hash alone", "leaf",
json!({ "object": om[2].to_json() }), leaf(&om[2])),
v("leaf_labelled_unmetered", "tag 0 beside a zero figure: the object is explicitly unmetered and its origin is bound", "leaf",
json!({ "object": om[3].to_json() }), leaf(&om[3])),
v("leaf_labelled_on_chip_counter", "tag 4 with its declared uncertainty and filtering state", "leaf",
json!({ "object": om[0].to_json() }), leaf(&om[0])),
v("leaf_labelled_calibrated_instrument", "tag 5 with its declared uncertainty and length-prefixed calibration reference", "leaf",
json!({ "object": om[1].to_json() }), leaf(&om[1])),
v("leaf_labelled_model_based", "tag 2, followed by nothing", "leaf",
json!({ "object": om[4].to_json() }), leaf(&om[4])),
]);
// ── groups (§2.3, §3 steps 1-3) ──
let gm_json = gm.to_json();
let gl_json = gl.to_json();
let tampered = |g: &Value, k: &str, val: Value| {
let mut t = g.clone();
t[k] = val;
t
};
let sealed = |mut g: GroupReceipt, edit: &dyn Fn(&mut GroupReceipt)| {
edit(&mut g);
resign(g, &key(0x11)).to_json()
};
let s_plus_l = {
const L: [u8; 32] = [
0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0x10,
];
let mut g = gm.clone();
let mut carry = 0u16;
for (s, l) in g.sig[32..].iter_mut().zip(L) {
let t = u16::from(*s) + u16::from(l) + carry;
*s = t as u8;
carry = t >> 8;
}
assert_eq!(carry, 0, "S + L < 2^254");
g.to_json()
};
let mut flipped = gm.clone();
flipped.sig[0] ^= 1;
let identity = {
let mut k = [0u8; 32];
k[0] = 1;
hexs(&k)
};
#[allow(deprecated)]
let hw = EnergyProvenance::HwShunt;
let no_parent = {
let mut t = gm_json.clone();
t.as_object_mut().expect("a group is an object").remove("parent_receipt_hash");
t
};
out.extend([
v("group_legacy_accept", "the mixed group in the legacy payload: it signs the total and no class", "group",
json!({ "group": gm_json }), group_accept(&gm)),
v("group_labelled_accept", "the labelled payload: the total's class (OnChipCounter, the weaker of a counter and an instrument, 14,000 ppm over 2 s), coverage Partial (one part is zero) and the parent link, chained to the mixed group", "group",
json!({ "group": gl_json, "previous": gm_json }), group_accept(&gl)),
v("group_bad_signature", "one bit flipped in sig", "group",
json!({ "group": flipped.to_json() }), reject(Some(2), Reason::SignatureVerifyFailed)),
v("group_signature_s_not_reduced", "S replaced by S + L: the verification equation still holds, and the strict rule of jwp-receipts 3.1 step 3 refuses S >= L", "group",
json!({ "group": s_plus_l }), reject(Some(2), Reason::SignatureVerifyFailed)),
v("group_signer_key_identity", "the identity point as signer_pubkey", "group",
json!({ "group": tampered(&gm_json, "signer_pubkey", identity.into()) }), reject(Some(1), Reason::BadSignerKey)),
v("group_missing_parent_field", "parent_receipt_hash absent rather than null", "group",
json!({ "group": no_parent }),
reject(Some(1), Reason::Malformed)),
v("group_signer_key_non_canonical", "signer_pubkey encodes y = p + 3, a point not of small order, in a form RFC 8032 section 5.1.3 refuses (y >= p): every point has one encoding", "group",
json!({ "group": tampered(&gm_json, "signer_pubkey", hexs(&NON_CANONICAL_KEY).into()) }), reject(Some(1), Reason::BadSignerKey)),
v("group_sign_version_unknown", "a labelled group whose sign_version is 3: a version this extension does not define is not the labelled form", "group",
json!({ "group": tampered(&gl_json, "sign_version", 3.into()) }), reject(Some(1), Reason::GroupLabelInvalid)),
v("group_sign_version_legacy", "a legacy group carrying sign_version 1 and no class: a sign_version other than 2 is refused, never read as the legacy form", "group",
json!({ "group": tampered(&gm_json, "sign_version", 1.into()) }), reject(Some(1), Reason::GroupLabelInvalid)),
v("group_sign_version_string", "sign_version written as the string \"2\"", "group",
json!({ "group": tampered(&gl_json, "sign_version", "2".into()) }), reject(Some(1), Reason::GroupLabelInvalid)),
v("group_legacy_with_class_members", "a legacy group carrying energy_provenance: refused, never read as the legacy form", "group",
json!({ "group": tampered(&gm_json, "energy_provenance", "ModelBased".into()) }), reject(Some(1), Reason::GroupLabelInvalid)),
v("group_labelled_class_unknown", "a labelled group naming a class energy-measurement section 2 does not define", "group",
json!({ "group": tampered(&gl_json, "energy_provenance", "Measured".into()) }), reject(Some(1), Reason::GroupLabelInvalid)),
v("group_labelled_class_tampered", "the total's declared uncertainty rewritten smaller after signing", "group",
json!({ "group": tampered(&gl_json, "energy_uncertainty", json!({ "relative_ppm": 1_000, "window_us": 2_000_000 })) }),
reject(Some(2), Reason::SignatureVerifyFailed)),
v("group_labelled_coverage_tampered", "coverage rewritten from Partial to Whole after signing", "group",
json!({ "group": tampered(&gl_json, "energy_coverage", "Whole".into()) }), reject(Some(2), Reason::SignatureVerifyFailed)),
v("group_labelled_parent_tampered", "the parent link rewritten after signing: the labelled payload signs it", "group",
json!({ "group": tampered(&gl_json, "parent_receipt_hash", hexs(&[0xab; 32]).into()) }), reject(Some(2), Reason::SignatureVerifyFailed)),
v("group_labelled_hwshunt", "a labelled group whose total is classed legacy HwShunt, correctly signed", "group",
json!({ "group": sealed(gl.clone(), &|g| g.label.as_mut().unwrap().class = Some(hw.clone())) }), reject(Some(2), Reason::GroupLabelInvalid)),
v("group_labelled_class_beside_zero_total", "a class beside a zero total, correctly signed", "group",
json!({ "group": sealed(gl.clone(), &|g| g.joules_total = 0) }), reject(Some(2), Reason::GroupLabelInvalid)),
v("group_labelled_unmetered_beside_figure", "the unmetered marker beside a non-zero total, correctly signed", "group",
json!({ "group": sealed(gl.clone(), &|g| g.label = Some(GroupLabel { class: None, coverage: Coverage::Whole })) }),
reject(Some(2), Reason::GroupLabelInvalid)),
v("group_labelled_below_resolution", "a 2,000 uJ total declared at 144 ppm: below the resolution floor, correctly signed", "group",
json!({ "group": sealed(gl.clone(), &|g| g.label.as_mut().unwrap().class = Some(chip(144, 2_000_000))) }),
reject(Some(2), Reason::GroupLabelInvalid)),
v("group_chain_break", "the parent named is not the previous group held", "group",
json!({ "group": gl_json, "previous": labelled_other_previous() }), reject(Some(3), Reason::ChainBreak)),
v("group_parent_not_held", "a parent the verifier does not hold is fetched, never accepted silently", "group",
json!({ "group": gl_json }), reject(Some(3), Reason::ParentNotHeld)),
]);
// ── objects (§3 steps 4-5) ──
let obj = |g: &Value, o: Value, e: u8| json!({ "group": g, "object": o, "envelope": hexs(&envelope(e)) });
let edit = |o: &ObjectReceipt, f: &dyn Fn(&mut Value)| {
let mut j = o.to_json();
f(&mut j);
j
};
let accept = json!({ "outcome": "accept" });
// A group whose one object carries `label` beside `joules`, built correctly.
let lone = |n: u8, joules: u64, label: Option<ObjectLabel>| {
let (g, o) = build(n, &[(joules, label)], 9, None, false);
obj(&g.to_json(), o[0].to_json(), n)
};
// Three objects: the last leaf of the first level is duplicated.
let (g3, o3) = build(30, &[(10, None), (20, None), (30, None)], 8, None, false);
let g3j = g3.to_json();
out.extend([
v("object_labelled_counter_accept", "a labelled leaf: the figure, the counter's class and evidence, and the origin are under the group signature", "object",
obj(&gm_json, om[0].to_json(), 0), accept.clone()),
v("object_labelled_instrument_accept", "a labelled leaf carrying a calibrated instrument", "object",
obj(&gm_json, om[1].to_json(), 1), accept.clone()),
v("object_legacy_accept", "a legacy leaf beside labelled ones in one group", "object",
obj(&gm_json, om[2].to_json(), 2), accept.clone()),
v("object_labelled_unmetered_accept", "tag 0 beside a zero figure", "object",
obj(&gm_json, om[3].to_json(), 3), accept.clone()),
v("object_legacy_figure_unsigned_accept", "a legacy leaf binds neither the figure nor the origin: both rewritten, the object still verifies", "object",
obj(&gm_json, edit(&om[2], &|j| { j["joules_micro"] = 9_999.into(); j["origin"] = "elsewhere.example".into(); }), 2), accept.clone()),
v("object_in_labelled_group_accept", "an object of the labelled group", "object",
obj(&gl_json, ol[1].to_json(), 11), accept.clone()),
v("object_envelope_mutated", "the envelope does not hash to content_hash", "object",
json!({ "group": gm_json, "object": om[0].to_json(), "envelope": hexs(b"WAI1 mutated in transit") }),
reject(Some(4), Reason::ContentHashMismatch)),
v("object_label_stripped", "the class members removed from a labelled object: its leaf is computed in the legacy form and misses the root", "object",
obj(&gm_json, edit(&om[0], &|j| { let m = j.as_object_mut().unwrap(); for k in ["leaf_version", "energy_provenance", "energy_uncertainty", "energy_counter_filtering"] { m.remove(k); } }), 0),
reject(Some(5), Reason::InclusionProofFailed)),
v("object_label_added", "a class added to a legacy object", "object",
obj(&gm_json, edit(&om[2], &|j| { j["leaf_version"] = 2.into(); j["energy_provenance"] = "ModelBased".into(); }), 2),
reject(Some(5), Reason::InclusionProofFailed)),
v("object_class_restated", "a labelled object's declared uncertainty rewritten smaller", "object",
obj(&gm_json, edit(&om[0], &|j| j["energy_uncertainty"]["relative_ppm"] = 2_000.into()), 0),
reject(Some(5), Reason::InclusionProofFailed)),
v("object_class_promoted", "a labelled object's ModelBased class rewritten to Estimator", "object",
obj(&gm_json, edit(&om[4], &|j| j["energy_provenance"] = "Estimator".into()), 4),
reject(Some(5), Reason::InclusionProofFailed)),
v("object_labelled_figure_altered", "a labelled object's figure rewritten", "object",
obj(&gm_json, edit(&om[1], &|j| j["joules_micro"] = 801.into()), 1), reject(Some(5), Reason::InclusionProofFailed)),
v("object_labelled_origin_altered", "a labelled object's origin rewritten: the labelled leaf binds it", "object",
obj(&gm_json, edit(&om[3], &|j| j["origin"] = "elsewhere.example".into()), 3), reject(Some(5), Reason::InclusionProofFailed)),
v("object_label_hwshunt", "a leaf labelled legacy HwShunt in a correctly built group", "object",
lone(40, 100, lab(hw.clone())), reject(Some(5), Reason::ObjectLabelInvalid)),
v("object_label_beside_zero", "a class beside a zero figure in a correctly built group", "object",
lone(41, 0, lab(EnergyProvenance::ModelBased)), reject(Some(5), Reason::ObjectLabelInvalid)),
v("object_label_unmetered_beside_figure", "tag 0 beside a non-zero figure in a correctly built group", "object",
lone(42, 100, Some(ObjectLabel::Unmetered)), reject(Some(5), Reason::ObjectLabelInvalid)),
v("object_label_below_resolution", "a 1 uJ figure declared at 288,675 ppm in a correctly built group", "object",
lone(43, 1, lab(chip(288_675, 1_000_000))), reject(Some(5), Reason::ObjectLabelInvalid)),
v("object_label_evidence_without_class", "a labelled object's class removed and its evidence kept: refused, never read as legacy", "object",
obj(&gm_json, edit(&om[0], &|j| { j.as_object_mut().unwrap().remove("energy_provenance"); }), 0),
reject(Some(5), Reason::ObjectLabelInvalid)),
v("object_label_members_without_leaf_version", "class members on an object without leaf_version 2", "object",
obj(&gm_json, edit(&om[4], &|j| { j.as_object_mut().unwrap().remove("leaf_version"); }), 4),
reject(Some(5), Reason::ObjectLabelInvalid)),
v("object_at_another_index", "an object presented at its neighbour's index with its own proof, which still walks to the root", "object",
obj(&gm_json, edit(&om[0], &|j| { j["object_id"] = 1.into(); j["merkle_proof"]["leaf_index"] = 1.into(); }), 0),
reject(Some(5), Reason::ProofPositionMismatch)),
v("object_past_the_end", "the last of three objects at index 3: the duplicated leaf's proof walks that position too", "object",
obj(&g3j, edit(&o3[2], &|j| { j["object_id"] = 3.into(); j["merkle_proof"]["leaf_index"] = 3.into(); j["merkle_proof"]["direction_bits"] = 3.into(); }), 32),
reject(Some(5), Reason::ProofPositionMismatch)),
v("object_id_not_its_index", "object_id other than leaf_index", "object",
obj(&gm_json, edit(&om[2], &|j| j["object_id"] = 0.into()), 2), reject(Some(5), Reason::ProofPositionMismatch)),
v("object_proof_short", "a proof one sibling short of the group's depth", "object",
obj(&gm_json, edit(&om[2], &|j| { j["merkle_proof"]["siblings"].as_array_mut().unwrap().pop(); }), 2),
reject(Some(5), Reason::ProofPositionMismatch)),
v("object_other_group", "an object of another group_id presented under this group", "object",
obj(&gm_json, edit(&om[2], &|j| j["group_id"] = 4.into()), 2), reject(Some(5), Reason::ProofPositionMismatch)),
v("object_under_unverified_group", "a valid proof against a group whose signature fails: no partial verification", "object",
obj(&flipped.to_json(), om[0].to_json(), 0), reject(Some(2), Reason::SignatureVerifyFailed)),
v("object_unknown_member", "a member the object receipt does not define", "object",
obj(&gm_json, edit(&om[2], &|j| j["note"] = "x".into()), 2), reject(Some(4), Reason::Malformed)),
]);
// ── the group total (§3) ──
let all_m = with_envelopes(&om, 0);
let all_l = with_envelopes(&ol, 10);
let total = |g: &Value, d: &[(ObjectReceipt, Vec<u8>)]| json!({ "group": g, "delivered": delivered_json(d) });
let (gv1l, ov1l) = build(10, &[(1_200, lab(chip(20_000, 1_000_000))), (800, lab(cal(5_000, 2_000_000, CAL_REF))), (0, Some(ObjectLabel::Unmetered))], 5, None, false);
let (gz, oz) = build(50, &[(0, None), (0, Some(ObjectLabel::Unmetered))], 6, None, false);
let (gzl, ozl) = build(50, &[(0, None), (0, Some(ObjectLabel::Unmetered))], 6, None, true);
let (gd, od) = build(60, &[(500, lab(chip(20_000, 1_000_000))), (500, lab(EnergyProvenance::ModelBased))], 7, None, false);
let (gc, oc) = build(70, &[(1_000, lab(cal(300, 1_000_000, "https://calibration.example.org/certificates/a"))), (1_000, lab(cal(300, 1_000_000, "https://calibration.example.org/certificates/b")))], 8, None, false);
let relabel = |g: &GroupReceipt, l: GroupLabel| {
let mut t = g.clone();
t.label = Some(l);
resign(t, &key(0x11)).to_json()
};
let lbl = gl.label.clone().unwrap();
let with_class = |c: EnergyProvenance| GroupLabel { class: Some(c), coverage: lbl.coverage };
let mut dup = with_envelopes(&od, 60);
let mut twin = od[0].clone();
twin.object_id = 1;
twin.merkle_proof.leaf_index = 1;
dup[1] = (twin, envelope(60));
let mut altered = all_m.clone();
altered[2].0.joules_micro = 501;
out.extend([
v("total_legacy_unlabelled_part", "every object of the mixed group held; one non-zero figure is unlabelled, so the total reads as unlabelled", "total",
total(&gm_json, &all_m), read("unlabelled")),
v("total_legacy_every_part_labelled", "a legacy group whose every non-zero part is labelled: the weakest class, as a name only", "total",
total(&gv1l.to_json(), &with_envelopes(&ov1l, 10)), read("weakest class=OnChipCounter coverage=Partial")),
v("total_legacy_all_zero", "every figure zero: unmetered, never zero joules", "total",
total(&gz.to_json(), &with_envelopes(&oz, 50)), read("unmetered")),
v("total_legacy_object_missing", "one object not held: unlabelled", "total",
total(&gm_json, &all_m[..4]), read("unlabelled")),
v("total_legacy_figure_altered", "a legacy object's unsigned figure rewritten: each object verifies, and the sum does not match the signed total", "total",
total(&gm_json, &altered), reject(None, Reason::GroupTotalMismatch)),
v("total_duplicate_cannot_hide_weaker_class", "the counter's object presented again at the model's index: refused at its position, so the model's index is not held and the total reads unlabelled rather than as a counter's", "total",
total(&gd.to_json(), &dup), read("unlabelled")),
v("total_duplicate_both_held", "the same two objects held properly: the weaker class", "total",
total(&gd.to_json(), &with_envelopes(&od, 60)), read("weakest class=ModelBased coverage=Whole")),
v("total_labelled_checked", "the labelled group with every object held: its signed class is what they compose to", "total",
total(&gl_json, &all_l), read("signed class=OnChipCounter relative_ppm=14000 window_us=2000000 coverage=Partial parts=checked")),
v("total_labelled_part_held", "the labelled group with one object held: its signed class stands, unchecked", "total",
total(&gl_json, &all_l[..1]), read("signed class=OnChipCounter relative_ppm=14000 window_us=2000000 coverage=Partial parts=unchecked")),
v("total_labelled_above_least", "a declaration above the least the parts compose to", "total",
total(&relabel(&gl, with_class(chip(14_001, 2_000_000))), &all_l),
read("signed class=OnChipCounter relative_ppm=14001 window_us=2000000 coverage=Partial parts=checked")),
v("total_labelled_below_least", "one ppm below the least the parts compose to", "total",
total(&relabel(&gl, with_class(chip(13_999, 2_000_000))), &all_l), reject(None, Reason::GroupUncertaintyBelowParts)),
v("total_labelled_window_not_the_longest", "the parts' longest window is 2 s; the group declares 1 s", "total",
total(&relabel(&gl, with_class(chip(14_000, 1_000_000))), &all_l), reject(None, Reason::GroupUncertaintyBelowParts)),
v("total_labelled_class_stronger_than_parts", "the group classes its total as a calibrated instrument; one part is a counter", "total",
total(&relabel(&gl, with_class(cal(14_000, 2_000_000, CAL_REF))), &all_l), reject(None, Reason::GroupClassDisagrees)),
v("total_labelled_coverage_whole", "the group says Whole; one part is zero", "total",
total(&relabel(&gl, GroupLabel { class: lbl.class.clone(), coverage: Coverage::Whole }), &all_l), reject(None, Reason::GroupCoverageDisagrees)),
v("total_labelled_over_unlabelled_part", "a labelled group over parts one of which is unlabelled: it may carry no class", "total",
total(&relabel(&gm, GroupLabel { class: Some(EnergyProvenance::ModelBased), coverage: Coverage::Partial }), &all_m),
reject(None, Reason::GroupClassDisagrees)),
v("total_labelled_two_certificates", "two instruments under two certificates: a calibrated-instrument total needs one", "total",
total(&relabel(&gc, GroupLabel { class: Some(cal(300, 1_000_000, "https://calibration.example.org/certificates/a")), coverage: Coverage::Whole }), &with_envelopes(&oc, 70)),
reject(None, Reason::GroupClassDisagrees)),
v("total_labelled_unmetered", "a labelled group of zero figures: the unmetered marker", "total",
total(&gzl.to_json(), &with_envelopes(&ozl, 50)), read("unmetered")),
v("total_group_does_not_verify", "the group's signature fails: no total is read", "total",
total(&flipped.to_json(), &all_m), reject(Some(2), Reason::SignatureVerifyFailed)),
]);
// ── the WAI_RECEIPT property (moq-streaming-format §5) ──
let prop = |o: &ObjectReceipt, j: u64, class: Option<&EnergyProvenance>, group: &GroupReceipt| WaiReceiptProperty {
content_hash: o.content_hash,
joules_micro: j,
acquisition: class.map_or(String::new(), |c| c.label().into()),
acquisition_evidence: class.map_or(Vec::new(), property_evidence),
group_receipt: group.link_hash(),
};
let against = |p: WaiReceiptProperty, o: &ObjectReceipt, e: u8| {
json!({ "property": p.to_json(), "group": gm_json, "object": o.to_json(), "envelope": hexs(&envelope(e)) })
};
let c0 = chip(20_000, 1_000_000);
let model = EnergyProvenance::ModelBased;
let claim_by = |k: u8, id: &str, j: u64, class: &EnergyProvenance| {
ClaimReceipt::seal(
&key(k),
id,
SubjectType::Object,
om[2].content_hash,
0,
T0,
ClaimBody::Energy(EnergyClaim::new(
j,
class,
Coverage::Whole,
"derived",
"https://transaction.science/wai/extensions/energy-measurement",
1,
"object",
None,
)),
None,
)
.expect("a valid claim")
.to_json()
};
let claim = |j: u64, class: &EnergyProvenance| claim_by(0x22, "did:example:meter", j, class);
let signed_by = |class: &str, k: u8| read(&format!("signed class={class} by={}", hexs(key(k).verifying_key().as_bytes())));
let by_claim = |p: WaiReceiptProperty, c: String| json!({ "property": p.to_json(), "claim": c });
let mut other_group = prop(&om[0], 1_200, Some(&c0), &gm);
other_group.group_receipt = gl.link_hash();
out.extend([
v("property_signed_by_leaf", "the property restates the labelled leaf: its class is signed", "property",
against(prop(&om[0], 1_200, Some(&c0), &gm), &om[0], 0), signed_by("OnChipCounter", 0x11)),
v("property_evidence_differs", "the property's declared uncertainty differs from the leaf's", "property",
against(prop(&om[0], 1_200, Some(&chip(10_000, 1_000_000)), &gm), &om[0], 0), reject(None, Reason::PropertyDisagrees)),
v("property_figure_differs", "the property's figure differs from the leaf's", "property",
against(prop(&om[0], 1_201, Some(&c0), &gm), &om[0], 0), reject(None, Reason::PropertyDisagrees)),
v("property_names_another_group", "the property names another group receipt", "property",
against(other_group, &om[0], 0), reject(None, Reason::PropertyDisagrees)),
v("property_class_beside_zero", "a class beside a zero figure", "property",
against(prop(&om[3], 0, Some(&model), &gm), &om[3], 3), reject(None, Reason::PropertyInvalid)),
v("property_figure_without_class", "a non-zero figure with no class", "property",
against(prop(&om[2], 500, None, &gm), &om[2], 2), reject(None, Reason::PropertyInvalid)),
v("property_legacy_leaf", "the object's leaf is legacy: the property's class is outside every signature, and the figure reads as unlabelled", "property",
against(prop(&om[2], 500, Some(&model), &gm), &om[2], 2), read("unsigned")),
v("property_unmetered", "a zero figure and no class beside an object explicitly unmetered", "property",
against(prop(&om[3], 0, None, &gm), &om[3], 3), read("unmetered")),
v("property_object_does_not_verify", "the envelope does not hash to the object's content hash: nothing is read", "property",
json!({ "property": prop(&om[0], 1_200, Some(&c0), &gm).to_json(), "group": gm_json, "object": om[0].to_json(), "envelope": hexs(b"WAI1 mutated") }),
reject(Some(4), Reason::ContentHashMismatch)),
v("property_signed_by_energy_claim", "an energy claim about the legacy object restates the property's figure and class", "property",
by_claim(prop(&om[2], 500, Some(&model), &gm), claim(500, &model)), signed_by("ModelBased", 0x22)),
v("property_signed_by_third_party_claim", "an energy claim signed by a relay's own key restates the property: the class reads as signed by that key, not by the publisher", "property",
by_claim(prop(&om[2], 500, Some(&model), &gm), claim_by(0x33, "did:example:relay", 500, &model)), signed_by("ModelBased", 0x33)),
v("property_energy_claim_disagrees", "the energy claim states another figure", "property",
by_claim(prop(&om[2], 500, Some(&model), &gm), claim(499, &model)), reject(None, Reason::PropertyDisagrees)),
v("property_energy_claim_altered", "an energy claim altered after signing is no signed statement: the property reads as unsigned", "property",
by_claim(prop(&om[2], 500, Some(&model), &gm), claim(500, &model).replacen("\"joules_micro\":500", "\"joules_micro\":501", 1)),
read("unsigned")),
]);
out
}
/// A group receipt, valid, that is not the labelled group's parent.
fn labelled_other_previous() -> Value {
build(80, &[(1, None)], 2, None, false).0.to_json()
}
/// The corpus, regenerated: byte-identical on every machine.
pub fn generate() -> String {
let vs: Vec<Value> = vectors()
.into_iter()
.map(|v| {
let mut m = Map::new();
m.insert("name".into(), v.name.into());
m.insert("about".into(), v.about.into());
m.insert("procedure".into(), v.procedure.into());
m.insert("input".into(), v.input);
m.insert("expected".into(), v.expected);
Value::Object(m)
})
.collect();
let mut doc = Map::new();
doc.insert(
"about".into(),
"jwp-receipts sections 2.2-2.4.2, 3 and 3.3, and moq-streaming-format section 5: labelled \
leaves, labelled group receipts, the group total and the WAI_RECEIPT property. Every \
vector is synthetic: keys are Ed25519 from fixed 32-byte seeds (0x11 publisher, 0x22 \
meter, 0x33 relay) and attest nothing; envelopes, figures, origins and calibration references are \
invented. Expected outcomes are declared per vector in the generator, not copied from a \
verifier. Regenerate with `wai_jwp_vectors gen`."
.into(),
);
doc.insert("vectors".into(), Value::Array(vs));
let mut s = serde_json::to_string_pretty(&Value::Object(doc)).expect("serialises");
s.push('\n');
s
}
fn err_label(e: &DeliveryError) -> String {
match e.step {
Some(s) => format!("reject step={s} reason={}", e.reason.label()),
None => format!("reject reason={}", e.reason.label()),
}
}
fn bytes_of(v: Option<&Value>) -> Result<Vec<u8>, String> {
v.and_then(Value::as_str).and_then(unhex).ok_or_else(|| "bad hex".into())
}
/// One vector's verdict under this verifier.
pub fn verdict(v: &Value) -> Result<String, String> {
let i = v.get("input").ok_or("no input")?;
let group = |k: &str| -> Result<Result<GroupReceipt, DeliveryError>, String> {
Ok(GroupReceipt::from_json(i.get(k).ok_or(format!("no {k}"))?))
};
match v.get("procedure").and_then(Value::as_str) {
Some("leaf") => {
let o = ObjectReceipt::from_json(i.get("object").ok_or("no object")?).map_err(|e| e.to_string())?;
Ok(format!("leaf={}", hex(&o.leaf())))
}
Some("group") => {
let g = match group("group")? {
Ok(g) => g,
Err(e) => return Ok(err_label(&e)),
};
if let Err(e) = g.verify() {
return Ok(err_label(&e));
}
let previous = match i.get("previous") {
None => None,
Some(p) => GroupReceipt::from_json(p).ok(),
};
Ok(match g.check_parent(previous.as_ref()) {
Ok(()) => format!("accept link_hash={} signing_payload={}", hex(&g.link_hash()), hex(&g.signing_payload())),
Err(e) => err_label(&e),
})
}
Some("object") => {
let g = match group("group")? {
Ok(g) => g,
Err(e) => return Ok(err_label(&e)),
};
let o = match ObjectReceipt::from_json(i.get("object").ok_or("no object")?) {
Ok(o) => o,
Err(e) => return Ok(err_label(&e)),
};
let env = bytes_of(i.get("envelope"))?;
Ok(match o.accept(&g, &env) {
Ok(()) => "accept".into(),
Err(e) => err_label(&e),
})
}
Some("total") => {
let g = match group("group")? {
Ok(g) => g,
Err(e) => return Ok(err_label(&e)),
};
let mut held = Vec::new();
for d in i.get("delivered").and_then(Value::as_array).ok_or("no delivered")? {
// An object that cannot be read is not held.
if let Ok(o) = ObjectReceipt::from_json(d.get("object").ok_or("no object")?) {
held.push((o, bytes_of(d.get("envelope"))?));
}
}
let ds: Vec<Delivered> = held.iter().map(|(o, e)| Delivered { object: o, envelope: e }).collect();
Ok(match read_group_total(&g, &ds) {
Ok(r) => format!("read {}", total_label(&r)),
Err(e) => err_label(&e),
})
}
Some("property") => {
let p = WaiReceiptProperty::from_json(i.get("property").ok_or("no property")?).ok_or("bad property")?;
let r = if let Some(c) = i.get("claim") {
let wire = c.as_str().ok_or("a claim is given as its wire form")?;
match ClaimReceipt::from_json(wire) {
Ok(claim) => p.check_against_claim(&claim),
// A claim that cannot be read is no signed statement.
Err(_) => p.check().map(|c| if c.is_some() { PropertyReading::Unsigned } else { PropertyReading::Unmetered }),
}
} else {
let g = match group("group")? {
Ok(g) => g,
Err(e) => return Ok(err_label(&e)),
};
let o = match ObjectReceipt::from_json(i.get("object").ok_or("no object")?) {
Ok(o) => o,
Err(e) => return Ok(err_label(&e)),
};
p.check_against_object(&o, &g, &bytes_of(i.get("envelope"))?)
};
Ok(match r {
Ok(r) => format!("read {}", property_label(&r)),
Err(e) => err_label(&e),
})
}
other => Err(format!("unknown procedure {other:?}")),
}
}
/// The verdict a vector declares.
pub fn expected(v: &Value) -> Result<String, String> {
let e = v.get("expected").ok_or("no expected")?;
let s = |k: &str| e.get(k).and_then(Value::as_str).ok_or(format!("expected.{k}"));
if let Some(l) = e.get("leaf").and_then(Value::as_str) {
return Ok(format!("leaf={l}"));
}
match s("outcome")? {
"accept" => match (e.get("link_hash"), e.get("signing_payload")) {
(Some(l), Some(p)) => Ok(format!(
"accept link_hash={} signing_payload={}",
l.as_str().ok_or("link_hash")?,
p.as_str().ok_or("signing_payload")?
)),
_ => Ok("accept".into()),
},
"read" => Ok(format!("read {}", s("reading")?)),
"reject" => Ok(match e.get("step").and_then(Value::as_u64) {
Some(st) => format!("reject step={st} reason={}", s("reason")?),
None => format!("reject reason={}", s("reason")?),
}),
o => Err(format!("bad outcome {o}")),
}
}
/// Run every vector of a corpus document: `(name, expected, got)`.
pub fn run(doc: &str) -> Result<Vec<(String, String, String)>, String> {
let d: Value = serde_json::from_str(doc).map_err(|e| e.to_string())?;
let vs = d.get("vectors").and_then(Value::as_array).ok_or("no vectors")?;
vs.iter()
.map(|v| {
let name = v.get("name").and_then(Value::as_str).unwrap_or("?").to_owned();
let want = expected(v).map_err(|e| format!("{name}: {e}"))?;
let got = verdict(v).map_err(|e| format!("{name}: {e}"))?;
Ok((name, want, got))
})
.collect()
}
/// The number of vectors the pinned corpus holds, so a truncated file fails.
pub fn vector_count() -> usize {
vectors().len()
}

View file

@ -0,0 +1,525 @@
use super::*;
use crate::energy_class::{CounterFiltering, Uncertainty};
use serde_json::json;
fn key(s: u8) -> SigningKey {
SigningKey::from_bytes(&[s; 32])
}
fn chip(ppm: u32, window_us: u64) -> EnergyProvenance {
EnergyProvenance::OnChipCounter { uncertainty: Uncertainty { relative_ppm: ppm, window_us }, filtering: CounterFiltering::Unknown }
}
fn cal(ppm: u32, r: &str) -> EnergyProvenance {
EnergyProvenance::CalibratedInstrument {
uncertainty: Uncertainty { relative_ppm: ppm, window_us: 2_000_000 },
calibration_ref: r.into(),
}
}
fn env(i: u8) -> Vec<u8> {
format!("WAI1 jwp-delivery test object {i}").into_bytes()
}
const T0: &str = "2026-09-30T12:00:00Z";
const T1: &str = "2026-09-30T12:00:01Z";
/// A group of `parts` (figure, label), sealed by key 0x11, legacy or labelled.
fn group(parts: &[(u64, Option<ObjectLabel>)], labelled: bool) -> (GroupReceipt, Vec<ObjectReceipt>) {
let mut b = GroupBuilder::default();
for (i, (j, l)) in parts.iter().enumerate() {
b.add(object_content_hash(&env(i as u8)), *j, "origin.example", l.clone());
}
b.finish(&key(0x11), "did:example:publisher", 7, 3, T0, T1, None, labelled).expect("composes")
}
fn delivered(objects: &[ObjectReceipt]) -> Vec<(ObjectReceipt, Vec<u8>)> {
objects.iter().enumerate().map(|(i, o)| (o.clone(), env(i as u8))).collect()
}
fn total(g: &GroupReceipt, d: &[(ObjectReceipt, Vec<u8>)]) -> Result<TotalReading, DeliveryError> {
let ds: Vec<Delivered> = d.iter().map(|(o, e)| Delivered { object: o, envelope: e }).collect();
read_group_total(g, &ds)
}
fn lab(p: EnergyProvenance) -> Option<ObjectLabel> {
Some(ObjectLabel::Class(p))
}
#[test]
fn the_legacy_leaf_and_tree_are_the_ones_every_profile_already_builds() {
let h = object_content_hash(b"one");
assert_eq!(root(&[leaf_legacy(&h)]), crate::world::receipt::merkle_root(&[h]));
let hs: Vec<[u8; 32]> = (0..5u8).map(|i| object_content_hash(&[i])).collect();
let leaves: Vec<[u8; 32]> = hs.iter().map(leaf_legacy).collect();
assert_eq!(root(&leaves), crate::world::receipt::merkle_root(&hs));
assert_eq!(root(&[]), [0u8; 32]);
}
#[test]
fn the_labelled_leaf_binds_the_figure_the_class_and_the_origin() {
let h = object_content_hash(b"x");
let base = leaf_labelled(&h, 1_000, &ObjectLabel::Class(chip(20_000, 1_000_000)), "o");
assert_ne!(base, leaf_legacy(&h));
for other in [
leaf_labelled(&h, 1_001, &ObjectLabel::Class(chip(20_000, 1_000_000)), "o"),
leaf_labelled(&h, 999, &ObjectLabel::Class(chip(20_000, 1_000_000)), "o"),
leaf_labelled(&h, 1_000, &ObjectLabel::Class(chip(20_001, 1_000_000)), "o"),
leaf_labelled(&h, 1_000, &ObjectLabel::Class(chip(19_999, 1_000_000)), "o"),
leaf_labelled(&h, 1_000, &ObjectLabel::Class(chip(20_000, 1_000_001)), "o"),
leaf_labelled(
&h,
1_000,
&ObjectLabel::Class(EnergyProvenance::OnChipCounter {
uncertainty: Uncertainty { relative_ppm: 20_000, window_us: 1_000_000 },
filtering: CounterFiltering::Off,
}),
"o",
),
leaf_labelled(&h, 1_000, &ObjectLabel::Class(cal(20_000, "c")), "o"),
leaf_labelled(&h, 1_000, &ObjectLabel::Class(EnergyProvenance::ModelBased), "o"),
leaf_labelled(&h, 1_000, &ObjectLabel::Class(chip(20_000, 1_000_000)), "p"),
leaf_labelled(&h, 1_000, &ObjectLabel::Class(chip(20_000, 1_000_000)), ""),
leaf_labelled(&h, 0, &ObjectLabel::Unmetered, "o"),
] {
assert_ne!(other, base);
}
// The origin is length-prefixed: moving bytes between the class and the
// origin changes the leaf.
assert_ne!(
leaf_labelled(&h, 5, &ObjectLabel::Class(cal(60_000, "ab")), "c"),
leaf_labelled(&h, 5, &ObjectLabel::Class(cal(60_000, "a")), "bc"),
);
}
#[test]
fn a_mixed_group_verifies_and_a_label_can_be_neither_restated_stripped_nor_added() {
let parts = [
(1_200, lab(chip(20_000, 1_000_000))),
(800, lab(cal(5_000, "https://calibration.example.org/c/1"))),
(500, None),
(0, Some(ObjectLabel::Unmetered)),
];
let (g, objs) = group(&parts, false);
g.verify().unwrap();
for (i, o) in objs.iter().enumerate() {
o.accept(&g, &env(i as u8)).unwrap();
}
let reject = |o: &ObjectReceipt, i: u8| o.accept(&g, &env(i)).unwrap_err();
let failed = DeliveryError::at(5, Reason::InclusionProofFailed);
let mut restated = objs[0].clone();
restated.label = lab(chip(19_000, 1_000_000));
assert_eq!(reject(&restated, 0), failed, "a class restated in transit");
let mut stripped = objs[0].clone();
stripped.label = None;
assert_eq!(reject(&stripped, 0), failed, "a label stripped");
let mut added = objs[2].clone();
added.label = lab(EnergyProvenance::ModelBased);
assert_eq!(reject(&added, 2), failed, "a label added to a legacy leaf");
let mut origin = objs[0].clone();
origin.origin = "elsewhere.example".into();
assert_eq!(reject(&origin, 0), failed, "the origin altered");
let mut figure = objs[1].clone();
figure.joules_micro += 1;
assert_eq!(reject(&figure, 1), failed, "a labelled figure altered");
// In the legacy form the figure and the origin are not bound.
let mut legacy = objs[2].clone();
legacy.joules_micro = 9_999;
legacy.origin = "elsewhere.example".into();
legacy.accept(&g, &env(2)).unwrap();
// The envelope is.
assert_eq!(objs[0].accept(&g, b"mutated").unwrap_err(), DeliveryError::at(4, Reason::ContentHashMismatch));
}
#[test]
fn a_label_that_breaks_its_rules_is_refused_in_a_correctly_built_tree() {
#[allow(deprecated)]
let hw = EnergyProvenance::HwShunt;
let invalid = DeliveryError::at(5, Reason::ObjectLabelInvalid);
for (j, l) in [
(100, lab(hw)),
(0, lab(chip(20_000, 1))),
(100, Some(ObjectLabel::Unmetered)),
(1, lab(chip(288_675, 1))),
(100, lab(EnergyProvenance::OnChipCounter { uncertainty: Uncertainty { relative_ppm: 0, window_us: 1 }, filtering: CounterFiltering::Off })),
] {
let (g, objs) = group(&[(j, l.clone())], false);
assert_eq!(objs[0].accept(&g, &env(0)).unwrap_err(), invalid, "{l:?} beside {j}");
}
}
#[test]
fn an_object_must_be_at_the_position_its_proof_walks() {
let (g, objs) = group(&[(10, None), (20, None), (30, None)], false);
let mismatch = DeliveryError::at(5, Reason::ProofPositionMismatch);
// The last leaf of an odd level is duplicated, so its proof also walks the
// position past the end of the group.
let mut past_end = objs[2].clone();
past_end.merkle_proof.leaf_index = 3;
past_end.merkle_proof.direction_bits = 3;
past_end.object_id = 3;
assert_eq!(past_end.merkle_proof.walk(&past_end.leaf()), g.root_hash, "the duplicate walks to the root");
assert_eq!(past_end.accept(&g, &env(2)).unwrap_err(), mismatch);
// An object presented at another's index, with its own proof.
let mut moved = objs[0].clone();
moved.merkle_proof.leaf_index = 1;
moved.object_id = 1;
assert_eq!(moved.merkle_proof.walk(&moved.leaf()), g.root_hash, "its proof still walks to the root");
assert_eq!(moved.accept(&g, &env(0)).unwrap_err(), mismatch);
// object_id other than the index, a short proof, another group.
let mut id = objs[1].clone();
id.object_id = 0;
assert_eq!(id.accept(&g, &env(1)).unwrap_err(), mismatch);
let mut short = objs[1].clone();
short.merkle_proof.siblings.pop();
assert_eq!(short.accept(&g, &env(1)).unwrap_err(), mismatch);
let mut other = objs[1].clone();
other.group_id += 1;
assert_eq!(other.accept(&g, &env(1)).unwrap_err(), mismatch);
assert_eq!(proof_len(1), 0);
assert_eq!(proof_len(2), 1);
assert_eq!(proof_len(3), 2);
assert_eq!(proof_len(4), 2);
assert_eq!(proof_len(5), 3);
}
/// Two objects of equal figure, one measured by a counter and one modelled.
/// Presented twice, the counter's object would hide the modelled one and the
/// total would read at the counter's class.
#[test]
fn a_duplicate_object_cannot_hide_a_weaker_class() {
let (g, objs) = group(&[(500, lab(chip(20_000, 1_000_000))), (500, lab(EnergyProvenance::ModelBased))], false);
let all = delivered(&objs);
assert_eq!(total(&g, &all), Ok(TotalReading::Weakest { kind: AcquisitionKind::ModelBased, coverage: Coverage::Whole }));
let mut twin = objs[0].clone();
twin.merkle_proof.leaf_index = 1;
twin.object_id = 1;
let hidden = vec![(objs[0].clone(), env(0)), (twin, env(0))];
assert_eq!(total(&g, &hidden), Ok(TotalReading::Unlabelled), "the second index is not held");
}
#[test]
fn a_legacy_total_reads_from_its_objects_labels() {
let both = |a: Option<ObjectLabel>, ja: u64, b: Option<ObjectLabel>, jb: u64| {
let (g, objs) = group(&[(ja, a), (jb, b)], false);
total(&g, &delivered(&objs))
};
let c = || lab(chip(20_000, 1_000_000));
assert_eq!(both(c(), 100, lab(cal(5_000, "r")), 200), Ok(TotalReading::Weakest { kind: AcquisitionKind::OnChipCounter, coverage: Coverage::Whole }));
assert_eq!(both(c(), 100, None, 0), Ok(TotalReading::Weakest { kind: AcquisitionKind::OnChipCounter, coverage: Coverage::Partial }));
assert_eq!(both(c(), 100, None, 5), Ok(TotalReading::Unlabelled), "one unlabelled non-zero figure");
assert_eq!(both(None, 0, Some(ObjectLabel::Unmetered), 0), Ok(TotalReading::Unmetered));
let (g, objs) = group(&[(100, c()), (200, None)], false);
let mut d = delivered(&objs);
d.pop();
assert_eq!(total(&g, &d), Ok(TotalReading::Unlabelled), "an object missing");
// A legacy figure is not bound, so altering it is accepted per object and
// caught only by the sum.
let mut d = delivered(&objs);
d[1].0.joules_micro = 201;
assert_eq!(total(&g, &d), Err(DeliveryError::of(Reason::GroupTotalMismatch)));
}
#[test]
fn the_link_hash_covers_the_payload_and_the_signature() {
let (g, _) = group(&[(1, None)], false);
let mut want = blake3::Hasher::new();
want.update(b"moq-jwp:receipt-link\x01");
want.update(&g.signing_payload());
want.update(&g.sig);
assert_eq!(g.link_hash(), *want.finalize().as_bytes());
}
#[test]
fn a_labelled_group_signs_its_class_its_coverage_and_its_parent() {
let parts = [(1_200, lab(chip(20_000, 1_000_000))), (800, lab(cal(5_000, "r"))), (0, Some(ObjectLabel::Unmetered))];
let (g, objs) = group(&parts, true);
g.verify().unwrap();
let l = g.label.clone().unwrap();
// OnChipCounter is the weaker of the two classes; one part is zero.
assert_eq!(l.coverage, Coverage::Partial);
// ⌈(1200·20000 + 800·5000) / 2000⌉ = 14000.
assert_eq!(l.class, Some(chip(14_000, 2_000_000)));
assert_eq!(
total(&g, &delivered(&objs)),
Ok(TotalReading::Signed { class: chip(14_000, 2_000_000), coverage: Coverage::Partial, checked: true })
);
let sig_failed = DeliveryError::at(2, Reason::SignatureVerifyFailed);
let mut t = g.clone();
t.label.as_mut().unwrap().class = Some(cal(14_000, "r"));
assert_eq!(t.verify(), Err(sig_failed), "class");
let mut t = g.clone();
t.label.as_mut().unwrap().coverage = Coverage::Whole;
assert_eq!(t.verify(), Err(sig_failed), "coverage");
let mut t = g.clone();
t.parent_receipt_hash = Some([7; 32]);
assert_eq!(t.verify(), Err(sig_failed), "parent");
// A verifier that predates the labelled form computes the legacy payload
// and refuses the signature.
let key = ed25519_dalek::VerifyingKey::from_bytes(&g.signer_pubkey).unwrap();
assert!(key.verify_strict(&legacy_payload(&g), &ed25519_dalek::Signature::from_bytes(&g.sig)).is_err());
// In the legacy form the parent is not signed.
let (v1, _) = group(&parts, false);
let mut t = v1.clone();
t.parent_receipt_hash = Some([7; 32]);
t.verify().unwrap();
}
#[test]
fn the_composition_rule() {
let p = |f: u64, l: Option<ObjectLabel>| (f, l);
let compose = |parts: Vec<(u64, Option<ObjectLabel>)>| {
let v: Vec<(u64, Option<&ObjectLabel>)> = parts.iter().map(|(f, l)| (*f, l.as_ref())).collect();
compose_group_class(&v)
};
// Mixed classes: the weakest, as a class name and its evidence.
assert_eq!(
compose(vec![p(10, lab(chip(30_000, 1))), p(10, lab(EnergyProvenance::ModelBased))]),
Some(Composition { class: Some(EnergyProvenance::ModelBased), coverage: Coverage::Whole })
);
// The least declaration rounds up, and the window is the longest.
assert_eq!(
compose(vec![p(1, lab(chip(300_000, 5))), p(2, lab(chip(200_000, 9)))]).unwrap().class,
Some(chip(233_334, 9))
);
// Filtering: common or Unknown.
let off = |ppm| {
lab(EnergyProvenance::OnChipCounter { uncertainty: Uncertainty { relative_ppm: ppm, window_us: 1 }, filtering: CounterFiltering::Off })
};
assert!(matches!(
compose(vec![p(1, off(300_000)), p(1, off(300_000))]).unwrap().class,
Some(EnergyProvenance::OnChipCounter { filtering: CounterFiltering::Off, .. })
));
assert!(matches!(
compose(vec![p(1, off(300_000)), p(1, lab(chip(300_000, 1)))]).unwrap().class,
Some(EnergyProvenance::OnChipCounter { filtering: CounterFiltering::Unknown, .. })
));
// Calibrated: one reference, or no class.
assert!(compose(vec![p(1, lab(cal(300_000, "a"))), p(1, lab(cal(300_000, "a")))]).unwrap().class.is_some());
assert_eq!(compose(vec![p(1, lab(cal(300_000, "a"))), p(1, lab(cal(300_000, "b")))]), None);
// An unlabelled non-zero part: no class; all zero: unmetered.
assert_eq!(compose(vec![p(1, lab(chip(300_000, 1))), p(1, None)]), None);
assert_eq!(compose(vec![p(0, None), p(0, Some(ObjectLabel::Unmetered))]), Some(Composition { class: None, coverage: Coverage::Whole }));
}
#[test]
fn a_labelled_group_is_checked_against_its_parts() {
let parts = [(1_000, lab(chip(30_000, 1_000_000))), (3_000, lab(chip(10_000, 2_000_000)))];
let (g, objs) = group(&parts, true);
let d = delivered(&objs);
// ⌈(1000·30000 + 3000·10000) / 4000⌉ = 15000, over the longer window.
assert_eq!(g.label.as_ref().unwrap().class, Some(chip(15_000, 2_000_000)));
let with = |class: Option<EnergyProvenance>, coverage: Coverage| {
let mut t = g.clone();
t.label = Some(GroupLabel { class, coverage });
total(&resign(t, &key(0x11)), &d)
};
let signed = |c| Ok(TotalReading::Signed { class: c, coverage: Coverage::Whole, checked: true });
assert_eq!(with(Some(chip(15_000, 2_000_000)), Coverage::Whole), signed(chip(15_000, 2_000_000)));
assert_eq!(with(Some(chip(15_001, 2_000_000)), Coverage::Whole), signed(chip(15_001, 2_000_000)), "more is allowed");
let below = Err(DeliveryError::of(Reason::GroupUncertaintyBelowParts));
assert_eq!(with(Some(chip(14_999, 2_000_000)), Coverage::Whole), below, "one ppm less is not");
assert_eq!(with(Some(chip(15_000, 1_000_000)), Coverage::Whole), below, "another window");
assert_eq!(with(Some(cal(15_000, "r")), Coverage::Whole), Err(DeliveryError::of(Reason::GroupClassDisagrees)));
assert_eq!(with(Some(EnergyProvenance::ModelBased), Coverage::Whole), Err(DeliveryError::of(Reason::GroupClassDisagrees)));
assert_eq!(with(Some(chip(15_000, 2_000_000)), Coverage::Partial), Err(DeliveryError::of(Reason::GroupCoverageDisagrees)));
// Holding part of the group, the signed class stands, unchecked.
assert_eq!(
total(&g, &d[..1]),
Ok(TotalReading::Signed { class: chip(15_000, 2_000_000), coverage: Coverage::Whole, checked: false })
);
// A group whose parts compose to no class may not carry one.
let (v1, objs) = group(&[(1, lab(cal(300_000, "a"))), (1, lab(cal(300_000, "b")))], false);
let mut t = v1.clone();
t.label = Some(GroupLabel { class: Some(cal(300_000, "a")), coverage: Coverage::Whole });
assert_eq!(total(&resign(t, &key(0x11)), &delivered(&objs)), Err(DeliveryError::of(Reason::GroupClassDisagrees)));
}
#[test]
fn a_group_label_is_checked_with_the_group_alone() {
let (g, _) = group(&[(100, lab(chip(20_000, 1)))], true);
let invalid = Err(DeliveryError::at(2, Reason::GroupLabelInvalid));
#[allow(deprecated)]
for (class, total_uj) in [
(Some(EnergyProvenance::HwShunt), 100),
(Some(chip(20_000, 1)), 0),
(None, 100),
(Some(chip(2_000, 1)), 100),
] {
let mut t = g.clone();
t.joules_total = total_uj;
t.label = Some(GroupLabel { class, coverage: Coverage::Whole });
assert_eq!(resign(t, &key(0x11)).verify(), invalid);
}
let mut t = g.clone();
t.joules_total = 0;
t.label = Some(GroupLabel { class: None, coverage: Coverage::Partial });
assert_eq!(resign(t, &key(0x11)).verify(), invalid, "the unmetered marker covers nothing partially");
}
#[test]
fn group_signatures_are_held_to_the_strict_rule() {
let (g, _) = group(&[(1, None)], false);
// S + L: the equation holds, the scalar is out of range.
let l: [u8; 32] = [
0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0x10,
];
let mut t = g.clone();
let mut carry = 0u16;
for (s, l) in t.sig[32..].iter_mut().zip(l) {
let v = u16::from(*s) + u16::from(l) + carry;
*s = v as u8;
carry = v >> 8;
}
assert_eq!(carry, 0);
assert_eq!(t.verify(), Err(DeliveryError::at(2, Reason::SignatureVerifyFailed)));
// The identity as a key.
let mut t = g.clone();
t.signer_pubkey = [0; 32];
t.signer_pubkey[0] = 1;
assert_eq!(t.verify(), Err(DeliveryError::at(1, Reason::BadSignerKey)));
}
/// A proof read from JSON may hold more siblings than direction bits. Walking
/// it neither shifts out of range nor is accepted: the position check refuses
/// it first.
#[test]
fn a_proof_longer_than_its_direction_bits_is_walked_and_refused() {
let (g, objs) = group(&[(10, None), (20, None)], false);
let mut long = objs[1].clone();
long.merkle_proof.siblings = vec![[7; 32]; 70];
long.merkle_proof.direction_bits = u64::MAX;
let walked = long.merkle_proof.walk(&long.leaf());
assert_ne!(walked, g.root_hash);
assert_eq!(long.accept(&g, &env(1)).unwrap_err(), DeliveryError::at(5, Reason::ProofPositionMismatch));
let back = ObjectReceipt::from_json(&long.to_json()).expect("any number of siblings reads");
assert_eq!(back.verify_inclusion(&g), Err(DeliveryError::at(5, Reason::ProofPositionMismatch)));
}
/// `sign_version` is a labelled member: any value but 2 is
/// `group_label_invalid`, as `leaf_version` is `object_label_invalid`.
#[test]
fn a_sign_version_other_than_2_is_group_label_invalid() {
let (g, _) = group(&[(1, lab(EnergyProvenance::ModelBased))], true);
let (legacy, _) = group(&[(1, None)], false);
for (base, v) in [(&g, json!(3)), (&g, json!(1)), (&g, json!("2")), (&legacy, json!(1)), (&legacy, json!(0))] {
let mut j = base.to_json();
j["sign_version"] = v.clone();
assert_eq!(GroupReceipt::from_json(&j), Err(DeliveryError::at(1, Reason::GroupLabelInvalid)), "{v}");
}
assert!(GroupReceipt::from_json(&g.to_json()).is_ok());
}
/// RFC 8032 §5.1.3 gives every point one encoding: `y = p + 3` encodes a point
/// of large order that a reducing decoder would accept, and it is refused as a
/// key, at step 1, before any signature is examined.
#[test]
fn a_non_canonical_key_encoding_is_refused() {
let (g, _) = group(&[(1, None)], false);
let mut t = g.clone();
t.signer_pubkey = [0xff; 32];
t.signer_pubkey[0] = 0xf0;
t.signer_pubkey[31] = 0x7f;
assert_eq!(t.verify(), Err(DeliveryError::at(1, Reason::BadSignerKey)));
}
#[test]
fn the_parent_link() {
let (g0, _) = group(&[(1, lab(EnergyProvenance::ModelBased))], true);
let mut b = GroupBuilder::default();
b.add(object_content_hash(&env(0)), 1, "o", lab(EnergyProvenance::ModelBased));
let (g1, _) = b.finish(&key(0x11), "did:example:publisher", 7, 4, T0, T1, Some(g0.link_hash()), true).unwrap();
assert_eq!(g1.check_parent(Some(&g0)), Ok(()));
assert_eq!(g1.check_parent(None), Err(DeliveryError::at(3, Reason::ParentNotHeld)));
assert_eq!(g0.check_parent(Some(&g1)), Err(DeliveryError::at(3, Reason::ChainBreak)));
let (other, _) = group(&[(2, lab(EnergyProvenance::ModelBased))], true);
assert_eq!(g1.check_parent(Some(&other)), Err(DeliveryError::at(3, Reason::ChainBreak)));
}
#[test]
fn both_forms_round_trip_through_json_and_a_label_is_never_dropped() {
let parts = [(1_200, lab(chip(20_000, 1_000_000))), (500, None), (0, Some(ObjectLabel::Unmetered))];
for labelled in [false, true] {
let parts: Vec<_> = if labelled { vec![parts[0].clone(), parts[2].clone()] } else { parts.to_vec() };
let (g, objs) = group(&parts, labelled);
assert_eq!(GroupReceipt::from_json(&g.to_json()), Ok(g.clone()));
for o in &objs {
assert_eq!(ObjectReceipt::from_json(&o.to_json()).as_ref(), Ok(o));
}
}
// Class members in a legacy group are refused, never read as legacy.
let (g, objs) = group(&parts, false);
let mut j = g.to_json();
j["energy_provenance"] = "ModelBased".into();
assert_eq!(GroupReceipt::from_json(&j), Err(DeliveryError::at(1, Reason::GroupLabelInvalid)));
// An object's unreadable label is refused at step 5.
let mut j = objs[1].to_json();
j["energy_provenance"] = "ModelBased".into();
let o = ObjectReceipt::from_json(&j).unwrap();
assert_eq!(o.accept(&g, &env(1)), Err(DeliveryError::at(5, Reason::ObjectLabelInvalid)));
let mut j = objs[0].to_json();
j.as_object_mut().unwrap().remove("energy_provenance");
let o = ObjectReceipt::from_json(&j).unwrap();
assert_eq!(o.accept(&g, &env(0)), Err(DeliveryError::at(5, Reason::ObjectLabelInvalid)), "evidence without a class");
let mut j = objs[0].to_json();
j["note"] = 1.into();
assert_eq!(ObjectReceipt::from_json(&j), Err(DeliveryError::at(4, Reason::Malformed)));
}
#[test]
fn a_property_reads_as_signed_only_where_a_signed_statement_restates_it() {
let parts = [(1_200, lab(chip(20_000, 1_000_000))), (500, None), (0, Some(ObjectLabel::Unmetered))];
let (g, objs) = group(&parts, false);
let prop = |i: usize, j: u64, class: Option<&EnergyProvenance>| WaiReceiptProperty {
content_hash: objs[i].content_hash,
joules_micro: j,
acquisition: class.map_or(String::new(), |c| c.label().into()),
acquisition_evidence: class.map_or(Vec::new(), property_evidence),
group_receipt: g.link_hash(),
};
let c = chip(20_000, 1_000_000);
assert_eq!(prop(0, 1_200, Some(&c)).check_against_object(&objs[0], &g, &env(0)), Ok(PropertyReading::Signed { class: c.clone(), by: g.signer_pubkey }));
let disagrees = Err(DeliveryError::of(Reason::PropertyDisagrees));
assert_eq!(prop(0, 1_200, Some(&chip(20_001, 1_000_000))).check_against_object(&objs[0], &g, &env(0)), disagrees);
assert_eq!(prop(0, 1_201, Some(&c)).check_against_object(&objs[0], &g, &env(0)), disagrees);
let mut other_group = prop(0, 1_200, Some(&c));
other_group.group_receipt = [9; 32];
assert_eq!(other_group.check_against_object(&objs[0], &g, &env(0)), disagrees);
assert_eq!(
prop(1, 500, Some(&EnergyProvenance::ModelBased)).check_against_object(&objs[1], &g, &env(1)),
Ok(PropertyReading::Unsigned),
"a legacy leaf restates nothing"
);
assert_eq!(prop(2, 0, None).check_against_object(&objs[2], &g, &env(2)), Ok(PropertyReading::Unmetered));
let invalid = Err(DeliveryError::of(Reason::PropertyInvalid));
assert_eq!(prop(2, 0, Some(&c)).check_against_object(&objs[2], &g, &env(2)), invalid, "a class beside zero");
assert_eq!(prop(1, 500, None).check_against_object(&objs[1], &g, &env(1)), invalid, "a figure without a class");
// An energy claim about the object.
use crate::claim_receipt::{ClaimBody, ClaimReceipt, EnergyClaim, SubjectType};
let claim = |j: u64, class: &EnergyProvenance| {
ClaimReceipt::seal(
&key(0x22),
"did:example:meter",
SubjectType::Object,
objs[1].content_hash,
0,
T0,
ClaimBody::Energy(EnergyClaim::new(j, class, Coverage::Whole, "derived", "https://example.org/m", 1, "u", None)),
None,
)
.unwrap()
};
let m = EnergyProvenance::ModelBased;
assert_eq!(
prop(1, 500, Some(&m)).check_against_claim(&claim(500, &m)),
Ok(PropertyReading::Signed { class: m.clone(), by: key(0x22).verifying_key().to_bytes() }),
"signed by the claim's signer, not the group's"
);
assert_eq!(prop(1, 500, Some(&m)).check_against_claim(&claim(501, &m)), disagrees);
let mut forged = claim(500, &m);
forged.sig[0] ^= 1;
assert_eq!(prop(1, 500, Some(&m)).check_against_claim(&forged), Ok(PropertyReading::Unsigned));
}

View file

@ -237,6 +237,18 @@ pub mod reconstruction_binding;
// dependencies, so it is compiled unconditionally.
pub mod energy_class;
// The integers a measurement report carries for a figure obtained under the
// bracketed protocol, the exact arithmetic every reader applies to them (the figure
// they give, the least declaration they support, the derived attribution verdict),
// the liveness rule a power reader's rail must meet, and the build record
// (energy-measurement §3.1, §6.2). Integer only, pure std, compiled unconditionally.
pub mod energy_report;
// What build.rs records about the build, shared with build.rs through `#[path]`;
// compiled into the crate for its tests.
#[cfg(test)]
mod build_info;
// The bracketed marginal protocol (energy-measurement §3) every reference meter
// runs: idle bracket, a burst released from a start barrier, busy bracket, and
// the §4 / §2.3 rules for when a figure may be sealed under a hardware class.
@ -280,6 +292,20 @@ pub mod claim_receipt;
#[cfg(feature = "receipt_claims")]
mod claim_text;
// JWP delivery receipts (extensions/jwp-receipts.md §2.2–§2.4.2, §3, §3.3): group and
// object receipts read and verified, in the legacy forms and the labelled ones — an
// object's figure, class and origin in its Merkle leaf, a group total's class under a
// versioned signing payload — with the position check, the group-total reading and the
// WAI_RECEIPT property's readings (moq-streaming-format §5).
#[cfg(feature = "receipt_claims")]
pub mod jwp_delivery;
// Figure profiles (energy-measurement §6.1): how a measurement or operating-point claim
// names the receipt that seals its figure, by that receipt's identity domain, and the
// figures each receipt family shows a report — read without changing any family.
#[cfg(feature = "receipt_claims")]
pub mod energy_figure;
// wai.world.model — generative-world transport: a signed, replay-equivalent action
// trace (the world CausalOp log) + per-frame provenance. The wire carries the
// action trace + a declared model_id, not the pixels; the sink renders. The neural
@ -310,7 +336,8 @@ pub mod quantum_envelope;
// The energy-accounted seal for a quantum reconstruction (extensions/quantum-sim
// § Receipt): a signed receipt separating the portable-exact WORK (n_ops·2^n
// amplitude updates, recomputable by anyone) from the measured-attested ENERGY
// (joules_micro from the sink's meter), both bound to a re-simulatable statevector
// (joules_micro from the sink's meter, and the class that labels it), both bound to a
// re-simulatable statevector
// hash. A JWP profile — reuses the world Merkle + Ed25519.
#[cfg(feature = "quantum_receipt")]
pub use wai_quantum::quantum_receipt;
@ -335,7 +362,8 @@ pub mod ternary;
// The energy-accounted seal for a ternary-MPS reconstruction (extensions/ternary-tt
// § Receipt): a signed receipt separating portable-exact WORK (trit_macs) from
// measured-attested ENERGY (joules_micro), bound to the WQTT payload hash and a
// measured-attested ENERGY (joules_micro and the class that labels it), bound to the
// WQTT payload hash and a
// re-decodable reconstruction hash. A JWP profile — reuses the world Merkle + Ed25519.
#[cfg(feature = "ternary_receipt")]
pub mod ternary_receipt;
@ -574,7 +602,8 @@ pub use wai_quantum::quantum_kernel;
// Phasor energy meter (extensions/quantum-sim § Receipt, `wai.quantum.phasor`): the
// energy-accounted seal for an interference computation — portable-exact phasor
// work (linear, recomputable) split from measured-attested joules, so qFHRR is an
// work (linear, recomputable) split from measured-attested joules and their class, so
// qFHRR is an
// accountable substrate, not merely a fast one. Reuses the JWP Merkle + Ed25519.
#[cfg(feature = "quantum_phasor_meter")]
pub use wai_quantum::quantum_phasor_meter;

View file

@ -30,7 +30,10 @@
//! labelled step signs under the `0x02` receipt domain and writes every
//! step's optional class (`0` when unlabelled) after that step's figure.
//! `total_joules_micro` carries no label of its own: a sum over steps of
//! different classes is only as strong as its weakest step.
//! different classes is only as strong as its weakest step. An unmetered step
//! adds its marker `0` to the sum, so a total over steps that include one covers
//! only the measured steps (`Partial`, energy-measurement §2) and a reader says
//! so; it is never the whole cost of the lineage.
use crate::energy_class::{
check_label, json_with_label, label_coheres, label_from_json, labelled_domain, write_label,
@ -68,7 +71,8 @@ pub struct ProvStep {
pub output_hash: [u8; 32],
/// Exact, deterministic work (e.g. pixels or samples processed).
pub work: u64,
/// Measured/carried microjoules for the step (0 = unmetered).
/// Measured/carried microjoules for the step; `0` is the unmetered marker,
/// never a figure of zero joules (energy-measurement §2).
pub joules_micro: u64,
/// How the step's `joules_micro` was acquired — an optional label
/// (`extensions/energy-measurement.md` §2.8). `None` is an unlabelled
@ -131,7 +135,8 @@ pub struct AssetReceipt {
pub group_id: u64,
pub steps: Vec<ProvStep>,
pub merkle_root: [u8; 32],
/// Σ of the steps' carried joules.
/// Σ of the steps' carried joules. A step carrying the unmetered marker adds
/// nothing, so with one the total covers only the measured steps.
pub total_joules_micro: u64,
pub parent_receipt_hash: Option<[u8; 32]>,
pub signer_pubkey: [u8; 32],
@ -249,6 +254,25 @@ impl AssetReceipt {
/// Confirm the receipt vouches for exactly this asset envelope:
/// recompute the content hash and compare. The tamper test — a
/// 1-bit change to the envelope breaks it.
/// The total as a reader presents it (energy-measurement §2): `UNMETERED`
/// when every step carries the unmetered marker, never `0 µJ`; the figure
/// labelled `Whole` when every step is measured; otherwise the figure
/// labelled `Partial`, naming the unmetered steps the total does not cover.
pub fn total_label(&self) -> String {
let unmetered: Vec<&str> = self.steps.iter().filter(|s| s.joules_micro == 0).map(|s| s.op.as_str()).collect();
if unmetered.len() == self.steps.len() {
"UNMETERED (no step measured)".to_owned()
} else if unmetered.is_empty() {
format!("{} µJ (Whole)", self.total_joules_micro)
} else {
format!(
"{} µJ over the measured steps only (Partial; unmetered: {})",
self.total_joules_micro,
unmetered.join(", ")
)
}
}
pub fn matches_asset(&self, envelope_bytes: &[u8]) -> bool {
crate::world::asset_content_hash(envelope_bytes) == self.asset_hash
}
@ -399,6 +423,21 @@ mod tests {
assert!(!wrong.verify());
}
/// An unmetered step is never presented as 0 J, and a total over steps that
/// include one says it covers only the measured steps.
#[test]
fn the_total_is_labelled_as_a_reader_presents_it() {
let (steps, asset) = chain();
let build = |steps: Vec<ProvStep>| AssetReceipt::build(&key(9), "did:web:wai#prov", asset, "image", "wai.image.jpeg", 0, steps, None);
let partial = build(steps.clone());
assert_eq!(partial.total_label(), "4290 µJ over the measured steps only (Partial; unmetered: capture)");
let none = build(steps.iter().cloned().map(|s| ProvStep { joules_micro: 0, ..s }).collect());
assert_eq!(none.total_joules_micro, 0);
assert_eq!(none.total_label(), "UNMETERED (no step measured)", "never 0 µJ");
let whole = build(steps.into_iter().map(|s| ProvStep { joules_micro: s.joules_micro.max(7), ..s }).collect());
assert_eq!(whole.total_label(), "4297 µJ (Whole)");
}
#[test]
fn json_round_trips_and_verifies() {
let (steps, asset) = chain();

View file

@ -423,3 +423,94 @@ mod labels {
assert!(legacy_back.verify());
}
}
// ---------------------------------------------------------------------------
// Pins for the labelled forms (energy-measurement §2.8).
//
// The legacy pins above hold unlabelled receipts to their pre-label bytes.
// These hold one labelled receipt of every family to the bytes the label
// gave it when it was introduced, so a later change to a labelled encoding —
// a byte leaking into `write_signed` or `write_label`, or a changed domain —
// fails here instead of re-signing quietly. The hex was produced by the code
// as the labels were first introduced and must never be edited to make a
// test pass.
// ---------------------------------------------------------------------------
mod labelled_pins {
use super::*;
use super::labels::chip;
#[cfg(feature = "world")]
#[test]
fn world_labelled_bytes() {
let r = world().with_energy_class(&key(9), chip(15_403)).unwrap();
assert!(r.verify());
assert_eq!(hx(&r.receipt_hash()), "32b7ef840b7140c369ecdd5d04c0b9ed2f361980aacfa072c0342d3ad4eb1057");
assert_eq!(json_hash(&r.to_json()), "6c8f53541d7f8a896a8a0e5e2873c076c5afab36c914a95d9e4bf3211b042855");
}
#[cfg(feature = "audio_scene")]
#[test]
fn audio_scene_labelled_bytes() {
let r = audio_scene().with_energy_class(&key(9), chip(15_403)).unwrap();
assert!(r.verify());
assert_eq!(hx(&r.receipt_hash()), "2fe5b5771dfd818a050c020fe365522f5ac5bfbbd133191e0663c25ab3b1533e");
}
#[cfg(feature = "film")]
#[test]
fn film_labelled_bytes() {
let r = film().with_energy_class(&key(9), chip(15_403)).unwrap();
assert!(r.verify());
assert_eq!(hx(&r.receipt_hash()), "9bd844ee24ea3903a273a79e509edaae3eef830509edb05a586482581d003e06");
assert_eq!(json_hash(&r.to_json()), "d8644a2f82e407b14d1ff57a2f12b733f63e931c62c00b9526880d2af55e329e");
}
#[cfg(feature = "haptic")]
#[test]
fn haptic_labelled_bytes() {
let r = haptic().with_energy_class(&key(9), chip(15_403)).unwrap();
assert!(r.verify());
assert_eq!(hx(&r.receipt_hash()), "e17f760cd90364feb9ac47e121a783c85035253c29bf640406c66cfc8cf3e487");
assert_eq!(json_hash(&r.to_json()), "1fb81b2976b6e49a568d63a0a60a872946c29b6751de88f30b12a3fa99dfaa85");
}
#[cfg(feature = "score")]
#[test]
fn score_labelled_bytes() {
let r = score().with_energy_class(&key(9), chip(15_403)).unwrap();
assert!(r.verify());
assert_eq!(hx(&r.receipt_hash()), "0e795647d7ef74bf400fee962e6bcf57292116e0a084da41a1c153de777a0bee");
assert_eq!(json_hash(&r.to_json()), "856823c9874b10bb5d78c01b576318bc0cd44e1f2112fbcd56f72204db38b544");
}
#[cfg(feature = "splat4d")]
#[test]
fn splat4d_labelled_bytes() {
let r = splat4d().with_energy_class(&key(9), chip(15_403)).unwrap();
assert!(r.verify());
assert_eq!(hx(&r.receipt_hash()), "410e26fdf5c60b8bba50f39d318a6e6e8c122f8a3807bb65f90dcf2a92c13a76");
assert_eq!(json_hash(&r.to_json()), "5efa236fa7b818685fda50ef7e52c9037c48c76b32eeb0fbfc092888a255156d");
}
#[cfg(feature = "ternary_receipt")]
#[test]
fn ternary_labelled_bytes() {
let r = ternary().with_energy_class(&key(1), chip(15_403)).unwrap();
assert!(r.verify());
assert_eq!(hx(&r.receipt_hash()), "3eaf95f6d3e7cbc2505f6bacfadac60f2cf7451041cf36c1c7307c1345120c3e");
assert_eq!(json_hash(&r.to_json()), "321f0c9d739ce6fd6e4bf6e32d663ef3e14f25339ad75799fce9f17159052413");
}
#[cfg(feature = "provenance")]
#[test]
fn provenance_labelled_bytes() {
let mut steps = provenance_steps();
steps[1] = steps[1].clone().with_energy_class(chip(15_403)).unwrap();
let r = provenance(steps);
assert!(r.verify());
assert_eq!(hx(&r.receipt_hash()), "db3979573fc3ff07a1c2d82256268c862d6a2d1737ea9636c876155ef9546ae1");
assert_eq!(json_hash(&r.to_json()), "d1a52b06b30dece511e701a86b86eb176b5fa414ae2084520a35a1e004fdcee0");
assert_eq!(hx(&r.steps[1].content_hash()), "8b94897f5b73b5b8e62b8328ee416368e8d087c65ee661fc188e4bcb5b97da42");
}
}

View file

@ -164,8 +164,8 @@ impl TernaryReceipt {
/// confirm it hashes to this receipt's `payload_hash`, decodes to its
/// `reconstruction_hash`, and matches its `trit_macs` — then that the
/// receipt verifies. Rejects a receipt whose claimed reconstruction is not
/// what the payload actually decodes to, on any machine. (Measured
/// `joules_micro` stays attested-only.)
/// what the payload actually decodes to, on any machine. (The measurement —
/// `joules_micro` and the class that labels it — stays attested-only.)
pub fn verify_reconstruction(&self, wqtt_payload: &[u8]) -> bool {
if *blake3::hash(wqtt_payload).as_bytes() != self.payload_hash {
return false;

View file

@ -12,18 +12,19 @@
//! 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.
//! [`VideoReceipt::verify_reconstruction`] — and every field except the
//! measurement 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:
//! That leaves the measurement attested-not-verifiable — `joules_micro` and the
//! class and evidence that describe how it was obtained — 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.
//! - `joules_micro` and `energy_provenance` — **measured-attested**: the real
//! marginal CPU energy the metering sink spent, and how it was acquired,
//! 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

View file

@ -21,8 +21,9 @@
//! Windows chain via `parent_receipt_hash`, so a channel's history is a hash
//! chain of program-as-run windows — the StreamOps audit trail. Each window's
//! manifest is still a `wai.video.manifest`, so the per-class dedup carries
//! through: one generation per window per class, not per viewer. Only
//! `joules_micro` is measured-attested. Reuses the manifest floor.
//! through: one generation per window per class, not per viewer. Only the
//! measurement — `joules_micro`, its acquisition class and the class's evidence —
//! is measured-attested: signed, and not re-measurable. Reuses the manifest floor.
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};

View file

@ -18,8 +18,9 @@
//! without the record ever carrying key material. A refusal carries no
//! commitment, and the coupling is enforced, not merely documented.
//!
//! Only `joules_micro` is measured-attested. Reuses the manifest module's
//! `EnergyProvenance` and the world Merkle floor's Ed25519 + `GrantRef`; links
//! Only the measurement — `joules_micro`, its acquisition class and the class's
//! evidence — is measured-attested: signed, and not re-measurable. Reuses the
//! manifest module's `EnergyProvenance` and the world Merkle floor's Ed25519 + `GrantRef`; links
//! to a manifest via `parent_receipt_hash`. Reuse, not reinvention.
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};

View file

@ -16,8 +16,9 @@
//! and confirms `manifest_hash == hash(render(segments))`. The composition, the
//! stitched ads, the blackouts applied and the compose work all stop being
//! claims and become checkable facts (see [`ManifestReceipt::verify_render`]).
//! - **Measured-attested** — `joules_micro`, the marginal energy the generation
//! sink spent, vouched for by the signature and nothing else. Same honesty
//! - **Measured-attested** — the measurement: `joules_micro`, the marginal
//! energy the generation sink spent, its acquisition class and the class's
//! evidence, vouched for by the signature and nothing else. Same honesty
//! ladder as [`crate::video_receipt`]: a figure without its acquisition class
//! is not comparable to another.
//!

View file

@ -17,8 +17,9 @@
//! overstated. `uj_per_unique_segment` is the encode-once energy, and it is
//! honest precisely because the uniqueness is recomputable.
//!
//! Only `joules_micro` is measured-attested. Reuses the manifest module's
//! `EnergyProvenance` and the world Merkle floor's Ed25519 + `GrantRef`.
//! Only the measurement — `joules_micro`, its acquisition class and the class's
//! evidence — is measured-attested: signed, and not re-measurable. Reuses the
//! manifest module's `EnergyProvenance` and the world Merkle floor's Ed25519 + `GrantRef`.
use std::collections::BTreeSet;

View file

@ -18,8 +18,9 @@
//! The decision itself is deterministic ([`decide`]): a pure function of the
//! candidate pod, the avail and the funds remaining, so an auditor re-runs it
//! and gets the same fill. The pod is bound by `pod_hash`, so the inputs to that
//! decision are fixed too. Only `joules_micro` is measured-attested — the same
//! honesty ladder as the rest of WAI.
//! decision are fixed too. Only the measurement — `joules_micro`, its acquisition
//! class and the class's evidence — is measured-attested: signed, and not
//! re-measurable, the same honesty ladder as the rest of WAI.
//!
//! Links to [`crate::wai_video_manifest`] by `manifest_hash` and chains from the
//! manifest receipt via `parent_receipt_hash`: the ad break belongs to a

View file

@ -57,7 +57,8 @@ pub struct SessionReceipt {
pub object_hashes: Vec<[u8; 32]>,
/// Merkle root over `object_hashes` (JWP §2.2).
pub merkle_root: [u8; 32],
/// Measured microjoules for the session; `0` = unmetered. Joules
/// Measured microjoules for the session; `0` is the unmetered marker, never a
/// figure of zero joules (energy-measurement §2). Joules
/// are *carried*, never fabricated — a real meter (RAPL natively, a
/// declared wall-clock estimate in a browser) supplies the figure;
/// it is NOT derived inside this builder, and a sink that didn't

View file

@ -217,6 +217,7 @@ fn every_decoded_output_states_a_sha256() {
("volumetric-conformance", NATIVE), ("world-conformance", NATIVE),
("quantum-conformance", NATIVE), ("quantum-ops-conformance", NATIVE),
("claim-conformance", "receipt claims: receipt content, no decoded output"),
("jwp-conformance", "JWP delivery receipts: receipt content, no decoded output"),
("oer-conformance", "OER/2 receipts: receipt content, no decoded output"),
("pin-conformance", "parameter-set pins: every digest there is a SHA-256 of a parameter set"),
("byte-exact-conformance", "goldens over tests/vectors, whose sidecars state the SHA-256"),

View file

@ -245,3 +245,35 @@ fn a_refined_class_carries_its_uncertainty_and_cannot_omit_it() {
);
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",
]
);
}