open-standards/map/map-rs/crates/map-receipt/src/lib.rs
Transaction Science 3861cd99fb map-receipt: HwShunt's doc says a vendor counter counts only while live
The doc comment on Provenance::HwShunt still read "RAPL/SMC/shunt", with no
liveness condition. The map spec in the previous commit now says a counter
counts only while it is demonstrably live (receipt.md §2). This brings the
doc comment into line. Doc comment only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-01 01:10:36 -04:00

219 lines
8.7 KiB
Rust

//! The Map receipt (SPEC §6).
//!
//! A [`Receipt`] is the signed, two-part energy record (`measured + estimated`,
//! integer microjoules) emitted for every resolve/decode of a tile. Every
//! reading carries a [`Provenance`] tag; a `measured` figure MUST come from
//! hardware. Receipts seal as **JCR-1** envelopes — the family's shared
//! signed-receipt format (canonical det-CBOR + COSE_Sign1/EdDSA) — so a Map
//! receipt verifies with the same tooling as a Joule Code or Sandbox receipt.
use ed25519_dalek::SigningKey;
use map_schema::{CellKey, ContentId, Tier};
use serde::{Deserialize, Serialize};
pub mod jcr1;
/// Reference estimator: microjoules per resolved tile by cascade tier
/// (non-normative, SPEC §6). The standard is that the figure is reported and
/// honestly tagged — not the constant. Energy rises with fidelity.
pub const UJ_BY_TIER: [(Tier, u64); 5] = [
(Tier::Cache, 1), // ~picojoules: a local store hit
(Tier::Base, 1_000), // ~microjoules: terrain/vector floor
(Tier::Field, 200_000), // ~sub-millijoules: a raster/field tile
(Tier::Feature, 5_000_000), // ~millijoules: vector features + entities
(Tier::Photoreal, 2_000_000_000), // ~joules: a splat tile
];
/// How honest an energy reading is (SPEC §6).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Provenance {
/// Read from a hardware energy interface: a shunt, or a vendor energy
/// counter (RAPL, NVML, Apple SMC/IOReport) while that counter is
/// demonstrably live for the domain the figure claims (see
/// `spec/receipt.md` §2). A counter that is present but not live is not a
/// hardware reading. Requires a present `measured_uj`.
HwShunt,
/// Computed from a calibrated per-operation model. `measured_uj` may be absent.
ModelBased,
/// A coarse constant estimator. `measured_uj` MUST be absent.
Estimator,
}
impl Provenance {
/// The JCR-1 wire label, matching the family's receipt encoding.
pub fn jcr1_label(self) -> &'static str {
match self {
Provenance::HwShunt => "HwShunt",
Provenance::ModelBased => "ModelBased",
Provenance::Estimator => "Estimator",
}
}
}
/// A two-part energy record, all integer microjoules (SPEC §6).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Energy {
/// Present ONLY when obtained from a hardware interface.
pub measured_uj: Option<u64>,
/// Always present.
pub estimated_uj: u64,
pub provenance: Provenance,
}
impl Energy {
/// The estimator of last resort: the reference per-tier constant. Tags the
/// reading `Estimator` and leaves `measured_uj` absent.
pub fn estimate_tier(tier: Tier) -> Self {
let estimated_uj = UJ_BY_TIER
.iter()
.find(|(t, _)| *t == tier)
.map(|(_, uj)| *uj)
.unwrap_or(0);
Energy { measured_uj: None, estimated_uj, provenance: Provenance::Estimator }
}
/// Enforce the provenance ↔ measured coupling (SPEC §6): `HwShunt` requires
/// `measured_uj`; `Estimator` forbids it.
pub fn check_provenance(&self) -> Result<(), ReceiptError> {
match (self.provenance, self.measured_uj) {
(Provenance::HwShunt, None) => Err(ReceiptError::HwShuntRequiresMeasured),
(Provenance::Estimator, Some(_)) => Err(ReceiptError::EstimatorMustNotCarryMeasured),
_ => Ok(()),
}
}
}
/// Errors from building, sealing, or verifying a receipt.
#[derive(Debug, thiserror::Error)]
pub enum ReceiptError {
#[error("HwShunt provenance requires a measured figure")]
HwShuntRequiresMeasured,
#[error("Estimator provenance must not carry a measured figure")]
EstimatorMustNotCarryMeasured,
#[error("receipt signature did not verify")]
BadSignature,
#[error("envelope payload does not match receipt")]
PayloadMismatch,
#[error(transparent)]
Jcr1(#[from] jcr1::Jcr1Error),
}
/// A signed energy receipt for one tile resolve (SPEC §6).
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Receipt {
/// The cell the request was for.
pub cell: CellKey,
/// The content id of the tile that closed the request.
pub content_id: ContentId,
/// The cascade tier that closed the request.
pub tier: Tier,
pub energy: Energy,
pub sequence: u64,
pub nonce: [u8; 8],
}
impl Receipt {
/// Project to JCR-1 wire JSON — the `map/1` profile of the family's receipt
/// format (SPEC §6). The cell key is rendered as its content id so the
/// signed core stays a flat uint/text map. Null fields are dropped.
pub fn to_jcr1_wire(&self, steward: &str) -> Result<serde_json::Value, ReceiptError> {
use serde_json::json;
let cell_id = ContentId::of(&self.cell)
.map_err(|e| jcr1::Jcr1Error::Envelope(e.to_string()))?;
let mut obj = serde_json::Map::new();
obj.insert("map_receipt".into(), json!("map/1"));
obj.insert("cell_id".into(), json!(cell_id.to_hex()));
obj.insert("content_id".into(), json!(self.content_id.to_hex()));
obj.insert("tier".into(), json!(self.tier.wire_tag()));
obj.insert("estimated_uj".into(), json!(self.energy.estimated_uj));
if let Some(m) = self.energy.measured_uj {
obj.insert("measured_uj".into(), json!(m));
}
obj.insert("energy_provenance".into(), json!(self.energy.provenance.jcr1_label()));
obj.insert("sequence".into(), json!(self.sequence));
obj.insert("nonce".into(), json!(hex::encode(self.nonce)));
obj.insert("steward".into(), json!(steward));
Ok(serde_json::Value::Object(obj))
}
/// The canonical signed-core bytes (JCR-1 det-CBOR).
pub fn signed_core(&self, steward: &str) -> Result<Vec<u8>, ReceiptError> {
self.energy.check_provenance()?;
Ok(jcr1::signed_core_bytes(&self.to_jcr1_wire(steward)?)?)
}
/// Seal into a JCR-1 COSE_Sign1 envelope. Validates the provenance coupling
/// first, so a dishonest receipt cannot be sealed.
pub fn seal(&self, key: &SigningKey, steward: &str) -> Result<Vec<u8>, ReceiptError> {
let core = self.signed_core(steward)?;
Ok(jcr1::cose_sign1_envelope(&core, key, steward))
}
/// Verify a JCR-1 envelope against a resolver key and confirm its payload is
/// exactly this receipt's canonical signed core.
pub fn verify_envelope(
&self,
envelope: &[u8],
steward: &str,
endpoint: &[u8; 32],
) -> Result<(), ReceiptError> {
let payload = jcr1::verify_cose_sign1(envelope, steward, endpoint)?;
if payload != self.signed_core(steward)? {
return Err(ReceiptError::PayloadMismatch);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::SigningKey;
use map_schema::{AltBand, Lod, S2CellId, TimeBucket};
fn cell() -> CellKey {
CellKey { s2: S2CellId(0x42), alt: AltBand(0), time: TimeBucket::ATEMPORAL, lod: Lod(0) }
}
fn rcpt(energy: Energy) -> Receipt {
Receipt {
cell: cell(),
content_id: ContentId::of_bytes(b"tile"),
tier: Tier::Photoreal,
energy,
sequence: 1,
nonce: [0, 0, 0, 0, 0, 0, 0, 1],
}
}
#[test]
fn estimate_orders_by_tier() {
assert!(Energy::estimate_tier(Tier::Cache).estimated_uj < Energy::estimate_tier(Tier::Base).estimated_uj);
assert!(Energy::estimate_tier(Tier::Feature).estimated_uj < Energy::estimate_tier(Tier::Photoreal).estimated_uj);
}
#[test]
fn estimator_seal_verify_roundtrips() {
let sk = SigningKey::from_bytes(&[9u8; 32]);
let steward = "did:example:resolver1";
let r = rcpt(Energy::estimate_tier(Tier::Photoreal));
let env = r.seal(&sk, steward).unwrap();
assert!(r.verify_envelope(&env, steward, &sk.verifying_key().to_bytes()).is_ok());
// A different steward must not verify.
assert!(r.verify_envelope(&env, "did:example:other", &sk.verifying_key().to_bytes()).is_err());
}
#[test]
fn hwshunt_without_measured_is_rejected() {
let sk = SigningKey::from_bytes(&[9u8; 32]);
let r = rcpt(Energy { measured_uj: None, estimated_uj: 10, provenance: Provenance::HwShunt });
assert!(matches!(r.seal(&sk, "s"), Err(ReceiptError::HwShuntRequiresMeasured)));
}
#[test]
fn estimator_with_measured_is_rejected() {
let sk = SigningKey::from_bytes(&[9u8; 32]);
let r = rcpt(Energy { measured_uj: Some(10), estimated_uj: 10, provenance: Provenance::Estimator });
assert!(matches!(r.seal(&sk, "s"), Err(ReceiptError::EstimatorMustNotCarryMeasured)));
}
}