homestar_runtime/
receipt.rs

1//! Runtime, extended representation of a [Receipt] for [Invocation]s and storage.
2//!
3//! [Receipt]: homestar_invocation::Receipt
4//! [Invocation]: homestar_invocation::Invocation
5
6use anyhow::anyhow;
7use diesel::{
8    backend::Backend,
9    deserialize::{self, FromSql},
10    serialize::{self, IsNull, Output, ToSql},
11    sql_types::Binary,
12    sqlite::Sqlite,
13    AsExpression, FromSqlRow, Identifiable, Insertable, Queryable, Selectable,
14};
15use homestar_invocation::{
16    authority::{Issuer, UcanPrf},
17    consts,
18    ipld::{DagCborRef, DagJson},
19    task, Pointer, Receipt as InvocationReceipt,
20};
21use homestar_wasm::io::Arg;
22use libipld::{cbor::DagCborCodec, cid::Cid, prelude::Codec, serde::from_ipld, Ipld};
23use semver::Version;
24use serde::{Deserialize, Serialize};
25use std::{collections::BTreeMap, fmt};
26
27pub(crate) mod metadata;
28
29/// General version key for receipts.
30pub const VERSION_KEY: &str = "version";
31/// [Receipt] header tag, for sharing over libp2p.
32pub const RECEIPT_TAG: &str = "ipvm/receipt";
33
34const CID_KEY: &str = "cid";
35const INSTRUCTION_KEY: &str = "instruction";
36const RAN_KEY: &str = "ran";
37const OUT_KEY: &str = "out";
38const ISSUER_KEY: &str = "iss";
39const METADATA_KEY: &str = "meta";
40const PROOF_KEY: &str = "prf";
41
42/// Receipt for [Invocation], including it's own Cid and a Cid for an [Instruction].
43///
44/// See [homestar_invocation::Receipt] for more info on some internal
45/// fields.
46///
47/// [Invocation]: homestar_invocation::Invocation
48/// [Instruction]: homestar_invocation::task::Instruction
49#[derive(Debug, Clone, PartialEq, Queryable, Insertable, Identifiable, Selectable)]
50#[diesel(table_name = crate::db::schema::receipts, primary_key(cid))]
51pub struct Receipt {
52    cid: Pointer,
53    ran: Pointer,
54    instruction: Pointer,
55    out: task::Result<Ipld>,
56    meta: LocalIpld,
57    issuer: Option<Issuer>,
58    prf: UcanPrf,
59    version: String,
60}
61
62impl fmt::Display for Receipt {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        write!(
65            f,
66            "Receipt: [cid: {}, instruction: {}, ran: {}, output: {:?}, metadata: {:?}, issuer: {:?}, version: {}]",
67            self.cid, self.instruction, self.ran, self.out, self.meta.0, self.issuer, self.version,
68        )
69    }
70}
71
72impl Receipt {
73    /// Generate a receipt.
74    pub fn new(
75        cid: Cid,
76        instruction: Pointer,
77        invocation_receipt: &InvocationReceipt<Ipld>,
78    ) -> Self {
79        Self {
80            cid: Pointer::new(cid),
81            ran: invocation_receipt.ran().to_owned(),
82            instruction,
83            out: invocation_receipt.out().to_owned(),
84            meta: LocalIpld(invocation_receipt.meta().to_owned()),
85            issuer: invocation_receipt.issuer().to_owned(),
86            prf: invocation_receipt.prf().to_owned(),
87            version: consts::INVOCATION_VERSION.to_string(),
88        }
89    }
90
91    /// Return a runtime [Receipt] given an [Instruction] [Pointer] and
92    /// [UCAN Invocation Receipt].
93    ///
94    /// [Instruction]: homestar_invocation::task::Instruction
95    /// [UCAN Invocation Receipt]: homestar_invocation::Receipt
96    pub fn try_with(
97        instruction: Pointer,
98        invocation_receipt: &InvocationReceipt<Ipld>,
99    ) -> anyhow::Result<Self> {
100        let cid = invocation_receipt.to_cid()?;
101        Ok(Receipt::new(cid, instruction, invocation_receipt))
102    }
103
104    /// Capsule-wrapper for [InvocationReceipt] to to be shared over libp2p as
105    /// DagCbor encoded bytes.
106    pub fn invocation_capsule(
107        invocation_receipt: &InvocationReceipt<Ipld>,
108    ) -> anyhow::Result<Vec<u8>> {
109        let receipt_ipld = Ipld::from(invocation_receipt);
110        let capsule = if let Ipld::Map(mut map) = receipt_ipld {
111            map.insert(VERSION_KEY.into(), consts::INVOCATION_VERSION.into());
112            Ok(Ipld::Map(BTreeMap::from([(
113                RECEIPT_TAG.into(),
114                Ipld::Map(map),
115            )])))
116        } else {
117            Err(anyhow!("receipt to Ipld conversion is not a map"))
118        }?;
119
120        DagCborCodec.encode(&capsule)
121    }
122
123    /// Get Ipld metadata on a [Receipt].
124    pub fn meta(&self) -> &Ipld {
125        self.meta.inner()
126    }
127
128    /// Set Ipld metadata on a [Receipt].
129    pub fn set_meta(&mut self, meta: Ipld) {
130        self.meta = LocalIpld(meta)
131    }
132
133    /// Get unique identifier of receipt.
134    pub fn cid(&self) -> Cid {
135        self.cid.cid()
136    }
137
138    /// Get unique identifier of receipt as a [String].
139    pub fn cid_as_string(&self) -> String {
140        self.cid().to_string()
141    }
142
143    /// Get inner Cid as bytes.
144    pub fn cid_as_bytes(&self) -> Vec<u8> {
145        self.cid().to_bytes()
146    }
147
148    /// Return the Pointer-wrapped Cid of the [Receipt]'s associated [Instruction].
149    ///
150    /// [Instruction]: homestar_invocation::task::Instruction
151    pub fn instruction(&self) -> &Pointer {
152        &self.instruction
153    }
154
155    /// Get instruction [Pointer] inner Cid as bytes.
156    pub fn instruction_cid_as_bytes(&self) -> Vec<u8> {
157        self.instruction.cid().to_bytes()
158    }
159
160    /// Get Cid in [Receipt] as a [String].
161    pub fn ran(&self) -> String {
162        self.ran.to_string()
163    }
164
165    /// Get executed result/value in [Receipt] as Ipld.
166    pub fn output(&self) -> &task::Result<Ipld> {
167        &self.out
168    }
169
170    /// Return [task::Result] output as [Arg] for execution.
171    pub fn output_as_arg(&self) -> task::Result<Arg> {
172        match self.out.to_owned() {
173            task::Result::Ok(res) => task::Result::Ok(res.into()),
174            task::Result::Error(res) => task::Result::Error(res.into()),
175            task::Result::Just(res) => task::Result::Just(res.into()),
176        }
177    }
178
179    /// Get executed result/value in [Receipt] as encoded Cbor.
180    pub fn output_encoded(&self) -> anyhow::Result<Vec<u8>> {
181        let ipld = Ipld::from(self.out.to_owned());
182        DagCborCodec.encode(&ipld)
183    }
184
185    /// Return semver [Version] of [Receipt].
186    pub fn version(&self) -> Result<Version, semver::Error> {
187        Version::parse(&self.version)
188    }
189}
190
191impl TryFrom<Receipt> for Vec<u8> {
192    type Error = anyhow::Error;
193
194    fn try_from(receipt: Receipt) -> Result<Self, Self::Error> {
195        let receipt_ipld = Ipld::from(receipt);
196        DagCborCodec.encode(&receipt_ipld)
197    }
198}
199
200impl TryFrom<Vec<u8>> for Receipt {
201    type Error = anyhow::Error;
202
203    fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
204        let ipld: Ipld = DagCborCodec.decode(&bytes)?;
205        ipld.try_into()
206    }
207}
208
209impl From<Receipt> for InvocationReceipt<Ipld> {
210    fn from(receipt: Receipt) -> Self {
211        InvocationReceipt::new(
212            receipt.ran,
213            receipt.out,
214            receipt.meta.0,
215            receipt.issuer,
216            receipt.prf,
217        )
218    }
219}
220
221impl From<&Receipt> for InvocationReceipt<Ipld> {
222    fn from(receipt: &Receipt) -> Self {
223        InvocationReceipt::new(
224            receipt.ran.clone(),
225            receipt.out.clone(),
226            receipt.meta.0.clone(),
227            receipt.issuer.clone(),
228            receipt.prf.clone(),
229        )
230    }
231}
232
233impl From<Receipt> for Ipld {
234    fn from(receipt: Receipt) -> Self {
235        Ipld::Map(BTreeMap::from([
236            (CID_KEY.into(), receipt.cid.into()),
237            (RAN_KEY.into(), receipt.ran.into()),
238            (INSTRUCTION_KEY.into(), receipt.instruction.into()),
239            (OUT_KEY.into(), receipt.out.into()),
240            (METADATA_KEY.into(), receipt.meta.0),
241            (
242                ISSUER_KEY.into(),
243                receipt
244                    .issuer
245                    .map(|issuer| issuer.to_string().into())
246                    .unwrap_or(Ipld::Null),
247            ),
248            (PROOF_KEY.into(), receipt.prf.into()),
249            (VERSION_KEY.into(), receipt.version.into()),
250        ]))
251    }
252}
253
254impl TryFrom<Ipld> for Receipt {
255    type Error = anyhow::Error;
256
257    fn try_from(ipld: Ipld) -> Result<Self, Self::Error> {
258        let map = from_ipld::<BTreeMap<String, Ipld>>(ipld)?;
259        let cid = from_ipld(
260            map.get(CID_KEY)
261                .ok_or_else(|| anyhow!("missing {CID_KEY}"))?
262                .to_owned(),
263        )?;
264        let ran = map
265            .get(RAN_KEY)
266            .ok_or_else(|| anyhow!("missing {RAN_KEY}"))?
267            .try_into()?;
268        let instruction = map
269            .get(INSTRUCTION_KEY)
270            .ok_or_else(|| anyhow!("missing {INSTRUCTION_KEY}"))?
271            .try_into()?;
272        let out = map
273            .get(OUT_KEY)
274            .ok_or_else(|| anyhow!("missing {OUT_KEY}"))?;
275        let meta = map
276            .get(METADATA_KEY)
277            .ok_or_else(|| anyhow!("missing {METADATA_KEY}"))?;
278        let issuer = map
279            .get(ISSUER_KEY)
280            .and_then(|ipld| match ipld {
281                Ipld::Null => None,
282                ipld => Some(ipld),
283            })
284            .and_then(|ipld| from_ipld(ipld.to_owned()).ok())
285            .map(Issuer::new);
286        let prf = map
287            .get(PROOF_KEY)
288            .ok_or_else(|| anyhow!("missing {PROOF_KEY}"))?;
289        let version = from_ipld(
290            map.get(VERSION_KEY)
291                .ok_or_else(|| anyhow!("missing {VERSION_KEY}"))?
292                .to_owned(),
293        )?;
294
295        Ok(Receipt {
296            cid: Pointer::new(cid),
297            ran,
298            instruction,
299            out: task::Result::try_from(out)?,
300            meta: LocalIpld(meta.to_owned()),
301            issuer,
302            prf: UcanPrf::try_from(prf)?,
303            version,
304        })
305    }
306}
307
308impl DagJson for Receipt where Ipld: From<Receipt> {}
309
310/// Wrapper-type for Ipld in order integrate to/from for local storage/db.
311#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, AsExpression, FromSqlRow)]
312#[diesel(sql_type = Binary)]
313pub struct LocalIpld(Ipld);
314
315impl LocalIpld {
316    /// Convert into owned, inner Ipld.
317    pub fn into_inner(self) -> Ipld {
318        self.0
319    }
320
321    /// Convert into referenced, inner Ipld.
322    pub fn inner(&self) -> &Ipld {
323        &self.0
324    }
325}
326
327impl ToSql<Binary, Sqlite> for LocalIpld
328where
329    [u8]: ToSql<Binary, Sqlite>,
330{
331    fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
332        out.set_value(DagCborCodec.encode(&self.0)?);
333        Ok(IsNull::No)
334    }
335}
336
337impl<DB> FromSql<Binary, DB> for LocalIpld
338where
339    DB: Backend,
340    *const [u8]: FromSql<Binary, DB>,
341{
342    fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result<Self> {
343        let raw_bytes = <*const [u8] as FromSql<Binary, DB>>::from_sql(bytes)?;
344        let raw_bytes: &[u8] = unsafe { &*raw_bytes };
345        let decoded = DagCborCodec.decode(raw_bytes)?;
346        Ok(LocalIpld(decoded))
347    }
348}
349
350#[cfg(test)]
351mod test {
352    use super::*;
353    use crate::{
354        db::{schema, Database},
355        settings::Settings,
356        test_utils::{self, db::MemoryDb},
357    };
358    use diesel::prelude::*;
359
360    #[test]
361    fn invocation_into_receipt() {
362        let (invocation, receipt) = test_utils::receipt::receipts();
363        assert_eq!(invocation.ran().to_string(), receipt.ran());
364        assert_eq!(invocation.out(), receipt.output());
365        assert_eq!(invocation.meta(), &receipt.meta.0);
366        assert_eq!(invocation.issuer(), &receipt.issuer);
367        assert_eq!(invocation.prf(), &receipt.prf);
368        assert_eq!(invocation.to_cid().unwrap(), receipt.cid());
369
370        let output_bytes = DagCborCodec
371            .encode::<Ipld>(&invocation.out().clone().into())
372            .unwrap();
373        assert_eq!(output_bytes, receipt.output_encoded().unwrap());
374
375        let receipt_from_invocation =
376            Receipt::try_with(receipt.instruction.clone(), &invocation).unwrap();
377        assert_eq!(receipt_from_invocation, receipt);
378
379        let invocation_from_receipt = InvocationReceipt::try_from(receipt).unwrap();
380        assert_eq!(invocation_from_receipt, invocation);
381    }
382
383    #[test]
384    fn receipt_sql_roundtrip() {
385        let mut conn = MemoryDb::setup_connection_pool(Settings::load().unwrap().node(), None)
386            .unwrap()
387            .conn()
388            .unwrap();
389        let (_, receipt) = test_utils::receipt::receipts();
390
391        let rows_inserted = diesel::insert_into(schema::receipts::table)
392            .values(&receipt)
393            .execute(&mut conn)
394            .unwrap();
395
396        assert_eq!(1, rows_inserted);
397        let inserted_receipt = schema::receipts::table.load::<Receipt>(&mut conn).unwrap();
398        assert_eq!(vec![receipt.clone()], inserted_receipt);
399    }
400
401    #[test]
402    fn receipt_to_json() {
403        let (_, receipt) = test_utils::receipt::receipts();
404        assert_eq!(
405            receipt.to_json_string().unwrap(),
406            format!(
407                r#"{{"cid":{{"/":"{}"}},"instruction":{{"/":"{}"}},"iss":null,"meta":null,"out":["ok",true],"prf":[],"ran":{{"/":"{}"}},"version":"{}"}}"#,
408                receipt.cid(),
409                receipt.instruction(),
410                receipt.ran(),
411                consts::INVOCATION_VERSION
412            )
413        );
414    }
415
416    #[test]
417    fn receipt_bytes_roundtrip() {
418        let (_, receipt) = test_utils::receipt::receipts();
419        let bytes: Vec<u8> = receipt.clone().try_into().unwrap();
420        let from_bytes = Receipt::try_from(bytes).unwrap();
421
422        assert_eq!(receipt, from_bytes);
423    }
424}