chia_sdk_driver/primitives/action_layer/
medieval_vault.rs

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    // Mark this as unsafe since the transaction may be replayable
163    //  across coin generations and networks if delegated puzzle is not
164    //  properly secured.
165    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}