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