Some checks failed
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (push) Has been cancelled
674 lines
30 KiB
Rust
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");
|
|
}
|
|
}
|