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