Skip to main content

chia_sdk_driver/layers/action_layer/
state_scheduler_layer.rs

1use chia_protocol::{Bytes, Bytes32};
2use chia_puzzle_types::Memos;
3use chia_puzzles::SINGLETON_TOP_LAYER_V1_1_HASH;
4use chia_sdk_types::{
5    Condition, Conditions,
6    puzzles::{STATE_SCHEDULER_PUZZLE_HASH, StateSchedulerLayerArgs, StateSchedulerLayerSolution},
7};
8use clvm_traits::{FromClvm, clvm_quote, match_quote};
9use clvmr::{Allocator, NodePtr};
10
11use crate::{DriverError, Layer, Puzzle, SpendContext, XchandlesRegistryReceivedMessagePrefix};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct StateSchedulerLayer {
15    pub receiver_singleton_struct_hash: Bytes32,
16    pub new_state_hash: Bytes32,
17    pub required_timestamp: u64,
18    pub new_puzzle_hash: Bytes32,
19}
20
21impl StateSchedulerLayer {
22    pub fn new(
23        receiver_singleton_struct_hash: Bytes32,
24        new_state_hash: Bytes32,
25        required_timestamp: u64,
26        new_puzzle_hash: Bytes32,
27    ) -> Self {
28        Self {
29            receiver_singleton_struct_hash,
30            new_state_hash,
31            required_timestamp,
32            new_puzzle_hash,
33        }
34    }
35}
36
37impl Layer for StateSchedulerLayer {
38    type Solution = StateSchedulerLayerSolution<()>;
39
40    fn parse_puzzle(allocator: &Allocator, puzzle: Puzzle) -> Result<Option<Self>, DriverError> {
41        let Some(puzzle) = puzzle.as_curried() else {
42            return Ok(None);
43        };
44
45        if puzzle.mod_hash != STATE_SCHEDULER_PUZZLE_HASH {
46            return Ok(None);
47        }
48
49        let args = StateSchedulerLayerArgs::<Bytes, NodePtr>::from_clvm(allocator, puzzle.args)?;
50
51        if args.singleton_mod_hash != SINGLETON_TOP_LAYER_V1_1_HASH.into() {
52            return Err(DriverError::NonStandardLayer);
53        }
54
55        let (_q, conditions) =
56            <match_quote!(Vec<Condition<NodePtr>>)>::from_clvm(allocator, args.inner_puzzle)?;
57        let (
58            Some(Condition::AssertSecondsAbsolute(assert_seconds_condition)),
59            Some(Condition::CreateCoin(create_coin_condition)),
60        ) = conditions
61            .into_iter()
62            .fold(
63                (None, None),
64                |(assert_seconds, create_coin), cond| match cond {
65                    Condition::AssertSecondsAbsolute(_) if assert_seconds.is_none() => {
66                        (Some(cond), create_coin)
67                    }
68                    Condition::CreateCoin(_) if create_coin.is_none() => {
69                        (assert_seconds, Some(cond))
70                    }
71                    _ => (assert_seconds, create_coin),
72                },
73            )
74        else {
75            return Err(DriverError::NonStandardLayer);
76        };
77
78        let prefix_and_message = args.prefix_and_message;
79        if prefix_and_message.len() != 33 {
80            return Err(DriverError::NonStandardLayer);
81        }
82        let new_state_hash = Bytes32::new(
83            prefix_and_message[1..]
84                .try_into()
85                .map_err(|_| DriverError::NonStandardLayer)?,
86        );
87
88        Ok(Some(Self {
89            receiver_singleton_struct_hash: args.receiver_singleton_struct_hash,
90            new_state_hash,
91            required_timestamp: assert_seconds_condition.seconds,
92            new_puzzle_hash: create_coin_condition.puzzle_hash,
93        }))
94    }
95
96    fn parse_solution(
97        allocator: &Allocator,
98        solution: NodePtr,
99    ) -> Result<Self::Solution, DriverError> {
100        StateSchedulerLayerSolution::from_clvm(allocator, solution).map_err(DriverError::FromClvm)
101    }
102
103    fn construct_puzzle(&self, ctx: &mut SpendContext) -> Result<NodePtr, DriverError> {
104        let base_conditions = Conditions::new()
105            .create_coin(self.new_puzzle_hash, 1, Memos::None)
106            .assert_seconds_absolute(self.required_timestamp);
107
108        let inner_puzzle = ctx.alloc(&clvm_quote!(base_conditions))?;
109
110        ctx.curry(StateSchedulerLayerArgs::<Bytes, NodePtr> {
111            singleton_mod_hash: SINGLETON_TOP_LAYER_V1_1_HASH.into(),
112            receiver_singleton_struct_hash: self.receiver_singleton_struct_hash,
113            prefix_and_message: XchandlesRegistryReceivedMessagePrefix::update_state(
114                self.new_state_hash.into(),
115            )
116            .into(),
117            inner_puzzle,
118        })
119    }
120
121    fn construct_solution(
122        &self,
123        ctx: &mut SpendContext,
124        solution: Self::Solution,
125    ) -> Result<NodePtr, DriverError> {
126        ctx.alloc(&solution)
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use chia_protocol::{Bytes, Bytes32};
133    use chia_puzzle_types::Memos;
134    use chia_puzzles::SINGLETON_TOP_LAYER_V1_1_HASH;
135    use chia_sdk_types::{
136        Condition, Conditions,
137        puzzles::{STATE_SCHEDULER_PUZZLE_HASH, StateSchedulerLayerArgs},
138    };
139    use clvm_traits::{clvm_quote, match_quote};
140    use clvm_utils::ToTreeHash;
141
142    use crate::{Layer, Puzzle, SpendContext, XchandlesRegistryReceivedMessagePrefix};
143
144    use super::*;
145
146    fn sample_layer() -> StateSchedulerLayer {
147        StateSchedulerLayer::new(
148            Bytes32::new([1; 32]),
149            Bytes32::new([2; 32]),
150            1_700_000_000,
151            Bytes32::new([3; 32]),
152        )
153    }
154
155    #[test]
156    fn test_state_scheduler_layer_roundtrip() -> anyhow::Result<()> {
157        let mut ctx = SpendContext::new();
158        let layer = sample_layer();
159
160        let ptr = layer.construct_puzzle(&mut ctx)?;
161        let puzzle = Puzzle::parse(&ctx, ptr);
162        let roundtrip = StateSchedulerLayer::parse_puzzle(&ctx, puzzle)?.expect("parse");
163
164        assert_eq!(roundtrip, layer);
165        assert_eq!(
166            hex::encode(ctx.tree_hash(ptr)),
167            hex::encode(layer_tree_hash(&layer))
168        );
169
170        Ok(())
171    }
172
173    #[test]
174    fn test_state_scheduler_layer_emits_assert_seconds_absolute() -> anyhow::Result<()> {
175        let mut ctx = SpendContext::new();
176        let layer = sample_layer();
177
178        let ptr = layer.construct_puzzle(&mut ctx)?;
179        let puzzle = Puzzle::parse(&ctx, ptr).as_curried().expect("curried");
180        let args = StateSchedulerLayerArgs::<Bytes, NodePtr>::from_clvm(&ctx, puzzle.args)?;
181        let (_q, conditions) =
182            <match_quote!(Vec<Condition<NodePtr>>)>::from_clvm(&ctx, args.inner_puzzle)?;
183
184        assert!(conditions.iter().any(|c| {
185            matches!(
186                c,
187                Condition::AssertSecondsAbsolute(cond) if cond.seconds == layer.required_timestamp
188            )
189        }));
190        assert!(
191            conditions
192                .iter()
193                .all(|c| !matches!(c, Condition::AssertHeightAbsolute(_)))
194        );
195
196        Ok(())
197    }
198
199    #[test]
200    fn test_state_scheduler_layer_rejects_height_absolute() -> anyhow::Result<()> {
201        let mut ctx = SpendContext::new();
202        let layer = sample_layer();
203
204        let height_conditions = Conditions::new()
205            .create_coin(layer.new_puzzle_hash, 1, Memos::None)
206            .assert_height_absolute(42);
207        let inner_puzzle = ctx.alloc(&clvm_quote!(height_conditions))?;
208        let ptr = ctx.curry(StateSchedulerLayerArgs::<chia_protocol::Bytes, NodePtr> {
209            singleton_mod_hash: SINGLETON_TOP_LAYER_V1_1_HASH.into(),
210            receiver_singleton_struct_hash: layer.receiver_singleton_struct_hash,
211            prefix_and_message: XchandlesRegistryReceivedMessagePrefix::update_state(
212                layer.new_state_hash.into(),
213            )
214            .into(),
215            inner_puzzle,
216        })?;
217
218        let puzzle = Puzzle::parse(&ctx, ptr);
219        let err = StateSchedulerLayer::parse_puzzle(&ctx, puzzle).unwrap_err();
220        assert!(matches!(err, DriverError::NonStandardLayer));
221
222        Ok(())
223    }
224
225    #[test]
226    fn test_outer_module_hash_unchanged() {
227        assert_eq!(
228            hex::encode(STATE_SCHEDULER_PUZZLE_HASH),
229            "8811d56e9efd2c9f449ea10cb00e00417b372f46d9d3a00ddf632f292de7e2c3"
230        );
231    }
232
233    fn layer_tree_hash(layer: &StateSchedulerLayer) -> clvm_utils::TreeHash {
234        let prefix_and_message: chia_protocol::Bytes =
235            XchandlesRegistryReceivedMessagePrefix::update_state(layer.new_state_hash.into())
236                .into();
237        StateSchedulerLayerArgs::<clvm_utils::TreeHash, _>::curry_tree_hash(
238            layer.receiver_singleton_struct_hash,
239            prefix_and_message.tree_hash(),
240            &clvm_quote!(vec![
241                Condition::<()>::create_coin(layer.new_puzzle_hash, 1, Memos::None),
242                Condition::assert_seconds_absolute(layer.required_timestamp),
243            ]),
244        )
245    }
246}