1use crate::tx::{EnchantedTx, Tx, by_txid, extended_normalized_spell};
2use anyhow::{anyhow, ensure};
3use charms_app_runner::AppRunner;
4use charms_data::{
5 App, AppInput, B32, Charms, Data, NativeOutput, TOKEN, Transaction, TxId, UtxoId, check,
6 is_simple_transfer,
7};
8use serde::{Deserialize, Serialize};
9use sha2::{Digest, Sha256};
10use std::collections::{BTreeMap, BTreeSet};
11
12pub mod ark;
13pub mod bitcoin_tx;
14pub mod cardano_tx;
15pub mod tx;
16
17pub const MOCK_SPELL_VK: &str = "7c38e8639a2eac0074cee920982b92376513e8940f4a7ca6859f17a728af5b0e";
18
19pub const V0_SPELL_VK: &str = "0x00e9398ac819e6dd281f81db3ada3fe5159c3cc40222b5ddb0e7584ed2327c5d";
21pub const V1_SPELL_VK: &str = "0x009f38f590ebca4c08c1e97b4064f39e4cd336eea4069669c5f5170a38a1ff97";
23pub const V2_SPELL_VK: &str = "0x00bd312b6026dbe4a2c16da1e8118d4fea31587a4b572b63155252d2daf69280";
25pub const V3_SPELL_VK: &str = "0x0034872b5af38c95fe82fada696b09a448f7ab0928273b7ac8c58ba29db774b9";
27pub const V4_SPELL_VK: &str = "0x00c707a155bf8dc18dc41db2994c214e93e906a3e97b4581db4345b3edd837c5";
29pub const V5_SPELL_VK: &str = "0x00e98665c417bd2e6e81c449af63b26ed5ad5c400ef55811b592450bf62c67cd";
31pub const V6_SPELL_VK: &str = "0x005a1df17094445572e4dd474b3e5dd9093936cba62ca3a62bb2ce63d9db8cba";
33pub const V7_SPELL_VK: &str = "0x0041d9843ec25ba04797a0ce29af364389f7eda9f7126ef39390c357432ad9aa";
35
36pub const V0: u32 = 0;
38pub const V1: u32 = 1;
40pub const V2: u32 = 2;
42pub const V3: u32 = 3;
44pub const V4: u32 = 4;
46pub const V5: u32 = 5;
48pub const V6: u32 = 6;
50pub const V7: u32 = 7;
52pub const V8: u32 = 8;
54
55pub const CURRENT_VERSION: u32 = V8;
57
58pub type NormalizedCharms = BTreeMap<u32, Data>;
61
62#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
64pub struct NormalizedTransaction {
65 #[serde(skip_serializing_if = "Option::is_none")]
68 pub ins: Option<Vec<UtxoId>>,
69
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub refs: Option<Vec<UtxoId>>,
73
74 pub outs: Vec<NormalizedCharms>,
80
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub beamed_outs: Option<BTreeMap<u32, B32>>,
84
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub coins: Option<Vec<NativeOutput>>,
88}
89
90impl NormalizedTransaction {
91 pub fn prev_txids(&self) -> Option<BTreeSet<&TxId>> {
94 self.ins
95 .as_ref()
96 .map(|ins| ins.iter().map(|utxo_id| &utxo_id.0).collect())
97 }
98}
99
100pub type Proof = Vec<u8>;
102
103#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
106pub struct NormalizedSpell {
107 pub version: u32,
109 pub tx: NormalizedTransaction,
111 pub app_public_inputs: BTreeMap<App, Data>,
113 #[serde(skip_serializing_if = "std::ops::Not::not", default)]
115 pub mock: bool,
116}
117
118impl Default for NormalizedSpell {
119 fn default() -> Self {
120 Self {
121 version: CURRENT_VERSION,
122 tx: Default::default(),
123 app_public_inputs: Default::default(),
124 mock: false,
125 }
126 }
127}
128
129pub fn utxo_id_hash(utxo_id: &UtxoId) -> B32 {
130 let hash = Sha256::digest(utxo_id.to_bytes());
131 B32(hash.into())
132}
133
134#[tracing::instrument(level = "debug", skip(prev_txs, spell_vk))]
136pub fn prev_spells(
137 prev_txs: &[Tx],
138 spell_vk: &str,
139 mock: bool,
140) -> BTreeMap<TxId, (NormalizedSpell, usize)> {
141 prev_txs
142 .iter()
143 .map(|tx| {
144 (
145 tx.tx_id(),
146 (
147 extended_normalized_spell(spell_vk, tx, mock),
148 tx.tx_outs_len(),
149 ),
150 )
151 })
152 .collect()
153}
154
155#[tracing::instrument(level = "debug", skip(spell, prev_spells))]
157pub fn well_formed(
158 spell: &NormalizedSpell,
159 prev_spells: &BTreeMap<TxId, (NormalizedSpell, usize)>,
160 tx_ins_beamed_source_utxos: &BTreeMap<usize, UtxoId>,
161) -> bool {
162 check!(spell.version == CURRENT_VERSION);
163 check!(ensure_no_zero_amounts(spell).is_ok());
164 let directly_created_by_prev_txns = |utxo_id: &UtxoId| -> bool {
165 let tx_id = utxo_id.0;
166 prev_spells
167 .get(&tx_id)
168 .is_some_and(|(n_spell, num_tx_outs)| {
169 let utxo_index = utxo_id.1;
170
171 let is_beamed_out = (n_spell.tx.beamed_outs.as_ref())
172 .and_then(|beamed_outs| beamed_outs.get(&utxo_index))
173 .is_some();
174
175 utxo_index <= *num_tx_outs as u32 && !is_beamed_out
176 })
177 };
178 check!({
179 spell.tx.outs.iter().all(|n_charm| {
180 n_charm
181 .keys()
182 .all(|&i| i < spell.app_public_inputs.len() as u32)
183 })
184 });
185 let Some(tx_ins) = &spell.tx.ins else {
188 eprintln!("no tx.ins");
189 return false;
190 };
191 check!(
192 tx_ins.iter().all(directly_created_by_prev_txns)
193 && (spell.tx.refs.iter().flatten()).all(directly_created_by_prev_txns)
194 );
195 let beamed_source_utxos_point_to_placeholder_dest_utxos = tx_ins_beamed_source_utxos
196 .iter()
197 .all(|(&i, beaming_source_utxo_id)| {
198 let tx_in_utxo_id = &tx_ins[i];
199 let prev_txid = tx_in_utxo_id.0;
200 let prev_tx = prev_spells.get(&prev_txid);
201 let Some((prev_spell, _tx_outs)) = prev_tx else {
202 return false;
204 };
205 check!(
207 (prev_spell.tx.outs)
208 .get(tx_in_utxo_id.1 as usize)
209 .is_none_or(|charms| charms.is_empty())
210 );
211
212 let beaming_txid = beaming_source_utxo_id.0;
213 let beaming_utxo_index = beaming_source_utxo_id.1;
214
215 prev_spells
216 .get(&beaming_txid)
217 .and_then(|(n_spell, _tx_outs)| {
218 (n_spell.tx.beamed_outs.as_ref())
219 .and_then(|beamed_outs| beamed_outs.get(&beaming_utxo_index))
220 })
221 .is_some_and(|dest_utxo_hash| dest_utxo_hash == &utxo_id_hash(tx_in_utxo_id))
222 });
223 check!(beamed_source_utxos_point_to_placeholder_dest_utxos);
224 true
225}
226
227pub fn apps(spell: &NormalizedSpell) -> Vec<App> {
229 spell.app_public_inputs.keys().cloned().collect()
230}
231
232pub fn to_tx(
234 spell: &NormalizedSpell,
235 prev_spells: &BTreeMap<TxId, (NormalizedSpell, usize)>,
236 tx_ins_beamed_source_utxos: &BTreeMap<usize, UtxoId>,
237 prev_txs: &[Tx],
238) -> Transaction {
239 let Some(tx_ins) = &spell.tx.ins else {
240 unreachable!("self.tx.ins MUST be Some at this point");
241 };
242
243 let tx_ins_beamed_source_utxos: BTreeMap<UtxoId, UtxoId> = tx_ins_beamed_source_utxos
244 .iter()
245 .map(|(&i, utxo_id)| (tx_ins[i].clone(), utxo_id.clone()))
246 .collect();
247
248 let from_utxo_id = |utxo_id: &UtxoId| -> (UtxoId, Charms) {
249 let (prev_spell, _) = &prev_spells[&utxo_id.0];
250 let charms = charms_in_utxo(prev_spell, utxo_id)
251 .or_else(|| {
252 tx_ins_beamed_source_utxos
253 .get(utxo_id)
254 .and_then(|beam_source_utxo_id| {
255 let prev_spell = &prev_spells[&beam_source_utxo_id.0].0;
256 charms_in_utxo(&prev_spell, beam_source_utxo_id)
257 })
258 })
259 .unwrap_or_default();
260 (utxo_id.clone(), charms)
261 };
262
263 let from_normalized_charms =
264 |n_charms: &NormalizedCharms| -> Charms { charms(spell, n_charms) };
265
266 let coin_from_input = |utxo_id: &UtxoId| -> NativeOutput {
267 let (prev_spell, _) = &prev_spells[&utxo_id.0];
268 let prev_coins = prev_spell.tx.coins.as_ref().expect(
269 "coins MUST NOT be none: we used `extended_normalized_spell` to get prev_spells",
270 );
271 prev_coins[utxo_id.1 as usize].clone()
272 };
273
274 let prev_txs = prev_txs.iter().map(|tx| (tx.tx_id(), tx.into())).collect();
275
276 Transaction {
277 ins: tx_ins.iter().map(from_utxo_id).collect(),
278 refs: spell.tx.refs.iter().flatten().map(from_utxo_id).collect(),
279 outs: spell.tx.outs.iter().map(from_normalized_charms).collect(),
280 coin_ins: Some(tx_ins.iter().map(coin_from_input).collect()),
281 coin_outs: spell.tx.coins.clone(),
282 prev_txs,
283 app_public_inputs: spell.app_public_inputs.clone(),
284 }
285}
286
287fn charms_in_utxo(prev_spell: &NormalizedSpell, utxo_id: &UtxoId) -> Option<Charms> {
288 (prev_spell.tx.outs)
289 .get(utxo_id.1 as usize)
290 .map(|n_charms| charms(prev_spell, n_charms))
291}
292
293pub fn charms(spell: &NormalizedSpell, n_charms: &NormalizedCharms) -> Charms {
295 let apps = apps(spell);
296 n_charms
297 .iter()
298 .map(|(&i, data)| (apps[i as usize].clone(), data.clone()))
299 .collect()
300}
301
302#[derive(Clone, Debug, Serialize, Deserialize)]
303pub struct SpellProverInput {
304 pub self_spell_vk: String,
305 pub prev_txs: Vec<Tx>,
306 pub spell: NormalizedSpell,
307 pub tx_ins_beamed_source_utxos: BTreeMap<usize, UtxoId>,
308 pub app_input: Option<AppInput>,
309}
310
311pub fn is_correct(
313 spell: &NormalizedSpell,
314 prev_txs: &Vec<Tx>,
315 app_input: Option<AppInput>,
316 spell_vk: &str,
317 tx_ins_beamed_source_utxos: &BTreeMap<usize, UtxoId>,
318 mock: bool,
319) -> bool {
320 check!(beaming_txs_have_finality_proofs(
321 prev_txs,
322 tx_ins_beamed_source_utxos
323 ));
324
325 let prev_spells = prev_spells(&prev_txs, spell_vk, mock);
326
327 check!(well_formed(spell, &prev_spells, tx_ins_beamed_source_utxos));
328
329 let Some(prev_txids) = spell.tx.prev_txids() else {
330 unreachable!("the spell is well formed: tx.ins MUST be Some");
331 };
332 let all_prev_txids: BTreeSet<_> = tx_ins_beamed_source_utxos
333 .values()
334 .map(|u| &u.0)
335 .chain(prev_txids)
336 .collect();
337 check!(all_prev_txids == prev_spells.keys().collect());
338
339 let apps = apps(spell);
340
341 let charms_tx = to_tx(spell, &prev_spells, tx_ins_beamed_source_utxos, &prev_txs);
342 let tx_is_simple_transfer_or_app_contracts_satisfied =
343 apps.iter().all(|app| is_simple_transfer(app, &charms_tx)) && app_input.is_none()
344 || app_input.is_some_and(|app_input| {
345 apps_satisfied(&app_input, &spell.app_public_inputs, &charms_tx)
346 });
347 check!(tx_is_simple_transfer_or_app_contracts_satisfied);
348
349 true
350}
351
352fn beaming_txs_have_finality_proofs(
353 prev_txs: &Vec<Tx>,
354 tx_ins_beamed_source_utxos: &BTreeMap<usize, UtxoId>,
355) -> bool {
356 let prev_txs_by_txid: BTreeMap<TxId, Tx> = by_txid(prev_txs);
357 let beaming_source_txids: BTreeSet<&TxId> = tx_ins_beamed_source_utxos
358 .values()
359 .map(|u| &u.0)
360 .collect::<BTreeSet<_>>();
361 beaming_source_txids.iter().all(|&txid| {
362 prev_txs_by_txid
363 .get(txid)
364 .is_some_and(|tx| tx.proven_final())
365 })
366}
367
368fn apps_satisfied(
369 app_input: &AppInput,
370 app_public_inputs: &BTreeMap<App, Data>,
371 tx: &Transaction,
372) -> bool {
373 let app_runner = AppRunner::new(false);
374 app_runner
375 .run_all(
376 &app_input.app_binaries,
377 &tx,
378 app_public_inputs,
379 &app_input.app_private_inputs,
380 )
381 .expect("all apps should run successfully");
382 true
383}
384
385pub fn ensure_no_zero_amounts(norm_spell: &NormalizedSpell) -> anyhow::Result<()> {
386 let apps = apps(norm_spell);
387 for out in &norm_spell.tx.outs {
388 for (i, data) in out {
389 let app = apps
390 .get(*i as usize)
391 .ok_or(anyhow!("no app for index {}", i))?;
392 if app.tag == TOKEN {
393 ensure!(
394 data.value::<u64>()? != 0,
395 "zero output amount for app {}",
396 app
397 );
398 };
399 }
400 }
401 Ok(())
402}
403
404#[cfg(test)]
405mod test {
406 #[test]
407 fn dummy() {}
408}