open-standards/jouleclaw/jouleclaw-rs/crates/jouleclaw-jcr1/examples/jcr1_mint.rs
Transaction Science c07d4c5b47
All checks were successful
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 2m25s
standards workspaces build + test / aac-rs (pull_request) Successful in 42s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 44s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 40s
standards workspaces build + test / map-rs (pull_request) Successful in 36s
standards workspaces build + test / mesh-rs (pull_request) Successful in 28s
standards workspaces build + test / openpay (pull_request) Successful in 1m38s
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 1m27s
jouleclaw public surface must stay documented / Public-surface audit (jouleclaw-rs) (pull_request) Successful in 57s
no-vanity-metrics / Scan committed Markdown for vanity metrics (pull_request) Successful in 3s
standards workspaces build + test / eoc-rs (pull_request) Successful in 1m49s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m17s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m11s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m6s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 2m32s
standards workspaces build + test / arl-rs (pull_request) Successful in 2m18s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 2m47s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 2m49s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 2m45s
standards workspaces build + test / sense-rs (pull_request) Successful in 2m44s
standards workspaces build + test / macOS energy backend (jcp-energy) (pull_request) Successful in 2m21s
jouleclaw-energy: RAPL is the sum of every package zone, each of which must be live and each wrapped at its own range
`RaplCounter` read one file, `/sys/class/powercap/intel-rapl:0/energy_uj`, and
its liveness probe watched that one file, cached per path. On a multi-socket
server that is package 0's energy alone, graded `HwShunt` for the domain
`CpuPkg` while every other socket's energy is missing. And nothing makes zone 0
a package: `intel-rapl:<n>` is only an identifier, and each zone says what it
is in its `name` file, so a host whose zone 0 is the platform zone (`psys`) or
a `dram` zone had that domain read and probed as the package. proof-cascade
does this right since 9334ec0c; eoc-meter now does too (26165a77); this is the
same rule here.

