mesh §3.6: the receipt sealed the model by name, so the name was not a binding #246

Merged
dcharlot merged 2 commits from dcharlot/bind-weights-and-precision into main 2026-10-14 09:10:32 -04:00 AGit
6 changed files with 252 additions and 6 deletions

View file

@ -225,6 +225,14 @@ decision from a warm one.
> probabilities of a published decision model differ between `fp16` and `fp32`
> even where the selected answer agrees, so a receipt that omits precision does
> not reproduce.
>
> The fields are `weights_digest` and `precision` (§6.1), both OPTIONAL. A
> backend that can address its weights **SHOULD** report both; one that cannot
> omits them, and the receipt then binds the model by `recipe` alone — a name,
> which an operator can re-point at other weights while every receipt sealed
> under it goes on verifying. That is the weaker basis, and the absence of the
> fields is how a verifier can tell it apart from the stronger one, rather than
> having to assume.
---
@ -309,7 +317,9 @@ and nulls; booleans are encoded as `0|1` uints and absent options dropped.
| `mesh_receipt` | `"mesh/1"` |
| `request_id` | hex content id of the request |
| `grant_id` | hex JCP grant id this inference debited (omitted if ungranted) |
| `backend`, `recipe` | the backend and composed model that served it |
| `backend`, `recipe` | the backend and composed model that served it — `recipe` is a name, read it with the two fields below |
| `weights_digest` | hex content address of the weights that answered (OPTIONAL — §3.6; absent means the receipt binds the model by name alone) |
| `precision` | the numeric precision the forward pass ran at, as the backend names it — `fp32`, `fp16`, `bf16`, `int8` (OPTIONAL — §3.6; paired with `weights_digest`, since neither reproduces alone) |
| `route` | the router's one-line justification |
| `tokens_in/out/accepted`, `cache_hit_tokens`, `spec_proposed/accepted` | the outcome counters |
| `fidelity_floor` | the floor the request demanded (uint) |

View file

