miden_standards/account/auth/multisig_smart/
component.rs1use alloc::vec::Vec;
2
3use miden_protocol::Word;
4use miden_protocol::account::component::{
5 AccountComponentCode,
6 AccountComponentMetadata,
7 SchemaType,
8 StorageSchema,
9 StorageSlotSchema,
10};
11use miden_protocol::account::{
12 AccountComponent,
13 StorageMap,
14 StorageMapKey,
15 StorageSlot,
16 StorageSlotName,
17};
18use miden_protocol::errors::AccountError;
19use miden_protocol::utils::sync::LazyLock;
20
21use super::super::multisig::{
25 APPROVER_PUBKEYS_SLOT_NAME,
26 APPROVER_SCHEME_ID_SLOT_NAME,
27 EXECUTED_TRANSACTIONS_SLOT_NAME,
28 THRESHOLD_CONFIG_SLOT_NAME,
29};
30use super::ProcedurePolicy;
31use crate::account::account_component_code;
32use crate::account::auth::{Approver, ApproverSet, AuthMultisig};
33
34account_component_code!(MULTISIG_SMART_CODE, "miden-standards-auth-multisig-smart.masp");
35
36static PROCEDURE_POLICIES_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
43 StorageSlotName::new("miden::standards::auth::multisig_smart::procedure_policies")
44 .expect("storage slot name should be valid")
45});
46
47#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct AuthMultisigSmartConfig {
53 approver_set: ApproverSet,
54 procedure_policies: Vec<(Word, ProcedurePolicy)>,
55}
56
57impl AuthMultisigSmartConfig {
58 pub fn new(approver_set: ApproverSet) -> Self {
60 Self {
61 approver_set,
62 procedure_policies: Vec::new(),
63 }
64 }
65
66 pub fn with_proc_policies(
68 mut self,
69 proc_policies: Vec<(Word, ProcedurePolicy)>,
70 ) -> Result<Self, AccountError> {
71 validate_proc_policies(self.approver_set.approvers().len() as u32, &proc_policies)?;
72 self.procedure_policies = proc_policies;
73 Ok(self)
74 }
75
76 pub fn approver_set(&self) -> &ApproverSet {
77 &self.approver_set
78 }
79
80 pub fn approvers(&self) -> &[Approver] {
81 self.approver_set.approvers()
82 }
83
84 pub fn default_threshold(&self) -> u32 {
85 self.approver_set.threshold().get()
86 }
87
88 pub fn procedure_policies(&self) -> &[(Word, ProcedurePolicy)] {
89 &self.procedure_policies
90 }
91}
92
93fn validate_proc_policies(
94 num_approvers: u32,
95 proc_policies: &[(Word, ProcedurePolicy)],
96) -> Result<(), AccountError> {
97 let mut policy_roots = alloc::collections::BTreeSet::new();
100 for (proc_root, _) in proc_policies {
101 if !policy_roots.insert(*proc_root) {
102 return Err(AccountError::other(
103 "duplicate procedure roots are not allowed in the procedure policy map",
104 ));
105 }
106 }
107
108 for (_, policy) in proc_policies {
109 if let Some(immediate_threshold) = policy.immediate_threshold()
110 && immediate_threshold > num_approvers
111 {
112 return Err(AccountError::other(
113 "procedure policy immediate threshold cannot exceed number of approvers",
114 ));
115 }
116 if let Some(delay_threshold) = policy.delay_threshold()
117 && delay_threshold > num_approvers
118 {
119 return Err(AccountError::other(
120 "procedure policy delay threshold cannot exceed number of approvers",
121 ));
122 }
123 }
124
125 Ok(())
126}
127
128#[derive(Debug)]
155pub struct AuthMultisigSmart {
156 config: AuthMultisigSmartConfig,
157}
158
159impl AuthMultisigSmart {
160 pub const NAME: &'static str = "miden::standards::auth::multisig_smart";
162
163 pub fn code() -> &'static AccountComponentCode {
165 &MULTISIG_SMART_CODE
166 }
167
168 pub fn new(config: AuthMultisigSmartConfig) -> Result<Self, AccountError> {
170 validate_proc_policies(config.approvers().len() as u32, config.procedure_policies())?;
171 Ok(Self { config })
172 }
173
174 pub fn threshold_config_slot() -> &'static StorageSlotName {
175 &THRESHOLD_CONFIG_SLOT_NAME
176 }
177
178 pub fn approver_public_keys_slot() -> &'static StorageSlotName {
179 &APPROVER_PUBKEYS_SLOT_NAME
180 }
181
182 pub fn approver_scheme_ids_slot() -> &'static StorageSlotName {
183 &APPROVER_SCHEME_ID_SLOT_NAME
184 }
185
186 pub fn executed_transactions_slot() -> &'static StorageSlotName {
187 &EXECUTED_TRANSACTIONS_SLOT_NAME
188 }
189
190 pub fn procedure_policies_slot() -> &'static StorageSlotName {
191 &PROCEDURE_POLICIES_SLOT_NAME
192 }
193
194 pub fn threshold_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
195 AuthMultisig::threshold_config_slot_schema()
196 }
197
198 pub fn approver_public_keys_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
199 AuthMultisig::approver_public_keys_slot_schema()
200 }
201
202 pub fn approver_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
203 AuthMultisig::approver_auth_scheme_slot_schema()
204 }
205
206 pub fn executed_transactions_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
207 AuthMultisig::executed_transactions_slot_schema()
208 }
209
210 pub fn procedure_policies_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
211 (
212 Self::procedure_policies_slot().clone(),
213 StorageSlotSchema::map(
214 "Procedure policies",
215 SchemaType::native_word(),
216 SchemaType::native_word(),
217 ),
218 )
219 }
220}
221
222impl From<AuthMultisigSmart> for AccountComponent {
223 fn from(multisig: AuthMultisigSmart) -> Self {
224 let mut storage_slots = Vec::with_capacity(5);
225
226 let num_approvers = multisig.config.approvers().len() as u32;
228 storage_slots.push(StorageSlot::with_value(
229 AuthMultisigSmart::threshold_config_slot().clone(),
230 Word::from([multisig.config.default_threshold(), num_approvers, 0, 0]),
231 ));
232
233 let map_entries = multisig.config.approvers().iter().enumerate().map(|(i, approver)| {
235 (StorageMapKey::from_index(i as u32), Word::from(approver.pub_key()))
236 });
237 storage_slots.push(StorageSlot::with_map(
238 AuthMultisigSmart::approver_public_keys_slot().clone(),
239 StorageMap::with_entries(map_entries).unwrap(),
240 ));
241
242 let scheme_id_entries =
244 multisig.config.approvers().iter().enumerate().map(|(i, approver)| {
245 (
246 StorageMapKey::from_index(i as u32),
247 Word::from([approver.auth_scheme() as u32, 0, 0, 0]),
248 )
249 });
250 storage_slots.push(StorageSlot::with_map(
251 AuthMultisigSmart::approver_scheme_ids_slot().clone(),
252 StorageMap::with_entries(scheme_id_entries).unwrap(),
253 ));
254
255 storage_slots.push(StorageSlot::with_map(
257 AuthMultisigSmart::executed_transactions_slot().clone(),
258 StorageMap::default(),
259 ));
260
261 let procedure_policies =
263 StorageMap::with_entries(multisig.config.procedure_policies().iter().map(
264 |(proc_root, policy)| (StorageMapKey::from_raw(*proc_root), policy.to_word()),
265 ))
266 .unwrap();
267 storage_slots.push(StorageSlot::with_map(
268 AuthMultisigSmart::procedure_policies_slot().clone(),
269 procedure_policies,
270 ));
271
272 let storage_schema = StorageSchema::new(vec![
273 AuthMultisigSmart::threshold_config_slot_schema(),
274 AuthMultisigSmart::approver_public_keys_slot_schema(),
275 AuthMultisigSmart::approver_auth_scheme_slot_schema(),
276 AuthMultisigSmart::executed_transactions_slot_schema(),
277 AuthMultisigSmart::procedure_policies_slot_schema(),
278 ])
279 .expect("storage schema should be valid");
280
281 let metadata = AccountComponentMetadata::new(AuthMultisigSmart::NAME)
282 .with_description("Multisig smart authentication component")
283 .with_storage_schema(storage_schema);
284
285 AccountComponent::new(AuthMultisigSmart::code().clone(), storage_slots, metadata).expect(
286 "multisig smart component should satisfy the requirements of a valid account component",
287 )
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use alloc::string::ToString;
294
295 use miden_protocol::account::AccountBuilder;
296 use miden_protocol::account::auth::AuthSecretKey;
297
298 use super::*;
299 use crate::account::wallets::BasicWallet;
300
301 #[test]
302 fn test_multisig_smart_component_setup() {
303 let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak();
304 let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak();
305 let approvers = vec![
306 Approver::new(sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
307 Approver::new(sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
308 ];
309 let num_approvers = approvers.len() as u32;
310 let default_threshold = 2u32;
311 let receive_asset_immediate_threshold = 1u32;
312
313 let approver_set =
314 ApproverSet::new(approvers, default_threshold).expect("invalid approver set");
315 let config = AuthMultisigSmartConfig::new(approver_set)
316 .with_proc_policies(vec![(
317 BasicWallet::receive_asset_root().as_word(),
318 ProcedurePolicy::with_immediate_threshold(receive_asset_immediate_threshold)
319 .expect("procedure policy should be valid"),
320 )])
321 .expect("procedure policy config should be valid");
322
323 let component =
324 AuthMultisigSmart::new(config).expect("multisig smart component creation failed");
325
326 let account = AccountBuilder::new([0; 32])
327 .with_component(component)
328 .with_component(BasicWallet)
329 .build()
330 .expect("account building failed");
331
332 let threshold_config = account
333 .storage()
334 .get_item(AuthMultisigSmart::threshold_config_slot())
335 .expect("threshold config should be present");
336 assert_eq!(threshold_config, Word::from([default_threshold, num_approvers, 0, 0]));
337
338 let receive_asset_policy = account
339 .storage()
340 .get_map_item(
341 AuthMultisigSmart::procedure_policies_slot(),
342 StorageMapKey::from_raw(BasicWallet::receive_asset_root().as_word()),
343 )
344 .expect("receive_asset policy should be present");
345 assert_eq!(
346 receive_asset_policy,
347 Word::from([receive_asset_immediate_threshold, 0u32, 0u32, 0u32])
348 );
349 }
350
351 #[test]
352 fn test_multisig_smart_component_rejects_duplicate_procedure_roots() {
353 let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak();
354 let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak();
355 let approvers = vec![
356 Approver::new(sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
357 Approver::new(sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
358 ];
359
360 let receive_asset_root = BasicWallet::receive_asset_root().as_word();
361 let policy_one =
362 ProcedurePolicy::with_immediate_threshold(1).expect("procedure policy should be valid");
363 let policy_two =
364 ProcedurePolicy::with_immediate_threshold(2).expect("procedure policy should be valid");
365
366 let approver_set = ApproverSet::new(approvers, 2).expect("invalid approver set");
367 let result = AuthMultisigSmartConfig::new(approver_set).with_proc_policies(vec![
368 (receive_asset_root, policy_one),
369 (receive_asset_root, policy_two),
370 ]);
371
372 assert!(
373 result
374 .unwrap_err()
375 .to_string()
376 .contains("duplicate procedure roots are not allowed in the procedure policy map")
377 );
378 }
379}