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
36procedure_root!(
37 AUTHORITY_FREEZE,
38 Authority::NAME,
39 Authority::FREEZE_PROC_NAME,
40 Authority::code()
41);
42
43procedure_root!(
44 AUTHORITY_UNFREEZE,
45 Authority::NAME,
46 Authority::UNFREEZE_PROC_NAME,
47 Authority::code()
48);
49
50static AUTHORITY_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
51 StorageSlotName::new("miden::standards::access::authority::authority_config")
52 .expect("storage slot name should be valid")
53});
54
55static AUTHORITY_PROCEDURE_ROLES_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
56 StorageSlotName::new("miden::standards::access::authority::procedure_roles")
57 .expect("storage slot name should be valid")
58});
59
60const AUTH_CONTROLLED: u8 = 0;
62const OWNER_CONTROLLED: u8 = 1;
64const RBAC_CONTROLLED: u8 = 2;
66
67#[repr(u8)]
110#[derive(Debug, Clone, PartialEq, Eq)]
111#[non_exhaustive]
112pub enum Authority {
113 AuthControlled = AUTH_CONTROLLED,
115 OwnerControlled = OWNER_CONTROLLED,
117 RbacControlled {
124 roles: BTreeMap<AccountProcedureRoot, RoleSymbol>,
125 } = RBAC_CONTROLLED,
126}
127
128impl Authority {
129 pub const NAME: &'static str = "miden::standards::components::access::authority";
131
132 const FREEZE_PROC_NAME: &'static str = "freeze";
134 const UNFREEZE_PROC_NAME: &'static str = "unfreeze";
136
137 pub fn code() -> &'static AccountComponentCode {
139 &AUTHORITY_CODE
140 }
141
142 pub fn freeze_root() -> AccountProcedureRoot {
152 *AUTHORITY_FREEZE
153 }
154
155 pub fn unfreeze_root() -> AccountProcedureRoot {
162 *AUTHORITY_UNFREEZE
163 }
164
165 pub fn authority_slot() -> &'static StorageSlotName {
167 &AUTHORITY_SLOT_NAME
168 }
169
170 pub fn procedure_roles_slot() -> &'static StorageSlotName {
172 &AUTHORITY_PROCEDURE_ROLES_SLOT_NAME
173 }
174
175 pub fn try_from_storage(storage: &AccountStorage) -> Result<Self, AuthorityError> {
177 let word = Self::read_config_word(storage)?;
178
179 let discriminant: u8 = word[0]
180 .as_canonical_u64()
181 .try_into()
182 .map_err(|_| AuthorityError::InvalidAuthority(word[0].as_canonical_u64()))?;
183
184 match discriminant {
185 AUTH_CONTROLLED => Ok(Self::AuthControlled),
186 OWNER_CONTROLLED => Ok(Self::OwnerControlled),
187 RBAC_CONTROLLED => {
188 let roles = Self::read_roles_from_storage(storage)?;
189 Ok(Self::RbacControlled { roles })
190 },
191 other => Err(AuthorityError::InvalidAuthority(other.into())),
192 }
193 }
194
195 pub fn try_read_frozen(storage: &AccountStorage) -> Result<bool, AuthorityError> {
200 let word = Self::read_config_word(storage)?;
201
202 Ok(word[1] != Felt::ZERO)
203 }
204
205 pub fn component_metadata(&self) -> AccountComponentMetadata {
207 let mut slots = vec![(
208 AUTHORITY_SLOT_NAME.clone(),
209 StorageSlotSchema::value(
210 "Authority configuration",
211 [
212 FeltSchema::u8("authority"),
213 FeltSchema::u8("is_frozen"),
214 FeltSchema::new_void(),
215 FeltSchema::new_void(),
216 ],
217 ),
218 )];
219
220 if matches!(self, Authority::RbacControlled { .. }) {
221 slots.push((
222 AUTHORITY_PROCEDURE_ROLES_SLOT_NAME.clone(),
223 StorageSlotSchema::map(
224 "Per-procedure role assignment (procedure root -> role symbol)",
225 SchemaType::native_word(),
226 SchemaType::role_symbol(),
227 ),
228 ));
229 }
230
231 let storage_schema = StorageSchema::new(slots).expect("storage schema should be valid");
232
233 AccountComponentMetadata::new(Self::NAME)
234 .with_description(
235 "Account-wide authority shared by procedures that gate state-mutating \
236 operations behind auth-only, owner-based, or RBAC role-based checks",
237 )
238 .with_storage_schema(storage_schema)
239 }
240
241 fn as_u8(&self) -> u8 {
246 match self {
247 Authority::AuthControlled => AUTH_CONTROLLED,
248 Authority::OwnerControlled => OWNER_CONTROLLED,
249 Authority::RbacControlled { .. } => RBAC_CONTROLLED,
250 }
251 }
252
253 fn to_word(&self) -> Word {
255 Word::new([Felt::from(self.as_u8()), Felt::ZERO, Felt::ZERO, Felt::ZERO])
256 }
257
258 fn read_config_word(storage: &AccountStorage) -> Result<Word, AuthorityError> {
264 let word = storage
265 .get_item(Self::authority_slot())
266 .map_err(AuthorityError::MissingStorageSlot)?;
267
268 if word[2] != Felt::ZERO || word[3] != Felt::ZERO || word[1].as_canonical_u64() > 1 {
269 return Err(AuthorityError::NonCanonicalConfig);
270 }
271
272 Ok(word)
273 }
274
275 fn read_roles_from_storage(
277 storage: &AccountStorage,
278 ) -> Result<BTreeMap<AccountProcedureRoot, RoleSymbol>, AuthorityError> {
279 let slot = storage
280 .slots()
281 .iter()
282 .find(|slot| slot.name().id() == AUTHORITY_PROCEDURE_ROLES_SLOT_NAME.id())
283 .ok_or(AuthorityError::MissingProcedureRolesSlot)?;
284
285 let StorageSlotContent::Map(map) = slot.content() else {
286 return Err(AuthorityError::MissingProcedureRolesSlot);
287 };
288
289 let mut roles = BTreeMap::new();
290 for (key, value) in map.entries() {
291 let proc_root = AccountProcedureRoot::from_raw(key.as_word());
292 let role = RoleSymbol::try_from(value[0]).map_err(AuthorityError::InvalidRoleSymbol)?;
293 roles.insert(proc_root, role);
294 }
295
296 Ok(roles)
297 }
298}
299
300impl From<Authority> for AccountComponent {
304 fn from(value: Authority) -> Self {
305 let metadata = value.component_metadata();
306
307 let mut slots = vec![StorageSlot::with_value(AUTHORITY_SLOT_NAME.clone(), value.to_word())];
308
309 if let Authority::RbacControlled { roles } = value {
310 let entries = roles.into_iter().map(|(proc_root, role)| {
311 (StorageMapKey::new(proc_root.as_word()), role_value_word(&role))
312 });
313 slots.push(StorageSlot::with_map(
314 AUTHORITY_PROCEDURE_ROLES_SLOT_NAME.clone(),
315 StorageMap::with_entries(entries)
316 .expect("authority procedure-roles map should be valid"),
317 ));
318 }
319
320 AccountComponent::new(Authority::code().clone(), slots, metadata).expect(
321 "authority component should satisfy the requirements of a valid account component",
322 )
323 }
324}
325
326fn role_value_word(role: &RoleSymbol) -> Word {
328 Word::new([role.into(), Felt::ZERO, Felt::ZERO, Felt::ZERO])
329}
330
331#[derive(Debug, Error)]
336pub enum AuthorityError {
337 #[error("invalid authority value: {0}")]
338 InvalidAuthority(u64),
339 #[error("authority configuration word is not in canonical form")]
340 NonCanonicalConfig,
341 #[error("invalid role symbol in authority storage")]
342 InvalidRoleSymbol(#[source] RoleSymbolError),
343 #[error("failed to read authority slot from storage")]
344 MissingStorageSlot(#[source] AccountError),
345 #[error("authority procedure-roles slot is missing or not a map")]
346 MissingProcedureRolesSlot,
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 fn storage_with_config(word: Word) -> AccountStorage {
355 let slot = StorageSlot::with_value(Authority::authority_slot().clone(), word);
356 AccountStorage::new(vec![slot]).expect("storage should be valid")
357 }
358
359 #[test]
360 fn canonical_config_is_accepted() {
361 let storage = storage_with_config(Word::from([u32::from(AUTH_CONTROLLED), 0, 0, 0]));
363 assert_eq!(Authority::try_from_storage(&storage).unwrap(), Authority::AuthControlled);
364 assert!(!Authority::try_read_frozen(&storage).unwrap());
365
366 let storage = storage_with_config(Word::from([u32::from(OWNER_CONTROLLED), 1, 0, 0]));
368 assert_eq!(Authority::try_from_storage(&storage).unwrap(), Authority::OwnerControlled);
369 assert!(Authority::try_read_frozen(&storage).unwrap());
370 }
371
372 #[test]
373 fn non_zero_reserved_felt_is_rejected() {
374 let storage = storage_with_config(Word::from([u32::from(OWNER_CONTROLLED), 0, 0, 7]));
376 assert!(matches!(
377 Authority::try_from_storage(&storage),
378 Err(AuthorityError::NonCanonicalConfig)
379 ));
380 assert!(matches!(
381 Authority::try_read_frozen(&storage),
382 Err(AuthorityError::NonCanonicalConfig)
383 ));
384
385 let storage = storage_with_config(Word::from([u32::from(OWNER_CONTROLLED), 0, 5, 0]));
387 assert!(matches!(
388 Authority::try_from_storage(&storage),
389 Err(AuthorityError::NonCanonicalConfig)
390 ));
391 }
392
393 #[test]
394 fn non_boolean_frozen_flag_is_rejected() {
395 let storage = storage_with_config(Word::from([u32::from(AUTH_CONTROLLED), 2, 0, 0]));
397 assert!(matches!(
398 Authority::try_from_storage(&storage),
399 Err(AuthorityError::NonCanonicalConfig)
400 ));
401 assert!(matches!(
402 Authority::try_read_frozen(&storage),
403 Err(AuthorityError::NonCanonicalConfig)
404 ));
405 }
406}