//! Execution receipts (spec §6.1), written by the host after a job ends, //! never by the job itself. use crate::canonical::{object_hash, Hash}; use oxy_core::cbor::{encode, Value}; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Receipt { pub job_name: String, #[serde(with = "hex32")] pub pipeline_hash: Hash, #[serde(with = "hex32")] pub scope_tree_hash: Hash, #[serde(with = "hex32s")] pub inputs: Vec, #[serde(with = "hex32")] pub environment: Hash, #[serde(with = "hex32s")] pub outputs: Vec, pub exit_code: u32, pub duration_ms: u64, } impl Receipt { pub fn encode(&self) -> Vec { let refs = |v: &[Hash]| Value::Array(v.iter().map(|h| Value::Bytes(h.to_vec())).collect()); encode(&Value::map([ (0, Value::Uint(1)), (1, Value::Text(self.job_name.clone())), (2, Value::Bytes(self.pipeline_hash.to_vec())), (3, Value::Bytes(self.scope_tree_hash.to_vec())), (4, refs(&self.inputs)), (5, Value::Bytes(self.environment.to_vec())), (6, refs(&self.outputs)), (7, Value::Uint(self.exit_code as u64)), (8, Value::Uint(self.duration_ms)), ])) } /// `BLAKE3("oxy1 receipt " || len || 0x00 || body)`. pub fn hash(&self) -> Hash { object_hash("receipt", &self.encode()) } /// The same receipt as an Oxygen object (Oxygen spec §14.6), which a /// runner signs and a repository keeps. pub fn to_oxy(&self) -> oxy_core::receipt::Receipt { use oxy_core::hash::ObjectRef; oxy_core::receipt::Receipt { job_name: self.job_name.clone(), pipeline_hash: ObjectRef(self.pipeline_hash), scope_tree_hash: ObjectRef(self.scope_tree_hash), inputs: self.inputs.iter().map(|h| ObjectRef(*h)).collect(), environment: ObjectRef(self.environment), outputs: self.outputs.iter().map(|h| ObjectRef(*h)).collect(), exit_code: self.exit_code, duration_ms: self.duration_ms, } } } /// What a job ran in (the receipt's `environment`): the pinned pieces of /// the isolation tier, the rootfs, argv and env. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Environment { pub runtime: String, /// Hashes of the tier's pinned parts (hypervisor, kernel, init), by name. pub parts: Vec<(String, String)>, pub rootfs: String, pub argv: Vec, pub env: Vec<(String, String)>, } impl Environment { pub fn hash(&self) -> Hash { let t = |s: &str| Value::Text(s.to_string()); let pairs = |v: &[(String, String)]| Value::Array(v.iter().map(|(a, b)| Value::Array(vec![t(a), t(b)])).collect()); let body = encode(&Value::map([ (0, t(&self.runtime)), (1, pairs(&self.parts)), (2, t(&self.rootfs)), (3, Value::Array(self.argv.iter().map(|a| t(a)).collect())), (4, pairs(&self.env)), ])); object_hash("environment", &body) } } mod hex32 { use serde::{Deserialize, Deserializer, Serializer}; pub fn serialize(h: &[u8; 32], s: S) -> Result { s.serialize_str(&hex::encode(h)) } pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[u8; 32], D::Error> { let s = String::deserialize(d)?; let v = hex::decode(&s).map_err(serde::de::Error::custom)?; v.try_into().map_err(|_| serde::de::Error::custom("expected 32 bytes")) } } mod hex32s { use serde::{Deserialize, Deserializer, Serializer}; pub fn serialize(v: &[[u8; 32]], s: S) -> Result { s.collect_seq(v.iter().map(hex::encode)) } pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { let v = Vec::::deserialize(d)?; v.into_iter() .map(|s| { let b = hex::decode(&s).map_err(serde::de::Error::custom)?; b.try_into().map_err(|_| serde::de::Error::custom("expected 32 bytes")) }) .collect() } }