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///
130/// # Fees
131///
132/// Before authenticating, `auth_tx_multisig_smart` pays the transaction fee: it creates a public
133/// TX_FEE note (see [`TxFeeNote`](crate::note::TxFeeNote)) funded from the account's vault, so on
134/// fee-charging chains the account must hold a sufficient balance of the native fee asset. The
135/// conversion info is committed to via the transaction's auth args (see
136/// [`FeeConversionInfo`](crate::account::auth::FeeConversionInfo)). On chains with a zero
137/// verification base fee no note is created. The fee note is created before the transaction
138/// summary, so it is covered by the approver signatures.
139///
140/// The fee payment also prices every network output note through its target's fee policy, creating
141/// a FEE_SPONSORSHIP note for each one that charges. The pricing is a foreign-procedure call, so a
142/// transaction creating network notes requires those targets to be provisioned as foreign accounts
143/// even where the verification base fee is zero.
144///
145/// The notes the authentication procedure creates to pay the fee do not count against a
146/// [`ProcedurePolicyNoteRestriction`](super::ProcedurePolicyNoteRestriction), so
147/// [`NoOutputNotes`](super::ProcedurePolicyNoteRestriction::NoOutputNotes) stays satisfiable on a
148/// fee-charging chain — it follows that such a policy no longer implies that literally no note
149/// leaves the account. Because a per-procedure policy can authorize a transaction below the
150/// account's default threshold and the conversion rate is host-supplied, the component bounds the
151/// payment to the native fee asset at at most twice the computed fee
152/// (`fee::assert_fee_bound`), so such a transaction can at most overpay the fee through the fee
153/// note, not move arbitrary value out of the account.
154#[derive(Debug)]
155pub struct AuthMultisigSmart {
156    config: AuthMultisigSmartConfig,
157}
158
159impl AuthMultisigSmart {
160    /// The name of the component.
161    pub const NAME: &'static str = "miden::standards::auth::multisig_smart";
162
163    /// Returns the [`AccountComponentCode`] of this component.
164    pub fn code() -> &'static AccountComponentCode {
165        &MULTISIG_SMART_CODE
166    }
167
168    /// Creates a new [`AuthMultisigSmart`] component from the provided configuration.
169    pub fn new(config: AuthMultisigSmartConfig) -> Result<Self, AccountError> {
170        validate_proc_policies(config.approvers().len() as u32, config.procedure_policies())?;
171        Ok(Self { config })
172    }
173
174    pub fn threshold_config_slot() -> &'static StorageSlotName {
175        &THRESHOLD_CONFIG_SLOT_NAME
176    }
177
178    pub fn approver_public_keys_slot() -> &'static StorageSlotName {
179        &APPROVER_PUBKEYS_SLOT_NAME
180    }
181
182    pub fn approver_scheme_ids_slot() -> &'static StorageSlotName {
183        &APPROVER_SCHEME_ID_SLOT_NAME
184    }
185
186    pub fn executed_transactions_slot() -> &'static StorageSlotName {
187        &EXECUTED_TRANSACTIONS_SLOT_NAME
188    }
189
190    pub fn procedure_policies_slot() -> &'static StorageSlotName {
191        &PROCEDURE_POLICIES_SLOT_NAME
192    }
193
194    pub fn threshold_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
195        AuthMultisig::threshold_config_slot_schema()
196    }
197
198    pub fn approver_public_keys_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
199        AuthMultisig::approver_public_keys_slot_schema()
200    }
201
202    pub fn approver_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
203        AuthMultisig::approver_auth_scheme_slot_schema()
204    }
205
206    pub fn executed_transactions_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
207        AuthMultisig::executed_transactions_slot_schema()
208    }
209
210    pub fn procedure_policies_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
211        (
212            Self::procedure_policies_slot().clone(),
213            StorageSlotSchema::map(
214                "Procedure policies",
215                SchemaType::native_word(),
216                SchemaType::native_word(),
217            ),
218        )
219    }
220}
221
222impl From<AuthMultisigSmart> for AccountComponent {
223    fn from(multisig: AuthMultisigSmart) -> Self {
224        let mut storage_slots = Vec::with_capacity(5);
225
226        // Threshold config slot (value: [threshold, num_approvers, 0, 0])
227        let num_approvers = multisig.config.approvers().len() as u32;
228        storage_slots.push(StorageSlot::with_value(
229            AuthMultisigSmart::threshold_config_slot().clone(),
230            Word::from([multisig.config.default_threshold(), num_approvers, 0, 0]),
231        ));
232
233        // Approver public keys slot (map)
234        let map_entries = multisig.config.approvers().iter().enumerate().map(|(i, approver)| {
235            (StorageMapKey::from_index(i as u32), Word::from(approver.pub_key()))
236        });
237        storage_slots.push(StorageSlot::with_map(
238            AuthMultisigSmart::approver_public_keys_slot().clone(),
239            StorageMap::with_entries(map_entries).unwrap(),
240        ));
241
242        // Approver scheme IDs slot
243        let scheme_id_entries =
244            multisig.config.approvers().iter().enumerate().map(|(i, approver)| {
245                (
246                    StorageMapKey::from_index(i as u32),
247                    Word::from([approver.auth_scheme() as u32, 0, 0, 0]),
248                )
249            });
250        storage_slots.push(StorageSlot::with_map(
251            AuthMultisigSmart::approver_scheme_ids_slot().clone(),
252            StorageMap::with_entries(scheme_id_entries).unwrap(),
253        ));
254
255        // Executed transactions slot (map)
256        storage_slots.push(StorageSlot::with_map(
257            AuthMultisigSmart::executed_transactions_slot().clone(),
258            StorageMap::default(),
259        ));
260
261        // Procedure policies slot (map)
262        let procedure_policies =
263            StorageMap::with_entries(multisig.config.procedure_policies().iter().map(
264                |(proc_root, policy)| (StorageMapKey::from_raw(*proc_root), policy.to_word()),
265            ))
266            .unwrap();
267        storage_slots.push(StorageSlot::with_map(
268            AuthMultisigSmart::procedure_policies_slot().clone(),
269            procedure_policies,
270        ));
271
272        let storage_schema = StorageSchema::new(vec![
273            AuthMultisigSmart::threshold_config_slot_schema(),
274            AuthMultisigSmart::approver_public_keys_slot_schema(),
275            AuthMultisigSmart::approver_auth_scheme_slot_schema(),
276            AuthMultisigSmart::executed_transactions_slot_schema(),
277            AuthMultisigSmart::procedure_policies_slot_schema(),
278        ])
279        .expect("storage schema should be valid");
280
281        let metadata = AccountComponentMetadata::new(AuthMultisigSmart::NAME)
282            .with_description("Multisig smart authentication component")
283            .with_storage_schema(storage_schema);
284
285        AccountComponent::new(AuthMultisigSmart::code().clone(), storage_slots, metadata).expect(
286            "multisig smart component should satisfy the requirements of a valid account component",
287        )
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use alloc::string::ToString;
294
295    use miden_protocol::account::AccountBuilder;
296    use miden_protocol::account::auth::AuthSecretKey;
297
298    use super::*;
299    use crate::account::wallets::BasicWallet;
300
301    #[test]
302    fn test_multisig_smart_component_setup() {
303        let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak();
304        let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak();
305        let approvers = vec![
306            Approver::new(sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
307            Approver::new(sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
308        ];
309        let num_approvers = approvers.len() as u32;
310        let default_threshold = 2u32;
311        let receive_asset_immediate_threshold = 1u32;
312
313        let approver_set =
314            ApproverSet::new(approvers, default_threshold).expect("invalid approver set");
315        let config = AuthMultisigSmartConfig::new(approver_set)
316            .with_proc_policies(vec![(
317                BasicWallet::receive_asset_root().as_word(),
318                ProcedurePolicy::with_immediate_threshold(receive_asset_immediate_threshold)
319                    .expect("procedure policy should be valid"),
320            )])
321            .expect("procedure policy config should be valid");
322
323        let component =
324            AuthMultisigSmart::new(config).expect("multisig smart component creation failed");
325
326        let account = AccountBuilder::new([0; 32])
327            .with_component(component)
328            .with_component(BasicWallet)
329            .build()
330            .expect("account building failed");
331
332        let threshold_config = account
333            .storage()
334            .get_item(AuthMultisigSmart::threshold_config_slot())
335            .expect("threshold config should be present");
336        assert_eq!(threshold_config, Word::from([default_threshold, num_approvers, 0, 0]));
337
338        let receive_asset_policy = account
339            .storage()
340            .get_map_item(
341                AuthMultisigSmart::procedure_policies_slot(),
342                StorageMapKey::from_raw(BasicWallet::receive_asset_root().as_word()),
343            )
344            .expect("receive_asset policy should be present");
345        assert_eq!(
346            receive_asset_policy,
347            Word::from([receive_asset_immediate_threshold, 0u32, 0u32, 0u32])
348        );
349    }
350
351    #[test]
352    fn test_multisig_smart_component_rejects_duplicate_procedure_roots() {
353        let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak();
354        let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak();
355        let approvers = vec![
356            Approver::new(sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
357            Approver::new(sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
358        ];
359
360        let receive_asset_root = BasicWallet::receive_asset_root().as_word();
361        let policy_one =
362            ProcedurePolicy::with_immediate_threshold(1).expect("procedure policy should be valid");
363        let policy_two =
364            ProcedurePolicy::with_immediate_threshold(2).expect("procedure policy should be valid");
365
366        let approver_set = ApproverSet::new(approvers, 2).expect("invalid approver set");
367        let result = AuthMultisigSmartConfig::new(approver_set).with_proc_policies(vec![
368            (receive_asset_root, policy_one),
369            (receive_asset_root, policy_two),
370        ]);
371
372        assert!(
373            result
374                .unwrap_err()
375                .to_string()
376                .contains("duplicate procedure roots are not allowed in the procedure policy map")
377        );
378    }
379}