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}