Skip to main content

chia_sdk_driver/primitives/
p2_parent_coin.rs

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    // also returns memo
125    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        // Server coins will be created with a list of strings as memos
214        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        // Test parsing
286        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        // Spend the p2_parent coin
308        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}