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