chik-sdk-driver 0.25.0

Driver code for interacting with standard puzzles on the Chik blockchain.
Documentation
use crate::{DriverError, Layer, P2OneOfManyLayer, Puzzle, Spend, SpendContext};
use chik_protocol::{Bytes, Bytes32};
use chik_puzzles::AUGMENTED_CONDITION_HASH;
use chik_sdk_types::{
    conditions::Remark,
    puzzles::{
        AugmentedConditionArgs, AugmentedConditionSolution, P2CurriedArgs, P2CurriedSolution,
        P2OneOfManySolution, P2_CURRIED_PUZZLE_HASH,
    },
    run_puzzle, Condition, MerkleTree,
};
use chik_streamable_macro::streamable;
use chik_traits::Streamable;
use klvm_traits::klvm_list;
use klvm_traits::FromKlvm;
use klvm_traits::ToKlvm;
use klvm_utils::{CurriedProgram, ToTreeHash, TreeHash};
use klvmr::{Allocator, NodePtr};

#[streamable]
pub struct VersionedBlob {
    version: u16,
    blob: Bytes,
}

#[streamable]
#[derive(Copy)]
pub struct Clawback {
    /// The number of seconds until this clawback can be claimed by the recipient.
    pub timelock: u64,
    /// The original sender of the coin, who can claw it back until claimed.
    pub sender_puzzle_hash: Bytes32,
    /// The intended recipient who can claim after the timelock period is up.
    pub receiver_puzzle_hash: Bytes32,
}

impl Clawback {
    pub fn parse_children(
        allocator: &mut Allocator,
        parent_puzzle: Puzzle, // this could be any puzzle type
        parent_solution: NodePtr,
    ) -> Result<Option<Vec<Self>>, DriverError>
    where
        Self: Sized,
    {
        let output = run_puzzle(allocator, parent_puzzle.ptr(), parent_solution)?;
        let conditions = Vec::<Condition>::from_klvm(allocator, output)?;
        let mut outputs = Vec::<Clawback>::new();
        let mut metadatas = Vec::<Clawback>::new();
        let mut puzhashes = Vec::<[u8; 32]>::with_capacity(conditions.len());
        for condition in conditions {
            match condition {
                Condition::CreateCoin(cc) => puzhashes.push(cc.puzzle_hash.into()),
                Condition::Remark(rm) => match allocator.sexp(rm.rest) {
                    klvmr::SExp::Atom => continue,
                    klvmr::SExp::Pair(first, rest) => {
                        match allocator.sexp(first) {
                            klvmr::SExp::Atom => {
                                let Some(atom) = allocator.small_number(first) else {
                                    continue;
                                };
                                if atom != 2 {
                                    continue;
                                } // magic number for Clawback in REMARK
                            }
                            klvmr::SExp::Pair(_, _) => continue,
                        }
                        // we have seen the magic number
                        // try to deserialise blob
                        match allocator.sexp(rest) {
                            klvmr::SExp::Atom => continue,
                            klvmr::SExp::Pair(r_first, _r_rest) => match allocator.sexp(r_first) {
                                klvmr::SExp::Atom => {
                                    let rest_atom = &allocator.atom(r_first);
                                    metadatas.push(
                                        Clawback::from_bytes_unchecked(
                                            VersionedBlob::from_bytes_unchecked(rest_atom)
                                                .map_err(|_| DriverError::InvalidMemo)?
                                                .blob
                                                .as_ref(),
                                        )
                                        .map_err(|_| DriverError::InvalidMemo)?,
                                    );
                                }
                                klvmr::SExp::Pair(_, _) => continue,
                            },
                        }
                    }
                },
                _ => {}
            }
        }
        for &clawback in &metadatas {
            if puzhashes.contains(&clawback.to_layer().tree_hash().to_bytes()) {
                outputs.push(clawback);
            }
        }
        Ok(Some(outputs))
    }

    pub fn receiver_path_puzzle_hash(&self) -> TreeHash {
        CurriedProgram {
            program: TreeHash::new(AUGMENTED_CONDITION_HASH),
            args: AugmentedConditionArgs::new(
                Condition::<TreeHash>::assert_seconds_relative(self.timelock),
                TreeHash::from(self.receiver_puzzle_hash),
            ),
        }
        .tree_hash()
    }

    pub fn receiver_path_puzzle(
        &self,
        ctx: &mut SpendContext,
        inner_puzzle: NodePtr,
    ) -> Result<NodePtr, DriverError> {
        ctx.curry(AugmentedConditionArgs::new(
            Condition::<NodePtr>::assert_seconds_relative(self.timelock),
            inner_puzzle,
        ))
    }

