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