miden_standards/account/access/
authority.rs1use alloc::collections::BTreeMap;
2use alloc::vec;
3
4use miden_protocol::account::component::{
5 AccountComponentCode,
6 AccountComponentMetadata,
7 FeltSchema,
8 SchemaType,
9 StorageSchema,
10 StorageSlotSchema,
11};
12use miden_protocol::account::{
13 AccountComponent,
14 AccountProcedureRoot,
15 AccountStorage,
16 RoleSymbol,
17 StorageMap,
18 StorageMapKey,
19 StorageSlot,
20 StorageSlotContent,
21 StorageSlotName,
22};
23use miden_protocol::errors::{AccountError, RoleSymbolError};
24use miden_protocol::utils::sync::LazyLock;
25use miden_protocol::{Felt, Word};
26use thiserror::Error;
27
28use crate::account::account_component_code;
29use crate::procedure_root;
30
31account_component_code!(AUTHORITY_CODE, "miden-standards-access-authority.masp");
35
36const AUTHORITY_LIBRARY_PATH: &str = "miden::standards::components::access::authority";
42
43procedure_root!(
44 AUTHORITY_FREEZE,
45 AUTHORITY_LIBRARY_PATH,
46 Authority::FREEZE_PROC_NAME,
47 Authority::code()
48);
49
50procedure_root!(
51 AUTHORITY_UNFREEZE,
52 AUTHORITY_LIBRARY_PATH,
53 Authority::UNFREEZE_PROC_NAME,
54 Authority::code()
55);
56
57static AUTHORITY_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
58 StorageSlotName::new("miden::standards::access::authority::authority_config")
59 .expect("storage slot name should be valid")
60});
61
62static AUTHORITY_PROCEDURE_ROLES_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
63 StorageSlotName::new("miden::standards::access::authority::procedure_roles")
64 .expect("storage slot name should be valid")
65});
66
67const AUTH_CONTROLLED: u8 = 0;
69const OWNER_CONTROLLED: u8 = 1;
71const RBAC_CONTROLLED: u8 = 2;
73
74#[repr(u8)]
117#[derive(Debug, Clone, PartialEq, Eq)]
118#[non_exhaustive]
119pub enum Authority {
120 AuthControlled = AUTH_CONTROLLED,
122 OwnerControlled = OWNER_CONTROLLED,
124 RbacControlled {
132 procedure_roles: BTreeMap<AccountProcedureRoot, RoleSymbol>,
133 } = RBAC_CONTROLLED,
134}
135
136impl Authority {
137 pub const NAME: &'static str = "miden::standards::access::authority";
139
140 const FREEZE_PROC_NAME: &'static str = "freeze";
142 const UNFREEZE_PROC_NAME: &'static str = "unfreeze";
144
145 pub fn code() -> &'static AccountComponentCode {
147 &AUTHORITY_CODE
148 }
149
150 pub fn freeze_root() -> AccountProcedureRoot {
160 *AUTHORITY_FREEZE
161 }
162
163 pub fn unfreeze_root() -> AccountProcedureRoot {
170 *AUTHORITY_UNFREEZE
171 }
172
173 pub fn authority_slot() -> &'static StorageSlotName {
175 &AUTHORITY_SLOT_NAME
176 }
177
178 pub fn procedure_roles_slot() -> &'static StorageSlotName {
180 &AUTHORITY_PROCEDURE_ROLES_SLOT_NAME
181 }
182
183 pub fn try_from_storage(storage: &AccountStorage) -> Result<Self, AuthorityError> {
185 let word = Self::read_config_word(storage)?;
186
187 let discriminant: u8 = word[0]
188 .as_canonical_u64()
189 .try_into()
190 .map_err(|_| AuthorityError::InvalidAuthority(word[0].as_canonical_u64()))?;
191
192 match discriminant {
193 AUTH_CONTROLLED => Ok(Self::AuthControlled),
194 OWNER_CONTROLLED => Ok(Self::OwnerControlled),
195 RBAC_CONTROLLED => {
196 let procedure_roles = Self::read_roles_from_storage(storage)?;
197 Ok(Self::RbacControlled { procedure_roles })
198 },
199 other => Err(AuthorityError::InvalidAuthority(other.into())),
200 }
201 }
202
203 pub fn try_read_frozen(storage: &AccountStorage) -> Result<bool, AuthorityError> {
208 let word = Self::read_config_word(storage)?;
209
210 Ok(word[1] != Felt::ZERO)
211 }
212
213 pub fn component_metadata(&self) -> AccountComponentMetadata {
215 let mut slots = vec![(
216 AUTHORITY_SLOT_NAME.clone(),
217 StorageSlotSchema::value(
218 "Authority configuration",
219 [
220 FeltSchema::u8("authority"),
221 FeltSchema::u8("is_frozen"),
222 FeltSchema::new_void(),
223 FeltSchema::new_void(),
224 ],
225 ),
226 )];
227
228 if matches!(self, Authority::RbacControlled { .. }) {
229 slots.push((
230 AUTHORITY_PROCEDURE_ROLES_SLOT_NAME.clone(),
231 StorageSlotSchema::map(
232 "Per-procedure role assignment (procedure root -> role symbol)",
233 SchemaType::native_word(),
234 SchemaType::role_symbol(),
235 ),
236 ));
237 }
238
239 let storage_schema = StorageSchema::new(slots).expect("storage schema should be valid");
240
241 AccountComponentMetadata::new(Self::NAME)
242 .with_description(
243 "Account-wide authority shared by procedures that gate state-mutating \
244 operations behind auth-only, owner-based, or RBAC role-based checks",
245 )
246 .with_storage_schema(storage_schema)
247 }
248
249 fn as_u8(&self) -> u8 {
254 match self {
255 Authority::AuthControlled => AUTH_CONTROLLED,
256 Authority::OwnerControlled => OWNER_CONTROLLED,
257 Authority::RbacControlled { .. } => RBAC_CONTROLLED,
258 }
259 }
260
261 fn to_word(&self) -> Word {
263 Word::new([Felt::from(self.as_u8()), Felt::ZERO, Felt::ZERO, Felt::ZERO])
264 }
265
266 fn read_config_word(storage: &AccountStorage) -> Result<Word, AuthorityError> {
272 let word = storage
273 .get_item(Self::authority_slot())
274 .map_err(AuthorityError::MissingStorageSlot)?;
275
276 if word[2] != Felt::ZERO || word[3] != Felt::ZERO || word[1].as_canonical_u64() > 1 {
277 return Err(AuthorityError::NonCanonicalConfig);
278 }
279
280 Ok(word)
281 }
282
283 fn read_roles_from_storage(
285 storage: &AccountStorage,
286 ) -> Result<BTreeMap<AccountProcedureRoot, RoleSymbol>, AuthorityError> {
287 let slot = storage
288 .slots()
289 .iter()
290 .find(|slot| slot.name().id() == AUTHORITY_PROCEDURE_ROLES_SLOT_NAME.id())
291 .ok_or(AuthorityError::MissingProcedureRolesSlot)?;
292
293 let StorageSlotContent::Map(map) = slot.content() else {
294 return Err(AuthorityError::MissingProcedureRolesSlot);
295 };
296
297 let mut roles = BTreeMap::new();
298 for (key, value) in map.entries() {
299 if value[1..4].iter().any(|v| *v != Felt::ZERO) {
301 return Err(AuthorityError::NonCanonicalConfig);
302 }
303 let proc_root = AccountProcedureRoot::from_raw(key.as_word());
304 let role = RoleSymbol::try_from(value[0]).map_err(AuthorityError::InvalidRoleSymbol)?;
305 roles.insert(proc_root, role);
306 }
307
308 Ok(roles)
309 }
310}
311
312impl From<Authority> for AccountComponent {
316 fn from(value: Authority) -> Self {
317 let metadata = value.component_metadata();
318
319 let mut slots = vec![StorageSlot::with_value(AUTHORITY_SLOT_NAME.clone(), value.to_word())];
320
321 if let Authority::RbacControlled { procedure_roles } = value {
322 let entries = procedure_roles.into_iter().map(|(proc_root, role)| {
323 (StorageMapKey::new(proc_root.as_word()), role_value_word(&role))
324 });
325 slots.push(StorageSlot::with_map(
326 AUTHORITY_PROCEDURE_ROLES_SLOT_NAME.clone(),
327 StorageMap::with_entries(entries)
328 .expect("authority procedure-roles map should be valid"),
329 ));
330 }
331
332 AccountComponent::new(Authority::code().clone(), slots, metadata).expect(
333 "authority component should satisfy the requirements of a valid account component",
334 )
335 }
336}
337
338fn role_value_word(role: &RoleSymbol) -> Word {
340 Word::new([role.into(), Felt::ZERO, Felt::ZERO, Felt::ZERO])
341}
342
343#[derive(Debug, Error)]
348pub enum AuthorityError {
349 #[error("invalid authority value: {0}")]
350 InvalidAuthority(u64),
351 #[error("authority configuration word is not in canonical form")]
352 NonCanonicalConfig,
353 #[error("invalid role symbol in authority storage")]
354 InvalidRoleSymbol(#[source] RoleSymbolError),
355 #[error("failed to read authority slot from storage")]
356 MissingStorageSlot(#[source] AccountError),
357 #[error("authority procedure-roles slot is missing or not a map")]
358 MissingProcedureRolesSlot,
359}
360
361#[cfg(test)]
362mod tests {
363 use assert_matches::assert_matches;
364
365 use super::*;
366
367 const ROLE_KEY_WORD: [u32; 4] = [1, 2, 3, 4];
369
370 fn storage_with_config(word: Word) -> AccountStorage {
372 let slot = StorageSlot::with_value(Authority::authority_slot().clone(), word);
373 AccountStorage::new(vec![slot]).expect("storage should be valid")
374 }
375
376 fn rbac_storage_with_role_value(role_value: Word) -> AccountStorage {
379 let config = StorageSlot::with_value(
380 Authority::authority_slot().clone(),
381 Word::from([u32::from(RBAC_CONTROLLED), 0, 0, 0]),
382 );
383 let key = StorageMapKey::new(Word::from(ROLE_KEY_WORD));
384 let map = StorageMap::with_entries([(key, role_value)]).expect("map should be valid");
385 let roles = StorageSlot::with_map(Authority::procedure_roles_slot().clone(), map);
386 AccountStorage::new(vec![config, roles]).expect("storage should be valid")
387 }
388
389 #[test]
390 fn canonical_config_is_accepted() {
391 let storage = storage_with_config(Word::from([u32::from(AUTH_CONTROLLED), 0, 0, 0]));
393 assert_eq!(Authority::try_from_storage(&storage).unwrap(), Authority::AuthControlled);
394 assert!(!Authority::try_read_frozen(&storage).unwrap());
395
396 let storage = storage_with_config(Word::from([u32::from(OWNER_CONTROLLED), 1, 0, 0]));
398 assert_eq!(Authority::try_from_storage(&storage).unwrap(), Authority::OwnerControlled);
399 assert!(Authority::try_read_frozen(&storage).unwrap());
400 }
401
402 #[test]
403 fn non_zero_reserved_felt_is_rejected() {
404 let storage = storage_with_config(Word::from([u32::from(OWNER_CONTROLLED), 0, 0, 7]));
406 assert!(matches!(
407 Authority::try_from_storage(&storage),
408 Err(AuthorityError::NonCanonicalConfig)
409 ));
410 assert!(matches!(
411 Authority::try_read_frozen(&storage),
412 Err(AuthorityError::NonCanonicalConfig)
413 ));
414
415 let storage = storage_with_config(Word::from([u32::from(OWNER_CONTROLLED), 0, 5, 0]));
417 assert!(matches!(
418 Authority::try_from_storage(&storage),
419 Err(AuthorityError::NonCanonicalConfig)
420 ));
421 }
422
423 #[test]
424 fn non_boolean_frozen_flag_is_rejected() {
425 let storage = storage_with_config(Word::from([u32::from(AUTH_CONTROLLED), 2, 0, 0]));
427 assert!(matches!(
428 Authority::try_from_storage(&storage),
429 Err(AuthorityError::NonCanonicalConfig)
430 ));
431 assert!(matches!(
432 Authority::try_read_frozen(&storage),
433 Err(AuthorityError::NonCanonicalConfig)
434 ));
435 }
436
437 #[test]
438 fn non_zero_reserved_felt_in_role_value_is_rejected() {
439 let role = RoleSymbol::new("ADMIN").unwrap();
440 let role_felt: Felt = (&role).into();
441 let expected_root = AccountProcedureRoot::from_raw(Word::from(ROLE_KEY_WORD));
442
443 let storage = rbac_storage_with_role_value(Word::new([
445 role_felt,
446 Felt::ZERO,
447 Felt::ZERO,
448 Felt::ZERO,
449 ]));
450 assert_matches!(
451 Authority::try_from_storage(&storage),
452 Ok(Authority::RbacControlled { procedure_roles })
453 if procedure_roles.get(&expected_root) == Some(&role)
454 );
455
456 let storage = rbac_storage_with_role_value(Word::new([
458 role_felt,
459 Felt::ZERO,
460 Felt::from(9u8),
461 Felt::ZERO,
462 ]));
463 assert_matches!(
464 Authority::try_from_storage(&storage),
465 Err(AuthorityError::NonCanonicalConfig)
466 );
467 }
468}