chia_protocol/
spend_bundle.rs

1use crate::coin_spend::CoinSpend;
2use crate::Bytes32;
3use crate::Coin;
4use chia_bls::G2Element;
5use chia_streamable_macro::streamable;
6use chia_traits::Streamable;
7use clvm_traits::FromClvm;
8use clvmr::allocator::{NodePtr, SExp};
9use clvmr::cost::Cost;
10use clvmr::error::EvalErr;
11use clvmr::op_utils::{first, rest};
12use clvmr::Allocator;
13
14#[cfg(feature = "py-bindings")]
15use pyo3::prelude::*;
16#[cfg(feature = "py-bindings")]
17use pyo3::types::PyType;
18
19#[streamable(subclass)]
20pub struct SpendBundle {
21    coin_spends: Vec<CoinSpend>,
22    aggregated_signature: G2Element,
23}
24
25impl SpendBundle {
26    pub fn aggregate(spend_bundles: &[SpendBundle]) -> SpendBundle {
27        let mut coin_spends = Vec::<CoinSpend>::new();
28        let mut aggregated_signature = G2Element::default();
29        for sb in spend_bundles {
30            coin_spends.extend_from_slice(&sb.coin_spends[..]);
31            aggregated_signature.aggregate(&sb.aggregated_signature);
32        }
33        SpendBundle {
34            coin_spends,
35            aggregated_signature,
36        }
37    }
38
39    pub fn name(&self) -> Bytes32 {
40        self.hash().into()
41    }
42
43    pub fn additions(&self) -> Result<Vec<Coin>, EvalErr> {
44        const CREATE_COIN_COST: Cost = 1_800_000;
45        const CREATE_COIN: u8 = 51;
46
47        let mut ret = Vec::<Coin>::new();
48        let mut cost_left = 11_000_000_000;
49        let mut a = Allocator::new();
50        let checkpoint = a.checkpoint();
51
52        for cs in &self.coin_spends {
53            a.restore_checkpoint(&checkpoint);
54            let (cost, mut conds) = cs.puzzle_reveal.run(&mut a, 0, cost_left, &cs.solution)?;
55            if cost > cost_left {
56                return Err(EvalErr::CostExceeded);
57            }
58            cost_left -= cost;
59            let parent_coin_info: Bytes32 = cs.coin.coin_id();
60
61            while let Some((c, tail)) = a.next(conds) {
62                conds = tail;
63                let op = first(&a, c)?;
64                let c = rest(&a, c)?;
65                let buf = match a.sexp(op) {
66                    SExp::Atom => a.atom(op),
67                    SExp::Pair(..) => {
68                        return Err(EvalErr::InvalidOpArg(op, "invalid condition".to_string()))
69                    }
70                };
71                let buf = buf.as_ref();
72                if buf.len() != 1 {
73                    continue;
74                }
75                if buf[0] == CREATE_COIN {
76                    let (puzzle_hash, (amount, _)) = <(Bytes32, (u64, NodePtr))>::from_clvm(&a, c)
77                        .map_err(|_| {
78                            EvalErr::InvalidOpArg(c, "failed to parse spend".to_string())
79                        })?;
80                    ret.push(Coin {
81                        parent_coin_info,
82                        puzzle_hash,
83                        amount,
84                    });
85                    if CREATE_COIN_COST > cost_left {
86                        return Err(EvalErr::CostExceeded);
87                    }
88                    cost_left -= CREATE_COIN_COST;
89                }
90            }
91        }
92        Ok(ret)
93    }
94}
95
96#[cfg(feature = "py-bindings")]
97#[pymethods]
98#[allow(clippy::needless_pass_by_value)]
99impl SpendBundle {
100    #[classmethod]
101    #[pyo3(name = "aggregate")]
102    fn py_aggregate(
103        cls: &Bound<'_, PyType>,
104        py: Python<'_>,
105        spend_bundles: Vec<Self>,
106    ) -> PyResult<PyObject> {
107        let aggregated = Bound::new(py, Self::aggregate(&spend_bundles))?;
108        if aggregated.is_exact_instance(cls) {
109            Ok(aggregated.into_pyobject(py)?.unbind().into_any())
110        } else {
111            let instance = cls.call_method1("from_parent", (aggregated.into_pyobject(py)?,))?;
112            Ok(instance.into_pyobject(py)?.unbind().into_any())
113        }
114    }
115
116    #[classmethod]
117    #[pyo3(name = "from_parent")]
118    pub fn from_parent(
119        cls: &Bound<'_, PyType>,
120        py: Python<'_>,
121        spend_bundle: Self,
122    ) -> PyResult<PyObject> {
123        // Convert result into potential child class
124        let instance = cls.call(
125            (spend_bundle.coin_spends, spend_bundle.aggregated_signature),
126            None,
127        )?;
128
129        Ok(instance.into_pyobject(py)?.unbind())
130    }
131
132    #[pyo3(name = "name")]
133    fn py_name(&self) -> Bytes32 {
134        self.name()
135    }
136
137    fn removals(&self) -> Vec<Coin> {
138        let mut ret = Vec::<Coin>::with_capacity(self.coin_spends.len());
139        for cs in &self.coin_spends {
140            ret.push(cs.coin);
141        }
142        ret
143    }
144
145    #[pyo3(name = "additions")]
146    fn py_additions(&self) -> PyResult<Vec<Coin>> {
147        self.additions()
148            .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use crate::Program;
156    use rstest::rstest;
157    use std::fs;
158
159    #[rstest]
160    #[case(
161        "e3c0",
162        "fd65e4b0f21322f78d1025e8a8ff7a1df77cd40b86885b851f4572e5ce06e4ff",
163        "e3c000a395f8f69d5e263a9548f13bffb1c4b701ab8f3faa03f7647c8750d077"
164    )]
165    #[case(
166        "bb13",
167        "6b2aaee962cb1de3fdeb1f0506c02df4b9e162e2af3dd1db22048454b5122a87",
168        "bb13d1e13438736c7ba0217c7b82ee4db56a7f4fb9d22c703c2152362b2314ee"
169    )]
170    fn test_additions_ff(
171        #[case] spend_file: &str,
172        #[case] expect_parent: &str,
173        #[case] expect_ph: &str,
174    ) {
175        let spend_bytes =
176            fs::read(format!("../../ff-tests/{spend_file}.spend")).expect("read file");
177        let spend = CoinSpend::from_bytes(&spend_bytes).expect("parse CoinSpend");
178        let bundle = SpendBundle::new(vec![spend], G2Element::default());
179
180        let additions = bundle.additions().expect("additions");
181
182        assert_eq!(additions.len(), 1);
183        assert_eq!(
184            additions[0].parent_coin_info.as_ref(),
185            &hex::decode(expect_parent).expect("hex::decode")
186        );
187        assert_eq!(
188            additions[0].puzzle_hash.as_ref(),
189            &hex::decode(expect_ph).expect("hex::decode")
190        );
191        assert_eq!(additions[0].amount, 1);
192    }
193
194    fn test_impl<F: Fn(Coin, SpendBundle)>(solution: &str, body: F) {
195        let solution = hex::decode(solution).expect("hex::decode");
196        let test_coin = Coin::new(
197            hex::decode("4444444444444444444444444444444444444444444444444444444444444444")
198                .unwrap()
199                .try_into()
200                .unwrap(),
201            hex::decode("3333333333333333333333333333333333333333333333333333333333333333")
202                .unwrap()
203                .try_into()
204                .unwrap(),
205            1,
206        );
207        let spend = CoinSpend::new(
208            test_coin,
209            Program::new(vec![1_u8].into()),
210            Program::new(solution.into()),
211        );
212        let bundle = SpendBundle::new(vec![spend], G2Element::default());
213        body(test_coin, bundle);
214    }
215
216    // TODO: Once we have condition types that implement ToClvm and an Encoder
217    // that serialize directly to bytes, these test solutions can be expressed
218    // in a much more readable way
219    #[test]
220    fn test_single_create_coin() {
221        // This is a solution to the identity puzzle:
222        // ((CREATE_COIN . (222222..22 . (1 . NIL))) .
223        // ))
224        let solution = "ff\
225ff33\
226ffa02222222222222222222222222222222222222222222222222222222222222222\
227ff01\
22880\
22980";
230        test_impl(solution, |test_coin: Coin, bundle: SpendBundle| {
231            let additions = bundle.additions().expect("additions");
232
233            let new_coin = Coin::new(
234                test_coin.coin_id(),
235                hex::decode("2222222222222222222222222222222222222222222222222222222222222222")
236                    .unwrap()
237                    .try_into()
238                    .unwrap(),
239                1,
240            );
241            assert_eq!(additions, [new_coin]);
242        });
243    }
244
245    #[test]
246    fn test_invalid_condition() {
247        // This is a solution to the identity puzzle:
248        // (((1 . CREATE_COIN) . (222222..22 . (1 . NIL))) .
249        // ))
250        let solution = "ff\
251ffff0133\
252ffa02222222222222222222222222222222222222222222222222222222222222222\
253ff01\
25480\
25580";
256
257        test_impl(solution, |_test_coin, bundle: SpendBundle| {
258            assert_eq!(
259                bundle.additions().unwrap_err().to_string(),
260                "InvalidOperatorArg: invalid condition"
261            );
262        });
263    }
264
265    #[test]
266    fn test_invalid_spend() {
267        // This is a solution to the identity puzzle:
268        // ((CREATE_COIN . (222222..22 . ((1 . 1) . NIL))) .
269        // ))
270        let solution = "ff\
271ff33\
272ffa02222222222222222222222222222222222222222222222222222222222222222\
273ffff0101\
27480\
27580";
276
277        test_impl(solution, |_test_coin, bundle: SpendBundle| {
278            assert_eq!(
279                bundle.additions().unwrap_err().to_string(),
280                "InvalidOperatorArg: failed to parse spend"
281            );
282        });
283    }
284}
285
286#[cfg(all(test, feature = "serde"))]
287mod serde_tests {
288    use chia_bls::Signature;
289    use indoc::indoc;
290
291    use crate::Program;
292
293    use super::*;
294
295    #[test]
296    fn test_json_spend_bundle() -> anyhow::Result<()> {
297        let json = serde_json::to_string_pretty(&SpendBundle::new(
298            vec![CoinSpend::new(
299                Coin::new([0; 32].into(), [1; 32].into(), 42),
300                Program::from(b"abc".to_vec()),
301                Program::from(b"xyz".to_vec()),
302            )],
303            Signature::default(),
304        ))?;
305
306        let output = indoc! {r#"{
307          "coin_spends": [
308            {
309              "coin": {
310                "parent_coin_info": "0x0000000000000000000000000000000000000000000000000000000000000000",
311                "puzzle_hash": "0x0101010101010101010101010101010101010101010101010101010101010101",
312                "amount": 42
313              },
314              "puzzle_reveal": "616263",
315              "solution": "78797a"
316            }
317          ],
318          "aggregated_signature": "0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
319        }"#};
320
321        assert_eq!(json, output);
322
323        Ok(())
324    }
325}