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 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 #[test]
217 fn test_single_create_coin() {
218 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 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 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}