Skip to main content

miden_standards/account/auth/multisig_smart/
component.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    StorageMap,
15    StorageMapKey,
16    StorageSlot,
17    StorageSlotName,
18};
19use miden_protocol::errors::AccountError;
20use miden_protocol::utils::sync::LazyLock;
21
22// Slots and schemas reused from `AuthMultisig` to keep the storage layout in sync. The statics
23// are exposed as `pub(super)` in the sibling `multisig` module; we reference them directly so
24// the sharing is visible at the use site rather than hidden behind delegating methods.
25use super::super::multisig::{
26    APPROVER_PUBKEYS_SLOT_NAME,
27    APPROVER_SCHEME_ID_SLOT_NAME,
28    EXECUTED_TRANSACTIONS_SLOT_NAME,
29    THRESHOLD_CONFIG_SLOT_NAME,
30};
31use super::ProcedurePolicy;
32use crate::account::account_component_code;
33use crate::account::auth::AuthMultisig;
34
35account_component_code!(MULTISIG_SMART_CODE, "auth/multisig_smart.masl");
36
37// CONSTANTS
38// ================================================================================================
39
40// Only the smart-specific procedure_policies slot needs its own constant here. The other four
41// slots (threshold config, approver public keys, approver scheme ids, executed transactions) are
42// reused from `AuthMultisig` via the imports above.
43static PROCEDURE_POLICIES_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
44    StorageSlotName::new("miden::standards::auth::multisig_smart::procedure_policies")
45        .expect("storage slot name should be valid")
46});
47
48// MULTISIG SMART AUTHENTICATION COMPONENT
49// ================================================================================================
50
51/// Configuration for [`AuthMultisigSmart`] component.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct AuthMultisigSmartConfig {
54    approvers: Vec<(PublicKeyCommitment, AuthScheme)>,
55    default_threshold: u32,
56    procedure_policies: Vec<(Word, ProcedurePolicy)>,
57}
58
59impl AuthMultisigSmartConfig {
60    /// Creates a new configuration with the given approvers and a default threshold.
61    ///
62    /// The `default_threshold` must be at least 1 and at most the number of approvers.
63    pub fn new(
64        approvers: Vec<(PublicKeyCommitment, AuthScheme)>,
65        default_threshold: u32,
66    ) -> Result<Self, AccountError> {
67        if default_threshold == 0 {
68            return Err(AccountError::other("threshold must be at least 1"));
69        }
70        if default_threshold > approvers.len() as u32 {
71            return Err(AccountError::other(
72                "threshold cannot be greater than number of approvers",
73            ));
74        }
75
76        let unique_approvers: alloc::collections::BTreeSet<_> =
77            approvers.iter().map(|(pk, _)| pk).collect();
78        if unique_approvers.len() != approvers.len() {
79            return Err(AccountError::other("duplicate approver public keys are not allowed"));
80        }
81
82        Ok(Self {
83            approvers,
84            default_threshold,
85            procedure_policies: vec![],
86        })
87    }
88
89    /// Attaches a per-procedure smart policy map.
90    pub fn with_proc_policies(
91        mut self,
92        proc_policies: Vec<(Word, ProcedurePolicy)>,
93    ) -> Result<Self, AccountError> {
94        validate_proc_policies(self.approvers.len() as u32, &proc_policies)?;
95        self.procedure_policies = proc_policies;
96        Ok(self)
97    }
98
99    pub fn approvers(&self) -> &[(PublicKeyCommitment, AuthScheme)] {
100        &self.approvers
101    }
102
103    pub fn default_threshold(&self) -> u32 {
104        self.default_threshold
105    }
106
107    pub fn procedure_policies(&self) -> &[(Word, ProcedurePolicy)] {
108        &self.procedure_policies
109    }
110}
111
112fn validate_proc_policies(
113    num_approvers: u32,
114    proc_policies: &[(Word, ProcedurePolicy)],
115) -> Result<(), AccountError> {
116    // Reject duplicate procedure roots. Catching it here turns the failure into a regular
117    // `AccountError` returned from `with_proc_policies` / `AuthMultisigSmart::new`.
118    let mut policy_roots = alloc::collections::BTreeSet::new();
119    for (proc_root, _) in proc_policies {
120        if !policy_roots.insert(*proc_root) {
121            return Err(AccountError::other(
122                "duplicate procedure roots are not allowed in the procedure policy map",
123            ));
124        }
125    }
126
127    for (_, policy) in proc_policies {
128        if let Some(immediate_threshold) = policy.immediate_threshold()
129            && immediate_threshold > num_approvers
130        {
131            return Err(AccountError::other(
132                "procedure policy immediate threshold cannot exceed number of approvers",
133            ));
134        }
135        if let Some(delay_threshold) = policy.delay_threshold()
136            && delay_threshold > num_approvers
137        {
138            return Err(AccountError::other(
139                "procedure policy delay threshold cannot exceed number of approvers",
140            ));
141        }
142    }
143
144    Ok(())
145}
146
147/// An [`AccountComponent`] implementing a multisig auth component with smart-policy slots.
148#[derive(Debug)]
149pub struct AuthMultisigSmart {
150    config: AuthMultisigSmartConfig,
151}
152
153impl AuthMultisigSmart {
154    /// The name of the component.
155    pub const NAME: &'static str = "miden::standards::components::auth::multisig_smart";
156
157    /// Returns the [`AccountComponentCode`] of this component.
158    pub fn code() -> &'static AccountComponentCode {
159        &MULTISIG_SMART_CODE
160    }
161
162    /// Creates a new [`AuthMultisigSmart`] component from the provided configuration.
163    pub fn new(config: AuthMultisigSmartConfig) -> Result<Self, AccountError> {
164        validate_proc_policies(config.approvers().len() as u32, config.procedure_policies())?;
165        Ok(Self { config })
166    }
167
168    pub fn threshold_config_slot() -> &'static StorageSlotName {
169        &THRESHOLD_CONFIG_SLOT_NAME
170    }
171
172    pub fn approver_public_keys_slot() -> &'static StorageSlotName {
173        &APPROVER_PUBKEYS_SLOT_NAME
174    }
175
176    pub fn approver_scheme_ids_slot() -> &'static StorageSlotName {
177        &APPROVER_SCHEME_ID_SLOT_NAME
178    }
179
180    pub fn executed_transactions_slot() -> &'static StorageSlotName {
181        &EXECUTED_TRANSACTIONS_SLOT_NAME
182    }
183
184    pub fn procedure_policies_slot() -> &'static StorageSlotName {
185        &PROCEDURE_POLICIES_SLOT_NAME
186    }
187
188    pub fn threshold_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
189        AuthMultisig::threshold_config_slot_schema()
190    }
191
192    pub fn approver_public_keys_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
193        AuthMultisig::approver_public_keys_slot_schema()
194    }
195
196    pub fn approver_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
197        AuthMultisig::approver_auth_scheme_slot_schema()
198    }
199
200    pub fn executed_transactions_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
201        AuthMultisig::executed_transactions_slot_schema()
202    }
203
204    pub fn procedure_policies_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
205        (
206            Self::procedure_policies_slot().clone(),
207            StorageSlotSchema::map(
208                "Procedure policies",
209                SchemaType::native_word(),
210                SchemaType::native_word(),
211            ),
212        )
213    }
214}
215
216impl From<AuthMultisigSmart> for AccountComponent {
217    fn from(multisig: AuthMultisigSmart) -> Self {
218        let mut storage_slots = Vec::with_capacity(5);
219
220        // Threshold config slot (value: [threshold, num_approvers, 0, 0])
221        let num_approvers = multisig.config.approvers().len() as u32;
222        storage_slots.push(StorageSlot::with_value(
223            AuthMultisigSmart::threshold_config_slot().clone(),
224            Word::from([multisig.config.default_threshold(), num_approvers, 0, 0]),
225        ));
226
227        // Approver public keys slot (map)
228        let map_entries =
229            multisig.config.approvers().iter().enumerate().map(|(i, (pub_key, _))| {
230                (StorageMapKey::from_index(i as u32), Word::from(*pub_key))
231            });
232        storage_slots.push(StorageSlot::with_map(
233            AuthMultisigSmart::approver_public_keys_slot().clone(),
234            StorageMap::with_entries(map_entries).unwrap(),
235        ));
236
237        // Approver scheme IDs slot
238        let scheme_id_entries =
239            multisig.config.approvers().iter().enumerate().map(|(i, (_, auth_scheme))| {
240                (StorageMapKey::from_index(i as u32), Word::from([*auth_scheme as u32, 0, 0, 0]))
241            });
242        storage_slots.push(StorageSlot::with_map(
243            AuthMultisigSmart::approver_scheme_ids_slot().clone(),
244            StorageMap::with_entries(scheme_id_entries).unwrap(),
245        ));
246
247        // Executed transactions slot (map)
248        storage_slots.push(StorageSlot::with_map(
249            AuthMultisigSmart::executed_transactions_slot().clone(),
250            StorageMap::default(),
251        ));
252
253        // Procedure policies slot (map)
254        let procedure_policies =
255            StorageMap::with_entries(multisig.config.procedure_policies().iter().map(
256                |(proc_root, policy)| (StorageMapKey::from_raw(*proc_root), policy.to_word()),
257            ))
258            .unwrap();
259        storage_slots.push(StorageSlot::with_map(
260            AuthMultisigSmart::procedure_policies_slot().clone(),
261            procedure_policies,
262        ));
263
264        let storage_schema = StorageSchema::new(vec![
265            AuthMultisigSmart::threshold_config_slot_schema(),
266            AuthMultisigSmart::approver_public_keys_slot_schema(),
267            AuthMultisigSmart::approver_auth_scheme_slot_schema(),
268            AuthMultisigSmart::executed_transactions_slot_schema(),
269            AuthMultisigSmart::procedure_policies_slot_schema(),
270        ])
271        .expect("storage schema should be valid");
272
273        let metadata = AccountComponentMetadata::new(AuthMultisigSmart::NAME)
274            .with_description("Multisig smart authentication component")
275            .with_storage_schema(storage_schema);
276
277        AccountComponent::new(AuthMultisigSmart::code().clone(), storage_slots, metadata).expect(
278            "multisig smart component should satisfy the requirements of a valid account component",
279        )
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use alloc::string::ToString;
286
287    use miden_protocol::account::AccountBuilder;
288    use miden_protocol::account::auth::AuthSecretKey;
289
290    use super::*;
291    use crate::account::wallets::BasicWallet;
292
293    #[test]
294    fn test_multisig_smart_component_setup() {
295        let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak();
296        let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak();
297        let approvers = vec![
298            (sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
299            (sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
300        ];
301        let num_approvers = approvers.len() as u32;
302        let default_threshold = 2u32;
303        let receive_asset_immediate_threshold = 1u32;
304
305        let config = AuthMultisigSmartConfig::new(approvers.clone(), default_threshold)
306            .expect("invalid multisig smart config")
307            .with_proc_policies(vec![(
308                BasicWallet::receive_asset_root().as_word(),
309                ProcedurePolicy::with_immediate_threshold(receive_asset_immediate_threshold)
310                    .expect("procedure policy should be valid"),
311            )])
312            .expect("procedure policy config should be valid");
313
314        let component =
315            AuthMultisigSmart::new(config).expect("multisig smart component creation failed");
316
317        let account = AccountBuilder::new([0; 32])
318            .with_auth_component(component)
319            .with_component(BasicWallet)
320            .build()
321            .expect("account building failed");
322
323        let threshold_config = account
324            .storage()
325            .get_item(AuthMultisigSmart::threshold_config_slot())
326            .expect("threshold config should be present");
327        assert_eq!(threshold_config, Word::from([default_threshold, num_approvers, 0, 0]));
328
329        let receive_asset_policy = account
330            .storage()
331            .get_map_item(
332                AuthMultisigSmart::procedure_policies_slot(),
333                BasicWallet::receive_asset_root().as_word(),
334            )
335            .expect("receive_asset policy should be present");
336        assert_eq!(
337            receive_asset_policy,
338            Word::from([receive_asset_immediate_threshold, 0u32, 0u32, 0u32])
339        );
340    }
341
342    #[test]
343    fn test_multisig_smart_component_error_cases() {
344        let sec_key = AuthSecretKey::new_ecdsa_k256_keccak();
345        let approvers = vec![(sec_key.public_key().to_commitment(), sec_key.auth_scheme())];
346
347        let result = AuthMultisigSmartConfig::new(approvers.clone(), 0);
348        assert!(result.unwrap_err().to_string().contains("threshold must be at least 1"));
349
350        let result = AuthMultisigSmartConfig::new(approvers, 2);
351        assert!(
352            result
353                .unwrap_err()
354                .to_string()
355                .contains("threshold cannot be greater than number of approvers")
356        );
357    }
358
359    #[test]
360    fn test_multisig_smart_component_rejects_duplicate_procedure_roots() {
361        let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak();
362        let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak();
363        let approvers = vec![
364            (sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
365            (sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
366        ];
367
368        let receive_asset_root = BasicWallet::receive_asset_root().as_word();
369        let policy_one =
370            ProcedurePolicy::with_immediate_threshold(1).expect("procedure policy should be valid");
371        let policy_two =
372            ProcedurePolicy::with_immediate_threshold(2).expect("procedure policy should be valid");
373
374        let result = AuthMultisigSmartConfig::new(approvers, 2)
375            .expect("base config should be valid")
376            .with_proc_policies(vec![
377                (receive_asset_root, policy_one),
378                (receive_asset_root, policy_two),
379            ]);
380
381        assert!(
382            result
383                .unwrap_err()
384                .to_string()
385                .contains("duplicate procedure roots are not allowed in the procedure policy map")
386        );
387    }
388
389    #[test]
390    fn test_multisig_smart_component_duplicate_approvers() {
391        let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak();
392        let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak();
393
394        let approvers = vec![
395            (sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
396            (sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
397            (sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
398        ];
399
400        let result = AuthMultisigSmartConfig::new(approvers, 2);
401        assert!(
402            result
403                .unwrap_err()
404                .to_string()
405                .contains("duplicate approver public keys are not allowed")
406        );
407    }
408}