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