@ -213,22 +213,37 @@ impl<'m> Mesh<'m> {
let fidelity_met = basis >= req.fidelity_floor;
// 4. Build, chain, and seal the receipt.
//
// Read off the outcome before it is moved into the receipt. `ServeOutcome`
// stopped being `Copy` when `precision` made it hold a `String`, so the
// fields the receipt lifts out of it are taken first.
let nonce = nonce_from(&req.id());
let weights_digest = served.outcome.weights_digest;
let precision = served.outcome.precision.clone();
let delivered = served.outcome.delivered_fidelity;
let receipt = MeshReceipt {
request_id: req.id(),
grant_id,
backend: backend.clone(),
recipe,
// Carried through from the party that ran the weights, absent where
// it did not say. No check is needed on the way past: `ContentId` is
// 32 bytes, so a digest that is not one cannot have been reported,
// and the field's presence therefore always carries its substance.
weights_digest,
precision,
route_justification: route.justification.clone(),
outcome: served.outcome,
// Cloned, not moved: the response carries the same outcome, and
// `ServeOutcome` is six counters plus two options and a short name.
outcome: served.outcome.clone(),
fidelity_floor: req.fidelity_floor,
// The pair travels together: their purpose is to make a divergence
// between promise and delivery checkable, and the advertisement
// alone adds nothing a verifier does not already have from
// `backend`. Both absent is the historical shape, which seals to
// the bytes the published pack shipped with.
fidelity_advertised: served.outcome.delivered_fidelity.map(|_| advertised),
fidelity_delivered: served.outcome.delivered_fidelity,
fidelity_advertised: delivered.map(|_| advertised),
fidelity_delivered: delivered,
fidelity_met,
energy,
sequence: 0, // stamped by the log
@ -382,6 +397,8 @@ mod tests {
spec_accepted: 0,
decision: None,
delivered_fidelity: self.serves,
weights_digest: None,
precision: None,
},
}
}
@ -517,4 +534,53 @@ mod tests {
added.sort();
assert_eq!(added, vec!["fidelity_advertised", "fidelity_delivered"]);
}
/// The model identity rides the whole path — engine, receipt, envelope —
/// and is signed rather than advisory (SPEC §3.6).
///
/// Same shape as the fidelity test above, for the same reason: two meshes
/// differing only in whether the engine reports what ran. If the identity
/// were carried but not sealed the envelopes would match, and a verifier
/// comparing signatures could not tell a receipt bound to these weights
/// from one bound to any others.
#[test]
fn the_model_identity_rides_the_path_and_is_signed() {
let meter = SteppingMeter::new("test-rapl", Provenance::HwShunt, 1_000_000);
let (sk, steward) = steward_key();
let digest = ContentId::of_bytes(b"ssm-1b weights, revision 3");
let mut by_name = Mesh::new(&meter, sk.clone(), steward.clone());
by_name.register(
BackendSpec::new("tiny", "ssm-1b", Fidelity::F1, 50_000),
Box::new(ReferenceEngine::new("tiny", "ssm-1b", Fidelity::F1, 50_000, 256)),
);
let mut bound = Mesh::new(&meter, sk, steward);
bound.register(
BackendSpec::new("tiny", "ssm-1b", Fidelity::F1, 50_000),
Box::new(
ReferenceEngine::new("tiny", "ssm-1b", Fidelity::F1, 50_000, 256)
.reporting_model_identity(digest, "fp16"),
),
);
let req = request(8, Fidelity::F1, 10_000_000);
let a = by_name.serve(&req, &[], None).expect("serve");
let b = bound.serve(&req, &[], None).expect("serve");
// Both name the same recipe — which is the point: the name alone does
// not distinguish them, and before these fields nothing else did.
assert_eq!(a.receipt.recipe, b.receipt.recipe);
assert!(a.receipt.weights_digest.is_none());
assert_eq!(b.receipt.weights_digest, Some(digest));
assert_eq!(b.receipt.precision.as_deref(), Some("fp16"));
assert_ne!(a.envelope, b.envelope, "the identity must be signed, not advisory");
let (wa, wb) = (a.receipt.to_jcr1_wire("s"), b.receipt.to_jcr1_wire("s"));
assert_eq!(wb["weights_digest"], serde_json::json!(digest.to_hex()));
let (oa, ob) = (wa.as_object().unwrap(), wb.as_object().unwrap());
let mut added: Vec<&String> = ob.keys().filter(|k| !oa.contains_key(*k)).collect();
added.sort();
assert_eq!(added, vec!["precision", "weights_digest"]);
}
}

View file

@ -123,6 +123,13 @@ mod engine {
// makes no claim: the gate falls back to the advertised
// rating, which is where these engines already stood.
delivered_fidelity: None,
// The weights are a real file, so a digest is obtainable —
// but once, at load, never per request. Until this engine does
// that it reports absent rather than something cheaper that
// would look like a binding: the field's presence is a claim to
// re-executability, and a partial digest is worse than none.
weights_digest: None,
precision: None,
};
Served { tokens, outcome }
}
@ -261,6 +268,13 @@ mod falcon {
// makes no claim: the gate falls back to the advertised
// rating, which is where these engines already stood.
delivered_fidelity: None,
// The weights are a real file, so a digest is obtainable —
// but once, at load, never per request. Until this engine does
// that it reports absent rather than something cheaper that
// would look like a binding: the field's presence is a claim to
// re-executability, and a partial digest is worse than none.
weights_digest: None,
precision: None,
};
Served { tokens, outcome }
}

View file

