Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Changes

- [BREAKING] Renamed `ProvenBatch::new` to `new_unchecked` ([#2687](https://github.com/0xMiden/miden-base/issues/2687)).
- Added `ShortCapitalString` type and related `TokenSymbol` and `RoleSymbol` types. ([#2690](https://github.com/0xMiden/protocol/pull/2690)).

## 0.14.0 (2026-03-23)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use miden_core::{Felt, Word};
use thiserror::Error;

use crate::account::auth::{AuthScheme, PublicKey};
use crate::asset::TokenSymbol;
use crate::asset::{RoleSymbol, TokenSymbol};
use crate::utils::serde::{
ByteReader,
ByteWriter,
Expand All @@ -32,6 +32,7 @@ pub static SCHEMA_TYPE_REGISTRY: LazyLock<SchemaTypeRegistry> = LazyLock::new(||
registry.register_felt_type::<Bool>();
registry.register_felt_type::<Felt>();
registry.register_felt_type::<TokenSymbol>();
registry.register_felt_type::<RoleSymbol>();
registry.register_felt_type::<AuthScheme>();
registry.register_word_type::<Word>();
registry.register_word_type::<PublicKey>();
Expand Down Expand Up @@ -185,6 +186,11 @@ impl SchemaType {
.expect("type is well formed")
}

/// Returns the schema type for RBAC role symbols.
pub fn role_symbol() -> SchemaType {
SchemaType::new("miden::standards::access::role_symbol").expect("type is well formed")
}

/// Returns a reference to the inner string.
pub fn as_str(&self) -> &str {
&self.0
Expand Down Expand Up @@ -488,6 +494,29 @@ impl FeltType for TokenSymbol {
}
}

impl FeltType for RoleSymbol {
fn type_name() -> SchemaType {
SchemaType::role_symbol()
}

fn parse_str(input: &str) -> Result<Felt, SchemaTypeError> {
let role_symbol = RoleSymbol::new(input).map_err(|err| {
SchemaTypeError::parse(input.to_string(), <Self as FeltType>::type_name(), err)
})?;
Ok(Felt::from(role_symbol))
}

fn display_felt(value: Felt) -> Result<String, SchemaTypeError> {
let role_symbol = RoleSymbol::try_from(value).map_err(|err| {
SchemaTypeError::ConversionError(format!(
"invalid role_symbol value `{}`: {err}",
value.as_canonical_u64()
))
})?;
Ok(role_symbol.to_string())
}
}

// WORD IMPLS FOR NATIVE TYPES
// ================================================================================================

Expand Down Expand Up @@ -818,5 +847,14 @@ mod tests {
assert!(SCHEMA_TYPE_REGISTRY.try_parse_felt(&bool_type, "yes").is_err());
assert!(SCHEMA_TYPE_REGISTRY.try_parse_felt(&bool_type, "2").is_err());
assert!(SCHEMA_TYPE_REGISTRY.validate_felt_value(&bool_type, Felt::new(2)).is_err());

let role_symbol_type = SchemaType::role_symbol();
let role_symbol =
SCHEMA_TYPE_REGISTRY.try_parse_felt(&role_symbol_type, "MINTER_ADMIN").unwrap();
assert_eq!(
SCHEMA_TYPE_REGISTRY.display_felt(&role_symbol_type, role_symbol),
"MINTER_ADMIN"
);
assert!(SCHEMA_TYPE_REGISTRY.try_parse_felt(&role_symbol_type, "minter").is_err());
}
}
6 changes: 5 additions & 1 deletion crates/miden-protocol/src/asset/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::account::AccountType;
use super::errors::{AssetError, TokenSymbolError};
use super::errors::{AssetError, RoleSymbolError, ShortCapitalStringError, TokenSymbolError};
use super::utils::serde::{
ByteReader,
ByteWriter,
Expand All @@ -20,6 +20,10 @@ pub use nonfungible::{NonFungibleAsset, NonFungibleAssetDetails};

mod token_symbol;
pub use token_symbol::TokenSymbol;
mod short_capital_string;
pub use short_capital_string::ShortCapitalString;
mod role_symbol;
pub use role_symbol::RoleSymbol;

mod asset_callbacks;
pub use asset_callbacks::AssetCallbacks;
Expand Down
131 changes: 131 additions & 0 deletions crates/miden-protocol/src/asset/role_symbol.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
use alloc::fmt;

use super::{Felt, RoleSymbolError, ShortCapitalString};

/// Represents a role symbol for role-based access control.
///
/// Role symbols can consist of up to 12 uppercase Latin characters and underscores, e.g.
/// "MINTER", "BURNER", "MINTER_ADMIN".
///
/// The label is stored as a [`ShortCapitalString`] and can be converted to a [`Felt`] encoding via
/// [`as_element()`](Self::as_element).
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct RoleSymbol(ShortCapitalString);

impl RoleSymbol {
/// Alphabet used for role symbols (`A-Z` and `_`).
pub const ALPHABET: &'static [u8; 27] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ_";

/// The length of the set of characters that can be used in a role symbol.
pub const ALPHABET_LENGTH: u64 = 27;

/// The minimum integer value of an encoded [`RoleSymbol`].
///
/// This value encodes the "A" role symbol.
pub const MIN_ENCODED_VALUE: u64 = 1;

/// The maximum integer value of an encoded [`RoleSymbol`].
///
/// This value encodes the "____________" role symbol (12 underscores).
pub const MAX_ENCODED_VALUE: u64 = 4052555153018976252;

/// Constructs a new [`RoleSymbol`] from a string, panicking on invalid input.
///
/// # Panics
///
/// Panics if:
/// - The length of the provided string is less than 1 or greater than 12.
/// - The provided role symbol contains characters outside `A-Z` and `_`.
pub fn new_unchecked(role_symbol: &str) -> Self {
Self::new(role_symbol).expect("invalid role symbol")
}

/// Creates a new [`RoleSymbol`] from the provided role symbol string.
///
/// # Errors
/// Returns an error if:
/// - The length of the provided string is less than 1 or greater than 12.
/// - The provided role symbol contains characters outside `A-Z` and `_`.
pub fn new(role_symbol: &str) -> Result<Self, RoleSymbolError> {
ShortCapitalString::from_ascii_uppercase_and_underscore(role_symbol)
.map(Self)
.map_err(Into::into)
}

/// Returns the [`Felt`] encoding of this role symbol.
pub fn as_element(&self) -> Felt {
self.0.as_element(Self::ALPHABET).expect("RoleSymbol alphabet is always valid")
}
}

impl fmt::Display for RoleSymbol {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}

impl From<RoleSymbol> for Felt {
fn from(role_symbol: RoleSymbol) -> Self {
role_symbol.as_element()
}
}

impl From<&RoleSymbol> for Felt {
fn from(role_symbol: &RoleSymbol) -> Self {
role_symbol.as_element()
}
}

impl TryFrom<&str> for RoleSymbol {
type Error = RoleSymbolError;

fn try_from(role_symbol: &str) -> Result<Self, Self::Error> {
Self::new(role_symbol)
}
}

impl TryFrom<Felt> for RoleSymbol {
type Error = RoleSymbolError;

/// Decodes a [`Felt`] representation of the role symbol into a [`RoleSymbol`].
fn try_from(felt: Felt) -> Result<Self, Self::Error> {
ShortCapitalString::try_from_encoded_felt(
felt,
Self::ALPHABET,
Self::MIN_ENCODED_VALUE,
Self::MAX_ENCODED_VALUE,
)
.map(Self)
.map_err(Into::into)
}
}

#[cfg(test)]
mod tests {
use alloc::string::ToString;

use assert_matches::assert_matches;

use super::{Felt, RoleSymbol, RoleSymbolError};

#[test]
fn test_role_symbol_roundtrip_and_validation() {
let role_symbols = ["MINTER", "BURNER", "MINTER_ADMIN", "A", "A_B_C"];
for role_symbol in role_symbols {
let encoded: Felt = RoleSymbol::new(role_symbol).unwrap().into();
let decoded = RoleSymbol::try_from(encoded).unwrap();
assert_eq!(decoded.to_string(), role_symbol);
}

assert_matches!(RoleSymbol::new("").unwrap_err(), RoleSymbolError::InvalidLength(0));
assert_matches!(
RoleSymbol::new("ABCDEFGHIJKLM").unwrap_err(),
RoleSymbolError::InvalidLength(13)
);
assert_matches!(
RoleSymbol::new("MINTER-ADMIN").unwrap_err(),
RoleSymbolError::InvalidCharacter
);
assert_matches!(RoleSymbol::new("mINTER").unwrap_err(), RoleSymbolError::InvalidCharacter);
}
}
Loading
Loading