wai: energy reports, labelled JWP leaves and what signatures cover #133
71 changed files with 16050 additions and 592 deletions
16
.github/workflows/joule-code-conformance.yml
vendored
16
.github/workflows/joule-code-conformance.yml
vendored
|
|
@ -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: |
|
||||
|
|
|
|||
58
.github/workflows/wai-conformance.yml
vendored
58
.github/workflows/wai-conformance.yml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
39
.github/workflows/wai-energy-receipts.yml
vendored
39
.github/workflows/wai-energy-receipts.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
2
joule-code/examples/transport-moqt/Cargo.lock
generated
2
joule-code/examples/transport-moqt/Cargo.lock
generated
|
|
@ -654,7 +654,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "joule-moq"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"blake3",
|
||||
|
|
|
|||
2
joule-code/joule-code-rs/Cargo.lock
generated
2
joule-code/joule-code-rs/Cargo.lock
generated
|
|
@ -1367,7 +1367,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "joule-moq"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"blake3",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -216,6 +216,7 @@ impl GroupHandle for MockGroupHandle {
|
|||
joules_micro: joules,
|
||||
origin: origin.clone(),
|
||||
merkle_proof: proof,
|
||||
energy_class: None,
|
||||
};
|
||||
fabric.deliver(
|
||||
&key,
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
124
wai/CHANGELOG.md
124
wai/CHANGELOG.md
|
|
@ -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
|
||||
|
|
|
|||
43
wai/SPEC.md
43
wai/SPEC.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>`,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
3
wai/jwp-conformance/.gitattributes
vendored
Normal 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
|
||||
64
wai/jwp-conformance/README.md
Normal file
64
wai/jwp-conformance/README.md
Normal 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.
|
||||
544
wai/jwp-conformance/jwp_verify.py
Normal file
544
wai/jwp-conformance/jwp_verify.py
Normal 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))
|
||||
4554
wai/jwp-conformance/vectors.json
Normal file
4554
wai/jwp-conformance/vectors.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
2
wai/wai-quantum-serve/Cargo.lock
generated
2
wai/wai-quantum-serve/Cargo.lock
generated
|
|
@ -598,7 +598,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wai-quantum-serve"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"ed25519-dalek",
|
||||
"wai",
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
98
wai/wai-rs/src/bin/wai_jwp_vectors.rs
Normal file
98
wai/wai-rs/src/bin/wai_jwp_vectors.rs
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
36
wai/wai-rs/src/build_info.rs
Normal file
36
wai/wai-rs/src/build_info.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
473
wai/wai-rs/src/energy_figure.rs
Normal file
473
wai/wai-rs/src/energy_figure.rs
Normal 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;
|
||||
336
wai/wai-rs/src/energy_figure/tests.rs
Normal file
336
wai/wai-rs/src/energy_figure/tests.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
808
wai/wai-rs/src/energy_report.rs
Normal file
808
wai/wai-rs/src/energy_report.rs
Normal 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:?}");
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
1228
wai/wai-rs/src/jwp_delivery.rs
Normal file
1228
wai/wai-rs/src/jwp_delivery.rs
Normal file
File diff suppressed because it is too large
Load diff
684
wai/wai-rs/src/jwp_delivery/corpus.rs
Normal file
684
wai/wai-rs/src/jwp_delivery/corpus.rs
Normal 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()
|
||||
}
|
||||
525
wai/wai-rs/src/jwp_delivery/tests.rs
Normal file
525
wai/wai-rs/src/jwp_delivery/tests.rs
Normal 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));
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
//!
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue