mesh: a grant is spent only by its issuer, at the steward it names, and its spend survives a restart #301

Merged
dcharlot merged 1 commit from feat/mesh-caller-auth-durable-spend into main 2026-10-18 00:42:18 -04:00
12 changed files with 757 additions and 104 deletions

View file

@ -544,6 +544,8 @@ dependencies = [
"mesh-receipt",
"mesh-router",
"mesh-schema",
"serde",
"serde_json",
"thiserror",
]
@ -566,7 +568,6 @@ name = "mesh-openai"
version = "0.1.0"
dependencies = [
"hex",
"jcp-schema",
"mesh-core",
"mesh-schema",
"serde_json",

View file

@ -33,7 +33,10 @@ Four roles — **caller**, **router**, **engine**, **steward** — and four phas
per serve: route, authorise, meter, seal. The grant is decided by JCP's own
runtime before any work runs, so its expiry, caveats and remaining budget bind
here as at any JCP enforcement point, and the metered joules are debited
against it afterwards. See [`SPEC.md`](SPEC.md) §2.
against it afterwards. A grant is spent only by its issuer, who signs each
presentation for one request, and only at the steward it was issued to; each
decision is journalled before the work, so a restart gives no budget back. See
[`SPEC.md`](SPEC.md) §2.
## The reference workspace

View file

@ -37,8 +37,8 @@ and proves it (§9).
A Mesh deployment has four roles:
- **Caller** — issues a `ServeRequest` and, optionally, the JCP grant that
authorises it.
- **Caller** — issues a `ServeRequest` and, optionally, presents a JCP grant it
issued to the steward, signed for that request (§2.1).
- **Router** — selects a backend (§4).
- **Engine** — a backend that serves the request (§5).
- **Steward** — the endpoint that meters the span (§5/§6) and seals the receipt
@ -48,19 +48,22 @@ One serve proceeds in four phases, in order:
1. **Route.** The router selects the cheapest admissible backend (§4). If none is
admissible, the serve fails with the router's recorded reason. When the
request rides on a JCP `SignedGrant` carrying a `ModelIn` caveat (JCP §4.9),
request rides on a grant whose chain carries a `ModelIn` caveat (JCP §4.9),
a backend whose model the grant does not permit is inadmissible, and so is a
backend that names no model.
2. **Authorise.** If the request rides on a grant, the steward is a JCP
enforcement point and MUST decide the grant in JCP's two-phase metered path
(JCP §7, §4.9) before any work is done. The frame it decides over names the
action `infer` on the request's content id, the request's `prompt_hash` as
payload, and, as `model`, the content address of the weights the chosen
backend runs, when the backend names one. Every check of that decision
applies: signature, expiry, revocation, and every cost-independent caveat.
The steward MUST also refuse the serve when what the grant has left (its
budget less what earlier serves on it were debited) is below the request's
joule ceiling. A failure aborts the serve before any work is done.
2. **Authorise.** If the request rides on a grant, the steward first verifies
the presentation (§2.1), then decides the grant as a JCP enforcement point,
and as the grant's holder, in JCP's two-phase metered path over its chain
(JCP §4.2, §7, §4.9). The frame it decides over names the action `infer` on
the request's content id, the request's `prompt_hash` as payload, and, as
`model`, the content address of the weights the chosen backend runs, when the
backend names one. Every check of that decision applies, on every link of
the chain: signature, attenuation, expiry, revocation, and every
cost-independent caveat. The steward MUST also refuse the serve when what the
grant has left (the least any link has left, its budget less what earlier
serves on it were debited) is below the request's joule ceiling. The
decision is recorded in the steward's journal (§2.2) before the engine runs.
A failure at any step aborts the serve before any work is done.
3. **Meter.** The steward opens an energy window, calls the engine, and closes
the window. Energy MUST be metered **around** the engine; an engine MUST NOT
self-report energy. With a grant, the steward then debits the metered joules
@ -74,6 +77,69 @@ One serve proceeds in four phases, in order:
4. **Seal.** The steward builds the receipt (§6), stamps it into its hash-chained
log, and seals it as a JCR-1 envelope.
### §2.1 Presenting a grant
A grant is a signed public object, so holding one MUST NOT be enough to spend
it. A caller presents a grant as:
```
Presentation {
chain: [signed-grant…] ; root first, leaf last (JCP §4.2); a chain of one for a direct grant
caller: did:key
nonce: bytes(16)
signature: bytes(64) ; Ed25519 by `caller` over canonical_cbor({request_id, grant_id, caller, nonce})
}
```
where `request_id` is the request's content id and `grant_id` the leaf's. The
steward MUST refuse a presentation whose `caller` is not the leaf's issuer, or
whose signature does not verify for the request being served. The leaf's issuer
is the party that authorised the steward to spend on its behalf, so it is the
only party that may.
The leaf's **subject is the steward**: the steward decides the chain as its
holder, under the identity of the key it seals receipts with, and MUST refuse a
grant held by another steward. One grant therefore spends at one steward. A
budget meant for several is sharded by its holder into sibling children, one
held by each steward, whose budgets sum to no more than the parent's (JCP §4.5);
each steward enforces only its own, and the total stays bounded without the
stewards coordinating.
A presentation spends once at a steward. The steward MUST refuse one it has
already spent, identified by the leaf and the nonce, so a captured presentation
cannot be replayed there. A caller that wants the same request served again
signs a new presentation with a new nonce, and that serve is debited too.
### §2.2 The decision journal
A grant's spend at a steward is state, and a steward that restarts with its
spend in memory would give every grant its budget back (JCP §4.11). A steward
that can restart MUST therefore keep a durable journal of its grant decisions:
- An `Authorised` entry, holding the presentation, the request, the model, the
backend and the decision time, MUST be written **before** the engine runs. A
serve whose authorisation cannot be recorded is refused.
- A `Debited` entry, holding the metered energy, is written after the debit.
On restart the steward MUST re-establish the spend before serving by replaying
the journal through the gate: each authorisation is verified and decided again,
at its recorded time, and each debit applied to the serve it belongs to. A
replay the gate refuses, a debit with no authorisation, or a presentation spent
twice means the journal is not one the steward produced, and the steward MUST
NOT serve from it. Replayed presentations count as spent.
An authorisation with no debit is a serve that was in flight when the steward
stopped, and whether the engine finished is not knowable from the journal. The
steward MUST NOT retry it or report it as completed or failed. It stays debited,
at its request's joule ceiling, which is what its authorisation admitted, and is
surfaced as unresolved, naming the caller and the backend that held it, for an
operator to settle.
The reference journal is an append-only file of one JSON entry per line, each
synced before the append returns. A final line with no newline is an append cut
short and is dropped: an authorisation never recorded never started its serve,
and a debit never recorded leaves its serve unresolved.
---
## §3 The request and the energy record
@ -372,18 +438,22 @@ any envelope breaks verification of that receipt and the chain link from the nex
A conforming implementation MUST:
1. Decide a serve's grant as a JCP enforcement point before any work (§2): reject
one that fails verification, has expired or been revoked, does not permit
1. Decide a serve's grant as a JCP enforcement point and as its holder before any
work (§2): reject a presentation not signed for the request by the grant's
issuer, or already spent here (§2.1); reject a chain that fails verification,
is held by another steward, has expired or been revoked, does not permit
`infer` on the request, carries a cost-independent caveat that does not hold,
or has less left than the request's joule ceiling; debit the metered joules
against it after the work, recording a breach on the receipt.
2. Choose the cheapest admissible backend and record every candidate weighed
against every link after the work, recording a breach on the receipt.
2. Where the steward can restart, journal each decision before its serve runs
and re-establish spend from the journal before serving again (§2.2).
3. Choose the cheapest admissible backend and record every candidate weighed
(§4).
3. Meter energy around the engine and tag provenance honestly, enforcing the
4. Meter energy around the engine and tag provenance honestly, enforcing the
`Energy` coupling before sealing (§3.3).
4. Seal the receipt as a JCR-1 `mesh/1` envelope and compute
5. Seal the receipt as a JCR-1 `mesh/1` envelope and compute
`uj_per_accepted_token` (§6).
5. Maintain a tamper-evident hash chain (§6.3).
6. Maintain a tamper-evident hash chain (§6.3).
The published vectors in [`../conformance/v1/`](../conformance/v1/) pin a
deterministic reference serve. `reference/signed_core.hex` MUST canonical-CBOR
@ -400,18 +470,21 @@ cargo run -p mesh-conformance --bin gen-vectors -- ../conformance/v1
Mesh composes proven primitives: Ed25519 signatures, BLAKE3 content addressing,
RFC 9052 COSE_Sign1, and the JCP capability model. It introduces no new
cryptography. Two properties are correctness, not style:
cryptography. These properties are correctness, not style:
- **Authorisation happens outside the model.** The grant check (§2) is enforced
by the steward before any inference runs; a prompt cannot grant itself budget
or capability.
- **The caller is not authenticated.** Mesh honours a grant as a bearer token
of its subject: whoever presents it is decided as the subject. A grant that
must not be usable by anyone who obtains it needs a binding Mesh can check
without an identity, such as `PayloadIs` or a short `NotAfter`.
- **A grant's spend is per steward.** The debit ledger is the steward's own, in
memory. A grant presented to several stewards, or to one that restarts, is
spent separately at each (JCP §4.5, §4.11).
- **A grant spends only for its issuer, once per presentation, at one steward.**
A presentation is signed by the grant's issuer for one request (§2.1), so a
party that obtains a grant, or a captured presentation, cannot spend it; and
the steward is the grant's holder, so a grant cannot be spent at a steward it
was not issued to. Spreading a budget over stewards is sharding.
- **Spend survives the process.** The journal is written before the work and
replayed through the gate on restart (§2.2), so a restart gives no budget back
and a crash mid-serve leaves the serve debited and unresolved rather than
free. A journal is only as durable as the storage under it, and a steward
with an in-memory journal has no such guarantee.
- **Honesty is enforced at seal time.** A receipt cannot claim a measurement it
does not have (§3.3). An operator without a hardware meter produces honest
`Estimator` receipts.

View file

@ -577,6 +577,7 @@ dependencies = [
"mesh-receipt",
"mesh-router",
"mesh-schema",
"serde",
"serde_json",
"thiserror 2.0.20",
]
@ -621,7 +622,6 @@ version = "0.1.0"
dependencies = [
"ed25519-dalek",
"hex",
"jcp-schema",
"jouleclaw-backend-apple",
"mesh-core",
"mesh-engine",

View file

@ -18,10 +18,11 @@ jcp-grant = { workspace = true }
jcp-runtime = { workspace = true }
ed25519-dalek = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
[dev-dependencies]
hex = { workspace = true }
serde_json = { workspace = true }
# The GPU load for `ProcessMeter`'s liveness probe in tests/process_meter.rs.
[target.'cfg(target_os = "macos")'.dev-dependencies]

View file

@ -17,7 +17,7 @@
use ed25519_dalek::SigningKey;
use jcp_schema::{Capability, Did, Grant};
use mesh_core::{Mesh, INFER_ACTION};
use mesh_core::{Mesh, Presentation, INFER_ACTION};
use mesh_engine::ReferenceEngine;
use mesh_meter::Meter;
use mesh_router::BackendSpec;
@ -85,12 +85,15 @@ fn main() {
nonce: [0u8; 16],
};
let signed = jcp_grant::issue(&caller_sk, grant).expect("issue grant");
// The caller presents it, signing for this request: the steward spends a
// grant only for its issuer, and each presentation only once.
let presented = Presentation::sign(vec![signed], &req, &caller_sk, [1u8; 16]);
// --- serve ---
// The prompt token payload travels with the serve call (here a small fixed
// stand-in) but is never sealed into the receipt — only `prompt_hash` is.
let prompt_tokens: Vec<u32> = vec![1, 2, 3, 4, 5, 6, 7];
let resp = mesh.serve(&req, &prompt_tokens, Some(&signed)).expect("serve");
let resp = mesh.serve(&req, &prompt_tokens, Some(&presented)).expect("serve");
println!("\nroute: {}", resp.route.justification);
for c in &resp.route.candidates {

View file

@ -0,0 +1,125 @@
//! The steward's decision journal (SPEC §2.2): where a grant's spend at this
//! steward survives the process (JCP §4.11).
use std::fs::{File, OpenOptions};
use std::io::{self, Read, Write};
use std::path::Path;
use jcp_schema::{ContentId, Did};
use mesh_schema::{Energy, ServeRequest};
use serde::{Deserialize, Serialize};
use crate::Presentation;
/// One record in a steward's decision journal.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum JournalEntry {
/// A grant was authorised for a serve. Written before the engine runs, so
/// a serve the steward was interrupted in is still on record.
Authorised {
presentation: Presentation,
request: ServeRequest,
/// The model the chosen backend named, which the grant was decided over.
model: Option<ContentId>,
/// The backend the serve was dispatched to.
backend: String,
/// When the grant was decided, unix seconds.
now: u64,
},
/// The serve's metered joules were debited against the grant.
Debited { grant_id: ContentId, nonce: [u8; 16], energy: Energy },
}
/// A serve the journal shows authorised and never debited: the steward stopped
/// while it was in flight, and whether the engine finished is not knowable from
/// the record (JCP §4.11).
///
/// It stays debited, at the request's joule ceiling, which is what its
/// authorisation admitted. It is not retried and not reported as done or as
/// failed; the backend that held it is named so an operator can settle it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Unresolved {
pub grant_id: ContentId,
pub request_id: ContentId,
pub caller: Did,
pub backend: String,
pub debited_uj: u64,
}
/// Where a steward records its grant decisions.
pub trait Journal {
/// Record one entry durably. An error refuses the serve it belongs to: a
/// decision that could not be recorded is not acted on.
fn append(&mut self, entry: &JournalEntry) -> io::Result<()>;
}
/// A journal in memory, and the default. It is enough for a steward that never
/// restarts; one that can restart MUST use a durable journal such as
/// [`FileJournal`] (SPEC §2.2).
#[derive(Clone, Debug, Default)]
pub struct MemoryJournal {
entries: Vec<JournalEntry>,
}
impl MemoryJournal {
/// The entries recorded so far, in order.
pub fn entries(&self) -> &[JournalEntry] {
&self.entries
}
}
impl Journal for MemoryJournal {
fn append(&mut self, entry: &JournalEntry) -> io::Result<()> {
self.entries.push(entry.clone());
Ok(())
}
}
/// A journal in an append-only file: one JSON entry per line, each synced to
/// disk before [`append`](Journal::append) returns.
pub struct FileJournal {
file: File,
}
impl FileJournal {
/// Open the journal at `path`, creating it if absent. Returns it with the
/// entries it already holds, for
/// [`Mesh::with_journal`](crate::Mesh::with_journal) to replay.
///
/// A final line with no newline is an append the steward was interrupted
/// in. It is dropped and cut from the file, which is safe either way: an
/// authorisation that was never recorded never started a serve, and a debit
/// that was never recorded leaves its serve authorised and undebited, which
/// resolves as [`Unresolved`]. Any other line that does not parse is an
/// error, because a journal that cannot be read cannot re-establish spend.
pub fn open(path: impl AsRef<Path>) -> io::Result<(Self, Vec<JournalEntry>)> {
let mut file = OpenOptions::new().read(true).append(true).create(true).open(path)?;
let mut bytes = Vec::new();
file.read_to_end(&mut bytes)?;
let complete = bytes.iter().rposition(|&b| b == b'\n').map_or(0, |i| i + 1);
if complete < bytes.len() {
file.set_len(complete as u64)?;
file.sync_data()?;
}
let mut entries = Vec::new();
for (n, line) in bytes[..complete].split(|&b| b == b'\n').enumerate() {
if line.is_empty() {
continue;
}
let entry = serde_json::from_slice(line).map_err(|e| {
io::Error::new(io::ErrorKind::InvalidData, format!("journal line {}: {e}", n + 1))
})?;
entries.push(entry);
}
Ok((FileJournal { file }, entries))
}
}
impl Journal for FileJournal {
fn append(&mut self, entry: &JournalEntry) -> io::Result<()> {
let mut line = serde_json::to_vec(entry)?;
line.push(b'\n');
self.file.write_all(&line)?;
self.file.sync_data()
}
}

View file

@ -3,13 +3,14 @@
//! [`Mesh`] ties the four moving parts together for one metered inference:
//!
//! 1. **Route** — pick the cheapest-sufficient backend (`mesh-router`). With a
//! JCP [`SignedGrant`], a backend whose model the grant does not permit is
//! inadmissible.
//! 2. **Authorise** — decide the grant in JCP's two-phase metered path
//! (`jcp-runtime`), over a frame naming `infer` on this exact request and
//! the chosen backend's model: signature, expiry, revocation, every
//! cost-independent caveat, and whether what the grant has left covers the
//! request ceiling. All of it before any work runs.
//! grant, a backend whose model the grant does not permit is inadmissible.
//! 2. **Authorise** — check the caller signed the [`Presentation`] and is the
//! grant's issuer, then decide the grant's chain in JCP's two-phase metered
//! path (`jcp-runtime`) as its holder, this steward, over a frame naming
//! `infer` on this exact request and the chosen backend's model: every
//! link's signature, attenuation, expiry and cost-independent caveats, and
//! whether what the grant has left covers the request ceiling. The decision
//! is written to the [`Journal`] before any work runs.
//! 3. **Meter** — open an energy window around the engine's `serve` call
//! (`mesh-meter`), so the energy figure is measured, not self-reported, and
//! debit the grant with it, where a per-call cost caveat binds.
@ -29,11 +30,17 @@
))]
compile_error!("a hw-meter-* feature must enable mesh-core's `hw-meter` feature");
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use ed25519_dalek::SigningKey;
use jcp_runtime::Runtime;
use jcp_schema::{Caveat, ContentId, DenyReason, Frame, FrameKind, SignedGrant, Trust, Verdict};
use jcp_schema::{Caveat, ContentId, DenyReason, Did, Frame, FrameKind, SignedGrant, Trust, Verdict};
mod journal;
mod presentation;
pub use journal::{FileJournal, Journal, JournalEntry, MemoryJournal, Unresolved};
pub use presentation::Presentation;
use mesh_engine::ServingEngine;
use mesh_meter::{Meter, Session};
use mesh_receipt::{MeshReceipt, ReceiptError, ReceiptLog};
@ -62,9 +69,28 @@ pub enum MeshError {
BudgetTooLow { budget_uj: u64, ceiling_uj: u64 },
/// The JCP decision refused the grant for a reason the variants above do
/// not name: it expired, was revoked, carries a caveat that does not hold,
/// or carries a flow ceiling that Mesh computes no provenance for.
/// a chain that is broken or amplifies, or a flow ceiling that Mesh
/// computes no provenance for.
#[error("grant denied: {}", .0.label())]
GrantDenied(DenyReason),
/// The grant is held by another steward: its subject is not this one's key.
#[error("the grant is held by another steward, not this one")]
NotThisSteward,
/// The presentation was not signed by the grant's issuer, or not for this
/// request.
#[error("caller not authenticated: {0}")]
CallerNotAuthenticated(String),
/// This presentation was already spent at this steward.
#[error("this presentation was already spent at this steward")]
Replayed,
/// The decision could not be written to the journal, so it was not acted
/// on; or, after the work, its debit could not be.
#[error("journal: {0}")]
Journal(String),
/// A journal being replayed holds an entry the gate would not have
/// allowed, or a debit for a serve it never authorised.
#[error("journal entry {entry} is inconsistent: {reason}")]
JournalInconsistent { entry: usize, reason: String },
/// The engine reported running other weights than the ones it named
/// before serving, which are the ones the grant was decided over.
#[error("backend `{backend}` named model {named} before serving but reported {reported}")]
@ -116,24 +142,103 @@ pub struct Mesh<'m> {
/// caveat and the grant's cumulative spend therefore bind at Mesh as they
/// do at any other JCP enforcement point.
grants: Runtime,
/// The steward's identity as a grant holder: the subject a grant must name
/// to be spent here.
steward_did: Did,
/// Where each grant decision is recorded before its serve runs.
journal: Box<dyn Journal>,
/// Presentations already spent here, by leaf and nonce.
spent_presentations: HashSet<(ContentId, [u8; 16])>,
/// Serves a replayed journal showed in flight when the steward stopped.
unresolved: Vec<Unresolved>,
/// Unix seconds, for expiry and time caveats.
clock: fn() -> u64,
}
impl<'m> Mesh<'m> {
/// Build a mesh that meters with `meter` and seals receipts under `steward`
/// (the COSE `kid`) using `steward_sk`.
/// (the COSE `kid`) using `steward_sk`. The steward holds grants under that
/// key's identity, and records its grant decisions in memory until
/// [`with_journal`](Self::with_journal) gives it a durable journal.
pub fn new(meter: &'m dyn Meter, steward_sk: SigningKey, steward: impl Into<String>) -> Self {
let steward_did = Did::from_verifying_key(&steward_sk.verifying_key());
Self {
router: Router::new(),
engines: HashMap::new(),
meter,
log: ReceiptLog::new(steward_sk, steward),
grants: Runtime::new(),
steward_did,
journal: Box::new(MemoryJournal::default()),
spent_presentations: HashSet::new(),
unresolved: Vec::new(),
clock: system_clock,
}
}
/// Record grant decisions in `journal`, first re-establishing the spend
/// from `recorded`, the entries it already holds, by replaying each
/// decision through the gate (SPEC §2.2, JCP §4.11). Call it on a new mesh,
/// before serving.
///
/// A replayed authorisation must still verify and be allowed, and a debit
/// must belong to a serve the journal authorised; otherwise the journal is
/// inconsistent and the mesh is not built. A serve authorised and never
/// debited was in flight when the steward stopped: it is debited at its
/// request's ceiling and listed in [`unresolved`](Self::unresolved).
pub fn with_journal(mut self, journal: impl Journal + 'static, recorded: &[JournalEntry]) -> Result<Self, MeshError> {
let inconsistent = |entry: usize, reason: String| MeshError::JournalInconsistent { entry, reason };
let mut open: Vec<(&Presentation, &ServeRequest, &str)> = Vec::new();
for (i, entry) in recorded.iter().enumerate() {
match entry {
JournalEntry::Authorised { presentation, request, model, backend, now } => {
presentation.verify(request).map_err(|r| inconsistent(i, r))?;
if self.spent_presentations.contains(&presentation.spend_key()) {
return Err(inconsistent(i, "the presentation was spent twice".into()));
}
self.authorize(request, presentation, *model, *now)
.map_err(|e| inconsistent(i, e.to_string()))?;
self.spent_presentations.insert(presentation.spend_key());
open.push((presentation, request, backend));
}
JournalEntry::Debited { grant_id, nonce, energy } => {
let at = open
.iter()
.position(|(p, ..)| p.spend_key() == (*grant_id, *nonce))
.ok_or_else(|| inconsistent(i, "a debit for a serve the journal never authorised".into()))?;
let (p, ..) = open.remove(at);
self.grants.charge_chained(&p.chain, jcp_energy(energy));
}
}
}
for (p, request, backend) in open {
let ceiling = request.joule_ceiling_uj;
self.grants.charge_chained(&p.chain, jcp_schema::Energy::estimator(ceiling));
self.unresolved.push(Unresolved {
grant_id: p.spend_key().0,
request_id: request.id(),
caller: p.caller,
backend: backend.to_string(),
debited_uj: ceiling,
});
}
self.journal = Box::new(journal);
Ok(self)
}
/// Serves a replayed journal showed in flight when the steward stopped
/// (JCP §4.11): each stays debited at its request's ceiling, and none is
/// retried.
pub fn unresolved(&self) -> &[Unresolved] {
&self.unresolved
}
/// The steward's identity as a grant holder: the subject a grant must name
/// to be spent here.
pub fn steward_did(&self) -> Did {
self.steward_did
}
/// Decide grants against `clock` (unix seconds) instead of the system
/// clock.
pub fn with_clock(mut self, clock: fn() -> u64) -> Self {
@ -168,35 +273,29 @@ impl<'m> Mesh<'m> {
self.log.head()
}
/// Authorise a request against an optional JCP grant, over the frame that
/// names the model the chosen backend runs. Returns the grant id to record
/// on the receipt (or `None` for an ungranted request). Debits nothing.
/// Decide a presented grant for `req`, as its holder, over the frame that
/// names the model the chosen backend runs. Returns the leaf's id, to
/// record on the receipt. Debits nothing. The presentation has already
/// been verified.
fn authorize(
&mut self,
req: &ServeRequest,
grant: Option<&SignedGrant>,
p: &Presentation,
model: Option<ContentId>,
) -> Result<Option<ContentId>, MeshError> {
let Some(sg) = grant else {
return Ok(None);
};
// Mesh does not authenticate who presents a grant (SPEC §8): it honours
// one as a bearer token of its subject, so the subject is the caller.
let d = self.grants.authorize(
&infer_frame(sg, req, model),
sg,
sg.grant.subject,
(self.clock)(),
);
now: u64,
) -> Result<ContentId, MeshError> {
let leaf = p.leaf().ok_or_else(|| MeshError::GrantInvalid("empty chain".into()))?;
let d = self.grants.authorize_chained(&infer_frame(leaf, req, model), &p.chain, self.steward_did, now);
match d.verdict {
Verdict::Allow => {}
Verdict::Deny(DenyReason::BadSignature) => {
let why = jcp_grant::verify(sg).err().map(|e| e.to_string());
let why = jcp_grant::verify_chain(&p.chain).err().map(|e| e.to_string());
return Err(MeshError::GrantInvalid(why.unwrap_or_else(|| "bad-signature".into())));
}
Verdict::Deny(DenyReason::WrongResource | DenyReason::ActionNotGranted) => {
return Err(MeshError::Unauthorized);
}
Verdict::Deny(DenyReason::WrongSubject) => return Err(MeshError::NotThisSteward),
Verdict::Deny(DenyReason::BudgetExceeded) => {
return Err(MeshError::BudgetTooLow { budget_uj: 0, ceiling_uj: req.joule_ceiling_uj });
}
@ -208,7 +307,7 @@ impl<'m> Mesh<'m> {
ceiling_uj: req.joule_ceiling_uj,
});
}
Ok(Some(sg.grant.id()))
Ok(leaf.grant.id())
}
/// Serve one metered inference. See the module docs for the four phases.
@ -221,19 +320,29 @@ impl<'m> Mesh<'m> {
&mut self,
req: &ServeRequest,
prompt: &[u32],
grant: Option<&SignedGrant>,
grant: Option<&Presentation>,
) -> Result<ServeResponse, MeshError> {
// 0. The caller must be the grant's issuer and have signed this
// presentation for this request, and the presentation must not have
// been spent here before.
if let Some(p) = grant {
p.verify(req).map_err(MeshError::CallerNotAuthenticated)?;
if self.spent_presentations.contains(&p.spend_key()) {
return Err(MeshError::Replayed);
}
}
// 1. Route. With a grant, a backend whose model the grant does not
// permit is inadmissible, so the route is the cheapest backend the
// grant allows rather than one the decision would then refuse. This
// only steers: the decision in step 2 is what authorises.
let route = match grant {
None => self.router.route(req),
Some(sg) => {
Some(p) => {
let engines = &self.engines;
self.router.route_where(req, |b| {
let model = engines.get(&b.0).and_then(|e| e.weights_digest());
if model_permitted(sg, &infer_frame(sg, req, model)) {
if model_permitted(p, req, model) {
Ok(())
} else {
Err(match model {
@ -250,8 +359,27 @@ impl<'m> Mesh<'m> {
.ok_or_else(|| MeshError::NoRoute(route.justification.clone()))?;
let model = self.engines.get(&backend.0).and_then(|e| e.weights_digest());
// 2. Authorise, before any work runs.
let grant_id = self.authorize(req, grant, model)?;
// 2. Authorise, and record the decision, before any work runs: a
// decision recorded after the work would lose its debit if the
// steward stopped in between (JCP §4.11).
let grant_id = match grant {
None => None,
Some(p) => {
let now = (self.clock)();
let id = self.authorize(req, p, model, now)?;
self.journal
.append(&JournalEntry::Authorised {
presentation: p.clone(),
request: req.clone(),
model,
backend: backend.0.clone(),
now,
})
.map_err(|e| MeshError::Journal(e.to_string()))?;
self.spent_presentations.insert(p.spend_key());
Some(id)
}
};
let chosen_fidelity = route
.chosen_fidelity
.expect("a chosen route carries its fidelity");
@ -274,11 +402,18 @@ impl<'m> Mesh<'m> {
// real, so they are debited whatever comes next, including a
// refusal to seal below; a breach is recorded on the receipt.
let grant_breach = match grant {
Some(sg) => match self.grants.charge(&sg.grant, jcp_energy(&energy)).verdict {
Verdict::Allow => None,
Verdict::Deny(r) => Some(r.label().to_string()),
},
None => None,
Some(p) => {
let d = self.grants.charge_chained(&p.chain, jcp_energy(&energy));
let (grant_id, nonce) = p.spend_key();
self.journal
.append(&JournalEntry::Debited { grant_id, nonce, energy })
.map_err(|e| MeshError::Journal(e.to_string()))?;
match d.verdict {
Verdict::Allow => None,
Verdict::Deny(r) => Some(r.label().to_string()),
}
}
};
// 3b. The engine must have run the weights it named before serving:
@ -382,15 +517,17 @@ fn infer_frame(sg: &SignedGrant, req: &ServeRequest, model: Option<ContentId>) -
}
}
/// Whether the grant's model caveats admit `frame`'s model. `ModelIn` is the
/// only caveat that depends on which backend serves, so it is the only one
/// that steers the route.
fn model_permitted(sg: &SignedGrant, frame: &Frame) -> bool {
sg.grant
.caveats
/// Whether every link's model caveats admit `model`. `ModelIn` is the only
/// caveat that depends on which backend serves, so it is the only one that
/// steers the route.
fn model_permitted(p: &Presentation, req: &ServeRequest, model: Option<ContentId>) -> bool {
let Some(leaf) = p.leaf() else { return false };
let frame = infer_frame(leaf, req, model);
p.chain
.iter()
.flat_map(|sg| &sg.grant.caveats)
.filter(|c| matches!(c, Caveat::ModelIn(_)))
.all(|c| c.holds(frame, sg.grant.subject, 0, 0))
.all(|c| c.holds(&frame, leaf.grant.subject, 0, 0))
}
/// The metered span as JCP's energy type, for the debit. Only the debited
@ -460,13 +597,26 @@ mod tests {
m
}
/// A grant over exactly `req`, held by a fixed subject.
/// The key that issues test grants and presents them: the caller.
fn caller_key() -> SigningKey {
SigningKey::from_bytes(&[42u8; 32])
}
fn key_did(k: &SigningKey) -> Did {
Did::from_verifying_key(&k.verifying_key())
}
/// A root grant over exactly `req`, issued by the caller and held by the
/// steward of [`steward_key`].
fn grant_over(req: &ServeRequest, budget_uj: u64, expiry: u64, caveats: Vec<Caveat>) -> SignedGrant {
let issuer = SigningKey::from_bytes(&[42u8; 32]);
let holder = SigningKey::from_bytes(&[43u8; 32]);
grant_to(key_did(&steward_key().0), req, budget_uj, expiry, caveats)
}
fn grant_to(holder: Did, req: &ServeRequest, budget_uj: u64, expiry: u64, caveats: Vec<Caveat>) -> SignedGrant {
let issuer = caller_key();
let g = jcp_schema::Grant {
issuer: jcp_schema::Did::from_verifying_key(&issuer.verifying_key()),
subject: jcp_schema::Did::from_verifying_key(&holder.verifying_key()),
issuer: key_did(&issuer),
subject: holder,
capability: jcp_schema::Capability::new(req.id(), [INFER_ACTION]),
budget_uj,
budget_funds: None,
@ -479,6 +629,20 @@ mod tests {
jcp_grant::issue(&issuer, g).expect("issue grant")
}
/// `sg` presented by the caller for `req`, under a nonce no other
/// presentation in the test process uses.
fn present(sg: &SignedGrant, req: &ServeRequest) -> Presentation {
present_chain(vec![sg.clone()], req)
}
fn present_chain(chain: Vec<SignedGrant>, req: &ServeRequest) -> Presentation {
static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
let n = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let mut nonce = [0u8; 16];
nonce[..8].copy_from_slice(&n.to_be_bytes());
Presentation::sign(chain, req, &caller_key(), nonce)
}
fn at_1000() -> u64 {
1_000
}
@ -502,7 +666,7 @@ mod tests {
),
];
for (sg, reason) in &refused {
match mesh.serve(&req, &[], Some(sg)) {
match mesh.serve(&req, &[], Some(&present(sg, &req))) {
Err(MeshError::GrantDenied(r)) => assert_eq!(r, *reason),
other => panic!("expected {reason:?}, got {other:?}"),
}
@ -511,7 +675,7 @@ mod tests {
assert_eq!(mesh.head(), None, "nothing ran, so nothing was sealed");
let fine = grant_over(&req, 20_000_000, 1_001, vec![Caveat::NotAfter(1_000), Caveat::PayloadIs(req.prompt_hash)]);
let resp = mesh.serve(&req, &[], Some(&fine)).expect("serve");
let resp = mesh.serve(&req, &[], Some(&present(&fine, &req))).expect("serve");
assert_eq!(resp.receipt.grant_id, Some(fine.grant.id()));
assert_eq!(mesh.spent(&fine.grant.id()), 1_000);
}
@ -525,12 +689,12 @@ mod tests {
let mut forged = grant_over(&req, 20_000_000, 0, vec![]);
forged.grant.budget_uj = 1_000_000_000;
assert!(matches!(mesh.serve(&req, &[], Some(&forged)), Err(MeshError::GrantInvalid(_))));
assert!(matches!(mesh.serve(&req, &[], Some(&present(&forged, &req))), Err(MeshError::GrantInvalid(_))));
let elsewhere = grant_over(&request(9, Fidelity::F1, 10_000_000), 20_000_000, 0, vec![]);
assert!(matches!(mesh.serve(&req, &[], Some(&elsewhere)), Err(MeshError::Unauthorized)));
assert!(matches!(mesh.serve(&req, &[], Some(&present(&elsewhere, &req))), Err(MeshError::Unauthorized)));
let starved = grant_over(&req, 1, 0, vec![]);
assert!(matches!(
mesh.serve(&req, &[], Some(&starved)),
mesh.serve(&req, &[], Some(&present(&starved, &req))),
Err(MeshError::BudgetTooLow { budget_uj: 1, ceiling_uj: 10_000_000 })
));
assert_eq!(mesh.head(), None);
@ -547,12 +711,12 @@ mod tests {
let sg = grant_over(&req, 10_000_000, 0, vec![]);
for n in 1..=3 {
mesh.serve(&req, &[], Some(&sg)).expect("within what the grant has left");
mesh.serve(&req, &[], Some(&present(&sg, &req))).expect("within what the grant has left");
assert_eq!(mesh.spent(&sg.grant.id()), 3_000_000 * n);
}
// 1 J left, below the 4 J ceiling: refused before any work.
assert!(matches!(
mesh.serve(&req, &[], Some(&sg)),
mesh.serve(&req, &[], Some(&present(&sg, &req))),
Err(MeshError::BudgetTooLow { budget_uj: 1_000_000, ceiling_uj: 4_000_000 })
));
assert_eq!(mesh.spent(&sg.grant.id()), 9_000_000);
@ -584,10 +748,10 @@ mod tests {
let req = request(8, Fidelity::F1, 100_000_000);
let open = grant_over(&req, 1_000_000_000, 0, vec![]);
assert_eq!(mesh.serve(&req, &[], Some(&open)).unwrap().receipt.backend, BackendId::new("anon"));
assert_eq!(mesh.serve(&req, &[], Some(&present(&open, &req))).unwrap().receipt.backend, BackendId::new("anon"));
let only_b = grant_over(&req, 1_000_000_000, 0, vec![Caveat::ModelIn(vec![b])]);
let resp = mesh.serve(&req, &[], Some(&only_b)).expect("serve");
let resp = mesh.serve(&req, &[], Some(&present(&only_b, &req))).expect("serve");
assert_eq!(resp.receipt.backend, BackendId::new("frontier"));
assert_eq!(resp.receipt.weights_digest, Some(b));
let reason = |id: &str| {
@ -600,7 +764,7 @@ mod tests {
let head = mesh.head();
let absent = grant_over(&req, 1_000_000_000, 0, vec![Caveat::ModelIn(vec![ContentId::of_bytes(b"absent")])]);
assert!(matches!(mesh.serve(&req, &[], Some(&absent)), Err(MeshError::NoRoute(_))));
assert!(matches!(mesh.serve(&req, &[], Some(&present(&absent, &req))), Err(MeshError::NoRoute(_))));
assert_eq!(mesh.head(), head);
assert_eq!(mesh.spent(&absent.grant.id()), 0);
}
@ -649,7 +813,7 @@ mod tests {
);
let req = request(8, Fidelity::F1, 10_000_000);
let sg = grant_over(&req, 20_000_000, 0, vec![Caveat::ModelIn(vec![named])]);
assert!(matches!(mesh.serve(&req, &[], Some(&sg)), Err(MeshError::ModelMismatch { .. })));
assert!(matches!(mesh.serve(&req, &[], Some(&present(&sg, &req))), Err(MeshError::ModelMismatch { .. })));
assert_eq!(mesh.head(), None);
assert_eq!(mesh.spent(&sg.grant.id()), 2_000);
}
@ -663,18 +827,215 @@ mod tests {
let req = request(8, Fidelity::F1, 10_000_000);
let capped = grant_over(&req, 20_000_000, 0, vec![Caveat::MaxCostUj(1_000_000)]);
let resp = mesh.serve(&req, &[], Some(&capped)).expect("the work ran, so the serve seals");
let resp = mesh.serve(&req, &[], Some(&present(&capped, &req))).expect("the work ran, so the serve seals");
assert_eq!(resp.receipt.grant_breach.as_deref(), Some("caveat-unsatisfied"));
assert_eq!(mesh.spent(&capped.grant.id()), 3_000_000);
assert_eq!(resp.receipt.to_jcr1_wire(mesh.steward())["grant_breach"], "caveat-unsatisfied");
assert!(resp.receipt.verify_envelope(&resp.envelope, mesh.steward(), &mesh.verifying_key()).is_ok());
let roomy = grant_over(&req, 20_000_000, 0, vec![Caveat::MaxCostUj(5_000_000)]);
let ok = mesh.serve(&req, &[], Some(&roomy)).expect("serve");
let ok = mesh.serve(&req, &[], Some(&present(&roomy, &req))).expect("serve");
assert_eq!(ok.receipt.grant_breach, None);
assert!(ok.receipt.to_jcr1_wire(mesh.steward()).get("grant_breach").is_none());
}
/// A grant spends only at the steward it names as holder, so the same
/// grant presented to another steward is refused there.
#[test]
fn a_grant_held_by_another_steward_is_refused() {
let meter = SteppingMeter::new("test-rapl", Provenance::HwShunt, 1_000);
let mut mesh = mesh_with(&meter);
let req = request(8, Fidelity::F1, 10_000_000);
let elsewhere = grant_to(key_did(&SigningKey::from_bytes(&[12u8; 32])), &req, 20_000_000, 0, vec![]);
assert!(matches!(mesh.serve(&req, &[], Some(&present(&elsewhere, &req))), Err(MeshError::NotThisSteward)));
assert_eq!(mesh.head(), None);
}
/// Only the grant's issuer can spend it, and only on the request it signed
/// for. A grant is a public object, so holding one is not enough.
#[test]
fn the_caller_must_be_the_grants_issuer() {
let meter = SteppingMeter::new("test-rapl", Provenance::HwShunt, 1_000);
let mut mesh = mesh_with(&meter);
let req = request(8, Fidelity::F1, 10_000_000);
let sg = grant_over(&req, 20_000_000, 0, vec![]);
let thief = SigningKey::from_bytes(&[66u8; 32]);
let own = Presentation::sign(vec![sg.clone()], &req, &thief, [1; 16]);
assert!(matches!(mesh.serve(&req, &[], Some(&own)), Err(MeshError::CallerNotAuthenticated(_))));
let mut claimed = Presentation::sign(vec![sg.clone()], &req, &thief, [2; 16]);
claimed.caller = key_did(&caller_key());
assert!(matches!(mesh.serve(&req, &[], Some(&claimed)), Err(MeshError::CallerNotAuthenticated(_))));
let misdirected = present(&sg, &request(9, Fidelity::F1, 10_000_000));
assert!(matches!(mesh.serve(&req, &[], Some(&misdirected)), Err(MeshError::CallerNotAuthenticated(_))));
assert_eq!(mesh.spent(&sg.grant.id()), 0);
mesh.serve(&req, &[], Some(&present(&sg, &req))).expect("the issuer's own presentation");
}
/// A presentation spends once at a steward: a captured one replayed there
/// is refused, and debits nothing.
#[test]
fn a_presentation_is_spent_once() {
let meter = SteppingMeter::new("test-rapl", Provenance::HwShunt, 1_000);
let mut mesh = mesh_with(&meter);
let req = request(8, Fidelity::F1, 10_000_000);
let sg = grant_over(&req, 50_000_000, 0, vec![]);
let p = present(&sg, &req);
mesh.serve(&req, &[], Some(&p)).expect("serve");
assert!(matches!(mesh.serve(&req, &[], Some(&p)), Err(MeshError::Replayed)));
assert_eq!(mesh.spent(&sg.grant.id()), 1_000);
mesh.serve(&req, &[], Some(&present(&sg, &req))).expect("a new presentation is a new spend");
assert_eq!(mesh.spent(&sg.grant.id()), 2_000);
}
/// One budget across two stewards: the holder shards it (JCP §4.5), each
/// shard held by one steward, and each spends only at its holder. Shards
/// cannot sum past their parent, which bounds the total without the
/// stewards coordinating.
#[test]
fn shards_spread_one_budget_across_stewards() {
let meter = SteppingMeter::new("test-rapl", Provenance::HwShunt, 1_000);
let (sk_a, _) = steward_key();
let sk_b = SigningKey::from_bytes(&[12u8; 32]);
let mut a = mesh_with(&meter);
let mut b = Mesh::new(&meter, sk_b.clone(), "did:example:mesh-steward-b");
b.register(
BackendSpec::new("tiny", "ssm-1b", Fidelity::F1, 50_000),
Box::new(ReferenceEngine::new("tiny", "ssm-1b", Fidelity::F1, 50_000, 64)),
);
let req = request(8, Fidelity::F1, 1_000_000);
let root = grant_to(key_did(&caller_key()), &req, 10_000_000, 0, vec![]);
let slices = |b_uj| [(key_did(&sk_a), 6_000_000), (key_did(&sk_b), b_uj)];
assert!(jcp_grant::shard(&root, &caller_key(), &slices(5_000_000), 0, [8; 16]).is_err());
let shards = jcp_grant::shard(&root, &caller_key(), &slices(4_000_000), 0, [8; 16]).unwrap();
let for_a = vec![root.clone(), shards[0].clone()];
let for_b = vec![root.clone(), shards[1].clone()];
a.serve(&req, &[], Some(&present_chain(for_a.clone(), &req))).expect("A spends its shard");
b.serve(&req, &[], Some(&present_chain(for_b.clone(), &req))).expect("B spends its shard");
assert!(matches!(b.serve(&req, &[], Some(&present_chain(for_a, &req))), Err(MeshError::NotThisSteward)));
assert!(matches!(a.serve(&req, &[], Some(&present_chain(for_b, &req))), Err(MeshError::NotThisSteward)));
}
/// A journal whose entries the test reads after the mesh is gone.
#[derive(Clone, Default)]
struct SharedJournal(std::rc::Rc<std::cell::RefCell<Vec<JournalEntry>>>);
impl Journal for SharedJournal {
fn append(&mut self, entry: &JournalEntry) -> std::io::Result<()> {
self.0.borrow_mut().push(entry.clone());
Ok(())
}
}
/// A restart is not a fresh budget. The steward replays its journal
/// through the gate before serving, so the grant has what it had left, and
/// a presentation spent before the restart is still spent.
#[test]
fn a_restart_resumes_the_spend_from_the_journal() {
let meter = SteppingMeter::new("test-rapl", Provenance::HwShunt, 3_000_000);
let req = request(8, Fidelity::F1, 4_000_000);
let sg = grant_over(&req, 10_000_000, 0, vec![]);
let journal = SharedJournal::default();
let first = present(&sg, &req);
{
let mut mesh = mesh_with(&meter).with_journal(journal.clone(), &[]).unwrap();
mesh.serve(&req, &[], Some(&first)).unwrap();
mesh.serve(&req, &[], Some(&present(&sg, &req))).unwrap();
}
let recorded = journal.0.borrow().clone();
assert_eq!(recorded.len(), 4, "two authorisations and two debits");
let mut mesh = mesh_with(&meter).with_journal(journal.clone(), &recorded).unwrap();
assert_eq!(mesh.spent(&sg.grant.id()), 6_000_000, "the restart gave no budget back");
assert!(mesh.unresolved().is_empty());
assert!(matches!(mesh.serve(&req, &[], Some(&first)), Err(MeshError::Replayed)));
mesh.serve(&req, &[], Some(&present(&sg, &req))).expect("4 J left covers the 4 J ceiling");
assert!(matches!(
mesh.serve(&req, &[], Some(&present(&sg, &req))),
Err(MeshError::BudgetTooLow { budget_uj: 1_000_000, .. })
));
}
/// A serve authorised and never debited was in flight when the steward
/// stopped. After the restart it stays debited at its ceiling, is not
/// retried, and is listed with the backend that held it.
#[test]
fn an_in_flight_serve_is_debited_and_unresolved_after_a_restart() {
let meter = SteppingMeter::new("test-rapl", Provenance::HwShunt, 1_000);
let req = request(8, Fidelity::F1, 4_000_000);
let sg = grant_over(&req, 10_000_000, 0, vec![]);
let journal = SharedJournal::default();
let mut before = mesh_with(&meter).with_journal(journal.clone(), &[]).unwrap();
before.serve(&req, &[], Some(&present(&sg, &req))).unwrap();
drop(before);
let recorded = vec![journal.0.borrow()[0].clone()];
let mesh = mesh_with(&meter).with_journal(MemoryJournal::default(), &recorded).unwrap();
assert_eq!(mesh.spent(&sg.grant.id()), 4_000_000);
let [u] = mesh.unresolved() else { panic!("one unresolved serve, got {:?}", mesh.unresolved()) };
assert_eq!((u.grant_id, u.request_id, u.debited_uj), (sg.grant.id(), req.id(), 4_000_000));
assert_eq!((u.backend.as_str(), u.caller), ("tiny", key_did(&caller_key())));
}
/// A journal is replayed through the gate, not trusted. An entry the gate
/// would not have allowed, a debit with no authorisation, or a presentation
/// spent twice each refuse to build the mesh.
#[test]
fn a_journal_the_gate_would_not_have_produced_is_refused() {
let meter = SteppingMeter::new("test-rapl", Provenance::HwShunt, 1_000);
let req = request(8, Fidelity::F1, 4_000_000);
let sg = grant_over(&req, 10_000_000, 0, vec![]);
let journal = SharedJournal::default();
let mut before = mesh_with(&meter).with_journal(journal.clone(), &[]).unwrap();
before.serve(&req, &[], Some(&present(&sg, &req))).unwrap();
drop(before);
let recorded = journal.0.borrow().clone();
let resume = |entries: &[JournalEntry]| mesh_with(&meter).with_journal(MemoryJournal::default(), entries).err();
let mut forged = recorded.clone();
if let JournalEntry::Authorised { presentation, .. } = &mut forged[0] {
presentation.nonce[15] ^= 1;
}
assert!(matches!(resume(&forged), Some(MeshError::JournalInconsistent { entry: 0, .. })));
assert!(matches!(resume(&recorded[1..]), Some(MeshError::JournalInconsistent { entry: 0, .. })));
let twice = [recorded[0].clone(), recorded[1].clone(), recorded[0].clone()];
assert!(matches!(resume(&twice), Some(MeshError::JournalInconsistent { entry: 2, .. })));
assert!(resume(&recorded).is_none());
}
/// The file journal round-trips, and an append a crash cut short is
/// dropped and cut from the file rather than failing the restart.
#[test]
fn the_file_journal_survives_a_torn_final_line() {
use std::io::Write;
let dir = std::env::temp_dir().join(format!("mesh-journal-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("journal.jsonl");
let _ = std::fs::remove_file(&path);
let meter = SteppingMeter::new("test-rapl", Provenance::HwShunt, 1_000);
let req = request(8, Fidelity::F1, 4_000_000);
let sg = grant_over(&req, 10_000_000, 0, vec![]);
{
let (journal, recorded) = FileJournal::open(&path).unwrap();
assert!(recorded.is_empty());
let mut mesh = mesh_with(&meter).with_journal(journal, &recorded).unwrap();
mesh.serve(&req, &[], Some(&present(&sg, &req))).unwrap();
}
let mut file = std::fs::OpenOptions::new().append(true).open(&path).unwrap();
file.write_all(br#"{"Authorised":{"presentation":{"cha"#).unwrap();
drop(file);
let (journal, recorded) = FileJournal::open(&path).unwrap();
assert_eq!(recorded.len(), 2);
assert!(std::fs::read(&path).unwrap().ends_with(b"\n"), "the torn line was cut");
let mesh = mesh_with(&meter).with_journal(journal, &recorded).unwrap();
assert_eq!(mesh.spent(&sg.grant.id()), 1_000);
assert!(mesh.unresolved().is_empty());
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn measured_end_to_end_seals_and_verifies() {
// A HwShunt stepping meter yields a deterministic 3_200_000 µJ span,

View file

@ -0,0 +1,86 @@
//! A grant as a caller presents it to a steward (SPEC §2.1).
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier};
use jcp_schema::{canonical_cbor, ContentId, Did, SignedGrant};
use mesh_schema::ServeRequest;
use serde::{Deserialize, Serialize};
/// A grant as a caller presents it to a steward (SPEC §2.1): the delegation
/// chain whose leaf the steward holds, and the caller's signature over the
/// request it is being spent on.
///
/// The caller is the leaf's issuer, the party that authorised the steward to
/// spend on its behalf. A chain is a public object, so holding one is not
/// enough to spend it; the signature is what a party without the issuer's key
/// cannot produce. The nonce makes each presentation spendable once at a
/// steward, so a captured one cannot be replayed there.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Presentation {
/// The delegation chain, root first, leaf last (JCP §4.2). A grant issued
/// directly to the steward is a chain of one.
pub chain: Vec<SignedGrant>,
/// Who presents it. A steward accepts it only from the leaf's issuer.
pub caller: Did,
/// Distinguishes this presentation from every other the caller makes.
pub nonce: [u8; 16],
/// Ed25519 by `caller` over the canonical bytes of the request id, the
/// leaf's id, the caller and the nonce.
pub signature: Vec<u8>,
}
/// What a presentation's signature covers.
#[derive(Serialize)]
struct Presented<'a> {
request_id: ContentId,
grant_id: ContentId,
caller: &'a Did,
nonce: [u8; 16],
}
impl Presentation {
/// Present `chain` for `req`, signed with `caller_sk`. A steward accepts it
/// only if that key is the leaf's issuer.
pub fn sign(chain: Vec<SignedGrant>, req: &ServeRequest, caller_sk: &SigningKey, nonce: [u8; 16]) -> Self {
let caller = Did::from_verifying_key(&caller_sk.verifying_key());
let mut p = Presentation { chain, caller, nonce, signature: Vec::new() };
p.signature = caller_sk.sign(&p.signed_bytes(req)).to_bytes().to_vec();
p
}
/// The grant this presentation spends: the chain's leaf.
pub fn leaf(&self) -> Option<&SignedGrant> {
self.chain.last()
}
/// Check that the caller is the leaf's issuer and signed this presentation
/// for `req`. The error says which failed.
pub fn verify(&self, req: &ServeRequest) -> Result<(), String> {
let leaf = self.leaf().ok_or("the presentation carries no grant")?;
if self.caller != leaf.grant.issuer {
return Err(format!(
"presented by {}, but the grant was issued by {}",
self.caller.to_did_key(),
leaf.grant.issuer.to_did_key()
));
}
let key = self.caller.verifying_key().map_err(|e| e.to_string())?;
let signature = Signature::from_slice(&self.signature).map_err(|e| e.to_string())?;
key.verify(&self.signed_bytes(req), &signature)
.map_err(|_| "the caller's signature does not verify for this request".to_string())
}
/// What a steward spends this presentation under, once: the leaf and the
/// nonce. The caller is the leaf's issuer, so it adds nothing.
pub(crate) fn spend_key(&self) -> (ContentId, [u8; 16]) {
(self.leaf_id(), self.nonce)
}
fn leaf_id(&self) -> ContentId {
self.leaf().map_or(ContentId([0; 32]), |l| l.grant.id())
}
fn signed_bytes(&self, req: &ServeRequest) -> Vec<u8> {
canonical_cbor(&Presented { request_id: req.id(), grant_id: self.leaf_id(), caller: &self.caller, nonce: self.nonce })
.expect("a presentation's fields are canonical")
}
}

View file

@ -14,7 +14,7 @@ use std::time::Instant;
use ed25519_dalek::SigningKey;
use jcp_schema::{Capability, Did, Grant};
use jouleclaw_loader_gguf::bpe_tokenizer::BpeTokenizer;
use mesh_core::{Mesh, MeshError, INFER_ACTION};
use mesh_core::{Mesh, MeshError, Presentation, INFER_ACTION};
use mesh_engine_gguf::FalconEngine;
#[cfg_attr(feature = "metal", allow(unused_imports))]
use mesh_meter::{EstimatorMeter, Meter};
@ -91,6 +91,7 @@ fn main() {
let ids0 = tk.encode(prompts[0]);
let req0 = request(prompts[0], ids0.len(), n_tokens);
let starved = grant_for(&req0, &caller_sk, &steward_sk, 1); // 1 µJ budget
let starved = Presentation::sign(vec![starved], &req0, &caller_sk, [0u8; 16]);
match mesh.serve(&req0, &ids0, Some(&starved)) {
Err(MeshError::BudgetTooLow { budget_uj, ceiling_uj }) => {
println!("AUTH: underfunded grant refused — budget {budget_uj} µJ < ceiling {ceiling_uj} µJ (no decode ran)\n");
@ -104,6 +105,7 @@ fn main() {
let ids = tk.encode(prompt_text);
let req = request(prompt_text, ids.len(), n_tokens);
let grant = grant_for(&req, &caller_sk, &steward_sk, req.joule_ceiling_uj);
let grant = Presentation::sign(vec![grant], &req, &caller_sk, [0u8; 16]);
let t = Instant::now();
let resp = mesh.serve(&req, &ids, Some(&grant)).expect("serve");
let dt = t.elapsed();

View file

@ -10,7 +10,6 @@ description = "OpenAI-compatible API ingest for Mesh — translate a /v1/chat/co
[dependencies]
mesh-schema = { workspace = true }
mesh-core = { workspace = true }
jcp-schema = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
hex = { workspace = true }

View file

@ -28,8 +28,7 @@
//! a request omits them this crate makes an explicit ~chars/4 estimate (honestly
//! a stand-in until the engine reports the real count).
use jcp_schema::SignedGrant;
use mesh_core::{Mesh, MeshError, ServeResponse};
use mesh_core::{Mesh, MeshError, Presentation, ServeResponse};
use mesh_schema::{ContentId, Fidelity, ServeRequest};
use serde_json::{json, Value};
@ -193,7 +192,7 @@ pub trait ChatBackend {
///
/// 1. [`to_serve_request`] parses the request (with the real token count from
/// `backend.encode`).
/// 2. `mesh.serve` authorises the optional JCP `grant`, routes cheapest-sufficient,
/// 2. `mesh.serve` authorises the optional presented JCP `grant`, routes cheapest-sufficient,
/// meters the decode, and seals a `mesh/1` receipt.
/// 3. `backend.decode` turns the served tokens into the assistant message.
/// 4. The response's `usage.energy` carries the wedge metric, the route, **and the
@ -202,7 +201,7 @@ pub trait ChatBackend {
pub fn complete_chat<B: ChatBackend>(
mesh: &mut Mesh,
backend: &B,
grant: Option<&SignedGrant>,
grant: Option<&Presentation>,
req: &Value,
default_ceiling_uj: u64,
created: u64,
@ -408,7 +407,7 @@ fn sealed_responses_response(
pub fn complete_responses<B: ChatBackend>(
mesh: &mut Mesh,
backend: &B,
grant: Option<&SignedGrant>,
grant: Option<&Presentation>,
req: &Value,
default_ceiling_uj: u64,
created: u64,