    pub fn sender_path_puzzle_hash(&self) -> TreeHash {
        CurriedProgram {
            program: P2_CURRIED_PUZZLE_HASH,
            args: P2CurriedArgs::new(self.sender_puzzle_hash),
        }
        .tree_hash()
    }

    pub fn sender_path_puzzle(&self, ctx: &mut SpendContext) -> Result<NodePtr, DriverError> {
        ctx.curry(P2CurriedArgs::new(self.sender_puzzle_hash))
    }

    pub fn merkle_tree(&self) -> MerkleTree {
        MerkleTree::new(&[
            self.receiver_path_puzzle_hash().into(),
            self.sender_path_puzzle_hash().into(),
        ])
    }

    pub fn to_layer(&self) -> P2OneOfManyLayer {
        P2OneOfManyLayer::new(self.merkle_tree().root())
    }

    // this function returns the Remark condition required to hint at this clawback
    // it should be included alongside the createcoin that creates this
    pub fn get_remark_condition(
        &self,
        allocator: &mut Allocator,
    ) -> Result<Remark<NodePtr>, DriverError> {
        let vb = VersionedBlob {
            version: 1,
            blob: self
                .to_bytes()
                .map_err(|_| DriverError::InvalidMemo)?
                .into(),
        };
        // 2 is the magic number for clawback
        let node_ptr = klvm_list!(
            2,
            Bytes::new(vb.to_bytes().map_err(|_| DriverError::InvalidMemo)?)
        )
        .to_klvm(allocator)?;

        Ok(Remark::new(node_ptr))
    }

    pub fn receiver_spend(
        &self,
        ctx: &mut SpendContext,
        spend: Spend,
    ) -> Result<Spend, DriverError> {
        let merkle_tree = self.merkle_tree();

        let puzzle = self.receiver_path_puzzle(ctx, spend.puzzle)?;
        let solution = ctx.alloc(&AugmentedConditionSolution::new(spend.solution))?;

        let proof = merkle_tree
            .proof(ctx.tree_hash(puzzle).into())
            .ok_or(DriverError::InvalidMerkleProof)?;

        P2OneOfManyLayer::new(merkle_tree.root())
            .construct_spend(ctx, P2OneOfManySolution::new(proof, puzzle, solution))
    }

    pub fn sender_spend(&self, ctx: &mut SpendContext, spend: Spend) -> Result<Spend, DriverError> {
        let merkle_tree = self.merkle_tree();

        let puzzle = self.sender_path_puzzle(ctx)?;
        let solution = ctx.alloc(&P2CurriedSolution::new(spend.puzzle, spend.solution))?;

        let proof = merkle_tree
            .proof(ctx.tree_hash(puzzle).into())
            .ok_or(DriverError::InvalidMerkleProof)?;

        P2OneOfManyLayer::new(merkle_tree.root())
            .construct_spend(ctx, P2OneOfManySolution::new(proof, puzzle, solution))
    }
}

#[cfg(test)]
mod tests {
    use chik_protocol::{Coin, SpendBundle};
    use chik_puzzle_types::Memos;
    use chik_sdk_test::Simulator;
    use chik_sdk_types::Conditions;
    use klvm_traits::ToKlvm;

    use crate::{SpendWithConditions, StandardLayer};

    use super::*;

    #[test]
    #[allow(clippy::similar_names)]
    fn test_clawback_coin_claim() -> anyhow::Result<()> {
        let mut sim = Simulator::new();
        let ctx = &mut SpendContext::new();

        let alice = sim.bls(1);
        let alice_p2 = StandardLayer::new(alice.pk);

        let bob = sim.bls(1);
        let bob_p2 = StandardLayer::new(bob.pk);

        let clawback = Clawback {
            timelock: 1,
            sender_puzzle_hash: alice.puzzle_hash,
            receiver_puzzle_hash: bob.puzzle_hash,
        };
        let clawback_puzzle_hash = clawback.to_layer().tree_hash().into();
        let coin = alice.coin;
        let conditions = Conditions::new()
            .create_coin(clawback_puzzle_hash, 1, Memos::None)
            .with(clawback.get_remark_condition(ctx)?);
        alice_p2.spend(ctx, coin, conditions)?;

        let cs = ctx.take();

        let clawback_coin = Coin::new(coin.coin_id(), clawback_puzzle_hash, 1);

        sim.spend_coins(cs, &[alice.sk])?;

        let puzzle_reveal = sim
            .puzzle_reveal(coin.coin_id())
            .expect("missing puzzle")
            .to_klvm(ctx)?;

        let solution = sim
            .solution(coin.coin_id())
            .expect("missing solution")
            .to_klvm(ctx)?;

        let puzzle = Puzzle::parse(ctx, puzzle_reveal);

        // check we can recreate Clawback from the spend
        let children = Clawback::parse_children(ctx, puzzle, solution)
            .expect("we should have found the child")
            .expect("we should have found children");
        assert_eq!(children.len(), 1);
        assert_eq!(children[0], clawback);

        let bob_inner = bob_p2.spend_with_conditions(ctx, Conditions::new().reserve_fee(1))?;
        let receiver_spend = clawback.receiver_spend(ctx, bob_inner)?;
        ctx.spend(clawback_coin, receiver_spend)?;

        sim.spend_coins(ctx.take(), &[bob.sk])?;

        Ok(())
    }

