//! 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, /// 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 { 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, 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, 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))); } }