From 4952e8ac3486c90fdd322a5db74228efabb74f9b Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Thu, 1 Oct 2026 01:26:57 -0400 Subject: [PATCH] jcp-gateway, joule-ui-serve: name the plaintext figure cost_uj Two plaintext keys called `measured_uj` carried the call's cost, which is the meter's estimate wherever no live counter read the span: - jcp-gateway's `_meta` block on an allowed tools/call; - joule-ui-serve's /action response. Their grade already rode beside them, but the name still said "measured". Nothing outside these crates reads either key: no spec, vector or site defines them. - jcp-gateway: `_meta.cost_uj`, the key its sealed receipt already uses for the same figure, beside `energy_provenance` and, when partial, `energy_coverage`. `Relayed::Allowed { cost_uj }` follows. - joule-ui-serve: `cost_uj` and `energy_provenance`, and the page scripts in joule-ui-render and the serve binary read those. The energy record's own `measured_uj`, present only for a hardware reading, is unchanged everywhere, signed vectors included. Tests assert the old key is gone. Co-Authored-By: Claude Opus 5.5 --- .../crates/jcp-gateway/src/bin/jcp_gateway.rs | 4 +- jcp/jcp-rs/crates/jcp-gateway/src/lib.rs | 52 +++++++++---------- .../crates/joule-ui-render/src/lib.rs | 2 +- .../joule-ui-serve/src/bin/joule_ui_serve.rs | 10 ++-- .../crates/joule-ui-serve/src/lib.rs | 19 +++---- 5 files changed, 44 insertions(+), 43 deletions(-) diff --git a/jcp/jcp-rs/crates/jcp-gateway/src/bin/jcp_gateway.rs b/jcp/jcp-rs/crates/jcp-gateway/src/bin/jcp_gateway.rs index 84126ad08..49290cd2d 100644 --- a/jcp/jcp-rs/crates/jcp-gateway/src/bin/jcp_gateway.rs +++ b/jcp/jcp-rs/crates/jcp-gateway/src/bin/jcp_gateway.rs @@ -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()?; diff --git a/jcp/jcp-rs/crates/jcp-gateway/src/lib.rs b/jcp/jcp-rs/crates/jcp-gateway/src/lib.rs index edcc28bfb..9123fb233 100644 --- a/jcp/jcp-rs/crates/jcp-gateway/src/lib.rs +++ b/jcp/jcp-rs/crates/jcp-gateway/src/lib.rs @@ -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}"); diff --git a/joule-ui/joule-ui-rs/crates/joule-ui-render/src/lib.rs b/joule-ui/joule-ui-rs/crates/joule-ui-render/src/lib.rs index cd1fc6d51..dfe7f331d 100644 --- a/joule-ui/joule-ui-rs/crates/joule-ui-render/src/lib.rs +++ b/joule-ui/joule-ui-rs/crates/joule-ui-render/src/lib.rs @@ -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;}}\ diff --git a/joule-ui/joule-ui-rs/crates/joule-ui-serve/src/bin/joule_ui_serve.rs b/joule-ui/joule-ui-rs/crates/joule-ui-serve/src/bin/joule_ui_serve.rs index 65525eb95..cf25a8429 100644 --- a/joule-ui/joule-ui-rs/crates/joule-ui-serve/src/bin/joule_ui_serve.rs +++ b/joule-ui/joule-ui-rs/crates/joule-ui-serve/src/bin/joule_ui_serve.rs @@ -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)); diff --git a/joule-ui/joule-ui-rs/crates/joule-ui-serve/src/lib.rs b/joule-ui/joule-ui-rs/crates/joule-ui-serve/src/lib.rs index becc55ae8..e6992e691 100644 --- a/joule-ui/joule-ui-rs/crates/joule-ui-serve/src/lib.rs +++ b/joule-ui/joule-ui-rs/crates/joule-ui-serve/src/lib.rs @@ -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); -- 2.54.0 (Apple Git-157)