jcp: an energy figure must say how much of the work it covers #66
2 changed files with 127 additions and 4 deletions
|
|
@ -101,6 +101,15 @@ pub fn to_jcr1_wire(frame: &Frame, grant: &Grant, decision: &Decision, steward:
|
|||
}
|
||||
obj.insert("cost_uj".into(), json!(decision.cost.uj()));
|
||||
obj.insert("energy_provenance".into(), json!(decision.cost.provenance.jcr1_label()));
|
||||
// Coverage rides beside provenance, but only when it is `Partial`: the
|
||||
// whole-task case is the default and is omitted, so every receipt sealed
|
||||
// before this field existed canonicalizes to the same bytes and still
|
||||
// verifies. Without this the distinction would exist in the type and vanish
|
||||
// in the artifact, which is the failure the field was added to prevent — a
|
||||
// partial reading would reach a verifier looking exactly like a whole one.
|
||||
if !decision.cost.coverage.is_whole() {
|
||||
obj.insert("energy_coverage".into(), json!(decision.cost.coverage.label()));
|
||||
}
|
||||
obj.insert("budget_remaining_uj".into(), json!(decision.budget_remaining_uj));
|
||||
obj.insert("budget_uj".into(), json!(grant.budget_uj));
|
||||
obj.insert("steward".into(), json!(steward));
|
||||
|
|
@ -707,6 +716,8 @@ mod tests {
|
|||
/// `conformance/v1/vectors.json` before `payload_hash` existed, sealed under
|
||||
/// `jcp/1`. They MUST still verify — version dispatch recomputes the legacy
|
||||
/// preimage — or the bump silently invalidated every receipt in the wild.
|
||||
|
||||
|
||||
#[test]
|
||||
fn published_v1_receipts_still_verify() {
|
||||
const V1_ALLOW: &str = "d284581fa201270458196469643a6578616d706c653a6a63702d7265666572656e6365a05901d2ae65747275737467636f6e74726f6c66616374696f6e64726561646669737375657278386469643a6b65793a7a364d6b6f6e334e656364364e6b6b79666f476f48786964327a6e476335394c55334b376d756261526346624c664c5867636f73745f756a181e677374657761726478196469643a6578616d706c653a6a63702d7265666572656e6365677375626a65637478386469643a6b65793a7a364d6b6f39685467674d776a535445614a615055664536747163793278765536426e4e713365336f38715642697948677665726469637465616c6c6f77686772616e745f6964784033623161356139636262373831353134636637353162613030376530373765383038333937376234663037623130346433366565393034643932383939386334687265736f75726365784030663766623535636565393361623937373738323936303865383531666133316536386531326462396466633061383032366164323938333134636633663932696275646765745f756a18646a6672616d655f6b696e6469746f6f6c2d63616c6c6b6a63705f72656365697074656a63702f3171656e657267795f70726f76656e616e63656748775368756e74736275646765745f72656d61696e696e675f756a184658405a7be56b33956c069d4e23127baa7b090e5028b9704dff6ca5ac46da546534e667151f80d7d7a605bcc31ea2bb33ec971e3e8b8794f7248a18b34c33bc438509";
|
||||
|
|
|
|||
|
|
@ -125,6 +125,38 @@ impl Provenance {
|
|||
}
|
||||
}
|
||||
|
||||
/// How much of a unit of work an energy figure accounts for.
|
||||
///
|
||||
/// Deliberately separate from [`Provenance`]: a reading can be hardware-rooted
|
||||
/// and still cover only part of the work, and reporting the two as one number
|
||||
/// overstates it. A partial figure is not wrong — it is incomplete, and saying
|
||||
/// which is the whole point.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Coverage {
|
||||
/// The figure accounts for the whole unit of work.
|
||||
#[default]
|
||||
Whole,
|
||||
/// The figure accounts for only part of it. The remainder is **unmeasured**,
|
||||
/// which is not the same as zero, and a `Partial` figure MUST NOT be
|
||||
/// presented as a task total.
|
||||
Partial,
|
||||
}
|
||||
|
||||
impl Coverage {
|
||||
/// Whether this is the default, wire-omitted case.
|
||||
pub fn is_whole(&self) -> bool {
|
||||
matches!(self, Coverage::Whole)
|
||||
}
|
||||
|
||||
/// The wire label.
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Coverage::Whole => "Whole",
|
||||
Coverage::Partial => "Partial",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A two-part energy reading, integer microjoules (SPEC §5).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Energy {
|
||||
|
|
@ -132,18 +164,53 @@ pub struct Energy {
|
|||
pub measured_uj: Option<u64>,
|
||||
pub estimated_uj: u64,
|
||||
pub provenance: Provenance,
|
||||
/// How much of the work this figure accounts for.
|
||||
///
|
||||
/// [`Provenance`] answers *how* the number was acquired; it says nothing
|
||||
/// about *how much of the task it covers*, and the two are routinely
|
||||
/// conflated to the figure's advantage. On a host where the accelerator
|
||||
/// exposes an energy counter but the CPU does not, a caller reads the
|
||||
/// accelerator and stamps `HwShunt` on a figure presented as the task's
|
||||
/// energy — the CPU's share is not zero, it is unmeasured and silently
|
||||
/// missing.
|
||||
///
|
||||
/// Defaults to [`Coverage::Whole`] and is omitted from the wire in that
|
||||
/// case, so every previously sealed receipt and published vector is
|
||||
/// unchanged.
|
||||
#[serde(default, skip_serializing_if = "Coverage::is_whole")]
|
||||
pub coverage: Coverage,
|
||||
}
|
||||
|
||||
impl Energy {
|
||||
pub fn modeled(uj: u64) -> Self {
|
||||
Energy { measured_uj: None, estimated_uj: uj, provenance: Provenance::ModelBased }
|
||||
Energy { measured_uj: None, estimated_uj: uj, provenance: Provenance::ModelBased, coverage: Coverage::Whole }
|
||||
}
|
||||
pub fn measured(uj: u64) -> Self {
|
||||
Energy { measured_uj: Some(uj), estimated_uj: uj, provenance: Provenance::HwShunt }
|
||||
Energy { measured_uj: Some(uj), estimated_uj: uj, provenance: Provenance::HwShunt, coverage: Coverage::Whole }
|
||||
}
|
||||
pub fn estimator(uj: u64) -> Self {
|
||||
Energy { measured_uj: None, estimated_uj: uj, provenance: Provenance::Estimator }
|
||||
Energy { measured_uj: None, estimated_uj: uj, provenance: Provenance::Estimator, coverage: Coverage::Whole }
|
||||
}
|
||||
/// A hardware reading that covers only part of the work — an accelerator
|
||||
/// counter on a host whose CPU has none, say.
|
||||
///
|
||||
/// Exists so the honest case is as easy to express as the flattering one:
|
||||
/// without it, a caller with a partial reading reaches for
|
||||
/// [`Energy::measured`] and the figure silently claims the whole task.
|
||||
pub fn measured_partial(uj: u64) -> Self {
|
||||
Energy {
|
||||
measured_uj: Some(uj),
|
||||
estimated_uj: uj,
|
||||
provenance: Provenance::HwShunt,
|
||||
coverage: Coverage::Partial,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this figure accounts for the whole unit of work.
|
||||
pub fn is_whole(&self) -> bool {
|
||||
self.coverage.is_whole()
|
||||
}
|
||||
|
||||
/// The figure to debit a budget by: the hardware reading if present, else the estimate.
|
||||
pub fn uj(&self) -> u64 {
|
||||
self.measured_uj.unwrap_or(self.estimated_uj)
|
||||
|
|
@ -609,10 +676,55 @@ mod tests {
|
|||
assert!(Energy::modeled(10).check_provenance().is_ok());
|
||||
assert!(Energy::measured(10).check_provenance().is_ok());
|
||||
assert!(Energy::estimator(10).check_provenance().is_ok());
|
||||
let bad = Energy { measured_uj: None, estimated_uj: 1, provenance: Provenance::HwShunt };
|
||||
let bad = Energy {
|
||||
measured_uj: None,
|
||||
estimated_uj: 1,
|
||||
provenance: Provenance::HwShunt,
|
||||
coverage: Coverage::Whole,
|
||||
};
|
||||
assert!(bad.check_provenance().is_err());
|
||||
}
|
||||
|
||||
/// Provenance and coverage answer different questions, and a figure that
|
||||
/// conflates them overstates itself.
|
||||
#[test]
|
||||
fn coverage_is_separate_from_provenance() {
|
||||
// The flattering case the constructor made easy: a hardware reading
|
||||
// that silently claims the whole task.
|
||||
let whole = Energy::measured(10);
|
||||
assert_eq!(whole.provenance, Provenance::HwShunt);
|
||||
assert!(whole.is_whole());
|
||||
|
||||
// The honest case, now equally easy to express. Same provenance — the
|
||||
// counter is just as real — but it accounts for part of the work.
|
||||
let part = Energy::measured_partial(10);
|
||||
assert_eq!(part.provenance, Provenance::HwShunt, "a partial reading is still a sensor reading");
|
||||
assert!(!part.is_whole());
|
||||
assert_eq!(part.coverage, Coverage::Partial);
|
||||
|
||||
// Partial is orthogonal to the provenance coupling, so it must not
|
||||
// disturb it.
|
||||
part.check_provenance().expect("HwShunt + measured is still coherent");
|
||||
}
|
||||
|
||||
/// Absence is the default and is omitted from the wire, so every receipt
|
||||
/// and vector sealed before this field existed is unchanged.
|
||||
#[test]
|
||||
fn whole_coverage_is_omitted_from_the_wire() {
|
||||
let whole = serde_json::to_string(&Energy::measured(10)).unwrap();
|
||||
assert!(!whole.contains("coverage"), "default coverage must not appear: {whole}");
|
||||
|
||||
let part = serde_json::to_string(&Energy::measured_partial(10)).unwrap();
|
||||
assert!(part.contains("Partial"), "a partial figure must say so: {part}");
|
||||
|
||||
// And a payload written before the field existed still parses, as Whole.
|
||||
let legacy: Energy =
|
||||
serde_json::from_str(r#"{"measured_uj":10,"estimated_uj":10,"provenance":"HwShunt"}"#)
|
||||
.expect("legacy payload must still parse");
|
||||
assert!(legacy.is_whole());
|
||||
assert_eq!(legacy, Energy::measured(10));
|
||||
}
|
||||
|
||||
fn grant_without_funds() -> Grant {
|
||||
let r = ContentId::of_bytes(b"db");
|
||||
Grant {
|
||||
|
|
|
|||
Loading…
Reference in a new issue