1use chia_bls::PublicKey;
2use chia_protocol::{Bytes32, Coin, CoinSpend};
3use chia_puzzle_types::{
4 EveProof, LineageProof, Memos, Proof,
5 singleton::{LauncherSolution, SingletonArgs, SingletonSolution, SingletonStruct},
6};
7use chia_puzzles::{SINGLETON_LAUNCHER_HASH, SINGLETON_TOP_LAYER_V1_1_HASH};
8use chia_sdk_types::{
9 Condition, Conditions,
10 puzzles::{P2MOfNDelegateDirectArgs, P2MOfNDelegateDirectSolution, StateSchedulerLayerArgs},
11};
12use clvm_traits::{FromClvm, ToClvm, clvm_quote};
13use clvm_utils::ToTreeHash;
14use clvmr::{Allocator, NodePtr, serde::node_from_bytes};
15
16use crate::{
17 DriverError, Layer, MOfNLayer, Puzzle, Singleton, SingletonInfo, SingletonLayer, Spend,
18 SpendContext,
19};
20
21use super::{MedievalVaultHint, MedievalVaultInfo};
22
23pub type MedievalVault = Singleton<MedievalVaultInfo>;
24
25impl MedievalVault {
26 pub fn from_launcher_spend(
27 ctx: &mut SpendContext,
28 launcher_spend: &CoinSpend,
29 ) -> Result<Option<Self>, DriverError> {
30 if launcher_spend.coin.puzzle_hash != SINGLETON_LAUNCHER_HASH.into() {
31 return Ok(None);
32 }
33
34 let solution = node_from_bytes(ctx, &launcher_spend.solution)?;
35 let solution = ctx.extract::<LauncherSolution<NodePtr>>(solution)?;
36
37 let Ok(hint) = ctx.extract::<MedievalVaultHint>(solution.key_value_list) else {
38 return Ok(None);
39 };
40
41 let info = MedievalVaultInfo::from_hint(hint);
42
43 let new_coin = Coin::new(
44 launcher_spend.coin.coin_id(),
45 SingletonArgs::curry_tree_hash(info.launcher_id, info.inner_puzzle_hash()).into(),
46 1,
47 );
48
49 if launcher_spend.coin.amount != new_coin.amount
50 || new_coin.puzzle_hash != solution.singleton_puzzle_hash
51 {
52 return Ok(None);
53 }
54
55 Ok(Some(Self::new(
56 new_coin,
57 Proof::Eve(EveProof {
58 parent_parent_coin_info: launcher_spend.coin.parent_coin_info,
59 parent_amount: launcher_spend.coin.amount,
60 }),
61 info,
62 )))
63 }
64
65 pub fn child(&self, new_m: usize, new_public_key_list: Vec<PublicKey>) -> Self {
66 let child_proof = Proof::Lineage(LineageProof {
67 parent_parent_coin_info: self.coin.parent_coin_info,
68 parent_inner_puzzle_hash: self.info.inner_puzzle_hash().into(),
69 parent_amount: self.coin.amount,
70 });
71
72 let child_info = MedievalVaultInfo::new(self.info.launcher_id, new_m, new_public_key_list);
73 let child_inner_puzzle_hash = child_info.inner_puzzle_hash();
74
75 Self {
76 coin: Coin::new(
77 self.coin.coin_id(),
78 SingletonArgs::curry_tree_hash(self.info.launcher_id, child_inner_puzzle_hash)
79 .into(),
80 1,
81 ),
82 proof: child_proof,
83 info: child_info,
84 }
85 }
86
87 pub fn from_parent_spend(
88 ctx: &mut SpendContext,
89 parent_spend: &CoinSpend,
90 ) -> Result<Option<Self>, DriverError> {
91 if parent_spend.coin.puzzle_hash == SINGLETON_LAUNCHER_HASH.into() {
92 return Self::from_launcher_spend(ctx, parent_spend);
93 }
94
95 let solution = node_from_bytes(ctx, &parent_spend.solution)?;
96 let puzzle = node_from_bytes(ctx, &parent_spend.puzzle_reveal)?;
97
98 let puzzle_puzzle = Puzzle::from_clvm(ctx, puzzle)?;
99 let Some(parent_layers) = SingletonLayer::<MOfNLayer>::parse_puzzle(ctx, puzzle_puzzle)?
100 else {
101 return Ok(None);
102 };
103
104 let output = ctx.run(puzzle, solution)?;
105 let output = ctx.extract::<Conditions<NodePtr>>(output)?;
106 let recreate_condition = output
107 .into_iter()
108 .find(|c| matches!(c, Condition::CreateCoin(..)));
109 let Some(Condition::CreateCoin(recreate_condition)) = recreate_condition else {
110 return Ok(None);
111 };
112
113 let (new_m, new_pubkeys) = if let Memos::Some(memos) = recreate_condition.memos {
114 if let Ok(memos) = ctx.extract::<MedievalVaultHint>(memos) {
115 (memos.m, memos.public_key_list)
116 } else {
117 (
118 parent_layers.inner_puzzle.m,
119 parent_layers.inner_puzzle.public_key_list.clone(),
120 )
121 }
122 } else {
123 (
124 parent_layers.inner_puzzle.m,
125 parent_layers.inner_puzzle.public_key_list.clone(),
126 )
127 };
128
129 let parent_info = MedievalVaultInfo::new(
130 parent_layers.launcher_id,
131 parent_layers.inner_puzzle.m,
132 parent_layers.inner_puzzle.public_key_list,
133 );
134 let new_info = MedievalVaultInfo::new(parent_layers.launcher_id, new_m, new_pubkeys);
135
136 let new_coin = Coin::new(
137 parent_spend.coin.coin_id(),
138 SingletonArgs::curry_tree_hash(parent_layers.launcher_id, new_info.inner_puzzle_hash())
139 .into(),
140 1,
141 );
142
143 Ok(Some(Self::new(
144 new_coin,
145 Proof::Lineage(LineageProof {
146 parent_parent_coin_info: parent_spend.coin.parent_coin_info,
147 parent_inner_puzzle_hash: parent_info.inner_puzzle_hash().into(),
148 parent_amount: parent_spend.coin.amount,
149 }),
150 new_info,
151 )))
152 }
153
154 pub fn delegated_conditions(
155 conditions: Conditions,
156 coin_id: Bytes32,
157 genesis_challenge: NodePtr,
158 ) -> Conditions {
159 MOfNLayer::ensure_non_replayable(conditions, coin_id, genesis_challenge)
160 }
161
162 pub fn spend_sunsafe(
166 self,
167 ctx: &mut SpendContext,
168 used_pubkeys: &[PublicKey],
169 delegated_puzzle: NodePtr,
170 delegated_solution: NodePtr,
171 ) -> Result<(), DriverError> {
172 let lineage_proof = self.proof;
173 let coin = self.coin;
174
175 let layers = self.info.into_layers();
176
177 let puzzle = layers.construct_puzzle(ctx)?;
178 let solution = layers.construct_solution(
179 ctx,
180 SingletonSolution {
181 lineage_proof,
182 amount: coin.amount,
183 inner_solution: P2MOfNDelegateDirectSolution {
184 selectors: P2MOfNDelegateDirectArgs::selectors_for_used_pubkeys(
185 &self.info.public_key_list,
186 used_pubkeys,
187 ),
188 delegated_puzzle,
189 delegated_solution,
190 },
191 },
192 )?;
193
194 ctx.spend(coin, Spend::new(puzzle, solution))?;
195
196 Ok(())
197 }
198
199 pub fn spend(
200 self,
201 ctx: &mut SpendContext,
202 used_pubkeys: &[PublicKey],
203 conditions: Conditions,
204 genesis_challenge: Bytes32,
205 ) -> Result<(), DriverError> {
206 let genesis_challenge = ctx.alloc(&genesis_challenge)?;
207 let delegated_puzzle = ctx.alloc(&clvm_quote!(Self::delegated_conditions(
208 conditions,
209 self.coin.coin_id(),
210 genesis_challenge
211 )))?;
212
213 self.spend_sunsafe(ctx, used_pubkeys, delegated_puzzle, NodePtr::NIL)
214 }
215
216 pub fn rekey_create_coin_unsafe(
217 ctx: &mut SpendContext,
218 launcher_id: Bytes32,
219 new_m: usize,
220 new_pubkeys: Vec<PublicKey>,
221 ) -> Result<Conditions, DriverError> {
222 let new_info = MedievalVaultInfo::new(launcher_id, new_m, new_pubkeys);
223
224 let memos = ctx.alloc(&new_info.to_hint())?;
225 Ok(Conditions::new().create_coin(
226 new_info.inner_puzzle_hash().into(),
227 1,
228 Memos::Some(memos),
229 ))
230 }
231
232 pub fn delegated_puzzle_for_rekey(
233 ctx: &mut SpendContext,
234 launcher_id: Bytes32,
235 new_m: usize,
236 new_pubkeys: Vec<PublicKey>,
237 coin_id: Bytes32,
238 genesis_challenge: Bytes32,
239 ) -> Result<NodePtr, DriverError> {
240 let genesis_challenge = ctx.alloc(&genesis_challenge)?;
241 let conditions = Self::rekey_create_coin_unsafe(ctx, launcher_id, new_m, new_pubkeys)?;
242
243 ctx.alloc(&clvm_quote!(Self::delegated_conditions(
244 conditions,
245 coin_id,
246 genesis_challenge
247 )))
248 }
249
250 pub fn delegated_puzzle_for_flexible_send_message<M>(
251 ctx: &mut SpendContext,
252 message: M,
253 receiver_launcher_id: Bytes32,
254 my_coin: Coin,
255 my_info: &MedievalVaultInfo,
256 genesis_challenge: Bytes32,
257 ) -> Result<NodePtr, DriverError>
258 where
259 M: ToClvm<Allocator>,
260 {
261 let conditions = Conditions::new().create_coin(
262 my_info.inner_puzzle_hash().into(),
263 my_coin.amount,
264 ctx.hint(my_info.launcher_id)?,
265 );
266 let genesis_challenge = ctx.alloc(&genesis_challenge)?;
267
268 let innermost_delegated_puzzle_ptr = ctx.alloc(&clvm_quote!(
269 Self::delegated_conditions(conditions, my_coin.coin_id(), genesis_challenge)
270 ))?;
271
272 ctx.curry(StateSchedulerLayerArgs::<M, NodePtr> {
273 singleton_mod_hash: SINGLETON_TOP_LAYER_V1_1_HASH.into(),
274 receiver_singleton_struct_hash: SingletonStruct::new(receiver_launcher_id)
275 .tree_hash()
276 .into(),
277 message,
278 inner_puzzle: innermost_delegated_puzzle_ptr,
279 })
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use chia_sdk_test::Simulator;
286 use chia_sdk_types::TESTNET11_CONSTANTS;
287
288 use crate::Launcher;
289
290 use super::*;
291
292 #[test]
293 fn test_medieval_vault() -> anyhow::Result<()> {
294 let ctx = &mut SpendContext::new();
295 let mut sim = Simulator::new();
296
297 let user1 = sim.bls(0);
298 let user2 = sim.bls(0);
299 let user3 = sim.bls(0);
300
301 let multisig_configs = [
302 (1, vec![user1.pk, user2.pk]),
303 (2, vec![user1.pk, user2.pk]),
304 (3, vec![user1.pk, user2.pk, user3.pk]),
305 (3, vec![user1.pk, user2.pk, user3.pk]),
306 (1, vec![user1.pk, user2.pk, user3.pk]),
307 (2, vec![user1.pk, user2.pk, user3.pk]),
308 ];
309
310 let launcher_coin = sim.new_coin(SINGLETON_LAUNCHER_HASH.into(), 1);
311 let launcher = Launcher::new(launcher_coin.parent_coin_info, 1);
312 let launch_hints = MedievalVaultHint {
313 my_launcher_id: launcher_coin.coin_id(),
314 m: multisig_configs[0].0,
315 public_key_list: multisig_configs[0].1.clone(),
316 };
317 let (_conds, first_vault_coin) = launcher.spend(
318 ctx,
319 P2MOfNDelegateDirectArgs::curry_tree_hash(
320 multisig_configs[0].0,
321 multisig_configs[0].1.clone(),
322 )
323 .into(),
324 launch_hints,
325 )?;
326
327 let spends = ctx.take();
328 let launcher_spend = spends.first().unwrap().clone();
329 sim.spend_coins(spends, &[])?;
330
331 let mut vault = MedievalVault::from_parent_spend(ctx, &launcher_spend)?.unwrap();
332 assert_eq!(vault.coin, first_vault_coin);
333
334 let mut current_vault_info = MedievalVaultInfo {
335 launcher_id: launcher_coin.coin_id(),
336 m: multisig_configs[0].0,
337 public_key_list: multisig_configs[0].1.clone(),
338 };
339 assert_eq!(vault.info, current_vault_info);
340
341 for (i, (m, pubkeys)) in multisig_configs.clone().into_iter().enumerate().skip(1) {
342 let mut recreate_memos = ctx.alloc(&vec![vault.info.launcher_id])?;
343
344 let info_changed =
345 multisig_configs[i - 1].0 != m || multisig_configs[i - 1].1 != pubkeys;
346 if info_changed {
347 recreate_memos = ctx.alloc(&MedievalVaultHint {
348 my_launcher_id: vault.info.launcher_id,
349 m,
350 public_key_list: pubkeys.clone(),
351 })?;
352 }
353 current_vault_info = MedievalVaultInfo {
354 launcher_id: vault.info.launcher_id,
355 m,
356 public_key_list: pubkeys.clone(),
357 };
358
359 let recreate_condition = Conditions::<NodePtr>::new().create_coin(
360 current_vault_info.inner_puzzle_hash().into(),
361 1,
362 Memos::Some(recreate_memos),
363 );
364
365 let mut used_keys = 0;
366 let mut used_pubkeys = vec![];
367 while used_keys < vault.info.m {
368 used_pubkeys.push(current_vault_info.public_key_list[used_keys]);
369 used_keys += 1;
370 }
371 vault.clone().spend(
372 ctx,
373 &used_pubkeys,
374 recreate_condition,
375 TESTNET11_CONSTANTS.genesis_challenge,
376 )?;
377
378 let spends = ctx.take();
379 let vault_spend = spends.first().unwrap().clone();
380 sim.spend_coins(
381 spends,
382 &[user1.sk.clone(), user2.sk.clone(), user3.sk.clone()],
383 )?;
384
385 let check_vault = vault.child(m, pubkeys);
386
387 vault = MedievalVault::from_parent_spend(ctx, &vault_spend)?.unwrap();
388 assert_eq!(vault.info, current_vault_info);
389 assert_eq!(vault, check_vault);
390 }
391
392 Ok(())
393 }
394}