1use crate::coin_spend::CoinSpend;
2use crate::Bytes32;
3use crate::Coin;
4use chik_bls::G2Element;
5use chik_streamable_macro::streamable;
6use chik_traits::Streamable;
7use klvm_traits::FromKlvm;
8use klvmr::allocator::{NodePtr, SExp};
9use klvmr::cost::Cost;
10use klvmr::op_utils::{first, rest};
11use klvmr::reduction::EvalErr;
12use klvmr::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(a.nil(), "cost exceeded".to_string()));
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(..) => return Err(EvalErr(op, "invalid condition".to_string())),
68 };
69 let buf = buf.as_ref();
70 if buf.len() != 1 {
71 continue;
72 }
73 if buf[0] == CREATE_COIN {
74 let (puzzle_hash, (amount, _)) = <(Bytes32, (u64, NodePtr))>::from_klvm(&a, c)
75 .map_err(|_| EvalErr(c, "failed to parse spend".to_string()))?;
76 ret.push(Coin {
77 parent_coin_info,
78 puzzle_hash,
79 amount,
80 });
81 if CREATE_COIN_COST > cost_left {
82 return Err(EvalErr(a.nil(), "cost exceeded".to_string()));
83 }
84 cost_left -= CREATE_COIN_COST;
85 }
86 }
87 }
88 Ok(ret)
89 }
90}
91
92#[cfg(feature = "py-bindings")]
93#[pymethods]
94#[allow(clippy::needless_pass_by_value)]
95impl SpendBundle {
96 #[classmethod]
97 #[pyo3(name = "aggregate")]
98 fn py_aggregate(
99 cls: &Bound<'_, PyType>,
100 py: Python<'_>,
101 spend_bundles: Vec<Self>,
102 ) -> PyResult<PyObject> {
103 let aggregated = Bound::new(py, Self::aggregate(&spend_bundles))?;
104 if aggregated.is_exact_instance(cls) {
105 Ok(aggregated.into_pyobject(py)?.unbind().into_any())
106 } else {
107 let instance = cls.call_method1("from_parent", (aggregated.into_pyobject(py)?,))?;
108 Ok(instance.into_pyobject(py)?.unbind().into_any())
109 }
110 }
111
112 #[classmethod]
113 #[pyo3(name = "from_parent")]
114 pub fn from_parent(
115 cls: &Bound<'_, PyType>,
116 py: Python<'_>,
117 spend_bundle: Self,
118 ) -> PyResult<PyObject> {
119 let instance = cls.call(
121 (spend_bundle.coin_spends, spend_bundle.aggregated_signature),
122 None,
123 )?;
124
125 Ok(instance.into_pyobject(py)?.unbind())
126 }
127
128 #[pyo3(name = "name")]
129 fn py_name(&self) -> Bytes32 {
130 self.name()
131 }
132
133 fn removals(&self) -> Vec<Coin> {
134 let mut ret = Vec::<Coin>::with_capacity(self.coin_spends.len());
135 for cs in &self.coin_spends {
136 ret.push(cs.coin);
137 }
138 ret
139 }
140
141 #[pyo3(name = "additions")]
142 fn py_additions(&self) -> PyResult<Vec<Coin>> {
143 self.additions()
144 .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.1))
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use crate::Program;
152 use rstest::rstest;
153 use std::fs;
154
155 #[rstest]
156 #[case(
157 "e3c0",
158 "fd65e4b0f21322f78d1025e8a8ff7a1df77cd40b86885b851f4572e5ce06e4ff",
159 "e3c000a395f8f69d5e263a9548f13bffb1c4b701ab8f3faa03f7647c8750d077"
160 )]
161 #[case(
162 "bb13",
163 "6b2aaee962cb1de3fdeb1f0506c02df4b9e162e2af3dd1db22048454b5122a87",
164 "bb13d1e13438736c7ba0217c7b82ee4db56a7f4fb9d22c703c2152362b2314ee"
165 )]
166 fn test_additions_ff(
167 #[case] spend_file: &str,
168 #[case] expect_parent: &str,
169 #[case] expect_ph: &str,
170 ) {
171 let spend_bytes =
172 fs::read(format!("../../ff-tests/{spend_file}.spend")).expect("read file");
173 let spend = CoinSpend::from_bytes(&spend_bytes).expect("parse CoinSpend");
174 let bundle = SpendBundle::new(vec![spend], G2Element::default());
175
176 let additions = bundle.additions().expect("additions");
177
178 assert_eq!(additions.len(), 1);
179 assert_eq!(
180 additions[0].parent_coin_info.as_ref(),
181 &hex::decode(expect_parent).expect("hex::decode")
182 );
183 assert_eq!(
184 additions[0].puzzle_hash.as_ref(),
185 &hex::decode(expect_ph).expect("hex::decode")
186 );
187 assert_eq!(additions[0].amount, 1);
188 }
189
190 fn test_impl<F: Fn(Coin, SpendBundle)>(solution: &str, body: F) {
191 let solution = hex::decode(solution).expect("hex::decode");
192 let test_coin = Coin::new(
193 hex::decode("4444444444444444444444444444444444444444444444444444444444444444")
194 .unwrap()
195 .try_into()
196 .unwrap(),
197 hex::decode("3333333333333333333333333333333333333333333333333333333333333333")
198 .unwrap()
199 .try_into()
200 .unwrap(),
201 1,
202 );
203 let spend = CoinSpend::new(
204 test_coin,
205 Program::new(vec![1_u8].into()),
206 Program::new(solution.into()),
207 );
208 let bundle = SpendBundle::new(vec![spend], G2Element::default());
209 body(test_coin, bundle);
210 }
211
212 #[test]
216 fn test_single_create_coin() {
217 let solution = "ff\
221ff33\
222ffa02222222222222222222222222222222222222222222222222222222222222222\
223ff01\
22480\
22580";
226 test_impl(solution, |test_coin: Coin, bundle: SpendBundle| {
227 let additions = bundle.additions().expect("additions");
228
229 let new_coin = Coin::new(
230 test_coin.coin_id(),
231 hex::decode("2222222222222222222222222222222222222222222222222222222222222222")
232 .unwrap()
233 .try_into()
234 .unwrap(),
235 1,
236 );
237 assert_eq!(additions, [new_coin]);
238 });
239 }
240
241 #[test]
242 fn test_invalid_condition() {
243 let solution = "ff\
247ffff0133\
248ffa02222222222222222222222222222222222222222222222222222222222222222\
249ff01\
25080\
25180";
252
253 test_impl(solution, |_test_coin, bundle: SpendBundle| {
254 assert_eq!(bundle.additions().unwrap_err().1, "invalid condition");
255 });
256 }
257
258 #[test]
259 fn test_invalid_spend() {
260 let solution = "ff\
264ff33\
265ffa02222222222222222222222222222222222222222222222222222222222222222\
266ffff0101\
26780\
26880";
269
270 test_impl(solution, |_test_coin, bundle: SpendBundle| {
271 assert_eq!(bundle.additions().unwrap_err().1, "failed to parse spend");
272 });
273 }
274}
275
276#[cfg(all(test, feature = "serde"))]
277mod serde_tests {
278 use chik_bls::Signature;
279 use indoc::indoc;
280
281 use crate::Program;
282
283 use super::*;
284
285 #[test]
286 fn test_json_spend_bundle() -> anyhow::Result<()> {
287 let json = serde_json::to_string_pretty(&SpendBundle::new(
288 vec![CoinSpend::new(
289 Coin::new([0; 32].into(), [1; 32].into(), 42),
290 Program::from(b"abc".to_vec()),
291 Program::from(b"xyz".to_vec()),
292 )],
293 Signature::default(),
294 ))?;
295
296 let output = indoc! {r#"{
297 "coin_spends": [
298 {
299 "coin": {
300 "parent_coin_info": "0x0000000000000000000000000000000000000000000000000000000000000000",
301 "puzzle_hash": "0x0101010101010101010101010101010101010101010101010101010101010101",
302 "amount": 42
303 },
304 "puzzle_reveal": "616263",
305 "solution": "78797a"
306 }
307 ],
308 "aggregated_signature": "0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
309 }"#};
310
311 assert_eq!(json, output);
312
313 Ok(())
314 }
315}