Skip to main content

miden_standards/account/auth/
multisig.rs

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