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