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 serde_with::{DisplayFromStr, IfIsHumanReadable, serde_as};
10use sha2::{Digest, Sha256};
11use std::collections::{BTreeMap, BTreeSet};
12
13pub mod ark;
14pub mod bitcoin_tx;
15pub mod cardano_tx;
16pub mod sorted_app_map;
17pub mod tx;
18
19pub const MOCK_SPELL_VK: &str = "7c38e8639a2eac0074cee920982b92376513e8940f4a7ca6859f17a728af5b0e";
20
21pub const V0_SPELL_VK: &str = "0x00e9398ac819e6dd281f81db3ada3fe5159c3cc40222b5ddb0e7584ed2327c5d";
23pub const V1_SPELL_VK: &str = "0x009f38f590ebca4c08c1e97b4064f39e4cd336eea4069669c5f5170a38a1ff97";
25pub const V2_SPELL_VK: &str = "0x00bd312b6026dbe4a2c16da1e8118d4fea31587a4b572b63155252d2daf69280";
27pub const V3_SPELL_VK: &str = "0x0034872b5af38c95fe82fada696b09a448f7ab0928273b7ac8c58ba29db774b9";
29pub const V4_SPELL_VK: &str = "0x00c707a155bf8dc18dc41db2994c214e93e906a3e97b4581db4345b3edd837c5";
31pub const V5_SPELL_VK: &str = "0x00e98665c417bd2e6e81c449af63b26ed5ad5c400ef55811b592450bf62c67cd";
33pub const V6_SPELL_VK: &str = "0x005a1df17094445572e4dd474b3e5dd9093936cba62ca3a62bb2ce63d9db8cba";
35pub const V7_SPELL_VK: &str = "0x0041d9843ec25ba04797a0ce29af364389f7eda9f7126ef39390c357432ad9aa";
37pub const V8_SPELL_VK: &str = "0x00e440d40e331c16bc4c78d2dbc6bb35876e6ea944e943de359a075e07385abc";
39pub const V9_SPELL_VK: &str = "0x00713f077ec2bd68157512835dc678053565a889935ecd5789ce2fa097c93ee9";
41pub const V10_SPELL_VK: &str = "0x00ccf030317cae019a4cd3c8557b2c5b522050e7e562e3adf287cd5ad596511f";
43
44pub const V0: u32 = 0;
46pub const V1: u32 = 1;
48pub const V2: u32 = 2;
50pub const V3: u32 = 3;
52pub const V4: u32 = 4;
54pub const V5: u32 = 5;
56pub const V6: u32 = 6;
58pub const V7: u32 = 7;
60pub const V8: u32 = 8;
62pub const V9: u32 = 9;
64pub const V10: u32 = 10;
66pub const V11: u32 = 11;
68
69pub const CURRENT_VERSION: u32 = V11;
71
72#[serde_as]
77#[cfg_attr(test, derive(test_strategy::Arbitrary))]
78#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
79pub struct BeamSource(
80 #[serde_as(as = "IfIsHumanReadable<DisplayFromStr>")] pub UtxoId,
81 #[serde(skip_serializing_if = "Option::is_none", default)] pub Option<u64>,
82);
83
84pub type NormalizedCharms = BTreeMap<u32, Data>;
87
88#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
90pub struct NormalizedTransaction {
91 #[serde(skip_serializing_if = "Option::is_none")]
94 pub ins: Option<Vec<UtxoId>>,
95
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub refs: Option<Vec<UtxoId>>,
99
100 pub outs: Vec<NormalizedCharms>,
106
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub beamed_outs: Option<BTreeMap<u32, B32>>,
110
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub coins: Option<Vec<NativeOutput>>,
114}
115
116impl NormalizedTransaction {
117 pub fn prev_txids(&self) -> Option<BTreeSet<&TxId>> {
120 self.ins
121 .as_ref()
122 .map(|ins| ins.iter().map(|utxo_id| &utxo_id.0).collect())
123 }
124}
125
126pub type Proof = Vec<u8>;
128
129#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
132pub struct NormalizedSpell {
133 pub version: u32,
135 pub tx: NormalizedTransaction,
137 #[serde(deserialize_with = "sorted_app_map::deserialize")]
139 pub app_public_inputs: BTreeMap<App, Data>,
140 #[serde(skip_serializing_if = "std::ops::Not::not", default)]
142 pub mock: bool,
143}
144
145impl Default for NormalizedSpell {
146 fn default() -> Self {
147 Self {
148 version: CURRENT_VERSION,
149 tx: Default::default(),
150 app_public_inputs: Default::default(),
151 mock: false,
152 }
153 }
154}
155
156pub fn utxo_id_hash(utxo_id: &UtxoId) -> B32 {
157 let hash = Sha256::digest(utxo_id.to_bytes());
158 B32(hash.into())
159}
160
161pub fn utxo_id_hash_with_nonce(utxo_id: &UtxoId, nonce: Option<u64>) -> B32 {
162 let mut hash = Sha256::default();
163 hash.update(utxo_id.to_bytes());
164 if let Some(nonce) = nonce {
165 hash.update(&nonce.to_le_bytes());
166 }
167 B32(hash.finalize().into())
168}
169
170#[tracing::instrument(level = "debug", skip(prev_txs, spell_vk))]
172pub fn prev_spells(
173 prev_txs: &[Tx],
174 spell_vk: &str,
175 mock: bool,
176) -> anyhow::Result<BTreeMap<TxId, (NormalizedSpell, usize)>> {
177 prev_txs
178 .iter()
179 .map(|tx| {
180 Ok((
181 tx.tx_id(),
182 (
183 extended_normalized_spell(spell_vk, tx, mock)?,
184 tx.tx_outs_len(),
185 ),
186 ))
187 })
188 .collect()
189}
190
191#[tracing::instrument(level = "debug", skip(spell, prev_spells))]
193pub fn well_formed(
194 spell: &NormalizedSpell,
195 prev_spells: &BTreeMap<TxId, (NormalizedSpell, usize)>,
196 tx_ins_beamed_source_utxos: &BTreeMap<usize, BeamSource>,
197) -> bool {
198 check!(spell.version == CURRENT_VERSION);
199 check!(ensure_no_zero_amounts(spell).is_ok());
200 let directly_created_by_prev_txns = |utxo_id: &UtxoId| -> bool {
201 let tx_id = utxo_id.0;
202 prev_spells
203 .get(&tx_id)
204 .is_some_and(|(n_spell, num_tx_outs)| {
205 let utxo_index = utxo_id.1;
206
207 let is_beamed_out = beamed_out_to_hash(n_spell, utxo_index).is_some();
208
209 utxo_index <= *num_tx_outs as u32 && !is_beamed_out
210 })
211 };
212 check!({
213 spell.tx.outs.iter().all(|n_charm| {
214 n_charm
215 .keys()
216 .all(|&i| i < spell.app_public_inputs.len() as u32)
217 })
218 });
219 let Some(tx_ins) = &spell.tx.ins else {
222 eprintln!("no tx.ins");
223 return false;
224 };
225 check!(
226 tx_ins.iter().all(directly_created_by_prev_txns)
227 && (spell.tx.refs.iter().flatten()).all(directly_created_by_prev_txns)
228 );
229 let beamed_source_utxos_point_to_placeholder_dest_utxos = tx_ins_beamed_source_utxos
230 .iter()
231 .all(|(&i, beaming_source)| {
232 let tx_in_utxo_id = &tx_ins[i];
233 let prev_txid = tx_in_utxo_id.0;
234 let prev_tx = prev_spells.get(&prev_txid);
235 let Some((prev_spell, _tx_outs)) = prev_tx else {
236 return false;
238 };
239 check!(
241 (prev_spell.tx.outs)
242 .get(tx_in_utxo_id.1 as usize)
243 .is_none_or(|charms| charms.is_empty()
244 || beamed_out_to_hash(prev_spell, tx_in_utxo_id.1).is_some())
245 );
246
247 let beaming_source_utxo_id = &beaming_source.0;
248 let beaming_txid = beaming_source_utxo_id.0;
249 let beaming_utxo_index = beaming_source_utxo_id.1;
250
251 prev_spells
252 .get(&beaming_txid)
253 .and_then(|(n_spell, _tx_outs)| beamed_out_to_hash(n_spell, beaming_utxo_index))
254 .is_some_and(|dest_utxo_hash| {
255 dest_utxo_hash == &utxo_id_hash_with_nonce(tx_in_utxo_id, beaming_source.1)
256 })
257 });
258 check!(beamed_source_utxos_point_to_placeholder_dest_utxos);
259 true
260}
261
262pub fn apps(spell: &NormalizedSpell) -> Vec<App> {
264 spell.app_public_inputs.keys().cloned().collect()
265}
266
267pub fn to_tx(
269 spell: &NormalizedSpell,
270 prev_spells: &BTreeMap<TxId, (NormalizedSpell, usize)>,
271 tx_ins_beamed_source_utxos: &BTreeMap<usize, BeamSource>,
272 prev_txs: &[Tx],
273) -> Transaction {
274 let Some(tx_ins) = &spell.tx.ins else {
275 unreachable!("self.tx.ins MUST be Some at this point");
276 };
277
278 let tx_ins_beamed_source_utxos: BTreeMap<UtxoId, UtxoId> = tx_ins_beamed_source_utxos
279 .iter()
280 .map(|(&i, bs)| (tx_ins[i].clone(), bs.0.clone()))
281 .collect();
282
283 let from_utxo_id = |utxo_id: &UtxoId| -> (UtxoId, Charms) {
284 let (prev_spell, _) = &prev_spells[&utxo_id.0];
285 let charms = charms_in_utxo(prev_spell, utxo_id)
286 .or_else(|| {
287 tx_ins_beamed_source_utxos
288 .get(utxo_id)
289 .and_then(|beam_source_utxo_id| {
290 let prev_spell = &prev_spells[&beam_source_utxo_id.0].0;
291 charms_in_utxo(&prev_spell, beam_source_utxo_id)
292 })
293 })
294 .unwrap_or_default();
295 (utxo_id.clone(), charms)
296 };
297
298 let from_normalized_charms =
299 |n_charms: &NormalizedCharms| -> Charms { charms(spell, n_charms) };
300
301 let coin_from_input = |utxo_id: &UtxoId| -> NativeOutput {
302 let (prev_spell, _) = &prev_spells[&utxo_id.0];
303 let prev_coins = prev_spell.tx.coins.as_ref().expect(
304 "coins MUST NOT be none: we used `extended_normalized_spell` to get prev_spells",
305 );
306 prev_coins[utxo_id.1 as usize].clone()
307 };
308
309 let prev_txs = prev_txs.iter().map(|tx| (tx.tx_id(), tx.into())).collect();
310
311 Transaction {
312 ins: tx_ins.iter().map(from_utxo_id).collect(),
313 refs: spell.tx.refs.iter().flatten().map(from_utxo_id).collect(),
314 outs: spell.tx.outs.iter().map(from_normalized_charms).collect(),
315 coin_ins: Some(tx_ins.iter().map(coin_from_input).collect()),
316 coin_outs: spell.tx.coins.clone(),
317 prev_txs,
318 app_public_inputs: spell.app_public_inputs.clone(),
319 }
320}
321
322fn charms_in_utxo(prev_spell: &NormalizedSpell, utxo_id: &UtxoId) -> Option<Charms> {
323 (prev_spell.tx.outs)
324 .get(utxo_id.1 as usize)
325 .map(|n_charms| charms(prev_spell, n_charms))
326}
327
328pub fn charms(spell: &NormalizedSpell, n_charms: &NormalizedCharms) -> Charms {
330 let apps = apps(spell);
331 n_charms
332 .iter()
333 .map(|(&i, data)| (apps[i as usize].clone(), data.clone()))
334 .collect()
335}
336
337#[derive(Clone, Debug, Serialize, Deserialize)]
338pub struct SpellProverInput {
339 pub self_spell_vk: String,
340 pub prev_txs: Vec<Tx>,
341 pub spell: NormalizedSpell,
342 pub tx_ins_beamed_source_utxos: BTreeMap<usize, BeamSource>,
343 pub app_input: Option<AppInput>,
344}
345
346pub fn is_correct(
348 spell: &NormalizedSpell,
349 prev_txs: &Vec<Tx>,
350 app_input: Option<AppInput>,
351 spell_vk: &str,
352 tx_ins_beamed_source_utxos: &BTreeMap<usize, BeamSource>,
353) -> anyhow::Result<bool> {
354 ensure!(beaming_txs_have_finality_proofs(
355 prev_txs,
356 tx_ins_beamed_source_utxos
357 ));
358
359 let prev_spells = prev_spells(&prev_txs, spell_vk, spell.mock)?;
360
361 ensure!(well_formed(spell, &prev_spells, tx_ins_beamed_source_utxos));
362
363 let Some(prev_txids) = spell.tx.prev_txids() else {
364 unreachable!("the spell is well formed: tx.ins MUST be Some");
365 };
366 let all_prev_txids: BTreeSet<_> = tx_ins_beamed_source_utxos
367 .values()
368 .map(|bs| &(bs.0).0)
369 .chain(prev_txids)
370 .collect();
371 ensure!(all_prev_txids == prev_spells.keys().collect());
372
373 let apps = apps(spell);
374
375 let charms_tx = to_tx(spell, &prev_spells, tx_ins_beamed_source_utxos, &prev_txs);
376 let tx_is_simple_transfer_or_app_contracts_satisfied =
377 apps.iter().all(|app| is_simple_transfer(app, &charms_tx)) && app_input.is_none()
378 || app_input.is_some_and(|app_input| {
379 apps_satisfied(&app_input, &spell.app_public_inputs, &charms_tx)
380 });
381 ensure!(tx_is_simple_transfer_or_app_contracts_satisfied);
382
383 Ok(true)
384}
385
386fn beaming_txs_have_finality_proofs(
387 prev_txs: &Vec<Tx>,
388 tx_ins_beamed_source_utxos: &BTreeMap<usize, BeamSource>,
389) -> bool {
390 let prev_txs_by_txid: BTreeMap<TxId, Tx> = by_txid(prev_txs);
391 let beaming_source_txids: BTreeSet<&TxId> = tx_ins_beamed_source_utxos
392 .values()
393 .map(|bs| &(bs.0).0)
394 .collect::<BTreeSet<_>>();
395 beaming_source_txids.iter().all(|&txid| {
396 prev_txs_by_txid
397 .get(txid)
398 .is_some_and(|tx| tx.proven_final())
399 })
400}
401
402fn apps_satisfied(
403 app_input: &AppInput,
404 app_public_inputs: &BTreeMap<App, Data>,
405 tx: &Transaction,
406) -> bool {
407 let app_runner = AppRunner::new(false);
408 app_runner
409 .run_all(
410 &app_input.app_binaries,
411 &tx,
412 app_public_inputs,
413 &app_input.app_private_inputs,
414 )
415 .expect("all apps should run successfully");
416 true
417}
418
419pub fn ensure_no_zero_amounts(norm_spell: &NormalizedSpell) -> anyhow::Result<()> {
420 let apps = apps(norm_spell);
421 for out in &norm_spell.tx.outs {
422 for (i, data) in out {
423 let app = apps
424 .get(*i as usize)
425 .ok_or(anyhow!("no app for index {}", i))?;
426 if app.tag == TOKEN {
427 ensure!(
428 data.value::<u64>()? != 0,
429 "zero output amount for app {}",
430 app
431 );
432 };
433 }
434 }
435 Ok(())
436}
437
438pub fn beamed_out_to_hash(spell: &NormalizedSpell, i: u32) -> Option<&B32> {
439 (spell.tx.beamed_outs)
440 .as_ref()
441 .and_then(|beamed| beamed.get(&i))
442}
443
444#[cfg(test)]
445mod test {
446 use crate::BeamSource;
447 use charms_data::UtxoId;
448 use test_strategy::proptest;
449
450 #[test]
451 fn dummy() {}
452
453 #[proptest]
454 fn beaming_source_json_parse_with_nonce(utxo_id: UtxoId, nonce: u64) {
455 let s0 = format!(r#"["{}",{}]"#, utxo_id, nonce);
456 let bs: BeamSource = serde_json::from_str(&s0).unwrap();
457 let s1 = serde_json::to_string(&bs).unwrap();
458 assert_eq!(s1, s0);
459 }
460
461 #[proptest]
462 fn beaming_source_json_parse_no_nonce(utxo_id: UtxoId) {
463 let s0 = format!(r#"["{}"]"#, utxo_id);
464 let bs: BeamSource = serde_json::from_str(&s0).unwrap();
465 let s1 = serde_json::to_string(&bs).unwrap();
466 assert_eq!(s1, s0);
467 }
468
469 #[proptest]
470 fn beaming_source_json_print(bs0: BeamSource) {
471 let s = serde_json::to_string(&bs0).unwrap();
472 let bs1: BeamSource = serde_json::from_str(&s).unwrap();
473 assert_eq!(bs1, bs0);
474 }
475}