diff --git a/.github/workflows/standards-build.yml b/.github/workflows/standards-build.yml index 9e9f7d9fe..372b33048 100644 --- a/.github/workflows/standards-build.yml +++ b/.github/workflows/standards-build.yml @@ -99,7 +99,12 @@ jobs: - { name: proof-rs, dir: proof/proof-rs, runner: linux, z3: true, conformance: proof-conformance } - { name: joulehook-rs, dir: joulehook/joulehook-rs, runner: linux } - { name: eoc-rs, dir: eoc/eoc-rs, runner: linux, feature_tests: "-p eoc-meter --features cuda" } - - { name: mesh-rs, dir: mesh/mesh-rs, runner: linux-arm64, feature_tests: "-p mesh-engine-gguf --features gguf" } + # mesh-openai's `serve_example` test (the reference server seals what + # its meter read, never a fixture's figure) has required-features + # `server`, so `cargo test --workspace` builds none of it; naming the + # package here is what runs it. Selecting mesh-engine-gguf alone + # compiles mesh-openai with `server` but runs none of its tests. + - { name: mesh-rs, dir: mesh/mesh-rs, runner: linux-arm64, feature_tests: "-p mesh-engine-gguf -p mesh-openai --features mesh-engine-gguf/gguf,mesh-openai/server" } - { name: joule-ui-rs, dir: joule-ui/joule-ui-rs, runner: linux } # jouleclaw-rs is the one entry whose runner IS a correctness choice. # It reaches candle → ug → gemm, which uses inline aarch64 assembly @@ -234,7 +239,8 @@ jobs: # The NVML backends sit behind features no workspace member enables # (jcp-energy `nvml`, eoc-meter `cuda`, jouleclaw-energy `nvml`), so the # step above never compiles them, let alone runs their tests. Here they - # build and their tests run. The container has no GPU and Docker masks + # build and their tests run, as do mesh-rs's feature-gated tests (the + # gguf engines; mesh-openai's `server` and its serve_example test). The container has no GPU and Docker masks # powercap, so the hardware tests take their "unavailable, and the # oracle agrees" branch; the rule's unit tests run in full. - name: Feature-gated energy tests diff --git a/jcp/README.md b/jcp/README.md index 8f70235b3..50fca16c6 100644 --- a/jcp/README.md +++ b/jcp/README.md @@ -51,7 +51,7 @@ jcp/ - **`jcp-threshold`** — threshold actors (SPEC §2.2): a `did:key` whose private key is split k-of-n with FROST (RFC 9591); the group key is an ordinary Ed25519 key, so a 2-of-3 team issues, holds, and signs wire requests with nothing else in JCP changing. Shares come from a trusted dealer or **distributed key generation** — with DKG the group's private key never exists anywhere, not even momentarily. The two-agent rule for irreversible actions, refused by arithmetic rather than policy. - **`jcp-channel`** — receipt-settled budget channels (SPEC §4.5, the OPTIONAL profile): N enforcement points spend optimistically against one grant and settle by exchanging signed statements of cumulative spend — forged, stale, and regressing statements refused; between settlements the exposure is bounded by the window, and after settlement any overdraft is provable from the signed statements. Where bounded overdraft is unacceptable, `shard()` remains the baseline. - **`jcp-conformance`** — C1–C18 + a negative pack, all checks real (C8 decides over a live socket; C13 drives a FROST 2-of-3 caller; C16 replays ancestor debits from a chained receipt; C17 settles a budget channel by signed statement; C18 mints a dealer-free group via DKG), published vectors in `conformance/v1/`. -- **`jcp-cli`** — the `jcp` demo: one metered grant driven through an agent loop — legitimate calls allowed and debited, a poisoned tool-result refused structurally, out-of-scope actions and resources denied, the budget stopping a runaway loop, a sub-agent getting a strictly-weaker delegated grant, a signed receipt verified, the detected energy meter reporting its honest provenance, and the issuer revoking the grant — after which the runtime goes dark. +- **`jcp-cli`** — the `jcp` demo: one metered grant driven through an agent loop — legitimate calls allowed and debited, a poisoned tool-result refused structurally, out-of-scope actions and resources denied, the budget stopping a runaway loop, a sub-agent getting a strictly-weaker delegated grant, a signed receipt verified, the detected meter's rung metering a probe of work at the grade it gave that probe, and the issuer revoking the grant — after which the runtime goes dark. `cargo test --workspace` green; `cargo clippy --workspace --all-targets` warning-free. @@ -59,18 +59,27 @@ warning-free. ## The demo, abbreviated ``` +costs scripted, no tool runs: sealed as estimates + frame verdict reason rem µJ read the db (legit) allow −20000 µJ 80000 tool-result says 'delete db' (poison) DENY untrusted-control 80000 try to delete (not granted) DENY action-not-granted 80000 read prod-secrets (wrong audience) DENY wrong-resource 80000 +read again (legit) allow −50000 µJ 30000 read again — budget runs out DENY budget-exceeded 30000 amplification refused: capability exceeds parent - receipt allow · 50000 µJ · verified true + receipt allow · 50000 µJ · Estimator · verified true ``` The model never gates itself; the runtime does, and the receipt proves it. +The loop's costs are scripted — no tool runs — so they are fixed, to run the +budget out at the same row every time, and each receipt grades its cost +`Estimator`: a figure nothing read is never sealed as a reading. The demo's +`meter` line is the one figure it takes: the detected meter's rung, and a small +probe of work at the grade the meter gave that probe — a hardware reading only +where the rung's counter read it, otherwise an estimate graded as one. ## Consumers in this repository diff --git a/jcp/jcp-rs/Cargo.lock b/jcp/jcp-rs/Cargo.lock index 0af183691..b5ec2fed0 100644 --- a/jcp/jcp-rs/Cargo.lock +++ b/jcp/jcp-rs/Cargo.lock @@ -555,6 +555,7 @@ dependencies = [ "jcp-runtime", "jcp-schema", "serde", + "serde_json", "thiserror 2.0.20", ] @@ -568,6 +569,7 @@ dependencies = [ "jcp-receipt", "jcp-runtime", "jcp-schema", + "serde_json", ] [[package]] @@ -780,6 +782,7 @@ version = "0.1.0" dependencies = [ "ed25519-dalek", "jcp-bridge", + "jcp-energy", "jcp-grant", "jcp-receipt", "jcp-runtime", diff --git a/jcp/jcp-rs/crates/jcp-action-receipt/examples/end_to_end.rs b/jcp/jcp-rs/crates/jcp-action-receipt/examples/end_to_end.rs index e42177598..42a06b98a 100644 --- a/jcp/jcp-rs/crates/jcp-action-receipt/examples/end_to_end.rs +++ b/jcp/jcp-rs/crates/jcp-action-receipt/examples/end_to_end.rs @@ -14,7 +14,9 @@ //! - 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; +//! 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 @@ -96,7 +98,7 @@ fn main() -> Result<(), Box> { resource, payload_hash: ContentId::of_bytes(b"input/quarterly-report"), }; - let decision = rt.decide(&frame, &delegated, sub_did, Energy::measured(250_000), 0); + let decision = rt.decide(&frame, &delegated, sub_did, authorised_cost(), 0); let verdict = match decision.verdict { Verdict::Allow => "allow", Verdict::Deny(_) => "deny", @@ -250,3 +252,12 @@ fn main() -> Result<(), Box> { ); 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) +} diff --git a/jcp/jcp-rs/crates/jcp-action-receipt/tests/end_to_end_example.rs b/jcp/jcp-rs/crates/jcp-action-receipt/tests/end_to_end_example.rs new file mode 100644 index 000000000..497ecaf45 --- /dev/null +++ b/jcp/jcp-rs/crates/jcp-action-receipt/tests/end_to_end_example.rs @@ -0,0 +1,21 @@ +//! The `end_to_end` example authorises its action before the work runs, so +//! the cost that decision debits and the authority receipt seals is an +//! estimate — never a hardware reading of work that had not happened. (The +//! work's own energy is metered, at the meter's grade, in step 3.) Checked on +//! the cost the example uses (the example file is compiled in here as a +//! module). + +#[allow(dead_code)] +#[path = "../examples/end_to_end.rs"] +mod end_to_end; + +use jcp_schema::Provenance; + +#[test] +fn the_authorisation_is_debited_an_estimate() { + let cost = end_to_end::authorised_cost(); + eprintln!("authorised cost: {} µJ [{}] measured {:?}", cost.uj(), cost.provenance.jcr1_label(), cost.measured_uj); + assert_eq!(cost.provenance, Provenance::Estimator, "a pre-run cost graded {:?}", cost.provenance); + assert_eq!(cost.measured_uj, None, "a measured figure for work that had not run"); + assert!(cost.check_provenance().is_ok(), "{cost:?}"); +} diff --git a/jcp/jcp-rs/crates/jcp-cite/Cargo.toml b/jcp/jcp-rs/crates/jcp-cite/Cargo.toml index 16832ec1b..0772ecca5 100644 --- a/jcp/jcp-rs/crates/jcp-cite/Cargo.toml +++ b/jcp/jcp-rs/crates/jcp-cite/Cargo.toml @@ -16,5 +16,8 @@ ed25519-dalek = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } +[dev-dependencies] +serde_json = { workspace = true } + [lints] workspace = true diff --git a/jcp/jcp-rs/crates/jcp-cite/examples/cite.rs b/jcp/jcp-rs/crates/jcp-cite/examples/cite.rs index f6d9ceb88..5db4ede92 100644 --- a/jcp/jcp-rs/crates/jcp-cite/examples/cite.rs +++ b/jcp/jcp-rs/crates/jcp-cite/examples/cite.rs @@ -6,6 +6,11 @@ //! ```text //! cargo run -p jcp-cite --example cite //! ``` +//! +//! The retrieval is illustrative: no document is fetched, so the energy the +//! citation records is a chosen figure, and the decision it is debited under +//! seals it as an estimate ([`retrieval_cost`]) — never as a hardware reading +//! of work no meter saw. use ed25519_dalek::SigningKey; use jcp_cite::{verify, Citation, Source}; @@ -37,10 +42,14 @@ fn main() { Source::new(ContentId::of_bytes(doc_a), g_finance).at("/finance/q3-report"), Source::new(ContentId::of_bytes(doc_b), g_people).at("/people/q4-plan"), ], - 4_200, // µJ the retrieval consumed + 4_200, // µJ the retrieval is put at: illustrative, nothing read it [7; 16], ); - println!("agent read {} sources → answer, {} µJ", citation.sources.len(), citation.energy_uj); + println!( + "agent read {} sources → answer, {} µJ (illustrative: sealed as an estimate)", + citation.sources.len(), + citation.energy_uj + ); // The retrieval is decided and metered under a real grant, and the receipt // commits the citation by content id. @@ -58,7 +67,7 @@ fn main() { }; let sg = issue(&issuer, grant.clone()).unwrap(); let frame = citation.frame(grant.id(), corpus).unwrap(); - let decision = Runtime::new().decide(&frame, &sg, agent_did, Energy::measured(citation.energy_uj), 1); + let decision = Runtime::new().decide(&frame, &sg, agent_did, retrieval_cost(&citation), 1); let steward = SigningKey::from_bytes(&[9u8; 32]); let receipt = seal(&frame, &grant, &decision, &steward, "did:example:steward").unwrap(); let core = extract(&receipt, "did:example:steward", &steward.verifying_key().to_bytes()).unwrap(); @@ -93,3 +102,10 @@ fn main() { } } } + +/// The cost the retrieval is debited and sealed at: the citation's recorded +/// figure, graded `Estimator` with no measured figure, because nothing in +/// this example read it. +pub fn retrieval_cost(citation: &Citation) -> Energy { + Energy::estimator(citation.energy_uj) +} diff --git a/jcp/jcp-rs/crates/jcp-cite/tests/cite_example.rs b/jcp/jcp-rs/crates/jcp-cite/tests/cite_example.rs new file mode 100644 index 000000000..6780b74f7 --- /dev/null +++ b/jcp/jcp-rs/crates/jcp-cite/tests/cite_example.rs @@ -0,0 +1,57 @@ +//! The `cite` example's retrieval is illustrative — no document is fetched — +//! so the cost it is decided and sealed at must be an estimate, never a +//! hardware reading. Checked on the cost the example builds (the example +//! file is compiled in here as a module) and on the `jcp` receipt sealed +//! over it, decoded from the signed core. + +#[allow(dead_code)] +#[path = "../examples/cite.rs"] +mod cite; + +use ed25519_dalek::SigningKey; +use jcp_cite::{Citation, Source}; +use jcp_grant::issue; +use jcp_runtime::Runtime; +use jcp_schema::{Capability, ContentId, Did, Grant, Provenance}; + +#[test] +fn the_cite_examples_retrieval_is_sealed_as_an_estimate() { + let issuer = SigningKey::from_bytes(&[1u8; 32]); + let agent = Did::from_verifying_key(&SigningKey::from_bytes(&[2u8; 32]).verifying_key()); + let corpus = ContentId::of_bytes(b"acme-workspace"); + let citation = Citation::new( + agent, + ContentId::of_bytes(b"answer"), + vec![Source::new(ContentId::of_bytes(b"doc"), ContentId::of_bytes(b"grant:finance"))], + 4_200, + [7; 16], + ); + let cost = cite::retrieval_cost(&citation); + eprintln!("retrieval cost: {} µJ [{}] measured {:?}", cost.uj(), cost.provenance.jcr1_label(), cost.measured_uj); + assert_eq!(cost.provenance, Provenance::Estimator, "an illustrative retrieval graded {:?}", cost.provenance); + assert_eq!(cost.measured_uj, None, "a measured figure nothing read"); + assert_eq!(cost.uj(), citation.energy_uj, "the receipt's cost is the citation's figure"); + + let grant = Grant { + issuer: Did::from_verifying_key(&issuer.verifying_key()), + subject: agent, + capability: Capability::new(corpus, ["retrieve"]), + budget_uj: 1_000_000, + budget_funds: None, + caveats: Vec::new(), + expiry: 0, + parent: None, + delegate_depth: None, + nonce: [0; 16], + }; + let sg = issue(&issuer, grant.clone()).expect("issue"); + let frame = citation.frame(grant.id(), corpus).expect("frame"); + let decision = Runtime::new().decide(&frame, &sg, agent, cost, 1); + assert!(decision.is_allow(), "{:?}", decision.verdict); + let steward = SigningKey::from_bytes(&[9u8; 32]); + let env = jcp_receipt::seal(&frame, &grant, &decision, &steward, "did:example:steward").expect("seal"); + let core = jcp_receipt::jcr1::verify_cose_sign1(&env, "did:example:steward", &steward.verifying_key().to_bytes()).expect("verifies"); + let sealed: serde_json::Value = jcp_schema::from_canonical_cbor(&core).expect("decodes"); + eprintln!("sealed: cost_uj {} [{}]", sealed["cost_uj"], sealed["energy_provenance"]); + assert_eq!(sealed["energy_provenance"].as_str(), Some("Estimator"), "{sealed}"); +} diff --git a/jcp/jcp-rs/crates/jcp-cli/Cargo.toml b/jcp/jcp-rs/crates/jcp-cli/Cargo.toml index a68926430..b273af621 100644 --- a/jcp/jcp-rs/crates/jcp-cli/Cargo.toml +++ b/jcp/jcp-rs/crates/jcp-cli/Cargo.toml @@ -18,5 +18,8 @@ jcp-receipt = { workspace = true } jcp-energy = { workspace = true } ed25519-dalek = { workspace = true } +[dev-dependencies] +serde_json = { workspace = true } + [lints] workspace = true diff --git a/jcp/jcp-rs/crates/jcp-cli/src/main.rs b/jcp/jcp-rs/crates/jcp-cli/src/main.rs index c2e88f69c..238d41ff8 100644 --- a/jcp/jcp-rs/crates/jcp-cli/src/main.rs +++ b/jcp/jcp-rs/crates/jcp-cli/src/main.rs @@ -5,9 +5,17 @@ //! calls are allowed and debited; a poisoned tool-result is refused structurally; //! out-of-scope actions and resources are denied by capability and audience //! binding; the budget stops a runaway loop; and a sub-agent gets a strictly -//! weaker delegated grant. The energy figure comes from the detected meter -//! ladder; the issuer then revokes the grant and the runtime goes dark. Every -//! decision seals as a signed JCR-1 receipt. +//! weaker delegated grant. The issuer then revokes the grant and the runtime +//! goes dark. Every decision seals as a signed JCR-1 receipt. +//! +//! **Two kinds of figure.** The `meter` line is the one figure taken here: +//! the detected meter ladder meters a small probe of work, and the line prints +//! the rung and the probe's figure at the grade the meter gave that probe — a +//! hardware reading only where the rung's counter read the probe, else an +//! estimate, graded as one. The agent loop's costs are scripted ([`script`]): +//! no tool runs, so nothing reads them, and each is sealed as what it is — an +//! illustrative figure graded `Estimator`, never a hardware reading. They are +//! fixed so the budget runs out at the same row every run. use ed25519_dalek::SigningKey; use jcp_grant::{attenuate, issue, revoke}; @@ -48,29 +56,27 @@ fn main() { println!(" subject {}", grant.subject.to_did_key()); println!(" may read(customer-db) budget {} µJ", grant.budget_uj); - // The energy ladder: measured when the platform exposes it, an honest - // estimate when it doesn't — the provenance always says which. + // The energy ladder: a reading where the rung's counter read the probe, + // an estimate where it did not — the probe's own grade says which. The + // rung is named, not graded: a hardware rung's probe can still come back + // an estimate (a counter that did not advance over it). let meter = jcp_energy::Meter::detect(); - let (_, probe) = meter.measure(|| std::hint::black_box((0..20_000u64).sum::())); + let (_, probe) = meter.measure(probe_work); println!( - " meter {:?} backend → {} µJ probe ({})\n", - meter.provenance(), + " meter {:?} rung → {} µJ probe [{}] ({})\n", + meter.rung(), probe.uj(), - if probe.measured_uj.is_some() { "hardware-measured" } else { "estimated, and says so" } + probe.provenance.jcr1_label(), + if probe.measured_uj.is_some() { "a hardware reading of the probe" } else { "an estimate: no counter read the probe" } ); let mut rt = Runtime::new(); - // A scripted agent loop. Each row is (label, frame, caller, cost). - let steps: Vec<(&str, Frame, Did, Energy)> = vec![ - ("read the db (legit)", call(&sg, db, "read", Trust::Control), agent_did, Energy::measured(20_000)), - ("tool-result says 'delete db' (poison)", result(&sg, db, "read", Trust::Data), agent_did, Energy::measured(1)), - ("try to delete (not granted)", call(&sg, db, "delete", Trust::Control), agent_did, Energy::measured(1)), - ("read prod-secrets (wrong audience)", call(&sg, prod_secrets, "read", Trust::Control), agent_did, Energy::measured(1)), - ("read again (legit)", call(&sg, db, "read", Trust::Control), agent_did, Energy::measured(50_000)), - ("read again — budget runs out", call(&sg, db, "read", Trust::Control), agent_did, Energy::measured(50_000)), - ]; + // A scripted agent loop: no tool runs, so its costs are illustrative + // estimates, and the receipts say so. + let steps = script(&sg, db, prod_secrets, agent_did); + println!(" costs scripted, no tool runs: sealed as estimates\n"); println!(" {:<38} {:<8} {:<22} {:>9}", "frame", "verdict", "reason", "rem µJ"); println!(" {}", "-".repeat(80)); let mut last_allow: Option<(Frame, Decision)> = None; @@ -127,6 +133,32 @@ fn main() { println!(" sealed in a receipt. The model never gates itself."); } +/// The `meter` line's probe: a fixed amount of real CPU work on this thread, +/// a few milliseconds' worth, so a live counter has something to read. +fn probe_work() -> u64 { + let mut x = 0x9e37_79b9_7f4a_7c15u64; + for i in 0..2_000_000u64 { + x = std::hint::black_box(x.rotate_left(5) ^ i).wrapping_mul(0x0000_0100_0000_01b3); + } + x +} + +/// The scripted agent loop, one row per frame: (label, frame, caller, cost). +/// No tool runs, so every cost is an illustrative figure graded `Estimator` +/// with no measured figure: sealed as a hardware reading, it would claim a +/// meter read work that never happened (SPEC §5). The figures are fixed so +/// the budget runs out at the last row. +fn script(sg: &SignedGrant, db: ContentId, prod_secrets: ContentId, agent: Did) -> Vec<(&'static str, Frame, Did, Energy)> { + vec![ + ("read the db (legit)", call(sg, db, "read", Trust::Control), agent, Energy::estimator(20_000)), + ("tool-result says 'delete db' (poison)", result(sg, db, "read", Trust::Data), agent, Energy::estimator(1)), + ("try to delete (not granted)", call(sg, db, "delete", Trust::Control), agent, Energy::estimator(1)), + ("read prod-secrets (wrong audience)", call(sg, prod_secrets, "read", Trust::Control), agent, Energy::estimator(1)), + ("read again (legit)", call(sg, db, "read", Trust::Control), agent, Energy::estimator(50_000)), + ("read again — budget runs out", call(sg, db, "read", Trust::Control), agent, Energy::estimator(50_000)), + ] +} + fn call(sg: &SignedGrant, resource: ContentId, action: &str, trust: Trust) -> Frame { Frame { kind: FrameKind::ToolCall, trust, grant_id: sg.grant.id(), action: action.into(), resource, payload_hash: ContentId::of_bytes(b"q") } } @@ -143,3 +175,53 @@ fn truncate(s: &str, n: usize) -> String { t } } + +#[cfg(test)] +mod tests { + use super::*; + use jcp_schema::Provenance; + use serde_json::Value; + + /// Every scripted cost is an estimate, and so is what the receipt of the + /// last allowed row seals — decoded from the signed core itself. + #[test] + fn the_scripted_costs_are_sealed_as_estimates() { + let issuer = SigningKey::from_bytes(&[1u8; 32]); + let agent = Did::from_verifying_key(&SigningKey::from_bytes(&[2u8; 32]).verifying_key()); + let db = ContentId::of_bytes(b"customer-db"); + let grant = Grant { + issuer: Did::from_verifying_key(&issuer.verifying_key()), + subject: agent, + capability: Capability::new(db, ["read"]), + budget_uj: 100_000, + budget_funds: None, + caveats: Vec::new(), + expiry: 0, + parent: None, + delegate_depth: None, + nonce: [9; 16], + }; + let sg = issue(&issuer, grant.clone()).expect("issue"); + let steps = script(&sg, db, ContentId::of_bytes(b"prod-secrets"), agent); + let mut rt = Runtime::new(); + let mut last_allow = None; + for (label, frame, caller, cost) in steps { + eprintln!("{label}: {} µJ [{}] measured {:?}", cost.uj(), cost.provenance.jcr1_label(), cost.measured_uj); + assert_eq!(cost.provenance, Provenance::Estimator, "{label}: a scripted cost graded {:?}", cost.provenance); + assert_eq!(cost.measured_uj, None, "{label}: a measured figure nothing read"); + let d = rt.decide(&frame, &sg, caller, cost, 0); + if d.is_allow() { + last_allow = Some((frame, d)); + } + } + let (frame, d) = last_allow.expect("the script allows a read"); + let steward_sk = SigningKey::from_bytes(&[7u8; 32]); + let steward = "did:example:jcp-runtime"; + let env = jcp_receipt::seal(&frame, &grant, &d, &steward_sk, steward).expect("seal"); + let core = jcp_receipt::jcr1::verify_cose_sign1(&env, steward, &steward_sk.verifying_key().to_bytes()).expect("verifies"); + let sealed: Value = jcp_schema::from_canonical_cbor(&core).expect("decodes"); + eprintln!("sealed: cost_uj {} [{}]", sealed["cost_uj"], sealed["energy_provenance"]); + assert_eq!(sealed["energy_provenance"].as_str(), Some("Estimator"), "{sealed}"); + assert_eq!(sealed["cost_uj"].as_u64(), Some(50_000), "{sealed}"); + } +} diff --git a/jcp/jcp-rs/crates/jcp-cli/tests/demo.rs b/jcp/jcp-rs/crates/jcp-cli/tests/demo.rs new file mode 100644 index 000000000..067f9e3dd --- /dev/null +++ b/jcp/jcp-rs/crates/jcp-cli/tests/demo.rs @@ -0,0 +1,37 @@ +//! The `jcp` demo, run as a user runs it: the receipt it seals for its last +//! allowed row is over a scripted cost no tool spent, so the line it prints +//! for that receipt must grade it `Estimator`, never `HwShunt`. + +use std::process::Command; + +#[test] +fn the_demo_receipt_is_graded_an_estimate() { + let out = Command::new(env!("CARGO_BIN_EXE_jcp")).output().expect("run the jcp binary"); + assert!(out.status.success(), "jcp failed: {}", String::from_utf8_lossy(&out.stderr)); + let stdout = String::from_utf8(out.stdout).expect("utf-8 output"); + eprintln!("{stdout}"); + let receipt = stdout.lines().find(|l| l.trim_start().starts_with("receipt")).expect("a receipt line"); + assert!(receipt.contains("· Estimator ·"), "the scripted cost's receipt is not graded an estimate: {receipt}"); + assert!(!receipt.contains("HwShunt"), "the scripted cost's receipt claims a hardware reading: {receipt}"); + assert!(receipt.ends_with("verified true"), "{receipt}"); + assert!(stdout.contains("budget-exceeded"), "the script no longer runs the budget out:\n{stdout}"); +} + +/// The `meter` line prints the probe's figure at the grade the meter gave +/// that probe, and no other grade beside it: a hardware rung whose probe came +/// back an estimate must not be labelled with the rung's best grade. +#[test] +fn the_meter_line_names_only_the_probes_grade() { + let out = Command::new(env!("CARGO_BIN_EXE_jcp")).output().expect("run the jcp binary"); + assert!(out.status.success(), "jcp failed: {}", String::from_utf8_lossy(&out.stderr)); + let stdout = String::from_utf8(out.stdout).expect("utf-8 output"); + let line = stdout.lines().find(|l| l.trim_start().starts_with("meter")).expect("a meter line"); + eprintln!("{line}"); + let grades: Vec<&str> = ["HwShunt", "ModelBased", "Estimator"].into_iter().filter(|g| line.contains(g)).collect(); + assert_eq!(grades.len(), 1, "the meter line names {grades:?}: {line}"); + let grade = grades[0]; + assert!(line.contains(&format!("[{grade}]")), "the grade printed is not the probe's: {line}"); + if grade != "HwShunt" { + assert!(!line.contains("hardware-measured") && !line.contains("reading,"), "an estimate called a reading: {line}"); + } +} diff --git a/jcp/jcp-rs/crates/jcp-efficiency/MAPPING.md b/jcp/jcp-rs/crates/jcp-efficiency/MAPPING.md index a876ae1d7..f5ad3a7f0 100644 --- a/jcp/jcp-rs/crates/jcp-efficiency/MAPPING.md +++ b/jcp/jcp-rs/crates/jcp-efficiency/MAPPING.md @@ -157,8 +157,15 @@ attestation via `sandbox-tee::energy` to derive the honest trust level; (4) conf the attested energy equals `energy_uj`. The receipt never asserts a level it cannot prove. The whole loop is a runnable demo: `sandbox-tee/examples/attested_efficiency.rs` (`cargo run -p sandbox-tee --example -attested_efficiency`) attests a reading, commits it into an `efficiency/1` receipt, -re-verifies both from scratch, and derives `IntegrityAttested`. +attested_efficiency`) runs a small task, attests the figure this host's meter +gave it under the counter class that obtained it, commits it into an +`efficiency/1` receipt at the same grade, re-verifies both from scratch, and +derives `IntegrityAttested`. Where a vendor counter read the task's CPU work +(the kernel's per-process count on macOS, a RAPL package on Linux) the +statement names that counter and the receipt seals `HwShunt`; where none did, +the figure is the meter's estimate, attested as `Estimator` and sealed +`Estimator` — the report binds the figure as it was obtained and does not make +an estimate a reading. **What `PhysicallyRooted` actually requires (an honest clarification).** It is *not* reached by standing up a cloud confidential VM. A cloud SEV-SNP/TDX instance gives a diff --git a/jcp/jcp-rs/crates/jcp-efficiency/examples/session.rs b/jcp/jcp-rs/crates/jcp-efficiency/examples/session.rs index 3bbf868db..e7b0fdb0b 100644 --- a/jcp/jcp-rs/crates/jcp-efficiency/examples/session.rs +++ b/jcp/jcp-rs/crates/jcp-efficiency/examples/session.rs @@ -1,14 +1,23 @@ -//! A live agentic-fleet efficiency demo: five coding tasks, each sealed as an +//! An agentic-fleet efficiency demo: five coding tasks, each sealed as an //! `efficiency/1` receipt, then verified and aggregated — surfacing the waste a //! "cost per token" number is designed to hide. //! //! cargo run -p jcp-efficiency --example session +//! +//! The fleet is illustrative: no task runs here, so its token counts, wall +//! times and energy figures are chosen, not read. Each energy figure is +//! sealed as what it is, an estimate (`Estimator`, no measured figure) — +//! never a hardware reading, which would claim a meter read work that never +//! happened. A binding that meters real work seals its meter's figure at the +//! meter's grade. use ed25519_dalek::SigningKey; use jcp_efficiency::{aggregate, verify_chain, Efficiency, EfficiencyLog}; use jcp_schema::Energy; -fn task( +/// One illustrative task's `efficiency/1` record: its energy figure is an +/// estimate, since no task runs here. +pub fn task( id: &str, completed: bool, tokens_out: u64, @@ -22,7 +31,7 @@ fn task( verdict: if completed { "tests-pass".into() } else { "gate-failed".into() }, tokens_in: tokens_out * 8, // agentic loops: accumulated context dwarfs output tokens_out, - energy: Energy::measured(uj), // a HwShunt reading in this demo + energy: Energy::estimator(uj), // illustrative: nothing ran, nothing was read wall_ns, value_uusd, energy_attestation: None, @@ -42,7 +51,7 @@ fn main() { task("migrate-schema", false, 3_800, 8_200_000, 95_000_000_000, None), // wrong → reverted ]; - println!("sealing {} efficiency/1 receipts…\n", tasks.len()); + println!("sealing {} efficiency/1 receipts (illustrative tasks: energy sealed as estimates)…\n", tasks.len()); for t in &tasks { log.append(t); let mark = if t.completed { "✓" } else { "✗ spend bought nothing" }; diff --git a/jcp/jcp-rs/crates/jcp-efficiency/tests/session_example.rs b/jcp/jcp-rs/crates/jcp-efficiency/tests/session_example.rs new file mode 100644 index 000000000..d17b53228 --- /dev/null +++ b/jcp/jcp-rs/crates/jcp-efficiency/tests/session_example.rs @@ -0,0 +1,31 @@ +//! The `session` example's fleet is illustrative — no task runs — so every +//! energy figure it seals must be an estimate, never a hardware reading. +//! Checked on the record the example builds (the example file is compiled in +//! here as a module) and on the `efficiency/1` receipt sealed from it, +//! recovered through the signature check. + +#[allow(dead_code)] +#[path = "../examples/session.rs"] +mod session; + +use ed25519_dalek::SigningKey; +use jcp_efficiency::{verify_chain, EfficiencyLog}; +use jcp_schema::Provenance; + +#[test] +fn the_session_examples_figures_are_sealed_as_estimates() { + let sk = SigningKey::from_bytes(&[0xed; 32]); + let mut log = EfficiencyLog::new(sk, "did:jcp:efficiency-demo"); + for (id, completed) in [("fix-auth-bug", true), ("refactor-parser", false)] { + let t = session::task(id, completed, 1_200, 3_500_000, 42_000_000_000, completed.then_some(5_000_000)); + eprintln!("{id}: {} µJ [{}] measured {:?}", t.energy.uj(), t.energy.provenance.jcr1_label(), t.energy.measured_uj); + assert_eq!(t.energy.provenance, Provenance::Estimator, "{id}: an illustrative figure graded {:?}", t.energy.provenance); + assert_eq!(t.energy.measured_uj, None, "{id}: a measured figure nothing read"); + log.append(&t); + } + let recs = verify_chain(log.receipts(), log.steward(), &log.steward_pubkey()).expect("chain verifies"); + for r in &recs { + eprintln!("sealed {}: {} µJ [{}]", r.task, r.energy_uj, r.provenance); + assert_eq!(r.provenance, "Estimator", "{}: sealed as {}", r.task, r.provenance); + } +} diff --git a/jcp/jcp-rs/crates/jcp-runtime/src/lib.rs b/jcp/jcp-rs/crates/jcp-runtime/src/lib.rs index 924f9a54b..b4d3439fe 100644 --- a/jcp/jcp-rs/crates/jcp-runtime/src/lib.rs +++ b/jcp/jcp-rs/crates/jcp-runtime/src/lib.rs @@ -31,6 +31,17 @@ impl Decision { } } +/// The cost [`Runtime::authorize`] puts on its decisions: zero, because no +/// call has run yet — a refusal never runs, and an authorization only lets +/// it. The zero is exact, but nothing read it, so it is graded `Estimator` +/// with no measured figure: sealed as `HwShunt`, every refusal's receipt +/// would claim a hardware reading of a call that never happened, and a +/// ledger's "all readings" floor would count it as one (SPEC §5: a figure +/// never claims better provenance than how it was obtained). +fn nothing_ran() -> Energy { + Energy::estimator(0) +} + /// A price for one action, in a currency's minor units (SPEC §4.8). /// /// The money dual of [`Energy`] — with one deliberate difference: energy carries @@ -496,7 +507,8 @@ impl Runtime { /// because for a metered gateway the true cost is unknown until the call /// runs. Gate with this, run the tool, measure the joules, then /// [`charge`](Self::charge) the measured figure. The returned `Decision` - /// carries the budget remaining and zero cost (nothing was debited). + /// carries the budget remaining and zero cost (nothing was debited, and + /// nothing has run), graded `Estimator`: no meter read that zero. pub fn authorize(&mut self, frame: &Frame, sg: &SignedGrant, caller: Did, now: u64) -> Decision { let g = &sg.grant; let gid = g.id(); @@ -506,20 +518,20 @@ impl Runtime { { return Decision { verdict: Verdict::Deny(r), - cost: Energy::measured(0), + cost: nothing_ran(), budget_remaining_uj: remaining, }; } if remaining == 0 { return Decision { verdict: Verdict::Deny(DenyReason::BudgetExceeded), - cost: Energy::measured(0), + cost: nothing_ran(), budget_remaining_uj: 0, }; } Decision { verdict: Verdict::Allow, - cost: Energy::measured(0), + cost: nothing_ran(), budget_remaining_uj: remaining, } } @@ -571,7 +583,7 @@ impl Runtime { let remaining = g.budget_uj.saturating_sub(self.spent(&gid)); let deny = |r: DenyReason| Decision { verdict: Verdict::Deny(r), - cost: Energy::measured(0), + cost: nothing_ran(), budget_remaining_uj: remaining, }; if let Some(r) = self @@ -583,7 +595,7 @@ impl Runtime { if remaining == 0 { return Decision { verdict: Verdict::Deny(DenyReason::BudgetExceeded), - cost: Energy::measured(0), + cost: nothing_ran(), budget_remaining_uj: 0, }; } @@ -597,7 +609,7 @@ impl Runtime { } } } - Decision { verdict: Verdict::Allow, cost: Energy::measured(0), budget_remaining_uj: remaining } + Decision { verdict: Verdict::Allow, cost: nothing_ran(), budget_remaining_uj: remaining } } /// The flow-aware [`charge`](Self::charge): debit the measured tool joules as @@ -1493,4 +1505,29 @@ mod tests { .decide_chained_flow(&f, &[root_sg], did(&holder), Energy::measured(10), 10, &FlowFacts::bottom()) .is_allow()); } + + /// `authorize` puts a zero on every decision it returns — allow and each + /// refusal — because nothing has run. Nothing read that zero either, so + /// it must be graded `Estimator`, never a hardware reading: a refusal's + /// receipt seals this cost. + #[test] + fn an_authorizations_zero_is_never_a_reading() { + let (sg, holder, r) = setup(); + let mut rt = Runtime::new(); + let cases = [ + ("allow", rt.authorize(&frame(&sg, r, "read", Trust::Control), &sg, holder, 10)), + ("untrusted control", rt.authorize(&frame(&sg, r, "read", Trust::Data), &sg, holder, 10)), + ("action not granted", rt.authorize(&frame(&sg, r, "delete", Trust::Control), &sg, holder, 10)), + ("flow allow", rt.authorize_flow(&frame(&sg, r, "read", Trust::Control), &sg, holder, 10, &FlowFacts::bottom())), + ]; + rt.charge(&sg.grant, Energy::measured(100)); + let exhausted = rt.authorize(&frame(&sg, r, "read", Trust::Control), &sg, holder, 10); + let exhausted_flow = rt.authorize_flow(&frame(&sg, r, "read", Trust::Control), &sg, holder, 10, &FlowFacts::bottom()); + for (what, d) in cases.into_iter().chain([("budget spent", exhausted), ("flow, budget spent", exhausted_flow)]) { + eprintln!("{what}: {:?} cost {} µJ [{}] measured {:?}", d.verdict, d.cost.uj(), d.cost.provenance.jcr1_label(), d.cost.measured_uj); + assert_eq!(d.cost.uj(), 0, "{what}: authorize debits nothing"); + assert_eq!(d.cost.provenance, jcp_schema::Provenance::Estimator, "{what}: a zero nothing read graded {:?}", d.cost.provenance); + assert_eq!(d.cost.measured_uj, None, "{what}: a measured zero nothing read"); + } + } } diff --git a/jcp/jcp-rs/crates/jcp-x402/Cargo.toml b/jcp/jcp-rs/crates/jcp-x402/Cargo.toml index 8632912c1..6a82d9a3e 100644 --- a/jcp/jcp-rs/crates/jcp-x402/Cargo.toml +++ b/jcp/jcp-rs/crates/jcp-x402/Cargo.toml @@ -17,5 +17,9 @@ ed25519-dalek = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } +# The pay example meters its paid compute with this host's meter. +[dev-dependencies] +jcp-energy = { workspace = true } + [lints] workspace = true diff --git a/jcp/jcp-rs/crates/jcp-x402/examples/pay.rs b/jcp/jcp-rs/crates/jcp-x402/examples/pay.rs index 04329c8a1..f69510871 100644 --- a/jcp/jcp-rs/crates/jcp-x402/examples/pay.rs +++ b/jcp/jcp-rs/crates/jcp-x402/examples/pay.rs @@ -65,29 +65,42 @@ fn main() { println!("charge in EURC (grant is USDC) → {}", verdict(&profile.authorize(&eurc, 1))); // 6. THE COUPLING. The host settles the authorized charge on chain and gets - // back a transaction; the meter reports what the paid compute actually - // consumed. Sealing them together produces the artifact neither side has: - // the payment rail knows the transaction but not the energy, an energy - // meter knows the joules but not the transaction. + // back a transaction; its meter reports what the paid compute consumed, + // at the grade it read it. Sealing them together produces the artifact + // neither side has: the payment rail knows the transaction but not the + // energy, an energy meter knows the joules but not the transaction. + // Here no transaction settles (its hash is illustrative). The paid + // compute is a small workload run here, metered by this host's meter as + // it ran, and committed at the grade the meter gave it: a hardware + // reading only where a live counter read the work, else the estimate, + // which the record names as one. let settled = X402Settlement { transaction: Some("0xdeadbeefcafe".into()), network: "base".into(), payer: Some("0xAgentWallet".into()), }; - let measured_uj = 5_812; // what the meter read, not the 6000 µJ estimate - let g = profile.seal_settlement(&req, &settled, measured_uj); - println!("\nsettled {} → sealed {}", settled.transaction.as_deref().unwrap_or("-"), verdict(&g)); + let meter = jcp_energy::Meter::detect(); + let (_, energy) = meter.measure(paid_compute); + let g = profile.seal_settlement(&req, &settled, energy); + println!("\nsettled {} (illustrative tx) → sealed {}", settled.transaction.as_deref().unwrap_or("-"), verdict(&g)); + println!( + " paid compute: {} µJ [{}], metered here by the {:?} rung", + energy.uj(), + energy.provenance.jcr1_label(), + meter.rung() + ); // A third party verifies the binding without trusting the gateway: it // recomputes the coupling id from the settlement record it holds and // checks it against the receipt's payload_hash (jcp/2, SPEC §8). let core = jcp_receipt::extract(&g.receipt, profile.steward(), &profile.steward_pubkey()) .expect("settlement receipt verifies"); - let expected = settled.coupling_id(&req.resource, measured_uj).to_hex(); + let expected = settled.coupling_id(&req.resource, &energy).to_hex(); println!( - " coupling verified: {} (tx + {} µJ bound into payload_hash {}…)", + " coupling verified: {} (tx + {} µJ [{}], bound into payload_hash {}…)", core.payload_hash.as_deref() == Some(expected.as_str()), - measured_uj, + energy.uj(), + energy.provenance.jcr1_label(), &expected[..16] ); @@ -96,7 +109,7 @@ fn main() { let other = X402Settlement { transaction: Some("0xfeed".into()), ..settled.clone() }; println!( " re-point to another tx → id matches: {}", - core.payload_hash.as_deref() == Some(other.coupling_id(&req.resource, measured_uj).to_hex().as_str()) + core.payload_hash.as_deref() == Some(other.coupling_id(&req.resource, &energy).to_hex().as_str()) ); // 7. Every decision sealed a JCR-1 receipt that verifies under the steward @@ -120,6 +133,16 @@ fn main() { ); } +/// The paid compute: a fixed amount of real CPU work on this thread, so the +/// meter has something to read. +fn paid_compute() -> u64 { + let mut x = 0x9e37_79b9_7f4a_7c15u64; + for i in 0..5_000_000u64 { + x = std::hint::black_box(x.rotate_left(5) ^ i).wrapping_mul(0x0000_0100_0000_01b3); + } + x +} + /// Read the verdict the way a verifier would — off the sealed decision. fn verdict(g: &Gated) -> String { match g.decision.verdict { diff --git a/jcp/jcp-rs/crates/jcp-x402/src/lib.rs b/jcp/jcp-rs/crates/jcp-x402/src/lib.rs index ce81d2a53..5806eed6c 100644 --- a/jcp/jcp-rs/crates/jcp-x402/src/lib.rs +++ b/jcp/jcp-rs/crates/jcp-x402/src/lib.rs @@ -32,8 +32,8 @@ //! //! - After the host settles on chain, [`X402Profile::seal_settlement`] seals a //! receipt whose `payload_hash` commits to the **settled transaction hash and -//! the measured joules the paid compute consumed** — the coupling neither side -//! produces alone. The payment rail knows the transaction but not the energy; +//! the joules the paid compute consumed, at the grade of the meter that +//! produced them** — the coupling neither side produces alone. The payment rail knows the transaction but not the energy; //! an energy meter knows the joules but not the transaction. A verifier holding //! the settlement record recomputes its content id and checks it against the //! receipt, so the binding is cryptographic, not asserted. @@ -194,20 +194,33 @@ pub struct X402Settlement { impl X402Settlement { /// The canonical coupling record: the settled transaction, the resource paid - /// for, and the **measured** joules the paid compute consumed. + /// for, and the joules the paid compute consumed **with the grade of the + /// meter that produced them** (`energy_provenance`, and `energy_coverage` + /// for how much of the compute the figure accounts for). + /// + /// The grade is part of the record so the figure cannot claim more than how + /// it was obtained: an estimate is committed as an estimate, and a verifier + /// reads a hardware reading only where the record says `HwShunt`. A figure + /// whose grade does not hold together ([`Energy::check_provenance`]: a + /// `HwShunt` with no measured figure, an `Estimator` with one) is committed + /// as an estimate of its debit figure. /// /// A receipt sealed over this commits to its content id (`payload_hash`, /// SPEC §8), so a verifier holding the record can recompute the hash and - /// confirm the receipt binds *this* transaction to *this* energy figure. - /// Use [`coupling_id`](Self::coupling_id) to compute that id independently. - pub fn coupling_record(&self, resource: &str, joules_uj: u64) -> Vec { + /// confirm the receipt binds *this* transaction to *this* energy figure at + /// *this* grade. Use [`coupling_id`](Self::coupling_id) to compute that id + /// independently. + pub fn coupling_record(&self, resource: &str, energy: &Energy) -> Vec { + let energy = if energy.check_provenance().is_ok() { *energy } else { Energy::estimator(energy.uj()) }; let record = json!({ "x402_settlement": { "network": self.network, "transaction": self.transaction, "payer": self.payer, "resource": resource, - "joules_uj": joules_uj, + "joules_uj": energy.uj(), + "energy_provenance": energy.provenance.jcr1_label(), + "energy_coverage": energy.coverage.label(), } }); serde_json::to_vec(&record).unwrap_or_default() @@ -215,8 +228,8 @@ impl X402Settlement { /// The content id a settlement receipt's `payload_hash` must equal — the /// check a third party runs to verify the coupling. - pub fn coupling_id(&self, resource: &str, joules_uj: u64) -> ContentId { - ContentId::of_bytes(&self.coupling_record(resource, joules_uj)) + pub fn coupling_id(&self, resource: &str, energy: &Energy) -> ContentId { + ContentId::of_bytes(&self.coupling_record(resource, energy)) } } @@ -341,7 +354,8 @@ impl X402Profile { /// **Seal the settlement** — after the host settles an authorized charge on /// chain, seal a receipt whose `payload_hash` commits to the on-chain - /// transaction and the **measured** joules the paid compute consumed. + /// transaction and the joules the paid compute consumed, at the grade of + /// the meter that produced them ([`X402Settlement::coupling_record`]). /// /// This is the artifact neither side produces alone: the payment rail knows /// the transaction but not the energy; an energy meter knows the joules but @@ -350,20 +364,27 @@ impl X402Profile { /// that carries the record. A verifier checks it by recomputing /// [`X402Settlement::coupling_id`] and comparing it to the receipt's /// `payload_hash`. + /// + /// The receipt's own cost is the zero it debits, graded `Estimator`: the + /// zero is an accounting fact, not a reading, and sealed `HwShunt` it + /// would claim a hardware reading of 0 µJ beside a record whose joules are + /// the host's. `energy` is the host's figure for the paid compute, as its + /// meter graded it, committed with that grade: a host with only an + /// estimate passes an estimate, and the record says so. pub fn seal_settlement( &mut self, req: &X402Requirements, settlement: &X402Settlement, - measured_uj: u64, + energy: Energy, ) -> Gated { - let payload = settlement.coupling_record(&req.resource, measured_uj); + let payload = settlement.coupling_record(&req.resource, &energy); let gated = self.bridge.call_x402_priced( &self.grant, self.caller, &req.network, &req.pay_to, &payload, - Energy::measured(0), // records the coupling; does not re-debit + Energy::estimator(0), // records the coupling; debits nothing, reads nothing &Price::new(&req.asset, 0), self.now, ); @@ -511,8 +532,8 @@ mod tests { } /// THE coupling: the settlement receipt cryptographically binds the settled - /// on-chain transaction to the measured joules. Possible only because the - /// `jcp/2` profile commits `payload_hash` (SPEC §8). + /// on-chain transaction to the paid compute's joules at their grade. + /// Possible only because the `jcp/2` profile commits `payload_hash` (SPEC §8). #[test] fn settlement_receipt_binds_tx_to_measured_joules() { let req = ingest_requirements(&sample_req()).unwrap(); @@ -532,33 +553,66 @@ mod tests { network: "base".into(), payer: Some("0xAgentWallet".into()), }; - let g = profile.seal_settlement(&req, &settled, 4_200); + let read = Energy::measured(4_200); + let g = profile.seal_settlement(&req, &settled, read); assert!(g.allowed()); // A third party recomputes the coupling id and checks the receipt. let core = jcp_receipt::extract(&g.receipt, profile.steward(), &profile.steward_pubkey()).unwrap(); assert_eq!( core.payload_hash.as_deref(), - Some(settled.coupling_id(&req.resource, 4_200).to_hex().as_str()), + Some(settled.coupling_id(&req.resource, &read).to_hex().as_str()), "the receipt must bind THIS transaction to THIS energy figure" ); - // A different tx hash — or different joules — yields a different id, so - // the receipt cannot be re-pointed at another settlement. + // A different tx hash, different joules, or the same joules at another + // grade yields a different id, so the receipt cannot be re-pointed at + // another settlement, nor its figure re-graded. let other_tx = X402Settlement { transaction: Some("0xfeed".into()), ..settled.clone() }; - assert_ne!( - core.payload_hash.as_deref(), - Some(other_tx.coupling_id(&req.resource, 4_200).to_hex().as_str()) - ); - assert_ne!( - core.payload_hash.as_deref(), - Some(settled.coupling_id(&req.resource, 9_999).to_hex().as_str()) - ); + for (what, other) in [ + ("another tx", other_tx.coupling_id(&req.resource, &read)), + ("other joules", settled.coupling_id(&req.resource, &Energy::measured(9_999))), + ("an estimate of the same joules", settled.coupling_id(&req.resource, &Energy::estimator(4_200))), + ("a partial reading", settled.coupling_id(&req.resource, &Energy::measured_partial(4_200))), + ] { + assert_ne!(core.payload_hash.as_deref(), Some(other.to_hex().as_str()), "{what}"); + } // Two receipts on the ledger: the authorize and the settlement. assert_eq!(profile.receipts().len(), 2); // The settlement did not re-debit; only the authorize estimate counted. assert_eq!(profile.spent_uj(), 4_200); + + // Its own cost is the zero it debits, which nothing read: sealed as an + // estimate, never as a hardware reading of 0 µJ. + let sealed = jcp_receipt::jcr1::verify_cose_sign1(&g.receipt, profile.steward(), &profile.steward_pubkey()) + .expect("the settlement receipt verifies"); + let sealed: Value = jcp_schema::from_canonical_cbor(&sealed).expect("decodes"); + eprintln!("settlement receipt: cost_uj {} [{}]", sealed["cost_uj"], sealed["energy_provenance"]); + assert_eq!(sealed["cost_uj"].as_u64(), Some(0), "{sealed}"); + assert_eq!(sealed["energy_provenance"].as_str(), Some("Estimator"), "a zero nothing read sealed as: {sealed}"); + } + + /// The coupling record states its figure's grade, so a verifier reads a + /// hardware reading only where the host's meter read one; a figure whose + /// grade does not hold together is committed as an estimate. + #[test] + fn the_coupling_record_carries_the_figures_grade() { + let settled = X402Settlement { transaction: Some("0xabc".into()), network: "base".into(), payer: None }; + let record = |e: Energy| -> Value { + serde_json::from_slice::(&settled.coupling_record("/r", &e)).unwrap()["x402_settlement"].clone() + }; + let read = record(Energy::measured(4_200)); + assert_eq!((read["joules_uj"].as_u64(), read["energy_provenance"].as_str()), (Some(4_200), Some("HwShunt"))); + assert_eq!(read["energy_coverage"].as_str(), Some("Whole")); + let est = record(Energy::estimator(4_200)); + assert_eq!(est["energy_provenance"].as_str(), Some("Estimator")); + assert_eq!(record(Energy::modeled(4_200))["energy_provenance"].as_str(), Some("ModelBased")); + assert_eq!(record(Energy::measured_partial(4_200))["energy_coverage"].as_str(), Some("Partial")); + // A HwShunt grade with no measured figure is not a reading. + let forged = Energy { measured_uj: None, ..Energy::measured(4_200) }; + let forged = record(forged); + assert_eq!((forged["joules_uj"].as_u64(), forged["energy_provenance"].as_str()), (Some(4_200), Some("Estimator"))); } /// A charge to a merchant the grant is not scoped to is denied on audience. diff --git a/jouleclaw/jouleclaw-rs/crates/jouleclaw-corpus/examples/emit_corpus.rs b/jouleclaw/jouleclaw-rs/crates/jouleclaw-corpus/examples/emit_corpus.rs index 37fbb0213..c0a664f35 100644 --- a/jouleclaw/jouleclaw-rs/crates/jouleclaw-corpus/examples/emit_corpus.rs +++ b/jouleclaw/jouleclaw-rs/crates/jouleclaw-corpus/examples/emit_corpus.rs @@ -6,14 +6,17 @@ //! //! The output file is directly loadable by an EOC router trainer's JSONL //! loader. This example stands in a synthetic ledger for the receipts a -//! real deployment would read off disk. +//! real deployment would read off disk. Its figures are chosen, not read, +//! so every stand-in receipt is graded `Estimator`: the corpus carries the +//! grade as a feature, and a real deployment's receipts carry their own. use jouleclaw_corpus::{CorpusBuilder, write_jsonl}; use jouleclaw_energy::Provenance; use jouleclaw_intent::{IntentVerdict, QueryIntent}; use jouleclaw_prov::{CascadeTier, Receipt}; -fn receipt(tier: CascadeTier, uj: u64, intent: QueryIntent, confidence: f64) -> Receipt { +/// One stand-in receipt of the synthetic ledger, its figure an estimate. +pub fn receipt(tier: CascadeTier, uj: u64, intent: QueryIntent, confidence: f64) -> Receipt { Receipt { context: None, jc_receipt: "1".into(), @@ -22,7 +25,11 @@ fn receipt(tier: CascadeTier, uj: u64, intent: QueryIntent, confidence: f64) -> input_hash: "a".repeat(64), tier, joules_uj: uj, - energy_provenance: Provenance::HwShunt, + // A stand-in: nothing ran and no meter read `uj`, so it is graded + // what it is, an estimate. The corpus encodes the grade as a feature + // (its provenance one-hot); a stand-in graded `HwShunt` would teach a + // router that these figures were hardware readings. + energy_provenance: Provenance::Estimator, tools_touched: Vec::new(), claims: Vec::new(), eoc_stage: None, diff --git a/jouleclaw/jouleclaw-rs/crates/jouleclaw-corpus/examples/emit_corpus_semantic.rs b/jouleclaw/jouleclaw-rs/crates/jouleclaw-corpus/examples/emit_corpus_semantic.rs index c3fd0ea4e..29ee24275 100644 --- a/jouleclaw/jouleclaw-rs/crates/jouleclaw-corpus/examples/emit_corpus_semantic.rs +++ b/jouleclaw/jouleclaw-rs/crates/jouleclaw-corpus/examples/emit_corpus_semantic.rs @@ -12,7 +12,8 @@ //! is why this example is behind the `candle` feature and asks for the network //! once. The `build_with_text` join — pairing each receipt with the query that //! produced it — is exactly what a real deployment does from its own logs; here -//! we stand in a small labelled ledger. +//! we stand in a small labelled ledger, its figures chosen, not read, and so +//! graded `Estimator`. use jouleclaw_corpus::{CandleTextEmbedder, CorpusBuilder, TextEmbedder, write_jsonl}; use jouleclaw_embed_candle::{CandleEmbedder, EmbedderConfig}; @@ -30,7 +31,11 @@ fn receipt(tier: CascadeTier, uj: u64, intent: QueryIntent) -> Receipt { input_hash: "a".repeat(64), tier, joules_uj: uj, - energy_provenance: Provenance::HwShunt, + // A stand-in: nothing ran and no meter read `uj`, so it is graded + // what it is, an estimate. The corpus encodes the grade as a feature + // (its provenance one-hot); a stand-in graded `HwShunt` would teach a + // router that these figures were hardware readings. + energy_provenance: Provenance::Estimator, tools_touched: Vec::new(), claims: Vec::new(), eoc_stage: None, diff --git a/jouleclaw/jouleclaw-rs/crates/jouleclaw-corpus/tests/emit_corpus_example.rs b/jouleclaw/jouleclaw-rs/crates/jouleclaw-corpus/tests/emit_corpus_example.rs new file mode 100644 index 000000000..009d17273 --- /dev/null +++ b/jouleclaw/jouleclaw-rs/crates/jouleclaw-corpus/tests/emit_corpus_example.rs @@ -0,0 +1,36 @@ +//! The `emit_corpus` example stands in a synthetic ledger: its figures are +//! chosen, not read, so every stand-in receipt must be graded `Estimator`, +//! and the corpus rows built from them must carry that grade in their +//! provenance one-hot (dims 7–9: HwShunt / ModelBased / Estimator) — never +//! the hardware-reading slot. The example file is compiled in here as a +//! module, so its own `receipt` builds what is checked. + +#[allow(dead_code)] +#[path = "../examples/emit_corpus.rs"] +mod emit_corpus; + +use jouleclaw_corpus::CorpusBuilder; +use jouleclaw_energy::Provenance; +use jouleclaw_intent::QueryIntent; +use jouleclaw_prov::CascadeTier; + +#[test] +fn the_synthetic_ledger_is_graded_an_estimate() { + let ledger = vec![ + emit_corpus::receipt(CascadeTier::L0Cache, 12, QueryIntent::Comprehension, 0.91), + emit_corpus::receipt(CascadeTier::L2Embed, 4_100, QueryIntent::Comprehension, 0.72), + emit_corpus::receipt(CascadeTier::L3Model, 2_400_000, QueryIntent::Generation, 0.94), + emit_corpus::receipt(CascadeTier::L4Wire, 61_000_000, QueryIntent::Generation, 0.97), + ]; + for r in &ledger { + eprintln!("{}: {} µJ [{:?}]", r.id, r.joules_uj, r.energy_provenance); + assert_eq!(r.energy_provenance, Provenance::Estimator, "{}: a stand-in figure graded {:?}", r.id, r.energy_provenance); + } + let (rows, stats) = CorpusBuilder::default().with_monotone_negatives(true).build(&ledger); + eprintln!("{} rows from {} receipts", rows.len(), stats.receipts_seen); + assert!(!rows.is_empty(), "the corpus built no rows"); + for row in &rows { + let one_hot = &row.embedding[7..10]; + assert_eq!(one_hot, &[0.0, 0.0, 1.0], "a row's provenance one-hot is not Estimator's: {one_hot:?}"); + } +} diff --git a/jouleclaw/jouleclaw-rs/crates/jouleclaw-harness-econ/src/lib.rs b/jouleclaw/jouleclaw-rs/crates/jouleclaw-harness-econ/src/lib.rs index 6cb27ca4a..2c9dcee94 100644 --- a/jouleclaw/jouleclaw-rs/crates/jouleclaw-harness-econ/src/lib.rs +++ b/jouleclaw/jouleclaw-rs/crates/jouleclaw-harness-econ/src/lib.rs @@ -202,7 +202,9 @@ pub struct EconReport { pub joules_per_verified_fix: f64, /// **Headline: dollars per verified fix** (`f64::INFINITY` if none verified). pub usd_per_verified_fix: f64, - /// Worst energy provenance observed — the honesty floor of the joule figures. + /// Worst energy provenance observed — the honesty floor of the joule + /// figures. `Estimator` while cascade answers carry no grade of their own: + /// every tier's joules are the figure the tier reported, not a reading. pub energy_provenance: Provenance, } @@ -494,14 +496,18 @@ impl Tier for FixedModelTier { } } -/// Honest provenance of a tier's accounted joules. Local deterministic tiers -/// (cache / skill) are exact by construction; model tiers are estimates unless -/// the tier itself carries a hardware figure (which this v0 cannot see). -fn provenance_of(tier: TierId) -> Provenance { - match tier { - TierId::L0 | TierId::L0_5ToolCompute => Provenance::HwShunt, - _ => Provenance::Estimator, - } +/// The grade of a tier's accounted joules, as far as this harness can know +/// it. A cascade answer carries the joules its tier reported and no grade: a +/// compiled skill's constant (`jouleclaw_skill::SKILL_JOULES`, 20 nJ a +/// resolution), a cache's estimate of a lookup, a model tier's own figure. No +/// tier's figure is a reading this harness took or can see the grade of, so +/// every one is graded `Estimator` — a cache or skill hit included: its figure +/// is exact only as a constant is, and sealed `HwShunt` it would claim a +/// hardware reading nothing took. A run whose model dispatches are metered +/// reports each dispatch's own grade beside the report ([`window_joules`], +/// [`span_joules`]); the report's floor stays an estimate's. +fn provenance_of(_tier: TierId) -> Provenance { + Provenance::Estimator } /// Degrade to the worse of two provenances (same floor rule as `jouleclaw-prov`). @@ -642,6 +648,52 @@ mod tests { assert_eq!(joules_obtained(Provenance::ModelBased), "measured"); } + /// A run that only ever hits a compiled skill is billed the skill's + /// constant (`SKILL_JOULES` a resolution): a figure nothing read, so the + /// report's floor must say `Estimator`, never `HwShunt`. + #[test] + fn a_skill_only_run_is_reported_as_an_estimate() { + let prompt = "generate getter for field user_id"; + let answer = "fn user_id(&self) -> &str { &self.user_id }"; + let tasks = vec![Task::text( + "getter-user_id", + "getter", + q(prompt), + move |o| matches!(o, AnswerOutput::Text(t) if t == answer), + )]; + let harness = EconHarness::new(Box::new(ConstantSkillInducer), 3); + let report = harness.run( + move |store| { + // The class is compiled before the first round, so no round + // reaches a model: every answer is the skill's. + let skill = ConstantSkillInducer + .induce(prompt, answer, TierId::L0_5ToolCompute) + .expect("a constant skill"); + store.lock().unwrap().register(skill); + let mut casc = Cascade::new(); + casc.register(Box::new(SkillTier::new(store))); + Runtime::new_without_l0(casc) + }, + &tasks, + 3, + ); + eprintln!( + "skill-only run: {} rows on tiers {:?}, {} J, floor {:?}", + report.rows.len(), + report.rows.iter().map(|r| r.tier_tag.as_str()).collect::>(), + report.total_joules, + report.energy_provenance + ); + assert_eq!(report.verified, 3, "every round resolves through the skill"); + assert!(report.rows.iter().all(|r| r.hit_skill), "a row missed the skill: {:?}", report.rows); + assert!( + (report.total_joules - 3.0 * SKILL_JOULES).abs() < 1e-15, + "billed {} J, not the skill's constant ×3", + report.total_joules + ); + assert_eq!(report.energy_provenance, Provenance::Estimator, "a skill's constant reported as a reading"); + } + #[test] fn an_estimate_is_never_reported_as_measured() { assert_eq!(joules_obtained(Provenance::Estimator), "estimated"); diff --git a/jouleclaw/jouleclaw-rs/crates/jouleclaw-hydra-client/clients/go/jclaw_hydra.go b/jouleclaw/jouleclaw-rs/crates/jouleclaw-hydra-client/clients/go/jclaw_hydra.go index d638ecbe8..be5b346c8 100644 --- a/jouleclaw/jouleclaw-rs/crates/jouleclaw-hydra-client/clients/go/jclaw_hydra.go +++ b/jouleclaw/jouleclaw-rs/crates/jouleclaw-hydra-client/clients/go/jclaw_hydra.go @@ -322,9 +322,32 @@ func viol(parent NodeShape, p PropertyShape, msg string) Violation { // TransportResponse is what a Transport returns to the client. type TransportResponse struct { - Status int - Body interface{} + Status int + Body interface{} + // JoulesUJ is the transport's figure for the dispatch's energy, if it + // has one. nil: none, and the client emits a zero graded "Estimator". JoulesUJ *uint64 + // JoulesProvenance is the grade of the meter that produced JoulesUJ: + // "HwShunt" only for a hardware reading, "ModelBased", or "Estimator". + // Empty (or any other value) is sealed "Estimator": the client cannot + // tell how a transport obtained a figure, and a figure must not claim + // better provenance than how it was obtained. + JoulesProvenance string +} + +// GradedJoules is the figure and grade the client seals: the transport's +// figure at its stated grade, an ungraded figure as an estimate, and no +// figure as a zero estimate. Matches TransportResponse::graded_joules. +func (r *TransportResponse) GradedJoules() (uint64, string) { + if r.JoulesUJ == nil { + return 0, "Estimator" + } + switch r.JoulesProvenance { + case "HwShunt", "ModelBased", "Estimator": + return *r.JoulesUJ, r.JoulesProvenance + default: + return *r.JoulesUJ, "Estimator" + } } // Transport pluggable transport interface. @@ -458,12 +481,7 @@ func (c *HydraClient) Call(opName string, input interface{}, transport Transport break } } - provenance := "Estimator" - joules := uint64(0) - if resp.JoulesUJ != nil { - provenance = "HwShunt" - joules = *resp.JoulesUJ - } + joules, provenance := resp.GradedJoules() return &CallReport{ ToolID: toolID, Status: resp.Status, diff --git a/jouleclaw/jouleclaw-rs/crates/jouleclaw-hydra-client/clients/go/jclaw_hydra_test.go b/jouleclaw/jouleclaw-rs/crates/jouleclaw-hydra-client/clients/go/jclaw_hydra_test.go index 0d71bc173..0cfdc0824 100644 --- a/jouleclaw/jouleclaw-rs/crates/jouleclaw-hydra-client/clients/go/jclaw_hydra_test.go +++ b/jouleclaw/jouleclaw-rs/crates/jouleclaw-hydra-client/clients/go/jclaw_hydra_test.go @@ -119,3 +119,46 @@ func TestValidationConformance(t *testing.T) { } } } + +// A transport's joule figure says nothing of how it was obtained: an +// ungraded figure is sealed as an estimate, never as a hardware reading. +func TestUngradedJoulesAreSealedAsEstimates(t *testing.T) { + doc := loadConformance(t) + apiBytes, _ := json.Marshal(doc["api"]) + api, err := ParseAPI(apiBytes) + if err != nil { + t.Fatalf("parse api: %v", err) + } + client := NewClient(api, shapeGraphForTests()) + reports := doc["call_reports"].([]interface{}) + v := reports[0].(map[string]interface{}) + op := v["operation"].(string) + opID := client.FindOperation(op).ID + seven := uint64(7) + + transport := NewMockTransport().Respond(opID, &TransportResponse{Status: 200, Body: map[string]interface{}{}, JoulesUJ: &seven}) + got, err := client.Call(op, v["input"], transport) + if err != nil { + t.Fatalf("call: %v", err) + } + if got.Touch["joules_uj"] != uint64(7) || got.Touch["energy_provenance"] != "Estimator" { + t.Errorf("an ungraded figure sealed as %v", got.Touch) + } +} + +// A graded figure keeps the grade the transport gave it. +func TestGradedJoulesKeepTheirGrade(t *testing.T) { + nine := uint64(9) + for _, grade := range []string{"HwShunt", "ModelBased", "Estimator"} { + r := &TransportResponse{JoulesUJ: &nine, JoulesProvenance: grade} + if uj, p := r.GradedJoules(); uj != 9 || p != grade { + t.Errorf("%s: sealed %d %s", grade, uj, p) + } + } + if _, p := (&TransportResponse{JoulesUJ: &nine, JoulesProvenance: "Measured"}).GradedJoules(); p != "Estimator" { + t.Errorf("an unknown grade sealed as %s", p) + } + if uj, p := (&TransportResponse{JoulesProvenance: "HwShunt"}).GradedJoules(); uj != 0 || p != "Estimator" { + t.Errorf("no figure sealed as %d %s", uj, p) + } +} diff --git a/jouleclaw/jouleclaw-rs/crates/jouleclaw-hydra-client/clients/python/jclaw_hydra/client.py b/jouleclaw/jouleclaw-rs/crates/jouleclaw-hydra-client/clients/python/jclaw_hydra/client.py index 79380ad19..bc7ae7de4 100644 --- a/jouleclaw/jouleclaw-rs/crates/jouleclaw-hydra-client/clients/python/jclaw_hydra/client.py +++ b/jouleclaw/jouleclaw-rs/crates/jouleclaw-hydra-client/clients/python/jclaw_hydra/client.py @@ -35,11 +35,32 @@ class InputInvalid(ClientError): self.report = report +GRADES = ("HwShunt", "ModelBased", "Estimator") + + @dataclass class TransportResponse: status: int body: Any + # The transport's figure for the dispatch's energy, if it has one. + # None: none, and the client emits a zero graded "Estimator". joules_uj: Optional[int] = None + # The grade of the meter that produced `joules_uj`: "HwShunt" only for a + # hardware reading, "ModelBased", or "Estimator". None (or any other + # value) is sealed "Estimator": the client cannot tell how a transport + # obtained a figure, and a figure must not claim better provenance than + # how it was obtained. + joules_provenance: Optional[str] = None + + def graded_joules(self) -> "tuple[int, str]": + """The figure and grade the client seals: the transport's figure at + its stated grade, an ungraded figure as an estimate, and no figure as + a zero estimate. Matches `TransportResponse::graded_joules` (Rust).""" + if self.joules_uj is None: + return 0, "Estimator" + if self.joules_provenance in GRADES: + return self.joules_uj, self.joules_provenance + return self.joules_uj, "Estimator" class Transport(Protocol): @@ -138,8 +159,7 @@ class HydraClient: status_matches = any( s.get("code") == response.status for s in op.status_codes ) - provenance = "HwShunt" if response.joules_uj is not None else "Estimator" - joules_uj = response.joules_uj or 0 + joules_uj, provenance = response.graded_joules() return CallReport( tool_id=tool_id, status=response.status, diff --git a/jouleclaw/jouleclaw-rs/crates/jouleclaw-hydra-client/clients/python/tests/test_energy_grade.py b/jouleclaw/jouleclaw-rs/crates/jouleclaw-hydra-client/clients/python/tests/test_energy_grade.py new file mode 100644 index 000000000..6705d2856 --- /dev/null +++ b/jouleclaw/jouleclaw-rs/crates/jouleclaw-hydra-client/clients/python/tests/test_energy_grade.py @@ -0,0 +1,48 @@ +"""A transport's joule figure says nothing of how it was obtained: the +client seals an ungraded figure as an estimate, never as a hardware +reading, and a graded one at the grade the transport gave it. Matches +`TransportResponse::graded_joules` in the Rust client and +`GradedJoules` in the Go one. + +Run: `python -m pytest tests/` (requires `blake3`). +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(ROOT)) + +from test_conformance import VECTORS, shapes # noqa: E402 + +from jclaw_hydra import HydraClient, MockTransport, TransportResponse, parse_api # noqa: E402 + + +def _call(response: TransportResponse) -> dict: + doc = json.loads(VECTORS.read_text()) + client = HydraClient(parse_api(doc["api"]), shapes=shapes()) + vector = doc["call_reports"][0] + op = client.find_operation(vector["operation"]) + transport = MockTransport().respond(op.id, response) + return client.call(vector["operation"], vector["input"], transport).touch + + +def test_an_ungraded_figure_is_sealed_as_an_estimate() -> None: + touch = _call(TransportResponse(status=200, body={}, joules_uj=7)) + assert (touch["joules_uj"], touch["energy_provenance"]) == (7, "Estimator"), touch + + +def test_a_graded_figure_keeps_its_grade() -> None: + for grade in ("HwShunt", "ModelBased", "Estimator"): + touch = _call(TransportResponse(status=200, body={}, joules_uj=9, joules_provenance=grade)) + assert (touch["joules_uj"], touch["energy_provenance"]) == (9, grade), touch + + +def test_an_unknown_grade_or_no_figure_is_an_estimate() -> None: + touch = _call(TransportResponse(status=200, body={}, joules_uj=9, joules_provenance="Measured")) + assert touch["energy_provenance"] == "Estimator", touch + touch = _call(TransportResponse(status=200, body={}, joules_provenance="HwShunt")) + assert (touch["joules_uj"], touch["energy_provenance"]) == (0, "Estimator"), touch diff --git a/jouleclaw/jouleclaw-rs/crates/jouleclaw-hydra-client/conformance/README.md b/jouleclaw/jouleclaw-rs/crates/jouleclaw-hydra-client/conformance/README.md index 356d7dc55..8cbb7d576 100644 --- a/jouleclaw/jouleclaw-rs/crates/jouleclaw-hydra-client/conformance/README.md +++ b/jouleclaw/jouleclaw-rs/crates/jouleclaw-hydra-client/conformance/README.md @@ -36,8 +36,9 @@ The three implementations live at: # Rust (regenerator; also the oracle) cargo run -p jouleclaw-hydra-client --example gen_conformance_vectors -# Python (requires `pip install blake3`) +# Python (requires `pip install blake3`; the energy-grade tests also pytest) python3 crates/jouleclaw-hydra-client/clients/python/tests/test_conformance.py +python3 -m pytest crates/jouleclaw-hydra-client/clients/python/tests/ # Go (uses lukechampine.com/blake3 via go.mod) cd crates/jouleclaw-hydra-client/clients/go && go test -v ./... diff --git a/jouleclaw/jouleclaw-rs/crates/jouleclaw-hydra-client/src/lib.rs b/jouleclaw/jouleclaw-rs/crates/jouleclaw-hydra-client/src/lib.rs index 8cda1ecd4..8dba58786 100644 --- a/jouleclaw/jouleclaw-rs/crates/jouleclaw-hydra-client/src/lib.rs +++ b/jouleclaw/jouleclaw-rs/crates/jouleclaw-hydra-client/src/lib.rs @@ -121,10 +121,16 @@ pub struct TransportResponse { pub status: u16, /// Response payload — typically JSON; opaque bytes welcome too. pub body: Value, - /// Joules spent in the inner dispatch, if the transport measured - /// them. `None` means the transport did not observe energy; the - /// client emits a `Provenance::Estimator` receipt entry. + /// Joules spent in the inner dispatch, if the transport has a figure + /// for them. `None` means it has none; the client emits a zero graded + /// `Provenance::Estimator`. pub joules_uj: Option, + /// The grade of the meter that produced `joules_uj`, as the transport + /// knows it: `HwShunt` only for a hardware reading. `None` — a figure + /// with no grade — is sealed `Provenance::Estimator`: the client cannot + /// tell how a transport obtained a figure, and a figure must not claim + /// better provenance than how it was obtained. + pub joules_provenance: Option, } impl TransportResponse { @@ -133,6 +139,25 @@ impl TransportResponse { status: 200, body, joules_uj: None, + joules_provenance: None, + } + } + + /// This response with a joule figure and the grade of the meter that + /// produced it. + pub fn with_joules(mut self, uj: u64, provenance: Provenance) -> Self { + self.joules_uj = Some(uj); + self.joules_provenance = Some(provenance); + self + } + + /// The figure and grade the client seals: the transport's figure at its + /// stated grade, an ungraded figure as an estimate, and no figure as a + /// zero estimate. + pub fn graded_joules(&self) -> (u64, Provenance) { + match self.joules_uj { + Some(uj) => (uj, self.joules_provenance.unwrap_or(Provenance::Estimator)), + None => (0, Provenance::Estimator), } } } @@ -180,6 +205,7 @@ impl Transport for MockTransport { status: 200, body: serde_json::json!({ "echo": input }), joules_uj: None, + joules_provenance: None, }) } } @@ -326,14 +352,11 @@ impl HydraClient { .iter() .any(|s| s.code == response.status); + let (joules_uj, energy_provenance) = response.graded_joules(); let touch = ToolTouch { tool_id: tool_id.clone(), - joules_uj: response.joules_uj.unwrap_or(0), - energy_provenance: if response.joules_uj.is_some() { - Provenance::HwShunt - } else { - Provenance::Estimator - }, + joules_uj, + energy_provenance, }; Ok(CallReport { @@ -693,6 +716,7 @@ mod tests { status: 201, body: json!({ "ok": true, "id": "n1.x" }), joules_uj: Some(7), + joules_provenance: Some(Provenance::HwShunt), }, ); let report = c @@ -766,6 +790,41 @@ mod tests { assert_eq!(report.body["echo"], json!({ "completed": true })); } + /// A transport's figure says nothing of how it was obtained, so an + /// ungraded figure is sealed as an estimate, never as a hardware reading. + #[tokio::test] + async fn an_ungraded_transport_figure_is_sealed_as_an_estimate() { + let c = client(); + let t = MockTransport::new().respond( + "_:op_jouleclaw_list_nodes", + TransportResponse { + status: 200, + body: json!([]), + joules_uj: Some(7), + joules_provenance: None, + }, + ); + let report = c.call("jouleclaw:list_nodes", json!({}), &t).await.unwrap(); + assert_eq!(report.touch.joules_uj, 7); + assert!( + matches!(report.touch.energy_provenance, Provenance::Estimator), + "an ungraded figure sealed as {:?}", + report.touch.energy_provenance + ); + } + + /// A graded figure keeps the grade the transport gave it, whatever it is. + #[tokio::test] + async fn a_graded_transport_figure_keeps_its_grade() { + let c = client(); + for grade in [Provenance::HwShunt, Provenance::ModelBased, Provenance::Estimator] { + let t = MockTransport::new() + .respond("_:op_jouleclaw_list_nodes", TransportResponse::ok(json!([])).with_joules(9, grade)); + let report = c.call("jouleclaw:list_nodes", json!({}), &t).await.unwrap(); + assert_eq!((report.touch.joules_uj, report.touch.energy_provenance), (9, grade)); + } + } + #[tokio::test] async fn call_records_estimator_provenance_when_transport_skips_energy() { let c = client(); @@ -845,6 +904,7 @@ mod tests { status: 200, body: json!([]), joules_uj: None, + joules_provenance: None, }, ); let r = c diff --git a/jouleclaw/jouleclaw-rs/crates/jouleclaw-stack/examples/ledger_breakdown.rs b/jouleclaw/jouleclaw-rs/crates/jouleclaw-stack/examples/ledger_breakdown.rs index 7a041d02f..5bb0fb864 100644 --- a/jouleclaw/jouleclaw-rs/crates/jouleclaw-stack/examples/ledger_breakdown.rs +++ b/jouleclaw/jouleclaw-rs/crates/jouleclaw-stack/examples/ledger_breakdown.rs @@ -8,6 +8,11 @@ //! //! Run with: //! cargo run -p jouleclaw-stack --example ledger_breakdown +//! +//! The workload is simulated: no tier runs and no meter reads a figure, so +//! every tool touch is graded `Estimator` ([`simulated_touch`]), and the +//! summary's provenance floor says so. A deployment's receipts carry the +//! grade of whatever metered each touch. use jouleclaw_energy::Provenance; use jouleclaw_intent::{HeuristicClassifier, IntentClassifier}; @@ -37,15 +42,7 @@ fn main() { let mut b = ReceiptBuilder::new() .input_hash(input_hash(q)) .tier(*tier) - .account_tool(ToolTouch { - tool_id: tool_id.to_string(), - joules_uj: *joules_uj, - energy_provenance: if matches!(tier, CascadeTier::L3Model) { - Provenance::ModelBased - } else { - Provenance::HwShunt - }, - }) + .account_tool(simulated_touch(tool_id, *joules_uj)) .intent(intent); if let Some(m) = model_id { b = b.model_id(m); @@ -71,6 +68,14 @@ fn main() { print_rows("by intent", &summary.by_intent); } +/// A simulated tool touch: its figure is chosen, not read, so it is graded +/// `Estimator` at every tier. Graded `HwShunt` (as the cheap tiers were) it +/// would claim a hardware reading, and `ModelBased` (as the model tier was) an +/// energy model's figure, of work that never ran. +pub fn simulated_touch(tool_id: &str, joules_uj: u64) -> ToolTouch { + ToolTouch { tool_id: tool_id.to_string(), joules_uj, energy_provenance: Provenance::Estimator } +} + fn print_rows(label: &str, rows: &[jouleclaw_ledger::CostRow]) { if rows.is_empty() { return; diff --git a/jouleclaw/jouleclaw-rs/crates/jouleclaw-stack/tests/ledger_breakdown_example.rs b/jouleclaw/jouleclaw-rs/crates/jouleclaw-stack/tests/ledger_breakdown_example.rs new file mode 100644 index 000000000..b29d7d4bb --- /dev/null +++ b/jouleclaw/jouleclaw-rs/crates/jouleclaw-stack/tests/ledger_breakdown_example.rs @@ -0,0 +1,36 @@ +//! The `ledger_breakdown` example simulates its workload: no tier runs and +//! no meter reads a figure, so every tool touch it seals must be graded +//! `Estimator`, and so must the ledger summary's provenance floor and every +//! row of it — at the model tier as at the cheap ones. The example file is +//! compiled in here as a module, so its own `simulated_touch` builds what is +//! sealed. + +#[allow(dead_code)] +#[path = "../examples/ledger_breakdown.rs"] +mod ledger_breakdown; + +use jouleclaw_energy::Provenance; +use jouleclaw_ledger::{InMemoryLedger, Ledger, LedgerStore, ProvenanceFloor, Window}; +use jouleclaw_prov::{input_hash, CascadeTier, ReceiptBuilder}; + +#[test] +fn the_simulated_workload_is_sealed_as_estimates() { + let mut ledger = InMemoryLedger::new(); + for (q, tier, uj, tool) in [ + ("explain the cascade", CascadeTier::L0Cache, 5, "cache.lookup"), + ("what does jouleclaw-prov do?", CascadeTier::L1Lawful, 12, "iac:lawful.lookup"), + ("write a hello-world fn", CascadeTier::L3Model, 1_200_000, "model:any"), + ] { + let touch = ledger_breakdown::simulated_touch(tool, uj); + eprintln!("{tool}: {} µJ [{:?}]", touch.joules_uj, touch.energy_provenance); + assert_eq!(touch.energy_provenance, Provenance::Estimator, "{tool}: a simulated figure graded {:?}", touch.energy_provenance); + let receipt = ReceiptBuilder::new().input_hash(input_hash(q)).tier(tier).account_tool(touch).seal().expect("seal"); + ledger.append(receipt).expect("append"); + } + let summary = Ledger::new(&ledger).summary(&Window::any()); + eprintln!("summary floor {:?} over {} receipts", summary.provenance_floor, summary.receipt_count); + assert_eq!(summary.provenance_floor, ProvenanceFloor::Estimator, "the simulated ledger's floor"); + for r in summary.by_tier.iter().chain(&summary.by_tool) { + assert_eq!(r.provenance_floor, ProvenanceFloor::Estimator, "row {}: {:?}", r.key, r.provenance_floor); + } +} diff --git a/jouledesk/SPEC.md b/jouledesk/SPEC.md index 1263f01eb..ca347c9b1 100644 --- a/jouledesk/SPEC.md +++ b/jouledesk/SPEC.md @@ -131,7 +131,10 @@ tasks that ran but never completed against the results actually delivered*. That the number a "tasks handled" count hides. Where a skill's work is itself token-based (an LLM skill), the receipt also carries the per-token figures (µJ per accepted token, tokens per completed unit, waste fraction); for a general task, joules-per-completed- -unit is the honest headline. The teammate's contribution is *measured*, not asserted. +unit is the honest headline. The teammate's contribution is *metered*, not asserted: +each task's figure carries the grade of the meter that produced it — a hardware +reading only where a live counter read the task, otherwise an estimate, graded as +one — and a rollup is no better than the weakest figure it sums. --- diff --git a/jouledesk/jouledesk-rs/crates/jouledesk/examples/channel.rs b/jouledesk/jouledesk-rs/crates/jouledesk/examples/channel.rs index c12d96017..652e02f6f 100644 --- a/jouledesk/jouledesk-rs/crates/jouledesk/examples/channel.rs +++ b/jouledesk/jouledesk-rs/crates/jouledesk/examples/channel.rs @@ -61,7 +61,7 @@ fn main() { Err(i) => println!(" ledger FAILED to verify at receipt {i}"), } let s = joule.energy_summary(); - println!(" the teammate's measured contribution (§6):"); + println!(" the teammate's contribution (§6), each figure at the grade of the meter that produced it:"); println!(" completed units : {}", s.completed_units); println!(" wasted units : {} (ran, produced no result)", s.wasted_units); println!( diff --git a/jouledesk/jouledesk-rs/crates/jouledesk/examples/room.rs b/jouledesk/jouledesk-rs/crates/jouledesk/examples/room.rs index 4a9e75a84..6af85c7d9 100644 --- a/jouledesk/jouledesk-rs/crates/jouledesk/examples/room.rs +++ b/jouledesk/jouledesk-rs/crates/jouledesk/examples/room.rs @@ -30,7 +30,7 @@ fn main() { if call.gate_turn(cap, est) { call.meter(tin, tout, est); // the boundary reports the real usage call.complete_turn(); - println!(" {who:>5} │ {said}\n └─ ✓ {cap} — served, metered {est} µJ"); + println!(" {who:>5} │ {said}\n └─ ✓ {cap} — served, an estimated {est} µJ"); } else { println!( " {who:>5} │ {said}\n └─ ✗ {cap} refused (unheld, or over the session ceiling) — decided outside the model" @@ -59,7 +59,7 @@ fn main() { println!( "\n The 'answer' turn about risks was refused — 5.5 mJ already committed, +3 mJ would\n \ break the 8 mJ ceiling. 'delete_recording' was refused: no such grant. What sealed is\n \ - exactly the authorised conversation, at its measured energy — a receipt of what @joule\n \ - did in the call that anyone in the room can verify." + exactly the authorised conversation, at its estimated energy (sealed `Estimator`) — a\n \ + receipt of what @joule did in the call that anyone in the room can verify." ); } diff --git a/jouledesk/jouledesk-rs/crates/jouledesk/src/lib.rs b/jouledesk/jouledesk-rs/crates/jouledesk/src/lib.rs index 154581e89..928372ed8 100644 --- a/jouledesk/jouledesk-rs/crates/jouledesk/src/lib.rs +++ b/jouledesk/jouledesk-rs/crates/jouledesk/src/lib.rs @@ -238,11 +238,17 @@ impl Teammate { /// teammate authorises it against its grants **outside the model**, runs it, and /// seals the decision. Returns the outcome. /// - /// The task's `efficiency/1` receipt carries its energy as a hardware reading - /// ([`EnergyProvenance::HwShunt`]). A binding whose figure came from anything - /// else — an energy model, an estimator — uses [`Self::tag_with_provenance`]. + /// The task's `efficiency/1` receipt carries the cost the dispatch's last + /// decision receipt sealed: the spend the skill reported, else the cost it + /// committed before it ran. Nothing here says how the skill obtained that + /// figure — the reference [`Skill`] reports a constant — so it is sealed as + /// an estimate ([`EnergyProvenance::Estimator`]), the grade the decision + /// receipt already gives it. Sealed as a hardware reading it would claim a + /// meter read work no meter saw (SPEC §6: energy "at honest provenance"). + /// A binding whose skill meters its work passes its meter's grade with + /// [`Self::tag_with_provenance`]. pub fn tag(&mut self, kind: &str, intent: &str) -> TagOutcome { - self.tag_with_provenance(kind, intent, EnergyProvenance::HwShunt) + self.tag_with_provenance(kind, intent, EnergyProvenance::Estimator) } /// [`Self::tag`], sealing the task's `efficiency/1` energy at `provenance`: how @@ -343,9 +349,12 @@ impl Teammate { self.eff.receipts() } - /// The teammate's measured contribution rolled up (SPEC §6): completed vs wasted + /// The teammate's contribution rolled up (SPEC §6): completed vs wasted /// units, joules per completed unit, and the **waste fraction** — the ROI a /// "tasks handled" count hides. Verifies the energy ledger before aggregating. + /// Each receipt's figure carries the grade of the meter that produced it; the + /// totals carry none of their own, so they are no better than the weakest + /// receipt they sum — a total over estimates is an estimate. pub fn energy_summary(&self) -> Totals { let recs = jcp_efficiency::verify_chain( self.eff.receipts(), @@ -483,11 +492,20 @@ mod tests { assert_eq!(t.tag_with_provenance("summarize", "the Q3 thread", provenance), TagOutcome::Done); assert_eq!(sealed_provenance(&t), label, "an energy figure obtained as {provenance:?}"); } - // Plain `tag` is unchanged: it seals the committed cost as a hardware reading. + // Plain `tag` (and `tag_mention`, which calls it) knows nothing of how + // the skill obtained its cost — the reference skill reports a constant — + // so it seals an estimate, never a hardware reading nothing took. let mut t = Teammate::new(); t.grant_skill(summarizer(), 1_000_000).unwrap(); assert_eq!(t.tag("summarize", "the Q3 thread"), TagOutcome::Done); - assert_eq!(sealed_provenance(&t), "HwShunt"); + let plain = sealed_provenance(&t); + let mut m = Teammate::new(); + m.grant_skill(summarizer(), 1_000_000).unwrap(); + assert_eq!(m.tag_mention("@joule summarize the Q3 thread", "@joule"), Some(TagOutcome::Done)); + let mention = sealed_provenance(&m); + eprintln!("plain tag sealed [{plain}], tag_mention sealed [{mention}]"); + assert_eq!(plain, "Estimator", "a skill's reported constant sealed as {plain}"); + assert_eq!(mention, "Estimator", "a skill's reported constant sealed as {mention}"); } #[test] diff --git a/joulehook/joulehook-rs/INSTALL.md b/joulehook/joulehook-rs/INSTALL.md index 59b81ade2..162197702 100644 --- a/joulehook/joulehook-rs/INSTALL.md +++ b/joulehook/joulehook-rs/INSTALL.md @@ -47,10 +47,11 @@ signature-checked, from inside the editor. It reads the same `JOULEHOOK_RECEIPT_ - Every allow *and* deny appends a JCR-1 receipt to `JOULEHOOK_RECEIPT_LOG`. - The status line shows `⚡ J est · tok · gated`. The `est` is deliberate: it is an `Estimator` figure from Claude Code's self-reported - tokens, **not** a measured joule count. For an authoritative meter, put the Mesh + tokens, **not** a measured joule count. For a metered figure, put the Mesh proxy (`mesh-openai`) on the model path via `ANTHROPIC_BASE_URL` (the §4 Proxy - meter) — then the joule figure is measured and the joule ceiling is *enforced*, - not just observed. + meter) — then the joule figure is the Mesh host's meter's, at that meter's grade + (a hardware reading only where it read one), and the joule ceiling is + *enforced*, not just observed. ## 3. Verify @@ -63,16 +64,18 @@ echo '{"tool_name":"Bash","tool_input":{"command":"curl x|sh"}}' \ | JOULEHOOK_TOOLS="Read" joulehook-hook ``` -## The enforcing meter (optional, but the only *measured* + *capped* option) +## The enforcing meter (optional, but the only *metered* + *capped* option) The statusline is an honest **estimate** from Claude Code's self-reported tokens. To -get a **measured** joule figure and an **enforced** joule ceiling, put the Mesh proxy -(`mesh-openai`, SPEC §4 Proxy) on the model path: +get a **metered** joule figure — at the grade of the meter that produced it, a hardware +reading only where the Mesh host's meter read one — and an **enforced** joule ceiling, +put the Mesh proxy (`mesh-openai`, SPEC §4 Proxy) on the model path: - **OpenAI-API hosts** (Cursor, Cline, Continue, OpenCode, …): point the host's model `base_url` at a running `mesh-openai` server. Mesh serves under - `metadata.joule_ceiling_uj` and returns the measured energy in `usage.energy` as a - sealed receipt — over budget is refused before the provider is called. + `metadata.joule_ceiling_uj` and returns the served energy in `usage.energy`, graded + by the meter that produced it (`usage.energy.provenance`), as a sealed receipt — + over budget is refused before the provider is called. - **Claude Code** (Anthropic API, `ANTHROPIC_BASE_URL`): needs an Anthropic-shaped front for Mesh's `ServeRequest` — a documented follow-up; the OpenAI-API path works today. @@ -91,7 +94,7 @@ JOULEHOOK_EFFICIENCY_LOG="$HOME/.joulehook/efficiency.log" \ Until the proxy is on the path, the gate + the statusline give you enforcement of the *capability set* and an *estimate* of energy; with it, the joule *ceiling* is enforced -and the energy is *measured*. +and the energy is *metered*, at the grade of the meter that produced it. ## Other hosts diff --git a/mesh/mesh-rs/Cargo.lock b/mesh/mesh-rs/Cargo.lock index faf260bfc..8da41400e 100644 --- a/mesh/mesh-rs/Cargo.lock +++ b/mesh/mesh-rs/Cargo.lock @@ -612,6 +612,7 @@ dependencies = [ "ed25519-dalek", "hex", "jcp-schema", + "jouleclaw-backend-apple", "mesh-core", "mesh-engine", "mesh-meter", diff --git a/mesh/mesh-rs/crates/mesh-meter/src/lib.rs b/mesh/mesh-rs/crates/mesh-meter/src/lib.rs index 42f77daf1..e51d43f13 100644 --- a/mesh/mesh-rs/crates/mesh-meter/src/lib.rs +++ b/mesh/mesh-rs/crates/mesh-meter/src/lib.rs @@ -269,6 +269,13 @@ mod hw { /// tagged `Estimator`. A failed read is never a zero reading, which would bill a /// span nothing, or bill it the counter's whole history. /// + /// A backend that is itself a whole-host estimate (the family's load + /// estimators, where the host has no live counter) takes the same path from + /// the start: those accrue by the millisecond, so a span shorter than one — + /// a served request, often — would read 0 µJ from them, an estimate that + /// bills work that ran nothing. This crate's estimator resolves a + /// microjoule, so such a span is billed its own length. + /// /// A backend whose readings do not cover the whole request /// ([`covers_whole_request`]: an NVML counter reads the GPUs alone) never /// yields a `HwShunt` span, live or not: `mesh/1` has no coverage field, so @@ -281,6 +288,9 @@ mod hw { inner: CompositeMeter, fallback: EstimatorMeter, state: Mutex, + /// The active backend is a whole-host estimate: every span is billed + /// through `fallback` instead. + estimates: bool, } /// Where the fallback estimate picks up. @@ -301,18 +311,22 @@ mod hw { } fn bridging(inner: CompositeMeter) -> Self { + let estimates = inner + .active() + .is_some_and(|m| m.provenance() == JpProvenance::Estimator && covers_whole_request(m.domain())); Self { inner, fallback: EstimatorMeter::default(), state: Mutex::new(Fallback::default()), + estimates, } } /// The active backend's short name (`"rapl"`, `"nvml"`, `"apple-ioreport"`, - /// `"apple-silicon"`, `"estimation"`, `"none"`), or `"estimator"` once a - /// read has failed and the bridge estimates. + /// `"none"`), or `"estimator"` where this crate's estimator bills the + /// spans: the backend is a whole-host estimate, or a read has failed. pub fn active_name(&self) -> &'static str { - if self.state().failed { + if self.estimates || self.state().failed { FALLBACK_NAME } else { self.inner.active_name() @@ -365,6 +379,11 @@ mod hw { } fn snapshot(&self) -> Sample { + if self.estimates { + // The backend has only a millisecond-resolution estimate of the + // whole host: bill through this crate's, which resolves a µJ. + return self.fallback.snapshot(); + } let mut state = self.state(); if !state.failed { match self.inner.read() { @@ -690,6 +709,31 @@ mod hw { assert!(!covers_whole_request(nvml.domain())); } + /// The family's whole-host estimators accrue by the millisecond, so a + /// span shorter than a millisecond read 0 µJ from them most of the time: + /// a served request billed nothing. Bridged, such a backend bills each + /// span through this crate's [`EstimatorMeter`], whose resolution is a + /// microjoule, so a span in which work ran is never free. + #[test] + fn an_estimator_backend_never_bills_a_short_span_nothing() { + use jouleclaw_energy::meter::EstimationMeter; + let m = HwMeter::bridging(CompositeMeter::new(vec![Box::new(EstimationMeter::new(65.0))])); + for i in 0..50 { + let s = Session::start(&m); + let t = std::time::Instant::now(); + let mut x = 1u64; + while t.elapsed() < Duration::from_micros(50) { + x = x.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1); + } + std::hint::black_box(x); + let e = s.stop(); + assert_eq!((e.provenance, e.measured_uj), (Provenance::Estimator, None), "span {i}: {e:?}"); + assert!(e.estimated_uj > 0, "span {i}: 50 µs of work billed nothing: {e:?}"); + assert!(e.check_provenance().is_ok()); + } + assert_eq!(m.name(), EstimatorMeter::default().name(), "the figure is this crate's estimator's"); + } + #[test] fn a_backend_that_reads_passes_through_unchanged() { let m = bridge(|_| false); @@ -731,7 +775,7 @@ mod hw { "IOReport is used only when its CPU rail is live" ); } - if matches!(m.name(), "apple-silicon" | "estimation") { + if m.name() == EstimatorMeter::default().name() { assert_eq!(e.provenance, Provenance::Estimator); assert_eq!(e.measured_uj, None); assert!(e.estimated_uj > 0, "{e:?}"); @@ -780,8 +824,13 @@ mod process { /// [`super::HwMeter`] bills a counter that reads part of a request. /// - a span with no figure at all is the estimator's. /// - /// Off macOS, or where the process counters are not live, the span meter - /// falls back to the family's host meter, whose grade it carries. + /// Off macOS, or where the process counters are not live or did not + /// advance over the span (a few microseconds of work, say), the span meter + /// falls back to the family's host meter, whose grade it carries. Where + /// that fallback is an estimate, the figure is this crate's + /// [`EstimatorMeter`] over the work's own window: the host meter's window + /// takes in the span meter's waits for the GPU rail at both ends, so its + /// estimate would bill the request for the meter's waiting. pub struct ProcessMeter { span: ProcessSpanMeter, uses: SpanUses, @@ -815,8 +864,16 @@ mod process { self.rest.snapshot() } + /// The estimator's window for the rest of the host opens once the span + /// meter has taken its starting readings, and closes before it takes + /// its last ones ([`ProcessWindow::close`]): the same window as the CPU + /// reading, which leaves out the snapshots' own waits for the GPU rail + /// (up to `GPU_IDLE_WINDOW` on a quiet GPU, `RAIL_EDGE_WAIT` on a busy + /// one). Billing those at the estimator's draw would charge a request + /// for the meter's waiting, not its work. fn open_span(&self) -> Option> { - Some(Box::new(ProcessWindow { meter: self, rest_at: self.rest.snapshot().uj, start: Some(self.span.start()) })) + let start = self.span.start(); + Some(Box::new(ProcessWindow { meter: self, rest_at: self.rest.snapshot().uj, start: Some(start) })) } } @@ -845,7 +902,10 @@ mod process { Ok(s) if s.coverage == Coverage::Whole => match s.provenance { JpProvenance::HwShunt => Energy::measured(s.uj), JpProvenance::ModelBased => Energy::modeled(s.uj), - JpProvenance::Estimator => Energy::estimator(s.uj), + // Only the host meter's fallback is a whole estimate, over a + // window that takes in the span meter's own snapshot waits: + // this crate's estimate over the work's window instead. + JpProvenance::Estimator => Energy::estimator(rest_uj), }, Ok(s) => Energy::estimator(s.uj.saturating_add(rest_uj)), Err(_) => Energy::estimator(rest_uj), @@ -869,6 +929,27 @@ mod process { }) } + /// A span this process's counters could not read — a CPU counter that + /// did not advance over a few microseconds of work — falls back to the + /// host meter, whose window opens before the span meter's first GPU + /// snapshot and closes after its last: a quarter of a second or more of + /// the meter's own waiting. Where that fallback is an estimate, the + /// request is billed this crate's estimate over the work's own window, + /// never the host estimator's over the meter's waits (9.48 J for 11 µs + /// of work, on an M5 Max). + #[test] + fn an_estimated_fallback_span_is_billed_the_works_own_window() { + let fallback = Ok(SpanEnergy { + uj: 9_480_000, + provenance: JpProvenance::Estimator, + coverage: Coverage::Whole, + source: SpanSource::Fallback { meter: "apple-silicon", why: "process CPU: did not advance".to_string() }, + }); + let e = energy_of(fallback, 169); + assert_eq!((e.provenance, e.measured_uj, e.estimated_uj), (Provenance::Estimator, None, 169)); + assert!(e.check_provenance().is_ok()); + } + /// Only a whole span keeps its hardware grade; a partial one is an /// estimate of the whole request, the reading plus the estimator's /// figure for the rest of the host; no figure is the estimator's. diff --git a/mesh/mesh-rs/crates/mesh-openai/Cargo.toml b/mesh/mesh-rs/crates/mesh-openai/Cargo.toml index 0b55a0c8b..030fd2968 100644 --- a/mesh/mesh-rs/crates/mesh-openai/Cargo.toml +++ b/mesh/mesh-rs/crates/mesh-openai/Cargo.toml @@ -14,6 +14,11 @@ jcp-schema = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } hex = { workspace = true } +# Only to choose the reference server example's meter (the features below): +# the library itself never meters, it carries what `mesh-core` sealed. +mesh-meter = { workspace = true, optional = true } +# The GPU load for `ProcessMeter`'s liveness probe, with the `metal` feature. +jouleclaw-backend-apple = { workspace = true, optional = true } [dev-dependencies] mesh-engine = { workspace = true } @@ -27,6 +32,17 @@ default = [] # A minimal, std-only /v1/chat/completions reference server (no async runtime, # no extra deps). Routes through mesh-core and returns the sealed receipt. server = [] +# The meter the reference server example serves with (its module docs). With +# none of these it is the load estimator, and every receipt says `Estimator`. +# `hw-meter`: the family's machine meter (`HwMeter::detect`), a hardware +# reading only where a live counter covers the whole request (a RAPL package). +hw-meter = ["dep:mesh-meter", "mesh-meter/hw-meter"] +hw-meter-rapl = ["hw-meter", "mesh-meter/hw-meter-rapl"] +hw-meter-nvml = ["hw-meter", "mesh-meter/hw-meter-nvml"] +hw-meter-apple = ["hw-meter", "mesh-meter/hw-meter-apple"] +# macOS: this process's own CPU and GPU energy over each serve +# (`ProcessMeter`), since the reference engine runs in this process. +metal = ["hw-meter-apple", "dep:jouleclaw-backend-apple"] # The reference server example uses the `server` module, so it only builds with # the feature on (keeps the default `--all-targets` build clean). @@ -34,5 +50,10 @@ server = [] name = "serve" required-features = ["server"] +# Drives the example's own meter and mesh through one served request. +[[test]] +name = "serve_example" +required-features = ["server"] + [lints] workspace = true diff --git a/mesh/mesh-rs/crates/mesh-openai/examples/serve.rs b/mesh/mesh-rs/crates/mesh-openai/examples/serve.rs index 319fb5daf..135848033 100644 --- a/mesh/mesh-rs/crates/mesh-openai/examples/serve.rs +++ b/mesh/mesh-rs/crates/mesh-openai/examples/serve.rs @@ -7,20 +7,50 @@ //! curl -s localhost:8088/v1/chat/completions \ //! -d '{"model":"gpt-mini","messages":[{"role":"user","content":"hi"}],"max_tokens":8}' //! ``` +//! +//! **What a receipt's figure is.** Each request's `usage.energy` carries the +//! energy of the served span — the deterministic reference engine's work, in +//! this process (the HTTP handling around it is outside the span) — at the +//! grade of whatever read it, in `usage.energy.provenance` and in the sealed +//! `mesh/1` receipt. The meter is the one this build can read: +//! +//! - `--features server,metal` (macOS): `mesh_meter::ProcessMeter`, this +//! process's own CPU and GPU energy over the span. The engine runs in this +//! process and uses no GPU, so where the GPU counter is proved live its GPU +//! energy is exactly zero and the span covers the whole request: `HwShunt`, +//! the kernel's count of this process's CPU energy. That count is every +//! thread of this process, so a request served while another of its +//! threads works reads that work too. Where the GPU counter is not proved +//! live (a GPU busy with other work fails its probe) the span covers part +//! of the request and is sealed as an estimate of all of it, `Estimator`. +//! A serve too short for the CPU count to advance over it (this engine's +//! are microseconds) has no reading either, and is the estimator's figure +//! for the serve, `Estimator`. +//! - `--features server,hw-meter` (or `hw-meter-rapl`, `-nvml`, `-apple`): +//! `mesh_meter::HwMeter::detect`, the machine's counter, a reading only +//! where a live one covers the whole request (a RAPL package on Linux) and +//! `mesh_meter::EstimatorMeter` otherwise. A machine counter reads +//! everything the host does over the span, not this request alone. +//! - `--features server` alone: `mesh_meter::EstimatorMeter`, wall time × an +//! assumed draw, `Estimator`. +//! +//! No build seals a figure nothing read: a `mesh/1` receipt has no coverage +//! field, so `HwShunt` there claims a hardware reading of the whole request +//! (SPEC §3.3), and only a meter that read one may say so. use std::net::TcpListener; use ed25519_dalek::SigningKey; use mesh_core::Mesh; use mesh_engine::ReferenceEngine; -use mesh_meter::{Provenance, SteppingMeter}; +use mesh_meter::Meter; use mesh_openai::{server, ChatBackend}; use mesh_router::BackendSpec; use mesh_schema::Fidelity; /// A trivial tokenizer bridge: bytes in, a deterministic note out. A real /// deployment pairs the model's own tokenizer with the engine that serves it. -struct ByteBackend; +pub struct ByteBackend; impl ChatBackend for ByteBackend { fn encode(&self, text: &str) -> Vec { text.bytes().map(u32::from).collect() @@ -30,20 +60,38 @@ impl ChatBackend for ByteBackend { } } -fn main() -> std::io::Result<()> { - let addr = std::env::args().nth(1).unwrap_or_else(|| "127.0.0.1:8088".to_string()); - let listener = TcpListener::bind(&addr)?; - eprintln!("mesh OpenAI server: http://{addr}/v1/chat/completions"); +/// The meter this build serves with: see the module docs for each feature's +/// meter and what its figures cover. +pub fn meter() -> Box { + #[cfg(feature = "metal")] + let meter: Box = Box::new(mesh_meter::ProcessMeter::new( + jouleclaw_backend_apple::gpu_liveness_load, + mesh_meter::SpanUses::CpuGpu, + )); + #[cfg(all(feature = "hw-meter", not(feature = "metal")))] + let meter: Box = Box::new(mesh_meter::HwMeter::detect()); + #[cfg(not(feature = "hw-meter"))] + let meter: Box = Box::new(mesh_meter::EstimatorMeter::default()); + meter +} - // A measured-grade meter (stepping, HwShunt) + the deterministic reference - // engine, behind one steward-signed receipt chain. - let meter = SteppingMeter::new("rapl", Provenance::HwShunt, 4_800_000); +/// The deterministic reference engine behind one steward-signed receipt +/// chain, metered by `meter`. +pub fn mesh(meter: &dyn Meter) -> Mesh<'_> { let sk = SigningKey::from_bytes(&[8u8; 32]); - let mut mesh = Mesh::new(&meter, sk, "did:example:mesh-demo"); + let mut mesh = Mesh::new(meter, sk, "did:example:mesh-demo"); mesh.register( BackendSpec::new("tiny", "ssm-1b", Fidelity::F2, 50_000), Box::new(ReferenceEngine::new("tiny", "ssm-1b", Fidelity::F2, 50_000, 32)), ); + mesh +} +fn main() -> std::io::Result<()> { + let addr = std::env::args().nth(1).unwrap_or_else(|| "127.0.0.1:8088".to_string()); + let listener = TcpListener::bind(&addr)?; + let meter = meter(); + eprintln!("mesh OpenAI server: http://{addr}/v1/chat/completions (meter: {})", meter.name()); + let mut mesh = mesh(meter.as_ref()); server::run(&listener, &mut mesh, &ByteBackend, 100_000_000) } diff --git a/mesh/mesh-rs/crates/mesh-openai/tests/serve_example.rs b/mesh/mesh-rs/crates/mesh-openai/tests/serve_example.rs new file mode 100644 index 000000000..5e1380d13 --- /dev/null +++ b/mesh/mesh-rs/crates/mesh-openai/tests/serve_example.rs @@ -0,0 +1,140 @@ +//! The reference server example (`examples/serve.rs`) seals what its meter +//! read, never a fixture's figure. +//! +//! Driven here through one real `/v1/chat/completions` request over a socket, +//! with the example's own meter and mesh (the example file is compiled into +//! this test as a module, so what is tested is what `cargo run --example +//! serve` runs). Its receipt's grade must be one this build's meter can give: +//! with no hardware meter feature, only `Estimator`, with no measured figure. +//! A hardware figure must have been read: with `metal` (this process's own +//! counters) it must lie within this process's CPU energy read around the +//! request by a reader that shares no code with the meter — the kernel's +//! `ri_energy_nj`, read raw. A fixture that steps a fixed 4.8 J per snapshot +//! and calls it `HwShunt` fails both: its grade in a build without a +//! hardware meter, and the bound in a build with one (a CPU-only serve of 16 +//! tokens is microjoules to millijoules here, nowhere near joules). +//! +//! And in every branch the request is billed something: a served request +//! ran, so a figure of 0 µJ — an estimator too coarse to see a span shorter +//! than its resolution — bills real work nothing. + +#[allow(dead_code)] +#[path = "../examples/serve.rs"] +mod serve; + +use std::io::{Read, Write}; +use std::net::{SocketAddr, TcpListener, TcpStream}; +use std::sync::mpsc; +use std::thread; + +use serde_json::Value; + +#[cfg(target_os = "macos")] +unsafe extern "C" { + fn proc_pid_rusage(pid: i32, flavor: i32, buffer: *mut std::ffi::c_void) -> i32; +} + +/// This process's CPU energy, nJ: `ri_energy_nj` of `rusage_info_v6`, read +/// here into a raw buffer at the SDK's offset (336), not through any meter. +/// `None` off macOS, where no such per-process count exists. +fn cpu_nj() -> Option { + #[cfg(target_os = "macos")] + { + let mut words = [0u64; 464 / 8]; + let rc = unsafe { proc_pid_rusage(std::process::id() as i32, 6, words.as_mut_ptr().cast()) }; + assert_eq!(rc, 0, "proc_pid_rusage"); + Some(words[336 / 8]) + } + #[cfg(not(target_os = "macos"))] + { + None + } +} + +/// POST one chat completion and return the response's JSON body. +fn post(addr: SocketAddr) -> Value { + let body = r#"{"model":"gpt-mini","messages":[{"role":"user","content":"hi"}],"max_tokens":16}"#; + let mut conn = TcpStream::connect(addr).expect("connect"); + let req = format!( + "POST /v1/chat/completions HTTP/1.1\r\nHost: x\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}", + body.len(), + body + ); + conn.write_all(req.as_bytes()).expect("send"); + let mut raw = String::new(); + conn.read_to_string(&mut raw).expect("read"); + assert!(raw.starts_with("HTTP/1.1 200 OK"), "status line: {}", &raw[..raw.len().min(80)]); + serde_json::from_str(raw.split("\r\n\r\n").nth(1).expect("response body")).expect("JSON body") +} + +#[test] +fn the_reference_server_seals_what_its_meter_read() { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind"); + let addr = listener.local_addr().expect("addr"); + let (built, meter_built) = mpsc::channel(); + // The server lives in one thread, as `main` runs it: the meter, the mesh + // and its receipt chain are built and stay there. + let server = thread::spawn(move || { + let meter = serve::meter(); + built.send(meter.name().to_string()).expect("report the meter"); + let mut mesh = serve::mesh(meter.as_ref()); + mesh_openai::server::serve_n(&listener, &mut mesh, &serve::ByteBackend, 100_000_000, 1).expect("serve"); + }); + // Building the meter runs its liveness probes (a third of a second of CPU + // and a GPU load, for `ProcessMeter`): wait for it, so the window below + // holds the request and not the probes. + let meter = meter_built.recv().expect("the server built its meter"); + + let before = cpu_nj(); + let v = post(addr); + let around_uj = cpu_nj().zip(before).map(|(after, before)| (after - before) / 1_000); + server.join().expect("server thread"); + + let energy = &v["usage"]["energy"]; + let receipt = &energy["receipt"]; + let provenance = energy["provenance"].as_str().expect("usage.energy.provenance"); + let measured = receipt.get("measured_uj").and_then(Value::as_u64); + eprintln!( + "serve example: meter {meter}; usage.energy {} µJ [{provenance}]; sealed {:?} measured {measured:?} estimated {}; \ + this process's CPU energy around the request {around_uj:?} µJ", + energy["uj"], receipt["energy_provenance"], receipt["estimated_uj"] + ); + + assert_eq!(receipt["mesh_receipt"], "mesh/1"); + let uj = energy["uj"].as_u64().expect("usage.energy.uj"); + assert!(uj > 0, "a served request was billed 0 µJ [{provenance}] by meter {meter}: {energy}"); + assert_eq!( + receipt["energy_provenance"].as_str(), + Some(provenance), + "the plaintext grade and the sealed one differ: {energy}" + ); + match provenance { + "Estimator" => assert_eq!(measured, None, "an estimate sealed with a measured figure: {receipt}"), + "HwShunt" => { + if !cfg!(feature = "hw-meter") { + panic!("a build with no hardware meter sealed a hardware reading, which nothing read: {receipt}"); + } + let uj = measured.expect("a HwShunt figure carries its measured figure"); + assert!(uj > 0, "a served span read no energy: {receipt}"); + // Only this process's own counters bound it by this process's + // energy; a machine counter reads the whole host. + if cfg!(feature = "metal") { + let around = around_uj.expect("macOS reads this process's CPU energy"); + assert!( + uj <= around + 1, + "the receipt read {uj} µJ, more than this process's {around} µJ of CPU energy around a CPU-only serve: \ + a figure nothing read" + ); + } + } + "ModelBased" => { + if !cfg!(feature = "hw-meter") || cfg!(feature = "metal") { + panic!("no meter of this build grades a span by a model: {receipt}"); + } + } + other => panic!("an unknown grade `{other}`: {receipt}"), + } + if !cfg!(feature = "hw-meter") { + assert_eq!(provenance, "Estimator", "a build with no hardware meter can only estimate: {receipt}"); + } +} diff --git a/sandbox/sandbox-rs/Cargo.lock b/sandbox/sandbox-rs/Cargo.lock index 0777052a6..7f4705b60 100644 --- a/sandbox/sandbox-rs/Cargo.lock +++ b/sandbox/sandbox-rs/Cargo.lock @@ -1899,6 +1899,7 @@ dependencies = [ "rand", "rsa", "sandbox-attest", + "sandbox-meter", "sandbox-schema", "serde_json", "sha2", diff --git a/sandbox/sandbox-rs/crates/sandbox-cli/src/main.rs b/sandbox/sandbox-rs/crates/sandbox-cli/src/main.rs index d9ffa2e9a..450557300 100644 --- a/sandbox/sandbox-rs/crates/sandbox-cli/src/main.rs +++ b/sandbox/sandbox-rs/crates/sandbox-cli/src/main.rs @@ -2,8 +2,21 @@ //! //! Subcommands: //! sandbox conformance run the C1–C5 conformance pack (SPEC §10) -//! sandbox demo seal a heterogeneous (CPU+GPU) run and print its ids +//! sandbox demo seal an illustrative heterogeneous (CPU+GPU) run and +//! print its ids +//! sandbox run-wasm execute a real WASM unit at tier S2, metered +//! sandbox confidential seal illustrative S6 (TEE) and S7 (sovereign) runs //! sandbox version print the spec/profile versions +//! +//! **`demo` and `confidential` run no unit.** They build a run report here +//! and seal it, to show the receipt and attestation each tier produces. With +//! no work done there is nothing to read, so every energy record they seal +//! is an illustrative figure graded `Estimator`, with no measured figure +//! ([`illustrative`]) — never a hardware reading (SPEC §6.3: `HwShunt` is a +//! figure read from a hardware energy interface for that device class). With +//! no device detected, every device identity they seal is a placeholder named +//! by capability and labelled illustrative ([`illustrative_identity`]). A +//! real unit's energy, metered as it ran, is `run-wasm`'s. use ed25519_dalek::SigningKey; use sandbox_attest::{DeviceIdentity, Locality, Outcome}; @@ -57,9 +70,11 @@ fn help() { "sandbox — reference checker/runner for the Sandbox standard\n\n\ USAGE:\n sandbox \n\n\ conformance run the C1–C5 conformance pack (SPEC §10)\n \ - demo seal a CPU+GPU run and print its receipt/attestation ids\n \ + demo seal an illustrative CPU+GPU run (no unit runs; its energy\n \ + records are estimates) and print its receipt/attestation ids\n \ run-wasm execute a real WASM unit at tier S2, meter fuel→joules, seal + verify\n \ - confidential seal S6 (TEE-attested) and S7 (sovereign) runs, verify the TEE report\n \ + confidential seal illustrative S6 (TEE-attested) and S7 (sovereign) runs, verify\n \ + the TEE report\n \ version print spec and profile versions" ); } @@ -91,27 +106,13 @@ fn confidential() { }); let (_rk, supervisor_vk) = host.verifying_keys(); - let report = || RunReport { - device_energy: vec![DeviceEnergy { - class: DeviceClass::Cpu, - measured_uj: Some(2_000), - estimated_uj: 2_100, - provenance: Provenance::HwShunt, - }], - device_identity: vec![DeviceIdentity { - class: DeviceClass::Cpu, - model: "AMD EPYC (SEV-SNP)".into(), - driver: "host".into(), - }], - outcome: Outcome::Completed, - journal: None, - storage: None, - }; + let report = confidential_report; let grants = Grants::default(); println!("Sandbox confidential — S6 (TEE) and S7 (sovereign) runs\n"); println!(" NOTE: the TEE report here is SOFTWARE-grounded (LocalSnpProvider);"); - println!(" a hardware root requires SEV-SNP/TDX via configfs-tsm on real CC hardware.\n"); + println!(" a hardware root requires SEV-SNP/TDX via configfs-tsm on real CC hardware."); + println!(" No unit runs: each run's energy record is an illustrative estimate.\n"); // S6 — confidential. let s6 = host @@ -135,6 +136,7 @@ fn confidential() { println!(" TEE kind: {:?}", ev.kind); println!(" measurement: {}…", &ev.measurement_hex[..16]); println!(" TCB: {}", ev.tcb); + print_energy(&s6.receipt.devices); println!(" attestation: {}", s6.attestation.content_id().unwrap()); println!(" TEE report verified against policy + VCEK ✓\n"); @@ -160,6 +162,7 @@ fn confidential() { println!(" [S7] tier: {}", s7.attestation.tier.wire_tag()); println!(" locality: {} / {}", loc.region, loc.jurisdiction); println!(" audit_prev: {}", s7.attestation.audit_prev.as_ref().unwrap()); + print_energy(&s7.receipt.devices); println!(" audit head: {} → {}", head_before, host.audit_head()); println!(" sovereign run bound + appended to the audit chain ✓"); } @@ -303,37 +306,7 @@ fn demo() { }; grants.host.inference = true; - let report = RunReport { - device_energy: vec![ - DeviceEnergy { - class: DeviceClass::Cpu, - measured_uj: Some(2_000), - estimated_uj: 2_100, - provenance: Provenance::HwShunt, - }, - DeviceEnergy { - class: DeviceClass::Gpu, - measured_uj: Some(50_000), - estimated_uj: 51_000, - provenance: Provenance::HwShunt, - }, - ], - device_identity: vec![ - DeviceIdentity { - class: DeviceClass::Cpu, - model: "Generic x86_64".into(), - driver: "host".into(), - }, - DeviceIdentity { - class: DeviceClass::Gpu, - model: "NVIDIA H100".into(), - driver: "560.x".into(), - }, - ], - outcome: Outcome::Completed, - journal: None, - storage: None, - }; + let report = demo_report(); let unit_id = ContentId::of_bytes(b"demo-unit"); let image_id = ContentId::of_bytes(b"demo-image"); @@ -354,7 +327,7 @@ fn demo() { .verify(&sealed.attestation_sig, &supervisor_vk) .expect("attestation verifies"); - println!("Sandbox demo — sealed heterogeneous (CPU+GPU) run\n"); + println!("Sandbox demo — sealed heterogeneous (CPU+GPU) run, illustrative: no unit runs\n"); println!(" tier: {}", tier.wire_tag()); println!(" receipt id: {}", sealed.receipt.content_id()); println!( @@ -364,6 +337,9 @@ fn demo() { ); match sealed.receipt.total.measured_uj { Some(m) => println!(" energy (meas): {m} uj total (all classes HwShunt)"), + None if sealed.receipt.devices.iter().all(|d| d.measured_uj.is_none()) => { + println!(" energy (meas): none (illustrative figures, sealed as estimates)") + } None => println!(" energy (meas): partial (not every class measured)"), } for d in &sealed.receipt.devices { @@ -383,3 +359,107 @@ fn demo() { ); println!(" receipt+attestation verified ✓"); } + +/// An illustrative energy record for `class`: `estimated_uj` graded +/// `Estimator`, with no measured figure. `demo` and `confidential` run no +/// unit, so nothing was read; a record of theirs that carried a measured +/// figure, or a `HwShunt` grade, would claim a hardware reading of work that +/// never happened (SPEC §6.3). +fn illustrative(class: DeviceClass, estimated_uj: u64) -> DeviceEnergy { + DeviceEnergy { + class, + measured_uj: None, + estimated_uj, + provenance: Provenance::Estimator, + } +} + +/// An illustrative device identity for `class`: a placeholder named by the +/// capability it stands for (`what`), labelled illustrative, with an +/// illustrative driver. `demo` and `confidential` detect no device, so an +/// identity of theirs that named a product or a driver release would claim a +/// device nothing saw. +fn illustrative_identity(class: DeviceClass, what: &str) -> DeviceIdentity { + DeviceIdentity { + class, + model: format!("illustrative {what} (none detected)"), + driver: "illustrative".into(), + } +} + +/// The run report `demo` seals: a CPU and a GPU record, both illustrative. +fn demo_report() -> RunReport { + RunReport { + device_energy: vec![ + illustrative(DeviceClass::Cpu, 2_100), + illustrative(DeviceClass::Gpu, 51_000), + ], + device_identity: vec![ + illustrative_identity(DeviceClass::Cpu, "host CPU"), + illustrative_identity(DeviceClass::Gpu, "accelerator GPU"), + ], + outcome: Outcome::Completed, + journal: None, + storage: None, + } +} + +/// The run report `confidential` seals for each of its S6 and S7 runs: one +/// illustrative CPU record. +fn confidential_report() -> RunReport { + RunReport { + device_energy: vec![illustrative(DeviceClass::Cpu, 2_100)], + device_identity: vec![illustrative_identity(DeviceClass::Cpu, "confidential-compute CPU")], + outcome: Outcome::Completed, + journal: None, + storage: None, + } +} + +/// Print a sealed receipt's energy records, each with its grade. +fn print_energy(devices: &[DeviceEnergy]) { + for d in devices { + println!( + " energy: {} est={} uj{} [{}]", + d.class.wire_tag(), + d.estimated_uj, + d.measured_uj.map(|m| format!(" meas={m} uj")).unwrap_or_default(), + d.provenance.label() + ); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// No unit runs in `demo` or `confidential`, so every record they seal + /// must be an estimate: `Estimator`, no measured figure. + #[test] + fn the_illustrative_runs_seal_no_reading() { + for (name, report) in [("demo", demo_report()), ("confidential", confidential_report())] { + assert!(!report.device_energy.is_empty(), "{name}: no records"); + for d in &report.device_energy { + eprintln!("{name}: {:?} est={} meas={:?} [{}]", d.class, d.estimated_uj, d.measured_uj, d.provenance.label()); + assert_eq!(d.provenance, Provenance::Estimator, "{name}: a record of no work graded {:?}", d.provenance); + assert_eq!(d.measured_uj, None, "{name}: a measured figure nothing read: {d:?}"); + assert!(d.check_provenance().is_ok(), "{name}: {d:?}"); + } + } + } + + /// Nothing is detected in `demo` or `confidential`, so the device + /// identities their attestations seal must say so: each one a + /// capability-named placeholder labelled illustrative, never a named + /// product or driver release that no probe saw. + #[test] + fn the_illustrative_runs_seal_no_detected_identity() { + for (name, report) in [("demo", demo_report()), ("confidential", confidential_report())] { + assert!(!report.device_identity.is_empty(), "{name}: no identities"); + for d in &report.device_identity { + assert!(d.model.starts_with("illustrative "), "{name}: an identity nothing detected: {d:?}"); + assert_eq!(d.driver, "illustrative", "{name}: a driver nothing detected: {d:?}"); + } + } + } +} diff --git a/sandbox/sandbox-rs/crates/sandbox-cli/tests/illustrative_runs.rs b/sandbox/sandbox-rs/crates/sandbox-cli/tests/illustrative_runs.rs new file mode 100644 index 000000000..cc3c34535 --- /dev/null +++ b/sandbox/sandbox-rs/crates/sandbox-cli/tests/illustrative_runs.rs @@ -0,0 +1,27 @@ +//! `sandbox demo` and `sandbox confidential` run no unit: every energy record +//! of the receipts they seal, as the binary prints them back, is an estimate — +//! `[Estimator]`, with no measured figure — and no line claims a hardware +//! reading. Run as a user runs them, through the built binary. + +use std::process::Command; + +fn run(cmd: &str) -> String { + let out = Command::new(env!("CARGO_BIN_EXE_sandbox")).arg(cmd).output().expect("run the sandbox binary"); + assert!(out.status.success(), "sandbox {cmd} failed: {}", String::from_utf8_lossy(&out.stderr)); + String::from_utf8(out.stdout).expect("utf-8 output") +} + +#[test] +fn the_illustrative_runs_print_estimates_only() { + for cmd in ["demo", "confidential"] { + let out = run(cmd); + eprintln!("sandbox {cmd}:\n{out}"); + let records: Vec<&str> = out.lines().filter(|l| l.contains(" est=")).collect(); + assert!(!records.is_empty(), "sandbox {cmd} printed no energy record:\n{out}"); + for r in &records { + assert!(r.trim_end().ends_with("[Estimator]"), "sandbox {cmd}: a record of no work not graded an estimate: {r}"); + assert!(!r.contains("meas="), "sandbox {cmd}: a measured figure nothing read: {r}"); + } + assert!(!out.contains("HwShunt"), "sandbox {cmd} claims a hardware reading:\n{out}"); + } +} diff --git a/sandbox/sandbox-rs/crates/sandbox-tee/Cargo.toml b/sandbox/sandbox-rs/crates/sandbox-tee/Cargo.toml index d7f82644f..b0aad61f6 100644 --- a/sandbox/sandbox-rs/crates/sandbox-tee/Cargo.toml +++ b/sandbox/sandbox-rs/crates/sandbox-tee/Cargo.toml @@ -34,11 +34,13 @@ signature = { version = "2", optional = true } amd-kds = ["dep:rsa", "dep:x509-cert", "dep:spki", "dep:der", "dep:signature"] # Dev-only, for the `attested_efficiency` example: composes this crate's energy -# attestation with the efficiency/1 receipt (jcp-rs). A dev-dependency, so no -# consumer of sandbox-tee pulls jcp-efficiency, and the receipt crate stays free -# of any dependency on the attestation layer. +# attestation with the efficiency/1 receipt (jcp-rs), over a figure this host's +# meter (sandbox-meter) read for a task the example runs. Dev-dependencies, so +# no consumer of sandbox-tee pulls jcp-efficiency or the meter, and the receipt +# crate stays free of any dependency on the attestation layer. [dev-dependencies] jcp-efficiency = { path = "../../../../jcp/jcp-rs/crates/jcp-efficiency" } +sandbox-meter.workspace = true jcp-schema = { path = "../../../../jcp/jcp-rs/crates/jcp-schema" } ed25519-dalek = "2" # RNG for generating the self-signed AMD-structured test chain (RSA/EC keygen). diff --git a/sandbox/sandbox-rs/crates/sandbox-tee/examples/attested_efficiency.rs b/sandbox/sandbox-rs/crates/sandbox-tee/examples/attested_efficiency.rs index 4f5727fa3..ecf63fbf7 100644 --- a/sandbox/sandbox-rs/crates/sandbox-tee/examples/attested_efficiency.rs +++ b/sandbox/sandbox-rs/crates/sandbox-tee/examples/attested_efficiency.rs @@ -1,118 +1,215 @@ -//! End-to-end: a completed coding task's measured energy is attested inside a TEE, -//! committed into an `efficiency/1` receipt, and then independently re-verified -//! from scratch — deriving the honest trust level, never asserting it. This is the -//! whole frontier composed in one runnable flow. +//! End-to-end: a small task runs here, this host's meter reads its energy, the +//! figure is attested inside a TEE, committed into an `efficiency/1` receipt, +//! and then independently re-verified from scratch — deriving the trust level, +//! never asserting it. This is the whole frontier composed in one runnable flow. //! //! cargo run -p sandbox-tee --example attested_efficiency +//! +//! **What the figure is.** The task is a bounded SHA-512 chain on this thread, +//! metered by [`PowerMeter::detect_in_process`] (the work runs on this +//! process's own threads). The attested counter class and the receipt's grade +//! both follow how the figure was obtained: +//! +//! - where a vendor counter read this process's CPU work over the span (the +//! kernel's per-process count on macOS, a RAPL package on Linux, IOReport's +//! CPU rails on Apple Silicon where they are live), the statement names that +//! counter's class and the receipt seals the reading, `HwShunt`; +//! - where none did (no live counter, or one that did not advance over the +//! span), the figure is the meter's estimate: attested as +//! [`CounterClass::Estimator`] and sealed `Estimator`. +//! +//! Either way the derived trust level is `IntegrityAttested`: a software-grounded +//! report binds the figure, as it was obtained, to a measured environment. What +//! the figure itself is — a reading or an estimate — is the counter class the +//! report binds, printed beside it. use ed25519_dalek::SigningKey; -use jcp_efficiency::{verify as verify_efficiency, Efficiency}; +use jcp_efficiency::{verify as verify_efficiency, Efficiency, EfficiencyReceipt}; use jcp_schema::{ContentId, Energy}; +use sandbox_meter::{Measurement, PowerMeter, Provenance}; +use sandbox_schema::DeviceClass; use sandbox_tee::energy::{ - attest_energy, verify_attested_energy, AttestTrust, CounterClass, EnergyStatement, + attest_energy, verify_attested_energy, AttestTrust, AttestedEnergy, CounterClass, EnergyStatement, }; use sandbox_tee::provider::LocalSnpProvider; use sandbox_tee::Grounding; +use sha2::{Digest, Sha512}; -fn main() { - // ── 1. A coding task completed: 900 accepted tokens over a 2 s window, and the - // on-device meter read 117 mJ. (A RAPL figure — a real sensor reading, - // but platform-self-reported.) - let energy_uj: u64 = 117_000; - let window_ns: u64 = 2_000_000_000; - let tokens_out: u64 = 900; +/// Links in the task's SHA-512 chain: a fixed amount of real CPU work, some +/// hundreds of milliseconds in a debug build and a few in a release one. A +/// span too short for the counter to advance over it is not a reading, and +/// comes back as the meter's estimate. +pub const CHAIN_LINKS: u64 = 20_000; - // ── 2. A verifier issues a fresh nonce; we attest the reading inside a TEE. The - // LocalSnpProvider mints a real ECDSA-P384 report — software-grounded. +/// The task: a SHA-512 chain of [`CHAIN_LINKS`] links from a fixed seed. Its +/// output is the last link, so the work cannot be optimised away. +pub fn task() -> [u8; 64] { + let mut link = [0x5au8; 64]; + for _ in 0..CHAIN_LINKS { + let next = Sha512::digest(link); + link.copy_from_slice(&next); + } + link +} + +/// The counter class of the probe among `sources` (a [`PowerMeter`]'s live +/// hardware probes) that reads CPU energy: only a known one is named. `None` +/// for a probe set with no CPU counter this example can name. +pub fn cpu_counter(sources: &[&str]) -> Option { + sources.iter().find_map(|s| match *s { + "process-energy" => Some(CounterClass::ProcessEnergy), + "rapl-powercap" => Some(CounterClass::Rapl), + "ioreport-energy-model" => Some(CounterClass::IoReport), + _ => None, + }) +} + +/// The figure to attest for span `m` on `meter`, and the class that obtained +/// it. A hardware reading of the CPU work — the span's `HwShunt` CPU records, +/// from a counter this example can name — is attested under that counter's +/// class and sealed as a reading. Anything else is the meter's estimate of the +/// span ([`PowerMeter::estimate`]): attested as `Estimator`, sealed as one. +pub fn figure(meter: &PowerMeter, m: &Measurement) -> (CounterClass, Energy) { + if m.reads_cpu() { + if let Some(class) = cpu_counter(&meter.sources()) { + let cpu_uj: u64 = m + .devices + .iter() + .filter(|d| d.class == DeviceClass::Cpu && d.provenance == Provenance::HwShunt) + .filter_map(|d| d.measured_uj) + .sum(); + return (class, Energy::measured(cpu_uj)); + } + } + (CounterClass::Estimator, Energy::estimator(meter.estimate(m.wall).total_uj())) +} + +/// One run of the composed flow. +pub struct Run { + /// The meter's live hardware probes (empty: the estimator alone). + pub sources: Vec<&'static str>, + /// The span over the task, as the meter recorded it. + pub measurement: Measurement, + /// The attested statement and its report. + pub attested: AttestedEnergy, + /// The `efficiency/1` receipt, as a relying party verified it. + pub receipt: EfficiencyReceipt, + /// The trust level derived from the attestation. + pub trust: AttestTrust, + /// The task's output. + pub output: [u8; 64], + /// Whether a tampered joule figure failed attestation verification. + pub tamper_rejected: bool, +} + +/// Run the task, meter it, attest the figure, seal the receipt, and verify the +/// composition from scratch. +pub fn run() -> Run { + // ── 1. The task runs here, metered as it runs. + let mut meter = PowerMeter::detect_in_process(); + let sources = meter.sources(); + let (output, measurement) = meter.measure(task); + let (counter, energy) = figure(&meter, &measurement); + let window_ns = u64::try_from(measurement.wall.as_nanos()).unwrap_or(u64::MAX); + + // ── 2. A verifier issues a fresh nonce; we attest the figure inside a TEE, + // under the class that obtained it. The LocalSnpProvider mints a real + // ECDSA-P384 report — software-grounded. let nonce = [0x5au8; 32]; - let measurement = [0x11u8; 48]; - let provider = LocalSnpProvider::new(measurement, 7); - let statement = EnergyStatement { - counter: CounterClass::Rapl, - energy_uj, - window_ns, - nonce, - }; - let attested = attest_energy(statement, &provider).expect("attest the reading"); + let measurement_digest = [0x11u8; 48]; + let provider = LocalSnpProvider::new(measurement_digest, 7); + let statement = EnergyStatement { counter, energy_uj: energy.uj(), window_ns, nonce }; + let attested = attest_energy(statement, &provider).expect("attest the figure"); - // ── 3. Seal the efficiency/1 receipt, committing to the attestation by content id. + // ── 3. Seal the efficiency/1 receipt at the figure's own grade, committing + // to the attestation by content id. Its output units are the chain's + // links; no value figure is stated (none is invented). let sk = SigningKey::from_bytes(&[0xefu8; 32]); let steward = "did:jcp:attested-efficiency-demo"; let eff = Efficiency { - task: "fix-auth-bug".into(), + task: "sha512-chain".into(), completed: true, - verdict: "tests-pass".into(), - tokens_in: tokens_out * 8, - tokens_out, - energy: Energy::modeled(energy_uj), + verdict: "chain-computed".into(), + tokens_in: 1, + tokens_out: CHAIN_LINKS, + energy, wall_ns: window_ns, - value_uusd: Some(5_000_000), + value_uusd: None, energy_attestation: None, } .with_energy_attestation(&attested.report); let envelope = eff.seal(0, &ContentId([0u8; 32]), &sk, steward); - println!( - "sealed efficiency/1 for '{}' — {} µJ over {} accepted tokens\n", - eff.task, energy_uj, tokens_out - ); - // ── 4. A relying party runs the COMPOSED verification, from scratch: // (a) the receipt verifies under the steward key. - let rec = verify_efficiency(&envelope, steward, &sk.verifying_key().to_bytes()) - .expect("efficiency/1 verifies"); + let receipt = + verify_efficiency(&envelope, steward, &sk.verifying_key().to_bytes()).expect("efficiency/1 verifies"); // (b) its commitment resolves to the attestation we hold. - let commitment = rec - .energy_attestation - .as_deref() - .expect("receipt carries an attestation commitment"); - let resolved = ContentId::of_bytes(&attested.report).to_hex(); - assert_eq!(commitment, resolved, "commitment must match the attestation"); - // (c) the attestation verifies — deriving the honest trust level from evidence. - let trust = verify_attested_energy( - &attested, - measurement, - &nonce, - &provider.vcek(), - Grounding::Software, - ) - .expect("attestation verifies"); + let commitment = receipt.energy_attestation.as_deref().expect("receipt carries an attestation commitment"); + assert_eq!(commitment, ContentId::of_bytes(&attested.report).to_hex(), "commitment must match the attestation"); + // (c) the attestation verifies — deriving the trust level from evidence. + let trust = verify_attested_energy(&attested, measurement_digest, &nonce, &provider.vcek(), Grounding::Software) + .expect("attestation verifies"); // (d) the attested joules equal the sealed joules. - assert_eq!( - attested.statement.energy_uj, rec.energy_uj, - "attested energy must match the receipt" + assert_eq!(attested.statement.energy_uj, receipt.energy_uj, "attested energy must match the receipt"); + + // Tampering is caught: rewrite the attested energy figure downstream. + let mut forged = attested.clone(); + forged.statement.energy_uj = forged.statement.energy_uj.wrapping_add(1); + let tamper_rejected = + verify_attested_energy(&forged, measurement_digest, &nonce, &provider.vcek(), Grounding::Software).is_err(); + + Run { sources, measurement, attested, receipt, trust, output, tamper_rejected } +} + +fn main() { + let r = run(); + let counter = r.attested.statement.counter; + let sources = if r.sources.is_empty() { "none — the estimator".to_string() } else { r.sources.join(", ") }; + + println!("task '{}' ran here: {} SHA-512 links in {:?}", r.receipt.task, CHAIN_LINKS, r.measurement.wall); + println!(" meter's live counters : {sources}"); + println!( + " sealed efficiency/1 : {} µJ [{}] over {} links\n", + r.receipt.energy_uj, r.receipt.provenance, r.receipt.tokens_out ); println!("composed verification ✓"); println!(" receipt : verified under the steward key"); - println!(" commitment : matches the attestation ({}…)", &commitment[..16]); + println!(" commitment : matches the attestation"); println!( - " attested energy : {} µJ == receipt energy {} µJ", - attested.statement.energy_uj, rec.energy_uj + " attested energy : {} µJ ({}) == receipt energy {} µJ [{}]", + r.attested.statement.energy_uj, + counter.tag(), + r.receipt.energy_uj, + r.receipt.provenance ); println!(" nonce : fresh (matches the issued challenge)"); - println!(" efficiency : {} µJ / accepted token", rec.uj_per_accepted_token); - println!(" TRUST LEVEL : {trust:?}"); + println!(" efficiency : {} µJ / link", r.receipt.uj_per_accepted_token); + println!(" TRUST LEVEL : {:?}, over a `{}` figure", r.trust, counter.tag()); println!(); - // ── 5. The honesty the whole exercise turns on. - assert_eq!(trust, AttestTrust::IntegrityAttested); - println!( - "This is IntegrityAttested, not PhysicallyRooted: a software-grounded report\n\ - over a platform-self-reported counter (RAPL) attests an honest *relay* inside\n\ - a measured environment — not a reading rooted independently of the platform.\n\ - (Not a claim that RAPL is inaccurate: it is a real sensor reading; nothing\n\ - binds it to the sensor.) An independent root needs a dedicated sensor signed\n\ - at the source (a shunt) on confidential hardware — derived here, never asserted." - ); + assert_eq!(r.trust, AttestTrust::IntegrityAttested); + if counter == CounterClass::Estimator { + println!( + "This is IntegrityAttested over an ESTIMATE: no counter on this host read the\n\ + task's CPU work over the span, so the figure is the meter's estimate, attested\n\ + under the `estimator` class and sealed `Estimator`. The report binds the figure\n\ + to a measured environment; it does not make the figure a reading." + ); + } else { + println!( + "This is IntegrityAttested, not PhysicallyRooted: a software-grounded report\n\ + over a platform-self-reported counter (`{}`) attests an honest *relay* inside\n\ + a measured environment — not a reading rooted independently of the platform.\n\ + (Not a claim that the counter is inaccurate: it is a real sensor reading;\n\ + nothing binds it to the sensor.) An independent root needs a dedicated sensor\n\ + signed at the source (a shunt) on confidential hardware — derived, never asserted.", + counter.tag() + ); + } - // Prove tampering is caught: rewrite the sealed energy figure downstream. - let mut forged = attested.clone(); - forged.statement.energy_uj = 1; - assert!( - verify_attested_energy(&forged, measurement, &nonce, &provider.vcek(), Grounding::Software) - .is_err(), - "a tampered energy figure must fail attestation verification" - ); + assert!(r.tamper_rejected, "a tampered energy figure must fail attestation verification"); println!("\n(a tampered joule figure fails attestation verification — checked.)"); + std::hint::black_box(r.output); } diff --git a/sandbox/sandbox-rs/crates/sandbox-tee/src/energy.rs b/sandbox/sandbox-rs/crates/sandbox-tee/src/energy.rs index b2322bcb4..36494b49e 100644 --- a/sandbox/sandbox-rs/crates/sandbox-tee/src/energy.rs +++ b/sandbox/sandbox-rs/crates/sandbox-tee/src/energy.rs @@ -69,6 +69,10 @@ pub enum CounterClass { Nvml, /// Apple IOReport — an on-SoC power figure the platform reports. IoReport, + /// The OS kernel's count of one process's own CPU energy (macOS + /// `proc_pid_rusage`, `ri_energy_nj`) — a real platform-reported figure + /// for that process's threads alone, relayed by the platform's software. + ProcessEnergy, /// Redfish / BMC telemetry — coarse, facility-level. Redfish, /// A vendor-provided model estimate. @@ -94,6 +98,7 @@ impl CounterClass { CounterClass::Rapl => "rapl", CounterClass::Nvml => "nvml", CounterClass::IoReport => "ioreport", + CounterClass::ProcessEnergy => "process_energy", CounterClass::Redfish => "redfish", CounterClass::ModelBased => "model_based", CounterClass::Estimator => "estimator", @@ -109,6 +114,7 @@ impl CounterClass { CounterClass::Redfish => 4, CounterClass::ModelBased => 5, CounterClass::Estimator => 6, + CounterClass::ProcessEnergy => 7, } } } @@ -333,6 +339,19 @@ mod tests { assert!(matches!(out, Err(TeeError::Policy(_))), "a replayed report must be rejected"); } + #[test] + fn every_counter_class_binds_a_distinct_statement() { + use CounterClass::*; + let all = [HwShunt, Rapl, Nvml, IoReport, ProcessEnergy, Redfish, ModelBased, Estimator]; + let mut seen = std::collections::BTreeSet::new(); + for c in all { + // The same figure under another class is another statement: an + // estimate cannot be passed off as a reading by relabelling it. + assert!(seen.insert(statement(c, 1_000, [0; 32]).report_data()), "{c:?} collides"); + assert_eq!(c.is_independently_rooted(), c == HwShunt, "{c:?}"); + } + } + #[test] fn the_trust_ladder_is_derived_not_asserted() { // The only path to a physical root: a physical counter AND hardware grounding. diff --git a/sandbox/sandbox-rs/crates/sandbox-tee/tests/attested_efficiency_example.rs b/sandbox/sandbox-rs/crates/sandbox-tee/tests/attested_efficiency_example.rs new file mode 100644 index 000000000..46e2463c5 --- /dev/null +++ b/sandbox/sandbox-rs/crates/sandbox-tee/tests/attested_efficiency_example.rs @@ -0,0 +1,89 @@ +//! The `attested_efficiency` example attests and seals what this host's meter +//! read for a task it ran, never a fixed figure under a counter nothing read. +//! +//! The example file is compiled into this test as a module, so what is tested +//! is what `cargo run --example attested_efficiency` runs. The attested counter +//! class must be one of the meter's live probes, over a span that has a +//! hardware reading of the CPU work, and the receipt must seal it `HwShunt`; +//! otherwise the class must be `Estimator` and the receipt `Estimator`. A +//! literal figure attested as RAPL on a host with no RAPL counter, and sealed +//! `ModelBased`, fails both. + +#[allow(dead_code)] +#[path = "../examples/attested_efficiency.rs"] +mod example; + +use sandbox_tee::energy::{AttestTrust, CounterClass}; + +/// The probe name a class's reading comes from, for the classes the example +/// can attest a reading under. +fn source_of(class: CounterClass) -> Option<&'static str> { + match class { + CounterClass::ProcessEnergy => Some("process-energy"), + CounterClass::Rapl => Some("rapl-powercap"), + CounterClass::IoReport => Some("ioreport-energy-model"), + _ => None, + } +} + +#[test] +fn the_example_attests_and_seals_what_the_meter_read() { + let r = example::run(); + let counter = r.attested.statement.counter; + eprintln!( + "attested_efficiency: sources {:?}; span {:?} from `{}`; attested {} µJ ({}); sealed {} µJ [{}]", + r.sources, + r.measurement.devices, + r.measurement.source, + r.attested.statement.energy_uj, + counter.tag(), + r.receipt.energy_uj, + r.receipt.provenance + ); + + assert_eq!(r.attested.statement.energy_uj, r.receipt.energy_uj); + assert!(r.receipt.energy_uj > 0, "a task that ran was billed nothing"); + assert_eq!(r.trust, AttestTrust::IntegrityAttested); + assert!(r.tamper_rejected); + + match counter { + CounterClass::Estimator => { + assert_eq!(r.receipt.provenance, "Estimator", "an estimate sealed as something else"); + } + reading => { + let source = source_of(reading) + .unwrap_or_else(|| panic!("the example attested a reading under `{}`, a class it cannot read", reading.tag())); + assert!( + r.sources.contains(&source), + "attested `{}` but the meter's live probes are {:?}: a counter nothing read", + reading.tag(), + r.sources + ); + assert!(r.measurement.reads_cpu(), "attested a reading of a span with no CPU reading: {:?}", r.measurement); + assert_eq!(r.receipt.provenance, "HwShunt", "a reading sealed at another grade"); + } + } + if r.sources.is_empty() { + assert_eq!(counter, CounterClass::Estimator, "no live counter, yet a reading was attested"); + } +} + +/// A span with no hardware reading of the CPU work is attested as the +/// estimate it is: `Estimator`, no measured figure, and not free. Driven with +/// the machine-wide meter, which on a host with no live machine counter (Apple +/// Silicon whose IOReport CPU rails are not live) has no reading to give. +#[test] +fn a_span_with_no_cpu_reading_is_attested_as_an_estimate() { + let mut meter = sandbox_meter::PowerMeter::detect(); + let (_, m) = meter.measure(example::task); + let (class, energy) = example::figure(&meter, &m); + eprintln!("machine meter: sources {:?}; span {:?}; attested as `{}`: {energy:?}", meter.sources(), m.devices, class.tag()); + if m.reads_cpu() && example::cpu_counter(&meter.sources()).is_some() { + assert_ne!(class, CounterClass::Estimator); + assert_eq!(energy.provenance, jcp_schema::Provenance::HwShunt); + } else { + assert_eq!(class, CounterClass::Estimator); + assert_eq!((energy.provenance, energy.measured_uj), (jcp_schema::Provenance::Estimator, None)); + } + assert!(energy.uj() > 0, "{energy:?}"); +} diff --git a/smart-byte/smart-byte-rs/crates/smart-byte/src/main.rs b/smart-byte/smart-byte-rs/crates/smart-byte/src/main.rs index 55192c408..efe49fa1d 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte/src/main.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte/src/main.rs @@ -54,7 +54,9 @@ enum EnvelopeCmd { /// US dollars in micros (six decimals). Used for cargo-type=usd. #[arg(long = "amount-micros", default_value_t = 0)] amount_micros: i64, - /// Microjoules. Used for cargo-type=joules, and as the measured cost. + /// Microjoules. Used for cargo-type=joules, and as the byte's cost — + /// sealed as an estimate: the CLI reads no meter, so a figure given here + /// is the issuer's claim, never a hardware measurement. #[arg(long = "microjoules", default_value_t = 0)] microjoules: u64, /// Optional path to write the CBOR-encoded envelope to. @@ -130,6 +132,15 @@ fn demo_holder() -> Aid { Aid::of_inception(Said::hash(b"smart-byte cli demo holder")) } +/// The cost `envelope new` seals: the figure given on the command line, as +/// an estimate. `JouleCost::measured_microjoules` is energy measured by +/// hardware, and the CLI measures nothing — no work runs to issue a demo byte +/// that a meter could read — so a figure put there would claim a measurement +/// nobody took. +fn demo_cost(microjoules: u64) -> JouleCost { + JouleCost::estimated(microjoules) +} + fn envelope_new( cargo_type: CargoType, amount_micros: i64, @@ -142,7 +153,7 @@ fn envelope_new( CargoType::Joules => Cargo::joules(microjoules), }; let issuer = demo_issuer(); - let envelope = Envelope::issue(&issuer, demo_holder(), [0; 16], cargo, JouleCost::measured(microjoules)); + let envelope = Envelope::issue(&issuer, demo_holder(), [0; 16], cargo, demo_cost(microjoules)); let bytes = envelope.to_cbor(); println!("identity : {}", envelope.identity()); @@ -217,3 +228,23 @@ fn cluster_simulate( fn hex8(hash: &[u8; 32]) -> String { hash.iter().take(8).map(|b| format!("{b:02x}")).collect() } + +#[cfg(test)] +mod tests { + use super::*; + + /// The CLI measures nothing, so the cost it seals carries no measured + /// figure, whatever the command line gives — checked on the issued byte + /// after a round trip through its canonical bytes. + #[test] + fn a_command_line_figure_is_sealed_as_an_estimate() { + for uj in [0, 1_234, 50_000_000] { + let env = Envelope::issue(&demo_issuer(), demo_holder(), [0; 16], Cargo::joules(uj), demo_cost(uj)); + let back = Envelope::from_cbor(&env.to_cbor()).expect("decode"); + let cost = back.joule_cost; + eprintln!("--microjoules {uj}: measured {} µJ, estimated {} µJ", cost.measured_microjoules, cost.estimated_microjoules); + assert_eq!(cost.measured_microjoules, 0, "a figure no meter read sealed as measured: {cost:?}"); + assert_eq!(cost.estimated_microjoules, uj, "{cost:?}"); + } + } +}