open-standards/mesh/mesh-rs/crates/mesh-receipt/src/lib.rs
dcharlot f7d4fb66be
Some checks failed
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (push) Has been cancelled
Merge pull request 'mesh: say what the energy figure covers — the served span, not the request' (#385) from fix/mesh-energy-span-scope into main
2026-10-21 10:20:04 -04:00

674 lines
30 KiB
Rust

//! The Mesh inference serving receipt (SPEC §6).
//!
//! A [`MeshReceipt`] is the signed, hash-chained record of one metered
//! inference. Its headline figure — the thing no `$/token` or `tok/s` dashboard
//! exposes — is [`MeshReceipt::uj_per_accepted_token`]: microjoules per accepted
//! output token, derived from a real energy span. The receipt also records the
//! route that was taken and *why*, the recipe that served it, and the honest
//! [`Provenance`] of the energy figure. A dishonest receipt (a `HwShunt` claim
//! with no measurement) cannot be sealed.
use ed25519_dalek::SigningKey;
use jcp_schema::ContentId;
use mesh_schema::{BackendId, Energy, Fidelity, SchemaError, ServeOutcome};
use serde::{Deserialize, Serialize};
use serde_json::json;
pub mod change;
pub mod jcr1;
pub use mesh_schema::Provenance;
/// Errors from building, signing, or verifying a receipt.
#[derive(Debug, thiserror::Error)]
pub enum ReceiptError {
/// `Provenance::HwShunt` with no `measured_uj`.
#[error("HwShunt provenance requires a measured figure")]
HwShuntRequiresMeasured,
/// `Provenance::Estimator` with a `measured_uj` present (dishonest).
#[error("Estimator provenance must not carry a measured figure")]
EstimatorMustNotCarryMeasured,
/// The envelope verified but its payload did not match this receipt's core.
#[error("envelope payload does not match receipt")]
PayloadMismatch,
#[error(transparent)]
Jcr1(#[from] jcr1::Jcr1Error),
}
impl From<SchemaError> for ReceiptError {
fn from(e: SchemaError) -> Self {
match e {
SchemaError::HwShuntRequiresMeasured => ReceiptError::HwShuntRequiresMeasured,
SchemaError::EstimatorMustNotCarryMeasured => {
ReceiptError::EstimatorMustNotCarryMeasured
}
}
}
}
/// A signed receipt for one metered inference (SPEC §6).
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct MeshReceipt {
/// Content id of the [`ServeRequest`](mesh_schema::ServeRequest).
pub request_id: ContentId,
/// The JCP grant id this inference debited, if it rode on one.
pub grant_id: Option<ContentId>,
/// The backend that served the request.
pub backend: BackendId,
/// The recipe (composed model) that served it.
///
/// A name. Read it with `weights_digest` and `precision`: alone it says
/// which label the operator gave the model, not which weights ran.
pub recipe: String,
/// The content address of the weights `recipe` named, when the backend
/// reported one (SPEC §3.6).
///
/// This is what makes `recipe` binding rather than advisory. Sealed into the
/// signed core, so re-pointing a name at different weights no longer leaves
/// past receipts verifying against a model that did not answer. `None` is
/// the historical shape, skipped on the wire — the receipt then binds the
/// model by name alone, and says so by the field's absence.
pub weights_digest: Option<ContentId>,
/// The numeric precision the forward pass ran at (SPEC §3.6), when the
/// backend reported one.
///
/// Travels with `weights_digest` because neither reproduces alone: the same
/// weights at `fp16` and `fp32` give different probabilities even where the
/// selected answer agrees. `None` is the historical shape, skipped.
pub precision: Option<String>,
/// One-line, auditable reason the router chose this route.
pub route_justification: String,
/// Serving counters (the inputs to the J/accepted-token figure).
pub outcome: ServeOutcome,
/// The fidelity floor the request demanded.
pub fidelity_floor: Fidelity,
/// The level the backend is **rated** to deliver — the advertisement the
/// router admitted it on (§4).
///
/// Sealed beside `fidelity_delivered` so a backend that serves below its
/// rating is attributable from the receipt alone. The two travel as a pair —
/// their point is the comparison, and the advertisement alone adds nothing a
/// verifier does not already have from `backend`. `None` is the historical
/// shape and skipped on the wire.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fidelity_advertised: Option<Fidelity>,
/// The level the backend says it actually **delivered**, where it reports
/// one (`ServeOutcome::delivered_fidelity`).
///
/// `None` is the historical shape and skipped on the wire; `fidelity_met`
/// then rests on the advertisement alone. Presence is the basis tag: no
/// separate field says which figure `fidelity_met` came from, because
/// presence already does, and a receipt predating the field means exactly
/// what an absent value means — the figure is an advertisement.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fidelity_delivered: Option<Fidelity>,
/// Whether the floor was met.
///
/// Derived from `fidelity_delivered` when the backend reported one, and from
/// the advertisement otherwise. Read it together with
/// `fidelity_delivered`: without that field it says the route table promised
/// the floor, not that the backend delivered it.
pub fidelity_met: bool,
/// The honest, provenance-tagged energy of the **served span** — the meter
/// session around `engine.serve`, and nothing before it.
///
/// Routing and grant authorisation run first and are **not** in this
/// figure, so it understates the energy of handling the request by
/// whatever they cost (SPEC §3.3). Negligible for a table-driven router;
/// not negligible where the router itself runs a model, and the shortfall
/// falls in the unsafe direction, because the grant is debited this figure
/// and admits further work on it. Do not present it as the request's
/// energy.
pub energy: Energy,
/// The JCP denial label when debiting the metered joules against the grant
/// breached it (SPEC §2): a per-call cost caveat the measured cost
/// exceeded. The work had already run, so the joules were debited anyway;
/// this field is what keeps the breach visible instead of absorbed.
/// `None`, skipped on the wire, means the debit was within the grant, or
/// the serve rode on no grant.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub grant_breach: Option<String>,
/// A commitment to the output the serve produced (SPEC §6.4), for a
/// generation: [`output_commitment`] over the output tokens, under an
/// opening the steward holds and discloses for this receipt alone.
///
/// Without it the receipt bound what was asked (`request_id`, which
/// content-addresses the prompt) and what it cost, and nothing about what
/// came back, so no one could later show which answer a receipt was for.
/// It is a commitment, not a hash of the tokens: a hash of a short output
/// (a one-token answer) can be reversed by trying every output, and a
/// receipt is shown to parties who were not owed the answer. With the
/// opening, anyone checks a disclosed output against the receipt
/// ([`MeshReceipt::opens`]); without it the commitment says nothing about
/// the output. `None`, skipped on the wire, for a typed decision (whose
/// answer is not carried) and for receipts sealed before the field existed.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_commitment: Option<ContentId>,
/// Monotonic sequence in the steward's receipt log.
pub sequence: u64,
/// Content id of the previous envelope (hash chain); `None` at genesis.
pub prev: Option<ContentId>,
/// Per-receipt nonce (derived from the request id).
pub nonce: [u8; 8],
}
impl MeshReceipt {
/// The wedge metric: microjoules per accepted output token. `None` when no
/// tokens were accepted (division undefined).
pub fn uj_per_accepted_token(&self) -> Option<u64> {
// `checked_div` yields None on zero accepted tokens (division undefined).
self.energy.uj().checked_div(self.outcome.tokens_accepted as u64)
}
/// The wedge metric for a backend that emits no tokens: microjoules per
/// answered decision (SPEC §3.6). `None` when the request was not a
/// decision, or answered no questions.
///
/// A typed decision's `tokens_accepted` is zero, so
/// [`Self::uj_per_accepted_token`] is undefined for every request in that
/// category — the receipt would seal with no efficiency figure at all. The
/// unit of work here is the answered question, not the accepted token.
pub fn uj_per_decision(&self) -> Option<u64> {
let d = self.outcome.decision?;
self.energy.uj().checked_div(d.questions_answered as u64)
}
/// Microjoules per option scored — the finer figure, since one question
/// over 20 options is more work than one over 2.
pub fn uj_per_option_scored(&self) -> Option<u64> {
let d = self.outcome.decision?;
self.energy.uj().checked_div(d.options_scored as u64)
}
/// Project to the JCR-1 `mesh/1` wire JSON. Booleans are encoded as `0|1`
/// uints and absent options dropped, so the signed core stays within the
/// det-CBOR type set (no bools, no nulls). `steward` is the endpoint
/// identity placed in the COSE `kid`.
pub fn to_jcr1_wire(&self, steward: &str) -> serde_json::Value {
let mut obj = serde_json::Map::new();
obj.insert("mesh_receipt".into(), json!("mesh/1"));
obj.insert("request_id".into(), json!(self.request_id.to_hex()));
if let Some(g) = &self.grant_id {
obj.insert("grant_id".into(), json!(g.to_hex()));
}
obj.insert("backend".into(), json!(self.backend.0));
obj.insert("recipe".into(), json!(self.recipe));
// Emitted only when the backend reported them, so a receipt that binds
// the model by name alone seals to the bytes it sealed to before these
// fields existed — the published pack's vectors included. `canonical_cbor`
// orders keys itself (by length, then lexicographically), so where these
// are inserted here never reaches the wire.
if let Some(d) = self.weights_digest {
obj.insert("weights_digest".into(), json!(d.to_hex()));
}
if let Some(p) = &self.precision {
obj.insert("precision".into(), json!(p));
}
obj.insert("route".into(), json!(self.route_justification));
obj.insert("tokens_in".into(), json!(self.outcome.tokens_in));
obj.insert("tokens_out".into(), json!(self.outcome.tokens_out));
obj.insert("tokens_accepted".into(), json!(self.outcome.tokens_accepted));
obj.insert("cache_hit_tokens".into(), json!(self.outcome.cache_hit_tokens));
obj.insert("spec_proposed".into(), json!(self.outcome.spec_proposed));
obj.insert("spec_accepted".into(), json!(self.outcome.spec_accepted));
obj.insert("fidelity_floor".into(), json!(self.fidelity_floor.0));
// Emitted only when known, so a receipt that rests on the advertisement
// alone seals to the bytes it sealed to before these fields existed.
if let Some(f) = self.fidelity_advertised {
obj.insert("fidelity_advertised".into(), json!(f.0));
}
if let Some(f) = self.fidelity_delivered {
obj.insert("fidelity_delivered".into(), json!(f.0));
}
obj.insert("fidelity_met".into(), json!(u8::from(self.fidelity_met)));
obj.insert("estimated_uj".into(), json!(self.energy.estimated_uj));
if let Some(m) = self.energy.measured_uj {
obj.insert("measured_uj".into(), json!(m));
}
if let Some(jpt) = self.uj_per_accepted_token() {
obj.insert("uj_per_accepted_token".into(), json!(jpt));
}
// A typed decision (§3.6). Emitted only when the request was one, so a
// generation's signed core is byte-identical to before.
if let Some(d) = self.outcome.decision {
obj.insert("questions_answered".into(), json!(d.questions_answered));
obj.insert("options_scored".into(), json!(d.options_scored));
obj.insert("schema_tokens".into(), json!(d.schema_tokens));
if let Some(jpd) = self.uj_per_decision() {
obj.insert("uj_per_decision".into(), json!(jpd));
}
if let Some(jpo) = self.uj_per_option_scored() {
obj.insert("uj_per_option_scored".into(), json!(jpo));
}
}
obj.insert(
"energy_provenance".into(),
json!(self.energy.provenance.jcr1_label()),
);
// Emitted only on a breach, so every receipt whose debit was within its
// grant seals to the bytes it sealed to before the field existed.
if let Some(b) = &self.grant_breach {
obj.insert("grant_breach".into(), json!(b));
}
// Emitted only for a serve that committed to its output, so a receipt
// sealed before the field existed keeps its bytes.
if let Some(c) = &self.output_commitment {
obj.insert("output_commitment".into(), json!(c.to_hex()));
}
obj.insert("sequence".into(), json!(self.sequence));
if let Some(p) = &self.prev {
obj.insert("prev".into(), json!(p.to_hex()));
}
obj.insert("nonce".into(), json!(hex_lower(&self.nonce)));
obj.insert("steward".into(), json!(steward));
serde_json::Value::Object(obj)
}
/// Whether `tokens` are the output this receipt committed to, served on
/// the prompt payload whose [`payload_digest`] is `payload`, under
/// `opening`: the check a party runs on what the steward disclosed for
/// this receipt (SPEC §6.4). `false` for a receipt that commits to no
/// output.
pub fn opens(&self, opening: &[u8; 32], payload: &ContentId, tokens: &[u32]) -> bool {
self.output_commitment == Some(output_commitment(opening, &self.request_id, payload, tokens))
}
/// The canonical signed-core bytes (JCR-1 det-CBOR). Validates the
/// provenance ↔ measured coupling first, so a dishonest receipt cannot be
/// turned into bytes a verifier would accept.
pub fn signed_core(&self, steward: &str) -> Result<Vec<u8>, ReceiptError> {
self.energy.check_provenance()?;
Ok(jcr1::signed_core_bytes(&self.to_jcr1_wire(steward))?)
}
/// Seal into a JCR-1 COSE_Sign1 envelope (SPEC §6).
pub fn seal(&self, key: &SigningKey, steward: &str) -> Result<Vec<u8>, ReceiptError> {
let core = self.signed_core(steward)?;
Ok(jcr1::cose_sign1_envelope(&core, key, steward))
}
/// Verify a sealed envelope against an endpoint key and confirm its payload
/// is exactly this receipt's canonical signed core.
pub fn verify_envelope(
&self,
envelope: &[u8],
steward: &str,
endpoint: &[u8; 32],
) -> Result<(), ReceiptError> {
let payload = jcr1::verify_cose_sign1(envelope, steward, endpoint)?;
if payload != self.signed_core(steward)? {
return Err(ReceiptError::PayloadMismatch);
}
Ok(())
}
}
/// The domain an output commitment is drawn in, so that it can equal no other
/// content address the family computes.
const OUTPUT_DOMAIN: &[u8] = b"mesh/1 output commitment\0";
/// The domain a prompt payload's digest is drawn in.
const PAYLOAD_DOMAIN: &[u8] = b"mesh/1 prompt payload\0";
/// The digest of the prompt payload an engine was handed: the content
/// address of the domain and the token ids as little-endian `u32`s (SPEC
/// §6.4).
///
/// The request's `prompt_hash` addresses the prompt's bytes, and the engine is
/// handed token ids. Nothing ties the two together without the tokenizer, so
/// the output commitment binds this digest as well.
pub fn payload_digest(prompt: &[u32]) -> ContentId {
let mut bytes = Vec::with_capacity(PAYLOAD_DOMAIN.len() + prompt.len() * 4);
bytes.extend_from_slice(PAYLOAD_DOMAIN);
for t in prompt {
bytes.extend_from_slice(&t.to_le_bytes());
}
ContentId::of_bytes(&bytes)
}
/// Commit to a generation's output `tokens`, for the request `request_id`
/// served on the prompt payload whose [`payload_digest`] is `payload`, under
/// `opening` (SPEC §6.4): the content address of the domain, the opening, the
/// request id, the payload digest and the tokens as little-endian `u32`s.
///
/// BLAKE3 with a secret 32-byte prefix is a commitment: without the opening
/// it reveals nothing about the tokens, however few they are, and no one can
/// find a second output under the same opening. Binding the request id means
/// an opening disclosed for one receipt opens no other. Binding the payload
/// means an output that opens it was served on that payload, so a replay can
/// tell a different payload from a different output.
pub fn output_commitment(opening: &[u8; 32], request_id: &ContentId, payload: &ContentId, tokens: &[u32]) -> ContentId {
let mut bytes = Vec::with_capacity(OUTPUT_DOMAIN.len() + 96 + tokens.len() * 4);
bytes.extend_from_slice(OUTPUT_DOMAIN);
bytes.extend_from_slice(opening);
bytes.extend_from_slice(&request_id.0);
bytes.extend_from_slice(&payload.0);
for t in tokens {
bytes.extend_from_slice(&t.to_le_bytes());
}
ContentId::of_bytes(&bytes)
}
/// A steward's hash-chained receipt log. Each [`append`](ReceiptLog::append)
/// stamps the next sequence and the prior envelope's content id, seals the
/// receipt, and advances the head — so the log is tamper-evident end to end.
pub struct ReceiptLog {
steward_sk: SigningKey,
steward: String,
head: Option<ContentId>,
seq: u64,
entries: Vec<Vec<u8>>,
}
impl ReceiptLog {
/// Start an empty log for `steward`, signing with `steward_sk`.
pub fn new(steward_sk: SigningKey, steward: impl Into<String>) -> Self {
Self {
steward_sk,
steward: steward.into(),
head: None,
seq: 0,
entries: Vec::new(),
}
}
/// The steward identity (COSE `kid`) this log signs under.
pub fn steward(&self) -> &str {
&self.steward
}
/// The steward's public key bytes (for verifiers).
pub fn verifying_key(&self) -> [u8; 32] {
self.steward_sk.verifying_key().to_bytes()
}
/// Content id of the most recent sealed envelope (the chain head).
pub fn head(&self) -> Option<ContentId> {
self.head
}
/// All sealed envelopes appended so far, oldest first.
pub fn entries(&self) -> &[Vec<u8>] {
&self.entries
}
/// Chain a receipt: stamp `sequence` + `prev`, seal, advance the head.
/// Returns the stamped receipt and its sealed envelope.
pub fn append(&mut self, receipt: MeshReceipt) -> Result<(MeshReceipt, Vec<u8>), ReceiptError> {
let (receipt, envelope) = self.seal_next(receipt)?;
self.advance(envelope.clone());
Ok((receipt, envelope))
}
/// Stamp and seal the next receipt without advancing the log, so a
/// steward can record it durably first and [`advance`](Self::advance)
/// only once it has. Sealing the same receipt again gives the same bytes.
pub fn seal_next(&self, mut receipt: MeshReceipt) -> Result<(MeshReceipt, Vec<u8>), ReceiptError> {
receipt.sequence = self.seq;
receipt.prev = self.head;
let envelope = receipt.seal(&self.steward_sk, &self.steward)?;
Ok((receipt, envelope))
}
/// Advance past an envelope [`seal_next`](Self::seal_next) produced.
pub fn advance(&mut self, envelope: Vec<u8>) {
self.head = Some(ContentId::of_bytes(&envelope));
self.seq += 1;
self.entries.push(envelope);
}
/// Continue a chain an earlier process sealed: the next receipt takes
/// `next_sequence` and chains to `head`, the content id of the last
/// envelope sealed. Envelopes sealed before are not in
/// [`entries`](Self::entries); they were handed out when sealed.
pub fn resume(&mut self, next_sequence: u64, head: Option<ContentId>) {
self.seq = next_sequence;
self.head = head;
}
/// The sequence the next receipt will take.
pub fn next_sequence(&self) -> u64 {
self.seq
}
}
/// Lowercase hex without external deps (nonce rendering for the wire form).
fn hex_lower(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push(char::from_digit((b >> 4) as u32, 16).unwrap());
s.push(char::from_digit((b & 0x0f) as u32, 16).unwrap());
}
s
}
#[cfg(test)]
mod tests {
use super::*;
use mesh_schema::{DecisionOutcome, Provenance};
fn outcome(accepted: u32) -> ServeOutcome {
ServeOutcome {
tokens_in: 8,
tokens_out: accepted,
tokens_accepted: accepted,
..Default::default()
}
}
fn receipt(energy: Energy, accepted: u32) -> MeshReceipt {
MeshReceipt {
request_id: ContentId::of_bytes(b"req"),
grant_id: None,
backend: BackendId::new("ref-f1"),
recipe: "tiny-greedy".into(),
weights_digest: None,
precision: None,
route_justification: "cheapest-sufficient".into(),
outcome: outcome(accepted),
fidelity_floor: Fidelity::F1,
fidelity_advertised: Some(Fidelity::F2),
fidelity_delivered: Some(Fidelity::F2),
fidelity_met: true,
energy,
grant_breach: None,
output_commitment: None,
sequence: 0,
prev: None,
nonce: [1, 2, 3, 4, 5, 6, 7, 8],
}
}
/// `recipe` is a name; `weights_digest` and `precision` are what make it
/// binding (SPEC §3.6).
///
/// Three properties, and the third is the one that matters. Without it the
/// other two would pass on fields that reached the struct and never reached
/// the signed core — present on the wire, absent from what the signature
/// covers, which is exactly the shape of a binding that is not one.
#[test]
fn the_model_identity_is_sealed_not_merely_carried() {
let steward = "did:example:mesh-endpoint";
let sk = SigningKey::from_bytes(&[9u8; 32]);
let digest = ContentId::of_bytes(b"the weights that answered");
// 1. Absent is the historical shape: it seals to the bytes it sealed to
// before these fields existed, which is what keeps the published
// pack's vectors verifying.
let bare = receipt(Energy::measured(1_000), 4);
assert!(bare.weights_digest.is_none() && bare.precision.is_none());
let bare_wire = bare.to_jcr1_wire(steward);
assert!(
bare_wire.get("weights_digest").is_none() && bare_wire.get("precision").is_none(),
"an unreported identity must be absent from the wire, not null or empty"
);
let bare_core = bare.signed_core(steward).unwrap();
// 2. Present, it reaches the wire as hex and as the backend's own name,
// and the receipt still seals and verifies.
let mut bound = receipt(Energy::measured(1_000), 4);
bound.weights_digest = Some(digest);
bound.precision = Some("fp16".into());
let bound_wire = bound.to_jcr1_wire(steward);
assert_eq!(bound_wire["weights_digest"], json!(digest.to_hex()));
assert_eq!(bound_wire["precision"], json!("fp16"));
let env = bound.seal(&sk, steward).unwrap();
assert!(bound
.verify_envelope(&env, steward, &sk.verifying_key().to_bytes())
.is_ok());
// 3. The binding is in the signed core. Same receipt but for the
// identity ⇒ different sealed bytes, so a signature over one cannot
// be read as a signature over the other.
let bound_core = bound.signed_core(steward).unwrap();
assert_ne!(
bare_core, bound_core,
"the identity must change the bytes the signature covers, or it binds nothing"
);
// And the digest is what distinguishes them, not just its presence:
// a different digest is a different core again.
let mut other = bound.clone();
other.weights_digest = Some(ContentId::of_bytes(b"different weights"));
assert_ne!(bound_core, other.signed_core(steward).unwrap());
}
/// A typed decision accepts no output tokens, so the token metric is
/// undefined and the decision metric is the one that exists.
///
/// The figure is the **CPU-only, process-attributed** one: 486 000 µJ for a
/// three-question decision on an M5 Max, 162 mJ per question, from the
/// kernel's per-process counter over a span far longer than its measured
/// 10 ms period.
///
/// Deliberately not the GPU figure. A GPU reading here comes from an SoC
/// rail that counts the whole machine, and on this host another process
/// was measured holding **99.5% of GPU time** in a 4 s window — so a rail
/// delta over a decision's span is an upper bound on the machine, not that
/// decision's energy. The per-process CPU counter is unaffected by that
/// contention, which is why the sound example uses it.
#[test]
fn a_decision_has_an_efficiency_figure_where_the_token_metric_has_none() {
let mut o = outcome(0);
o.decision = Some(DecisionOutcome {
questions_answered: 3,
options_scored: 8, // choice:3 + score:3 + noul:2
schema_tokens: 121,
});
let mut r = receipt(Energy::measured(486_000), 0);
r.outcome = o;
// The defect: the documented "wedge metric" is undefined here.
assert_eq!(r.uj_per_accepted_token(), None);
// The fix: the unit of work is the answered question.
assert_eq!(r.uj_per_decision(), Some(162_000));
assert_eq!(r.uj_per_option_scored(), Some(60_750));
// It survives the wire, and the wire says what it measured.
let wire = r.to_jcr1_wire("did:example:mesh-endpoint");
assert_eq!(wire["uj_per_decision"], json!(162_000));
assert_eq!(wire["questions_answered"], json!(3));
assert_eq!(wire["schema_tokens"], json!(121));
assert!(
wire.get("uj_per_accepted_token").is_none(),
"an undefined metric must be absent, not zero"
);
// And it still seals.
let sk = SigningKey::from_bytes(&[9u8; 32]);
let steward = "did:example:mesh-endpoint";
let env = r.seal(&sk, steward).unwrap();
assert!(r
.verify_envelope(&env, steward, &sk.verifying_key().to_bytes())
.is_ok());
}
/// The addition must be invisible to a generation: no new keys, so every
/// receipt sealed before it verifies byte-identically.
#[test]
fn a_generation_receipt_gains_no_wire_keys() {
let r = receipt(Energy::measured(3_200_000), 64);
let wire = r.to_jcr1_wire("did:example:mesh-endpoint");
for k in [
"questions_answered",
"options_scored",
"schema_tokens",
"uj_per_decision",
"uj_per_option_scored",
] {
assert!(wire.get(k).is_none(), "{k} must not appear for a generation");
}
assert_eq!(r.uj_per_decision(), None);
}
#[test]
fn measured_receipt_seal_verify_and_metric() {
let sk = SigningKey::from_bytes(&[9u8; 32]);
let steward = "did:example:mesh-endpoint";
// 64 accepted tokens cost 3_200_000 µJ measured → 50_000 µJ/token.
let r = receipt(Energy::measured(3_200_000), 64);
assert_eq!(r.uj_per_accepted_token(), Some(50_000));
let env = r.seal(&sk, steward).unwrap();
assert!(r
.verify_envelope(&env, steward, &sk.verifying_key().to_bytes())
.is_ok());
// Wrong steward must not verify.
assert!(r
.verify_envelope(&env, "did:example:other", &sk.verifying_key().to_bytes())
.is_err());
}
#[test]
fn dishonest_hwshunt_cannot_seal() {
let sk = SigningKey::from_bytes(&[9u8; 32]);
let r = receipt(
Energy {
measured_uj: None,
estimated_uj: 1_000,
provenance: Provenance::HwShunt,
},
10,
);
assert!(matches!(
r.seal(&sk, "did:example:x"),
Err(ReceiptError::HwShuntRequiresMeasured)
));
}
#[test]
fn chain_links_and_verifies() {
let sk = SigningKey::from_bytes(&[3u8; 32]);
let mut log = ReceiptLog::new(sk, "did:example:mesh");
let (r0, _e0) = log.append(receipt(Energy::estimator(1_000), 4)).unwrap();
let (r1, _e1) = log.append(receipt(Energy::estimator(2_000), 8)).unwrap();
assert_eq!(r0.sequence, 0);
assert!(r0.prev.is_none());
assert_eq!(r1.sequence, 1);
assert_eq!(r1.prev, Some(ContentId::of_bytes(&log.entries()[0])));
// Every entry verifies under the steward key.
let vk = log.verifying_key();
for (r, env) in [r0, r1].iter().zip(log.entries()) {
assert!(r.verify_envelope(env, log.steward(), &vk).is_ok());
}
}
/// The commitment binds the request, the opening, the prompt payload and
/// every token: change any one and it is another commitment, so an opening
/// disclosed for one receipt opens no other, and no other output, nor the
/// same output on another payload, opens it.
#[test]
fn an_output_commitment_binds_request_opening_payload_and_tokens() {
let (a, b) = (ContentId::of_bytes(b"request a"), ContentId::of_bytes(b"request b"));
let o = [3u8; 32];
let p = payload_digest(&[1, 2]);
let t = [7u32, 9, 11];
let c = output_commitment(&o, &a, &p, &t);
assert_ne!(c, output_commitment(&o, &b, &p, &t), "the request");
assert_ne!(c, output_commitment(&[4u8; 32], &a, &p, &t), "the opening");
assert_ne!(c, output_commitment(&o, &a, &payload_digest(&[1, 3]), &t), "the payload");
assert_ne!(c, output_commitment(&o, &a, &p, &[7, 9, 12]), "a token");
assert_ne!(c, output_commitment(&o, &a, &p, &[7, 9]), "the length");
assert_eq!(c, output_commitment(&o, &a, &p, &t), "and nothing else");
assert_ne!(payload_digest(&[]), payload_digest(&[0]), "an empty payload is not a zero token");
}
}