Skip to main content

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