miden_standards/account/auth/
singlesig_acl.rs1use alloc::vec::Vec;
2
3use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment};
4use miden_protocol::account::component::{
5 AccountComponentCode,
6 AccountComponentMetadata,
7 FeltSchema,
8 SchemaType,
9 StorageSchema,
10 StorageSlotSchema,
11};
12use miden_protocol::account::{
13 AccountCode,
14 AccountComponent,
15 AccountComponentName,
16 AccountProcedureRoot,
17 StorageMap,
18 StorageMapKey,
19 StorageSlot,
20 StorageSlotName,
21};
22use miden_protocol::errors::AccountError;
23use miden_protocol::utils::sync::LazyLock;
24use miden_protocol::{Felt, Word};
25
26use crate::account::account_component_code;
27
28account_component_code!(SINGLESIG_ACL_CODE, "auth/singlesig_acl.masl");
29
30static PUBKEY_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
34 StorageSlotName::new("miden::standards::auth::singlesig_acl::pub_key")
35 .expect("storage slot name should be valid")
36});
37
38static SCHEME_ID_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
39 StorageSlotName::new("miden::standards::auth::singlesig_acl::scheme")
40 .expect("storage slot name should be valid")
41});
42
43static CONFIG_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
44 StorageSlotName::new("miden::standards::auth::singlesig_acl::config")
45 .expect("storage slot name should be valid")
46});
47
48static TRIGGER_PROCEDURE_ROOT_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
49 StorageSlotName::new("miden::standards::auth::singlesig_acl::trigger_procedure_roots")
50 .expect("storage slot name should be valid")
51});
52
53#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct AuthSingleSigAclConfig {
56 pub auth_trigger_procedures: Vec<AccountProcedureRoot>,
58 pub allow_unauthorized_output_notes: bool,
61 pub allow_unauthorized_input_notes: bool,
64}
65
66impl AuthSingleSigAclConfig {
67 pub fn new() -> Self {
70 Self {
71 auth_trigger_procedures: vec![],
72 allow_unauthorized_output_notes: false,
73 allow_unauthorized_input_notes: false,
74 }
75 }
76
77 pub fn with_auth_trigger_procedures(mut self, procedures: Vec<AccountProcedureRoot>) -> Self {
79 self.auth_trigger_procedures = procedures;
80 self
81 }
82
83 pub fn with_allow_unauthorized_output_notes(mut self, allow: bool) -> Self {
85 self.allow_unauthorized_output_notes = allow;
86 self
87 }
88
89 pub fn with_allow_unauthorized_input_notes(mut self, allow: bool) -> Self {
91 self.allow_unauthorized_input_notes = allow;
92 self
93 }
94}
95
96impl Default for AuthSingleSigAclConfig {
97 fn default() -> Self {
98 Self::new()
99 }
100}
101
102pub struct AuthSingleSigAcl {
155 pub_key: PublicKeyCommitment,
156 auth_scheme: AuthScheme,
157 config: AuthSingleSigAclConfig,
158}
159
160impl AuthSingleSigAcl {
161 pub const NAME: &'static str = "miden::standards::components::auth::singlesig_acl";
163
164 pub const fn name() -> AccountComponentName {
166 AccountComponentName::from_static_str(Self::NAME)
167 }
168
169 pub fn code() -> &'static AccountComponentCode {
171 &SINGLESIG_ACL_CODE
172 }
173
174 pub fn new(
180 pub_key: PublicKeyCommitment,
181 auth_scheme: AuthScheme,
182 config: AuthSingleSigAclConfig,
183 ) -> Result<Self, AccountError> {
184 let max_procedures = AccountCode::MAX_NUM_PROCEDURES;
185 if config.auth_trigger_procedures.len() > max_procedures {
186 return Err(AccountError::other(format!(
187 "Cannot track more than {max_procedures} procedures (account limit)"
188 )));
189 }
190
191 Ok(Self { pub_key, auth_scheme, config })
192 }
193
194 pub fn public_key_slot() -> &'static StorageSlotName {
196 &PUBKEY_SLOT_NAME
197 }
198
199 pub fn scheme_id_slot() -> &'static StorageSlotName {
201 &SCHEME_ID_SLOT_NAME
202 }
203
204 pub fn config_slot() -> &'static StorageSlotName {
206 &CONFIG_SLOT_NAME
207 }
208
209 pub fn trigger_procedure_roots_slot() -> &'static StorageSlotName {
211 &TRIGGER_PROCEDURE_ROOT_SLOT_NAME
212 }
213
214 pub fn public_key_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
216 (
217 Self::public_key_slot().clone(),
218 StorageSlotSchema::value("Public key commitment", SchemaType::pub_key()),
219 )
220 }
221
222 pub fn config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
224 (
225 Self::config_slot().clone(),
226 StorageSlotSchema::value(
227 "ACL configuration",
228 [
229 FeltSchema::u32("num_trigger_procs").with_default(Felt::ZERO),
230 FeltSchema::bool("allow_unauthorized_output_notes").with_default(Felt::ZERO),
231 FeltSchema::bool("allow_unauthorized_input_notes").with_default(Felt::ZERO),
232 FeltSchema::new_void(),
233 ],
234 ),
235 )
236 }
237
238 pub fn auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
240 (
241 Self::scheme_id_slot().clone(),
242 StorageSlotSchema::value("Scheme ID", SchemaType::auth_scheme()),
243 )
244 }
245
246 pub fn trigger_procedure_roots_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
248 (
249 Self::trigger_procedure_roots_slot().clone(),
250 StorageSlotSchema::map(
251 "Trigger procedure roots",
252 SchemaType::u32(),
253 SchemaType::native_word(),
254 ),
255 )
256 }
257
258 pub fn component_metadata() -> AccountComponentMetadata {
260 let storage_schema = StorageSchema::new(vec![
261 Self::public_key_slot_schema(),
262 Self::auth_scheme_slot_schema(),
263 Self::config_slot_schema(),
264 Self::trigger_procedure_roots_slot_schema(),
265 ])
266 .expect("storage schema should be valid");
267
268 AccountComponentMetadata::new(Self::NAME)
269 .with_description(
270 "Authentication component with procedure-based ACL using ECDSA K256 Keccak or Falcon512 Poseidon2 signature scheme",
271 )
272 .with_storage_schema(storage_schema)
273 }
274}
275
276impl From<AuthSingleSigAcl> for AccountComponent {
277 fn from(singlesig_acl: AuthSingleSigAcl) -> Self {
278 let mut storage_slots = Vec::with_capacity(3);
279
280 storage_slots.push(StorageSlot::with_value(
282 AuthSingleSigAcl::public_key_slot().clone(),
283 singlesig_acl.pub_key.into(),
284 ));
285
286 storage_slots.push(StorageSlot::with_value(
288 AuthSingleSigAcl::scheme_id_slot().clone(),
289 Word::from([singlesig_acl.auth_scheme.as_u8(), 0, 0, 0]),
290 ));
291
292 let num_procs = singlesig_acl.config.auth_trigger_procedures.len() as u32;
294 storage_slots.push(StorageSlot::with_value(
295 AuthSingleSigAcl::config_slot().clone(),
296 Word::from([
297 num_procs,
298 u32::from(singlesig_acl.config.allow_unauthorized_output_notes),
299 u32::from(singlesig_acl.config.allow_unauthorized_input_notes),
300 0,
301 ]),
302 ));
303
304 let map_entries = singlesig_acl
308 .config
309 .auth_trigger_procedures
310 .iter()
311 .enumerate()
312 .map(|(i, proc_root)| (StorageMapKey::from_index(i as u32), proc_root.as_word()));
313
314 storage_slots.push(StorageSlot::with_map(
316 AuthSingleSigAcl::trigger_procedure_roots_slot().clone(),
317 StorageMap::with_entries(map_entries).unwrap(),
318 ));
319
320 let metadata = AuthSingleSigAcl::component_metadata();
321
322 AccountComponent::new(AuthSingleSigAcl::code().clone(), storage_slots, metadata).expect(
323 "singlesig ACL component should satisfy the requirements of a valid account component",
324 )
325 }
326}
327
328#[cfg(test)]
332mod tests {
333 use miden_protocol::Word;
334 use miden_protocol::account::AccountBuilder;
335
336 use super::*;
337 use crate::account::components::StandardAccountComponent;
338 use crate::account::wallets::BasicWallet;
339
340 struct AclTestConfig {
342 with_procedures: bool,
344 allow_unauthorized_output_notes: bool,
346 allow_unauthorized_input_notes: bool,
348 expected_config_slot: Word,
350 }
351
352 fn get_basic_wallet_procedures() -> Vec<AccountProcedureRoot> {
354 let procedures: Vec<AccountProcedureRoot> =
356 StandardAccountComponent::BasicWallet.procedure_roots().collect();
357
358 assert_eq!(procedures.len(), 2);
359 procedures
360 }
361
362 fn test_acl_component(config: AclTestConfig) {
364 let public_key = PublicKeyCommitment::from(Word::empty());
365 let auth_scheme = AuthScheme::Falcon512Poseidon2;
366
367 let mut acl_config = AuthSingleSigAclConfig::new()
369 .with_allow_unauthorized_output_notes(config.allow_unauthorized_output_notes)
370 .with_allow_unauthorized_input_notes(config.allow_unauthorized_input_notes);
371
372 let auth_trigger_procedures: Vec<AccountProcedureRoot> = if config.with_procedures {
373 let procedures = get_basic_wallet_procedures();
374 acl_config = acl_config.with_auth_trigger_procedures(procedures.clone());
375 procedures
376 } else {
377 vec![]
378 };
379
380 let component = AuthSingleSigAcl::new(public_key, auth_scheme, acl_config)
382 .expect("component creation failed");
383
384 let account = AccountBuilder::new([0; 32])
385 .with_auth_component(component)
386 .with_component(BasicWallet)
387 .build()
388 .expect("account building failed");
389
390 let public_key_slot = account
392 .storage()
393 .get_item(AuthSingleSigAcl::public_key_slot())
394 .expect("public key storage slot access failed");
395 assert_eq!(public_key_slot, public_key.into());
396
397 let config_slot = account
399 .storage()
400 .get_item(AuthSingleSigAcl::config_slot())
401 .expect("config storage slot access failed");
402 assert_eq!(config_slot, config.expected_config_slot);
403
404 if config.with_procedures {
406 for (i, expected_proc_root) in auth_trigger_procedures.iter().enumerate() {
407 let proc_root = account
408 .storage()
409 .get_map_item(
410 AuthSingleSigAcl::trigger_procedure_roots_slot(),
411 Word::from([i as u32, 0, 0, 0]),
412 )
413 .expect("storage map access failed");
414 assert_eq!(proc_root, expected_proc_root.as_word());
415 }
416 } else {
417 let proc_root = account
419 .storage()
420 .get_map_item(AuthSingleSigAcl::trigger_procedure_roots_slot(), Word::empty())
421 .expect("storage map access failed");
422 assert_eq!(proc_root, Word::empty());
423 }
424 }
425
426 #[test]
428 fn test_singlesig_acl_no_procedures() {
429 test_acl_component(AclTestConfig {
430 with_procedures: false,
431 allow_unauthorized_output_notes: false,
432 allow_unauthorized_input_notes: false,
433 expected_config_slot: Word::empty(), });
435 }
436
437 #[test]
439 fn test_singlesig_acl_with_two_procedures() {
440 test_acl_component(AclTestConfig {
441 with_procedures: true,
442 allow_unauthorized_output_notes: false,
443 allow_unauthorized_input_notes: false,
444 expected_config_slot: Word::from([2u32, 0, 0, 0]),
445 });
446 }
447
448 #[test]
450 fn test_ecdsa_k256_keccak_acl_with_allow_unauthorized_output_notes() {
451 test_acl_component(AclTestConfig {
452 with_procedures: false,
453 allow_unauthorized_output_notes: true,
454 allow_unauthorized_input_notes: false,
455 expected_config_slot: Word::from([0u32, 1, 0, 0]),
456 });
457 }
458
459 #[test]
461 fn test_ecdsa_k256_keccak_acl_with_procedures_and_allow_unauthorized_output_notes() {
462 test_acl_component(AclTestConfig {
463 with_procedures: true,
464 allow_unauthorized_output_notes: true,
465 allow_unauthorized_input_notes: false,
466 expected_config_slot: Word::from([2u32, 1, 0, 0]),
467 });
468 }
469
470 #[test]
472 fn test_ecdsa_k256_keccak_acl_with_allow_unauthorized_input_notes() {
473 test_acl_component(AclTestConfig {
474 with_procedures: false,
475 allow_unauthorized_output_notes: false,
476 allow_unauthorized_input_notes: true,
477 expected_config_slot: Word::from([0u32, 0, 1, 0]),
478 });
479 }
480
481 #[test]
483 fn test_ecdsa_k256_keccak_acl_with_both_allow_flags() {
484 test_acl_component(AclTestConfig {
485 with_procedures: true,
486 allow_unauthorized_output_notes: true,
487 allow_unauthorized_input_notes: true,
488 expected_config_slot: Word::from([2u32, 1, 1, 0]),
489 });
490 }
491}