From 9b57b968163d9f84d26e59ca61f5a572b02e5e1f Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 20 Oct 2026 13:58:36 -0400 Subject: [PATCH] efficiency/1: a partial energy figure is sealed as partial and never headlines as a task total MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Efficiency::wire()` sealed `energy_uj` and `provenance` and nothing about coverage. So a GPU-only reading (a hardware counter that reads the accelerator and not the CPU work that drove it) reached a verifier as the whole unit's energy. The receipt also sealed a `uj_per_accepted_token` over that reading, which presented it as what the work cost per token. jcp SPEC §5.2 already said a `Partial` figure MUST NOT be presented as a task total, and listed the receipts that carry coverage. efficiency/1 was not among them. `aggregate()` then summed energy across receipts with no coverage and no provenance, and its per-unit figure came out as clean as a whole one. §5.3 says aggregation takes the floor of both and is never a step at which a grade improves. Now: - The receipt carries `energy_coverage: "Partial"` when the figure is partial, and omits it when the figure is whole. A whole receipt, and every receipt sealed before the field existed, keeps its bytes. - A partial receipt seals no `uj_per_accepted_token`. A partial figure divided by tokens is not what the work cost per token, nor a bound on it, since a partial figure can fall short of the work or exceed it. - `read_payload`, split out of `verify` so the payload rules can be replayed without a signature, refuses: - a partial receipt carrying the headline; - any `energy_coverage` but "Partial" ("Whole" included; absence is Whole); - a provenance label that is not one of the three. - `Totals` carries `provenance_floor` and `coverage`. A total over any partial receipt is partial, gives no per-unit energy, and reports value per joule as its worst value (0.0). SPEC §5.2 now lists efficiency/1 with these rules. A published pack, `conformance/v1/efficiency.json` (from `gen_efficiency_vectors`), carries the readings and refusals and the totals sets must sum to. `tests/efficiency_pack.rs` replays it from disk. Tests: 3 new unit tests and 4 pack replays. All 9 mutations of the new rules are killed. The pack alone catches the headline, coverage-floor and provenance-floor mutations. Co-Authored-By: Claude Opus 5.5 --- jcp/SPEC.md | 10 + jcp/conformance/v1/efficiency.json | 228 ++++++++++++++++++ jcp/jcp-rs/crates/jcp-efficiency/MAPPING.md | 2 +- .../src/bin/gen_efficiency_vectors.rs | 134 ++++++++++ jcp/jcp-rs/crates/jcp-efficiency/src/lib.rs | 192 ++++++++++++++- jcp/jcp-rs/crates/jcp-efficiency/src/trace.rs | 4 +- .../jcp-efficiency/tests/efficiency_pack.rs | 85 +++++++ 7 files changed, 644 insertions(+), 11 deletions(-) create mode 100644 jcp/conformance/v1/efficiency.json create mode 100644 jcp/jcp-rs/crates/jcp-efficiency/src/bin/gen_efficiency_vectors.rs create mode 100644 jcp/jcp-rs/crates/jcp-efficiency/tests/efficiency_pack.rs diff --git a/jcp/SPEC.md b/jcp/SPEC.md index b1f6ad44c..9a32af7e7 100644 --- a/jcp/SPEC.md +++ b/jcp/SPEC.md @@ -926,6 +926,16 @@ interceptor chain (`jcp-intercept`) carries it as `coverage`, beside its `"Partial"`; a verifier MUST refuse any other value, `"Whole"` included, rather than read it either way. +The `efficiency/1` outcome receipt (§5.3) carries it as `energy_coverage`, +under the same one-value rule. Its headline figures are ratios of a task total: +energy per accepted token on the receipt, and energy per completed unit over a +ledger. So a `Partial` receipt MUST NOT carry `uj_per_accepted_token`, and a +verifier MUST refuse one that does. A total summed from receipts of which any +is `Partial` is `Partial` and gives no per-unit energy figure. A partial figure +divided by tokens is not what the work cost per token, nor a bound on it, +because a partial figure may fall short of the work or exceed it. The vectors +are in [`conformance/v1/efficiency.json`](conformance/v1/efficiency.json). + ### 5.3 Traceability across operation, activity and outcome (REQUIRED where an outcome claims energy) §5.1 grades a figure and §5.2 says how much of the work it covers. Neither says diff --git a/jcp/conformance/v1/efficiency.json b/jcp/conformance/v1/efficiency.json new file mode 100644 index 000000000..c2e3d6918 --- /dev/null +++ b/jcp/conformance/v1/efficiency.json @@ -0,0 +1,228 @@ +{ + "aggregate": { + "cases": [ + { + "expected": { + "completed_uj": 1600000, + "coverage": "Partial", + "provenance_floor": "Estimator", + "tokens_per_completed_unit": 3000, + "uj_per_completed_unit": null, + "wasted_uj": 400000 + }, + "input": [ + { + "completed": 1, + "efficiency_receipt": "efficiency/1", + "energy_uj": 1000000, + "prev": "0000000000000000000000000000000000000000000000000000000000000000", + "provenance": "HwShunt", + "seq": 0, + "steward": "did:example:steward", + "task": "whole", + "tokens_in": 2000, + "tokens_out": 500, + "uj_per_accepted_token": 2000, + "verdict": "tests-pass", + "wall_ns": 1000000000 + }, + { + "completed": 1, + "efficiency_receipt": "efficiency/1", + "energy_coverage": "Partial", + "energy_uj": 600000, + "prev": "0000000000000000000000000000000000000000000000000000000000000000", + "provenance": "HwShunt", + "seq": 0, + "steward": "did:example:steward", + "task": "gpu-only", + "tokens_in": 2000, + "tokens_out": 500, + "verdict": "tests-pass", + "wall_ns": 1000000000 + }, + { + "completed": 0, + "efficiency_receipt": "efficiency/1", + "energy_uj": 400000, + "prev": "0000000000000000000000000000000000000000000000000000000000000000", + "provenance": "Estimator", + "seq": 0, + "steward": "did:example:steward", + "task": "estimated", + "tokens_in": 800, + "tokens_out": 200, + "uj_per_accepted_token": 2000, + "verdict": "gate-failed", + "wall_ns": 1000000000 + } + ], + "note": "a total over a partial figure is partial and gives no per-unit energy; its provenance is the floor" + }, + { + "expected": { + "completed_uj": 1000000, + "coverage": "Whole", + "provenance_floor": "Estimator", + "tokens_per_completed_unit": 3500, + "uj_per_completed_unit": 1400000, + "wasted_uj": 400000 + }, + "input": [ + { + "completed": 1, + "efficiency_receipt": "efficiency/1", + "energy_uj": 1000000, + "prev": "0000000000000000000000000000000000000000000000000000000000000000", + "provenance": "HwShunt", + "seq": 0, + "steward": "did:example:steward", + "task": "whole", + "tokens_in": 2000, + "tokens_out": 500, + "uj_per_accepted_token": 2000, + "verdict": "tests-pass", + "wall_ns": 1000000000 + }, + { + "completed": 0, + "efficiency_receipt": "efficiency/1", + "energy_uj": 400000, + "prev": "0000000000000000000000000000000000000000000000000000000000000000", + "provenance": "Estimator", + "seq": 0, + "steward": "did:example:steward", + "task": "estimated", + "tokens_in": 800, + "tokens_out": 200, + "uj_per_accepted_token": 2000, + "verdict": "gate-failed", + "wall_ns": 1000000000 + } + ], + "note": "a total over whole figures gives its per-unit energy, at the weakest provenance" + } + ] + }, + "payload": { + "negative": [ + { + "expected": { + "refused": "presented as a task total" + }, + "input": { + "completed": 1, + "efficiency_receipt": "efficiency/1", + "energy_coverage": "Partial", + "energy_uj": 600000, + "prev": "0000000000000000000000000000000000000000000000000000000000000000", + "provenance": "HwShunt", + "seq": 0, + "steward": "did:example:steward", + "task": "gpu-only", + "tokens_in": 2000, + "tokens_out": 500, + "uj_per_accepted_token": 1200, + "verdict": "tests-pass", + "wall_ns": 1000000000 + }, + "note": "a GPU-only figure presented as a task total, with a per-token headline" + }, + { + "expected": { + "refused": "energy_coverage" + }, + "input": { + "completed": 1, + "efficiency_receipt": "efficiency/1", + "energy_coverage": "Whole", + "energy_uj": 1000000, + "prev": "0000000000000000000000000000000000000000000000000000000000000000", + "provenance": "HwShunt", + "seq": 0, + "steward": "did:example:steward", + "task": "whole", + "tokens_in": 2000, + "tokens_out": 500, + "uj_per_accepted_token": 2000, + "verdict": "tests-pass", + "wall_ns": 1000000000 + }, + "note": "an absent coverage is Whole; \"Whole\" on the wire is refused" + }, + { + "expected": { + "refused": "not a provenance label" + }, + "input": { + "completed": 1, + "efficiency_receipt": "efficiency/1", + "energy_uj": 1000000, + "prev": "0000000000000000000000000000000000000000000000000000000000000000", + "provenance": "Measured", + "seq": 0, + "steward": "did:example:steward", + "task": "whole", + "tokens_in": 2000, + "tokens_out": 500, + "uj_per_accepted_token": 2000, + "verdict": "tests-pass", + "wall_ns": 1000000000 + }, + "note": "a provenance that is not one of the three labels" + } + ], + "positive": [ + { + "expected": { + "energy_coverage": "Whole", + "energy_uj": 1000000, + "provenance": "HwShunt", + "uj_per_accepted_token": 2000 + }, + "input": { + "completed": 1, + "efficiency_receipt": "efficiency/1", + "energy_uj": 1000000, + "prev": "0000000000000000000000000000000000000000000000000000000000000000", + "provenance": "HwShunt", + "seq": 0, + "steward": "did:example:steward", + "task": "whole", + "tokens_in": 2000, + "tokens_out": 500, + "uj_per_accepted_token": 2000, + "verdict": "tests-pass", + "wall_ns": 1000000000 + }, + "note": "a whole figure seals its per-token headline and no coverage field" + }, + { + "expected": { + "energy_coverage": "Partial", + "energy_uj": 600000, + "provenance": "HwShunt", + "uj_per_accepted_token": null + }, + "input": { + "completed": 1, + "efficiency_receipt": "efficiency/1", + "energy_coverage": "Partial", + "energy_uj": 600000, + "prev": "0000000000000000000000000000000000000000000000000000000000000000", + "provenance": "HwShunt", + "seq": 0, + "steward": "did:example:steward", + "task": "gpu-only", + "tokens_in": 2000, + "tokens_out": 500, + "verdict": "tests-pass", + "wall_ns": 1000000000 + }, + "note": "a GPU-only figure seals its coverage and no per-token headline: it is no task total" + } + ] + }, + "profile": "efficiency/1", + "spec": "jcp/SPEC.md §5.2-5.3" +} diff --git a/jcp/jcp-rs/crates/jcp-efficiency/MAPPING.md b/jcp/jcp-rs/crates/jcp-efficiency/MAPPING.md index ecb1095d8..1acb78677 100644 --- a/jcp/jcp-rs/crates/jcp-efficiency/MAPPING.md +++ b/jcp/jcp-rs/crates/jcp-efficiency/MAPPING.md @@ -52,7 +52,7 @@ receipts. | SCI-for-AI concept | `efficiency/1` field / method | What TX adds | |---|---|---| -| `E` — energy of the functional unit | `energy_uj` + `provenance` | **Honest provenance** (`HwShunt`/`ModelBased`/`Estimator`) — SCI leaves `E`'s trust unstated | +| `E` — energy of the functional unit | `energy_uj` + `provenance` + `energy_coverage` | **Honest provenance** (`HwShunt`/`ModelBased`/`Estimator`) — SCI leaves `E`'s trust unstated — and **coverage**: a `Partial` figure (a GPU counter alone) is sealed as such and gives no per-token rate, since it is not `E` | | `R` — functional unit (per token / inference) | `tokens_out` (accepted) → `uj_per_accepted_token()` | The unit is **sealed**, not asserted | | `SCI` rate | `uj_per_accepted_token`, `tokens_per_sec()`, `uusd_per_accepted_token()` | Rates **recompute from sealed integers** — reproducible, tamper-evident | | — (no outcome concept in SCI) | `completed` + `verdict`, `aggregate()` | The **outcome anchor** + waste accounting SCI has no notion of | diff --git a/jcp/jcp-rs/crates/jcp-efficiency/src/bin/gen_efficiency_vectors.rs b/jcp/jcp-rs/crates/jcp-efficiency/src/bin/gen_efficiency_vectors.rs new file mode 100644 index 000000000..a75f96915 --- /dev/null +++ b/jcp/jcp-rs/crates/jcp-efficiency/src/bin/gen_efficiency_vectors.rs @@ -0,0 +1,134 @@ +//! Emit the `efficiency/1` payload conformance pack to `jcp/conformance/v1/`. +//! +//! jcp SPEC §5.2 says a `Partial` figure MUST NOT be presented as a task total, +//! and §5.3 that aggregation is not a step at which a grade improves. These are +//! the vectors a third party replays to check an independent `efficiency/1` +//! reader against those rules: payloads (the signed core, once its signature +//! holds) with the reading a conformant reader must reach, the refusals +//! included, and receipt sets with the totals they must sum to. The payloads +//! carry no signature, because the rules are about what a verified receipt +//! says, not about the signature, which `jcr1` vectors already cover. +//! +//! Run: `cargo run -p jcp-efficiency --bin gen_efficiency_vectors -- ../conformance/v1` + +use std::path::PathBuf; + +use jcp_efficiency::{aggregate, read_payload, Efficiency}; +use jcp_schema::{ContentId, Energy}; +use serde_json::{json, Value}; + +fn unit(task: &str, completed: bool, tokens_out: u64, energy: Energy) -> Efficiency { + Efficiency { + task: task.into(), + completed, + verdict: if completed { "tests-pass".into() } else { "gate-failed".into() }, + tokens_in: tokens_out * 4, + tokens_out, + energy, + wall_ns: 1_000_000_000, + value_uusd: None, + energy_attestation: None, + } +} + +fn payload(e: &Efficiency) -> Value { + e.payload(0, &ContentId([0u8; 32]), "did:example:steward") +} + +/// A case carries the input and the verdict a conformant reader must reach. +fn case(note: &str, input: Value, expected: Value) -> Value { + json!({ "note": note, "input": input, "expected": expected }) +} + +fn read(note: &str, p: Value) -> Value { + let r = read_payload(&p).unwrap_or_else(|e| panic!("{note}: a positive must read: {e}")); + case( + note, + p, + json!({ + "energy_uj": r.energy_uj, + "provenance": r.provenance, + "energy_coverage": r.energy_coverage.label(), + "uj_per_accepted_token": r.uj_per_accepted_token, + }), + ) +} + +fn refuse(note: &str, p: Value, because: &str) -> Value { + let err = read_payload(&p).expect_err(note).to_string(); + assert!(err.contains(because), "{note}: refused for '{err}', not '{because}'"); + case(note, p, json!({ "refused": because })) +} + +/// A set of payloads and the totals a conformant reader must sum them to. +fn totals(note: &str, ps: Vec) -> Value { + let rs: Vec<_> = ps.iter().map(|p| read_payload(p).expect("reads")).collect(); + let t = aggregate(&rs); + case( + note, + json!(ps), + json!({ + "completed_uj": t.completed_uj, + "wasted_uj": t.wasted_uj, + "coverage": t.coverage.label(), + "provenance_floor": t.provenance_floor.map(|p| p.jcr1_label()), + "uj_per_completed_unit": t.uj_per_completed_unit(), + "tokens_per_completed_unit": t.tokens_per_completed_unit(), + }), + ) +} + +fn main() { + let out = std::env::args() + .nth(1) + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../../conformance/v1")); + std::fs::create_dir_all(&out).expect("create output dir"); + + let whole = unit("whole", true, 500, Energy::measured(1_000_000)); + let gpu_only = unit("gpu-only", true, 500, Energy::measured_partial(600_000)); + let estimated = unit("estimated", false, 200, Energy::estimator(400_000)); + + let mut presented = payload(&gpu_only); + presented["uj_per_accepted_token"] = json!(1_200); + let mut said_whole = payload(&whole); + said_whole["energy_coverage"] = json!("Whole"); + let mut unnamed = payload(&whole); + unnamed["provenance"] = json!("Measured"); + + let pack = json!({ + "profile": "efficiency/1", + "spec": "jcp/SPEC.md §5.2-5.3", + "payload": { + "positive": [ + read("a whole figure seals its per-token headline and no coverage field", payload(&whole)), + read( + "a GPU-only figure seals its coverage and no per-token headline: it is no task total", + payload(&gpu_only) + ), + ], + "negative": [ + refuse( + "a GPU-only figure presented as a task total, with a per-token headline", + presented, + "presented as a task total" + ), + refuse("an absent coverage is Whole; \"Whole\" on the wire is refused", said_whole, "energy_coverage"), + refuse("a provenance that is not one of the three labels", unnamed, "not a provenance label"), + ], + }, + "aggregate": { "cases": [ + totals( + "a total over a partial figure is partial and gives no per-unit energy; its provenance is the floor", + vec![payload(&whole), payload(&gpu_only), payload(&estimated)], + ), + totals( + "a total over whole figures gives its per-unit energy, at the weakest provenance", + vec![payload(&whole), payload(&estimated)], + ), + ] }, + }); + let path = out.join("efficiency.json"); + std::fs::write(&path, serde_json::to_string_pretty(&pack).expect("serialise") + "\n").expect("write pack"); + println!("wrote {}", path.display()); +} diff --git a/jcp/jcp-rs/crates/jcp-efficiency/src/lib.rs b/jcp/jcp-rs/crates/jcp-efficiency/src/lib.rs index 67b21a143..0b6258df9 100644 --- a/jcp/jcp-rs/crates/jcp-efficiency/src/lib.rs +++ b/jcp/jcp-rs/crates/jcp-efficiency/src/lib.rs @@ -35,7 +35,7 @@ use ed25519_dalek::SigningKey; use jcp_receipt::jcr1; -use jcp_schema::{ContentId, Energy}; +use jcp_schema::{ContentId, Coverage, Energy, Provenance}; pub mod avoided; /// Energy traceability across operation, activity and outcome. @@ -95,7 +95,18 @@ impl Efficiency { /// sealed and read straight back out rather than recomputed. `mesh/SPEC.md` /// §6 states the family convention this matches: "omitted when no tokens were /// accepted". + /// + /// Also `None` for a `Partial` figure. The headline is the unit's energy + /// per token, a ratio of a task total, and a `Partial` figure MUST NOT be + /// presented as one (jcp SPEC §5.2). A GPU-only reading divided by the + /// tokens is not what the unit cost per token, and not a bound on it + /// either: a partial figure can be short of the work or over it. The + /// receipt seals `energy_uj` with `energy_coverage` instead, so a reader + /// still sees what was measured and how much of the work it covers. pub fn uj_per_accepted_token(&self) -> Option { + if !self.energy.is_whole() { + return None; + } self.energy.uj().checked_div(self.tokens_out) } @@ -142,6 +153,13 @@ impl Efficiency { jcr1::cose_sign1_envelope(&core, steward_sk, steward) } + /// The payload [`Self::seal`] signs, as JSON: what a verifier reads back + /// with [`read_payload`] once the signature holds. Published conformance + /// vectors are built from it. + pub fn payload(&self, seq: u64, prev: &ContentId, steward: &str) -> Value { + self.wire(seq, prev, steward) + } + fn wire(&self, seq: u64, prev: &ContentId, steward: &str) -> Value { // JCR-1: integers + strings only. `completed` is 0/1, energy is integer µJ, // and the rate figures (tokens/sec, joules-in-J) are recomputed on read. @@ -166,6 +184,13 @@ impl Efficiency { if let Some(uj) = self.uj_per_accepted_token() { o["uj_per_accepted_token"] = json!(uj); } + // Carried only when `Partial` (jcp SPEC §5.2), so a receipt sealed + // before the field existed, and every whole one, keeps its bytes. It + // dropped out of the receipt entirely: a GPU-only figure sealed as + // `HwShunt` reached a verifier as a whole-task total. + if !self.energy.is_whole() { + o["energy_coverage"] = json!(self.energy.coverage.label()); + } if let Some(v) = self.value_uusd { o["value_uusd"] = json!(v); if let Some(per) = self.uusd_per_accepted_token() { @@ -260,8 +285,12 @@ pub struct EfficiencyReceipt { pub tokens_out: u64, /// Total energy, microjoules. pub energy_uj: u64, - /// Energy provenance label. + /// Energy provenance label: `HwShunt`, `ModelBased` or `Estimator`. pub provenance: String, + /// How much of the unit the energy figure covers. `Partial` when the + /// receipt carries `energy_coverage`; such a receipt seals no + /// `uj_per_accepted_token`, and is not a task total. + pub energy_coverage: Coverage, /// Wall-clock nanoseconds. pub wall_ns: u64, /// Microjoules per accepted token (headline). `None` when the receipt sealed @@ -294,11 +323,34 @@ impl EfficiencyReceipt { } /// Verify a sealed `efficiency/1` receipt against the steward key and recover its -/// fields. A flipped byte anywhere fails the signature. +/// fields ([`read_payload`]). A flipped byte anywhere fails the signature. pub fn verify(envelope: &[u8], steward: &str, pubkey: &[u8; 32]) -> Result { let payload = jcr1::verify_cose_sign1(envelope, steward, pubkey)?; let v: Value = jcp_schema::from_canonical_cbor(&payload).map_err(|e| EfficiencyError::Schema(e.to_string()))?; + read_payload(&v) +} + +/// The provenance a sealed label names, and `None` for a label that is not one. +fn provenance_of(label: &str) -> Option { + [Provenance::HwShunt, Provenance::ModelBased, Provenance::Estimator] + .into_iter() + .find(|p| p.jcr1_label() == label) +} + +/// Read an `efficiency/1` payload, the signed core [`verify`] recovers once the +/// signature holds, and refuse one that is not a well-formed receipt. It is +/// apart from [`verify`] so that the payload's rules can be replayed from +/// published vectors with no signature in them +/// (`jcp/conformance/v1/efficiency.json`). Beyond the required fields: +/// +/// - `provenance` MUST be one of the three labels; +/// - `energy_coverage`, when present, MUST be `"Partial"`. An absent field is +/// `Whole`, so `"Whole"` and every other value are refused rather than read +/// either way; +/// - a `Partial` receipt MUST NOT carry `uj_per_accepted_token`, since that +/// would present a partial figure as a task total (jcp SPEC §5.2). +pub fn read_payload(v: &Value) -> Result { if v.get("efficiency_receipt").and_then(Value::as_str) != Some("efficiency/1") { return Err(EfficiencyError::Malformed("not an efficiency/1 receipt".into())); } @@ -313,6 +365,25 @@ pub fn verify(envelope: &[u8], steward: &str, pubkey: &[u8; 32]) -> Result Coverage::Whole, + Some(c) if c.as_str() == Some(Coverage::Partial.label()) => Coverage::Partial, + Some(c) => { + return Err(EfficiencyError::Malformed(format!( + "energy_coverage {c} is not \"Partial\", the one value it carries; an absent field is Whole" + ))) + } + }; + let uj_per_accepted_token = v.get("uj_per_accepted_token").and_then(Value::as_u64); + if energy_coverage == Coverage::Partial && uj_per_accepted_token.is_some() { + return Err(EfficiencyError::Malformed( + "a Partial figure presented as a task total: it carries uj_per_accepted_token".into(), + )); + } Ok(EfficiencyReceipt { task: s("task")?, completed: u("completed")? != 0, @@ -320,9 +391,10 @@ pub fn verify(envelope: &[u8], steward: &str, pubkey: &[u8; 32]) -> Result, + /// `Partial` if any receipt summed was. The energy totals then do not + /// cover the work, and no per-unit energy figure is given. + pub coverage: Coverage, } impl Totals { @@ -393,8 +472,13 @@ impl Totals { } /// Total microjoules per completed unit, including waste. `None` when nothing - /// completed — see [`Self::tokens_per_completed_unit`]. + /// completed — see [`Self::tokens_per_completed_unit`] — and when any + /// receipt summed was `Partial`: the energy totals then do not cover the + /// work, so their ratio is not what a result cost (jcp SPEC §5.2). pub fn uj_per_completed_unit(&self) -> Option { + if !self.coverage.is_whole() { + return None; + } self.completed_uj .saturating_add(self.wasted_uj) .checked_div(self.completed_units) @@ -407,9 +491,13 @@ impl Totals { /// so the fallback is this figure's *worst* value, not its optimum. The same /// holds for [`Efficiency::tokens_per_sec`]. A zero that understates is not the /// defect those `Option`s fix. + /// + /// `0.0` too when any receipt summed was `Partial`, for the same reason: + /// value over an energy total that does not cover the work is not value + /// per joule of it, and this figure's fallback is its worst value. pub fn value_per_joule(&self) -> f64 { let total_uj = self.completed_uj.saturating_add(self.wasted_uj); - if total_uj == 0 { + if total_uj == 0 || !self.coverage.is_whole() { 0.0 } else { // value (µUSD) per joule = value_uusd / (total_uj / 1e6) @@ -430,10 +518,19 @@ impl Totals { } } -/// Aggregate verified receipts into the honest efficiency ledger. +/// Aggregate verified receipts into the honest efficiency ledger. The energy +/// totals carry the floor of what they sum: the weakest provenance and, if any +/// receipt was `Partial`, `Partial` coverage. Aggregation is not a step at +/// which a grade improves (jcp SPEC §5.3), and a sum of partial figures is no +/// more a task total than one of them is. pub fn aggregate<'a>(receipts: impl IntoIterator) -> Totals { let mut t = Totals::default(); for r in receipts { + // A label [`read_payload`] would refuse counts as the weakest: a + // receipt built by hand is not given a grade it does not name. + let p = provenance_of(&r.provenance).unwrap_or(Provenance::Estimator); + t.provenance_floor = Some(t.provenance_floor.map_or(p, |f| trace::weakest_provenance(f, p))); + t.coverage = trace::weakest_coverage(t.coverage, r.energy_coverage); if r.completed { t.completed_units += 1; t.completed_tokens = t.completed_tokens.saturating_add(r.tokens_in).saturating_add(r.tokens_out); @@ -470,6 +567,85 @@ mod tests { } } + /// **A GPU-only figure is not a task total.** It used to seal as + /// `energy_uj` + `provenance: HwShunt` with no coverage, and a per-token + /// headline over it, so it reached a verifier as the whole task's energy + /// per token. Now it seals its coverage, and no headline. + #[test] + fn a_partial_figure_seals_its_coverage_and_no_task_total() { + let sk = key(); + let mut e = measurement("gpu-only", true, 500, 0, None); + e.energy = Energy::measured_partial(600_000); + assert_eq!(e.uj_per_accepted_token(), None, "a partial figure gives no per-token cost of the task"); + let p = e.payload(0, &ContentId([0u8; 32]), "did:example:steward"); + assert_eq!(p["energy_coverage"], "Partial"); + assert!(p.get("uj_per_accepted_token").is_none(), "{p}"); + + let env = e.seal(0, &ContentId([0u8; 32]), &sk, "did:example:steward"); + let r = verify(&env, "did:example:steward", &sk.verifying_key().to_bytes()).unwrap(); + assert_eq!(r.energy_coverage, Coverage::Partial); + assert_eq!((r.energy_uj, r.provenance.as_str()), (600_000, "HwShunt"), "what was measured still reads"); + assert_eq!(r.uj_per_accepted_token, None); + + // A whole figure carries no coverage field, so its bytes are what they + // were before the field existed, and it reads back `Whole`. + let whole = measurement("whole", true, 500, 1_000_000, None); + let p = whole.payload(0, &ContentId([0u8; 32]), "did:example:steward"); + assert!(p.get("energy_coverage").is_none(), "{p}"); + assert_eq!(read_payload(&p).unwrap().energy_coverage, Coverage::Whole); + } + + /// A reader refuses a partial figure presented as a task total, a coverage + /// other than `"Partial"` (absent is `Whole`), and a provenance label that + /// is not one of the three. + #[test] + fn a_reader_refuses_what_a_conformant_receipt_never_carries() { + let mut e = measurement("gpu-only", true, 500, 0, None); + e.energy = Energy::measured_partial(600_000); + let base = e.payload(0, &ContentId([0u8; 32]), "did:example:steward"); + let refused = |edit: &dyn Fn(&mut Value), because: &str| { + let mut p = base.clone(); + edit(&mut p); + let err = read_payload(&p).expect_err(because).to_string(); + assert!(err.contains(because), "{err}"); + }; + refused(&|p| p["uj_per_accepted_token"] = json!(1_200), "presented as a task total"); + refused(&|p| p["energy_coverage"] = json!("Whole"), "energy_coverage"); + refused(&|p| p["energy_coverage"] = json!("partial"), "energy_coverage"); + refused(&|p| p["provenance"] = json!("Measured"), "not a provenance label"); + } + + /// A total is never more measured, nor more complete, than what it sums + /// (jcp SPEC §5.3): one estimated receipt makes it estimated, and one + /// partial receipt makes it partial, with no per-unit energy figure. + #[test] + fn aggregation_takes_the_floor_of_provenance_and_coverage() { + let sk = key(); + let pk = sk.verifying_key().to_bytes(); + let mut log = EfficiencyLog::new(sk, "did:example:steward"); + let mut hw = measurement("a", true, 100, 0, Some(1_000_000)); + hw.energy = Energy::measured(500_000); + let mut gpu = measurement("b", true, 100, 0, Some(1_000_000)); + gpu.energy = Energy::measured_partial(300_000); + let mut est = measurement("c", false, 100, 0, None); + est.energy = Energy::estimator(200_000); + log.append(&hw); + let whole_only = aggregate(&verify_chain(log.receipts(), "did:example:steward", &pk).unwrap()); + assert_eq!((whole_only.coverage, whole_only.provenance_floor), (Coverage::Whole, Some(Provenance::HwShunt))); + assert_eq!(whole_only.uj_per_completed_unit(), Some(500_000)); + + log.append(&est); + log.append(&gpu); + let t = aggregate(&verify_chain(log.receipts(), "did:example:steward", &pk).unwrap()); + assert_eq!(t.provenance_floor, Some(Provenance::Estimator), "one estimate makes the total estimated"); + assert_eq!(t.coverage, Coverage::Partial, "one partial figure makes the total partial"); + assert_eq!(t.uj_per_completed_unit(), None, "a partial total is no cost per result"); + assert_eq!(t.value_per_joule(), 0.0, "nor value per joule of the work"); + assert_eq!(t.completed_uj, 800_000, "what was measured is still summed"); + assert_eq!(t.tokens_per_completed_unit(), Some(750), "token figures do not depend on the energy"); + assert_eq!(aggregate(&[]).provenance_floor, None, "an empty ledger names no grade"); + } + #[test] fn the_four_metrics_compute_from_the_inputs() { let e = measurement("t", true, 500, 1_000_000, Some(2_000_000)); diff --git a/jcp/jcp-rs/crates/jcp-efficiency/src/trace.rs b/jcp/jcp-rs/crates/jcp-efficiency/src/trace.rs index 8f0b10bc0..98dfd0e02 100644 --- a/jcp/jcp-rs/crates/jcp-efficiency/src/trace.rs +++ b/jcp/jcp-rs/crates/jcp-efficiency/src/trace.rs @@ -258,7 +258,7 @@ impl OutcomeTrace { /// /// A composite is never more measured than its least-measured part. Averaging /// would let one hardware reading launder a pile of estimates. -fn weakest_provenance(a: Provenance, b: Provenance) -> Provenance { +pub(crate) fn weakest_provenance(a: Provenance, b: Provenance) -> Provenance { fn rank(p: Provenance) -> u8 { match p { Provenance::HwShunt => 2, @@ -275,7 +275,7 @@ fn weakest_provenance(a: Provenance, b: Provenance) -> Provenance { /// `Partial` if either is partial. An unaccounted share does not become /// accounted for by being summed with accounted ones. -fn weakest_coverage(a: Coverage, b: Coverage) -> Coverage { +pub(crate) fn weakest_coverage(a: Coverage, b: Coverage) -> Coverage { match (a, b) { (Coverage::Whole, Coverage::Whole) => Coverage::Whole, _ => Coverage::Partial, diff --git a/jcp/jcp-rs/crates/jcp-efficiency/tests/efficiency_pack.rs b/jcp/jcp-rs/crates/jcp-efficiency/tests/efficiency_pack.rs new file mode 100644 index 000000000..140c8dec2 --- /dev/null +++ b/jcp/jcp-rs/crates/jcp-efficiency/tests/efficiency_pack.rs @@ -0,0 +1,85 @@ +//! Replay the published `efficiency/1` payload pack from disk. +//! +//! jcp SPEC §5.2-5.3 states the rules and `gen_efficiency_vectors` emits the +//! vectors. This reads those bytes back, the same bytes a third party would, +//! and checks the reference reader reproduces every recorded verdict. The +//! refusals are the half that matters: a reader that accepts a partial figure +//! presented as a task total has failed in the direction that flatters. + +use jcp_efficiency::{aggregate, read_payload}; +use serde_json::{json, Value}; + +fn pack() -> Value { + let p = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../conformance/v1/efficiency.json"); + // Assert, not skip: a `return` here would pass green in the one case this + // exists to catch, the pack going missing. + assert!(p.exists(), "conformance pack missing at {} — it is what this test replays", p.display()); + let bytes = std::fs::read(&p).unwrap_or_else(|e| panic!("read {}: {e}", p.display())); + serde_json::from_slice(&bytes).expect("pack parses") +} + +#[test] +fn the_pack_declares_its_profile_and_spec() { + let v = pack(); + assert_eq!(v["profile"], "efficiency/1"); + assert!(v["spec"].as_str().unwrap().contains("5.2")); +} + +#[test] +fn every_positive_payload_reads_as_recorded() { + let v = pack(); + let cases = v["payload"]["positive"].as_array().expect("positives"); + assert!(cases.len() >= 2, "expected the published positive cases"); + for c in cases { + let note = c["note"].as_str().unwrap_or("?"); + let r = read_payload(&c["input"]).unwrap_or_else(|e| panic!("{note}: must read: {e}")); + let got = json!({ + "energy_uj": r.energy_uj, + "provenance": r.provenance, + "energy_coverage": r.energy_coverage.label(), + "uj_per_accepted_token": r.uj_per_accepted_token, + }); + assert_eq!(got, c["expected"], "{note}"); + } +} + +#[test] +fn every_negative_payload_is_refused_for_its_recorded_reason() { + let v = pack(); + let cases = v["payload"]["negative"].as_array().expect("negatives"); + assert!(cases.len() >= 3, "expected the published refusals"); + for c in cases { + let note = c["note"].as_str().unwrap_or("?"); + let because = c["expected"]["refused"].as_str().expect("a refusal reason"); + match read_payload(&c["input"]) { + Ok(r) => panic!("{note}: accepted, as {r:?}"), + Err(e) => assert!(e.to_string().contains(because), "{note}: refused for '{e}', not '{because}'"), + } + } +} + +#[test] +fn every_aggregate_sums_to_its_recorded_totals() { + let v = pack(); + let cases = v["aggregate"]["cases"].as_array().expect("aggregate cases"); + assert!(cases.len() >= 2); + for c in cases { + let note = c["note"].as_str().unwrap_or("?"); + let rs: Vec<_> = c["input"] + .as_array() + .expect("payloads") + .iter() + .map(|p| read_payload(p).unwrap_or_else(|e| panic!("{note}: {e}"))) + .collect(); + let t = aggregate(&rs); + let got = json!({ + "completed_uj": t.completed_uj, + "wasted_uj": t.wasted_uj, + "coverage": t.coverage.label(), + "provenance_floor": t.provenance_floor.map(|p| p.jcr1_label()), + "uj_per_completed_unit": t.uj_per_completed_unit(), + "tokens_per_completed_unit": t.tokens_per_completed_unit(), + }); + assert_eq!(got, c["expected"], "{note}"); + } +} -- 2.54.0 (Apple Git-157)