chia-sdk-driver 0.36.0

Driver code for interacting with standard puzzles on the Chia blockchain.
Documentation
use chia_protocol::{Bytes, Bytes32};
use chia_puzzle_types::Memos;
use chia_puzzles::SINGLETON_TOP_LAYER_V1_1_HASH;
use chia_sdk_types::{
    Condition, Conditions,
    puzzles::{STATE_SCHEDULER_PUZZLE_HASH, StateSchedulerLayerArgs, StateSchedulerLayerSolution},
};
use clvm_traits::{FromClvm, clvm_quote, match_quote};
use clvmr::{Allocator, NodePtr};

use crate::{DriverError, Layer, Puzzle, SpendContext, XchandlesRegistryReceivedMessagePrefix};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StateSchedulerLayer {
    pub receiver_singleton_struct_hash: Bytes32,
    pub new_state_hash: Bytes32,
    pub required_timestamp: u64,
    pub new_puzzle_hash: Bytes32,
}

impl StateSchedulerLayer {
    pub fn new(
        receiver_singleton_struct_hash: Bytes32,
        new_state_hash: Bytes32,
        required_timestamp: u64,
        new_puzzle_hash: Bytes32,
    ) -> Self {
        Self {
            receiver_singleton_struct_hash,
            new_state_hash,
            required_timestamp,
            new_puzzle_hash,
        }
    }
}

impl Layer for StateSchedulerLayer {
    type Solution = StateSchedulerLayerSolution<()>;

    fn parse_puzzle(allocator: &Allocator, puzzle: Puzzle) -> Result<Option<Self>, DriverError> {
        let Some(puzzle) = puzzle.as_curried() else {
            return Ok(None);
        };

        if puzzle.mod_hash != STATE_SCHEDULER_PUZZLE_HASH {
            return Ok(None);
        }

        let args = StateSchedulerLayerArgs::<Bytes, NodePtr>::from_clvm(allocator, puzzle.args)?;

        if args.singleton_mod_hash != SINGLETON_TOP_LAYER_V1_1_HASH.into() {
            return Err(DriverError::NonStandardLayer);
        }

        let (_q, conditions) =
            <match_quote!(Vec<Condition<NodePtr>>)>::from_clvm(allocator, args.inner_puzzle)?;
        let (
            Some(Condition::AssertSecondsAbsolute(assert_seconds_condition)),
            Some(Condition::CreateCoin(create_coin_condition)),
        ) = conditions
            .into_iter()
            .fold(
                (None, None),
                |(assert_seconds, create_coin), cond| match cond {
                    Condition::AssertSecondsAbsolute(_) if assert_seconds.is_none() => {
                        (Some(cond), create_coin)
                    }
                    Condition::CreateCoin(_) if create_coin.is_none() => {
                        (assert_seconds, Some(cond))
                    }
                    _ => (assert_seconds, create_coin),
                },
            )
        else {
            return Err(DriverError::NonStandardLayer);
        };

        let prefix_and_message = args.prefix_and_message;
        if prefix_and_message.len() != 33 {
            return Err(DriverError::NonStandardLayer);
        }
        let new_state_hash = Bytes32::new(
            prefix_and_message[1..]
                .try_into()
                .map_err(|_| DriverError::NonStandardLayer)?,
        );

        Ok(Some(Self {
            receiver_singleton_struct_hash: args.receiver_singleton_struct_hash,
            new_state_hash,
            required_timestamp: assert_seconds_condition.seconds,
            new_puzzle_hash: create_coin_condition.puzzle_hash,
        }))
    }

    fn parse_solution(
        allocator: &Allocator,
        solution: NodePtr,
    ) -> Result<Self::Solution, DriverError> {
        StateSchedulerLayerSolution::from_clvm(allocator, solution).map_err(DriverError::FromClvm)
    }

    fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result<NodePtr, DriverError> {
        let base_conditions = Conditions::new()
            .create_coin(self.new_puzzle_hash, 1, Memos::None)
            .assert_seconds_absolute(self.required_timestamp);

        let inner_puzzle = ctx.alloc(&clvm_quote!(base_conditions))?;

        ctx.curry(StateSchedulerLayerArgs::<Bytes, NodePtr> {
            singleton_mod_hash: SINGLETON_TOP_LAYER_V1_1_HASH.into(),
            receiver_singleton_struct_hash: self.receiver_singleton_struct_hash,
            prefix_and_message: XchandlesRegistryReceivedMessagePrefix::update_state(
                self.new_state_hash.into(),
            )
            .into(),
            inner_puzzle,
        })
    }

    fn construct_solution(
        &self,
        ctx: &mut SpendContext,
        solution: Self::Solution,
    ) -> Result<NodePtr, DriverError> {
        ctx.alloc(&solution)
    }
}

