Skip to main content

miden_standards/account/auth/
guarded_multisig.rs

1use alloc::collections::BTreeMap;
2use alloc::vec::Vec;
3
4use miden_protocol::Word;
5use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment};
6use miden_protocol::account::component::{
7    AccountComponentCode,
8    AccountComponentMetadata,
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::multisig::{AuthMultisig, AuthMultisigConfig};
26use super::{Approver, ApproverSet};
27use crate::account::account_component_code;
28
29account_component_code!(GUARDED_MULTISIG_CODE, "miden-standards-auth-guarded-multisig.masp");
30
31// CONSTANTS
32// ================================================================================================
33
34static GUARDIAN_PUBKEY_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
35    StorageSlotName::new("miden::standards::auth::guardian::pub_key")
36        .expect("storage slot name should be valid")
37});
38
39static GUARDIAN_SCHEME_ID_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
40    StorageSlotName::new("miden::standards::auth::guardian::scheme")
41        .expect("storage slot name should be valid")
42});
43
44// MULTISIG AUTHENTICATION COMPONENT
45// ================================================================================================
46
47/// Configuration for [`AuthGuardedMultisig`] component.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct AuthGuardedMultisigConfig {
50    multisig: AuthMultisigConfig,
51    guardian_config: GuardianConfig,
52}
53
54/// Public configuration for the guardian signer.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct GuardianConfig {
57    approver: Approver,
58}
59
60impl GuardianConfig {
61    pub fn new(approver: Approver) -> Self {
62        Self { approver }
63    }
64
65    pub fn approver(&self) -> Approver {
66        self.approver
67    }
68
69    pub fn pub_key(&self) -> PublicKeyCommitment {
70        self.approver.pub_key()
71    }
72
73    pub fn auth_scheme(&self) -> AuthScheme {
74        self.approver.auth_scheme()
75    }
76
77    fn public_key_slot() -> &'static StorageSlotName {
78        &GUARDIAN_PUBKEY_SLOT_NAME
79    }
80
81    fn scheme_id_slot() -> &'static StorageSlotName {
82        &GUARDIAN_SCHEME_ID_SLOT_NAME
83    }
84
85    fn public_key_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
86        (
87            Self::public_key_slot().clone(),
88            StorageSlotSchema::map(
89                "Guardian public keys",
90                SchemaType::u32(),
91                SchemaType::pub_key(),
92            ),
93        )
94    }
95
96    fn auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
97        (
98            Self::scheme_id_slot().clone(),
99            StorageSlotSchema::map(
100                "Guardian scheme IDs",
101                SchemaType::u32(),
102                SchemaType::auth_scheme(),
103            ),
104        )
105    }
106
107    fn into_component_parts(self) -> (Vec<StorageSlot>, Vec<(StorageSlotName, StorageSlotSchema)>) {
108        let mut storage_slots = Vec::with_capacity(2);
109
110        // Guardian public key slot (map: [0, 0, 0, 0] -> pubkey)
111        let guardian_public_key_entries =
112            [(StorageMapKey::from_raw(Word::from([0u32, 0, 0, 0])), Word::from(self.pub_key()))];
113        storage_slots.push(StorageSlot::with_map(
114            Self::public_key_slot().clone(),
115            StorageMap::with_entries(guardian_public_key_entries).unwrap(),
116        ));
117
118        // Guardian scheme IDs slot (map: [0, 0, 0, 0] -> [scheme_id, 0, 0, 0])
119        let guardian_scheme_id_entries = [(
120            StorageMapKey::from_raw(Word::from([0u32, 0, 0, 0])),
121            Word::from([self.auth_scheme() as u32, 0, 0, 0]),
122        )];
123        storage_slots.push(StorageSlot::with_map(
124            Self::scheme_id_slot().clone(),
125            StorageMap::with_entries(guardian_scheme_id_entries).unwrap(),
126        ));
127
128        let slot_metadata = vec![Self::public_key_slot_schema(), Self::auth_scheme_slot_schema()];
129
130        (storage_slots, slot_metadata)
131    }
132}
133
134impl AuthGuardedMultisigConfig {
135    /// Creates a new configuration with the given approver set and guardian signer.
136    ///
137    /// The guardian public key must be different from all approver public keys.
138    pub fn new(
139        approver_set: ApproverSet,
140        guardian_config: GuardianConfig,
141    ) -> Result<Self, AccountError> {
142        if approver_set
143            .approvers()
144            .iter()
145            .any(|approver| approver.pub_key() == guardian_config.pub_key())
146        {
147            return Err(AccountError::other(
148                "guardian public key must be different from approvers",
149            ));
150        }
151
152        Ok(Self {
153            multisig: AuthMultisigConfig::new(approver_set),
154            guardian_config,
155        })
156    }
157
158    /// Attaches a per-procedure threshold map. Each procedure threshold must be at least 1 and
159    /// at most the number of approvers.
160    pub fn with_proc_thresholds(
161        mut self,
162        proc_thresholds: Vec<(AccountProcedureRoot, u32)>,
163    ) -> Result<Self, AccountError> {
164        self.multisig = self.multisig.with_proc_thresholds(proc_thresholds)?;
165        Ok(self)
166    }
167
168    pub fn approver_set(&self) -> &ApproverSet {
169        self.multisig.approver_set()
170    }
171
172    pub fn approvers(&self) -> &[Approver] {
173        self.multisig.approvers()
174    }
175
176    pub fn default_threshold(&self) -> u32 {
177        self.multisig.default_threshold()
178    }
179
180    pub fn proc_thresholds(&self) -> &BTreeMap<AccountProcedureRoot, u32> {
181        self.multisig.proc_thresholds()
182    }
183
184    pub fn guardian_config(&self) -> GuardianConfig {
185        self.guardian_config
186    }
187
188    fn into_parts(self) -> (AuthMultisigConfig, GuardianConfig) {
189        (self.multisig, self.guardian_config)
190    }
191}
192
193/// An [`AccountComponent`] implementing multisig authentication integrated with a state guardian.
194///
195/// It enforces a threshold of approver signatures for every transaction, with optional
196/// per-procedure threshold overrides. When a guardian is configured, multisig authorization is
197/// combined with guardian authorization, so operations require both multisig approval and a valid
198/// guardian signature. This substantially mitigates low-threshold state-withholding scenarios
199/// since the guardian is expected to forward state updates to other approvers.
200///
201/// # Fees
202///
203/// Before authenticating, `auth_tx_guarded_multisig` pays the transaction fee: it creates a public
204/// TX_FEE note (see [`TxFeeNote`](crate::note::TxFeeNote)) funded from the account's vault, so on
205/// fee-charging chains the account must hold a sufficient balance of the native fee asset. The
206/// conversion info is committed to via the transaction's auth args (see
207/// [`FeeConversionInfo`](super::FeeConversionInfo) and
208/// [`commit_fee_conversion_info`](super::commit_fee_conversion_info)). On chains with a zero
209/// verification base fee no note is created. The fee note is created before the transaction
210/// summary, so it is covered by the approver and guardian signatures; the auth args word continues
211/// to serve as the summary salt, and the uniqueness replay protection relies on originates from
212/// the caller-chosen salt.
213///
214/// The conversion rate is host-supplied, and guardian key rotation authenticates without a
215/// guardian signature and can be thresholded below the account's spending quorum. The component
216/// therefore bounds the payment: it must be in the native fee asset and at most twice the computed
217/// fee (`fee::assert_fee_bound`), so a rotation authorized below that quorum can at most overpay
218/// the fee, not move arbitrary value out of the account.
219///
220/// Rotation requires that the transaction create no notes beyond the fee note, so it works on a
221/// fee-charging chain — but it does require the vault to fund the fee. Rotation also forbids input
222/// notes, and assets can only enter a vault through an input note, so the funding transaction must
223/// be a separate one, and being a separate one it takes the ordinary path and needs a guardian
224/// signature. A guarded account on a fee-charging chain must therefore keep a standing balance of
225/// the native fee asset while its guardian key still works: a lost guardian key combined with an
226/// unfunded vault cannot be recovered.
227///
228/// # Privacy
229///
230/// Approvers and the guardian using [`AuthScheme::EcdsaK256Keccak`][scheme] disclose their public
231/// key and signature at proving time and therefore do not get public-key privacy; those using
232/// [`Falcon512Poseidon2`][falcon] do. See [`Approver`](super::Approver) for details.
233///
234/// [scheme]: miden_protocol::account::auth::AuthScheme::EcdsaK256Keccak
235/// [falcon]: miden_protocol::account::auth::AuthScheme::Falcon512Poseidon2
236#[derive(Debug)]
237pub struct AuthGuardedMultisig {
238    multisig: AuthMultisig,
239    guardian_config: GuardianConfig,
240}
241
242impl AuthGuardedMultisig {
243    /// The name of the component.
244    pub const NAME: &'static str = "miden::standards::auth::guarded_multisig";
245
246    /// Returns the canonical [`AccountComponentName`] of this component.
247    pub const fn name() -> AccountComponentName {
248        AccountComponentName::from_static_str(Self::NAME)
249    }
250
251    /// Returns the [`AccountComponentCode`] of this component.
252    pub fn code() -> &'static AccountComponentCode {
253        &GUARDED_MULTISIG_CODE
254    }
255
256    /// Creates a new [`AuthGuardedMultisig`] component from the provided configuration.
257    pub fn new(config: AuthGuardedMultisigConfig) -> Result<Self, AccountError> {
258        let (multisig_config, guardian_config) = config.into_parts();
259        Ok(Self {
260            multisig: AuthMultisig::new(multisig_config)?,
261            guardian_config,
262        })
263    }
264
265    /// Returns the [`StorageSlotName`] where the threshold configuration is stored.
266    pub fn threshold_config_slot() -> &'static StorageSlotName {
267        AuthMultisig::threshold_config_slot()
268    }
269
270    /// Returns the [`StorageSlotName`] where the approver public keys are stored.
271    pub fn approver_public_keys_slot() -> &'static StorageSlotName {
272        AuthMultisig::approver_public_keys_slot()
273    }
274
275    // Returns the [`StorageSlotName`] where the approver scheme IDs are stored.
276    pub fn approver_scheme_ids_slot() -> &'static StorageSlotName {
277        AuthMultisig::approver_scheme_ids_slot()
278    }
279
280    /// Returns the [`StorageSlotName`] where the executed transactions are stored.
281    pub fn executed_transactions_slot() -> &'static StorageSlotName {
282        AuthMultisig::executed_transactions_slot()
283    }
284
285    /// Returns the [`StorageSlotName`] where the procedure thresholds are stored.
286    pub fn procedure_thresholds_slot() -> &'static StorageSlotName {
287        AuthMultisig::procedure_thresholds_slot()
288    }
289
290    /// Returns the [`StorageSlotName`] where the guardian public key is stored.
291    pub fn guardian_public_key_slot() -> &'static StorageSlotName {
292        GuardianConfig::public_key_slot()
293    }
294
295    /// Returns the [`StorageSlotName`] where the guardian scheme IDs are stored.
296    pub fn guardian_scheme_id_slot() -> &'static StorageSlotName {
297        GuardianConfig::scheme_id_slot()
298    }
299
300    /// Returns the storage slot schema for the threshold configuration slot.
301    pub fn threshold_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
302        AuthMultisig::threshold_config_slot_schema()
303    }
304
305    /// Returns the storage slot schema for the approver public keys slot.
306    pub fn approver_public_keys_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
307        AuthMultisig::approver_public_keys_slot_schema()
308    }
309
310    // Returns the storage slot schema for the approver scheme IDs slot.
311    pub fn approver_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
312        AuthMultisig::approver_auth_scheme_slot_schema()
313    }
314
315    /// Returns the storage slot schema for the executed transactions slot.
316    pub fn executed_transactions_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
317        AuthMultisig::executed_transactions_slot_schema()
318    }
319
320    /// Returns the storage slot schema for the procedure thresholds slot.
321    pub fn procedure_thresholds_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
322        AuthMultisig::procedure_thresholds_slot_schema()
323    }
324
325    /// Returns the storage slot schema for the guardian public key slot.
326    pub fn guardian_public_key_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
327        GuardianConfig::public_key_slot_schema()
328    }
329
330    /// Returns the storage slot schema for the guardian scheme IDs slot.
331    pub fn guardian_auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
332        GuardianConfig::auth_scheme_slot_schema()
333    }
334
335    /// Returns the [`AccountComponentMetadata`] for this component.
336    pub fn component_metadata() -> AccountComponentMetadata {
337        let storage_schema = StorageSchema::new([
338            Self::threshold_config_slot_schema(),
339            Self::approver_public_keys_slot_schema(),
340            Self::approver_auth_scheme_slot_schema(),
341            Self::executed_transactions_slot_schema(),
342            Self::procedure_thresholds_slot_schema(),
343            Self::guardian_public_key_slot_schema(),
344            Self::guardian_auth_scheme_slot_schema(),
345        ])
346        .expect("storage schema should be valid");
347
348        AccountComponentMetadata::new(Self::NAME)
349            .with_description(
350                "Guarded multisig authentication component integrated \
351                 with a state guardian using hybrid signature schemes",
352            )
353            .with_storage_schema(storage_schema)
354    }
355}
356
357impl From<AuthGuardedMultisig> for AccountComponent {
358    fn from(multisig: AuthGuardedMultisig) -> Self {
359        let AuthGuardedMultisig { multisig, guardian_config } = multisig;
360        let multisig_component = AccountComponent::from(multisig);
361        let (guardian_slots, guardian_slot_metadata) = guardian_config.into_component_parts();
362
363        let mut storage_slots = multisig_component.storage_slots().to_vec();
364        storage_slots.extend(guardian_slots);
365
366        let mut slot_schemas: Vec<(StorageSlotName, StorageSlotSchema)> = multisig_component
367            .storage_schema()
368            .iter()
369            .map(|(slot_name, slot_schema)| (slot_name.clone(), slot_schema.clone()))
370            .collect();
371        slot_schemas.extend(guardian_slot_metadata);
372
373        let storage_schema =
374            StorageSchema::new(slot_schemas).expect("storage schema should be valid");
375
376        let metadata = AccountComponentMetadata::new(AuthGuardedMultisig::NAME)
377            .with_description(multisig_component.metadata().description())
378            .with_version(multisig_component.metadata().version().clone())
379            .with_storage_schema(storage_schema);
380
381        AccountComponent::new(AuthGuardedMultisig::code().clone(), storage_slots, metadata).expect(
382            "Guarded multisig auth component should satisfy the requirements of a valid \
383             account component",
384        )
385    }
386}
387
388// TESTS
389// ================================================================================================
390
391#[cfg(test)]
392mod tests {
393    use alloc::string::ToString;
394
395    use miden_protocol::Word;
396    use miden_protocol::account::AccountBuilder;
397    use miden_protocol::account::auth::AuthSecretKey;
398
399    use super::*;
400    use crate::account::wallets::BasicWallet;
401
402    fn approver(key: &AuthSecretKey) -> Approver {
403        Approver::new(key.public_key().to_commitment(), key.auth_scheme())
404    }
405
406    /// Test guarded multisig component setup with various configurations.
407    #[test]
408    fn test_guarded_multisig_component_setup() {
409        // Create test secret keys
410        let sec_key_1 = AuthSecretKey::new_falcon512_poseidon2();
411        let sec_key_2 = AuthSecretKey::new_falcon512_poseidon2();
412        let sec_key_3 = AuthSecretKey::new_falcon512_poseidon2();
413        let guardian_key = AuthSecretKey::new_ecdsa_k256_keccak();
414
415        // Create approvers list for multisig config
416        let approvers = vec![approver(&sec_key_1), approver(&sec_key_2), approver(&sec_key_3)];
417
418        let threshold = 2u32;
419
420        // Create guarded multisig component.
421        let approver_set =
422            ApproverSet::new(approvers.clone(), threshold).expect("invalid approver set");
423        let multisig_component = AuthGuardedMultisig::new(
424            AuthGuardedMultisigConfig::new(
425                approver_set,
426                GuardianConfig::new(approver(&guardian_key)),
427            )
428            .expect("invalid guarded multisig config"),
429        )
430        .expect("guarded multisig component creation failed");
431
432        // Build account with guarded multisig component.
433        let account = AccountBuilder::new([0; 32])
434            .with_component(multisig_component)
435            .with_component(BasicWallet)
436            .build()
437            .expect("account building failed");
438
439        // Verify config slot: [threshold, num_approvers, 0, 0]
440        let config_slot = account
441            .storage()
442            .get_item(AuthGuardedMultisig::threshold_config_slot())
443            .expect("config storage slot access failed");
444        assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
445
446        // Verify approver pub keys slot
447        for (i, expected) in approvers.iter().enumerate() {
448            let stored_pub_key = account
449                .storage()
450                .get_map_item(
451                    AuthGuardedMultisig::approver_public_keys_slot(),
452                    StorageMapKey::from_index(i as u32),
453                )
454                .expect("approver public key storage map access failed");
455            assert_eq!(stored_pub_key, Word::from(expected.pub_key()));
456        }
457
458        // Verify approver scheme IDs slot
459        for (i, expected) in approvers.iter().enumerate() {
460            let stored_scheme_id = account
461                .storage()
462                .get_map_item(
463                    AuthGuardedMultisig::approver_scheme_ids_slot(),
464                    StorageMapKey::from_index(i as u32),
465                )
466                .expect("approver scheme ID storage map access failed");
467            assert_eq!(stored_scheme_id, Word::from([expected.auth_scheme() as u32, 0, 0, 0]));
468        }
469
470        // Verify guardian signer is configured.
471        let guardian_public_key = account
472            .storage()
473            .get_map_item(
474                AuthGuardedMultisig::guardian_public_key_slot(),
475                StorageMapKey::from_index(0),
476            )
477            .expect("guardian public key storage map access failed");
478        assert_eq!(guardian_public_key, Word::from(guardian_key.public_key().to_commitment()));
479
480        let guardian_scheme_id = account
481            .storage()
482            .get_map_item(
483                AuthGuardedMultisig::guardian_scheme_id_slot(),
484                StorageMapKey::from_index(0),
485            )
486            .expect("guardian scheme ID storage map access failed");
487        assert_eq!(guardian_scheme_id, Word::from([guardian_key.auth_scheme() as u32, 0, 0, 0]));
488    }
489
490    /// Test guarded multisig component with minimum threshold (1 of 1).
491    #[test]
492    fn test_guarded_multisig_component_minimum_threshold() {
493        let approver_key = AuthSecretKey::new_ecdsa_k256_keccak();
494        let pub_key = approver_key.public_key().to_commitment();
495        let guardian_key = AuthSecretKey::new_falcon512_poseidon2();
496        let approvers = vec![approver(&approver_key)];
497        let threshold = 1u32;
498
499        let approver_set =
500            ApproverSet::new(approvers.clone(), threshold).expect("invalid approver set");
501        let multisig_component = AuthGuardedMultisig::new(
502            AuthGuardedMultisigConfig::new(
503                approver_set,
504                GuardianConfig::new(approver(&guardian_key)),
505            )
506            .expect("invalid guarded multisig config"),
507        )
508        .expect("guarded multisig component creation failed");
509
510        let account = AccountBuilder::new([0; 32])
511            .with_component(multisig_component)
512            .with_component(BasicWallet)
513            .build()
514            .expect("account building failed");
515
516        // Verify storage layout
517        let config_slot = account
518            .storage()
519            .get_item(AuthGuardedMultisig::threshold_config_slot())
520            .expect("config storage slot access failed");
521        assert_eq!(config_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
522
523        let stored_pub_key = account
524            .storage()
525            .get_map_item(
526                AuthGuardedMultisig::approver_public_keys_slot(),
527                StorageMapKey::from_index(0),
528            )
529            .expect("approver pub keys storage map access failed");
530        assert_eq!(stored_pub_key, Word::from(pub_key));
531
532        let stored_scheme_id = account
533            .storage()
534            .get_map_item(
535                AuthGuardedMultisig::approver_scheme_ids_slot(),
536                StorageMapKey::from_index(0),
537            )
538            .expect("approver scheme IDs storage map access failed");
539        assert_eq!(stored_scheme_id, Word::from([AuthScheme::EcdsaK256Keccak as u32, 0, 0, 0]));
540    }
541
542    /// Test guarded multisig component rejects a guardian key which is already an approver.
543    #[test]
544    fn test_guarded_multisig_component_guardian_not_approver() {
545        let sec_key_1 = AuthSecretKey::new_ecdsa_k256_keccak();
546        let sec_key_2 = AuthSecretKey::new_ecdsa_k256_keccak();
547
548        let approvers = vec![approver(&sec_key_1), approver(&sec_key_2)];
549        let approver_set = ApproverSet::new(approvers, 2).expect("invalid approver set");
550
551        let result =
552            AuthGuardedMultisigConfig::new(approver_set, GuardianConfig::new(approver(&sec_key_1)));
553
554        assert!(
555            result
556                .unwrap_err()
557                .to_string()
558                .contains("guardian public key must be different from approvers")
559        );
560    }
561}