Skip to main content

miden_standards/account/auth/
guarded_multisig.rs

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