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