miden_standards/account/auth/
singlesig_acl.rs1use alloc::vec::Vec;
2
3use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment};
4use miden_protocol::account::component::{
5 AccountComponentMetadata,
6 FeltSchema,
7 SchemaType,
8 StorageSchema,
9 StorageSlotSchema,
10};
11use miden_protocol::account::{
12 AccountCode,
13 AccountComponent,
14 StorageMap,
15 StorageMapKey,
16 StorageSlot,
17 StorageSlotName,
18};
19use miden_protocol::errors::AccountError;
20use miden_protocol::utils::sync::LazyLock;
21use miden_protocol::{Felt, Word};
22
23use crate::account::components::singlesig_acl_library;
24
25static PUBKEY_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
26 StorageSlotName::new("miden::standards::auth::singlesig_acl::pub_key")
27 .expect("storage slot name should be valid")
28});
29
30static SCHEME_ID_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
31 StorageSlotName::new("miden::standards::auth::singlesig_acl::scheme")
32 .expect("storage slot name should be valid")
33});
34
35static CONFIG_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
36 StorageSlotName::new("miden::standards::auth::singlesig_acl::config")
37 .expect("storage slot name should be valid")
38});
39
40static TRIGGER_PROCEDURE_ROOT_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
41 StorageSlotName::new("miden::standards::auth::singlesig_acl::trigger_procedure_roots")
42 .expect("storage slot name should be valid")
43});
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct AuthSingleSigAclConfig {
48 pub auth_trigger_procedures: Vec<Word>,
50 pub allow_unauthorized_output_notes: bool,
53 pub allow_unauthorized_input_notes: bool,
56}
57
58impl AuthSingleSigAclConfig {
59 pub fn new() -> Self {
62 Self {
63 auth_trigger_procedures: vec![],
64 allow_unauthorized_output_notes: false,
65 allow_unauthorized_input_notes: false,
66 }
67 }
68
69 pub fn with_auth_trigger_procedures(mut self, procedures: Vec<Word>) -> Self {
71 self.auth_trigger_procedures = procedures;
72 self
73 }
74
75 pub fn with_allow_unauthorized_output_notes(mut self, allow: bool) -> Self {
77 self.allow_unauthorized_output_notes = allow;
78 self
79 }
80
81 pub fn with_allow_unauthorized_input_notes(mut self, allow: bool) -> Self {
83 self.allow_unauthorized_input_notes = allow;
84 self
85 }
86}
87
88impl Default for AuthSingleSigAclConfig {
89 fn default() -> Self {
90 Self::new()
91 }
92}
93
94pub struct AuthSingleSigAcl {
149 pub_key: PublicKeyCommitment,
150 auth_scheme: AuthScheme,
151 config: AuthSingleSigAclConfig,
152}
153
154impl AuthSingleSigAcl {
155 pub const NAME: &'static str = "miden::auth::singlesig_acl";
157 pub fn new(
163 pub_key: PublicKeyCommitment,
164 auth_scheme: AuthScheme,
165 config: AuthSingleSigAclConfig,
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 { pub_key, auth_scheme, config })
175 }
176
177 pub fn public_key_slot() -> &'static StorageSlotName {
179 &PUBKEY_SLOT_NAME
180 }
181
182 pub fn scheme_id_slot() -> &'static StorageSlotName {
184 &SCHEME_ID_SLOT_NAME
185 }
186
187 pub fn config_slot() -> &'static StorageSlotName {
189 &CONFIG_SLOT_NAME
190 }
191
192 pub fn trigger_procedure_roots_slot() -> &'static StorageSlotName {
194 &TRIGGER_PROCEDURE_ROOT_SLOT_NAME
195 }
196
197 pub fn public_key_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
199 (
200 Self::public_key_slot().clone(),
201 StorageSlotSchema::value("Public key commitment", SchemaType::pub_key()),
202 )
203 }
204
205 pub fn config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
207 (
208 Self::config_slot().clone(),
209 StorageSlotSchema::value(
210 "ACL configuration",
211 [
212 FeltSchema::u32("num_trigger_procs").with_default(Felt::new(0)),
213 FeltSchema::u32("allow_unauthorized_output_notes").with_default(Felt::new(0)),
214 FeltSchema::u32("allow_unauthorized_input_notes").with_default(Felt::new(0)),
215 FeltSchema::new_void(),
216 ],
217 ),
218 )
219 }
220
221 pub fn auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
223 (
224 Self::scheme_id_slot().clone(),
225 StorageSlotSchema::value("Scheme ID", SchemaType::auth_scheme()),
226 )
227 }
228
229 pub fn trigger_procedure_roots_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
231 (
232 Self::trigger_procedure_roots_slot().clone(),
233 StorageSlotSchema::map(
234 "Trigger procedure roots",
235 SchemaType::u32(),
236 SchemaType::native_word(),
237 ),
238 )
239 }
240}
241
242impl From<AuthSingleSigAcl> for AccountComponent {
243 fn from(singlesig_acl: AuthSingleSigAcl) -> Self {
244 let mut storage_slots = Vec::with_capacity(3);
245
246 storage_slots.push(StorageSlot::with_value(
248 AuthSingleSigAcl::public_key_slot().clone(),
249 singlesig_acl.pub_key.into(),
250 ));
251
252 storage_slots.push(StorageSlot::with_value(
254 AuthSingleSigAcl::scheme_id_slot().clone(),
255 Word::from([singlesig_acl.auth_scheme.as_u8(), 0, 0, 0]),
256 ));
257
258 let num_procs = singlesig_acl.config.auth_trigger_procedures.len() as u32;
260 storage_slots.push(StorageSlot::with_value(
261 AuthSingleSigAcl::config_slot().clone(),
262 Word::from([
263 num_procs,
264 u32::from(singlesig_acl.config.allow_unauthorized_output_notes),
265 u32::from(singlesig_acl.config.allow_unauthorized_input_notes),
266 0,
267 ]),
268 ));
269
270 let map_entries = singlesig_acl
274 .config
275 .auth_trigger_procedures
276 .iter()
277 .enumerate()
278 .map(|(i, proc_root)| (StorageMapKey::from_index(i as u32), *proc_root));
279
280 storage_slots.push(StorageSlot::with_map(
282 AuthSingleSigAcl::trigger_procedure_roots_slot().clone(),
283 StorageMap::with_entries(map_entries).unwrap(),
284 ));
285
286 let storage_schema = StorageSchema::new(vec![
287 AuthSingleSigAcl::public_key_slot_schema(),
288 AuthSingleSigAcl::auth_scheme_slot_schema(),
289 AuthSingleSigAcl::config_slot_schema(),
290 AuthSingleSigAcl::trigger_procedure_roots_slot_schema(),
291 ])
292 .expect("storage schema should be valid");
293
294 let metadata = AccountComponentMetadata::new(AuthSingleSigAcl::NAME)
295 .with_description("Authentication component with procedure-based ACL using ECDSA K256 Keccak or Rpo Falcon 512 signature scheme")
296 .with_supports_all_types()
297 .with_storage_schema(storage_schema);
298
299 AccountComponent::new(singlesig_acl_library(), storage_slots, metadata).expect(
300 "singlesig ACL component should satisfy the requirements of a valid account component",
301 )
302 }
303}
304
305#[cfg(test)]
306mod tests {
307 use miden_protocol::Word;
308 use miden_protocol::account::AccountBuilder;
309
310 use super::*;
311 use crate::account::components::StandardAccountComponent;
312 use crate::account::wallets::BasicWallet;
313
314 struct AclTestConfig {
316 with_procedures: bool,
318 allow_unauthorized_output_notes: bool,
320 allow_unauthorized_input_notes: bool,
322 expected_config_slot: Word,
324 }
325
326 fn get_basic_wallet_procedures() -> Vec<Word> {
328 let procedures: Vec<Word> =
330 StandardAccountComponent::BasicWallet.procedure_digests().collect();
331
332 assert_eq!(procedures.len(), 2);
333 procedures
334 }
335
336 fn test_acl_component(config: AclTestConfig) {
338 let public_key = PublicKeyCommitment::from(Word::empty());
339 let auth_scheme = AuthScheme::Falcon512Rpo;
340
341 let mut acl_config = AuthSingleSigAclConfig::new()
343 .with_allow_unauthorized_output_notes(config.allow_unauthorized_output_notes)
344 .with_allow_unauthorized_input_notes(config.allow_unauthorized_input_notes);
345
346 let auth_trigger_procedures = if config.with_procedures {
347 let procedures = get_basic_wallet_procedures();
348 acl_config = acl_config.with_auth_trigger_procedures(procedures.clone());
349 procedures
350 } else {
351 vec![]
352 };
353
354 let component = AuthSingleSigAcl::new(public_key, auth_scheme, acl_config)
356 .expect("component creation failed");
357
358 let account = AccountBuilder::new([0; 32])
359 .with_auth_component(component)
360 .with_component(BasicWallet)
361 .build()
362 .expect("account building failed");
363
364 let public_key_slot = account
366 .storage()
367 .get_item(AuthSingleSigAcl::public_key_slot())
368 .expect("public key storage slot access failed");
369 assert_eq!(public_key_slot, public_key.into());
370
371 let config_slot = account
373 .storage()
374 .get_item(AuthSingleSigAcl::config_slot())
375 .expect("config storage slot access failed");
376 assert_eq!(config_slot, config.expected_config_slot);
377
378 if config.with_procedures {
380 for (i, expected_proc_root) in auth_trigger_procedures.iter().enumerate() {
381 let proc_root = account
382 .storage()
383 .get_map_item(
384 AuthSingleSigAcl::trigger_procedure_roots_slot(),
385 Word::from([i as u32, 0, 0, 0]),
386 )
387 .expect("storage map access failed");
388 assert_eq!(proc_root, *expected_proc_root);
389 }
390 } else {
391 let proc_root = account
393 .storage()
394 .get_map_item(AuthSingleSigAcl::trigger_procedure_roots_slot(), Word::empty())
395 .expect("storage map access failed");
396 assert_eq!(proc_root, Word::empty());
397 }
398 }
399
400 #[test]
402 fn test_singlesig_acl_no_procedures() {
403 test_acl_component(AclTestConfig {
404 with_procedures: false,
405 allow_unauthorized_output_notes: false,
406 allow_unauthorized_input_notes: false,
407 expected_config_slot: Word::empty(), });
409 }
410
411 #[test]
413 fn test_singlesig_acl_with_two_procedures() {
414 test_acl_component(AclTestConfig {
415 with_procedures: true,
416 allow_unauthorized_output_notes: false,
417 allow_unauthorized_input_notes: false,
418 expected_config_slot: Word::from([2u32, 0, 0, 0]),
419 });
420 }
421
422 #[test]
424 fn test_ecdsa_k256_keccak_acl_with_allow_unauthorized_output_notes() {
425 test_acl_component(AclTestConfig {
426 with_procedures: false,
427 allow_unauthorized_output_notes: true,
428 allow_unauthorized_input_notes: false,
429 expected_config_slot: Word::from([0u32, 1, 0, 0]),
430 });
431 }
432
433 #[test]
435 fn test_ecdsa_k256_keccak_acl_with_procedures_and_allow_unauthorized_output_notes() {
436 test_acl_component(AclTestConfig {
437 with_procedures: true,
438 allow_unauthorized_output_notes: true,
439 allow_unauthorized_input_notes: false,
440 expected_config_slot: Word::from([2u32, 1, 0, 0]),
441 });
442 }
443
444 #[test]
446 fn test_ecdsa_k256_keccak_acl_with_allow_unauthorized_input_notes() {
447 test_acl_component(AclTestConfig {
448 with_procedures: false,
449 allow_unauthorized_output_notes: false,
450 allow_unauthorized_input_notes: true,
451 expected_config_slot: Word::from([0u32, 0, 1, 0]),
452 });
453 }
454
455 #[test]
457 fn test_ecdsa_k256_keccak_acl_with_both_allow_flags() {
458 test_acl_component(AclTestConfig {
459 with_procedures: true,
460 allow_unauthorized_output_notes: true,
461 allow_unauthorized_input_notes: true,
462 expected_config_slot: Word::from([2u32, 1, 1, 0]),
463 });
464 }
465}