charms_client/
lib.rs

1use crate::tx::extract_and_verify_spell;
2use bitcoin::hashes::Hash;
3use charms_data::{App, Charms, Data, Transaction, TxId, UtxoId};
4use serde::{Deserialize, Serialize};
5use std::collections::{BTreeMap, BTreeSet};
6
7pub mod tx;
8
9/// Version `0` of the protocol.
10pub const V0: u32 = 0u32;
11/// Verification key for version `0` of the `charms-spell-checker` binary.
12pub const V0_SPELL_VK: &str = "0x00e9398ac819e6dd281f81db3ada3fe5159c3cc40222b5ddb0e7584ed2327c5d";
13/// Verification key for version `1` of the `charms-spell-checker` binary.
14pub const V1_SPELL_VK: &str = "0x009f38f590ebca4c08c1e97b4064f39e4cd336eea4069669c5f5170a38a1ff97";
15/// Version `1` of the protocol.
16pub const V1: u32 = 1u32;
17/// Version `2` of the protocol.
18pub const V2: u32 = 2u32;
19/// Current version of the protocol.
20pub const CURRENT_VERSION: u32 = V2;
21
22/// Maps the index of the charm's app (in [`NormalizedSpell`].`app_public_inputs`) to the charm's
23/// data.
24pub type NormalizedCharms = BTreeMap<usize, Data>;
25
26/// Normalized representation of a Charms transaction.
27#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
28pub struct NormalizedTransaction {
29    /// (Optional) input UTXO list. Is None when serialized in the transaction: the transaction
30    /// already lists all inputs. **Must** be in the order of the transaction inputs.
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub ins: Option<Vec<UtxoId>>,
33    /// Reference UTXO list. **May** be empty.
34    pub refs: BTreeSet<UtxoId>,
35    /// Output charms. **Must** be in the order of the transaction outputs.
36    /// When proving correctness of a spell, we can't know the transaction ID yet.
37    /// We only know the index of each output charm.
38    /// **Must** be in the order of the hosting transaction's outputs.
39    /// **Must not** be larger than the number of outputs in the hosting transaction.
40    pub outs: Vec<NormalizedCharms>,
41}
42
43impl NormalizedTransaction {
44    /// Return a sorted set of transaction IDs of the inputs.
45    pub fn prev_txids(&self) -> Option<BTreeSet<&TxId>> {
46        self.ins
47            .as_ref()
48            .map(|ins| ins.iter().map(|utxo_id| &utxo_id.0).collect())
49    }
50}
51
52/// Proof of correctness of a spell.
53pub type Proof = Box<[u8]>;
54
55/// Normalized representation of a spell.
56/// Can be committed as public input.
57#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
58pub struct NormalizedSpell {
59    /// Protocol version.
60    pub version: u32,
61    /// Transaction data.
62    pub tx: NormalizedTransaction,
63    /// Maps all `App`s in the transaction to (potentially empty) public input data.
64    pub app_public_inputs: BTreeMap<App, Data>,
65}
66
67/// Extract spells from previous transactions.
68#[tracing::instrument(level = "debug", skip(prev_txs, spell_vk))]
69pub fn prev_spells(
70    prev_txs: &Vec<bitcoin::Transaction>,
71    spell_vk: &str,
72) -> BTreeMap<TxId, (Option<NormalizedSpell>, usize)> {
73    prev_txs
74        .iter()
75        .map(|tx| {
76            let tx_id = TxId(tx.compute_txid().to_byte_array());
77            (
78                tx_id,
79                (
80                    extract_and_verify_spell(tx, spell_vk)
81                        .map_err(|e| {
82                            tracing::info!("no correct spell in tx {}: {}", tx_id, e);
83                        })
84                        .ok(),
85                    tx.output.len(),
86                ),
87            )
88        })
89        .collect()
90}
91
92/// Check if the spell is well-formed.
93#[tracing::instrument(level = "debug", skip(spell, prev_spells))]
94pub fn well_formed(
95    spell: &NormalizedSpell,
96    prev_spells: &BTreeMap<TxId, (Option<NormalizedSpell>, usize)>,
97) -> bool {
98    if spell.version != CURRENT_VERSION {
99        eprintln!(
100            "spell version {} is not the current version {}",
101            spell.version, CURRENT_VERSION
102        );
103        return false;
104    }
105    let created_by_prev_spells = |utxo_id: &UtxoId| -> bool {
106        prev_spells
107            .get(&utxo_id.0)
108            .and_then(|(_, num_tx_outs)| Some(utxo_id.1 as usize <= *num_tx_outs))
109            == Some(true)
110    };
111    if !spell
112        .tx
113        .outs
114        .iter()
115        .all(|n_charm| n_charm.keys().all(|i| i < &spell.app_public_inputs.len()))
116    {
117        eprintln!("charm app index higher than app_public_inputs.len()");
118        return false;
119    }
120    // check that UTXOs we're spending or referencing in this tx
121    // are created by pre-req transactions
122    let Some(tx_ins) = &spell.tx.ins else {
123        eprintln!("no tx.ins");
124        return false;
125    };
126    if !tx_ins.iter().all(created_by_prev_spells)
127        || !spell.tx.refs.iter().all(created_by_prev_spells)
128    {
129        eprintln!("input or reference UTXOs are not created by prev transactions");
130        return false;
131    }
132    true
133}
134
135/// Return the list of apps in the spell.
136pub fn apps(spell: &NormalizedSpell) -> Vec<App> {
137    spell.app_public_inputs.keys().cloned().collect()
138}
139
140/// Convert normalized spell to [`charms_data::Transaction`].
141pub fn to_tx(
142    spell: &NormalizedSpell,
143    prev_spells: &BTreeMap<TxId, (Option<NormalizedSpell>, usize)>,
144) -> Transaction {
145    let from_utxo_id = |utxo_id: &UtxoId| -> (UtxoId, Charms) {
146        let (prev_spell_opt, _) = &prev_spells[&utxo_id.0];
147        let charms = prev_spell_opt
148            .as_ref()
149            .and_then(|prev_spell| {
150                prev_spell
151                    .tx
152                    .outs
153                    .get(utxo_id.1 as usize)
154                    .map(|n_charms| charms(prev_spell, n_charms))
155            })
156            .unwrap_or_default();
157        (utxo_id.clone(), charms)
158    };
159
160    let from_normalized_charms =
161        |n_charms: &NormalizedCharms| -> Charms { charms(spell, n_charms) };
162
163    let Some(tx_ins) = &spell.tx.ins else {
164        unreachable!("self.tx.ins MUST be Some at this point");
165    };
166    Transaction {
167        ins: tx_ins.iter().map(from_utxo_id).collect(),
168        refs: spell.tx.refs.iter().map(from_utxo_id).collect(),
169        outs: spell.tx.outs.iter().map(from_normalized_charms).collect(),
170    }
171}
172
173/// Return [`charms_data::Charms`] for the given [`NormalizedCharms`].
174pub fn charms(spell: &NormalizedSpell, n_charms: &NormalizedCharms) -> Charms {
175    let apps = apps(spell);
176    n_charms
177        .iter()
178        .map(|(&i, data)| (apps[i].clone(), data.clone()))
179        .collect()
180}
181
182#[derive(Clone, Debug, Serialize, Deserialize)]
183pub struct SpellProverInput {
184    pub self_spell_vk: String,
185    pub prev_txs: Vec<bitcoin::Transaction>,
186    pub spell: NormalizedSpell,
187    /// indices of apps in the spell that have contract proofs
188    pub app_contract_proofs: BTreeSet<usize>, // proofs are provided in input stream data
189}
190
191#[cfg(test)]
192mod test {
193    #[test]
194    fn dummy() {}
195}