miden_lib/account/auth/ecdsa_k256_keccak_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::ecdsa_k256_keccak_acl_library;
8
9/// Configuration for [`AuthEcdsaK256KeccakAcl`] component.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct AuthEcdsaK256KeccakAclConfig {
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 AuthEcdsaK256KeccakAclConfig {
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 AuthEcdsaK256KeccakAclConfig {
53 fn default() -> Self {
54 Self::new()
55 }
56}
57
58/// An [`AccountComponent`] implementing a procedure-based Access Control List (ACL) using the
59/// EcdsaK256Keccak 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 EcdsaK256Keccak)
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 AuthEcdsaK256KeccakAcl {
113 pub_key: PublicKeyCommitment,
114 config: AuthEcdsaK256KeccakAclConfig,
115}
116
117impl AuthEcdsaK256KeccakAcl {
118 /// Creates a new [`AuthEcdsaK256KeccakAcl`] 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: AuthEcdsaK256KeccakAclConfig,
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<AuthEcdsaK256KeccakAcl> for AccountComponent {
139 fn from(ecdsa: AuthEcdsaK256KeccakAcl) -> Self {
140 let mut storage_slots = Vec::with_capacity(3);
141
142 // Slot 0: Public key
143 storage_slots.push(StorageSlot::Value(ecdsa.pub_key.into()));
144 // Slot 1: [num_tracked_procs, allow_unauthorized_output_notes,
145 // allow_unauthorized_input_notes, 0]
146 let num_procs = ecdsa.config.auth_trigger_procedures.len() as u32;
147 storage_slots.push(StorageSlot::Value(Word::from([
148 num_procs,
149 u32::from(ecdsa.config.allow_unauthorized_output_notes),
150 u32::from(ecdsa.config.allow_unauthorized_input_notes),
151 0,
152 ])));
153
154 // Slot 2: A map with tracked procedure roots
155 // We add the map even if there are no trigger procedures, to always maintain the same
156 // storage layout.
157 let map_entries = ecdsa
158 .config
159 .auth_trigger_procedures
160 .iter()
161 .enumerate()
162 .map(|(i, proc_root)| (Word::from([i as u32, 0, 0, 0]), *proc_root));
163
164 // Safe to unwrap because we know that the map keys are unique.
165 storage_slots.push(StorageSlot::Map(StorageMap::with_entries(map_entries).unwrap()));
166
167 AccountComponent::new(ecdsa_k256_keccak_acl_library(), storage_slots)
168 .expect(
169 "ACL auth component should satisfy the requirements of a valid account component",
170 )
171 .with_supports_all_types()
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use miden_objects::Word;
178 use miden_objects::account::AccountBuilder;
179
180 use super::*;
181 use crate::account::components::WellKnownComponent;
182 use crate::account::wallets::BasicWallet;
183
184 /// Test configuration for parametrized ACL tests
185 struct AclTestConfig {
186 /// Whether to include auth trigger procedures
187 with_procedures: bool,
188 /// Allow unauthorized output notes flag
189 allow_unauthorized_output_notes: bool,
190 /// Allow unauthorized input notes flag
191 allow_unauthorized_input_notes: bool,
192 /// Expected slot 1 value [num_procs, allow_output, allow_input, 0]
193 expected_slot_1: Word,
194 }
195
196 /// Helper function to get the basic wallet procedures for testing
197 fn get_basic_wallet_procedures() -> Vec<Word> {
198 // Get the two trigger procedures from BasicWallet: `receive_asset`, `move_asset_to_note`.
199 let procedures: Vec<Word> = WellKnownComponent::BasicWallet.procedure_digests().collect();
200
201 assert_eq!(procedures.len(), 2);
202 procedures
203 }
204
205 /// Parametrized test helper for ACL component testing
206 fn test_acl_component(config: AclTestConfig) {
207 let public_key = PublicKeyCommitment::from(Word::empty());
208
209 // Build the configuration
210 let mut acl_config = AuthEcdsaK256KeccakAclConfig::new()
211 .with_allow_unauthorized_output_notes(config.allow_unauthorized_output_notes)
212 .with_allow_unauthorized_input_notes(config.allow_unauthorized_input_notes);
213
214 let auth_trigger_procedures = if config.with_procedures {
215 let procedures = get_basic_wallet_procedures();
216 acl_config = acl_config.with_auth_trigger_procedures(procedures.clone());
217 procedures
218 } else {
219 vec![]
220 };
221
222 // Create component and account
223 let component =
224 AuthEcdsaK256KeccakAcl::new(public_key, acl_config).expect("component creation failed");
225
226 let account = AccountBuilder::new([0; 32])
227 .with_auth_component(component)
228 .with_component(BasicWallet)
229 .build()
230 .expect("account building failed");
231
232 // Assert public key in slot 0
233 let public_key_slot = account.storage().get_item(0).expect("storage slot 0 access failed");
234 assert_eq!(public_key_slot, public_key.into());
235
236 // Assert configuration in slot 1
237 let slot_1 = account.storage().get_item(1).expect("storage slot 1 access failed");
238 assert_eq!(slot_1, config.expected_slot_1);
239
240 // Assert procedure roots in map (slot 2)
241 if config.with_procedures {
242 for (i, expected_proc_root) in auth_trigger_procedures.iter().enumerate() {
243 let proc_root = account
244 .storage()
245 .get_map_item(2, Word::from([i as u32, 0, 0, 0]))
246 .expect("storage map access failed");
247 assert_eq!(proc_root, *expected_proc_root);
248 }
249 } else {
250 // When no procedures, the map should return empty for key [0,0,0,0]
251 let proc_root = account
252 .storage()
253 .get_map_item(2, Word::empty())
254 .expect("storage map access failed");
255 assert_eq!(proc_root, Word::empty());
256 }
257 }
258
259 /// Test ACL component with no procedures and both authorization flags set to false
260 #[test]
261 fn test_ecdsa_k256_keccak_acl_no_procedures() {
262 test_acl_component(AclTestConfig {
263 with_procedures: false,
264 allow_unauthorized_output_notes: false,
265 allow_unauthorized_input_notes: false,
266 expected_slot_1: Word::empty(), // [0, 0, 0, 0]
267 });
268 }
269
270 /// Test ACL component with two procedures and both authorization flags set to false
271 #[test]
272 fn test_ecdsa_k256_keccak_acl_with_two_procedures() {
273 test_acl_component(AclTestConfig {
274 with_procedures: true,
275 allow_unauthorized_output_notes: false,
276 allow_unauthorized_input_notes: false,
277 expected_slot_1: Word::from([2u32, 0, 0, 0]),
278 });
279 }
280
281 /// Test ACL component with no procedures and allow_unauthorized_output_notes set to true
282 #[test]
283 fn test_ecdsa_k256_keccak_acl_with_allow_unauthorized_output_notes() {
284 test_acl_component(AclTestConfig {
285 with_procedures: false,
286 allow_unauthorized_output_notes: true,
287 allow_unauthorized_input_notes: false,
288 expected_slot_1: Word::from([0u32, 1, 0, 0]),
289 });
290 }
291
292 /// Test ACL component with two procedures and allow_unauthorized_output_notes set to true
293 #[test]
294 fn test_ecdsa_k256_keccak_acl_with_procedures_and_allow_unauthorized_output_notes() {
295 test_acl_component(AclTestConfig {
296 with_procedures: true,
297 allow_unauthorized_output_notes: true,
298 allow_unauthorized_input_notes: false,
299 expected_slot_1: Word::from([2u32, 1, 0, 0]),
300 });
301 }
302
303 /// Test ACL component with no procedures and allow_unauthorized_input_notes set to true
304 #[test]
305 fn test_ecdsa_k256_keccak_acl_with_allow_unauthorized_input_notes() {
306 test_acl_component(AclTestConfig {
307 with_procedures: false,
308 allow_unauthorized_output_notes: false,
309 allow_unauthorized_input_notes: true,
310 expected_slot_1: Word::from([0u32, 0, 1, 0]),
311 });
312 }
313
314 /// Test ACL component with two procedures and both authorization flags set to true
315 #[test]
316 fn test_ecdsa_k256_keccak_acl_with_both_allow_flags() {
317 test_acl_component(AclTestConfig {
318 with_procedures: true,
319 allow_unauthorized_output_notes: true,
320 allow_unauthorized_input_notes: true,
321 expected_slot_1: Word::from([2u32, 1, 1, 0]),
322 });
323 }
324}