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