jcp-gateway, joule-ui-serve: name the plaintext figure cost_uj #128

Merged
dcharlot merged 1 commit from energy/cost-keys-named-for-what-they-carry into main 2026-10-01 06:22:25 -04:00
5 changed files with 44 additions and 43 deletions

View file

@ -113,7 +113,7 @@ fn main() -> std::io::Result<()> {
continue;
}
match gw.relay(&line, &mut up)? {
Relayed::Allowed { response, measured_uj, budget_remaining_uj } => {
Relayed::Allowed { response, cost_uj, budget_remaining_uj } => {
// How the joules were obtained, read back from the receipt the
// gateway just sealed, so this line cannot disagree with it.
let grade = gw
@ -121,7 +121,7 @@ fn main() -> std::io::Result<()> {
.last()
.and_then(|r| sealed_grade(r, &steward, &steward_pk))
.unwrap_or_else(|| "unverified".into());
eprintln!("jcp-gateway: allowed, {measured_uj} µJ [{grade}], {budget_remaining_uj} µJ left");
eprintln!("jcp-gateway: allowed, {cost_uj} µJ [{grade}], {budget_remaining_uj} µJ left");
if !response.is_empty() {
writeln!(stdout, "{response}")?;
stdout.flush()?;

View file

@ -57,10 +57,10 @@ pub enum Relayed {
/// A non-`tools/call` message forwarded verbatim; here is the response.
PassedThrough(String),
/// A `tools/call` the gateway authorized, metered, and forwarded.
/// `measured_uj` is the figure debited from the grant: a hardware reading
/// or the meter's estimate. The sealed receipt's `energy_provenance`, and
/// the same label beside the figure in the response's `_meta`, say which.
Allowed { response: String, measured_uj: u64, budget_remaining_uj: u64 },
/// `cost_uj` is the figure debited from the grant: a hardware reading or
/// the meter's estimate. The sealed receipt's `energy_provenance`, and the
/// same label beside the figure in the response's `_meta`, say which.
Allowed { response: String, cost_uj: u64, budget_remaining_uj: u64 },
/// A `tools/call` the gateway refused; the upstream was never contacted.
/// `response` is a JSON-RPC error to hand back to the client.
Denied { response: String, reason: String },
@ -273,7 +273,7 @@ impl Gateway {
// estimate when this host exposes no live counter or the span's CPU
// counter held. Either way it is debited, so a runaway is enforced.
let cost = energy_of(&measurement);
let measured_uj = cost.uj();
let cost_uj = cost.uj();
// Phase 3: debit the metered joules (and the tainted sub-budget, if the
// call was tainted); seal the accounting decision as jcp/3 when tainted.
@ -307,7 +307,7 @@ impl Gateway {
Ok(Relayed::Allowed {
response,
measured_uj,
cost_uj,
budget_remaining_uj: decision.budget_remaining_uj,
})
}
@ -369,10 +369,10 @@ fn jcp_meta(params: &Value) -> Option<&Value> {
/// for. Returns the response unchanged if it is not a JSON-RPC result object —
/// this must never mangle an upstream payload it does not understand.
///
/// The plaintext figure is graded beside itself. `measured_uj` keeps its name
/// for the clients already reading it, but it is the call's COST, which is a
/// hardware reading only where the span was one; on a host with no live energy
/// counter it is the meter's estimate. So `energy_provenance` (`HwShunt`,
/// The plaintext figure is graded beside itself. `cost_uj` is the call's cost,
/// the key the sealed receipt uses for it, and it is a hardware reading only
/// where the span was one; on a host with no live energy counter it is the
/// meter's estimate. So `energy_provenance` (`HwShunt`,
/// `ModelBased` or `Estimator`) rides next to it, with `energy_coverage:
/// "Partial"` when the figure reads only part of the call, the same two labels
/// the sealed receipt carries. A client that reads only the plaintext therefore
@ -392,7 +392,7 @@ fn attach_receipt(response: &str, receipt: &[u8], cost: &Energy, remaining_uj: u
let Some(meta) = meta else { return response.to_string() };
let mut jcp = json!({
"receipt_b64": b64(receipt),
"measured_uj": cost.uj(),
"cost_uj": cost.uj(),
"energy_provenance": cost.provenance.jcr1_label(),
"budget_remaining_uj": remaining_uj,
});
@ -555,7 +555,7 @@ mod tests {
fn the_caller_gets_its_receipt_back() {
let mut g = gw();
let mut up = MockServer { calls: 0 };
let Relayed::Allowed { response, measured_uj, budget_remaining_uj } =
let Relayed::Allowed { response, cost_uj, budget_remaining_uj } =
g.relay(&call("read_file", 1), &mut up).unwrap()
else {
panic!("expected Allowed")
@ -564,7 +564,7 @@ mod tests {
let v: Value = serde_json::from_str(&response).unwrap();
let meta = v.pointer(&format!("/result/_meta/{}", JCP_META_NS.replace('/', "~1")))
.expect("receipt must ride in result._meta under the namespaced key");
assert_eq!(meta["measured_uj"].as_u64(), Some(measured_uj));
assert_eq!(meta["cost_uj"].as_u64(), Some(cost_uj));
assert_eq!(meta["budget_remaining_uj"].as_u64(), Some(budget_remaining_uj));
// The returned receipt is the same sealed bytes the gateway logged —
@ -573,11 +573,11 @@ mod tests {
assert_eq!(b64_returned, b64(g.receipts().last().unwrap()));
// The plaintext figure carries its grade, and it is the grade the
// receipt sealed: a client reading `measured_uj` without decoding the
// receipt sealed: a client reading `cost_uj` without decoding the
// receipt is told whether it is a reading or an estimate.
let steward_pub = SigningKey::from_bytes(&[7u8; 32]).verifying_key().to_bytes();
let sealed = jcr1_payload(g.receipts().last().unwrap(), "did:jcp:gateway:files", &steward_pub);
let grade = meta["energy_provenance"].as_str().expect("energy_provenance beside measured_uj");
let grade = meta["energy_provenance"].as_str().expect("energy_provenance beside cost_uj");
assert_eq!(Some(grade), sealed.get("energy_provenance").and_then(Value::as_str), "{meta} vs {sealed}");
assert_eq!(meta.get("energy_coverage"), sealed.get("energy_coverage"), "{meta} vs {sealed}");
eprintln!("gateway _meta: hardware={} {meta}", g.metering_is_hardware());
@ -589,9 +589,8 @@ mod tests {
assert_eq!(v["result"]["ok"].as_bool(), Some(true));
}
/// `measured_uj` keeps its name for compatibility but is the call's cost,
/// so its grade rides beside it in plaintext for every shape the cost
/// takes: `energy_provenance` always, `energy_coverage` only when the
/// `cost_uj` is the call's cost, a reading or an estimate, so its grade
/// rides beside it in plaintext for every shape the cost takes: `energy_provenance` always, `energy_coverage` only when the
/// figure covers part of the call (whole is the default and is omitted,
/// as in the sealed receipt).
#[test]
@ -606,7 +605,8 @@ mod tests {
] {
let v: Value = serde_json::from_str(&attach_receipt(ok, b"r", &cost, 9)).unwrap();
let meta = v.pointer(&ptr).expect("the jcp _meta block");
assert_eq!(meta["measured_uj"].as_u64(), Some(cost.uj()), "{meta}");
assert_eq!(meta["cost_uj"].as_u64(), Some(cost.uj()), "{meta}");
assert!(meta.get("measured_uj").is_none(), "{meta}");
assert_eq!(meta["energy_provenance"].as_str(), Some(provenance), "{meta}");
assert_eq!(meta.get("energy_coverage").and_then(Value::as_str), coverage, "{meta}");
assert_eq!(meta["budget_remaining_uj"].as_u64(), Some(9));
@ -683,10 +683,10 @@ mod tests {
let mut up = MockServer { calls: 0 };
let r = g.relay(&call("read_file", 1), &mut up).unwrap();
match r {
Relayed::Allowed { measured_uj, budget_remaining_uj, .. } => {
Relayed::Allowed { cost_uj, budget_remaining_uj, .. } => {
assert_eq!(up.calls, 1, "upstream was contacted");
assert!(measured_uj > 0, "a real span costs joules, measured or estimated");
assert_eq!(budget_remaining_uj, 50_000_000 - measured_uj, "budget was debited by the cost");
assert!(cost_uj > 0, "a real span costs joules, measured or estimated");
assert_eq!(budget_remaining_uj, 50_000_000 - cost_uj, "budget was debited by the cost");
}
other => panic!("expected Allowed, got {other:?}"),
}
@ -812,14 +812,14 @@ mod tests {
let steward_pub = SigningKey::from_bytes(&[7u8; 32]).verifying_key().to_bytes();
let mut g = gw();
let mut up = MockServer { calls: 0 };
let Relayed::Allowed { measured_uj, .. } = g.relay(&call("read_file", 1), &mut up).unwrap() else {
let Relayed::Allowed { cost_uj, .. } = g.relay(&call("read_file", 1), &mut up).unwrap() else {
panic!("expected Allowed")
};
let payload = jcr1_payload(g.receipts().last().unwrap(), "did:jcp:gateway:files", &steward_pub);
let sealed = payload.get("energy_provenance").and_then(Value::as_str).expect("energy_provenance");
eprintln!("gateway call: hardware={} sealed={sealed} cost_uj={measured_uj}", g.metering_is_hardware());
assert_eq!(payload.get("cost_uj").and_then(Value::as_u64), Some(measured_uj));
assert!(measured_uj > 0, "a call that ran costs joules, measured or estimated");
eprintln!("gateway call: hardware={} sealed={sealed} cost_uj={cost_uj}", g.metering_is_hardware());
assert_eq!(payload.get("cost_uj").and_then(Value::as_u64), Some(cost_uj));
assert!(cost_uj > 0, "a call that ran costs joules, measured or estimated");
if g.metering_is_hardware() {
// A hardware meter reads the span, or estimates it if its CPU counter held.
assert!(matches!(sealed, "HwShunt" | "Estimator"), "{payload}");

View file

@ -141,7 +141,7 @@ fn js(endpoint: &str) -> String {
body:JSON.stringify({{action:action,value:value}})}});\
const j=await r.json();\
if(j.html!==undefined)document.getElementById('jui-root').innerHTML=j.html;\
if(j.verdict==='allow'){{note.textContent='sealed: '+action+' · '+j.measured_uj+' µJ ['+j.provenance+'] · seq '+j.seq;}}\
if(j.verdict==='allow'){{note.textContent='sealed: '+action+' · '+j.cost_uj+' µJ ['+j.energy_provenance+'] · seq '+j.seq;}}\
else{{note.textContent='denied: '+action+' — '+(j.reason||'');}}\
}}catch(e){{note.textContent='error: '+e;}}\
finally{{window.__juiBusy=false;}}\

View file

@ -328,7 +328,7 @@ fn js(action: &str) -> String {
const j=await r.json();\
if(j.html!==undefined)document.getElementById('jui-out').innerHTML=j.html;\
note.textContent=j.verdict==='allow'\
?('sealed: apply · '+j.measured_uj+' µJ ['+j.provenance+'] · seq '+j.seq)\
?('sealed: apply · '+j.cost_uj+' µJ ['+j.energy_provenance+'] · seq '+j.seq)\
:('denied: '+(j.reason||''));\
}}catch(e){{note.textContent='error: '+e;}}\
finally{{busy=false; if(pending!==null){{const p=pending;pending=null;send(p);}}}}\
@ -412,9 +412,9 @@ mod tests {
]));
let mut h = host(Box::new(SpanMeter::new(script)));
let first = fire(&mut h);
assert_eq!((first["provenance"].as_str(), first["measured_uj"].as_u64()), (Some("HwShunt"), Some(1_500)));
assert_eq!((first["energy_provenance"].as_str(), first["cost_uj"].as_u64()), (Some("HwShunt"), Some(1_500)));
let second = fire(&mut h);
assert_eq!((second["provenance"].as_str(), second["measured_uj"].as_u64()), (Some("Estimator"), Some(160_000)));
assert_eq!((second["energy_provenance"].as_str(), second["cost_uj"].as_u64()), (Some("Estimator"), Some(160_000)));
let chain = h.entries();
assert_eq!(verify_chain(chain), Ok(2));
@ -441,9 +441,9 @@ mod tests {
let mut h = host(Box::new(SpanMeter::new(script)));
let first = fire(&mut h);
assert_eq!((first["provenance"].as_str(), first["measured_uj"].as_u64()), (Some("Estimator"), Some(160_000)));
assert_eq!((first["energy_provenance"].as_str(), first["cost_uj"].as_u64()), (Some("Estimator"), Some(160_000)));
let second = fire(&mut h);
assert_eq!((second["provenance"].as_str(), second["measured_uj"].as_u64()), (Some("HwShunt"), Some(10_500)));
assert_eq!((second["energy_provenance"].as_str(), second["cost_uj"].as_u64()), (Some("HwShunt"), Some(10_500)));
let chain = h.entries();
assert_eq!(verify_chain(chain), Ok(2));

View file

@ -190,10 +190,10 @@ impl Host {
"html": html,
"verdict": it.verdict.tag(),
"reason": match &it.verdict { joule_ui_interaction::Verdict::Deny(r) => Some(r.clone()), _ => None },
// The figure, under the key the page already reads; it is a
// reading only when the grade beside it says `HwShunt`.
"measured_uj": it.energy.uj(),
"provenance": it.energy.provenance.label(),
// The interaction's cost: a reading only when the grade
// beside it says `HwShunt`, the meter's estimate otherwise.
"cost_uj": it.energy.uj(),
"energy_provenance": it.energy.provenance.label(),
"budget_remaining_uj": it.budget_remaining_uj,
"seq": sealed.seq,
"receipt_id": sealed.content_id().0,
@ -265,11 +265,12 @@ mod tests {
assert_eq!(r.status, 200);
let j: Value = serde_json::from_str(&r.body).unwrap();
assert_eq!(j["verdict"], "allow");
// `measured_uj` keeps its name for the page reading it, and is the
// figure whatever its grade; the grade rides beside it. A stub's
// figure is never served as a reading.
assert_eq!(j["measured_uj"], 2500);
assert_eq!(j["provenance"], "Estimator");
// `cost_uj` is the figure whatever its grade, and the grade rides
// beside it. A stub's figure is never served as a reading, and no key
// calls the figure measured.
assert_eq!(j["cost_uj"], 2500);
assert_eq!(j["energy_provenance"], "Estimator");
assert!(j.get("measured_uj").is_none(), "{j}");
assert!(j["html"].as_str().unwrap().contains("intensity 7"));
assert_eq!(j["seq"], 0);