charms_client/
lib.rs

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