Skip to main content

miden_standards/account/auth/
multisig.rs

1use alloc::collections::BTreeMap;
2use alloc::vec::Vec;
3use core::num::NonZeroU32;
4
5use miden_protocol::account::component::{
6    AccountComponentCode,
7    AccountComponentMetadata,
8    FeltSchema,
9    SchemaType,
10    StorageSchema,
11    StorageSlotSchema,
12};
13use miden_protocol::account::{
14    AccountComponent,
15    AccountComponentName,
16    AccountProcedureRoot,
17    StorageMap,
18    StorageMapKey,
19    StorageSlot,
20    StorageSlotName,
21};
22use miden_protocol::block::BlockNumber;
23use miden_protocol::crypto::SequentialCommit;
24use miden_protocol::errors::AccountError;
25use miden_protocol::utils::sync::LazyLock;
26use miden_protocol::{EMPTY_WORD, Felt, WORD_SIZE, Word, ZERO};
27
28use super::{Approver, ApproverSet, FeeConversionInfo};
29use crate::account::account_component_code;
30use crate::procedure_root;
31
32account_component_code!(MULTISIG_CODE, "miden-standards-auth-multisig.masp");
33
34// PROCEDURE ROOTS
35// ================================================================================================
36
37/// MASL library namespace used for procedure-root lookups. Distinct from [`AuthMultisig::NAME`],
38/// which mirrors the standards-side MASM module path.
39const MULTISIG_LIBRARY_PATH: &str = "miden::standards::components::auth::multisig";
40
41// Initialize the procedure root of the `set_procedure_threshold` procedure only once. It gates
42// edits to per-procedure overrides, so [`AuthMultisig::new`] uses it to reject overrides that
43// exceed its own threshold.
44procedure_root!(
45    MULTISIG_SET_PROCEDURE_THRESHOLD,
46    MULTISIG_LIBRARY_PATH,
47    AuthMultisig::SET_PROCEDURE_THRESHOLD_PROC_NAME,
48    AuthMultisig::code()
49);
50
51// CONSTANTS
52// ================================================================================================
53
54pub(super) static THRESHOLD_CONFIG_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
55    StorageSlotName::new("miden::standards::auth::multisig::threshold_config")
56        .expect("storage slot name should be valid")
57});
58
59pub(super) static APPROVER_PUBKEYS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
60    StorageSlotName::new("miden::standards::auth::multisig::approver_public_keys")
61        .expect("storage slot name should be valid")
62});
63
64pub(super) static APPROVER_SCHEME_ID_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
65    StorageSlotName::new("miden::standards::auth::multisig::approver_schemes")
66        .expect("storage slot name should be valid")
67});
68
69pub(super) static EXECUTED_TRANSACTIONS_SLOT_NAME: LazyLock<StorageSlotName> =
70    LazyLock::new(|| {
71        StorageSlotName::new("miden::standards::auth::multisig::executed_transactions")
72            .expect("storage slot name should be valid")
73    });
74
75static PROCEDURE_THRESHOLDS_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
76    StorageSlotName::new("miden::standards::auth::multisig::procedure_thresholds")
77        .expect("storage slot name should be valid")
78});
79
80// MULTISIG AUTHENTICATION COMPONENT
81// ================================================================================================
82
83/// Configuration for [`AuthMultisig`] component.
84#[derive(Debug, Clone, PartialEq, Eq)]
85pub struct AuthMultisigConfig {
86    approver_set: ApproverSet,
87    proc_thresholds: BTreeMap<AccountProcedureRoot, u32>,
88}
89
90impl AuthMultisigConfig {
91    /// Creates a new configuration from the given approver set.
92    pub fn new(approver_set: ApproverSet) -> Self {
93        Self {
94            approver_set,
95            proc_thresholds: BTreeMap::new(),
96        }
97    }
98
99    /// Attaches a per-procedure threshold map. Each procedure threshold must be at least 1 and
100    /// at most the number of approvers.
101    pub fn with_proc_thresholds(
102        mut self,
103        proc_thresholds: Vec<(AccountProcedureRoot, u32)>,
104    ) -> Result<Self, AccountError> {
105        let num_approvers = self.approver_set.approvers().len() as u32;
106        let mut thresholds = BTreeMap::new();
107        for (proc_root, threshold) in proc_thresholds {
108            if threshold == 0 {
109                return Err(AccountError::other("procedure threshold must be at least 1"));
110            }
111            if threshold > num_approvers {
112                return Err(AccountError::other(
113                    "procedure threshold cannot be greater than number of approvers",
114                ));
115            }
116            // The map keys the threshold by procedure root, so a repeated root is a caller mistake
117            // rather than a silent overwrite.
118            if thresholds.insert(proc_root, threshold).is_some() {
119                return Err(AccountError::other(
120                    "duplicate procedure roots are not allowed in the procedure threshold map",
121                ));
122            }
123        }
124        self.proc_thresholds = thresholds;
125        Ok(self)
126    }
127
128    pub fn approver_set(&self) -> &ApproverSet {
129        &self.approver_set
130    }
131
132    pub fn approvers(&self) -> &[Approver] {
133        self.approver_set.approvers()
134    }
135
136    pub fn default_threshold(&self) -> u32 {
137        self.approver_set.threshold().get()
138    }
139
140    pub fn proc_thresholds(&self) -> &BTreeMap<AccountProcedureRoot, u32> {
141        &self.proc_thresholds
142    }
143}
144
145/// An [`AccountComponent`] implementing a multisig authentication.
146///
147/// It enforces a threshold of approver signatures for every transaction, with optional
148/// per-procedure threshold overrides.
149///
150/// # Auth args
151///
152/// The transaction's auth args are the commitment to [`MultisigAuthArgs`].
153///
154/// # Fees
155///
156/// Before authenticating, `auth_tx_multisig` pays the transaction fee via
157/// `miden::standards::fee::pay_fee`: it creates a public TX_FEE note (see
158/// [`TxFeeNote`](crate::note::TxFeeNote)) funded from the account's vault, so on
159/// fee-charging chains the account must hold a sufficient balance of the native fee asset. The
160/// conversion info from the auth args must name the reference block's fee asset at rate 1/1 (see
161/// [`FeeConversionInfo::one_to_one`](super::FeeConversionInfo::one_to_one)). On chains with a
162/// zero verification base fee no note is created. The fee note is created before the transaction
163/// summary, so it is covered by the approver signatures.
164///
165/// # Expiration
166///
167/// Two independent expirations apply, and the earlier one ends the transaction's validity.
168///
169/// The approval expiration defines how long the signature stays usable. It is set with
170/// [`MultisigAuthArgs::with_approval_expiration_delta`] and is measured from the block the
171/// summary binds, and is bound by the summary itself.
172///
173/// The transaction's own expiration delta is a freshness bound: a procedure that reads mutable
174/// foreign state through FPI caps how stale that read may be.
175///
176/// Neither is set by default: the signatures of an approval without an expiration stay usable for
177/// as long as the summary they cover can be reproduced.
178///
179/// # Privacy
180///
181/// Approvers using [`AuthScheme::EcdsaK256Keccak`][scheme] disclose their public key and signature
182/// at proving time and therefore do not get public-key privacy; approvers using
183/// [`Falcon512Poseidon2`][falcon] do. See [`Approver`](super::Approver) for details.
184///
185/// [scheme]: miden_protocol::account::auth::AuthScheme::EcdsaK256Keccak
186/// [falcon]: miden_protocol::account::auth::AuthScheme::Falcon512Poseidon2
187///
188/// # Security: private accounts and state withholding
189///
190/// A private account's state lives off-chain; the chain only holds a commitment to it. Whoever
191/// advances the account must share the new state with the other approvers, otherwise those
192/// approvers can no longer reconstruct the state behind the on-chain commitment and are
193/// permanently locked out (and the signers retaining the state can drain its assets). This is a
194/// data-availability problem inherent to private state, not an authorization one: the threshold
195/// controls who *can* advance the state, not whether the resulting state is *shared*. A
196/// per-procedure threshold of one lets a single approver do this; more generally, any quorum
197/// smaller than the full approver set can advance the state and withhold it from the excluded
198/// approvers.
199///
200/// The only configurations that fully prevent withholding are a public account (state is on-chain,
201/// so nothing can be withheld), unanimity (`threshold == number of approvers`, so every approver
202/// signs and therefore sees every state transition), or pairing the multisig with a guardian via
203/// [`AuthGuardedMultisig`](super::AuthGuardedMultisig), whose guardian co-signs every transaction
204/// and forwards the new state. For a private `m`-of-`n` wallet among mutually distrusting
205/// approvers, prefer the guarded variant. The [`create_multisig_wallet`] helper enforces a related
206/// bound: on private accounts it rejects per-procedure thresholds below the default.
207///
208/// [`create_multisig_wallet`]: crate::account::wallets::create_multisig_wallet
209///
210/// # Security: growing the signer set does not re-scale overrides
211///
212/// Per-procedure threshold overrides are absolute signature counts, not ratios. Updating the signer
213/// set (via the `update_signers_and_threshold` account procedure) does not re-scale existing
214/// overrides: the only cross-check is that each override stays `<= num_approvers`, which keeps it
215/// reachable but never raises it. Growing the approver set therefore silently lowers the effective
216/// signing ratio of every override (e.g. a `2`-of-`2` override becomes `2`-of-`n`). To preserve the
217/// intended security level, re-evaluate the affected overrides and, where appropriate, raise them
218/// via `set_procedure_threshold` in the same transaction that grows the signer set.
219///
220/// # Security: a raised override is only as strong as the threshold of `set_procedure_threshold`
221///
222/// An override can demand *more* signatures for a sensitive operation than the default, but that
223/// extra protection is only as strong as the threshold guarding the procedure that can lower it,
224/// `set_procedure_threshold`. That guard is `set_procedure_threshold`'s own override if one is set,
225/// otherwise the default threshold; it is *not* necessarily the default. A group meeting that guard
226/// can strip a stronger override in two transactions: first they lower it, then, in a later
227/// transaction, they run the now-cheaper operation. Two transactions are required because the
228/// signatures needed are read from the state as of the start of the transaction, so a lowered
229/// override only takes effect in the next one.
230///
231/// For example, with 5 signers, a default of 2, `set_procedure_threshold` left at the default, and
232/// a transfer requiring 4: two signers cannot transfer directly, but they can lower the transfer's
233/// override to 2 in one transaction and transfer in the next.
234///
235/// It follows that setting an override higher than the threshold of `set_procedure_threshold`
236/// (which may be the default) is pointless, because the excess signatures can always be removed by
237/// that smaller group. To make a raised override hold, raise `set_procedure_threshold`'s own
238/// threshold to at least that value, so undoing the protection costs as many signatures as the
239/// operation it guards. [`AuthMultisig::new`] enforces this by rejecting any configuration whose
240/// override exceeds the threshold of `set_procedure_threshold`. Note that
241/// `update_signers_and_threshold` can also weaken an override by growing the signer set (see
242/// above), so protect it the same way where relevant.
243#[derive(Debug)]
244pub struct AuthMultisig {
245    config: AuthMultisigConfig,
246}
247
248impl AuthMultisig {
249    /// The name of the component.
250    pub const NAME: &'static str = "miden::standards::auth::multisig";
251
252    /// The name of the procedure that edits per-procedure threshold overrides.
253    const SET_PROCEDURE_THRESHOLD_PROC_NAME: &'static str = "set_procedure_threshold";
254
255    /// Returns the canonical [`AccountComponentName`] of this component.
256    pub const fn name() -> AccountComponentName {
257        AccountComponentName::from_static_str(Self::NAME)
258    }
259
260    /// Returns the [`AccountComponentCode`] of this component.
261    pub fn code() -> &'static AccountComponentCode {
262        &MULTISIG_CODE
263    }
264
265    /// Returns the procedure root of the `set_procedure_threshold` account procedure.
266    pub fn set_procedure_threshold_root() -> AccountProcedureRoot {
267        *MULTISIG_SET_PROCEDURE_THRESHOLD
268    }
269
270    /// Creates a new [`AuthMultisig`] component from the provided configuration.
271    ///
272    /// # Errors
273    ///
274    /// Returns an error if a per-procedure override exceeds the threshold that guards
275    /// `set_procedure_threshold` (its own override if set, otherwise the default threshold). Such
276    /// an override is not enforceable, since a group meeting that lower threshold can strip it
277    /// via `set_procedure_threshold`; see the type-level security notes.
278    pub fn new(config: AuthMultisigConfig) -> Result<Self, AccountError> {
279        // The threshold that must be met to edit overrides via `set_procedure_threshold`: its own
280        // override if configured, otherwise the default threshold.
281        let setter_threshold = config
282            .proc_thresholds()
283            .get(&Self::set_procedure_threshold_root())
284            .copied()
285            .unwrap_or_else(|| config.default_threshold());
286
287        for &threshold in config.proc_thresholds().values() {
288            if threshold > setter_threshold {
289                return Err(AccountError::other(format!(
290                    "per-procedure threshold override of {threshold} exceeds the threshold of \
291                     {setter_threshold} that guards set_procedure_threshold; such an override can \
292                     be removed by a smaller quorum. Raise the set_procedure_threshold override to \
293                     at least {threshold} to make it enforceable"
294                )));
295            }
296        }
297
298        Ok(Self { config })
299    }
300
301    /// Returns the [`StorageSlotName`] where the threshold configuration is stored.
302    pub fn threshold_config_slot() -> &'static StorageSlotName {
303        &THRESHOLD_CONFIG_SLOT_NAME
304    }
305
306    /// Returns the [`StorageSlotName`] where the approver public keys are stored.
307    pub fn approver_public_keys_slot() -> &'static StorageSlotName {
308        &APPROVER_PUBKEYS_SLOT_NAME
309    }
310
311    // Returns the [`StorageSlotName`] where the approver scheme IDs are stored.
312    pub fn approver_scheme_ids_slot() -> &'static StorageSlotName {
313        &APPROVER_SCHEME_ID_SLOT_NAME
314    }
315
316    /// Returns the [`StorageSlotName`] where the executed transactions are stored.
317    pub fn executed_transactions_slot() -> &'static StorageSlotName {
318        &EXECUTED_TRANSACTIONS_SLOT_NAME
319    }
320
321    /// Returns the [`StorageSlotName`] where the procedure thresholds are stored.
322    pub fn procedure_thresholds_slot() -> &'static StorageSlotName {
323        &PROCEDURE_THRESHOLDS_SLOT_NAME
324    }
325
326    /// Returns the storage slot schema for the threshold configuration slot.
327    pub fn threshold_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
328        (
329            Self::threshold_config_slot().clone(),
330            StorageSlotSchema::value(
331                "Threshold configuration",
332                [
333                    FeltSchema::u32("threshold"),
334                    FeltSchema::u32("num_approvers"),
335                    FeltSchema::new_void(),
336                    FeltSchema::new_void(),
337                ],
338            ),
339        )
340    }
341
342    /// Returns the storage slot schema for the approver public keys slot.
343    pub fn approver_public_keys_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
344        (
345            Self::approver_public_keys_slot().clone(),
346            StorageSlotSchema::map(
347                "Approver public keys",
348                SchemaType::u32(),
349                SchemaType::pub_key(),
350            ),
351        )
352    }
353
354    // Returns the storage slot schema for the approver scheme IDs slot.
355    pub fn approver_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
356        (
357            Self::approver_scheme_ids_slot().clone(),
358            StorageSlotSchema::map(
359                "Approver scheme IDs",
360                SchemaType::u32(),
361                SchemaType::auth_scheme(),
362            ),
363        )
364    }
365
366    /// Returns the storage slot schema for the executed transactions slot.
367    pub fn executed_transactions_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
368        (
369            Self::executed_transactions_slot().clone(),
370            StorageSlotSchema::map(
371                "Executed transactions",
372                SchemaType::native_word(),
373                SchemaType::native_word(),
374            ),
375        )
376    }
377
378    /// Returns the storage slot schema for the procedure thresholds slot.
379    pub fn procedure_thresholds_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
380        (
381            Self::procedure_thresholds_slot().clone(),
382            StorageSlotSchema::map(
383                "Procedure thresholds",
384                SchemaType::native_word(),
385                SchemaType::u32(),
386            ),
387        )
388    }
389
390    /// Returns the [`AccountComponentMetadata`] for this component.
391    pub fn component_metadata() -> AccountComponentMetadata {
392        let storage_schema = StorageSchema::new([
393            Self::threshold_config_slot_schema(),
394            Self::approver_public_keys_slot_schema(),
395            Self::approver_auth_scheme_slot_schema(),
396            Self::executed_transactions_slot_schema(),
397            Self::procedure_thresholds_slot_schema(),
398        ])
399        .expect("storage schema should be valid");
400
401        AccountComponentMetadata::new(Self::NAME)
402            .with_description("Multisig authentication component using hybrid signature schemes")
403            .with_storage_schema(storage_schema)
404    }
405}
406
407impl From<AuthMultisig> for AccountComponent {
408    fn from(multisig: AuthMultisig) -> Self {
409        let mut storage_slots = Vec::with_capacity(5);
410
411        // Threshold config slot (value: [threshold, num_approvers, 0, 0])
412        let num_approvers = multisig.config.approvers().len() as u32;
413        storage_slots.push(StorageSlot::with_value(
414            AuthMultisig::threshold_config_slot().clone(),
415            Word::from([multisig.config.default_threshold(), num_approvers, 0, 0]),
416        ));
417
418        // Approver public keys slot (map)
419        let map_entries = multisig.config.approvers().iter().enumerate().map(|(i, approver)| {
420            (StorageMapKey::from_index(i as u32), Word::from(approver.pub_key()))
421        });
422
423        // Safe to unwrap because we know that the map keys are unique.
424        storage_slots.push(StorageSlot::with_map(
425            AuthMultisig::approver_public_keys_slot().clone(),
426            StorageMap::with_entries(map_entries).unwrap(),
427        ));
428
429        // Approver scheme IDs slot (map): [index, 0, 0, 0] => [scheme_id, 0, 0, 0]
430        let scheme_id_entries =
431            multisig.config.approvers().iter().enumerate().map(|(i, approver)| {
432                (
433                    StorageMapKey::from_index(i as u32),
434                    Word::from([approver.auth_scheme() as u32, 0, 0, 0]),
435                )
436            });
437
438        storage_slots.push(StorageSlot::with_map(
439            AuthMultisig::approver_scheme_ids_slot().clone(),
440            StorageMap::with_entries(scheme_id_entries).unwrap(),
441        ));
442
443        // Executed transactions slot (map)
444        let executed_transactions = StorageMap::default();
445        storage_slots.push(StorageSlot::with_map(
446            AuthMultisig::executed_transactions_slot().clone(),
447            executed_transactions,
448        ));
449
450        // Procedure thresholds slot (map: PROC_ROOT -> threshold)
451        let proc_threshold_roots = StorageMap::with_entries(
452            multisig.config.proc_thresholds().iter().map(|(proc_root, threshold)| {
453                (StorageMapKey::from_raw(proc_root.as_word()), Word::from([*threshold, 0, 0, 0]))
454            }),
455        )
456        .unwrap();
457        storage_slots.push(StorageSlot::with_map(
458            AuthMultisig::procedure_thresholds_slot().clone(),
459            proc_threshold_roots,
460        ));
461
462        let metadata = AuthMultisig::component_metadata();
463
464        AccountComponent::new(AuthMultisig::code().clone(), storage_slots, metadata).expect(
465            "Multisig auth component should satisfy the requirements of a valid account component",
466        )
467    }
468}
469
470// MULTISIG AUTH ARGS
471// ================================================================================================
472
473/// The inputs the multisig authentication components receive through the transaction's auth args.
474///
475/// ```text
476/// AUTH_ARGS: [BLOCK_WORD, SALT, CONVERSION_INFO]
477/// ```
478///
479/// where `BLOCK_WORD` is `[bound_block_num, approval_expiration_block_num, 0, 0]`.
480#[derive(Debug, Clone, Copy, PartialEq, Eq)]
481pub struct MultisigAuthArgs {
482    bound_block_num: BlockNumber,
483    approval_expiration_block_num: Option<BlockNumber>,
484    salt: Word,
485    conversion_info: Option<FeeConversionInfo>,
486}
487
488impl MultisigAuthArgs {
489    /// Creates new multisig auth args binding the summary to the given block.
490    ///
491    /// The signers approve a transaction summary that commits to `bound_block_num`, so the party
492    /// executing the transaction must pass the same block number, no matter how far the chain has
493    /// advanced since. The block must be at or before the transaction's reference block and must
494    /// be tracked by the transaction's partial blockchain, since that is the only way the kernel
495    /// can read its commitment.
496    ///
497    /// The approval does not expire unless [`Self::with_approval_expiration_delta`] sets an
498    /// expiration.
499    ///
500    /// `salt` is bound by the transaction summary and is what makes otherwise identical
501    /// transactions distinguishable, which is what the replay protection of the multisig
502    /// components relies on. It should be chosen at random.
503    pub fn new(bound_block_num: BlockNumber, salt: Word) -> Self {
504        Self {
505            bound_block_num,
506            approval_expiration_block_num: None,
507            salt,
508            conversion_info: None,
509        }
510    }
511
512    /// Returns new multisig auth args whose approval expires `delta` blocks after the bound block.
513    ///
514    /// The transaction must be included by block `bound_block_num + delta`. The expiration is bound
515    /// by the transaction summary, so the party executing the transaction can neither shorten nor
516    /// extend it.
517    ///
518    /// # Errors
519    ///
520    /// Returns an error if `bound_block_num + delta` exceeds [`BlockNumber::MAX`].
521    pub fn with_approval_expiration_delta(
522        mut self,
523        delta: NonZeroU32,
524    ) -> Result<Self, AccountError> {
525        let expiration_block_num =
526            self.bound_block_num.as_u32().checked_add(delta.get()).ok_or_else(|| {
527                AccountError::other(
528                    "approval expiration block number exceeds the maximum block number",
529                )
530            })?;
531
532        self.approval_expiration_block_num = Some(BlockNumber::from(expiration_block_num));
533        Ok(self)
534    }
535
536    /// Returns new multisig auth args carrying the conversion info the fee payment needs.
537    ///
538    /// Must be [`FeeConversionInfo::one_to_one`] built with the reference block's fee faucet.
539    /// Anything else, or no conversion info at all, aborts on chains that charge a non-zero
540    /// verification base fee.
541    #[must_use]
542    pub fn with_conversion_info(mut self, conversion_info: FeeConversionInfo) -> Self {
543        self.conversion_info = Some(conversion_info);
544        self
545    }
546
547    // PUBLIC ACCESSORS
548    // --------------------------------------------------------------------------------------------
549
550    /// Returns the number of the block the transaction summary binds.
551    pub fn bound_block_num(&self) -> BlockNumber {
552        self.bound_block_num
553    }
554
555    /// Returns the first reference block at which the approvers' signatures are no longer valid,
556    /// or `None` if the approval does not expire.
557    pub fn approval_expiration_block_num(&self) -> Option<BlockNumber> {
558        self.approval_expiration_block_num
559    }
560
561    /// Returns the salt bound by the transaction summary.
562    pub fn salt(&self) -> Word {
563        self.salt
564    }
565
566    /// Returns the fee conversion info, or `None` if none was committed - in which case the fee
567    /// payment aborts on fee-charging chains.
568    pub fn conversion_info(&self) -> Option<FeeConversionInfo> {
569        self.conversion_info
570    }
571}
572
573impl SequentialCommit for MultisigAuthArgs {
574    type Commitment = Word;
575
576    fn to_elements(&self) -> Vec<Felt> {
577        let conversion_info = self.conversion_info.map_or(EMPTY_WORD, |info| info.to_word());
578        let approval_expiration = self.approval_expiration_block_num.map_or(Felt::ZERO, Felt::from);
579
580        let mut elements = Vec::with_capacity(3 * WORD_SIZE);
581        elements.extend([Felt::from(self.bound_block_num), approval_expiration, ZERO, ZERO]);
582        elements.extend(self.salt.iter());
583        elements.extend(conversion_info.iter());
584        elements
585    }
586}
587
588// TESTS
589// ================================================================================================
590
591#[cfg(test)]
592mod tests {
593    use alloc::string::ToString;
594
595    use miden_protocol::account::auth::AuthSecretKey;
596    use miden_protocol::account::{AccountBuilder, auth};
597
598    use super::*;
599    use crate::account::wallets::BasicWallet;
600
601    /// Test multisig component setup with various configurations
602    #[test]
603    fn test_multisig_component_setup() {
604        // Create test secret keys
605        let sec_key_1 = AuthSecretKey::new_falcon512_poseidon2();
606        let sec_key_2 = AuthSecretKey::new_falcon512_poseidon2();
607        let sec_key_3 = AuthSecretKey::new_falcon512_poseidon2();
608
609        // Create approvers list for multisig config
610        let approvers = vec![
611            Approver::new(sec_key_1.public_key().to_commitment(), sec_key_1.auth_scheme()),
612            Approver::new(sec_key_2.public_key().to_commitment(), sec_key_2.auth_scheme()),
613            Approver::new(sec_key_3.public_key().to_commitment(), sec_key_3.auth_scheme()),
614        ];
615
616        let threshold = 2u32;
617
618        // Create multisig component
619        let approver_set =
620            ApproverSet::new(approvers.clone(), threshold).expect("invalid approver set");
621        let multisig_component = AuthMultisig::new(AuthMultisigConfig::new(approver_set))
622            .expect("multisig component creation failed");
623
624        // Build account with multisig component
625        let account = AccountBuilder::new([0; 32])
626            .with_component(multisig_component)
627            .with_component(BasicWallet)
628            .build()
629            .expect("account building failed");
630
631        // Verify config slot: [threshold, num_approvers, 0, 0]
632        let config_slot = account
633            .storage()
634            .get_item(AuthMultisig::threshold_config_slot())
635            .expect("config storage slot access failed");
636        assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
637
638        // Verify approver pub keys slot
639        for (i, approver) in approvers.iter().enumerate() {
640            let stored_pub_key = account
641                .storage()
642                .get_map_item(
643                    AuthMultisig::approver_public_keys_slot(),
644                    StorageMapKey::from_index(i as u32),
645                )
646                .expect("approver public key storage map access failed");
647            assert_eq!(stored_pub_key, Word::from(approver.pub_key()));
648        }
649
650        // Verify approver scheme IDs slot
651        for (i, approver) in approvers.iter().enumerate() {
652            let stored_scheme_id = account
653                .storage()
654                .get_map_item(
655                    AuthMultisig::approver_scheme_ids_slot(),
656                    StorageMapKey::from_index(i as u32),
657                )
658                .expect("approver scheme ID storage map access failed");
659            assert_eq!(stored_scheme_id, Word::from([approver.auth_scheme() as u32, 0, 0, 0]));
660        }
661    }
662
663    /// Test multisig component with minimum threshold (1 of 1)
664    #[test]
665    fn test_multisig_component_minimum_threshold() {
666        let pub_key = AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment();
667        let approvers = vec![Approver::new(pub_key, auth::AuthScheme::EcdsaK256Keccak)];
668        let threshold = 1u32;
669
670        let approver_set =
671            ApproverSet::new(approvers.clone(), threshold).expect("invalid approver set");
672        let multisig_component = AuthMultisig::new(AuthMultisigConfig::new(approver_set))
673            .expect("multisig component creation failed");
674
675        let account = AccountBuilder::new([0; 32])
676            .with_component(multisig_component)
677            .with_component(BasicWallet)
678            .build()
679            .expect("account building failed");
680
681        // Verify storage layout
682        let config_slot = account
683            .storage()
684            .get_item(AuthMultisig::threshold_config_slot())
685            .expect("config storage slot access failed");
686        assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
687
688        let stored_pub_key = account
689            .storage()
690            .get_map_item(AuthMultisig::approver_public_keys_slot(), StorageMapKey::from_index(0))
691            .expect("approver pub keys storage map access failed");
692        assert_eq!(stored_pub_key, Word::from(pub_key));
693
694        let stored_scheme_id = account
695            .storage()
696            .get_map_item(AuthMultisig::approver_scheme_ids_slot(), StorageMapKey::from_index(0))
697            .expect("approver scheme IDs storage map access failed");
698        assert_eq!(
699            stored_scheme_id,
700            Word::from([auth::AuthScheme::EcdsaK256Keccak as u32, 0, 0, 0])
701        );
702    }
703
704    /// Test that a per-procedure threshold exceeding the number of approvers is rejected.
705    #[test]
706    fn test_proc_threshold_too_high() {
707        let pub_key = AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment();
708        let approvers = vec![Approver::new(pub_key, auth::AuthScheme::EcdsaK256Keccak)];
709        let approver_set = ApproverSet::new(approvers, 1).expect("invalid approver set");
710
711        let result = AuthMultisigConfig::new(approver_set)
712            .with_proc_thresholds(vec![(BasicWallet::receive_asset_root(), 2)]);
713        assert!(
714            result
715                .unwrap_err()
716                .to_string()
717                .contains("procedure threshold cannot be greater than number of approvers")
718        );
719    }
720
721    /// Test that an override exceeding the threshold guarding `set_procedure_threshold` (here the
722    /// default, since it has no override of its own) is rejected by `AuthMultisig::new`, because a
723    /// smaller quorum could lower it.
724    #[test]
725    fn test_proc_threshold_above_set_procedure_threshold_rejected() {
726        let approvers = vec![
727            Approver::new(
728                AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment(),
729                auth::AuthScheme::EcdsaK256Keccak,
730            ),
731            Approver::new(
732                AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment(),
733                auth::AuthScheme::EcdsaK256Keccak,
734            ),
735            Approver::new(
736                AuthSecretKey::new_ecdsa_k256_keccak().public_key().to_commitment(),
737                auth::AuthScheme::EcdsaK256Keccak,
738            ),
739        ];
740        let approver_set = ApproverSet::new(approvers, 2).expect("invalid approver set");
741
742        // The override (3) is within num_approvers, so `with_proc_thresholds` accepts it, but it
743        // exceeds the default threshold (2) that guards `set_procedure_threshold`.
744        let config = AuthMultisigConfig::new(approver_set)
745            .with_proc_thresholds(vec![(BasicWallet::receive_asset_root(), 3)])
746            .expect("an override within num_approvers is accepted by with_proc_thresholds");
747
748        let err = AuthMultisig::new(config).unwrap_err();
749        assert!(err.to_string().contains("exceeds the threshold"));
750    }
751}