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///
151/// # Security
152///
153/// A [`ProcedurePolicy`] threshold below the default acts like a lowered per-procedure override of
154/// [`AuthMultisig`], so a procedure with such a policy should seal the notes it creates for the
155/// reasons described there.
156#[derive(Debug)]
157pub struct AuthMultisigSmart {
158    config: AuthMultisigSmartConfig,
159}
160
161impl AuthMultisigSmart {
162    /// The name of the component.
163    pub const NAME: &'static str = "miden::standards::auth::multisig_smart";
164
165    /// The name of the procedure that edits per-procedure policies.
166    const SET_PROCEDURE_POLICY_PROC_NAME: &'static str = "set_procedure_policy";
167
168    /// Returns the [`AccountComponentCode`] of this component.
169    pub fn code() -> &'static AccountComponentCode {
170        &MULTISIG_SMART_CODE
171    }
172
173    /// Returns the procedure root of the `set_procedure_policy` account procedure.
174    pub fn set_procedure_policy_root() -> AccountProcedureRoot {
175        *MULTISIG_SMART_SET_PROCEDURE_POLICY
176    }
177
178    /// Creates a new [`AuthMultisigSmart`] component from the provided configuration.
179    pub fn new(config: AuthMultisigSmartConfig) -> Result<Self, AccountError> {
180        validate_proc_policies(config.approvers().len() as u32, config.procedure_policies())?;
181        Ok(Self { config })
182    }
183
184    pub fn threshold_config_slot() -> &'static StorageSlotName {
185        &THRESHOLD_CONFIG_SLOT_NAME
186    }
187
188    pub fn approver_public_keys_slot() -> &'static StorageSlotName {
189        &APPROVER_PUBKEYS_SLOT_NAME
190    }
191
192    pub fn approver_scheme_ids_slot() -> &'static StorageSlotName {
193        &APPROVER_SCHEME_ID_SLOT_NAME
194    }
195
196    pub fn executed_transactions_slot() -> &'static StorageSlotName {
197        &EXECUTED_TRANSACTIONS_SLOT_NAME
198    }
199
200    pub fn procedure_policies_slot() -> &'static StorageSlotName {
201        &PROCEDURE_POLICIES_SLOT_NAME
202    }
203
204    pub fn threshold_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
205        AuthMultisig::threshold_config_slot_schema()
206    }
207
208    pub fn approver_public_keys_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
209        AuthMultisig::approver_public_keys_slot_schema()
210    }
211
212    pub fn approver_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
213        AuthMultisig::approver_auth_scheme_slot_schema()
214    }
215
216    pub fn executed_transactions_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
217        AuthMultisig::executed_transactions_slot_schema()
218    }
219
220    pub fn procedure_policies_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
221        (
222            Self::procedure_policies_slot().clone(),
223            StorageSlotSchema::map(
224                "Procedure policies",
225                SchemaType::native_word(),
226                SchemaType::native_word(),
227            ),
228        )
229    }
230}
231
232impl From<AuthMultisigSmart> for AccountComponent {
233    fn from(multisig: AuthMultisigSmart) -> Self {
234        let mut storage_slots = Vec::with_capacity(5);
235
236        // Threshold config slot (value: [threshold, num_approvers, 0, 0])
237        let num_approvers = multisig.config.approvers().len() as u32;
238        storage_slots.push(StorageSlot::with_value(
239            AuthMultisigSmart::threshold_config_slot().clone(),
240            Word::from([multisig.config.default_threshold(), num_approvers, 0, 0]),
241        ));
242
243        // Approver public keys slot (map)
244        let map_entries = multisig.config.approvers().iter().enumerate().map(|(i, approver)| {
245            (StorageMapKey::from_index(i as u32), Word::from(approver.pub_key()))
246        });
247        storage_slots.push(StorageSlot::with_map(
248            AuthMultisigSmart::approver_public_keys_slot().clone(),
249            StorageMap::with_entries(map_entries).unwrap(),
250        ));
251
252        // Approver scheme IDs slot
253        let scheme_id_entries =
254            multisig.config.approvers().iter().enumerate().map(|(i, approver)| {
255                (
256                    StorageMapKey::from_index(i as u32),
257                    Word::from([approver.auth_scheme() as u32, 0, 0, 0]),
258                )
259            });
260        storage_slots.push(StorageSlot::with_map(
261            AuthMultisigSmart::approver_scheme_ids_slot().clone(),
262            StorageMap::with_entries(scheme_id_entries).unwrap(),
263        ));
264
265        // Executed transactions slot (map)
266        storage_slots.push(StorageSlot::with_map(
267            AuthMultisigSmart::executed_transactions_slot().clone(),
268            StorageMap::default(),
269        ));
270
271        // Procedure policies slot (map)
272        let procedure_policies =
273            StorageMap::with_entries(multisig.config.procedure_policies().iter().map(
274                |(proc_root, policy)| (StorageMapKey::from_raw(*proc_root), policy.to_word()),
275            ))
276            .unwrap();
277        storage_slots.push(StorageSlot::with_map(
278            AuthMultisigSmart::procedure_policies_slot().clone(),
279            procedure_policies,
280        ));
281
282        let storage_schema = StorageSchema::new(vec![
283            AuthMultisigSmart::threshold_config_slot_schema(),
284            AuthMultisigSmart::approver_public_keys_slot_schema(),
285            AuthMultisigSmart::approver_auth_scheme_slot_schema(),
286            AuthMultisigSmart::executed_transactions_slot_schema(),
287            AuthMultisigSmart::procedure_policies_slot_schema(),
288        ])
289        .expect("storage schema should be valid");
290
291        let metadata = AccountComponentMetadata::new(AuthMultisigSmart::NAME)
292            .with_description("Multisig smart authentication component")
293            .with_storage_schema(storage_schema);
294
295        AccountComponent::new(AuthMultisigSmart::code().clone(), storage_slots, metadata).expect(
296            "multisig smart component should satisfy the requirements of a valid account component",
297        )
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use alloc::string::ToString;
304
305    use miden_protocol::account::AccountBuilder;
306    use miden_protocol::account::auth::AuthSecretKey;
307
308    use super::*;
309    use crate::account::wallets::BasicWallet;
310
311    #[test]
312    fn test_multisig_smart_component_setup() {
313        let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak();
314        let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak();
315        let approvers = vec![
316            Approver::new(sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
317            Approver::new(sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
318        ];
319        let num_approvers = approvers.len() as u32;
320        let default_threshold = 2u32;
321        let receive_asset_immediate_threshold = 1u32;
322
323        let approver_set =
324            ApproverSet::new(approvers, default_threshold).expect("invalid approver set");
325        let config = AuthMultisigSmartConfig::new(approver_set)
326            .with_proc_policies(vec![(
327                BasicWallet::receive_asset_root().as_word(),
328                ProcedurePolicy::with_immediate_threshold(receive_asset_immediate_threshold)
329                    .expect("procedure policy should be valid"),
330            )])
331            .expect("procedure policy config should be valid");
332
333        let component =
334            AuthMultisigSmart::new(config).expect("multisig smart component creation failed");
335
336        let account = AccountBuilder::new([0; 32])
337            .with_component(component)
338            .with_component(BasicWallet)
339            .build()
340            .expect("account building failed");
341
342        let threshold_config = account
343            .storage()
344            .get_item(AuthMultisigSmart::threshold_config_slot())
345            .expect("threshold config should be present");
346        assert_eq!(threshold_config, Word::from([default_threshold, num_approvers, 0, 0]));
347
348        let receive_asset_policy = account
349            .storage()
350            .get_map_item(
351                AuthMultisigSmart::procedure_policies_slot(),
352                StorageMapKey::from_raw(BasicWallet::receive_asset_root().as_word()),
353            )
354            .expect("receive_asset policy should be present");
355        assert_eq!(
356            receive_asset_policy,
357            Word::from([receive_asset_immediate_threshold, 0u32, 0u32, 0u32])
358        );
359    }
360
361    #[test]
362    fn test_multisig_smart_component_rejects_duplicate_procedure_roots() {
363        let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak();
364        let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak();
365        let approvers = vec![
366            Approver::new(sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
367            Approver::new(sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
368        ];
369
370        let receive_asset_root = BasicWallet::receive_asset_root().as_word();
371        let policy_one =
372            ProcedurePolicy::with_immediate_threshold(1).expect("procedure policy should be valid");
373        let policy_two =
374            ProcedurePolicy::with_immediate_threshold(2).expect("procedure policy should be valid");
375
376        let approver_set = ApproverSet::new(approvers, 2).expect("invalid approver set");
377        let result = AuthMultisigSmartConfig::new(approver_set).with_proc_policies(vec![
378            (receive_asset_root, policy_one),
379            (receive_asset_root, policy_two),
380        ]);
381
382        assert!(
383            result
384                .unwrap_err()
385                .to_string()
386                .contains("duplicate procedure roots are not allowed in the procedure policy map")
387        );
388    }
389}