miden_lib/account/auth/
rpo_falcon_512_acl.rs

1use alloc::vec::Vec;
2
3use miden_objects::account::auth::PublicKeyCommitment;
4use miden_objects::account::{AccountCode, AccountComponent, StorageMap, StorageSlot};
5use miden_objects::{AccountError, Word};
6
7use crate::account::components::rpo_falcon_512_acl_library;
8
9/// Configuration for [`AuthRpoFalcon512Acl`] component.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct AuthRpoFalcon512AclConfig {
12    /// List of procedure roots that require authentication when called.
13    pub auth_trigger_procedures: Vec<Word>,
14    /// When `false`, creating output notes (sending notes to other accounts) requires
15    /// authentication. When `true`, output notes can be created without authentication.
16    pub allow_unauthorized_output_notes: bool,
17    /// When `false`, consuming input notes (processing notes sent to this account) requires
18    /// authentication. When `true`, input notes can be consumed without authentication.
19    pub allow_unauthorized_input_notes: bool,
20}
21
22impl AuthRpoFalcon512AclConfig {
23    /// Creates a new configuration with no trigger procedures and both flags set to `false` (most
24    /// restrictive).
25    pub fn new() -> Self {
26        Self {
27            auth_trigger_procedures: vec![],
28            allow_unauthorized_output_notes: false,
29            allow_unauthorized_input_notes: false,
30        }
31    }
32
33    /// Sets the list of procedure roots that require authentication when called.
34    pub fn with_auth_trigger_procedures(mut self, procedures: Vec<Word>) -> Self {
35        self.auth_trigger_procedures = procedures;
36        self
37    }
38
39    /// Sets whether unauthorized output notes are allowed.
40    pub fn with_allow_unauthorized_output_notes(mut self, allow: bool) -> Self {
41        self.allow_unauthorized_output_notes = allow;
42        self
43    }
44
45    /// Sets whether unauthorized input notes are allowed.
46    pub fn with_allow_unauthorized_input_notes(mut self, allow: bool) -> Self {
47        self.allow_unauthorized_input_notes = allow;
48        self
49    }
50}
51
52impl Default for AuthRpoFalcon512AclConfig {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58/// An [`AccountComponent`] implementing a procedure-based Access Control List (ACL) using the
59/// RpoFalcon512 signature scheme for authentication of transactions.
60///
61/// This component provides fine-grained authentication control based on three conditions:
62/// 1. **Procedure-based authentication**: Requires authentication when any of the specified trigger
63///    procedures are called during the transaction.
64/// 2. **Output note authentication**: Controls whether creating output notes requires
65///    authentication. Output notes are new notes created by the account and sent to other accounts
66///    (e.g., when transferring assets). When `allow_unauthorized_output_notes` is `false`, any
67///    transaction that creates output notes must be authenticated, ensuring account owners control
68///    when their account sends assets to other accounts.
69/// 3. **Input note authentication**: Controls whether consuming input notes requires
70///    authentication. Input notes are notes that were sent to this account by other accounts (e.g.,
71///    incoming asset transfers). When `allow_unauthorized_input_notes` is `false`, any transaction
72///    that consumes input notes must be authenticated, ensuring account owners control when their
73///    account processes incoming notes.
74///
75/// ## Authentication Logic
76///
77/// Authentication is required if ANY of the following conditions are true:
78/// - Any trigger procedure from the ACL was called
79/// - Output notes were created AND `allow_unauthorized_output_notes` is `false`
80/// - Input notes were consumed AND `allow_unauthorized_input_notes` is `false`
81///
82/// If none of these conditions are met, only the nonce is incremented without requiring a
83/// signature.
84///
85/// ## Use Cases
86///
87/// - **Restrictive mode** (`allow_unauthorized_output_notes=false`,
88///   `allow_unauthorized_input_notes=false`): All note operations require authentication, providing
89///   maximum security.
90/// - **Selective mode**: Allow some note operations without authentication while protecting
91///   specific procedures, useful for accounts that need to process certain operations
92///   automatically.
93/// - **Procedure-only mode** (`allow_unauthorized_output_notes=true`,
94///   `allow_unauthorized_input_notes=true`): Only specific procedures require authentication,
95///   allowing free note processing.
96///
97/// ## Storage Layout
98/// - Slot 0(value): Public key (same as RpoFalcon512)
99/// - Slot 1(value): [num_tracked_procs, allow_unauthorized_output_notes,
100///   allow_unauthorized_input_notes, 0]
101/// - Slot 2(map): A map with trigger procedure roots
102///
103/// ## Important Note on Procedure Detection
104/// The procedure-based authentication relies on the `was_procedure_called` kernel function,
105/// which only returns `true` if the procedure in question called into a kernel account API
106/// that is restricted to the account context. Procedures that don't interact with account
107/// state or kernel APIs may not be detected as "called" even if they were executed during
108/// the transaction. This is an important limitation to consider when designing trigger
109/// procedures for authentication.
110///
111/// This component supports all account types.
112pub struct AuthRpoFalcon512Acl {
113    pub_key: PublicKeyCommitment,
114    config: AuthRpoFalcon512AclConfig,
115}
116
117impl AuthRpoFalcon512Acl {
118    /// Creates a new [`AuthRpoFalcon512Acl`] component with the given `public_key` and
119    /// configuration.
120    ///
121    /// # Panics
122    /// Panics if more than [AccountCode::MAX_NUM_PROCEDURES] procedures are specified.
123    pub fn new(
124        pub_key: PublicKeyCommitment,
125        config: AuthRpoFalcon512AclConfig,
126    ) -> Result<Self, AccountError> {
127        let max_procedures = AccountCode::MAX_NUM_PROCEDURES;
128        if config.auth_trigger_procedures.len() > max_procedures {
129            return Err(AccountError::other(format!(
130                "Cannot track more than {max_procedures} procedures (account limit)"
131            )));
132        }
133
134        Ok(Self { pub_key, config })
135    }
136}
137
138impl From<AuthRpoFalcon512Acl> for AccountComponent {
139    fn from(falcon: AuthRpoFalcon512Acl) -> Self {
140        let mut storage_slots = Vec::with_capacity(3);
141
142        // Slot 0: Public key
143        storage_slots.push(StorageSlot::Value(falcon.pub_key.into()));
144
145        // Slot 1: [num_tracked_procs, allow_unauthorized_output_notes,
146        // allow_unauthorized_input_notes, 0]
147        let num_procs = falcon.config.auth_trigger_procedures.len() as u32;
148        storage_slots.push(StorageSlot::Value(Word::from([
149            num_procs,
150            u32::from(falcon.config.allow_unauthorized_output_notes),
151            u32::from(falcon.config.allow_unauthorized_input_notes),
152            0,
153        ])));
154
155        // Slot 2: A map with tracked procedure roots
156        // We add the map even if there are no trigger procedures, to always maintain the same
157        // storage layout.
158        let map_entries = falcon
159            .config
160            .auth_trigger_procedures
161            .iter()
162            .enumerate()
163            .map(|(i, proc_root)| (Word::from([i as u32, 0, 0, 0]), *proc_root));
164
165        // Safe to unwrap because we know that the map keys are unique.
166        storage_slots.push(StorageSlot::Map(StorageMap::with_entries(map_entries).unwrap()));
167
168        AccountComponent::new(rpo_falcon_512_acl_library(), storage_slots)
169            .expect(
170                "ACL auth component should satisfy the requirements of a valid account component",
171            )
172            .with_supports_all_types()
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use miden_objects::Word;
179    use miden_objects::account::AccountBuilder;
180
181    use super::*;
182    use crate::account::components::WellKnownComponent;
183    use crate::account::wallets::BasicWallet;
184
185    /// Test configuration for parametrized ACL tests
186    struct AclTestConfig {
187        /// Whether to include auth trigger procedures
188        with_procedures: bool,
189        /// Allow unauthorized output notes flag
190        allow_unauthorized_output_notes: bool,
191        /// Allow unauthorized input notes flag
192        allow_unauthorized_input_notes: bool,
193        /// Expected slot 1 value [num_procs, allow_output, allow_input, 0]
194        expected_slot_1: Word,
195    }
196
197    /// Helper function to get the basic wallet procedures for testing
198    fn get_basic_wallet_procedures() -> Vec<Word> {
199        // Get the two trigger procedures from BasicWallet: `receive_asset`, `move_asset_to_note`.
200        let procedures: Vec<Word> = WellKnownComponent::BasicWallet.procedure_digests().collect();
201
202        assert_eq!(procedures.len(), 2);
203        procedures
204    }
205
206    /// Parametrized test helper for ACL component testing
207    fn test_acl_component(config: AclTestConfig) {
208        let public_key = PublicKeyCommitment::from(Word::empty());
209
210        // Build the configuration
211        let mut acl_config = AuthRpoFalcon512AclConfig::new()
212            .with_allow_unauthorized_output_notes(config.allow_unauthorized_output_notes)
213            .with_allow_unauthorized_input_notes(config.allow_unauthorized_input_notes);
214
215        let auth_trigger_procedures = if config.with_procedures {
216            let procedures = get_basic_wallet_procedures();
217            acl_config = acl_config.with_auth_trigger_procedures(procedures.clone());
218            procedures
219        } else {
220            vec![]
221        };
222
223        // Create component and account
224        let component =
225            AuthRpoFalcon512Acl::new(public_key, acl_config).expect("component creation failed");
226
227        let account = AccountBuilder::new([0; 32])
228            .with_auth_component(component)
229            .with_component(BasicWallet)
230            .build()
231            .expect("account building failed");
232
233        // Assert public key in slot 0
234        let public_key_slot = account.storage().get_item(0).expect("storage slot 0 access failed");
235        assert_eq!(public_key_slot, public_key.into());
236
237        // Assert configuration in slot 1
238        let slot_1 = account.storage().get_item(1).expect("storage slot 1 access failed");
239        assert_eq!(slot_1, config.expected_slot_1);
240
241        // Assert procedure roots in map (slot 2)
242        if config.with_procedures {
243            for (i, expected_proc_root) in auth_trigger_procedures.iter().enumerate() {
244                let proc_root = account
245                    .storage()
246                    .get_map_item(2, Word::from([i as u32, 0, 0, 0]))
247                    .expect("storage map access failed");
248                assert_eq!(proc_root, *expected_proc_root);
249            }
250        } else {
251            // When no procedures, the map should return empty for key [0,0,0,0]
252            let proc_root = account
253                .storage()
254                .get_map_item(2, Word::empty())
255                .expect("storage map access failed");
256            assert_eq!(proc_root, Word::empty());
257        }
258    }
259
260    /// Test ACL component with no procedures and both authorization flags set to false
261    #[test]
262    fn test_rpo_falcon_512_acl_no_procedures() {
263        test_acl_component(AclTestConfig {
264            with_procedures: false,
265            allow_unauthorized_output_notes: false,
266            allow_unauthorized_input_notes: false,
267            expected_slot_1: Word::empty(), // [0, 0, 0, 0]
268        });
269    }
270
271    /// Test ACL component with two procedures and both authorization flags set to false
272    #[test]
273    fn test_rpo_falcon_512_acl_with_two_procedures() {
274        test_acl_component(AclTestConfig {
275            with_procedures: true,
276            allow_unauthorized_output_notes: false,
277            allow_unauthorized_input_notes: false,
278            expected_slot_1: Word::from([2u32, 0, 0, 0]),
279        });
280    }
281
282    /// Test ACL component with no procedures and allow_unauthorized_output_notes set to true
283    #[test]
284    fn test_rpo_falcon_512_acl_with_allow_unauthorized_output_notes() {
285        test_acl_component(AclTestConfig {
286            with_procedures: false,
287            allow_unauthorized_output_notes: true,
288            allow_unauthorized_input_notes: false,
289            expected_slot_1: Word::from([0u32, 1, 0, 0]),
290        });
291    }
292
293    /// Test ACL component with two procedures and allow_unauthorized_output_notes set to true
294    #[test]
295    fn test_rpo_falcon_512_acl_with_procedures_and_allow_unauthorized_output_notes() {
296        test_acl_component(AclTestConfig {
297            with_procedures: true,
298            allow_unauthorized_output_notes: true,
299            allow_unauthorized_input_notes: false,
300            expected_slot_1: Word::from([2u32, 1, 0, 0]),
301        });
302    }
303
304    /// Test ACL component with no procedures and allow_unauthorized_input_notes set to true
305    #[test]
306    fn test_rpo_falcon_512_acl_with_allow_unauthorized_input_notes() {
307        test_acl_component(AclTestConfig {
308            with_procedures: false,
309            allow_unauthorized_output_notes: false,
310            allow_unauthorized_input_notes: true,
311            expected_slot_1: Word::from([0u32, 0, 1, 0]),
312        });
313    }
314
315    /// Test ACL component with two procedures and both authorization flags set to true
316    #[test]
317    fn test_rpo_falcon_512_acl_with_both_allow_flags() {
318        test_acl_component(AclTestConfig {
319            with_procedures: true,
320            allow_unauthorized_output_notes: true,
321            allow_unauthorized_input_notes: true,
322            expected_slot_1: Word::from([2u32, 1, 1, 0]),
323        });
324    }
325}