miden_standards/account/auth/
multisig.rs1use alloc::collections::BTreeMap;
2use alloc::vec::Vec;
3
4use miden_protocol::Word;
5use miden_protocol::account::component::{
6 AccountComponentCode,
7 AccountComponentMetadata,
8 FeltSchema,
9 SchemaType,
10 StorageSchema,
11 StorageSlotSchema,
12};
13use miden_protocol::account::{
14 AccountComponent,
15 AccountComponentName,
16 AccountProcedureRoot,
17 StorageMap,
18 StorageMapKey,
19 StorageSlot,
20 StorageSlotName,
21};
22use miden_protocol::errors::AccountError;
23use miden_protocol::utils::sync::LazyLock;
24
25use super::{Approver, ApproverSet};
26use crate::account::account_component_code;
27use crate::procedure_root;
28
29account_component_code!(MULTISIG_CODE, "miden-standards-auth-multisig.masp");
30
31const MULTISIG_LIBRARY_PATH: &str = "miden::standards::components::auth::multisig";
37
38procedure_root!(
42 MULTISIG_SET_PROCEDURE_THRESHOLD,
43 MULTISIG_LIBRARY_PATH,
44 AuthMultisig::SET_PROCEDURE_THRESHOLD_PROC_NAME,
45 AuthMultisig::code()
46);
47
48pub(super) static THRESHOLD_CONFIG_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
52 StorageSlotName::new("miden::standards::auth::multisig::threshold_config")
53 .expect("storage slot name should be valid")
54});
55
56pub(super) static APPROVER_PUBKEYS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
57 StorageSlotName::new("miden::standards::auth::multisig::approver_public_keys")
58 .expect("storage slot name should be valid")
59});
60
61pub(super) static APPROVER_SCHEME_ID_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
62 StorageSlotName::new("miden::standards::auth::multisig::approver_schemes")
63 .expect("storage slot name should be valid")
64});
65
66pub(super) static EXECUTED_TRANSACTIONS_SLOT_NAME: LazyLock<StorageSlotName> =
67 LazyLock::new(|| {
68 StorageSlotName::new("miden::standards::auth::multisig::executed_transactions")
69 .expect("storage slot name should be valid")
70 });
71
72static PROCEDURE_THRESHOLDS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
73 StorageSlotName::new("miden::standards::auth::multisig::procedure_thresholds")
74 .expect("storage slot name should be valid")
75});
76
77#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct AuthMultisigConfig {
83 approver_set: ApproverSet,
84 proc_thresholds: BTreeMap<AccountProcedureRoot, u32>,
85}
86
87impl AuthMultisigConfig {
88 pub fn new(approver_set: ApproverSet) -> Self {
90 Self {
91 approver_set,
92 proc_thresholds: BTreeMap::new(),
93 }
94 }
95
96 pub fn with_proc_thresholds(
99 mut self,
100 proc_thresholds: Vec<(AccountProcedureRoot, u32)>,
101 ) -> Result<Self, AccountError> {
102 let num_approvers = self.approver_set.approvers().len() as u32;
103 let mut thresholds = BTreeMap::new();
104 for (proc_root, threshold) in proc_thresholds {
105 if threshold == 0 {
106 return Err(AccountError::other("procedure threshold must be at least 1"));
107 }
108 if threshold > num_approvers {
109 return Err(AccountError::other(
110 "procedure threshold cannot be greater than number of approvers",
111 ));
112 }
113 if thresholds.insert(proc_root, threshold).is_some() {
116 return Err(AccountError::other(
117 "duplicate procedure roots are not allowed in the procedure threshold map",
118 ));
119 }
120 }
121 self.proc_thresholds = thresholds;
122 Ok(self)
123 }
124
125 pub fn approver_set(&self) -> &ApproverSet {
126 &self.approver_set
127 }
128
129 pub fn approvers(&self) -> &[Approver] {
130 self.approver_set.approvers()
131 }
132
133 pub fn default_threshold(&self) -> u32 {
134 self.approver_set.threshold().get()
135 }
136
137 pub fn proc_thresholds(&self) -> &BTreeMap<AccountProcedureRoot, u32> {
138 &self.proc_thresholds
139 }
140}
141
142#[derive(Debug)]
231pub struct AuthMultisig {
232 config: AuthMultisigConfig,
233}
234
235impl AuthMultisig {
236 pub const NAME: &'static str = "miden::standards::auth::multisig";
238
239 const SET_PROCEDURE_THRESHOLD_PROC_NAME: &'static str = "set_procedure_threshold";
241
242 pub const fn name() -> AccountComponentName {
244 AccountComponentName::from_static_str(Self::NAME)
245 }
246
247 pub fn code() -> &'static AccountComponentCode {
249 &MULTISIG_CODE
250 }
251
252 pub fn set_procedure_threshold_root() -> AccountProcedureRoot {
254 *MULTISIG_SET_PROCEDURE_THRESHOLD
255 }
256
257 pub fn new(config: AuthMultisigConfig) -> Result<Self, AccountError> {
266 let setter_threshold = config
269 .proc_thresholds()
270 .get(&Self::set_procedure_threshold_root())
271 .copied()
272 .unwrap_or_else(|| config.default_threshold());
273
274 for &threshold in config.proc_thresholds().values() {
275 if threshold > setter_threshold {
276 return Err(AccountError::other(format!(
277 "per-procedure threshold override of {threshold} exceeds the threshold of \
278 {setter_threshold} that guards set_procedure_threshold; such an override can \
279 be removed by a smaller quorum. Raise the set_procedure_threshold override to \
280 at least {threshold} to make it enforceable"
281 )));
282 }
283 }
284
285 Ok(Self { config })
286 }
287
288 pub fn threshold_config_slot() -> &'static StorageSlotName {
290 &THRESHOLD_CONFIG_SLOT_NAME
291 }
292
293 pub fn approver_public_keys_slot() -> &'static StorageSlotName {
295 &APPROVER_PUBKEYS_SLOT_NAME
296 }
297
298 pub fn approver_scheme_ids_slot() -> &'static StorageSlotName {
300 &APPROVER_SCHEME_ID_SLOT_NAME
301 }
302
303 pub fn executed_transactions_slot() -> &'static StorageSlotName {
305 &EXECUTED_TRANSACTIONS_SLOT_NAME
306 }
307
308 pub fn procedure_thresholds_slot() -> &'static StorageSlotName {
310 &PROCEDURE_THRESHOLDS_SLOT_NAME
311 }
312
313 pub fn threshold_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
315 (
316 Self::threshold_config_slot().clone(),
317 StorageSlotSchema::value(
318 "Threshold configuration",
319 [
320 FeltSchema::u32("threshold"),
321 FeltSchema::u32("num_approvers"),
322 FeltSchema::new_void(),
323 FeltSchema::new_void(),
324 ],
325 ),
326 )
327 }
328
329 pub fn approver_public_keys_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
331 (
332 Self::approver_public_keys_slot().clone(),
333 StorageSlotSchema::map(
334 "Approver public keys",
335 SchemaType::u32(),
336 SchemaType::pub_key(),
337 ),
338 )
339 }
340
341 pub fn approver_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
343 (
344 Self::approver_scheme_ids_slot().clone(),
345 StorageSlotSchema::map(
346 "Approver scheme IDs",
347 SchemaType::u32(),
348 SchemaType::auth_scheme(),
349 ),
350 )
351 }
352
353 pub fn executed_transactions_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
355 (
356 Self::executed_transactions_slot().clone(),
357 StorageSlotSchema::map(
358 "Executed transactions",
359 SchemaType::native_word(),
360 SchemaType::native_word(),
361 ),
362 )
363 }
364
365 pub fn procedure_thresholds_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
367 (
368 Self::procedure_thresholds_slot().clone(),
369 StorageSlotSchema::map(
370 "Procedure thresholds",
371 SchemaType::native_word(),
372 SchemaType::u32(),
373 ),
374 )
375 }
376
377 pub fn component_metadata() -> AccountComponentMetadata {
379 let storage_schema = StorageSchema::new([
380 Self::threshold_config_slot_schema(),
381 Self::approver_public_keys_slot_schema(),
382 Self::approver_auth_scheme_slot_schema(),
383 Self::executed_transactions_slot_schema(),
384 Self::procedure_thresholds_slot_schema(),
385 ])
386 .expect("storage schema should be valid");
387
388 AccountComponentMetadata::new(Self::NAME)
389 .with_description("Multisig authentication component using hybrid signature schemes")
390 .with_storage_schema(storage_schema)
391 }
392}
393
394impl From<AuthMultisig> for AccountComponent {
395 fn from(multisig: AuthMultisig) -> Self {
396 let mut storage_slots = Vec::with_capacity(5);
397
398 let num_approvers = multisig.config.approvers().len() as u32;
400 storage_slots.push(StorageSlot::with_value(
401 AuthMultisig::threshold_config_slot().clone(),
402 Word::from([multisig.config.default_threshold(), num_approvers, 0, 0]),
403 ));
404
405 let map_entries = multisig.config.approvers().iter().enumerate().map(|(i, approver)| {
407 (StorageMapKey::from_index(i as u32), Word::from(approver.pub_key()))
408 });
409
410 storage_slots.push(StorageSlot::with_map(
412 AuthMultisig::approver_public_keys_slot().clone(),
413 StorageMap::with_entries(map_entries).unwrap(),
414 ));
415
416 let scheme_id_entries =
418 multisig.config.approvers().iter().enumerate().map(|(i, approver)| {
419 (
420 StorageMapKey::from_index(i as u32),
421 Word::from([approver.auth_scheme() as u32, 0, 0, 0]),
422 )
423 });
424
425 storage_slots.push(StorageSlot::with_map(
426 AuthMultisig::approver_scheme_ids_slot().clone(),
427 StorageMap::with_entries(scheme_id_entries).unwrap(),
428 ));
429
430 let executed_transactions = StorageMap::default();
432 storage_slots.push(StorageSlot::with_map(
433 AuthMultisig::executed_transactions_slot().clone(),
434 executed_transactions,
435 ));
436
437 let proc_threshold_roots = StorageMap::with_entries(
439 multisig.config.proc_thresholds().iter().map(|(proc_root, threshold)| {
440 (StorageMapKey::from_raw(proc_root.as_word()), Word::from([*threshold, 0, 0, 0]))
441 }),
442 )
443 .unwrap();
444 storage_slots.push(StorageSlot::with_map(
445 AuthMultisig::procedure_thresholds_slot().clone(),
446 proc_threshold_roots,
447 ));
448
449 let metadata = AuthMultisig::component_metadata();
450
451 AccountComponent::new(AuthMultisig::code().clone(), storage_slots, metadata).expect(
452 "Multisig auth component should satisfy the requirements of a valid account component",
453 )
454 }
455}
456
457#[cfg(test)]
461mod tests {
462 use alloc::string::ToString;
463
464 use miden_protocol::Word;
465 use miden_protocol::account::auth::AuthSecretKey;
466 use miden_protocol::account::{AccountBuilder, auth};
467
468 use super::*;
469 use crate::account::wallets::BasicWallet;
470
471 #[test]
473 fn test_multisig_component_setup() {
474 let sec_key_1 = AuthSecretKey::new_falcon512_poseidon2();
476 let sec_key_2 = AuthSecretKey::new_falcon512_poseidon2();
477 let sec_key_3 = AuthSecretKey::new_falcon512_poseidon2();
478
479 let approvers = vec![
481 Approver::new(sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
482 Approver::new(sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
483 Approver::new(sec_key_3.public_key().to_commitment(), sec_key_3.auth_scheme()),
484 ];
485
486 let threshold = 2u32;
487
488 let approver_set =
490 ApproverSet::new(approvers.clone(), threshold).expect("invalid approver set");
491 let multisig_component = AuthMultisig::new(AuthMultisigConfig::new(approver_set))
492 .expect("multisig component creation failed");
493
494 let account = AccountBuilder::new([0; 32])
496 .with_component(multisig_component)
497 .with_component(BasicWallet)
498 .build()
499 .expect("account building failed");
500
501 let config_slot = account
503 .storage()
504 .get_item(AuthMultisig::threshold_config_slot())
505 .expect("config storage slot access failed");
506 assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
507
508 for (i, approver) in approvers.iter().enumerate() {
510 let stored_pub_key = account
511 .storage()
512 .get_map_item(
513 AuthMultisig::approver_public_keys_slot(),
514 StorageMapKey::from_index(i as u32),
515 )
516 .expect("approver public key storage map access failed");
517 assert_eq!(stored_pub_key, Word::from(approver.pub_key()));
518 }
519
520 for (i, approver) in approvers.iter().enumerate() {
522 let stored_scheme_id = account
523 .storage()
524 .get_map_item(
525 AuthMultisig::approver_scheme_ids_slot(),
526 StorageMapKey::from_index(i as u32),
527 )
528 .expect("approver scheme ID storage map access failed");
529 assert_eq!(stored_scheme_id, Word::from([approver.auth_scheme() as u32, 0, 0, 0]));
530 }
531 }
532
533 #[test]
535 fn test_multisig_component_minimum_threshold() {
536 let pub_key = AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment();
537 let approvers = vec![Approver::new(pub_key, auth::AuthScheme::EcdsaK256Keccak)];
538 let threshold = 1u32;
539
540 let approver_set =
541 ApproverSet::new(approvers.clone(), threshold).expect("invalid approver set");
542 let multisig_component = AuthMultisig::new(AuthMultisigConfig::new(approver_set))
543 .expect("multisig component creation failed");
544
545 let account = AccountBuilder::new([0; 32])
546 .with_component(multisig_component)
547 .with_component(BasicWallet)
548 .build()
549 .expect("account building failed");
550
551 let config_slot = account
553 .storage()
554 .get_item(AuthMultisig::threshold_config_slot())
555 .expect("config storage slot access failed");
556 assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
557
558 let stored_pub_key = account
559 .storage()
560 .get_map_item(AuthMultisig::approver_public_keys_slot(), StorageMapKey::from_index(0))
561 .expect("approver pub keys storage map access failed");
562 assert_eq!(stored_pub_key, Word::from(pub_key));
563
564 let stored_scheme_id = account
565 .storage()
566 .get_map_item(AuthMultisig::approver_scheme_ids_slot(), StorageMapKey::from_index(0))
567 .expect("approver scheme IDs storage map access failed");
568 assert_eq!(
569 stored_scheme_id,
570 Word::from([auth::AuthScheme::EcdsaK256Keccak as u32, 0, 0, 0])
571 );
572 }
573
574 #[test]
576 fn test_proc_threshold_too_high() {
577 let pub_key = AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment();
578 let approvers = vec![Approver::new(pub_key, auth::AuthScheme::EcdsaK256Keccak)];
579 let approver_set = ApproverSet::new(approvers, 1).expect("invalid approver set");
580
581 let result = AuthMultisigConfig::new(approver_set)
582 .with_proc_thresholds(vec![(BasicWallet::receive_asset_root(), 2)]);
583 assert!(
584 result
585 .unwrap_err()
586 .to_string()
587 .contains("procedure threshold cannot be greater than number of approvers")
588 );
589 }
590
591 #[test]
595 fn test_proc_threshold_above_set_procedure_threshold_rejected() {
596 let approvers = vec![
597 Approver::new(
598 AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment(),
599 auth::AuthScheme::EcdsaK256Keccak,
600 ),
601 Approver::new(
602 AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment(),
603 auth::AuthScheme::EcdsaK256Keccak,
604 ),
605 Approver::new(
606 AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment(),
607 auth::AuthScheme::EcdsaK256Keccak,
608 ),
609 ];
610 let approver_set = ApproverSet::new(approvers, 2).expect("invalid approver set");
611
612 let config = AuthMultisigConfig::new(approver_set)
615 .with_proc_thresholds(vec![(BasicWallet::receive_asset_root(), 3)])
616 .expect("an override within num_approvers is accepted by with_proc_thresholds");
617
618 let err = AuthMultisig::new(config).unwrap_err();
619 assert!(err.to_string().contains("exceeds the threshold"));
620 }
621}