1use crate::{
2 CatLayer, CatMaker, DriverError, HashedPtr, Layer, P2ParentLayer, Puzzle, Spend, SpendContext,
3};
4use chia_protocol::{Bytes32, Coin};
5use chia_puzzle_types::{
6 CoinProof, LineageProof, Memos,
7 cat::{CatArgs, CatSolution},
8};
9use chia_puzzles::CAT_PUZZLE_HASH;
10use chia_sdk_types::{
11 Conditions, Mod,
12 puzzles::{P2ParentArgs, P2ParentSolution},
13 run_puzzle,
14};
15use clvm_traits::{FromClvm, ToClvm};
16use clvm_utils::{ToTreeHash, TreeHash};
17use clvmr::{Allocator, NodePtr};
18
19#[derive(Debug, Clone, PartialEq, Eq, Copy)]
20#[must_use]
21pub struct P2ParentCoin {
22 pub coin: Coin,
23 pub asset_id: Option<Bytes32>,
24 pub proof: LineageProof,
25}
26
27impl P2ParentCoin {
28 pub fn new(coin: Coin, asset_id: Option<Bytes32>, proof: LineageProof) -> Self {
29 Self {
30 coin,
31 asset_id,
32 proof,
33 }
34 }
35
36 pub fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result<NodePtr, DriverError> {
37 if let Some(asset_id) = self.asset_id {
38 CatLayer::new(asset_id, P2ParentLayer::cat(asset_id.tree_hash())).construct_puzzle(ctx)
39 } else {
40 P2ParentLayer::xch().construct_puzzle(ctx)
41 }
42 }
43
44 pub fn inner_puzzle_hash(asset_id: Option<Bytes32>) -> TreeHash {
45 P2ParentArgs {
46 cat_maker: if let Some(asset_id) = asset_id {
47 CatMaker::Default {
48 tail_hash_hash: asset_id.tree_hash(),
49 }
50 .curry_tree_hash()
51 } else {
52 CatMaker::Xch.curry_tree_hash()
53 },
54 }
55 .curry_tree_hash()
56 }
57
58 pub fn puzzle_hash(asset_id: Option<Bytes32>) -> TreeHash {
59 let inner_puzzle_hash = Self::inner_puzzle_hash(asset_id);
60
61 if let Some(asset_id) = asset_id {
62 CatArgs::curry_tree_hash(asset_id, inner_puzzle_hash)
63 } else {
64 inner_puzzle_hash
65 }
66 }
67
68 pub fn construct_solution<CMS>(
69 &self,
70 ctx: &mut SpendContext,
71 delegated_spend: Spend,
72 cat_maker_solution: CMS,
73 ) -> Result<NodePtr, DriverError>
74 where
75 CMS: ToClvm<Allocator>,
76 {
77 let inner_solution = P2ParentSolution {
78 parent_parent_id: self.proof.parent_parent_coin_info,
79 parent_amount: self.proof.parent_amount,
80 parent_inner_puzzle: delegated_spend.puzzle,
81 parent_solution: delegated_spend.solution,
82 cat_maker_solution: cat_maker_solution.to_clvm(ctx)?,
83 };
84
85 if let Some(asset_id) = self.asset_id {
86 let inner_layer = P2ParentLayer::cat(asset_id.tree_hash());
87
88 CatLayer::new(asset_id, inner_layer).construct_solution(
89 ctx,
90 CatSolution {
91 inner_puzzle_solution: inner_solution,
92 lineage_proof: Some(self.proof),
93 prev_coin_id: self.coin.coin_id(),
94 this_coin_info: self.coin,
95 next_coin_proof: CoinProof {
96 parent_coin_info: self.coin.parent_coin_info,
97 inner_puzzle_hash: Self::inner_puzzle_hash(self.asset_id).into(),
98 amount: self.coin.amount,
99 },
100 prev_subtotal: 0,
101 extra_delta: 0,
102 },
103 )
104 } else {
105 P2ParentLayer::xch().construct_solution(ctx, inner_solution)
106 }
107 }
108
109 pub fn spend<CMS>(
110 &self,
111 ctx: &mut SpendContext,
112 delegated_spend: Spend,
113 cat_maker_solution: CMS,
114 ) -> Result<(), DriverError>
115 where
116 CMS: ToClvm<Allocator>,
117 {
118 let puzzle = self.construct_puzzle(ctx)?;
119 let solution = self.construct_solution(ctx, delegated_spend, cat_maker_solution)?;
120
121 ctx.spend(self.coin, Spend::new(puzzle, solution))
122 }
123
124 pub fn parse_child(
126 allocator: &mut Allocator,
127 parent_coin: Coin,
128 parent_puzzle: Puzzle,
129 parent_solution: NodePtr,
130 ) -> Result<Option<(Self, Memos)>, DriverError> {
131 let (parent_inner_puzzle_hash, asset_id) =
132 if parent_puzzle.mod_hash() == CAT_PUZZLE_HASH.into() {
133 let Some(parent_puzzle) = parent_puzzle.as_curried() else {
134 return Err(DriverError::Custom(
135 "Expected parent puzzle to be curried but it's not.".to_string(),
136 ));
137 };
138
139 let args = CatArgs::<HashedPtr>::from_clvm(allocator, parent_puzzle.args)?;
140 (args.inner_puzzle.tree_hash().into(), Some(args.asset_id))
141 } else {
142 (parent_coin.puzzle_hash, None)
143 };
144
145 let proof = LineageProof {
146 parent_parent_coin_info: parent_coin.parent_coin_info,
147 parent_inner_puzzle_hash,
148 parent_amount: parent_coin.amount,
149 };
150
151 let expected_puzzle_hash: Bytes32 = Self::puzzle_hash(asset_id).into();
152
153 let parent_output = run_puzzle(allocator, parent_puzzle.ptr(), parent_solution)?;
154 let parent_conditions = Conditions::<NodePtr>::from_clvm(allocator, parent_output)?;
155 let Some(create_coin) = parent_conditions.iter().find_map(|c| {
156 c.as_create_coin()
157 .filter(|&create_coin| create_coin.puzzle_hash == expected_puzzle_hash)
158 }) else {
159 return Ok(None);
160 };
161
162 Ok(Some((
163 Self {
164 coin: Coin::new(
165 parent_coin.coin_id(),
166 expected_puzzle_hash,
167 create_coin.amount,
168 ),
169 asset_id,
170 proof,
171 },
172 create_coin.memos,
173 )))
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use std::slice;
180
181 use chia_protocol::Bytes;
182 use chia_sdk_test::{Benchmark, Simulator};
183 use chia_sdk_types::puzzles::{P2_PARENT_PUZZLE, P2_PARENT_PUZZLE_HASH};
184 use clvm_utils::tree_hash;
185 use clvmr::serde::node_from_bytes;
186 use rstest::rstest;
187
188 use crate::{Cat, CatSpend, FungibleAsset, SpendWithConditions, StandardLayer};
189
190 use super::*;
191
192 #[test]
193 fn test_puzzle_hash() {
194 let mut allocator = Allocator::new();
195
196 let ptr = node_from_bytes(&mut allocator, &P2_PARENT_PUZZLE).unwrap();
197 assert_eq!(tree_hash(&allocator, ptr), P2_PARENT_PUZZLE_HASH);
198 }
199
200 #[rstest]
201 #[case::xch(false)]
202 #[case::cat(true)]
203 fn test_p2_parent(#[case] cat_mode: bool) -> anyhow::Result<()> {
204 let mut ctx = SpendContext::new();
205 let mut sim = Simulator::new();
206 let mut benchmark = Benchmark::new(format!(
207 "P2 Parent Coin ({})",
208 if cat_mode { "CAT" } else { "XCH" }
209 ));
210
211 let parent_bls = sim.bls(1337);
212
213 let server_list = vec![
215 Bytes::new(b"yak1".to_vec()),
216 Bytes::new(b"yak2".to_vec()),
217 Bytes::new(b"yak3".to_vec()),
218 ];
219
220 let (expected_coin, expected_asset_id, expected_lp) = if cat_mode {
221 let (issue_cat, cats) = Cat::single_issuance(
222 &mut ctx,
223 parent_bls.coin.coin_id(),
224 None,
225 parent_bls.coin.amount,
226 Conditions::new().create_coin(
227 parent_bls.puzzle_hash,
228 parent_bls.coin.amount,
229 Memos::None,
230 ),
231 )?;
232 StandardLayer::new(parent_bls.pk).spend(&mut ctx, parent_bls.coin, issue_cat)?;
233 sim.spend_coins(ctx.take(), slice::from_ref(&parent_bls.sk))?;
234
235 let parent_conds = Conditions::new().create_coin(
236 P2ParentCoin::inner_puzzle_hash(Some(cats[0].info.asset_id)).into(),
237 1337,
238 ctx.memos(&server_list)?,
239 );
240 let parent_cat_inner_spend =
241 StandardLayer::new(parent_bls.pk).spend_with_conditions(&mut ctx, parent_conds)?;
242
243 let cats = Cat::spend_all(&mut ctx, &[CatSpend::new(cats[0], parent_cat_inner_spend)])?;
244
245 (
246 cats[0].coin,
247 Some(cats[0].info.asset_id),
248 cats[0].lineage_proof.unwrap(),
249 )
250 } else {
251 let parent_conds = Conditions::new().create_coin(
252 P2ParentCoin::puzzle_hash(None).into(),
253 1337,
254 ctx.memos(&server_list)?,
255 );
256 let parent_inner_spend =
257 StandardLayer::new(parent_bls.pk).spend_with_conditions(&mut ctx, parent_conds)?;
258
259 ctx.spend(parent_bls.coin, parent_inner_spend)?;
260
261 (
262 parent_bls.coin.make_child(
263 P2ParentCoin::puzzle_hash(None).into(),
264 parent_bls.coin.amount,
265 ),
266 None,
267 LineageProof {
268 parent_parent_coin_info: parent_bls.coin.parent_coin_info,
269 parent_inner_puzzle_hash: parent_bls.coin.puzzle_hash,
270 parent_amount: parent_bls.coin.amount,
271 },
272 )
273 };
274
275 let spends = ctx.take();
276 let launch_spend = spends.last().unwrap().clone();
277 benchmark.add_spends(
278 &mut ctx,
279 &mut sim,
280 spends,
281 "create",
282 slice::from_ref(&parent_bls.sk),
283 )?;
284
285 let parent_puzzle = ctx.alloc(&launch_spend.puzzle_reveal)?;
287 let parent_puzzle = Puzzle::parse(&ctx, parent_puzzle);
288 let parent_solution = ctx.alloc(&launch_spend.solution)?;
289 let (p2_parent_coin, memos) =
290 P2ParentCoin::parse_child(&mut ctx, launch_spend.coin, parent_puzzle, parent_solution)?
291 .unwrap();
292
293 assert_eq!(
294 p2_parent_coin,
295 P2ParentCoin {
296 coin: expected_coin,
297 asset_id: expected_asset_id,
298 proof: expected_lp,
299 },
300 );
301 let Memos::Some(memos) = memos else {
302 panic!("Expected memos");
303 };
304 let memos = ctx.extract::<Vec<Bytes>>(memos)?;
305 assert_eq!(memos, server_list);
306
307 let new_coin_inner_puzzle_hash = Bytes32::new([0; 32]);
309 let new_coin = Coin::new(
310 p2_parent_coin.coin.coin_id(),
311 if cat_mode {
312 CatArgs::curry_tree_hash(
313 p2_parent_coin.asset_id.unwrap(),
314 new_coin_inner_puzzle_hash.into(),
315 )
316 .into()
317 } else {
318 new_coin_inner_puzzle_hash
319 },
320 p2_parent_coin.coin.amount,
321 );
322
323 let delegated_spend = StandardLayer::new(parent_bls.pk).spend_with_conditions(
324 &mut ctx,
325 Conditions::new().create_coin(new_coin_inner_puzzle_hash, new_coin.amount, Memos::None),
326 )?;
327 p2_parent_coin.spend(&mut ctx, delegated_spend, ())?;
328
329 let spends = ctx.take();
330 benchmark.add_spends(
331 &mut ctx,
332 &mut sim,
333 spends,
334 "spend",
335 slice::from_ref(&parent_bls.sk),
336 )?;
337
338 assert!(sim.coin_state(new_coin.coin_id()).is_some());
339
340 benchmark.print_summary(Some(&format!(
341 "p2-parent-coin-{}.costs",
342 if cat_mode { "cat" } else { "xch" }
343 )));
344
345 Ok(())
346 }
347}