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