chia_sdk_driver/layers/action_layer/
state_scheduler_layer.rs1use 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}