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