Skip to main content

charms_client/
tx.rs

1use crate::{
2    CURRENT_VERSION, MOCK_SPELL_VK, NormalizedSpell, V0, V0_SPELL_VK, V1, V1_SPELL_VK, V2,
3    V2_SPELL_VK, V3, V3_SPELL_VK, V4, V4_SPELL_VK, V5, V5_SPELL_VK, V6, V6_SPELL_VK, V7,
4    V7_SPELL_VK, V8, V8_SPELL_VK, V9, V9_SPELL_VK, V10, V10_SPELL_VK, ark, bitcoin_tx::BitcoinTx,
5    cardano_tx::CardanoTx,
6};
7use anyhow::{anyhow, bail};
8use charms_data::{NativeOutput, TxId, UtxoId, util};
9use enum_dispatch::enum_dispatch;
10use serde::{Deserialize, Serialize};
11use sp1_primitives::io::SP1PublicValues;
12use sp1_verifier::Groth16Verifier;
13use std::collections::BTreeMap;
14use strum::{AsRefStr, EnumDiscriminants, EnumString};
15
16#[enum_dispatch]
17pub trait EnchantedTx {
18    fn extract_and_verify_spell(
19        &self,
20        spell_vk: &str,
21        mock: bool,
22    ) -> anyhow::Result<NormalizedSpell>;
23    fn tx_outs_len(&self) -> usize;
24    fn tx_id(&self) -> TxId;
25    fn hex(&self) -> String;
26    fn spell_ins(&self) -> Vec<UtxoId>;
27    fn all_coin_outs(&self, spell: &NormalizedSpell) -> anyhow::Result<Vec<NativeOutput>>;
28    fn proven_final(&self) -> bool;
29}
30
31#[enum_dispatch(EnchantedTx)]
32#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, EnumDiscriminants)]
33#[serde(rename_all = "snake_case")]
34#[strum_discriminants(
35    name(Chain),
36    derive(AsRefStr, EnumString, Ord, PartialOrd, Serialize, Deserialize),
37    serde(rename_all = "snake_case"),
38    strum(serialize_all = "snake_case")
39)]
40pub enum Tx {
41    Bitcoin(BitcoinTx),
42    Cardano(CardanoTx),
43}
44
45impl TryFrom<&str> for Tx {
46    type Error = anyhow::Error;
47
48    fn try_from(hex: &str) -> Result<Self, Self::Error> {
49        if let Ok(b_tx) = BitcoinTx::from_hex(hex) {
50            Ok(Self::Bitcoin(b_tx))
51        } else if let Ok(c_tx) = CardanoTx::from_hex(hex) {
52            Ok(Self::Cardano(c_tx))
53        } else {
54            bail!("invalid hex")
55        }
56    }
57}
58
59impl Tx {
60    pub fn new(tx: impl Into<Tx>) -> Self {
61        tx.into()
62    }
63
64    pub fn hex(&self) -> String {
65        match self {
66            Tx::Bitcoin(tx) => tx.hex(),
67            Tx::Cardano(tx) => tx.hex(),
68        }
69    }
70}
71
72/// Extract a [`NormalizedSpell`] from a transaction and verify it.
73/// Incorrect spells are rejected.
74#[tracing::instrument(level = "debug", skip_all)]
75pub fn committed_normalized_spell(
76    spell_vk: &str,
77    tx: &Tx,
78    mock: bool,
79) -> anyhow::Result<NormalizedSpell> {
80    tx.extract_and_verify_spell(spell_vk, mock)
81}
82
83/// Extract and verify [`NormalizedSpell`] from a transaction. Return an empty spell if the
84/// transaction does not have one. Extend with native coin output amounts if necessary.
85pub fn extended_normalized_spell(
86    spell_vk: &str,
87    tx: &Tx,
88    mock: bool,
89) -> anyhow::Result<NormalizedSpell> {
90    match tx.extract_and_verify_spell(spell_vk, mock) {
91        Ok(mut spell) => {
92            spell.tx.coins = Some(tx.all_coin_outs(&spell)?);
93            Ok(spell)
94        }
95        Err(_) => {
96            let mut spell = NormalizedSpell::default();
97            spell.tx.ins = Some(tx.spell_ins());
98            spell.tx.outs = vec![];
99            spell.tx.coins = Some(tx.all_coin_outs(&spell)?);
100            Ok(spell)
101        }
102    }
103}
104
105pub fn spell_vk(spell_version: u32, spell_vk: &str, mock: bool) -> anyhow::Result<&str> {
106    if mock {
107        return Ok(MOCK_SPELL_VK);
108    }
109    match spell_version {
110        CURRENT_VERSION => Ok(spell_vk),
111        V10 => Ok(V10_SPELL_VK),
112        V9 => Ok(V9_SPELL_VK),
113        V8 => Ok(V8_SPELL_VK),
114        V7 => Ok(V7_SPELL_VK),
115        V6 => Ok(V6_SPELL_VK),
116        V5 => Ok(V5_SPELL_VK),
117        V4 => Ok(V4_SPELL_VK),
118        V3 => Ok(V3_SPELL_VK),
119        V2 => Ok(V2_SPELL_VK),
120        V1 => Ok(V1_SPELL_VK),
121        V0 => Ok(V0_SPELL_VK),
122        _ => bail!("unsupported spell version: {}", spell_version),
123    }
124}
125
126pub fn groth16_vk(spell_version: u32, mock: bool) -> anyhow::Result<&'static [u8]> {
127    if mock {
128        return Ok(MOCK_GROTH16_VK_BYTES);
129    }
130    match spell_version {
131        CURRENT_VERSION => Ok(CURRENT_GROTH16_VK_BYTES),
132        V10 => Ok(V10_GROTH16_VK_BYTES),
133        V9 => Ok(V9_GROTH16_VK_BYTES),
134        V8 => Ok(V8_GROTH16_VK_BYTES),
135        V7 => Ok(V7_GROTH16_VK_BYTES),
136        V6 => Ok(V6_GROTH16_VK_BYTES),
137        V5 => Ok(V5_GROTH16_VK_BYTES),
138        V4 => Ok(V4_GROTH16_VK_BYTES),
139        V3 => Ok(V3_GROTH16_VK_BYTES),
140        V2 => Ok(V2_GROTH16_VK_BYTES),
141        V1 => Ok(V1_GROTH16_VK_BYTES),
142        V0 => Ok(V0_GROTH16_VK_BYTES),
143        _ => bail!("unsupported spell version: {}", spell_version),
144    }
145}
146
147pub const MOCK_GROTH16_VK_BYTES: &'static [u8] = include_bytes!("../vk/mock/mock-groth16-vk.bin");
148
149pub const V0_GROTH16_VK_BYTES: &'static [u8] = include_bytes!("../vk/v0/groth16_vk.bin");
150pub const V1_GROTH16_VK_BYTES: &'static [u8] = include_bytes!("../vk/v1/groth16_vk.bin");
151pub const V2_GROTH16_VK_BYTES: &'static [u8] = V1_GROTH16_VK_BYTES;
152pub const V3_GROTH16_VK_BYTES: &'static [u8] = V1_GROTH16_VK_BYTES;
153pub const V4_GROTH16_VK_BYTES: &'static [u8] = include_bytes!("../vk/v4/groth16_vk.bin");
154pub const V5_GROTH16_VK_BYTES: &'static [u8] = V4_GROTH16_VK_BYTES;
155pub const V6_GROTH16_VK_BYTES: &'static [u8] = V4_GROTH16_VK_BYTES;
156pub const V7_GROTH16_VK_BYTES: &'static [u8] = V4_GROTH16_VK_BYTES;
157pub const V8_GROTH16_VK_BYTES: &'static [u8] = V4_GROTH16_VK_BYTES;
158pub const V9_GROTH16_VK_BYTES: &'static [u8] = V4_GROTH16_VK_BYTES;
159pub const V10_GROTH16_VK_BYTES: &'static [u8] = V4_GROTH16_VK_BYTES;
160pub const V11_GROTH16_VK_BYTES: &'static [u8] = V4_GROTH16_VK_BYTES;
161pub const CURRENT_GROTH16_VK_BYTES: &'static [u8] = V11_GROTH16_VK_BYTES;
162
163pub fn to_serialized_pv<T: Serialize>(spell_version: u32, t: &T) -> Vec<u8> {
164    match spell_version {
165        CURRENT_VERSION | V10 | V9 | V8 | V7 | V6 | V5 | V4 | V3 | V2 | V1 => {
166            // we commit to CBOR-encoded tuple `(spell_vk, n_spell)`
167            util::write(t).unwrap()
168        }
169        V0 => {
170            // we used to commit to the tuple `(spell_vk, n_spell)`, which was serialized internally
171            // by SP1
172            let mut pv = SP1PublicValues::new();
173            pv.write(t);
174            pv.to_vec()
175        }
176        _ => unreachable!(),
177    }
178}
179
180pub fn verify_snark_proof(
181    proof: &[u8],
182    public_inputs: &[u8],
183    vk_hash: &str,
184    spell_version: u32,
185    mock: bool,
186) -> anyhow::Result<()> {
187    let groth16_vk = groth16_vk(spell_version, mock)?;
188    match mock {
189        false => Groth16Verifier::verify(proof, public_inputs, vk_hash, groth16_vk)
190            .map_err(|e| anyhow!("could not verify spell proof: {}", e)),
191        true => ark::verify_groth16_proof(proof, public_inputs, groth16_vk),
192    }
193}
194
195pub fn by_txid(prev_txs: &[Tx]) -> BTreeMap<TxId, Tx> {
196    prev_txs
197        .iter()
198        .map(|prev_tx| (prev_tx.tx_id(), prev_tx.clone()))
199        .collect::<BTreeMap<_, _>>()
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use std::str::FromStr;
206
207    #[test]
208    fn chain_names() {
209        assert_eq!(Chain::Bitcoin.as_ref(), "bitcoin");
210        assert_eq!(Chain::Cardano.as_ref(), "cardano");
211    }
212
213    #[test]
214    fn chain_name_from_str() {
215        assert_eq!(Chain::from_str("bitcoin").unwrap(), Chain::Bitcoin);
216        assert_eq!(Chain::from_str("cardano").unwrap(), Chain::Cardano);
217    }
218
219    #[test]
220    fn chain_name_deserialize() {
221        assert_eq!(
222            serde_json::from_str::<Chain>(r#""bitcoin""#).unwrap(),
223            Chain::Bitcoin
224        );
225        assert_eq!(
226            serde_json::from_str::<Chain>(r#""cardano""#).unwrap(),
227            Chain::Cardano
228        );
229    }
230
231    #[test]
232    fn chain_name_serialize() {
233        assert_eq!(
234            serde_json::to_string(&Chain::Bitcoin).unwrap(),
235            r#""bitcoin""#
236        );
237        assert_eq!(
238            serde_json::to_string(&Chain::Cardano).unwrap(),
239            r#""cardano""#
240        );
241    }
242
243    #[test]
244    fn ser_to_json() {
245        let c_tx_hex = "84a400d901028182582011a2338987035057f6c36286cf5aadc02573059b2cde9790017eb4e148f0c67a0001828258390174f84e13070bb755eaa01cb717da8c7450daf379948e979f6de99d26ba89ff199fde572546b9a044eb129ad2edb184bd79cde63ab4b47aec1a01312d008258390184f1c3b1fff5241088acc4ce0aec81f45a71a70e35c94e30a70b7cdfeb0785cdec744029db6b4f344b1123497c9cabfeeb94af20fcfddfe01a33e578fd021a000299e90758201e8eb8575d879922d701c12daa7366cb71b6518a9500e083a966a8e66b56ed23a10081825820ea444825bbd5cc97b6c795437849fe55694b52e2f51485ac76ca2d9f991e83305840d59db4fa0b4bb233504f5e6826261a2e18b2e22cb3df4f631ab77d94d62e8df3200536271f3f3a625bc86919714972964f070f909f145b342f2889f58ccc210ff5a11902a2a1636d736765546f6b656f";
246
247        let b_tx_hex = "0200000000010115ccf0534b7969e5ac0f4699e51bf7805168244057059caa333397fcf8a9acdd0000000000fdffffff027a6faf85150000001600147b458433d0c04323426ef88365bd4cfef141ac7520a107000000000022512087a397fc19d816b6f938dad182a54c778d2d5db8b31f4528a758b989d42f0b78024730440220072d64b2e3bbcd27bd79cb8859c83ca524dad60dc6310569c2a04c997d116381022071d4df703d037a9fe16ccb1a2b8061f10cda86ccbb330a49c5dcc95197436c960121030db9616d96a7b7a8656191b340f77e905ee2885a09a7a1e80b9c8b64ec746fb300000000";
248
249        let c_tx: Tx = Tx::try_from(c_tx_hex).unwrap();
250        let Tx::Cardano(_) = c_tx.clone() else {
251            unreachable!("not a cardano tx: {c_tx:?}")
252        };
253        let b_tx: Tx = Tx::try_from(b_tx_hex).unwrap();
254        let Tx::Bitcoin(_) = b_tx.clone() else {
255            unreachable!("not a bitcoin tx: {b_tx:?}")
256        };
257
258        let v = vec![b_tx, c_tx];
259        let json_str = serde_json::to_string_pretty(&v).unwrap();
260        eprintln!("{json_str}");
261    }
262
263    #[test]
264    fn ser_to_cbor() {
265        let c_tx_hex = "84a400d901028182582011a2338987035057f6c36286cf5aadc02573059b2cde9790017eb4e148f0c67a0001828258390174f84e13070bb755eaa01cb717da8c7450daf379948e979f6de99d26ba89ff199fde572546b9a044eb129ad2edb184bd79cde63ab4b47aec1a01312d008258390184f1c3b1fff5241088acc4ce0aec81f45a71a70e35c94e30a70b7cdfeb0785cdec744029db6b4f344b1123497c9cabfeeb94af20fcfddfe01a33e578fd021a000299e90758201e8eb8575d879922d701c12daa7366cb71b6518a9500e083a966a8e66b56ed23a10081825820ea444825bbd5cc97b6c795437849fe55694b52e2f51485ac76ca2d9f991e83305840d59db4fa0b4bb233504f5e6826261a2e18b2e22cb3df4f631ab77d94d62e8df3200536271f3f3a625bc86919714972964f070f909f145b342f2889f58ccc210ff5a11902a2a1636d736765546f6b656f";
266
267        let b_tx_hex = "0200000000010115ccf0534b7969e5ac0f4699e51bf7805168244057059caa333397fcf8a9acdd0000000000fdffffff027a6faf85150000001600147b458433d0c04323426ef88365bd4cfef141ac7520a107000000000022512087a397fc19d816b6f938dad182a54c778d2d5db8b31f4528a758b989d42f0b78024730440220072d64b2e3bbcd27bd79cb8859c83ca524dad60dc6310569c2a04c997d116381022071d4df703d037a9fe16ccb1a2b8061f10cda86ccbb330a49c5dcc95197436c960121030db9616d96a7b7a8656191b340f77e905ee2885a09a7a1e80b9c8b64ec746fb300000000";
268
269        let c_tx: Tx = Tx::try_from(c_tx_hex).unwrap();
270        let b_tx: Tx = Tx::try_from(b_tx_hex).unwrap();
271
272        let v0 = vec![b_tx, c_tx];
273        let v0_cbor = ciborium::Value::serialized(&v0).unwrap();
274
275        let v1: Vec<Tx> = ciborium::Value::deserialized(&v0_cbor).unwrap();
276        let v1_cbor = ciborium::Value::serialized(&v1).unwrap();
277        assert_eq!(v0, v1);
278        assert_eq!(v0_cbor, v1_cbor);
279    }
280}