1use crate::tx::{EnchantedTx, Tx, extract_and_verify_spell};
2use charms_data::{App, AppInput, B32, Charms, Data, Transaction, TxId, UtxoId, check};
3use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5use std::collections::{BTreeMap, BTreeSet};
6
7pub mod ark;
8pub mod bitcoin_tx;
9pub mod cardano_tx;
10pub mod tx;
11
12pub const MOCK_SPELL_VK: &str = "7c38e8639a2eac0074cee920982b92376513e8940f4a7ca6859f17a728af5b0e";
13
14pub const V0_SPELL_VK: &str = "0x00e9398ac819e6dd281f81db3ada3fe5159c3cc40222b5ddb0e7584ed2327c5d";
16pub const V1_SPELL_VK: &str = "0x009f38f590ebca4c08c1e97b4064f39e4cd336eea4069669c5f5170a38a1ff97";
18pub const V2_SPELL_VK: &str = "0x00bd312b6026dbe4a2c16da1e8118d4fea31587a4b572b63155252d2daf69280";
20pub const V3_SPELL_VK: &str = "0x0034872b5af38c95fe82fada696b09a448f7ab0928273b7ac8c58ba29db774b9";
22pub const V4_SPELL_VK: &str = "0x00c707a155bf8dc18dc41db2994c214e93e906a3e97b4581db4345b3edd837c5";
24pub const V5_SPELL_VK: &str = "0x00e98665c417bd2e6e81c449af63b26ed5ad5c400ef55811b592450bf62c67cd";
26pub const V6_SPELL_VK: &str = "0x005a1df17094445572e4dd474b3e5dd9093936cba62ca3a62bb2ce63d9db8cba";
28
29pub const V0: u32 = 0;
31pub const V1: u32 = 1;
33pub const V2: u32 = 2;
35pub const V3: u32 = 3;
37pub const V4: u32 = 4;
39pub const V5: u32 = 5;
41pub const V6: u32 = 6;
43pub const V7: u32 = 7;
45
46pub const CURRENT_VERSION: u32 = V7;
48
49pub type NormalizedCharms = BTreeMap<u32, Data>;
52
53#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
55pub struct NormalizedTransaction {
56 #[serde(skip_serializing_if = "Option::is_none")]
59 pub ins: Option<Vec<UtxoId>>,
60
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub refs: Option<Vec<UtxoId>>,
64
65 pub outs: Vec<NormalizedCharms>,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub beamed_outs: Option<BTreeMap<u32, B32>>,
75}
76
77impl NormalizedTransaction {
78 pub fn prev_txids(&self) -> Option<BTreeSet<&TxId>> {
81 self.ins
82 .as_ref()
83 .map(|ins| ins.iter().map(|utxo_id| &utxo_id.0).collect())
84 }
85}
86
87pub type Proof = Vec<u8>;
89
90#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
93pub struct NormalizedSpell {
94 pub version: u32,
96 pub tx: NormalizedTransaction,
98 pub app_public_inputs: BTreeMap<App, Data>,
100 #[serde(skip_serializing_if = "std::ops::Not::not", default)]
102 pub mock: bool,
103}
104
105pub fn utxo_id_hash(utxo_id: &UtxoId) -> B32 {
106 let hash = Sha256::digest(utxo_id.to_bytes());
107 B32(hash.into())
108}
109
110#[tracing::instrument(level = "debug", skip(prev_txs, spell_vk))]
112pub fn prev_spells(
113 prev_txs: &Vec<Tx>,
114 spell_vk: &str,
115 mock: bool,
116) -> BTreeMap<TxId, (Option<NormalizedSpell>, usize)> {
117 prev_txs
118 .iter()
119 .map(|tx| {
120 let tx_id = tx.tx_id();
121 (
122 tx_id,
123 (
124 extract_and_verify_spell(spell_vk, tx, mock)
125 .map_err(|e| {
126 tracing::info!("no correct spell in tx {}: {}", tx_id, e);
127 })
128 .ok(),
129 tx.tx_outs_len(),
130 ),
131 )
132 })
133 .collect()
134}
135
136#[tracing::instrument(level = "debug", skip(spell, prev_spells))]
138pub fn well_formed(
139 spell: &NormalizedSpell,
140 prev_spells: &BTreeMap<TxId, (Option<NormalizedSpell>, usize)>,
141 tx_ins_beamed_source_utxos: &BTreeMap<UtxoId, UtxoId>,
142) -> bool {
143 check!(spell.version == CURRENT_VERSION);
144 let directly_created_by_prev_txns = |utxo_id: &UtxoId| -> bool {
145 let tx_id = utxo_id.0;
146 prev_spells
147 .get(&tx_id)
148 .is_some_and(|(n_spell_opt, num_tx_outs)| {
149 let utxo_index = utxo_id.1;
150
151 let is_beamed_out = n_spell_opt
152 .as_ref()
153 .and_then(|n_spell| n_spell.tx.beamed_outs.as_ref())
154 .and_then(|beamed_outs| beamed_outs.get(&utxo_index))
155 .is_some();
156
157 utxo_index <= *num_tx_outs as u32 && !is_beamed_out
158 })
159 };
160 check!({
161 spell.tx.outs.iter().all(|n_charm| {
162 n_charm
163 .keys()
164 .all(|&i| i < spell.app_public_inputs.len() as u32)
165 })
166 });
167 let Some(tx_ins) = &spell.tx.ins else {
170 eprintln!("no tx.ins");
171 return false;
172 };
173 check!(
174 tx_ins.iter().all(directly_created_by_prev_txns)
175 && spell
176 .tx
177 .refs
178 .iter()
179 .flatten()
180 .all(directly_created_by_prev_txns)
181 );
182 let beamed_source_utxos_point_to_placeholder_dest_utxos = tx_ins_beamed_source_utxos
183 .iter()
184 .all(|(tx_in_utxo_id, beaming_source_utxo_id)| {
185 let prev_txid = tx_in_utxo_id.0;
186 let prev_tx = prev_spells.get(&prev_txid);
187 let Some((prev_spell_opt, _tx_outs)) = prev_tx else {
188 return false;
190 };
191 check!(prev_spell_opt.is_none());
193
194 let beaming_txid = beaming_source_utxo_id.0;
195 let beaming_utxo_index = beaming_source_utxo_id.1;
196
197 prev_spells
198 .get(&beaming_txid)
199 .and_then(|(n_spell_opt, _tx_outs)| {
200 n_spell_opt.as_ref().and_then(|n_spell| {
201 n_spell
202 .tx
203 .beamed_outs
204 .as_ref()
205 .and_then(|beamed_outs| beamed_outs.get(&beaming_utxo_index))
206 })
207 })
208 .is_some_and(|dest_utxo_hash| dest_utxo_hash == &utxo_id_hash(tx_in_utxo_id))
209 });
210 check!(beamed_source_utxos_point_to_placeholder_dest_utxos);
211 true
212}
213
214pub fn apps(spell: &NormalizedSpell) -> Vec<App> {
216 spell.app_public_inputs.keys().cloned().collect()
217}
218
219pub fn to_tx(
221 spell: &NormalizedSpell,
222 prev_spells: &BTreeMap<TxId, (Option<NormalizedSpell>, usize)>,
223 tx_ins_beamed_source_utxos: &BTreeMap<UtxoId, UtxoId>,
224) -> Transaction {
225 let from_utxo_id = |utxo_id: &UtxoId| -> (UtxoId, Charms) {
226 let (prev_spell_opt, _) = &prev_spells[&utxo_id.0];
227 let charms = prev_spell_opt
228 .as_ref()
229 .and_then(|prev_spell| charms_in_utxo(prev_spell, utxo_id))
230 .or_else(|| {
231 tx_ins_beamed_source_utxos
232 .get(utxo_id)
233 .and_then(|beam_source_utxo_id| {
234 prev_spells[&beam_source_utxo_id.0]
235 .0
236 .as_ref()
237 .and_then(|prev_spell| charms_in_utxo(prev_spell, beam_source_utxo_id))
238 })
239 })
240 .unwrap_or_default();
241 (utxo_id.clone(), charms)
242 };
243
244 let from_normalized_charms =
245 |n_charms: &NormalizedCharms| -> Charms { charms(spell, n_charms) };
246
247 let Some(tx_ins) = &spell.tx.ins else {
248 unreachable!("self.tx.ins MUST be Some at this point");
249 };
250 Transaction {
251 ins: tx_ins.iter().map(from_utxo_id).collect(),
252 refs: spell.tx.refs.iter().flatten().map(from_utxo_id).collect(),
253 outs: spell.tx.outs.iter().map(from_normalized_charms).collect(),
254 }
255}
256
257fn charms_in_utxo(prev_spell: &NormalizedSpell, utxo_id: &UtxoId) -> Option<Charms> {
258 prev_spell
259 .tx
260 .outs
261 .get(utxo_id.1 as usize)
262 .map(|n_charms| charms(prev_spell, n_charms))
263}
264
265pub fn charms(spell: &NormalizedSpell, n_charms: &NormalizedCharms) -> Charms {
267 let apps = apps(spell);
268 n_charms
269 .iter()
270 .map(|(&i, data)| (apps[i as usize].clone(), data.clone()))
271 .collect()
272}
273
274#[derive(Clone, Debug, Serialize, Deserialize)]
275pub struct SpellProverInput {
276 pub self_spell_vk: String,
277 pub prev_txs: Vec<Tx>,
278 pub spell: NormalizedSpell,
279 pub tx_ins_beamed_source_utxos: BTreeMap<UtxoId, UtxoId>,
280 pub app_input: Option<AppInput>,
282}
283
284#[cfg(test)]
285mod test {
286 #[test]
287 fn dummy() {}
288}