@ -13,7 +13,7 @@
#![forbid(unsafe_code)]
use mesh_schema::{BackendId, Fidelity, ServeOutcome, ServeRequest};
use mesh_schema::{BackendId, ContentId, Fidelity, ServeOutcome, ServeRequest};
/// What an engine returns from one `serve`: the generated token ids and the
/// serving counters.
@ -69,6 +69,14 @@ pub struct ReferenceEngine {
/// it unconditionally would change every published vector and break every
/// verifier that byte-compares them.
report_delivered: bool,
/// What to report as the weights' address and the arithmetic's precision.
/// Both `None` by default, for the same reason as `report_delivered`: the
/// published `mesh/1` vectors are generated from this engine, and a receipt
/// that carries either field seals to different bytes. This engine answers
/// from a table, so it has no weights of its own to address — the switch
/// exists to make the fields exercisable end to end.
weights_digest: Option<ContentId>,
precision: Option<String>,
/// Seeds served before — a repeat counts as a cache hit.
seen: std::collections::HashSet<u64>,
}
@ -95,6 +103,8 @@ impl ReferenceEngine {
work_per_token,
spec_accept_permille: 0,
report_delivered: false,
weights_digest: None,
precision: None,
seen: std::collections::HashSet::new(),
}
}
@ -113,6 +123,21 @@ impl ReferenceEngine {
self
}
/// Report a weights address and a precision, as a backend serving real
/// weights would (SPEC §3.6).
///
/// Off by default for the same reason as `reporting_delivered_fidelity`:
/// the published vectors are built from this engine, and either field
/// changes the sealed bytes. With it, a vector exercises the binding; a
/// vector built without it keeps the bytes the pack shipped with. A real
/// backend needs no switch — it digests its weights once at load and
/// reports what it ran.
pub fn reporting_model_identity(mut self, weights_digest: ContentId, precision: &str) -> Self {
self.weights_digest = Some(weights_digest);
self.precision = Some(precision.to_string());
self
}
pub fn with_speculation(mut self, accept_permille: u32) -> Self {
self.spec_accept_permille = accept_permille.min(1000);
self
@ -201,6 +226,12 @@ impl ServingEngine for ReferenceEngine {
spec_accepted,
decision: None,
delivered_fidelity: self.report_delivered.then_some(self.fidelity),
// This engine answers from a table: there are no weights to
// address and no arithmetic whose precision could be reported.
// A backend serving real weights digests them once at load and
// reports it here; absent is the honest answer for this one.
weights_digest: self.weights_digest,
precision: self.precision.clone(),
};
Served { tokens, outcome }
}

View file

@ -55,7 +55,26 @@ pub struct MeshReceipt {
/// 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).
@ -140,6 +159,17 @@ impl MeshReceipt {
}
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));
@ -306,6 +336,8 @@ mod tests {
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,
@ -319,6 +351,60 @@ mod tests {
}
}
/// `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.
///

View file

@ -272,7 +272,10 @@ pub struct RouteDecision {
}
/// Per-request serving counters — the inputs to the J/accepted-token metric.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
// `Copy` was dropped when `precision` arrived: an open-vocabulary name is a
// `String`, and nothing in the workspace relied on copying this — removing the
// derive produced no error anywhere outside the new fields themselves.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServeOutcome {
pub tokens_in: u32,
pub tokens_out: u32,
@ -314,6 +317,42 @@ pub struct ServeOutcome {
/// basis.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub delivered_fidelity: Option<Fidelity>,
/// The content address of the weights that answered, as the party that ran
/// them reports it (SPEC §3.6).
///
/// `recipe` is a name, and a name is not a binding: re-point it at
/// different weights, a different quantization or a different revision and
/// every receipt sealed under it still verifies while describing a decode
/// some other model produced. A decision is one forward pass with no
/// sampling, so given the weights and the precision it is reproducible —
/// which is what makes a decision receipt checkable by re-execution rather
/// than merely signed.
///
/// A [`ContentId`], not a string, so a digest that is not one cannot be
/// reported at all. The field's presence is the claim that the model is
/// bound by content; a 32-byte address keeps that claim structural instead
/// of something a validator has to catch later.
///
/// `None` is the historical shape: skipped on the wire, and the receipt then
/// binds the model by name alone, which is the older and weaker basis.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub weights_digest: Option<ContentId>,
/// The numeric precision the forward pass ran at, named as the backend names
/// it (`fp32`, `fp16`, `bf16`, `int8`).
///
/// A `String`, not an enum, because the vocabulary is open and this standard
/// does not own it. An enum would either refuse a precision that really ran
/// or need a catch-all variant, and a catch-all that discards the name
/// records less than the string does.
///
/// Load-bearing for reproduction rather than descriptive: a published
/// decision model's probabilities differ between `fp16` and `fp32` even
/// where the selected answer agrees, so a receipt that omits the precision
/// does not reproduce (SPEC §3.6). Stored-weight quantization is the
/// digest's business; this is what the arithmetic ran in. `None` is the
/// historical shape.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub precision: Option<String>,
}
/// Per-request decision counters — the inputs to the µJ/decision metric