Skip to main content

miden_standards/account/auth/
multisig.rs

1use alloc::collections::BTreeSet;
2use alloc::vec::Vec;
3
4use miden_protocol::Word;
5use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment};
6use miden_protocol::account::component::{
7    AccountComponentMetadata,
8    FeltSchema,
9    SchemaType,
10    StorageSchema,
11    StorageSlotSchema,
12};
13use miden_protocol::account::{
14    AccountComponent,
15    AccountType,
16    StorageMap,
17    StorageMapKey,
18    StorageSlot,
19    StorageSlotName,
20};
21use miden_protocol::errors::AccountError;
22use miden_protocol::utils::sync::LazyLock;
23
24use crate::account::components::multisig_library;
25
26// CONSTANTS
27// ================================================================================================
28
29static THRESHOLD_CONFIG_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
30    StorageSlotName::new("miden::standards::auth::multisig::threshold_config")
31        .expect("storage slot name should be valid")
32});
33
34static APPROVER_PUBKEYS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
35    StorageSlotName::new("miden::standards::auth::multisig::approver_public_keys")
36        .expect("storage slot name should be valid")
37});
38
39static APPROVER_SCHEME_ID_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
40    StorageSlotName::new("miden::standards::auth::multisig::approver_schemes")
41        .expect("storage slot name should be valid")
42});
43
44static EXECUTED_TRANSACTIONS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
45    StorageSlotName::new("miden::standards::auth::multisig::executed_transactions")
46        .expect("storage slot name should be valid")
47});
48
49static PROCEDURE_THRESHOLDS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
50    StorageSlotName::new("miden::standards::auth::multisig::procedure_thresholds")
51        .expect("storage slot name should be valid")
52});
53
54// MULTISIG AUTHENTICATION COMPONENT
55// ================================================================================================
56
57/// Configuration for [`AuthMultisig`] component.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct AuthMultisigConfig {
60    approvers: Vec<(PublicKeyCommitment, AuthScheme)>,
61    default_threshold: u32,
62    proc_thresholds: Vec<(Word, u32)>,
63}
64
65impl AuthMultisigConfig {
66    /// Creates a new configuration with the given approvers and a default threshold.
67    ///
68    /// The `default_threshold` must be at least 1 and at most the number of approvers.
69    pub fn new(
70        approvers: Vec<(PublicKeyCommitment, AuthScheme)>,
71        default_threshold: u32,
72    ) -> Result<Self, AccountError> {
73        if default_threshold == 0 {
74            return Err(AccountError::other("threshold must be at least 1"));
75        }
76        if default_threshold > approvers.len() as u32 {
77            return Err(AccountError::other(
78                "threshold cannot be greater than number of approvers",
79            ));
80        }
81
82        // Check for duplicate approvers
83        let unique_approvers: BTreeSet<_> = approvers.iter().map(|(pk, _)| pk).collect();
84
85        if unique_approvers.len() != approvers.len() {
86            return Err(AccountError::other("duplicate approver public keys are not allowed"));
87        }
88
89        Ok(Self {
90            approvers,
91            default_threshold,
92            proc_thresholds: vec![],
93        })
94    }
95
96    /// Attaches a per-procedure threshold map. Each procedure threshold must be at least 1 and
97    /// at most the number of approvers.
98    pub fn with_proc_thresholds(
99        mut self,
100        proc_thresholds: Vec<(Word, u32)>,
101    ) -> Result<Self, AccountError> {
102        for (_, threshold) in &proc_thresholds {
103            if *threshold == 0 {
104                return Err(AccountError::other("procedure threshold must be at least 1"));
105            }
106            if *threshold > self.approvers.len() as u32 {
107                return Err(AccountError::other(
108                    "procedure threshold cannot be greater than number of approvers",
109                ));
110            }
111        }
112        self.proc_thresholds = proc_thresholds;
113        Ok(self)
114    }
115
116    pub fn approvers(&self) -> &[(PublicKeyCommitment, AuthScheme)] {
117        &self.approvers
118    }
119
120    pub fn default_threshold(&self) -> u32 {
121        self.default_threshold
122    }
123
124    pub fn proc_thresholds(&self) -> &[(Word, u32)] {
125        &self.proc_thresholds
126    }
127}
128
129/// An [`AccountComponent`] implementing a multisig authentication.
130///
131/// It enforces a threshold of approver signatures for every transaction, with optional
132/// per-procedure threshold overrides. Non-uniform thresholds (especially a threshold of one)
133/// should be used with caution for private multisig accounts, without Private State Manager (PSM),
134/// a single approver may advance state and withhold updates from other approvers, effectively
135/// locking them out.
136///
137/// This component supports all account types.
138#[derive(Debug)]
139pub struct AuthMultisig {
140    config: AuthMultisigConfig,
141}
142
143impl AuthMultisig {
144    /// The name of the component.
145    pub const NAME: &'static str = "miden::standards::components::auth::multisig";
146
147    /// Creates a new [`AuthMultisig`] component from the provided configuration.
148    pub fn new(config: AuthMultisigConfig) -> Result<Self, AccountError> {
149        Ok(Self { config })
150    }
151
152    /// Returns the [`StorageSlotName`] where the threshold configuration is stored.
153    pub fn threshold_config_slot() -> &'static StorageSlotName {
154        &THRESHOLD_CONFIG_SLOT_NAME
155    }
156
157    /// Returns the [`StorageSlotName`] where the approver public keys are stored.
158    pub fn approver_public_keys_slot() -> &'static StorageSlotName {
159        &APPROVER_PUBKEYS_SLOT_NAME
160    }
161
162    // Returns the [`StorageSlotName`] where the approver scheme IDs are stored.
163    pub fn approver_scheme_ids_slot() -> &'static StorageSlotName {
164        &APPROVER_SCHEME_ID_SLOT_NAME
165    }
166
167    /// Returns the [`StorageSlotName`] where the executed transactions are stored.
168    pub fn executed_transactions_slot() -> &'static StorageSlotName {
169        &EXECUTED_TRANSACTIONS_SLOT_NAME
170    }
171
172    /// Returns the [`StorageSlotName`] where the procedure thresholds are stored.
173    pub fn procedure_thresholds_slot() -> &'static StorageSlotName {
174        &PROCEDURE_THRESHOLDS_SLOT_NAME
175    }
176
177    /// Returns the storage slot schema for the threshold configuration slot.
178    pub fn threshold_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
179        (
180            Self::threshold_config_slot().clone(),
181            StorageSlotSchema::value(
182                "Threshold configuration",
183                [
184                    FeltSchema::u32("threshold"),
185                    FeltSchema::u32("num_approvers"),
186                    FeltSchema::new_void(),
187                    FeltSchema::new_void(),
188                ],
189            ),
190        )
191    }
192
193    /// Returns the storage slot schema for the approver public keys slot.
194    pub fn approver_public_keys_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
195        (
196            Self::approver_public_keys_slot().clone(),
197            StorageSlotSchema::map(
198                "Approver public keys",
199                SchemaType::u32(),
200                SchemaType::pub_key(),
201            ),
202        )
203    }
204
205    // Returns the storage slot schema for the approver scheme IDs slot.
206    pub fn approver_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
207        (
208            Self::approver_scheme_ids_slot().clone(),
209            StorageSlotSchema::map(
210                "Approver scheme IDs",
211                SchemaType::u32(),
212                SchemaType::auth_scheme(),
213            ),
214        )
215    }
216
217    /// Returns the storage slot schema for the executed transactions slot.
218    pub fn executed_transactions_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
219        (
220            Self::executed_transactions_slot().clone(),
221            StorageSlotSchema::map(
222                "Executed transactions",
223                SchemaType::native_word(),
224                SchemaType::native_word(),
225            ),
226        )
227    }
228
229    /// Returns the storage slot schema for the procedure thresholds slot.
230    pub fn procedure_thresholds_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
231        (
232            Self::procedure_thresholds_slot().clone(),
233            StorageSlotSchema::map(
234                "Procedure thresholds",
235                SchemaType::native_word(),
236                SchemaType::u32(),
237            ),
238        )
239    }
240
241    /// Returns the [`AccountComponentMetadata`] for this component.
242    pub fn component_metadata() -> AccountComponentMetadata {
243        let storage_schema = StorageSchema::new([
244            Self::threshold_config_slot_schema(),
245            Self::approver_public_keys_slot_schema(),
246            Self::approver_auth_scheme_slot_schema(),
247            Self::executed_transactions_slot_schema(),
248            Self::procedure_thresholds_slot_schema(),
249        ])
250        .expect("storage schema should be valid");
251
252        AccountComponentMetadata::new(Self::NAME, AccountType::all())
253            .with_description("Multisig authentication component using hybrid signature schemes")
254            .with_storage_schema(storage_schema)
255    }
256}
257
258impl From<AuthMultisig> for AccountComponent {
259    fn from(multisig: AuthMultisig) -> Self {
260        let mut storage_slots = Vec::with_capacity(5);
261
262        // Threshold config slot (value: [threshold, num_approvers, 0, 0])
263        let num_approvers = multisig.config.approvers().len() as u32;
264        storage_slots.push(StorageSlot::with_value(
265            AuthMultisig::threshold_config_slot().clone(),
266            Word::from([multisig.config.default_threshold(), num_approvers, 0, 0]),
267        ));
268
269        // Approver public keys slot (map)
270        let map_entries =
271            multisig.config.approvers().iter().enumerate().map(|(i, (pub_key, _))| {
272                (StorageMapKey::from_index(i as u32), Word::from(*pub_key))
273            });
274
275        // Safe to unwrap because we know that the map keys are unique.
276        storage_slots.push(StorageSlot::with_map(
277            AuthMultisig::approver_public_keys_slot().clone(),
278            StorageMap::with_entries(map_entries).unwrap(),
279        ));
280
281        // Approver scheme IDs slot (map): [index, 0, 0, 0] => [scheme_id, 0, 0, 0]
282        let scheme_id_entries =
283            multisig.config.approvers().iter().enumerate().map(|(i, (_, auth_scheme))| {
284                (StorageMapKey::from_index(i as u32), Word::from([*auth_scheme as u32, 0, 0, 0]))
285            });
286
287        storage_slots.push(StorageSlot::with_map(
288            AuthMultisig::approver_scheme_ids_slot().clone(),
289            StorageMap::with_entries(scheme_id_entries).unwrap(),
290        ));
291
292        // Executed transactions slot (map)
293        let executed_transactions = StorageMap::default();
294        storage_slots.push(StorageSlot::with_map(
295            AuthMultisig::executed_transactions_slot().clone(),
296            executed_transactions,
297        ));
298
299        // Procedure thresholds slot (map: PROC_ROOT -> threshold)
300        let proc_threshold_roots = StorageMap::with_entries(
301            multisig.config.proc_thresholds().iter().map(|(proc_root, threshold)| {
302                (StorageMapKey::from_raw(*proc_root), Word::from([*threshold, 0, 0, 0]))
303            }),
304        )
305        .unwrap();
306        storage_slots.push(StorageSlot::with_map(
307            AuthMultisig::procedure_thresholds_slot().clone(),
308            proc_threshold_roots,
309        ));
310
311        let metadata = AuthMultisig::component_metadata();
312
313        AccountComponent::new(multisig_library(), storage_slots, metadata).expect(
314            "Multisig auth component should satisfy the requirements of a valid account component",
315        )
316    }
317}
318
319// TESTS
320// ================================================================================================
321
322#[cfg(test)]
323mod tests {
324    use alloc::string::ToString;
325
326    use miden_protocol::Word;
327    use miden_protocol::account::auth::AuthSecretKey;
328    use miden_protocol::account::{AccountBuilder, auth};
329
330    use super::*;
331    use crate::account::wallets::BasicWallet;
332
333    /// Test multisig component setup with various configurations
334    #[test]
335    fn test_multisig_component_setup() {
336        // Create test secret keys
337        let sec_key_1 = AuthSecretKey::new_falcon512_poseidon2();
338        let sec_key_2 = AuthSecretKey::new_falcon512_poseidon2();
339        let sec_key_3 = AuthSecretKey::new_falcon512_poseidon2();
340
341        // Create approvers list for multisig config
342        let approvers = vec![
343            (sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
344            (sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
345            (sec_key_3.public_key().to_commitment(), sec_key_3.auth_scheme()),
346        ];
347
348        let threshold = 2u32;
349
350        // Create multisig component
351        let multisig_component = AuthMultisig::new(
352            AuthMultisigConfig::new(approvers.clone(), threshold).expect("invalid multisig config"),
353        )
354        .expect("multisig component creation failed");
355
356        // Build account with multisig component
357        let account = AccountBuilder::new([0; 32])
358            .with_auth_component(multisig_component)
359            .with_component(BasicWallet)
360            .build()
361            .expect("account building failed");
362
363        // Verify config slot: [threshold, num_approvers, 0, 0]
364        let config_slot = account
365            .storage()
366            .get_item(AuthMultisig::threshold_config_slot())
367            .expect("config storage slot access failed");
368        assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
369
370        // Verify approver pub keys slot
371        for (i, (expected_pub_key, _)) in approvers.iter().enumerate() {
372            let stored_pub_key = account
373                .storage()
374                .get_map_item(
375                    AuthMultisig::approver_public_keys_slot(),
376                    Word::from([i as u32, 0, 0, 0]),
377                )
378                .expect("approver public key storage map access failed");
379            assert_eq!(stored_pub_key, Word::from(*expected_pub_key));
380        }
381
382        // Verify approver scheme IDs slot
383        for (i, (_, expected_auth_scheme)) in approvers.iter().enumerate() {
384            let stored_scheme_id = account
385                .storage()
386                .get_map_item(
387                    AuthMultisig::approver_scheme_ids_slot(),
388                    Word::from([i as u32, 0, 0, 0]),
389                )
390                .expect("approver scheme ID storage map access failed");
391            assert_eq!(stored_scheme_id, Word::from([*expected_auth_scheme as u32, 0, 0, 0]));
392        }
393    }
394
395    /// Test multisig component with minimum threshold (1 of 1)
396    #[test]
397    fn test_multisig_component_minimum_threshold() {
398        let pub_key = AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment();
399        let approvers = vec![(pub_key, auth::AuthScheme::EcdsaK256Keccak)];
400        let threshold = 1u32;
401
402        let multisig_component = AuthMultisig::new(
403            AuthMultisigConfig::new(approvers.clone(), threshold).expect("invalid multisig config"),
404        )
405        .expect("multisig component creation failed");
406
407        let account = AccountBuilder::new([0; 32])
408            .with_auth_component(multisig_component)
409            .with_component(BasicWallet)
410            .build()
411            .expect("account building failed");
412
413        // Verify storage layout
414        let config_slot = account
415            .storage()
416            .get_item(AuthMultisig::threshold_config_slot())
417            .expect("config storage slot access failed");
418        assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
419
420        let stored_pub_key = account
421            .storage()
422            .get_map_item(AuthMultisig::approver_public_keys_slot(), Word::from([0u32, 0, 0, 0]))
423            .expect("approver pub keys storage map access failed");
424        assert_eq!(stored_pub_key, Word::from(pub_key));
425
426        let stored_scheme_id = account
427            .storage()
428            .get_map_item(AuthMultisig::approver_scheme_ids_slot(), Word::from([0u32, 0, 0, 0]))
429            .expect("approver scheme IDs storage map access failed");
430        assert_eq!(
431            stored_scheme_id,
432            Word::from([auth::AuthScheme::EcdsaK256Keccak as u32, 0, 0, 0])
433        );
434    }
435
436    /// Test multisig component error cases
437    #[test]
438    fn test_multisig_component_error_cases() {
439        let pub_key = AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment();
440        let approvers = vec![(pub_key, auth::AuthScheme::EcdsaK256Keccak)];
441
442        // Test threshold = 0 (should fail)
443        let result = AuthMultisigConfig::new(approvers.clone(), 0);
444        assert!(result.unwrap_err().to_string().contains("threshold must be at least 1"));
445
446        // Test threshold > number of approvers (should fail)
447        let result = AuthMultisigConfig::new(approvers, 2);
448        assert!(
449            result
450                .unwrap_err()
451                .to_string()
452                .contains("threshold cannot be greater than number of approvers")
453        );
454    }
455
456    /// Test multisig component with duplicate approvers (should fail)
457    #[test]
458    fn test_multisig_component_duplicate_approvers() {
459        // Create secret keys for approvers
460        let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak();
461        let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak();
462
463        // Create approvers list with duplicate public keys
464        let approvers = vec![
465            (sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
466            (sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
467            (sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
468        ];
469
470        let result = AuthMultisigConfig::new(approvers, 2);
471        assert!(
472            result
473                .unwrap_err()
474                .to_string()
475                .contains("duplicate approver public keys are not allowed")
476        );
477    }
478}