use alloc::collections::BTreeMap;
use alloc::vec;
use miden_protocol::account::component::{
AccountComponentCode,
AccountComponentMetadata,
FeltSchema,
SchemaType,
StorageSchema,
StorageSlotSchema,
};
use miden_protocol::account::{
AccountComponent,
AccountProcedureRoot,
AccountStorage,
RoleSymbol,
StorageMap,
StorageMapKey,
StorageSlot,
StorageSlotContent,
StorageSlotName,
};
use miden_protocol::errors::{AccountError, RoleSymbolError};
use miden_protocol::utils::sync::LazyLock;
use miden_protocol::{Felt, Word};
use thiserror::Error;
use crate::account::account_component_code;
use crate::procedure_root;
account_component_code!(AUTHORITY_CODE, "miden-standards-access-authority.masp");
procedure_root!(
AUTHORITY_FREEZE,
Authority::NAME,
Authority::FREEZE_PROC_NAME,
Authority::code()
);
procedure_root!(
AUTHORITY_UNFREEZE,
Authority::NAME,
Authority::UNFREEZE_PROC_NAME,
Authority::code()
);
static AUTHORITY_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::access::authority::authority_config")
.expect("storage slot name should be valid")
});
static AUTHORITY_PROCEDURE_ROLES_SLOT_NAME: LazyLock<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::access::authority::procedure_roles")
.expect("storage slot name should be valid")
});
const AUTH_CONTROLLED: u8 = 0;
const OWNER_CONTROLLED: u8 = 1;
const RBAC_CONTROLLED: u8 = 2;
#[repr(u8)]
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Authority {
AuthControlled = AUTH_CONTROLLED,
OwnerControlled = OWNER_CONTROLLED,
RbacControlled {
roles: BTreeMap<AccountProcedureRoot, RoleSymbol>,
} = RBAC_CONTROLLED,
}
impl Authority {
pub const NAME: &'static str = "miden::standards::components::access::authority";
const FREEZE_PROC_NAME: &'static str = "freeze";
const UNFREEZE_PROC_NAME: &'static str = "unfreeze";
pub fn code() -> &'static AccountComponentCode {
&AUTHORITY_CODE
}
pub fn freeze_root() -> AccountProcedureRoot {
*AUTHORITY_FREEZE
}
pub fn unfreeze_root() -> AccountProcedureRoot {
*AUTHORITY_UNFREEZE
}
pub fn authority_slot() -> &'static StorageSlotName {
&AUTHORITY_SLOT_NAME
}
pub fn procedure_roles_slot() -> &'static StorageSlotName {
&AUTHORITY_PROCEDURE_ROLES_SLOT_NAME
}
pub fn try_from_storage(storage: &AccountStorage) -> Result<Self, AuthorityError> {
let word = Self::read_config_word(storage)?;
let discriminant: u8 = word[0]
.as_canonical_u64()
.try_into()
.map_err(|_| AuthorityError::InvalidAuthority(word[0].as_canonical_u64()))?;
match discriminant {
AUTH_CONTROLLED => Ok(Self::AuthControlled),
OWNER_CONTROLLED => Ok(Self::OwnerControlled),
RBAC_CONTROLLED => {
let roles = Self::read_roles_from_storage(storage)?;
Ok(Self::RbacControlled { roles })
},
other => Err(AuthorityError::InvalidAuthority(other.into())),
}
}
pub fn try_read_frozen(storage: &AccountStorage) -> Result<bool, AuthorityError> {
let word = Self::read_config_word(storage)?;
Ok(word[1] != Felt::ZERO)
}
pub fn component_metadata(&self) -> AccountComponentMetadata {
let mut slots = vec![(
AUTHORITY_SLOT_NAME.clone(),
StorageSlotSchema::value(
"Authority configuration",
[
FeltSchema::u8("authority"),
FeltSchema::u8("is_frozen"),
FeltSchema::new_void(),
FeltSchema::new_void(),
],
),
)];
if matches!(self, Authority::RbacControlled { .. }) {
slots.push((
AUTHORITY_PROCEDURE_ROLES_SLOT_NAME.clone(),
StorageSlotSchema::map(
"Per-procedure role assignment (procedure root -> role symbol)",
SchemaType::native_word(),
SchemaType::role_symbol(),
),
));
}
let storage_schema = StorageSchema::new(slots).expect("storage schema should be valid");
AccountComponentMetadata::new(Self::NAME)
.with_description(
"Account-wide authority shared by procedures that gate state-mutating \
operations behind auth-only, owner-based, or RBAC role-based checks",
)
.with_storage_schema(storage_schema)
}
fn as_u8(&self) -> u8 {
match self {
Authority::AuthControlled => AUTH_CONTROLLED,
Authority::OwnerControlled => OWNER_CONTROLLED,
Authority::RbacControlled { .. } => RBAC_CONTROLLED,
}
}
fn to_word(&self) -> Word {
Word::new([Felt::from(self.as_u8()), Felt::ZERO, Felt::ZERO, Felt::ZERO])
}
fn read_config_word(storage: &AccountStorage) -> Result<Word, AuthorityError> {
let word = storage
.get_item(Self::authority_slot())
.map_err(AuthorityError::MissingStorageSlot)?;
if word[2] != Felt::ZERO || word[3] != Felt::ZERO || word[1].as_canonical_u64() > 1 {
return Err(AuthorityError::NonCanonicalConfig);
}
Ok(word)
}
fn read_roles_from_storage(
storage: &AccountStorage,
) -> Result<BTreeMap<AccountProcedureRoot, RoleSymbol>, AuthorityError> {
let slot = storage
.slots()
.iter()
.find(|slot| slot.name().id() == AUTHORITY_PROCEDURE_ROLES_SLOT_NAME.id())
.ok_or(AuthorityError::MissingProcedureRolesSlot)?;
let StorageSlotContent::Map(map) = slot.content() else {
return Err(AuthorityError::MissingProcedureRolesSlot);
};
let mut roles = BTreeMap::new();
for (key, value) in map.entries() {
let proc_root = AccountProcedureRoot::from_raw(key.as_word());
let role = RoleSymbol::try_from(value[0]).map_err(AuthorityError::InvalidRoleSymbol)?;
roles.insert(proc_root, role);
}
Ok(roles)
}
}
impl From<Authority> for AccountComponent {
fn from(value: Authority) -> Self {
let metadata = value.component_metadata();
let mut slots = vec![StorageSlot::with_value(AUTHORITY_SLOT_NAME.clone(), value.to_word())];
if let Authority::RbacControlled { roles } = value {
let entries = roles.into_iter().map(|(proc_root, role)| {
(StorageMapKey::new(proc_root.as_word()), role_value_word(&role))
});
slots.push(StorageSlot::with_map(
AUTHORITY_PROCEDURE_ROLES_SLOT_NAME.clone(),
StorageMap::with_entries(entries)
.expect("authority procedure-roles map should be valid"),
));
}
AccountComponent::new(Authority::code().clone(), slots, metadata).expect(
"authority component should satisfy the requirements of a valid account component",
)
}
}
fn role_value_word(role: &RoleSymbol) -> Word {
Word::new([role.into(), Felt::ZERO, Felt::ZERO, Felt::ZERO])
}
#[derive(Debug, Error)]
pub enum AuthorityError {
#[error("invalid authority value: {0}")]
InvalidAuthority(u64),
#[error("authority configuration word is not in canonical form")]
NonCanonicalConfig,
#[error("invalid role symbol in authority storage")]
InvalidRoleSymbol(#[source] RoleSymbolError),
#[error("failed to read authority slot from storage")]
MissingStorageSlot(#[source] AccountError),
#[error("authority procedure-roles slot is missing or not a map")]
MissingProcedureRolesSlot,
}
#[cfg(test)]
mod tests {
use super::*;
fn storage_with_config(word: Word) -> AccountStorage {
let slot = StorageSlot::with_value(Authority::authority_slot().clone(), word);
AccountStorage::new(vec![slot]).expect("storage should be valid")
}
#[test]
fn canonical_config_is_accepted() {
let storage = storage_with_config(Word::from([u32::from(AUTH_CONTROLLED), 0, 0, 0]));
assert_eq!(Authority::try_from_storage(&storage).unwrap(), Authority::AuthControlled);
assert!(!Authority::try_read_frozen(&storage).unwrap());
let storage = storage_with_config(Word::from([u32::from(OWNER_CONTROLLED), 1, 0, 0]));
assert_eq!(Authority::try_from_storage(&storage).unwrap(), Authority::OwnerControlled);
assert!(Authority::try_read_frozen(&storage).unwrap());
}
#[test]
fn non_zero_reserved_felt_is_rejected() {
let storage = storage_with_config(Word::from([u32::from(OWNER_CONTROLLED), 0, 0, 7]));
assert!(matches!(
Authority::try_from_storage(&storage),
Err(AuthorityError::NonCanonicalConfig)
));
assert!(matches!(
Authority::try_read_frozen(&storage),
Err(AuthorityError::NonCanonicalConfig)
));
let storage = storage_with_config(Word::from([u32::from(OWNER_CONTROLLED), 0, 5, 0]));
assert!(matches!(
Authority::try_from_storage(&storage),
Err(AuthorityError::NonCanonicalConfig)
));
}
#[test]
fn non_boolean_frozen_flag_is_rejected() {
let storage = storage_with_config(Word::from([u32::from(AUTH_CONTROLLED), 2, 0, 0]));
assert!(matches!(
Authority::try_from_storage(&storage),
Err(AuthorityError::NonCanonicalConfig)
));
assert!(matches!(
Authority::try_read_frozen(&storage),
Err(AuthorityError::NonCanonicalConfig)
));
}
}