charms_client/
lib.rs

1use crate::tx::{EnchantedTx, Tx, extract_and_verify_spell};
2use charms_app_runner::AppInput;
3use charms_data::{App, B32, Charms, Data, Transaction, TxId, UtxoId, check};
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6use std::collections::{BTreeMap, BTreeSet};
7
8pub mod ark;
9pub mod bitcoin_tx;
10pub mod cardano_tx;
11pub mod tx;
12
13pub const MOCK_SPELL_VK: &str = "7c38e8639a2eac0074cee920982b92376513e8940f4a7ca6859f17a728af5b0e";
14
15/// Verification key for version `0` of the protocol implemented by `charms-spell-checker` binary.
16pub const V0_SPELL_VK: &str = "0x00e9398ac819e6dd281f81db3ada3fe5159c3cc40222b5ddb0e7584ed2327c5d";
17/// Verification key for version `1` of the protocol implemented by `charms-spell-checker` binary.
18pub const V1_SPELL_VK: &str = "0x009f38f590ebca4c08c1e97b4064f39e4cd336eea4069669c5f5170a38a1ff97";
19/// Verification key for version `2` of the protocol implemented by `charms-spell-checker` binary.
20pub const V2_SPELL_VK: &str = "0x00bd312b6026dbe4a2c16da1e8118d4fea31587a4b572b63155252d2daf69280";
21/// Verification key for version `3` of the protocol implemented by `charms-spell-checker` binary.
22pub const V3_SPELL_VK: &str = "0x0034872b5af38c95fe82fada696b09a448f7ab0928273b7ac8c58ba29db774b9";
23/// Verification key for version `4` of the protocol implemented by `charms-spell-checker` binary.
24pub const V4_SPELL_VK: &str = "0x00c707a155bf8dc18dc41db2994c214e93e906a3e97b4581db4345b3edd837c5";
25/// Verification key for version `5` of the protocol implemented by `charms-spell-checker` binary.
26pub const V5_SPELL_VK: &str = "0x00e98665c417bd2e6e81c449af63b26ed5ad5c400ef55811b592450bf62c67cd";
27/// Verification key for version `6` of the protocol implemented by `charms-proof-wrapper` binary.
28pub const V6_SPELL_VK: &str = "0x005a1df17094445572e4dd474b3e5dd9093936cba62ca3a62bb2ce63d9db8cba";
29
30/// Version `0` of the protocol.
31pub const V0: u32 = 0;
32/// Version `1` of the protocol.
33pub const V1: u32 = 1;
34/// Version `2` of the protocol.
35pub const V2: u32 = 2;
36/// Version `3` of the protocol.
37pub const V3: u32 = 3;
38/// Version `4` of the protocol.
39pub const V4: u32 = 4;
40/// Version `5` of the protocol.
41pub const V5: u32 = 5;
42/// Version `6` of the protocol.
43pub const V6: u32 = 6;
44/// Version `7` of the protocol.
45pub const V7: u32 = 7;
46
47/// Current version of the protocol.
48pub const CURRENT_VERSION: u32 = V7;
49
50/// Maps the index of the charm's app (in [`NormalizedSpell`].`app_public_inputs`) to the charm's
51/// data.
52pub type NormalizedCharms = BTreeMap<u32, Data>;
53
54/// Normalized representation of a Charms transaction.
55#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
56pub struct NormalizedTransaction {
57    /// (Optional) input UTXO list. Is None when serialized in the transaction: the transaction
58    /// already lists all inputs. **Must** be in the order of the transaction inputs.
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub ins: Option<Vec<UtxoId>>,
61
62    /// Reference UTXO list. **May** be empty.
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub refs: Option<Vec<UtxoId>>,
65
66    /// Output charms. **Must** be in the order of the transaction outputs.
67    /// When proving spell correctness, we can't know the transaction ID yet.
68    /// We only know the index of each output charm.
69    /// **Must** be in the order of the hosting transaction's outputs.
70    /// **Must not** be larger than the number of outputs in the hosting transaction.
71    pub outs: Vec<NormalizedCharms>,
72
73    /// Optional mapping from the beamed output index to the destination UtxoId hash.
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub beamed_outs: Option<BTreeMap<u32, B32>>,
76}
77
78impl NormalizedTransaction {
79    /// Return a sorted set of transaction IDs of the inputs.
80    /// Including source tx_ids for beamed inputs.
81    pub fn prev_txids(&self) -> Option<BTreeSet<&TxId>> {
82        self.ins
83            .as_ref()
84            .map(|ins| ins.iter().map(|utxo_id| &utxo_id.0).collect())
85    }
86}
87
88/// Proof of spell correctness.
89pub type Proof = Vec<u8>;
90
91/// Normalized representation of a spell.
92/// Can be committed as public input.
93#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
94pub struct NormalizedSpell {
95    /// Protocol version.
96    pub version: u32,
97    /// Transaction data.
98    pub tx: NormalizedTransaction,
99    /// Maps all `App`s in the transaction to (potentially empty) public input data.
100    pub app_public_inputs: BTreeMap<App, Data>,
101    /// Is this a mock spell?
102    #[serde(skip_serializing_if = "std::ops::Not::not", default)]
103    pub mock: bool,
104}
105
106pub fn utxo_id_hash(utxo_id: &UtxoId) -> B32 {
107    let hash = Sha256::digest(utxo_id.to_bytes());
108    B32(hash.into())
109}
110
111/// Extract spells from previous transactions.
112#[tracing::instrument(level = "debug", skip(prev_txs, spell_vk))]
113pub fn prev_spells(
114    prev_txs: &Vec<Tx>,
115    spell_vk: &str,
116    mock: bool,
117) -> BTreeMap<TxId, (Option<NormalizedSpell>, usize)> {
118    prev_txs
119        .iter()
120        .map(|tx| {
121            let tx_id = tx.tx_id();
122            (
123                tx_id,
124                (
125                    extract_and_verify_spell(spell_vk, tx, mock)
126                        .map_err(|e| {
127                            tracing::info!("no correct spell in tx {}: {}", tx_id, e);
128                        })
129                        .ok(),
130                    tx.tx_outs_len(),
131                ),
132            )
133        })
134        .collect()
135}
136
137/// Check if the spell is well-formed.
138#[tracing::instrument(level = "debug", skip(spell, prev_spells))]
139pub fn well_formed(
140    spell: &NormalizedSpell,
141    prev_spells: &BTreeMap<TxId, (Option<NormalizedSpell>, usize)>,
142    tx_ins_beamed_source_utxos: &BTreeMap<UtxoId, UtxoId>,
143) -> bool {
144    check!(spell.version == CURRENT_VERSION);
145    let directly_created_by_prev_txns = |utxo_id: &UtxoId| -> bool {
146        let tx_id = utxo_id.0;
147        prev_spells
148            .get(&tx_id)
149            .is_some_and(|(n_spell_opt, num_tx_outs)| {
150                let utxo_index = utxo_id.1;
151
152                let is_beamed_out = n_spell_opt
153                    .as_ref()
154                    .and_then(|n_spell| n_spell.tx.beamed_outs.as_ref())
155                    .and_then(|beamed_outs| beamed_outs.get(&utxo_index))
156                    .is_some();
157
158                utxo_index <= *num_tx_outs as u32 && !is_beamed_out
159            })
160    };
161    check!({
162        spell.tx.outs.iter().all(|n_charm| {
163            n_charm
164                .keys()
165                .all(|&i| i < spell.app_public_inputs.len() as u32)
166        })
167    });
168    // check that UTXOs we're spending or referencing in this tx
169    // are created by pre-req transactions
170    let Some(tx_ins) = &spell.tx.ins else {
171        eprintln!("no tx.ins");
172        return false;
173    };
174    check!(
175        tx_ins.iter().all(directly_created_by_prev_txns)
176            && spell
177                .tx
178                .refs
179                .iter()
180                .flatten()
181                .all(directly_created_by_prev_txns)
182    );
183    let beamed_source_utxos_point_to_placeholder_dest_utxos = tx_ins_beamed_source_utxos
184        .iter()
185        .all(|(tx_in_utxo_id, beaming_source_utxo_id)| {
186            let prev_txid = tx_in_utxo_id.0;
187            let prev_tx = prev_spells.get(&prev_txid);
188            let Some((prev_spell_opt, _tx_outs)) = prev_tx else {
189                // prev_tx should be provided, so we know it doesn't carry a spell
190                return false;
191            };
192            // prev_tx must exist but not carry a spell
193            check!(prev_spell_opt.is_none());
194
195            let beaming_txid = beaming_source_utxo_id.0;
196            let beaming_utxo_index = beaming_source_utxo_id.1;
197
198            prev_spells
199                .get(&beaming_txid)
200                .and_then(|(n_spell_opt, _tx_outs)| {
201                    n_spell_opt.as_ref().and_then(|n_spell| {
202                        n_spell
203                            .tx
204                            .beamed_outs
205                            .as_ref()
206                            .and_then(|beamed_outs| beamed_outs.get(&beaming_utxo_index))
207                    })
208                })
209                .is_some_and(|dest_utxo_hash| dest_utxo_hash == &utxo_id_hash(tx_in_utxo_id))
210        });
211    check!(beamed_source_utxos_point_to_placeholder_dest_utxos);
212    true
213}
214
215/// Return the list of apps in the spell.
216pub fn apps(spell: &NormalizedSpell) -> Vec<App> {
217    spell.app_public_inputs.keys().cloned().collect()
218}
219
220/// Convert normalized spell to [`charms_data::Transaction`].
221pub fn to_tx(
222    spell: &NormalizedSpell,
223    prev_spells: &BTreeMap<TxId, (Option<NormalizedSpell>, usize)>,
224    tx_ins_beamed_source_utxos: &BTreeMap<UtxoId, UtxoId>,
225) -> Transaction {
226    let from_utxo_id = |utxo_id: &UtxoId| -> (UtxoId, Charms) {
227        let (prev_spell_opt, _) = &prev_spells[&utxo_id.0];
228        let charms = prev_spell_opt
229            .as_ref()
230            .and_then(|prev_spell| charms_in_utxo(prev_spell, utxo_id))
231            .or_else(|| {
232                tx_ins_beamed_source_utxos
233                    .get(utxo_id)
234                    .and_then(|beam_source_utxo_id| {
235                        prev_spells[&beam_source_utxo_id.0]
236                            .0
237                            .as_ref()
238                            .and_then(|prev_spell| charms_in_utxo(prev_spell, beam_source_utxo_id))
239                    })
240            })
241            .unwrap_or_default();
242        (utxo_id.clone(), charms)
243    };
244
245    let from_normalized_charms =
246        |n_charms: &NormalizedCharms| -> Charms { charms(spell, n_charms) };
247
248    let Some(tx_ins) = &spell.tx.ins else {
249        unreachable!("self.tx.ins MUST be Some at this point");
250    };
251    Transaction {
252        ins: tx_ins.iter().map(from_utxo_id).collect(),
253        refs: spell.tx.refs.iter().flatten().map(from_utxo_id).collect(),
254        outs: spell.tx.outs.iter().map(from_normalized_charms).collect(),
255    }
256}
257
258fn charms_in_utxo(prev_spell: &NormalizedSpell, utxo_id: &UtxoId) -> Option<Charms> {
259    prev_spell
260        .tx
261        .outs
262        .get(utxo_id.1 as usize)
263        .map(|n_charms| charms(prev_spell, n_charms))
264}
265
266/// Return [`charms_data::Charms`] for the given [`NormalizedCharms`].
267pub fn charms(spell: &NormalizedSpell, n_charms: &NormalizedCharms) -> Charms {
268    let apps = apps(spell);
269    n_charms
270        .iter()
271        .map(|(&i, data)| (apps[i as usize].clone(), data.clone()))
272        .collect()
273}
274
275#[derive(Clone, Debug, Serialize, Deserialize)]
276pub struct SpellProverInput {
277    pub self_spell_vk: String,
278    pub prev_txs: Vec<Tx>,
279    pub spell: NormalizedSpell,
280    pub tx_ins_beamed_source_utxos: BTreeMap<UtxoId, UtxoId>,
281    /// indices of apps in the spell that have contract proofs
282    pub app_input: Option<AppInput>,
283}
284
285#[cfg(test)]
286mod test {
287    #[test]
288    fn dummy() {}
289}