chia_protocol/
spend_bundle.rs

1use crate::Bytes32;
2use crate::Coin;
3use crate::coin_spend::CoinSpend;
4use chia_bls::G2Element;
5use chia_streamable_macro::streamable;
6use chia_traits::Streamable;
7use clvm_traits::FromClvm;
8use clvmr::Allocator;
9use clvmr::allocator::{NodePtr, SExp};
10use clvmr::cost::Cost;
11use clvmr::error::EvalErr;
12use clvmr::op_utils::{first, rest};
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<Py<PyAny>> {
107        let aggregated = Bound::new(py, Self::aggregate(&spend_bundles))?;
108        if aggregated.is_exact_instance(cls) {
109            Ok(aggregated.into_any().unbind())
110        } else {
111            let aggregated_py = aggregated.into_any().unbind();
112            let instance = cls.call_method1("from_parent", (Py::clone_ref(&aggregated_py, py),))?;
113            Ok(instance.into_any().unbind())
114        }
115    }
116
117    #[classmethod]
118    #[pyo3(name = "from_parent")]
119    pub fn from_parent(cls: &Bound<'_, PyType>, spend_bundle: Self) -> PyResult<Py<PyAny>> {
120        // Convert result into potential child class
121        let instance = cls.call(
122            (spend_bundle.coin_spends, spend_bundle.aggregated_signature),
123            None,
124        )?;
125
126        Ok(instance.into_any().unbind())
127    }
128
129    #[pyo3(name = "name")]
130    fn py_name(&self) -> Bytes32 {
131        self.name()
132    }
133
134    fn removals(&self) -> Vec<Coin> {
135        let mut ret = Vec::<Coin>::with_capacity(self.coin_spends.len());
136        for cs in &self.coin_spends {
137            ret.push(cs.coin);
138        }
139        ret
140    }
141
142    #[pyo3(name = "additions")]
143    fn py_additions(&self) -> PyResult<Vec<Coin>> {
144        self.additions()
145            .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use crate::Program;
153    use rstest::rstest;
154    use std::fs;
155
156    #[rstest]
157    #[case(
158        "e3c0",
159        "fd65e4b0f21322f78d1025e8a8ff7a1df77cd40b86885b851f4572e5ce06e4ff",
160        "e3c000a395f8f69d5e263a9548f13bffb1c4b701ab8f3faa03f7647c8750d077"
161    )]
162    #[case(
163        "bb13",
164        "6b2aaee962cb1de3fdeb1f0506c02df4b9e162e2af3dd1db22048454b5122a87",
165        "bb13d1e13438736c7ba0217c7b82ee4db56a7f4fb9d22c703c2152362b2314ee"
166    )]
167    fn test_additions_ff(
168        #[case] spend_file: &str,
169        #[case] expect_parent: &str,
170        #[case] expect_ph: &str,
171    ) {
172        let spend_bytes =
173            fs::read(format!("../../ff-tests/{spend_file}.spend")).expect("read file");
174        let spend = CoinSpend::from_bytes(&spend_bytes).expect("parse CoinSpend");
175        let bundle = SpendBundle::new(vec![spend], G2Element::default());
176
177        let additions = bundle.additions().expect("additions");
178
179        assert_eq!(additions.len(), 1);
180        assert_eq!(
181            additions[0].parent_coin_info.as_ref(),
182            &hex::decode(expect_parent).expect("hex::decode")
183        );
184        assert_eq!(
185            additions[0].puzzle_hash.as_ref(),
186            &hex::decode(expect_ph).expect("hex::decode")
187        );
188        assert_eq!(additions[0].amount, 1);
189    }
190
191    fn test_impl<F: Fn(Coin, SpendBundle)>(solution: &str, body: F) {
192        let solution = hex::decode(solution).expect("hex::decode");
193        let test_coin = Coin::new(
194            hex::decode("4444444444444444444444444444444444444444444444444444444444444444")
195                .unwrap()
196                .try_into()
197                .unwrap(),
198            hex::decode("3333333333333333333333333333333333333333333333333333333333333333")
199                .unwrap()
200                .try_into()
201                .unwrap(),
202            1,
203        );
204        let spend = CoinSpend::new(
205            test_coin,
206            Program::new(vec![1_u8].into()),
207            Program::new(solution.into()),
208        );
209        let bundle = SpendBundle::new(vec![spend], G2Element::default());
210        body(test_coin, bundle);
211    }
212
213    // TODO: Once we have condition types that implement ToClvm and an Encoder
214    // that serialize directly to bytes, these test solutions can be expressed
215    // in a much more readable way
216    #[test]
217    fn test_single_create_coin() {
218        // This is a solution to the identity puzzle:
219        // ((CREATE_COIN . (222222..22 . (1 . NIL))) .
220        // ))
221        let solution = "ff\
222ff33\
223ffa02222222222222222222222222222222222222222222222222222222222222222\
224ff01\
22580\
22680";
227        test_impl(solution, |test_coin: Coin, bundle: SpendBundle| {
228            let additions = bundle.additions().expect("additions");
229
230            let new_coin = Coin::new(
231                test_coin.coin_id(),
232                hex::decode("2222222222222222222222222222222222222222222222222222222222222222")
233                    .unwrap()
234                    .try_into()
235                    .unwrap(),
236                1,
237            );
238            assert_eq!(additions, [new_coin]);
239        });
240    }
241
242    #[test]
243    fn test_invalid_condition() {
244        // This is a solution to the identity puzzle:
245        // (((1 . CREATE_COIN) . (222222..22 . (1 . NIL))) .
246        // ))
247        let solution = "ff\
248ffff0133\
249ffa02222222222222222222222222222222222222222222222222222222222222222\
250ff01\
25180\
25280";
253
254        test_impl(solution, |_test_coin, bundle: SpendBundle| {
255            assert_eq!(
256                bundle.additions().unwrap_err().to_string(),
257                "InvalidOperatorArg: invalid condition"
258            );
259        });
260    }
261
262    #[test]
263    fn test_invalid_spend() {
264        // This is a solution to the identity puzzle:
265        // ((CREATE_COIN . (222222..22 . ((1 . 1) . NIL))) .
266        // ))
267        let solution = "ff\
268ff33\
269ffa02222222222222222222222222222222222222222222222222222222222222222\
270ffff0101\
27180\
27280";
273
274        test_impl(solution, |_test_coin, bundle: SpendBundle| {
275            assert_eq!(
276                bundle.additions().unwrap_err().to_string(),
277                "InvalidOperatorArg: failed to parse spend"
278            );
279        });
280    }
281}
282
283#[cfg(all(test, feature = "serde"))]
284mod serde_tests {
285    use chia_bls::Signature;
286    use indoc::indoc;
287
288    use crate::Program;
289
290    use super::*;
291
292    #[test]
293    fn test_json_spend_bundle() -> anyhow::Result<()> {
294        let json = serde_json::to_string_pretty(&SpendBundle::new(
295            vec![CoinSpend::new(
296                Coin::new([0; 32].into(), [1; 32].into(), 42),
297                Program::from(b"abc".to_vec()),
298                Program::from(b"xyz".to_vec()),
299            )],
300            Signature::default(),
301        ))?;
302
303        let output = indoc! {r#"{
304          "coin_spends": [
305            {
306              "coin": {
307                "parent_coin_info": "0x0000000000000000000000000000000000000000000000000000000000000000",
308                "puzzle_hash": "0x0101010101010101010101010101010101010101010101010101010101010101",
309                "amount": 42
310              },
311              "puzzle_reveal": "616263",
312              "solution": "78797a"
313            }
314          ],
315          "aggregated_signature": "0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
316        }"#};
317
318        assert_eq!(json, output);
319
320        Ok(())
321    }
322}