    #[test]
    fn test_clawback_compatible_with_python() -> anyhow::Result<()> {
        let ctx = &mut SpendContext::new();
        let bytes = hex_literal::hex!("00000001e3b0c44298fc1c149afbf4c8996fb924000000000000000000000000000000014eb7420f8651b09124e1d40cdc49eeddacbaa0c25e6ae5a0a482fac8e3b5259f000001977420dc00ff02ffff01ff02ffff01ff02ffff03ff0bffff01ff02ffff03ffff09ff05ffff1dff0bffff1effff0bff0bffff02ff06ffff04ff02ffff04ff17ff8080808080808080ffff01ff02ff17ff2f80ffff01ff088080ff0180ffff01ff04ffff04ff04ffff04ff05ffff04ffff02ff06ffff04ff02ffff04ff17ff80808080ff80808080ffff02ff17ff2f808080ff0180ffff04ffff01ff32ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff06ffff04ff02ffff04ff09ff80808080ffff02ff06ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080ffff04ffff01b0b50b02adba343fff8bf3a94e92ed7df43743aedf0006b81a6c00ae573c0cce7d08216f60886fe84e4078a5209b0e5171ff018080ff80ffff01ffff33ffa0aeb663f32c4cfe1122710bc03cdc086f87e3243c055e8bebba42189cafbaf465ff840098968080ffff01ff02ffc04e00010000004800000000000000644eb7420f8651b09124e1d40cdc49eeddacbaa0c25e6ae5a0a482fac8e3b5259f5abb5d5568b4a7411dd97b3356cfedfac09b5fb35621a7fa29ab9b59dc905fb68080ff8080a8a06f869d849d69f194df0c5e003a302aa360309a8a75eb50867f8f4c90484d8fe6cc63d4d3bc1f4d5ac456e75678ad09209f744a4aea5857e2771f0c351623f90f72418d086862c66d4270d8b04c13814d8279050ff9e9944c8d491377da87");
        let sb = SpendBundle::from_bytes(&bytes)?;
        let puzzle_klvm = sb.coin_spends[0].puzzle_reveal.to_klvm(ctx)?;
        let puz = Puzzle::parse(ctx, puzzle_klvm);
        let sol = sb.coin_spends[0].solution.to_klvm(ctx)?;
        let children = Clawback::parse_children(ctx, puz, sol)
            .expect("we should have found the child")
            .expect("we should have found children");
        assert_eq!(children.len(), 1);
        Ok(())
    }

    #[test]
    #[allow(clippy::similar_names)]
    fn test_clawback_coin_clawback() -> anyhow::Result<()> {
        let mut sim = Simulator::new();
        let ctx = &mut SpendContext::new();

        let alice = sim.bls(1);
        let alice_p2 = StandardLayer::new(alice.pk);

        let clawback = Clawback {
            timelock: u64::MAX,
            sender_puzzle_hash: alice.puzzle_hash,
            receiver_puzzle_hash: Bytes32::default(),
        };
        let clawback_puzzle_hash = clawback.to_layer().tree_hash().into();

        alice_p2.spend(
            ctx,
            alice.coin,
            Conditions::new().create_coin(clawback_puzzle_hash, 1, Memos::None),
        )?;
        let clawback_coin = Coin::new(alice.coin.coin_id(), clawback_puzzle_hash, 1);

        sim.spend_coins(ctx.take(), &[alice.sk.clone()])?;

        let inner = alice_p2.spend_with_conditions(ctx, Conditions::new().reserve_fee(1))?;
        let sender_spend = clawback.sender_spend(ctx, inner)?;
        ctx.spend(clawback_coin, sender_spend)?;

        sim.spend_coins(ctx.take(), &[alice.sk])?;

        Ok(())
    }
}