Skip to main content

charms_client/
lib.rs

1use crate::tx::{EnchantedTx, Tx, by_txid, extended_normalized_spell};
2use anyhow::{anyhow, ensure};
3use charms_app_runner::AppRunner;
4use charms_data::{
5    App, AppInput, B32, Charms, Data, NativeOutput, TOKEN, Transaction, TxId, UtxoId, check,
6    is_simple_transfer,
7};
8use serde::{Deserialize, Serialize};
9use serde_with::{DisplayFromStr, IfIsHumanReadable, serde_as};
10use sha2::{Digest, Sha256};
11use std::collections::{BTreeMap, BTreeSet};
12
13pub mod ark;
14pub mod bitcoin_tx;
15pub mod cardano_tx;
16pub mod sorted_app_map;
17pub mod tx;
18
19pub const MOCK_SPELL_VK: &str = "7c38e8639a2eac0074cee920982b92376513e8940f4a7ca6859f17a728af5b0e";
20
21/// Verification key for version `0` of the protocol implemented by `charms-spell-checker` binary.
22pub const V0_SPELL_VK: &str = "0x00e9398ac819e6dd281f81db3ada3fe5159c3cc40222b5ddb0e7584ed2327c5d";
23/// Verification key for version `1` of the protocol implemented by `charms-spell-checker` binary.
24pub const V1_SPELL_VK: &str = "0x009f38f590ebca4c08c1e97b4064f39e4cd336eea4069669c5f5170a38a1ff97";
25/// Verification key for version `2` of the protocol implemented by `charms-spell-checker` binary.
26pub const V2_SPELL_VK: &str = "0x00bd312b6026dbe4a2c16da1e8118d4fea31587a4b572b63155252d2daf69280";
27/// Verification key for version `3` of the protocol implemented by `charms-spell-checker` binary.
28pub const V3_SPELL_VK: &str = "0x0034872b5af38c95fe82fada696b09a448f7ab0928273b7ac8c58ba29db774b9";
29/// Verification key for version `4` of the protocol implemented by `charms-spell-checker` binary.
30pub const V4_SPELL_VK: &str = "0x00c707a155bf8dc18dc41db2994c214e93e906a3e97b4581db4345b3edd837c5";
31/// Verification key for version `5` of the protocol implemented by `charms-spell-checker` binary.
32pub const V5_SPELL_VK: &str = "0x00e98665c417bd2e6e81c449af63b26ed5ad5c400ef55811b592450bf62c67cd";
33/// Verification key for version `6` of the protocol implemented by `charms-proof-wrapper` binary.
34pub const V6_SPELL_VK: &str = "0x005a1df17094445572e4dd474b3e5dd9093936cba62ca3a62bb2ce63d9db8cba";
35/// Verification key for version `7` of the protocol implemented by `charms-proof-wrapper` binary.
36pub const V7_SPELL_VK: &str = "0x0041d9843ec25ba04797a0ce29af364389f7eda9f7126ef39390c357432ad9aa";
37/// Verification key for version `8` of the protocol implemented by `charms-proof-wrapper` binary.
38pub const V8_SPELL_VK: &str = "0x00e440d40e331c16bc4c78d2dbc6bb35876e6ea944e943de359a075e07385abc";
39/// Verification key for version `9` of the protocol implemented by `charms-proof-wrapper` binary.
40pub const V9_SPELL_VK: &str = "0x00713f077ec2bd68157512835dc678053565a889935ecd5789ce2fa097c93ee9";
41/// Verification key for version `10` of the protocol implemented by `charms-proof-wrapper` binary.
42pub const V10_SPELL_VK: &str = "0x00ccf030317cae019a4cd3c8557b2c5b522050e7e562e3adf287cd5ad596511f";
43/// Verification key for version `11` of the protocol implemented by `charms-proof-wrapper` binary.
44pub const V11_SPELL_VK: &str = "0x00d41d49f54303acee4e7d064a31e0c9bd2e1bbdb60f39170a1461c71015c308";
45/// Verification key for version `12` of the protocol implemented by `charms-proof-wrapper` binary.
46pub const V12_SPELL_VK: &str = "0x00cd44537c67da0dc50b88e794deed43c4507a862070ed83c99941789811a6a0";
47
48/// Version `0` of the protocol.
49pub const V0: u32 = 0;
50/// Version `1` of the protocol.
51pub const V1: u32 = 1;
52/// Version `2` of the protocol.
53pub const V2: u32 = 2;
54/// Version `3` of the protocol.
55pub const V3: u32 = 3;
56/// Version `4` of the protocol.
57pub const V4: u32 = 4;
58/// Version `5` of the protocol.
59pub const V5: u32 = 5;
60/// Version `6` of the protocol.
61pub const V6: u32 = 6;
62/// Version `7` of the protocol.
63pub const V7: u32 = 7;
64/// Version `8` of the protocol.
65pub const V8: u32 = 8;
66/// Version `9` of the protocol.
67pub const V9: u32 = 9;
68/// Version `10` of the protocol.
69pub const V10: u32 = 10;
70/// Version `11` of the protocol.
71pub const V11: u32 = 11;
72/// Version `12` of the protocol.
73pub const V12: u32 = 12;
74/// Version `13` of the protocol.
75pub const V13: u32 = 13;
76
77/// Current version of the protocol.
78pub const CURRENT_VERSION: u32 = V13;
79
80/// Source of a beamed input: the UTXO that beamed the charms, with an optional nonce.
81///
82/// When the nonce is `Some`, its LE bytes are appended to the destination UTXO ID bytes
83/// before hashing for comparison with the hash in the beaming transaction's `beamed_outs`.
84#[serde_as]
85#[cfg_attr(test, derive(test_strategy::Arbitrary))]
86#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
87pub struct BeamSource(
88    #[serde_as(as = "IfIsHumanReadable<DisplayFromStr>")] pub UtxoId,
89    #[serde(skip_serializing_if = "Option::is_none", default)] pub Option<u64>,
90);
91
92/// Maps the index of the charm's app (in [`NormalizedSpell`].`app_public_inputs`) to the charm's
93/// data.
94pub type NormalizedCharms = BTreeMap<u32, Data>;
95
96/// Normalized representation of a Charms transaction.
97#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
98pub struct NormalizedTransaction {
99    /// (Optional) input UTXO list. Is None when serialized in the transaction: the transaction
100    /// already lists all inputs. **Must** be in the order of the transaction inputs.
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub ins: Option<Vec<UtxoId>>,
103
104    /// Reference UTXO list. **May** be empty.
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub refs: Option<Vec<UtxoId>>,
107
108    /// Output charms. **Must** be in the order of the transaction outputs.
109    /// When proving spell correctness, we can't know the transaction ID yet.
110    /// We only know the index of each output charm.
111    /// **Must** be in the order of the hosting transaction's outputs.
112    /// **Must not** be larger than the number of outputs in the hosting transaction.
113    pub outs: Vec<NormalizedCharms>,
114
115    /// Optional mapping from the beamed output index to the destination UtxoId hash.
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub beamed_outs: Option<BTreeMap<u32, B32>>,
118
119    /// Amounts of native coin in transaction outputs.
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub coins: Option<Vec<NativeOutput>>,
122}
123
124impl NormalizedTransaction {
125    /// Return a sorted set of transaction IDs of the inputs.
126    /// Including source tx_ids for beamed inputs.
127    pub fn prev_txids(&self) -> Option<BTreeSet<&TxId>> {
128        self.ins
129            .as_ref()
130            .map(|ins| ins.iter().map(|utxo_id| &utxo_id.0).collect())
131    }
132}
133
134/// Proof of spell correctness.
135pub type Proof = Vec<u8>;
136
137/// Normalized representation of a spell.
138/// Can be committed as public input.
139#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
140pub struct NormalizedSpell {
141    /// Protocol version.
142    pub version: u32,
143    /// Transaction data.
144    pub tx: NormalizedTransaction,
145    /// Maps all `App`s in the transaction to (potentially empty) public input data.
146    #[serde(deserialize_with = "sorted_app_map::deserialize")]
147    pub app_public_inputs: BTreeMap<App, Data>,
148    /// Is this a mock spell?
149    #[serde(skip_serializing_if = "std::ops::Not::not", default)]
150    pub mock: bool,
151}
152
153impl Default for NormalizedSpell {
154    fn default() -> Self {
155        Self {
156            version: CURRENT_VERSION,
157            tx: Default::default(),
158            app_public_inputs: Default::default(),
159            mock: false,
160        }
161    }
162}
163
164pub fn utxo_id_hash(utxo_id: &UtxoId) -> B32 {
165    let hash = Sha256::digest(utxo_id.to_bytes());
166    B32(hash.into())
167}
168
169pub fn utxo_id_hash_with_nonce(utxo_id: &UtxoId, nonce: Option<u64>) -> B32 {
170    let mut hash = Sha256::default();
171    hash.update(utxo_id.to_bytes());
172    if let Some(nonce) = nonce {
173        hash.update(&nonce.to_le_bytes());
174    }
175    B32(hash.finalize().into())
176}
177
178/// Extract spells from previous transactions.
179#[tracing::instrument(level = "debug", skip(prev_txs, spell_vk))]
180pub fn prev_spells(
181    prev_txs: &[Tx],
182    spell_vk: &str,
183    norm_spell: &NormalizedSpell,
184) -> anyhow::Result<BTreeMap<TxId, (NormalizedSpell, usize)>> {
185    prev_txs
186        .iter()
187        .map(|tx| {
188            Ok((
189                tx.tx_id(),
190                (
191                    extended_normalized_spell(spell_vk, norm_spell, tx)?,
192                    tx.tx_outs_len(),
193                ),
194            ))
195        })
196        .collect()
197}
198
199/// Check if the spell is well-formed.
200#[tracing::instrument(level = "debug", skip(spell, prev_spells))]
201pub fn well_formed(
202    spell: &NormalizedSpell,
203    prev_spells: &BTreeMap<TxId, (NormalizedSpell, usize)>,
204    tx_ins_beamed_source_utxos: &BTreeMap<usize, BeamSource>,
205) -> bool {
206    check!(spell.version == CURRENT_VERSION);
207    check!(ensure_no_zero_amounts(spell).is_ok());
208    let directly_created_by_prev_txns = |utxo_id: &UtxoId| -> bool {
209        let tx_id = utxo_id.0;
210        prev_spells
211            .get(&tx_id)
212            .is_some_and(|(n_spell, num_tx_outs)| {
213                let utxo_index = utxo_id.1;
214
215                let is_beamed_out = beamed_out_to_hash(n_spell, utxo_index).is_some();
216
217                utxo_index <= *num_tx_outs as u32 && !is_beamed_out
218            })
219    };
220    check!({
221        spell.tx.outs.iter().all(|n_charm| {
222            n_charm
223                .keys()
224                .all(|&i| i < spell.app_public_inputs.len() as u32)
225        })
226    });
227    // check that UTXOs we're spending or referencing in this tx
228    // are created by pre-req transactions
229    let Some(tx_ins) = &spell.tx.ins else {
230        eprintln!("no tx.ins");
231        return false;
232    };
233    check!(
234        tx_ins.iter().all(directly_created_by_prev_txns)
235            && (spell.tx.refs.iter().flatten()).all(directly_created_by_prev_txns)
236    );
237    let beamed_source_utxos_point_to_placeholder_dest_utxos = tx_ins_beamed_source_utxos
238        .iter()
239        .all(|(&i, beaming_source)| {
240            let tx_in_utxo_id = &tx_ins[i];
241            let prev_txid = tx_in_utxo_id.0;
242            let prev_tx = prev_spells.get(&prev_txid);
243            let Some((prev_spell, _tx_outs)) = prev_tx else {
244                // prev_tx should be provided, so we know it doesn't carry a spell
245                return false;
246            };
247            // tx_in_utxo must exist and either have no Charms or be beamed out
248            check!(
249                (prev_spell.tx.outs)
250                    .get(tx_in_utxo_id.1 as usize)
251                    .is_none_or(|charms| charms.is_empty()
252                        || beamed_out_to_hash(prev_spell, tx_in_utxo_id.1).is_some())
253            );
254
255            let beaming_source_utxo_id = &beaming_source.0;
256            let beaming_txid = beaming_source_utxo_id.0;
257            let beaming_utxo_index = beaming_source_utxo_id.1;
258
259            prev_spells
260                .get(&beaming_txid)
261                .and_then(|(n_spell, _tx_outs)| beamed_out_to_hash(n_spell, beaming_utxo_index))
262                .is_some_and(|dest_utxo_hash| {
263                    dest_utxo_hash == &utxo_id_hash_with_nonce(tx_in_utxo_id, beaming_source.1)
264                })
265        });
266    check!(beamed_source_utxos_point_to_placeholder_dest_utxos);
267    true
268}
269
270/// Return the list of apps in the spell.
271pub fn apps(spell: &NormalizedSpell) -> Vec<App> {
272    spell.app_public_inputs.keys().cloned().collect()
273}
274
275/// Convert normalized spell to [`charms_data::Transaction`].
276pub fn to_tx(
277    spell: &NormalizedSpell,
278    prev_spells: &BTreeMap<TxId, (NormalizedSpell, usize)>,
279    tx_ins_beamed_source_utxos: &BTreeMap<usize, BeamSource>,
280    prev_txs: &[Tx],
281) -> Transaction {
282    let Some(tx_ins) = &spell.tx.ins else {
283        unreachable!("self.tx.ins MUST be Some at this point");
284    };
285
286    let tx_ins_beamed_source_utxos: BTreeMap<UtxoId, UtxoId> = tx_ins_beamed_source_utxos
287        .iter()
288        .map(|(&i, bs)| (tx_ins[i].clone(), bs.0.clone()))
289        .collect();
290
291    let from_utxo_id = |utxo_id: &UtxoId| -> (UtxoId, Charms) {
292        let (prev_spell, _) = &prev_spells[&utxo_id.0];
293        let charms = charms_in_utxo(prev_spell, utxo_id)
294            .or_else(|| {
295                tx_ins_beamed_source_utxos
296                    .get(utxo_id)
297                    .and_then(|beam_source_utxo_id| {
298                        let prev_spell = &prev_spells[&beam_source_utxo_id.0].0;
299                        charms_in_utxo(&prev_spell, beam_source_utxo_id)
300                    })
301            })
302            .unwrap_or_default();
303        (utxo_id.clone(), charms)
304    };
305
306    let from_normalized_charms =
307        |n_charms: &NormalizedCharms| -> Charms { charms(spell, n_charms) };
308
309    let coin_from_input = |utxo_id: &UtxoId| -> NativeOutput {
310        let (prev_spell, _) = &prev_spells[&utxo_id.0];
311        let prev_coins = prev_spell.tx.coins.as_ref().expect(
312            "coins MUST NOT be none: we used `extended_normalized_spell` to get prev_spells",
313        );
314        prev_coins[utxo_id.1 as usize].clone()
315    };
316
317    let prev_txs = prev_txs.iter().map(|tx| (tx.tx_id(), tx.into())).collect();
318
319    Transaction {
320        ins: tx_ins.iter().map(from_utxo_id).collect(),
321        refs: spell.tx.refs.iter().flatten().map(from_utxo_id).collect(),
322        outs: spell.tx.outs.iter().map(from_normalized_charms).collect(),
323        coin_ins: Some(tx_ins.iter().map(coin_from_input).collect()),
324        coin_outs: spell.tx.coins.clone(),
325        prev_txs,
326        app_public_inputs: spell.app_public_inputs.clone(),
327    }
328}
329
330pub fn charms_in_utxo(prev_spell: &NormalizedSpell, utxo_id: &UtxoId) -> Option<Charms> {
331    (prev_spell.tx.outs)
332        .get(utxo_id.1 as usize)
333        .map(|n_charms| charms(prev_spell, n_charms))
334        .filter(|c| !c.is_empty())
335}
336
337/// Return [`charms_data::Charms`] for the given [`NormalizedCharms`].
338pub fn charms(spell: &NormalizedSpell, n_charms: &NormalizedCharms) -> Charms {
339    let apps = apps(spell);
340    n_charms
341        .iter()
342        .map(|(&i, data)| (apps[i as usize].clone(), data.clone()))
343        .collect()
344}
345
346#[derive(Clone, Debug, Serialize, Deserialize)]
347pub struct SpellProverInput {
348    pub self_spell_vk: String,
349    pub prev_txs: Vec<Tx>,
350    pub spell: NormalizedSpell,
351    pub tx_ins_beamed_source_utxos: BTreeMap<usize, BeamSource>,
352    pub app_input: Option<AppInput>,
353}
354
355/// Check if the spell is correct.
356pub fn is_correct(
357    spell: &NormalizedSpell,
358    prev_txs: &Vec<Tx>,
359    app_input: Option<AppInput>,
360    spell_vk: &str,
361    tx_ins_beamed_source_utxos: &BTreeMap<usize, BeamSource>,
362) -> anyhow::Result<bool> {
363    ensure!(beaming_txs_have_finality_proofs(
364        prev_txs,
365        tx_ins_beamed_source_utxos
366    ));
367
368    let prev_spells = prev_spells(&prev_txs, spell_vk, &spell)?;
369
370    ensure!(well_formed(spell, &prev_spells, tx_ins_beamed_source_utxos));
371
372    let Some(prev_txids) = spell.tx.prev_txids() else {
373        unreachable!("the spell is well formed: tx.ins MUST be Some");
374    };
375    let all_prev_txids: BTreeSet<_> = tx_ins_beamed_source_utxos
376        .values()
377        .map(|bs| &(bs.0).0)
378        .chain(prev_txids)
379        .collect();
380    ensure!(all_prev_txids == prev_spells.keys().collect());
381
382    let apps = apps(spell);
383
384    let charms_tx = to_tx(spell, &prev_spells, tx_ins_beamed_source_utxos, &prev_txs);
385    let tx_is_simple_transfer_or_app_contracts_satisfied =
386        apps.iter().all(|app| is_simple_transfer(app, &charms_tx)) && app_input.is_none()
387            || app_input.is_some_and(|app_input| {
388                apps_satisfied(&app_input, &spell.app_public_inputs, &charms_tx)
389            });
390    ensure!(tx_is_simple_transfer_or_app_contracts_satisfied);
391
392    Ok(true)
393}
394
395fn beaming_txs_have_finality_proofs(
396    prev_txs: &Vec<Tx>,
397    tx_ins_beamed_source_utxos: &BTreeMap<usize, BeamSource>,
398) -> bool {
399    let prev_txs_by_txid: BTreeMap<TxId, Tx> = by_txid(prev_txs);
400    let beaming_source_txids: BTreeSet<&TxId> = tx_ins_beamed_source_utxos
401        .values()
402        .map(|bs| &(bs.0).0)
403        .collect::<BTreeSet<_>>();
404    beaming_source_txids.iter().all(|&txid| {
405        prev_txs_by_txid
406            .get(txid)
407            .is_some_and(|tx| tx.proven_final())
408    })
409}
410
411fn apps_satisfied(
412    app_input: &AppInput,
413    app_public_inputs: &BTreeMap<App, Data>,
414    tx: &Transaction,
415) -> bool {
416    let app_runner = AppRunner::new(false);
417    app_runner
418        .run_all(
419            &app_input.app_binaries,
420            &tx,
421            app_public_inputs,
422            &app_input.app_private_inputs,
423        )
424        .expect("all apps should run successfully");
425    true
426}
427
428pub fn ensure_no_zero_amounts(norm_spell: &NormalizedSpell) -> anyhow::Result<()> {
429    let apps = apps(norm_spell);
430    for out in &norm_spell.tx.outs {
431        for (i, data) in out {
432            let app = apps
433                .get(*i as usize)
434                .ok_or(anyhow!("no app for index {}", i))?;
435            if app.tag == TOKEN {
436                ensure!(
437                    data.value::<u64>()? != 0,
438                    "zero output amount for app {}",
439                    app
440                );
441            };
442        }
443    }
444    Ok(())
445}
446
447pub fn beamed_out_to_hash(spell: &NormalizedSpell, i: u32) -> Option<&B32> {
448    (spell.tx.beamed_outs)
449        .as_ref()
450        .and_then(|beamed| beamed.get(&i))
451}
452
453#[cfg(test)]
454mod test {
455    use crate::{BeamSource, NormalizedSpell};
456    use charms_data::{UtxoId, util};
457    use test_strategy::proptest;
458
459    #[test]
460    fn dummy() {}
461
462    #[test]
463    fn decode_cbor() {
464        let s = "a36776657273696f6e0c627478a363696e73825824769119fa89a524c08a7cc0998e9d911eb92151832daa3f7ddae68490860c8f570000000058247863c61916d5787e1e2dd2ef8e3e10bdbb6fa98f8a08b49ed0c6381af383146b03000000646f75747381a2011b00000005d21dba0000a7656d616b6572782a626331716a38766c3665347a356d74396c367365787238683438396e326b3933707674347937686c6d3769657865635f74797065a1677061727469616ca064736964656361736b657072696365821901a31a05f5e10066616d6f756e741a0001992e687175616e746974791b00000005d21dba00656173736574a165746f6b656e7883742f336437666537653463656136313231393437616637336437306535313139626562643861613562376564666537346266616636653737396131383437626439622f6339373564346530633239326662393565666264613563313333313264366163316438623561656666376630663165353537383634356132646137306666356665636f696e7381a266616d6f756e7419022264646573749600140d18b018490b183c0c181918a61854189d184d18fb188e18a518ca101880188c184318cb716170705f7075626c69635f696e70757473a283616298200000000000000000000000000000000000000000000000000000000000000000982018a4187118d318fc18c4183618ae187c18bc0e0c188218a6188c18dc188e00183e18e2181e18f8181918e118ac18f8183418e1181c184318ce184718d8f68361749820183d187f18e718e418ce18a6121819184718af187318d70e1851181918be18bd188a18a518b718ed18fe187418bf18af186e1877189a1818184718bd189b982018c9187518d418e018c2189218fb189518ef18bd18a518c118331218d618ac181d188b185a18ef18f718f018f118e518571886184518a218da187018ff185ff6";
465        let ns: NormalizedSpell = util::read(hex::decode(s).unwrap().as_slice()).unwrap();
466        dbg!(ns);
467    }
468
469    #[proptest]
470    fn beaming_source_json_parse_with_nonce(utxo_id: UtxoId, nonce: u64) {
471        let s0 = format!(r#"["{}",{}]"#, utxo_id, nonce);
472        let bs: BeamSource = serde_json::from_str(&s0).unwrap();
473        let s1 = serde_json::to_string(&bs).unwrap();
474        assert_eq!(s1, s0);
475    }
476
477    #[proptest]
478    fn beaming_source_json_parse_no_nonce(utxo_id: UtxoId) {
479        let s0 = format!(r#"["{}"]"#, utxo_id);
480        let bs: BeamSource = serde_json::from_str(&s0).unwrap();
481        let s1 = serde_json::to_string(&bs).unwrap();
482        assert_eq!(s1, s0);
483    }
484
485    #[proptest]
486    fn beaming_source_json_print(bs0: BeamSource) {
487        let s = serde_json::to_string(&bs0).unwrap();
488        let bs1: BeamSource = serde_json::from_str(&s).unwrap();
489        assert_eq!(bs1, bs0);
490    }
491}