miden_lib/account/auth/
mod.rs

1use alloc::vec::Vec;
2
3use miden_objects::account::{AccountCode, AccountComponent, StorageMap, StorageSlot};
4use miden_objects::crypto::dsa::rpo_falcon512::PublicKey;
5use miden_objects::{AccountError, Word};
6
7use crate::account::components::{
8    multisig_library,
9    no_auth_library,
10    rpo_falcon_512_acl_library,
11    rpo_falcon_512_library,
12};
13
14/// An [`AccountComponent`] implementing the RpoFalcon512 signature scheme for authentication of
15/// transactions.
16///
17/// It reexports the procedures from `miden::contracts::auth::basic`. When linking against this
18/// component, the `miden` library (i.e. [`MidenLib`](crate::MidenLib)) must be available to the
19/// assembler which is the case when using [`TransactionKernel::assembler()`][kasm]. The procedures
20/// of this component are:
21/// - `auth__tx_rpo_falcon512`, which can be used to verify a signature provided via the advice
22///   stack to authenticate a transaction.
23///
24/// This component supports all account types.
25///
26/// [kasm]: crate::transaction::TransactionKernel::assembler
27pub struct AuthRpoFalcon512 {
28    public_key: PublicKey,
29}
30
31impl AuthRpoFalcon512 {
32    /// Creates a new [`AuthRpoFalcon512`] component with the given `public_key`.
33    pub fn new(public_key: PublicKey) -> Self {
34        Self { public_key }
35    }
36}
37
38impl From<AuthRpoFalcon512> for AccountComponent {
39    fn from(falcon: AuthRpoFalcon512) -> Self {
40        AccountComponent::new(
41            rpo_falcon_512_library(),
42            vec![StorageSlot::Value(falcon.public_key.into())],
43        )
44        .expect("falcon component should satisfy the requirements of a valid account component")
45        .with_supports_all_types()
46    }
47}
48
49/// Configuration for [`AuthRpoFalcon512Acl`] component.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct AuthRpoFalcon512AclConfig {
52    /// List of procedure roots that require authentication when called.
53    pub auth_trigger_procedures: Vec<Word>,
54    /// When `false`, creating output notes (sending notes to other accounts) requires
55    /// authentication. When `true`, output notes can be created without authentication.
56    pub allow_unauthorized_output_notes: bool,
57    /// When `false`, consuming input notes (processing notes sent to this account) requires
58    /// authentication. When `true`, input notes can be consumed without authentication.
59    pub allow_unauthorized_input_notes: bool,
60}
61
62impl AuthRpoFalcon512AclConfig {
63    /// Creates a new configuration with no trigger procedures and both flags set to `false` (most
64    /// restrictive).
65    pub fn new() -> Self {
66        Self {
67            auth_trigger_procedures: vec![],
68            allow_unauthorized_output_notes: false,
69            allow_unauthorized_input_notes: false,
70        }
71    }
72
73    /// Sets the list of procedure roots that require authentication when called.
74    pub fn with_auth_trigger_procedures(mut self, procedures: Vec<Word>) -> Self {
75        self.auth_trigger_procedures = procedures;
76        self
77    }
78
79    /// Sets whether unauthorized output notes are allowed.
80    pub fn with_allow_unauthorized_output_notes(mut self, allow: bool) -> Self {
81        self.allow_unauthorized_output_notes = allow;
82        self
83    }
84
85    /// Sets whether unauthorized input notes are allowed.
86    pub fn with_allow_unauthorized_input_notes(mut self, allow: bool) -> Self {
87        self.allow_unauthorized_input_notes = allow;
88        self
89    }
90}
91
92impl Default for AuthRpoFalcon512AclConfig {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98/// An [`AccountComponent`] implementing a procedure-based Access Control List (ACL) using the
99/// RpoFalcon512 signature scheme for authentication of transactions.
100///
101/// This component provides fine-grained authentication control based on three conditions:
102/// 1. **Procedure-based authentication**: Requires authentication when any of the specified trigger
103///    procedures are called during the transaction.
104/// 2. **Output note authentication**: Controls whether creating output notes requires
105///    authentication. Output notes are new notes created by the account and sent to other accounts
106///    (e.g., when transferring assets). When `allow_unauthorized_output_notes` is `false`, any
107///    transaction that creates output notes must be authenticated, ensuring account owners control
108///    when their account sends assets to other accounts.
109/// 3. **Input note authentication**: Controls whether consuming input notes requires
110///    authentication. Input notes are notes that were sent to this account by other accounts (e.g.,
111///    incoming asset transfers). When `allow_unauthorized_input_notes` is `false`, any transaction
112///    that consumes input notes must be authenticated, ensuring account owners control when their
113///    account processes incoming notes.
114///
115/// ## Authentication Logic
116///
117/// Authentication is required if ANY of the following conditions are true:
118/// - Any trigger procedure from the ACL was called
119/// - Output notes were created AND `allow_unauthorized_output_notes` is `false`
120/// - Input notes were consumed AND `allow_unauthorized_input_notes` is `false`
121///
122/// If none of these conditions are met, only the nonce is incremented without requiring a
123/// signature.
124///
125/// ## Use Cases
126///
127/// - **Restrictive mode** (`allow_unauthorized_output_notes=false`,
128///   `allow_unauthorized_input_notes=false`): All note operations require authentication, providing
129///   maximum security.
130/// - **Selective mode**: Allow some note operations without authentication while protecting
131///   specific procedures, useful for accounts that need to process certain operations
132///   automatically.
133/// - **Procedure-only mode** (`allow_unauthorized_output_notes=true`,
134///   `allow_unauthorized_input_notes=true`): Only specific procedures require authentication,
135///   allowing free note processing.
136///
137/// ## Storage Layout
138/// - Slot 0(value): Public key (same as RpoFalcon512)
139/// - Slot 1(value): [num_tracked_procs, allow_unauthorized_output_notes,
140///   allow_unauthorized_input_notes, 0]
141/// - Slot 2(map): A map with trigger procedure roots
142///
143/// ## Important Note on Procedure Detection
144/// The procedure-based authentication relies on the `was_procedure_called` kernel function,
145/// which only returns `true` if the procedure in question called into a kernel account API
146/// that is restricted to the account context. Procedures that don't interact with account
147/// state or kernel APIs may not be detected as "called" even if they were executed during
148/// the transaction. This is an important limitation to consider when designing trigger
149/// procedures for authentication.
150///
151/// This component supports all account types.
152pub struct AuthRpoFalcon512Acl {
153    public_key: PublicKey,
154    config: AuthRpoFalcon512AclConfig,
155}
156
157impl AuthRpoFalcon512Acl {
158    /// Creates a new [`AuthRpoFalcon512Acl`] component with the given `public_key` and
159    /// configuration.
160    ///
161    /// # Panics
162    /// Panics if more than [AccountCode::MAX_NUM_PROCEDURES] procedures are specified.
163    pub fn new(
164        public_key: PublicKey,
165        config: AuthRpoFalcon512AclConfig,
166    ) -> Result<Self, AccountError> {
167        let max_procedures = AccountCode::MAX_NUM_PROCEDURES;
168        if config.auth_trigger_procedures.len() > max_procedures {
169            return Err(AccountError::other(format!(
170                "Cannot track more than {max_procedures} procedures (account limit)"
171            )));
172        }
173
174        Ok(Self { public_key, config })
175    }
176}
177
178impl From<AuthRpoFalcon512Acl> for AccountComponent {
179    fn from(falcon: AuthRpoFalcon512Acl) -> Self {
180        let mut storage_slots = Vec::with_capacity(3);
181
182        // Slot 0: Public key
183        storage_slots.push(StorageSlot::Value(falcon.public_key.into()));
184
185        // Slot 1: [num_tracked_procs, allow_unauthorized_output_notes,
186        // allow_unauthorized_input_notes, 0]
187        let num_procs = falcon.config.auth_trigger_procedures.len() as u32;
188        storage_slots.push(StorageSlot::Value(Word::from([
189            num_procs,
190            u32::from(falcon.config.allow_unauthorized_output_notes),
191            u32::from(falcon.config.allow_unauthorized_input_notes),
192            0,
193        ])));
194
195        // Slot 2: A map with tracked procedure roots
196        // We add the map even if there are no trigger procedures, to always maintain the same
197        // storage layout.
198        let map_entries = falcon
199            .config
200            .auth_trigger_procedures
201            .iter()
202            .enumerate()
203            .map(|(i, proc_root)| (Word::from([i as u32, 0, 0, 0]), *proc_root));
204
205        // Safe to unwrap because we know that the map keys are unique.
206        storage_slots.push(StorageSlot::Map(StorageMap::with_entries(map_entries).unwrap()));
207
208        AccountComponent::new(rpo_falcon_512_acl_library(), storage_slots)
209            .expect(
210                "ACL auth component should satisfy the requirements of a valid account component",
211            )
212            .with_supports_all_types()
213    }
214}
215
216/// An [`AccountComponent`] implementing a no-authentication scheme.
217///
218/// This component provides **no authentication**! It only checks if the account
219/// state has actually changed during transaction execution by comparing the initial
220/// account commitment with the current commitment and increments the nonce if
221/// they differ. This avoids unnecessary nonce increments for transactions that don't
222/// modify the account state.
223///
224/// It exports the procedure `auth__no_auth`, which:
225/// - Checks if the account state has changed by comparing initial and final commitments
226/// - Only increments the nonce if the account state has actually changed
227/// - Provides no cryptographic authentication
228///
229/// This component supports all account types.
230pub struct NoAuth;
231
232impl NoAuth {
233    /// Creates a new [`NoAuth`] component.
234    pub fn new() -> Self {
235        Self
236    }
237}
238
239impl Default for NoAuth {
240    fn default() -> Self {
241        Self::new()
242    }
243}
244
245impl From<NoAuth> for AccountComponent {
246    fn from(_: NoAuth) -> Self {
247        AccountComponent::new(no_auth_library(), vec![])
248            .expect("NoAuth component should satisfy the requirements of a valid account component")
249            .with_supports_all_types()
250    }
251}
252
253// MULTISIG AUTHENTICATION COMPONENT
254// ================================================================================================
255
256/// An [`AccountComponent`] implementing a multisig based on RpoFalcon512 signatures.
257///
258/// This component requires a threshold number of signatures from a set of approvers.
259///
260/// The storage layout is:
261/// - Slot 0(value): [threshold, num_approvers, 0, 0]
262/// - Slot 1(map): A map with approver public keys (index -> pubkey)
263/// - Slot 2(map): A map which stores executed transactions
264///
265/// This component supports all account types.
266#[derive(Debug)]
267pub struct AuthRpoFalcon512Multisig {
268    threshold: u32,
269    approvers: Vec<PublicKey>,
270}
271
272impl AuthRpoFalcon512Multisig {
273    /// Creates a new [`AuthRpoFalcon512Multisig`] component with the given `threshold` and
274    /// list of approver public keys.
275    ///
276    /// # Errors
277    /// Returns an error if threshold is 0 or greater than the number of approvers.
278    pub fn new(threshold: u32, approvers: Vec<PublicKey>) -> Result<Self, AccountError> {
279        if threshold == 0 {
280            return Err(AccountError::other("threshold must be at least 1"));
281        }
282
283        if threshold > approvers.len() as u32 {
284            return Err(AccountError::other(
285                "threshold cannot be greater than number of approvers",
286            ));
287        }
288
289        Ok(Self { threshold, approvers })
290    }
291}
292
293impl From<AuthRpoFalcon512Multisig> for AccountComponent {
294    fn from(multisig: AuthRpoFalcon512Multisig) -> Self {
295        let mut storage_slots = Vec::with_capacity(3);
296
297        // Slot 0: [threshold, num_approvers, 0, 0]
298        let num_approvers = multisig.approvers.len() as u32;
299        storage_slots.push(StorageSlot::Value(Word::from([
300            multisig.threshold,
301            num_approvers,
302            0,
303            0,
304        ])));
305
306        // Slot 1: A map with approver public keys
307        let map_entries = multisig
308            .approvers
309            .iter()
310            .enumerate()
311            .map(|(i, pub_key)| (Word::from([i as u32, 0, 0, 0]), (*pub_key).into()));
312
313        // Safe to unwrap because we know that the map keys are unique.
314        storage_slots.push(StorageSlot::Map(StorageMap::with_entries(map_entries).unwrap()));
315
316        // Slot 2: A map which stores executed transactions
317        let executed_transactions = StorageMap::default();
318        storage_slots.push(StorageSlot::Map(executed_transactions));
319
320        AccountComponent::new(multisig_library(), storage_slots)
321            .expect("Multisig auth component should satisfy the requirements of a valid account component")
322            .with_supports_all_types()
323    }
324}
325
326// TESTS
327// ================================================================================================
328
329#[cfg(test)]
330mod tests {
331    use alloc::string::ToString;
332
333    use miden_objects::Word;
334    use miden_objects::account::AccountBuilder;
335
336    use super::*;
337    use crate::account::components::WellKnownComponent;
338    use crate::account::wallets::BasicWallet;
339
340    /// Test configuration for parametrized ACL tests
341    struct AclTestConfig {
342        /// Whether to include auth trigger procedures
343        with_procedures: bool,
344        /// Allow unauthorized output notes flag
345        allow_unauthorized_output_notes: bool,
346        /// Allow unauthorized input notes flag
347        allow_unauthorized_input_notes: bool,
348        /// Expected slot 1 value [num_procs, allow_output, allow_input, 0]
349        expected_slot_1: Word,
350    }
351
352    /// Helper function to get the basic wallet procedures for testing
353    fn get_basic_wallet_procedures() -> Vec<Word> {
354        // Get the two trigger procedures from BasicWallet: `receive_asset`, `move_asset_to_note`.
355        let procedures: Vec<Word> = WellKnownComponent::BasicWallet.procedure_digests().collect();
356
357        assert_eq!(procedures.len(), 2);
358        procedures
359    }
360
361    /// Parametrized test helper for ACL component testing
362    fn test_acl_component(config: AclTestConfig) {
363        let public_key = PublicKey::new(Word::empty());
364
365        // Build the configuration
366        let mut acl_config = AuthRpoFalcon512AclConfig::new()
367            .with_allow_unauthorized_output_notes(config.allow_unauthorized_output_notes)
368            .with_allow_unauthorized_input_notes(config.allow_unauthorized_input_notes);
369
370        let auth_trigger_procedures = if config.with_procedures {
371            let procedures = get_basic_wallet_procedures();
372            acl_config = acl_config.with_auth_trigger_procedures(procedures.clone());
373            procedures
374        } else {
375            vec![]
376        };
377
378        // Create component and account
379        let component =
380            AuthRpoFalcon512Acl::new(public_key, acl_config).expect("component creation failed");
381
382        let (account, _) = AccountBuilder::new([0; 32])
383            .with_auth_component(component)
384            .with_component(BasicWallet)
385            .build()
386            .expect("account building failed");
387
388        // Assert public key in slot 0
389        let public_key_slot = account.storage().get_item(0).expect("storage slot 0 access failed");
390        assert_eq!(public_key_slot, Word::from(public_key));
391
392        // Assert configuration in slot 1
393        let slot_1 = account.storage().get_item(1).expect("storage slot 1 access failed");
394        assert_eq!(slot_1, config.expected_slot_1);
395
396        // Assert procedure roots in map (slot 2)
397        if config.with_procedures {
398            for (i, expected_proc_root) in auth_trigger_procedures.iter().enumerate() {
399                let proc_root = account
400                    .storage()
401                    .get_map_item(2, Word::from([i as u32, 0, 0, 0]))
402                    .expect("storage map access failed");
403                assert_eq!(proc_root, *expected_proc_root);
404            }
405        } else {
406            // When no procedures, the map should return empty for key [0,0,0,0]
407            let proc_root = account
408                .storage()
409                .get_map_item(2, Word::empty())
410                .expect("storage map access failed");
411            assert_eq!(proc_root, Word::empty());
412        }
413    }
414
415    /// Test ACL component with no procedures and both authorization flags set to false
416    #[test]
417    fn test_rpo_falcon_512_acl_no_procedures() {
418        test_acl_component(AclTestConfig {
419            with_procedures: false,
420            allow_unauthorized_output_notes: false,
421            allow_unauthorized_input_notes: false,
422            expected_slot_1: Word::empty(), // [0, 0, 0, 0]
423        });
424    }
425
426    /// Test ACL component with two procedures and both authorization flags set to false
427    #[test]
428    fn test_rpo_falcon_512_acl_with_two_procedures() {
429        test_acl_component(AclTestConfig {
430            with_procedures: true,
431            allow_unauthorized_output_notes: false,
432            allow_unauthorized_input_notes: false,
433            expected_slot_1: Word::from([2u32, 0, 0, 0]),
434        });
435    }
436
437    /// Test ACL component with no procedures and allow_unauthorized_output_notes set to true
438    #[test]
439    fn test_rpo_falcon_512_acl_with_allow_unauthorized_output_notes() {
440        test_acl_component(AclTestConfig {
441            with_procedures: false,
442            allow_unauthorized_output_notes: true,
443            allow_unauthorized_input_notes: false,
444            expected_slot_1: Word::from([0u32, 1, 0, 0]),
445        });
446    }
447
448    /// Test ACL component with two procedures and allow_unauthorized_output_notes set to true
449    #[test]
450    fn test_rpo_falcon_512_acl_with_procedures_and_allow_unauthorized_output_notes() {
451        test_acl_component(AclTestConfig {
452            with_procedures: true,
453            allow_unauthorized_output_notes: true,
454            allow_unauthorized_input_notes: false,
455            expected_slot_1: Word::from([2u32, 1, 0, 0]),
456        });
457    }
458
459    /// Test ACL component with no procedures and allow_unauthorized_input_notes set to true
460    #[test]
461    fn test_rpo_falcon_512_acl_with_allow_unauthorized_input_notes() {
462        test_acl_component(AclTestConfig {
463            with_procedures: false,
464            allow_unauthorized_output_notes: false,
465            allow_unauthorized_input_notes: true,
466            expected_slot_1: Word::from([0u32, 0, 1, 0]),
467        });
468    }
469
470    /// Test ACL component with two procedures and both authorization flags set to true
471    #[test]
472    fn test_rpo_falcon_512_acl_with_both_allow_flags() {
473        test_acl_component(AclTestConfig {
474            with_procedures: true,
475            allow_unauthorized_output_notes: true,
476            allow_unauthorized_input_notes: true,
477            expected_slot_1: Word::from([2u32, 1, 1, 0]),
478        });
479    }
480
481    #[test]
482    fn test_no_auth_component() {
483        // Create an account using the NoAuth component
484        let (_account, _) = AccountBuilder::new([0; 32])
485            .with_auth_component(NoAuth)
486            .with_component(BasicWallet)
487            .build()
488            .expect("account building failed");
489    }
490
491    // MULTISIG TESTS
492    // ============================================================================================
493
494    /// Test multisig component setup with various configurations
495    #[test]
496    fn test_multisig_component_setup() {
497        // Create test public keys
498        let pub_key_1 = PublicKey::new(Word::from([1u32, 0, 0, 0]));
499        let pub_key_2 = PublicKey::new(Word::from([2u32, 0, 0, 0]));
500        let pub_key_3 = PublicKey::new(Word::from([3u32, 0, 0, 0]));
501        let approvers = vec![pub_key_1, pub_key_2, pub_key_3];
502        let threshold = 2u32;
503
504        // Create multisig component
505        let multisig_component = AuthRpoFalcon512Multisig::new(threshold, approvers.clone())
506            .expect("multisig component creation failed");
507
508        // Build account with multisig component
509        let (account, _) = AccountBuilder::new([0; 32])
510            .with_auth_component(multisig_component)
511            .with_component(BasicWallet)
512            .build()
513            .expect("account building failed");
514
515        // Verify slot 0: [threshold, num_approvers, 0, 0]
516        let threshold_slot = account.storage().get_item(0).expect("storage slot 0 access failed");
517        assert_eq!(threshold_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
518
519        // Verify slot 1: Approver public keys in map
520        for (i, expected_pub_key) in approvers.iter().enumerate() {
521            let stored_pub_key = account
522                .storage()
523                .get_map_item(1, Word::from([i as u32, 0, 0, 0]))
524                .expect("storage map access failed");
525            assert_eq!(stored_pub_key, Word::from(*expected_pub_key));
526        }
527    }
528
529    /// Test multisig component with minimum threshold (1 of 1)
530    #[test]
531    fn test_multisig_component_minimum_threshold() {
532        let pub_key = PublicKey::new(Word::from([42u32, 0, 0, 0]));
533        let approvers = vec![pub_key];
534        let threshold = 1u32;
535
536        let multisig_component = AuthRpoFalcon512Multisig::new(threshold, approvers.clone())
537            .expect("multisig component creation failed");
538
539        let (account, _) = AccountBuilder::new([0; 32])
540            .with_auth_component(multisig_component)
541            .with_component(BasicWallet)
542            .build()
543            .expect("account building failed");
544
545        // Verify storage layout
546        let threshold_slot = account.storage().get_item(0).expect("storage slot 0 access failed");
547        assert_eq!(threshold_slot, Word::from([threshold, approvers.len() as u32, 0, 0]));
548
549        let stored_pub_key = account
550            .storage()
551            .get_map_item(1, Word::from([0u32, 0, 0, 0]))
552            .expect("storage map access failed");
553        assert_eq!(stored_pub_key, Word::from(pub_key));
554    }
555
556    /// Test multisig component error cases
557    #[test]
558    fn test_multisig_component_error_cases() {
559        let pub_key = PublicKey::new(Word::from([1u32, 0, 0, 0]));
560        let approvers = vec![pub_key];
561
562        // Test threshold = 0 (should fail)
563        let result = AuthRpoFalcon512Multisig::new(0, approvers.clone());
564        assert!(result.unwrap_err().to_string().contains("threshold must be at least 1"));
565
566        // Test threshold > number of approvers (should fail)
567        let result = AuthRpoFalcon512Multisig::new(2, approvers);
568        assert!(
569            result
570                .unwrap_err()
571                .to_string()
572                .contains("threshold cannot be greater than number of approvers")
573        );
574    }
575}