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)]
229pub struct AuthMultisig {
230 config: AuthMultisigConfig,
231}
232
233impl AuthMultisig {
234 pub const NAME: &'static str = "miden::standards::auth::multisig";
236
237 const SET_PROCEDURE_THRESHOLD_PROC_NAME: &'static str = "set_procedure_threshold";
239
240 pub const fn name() -> AccountComponentName {
242 AccountComponentName::from_static_str(Self::NAME)
243 }
244
245 pub fn code() -> &'static AccountComponentCode {
247 &MULTISIG_CODE
248 }
249
250 pub fn set_procedure_threshold_root() -> AccountProcedureRoot {
252 *MULTISIG_SET_PROCEDURE_THRESHOLD
253 }
254
255 pub fn new(config: AuthMultisigConfig) -> Result<Self, AccountError> {
264 let setter_threshold = config
267 .proc_thresholds()
268 .get(&Self::set_procedure_threshold_root())
269 .copied()
270 .unwrap_or_else(|| config.default_threshold());
271
272 for &threshold in config.proc_thresholds().values() {
273 if threshold > setter_threshold {
274 return Err(AccountError::other(format!(
275 "per-procedure threshold override of {threshold} exceeds the threshold of \
276 {setter_threshold} that guards set_procedure_threshold; such an override can \
277 be removed by a smaller quorum. Raise the set_procedure_threshold override to \
278 at least {threshold} to make it enforceable"
279 )));
280 }
281 }
282
283 Ok(Self { config })
284 }
285
286 pub fn threshold_config_slot() -> &'static StorageSlotName {
288 &THRESHOLD_CONFIG_SLOT_NAME
289 }
290
291 pub fn approver_public_keys_slot() -> &'static StorageSlotName {
293 &APPROVER_PUBKEYS_SLOT_NAME
294 }
295
296 pub fn approver_scheme_ids_slot() -> &'static StorageSlotName {
298 &APPROVER_SCHEME_ID_SLOT_NAME
299 }
300
301 pub fn executed_transactions_slot() -> &'static StorageSlotName {
303 &EXECUTED_TRANSACTIONS_SLOT_NAME
304 }
305
306 pub fn procedure_thresholds_slot() -> &'static StorageSlotName {
308 &PROCEDURE_THRESHOLDS_SLOT_NAME
309 }
310
311 pub fn threshold_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
313 (
314 Self::threshold_config_slot().clone(),
315 StorageSlotSchema::value(
316 "Threshold configuration",
317 [
318 FeltSchema::u32("threshold"),
319 FeltSchema::u32("num_approvers"),
320 FeltSchema::new_void(),
321 FeltSchema::new_void(),
322 ],
323 ),
324 )
325 }
326
327 pub fn approver_public_keys_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
329 (
330 Self::approver_public_keys_slot().clone(),
331 StorageSlotSchema::map(
332 "Approver public keys",
333 SchemaType::u32(),
334 SchemaType::pub_key(),
335 ),
336 )
337 }
338
339 pub fn approver_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
341 (
342 Self::approver_scheme_ids_slot().clone(),
343 StorageSlotSchema::map(
344 "Approver scheme IDs",
345 SchemaType::u32(),
346 SchemaType::auth_scheme(),
347 ),
348 )
349 }
350
351 pub fn executed_transactions_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
353 (
354 Self::executed_transactions_slot().clone(),
355 StorageSlotSchema::map(
356 "Executed transactions",
357 SchemaType::native_word(),
358 SchemaType::native_word(),
359 ),
360 )
361 }
362
363 pub fn procedure_thresholds_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
365 (
366 Self::procedure_thresholds_slot().clone(),
367 StorageSlotSchema::map(
368 "Procedure thresholds",
369 SchemaType::native_word(),
370 SchemaType::u32(),
371 ),
372 )
373 }
374
375 pub fn component_metadata() -> AccountComponentMetadata {
377 let storage_schema = StorageSchema::new([
378 Self::threshold_config_slot_schema(),
379 Self::approver_public_keys_slot_schema(),
380 Self::approver_auth_scheme_slot_schema(),
381 Self::executed_transactions_slot_schema(),
382 Self::procedure_thresholds_slot_schema(),
383 ])
384 .expect("storage schema should be valid");
385
386 AccountComponentMetadata::new(Self::NAME)
387 .with_description("Multisig authentication component using hybrid signature schemes")
388 .with_storage_schema(storage_schema)
389 }
390}
391
392impl From<AuthMultisig> for AccountComponent {
393 fn from(multisig: AuthMultisig) -> Self {
394 let mut storage_slots = Vec::with_capacity(5);
395
396 let num_approvers = multisig.config.approvers().len() as u32;
398 storage_slots.push(StorageSlot::with_value(
399 AuthMultisig::threshold_config_slot().clone(),
400 Word::from([multisig.config.default_threshold(), num_approvers, 0, 0]),
401 ));
402
403 let map_entries = multisig.config.approvers().iter().enumerate().map(|(i, approver)| {
405 (StorageMapKey::from_index(i as u32), Word::from(approver.pub_key()))
406 });
407
408 storage_slots.push(StorageSlot::with_map(
410 AuthMultisig::approver_public_keys_slot().clone(),
411 StorageMap::with_entries(map_entries).unwrap(),
412 ));
413
414 let scheme_id_entries =
416 multisig.config.approvers().iter().enumerate().map(|(i, approver)| {
417 (
418 StorageMapKey::from_index(i as u32),
419 Word::from([approver.auth_scheme() as u32, 0, 0, 0]),
420 )
421 });
422
423 storage_slots.push(StorageSlot::with_map(
424 AuthMultisig::approver_scheme_ids_slot().clone(),
425 StorageMap::with_entries(scheme_id_entries).unwrap(),
426 ));
427
428 let executed_transactions = StorageMap::default();
430 storage_slots.push(StorageSlot::with_map(
431 AuthMultisig::executed_transactions_slot().clone(),
432 executed_transactions,
433 ));
434
435 let proc_threshold_roots = StorageMap::with_entries(
437 multisig.config.proc_thresholds().iter().map(|(proc_root, threshold)| {
438 (StorageMapKey::from_raw(proc_root.as_word()), Word::from([*threshold, 0, 0, 0]))
439 }),
440 )
441 .unwrap();
442 storage_slots.push(StorageSlot::with_map(
443 AuthMultisig::procedure_thresholds_slot().clone(),
444 proc_threshold_roots,
445 ));
446
447 let metadata = AuthMultisig::component_metadata();
448
449 AccountComponent::new(AuthMultisig::code().clone(), storage_slots, metadata).expect(
450 "Multisig auth component should satisfy the requirements of a valid account component",
451 )
452 }
453}
454
455#[cfg(test)]
459mod tests {
460 use alloc::string::ToString;
461
462 use miden_protocol::Word;
463 use miden_protocol::account::auth::AuthSecretKey;
464 use miden_protocol::account::{AccountBuilder, auth};
465
466 use super::*;
467 use crate::account::wallets::BasicWallet;
468
469 #[test]
471 fn test_multisig_component_setup() {
472 let sec_key_1 = AuthSecretKey::new_falcon512_poseidon2();
474 let sec_key_2 = AuthSecretKey::new_falcon512_poseidon2();
475 let sec_key_3 = AuthSecretKey::new_falcon512_poseidon2();
476
477 let approvers = vec![
479 Approver::new(sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
480 Approver::new(sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
481 Approver::new(sec_key_3.public_key().to_commitment(), sec_key_3.auth_scheme()),
482 ];
483
484 let threshold = 2u32;
485
486 let approver_set =
488 ApproverSet::new(approvers.clone(), threshold).expect("invalid approver set");
489 let multisig_component = AuthMultisig::new(AuthMultisigConfig::new(approver_set))
490 .expect("multisig component creation failed");
491
492 let account = AccountBuilder::new([0; 32])
494 .with_component(multisig_component)
495 .with_component(BasicWallet)
496 .build()
497 .expect("account building failed");
498
499 let config_slot = account
501 .storage()
502 .get_item(AuthMultisig::threshold_config_slot())
503 .expect("config storage slot access failed");
504 assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
505
506 for (i, approver) in approvers.iter().enumerate() {
508 let stored_pub_key = account
509 .storage()
510 .get_map_item(
511 AuthMultisig::approver_public_keys_slot(),
512 StorageMapKey::from_index(i as u32),
513 )
514 .expect("approver public key storage map access failed");
515 assert_eq!(stored_pub_key, Word::from(approver.pub_key()));
516 }
517
518 for (i, approver) in approvers.iter().enumerate() {
520 let stored_scheme_id = account
521 .storage()
522 .get_map_item(
523 AuthMultisig::approver_scheme_ids_slot(),
524 StorageMapKey::from_index(i as u32),
525 )
526 .expect("approver scheme ID storage map access failed");
527 assert_eq!(stored_scheme_id, Word::from([approver.auth_scheme() as u32, 0, 0, 0]));
528 }
529 }
530
531 #[test]
533 fn test_multisig_component_minimum_threshold() {
534 let pub_key = AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment();
535 let approvers = vec![Approver::new(pub_key, auth::AuthScheme::EcdsaK256Keccak)];
536 let threshold = 1u32;
537
538 let approver_set =
539 ApproverSet::new(approvers.clone(), threshold).expect("invalid approver set");
540 let multisig_component = AuthMultisig::new(AuthMultisigConfig::new(approver_set))
541 .expect("multisig component creation failed");
542
543 let account = AccountBuilder::new([0; 32])
544 .with_component(multisig_component)
545 .with_component(BasicWallet)
546 .build()
547 .expect("account building failed");
548
549 let config_slot = account
551 .storage()
552 .get_item(AuthMultisig::threshold_config_slot())
553 .expect("config storage slot access failed");
554 assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
555
556 let stored_pub_key = account
557 .storage()
558 .get_map_item(AuthMultisig::approver_public_keys_slot(), StorageMapKey::from_index(0))
559 .expect("approver pub keys storage map access failed");
560 assert_eq!(stored_pub_key, Word::from(pub_key));
561
562 let stored_scheme_id = account
563 .storage()
564 .get_map_item(AuthMultisig::approver_scheme_ids_slot(), StorageMapKey::from_index(0))
565 .expect("approver scheme IDs storage map access failed");
566 assert_eq!(
567 stored_scheme_id,
568 Word::from([auth::AuthScheme::EcdsaK256Keccak as u32, 0, 0, 0])
569 );
570 }
571
572 #[test]
574 fn test_proc_threshold_too_high() {
575 let pub_key = AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment();
576 let approvers = vec![Approver::new(pub_key, auth::AuthScheme::EcdsaK256Keccak)];
577 let approver_set = ApproverSet::new(approvers, 1).expect("invalid approver set");
578
579 let result = AuthMultisigConfig::new(approver_set)
580 .with_proc_thresholds(vec![(BasicWallet::receive_asset_root(), 2)]);
581 assert!(
582 result
583 .unwrap_err()
584 .to_string()
585 .contains("procedure threshold cannot be greater than number of approvers")
586 );
587 }
588
589 #[test]
593 fn test_proc_threshold_above_set_procedure_threshold_rejected() {
594 let approvers = vec![
595 Approver::new(
596 AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment(),
597 auth::AuthScheme::EcdsaK256Keccak,
598 ),
599 Approver::new(
600 AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment(),
601 auth::AuthScheme::EcdsaK256Keccak,
602 ),
603 Approver::new(
604 AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment(),
605 auth::AuthScheme::EcdsaK256Keccak,
606 ),
607 ];
608 let approver_set = ApproverSet::new(approvers, 2).expect("invalid approver set");
609
610 let config = AuthMultisigConfig::new(approver_set)
613 .with_proc_thresholds(vec![(BasicWallet::receive_asset_root(), 3)])
614 .expect("an override within num_approvers is accepted by with_proc_thresholds");
615
616 let err = AuthMultisig::new(config).unwrap_err();
617 assert!(err.to_string().contains("exceeds the threshold"));
618 }
619}