no fabricated readings: every figure sealed as a reading was read #212
48 changed files with 1703 additions and 298 deletions
10
.github/workflows/standards-build.yml
vendored
10
.github/workflows/standards-build.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
3
jcp/jcp-rs/Cargo.lock
generated
3
jcp/jcp-rs/Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<dyn std::error::Error>> {
|
|||
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<dyn std::error::Error>> {
|
|||
);
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:?}");
|
||||
}
|
||||
|
|
@ -16,5 +16,8 @@ ed25519-dalek = { workspace = true }
|
|||
serde = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
57
jcp/jcp-rs/crates/jcp-cite/tests/cite_example.rs
Normal file
57
jcp/jcp-rs/crates/jcp-cite/tests/cite_example.rs
Normal file
|
|
@ -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}");
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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::<u64>()));
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
37
jcp/jcp-rs/crates/jcp-cli/tests/demo.rs
Normal file
37
jcp/jcp-rs/crates/jcp-cli/tests/demo.rs
Normal file
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" };
|
||||
|
|
|
|||
31
jcp/jcp-rs/crates/jcp-efficiency/tests/session_example.rs
Normal file
31
jcp/jcp-rs/crates/jcp-efficiency/tests/session_example.rs
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<u8> {
|
||||
/// 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<u8> {
|
||||
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::<Value>(&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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:?}");
|
||||
}
|
||||
}
|
||||
|
|
@ -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::<Vec<_>>(),
|
||||
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");
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 ./...
|
||||
|
|
|
|||
|
|
@ -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<u64>,
|
||||
/// 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<Provenance>,
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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!(
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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 `⚡ <joules> J est · <tokens> tok · <n> 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
|
||||
|
||||
|
|
|
|||
1
mesh/mesh-rs/Cargo.lock
generated
1
mesh/mesh-rs/Cargo.lock
generated
|
|
@ -612,6 +612,7 @@ dependencies = [
|
|||
"ed25519-dalek",
|
||||
"hex",
|
||||
"jcp-schema",
|
||||
"jouleclaw-backend-apple",
|
||||
"mesh-core",
|
||||
"mesh-engine",
|
||||
"mesh-meter",
|
||||
|
|
|
|||
|
|
@ -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<Fallback>,
|
||||
/// 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<Box<dyn Window + '_>> {
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<u32> {
|
||||
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<dyn Meter> {
|
||||
#[cfg(feature = "metal")]
|
||||
let meter: Box<dyn Meter> = 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<dyn Meter> = Box::new(mesh_meter::HwMeter::detect());
|
||||
#[cfg(not(feature = "hw-meter"))]
|
||||
let meter: Box<dyn Meter> = 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)
|
||||
}
|
||||
|
|
|
|||
140
mesh/mesh-rs/crates/mesh-openai/tests/serve_example.rs
Normal file
140
mesh/mesh-rs/crates/mesh-openai/tests/serve_example.rs
Normal file
|
|
@ -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<u64> {
|
||||
#[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}");
|
||||
}
|
||||
}
|
||||
1
sandbox/sandbox-rs/Cargo.lock
generated
1
sandbox/sandbox-rs/Cargo.lock
generated
|
|
@ -1899,6 +1899,7 @@ dependencies = [
|
|||
"rand",
|
||||
"rsa",
|
||||
"sandbox-attest",
|
||||
"sandbox-meter",
|
||||
"sandbox-schema",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
|
|
|
|||
|
|
@ -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 <conformance|demo|run-wasm|version>\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:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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<CounterClass> {
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:?}");
|
||||
}
|
||||
|
|
@ -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:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue