1use 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 AccountComponentName,
15 AccountProcedureRoot,
16 StorageMap,
17 StorageMapKey,
18 StorageSlot,
19 StorageSlotName,
20};
21use miden_protocol::errors::AccountError;
22use miden_protocol::utils::sync::LazyLock;
23
24use super::multisig::{AuthMultisig, AuthMultisigConfig};
25use crate::account::account_component_code;
26
27account_component_code!(GUARDED_MULTISIG_CODE, "auth/guarded_multisig.masl");
28
29static GUARDIAN_PUBKEY_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
33 StorageSlotName::new("miden::standards::auth::guardian::pub_key")
34 .expect("storage slot name should be valid")
35});
36
37static GUARDIAN_SCHEME_ID_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
38 StorageSlotName::new("miden::standards::auth::guardian::scheme")
39 .expect("storage slot name should be valid")
40});
41
42#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct AuthGuardedMultisigConfig {
48 multisig: AuthMultisigConfig,
49 guardian_config: GuardianConfig,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct GuardianConfig {
55 pub_key: PublicKeyCommitment,
56 auth_scheme: AuthScheme,
57}
58
59impl GuardianConfig {
60 pub fn new(pub_key: PublicKeyCommitment, auth_scheme: AuthScheme) -> Self {
61 Self { pub_key, auth_scheme }
62 }
63
64 pub fn pub_key(&self) -> PublicKeyCommitment {
65 self.pub_key
66 }
67
68 pub fn auth_scheme(&self) -> AuthScheme {
69 self.auth_scheme
70 }
71
72 fn public_key_slot() -> &'static StorageSlotName {
73 &GUARDIAN_PUBKEY_SLOT_NAME
74 }
75
76 fn scheme_id_slot() -> &'static StorageSlotName {
77 &GUARDIAN_SCHEME_ID_SLOT_NAME
78 }
79
80 fn public_key_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
81 (
82 Self::public_key_slot().clone(),
83 StorageSlotSchema::map(
84 "Guardian public keys",
85 SchemaType::u32(),
86 SchemaType::pub_key(),
87 ),
88 )
89 }
90
91 fn auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
92 (
93 Self::scheme_id_slot().clone(),
94 StorageSlotSchema::map(
95 "Guardian scheme IDs",
96 SchemaType::u32(),
97 SchemaType::auth_scheme(),
98 ),
99 )
100 }
101
102 fn into_component_parts(self) -> (Vec<StorageSlot>, Vec<(StorageSlotName, StorageSlotSchema)>) {
103 let mut storage_slots = Vec::with_capacity(2);
104
105 let guardian_public_key_entries =
107 [(StorageMapKey::from_raw(Word::from([0u32, 0, 0, 0])), Word::from(self.pub_key))];
108 storage_slots.push(StorageSlot::with_map(
109 Self::public_key_slot().clone(),
110 StorageMap::with_entries(guardian_public_key_entries).unwrap(),
111 ));
112
113 let guardian_scheme_id_entries = [(
115 StorageMapKey::from_raw(Word::from([0u32, 0, 0, 0])),
116 Word::from([self.auth_scheme as u32, 0, 0, 0]),
117 )];
118 storage_slots.push(StorageSlot::with_map(
119 Self::scheme_id_slot().clone(),
120 StorageMap::with_entries(guardian_scheme_id_entries).unwrap(),
121 ));
122
123 let slot_metadata = vec![Self::public_key_slot_schema(), Self::auth_scheme_slot_schema()];
124
125 (storage_slots, slot_metadata)
126 }
127}
128
129impl AuthGuardedMultisigConfig {
130 pub fn new(
135 approvers: Vec<(PublicKeyCommitment, AuthScheme)>,
136 default_threshold: u32,
137 guardian_config: GuardianConfig,
138 ) -> Result<Self, AccountError> {
139 let multisig = AuthMultisigConfig::new(approvers, default_threshold)?;
140 if multisig
141 .approvers()
142 .iter()
143 .any(|(approver, _)| *approver == guardian_config.pub_key())
144 {
145 return Err(AccountError::other(
146 "guardian public key must be different from approvers",
147 ));
148 }
149
150 Ok(Self { multisig, guardian_config })
151 }
152
153 pub fn with_proc_thresholds(
156 mut self,
157 proc_thresholds: Vec<(AccountProcedureRoot, u32)>,
158 ) -> Result<Self, AccountError> {
159 self.multisig = self.multisig.with_proc_thresholds(proc_thresholds)?;
160 Ok(self)
161 }
162
163 pub fn approvers(&self) -> &[(PublicKeyCommitment, AuthScheme)] {
164 self.multisig.approvers()
165 }
166
167 pub fn default_threshold(&self) -> u32 {
168 self.multisig.default_threshold()
169 }
170
171 pub fn proc_thresholds(&self) -> &[(AccountProcedureRoot, u32)] {
172 self.multisig.proc_thresholds()
173 }
174
175 pub fn guardian_config(&self) -> GuardianConfig {
176 self.guardian_config
177 }
178
179 fn into_parts(self) -> (AuthMultisigConfig, GuardianConfig) {
180 (self.multisig, self.guardian_config)
181 }
182}
183
184#[derive(Debug)]
192pub struct AuthGuardedMultisig {
193 multisig: AuthMultisig,
194 guardian_config: GuardianConfig,
195}
196
197impl AuthGuardedMultisig {
198 pub const NAME: &'static str = "miden::standards::components::auth::guarded_multisig";
200
201 pub const fn name() -> AccountComponentName {
203 AccountComponentName::from_static_str(Self::NAME)
204 }
205
206 pub fn code() -> &'static AccountComponentCode {
208 &GUARDED_MULTISIG_CODE
209 }
210
211 pub fn new(config: AuthGuardedMultisigConfig) -> Result<Self, AccountError> {
213 let (multisig_config, guardian_config) = config.into_parts();
214 Ok(Self {
215 multisig: AuthMultisig::new(multisig_config)?,
216 guardian_config,
217 })
218 }
219
220 pub fn threshold_config_slot() -> &'static StorageSlotName {
222 AuthMultisig::threshold_config_slot()
223 }
224
225 pub fn approver_public_keys_slot() -> &'static StorageSlotName {
227 AuthMultisig::approver_public_keys_slot()
228 }
229
230 pub fn approver_scheme_ids_slot() -> &'static StorageSlotName {
232 AuthMultisig::approver_scheme_ids_slot()
233 }
234
235 pub fn executed_transactions_slot() -> &'static StorageSlotName {
237 AuthMultisig::executed_transactions_slot()
238 }
239
240 pub fn procedure_thresholds_slot() -> &'static StorageSlotName {
242 AuthMultisig::procedure_thresholds_slot()
243 }
244
245 pub fn guardian_public_key_slot() -> &'static StorageSlotName {
247 GuardianConfig::public_key_slot()
248 }
249
250 pub fn guardian_scheme_id_slot() -> &'static StorageSlotName {
252 GuardianConfig::scheme_id_slot()
253 }
254
255 pub fn threshold_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
257 AuthMultisig::threshold_config_slot_schema()
258 }
259
260 pub fn approver_public_keys_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
262 AuthMultisig::approver_public_keys_slot_schema()
263 }
264
265 pub fn approver_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
267 AuthMultisig::approver_auth_scheme_slot_schema()
268 }
269
270 pub fn executed_transactions_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
272 AuthMultisig::executed_transactions_slot_schema()
273 }
274
275 pub fn procedure_thresholds_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
277 AuthMultisig::procedure_thresholds_slot_schema()
278 }
279
280 pub fn guardian_public_key_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
282 GuardianConfig::public_key_slot_schema()
283 }
284
285 pub fn guardian_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
287 GuardianConfig::auth_scheme_slot_schema()
288 }
289
290 pub fn component_metadata() -> AccountComponentMetadata {
292 let storage_schema = StorageSchema::new([
293 Self::threshold_config_slot_schema(),
294 Self::approver_public_keys_slot_schema(),
295 Self::approver_auth_scheme_slot_schema(),
296 Self::executed_transactions_slot_schema(),
297 Self::procedure_thresholds_slot_schema(),
298 Self::guardian_public_key_slot_schema(),
299 Self::guardian_auth_scheme_slot_schema(),
300 ])
301 .expect("storage schema should be valid");
302
303 AccountComponentMetadata::new(Self::NAME)
304 .with_description(
305 "Guarded multisig authentication component integrated \
306 with a state guardian using hybrid signature schemes",
307 )
308 .with_storage_schema(storage_schema)
309 }
310}
311
312impl From<AuthGuardedMultisig> for AccountComponent {
313 fn from(multisig: AuthGuardedMultisig) -> Self {
314 let AuthGuardedMultisig { multisig, guardian_config } = multisig;
315 let multisig_component = AccountComponent::from(multisig);
316 let (guardian_slots, guardian_slot_metadata) = guardian_config.into_component_parts();
317
318 let mut storage_slots = multisig_component.storage_slots().to_vec();
319 storage_slots.extend(guardian_slots);
320
321 let mut slot_schemas: Vec<(StorageSlotName, StorageSlotSchema)> = multisig_component
322 .storage_schema()
323 .iter()
324 .map(|(slot_name, slot_schema)| (slot_name.clone(), slot_schema.clone()))
325 .collect();
326 slot_schemas.extend(guardian_slot_metadata);
327
328 let storage_schema =
329 StorageSchema::new(slot_schemas).expect("storage schema should be valid");
330
331 let metadata = AccountComponentMetadata::new(AuthGuardedMultisig::NAME)
332 .with_description(multisig_component.metadata().description())
333 .with_version(multisig_component.metadata().version().clone())
334 .with_storage_schema(storage_schema);
335
336 AccountComponent::new(AuthGuardedMultisig::code().clone(), storage_slots, metadata).expect(
337 "Guarded multisig auth component should satisfy the requirements of a valid \
338 account component",
339 )
340 }
341}
342
343#[cfg(test)]
347mod tests {
348 use alloc::string::ToString;
349
350 use miden_protocol::Word;
351 use miden_protocol::account::AccountBuilder;
352 use miden_protocol::account::auth::AuthSecretKey;
353
354 use super::*;
355 use crate::account::wallets::BasicWallet;
356
357 #[test]
359 fn test_guarded_multisig_component_setup() {
360 let sec_key_1 = AuthSecretKey::new_falcon512_poseidon2();
362 let sec_key_2 = AuthSecretKey::new_falcon512_poseidon2();
363 let sec_key_3 = AuthSecretKey::new_falcon512_poseidon2();
364 let guardian_key = AuthSecretKey::new_ecdsa_k256_keccak();
365
366 let approvers = vec![
368 (sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
369 (sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
370 (sec_key_3.public_key().to_commitment(), sec_key_3.auth_scheme()),
371 ];
372
373 let threshold = 2u32;
374
375 let multisig_component = AuthGuardedMultisig::new(
377 AuthGuardedMultisigConfig::new(
378 approvers.clone(),
379 threshold,
380 GuardianConfig::new(
381 guardian_key.public_key().to_commitment(),
382 guardian_key.auth_scheme(),
383 ),
384 )
385 .expect("invalid multisig config"),
386 )
387 .expect("guarded multisig component creation failed");
388
389 let account = AccountBuilder::new([0; 32])
391 .with_auth_component(multisig_component)
392 .with_component(BasicWallet)
393 .build()
394 .expect("account building failed");
395
396 let config_slot = account
398 .storage()
399 .get_item(AuthGuardedMultisig::threshold_config_slot())
400 .expect("config storage slot access failed");
401 assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
402
403 for (i, (expected_pub_key, _)) in approvers.iter().enumerate() {
405 let stored_pub_key = account
406 .storage()
407 .get_map_item(
408 AuthGuardedMultisig::approver_public_keys_slot(),
409 Word::from([i as u32, 0, 0, 0]),
410 )
411 .expect("approver public key storage map access failed");
412 assert_eq!(stored_pub_key, Word::from(*expected_pub_key));
413 }
414
415 for (i, (_, expected_auth_scheme)) in approvers.iter().enumerate() {
417 let stored_scheme_id = account
418 .storage()
419 .get_map_item(
420 AuthGuardedMultisig::approver_scheme_ids_slot(),
421 Word::from([i as u32, 0, 0, 0]),
422 )
423 .expect("approver scheme ID storage map access failed");
424 assert_eq!(stored_scheme_id, Word::from([*expected_auth_scheme as u32, 0, 0, 0]));
425 }
426
427 let guardian_public_key = account
429 .storage()
430 .get_map_item(
431 AuthGuardedMultisig::guardian_public_key_slot(),
432 Word::from([0u32, 0, 0, 0]),
433 )
434 .expect("guardian public key storage map access failed");
435 assert_eq!(guardian_public_key, Word::from(guardian_key.public_key().to_commitment()));
436
437 let guardian_scheme_id = account
438 .storage()
439 .get_map_item(
440 AuthGuardedMultisig::guardian_scheme_id_slot(),
441 Word::from([0u32, 0, 0, 0]),
442 )
443 .expect("guardian scheme ID storage map access failed");
444 assert_eq!(guardian_scheme_id, Word::from([guardian_key.auth_scheme() as u32, 0, 0, 0]));
445 }
446
447 #[test]
449 fn test_guarded_multisig_component_minimum_threshold() {
450 let pub_key = AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment();
451 let guardian_key = AuthSecretKey::new_falcon512_poseidon2();
452 let approvers = vec![(pub_key, AuthScheme::EcdsaK256Keccak)];
453 let threshold = 1u32;
454
455 let multisig_component = AuthGuardedMultisig::new(
456 AuthGuardedMultisigConfig::new(
457 approvers.clone(),
458 threshold,
459 GuardianConfig::new(
460 guardian_key.public_key().to_commitment(),
461 guardian_key.auth_scheme(),
462 ),
463 )
464 .expect("invalid multisig config"),
465 )
466 .expect("guarded multisig component creation failed");
467
468 let account = AccountBuilder::new([0; 32])
469 .with_auth_component(multisig_component)
470 .with_component(BasicWallet)
471 .build()
472 .expect("account building failed");
473
474 let config_slot = account
476 .storage()
477 .get_item(AuthGuardedMultisig::threshold_config_slot())
478 .expect("config storage slot access failed");
479 assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
480
481 let stored_pub_key = account
482 .storage()
483 .get_map_item(
484 AuthGuardedMultisig::approver_public_keys_slot(),
485 Word::from([0u32, 0, 0, 0]),
486 )
487 .expect("approver pub keys storage map access failed");
488 assert_eq!(stored_pub_key, Word::from(pub_key));
489
490 let stored_scheme_id = account
491 .storage()
492 .get_map_item(
493 AuthGuardedMultisig::approver_scheme_ids_slot(),
494 Word::from([0u32, 0, 0, 0]),
495 )
496 .expect("approver scheme IDs storage map access failed");
497 assert_eq!(stored_scheme_id, Word::from([AuthScheme::EcdsaK256Keccak as u32, 0, 0, 0]));
498 }
499
500 #[test]
502 fn test_guarded_multisig_component_with_guardian() {
503 let sec_key_1 = AuthSecretKey::new_falcon512_poseidon2();
504 let sec_key_2 = AuthSecretKey::new_falcon512_poseidon2();
505 let guardian_key = AuthSecretKey::new_ecdsa_k256_keccak();
506
507 let approvers = vec![
508 (sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
509 (sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
510 ];
511
512 let multisig_component = AuthGuardedMultisig::new(
513 AuthGuardedMultisigConfig::new(
514 approvers,
515 2,
516 GuardianConfig::new(
517 guardian_key.public_key().to_commitment(),
518 guardian_key.auth_scheme(),
519 ),
520 )
521 .expect("invalid multisig config"),
522 )
523 .expect("guarded multisig component creation failed");
524
525 let account = AccountBuilder::new([0; 32])
526 .with_auth_component(multisig_component)
527 .with_component(BasicWallet)
528 .build()
529 .expect("account building failed");
530
531 let guardian_public_key = account
532 .storage()
533 .get_map_item(
534 AuthGuardedMultisig::guardian_public_key_slot(),
535 Word::from([0u32, 0, 0, 0]),
536 )
537 .expect("guardian public key storage map access failed");
538 assert_eq!(guardian_public_key, Word::from(guardian_key.public_key().to_commitment()));
539
540 let guardian_scheme_id = account
541 .storage()
542 .get_map_item(
543 AuthGuardedMultisig::guardian_scheme_id_slot(),
544 Word::from([0u32, 0, 0, 0]),
545 )
546 .expect("guardian scheme ID storage map access failed");
547 assert_eq!(guardian_scheme_id, Word::from([guardian_key.auth_scheme() as u32, 0, 0, 0]));
548 }
549
550 #[test]
552 fn test_guarded_multisig_component_error_cases() {
553 let pub_key = AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment();
554 let guardian_key = AuthSecretKey::new_falcon512_poseidon2();
555 let approvers = vec![(pub_key, AuthScheme::EcdsaK256Keccak)];
556
557 let result = AuthGuardedMultisigConfig::new(
559 approvers,
560 2,
561 GuardianConfig::new(
562 guardian_key.public_key().to_commitment(),
563 guardian_key.auth_scheme(),
564 ),
565 );
566
567 assert!(
568 result
569 .unwrap_err()
570 .to_string()
571 .contains("threshold cannot be greater than number of approvers")
572 );
573 }
574
575 #[test]
577 fn test_guarded_multisig_component_duplicate_approvers() {
578 let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak();
580 let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak();
581 let guardian_key = AuthSecretKey::new_falcon512_poseidon2();
582
583 let approvers = vec![
585 (sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
586 (sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
587 (sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
588 ];
589
590 let result = AuthGuardedMultisigConfig::new(
591 approvers,
592 2,
593 GuardianConfig::new(
594 guardian_key.public_key().to_commitment(),
595 guardian_key.auth_scheme(),
596 ),
597 );
598 assert!(
599 result
600 .unwrap_err()
601 .to_string()
602 .contains("duplicate approver public keys are not allowed")
603 );
604 }
605
606 #[test]
608 fn test_guarded_multisig_component_guardian_not_approver() {
609 let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak();
610 let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak();
611
612 let approvers = vec![
613 (sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
614 (sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
615 ];
616
617 let result = AuthGuardedMultisigConfig::new(
618 approvers,
619 2,
620 GuardianConfig::new(sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
621 );
622
623 assert!(
624 result
625 .unwrap_err()
626 .to_string()
627 .contains("guardian public key must be different from approvers")
628 );
629 }
630}