- The package set is listed from the powercap directory: every top-level
  `intel-rapl:<n>` zone whose `name` reads `package-<digits>`. Left out wherever
  they sit, zone 0 included: `psys` (the platform zone, whose energy includes
  the packages', so summing it beside them counts them twice), `dram`, the
  `core`/`uncore`/`dram` sub-zones (`intel-rapl:0:0`), the control type, and
  the MMIO interface (`intel-rapl-mmio:0`, also named `package-0`, which
  republishes a package). The listing reads the directory, not fixed indices.
- Liveness: one channel per package, and the shared rule asks every channel to
  be live, so RAPL is live only when EVERY package keeps advancing while the
  probe burns CPU. A host with no package zone (only `psys`) is `Unavailable`
  with nothing opened or burned.
- The cache is per package set, keyed by the package zones' directories, not
  per file path. `RaplCounter::new()` lists the set and reuses a verdict for
  the same set; the counter sums exactly the packages that were probed.
- `read()` returns a running total: the first read's package counters summed,
  then each package's delta added, each taken modulo its own
  `max_energy_range_uj`. Summing the raw counters would make one package's
  wrap a fall of the sum, and with sockets at different ranges no single range
  is right. Before this, a wrap of the one zone was a fall, which `jcr1_mint`
  and the like take as no reading; now a wrap is read across. The files are
  read under the counter's lock, so two threads' reads are folded in the order
  they were taken, and an older read is never taken for a wrap. A package that
  cannot be read fails the read (`Platform`): no figure that leaves a package
  out. A package whose counter falls while its range is unknown (absent, or 0)
  makes that read `CounterWrap`, and every later one too, because the total
  would be short by energy no one can know.
- The read path is no longer compiled only for Linux: off Linux the counter is
  still never available (`rapl_liveness` is `Unavailable` there, with no
  probe), but the code it would run is the code the tests run on a Mac.

API: `RAPL_ENERGY_PATH` and `RaplCounter::with_path(&'static str)` are gone, as
a single `energy_uj` path cannot say which domain it is or stand for every
package; `RaplCounter::with_root(dir)` takes a powercap directory instead (a
container or chroot that mounts sysfs elsewhere), and `POWERCAP_ROOT` names
the default. `rapl_liveness()` takes no argument: it is this host's package
set. Nothing in the monorepo called the removed items. Docs that said a RAPL
figure is one file are corrected (`liveness.rs`, `detect_meter`, and the
`jcr1_mint` example's header).

Tests, on fake powercap directories under the temp dir (removed when done), so
they run on macOS:
- a two-socket server (the control type, package-0 and package-1 with core and
  dram sub-zones, psys, a top-level dram, intel-rapl-mmio:0) lists packages 0
  and 1 only, and with every zone moving the delta is the two packages' energy
  (250,000 + 400,000 uJ) and nothing else;
- zone 0 named psys beside package zone 1: zone 1 alone is listed, sampled and
  summed;
- per-zone wrap: package 0 wraps at 262,143,328,850 while package 1 advances,
  then package 1 wraps at 65,532,610,987: deltas of 1,700 and 2,500 uJ; taken
  at the other zone's range, package 1's wrap would be off by the difference
  of the two ranges;
- a package with no range, absent or 0, is summed while it rises; its fall is
  `CounterWrap`, and so is the next read;
- an unreadable package fails the read and the opener;
- an only-psys host, an empty directory and a missing one: no package, the
  real probe returns `Unavailable` in under 100 ms (no burn), and
  `RaplCounter::with_root` is unavailable and reads `NoCounter`;
- scripted: two packages advancing are Live, one held is Frozen;
- the cache: once per package set, and a set with one more package is another
  set; the key of a listed tree is its package directories;
- the production probe (real reads, real CPU burn, real clock) on a two-socket
  tree: held packages are Frozen after the whole budget; both packages advanced
  by a writer thread are Live inside it; package 0 and psys advanced while
  package 1 holds is Frozen.

Falsified, each mutant applied to this commit, run, and restored with
`git checkout` (blob hash equal to HEAD's):
- back to `intel-rapl:0` only, whatever its name: 8 of the 14 RAPL tests fail;
- every top-level zone summed (no name check): 5 fail;
- every package wrapped at package 0's range: 1 fails, the per-zone wrap test;
- the probe watching the first package alone: 1 fails, the production probe
  with package 1 held;
- the cache keyed by the first package's path alone: 1 fails.

What ran where, on this M5 Max (macOS 27.0.1): jouleclaw-energy `--features
rapl` 48 lib + 2 integration tests pass, `--features ioreport,rapl,nvml` 111 +
2, default features 34 + 2; mesh-meter 2 by default, 10 with
`--features hw-meter-rapl`, 12 with `--features hw-meter-apple`;
jouledesk-github 12; the `jcr1_mint` example compiles. For
x86_64-unknown-linux-gnu, `cargo check --all-targets --features rapl` and
`cargo clippy --all-targets` with `rapl` and with `rapl,nvml` pass with no
warning in a file this touches (clippy's existing ones are in jouleclaw-core,
the ledger tests and the IOReport FFI), and mesh-meter's clippy with
`hw-meter-rapl` is clean. The sysfs reads under the real powercap directory
and the Linux-only cached probe are type-checked, not run: there is no Linux
host with readable RAPL here.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 22:20:49 -04:00

175 lines
7.7 KiB
Rust

//! `jcr1_mint` — mint a fresh JCR-1 receipt from an energy reading.
//!
//! Reads CPU energy through `jouleclaw-energy::rapl`: the sum of every
//! powercap package zone (`/sys/class/powercap/intel-rapl:<n>` named
//! `package-<n>`), no FFI. On a system
//! without RAPL (Apple, BSD, CI sandboxes, this Mac the example is
//! built on) the producer does NOT fake a `HwShunt` reading; it falls
//! back to a time-based `Estimator` value rounded to the 1 mJ
//! resolution floor, exactly as JCR-1 §2.3 requires.
//!
//! Output: a single-vector JSON document in the same shape as
//! `conformance/vectors.json`, accepted by both the Python and Go
//! reference verifiers. Rust mints; Python and Go verify — three
//! independent implementations agreeing on one standard.
//!
//! Run:
//! cargo run -p jouleclaw-jcr1 --example jcr1_mint -- /tmp/minted.json
use ed25519_dalek::SigningKey;
use jouleclaw_energy::rapl::RaplCounter;
use jouleclaw_energy::{EnergyCounter, Provenance};
use jouleclaw_intent::HeuristicClassifier;
use jouleclaw_jcr1::{
canonical_signed_core, cose_sign1_envelope, hmac_tool_hex, to_jcr1_wire, TEST_KEYS_JSON,
};
use jouleclaw_prov::{input_hash, CascadeTier, ReceiptBuilder, ToolReceipt, ToolTouch};
use std::fs;
use std::time::Instant;
// Fixed-but-illustrative timestamp so the receipt is reproducible by
// description; in production this is the actual `Utc::now().to_rfc3339()`
// at cascade close.
const CLOSED_AT: &str = "2026-05-31T18:04:22Z";
/// Measure energy of one unit of deterministic work (stand-in for an L1
/// tool execution) via the typed `EnergyCounter` trait. If a real
/// hardware counter is available (Linux/x86 RAPL via sysfs) the
/// reading is `HwShunt` at microjoule resolution. Otherwise the
/// fallback estimator returns an `Estimator` value rounded to the
/// JCR-1 §2.3 1 mJ floor.
///
/// Both paths route through `jouleclaw-energy` rather than raw sysfs
/// reads, so the energy provenance is the trait's own
/// `provenance()` declaration — no chance of the producer accidentally
/// upgrading an Estimator span to look like HwShunt.
fn measure_resolution_energy() -> (u64, Provenance) {
let rapl = RaplCounter::new();
let start = if rapl.is_available() { rapl.read().ok() } else { None };
let t = Instant::now();
// A small bounded unit of deterministic work — enough to consume
// measurable time on any platform.
let mut acc = 0u64;
for i in 0..200_000u64 {
acc = acc.wrapping_add(blake3::hash(&i.to_le_bytes()).as_bytes()[0] as u64);
}
std::hint::black_box(acc);
let elapsed = t.elapsed();
match (start, rapl.is_available().then(|| rapl.read().ok()).flatten()) {
(Some(a), Some(b)) if b.uj > a.uj => (b.uj - a.uj, rapl.provenance()),
_ => {
// No hardware counter. Estimate from elapsed time at a
// conservative ~6 W CPU draw, then ROUND to 1 mJ and tag
// `Estimator` (JCR-1 §2.3 resolution floor).
let est_uj = (elapsed.as_secs_f64() * 6.0 * 1_000_000.0) as u64;
let floored = ((est_uj + 500) / 1000).max(1) * 1000;
(floored, Provenance::Estimator)
}
}
}
fn main() {
let out_path = std::env::args()
.nth(1)
.unwrap_or_else(|| "minted-vectors.json".to_string());
// 1. Derive the signing key from the published test seed so any
// verifier with `conformance/test-keys.json` accepts the result.
let keys: serde_json::Value = serde_json::from_str(TEST_KEYS_JSON).expect("parse keys");
let seed_hex = keys["ed25519_seed_hex"].as_str().unwrap();
let seed: [u8; 32] = hex::decode(seed_hex).unwrap().try_into().unwrap();
let sk = SigningKey::from_bytes(&seed);
let pubkey_hex = hex::encode(sk.verifying_key().to_bytes());
let steward = keys["steward_kid"].as_str().unwrap();
let gw_id = "gw-edge-7";
let gw_key = keys["gateway_hmac_keys"][gw_id].as_str().unwrap().as_bytes();
// 2. Measure energy.
let (joules_uj, prov) = measure_resolution_energy();
let prov_tag = match prov {
Provenance::HwShunt => "HwShunt",
Provenance::ModelBased => "ModelBased",
Provenance::Estimator => "Estimator",
};
// 3. Build a receipt — an L1 IaC describe call that closed the query.
let tool_id = "iac:aws.ec2.describe";
let query = "list my ec2 instances in us-east-1 (minted live)";
let input_h = input_hash(query);
let classifier = HeuristicClassifier::new();
let _intent_v = jouleclaw_intent::IntentClassifier::classify(&classifier, query);
let receipt = ReceiptBuilder::new()
.input_hash(&input_h)
.tier(CascadeTier::L1Lawful)
.steward(steward)
.account_tool(ToolTouch {
tool_id: tool_id.into(),
joules_uj,
energy_provenance: prov,
})
.account_tool_receipt(ToolReceipt {
tool_id: tool_id.into(),
joules_uj,
energy_provenance: prov,
minted_at: CLOSED_AT.into(),
gateway_id: gw_id.into(),
hmac_sha256_hex: hmac_tool_hex(gw_key, tool_id, joules_uj, prov_tag, CLOSED_AT, gw_id),
})
// Wave-8 fields — present on the receipt but NOT in the JCR-1
// signed core (proposed for v1.1):
// .intent(_intent_v)
// .model_id("…")
.seal()
.expect("seal");
// 4. Project to JCR-1 wire form. We also compute the canonical core
// via the typed Receipt path to prove it matches the wire-form
// path; the actual envelope is signed over the wire-form core
// after the illustrative timestamp override below.
let wire = to_jcr1_wire(&receipt).expect("project");
let _ = canonical_signed_core(&receipt).expect("encode");
// 5. Override the auto-generated id + closed_at with the illustrative
// fixed values so the wire form matches the suite shape exactly.
// (In production these come from the cascade walker.)
let mut wire = wire;
{
let obj = wire.as_object_mut().unwrap();
obj.insert("id".into(), serde_json::json!("rcpt-mint-0001"));
obj.insert("closed_at".into(), serde_json::json!(CLOSED_AT));
}
// Re-canonicalise with the patched fields so envelope.payload == core.
let core = jouleclaw_jcr1::signed_core_bytes(&wire).expect("encode");
let envelope = cose_sign1_envelope(&core, &sk, steward);
// 6. Emit in the suite format both verifiers consume.
let out = serde_json::json!({
"spec": "JCR-1",
"canonical_cbor": "RFC 8949 §4.2.1 length-first key order",
"envelope": "COSE_Sign1 (RFC 9052), EdDSA/Ed25519",
"signed_core_excludes": ["token_attributions"],
"vector_count": 1,
"vectors": [{
"n": 1,
"description": format!(
"LIVE-MINTED by jouleclaw-jcr1: L1 IaC call closed the query; energy = {prov_tag} ({joules_uj} µJ)."
),
"receipt": wire,
"signed_core_cbor_hex": hex::encode(&core),
"cose_sign1_hex": hex::encode(&envelope),
}]
});
fs::write(&out_path, serde_json::to_string_pretty(&out).unwrap()).expect("write");
println!("jouleclaw-jcr1 producer — minted 1 receipt");
println!(" derived ed25519 public : {pubkey_hex}");
println!(" energy provenance : {prov_tag}");
println!(" joules_uj : {joules_uj}");
println!(" signed core : {} bytes", core.len());
println!(" envelope (COSE_Sign1) : {} bytes", envelope.len());
println!(" output : {out_path}");
println!();
println!("Verify with either independent implementation:");
println!(" python3 conformance/verify.py {out_path} conformance/test-keys.json");
println!(" go run conformance/verify.go {out_path} conformance/test-keys.json");
}