charms_data/
lib.rs

1use anyhow::{anyhow, ensure, Result};
2use ark_std::{
3    cmp::Ordering,
4    collections::BTreeMap,
5    format,
6    string::{String, ToString},
7    vec::Vec,
8};
9use ciborium::Value;
10use core::{convert::TryInto, fmt};
11use serde::{
12    de,
13    de::{DeserializeOwned, SeqAccess, Visitor},
14    ser::SerializeTuple,
15    Deserialize, Deserializer, Serialize, Serializer,
16};
17pub mod util;
18
19/// Macro to check a condition and return false (early) if it does not hold.
20/// This is useful for checking pre-requisite conditions in predicate-type functions.
21/// Inspired by the `ensure!` macro from the `anyhow` crate.
22/// The function must return a boolean.
23/// Example:
24/// ```rust
25/// use charms_data::check;
26///
27/// fn b_is_multiple_of_a(a: u32, b: u32) -> bool {
28///     check!(a <= b && a != 0);    // returns false early if `a` is greater than `b` or `a` is zero
29///     match b % a {
30///         0 => true,
31///         _ => false,
32///     }
33/// }
34#[macro_export]
35macro_rules! check {
36    ($condition:expr) => {
37        if !$condition {
38            eprintln!("condition does not hold: {}", stringify!($condition));
39            return false;
40        }
41    };
42}
43
44/// Represents a transaction involving Charms.
45/// A Charms transaction sits on top of a Bitcoin transaction. Therefore, it transforms a set of
46/// input UTXOs into a set of output UTXOs.
47/// A Charms transaction may also reference other valid UTXOs that are not being spent or created.
48#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
49pub struct Transaction {
50    /// Input UTXOs.
51    pub ins: BTreeMap<UtxoId, Charms>,
52    /// Reference UTXOs.
53    pub refs: BTreeMap<UtxoId, Charms>,
54    /// Output charms.
55    pub outs: Vec<Charms>,
56}
57
58/// Charms are tokens, NFTs or instances of arbitrary app state.
59/// This type alias represents a collection of charms.
60/// Structurally it is a map of `app -> data`.
61pub type Charms = BTreeMap<App, Data>;
62
63/// ID of a UTXO (Unspent Transaction Output) in the underlying ledger system (e.g. Bitcoin).
64/// A UTXO ID is a pair of `(transaction ID, index of the output)`.
65#[cfg_attr(test, derive(test_strategy::Arbitrary))]
66#[derive(Clone, Default, Eq, Ord, PartialEq, PartialOrd)]
67pub struct UtxoId(pub TxId, pub u32);
68
69impl UtxoId {
70    /// Convert to a byte array (of 36 bytes).
71    pub fn to_bytes(&self) -> [u8; 36] {
72        let mut bytes = [0u8; 36];
73        bytes[..32].copy_from_slice(&self.0 .0); // Copy TxId
74        bytes[32..].copy_from_slice(&self.1.to_le_bytes()); // Copy index as little-endian
75        bytes
76    }
77
78    /// Create `UtxoId` from a byte array (of 36 bytes).
79    pub fn from_bytes(bytes: [u8; 36]) -> Self {
80        let mut txid_bytes = [0u8; 32];
81        txid_bytes.copy_from_slice(&bytes[..32]);
82        let index = u32::from_le_bytes(bytes[32..].try_into().unwrap());
83        UtxoId(TxId(txid_bytes), index)
84    }
85
86    /// Try to create `UtxoId` from a string in the format `txid_hex:index`.
87    /// Example:
88    /// ```
89    /// use charms_data::UtxoId;
90    /// let utxo_id = UtxoId::from_str("92077a14998b31367efeec5203a00f1080facdb270cbf055f09b66ae0a273c7d:3").unwrap();
91    /// ```
92    pub fn from_str(s: &str) -> Result<Self> {
93        let parts: Vec<&str> = s.split(':').collect();
94        if parts.len() != 2 {
95            return Err(anyhow!("expected format: txid_hex:index"));
96        }
97
98        let txid = TxId::from_str(parts[0])?;
99
100        let index = parts[1]
101            .parse::<u32>()
102            .map_err(|e| anyhow!("invalid index: {}", e))?;
103
104        Ok(UtxoId(txid, index))
105    }
106
107    fn to_string_internal(&self) -> String {
108        format!("{}:{}", self.0.to_string(), self.1)
109    }
110}
111
112impl fmt::Display for UtxoId {
113    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
114        self.to_string_internal().fmt(f)
115    }
116}
117
118impl fmt::Debug for UtxoId {
119    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
120        write!(f, "UtxoId({})", self.to_string_internal())
121    }
122}
123
124impl Serialize for UtxoId {
125    fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
126    where
127        S: Serializer,
128    {
129        if serializer.is_human_readable() {
130            serializer.serialize_str(&self.to_string())
131        } else {
132            serializer.serialize_bytes(self.to_bytes().as_ref())
133        }
134    }
135}
136
137impl<'de> Deserialize<'de> for UtxoId {
138    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
139    where
140        D: Deserializer<'de>,
141    {
142        struct UtxoIdVisitor;
143
144        impl<'de> Visitor<'de> for UtxoIdVisitor {
145            type Value = UtxoId;
146
147            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
148                formatter.write_str("a string in format 'txid_hex:index' or a tuple (TxId, u32)")
149            }
150
151            // Handle human-readable format ("txid_hex:index")
152            fn visit_str<E>(self, value: &str) -> Result<UtxoId, E>
153            where
154                E: de::Error,
155            {
156                UtxoId::from_str(value).map_err(E::custom)
157            }
158
159            // Handle non-human-readable byte format [u8; 36]
160            fn visit_bytes<E>(self, v: &[u8]) -> core::result::Result<Self::Value, E>
161            where
162                E: de::Error,
163            {
164                Ok(UtxoId::from_bytes(v.try_into().map_err(|e| {
165                    E::custom(format!("invalid utxo_id bytes: {}", e))
166                })?))
167            }
168        }
169
170        if deserializer.is_human_readable() {
171            deserializer.deserialize_str(UtxoIdVisitor)
172        } else {
173            deserializer.deserialize_bytes(UtxoIdVisitor)
174        }
175    }
176}
177
178/// App represents an application that can be used to create, transform or destroy charms (tokens,
179/// NFTs and other instances of app data).
180///
181/// An app is identified by a single character `tag`, a 32-byte `identity` and a 32-byte `vk`
182/// (verification key).
183/// The `tag` is a single character that represents the type of the app, with two special values:
184/// - `TOKEN` (tag `t`) for tokens,
185/// - `NFT` (tag `n`) for NFTs.
186///
187/// Other values of `tag` are perfectly legal. The above ones are special: tokens and NFTs can be
188/// transferred without providing the app's implementation (RISC-V binary).
189///
190/// The `vk` is a 32-byte byte string (hash) that is used to verify proofs that the app's contract
191/// is satisfied (against the certain transaction, additional public input and private input).
192///
193/// The `identity` is a 32-byte byte string (hash) that uniquely identifies the app among other apps
194/// implemented using the same code.
195#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
196#[derive(Clone, Default, Eq, Ord, PartialEq, PartialOrd)]
197pub struct App {
198    pub tag: char,
199    pub identity: B32,
200    pub vk: B32,
201}
202
203impl fmt::Display for App {
204    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
205        write!(f, "{}/{}/{}", self.tag, self.identity, self.vk)
206    }
207}
208
209impl fmt::Debug for App {
210    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
211        write!(f, "App({}/{}/{})", self.tag, self.identity, self.vk)
212    }
213}
214
215impl Serialize for App {
216    fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
217    where
218        S: Serializer,
219    {
220        if serializer.is_human_readable() {
221            serializer.serialize_str(&self.to_string())
222        } else {
223            let mut s = serializer.serialize_tuple(3)?;
224            s.serialize_element(&self.tag)?;
225            s.serialize_element(&self.identity)?;
226            s.serialize_element(&self.vk)?;
227            s.end()
228        }
229    }
230}
231
232impl<'de> Deserialize<'de> for App {
233    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
234    where
235        D: Deserializer<'de>,
236    {
237        struct AppVisitor;
238
239        impl<'de> Visitor<'de> for AppVisitor {
240            type Value = App;
241
242            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
243                formatter.write_str("a string in format 'tag_char/identity_hex/vk_hex' or a struct with tag, identity and vk fields")
244            }
245
246            // Handle human-readable format ("tag_char/identity_hex/vk_hex")
247            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
248            where
249                E: de::Error,
250            {
251                // Split the string at '/'
252                let parts: Vec<&str> = value.split('/').collect();
253                if parts.len() != 3 {
254                    return Err(E::custom("expected format: tag_char/identity_hex/vk_hex"));
255                }
256
257                // Decode the hex strings
258                let tag: char = {
259                    let mut chars = parts[0].chars();
260                    let Some(tag) = chars.next() else {
261                        return Err(E::custom("expected tag"));
262                    };
263                    let None = chars.next() else {
264                        return Err(E::custom("tag must be a single character"));
265                    };
266                    tag
267                };
268
269                let identity = B32::from_str(parts[1]).map_err(E::custom)?;
270
271                let vk = B32::from_str(parts[2]).map_err(E::custom)?;
272
273                Ok(App { tag, identity, vk })
274            }
275
276            fn visit_seq<A>(self, mut seq: A) -> core::result::Result<Self::Value, A::Error>
277            where
278                A: SeqAccess<'de>,
279            {
280                let tag = seq
281                    .next_element()?
282                    .ok_or_else(|| de::Error::missing_field("tag"))?;
283                let id = seq
284                    .next_element()?
285                    .ok_or_else(|| de::Error::missing_field("id"))?;
286                let vk_hash = seq
287                    .next_element()?
288                    .ok_or_else(|| de::Error::missing_field("vk_hash"))?;
289
290                Ok(App {
291                    tag,
292                    identity: id,
293                    vk: vk_hash,
294                })
295            }
296        }
297
298        if deserializer.is_human_readable() {
299            deserializer.deserialize_str(AppVisitor)
300        } else {
301            deserializer.deserialize_tuple(3, AppVisitor)
302        }
303    }
304}
305
306/// ID (hash) of a transaction in the underlying ledger (Bitcoin).
307#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
308#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
309pub struct TxId(pub [u8; 32]);
310
311impl TxId {
312    /// Try to create `TxId` from a string of 64 hex characters.
313    /// Note that string representation of transaction IDs in Bitcoin is reversed, and so is ours
314    /// (for compatibility).
315    ///
316    /// Example:
317    /// ```
318    /// use charms_data::TxId;
319    /// let tx_id = TxId::from_str("92077a14998b31367efeec5203a00f1080facdb270cbf055f09b66ae0a273c7d").unwrap();
320    /// ```
321    pub fn from_str(s: &str) -> Result<Self> {
322        ensure!(s.len() == 64, "expected 64 hex characters");
323        let bytes = hex::decode(s).map_err(|e| anyhow!("invalid txid hex: {}", e))?;
324        let mut txid: [u8; 32] = bytes.try_into().unwrap();
325        txid.reverse();
326        Ok(TxId(txid))
327    }
328
329    fn to_string_internal(&self) -> String {
330        let mut txid = self.0;
331        txid.reverse();
332        hex::encode(&txid)
333    }
334}
335
336impl fmt::Display for TxId {
337    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
338        self.to_string_internal().fmt(f)
339    }
340}
341
342impl fmt::Debug for TxId {
343    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
344        write!(f, "TxId({})", self.to_string_internal())
345    }
346}
347
348impl Serialize for TxId {
349    fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
350    where
351        S: Serializer,
352    {
353        if serializer.is_human_readable() {
354            serializer.serialize_str(&self.to_string())
355        } else {
356            serializer.serialize_bytes(&self.0)
357        }
358    }
359}
360
361impl<'de> Deserialize<'de> for TxId {
362    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
363    where
364        D: Deserializer<'de>,
365    {
366        struct TxIdVisitor;
367
368        impl<'de> Visitor<'de> for TxIdVisitor {
369            type Value = TxId;
370
371            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
372                formatter.write_str("a string of 64 hex characters or a byte array of 32 bytes")
373            }
374
375            // Handle human-readable format ("txid_hex")
376            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
377            where
378                E: de::Error,
379            {
380                TxId::from_str(value).map_err(E::custom)
381            }
382
383            // Handle non-human-readable byte format [u8; 32]
384            fn visit_bytes<E>(self, v: &[u8]) -> core::result::Result<Self::Value, E>
385            where
386                E: de::Error,
387            {
388                Ok(TxId(v.try_into().map_err(|e| {
389                    E::custom(format!("invalid txid bytes: {}", e))
390                })?))
391            }
392        }
393
394        if deserializer.is_human_readable() {
395            deserializer.deserialize_str(TxIdVisitor)
396        } else {
397            deserializer.deserialize_bytes(TxIdVisitor)
398        }
399    }
400}
401
402/// 32-byte byte string (e.g. a hash, like SHA256).
403#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
404#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Default, Serialize, Deserialize)]
405pub struct B32(pub [u8; 32]);
406
407impl B32 {
408    /// Try to create `B32` from a string of 64 hex characters.
409    pub fn from_str(s: &str) -> Result<Self> {
410        ensure!(s.len() == 64, "expected 64 hex characters");
411        let bytes = hex::decode(s).map_err(|e| anyhow!("invalid hex: {}", e))?;
412        let hash: [u8; 32] = bytes.try_into().unwrap();
413        Ok(B32(hash))
414    }
415}
416
417impl AsRef<[u8]> for B32 {
418    fn as_ref(&self) -> &[u8] {
419        &self.0
420    }
421}
422
423impl fmt::Display for B32 {
424    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
425        hex::encode(&self.0).fmt(f)
426    }
427}
428
429impl fmt::Debug for B32 {
430    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
431        write!(f, "VkHash({})", hex::encode(&self.0))
432    }
433}
434
435/// Represents a data value that is guaranteed to be serialized/deserialized to/from CBOR.
436#[derive(Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
437pub struct Data(Value);
438
439impl Eq for Data {}
440
441impl Ord for Data {
442    fn cmp(&self, other: &Self) -> Ordering {
443        self.0
444            .partial_cmp(&other.0)
445            .expect("Value comparison should have succeeded")
446    }
447}
448
449impl Data {
450    /// Create an empty data value.
451    pub fn empty() -> Self {
452        Self(Value::Null)
453    }
454
455    /// Check if the data value is empty.
456    pub fn is_empty(&self) -> bool {
457        self.0.is_null()
458    }
459
460    /// Try to cast to a value of a deserializable type (implementing
461    /// `serde::de::DeserializeOwned`).
462    pub fn value<T: DeserializeOwned>(&self) -> Result<T> {
463        self.0
464            .deserialized()
465            .map_err(|e| anyhow!("deserialization error: {}", e))
466    }
467
468    /// Serialize to bytes.
469    pub fn bytes(&self) -> Vec<u8> {
470        util::write(&self).expect("serialization should have succeeded")
471    }
472}
473
474impl<T> From<&T> for Data
475where
476    T: Serialize,
477{
478    fn from(value: &T) -> Self {
479        Self(Value::serialized(value).expect("casting to a CBOR Value should have succeeded"))
480    }
481}
482
483impl Default for Data {
484    fn default() -> Self {
485        Self::empty()
486    }
487}
488
489impl fmt::Debug for Data {
490    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
491        write!(f, "Data({})", format!("{:?}", &self.0))
492    }
493}
494
495/// Special `App.tag` value for fungible tokens. See [`App`] for more details.
496pub const TOKEN: char = 't';
497/// Special `App.tag` value for non-fungible tokens (NFTs). See [`App`] for more details.
498pub const NFT: char = 'n';
499
500/// Check if the transaction is a simple transfer of assets specified by `app`.
501pub fn is_simple_transfer(app: &App, tx: &Transaction) -> bool {
502    match app.tag {
503        TOKEN => token_amounts_balanced(app, tx),
504        NFT => nft_state_preserved(app, tx),
505        _ => false,
506    }
507}
508
509/// Check if the provided app's token amounts are balanced in the transaction. This means that the
510/// sum of the token amounts in the `tx` inputs is equal to the sum of the token amounts in the `tx`
511/// outputs.
512pub fn token_amounts_balanced(app: &App, tx: &Transaction) -> bool {
513    match (
514        sum_token_amount(app, tx.ins.values()),
515        sum_token_amount(app, tx.outs.iter()),
516    ) {
517        (Ok(amount_in), Ok(amount_out)) => amount_in == amount_out,
518        (..) => false,
519    }
520}
521
522/// Check if the NFT states are preserved in the transaction. This means that the NFTs (created by
523/// the provided `app`) in the `tx` inputs are the same as the NFTs in the `tx` outputs.
524pub fn nft_state_preserved(app: &App, tx: &Transaction) -> bool {
525    let nft_states_in = app_state_multiset(app, tx.ins.values());
526    let nft_states_out = app_state_multiset(app, tx.outs.iter());
527
528    nft_states_in == nft_states_out
529}
530
531pub fn app_datas<'a>(
532    app: &'a App,
533    strings_of_charms: impl Iterator<Item = &'a Charms>,
534) -> impl Iterator<Item = &'a Data> {
535    strings_of_charms.filter_map(|charms| charms.get(app))
536}
537
538fn app_state_multiset<'a>(
539    app: &App,
540    strings_of_charms: impl Iterator<Item = &'a Charms>,
541) -> BTreeMap<&'a Data, usize> {
542    strings_of_charms
543        .filter_map(|charms| charms.get(app))
544        .fold(BTreeMap::new(), |mut r, s| {
545            match r.get_mut(&s) {
546                Some(count) => *count += 1,
547                None => {
548                    r.insert(s, 1);
549                }
550            }
551            r
552        })
553}
554
555/// Sum the token amounts in the provided `strings_of_charms`.
556pub fn sum_token_amount<'a>(
557    app: &App,
558    strings_of_charms: impl Iterator<Item = &'a Charms>,
559) -> Result<u64> {
560    ensure!(app.tag == TOKEN);
561    strings_of_charms.fold(Ok(0u64), |amount, charms| match charms.get(app) {
562        Some(state) => Ok(amount? + state.value::<u64>()?),
563        None => amount,
564    })
565}
566
567#[cfg(test)]
568mod tests {
569    use super::*;
570    use ciborium::Value;
571    use proptest::prelude::*;
572    use test_strategy::proptest;
573
574    #[proptest]
575    fn doesnt_crash(s: String) {
576        let _ = TxId::from_str(&s);
577    }
578
579    #[proptest]
580    fn txid_roundtrip(txid: TxId) {
581        let s = txid.to_string();
582        let txid2 = TxId::from_str(&s).unwrap();
583        prop_assert_eq!(txid, txid2);
584    }
585
586    #[proptest]
587    fn vk_serde_roundtrip(vk: B32) {
588        let bytes = util::write(&vk).unwrap();
589        let vk2 = util::read(bytes.as_slice()).unwrap();
590        prop_assert_eq!(vk, vk2);
591    }
592
593    #[proptest]
594    fn app_serde_roundtrip(app: App) {
595        let bytes = util::write(&app).unwrap();
596        let app2 = util::read(bytes.as_slice()).unwrap();
597        prop_assert_eq!(app, app2);
598    }
599
600    #[proptest]
601    fn utxo_id_serde_roundtrip(utxo_id: UtxoId) {
602        let bytes = util::write(&utxo_id).unwrap();
603        let utxo_id2 = util::read(bytes.as_slice()).unwrap();
604        prop_assert_eq!(utxo_id, utxo_id2);
605    }
606
607    #[proptest]
608    fn tx_id_serde_roundtrip(tx_id: TxId) {
609        let bytes = util::write(&tx_id).unwrap();
610        let tx_id2 = util::read(bytes.as_slice()).unwrap();
611        prop_assert_eq!(tx_id, tx_id2);
612    }
613
614    #[test]
615    fn minimal_txid() {
616        let tx_id_bytes: [u8; 32] = [&[1u8], [0u8; 31].as_ref()].concat().try_into().unwrap();
617        let tx_id = TxId(tx_id_bytes);
618        let tx_id_str = tx_id.to_string();
619        let tx_id_str_expected = "0000000000000000000000000000000000000000000000000000000000000001";
620        assert_eq!(tx_id_str, tx_id_str_expected);
621    }
622
623    #[test]
624    fn data_dbg() {
625        let v = 42u64;
626        let data: Data = Data::from(&v);
627        assert_eq!(format!("{:?}", data), format!("Data({:?})", Value::from(v)));
628
629        let data = Data::empty();
630        assert_eq!(format!("{:?}", data), "Data(Null)");
631
632        let vec1: Vec<u64> = vec![];
633        let data: Data = Data::from(&vec1);
634        assert_eq!(format!("{:?}", data), "Data(Array([]))");
635    }
636
637    #[test]
638    fn data_bytes() {
639        let v = ("42u64", 42u64);
640        let data = Data::from(&v);
641        let value = Value::serialized(&v).expect("serialization should have succeeded");
642
643        let buf = util::write(&value).expect("serialization should have succeeded");
644
645        assert_eq!(data.bytes(), buf);
646    }
647
648    #[test]
649    fn dummy() {}
650}