jcp-gateway, joule-ui-serve: name the plaintext figure cost_uj #128
5 changed files with 44 additions and 43 deletions
|
|
@ -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()?;
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -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;}}\
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue