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