charms_data/
lib.rs

1use anyhow::{Result, anyhow, ensure};
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    Deserialize, Deserializer, Serialize, Serializer, de,
13    de::{DeserializeOwned, SeqAccess, Visitor},
14    ser::SerializeTuple,
15};
16pub mod util;
17
18/// Macro to check a condition and return false (early) if it does not hold.
19/// This is useful for checking pre-requisite conditions in predicate-type functions.
20/// Inspired by the `ensure!` macro from the `anyhow` crate.
21/// The function must return a boolean.
22/// Example:
23/// ```rust
24/// use charms_data::check;
25///
26/// fn b_is_multiple_of_a(a: u32, b: u32) -> bool {
27///     check!(a <= b && a != 0);    // returns false early if `a` is greater than `b` or `a` is zero
28///     match b % a {
29///         0 => true,
30///         _ => false,
31///     }
32/// }
33#[macro_export]
34macro_rules! check {
35    ($condition:expr) => {
36        if !$condition {
37            eprintln!("condition does not hold: {}", stringify!($condition));
38            return false;
39        }
40    };
41}
42
43/// Represents a transaction involving Charms.
44/// A Charms transaction sits on top of a Bitcoin transaction. Therefore, it transforms a set of
45/// input UTXOs into a set of output UTXOs.
46/// A Charms transaction may also reference other valid UTXOs that are not being spent or created.
47#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
48pub struct Transaction {
49    /// Input UTXOs.
50    pub ins: Vec<(UtxoId, Charms)>,
51    /// Reference UTXOs.
52    pub refs: Vec<(UtxoId, Charms)>,
53    /// Output charms.
54    pub outs: Vec<Charms>,
55}
56
57/// Charms are tokens, NFTs or instances of arbitrary app state.
58/// This type alias represents a collection of charms.
59/// Structurally it is a map of `app -> data`.
60pub type Charms = BTreeMap<App, Data>;
61
62/// ID of a UTXO (Unspent Transaction Output) in the underlying ledger system (e.g. Bitcoin).
63/// A UTXO ID is a pair of `(transaction ID, index of the output)`.
64#[cfg_attr(test, derive(test_strategy::Arbitrary))]
65#[derive(Clone, Default, Eq, Ord, PartialEq, PartialOrd)]
66pub struct UtxoId(pub TxId, pub u32);
67
68impl UtxoId {
69    /// Convert to a byte array (of 36 bytes).
70    pub fn to_bytes(&self) -> [u8; 36] {
71        let mut bytes = [0u8; 36];
72        bytes[..32].copy_from_slice(&self.0.0); // Copy TxId
73        bytes[32..].copy_from_slice(&self.1.to_le_bytes()); // Copy index as little-endian
74        bytes
75    }
76
77    /// Create `UtxoId` from a byte array (of 36 bytes).
78    pub fn from_bytes(bytes: [u8; 36]) -> Self {
79        let mut txid_bytes = [0u8; 32];
80        txid_bytes.copy_from_slice(&bytes[..32]);
81        let index = u32::from_le_bytes(bytes[32..].try_into().expect("exactly 4 bytes expected"));
82        UtxoId(TxId(txid_bytes), index)
83    }
84
85    /// Try to create `UtxoId` from a string in the format `txid_hex:index`.
86    /// Example:
87    /// ```
88    /// use charms_data::UtxoId;
89    /// let utxo_id = UtxoId::from_str("92077a14998b31367efeec5203a00f1080facdb270cbf055f09b66ae0a273c7d:3").unwrap();
90    /// ```
91    pub fn from_str(s: &str) -> Result<Self> {
92        let parts: Vec<&str> = s.split(':').collect();
93        if parts.len() != 2 {
94            return Err(anyhow!("expected format: txid_hex:index"));
95        }
96
97        let txid = TxId::from_str(parts[0])?;
98
99        let index = parts[1]
100            .parse::<u32>()
101            .map_err(|e| anyhow!("invalid index: {}", e))?;
102
103        Ok(UtxoId(txid, index))
104    }
105
106    fn to_string_internal(&self) -> String {
107        format!("{}:{}", self.0.to_string(), self.1)
108    }
109}
110
111impl fmt::Display for UtxoId {
112    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
113        self.to_string_internal().fmt(f)
114    }
115}
116
117impl fmt::Debug for UtxoId {
118    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
119        write!(f, "UtxoId({})", self.to_string_internal())
120    }
121}
122
123impl Serialize for UtxoId {
124    fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
125    where
126        S: Serializer,
127    {
128        if serializer.is_human_readable() {
129            serializer.serialize_str(&self.to_string())
130        } else {
131            serializer.serialize_bytes(self.to_bytes().as_ref())
132        }
133    }
134}
135
136impl<'de> Deserialize<'de> for UtxoId {
137    fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
138    where
139        D: Deserializer<'de>,
140    {
141        struct UtxoIdVisitor;
142
143        impl<'de> Visitor<'de> for UtxoIdVisitor {
144            type Value = UtxoId;
145
146            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
147                formatter.write_str("a string in format 'txid_hex:index' or a tuple (TxId, u32)")
148            }
149
150            // Handle human-readable format ("txid_hex:index")
151            fn visit_str<E>(self, value: &str) -> Result<UtxoId, E>
152            where
153                E: de::Error,
154            {
155                UtxoId::from_str(value).map_err(E::custom)
156            }
157
158            // Handle non-human-readable byte format [u8; 36]
159            fn visit_bytes<E>(self, v: &[u8]) -> core::result::Result<Self::Value, E>
160            where
161                E: de::Error,
162            {
163                Ok(UtxoId::from_bytes(v.try_into().map_err(|e| {
164                    E::custom(format!("invalid utxo_id bytes: {}", e))
165                })?))
166            }
167        }
168
169        if deserializer.is_human_readable() {
170            deserializer.deserialize_str(UtxoIdVisitor)
171        } else {
172            deserializer.deserialize_bytes(UtxoIdVisitor)
173        }
174    }
175}
176
177/// App represents an application that can be used to create, transform or destroy charms (tokens,
178/// NFTs and other instances of app data).
179///
180/// An app is identified by a single character `tag`, a 32-byte `identity` and a 32-byte `vk`
181/// (verification key).
182/// The `tag` is a single character that represents the type of the app, with two special values:
183/// - `TOKEN` (tag `t`) for tokens,
184/// - `NFT` (tag `n`) for NFTs.
185///
186/// Other values of `tag` are perfectly legal. The above ones are special: tokens and NFTs can be
187/// transferred without providing the app's implementation (RISC-V binary).
188///
189/// The `vk` is a 32-byte byte string (hash) that is used to verify proofs that the app's contract
190/// is satisfied (against the certain transaction, additional public input and private input).
191///
192/// The `identity` is a 32-byte byte string (hash) that uniquely identifies the app among other apps
193/// implemented using the same code.
194#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
195#[derive(Clone, Default, Eq, Ord, PartialEq, PartialOrd)]
196pub struct App {
197    pub tag: char,
198    pub identity: B32,
199    pub vk: B32,
200}
201
202impl fmt::Display for App {
203    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
204        write!(f, "{}/{}/{}", self.tag, self.identity, self.vk)
205    }
206}
207
208impl fmt::Debug for App {
209    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
210        write!(f, "App({}/{}/{})", self.tag, self.identity, self.vk)
211    }
212}
213
214impl Serialize for App {
215    fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
216    where
217        S: Serializer,
218    {
219        if serializer.is_human_readable() {
220            serializer.serialize_str(&self.to_string())
221        } else {
222            let mut s = serializer.serialize_tuple(3)?;
223            s.serialize_element(&self.tag)?;
224            s.serialize_element(&self.identity)?;
225            s.serialize_element(&self.vk)?;
226            s.end()
227        }
228    }
229}
230
231impl<'de> Deserialize<'de> for App {
232    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
233    where
234        D: Deserializer<'de>,
235    {
236        struct AppVisitor;
237
238        impl<'de> Visitor<'de> for AppVisitor {
239            type Value = App;
240
241            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
242                formatter.write_str("a string in format 'tag_char/identity_hex/vk_hex' or a struct with tag, identity and vk fields")
243            }
244
245            // Handle human-readable format ("tag_char/identity_hex/vk_hex")
246            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
247            where
248                E: de::Error,
249            {
250                // Split the string at '/'
251                let mut parts: Vec<&str> = value.split('/').collect();
252                if parts[0].is_empty() && parts[1].is_empty() {
253                    parts.remove(0);
254                    parts[0] = "/";
255                }
256                let parts = parts;
257                if parts.len() != 3 {
258                    return Err(E::custom("expected format: tag_char/identity_hex/vk_hex"));
259                }
260
261                // Decode the hex strings
262                let tag: char = {
263                    let mut chars = parts[0].chars();
264                    let Some(tag) = chars.next() else {
265                        return Err(E::custom("expected tag"));
266                    };
267                    let None = chars.next() else {
268                        return Err(E::custom("tag must be a single character"));
269                    };
270                    tag
271                };
272
273                let identity = B32::from_str(parts[1]).map_err(E::custom)?;
274
275                let vk = B32::from_str(parts[2]).map_err(E::custom)?;
276
277                Ok(App { tag, identity, vk })
278            }
279
280            fn visit_seq<A>(self, mut seq: A) -> core::result::Result<Self::Value, A::Error>
281            where
282                A: SeqAccess<'de>,
283            {
284                let tag = seq
285                    .next_element()?
286                    .ok_or_else(|| de::Error::missing_field("tag"))?;
287                let identity = seq
288                    .next_element()?
289                    .ok_or_else(|| de::Error::missing_field("identity"))?;
290                let vk = seq
291                    .next_element()?
292                    .ok_or_else(|| de::Error::missing_field("vk"))?;
293
294                Ok(App { tag, identity, vk })
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().expect("exactly 32 bytes expected");
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)]
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().expect("exactly 32 bytes expected");
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, "Bytes32({})", hex::encode(&self.0))
432    }
433}
434
435impl Serialize for B32 {
436    fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
437    where
438        S: Serializer,
439    {
440        if serializer.is_human_readable() {
441            serializer.serialize_str(&self.to_string())
442        } else {
443            let mut seq = serializer.serialize_tuple(32)?;
444            for &byte in &self.0 {
445                seq.serialize_element(&byte)?;
446            }
447            seq.end()
448        }
449    }
450}
451
452impl<'de> Deserialize<'de> for B32 {
453    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
454    where
455        D: Deserializer<'de>,
456    {
457        struct B32Visitor;
458
459        impl<'de> Visitor<'de> for B32Visitor {
460            type Value = B32;
461
462            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
463                formatter.write_str("a string of 64 hex characters or a sequence of 32 bytes")
464            }
465
466            // Handle human-readable format ("hex")
467            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
468            where
469                E: de::Error,
470            {
471                B32::from_str(value).map_err(E::custom)
472            }
473
474            // Handle non-human-readable byte format [u8; 32]
475            fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Self::Value, A::Error>
476            where
477                A: SeqAccess<'de>,
478            {
479                let mut bytes = [0u8; 32];
480                for i in 0..32 {
481                    bytes[i] = seq
482                        .next_element()?
483                        .ok_or_else(|| serde::de::Error::invalid_length(i, &"32 elements"))?;
484                }
485
486                // Check if there are extra elements
487                if seq.next_element::<u8>()?.is_some() {
488                    return Err(serde::de::Error::invalid_length(33, &"exactly 32 elements"));
489                }
490
491                Ok(B32(bytes))
492            }
493        }
494
495        if deserializer.is_human_readable() {
496            deserializer.deserialize_str(B32Visitor)
497        } else {
498            deserializer.deserialize_tuple(32, B32Visitor)
499        }
500    }
501}
502
503/// Represents a data value that is guaranteed to be serialized/deserialized to/from CBOR.
504#[derive(Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
505pub struct Data(Value);
506
507impl Eq for Data {}
508
509impl Ord for Data {
510    fn cmp(&self, other: &Self) -> Ordering {
511        self.0
512            .partial_cmp(&other.0)
513            .expect("Value comparison should have succeeded") // PANIC: will panic if CBOR Value comparison returns None (NaN or incomparable). We expect this to never happen (famous last words).
514    }
515}
516
517impl Data {
518    /// Create an empty data value.
519    pub fn empty() -> Self {
520        Self(Value::Null)
521    }
522
523    /// Check if the data value is empty.
524    pub fn is_empty(&self) -> bool {
525        self.0.is_null()
526    }
527
528    /// Try to cast to a value of a deserializable type (implementing
529    /// `serde::de::DeserializeOwned`).
530    pub fn value<T: DeserializeOwned>(&self) -> Result<T> {
531        self.0
532            .deserialized()
533            .map_err(|e| anyhow!("deserialization error: {}", e))
534    }
535
536    /// Serialize to bytes.
537    pub fn bytes(&self) -> Vec<u8> {
538        util::write(&self).expect("serialization is expected to succeed")
539    }
540}
541
542impl<T> From<&T> for Data
543where
544    T: Serialize,
545{
546    fn from(value: &T) -> Self {
547        Self(Value::serialized(value).expect("casting to a CBOR Value is expected to succeed"))
548    }
549}
550
551impl Default for Data {
552    fn default() -> Self {
553        Self::empty()
554    }
555}
556
557impl fmt::Debug for Data {
558    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
559        write!(f, "Data({})", format!("{:?}", &self.0))
560    }
561}
562
563/// Special `App.tag` value for fungible tokens. See [`App`] for more details.
564pub const TOKEN: char = 't';
565/// Special `App.tag` value for non-fungible tokens (NFTs). See [`App`] for more details.
566pub const NFT: char = 'n';
567
568/// Check if the transaction is a simple transfer of assets specified by `app`.
569pub fn is_simple_transfer(app: &App, tx: &Transaction) -> bool {
570    match app.tag {
571        TOKEN => token_amounts_balanced(app, tx),
572        NFT => nft_state_preserved(app, tx),
573        _ => false,
574    }
575}
576
577/// Check if the provided app's token amounts are balanced in the transaction. This means that the
578/// sum of the token amounts in the `tx` inputs is equal to the sum of the token amounts in the `tx`
579/// outputs.
580pub fn token_amounts_balanced(app: &App, tx: &Transaction) -> bool {
581    match (
582        sum_token_amount(app, tx.ins.iter().map(|(_, v)| v)),
583        sum_token_amount(app, tx.outs.iter()),
584    ) {
585        (Ok(amount_in), Ok(amount_out)) => amount_in == amount_out,
586        (..) => false,
587    }
588}
589
590/// Check if the NFT states are preserved in the transaction. This means that the NFTs (created by
591/// the provided `app`) in the `tx` inputs are the same as the NFTs in the `tx` outputs.
592pub fn nft_state_preserved(app: &App, tx: &Transaction) -> bool {
593    let nft_states_in = app_state_multiset(app, tx.ins.iter().map(|(_, v)| v));
594    let nft_states_out = app_state_multiset(app, tx.outs.iter());
595
596    nft_states_in == nft_states_out
597}
598
599/// Deprecated. Use [charm_values] instead.
600#[deprecated(since = "0.7.0", note = "use `charm_values` instead")]
601pub fn app_datas<'a>(
602    app: &'a App,
603    strings_of_charms: impl Iterator<Item = &'a Charms>,
604) -> impl Iterator<Item = &'a Data> {
605    charm_values(app, strings_of_charms)
606}
607
608/// Iterate over all charm values of a given app (charm key) in the given outputs
609/// (strings of charms).
610pub fn charm_values<'a>(
611    app: &'a App,
612    strings_of_charms: impl Iterator<Item = &'a Charms>,
613) -> impl Iterator<Item = &'a Data> {
614    strings_of_charms.filter_map(|charms| charms.get(app))
615}
616
617fn app_state_multiset<'a>(
618    app: &App,
619    strings_of_charms: impl Iterator<Item = &'a Charms>,
620) -> BTreeMap<&'a Data, usize> {
621    strings_of_charms
622        .filter_map(|charms| charms.get(app))
623        .fold(BTreeMap::new(), |mut r, s| {
624            match r.get_mut(&s) {
625                Some(count) => *count += 1,
626                None => {
627                    r.insert(s, 1);
628                }
629            }
630            r
631        })
632}
633
634/// Sum the token amounts in the provided `strings_of_charms`.
635pub fn sum_token_amount<'a>(
636    app: &App,
637    strings_of_charms: impl Iterator<Item = &'a Charms>,
638) -> Result<u64> {
639    ensure!(app.tag == TOKEN);
640    strings_of_charms.fold(Ok(0u64), |amount, charms| match charms.get(app) {
641        Some(state) => Ok(amount? + state.value::<u64>()?),
642        None => amount,
643    })
644}
645
646#[cfg(test)]
647mod tests {
648    use super::*;
649    use ciborium::Value;
650    use proptest::prelude::*;
651    use test_strategy::proptest;
652
653    #[proptest]
654    fn doesnt_crash(s: String) {
655        let _ = TxId::from_str(&s);
656    }
657
658    #[proptest]
659    fn txid_roundtrip(txid: TxId) {
660        let s = txid.to_string();
661        let txid2 = TxId::from_str(&s).unwrap();
662        prop_assert_eq!(txid, txid2);
663    }
664
665    #[proptest]
666    fn vk_serde_roundtrip(vk: B32) {
667        let bytes = util::write(&vk).unwrap();
668        let vk2 = util::read(bytes.as_slice()).unwrap();
669        prop_assert_eq!(vk, vk2);
670    }
671
672    #[proptest]
673    fn vk_serde_json_roundtrip(vk: App) {
674        let json_str = serde_json::to_string(&vk).unwrap();
675        // dbg!(&json_str);
676        let vk2 = serde_json::from_str(&json_str).unwrap();
677        prop_assert_eq!(vk, vk2);
678    }
679
680    #[proptest]
681    fn vk_serde_yaml_roundtrip(vk: App) {
682        let yaml_str = serde_yaml::to_string(&vk).unwrap();
683        // dbg!(&yaml_str);
684        let vk2 = serde_yaml::from_str(&yaml_str).unwrap();
685        prop_assert_eq!(vk, vk2);
686    }
687
688    #[proptest]
689    fn app_serde_roundtrip(app: App) {
690        let bytes = util::write(&app).unwrap();
691        let app2 = util::read(bytes.as_slice()).unwrap();
692        prop_assert_eq!(app, app2);
693    }
694
695    #[proptest]
696    fn utxo_id_serde_roundtrip(utxo_id: UtxoId) {
697        let bytes = util::write(&utxo_id).unwrap();
698        let utxo_id2 = util::read(bytes.as_slice()).unwrap();
699        prop_assert_eq!(utxo_id, utxo_id2);
700    }
701
702    #[proptest]
703    fn tx_id_serde_roundtrip(tx_id: TxId) {
704        let bytes = util::write(&tx_id).unwrap();
705        let tx_id2 = util::read(bytes.as_slice()).unwrap();
706        prop_assert_eq!(tx_id, tx_id2);
707    }
708
709    #[test]
710    fn minimal_txid() {
711        let tx_id_bytes: [u8; 32] = [&[1u8], [0u8; 31].as_ref()].concat().try_into().unwrap();
712        let tx_id = TxId(tx_id_bytes);
713        let tx_id_str = tx_id.to_string();
714        let tx_id_str_expected = "0000000000000000000000000000000000000000000000000000000000000001";
715        assert_eq!(tx_id_str, tx_id_str_expected);
716    }
717
718    #[test]
719    fn data_dbg() {
720        let v = 42u64;
721        let data: Data = Data::from(&v);
722        assert_eq!(format!("{:?}", data), format!("Data({:?})", Value::from(v)));
723
724        let data = Data::empty();
725        assert_eq!(format!("{:?}", data), "Data(Null)");
726
727        let vec1: Vec<u64> = vec![];
728        let data: Data = Data::from(&vec1);
729        assert_eq!(format!("{:?}", data), "Data(Array([]))");
730    }
731
732    #[test]
733    fn data_bytes() {
734        let v = ("42u64", 42u64);
735        let data = Data::from(&v);
736        let value = Value::serialized(&v).expect("serialization should have succeeded");
737
738        let buf = util::write(&value).expect("serialization should have succeeded");
739
740        assert_eq!(data.bytes(), buf);
741    }
742
743    #[test]
744    fn dummy() {}
745}
746
747#[derive(Clone, Debug, Serialize, Deserialize)]
748pub struct AppInput {
749    pub app_binaries: BTreeMap<B32, Vec<u8>>,
750    pub app_public_inputs: BTreeMap<App, Data>,
751    pub app_private_inputs: BTreeMap<App, Data>,
752}