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