chia_sdk_driver/primitives/
clawback.rs

1use crate::{DriverError, Layer, P2OneOfManyLayer, Puzzle, Spend, SpendContext};
2use chia_protocol::{Bytes, Bytes32};
3use chia_puzzles::AUGMENTED_CONDITION_HASH;
4use chia_sdk_types::{
5    conditions::Remark,
6    puzzles::{
7        AugmentedConditionArgs, AugmentedConditionSolution, P2CurriedArgs, P2CurriedSolution,
8        P2OneOfManySolution, P2_CURRIED_PUZZLE_HASH,
9    },
10    run_puzzle, Condition, MerkleTree,
11};
12use chia_streamable_macro::streamable;
13use chia_traits::Streamable;
14use clvm_traits::clvm_list;
15use clvm_traits::FromClvm;
16use clvm_traits::ToClvm;
17use clvm_utils::{CurriedProgram, ToTreeHash, TreeHash};
18use clvmr::{Allocator, NodePtr};
19
20#[streamable]
21pub struct VersionedBlob {
22    version: u16,
23    blob: Bytes,
24}
25
26#[streamable]
27#[derive(Copy)]
28pub struct Clawback {
29    /// The number of seconds until this clawback can be claimed by the recipient.
30    pub timelock: u64,
31    /// The original sender of the coin, who can claw it back until claimed.
32    pub sender_puzzle_hash: Bytes32,
33    /// The intended recipient who can claim after the timelock period is up.
34    pub receiver_puzzle_hash: Bytes32,
35}
36
37impl Clawback {
38    pub fn parse_children(
39        allocator: &mut Allocator,
40        parent_puzzle: Puzzle, // this could be any puzzle type
41        parent_solution: NodePtr,
42    ) -> Result<Option<Vec<Self>>, DriverError>
43    where
44        Self: Sized,
45    {
46        let output = run_puzzle(allocator, parent_puzzle.ptr(), parent_solution)?;
47        let conditions = Vec::<Condition>::from_clvm(allocator, output)?;
48        let mut outputs = Vec::<Clawback>::new();
49        let mut metadatas = Vec::<Clawback>::new();
50        let mut puzhashes = Vec::<[u8; 32]>::with_capacity(conditions.len());
51        for condition in conditions {
52            match condition {
53                Condition::CreateCoin(cc) => puzhashes.push(cc.puzzle_hash.into()),
54                Condition::Remark(rm) => match allocator.sexp(rm.rest) {
55                    clvmr::SExp::Atom => continue,
56                    clvmr::SExp::Pair(first, rest) => {
57                        match allocator.sexp(first) {
58                            clvmr::SExp::Atom => {
59                                let Some(atom) = allocator.small_number(first) else {
60                                    continue;
61                                };
62                                if atom != 2 {
63                                    continue;
64                                } // magic number for Clawback in REMARK
65                            }
66                            clvmr::SExp::Pair(_, _) => continue,
67                        }
68                        // we have seen the magic number
69                        // try to deserialise blob
70                        match allocator.sexp(rest) {
71                            clvmr::SExp::Atom => continue,
72                            clvmr::SExp::Pair(r_first, _r_rest) => match allocator.sexp(r_first) {
73                                clvmr::SExp::Atom => {
74                                    let rest_atom = &allocator.atom(r_first);
75                                    metadatas.push(
76                                        Clawback::from_bytes_unchecked(
77                                            VersionedBlob::from_bytes_unchecked(rest_atom)
78                                                .map_err(|_| DriverError::InvalidMemo)?
79                                                .blob
80                                                .as_ref(),
81                                        )
82                                        .map_err(|_| DriverError::InvalidMemo)?,
83                                    );
84                                }
85                                clvmr::SExp::Pair(_, _) => continue,
86                            },
87                        }
88                    }
89                },
90                _ => {}
91            }
92        }
93        for &clawback in &metadatas {
94            if puzhashes.contains(&clawback.to_layer().tree_hash().to_bytes()) {
95                outputs.push(clawback);
96            }
97        }
98        Ok(Some(outputs))
99    }
100
101    pub fn receiver_path_puzzle_hash(&self) -> TreeHash {
102        CurriedProgram {
103            program: TreeHash::new(AUGMENTED_CONDITION_HASH),
104            args: AugmentedConditionArgs::new(
105                Condition::<TreeHash>::assert_seconds_relative(self.timelock),
106                TreeHash::from(self.receiver_puzzle_hash),
107            ),
108        }
109        .tree_hash()
110    }
111
112    pub fn receiver_path_puzzle(
113        &self,
114        ctx: &mut SpendContext,
115        inner_puzzle: NodePtr,
116    ) -> Result<NodePtr, DriverError> {
117        ctx.curry(AugmentedConditionArgs::new(
118            Condition::<NodePtr>::assert_seconds_relative(self.timelock),
119            inner_puzzle,
120        ))
121    }
122
123    pub fn sender_path_puzzle_hash(&self) -> TreeHash {
124        CurriedProgram {
125            program: P2_CURRIED_PUZZLE_HASH,
126            args: P2CurriedArgs::new(self.sender_puzzle_hash),
127        }
128        .tree_hash()
129    }
130
131    pub fn sender_path_puzzle(&self, ctx: &mut SpendContext) -> Result<NodePtr, DriverError> {
132        ctx.curry(P2CurriedArgs::new(self.sender_puzzle_hash))
133    }
134
135    pub fn merkle_tree(&self) -> MerkleTree {
136        MerkleTree::new(&[
137            self.receiver_path_puzzle_hash().into(),
138            self.sender_path_puzzle_hash().into(),
139        ])
140    }
141
142    pub fn to_layer(&self) -> P2OneOfManyLayer {
143        P2OneOfManyLayer::new(self.merkle_tree().root())
144    }
145
146    // this function returns the Remark condition required to hint at this clawback
147    // it should be included alongside the createcoin that creates this
148    pub fn get_remark_condition(
149        &self,
150        allocator: &mut Allocator,
151    ) -> Result<Remark<NodePtr>, DriverError> {
152        let vb = VersionedBlob {
153            version: 1,
154            blob: self
155                .to_bytes()
156                .map_err(|_| DriverError::InvalidMemo)?
157                .into(),
158        };
159        // 2 is the magic number for clawback
160        let node_ptr = clvm_list!(
161            2,
162            Bytes::new(vb.to_bytes().map_err(|_| DriverError::InvalidMemo)?)
163        )
164        .to_clvm(allocator)?;
165
166        Ok(Remark::new(node_ptr))
167    }
168
169    pub fn receiver_spend(
170        &self,
171        ctx: &mut SpendContext,
172        spend: Spend,
173    ) -> Result<Spend, DriverError> {
174        let merkle_tree = self.merkle_tree();
175
176        let puzzle = self.receiver_path_puzzle(ctx, spend.puzzle)?;
177        let solution = ctx.alloc(&AugmentedConditionSolution::new(spend.solution))?;
178
179        let proof = merkle_tree
180            .proof(ctx.tree_hash(puzzle).into())
181            .ok_or(DriverError::InvalidMerkleProof)?;
182
183        P2OneOfManyLayer::new(merkle_tree.root())
184            .construct_spend(ctx, P2OneOfManySolution::new(proof, puzzle, solution))
185    }
186
187    pub fn sender_spend(&self, ctx: &mut SpendContext, spend: Spend) -> Result<Spend, DriverError> {
188        let merkle_tree = self.merkle_tree();
189
190        let puzzle = self.sender_path_puzzle(ctx)?;
191        let solution = ctx.alloc(&P2CurriedSolution::new(spend.puzzle, spend.solution))?;
192
193        let proof = merkle_tree
194            .proof(ctx.tree_hash(puzzle).into())
195            .ok_or(DriverError::InvalidMerkleProof)?;
196
197        P2OneOfManyLayer::new(merkle_tree.root())
198            .construct_spend(ctx, P2OneOfManySolution::new(proof, puzzle, solution))
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use chia_protocol::{Coin, SpendBundle};
205    use chia_puzzle_types::Memos;
206    use chia_sdk_test::Simulator;
207    use chia_sdk_types::Conditions;
208    use clvm_traits::ToClvm;
209
210    use crate::{SpendWithConditions, StandardLayer};
211
212    use super::*;
213
214    #[test]
215    #[allow(clippy::similar_names)]
216    fn test_clawback_coin_claim() -> anyhow::Result<()> {
217        let mut sim = Simulator::new();
218        let ctx = &mut SpendContext::new();
219
220        let alice = sim.bls(1);
221        let alice_p2 = StandardLayer::new(alice.pk);
222
223        let bob = sim.bls(1);
224        let bob_p2 = StandardLayer::new(bob.pk);
225
226        let clawback = Clawback {
227            timelock: 1,
228            sender_puzzle_hash: alice.puzzle_hash,
229            receiver_puzzle_hash: bob.puzzle_hash,
230        };
231        let clawback_puzzle_hash = clawback.to_layer().tree_hash().into();
232        let coin = alice.coin;
233        let conditions = Conditions::new()
234            .create_coin(clawback_puzzle_hash, 1, Memos::None)
235            .with(clawback.get_remark_condition(ctx)?);
236        alice_p2.spend(ctx, coin, conditions)?;
237
238        let cs = ctx.take();
239
240        let clawback_coin = Coin::new(coin.coin_id(), clawback_puzzle_hash, 1);
241
242        sim.spend_coins(cs, &[alice.sk])?;
243
244        let puzzle_reveal = sim
245            .puzzle_reveal(coin.coin_id())
246            .expect("missing puzzle")
247            .to_clvm(ctx)?;
248
249        let solution = sim
250            .solution(coin.coin_id())
251            .expect("missing solution")
252            .to_clvm(ctx)?;
253
254        let puzzle = Puzzle::parse(ctx, puzzle_reveal);
255
256        // check we can recreate Clawback from the spend
257        let children = Clawback::parse_children(ctx, puzzle, solution)
258            .expect("we should have found the child")
259            .expect("we should have found children");
260        assert_eq!(children.len(), 1);
261        assert_eq!(children[0], clawback);
262
263        let bob_inner = bob_p2.spend_with_conditions(ctx, Conditions::new().reserve_fee(1))?;
264        let receiver_spend = clawback.receiver_spend(ctx, bob_inner)?;
265        ctx.spend(clawback_coin, receiver_spend)?;
266
267        sim.spend_coins(ctx.take(), &[bob.sk])?;
268
269        Ok(())
270    }
271
272    #[test]
273    fn test_clawback_compatible_with_python() -> anyhow::Result<()> {
274        let ctx = &mut SpendContext::new();
275        let bytes = hex_literal::hex!("00000001e3b0c44298fc1c149afbf4c8996fb924000000000000000000000000000000014eb7420f8651b09124e1d40cdc49eeddacbaa0c25e6ae5a0a482fac8e3b5259f000001977420dc00ff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b0b50b02adba343fff8bf3a94e92ed7df43743aedf0006b81a6c00ae573c0cce7d08216f60886fe84e4078a5209b0e5171ff018080ff80ffff01ffff33ffa0aeb663f32c4cfe1122710bc03cdc086f87e3243c055e8bebba42189cafbaf465ff840098968080ffff01ff02ffc04e00010000004800000000000000644eb7420f8651b09124e1d40cdc49eeddacbaa0c25e6ae5a0a482fac8e3b5259f5abb5d5568b4a7411dd97b3356cfedfac09b5fb35621a7fa29ab9b59dc905fb68080ff8080a8a06f869d849d69f194df0c5e003a302aa360309a8a75eb50867f8f4c90484d8fe6cc63d4d3bc1f4d5ac456e75678ad09209f744a4aea5857e2771f0c351623f90f72418d086862c66d4270d8b04c13814d8279050ff9e9944c8d491377da87");
276        let sb = SpendBundle::from_bytes(&bytes)?;
277        let puzzle_clvm = sb.coin_spends[0].puzzle_reveal.to_clvm(ctx)?;
278        let puz = Puzzle::parse(ctx, puzzle_clvm);
279        let sol = sb.coin_spends[0].solution.to_clvm(ctx)?;
280        let children = Clawback::parse_children(ctx, puz, sol)
281            .expect("we should have found the child")
282            .expect("we should have found children");
283        assert_eq!(children.len(), 1);
284        Ok(())
285    }
286
287    #[test]
288    #[allow(clippy::similar_names)]
289    fn test_clawback_coin_clawback() -> anyhow::Result<()> {
290        let mut sim = Simulator::new();
291        let ctx = &mut SpendContext::new();
292
293        let alice = sim.bls(1);
294        let alice_p2 = StandardLayer::new(alice.pk);
295
296        let clawback = Clawback {
297            timelock: u64::MAX,
298            sender_puzzle_hash: alice.puzzle_hash,
299            receiver_puzzle_hash: Bytes32::default(),
300        };
301        let clawback_puzzle_hash = clawback.to_layer().tree_hash().into();
302
303        alice_p2.spend(
304            ctx,
305            alice.coin,
306            Conditions::new().create_coin(clawback_puzzle_hash, 1, Memos::None),
307        )?;
308        let clawback_coin = Coin::new(alice.coin.coin_id(), clawback_puzzle_hash, 1);
309
310        sim.spend_coins(ctx.take(), &[alice.sk.clone()])?;
311
312        let inner = alice_p2.spend_with_conditions(ctx, Conditions::new().reserve_fee(1))?;
313        let sender_spend = clawback.sender_spend(ctx, inner)?;
314        ctx.spend(clawback_coin, sender_spend)?;
315
316        sim.spend_coins(ctx.take(), &[alice.sk])?;
317
318        Ok(())
319    }
320}