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