open-standards/jcp/jcp-rs/crates/jcp-action-receipt/examples/end_to_end.rs
Transaction Science 62a81b3472
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
mesh/1 + AR-1: receipts commit to the output; deterministic serves replay
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]>
2026-10-21 04:35:32 -04:00

265 lines
10 KiB
Rust

//! End-to-end Action Receipt on real hardware.
//!
//! ```text
//! cargo run -p jcp-action-receipt --example end_to_end -- /tmp/action.hex
//! ```
//!
//! Implements §5 step 6 of the blueprint: **one agent action, under a real
//! delegated grant, with energy read off this host's own counters, sealed into
//! an AR-1 receipt that a third party verifies from the published spec alone.**
//!
//! Only the workload is a stand-in, so the example runs anywhere. Everything
//! around it is the real machinery:
//!
//! - the grant is **really issued** and **really attenuated**, and amplification
//! is really refused;
//! - the authorisation is a **real `decide` call** against a real budget, and
//! the receipt records the verdict that call returned. It is decided before
//! the work runs, so the cost it debits is the action's estimate
//! ([`authorised_cost`], `Estimator`) — the work's reading is step 3's;
//! - the joules are **metered**, and the span's provenance is carried through
//! verbatim: `HwShunt` only when every record the meter returned is a
//! hardware reading. Where the host has no live counter — or the span's CPU
//! counter held — the meter returns its `Estimator` record, and this example
//! says so rather than inventing `HwShunt`;
//! - the tier is stamped from what is **actually running**. This is in-process
//! with no isolation boundary crossed, so it is `S0`. Claiming `S5` without a
//! microVM is exactly the overreach the ladder exists to prevent.
use ed25519_dalek::SigningKey;
use jcp_action_receipt::{ActionReceipt, Effect, EffectClass, Egress, EgressClass, verify};
use jcp_grant::{attenuate, issue};
use jcp_receipt as authority;
use jcp_runtime::Runtime;
use jcp_schema::{
Capability, ContentId, Did, Energy, Frame, FrameKind, Grant, Trust, Verdict,
};
use sandbox_meter::{DeviceEnergy, PowerMeter, Receipt as WorkReceipt};
use sandbox_schema::Tier;
const STEWARD: &str = "did:example:ar-e2e";
fn main() -> Result<(), Box<dyn std::error::Error>> {
let out = std::env::args().nth(1).unwrap_or_else(|| "action.hex".into());
// ── 1. A real grant, really delegated ────────────────────────────
let issuer = SigningKey::from_bytes(&[11u8; 32]);
let agent = SigningKey::from_bytes(&[22u8; 32]);
let subagent = SigningKey::from_bytes(&[33u8; 32]);
let agent_did = Did::from_verifying_key(&agent.verifying_key());
let sub_did = Did::from_verifying_key(&subagent.verifying_key());
let resource = ContentId::of_bytes(b"resource/quarterly-report");
let root = Grant {
issuer: Did::from_verifying_key(&issuer.verifying_key()),
subject: agent_did,
capability: Capability::new(resource, ["read", "summarise"]),
budget_uj: 50_000_000,
budget_funds: None,
caveats: Vec::new(),
expiry: 0,
parent: None,
delegate_depth: None,
nonce: [1u8; 16],
};
let sg = issue(&issuer, root.clone())?;
// Strictly narrower: read only, one fifth of the ceiling.
let delegated = attenuate(
&sg,
&agent,
sub_did,
Capability::new(resource, ["read"]),
10_000_000,
0,
[2u8; 16],
)?;
// And prove the runtime really refuses amplification, rather than asserting it.
let amplified = attenuate(
&sg,
&agent,
sub_did,
Capability::new(resource, ["read", "delete"]),
10_000_000,
0,
[3u8; 16],
);
assert!(amplified.is_err(), "amplification must be refused");
println!("1. grant issued, attenuated to read-only — amplification refused: {}",
amplified.unwrap_err());
// ── 2. A real authorisation decision ─────────────────────────────
let mut rt = Runtime::new();
let frame = Frame {
kind: FrameKind::ToolCall,
trust: Trust::Control,
grant_id: delegated.grant.id(),
action: "read".into(),
resource,
payload_hash: ContentId::of_bytes(b"input/quarterly-report"),
model: None,
};
let decision = rt.decide(&frame, &delegated, sub_did, authorised_cost(), 0);
let verdict = match decision.verdict {
Verdict::Allow => "allow",
Verdict::Deny(_) => "deny",
};
// Seal the REAL authority receipt from the decision that just happened, and
// take its content id from the bytes rather than naming a placeholder.
let steward_key = SigningKey::from_bytes(&[44u8; 32]);
let authority_env =
authority::seal(&frame, &delegated.grant, &decision, &steward_key, STEWARD)?;
let authority_core =
authority::signed_core(&frame, &delegated.grant, &decision, STEWARD)?;
let authority_id = ContentId::of_bytes(&authority_core);
// Prove it verifies on its own terms before binding it.
authority::verify(
&frame,
&delegated.grant,
&decision,
&authority_env,
STEWARD,
steward_key.verifying_key().as_bytes(),
)?;
println!(
"2. decide() → {verdict}, {} µJ remaining; authority receipt sealed + verified, id {}",
decision.budget_remaining_uj,
&authority_id.to_hex()[..12]
);
// ── 3. Real work, really measured ────────────────────────────────
let mut meter = PowerMeter::detect();
let sources = meter.sources().join(",");
let is_hw = meter.is_hardware();
let span = meter.start();
let mut acc: u64 = 0;
for i in 0..40_000_000u64 {
acc = acc.wrapping_add(i ^ (acc >> 7)).rotate_left(3);
}
let measurement = span.stop(&mut meter);
std::hint::black_box(acc);
let joules_uj = measurement.total_uj();
// The span's own provenance, the weakest of its records. A hardware meter
// still returns its estimator record for a span whose CPU counter held,
// so `is_hardware()` says nothing about this span.
let provenance = {
use sandbox_meter::Provenance as P;
let records = &measurement.devices;
if records.is_empty() || records.iter().any(|d| d.provenance == P::Estimator) {
"Estimator"
} else if records.iter().all(|d| d.provenance == P::HwShunt && d.measured_uj.is_some()) {
"HwShunt"
} else {
"ModelBased"
}
};
println!(
"3. work metered — {joules_uj} µJ [{provenance}] over {:.2}s via {sources}",
measurement.wall.as_secs_f64()
);
if provenance != "HwShunt" {
println!(
" note: this span was not a hardware reading; provenance is {provenance}, not HwShunt ({})",
if is_hw { "its CPU counter held" } else { "no live energy counter here" }
);
}
// Build the REAL work receipt from the devices the meter actually reported.
// `Receipt::build` enforces the provenance/measured coupling itself, so a
// mislabelled device is refused here rather than propagating into AR-1.
let devices: Vec<DeviceEnergy> = measurement
.devices
.iter()
.map(|d| DeviceEnergy {
class: d.class,
measured_uj: d.measured_uj,
estimated_uj: d.estimated_uj,
provenance: d.provenance,
})
.collect();
let work = WorkReceipt::build(
sandbox_schema::ContentId::of_bytes(b"unit/quarterly-report-summarise"),
Tier::S0,
devices,
1,
[7u8; 8],
)?;
work.validate()?;
// The two families agree on BLAKE3 and disagree only on presentation:
// sandbox renders `b3:<base64url>`, jcp carries raw 32 bytes. Re-addressing
// the same canonical core with jcp's hasher yields the identical digest, so
// the binding is exact rather than a hash of a hash.
let work_id = ContentId::of_bytes(&work.canonical_core());
debug_assert_eq!(
work.content_id().as_str().trim_start_matches("b3:").len(),
43,
"sandbox content id should be b3:<base64url of 32 bytes>"
);
println!(
" work receipt sealed over {} metered device(s), id {}",
measurement.devices.len(),
&work_id.to_hex()[..12]
);
// ── 4. Seal the Action Receipt ───────────────────────────────────
let receipt = ActionReceipt {
closed_at: "2026-08-25T12:00:00Z".into(),
grant_id: delegated.grant.id(),
chain_ids: vec![sg.grant.id(), delegated.grant.id()],
authority_receipt: authority_id,
verdict: verdict.into(),
attestation_id: ContentId::of_bytes(b"attest/e2e-local"),
proof_cell_id: None,
tier: "S0".into(),
grounding: if is_hw { "hardware" } else { "software" }.into(),
work_receipt: work_id,
input_hash: ContentId::of_bytes(b"input/quarterly-report").to_hex(),
output_hash: None,
joules_uj,
energy_provenance: provenance.into(),
effect: Effect {
// read-and-summarise changed nothing in the world
class: EffectClass::None,
target: resource,
before: None,
after: None,
compensator: None,
plan_receipt: None,
},
egress: Egress {
// resolved entirely in this process
class: EgressClass::None,
bytes: 0,
peer: None,
},
steward: STEWARD.into(),
};
let envelope = receipt.seal(&steward_key)?;
println!("4. sealed — receipt id {}", receipt.id()?.to_hex());
// ── 5. Verify it back, as a third party would ────────────────────
let v = verify(&envelope, &steward_key.verifying_key(), STEWARD)?;
assert_eq!(v.id, receipt.id()?);
println!("5. verified from the envelope alone — {} byte canonical core", v.core.len());
std::fs::write(&out, hex::encode(&envelope))?;
println!();
println!("wrote {out}");
println!(
"verify independently:\n ar-verify {out} --pubkey {} --steward {STEWARD}",
hex::encode(steward_key.verifying_key().as_bytes())
);
Ok(())
}
/// The cost the authorisation debits: decided before the work runs, it is
/// the action's estimate, 0.25 J, graded `Estimator` with no measured figure.
/// Sealed as a reading, the authority receipt would claim a meter read work
/// that had not yet happened; the joules a meter did read are the work
/// receipt's, and the action receipt's.
pub fn authorised_cost() -> Energy {
Energy::estimator(250_000)
}