#[cfg(test)]
mod tests {
    use chia_protocol::{Bytes, Bytes32};
    use chia_puzzle_types::Memos;
    use chia_puzzles::SINGLETON_TOP_LAYER_V1_1_HASH;
    use chia_sdk_types::{
        Condition, Conditions,
        puzzles::{STATE_SCHEDULER_PUZZLE_HASH, StateSchedulerLayerArgs},
    };
    use clvm_traits::{clvm_quote, match_quote};
    use clvm_utils::ToTreeHash;

    use crate::{Layer, Puzzle, SpendContext, XchandlesRegistryReceivedMessagePrefix};

    use super::*;

    fn sample_layer() -> StateSchedulerLayer {
        StateSchedulerLayer::new(
            Bytes32::new([1; 32]),
            Bytes32::new([2; 32]),
            1_700_000_000,
            Bytes32::new([3; 32]),
        )
    }

    #[test]
    fn test_state_scheduler_layer_roundtrip() -> anyhow::Result<()> {
        let mut ctx = SpendContext::new();
        let layer = sample_layer();

        let ptr = layer.construct_puzzle(&mut ctx)?;
        let puzzle = Puzzle::parse(&ctx, ptr);
        let roundtrip = StateSchedulerLayer::parse_puzzle(&ctx, puzzle)?.expect("parse");

        assert_eq!(roundtrip, layer);
        assert_eq!(
            hex::encode(ctx.tree_hash(ptr)),
            hex::encode(layer_tree_hash(&layer))
        );

        Ok(())
    }

    #[test]
    fn test_state_scheduler_layer_emits_assert_seconds_absolute() -> anyhow::Result<()> {
        let mut ctx = SpendContext::new();
        let layer = sample_layer();

        let ptr = layer.construct_puzzle(&mut ctx)?;
        let puzzle = Puzzle::parse(&ctx, ptr).as_curried().expect("curried");
        let args = StateSchedulerLayerArgs::<Bytes, NodePtr>::from_clvm(&ctx, puzzle.args)?;
        let (_q, conditions) =
            <match_quote!(Vec<Condition<NodePtr>>)>::from_clvm(&ctx, args.inner_puzzle)?;

        assert!(conditions.iter().any(|c| {
            matches!(
                c,
                Condition::AssertSecondsAbsolute(cond) if cond.seconds == layer.required_timestamp
            )
        }));
        assert!(
            conditions
                .iter()
                .all(|c| !matches!(c, Condition::AssertHeightAbsolute(_)))
        );

        Ok(())
    }

    #[test]
    fn test_state_scheduler_layer_rejects_height_absolute() -> anyhow::Result<()> {
        let mut ctx = SpendContext::new();
        let layer = sample_layer();

        let height_conditions = Conditions::new()
            .create_coin(layer.new_puzzle_hash, 1, Memos::None)
            .assert_height_absolute(42);
        let inner_puzzle = ctx.alloc(&clvm_quote!(height_conditions))?;
        let ptr = ctx.curry(StateSchedulerLayerArgs::<chia_protocol::Bytes, NodePtr> {
            singleton_mod_hash: SINGLETON_TOP_LAYER_V1_1_HASH.into(),
            receiver_singleton_struct_hash: layer.receiver_singleton_struct_hash,
            prefix_and_message: XchandlesRegistryReceivedMessagePrefix::update_state(
                layer.new_state_hash.into(),
            )
            .into(),
            inner_puzzle,
        })?;

        let puzzle = Puzzle::parse(&ctx, ptr);
        let err = StateSchedulerLayer::parse_puzzle(&ctx, puzzle).unwrap_err();
        assert!(matches!(err, DriverError::NonStandardLayer));

        Ok(())
    }

    #[test]
    fn test_outer_module_hash_unchanged() {
        assert_eq!(
            hex::encode(STATE_SCHEDULER_PUZZLE_HASH),
            "8811d56e9efd2c9f449ea10cb00e00417b372f46d9d3a00ddf632f292de7e2c3"
        );
    }

    fn layer_tree_hash(layer: &StateSchedulerLayer) -> clvm_utils::TreeHash {
        let prefix_and_message: chia_protocol::Bytes =
            XchandlesRegistryReceivedMessagePrefix::update_state(layer.new_state_hash.into())
                .into();
        StateSchedulerLayerArgs::<clvm_utils::TreeHash, _>::curry_tree_hash(
            layer.receiver_singleton_struct_hash,
            prefix_and_message.tree_hash(),
            &clvm_quote!(vec![
                Condition::<()>::create_coin(layer.new_puzzle_hash, 1, Memos::None),
                Condition::assert_seconds_absolute(layer.required_timestamp),
            ]),
        )
    }
}