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 AccountType,
15 StorageMap,
16 StorageMapKey,
17 StorageSlot,
18 StorageSlotName,
19};
20use miden_protocol::errors::AccountError;
21use miden_protocol::utils::sync::LazyLock;
22use miden_protocol::{Felt, Word};
23
24use crate::account::components::singlesig_acl_library;
25
26static PUBKEY_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
30 StorageSlotName::new("miden::standards::auth::singlesig_acl::pub_key")
31 .expect("storage slot name should be valid")
32});
33
34static SCHEME_ID_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
35 StorageSlotName::new("miden::standards::auth::singlesig_acl::scheme")
36 .expect("storage slot name should be valid")
37});
38
39static CONFIG_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
40 StorageSlotName::new("miden::standards::auth::singlesig_acl::config")
41 .expect("storage slot name should be valid")
42});
43
44static TRIGGER_PROCEDURE_ROOT_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
45 StorageSlotName::new("miden::standards::auth::singlesig_acl::trigger_procedure_roots")
46 .expect("storage slot name should be valid")
47});
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct AuthSingleSigAclConfig {
52 pub auth_trigger_procedures: Vec<Word>,
54 pub allow_unauthorized_output_notes: bool,
57 pub allow_unauthorized_input_notes: bool,
60}
61
62impl AuthSingleSigAclConfig {
63 pub fn new() -> Self {
66 Self {
67 auth_trigger_procedures: vec![],
68 allow_unauthorized_output_notes: false,
69 allow_unauthorized_input_notes: false,
70 }
71 }
72
73 pub fn with_auth_trigger_procedures(mut self, procedures: Vec<Word>) -> Self {
75 self.auth_trigger_procedures = procedures;
76 self
77 }
78
79 pub fn with_allow_unauthorized_output_notes(mut self, allow: bool) -> Self {
81 self.allow_unauthorized_output_notes = allow;
82 self
83 }
84
85 pub fn with_allow_unauthorized_input_notes(mut self, allow: bool) -> Self {
87 self.allow_unauthorized_input_notes = allow;
88 self
89 }
90}
91
92impl Default for AuthSingleSigAclConfig {
93 fn default() -> Self {
94 Self::new()
95 }
96}
97
98pub struct AuthSingleSigAcl {
153 pub_key: PublicKeyCommitment,
154 auth_scheme: AuthScheme,
155 config: AuthSingleSigAclConfig,
156}
157
158impl AuthSingleSigAcl {
159 pub const NAME: &'static str = "miden::standards::components::auth::singlesig_acl";
161 pub fn new(
167 pub_key: PublicKeyCommitment,
168 auth_scheme: AuthScheme,
169 config: AuthSingleSigAclConfig,
170 ) -> Result<Self, AccountError> {
171 let max_procedures = AccountCode::MAX_NUM_PROCEDURES;
172 if config.auth_trigger_procedures.len() > max_procedures {
173 return Err(AccountError::other(format!(
174 "Cannot track more than {max_procedures} procedures (account limit)"
175 )));
176 }
177
178 Ok(Self { pub_key, auth_scheme, config })
179 }
180
181 pub fn public_key_slot() -> &'static StorageSlotName {
183 &PUBKEY_SLOT_NAME
184 }
185
186 pub fn scheme_id_slot() -> &'static StorageSlotName {
188 &SCHEME_ID_SLOT_NAME
189 }
190
191 pub fn config_slot() -> &'static StorageSlotName {
193 &CONFIG_SLOT_NAME
194 }
195
196 pub fn trigger_procedure_roots_slot() -> &'static StorageSlotName {
198 &TRIGGER_PROCEDURE_ROOT_SLOT_NAME
199 }
200
201 pub fn public_key_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
203 (
204 Self::public_key_slot().clone(),
205 StorageSlotSchema::value("Public key commitment", SchemaType::pub_key()),
206 )
207 }
208
209 pub fn config_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
211 (
212 Self::config_slot().clone(),
213 StorageSlotSchema::value(
214 "ACL configuration",
215 [
216 FeltSchema::u32("num_trigger_procs").with_default(Felt::new(0)),
217 FeltSchema::bool("allow_unauthorized_output_notes").with_default(Felt::new(0)),
218 FeltSchema::bool("allow_unauthorized_input_notes").with_default(Felt::new(0)),
219 FeltSchema::new_void(),
220 ],
221 ),
222 )
223 }
224
225 pub fn auth_scheme_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
227 (
228 Self::scheme_id_slot().clone(),
229 StorageSlotSchema::value("Scheme ID", SchemaType::auth_scheme()),
230 )
231 }
232
233 pub fn trigger_procedure_roots_slot_schema() -> (StorageSlotName, StorageSlotSchema) {
235 (
236 Self::trigger_procedure_roots_slot().clone(),
237 StorageSlotSchema::map(
238 "Trigger procedure roots",
239 SchemaType::u32(),
240 SchemaType::native_word(),
241 ),
242 )
243 }
244
245 pub fn component_metadata() -> AccountComponentMetadata {
247 let storage_schema = StorageSchema::new(vec![
248 Self::public_key_slot_schema(),
249 Self::auth_scheme_slot_schema(),
250 Self::config_slot_schema(),
251 Self::trigger_procedure_roots_slot_schema(),
252 ])
253 .expect("storage schema should be valid");
254
255 AccountComponentMetadata::new(Self::NAME, AccountType::all())
256 .with_description(
257 "Authentication component with procedure-based ACL using ECDSA K256 Keccak or Falcon512 Poseidon2 signature scheme",
258 )
259 .with_storage_schema(storage_schema)
260 }
261}
262
263impl From<AuthSingleSigAcl> for AccountComponent {
264 fn from(singlesig_acl: AuthSingleSigAcl) -> Self {
265 let mut storage_slots = Vec::with_capacity(3);
266
267 storage_slots.push(StorageSlot::with_value(
269 AuthSingleSigAcl::public_key_slot().clone(),
270 singlesig_acl.pub_key.into(),
271 ));
272
273 storage_slots.push(StorageSlot::with_value(
275 AuthSingleSigAcl::scheme_id_slot().clone(),
276 Word::from([singlesig_acl.auth_scheme.as_u8(), 0, 0, 0]),
277 ));
278
279 let num_procs = singlesig_acl.config.auth_trigger_procedures.len() as u32;
281 storage_slots.push(StorageSlot::with_value(
282 AuthSingleSigAcl::config_slot().clone(),
283 Word::from([
284 num_procs,
285 u32::from(singlesig_acl.config.allow_unauthorized_output_notes),
286 u32::from(singlesig_acl.config.allow_unauthorized_input_notes),
287 0,
288 ]),
289 ));
290
291 let map_entries = singlesig_acl
295 .config
296 .auth_trigger_procedures
297 .iter()
298 .enumerate()
299 .map(|(i, proc_root)| (StorageMapKey::from_index(i as u32), *proc_root));
300
301 storage_slots.push(StorageSlot::with_map(
303 AuthSingleSigAcl::trigger_procedure_roots_slot().clone(),
304 StorageMap::with_entries(map_entries).unwrap(),
305 ));
306
307 let metadata = AuthSingleSigAcl::component_metadata();
308
309 AccountComponent::new(singlesig_acl_library(), storage_slots, metadata).expect(
310 "singlesig ACL component should satisfy the requirements of a valid account component",
311 )
312 }
313}
314
315#[cfg(test)]
319mod tests {
320 use miden_protocol::Word;
321 use miden_protocol::account::AccountBuilder;
322
323 use super::*;
324 use crate::account::components::StandardAccountComponent;
325 use crate::account::wallets::BasicWallet;
326
327 struct AclTestConfig {
329 with_procedures: bool,
331 allow_unauthorized_output_notes: bool,
333 allow_unauthorized_input_notes: bool,
335 expected_config_slot: Word,
337 }
338
339 fn get_basic_wallet_procedures() -> Vec<Word> {
341 let procedures: Vec<Word> =
343 StandardAccountComponent::BasicWallet.procedure_digests().collect();
344
345 assert_eq!(procedures.len(), 2);
346 procedures
347 }
348
349 fn test_acl_component(config: AclTestConfig) {
351 let public_key = PublicKeyCommitment::from(Word::empty());
352 let auth_scheme = AuthScheme::Falcon512Poseidon2;
353
354 let mut acl_config = AuthSingleSigAclConfig::new()
356 .with_allow_unauthorized_output_notes(config.allow_unauthorized_output_notes)
357 .with_allow_unauthorized_input_notes(config.allow_unauthorized_input_notes);
358
359 let auth_trigger_procedures = if config.with_procedures {
360 let procedures = get_basic_wallet_procedures();
361 acl_config = acl_config.with_auth_trigger_procedures(procedures.clone());
362 procedures
363 } else {
364 vec![]
365 };
366
367 let component = AuthSingleSigAcl::new(public_key, auth_scheme, acl_config)
369 .expect("component creation failed");
370
371 let account = AccountBuilder::new([0; 32])
372 .with_auth_component(component)
373 .with_component(BasicWallet)
374 .build()
375 .expect("account building failed");
376
377 let public_key_slot = account
379 .storage()
380 .get_item(AuthSingleSigAcl::public_key_slot())
381 .expect("public key storage slot access failed");
382 assert_eq!(public_key_slot, public_key.into());
383
384 let config_slot = account
386 .storage()
387 .get_item(AuthSingleSigAcl::config_slot())
388 .expect("config storage slot access failed");
389 assert_eq!(config_slot, config.expected_config_slot);
390
391 if config.with_procedures {
393 for (i, expected_proc_root) in auth_trigger_procedures.iter().enumerate() {
394 let proc_root = account
395 .storage()
396 .get_map_item(
397 AuthSingleSigAcl::trigger_procedure_roots_slot(),
398 Word::from([i as u32, 0, 0, 0]),
399 )
400 .expect("storage map access failed");
401 assert_eq!(proc_root, *expected_proc_root);
402 }
403 } else {
404 let proc_root = account
406 .storage()
407 .get_map_item(AuthSingleSigAcl::trigger_procedure_roots_slot(), Word::empty())
408 .expect("storage map access failed");
409 assert_eq!(proc_root, Word::empty());
410 }
411 }
412
413 #[test]
415 fn test_singlesig_acl_no_procedures() {
416 test_acl_component(AclTestConfig {
417 with_procedures: false,
418 allow_unauthorized_output_notes: false,
419 allow_unauthorized_input_notes: false,
420 expected_config_slot: Word::empty(), });
422 }
423
424 #[test]
426 fn test_singlesig_acl_with_two_procedures() {
427 test_acl_component(AclTestConfig {
428 with_procedures: true,
429 allow_unauthorized_output_notes: false,
430 allow_unauthorized_input_notes: false,
431 expected_config_slot: Word::from([2u32, 0, 0, 0]),
432 });
433 }
434
435 #[test]
437 fn test_ecdsa_k256_keccak_acl_with_allow_unauthorized_output_notes() {
438 test_acl_component(AclTestConfig {
439 with_procedures: false,
440 allow_unauthorized_output_notes: true,
441 allow_unauthorized_input_notes: false,
442 expected_config_slot: Word::from([0u32, 1, 0, 0]),
443 });
444 }
445
446 #[test]
448 fn test_ecdsa_k256_keccak_acl_with_procedures_and_allow_unauthorized_output_notes() {
449 test_acl_component(AclTestConfig {
450 with_procedures: true,
451 allow_unauthorized_output_notes: true,
452 allow_unauthorized_input_notes: false,
453 expected_config_slot: Word::from([2u32, 1, 0, 0]),
454 });
455 }
456
457 #[test]
459 fn test_ecdsa_k256_keccak_acl_with_allow_unauthorized_input_notes() {
460 test_acl_component(AclTestConfig {
461 with_procedures: false,
462 allow_unauthorized_output_notes: false,
463 allow_unauthorized_input_notes: true,
464 expected_config_slot: Word::from([0u32, 0, 1, 0]),
465 });
466 }
467
468 #[test]
470 fn test_ecdsa_k256_keccak_acl_with_both_allow_flags() {
471 test_acl_component(AclTestConfig {
472 with_procedures: true,
473 allow_unauthorized_output_notes: true,
474 allow_unauthorized_input_notes: true,
475 expected_config_slot: Word::from([2u32, 1, 1, 0]),
476 });
477 }
478}