Skip to main content

miden_standards/account/auth/
guarded_multisig.rs

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
29// CONSTANTS
30// ================================================================================================
31
32static 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// MULTISIG AUTHENTICATION COMPONENT
43// ================================================================================================
44
45/// Configuration for [`AuthGuardedMultisig`] component.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct AuthGuardedMultisigConfig {
48    multisig: AuthMultisigConfig,
49    guardian_config: GuardianConfig,
50}
51
52/// Public configuration for the guardian signer.
53#[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        // Guardian public key slot (map: [0, 0, 0, 0] -> pubkey)
106        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        // Guardian scheme IDs slot (map: [0, 0, 0, 0] -> [scheme_id, 0, 0, 0])
114        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    /// Creates a new configuration with the given approvers, default threshold and guardian signer.
131    ///
132    /// The `default_threshold` must be at least 1 and at most the number of approvers.
133    /// The guardian public key must be different from all approver public keys.
134    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    /// Attaches a per-procedure threshold map. Each procedure threshold must be at least 1 and
154    /// at most the number of approvers.
155    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/// An [`AccountComponent`] implementing multisig authentication integrated with a state guardian.
185///
186/// It enforces a threshold of approver signatures for every transaction, with optional
187/// per-procedure threshold overrides. When a guardian is configured, multisig authorization is
188/// combined with guardian authorization, so operations require both multisig approval and a valid
189/// guardian signature. This substantially mitigates low-threshold state-withholding scenarios
190/// since the guardian is expected to forward state updates to other approvers.
191#[derive(Debug)]
192pub struct AuthGuardedMultisig {
193    multisig: AuthMultisig,
194    guardian_config: GuardianConfig,
195}
196
197impl AuthGuardedMultisig {
198    /// The name of the component.
199    pub const NAME: &'static str = "miden::standards::components::auth::guarded_multisig";
200
201    /// Returns the canonical [`AccountComponentName`] of this component.
202    pub const fn name() -> AccountComponentName {
203        AccountComponentName::from_static_str(Self::NAME)
204    }
205
206    /// Returns the [`AccountComponentCode`] of this component.
207    pub fn code() -> &'static AccountComponentCode {
208        &GUARDED_MULTISIG_CODE
209    }
210
211    /// Creates a new [`AuthGuardedMultisig`] component from the provided configuration.
212    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    /// Returns the [`StorageSlotName`] where the threshold configuration is stored.
221    pub fn threshold_config_slot() -> &'static StorageSlotName {
222        AuthMultisig::threshold_config_slot()
223    }
224
225    /// Returns the [`StorageSlotName`] where the approver public keys are stored.
226    pub fn approver_public_keys_slot() -> &'static StorageSlotName {
227        AuthMultisig::approver_public_keys_slot()
228    }
229
230    // Returns the [`StorageSlotName`] where the approver scheme IDs are stored.
231    pub fn approver_scheme_ids_slot() -> &'static StorageSlotName {
232        AuthMultisig::approver_scheme_ids_slot()
233    }
234
235    /// Returns the [`StorageSlotName`] where the executed transactions are stored.
236    pub fn executed_transactions_slot() -> &'static StorageSlotName {
237        AuthMultisig::executed_transactions_slot()
238    }
239
240    /// Returns the [`StorageSlotName`] where the procedure thresholds are stored.
241    pub fn procedure_thresholds_slot() -> &'static StorageSlotName {
242        AuthMultisig::procedure_thresholds_slot()
243    }
244
245    /// Returns the [`StorageSlotName`] where the guardian public key is stored.
246    pub fn guardian_public_key_slot() -> &'static StorageSlotName {
247        GuardianConfig::public_key_slot()
248    }
249
250    /// Returns the [`StorageSlotName`] where the guardian scheme IDs are stored.
251    pub fn guardian_scheme_id_slot() -> &'static StorageSlotName {
252        GuardianConfig::scheme_id_slot()
253    }
254
255    /// Returns the storage slot schema for the threshold configuration slot.
256    pub fn threshold_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
257        AuthMultisig::threshold_config_slot_schema()
258    }
259
260    /// Returns the storage slot schema for the approver public keys slot.
261    pub fn approver_public_keys_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
262        AuthMultisig::approver_public_keys_slot_schema()
263    }
264
265    // Returns the storage slot schema for the approver scheme IDs slot.
266    pub fn approver_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
267        AuthMultisig::approver_auth_scheme_slot_schema()
268    }
269
270    /// Returns the storage slot schema for the executed transactions slot.
271    pub fn executed_transactions_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
272        AuthMultisig::executed_transactions_slot_schema()
273    }
274
275    /// Returns the storage slot schema for the procedure thresholds slot.
276    pub fn procedure_thresholds_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
277        AuthMultisig::procedure_thresholds_slot_schema()
278    }
279
280    /// Returns the storage slot schema for the guardian public key slot.
281    pub fn guardian_public_key_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
282        GuardianConfig::public_key_slot_schema()
283    }
284
285    /// Returns the storage slot schema for the guardian scheme IDs slot.
286    pub fn guardian_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
287        GuardianConfig::auth_scheme_slot_schema()
288    }
289
290    /// Returns the [`AccountComponentMetadata`] for this component.
291    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// TESTS
344// ================================================================================================
345
346#[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 guarded multisig component setup with various configurations.
358    #[test]
359    fn test_guarded_multisig_component_setup() {
360        // Create test secret keys
361        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        // Create approvers list for multisig config
367        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        // Create guarded multisig component.
376        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        // Build account with guarded multisig component.
390        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        // Verify config slot: [threshold, num_approvers, 0, 0]
397        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        // Verify approver pub keys slot
404        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        // Verify approver scheme IDs slot
416        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        // Verify guardian signer is configured.
428        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 guarded multisig component with minimum threshold (1 of 1).
448    #[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        // Verify storage layout
475        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 guarded multisig component setup with a guardian.
501    #[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 guarded multisig component error cases.
551    #[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        // Test threshold > number of approvers (should fail)
558        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 guarded multisig component with duplicate approvers (should fail).
576    #[test]
577    fn test_guarded_multisig_component_duplicate_approvers() {
578        // Create secret keys for approvers
579        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        // Create approvers list with duplicate public keys
584        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 guarded multisig component rejects a guardian key which is already an approver.
607    #[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}