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::issue_with_coin(
222                &mut ctx,
223                parent_bls.coin.coin_id(),
224                parent_bls.coin.amount,
225                Conditions::new().create_coin(
226                    parent_bls.puzzle_hash,
227                    parent_bls.coin.amount,
228                    Memos::None,
229                ),
230            )?;
231            StandardLayer::new(parent_bls.pk).spend(&mut ctx, parent_bls.coin, issue_cat)?;
232            sim.spend_coins(ctx.take(), slice::from_ref(&parent_bls.sk))?;
233
234            let parent_conds = Conditions::new().create_coin(
235                P2ParentCoin::inner_puzzle_hash(Some(cats[0].info.asset_id)).into(),
236                1337,
237                ctx.memos(&server_list)?,
238            );
239            let parent_cat_inner_spend =
240                StandardLayer::new(parent_bls.pk).spend_with_conditions(&mut ctx, parent_conds)?;
241
242            let cats = Cat::spend_all(&mut ctx, &[CatSpend::new(cats[0], parent_cat_inner_spend)])?;
243
244            (
245                cats[0].coin,
246                Some(cats[0].info.asset_id),
247                cats[0].lineage_proof.unwrap(),
248            )
249        } else {
250            let parent_conds = Conditions::new().create_coin(
251                P2ParentCoin::puzzle_hash(None).into(),
252                1337,
253                ctx.memos(&server_list)?,
254            );
255            let parent_inner_spend =
256                StandardLayer::new(parent_bls.pk).spend_with_conditions(&mut ctx, parent_conds)?;
257
258            ctx.spend(parent_bls.coin, parent_inner_spend)?;
259
260            (
261                parent_bls.coin.make_child(
262                    P2ParentCoin::puzzle_hash(None).into(),
263                    parent_bls.coin.amount,
264                ),
265                None,
266                LineageProof {
267                    parent_parent_coin_info: parent_bls.coin.parent_coin_info,
268                    parent_inner_puzzle_hash: parent_bls.coin.puzzle_hash,
269                    parent_amount: parent_bls.coin.amount,
270                },
271            )
272        };
273
274        let spends = ctx.take();
275        let launch_spend = spends.last().unwrap().clone();
276        benchmark.add_spends(
277            &mut ctx,
278            &mut sim,
279            spends,
280            "create",
281            slice::from_ref(&parent_bls.sk),
282        )?;
283
284        // Test parsing
285        let parent_puzzle = ctx.alloc(&launch_spend.puzzle_reveal)?;
286        let parent_puzzle = Puzzle::parse(&ctx, parent_puzzle);
287        let parent_solution = ctx.alloc(&launch_spend.solution)?;
288        let (p2_parent_coin, memos) =
289            P2ParentCoin::parse_child(&mut ctx, launch_spend.coin, parent_puzzle, parent_solution)?
290                .unwrap();
291
292        assert_eq!(
293            p2_parent_coin,
294            P2ParentCoin {
295                coin: expected_coin,
296                asset_id: expected_asset_id,
297                proof: expected_lp,
298            },
299        );
300        let Memos::Some(memos) = memos else {
301            panic!("Expected memos");
302        };
303        let memos = ctx.extract::<Vec<Bytes>>(memos)?;
304        assert_eq!(memos, server_list);
305
306        // Spend the p2_parent coin
307        let new_coin_inner_puzzle_hash = Bytes32::new([0; 32]);
308        let new_coin = Coin::new(
309            p2_parent_coin.coin.coin_id(),
310            if cat_mode {
311                CatArgs::curry_tree_hash(
312                    p2_parent_coin.asset_id.unwrap(),
313                    new_coin_inner_puzzle_hash.into(),
314                )
315                .into()
316            } else {
317                new_coin_inner_puzzle_hash
318            },
319            p2_parent_coin.coin.amount,
320        );
321
322        let delegated_spend = StandardLayer::new(parent_bls.pk).spend_with_conditions(
323            &mut ctx,
324            Conditions::new().create_coin(new_coin_inner_puzzle_hash, new_coin.amount, Memos::None),
325        )?;
326        p2_parent_coin.spend(&mut ctx, delegated_spend, ())?;
327
328        let spends = ctx.take();
329        benchmark.add_spends(
330            &mut ctx,
331            &mut sim,
332            spends,
333            "spend",
334            slice::from_ref(&parent_bls.sk),
335        )?;
336
337        assert!(sim.coin_state(new_coin.coin_id()).is_some());
338
339        benchmark.print_summary(Some(&format!(
340            "p2-parent-coin-{}.costs",
341            if cat_mode { "cat" } else { "xch" }
342        )));
343
344        Ok(())
345    }
346}