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 super::{Approver, ApproverSet};
26use crate::account::account_component_code;
27
28account_component_code!(GUARDED_MULTISIG_CODE, "miden-standards-auth-guarded-multisig.masp");
29
30// CONSTANTS
31// ================================================================================================
32
33static GUARDIAN_PUBKEY_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
34    StorageSlotName::new("miden::standards::auth::guardian::pub_key")
35        .expect("storage slot name should be valid")
36});
37
38static GUARDIAN_SCHEME_ID_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
39    StorageSlotName::new("miden::standards::auth::guardian::scheme")
40        .expect("storage slot name should be valid")
41});
42
43// MULTISIG AUTHENTICATION COMPONENT
44// ================================================================================================
45
46/// Configuration for [`AuthGuardedMultisig`] component.
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct AuthGuardedMultisigConfig {
49    multisig: AuthMultisigConfig,
50    guardian_config: GuardianConfig,
51}
52
53/// Public configuration for the guardian signer.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct GuardianConfig {
56    approver: Approver,
57}
58
59impl GuardianConfig {
60    pub fn new(approver: Approver) -> Self {
61        Self { approver }
62    }
63
64    pub fn approver(&self) -> Approver {
65        self.approver
66    }
67
68    pub fn pub_key(&self) -> PublicKeyCommitment {
69        self.approver.pub_key()
70    }
71
72    pub fn auth_scheme(&self) -> AuthScheme {
73        self.approver.auth_scheme()
74    }
75
76    fn public_key_slot() -> &'static StorageSlotName {
77        &GUARDIAN_PUBKEY_SLOT_NAME
78    }
79
80    fn scheme_id_slot() -> &'static StorageSlotName {
81        &GUARDIAN_SCHEME_ID_SLOT_NAME
82    }
83
84    fn public_key_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
85        (
86            Self::public_key_slot().clone(),
87            StorageSlotSchema::map(
88                "Guardian public keys",
89                SchemaType::u32(),
90                SchemaType::pub_key(),
91            ),
92        )
93    }
94
95    fn auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
96        (
97            Self::scheme_id_slot().clone(),
98            StorageSlotSchema::map(
99                "Guardian scheme IDs",
100                SchemaType::u32(),
101                SchemaType::auth_scheme(),
102            ),
103        )
104    }
105
106    fn into_component_parts(self) -> (Vec<StorageSlot>, Vec<(StorageSlotName, StorageSlotSchema)>) {
107        let mut storage_slots = Vec::with_capacity(2);
108
109        // Guardian public key slot (map: [0, 0, 0, 0] -> pubkey)
110        let guardian_public_key_entries =
111            [(StorageMapKey::from_raw(Word::from([0u32, 0, 0, 0])), Word::from(self.pub_key()))];
112        storage_slots.push(StorageSlot::with_map(
113            Self::public_key_slot().clone(),
114            StorageMap::with_entries(guardian_public_key_entries).unwrap(),
115        ));
116
117        // Guardian scheme IDs slot (map: [0, 0, 0, 0] -> [scheme_id, 0, 0, 0])
118        let guardian_scheme_id_entries = [(
119            StorageMapKey::from_raw(Word::from([0u32, 0, 0, 0])),
120            Word::from([self.auth_scheme() as u32, 0, 0, 0]),
121        )];
122        storage_slots.push(StorageSlot::with_map(
123            Self::scheme_id_slot().clone(),
124            StorageMap::with_entries(guardian_scheme_id_entries).unwrap(),
125        ));
126
127        let slot_metadata = vec![Self::public_key_slot_schema(), Self::auth_scheme_slot_schema()];
128
129        (storage_slots, slot_metadata)
130    }
131}
132
133impl AuthGuardedMultisigConfig {
134    /// Creates a new configuration with the given approver set and guardian signer.
135    ///
136    /// The guardian public key must be different from all approver public keys.
137    pub fn new(
138        approver_set: ApproverSet,
139        guardian_config: GuardianConfig,
140    ) -> Result<Self, AccountError> {
141        if approver_set
142            .approvers()
143            .iter()
144            .any(|approver| approver.pub_key() == guardian_config.pub_key())
145        {
146            return Err(AccountError::other(
147                "guardian public key must be different from approvers",
148            ));
149        }
150
151        Ok(Self {
152            multisig: AuthMultisigConfig::new(approver_set),
153            guardian_config,
154        })
155    }
156
157    /// Attaches a per-procedure threshold map. Each procedure threshold must be at least 1 and
158    /// at most the number of approvers.
159    pub fn with_proc_thresholds(
160        mut self,
161        proc_thresholds: Vec<(AccountProcedureRoot, u32)>,
162    ) -> Result<Self, AccountError> {
163        self.multisig = self.multisig.with_proc_thresholds(proc_thresholds)?;
164        Ok(self)
165    }
166
167    pub fn approver_set(&self) -> &ApproverSet {
168        self.multisig.approver_set()
169    }
170
171    pub fn approvers(&self) -> &[Approver] {
172        self.multisig.approvers()
173    }
174
175    pub fn default_threshold(&self) -> u32 {
176        self.multisig.default_threshold()
177    }
178
179    pub fn proc_thresholds(&self) -> &[(AccountProcedureRoot, u32)] {
180        self.multisig.proc_thresholds()
181    }
182
183    pub fn guardian_config(&self) -> GuardianConfig {
184        self.guardian_config
185    }
186
187    fn into_parts(self) -> (AuthMultisigConfig, GuardianConfig) {
188        (self.multisig, self.guardian_config)
189    }
190}
191
192/// An [`AccountComponent`] implementing multisig authentication integrated with a state guardian.
193///
194/// It enforces a threshold of approver signatures for every transaction, with optional
195/// per-procedure threshold overrides. When a guardian is configured, multisig authorization is
196/// combined with guardian authorization, so operations require both multisig approval and a valid
197/// guardian signature. This substantially mitigates low-threshold state-withholding scenarios
198/// since the guardian is expected to forward state updates to other approvers.
199///
200/// # Privacy
201///
202/// Approvers and the guardian using [`AuthScheme::EcdsaK256Keccak`][scheme] disclose their public
203/// key and signature at proving time and therefore do not get public-key privacy; those using
204/// [`Falcon512Poseidon2`][falcon] do. See [`Approver`](super::Approver) for details.
205///
206/// [scheme]: miden_protocol::account::auth::AuthScheme::EcdsaK256Keccak
207/// [falcon]: miden_protocol::account::auth::AuthScheme::Falcon512Poseidon2
208#[derive(Debug)]
209pub struct AuthGuardedMultisig {
210    multisig: AuthMultisig,
211    guardian_config: GuardianConfig,
212}
213
214impl AuthGuardedMultisig {
215    /// The name of the component.
216    pub const NAME: &'static str = "miden::standards::components::auth::guarded_multisig";
217
218    /// Returns the canonical [`AccountComponentName`] of this component.
219    pub const fn name() -> AccountComponentName {
220        AccountComponentName::from_static_str(Self::NAME)
221    }
222
223    /// Returns the [`AccountComponentCode`] of this component.
224    pub fn code() -> &'static AccountComponentCode {
225        &GUARDED_MULTISIG_CODE
226    }
227
228    /// Creates a new [`AuthGuardedMultisig`] component from the provided configuration.
229    pub fn new(config: AuthGuardedMultisigConfig) -> Result<Self, AccountError> {
230        let (multisig_config, guardian_config) = config.into_parts();
231        Ok(Self {
232            multisig: AuthMultisig::new(multisig_config)?,
233            guardian_config,
234        })
235    }
236
237    /// Returns the [`StorageSlotName`] where the threshold configuration is stored.
238    pub fn threshold_config_slot() -> &'static StorageSlotName {
239        AuthMultisig::threshold_config_slot()
240    }
241
242    /// Returns the [`StorageSlotName`] where the approver public keys are stored.
243    pub fn approver_public_keys_slot() -> &'static StorageSlotName {
244        AuthMultisig::approver_public_keys_slot()
245    }
246
247    // Returns the [`StorageSlotName`] where the approver scheme IDs are stored.
248    pub fn approver_scheme_ids_slot() -> &'static StorageSlotName {
249        AuthMultisig::approver_scheme_ids_slot()
250    }
251
252    /// Returns the [`StorageSlotName`] where the executed transactions are stored.
253    pub fn executed_transactions_slot() -> &'static StorageSlotName {
254        AuthMultisig::executed_transactions_slot()
255    }
256
257    /// Returns the [`StorageSlotName`] where the procedure thresholds are stored.
258    pub fn procedure_thresholds_slot() -> &'static StorageSlotName {
259        AuthMultisig::procedure_thresholds_slot()
260    }
261
262    /// Returns the [`StorageSlotName`] where the guardian public key is stored.
263    pub fn guardian_public_key_slot() -> &'static StorageSlotName {
264        GuardianConfig::public_key_slot()
265    }
266
267    /// Returns the [`StorageSlotName`] where the guardian scheme IDs are stored.
268    pub fn guardian_scheme_id_slot() -> &'static StorageSlotName {
269        GuardianConfig::scheme_id_slot()
270    }
271
272    /// Returns the storage slot schema for the threshold configuration slot.
273    pub fn threshold_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
274        AuthMultisig::threshold_config_slot_schema()
275    }
276
277    /// Returns the storage slot schema for the approver public keys slot.
278    pub fn approver_public_keys_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
279        AuthMultisig::approver_public_keys_slot_schema()
280    }
281
282    // Returns the storage slot schema for the approver scheme IDs slot.
283    pub fn approver_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
284        AuthMultisig::approver_auth_scheme_slot_schema()
285    }
286
287    /// Returns the storage slot schema for the executed transactions slot.
288    pub fn executed_transactions_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
289        AuthMultisig::executed_transactions_slot_schema()
290    }
291
292    /// Returns the storage slot schema for the procedure thresholds slot.
293    pub fn procedure_thresholds_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
294        AuthMultisig::procedure_thresholds_slot_schema()
295    }
296
297    /// Returns the storage slot schema for the guardian public key slot.
298    pub fn guardian_public_key_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
299        GuardianConfig::public_key_slot_schema()
300    }
301
302    /// Returns the storage slot schema for the guardian scheme IDs slot.
303    pub fn guardian_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
304        GuardianConfig::auth_scheme_slot_schema()
305    }
306
307    /// Returns the [`AccountComponentMetadata`] for this component.
308    pub fn component_metadata() -> AccountComponentMetadata {
309        let storage_schema = StorageSchema::new([
310            Self::threshold_config_slot_schema(),
311            Self::approver_public_keys_slot_schema(),
312            Self::approver_auth_scheme_slot_schema(),
313            Self::executed_transactions_slot_schema(),
314            Self::procedure_thresholds_slot_schema(),
315            Self::guardian_public_key_slot_schema(),
316            Self::guardian_auth_scheme_slot_schema(),
317        ])
318        .expect("storage schema should be valid");
319
320        AccountComponentMetadata::new(Self::NAME)
321            .with_description(
322                "Guarded multisig authentication component integrated \
323                 with a state guardian using hybrid signature schemes",
324            )
325            .with_storage_schema(storage_schema)
326    }
327}
328
329impl From<AuthGuardedMultisig> for AccountComponent {
330    fn from(multisig: AuthGuardedMultisig) -> Self {
331        let AuthGuardedMultisig { multisig, guardian_config } = multisig;
332        let multisig_component = AccountComponent::from(multisig);
333        let (guardian_slots, guardian_slot_metadata) = guardian_config.into_component_parts();
334
335        let mut storage_slots = multisig_component.storage_slots().to_vec();
336        storage_slots.extend(guardian_slots);
337
338        let mut slot_schemas: Vec<(StorageSlotName, StorageSlotSchema)> = multisig_component
339            .storage_schema()
340            .iter()
341            .map(|(slot_name, slot_schema)| (slot_name.clone(), slot_schema.clone()))
342            .collect();
343        slot_schemas.extend(guardian_slot_metadata);
344
345        let storage_schema =
346            StorageSchema::new(slot_schemas).expect("storage schema should be valid");
347
348        let metadata = AccountComponentMetadata::new(AuthGuardedMultisig::NAME)
349            .with_description(multisig_component.metadata().description())
350            .with_version(multisig_component.metadata().version().clone())
351            .with_storage_schema(storage_schema);
352
353        AccountComponent::new(AuthGuardedMultisig::code().clone(), storage_slots, metadata).expect(
354            "Guarded multisig auth component should satisfy the requirements of a valid \
355             account component",
356        )
357    }
358}
359
360// TESTS
361// ================================================================================================
362
363#[cfg(test)]
364mod tests {
365    use alloc::string::ToString;
366
367    use miden_protocol::Word;
368    use miden_protocol::account::AccountBuilder;
369    use miden_protocol::account::auth::AuthSecretKey;
370
371    use super::*;
372    use crate::account::wallets::BasicWallet;
373
374    fn approver(key: &AuthSecretKey) -> Approver {
375        Approver::new(key.public_key().to_commitment(), key.auth_scheme())
376    }
377
378    /// Test guarded multisig component setup with various configurations.
379    #[test]
380    fn test_guarded_multisig_component_setup() {
381        // Create test secret keys
382        let sec_key_1 = AuthSecretKey::new_falcon512_poseidon2();
383        let sec_key_2 = AuthSecretKey::new_falcon512_poseidon2();
384        let sec_key_3 = AuthSecretKey::new_falcon512_poseidon2();
385        let guardian_key = AuthSecretKey::new_ecdsa_k256_keccak();
386
387        // Create approvers list for multisig config
388        let approvers = vec![approver(&sec_key_1), approver(&sec_key_2), approver(&sec_key_3)];
389
390        let threshold = 2u32;
391
392        // Create guarded multisig component.
393        let approver_set =
394            ApproverSet::new(approvers.clone(), threshold).expect("invalid approver set");
395        let multisig_component = AuthGuardedMultisig::new(
396            AuthGuardedMultisigConfig::new(
397                approver_set,
398                GuardianConfig::new(approver(&guardian_key)),
399            )
400            .expect("invalid guarded multisig config"),
401        )
402        .expect("guarded multisig component creation failed");
403
404        // Build account with guarded multisig component.
405        let account = AccountBuilder::new([0; 32])
406            .with_auth_component(multisig_component)
407            .with_component(BasicWallet)
408            .build()
409            .expect("account building failed");
410
411        // Verify config slot: [threshold, num_approvers, 0, 0]
412        let config_slot = account
413            .storage()
414            .get_item(AuthGuardedMultisig::threshold_config_slot())
415            .expect("config storage slot access failed");
416        assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
417
418        // Verify approver pub keys slot
419        for (i, expected) in approvers.iter().enumerate() {
420            let stored_pub_key = account
421                .storage()
422                .get_map_item(
423                    AuthGuardedMultisig::approver_public_keys_slot(),
424                    StorageMapKey::from_index(i as u32),
425                )
426                .expect("approver public key storage map access failed");
427            assert_eq!(stored_pub_key, Word::from(expected.pub_key()));
428        }
429
430        // Verify approver scheme IDs slot
431        for (i, expected) in approvers.iter().enumerate() {
432            let stored_scheme_id = account
433                .storage()
434                .get_map_item(
435                    AuthGuardedMultisig::approver_scheme_ids_slot(),
436                    StorageMapKey::from_index(i as u32),
437                )
438                .expect("approver scheme ID storage map access failed");
439            assert_eq!(stored_scheme_id, Word::from([expected.auth_scheme() as u32, 0, 0, 0]));
440        }
441
442        // Verify guardian signer is configured.
443        let guardian_public_key = account
444            .storage()
445            .get_map_item(
446                AuthGuardedMultisig::guardian_public_key_slot(),
447                StorageMapKey::from_index(0),
448            )
449            .expect("guardian public key storage map access failed");
450        assert_eq!(guardian_public_key, Word::from(guardian_key.public_key().to_commitment()));
451
452        let guardian_scheme_id = account
453            .storage()
454            .get_map_item(
455                AuthGuardedMultisig::guardian_scheme_id_slot(),
456                StorageMapKey::from_index(0),
457            )
458            .expect("guardian scheme ID storage map access failed");
459        assert_eq!(guardian_scheme_id, Word::from([guardian_key.auth_scheme() as u32, 0, 0, 0]));
460    }
461
462    /// Test guarded multisig component with minimum threshold (1 of 1).
463    #[test]
464    fn test_guarded_multisig_component_minimum_threshold() {
465        let approver_key = AuthSecretKey::new_ecdsa_k256_keccak();
466        let pub_key = approver_key.public_key().to_commitment();
467        let guardian_key = AuthSecretKey::new_falcon512_poseidon2();
468        let approvers = vec![approver(&approver_key)];
469        let threshold = 1u32;
470
471        let approver_set =
472            ApproverSet::new(approvers.clone(), threshold).expect("invalid approver set");
473        let multisig_component = AuthGuardedMultisig::new(
474            AuthGuardedMultisigConfig::new(
475                approver_set,
476                GuardianConfig::new(approver(&guardian_key)),
477            )
478            .expect("invalid guarded multisig config"),
479        )
480        .expect("guarded multisig component creation failed");
481
482        let account = AccountBuilder::new([0; 32])
483            .with_auth_component(multisig_component)
484            .with_component(BasicWallet)
485            .build()
486            .expect("account building failed");
487
488        // Verify storage layout
489        let config_slot = account
490            .storage()
491            .get_item(AuthGuardedMultisig::threshold_config_slot())
492            .expect("config storage slot access failed");
493        assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
494
495        let stored_pub_key = account
496            .storage()
497            .get_map_item(
498                AuthGuardedMultisig::approver_public_keys_slot(),
499                StorageMapKey::from_index(0),
500            )
501            .expect("approver pub keys storage map access failed");
502        assert_eq!(stored_pub_key, Word::from(pub_key));
503
504        let stored_scheme_id = account
505            .storage()
506            .get_map_item(
507                AuthGuardedMultisig::approver_scheme_ids_slot(),
508                StorageMapKey::from_index(0),
509            )
510            .expect("approver scheme IDs storage map access failed");
511        assert_eq!(stored_scheme_id, Word::from([AuthScheme::EcdsaK256Keccak as u32, 0, 0, 0]));
512    }
513
514    /// Test guarded multisig component rejects a guardian key which is already an approver.
515    #[test]
516    fn test_guarded_multisig_component_guardian_not_approver() {
517        let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak();
518        let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak();
519
520        let approvers = vec![approver(&sec_key_1), approver(&sec_key_2)];
521        let approver_set = ApproverSet::new(approvers, 2).expect("invalid approver set");
522
523        let result =
524            AuthGuardedMultisigConfig::new(approver_set, GuardianConfig::new(approver(&sec_key_1)));
525
526        assert!(
527            result
528                .unwrap_err()
529                .to_string()
530                .contains("guardian public key must be different from approvers")
531        );
532    }
533}