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.
68pub fn prev_spells(
69    prev_txs: &Vec<bitcoin::Transaction>,
70    spell_vk: &str,
71) -> BTreeMap<TxId, (Option<NormalizedSpell>, usize)> {
72    prev_txs
73        .iter()
74        .map(|tx| {
75            let tx_id = TxId(tx.compute_txid().to_byte_array());
76            (
77                tx_id,
78                (
79                    extract_and_verify_spell(tx, spell_vk)
80                        .map_err(|e| {
81                            eprintln!("no correct spell in tx {}: {}", tx_id, e);
82                        })
83                        .ok(),
84                    tx.output.len(),
85                ),
86            )
87        })
88        .collect()
89}
90
91/// Check if the spell is well-formed.
92pub fn well_formed(
93    spell: &NormalizedSpell,
94    prev_spells: &BTreeMap<TxId, (Option<NormalizedSpell>, usize)>,
95) -> bool {
96    if spell.version != CURRENT_VERSION {
97        eprintln!(
98            "spell version {} is not the current version {}",
99            spell.version, CURRENT_VERSION
100        );
101        return false;
102    }
103    let created_by_prev_spells = |utxo_id: &UtxoId| -> bool {
104        prev_spells
105            .get(&utxo_id.0)
106            .and_then(|(_, num_tx_outs)| Some(utxo_id.1 as usize <= *num_tx_outs))
107            == Some(true)
108    };
109    if !spell
110        .tx
111        .outs
112        .iter()
113        .all(|n_charm| n_charm.keys().all(|i| i < &spell.app_public_inputs.len()))
114    {
115        eprintln!("charm app index higher than app_public_inputs.len()");
116        return false;
117    }
118    // check that UTXOs we're spending or referencing in this tx
119    // are created by pre-req transactions
120    let Some(tx_ins) = &spell.tx.ins else {
121        eprintln!("no tx.ins");
122        return false;
123    };
124    if !tx_ins.iter().all(created_by_prev_spells)
125        || !spell.tx.refs.iter().all(created_by_prev_spells)
126    {
127        eprintln!("input or reference UTXOs are not created by prev transactions");
128        return false;
129    }
130    true
131}
132
133/// Return the list of apps in the spell.
134pub fn apps(spell: &NormalizedSpell) -> Vec<App> {
135    spell.app_public_inputs.keys().cloned().collect()
136}
137
138/// Convert normalized spell to [`charms_data::Transaction`].
139pub fn to_tx(
140    spell: &NormalizedSpell,
141    prev_spells: &BTreeMap<TxId, (Option<NormalizedSpell>, usize)>,
142) -> Transaction {
143    let from_utxo_id = |utxo_id: &UtxoId| -> (UtxoId, Charms) {
144        let (prev_spell_opt, _) = &prev_spells[&utxo_id.0];
145        let charms = prev_spell_opt
146            .as_ref()
147            .and_then(|prev_spell| {
148                prev_spell
149                    .tx
150                    .outs
151                    .get(utxo_id.1 as usize)
152                    .map(|n_charms| charms(prev_spell, n_charms))
153            })
154            .unwrap_or_default();
155        (utxo_id.clone(), charms)
156    };
157
158    let from_normalized_charms =
159        |n_charms: &NormalizedCharms| -> Charms { charms(spell, n_charms) };
160
161    let Some(tx_ins) = &spell.tx.ins else {
162        unreachable!("self.tx.ins MUST be Some at this point");
163    };
164    Transaction {
165        ins: tx_ins.iter().map(from_utxo_id).collect(),
166        refs: spell.tx.refs.iter().map(from_utxo_id).collect(),
167        outs: spell.tx.outs.iter().map(from_normalized_charms).collect(),
168    }
169}
170
171/// Return [`charms_data::Charms`] for the given [`NormalizedCharms`].
172pub fn charms(spell: &NormalizedSpell, n_charms: &NormalizedCharms) -> Charms {
173    let apps = apps(spell);
174    n_charms
175        .iter()
176        .map(|(&i, data)| (apps[i].clone(), data.clone()))
177        .collect()
178}
179
180#[derive(Clone, Debug, Serialize, Deserialize)]
181pub struct SpellProverInput {
182    pub self_spell_vk: String,
183    pub prev_txs: Vec<bitcoin::Transaction>,
184    pub spell: NormalizedSpell,
185    /// indices of apps in the spell that have contract proofs
186    pub app_contract_proofs: BTreeSet<usize>, // proofs are provided in input stream data
187}
188
189#[cfg(test)]
190mod test {
191    #[test]
192    fn dummy() {}
193}