All checks were successful
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 6s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 44s
standards workspaces build + test / joule-ui-rs (joule-ui/joule-ui-rs, joule-ui-rs, linux) (pull_request) Successful in 1m22s
standards workspaces build + test / jcp-rs (jcp-conformance, jcp/jcp-rs, -p jcp-energy --features nvml, jcp-rs, linux) (pull_request) Successful in 1m41s
standards workspaces build + test / mesh-rs (mesh/mesh-rs, -p mesh-engine-gguf -p mesh-openai --features mesh-engine-gguf/gguf,mesh-openai/server, mesh-rs, linux-arm64) (pull_request) Successful in 1m23s
standards workspaces build + test / joulehook-rs (joulehook/joulehook-rs, joulehook-rs, linux) (pull_request) Successful in 1m11s
standards workspaces build + test / jouleclaw-rs (-nodebug, jouleclaw/jouleclaw-rs, -p jouleclaw-energy --features rapl,nvml, jouleclaw-rs, linux-arm64, -C target-feature=+fp16) (pull_request) Successful in 3m7s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 3m54s
standards workspaces build + test / sandbox-rs (sandbox-conformance, sandbox/sandbox-rs, sandbox-rs, linux) (pull_request) Successful in 2m16s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 7m37s
standards workspaces build + test / jouledesk-rs (jouledesk/jouledesk-rs, jouledesk-rs, macos) (pull_request) Successful in 2m42s
A mesh receipt bound what was asked (request_id) and what it cost, and
nothing about what came back, so no one could later show which answer a
receipt was for. AR-1 had the same gap beside input_hash.
mesh/1 (SPEC §6.4):
- output_commitment: a salted BLAKE3 commitment over the request id, the
digest of the prompt payload the engine was handed, and the output tokens.
It is a commitment, not a hash, because a short output is reversible from
its hash. The opening is derived per request from a key drawn from the
steward's signing key, so a restarted steward can still disclose it.
The field is absent for a typed decision, whose answer the engine
interface does not carry, and omitted when absent, so older receipts keep
their bytes.
- The payload is bound because prompt_hash addresses the prompt's bytes
while an engine is handed token ids, so request_id alone cannot confirm
what a replay runs on.
- ServingEngine::deterministic (default false; the reference engine true).
- mesh_core::replay re-executes a sealed serve. Its verdicts:
- Verified: the re-run opens the commitment.
- Diverged { first_difference }: only when a disclosed output opens the
commitment on the same payload and opening, so the mismatch is the
serve's and not the replayer's input.
- NotReplayable(why): the engine samples; another backend, recipe or
weights; another request; no commitment; or a mismatch nothing
attributes.
- The conformance pack is regenerated (the reference receipt gains the
field; the envelope grows from 663 to 747 bytes).
AR-1: output_hash, an optional field after input_hash, sealed only when
bound (§2.3). It recommends a commitment over a plain hash and names
mesh's output_commitment as one. The published AR-1 vectors regenerate
byte-identical.
Also: ar-verify's `len() % 2 == 0` becomes `is_multiple_of(2)`, a clippy
error that predates this change, so the crate is clippy-clean.
Tests:
- mesh: the commitment binds request, opening, payload and tokens; a
generation commits and a decision does not; deterministic replay
verifies and locates a drift; replay refuses what it cannot settle,
including a wrong payload with the honest output disclosed; a restarted
steward reopens.
- AR-1: output_hash sealed when bound and absent otherwise.
- Mutation testing: 20 mutants, all killed (13, then 7 more after
binding the payload; one of the 13 targets code since replaced).
Verified on the build host:
- mesh-rs: 94 tests plus 19 behind features; clippy and rustdoc clean.
- jcp-rs: 565 tests and the conformance binary; jcp-action-receipt
clippy-clean.
- Repo checks pass.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
245 lines
8.8 KiB
Rust
245 lines
8.8 KiB
Rust
//! Emit the AR-1 conformance vector pack.
|
|
//!
|
|
//! ```text
|
|
//! cargo run -p jcp-action-receipt --example emit_vectors -- jcp/conformance/v1
|
|
//! ```
|
|
//!
|
|
//! Writes `action_receipt.json` and `action_receipt_meta.json`, following the
|
|
//! shape the existing JCP packs use: a `meta` file declaring the algorithm,
|
|
//! profile, public key and steward, and a vectors file carrying signed
|
|
//! envelopes as hex.
|
|
//!
|
|
//! The signing key is a **published fixture**, deliberately: a conformance
|
|
//! vector nobody else can verify is not a conformance vector. It is the
|
|
//! all-`0x2a` key and must never be used for anything real.
|
|
//!
|
|
//! Both positive and negative vectors ship. The negatives are the point — they
|
|
//! pin the three structural rules, so an implementation that accepts a
|
|
//! well-signed but ill-formed receipt fails the pack rather than passing it.
|
|
|
|
use ed25519_dalek::SigningKey;
|
|
use jcp_action_receipt::{
|
|
ActionReceipt, Effect, EffectClass, Egress, EgressClass, verify,
|
|
};
|
|
use jcp_receipt::jcr1;
|
|
use jcp_schema::ContentId;
|
|
use serde_json::{Value, json};
|
|
|
|
/// Published fixture key. Not a secret; never use it for anything real.
|
|
const FIXTURE_SEED: [u8; 32] = [0x2a; 32];
|
|
|
|
const STEWARD: &str = "did:example:ar-reference";
|
|
|
|
fn cid(tag: &str) -> ContentId {
|
|
ContentId::of_bytes(tag.as_bytes())
|
|
}
|
|
|
|
fn base() -> ActionReceipt {
|
|
ActionReceipt {
|
|
closed_at: "2026-08-25T12:00:00Z".into(),
|
|
grant_id: cid("grant/root"),
|
|
chain_ids: vec![cid("grant/root"), cid("grant/delegated")],
|
|
authority_receipt: cid("jcp-receipt/allow"),
|
|
verdict: "allow".into(),
|
|
attestation_id: cid("attest/eat"),
|
|
proof_cell_id: Some(cid("proof/cell")),
|
|
tier: "S5".into(),
|
|
grounding: "hardware".into(),
|
|
work_receipt: cid("jcr1/resolution"),
|
|
input_hash: "a".repeat(64),
|
|
output_hash: None,
|
|
joules_uj: 19_538_280,
|
|
energy_provenance: "HwShunt".into(),
|
|
effect: Effect {
|
|
class: EffectClass::Reversible,
|
|
target: cid("target/document"),
|
|
before: Some("b".repeat(64)),
|
|
after: Some("c".repeat(64)),
|
|
compensator: None,
|
|
plan_receipt: None,
|
|
},
|
|
egress: Egress {
|
|
class: EgressClass::None,
|
|
bytes: 0,
|
|
peer: None,
|
|
},
|
|
steward: STEWARD.into(),
|
|
}
|
|
}
|
|
|
|
/// A signed vector plus the properties a verifier must reproduce.
|
|
fn positive(name: &str, note: &str, r: &ActionReceipt, key: &SigningKey) -> Value {
|
|
let env = r.seal(key).expect("valid receipt must seal");
|
|
// Prove the vector before publishing it — a pack that ships an
|
|
// unverifiable vector is worse than no pack.
|
|
let v = verify(&env, &key.verifying_key(), STEWARD).expect("emitted vector must verify");
|
|
assert_eq!(v.id, r.id().unwrap());
|
|
json!({
|
|
"name": name,
|
|
"note": note,
|
|
"expect": "verify",
|
|
"id_hex": r.id().unwrap().to_hex(),
|
|
"core_hex": hex::encode(r.signed_core().unwrap()),
|
|
"envelope_hex": hex::encode(&env),
|
|
"wire": r.to_wire(),
|
|
})
|
|
}
|
|
|
|
/// A negative vector: correctly signed, structurally invalid. Built by signing
|
|
/// a hand-edited projection so the signature is genuine — an implementation
|
|
/// that only checks signatures will accept it, which is exactly what the pack
|
|
/// is designed to catch.
|
|
fn negative(name: &str, note: &str, expect: &str, wire: Value, key: &SigningKey) -> Value {
|
|
let core = jcr1::signed_core_bytes(&wire).expect("core");
|
|
let env = jcr1::cose_sign1_envelope(&core, key, STEWARD);
|
|
// The signature really is valid...
|
|
jcr1::verify_cose_sign1(&env, STEWARD, key.verifying_key().as_bytes())
|
|
.expect("negative vector must carry a genuine signature");
|
|
// ...and AR-1 must still refuse it.
|
|
assert!(
|
|
verify(&env, &key.verifying_key(), STEWARD).is_err(),
|
|
"negative vector `{name}` was accepted — the rule it pins is not enforced"
|
|
);
|
|
json!({
|
|
"name": name,
|
|
"note": note,
|
|
"expect": expect,
|
|
"envelope_hex": hex::encode(&env),
|
|
"wire": wire,
|
|
})
|
|
}
|
|
|
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
let out_dir = std::env::args().nth(1).unwrap_or_else(|| ".".into());
|
|
let key = SigningKey::from_bytes(&FIXTURE_SEED);
|
|
|
|
let mut vectors = Vec::new();
|
|
|
|
// ── positives: one per effect class, one per egress class ──────────
|
|
vectors.push(positive(
|
|
"reversible-local",
|
|
"Read-modify-write resolved on-device. Nothing left the machine, so egress is `none` with zero bytes.",
|
|
&base(),
|
|
&key,
|
|
));
|
|
|
|
let mut r = base();
|
|
r.effect.class = EffectClass::None;
|
|
r.effect.before = None;
|
|
r.effect.after = None;
|
|
vectors.push(positive(
|
|
"read-only",
|
|
"A query that changed nothing. Safe to replay; no before/after state to record.",
|
|
&r,
|
|
&key,
|
|
));
|
|
|
|
let mut r = base();
|
|
r.effect.class = EffectClass::Compensable;
|
|
r.effect.compensator = Some(cid("grant/refund"));
|
|
vectors.push(positive(
|
|
"compensable-with-compensator",
|
|
"Cannot be undone, but the grant that could compensate is named at the time of acting — not discovered afterwards.",
|
|
&r,
|
|
&key,
|
|
));
|
|
|
|
let mut r = base();
|
|
r.effect.class = EffectClass::Irreversible;
|
|
r.effect.plan_receipt = Some(cid("plan/receipt"));
|
|
vectors.push(positive(
|
|
"irreversible-with-plan",
|
|
"An irreversible commit carrying the plan receipt that preceded it. This is the only shape in which `irreversible` is well-formed.",
|
|
&r,
|
|
&key,
|
|
));
|
|
|
|
let mut r = base();
|
|
r.egress.class = EgressClass::AttestedEnclave;
|
|
r.egress.bytes = 4_096;
|
|
r.egress.peer = Some(cid("attest/peer-enclave"));
|
|
vectors.push(positive(
|
|
"egress-attested-enclave",
|
|
"Work left the device to somewhere that proved what it was running. The peer attestation is bound in the receipt.",
|
|
&r,
|
|
&key,
|
|
));
|
|
|
|
let mut r = base();
|
|
r.egress.class = EgressClass::ThirdParty;
|
|
r.egress.bytes = 8_192;
|
|
r.egress.peer = None;
|
|
r.tier = "S0".into();
|
|
r.grounding = "software".into();
|
|
vectors.push(positive(
|
|
"egress-third-party-honest",
|
|
"An unattested destination, honestly recorded rather than upgraded. Trust is not an egress class.",
|
|
&r,
|
|
&key,
|
|
));
|
|
|
|
let mut r = base();
|
|
r.energy_provenance = "Estimator".into();
|
|
r.joules_uj = 1_200_000;
|
|
vectors.push(positive(
|
|
"estimated-energy",
|
|
"Energy from an estimator, not a hardware counter. `HwShunt` and `Estimator` are different claims and must never collapse into one number.",
|
|
&r,
|
|
&key,
|
|
));
|
|
|
|
// ── negatives: the three structural rules ──────────────────────────
|
|
let mut w = base().to_wire();
|
|
w["effect"]["class"] = json!("irreversible");
|
|
vectors.push(negative(
|
|
"irreversible-without-plan",
|
|
"Correctly signed, structurally invalid: an irreversible effect with no plan receipt. A verifier that only checks signatures accepts this.",
|
|
"reject:irreversible-without-plan",
|
|
w,
|
|
&key,
|
|
));
|
|
|
|
let mut w = base().to_wire();
|
|
w["egress"]["bytes"] = json!(4_096u64);
|
|
vectors.push(negative(
|
|
"egress-none-with-bytes",
|
|
"Claims nothing left the device while counting 4096 bytes that did. The claim is falsified by its own byte count.",
|
|
"reject:egress-none-with-bytes",
|
|
w,
|
|
&key,
|
|
));
|
|
|
|
let mut w = base().to_wire();
|
|
w["egress"]["class"] = json!("attested_enclave");
|
|
w["egress"]["bytes"] = json!(4_096u64);
|
|
vectors.push(negative(
|
|
"attested-egress-without-peer",
|
|
"Claims an attested destination with no peer attestation to bind. The router may not claim a stronger class than its evidence supports.",
|
|
"reject:attested-egress-without-peer",
|
|
w,
|
|
&key,
|
|
));
|
|
|
|
let meta = json!({
|
|
"alg": "EdDSA (COSE -8)",
|
|
"profile": jcp_action_receipt::AR_PROFILE,
|
|
"pubkey_hex": hex::encode(key.verifying_key().as_bytes()),
|
|
"steward": STEWARD,
|
|
"note": "Fixture key, published deliberately so third parties can verify. Never use it for anything real.",
|
|
"positives": vectors.iter().filter(|v| v["expect"] == "verify").count(),
|
|
"negatives": vectors.iter().filter(|v| v["expect"] != "verify").count(),
|
|
});
|
|
|
|
let vpath = format!("{out_dir}/action_receipt.json");
|
|
let mpath = format!("{out_dir}/action_receipt_meta.json");
|
|
std::fs::write(&vpath, serde_json::to_string_pretty(&json!({ "vectors": vectors }))? + "\n")?;
|
|
std::fs::write(&mpath, serde_json::to_string_pretty(&meta)? + "\n")?;
|
|
|
|
println!("wrote {vpath}");
|
|
println!("wrote {mpath}");
|
|
println!(
|
|
" {} positive, {} negative",
|
|
meta["positives"], meta["negatives"]
|
|
);
|
|
Ok(())
|
|
}
|