Skip to main content

miden_standards/account/auth/
multisig_psm.rs

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