diff --git a/CHANGELOG.md b/CHANGELOG.md index f7d6c1de16..797587ebee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Updated `SwapNote::build_tag` to use 1-bit `NoteType` encoding, increasing script root bits from 14 to 15 ([#2758](https://github.com/0xMiden/protocol/pull/2758)). - Added `AssetAmount` wrapper type for validated fungible asset amounts ([#2721](https://github.com/0xMiden/protocol/pull/2721)). - [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)). - Added `ShortCapitalString` type and related `TokenSymbol` and `RoleSymbol` types ([#2690](https://github.com/0xMiden/protocol/pull/2690)). - [BREAKING] Renamed the guarded multisig component-facing APIs from `multisig_guardian` / `AuthMultisigGuardian` to `guarded_multisig` / `AuthGuardedMultisig`, while retaining the `guardian` auth namespace and guardian-specific procedures. - Added shared `ProcedurePolicy` for AuthMultisig ([#2670](https://github.com/0xMiden/protocol/pull/2670)). diff --git a/crates/miden-standards/asm/account_components/access/role_based_access_control.masm b/crates/miden-standards/asm/account_components/access/role_based_access_control.masm new file mode 100644 index 0000000000..d8c5d9f3a6 --- /dev/null +++ b/crates/miden-standards/asm/account_components/access/role_based_access_control.masm @@ -0,0 +1,22 @@ +# The MASM code of the RoleBasedAccessControl Account Component. +# +# See the `RoleBasedAccessControl` Rust type's documentation for more details. + +pub use ::miden::standards::access::role_based_access_control::assert_sender_is_root_admin +pub use ::miden::standards::access::role_based_access_control::assert_sender_has_role +pub use ::miden::standards::access::role_based_access_control::get_root_admin +pub use ::miden::standards::access::role_based_access_control::get_nominated_root_admin +pub use ::miden::standards::access::role_based_access_control::transfer_root_admin +pub use ::miden::standards::access::role_based_access_control::accept_root_admin +pub use ::miden::standards::access::role_based_access_control::renounce_root_admin +pub use ::miden::standards::access::role_based_access_control::role_exists +pub use ::miden::standards::access::role_based_access_control::get_role_admin +pub use ::miden::standards::access::role_based_access_control::get_role_member_count +pub use ::miden::standards::access::role_based_access_control::has_role +pub use ::miden::standards::access::role_based_access_control::get_role_member +pub use ::miden::standards::access::role_based_access_control::get_active_role_count +pub use ::miden::standards::access::role_based_access_control::get_active_role +pub use ::miden::standards::access::role_based_access_control::set_role_admin +pub use ::miden::standards::access::role_based_access_control::grant_role +pub use ::miden::standards::access::role_based_access_control::revoke_role +pub use ::miden::standards::access::role_based_access_control::renounce_role diff --git a/crates/miden-standards/asm/standards/access/role_based_access_control.masm b/crates/miden-standards/asm/standards/access/role_based_access_control.masm new file mode 100644 index 0000000000..f6fb89e2bd --- /dev/null +++ b/crates/miden-standards/asm/standards/access/role_based_access_control.masm @@ -0,0 +1,1466 @@ +use miden::protocol::account_id +use miden::protocol::active_account +use miden::protocol::active_note +use miden::protocol::native_account + +# CONSTANTS +# ================================================================================================= + +# Root-admin config after renounce. +# Layout: [0, 0, 0, 0] +const RENOUNCED_ROOT_ADMIN_CONFIG = [0, 0, 0, 0] + +# Root admin config slot. +# Layout: [root_admin_suffix, root_admin_prefix, nominated_root_admin_suffix, nominated_root_admin_prefix] +const ROOT_ADMIN_CONFIG_SLOT = word("miden::standards::access::role_based_access_control::admin_config") + +# Global RBAC state slot. +# Layout: [active_role_count, 0, 0, 0] +const RBAC_STATE_SLOT = word("miden::standards::access::role_based_access_control::state") + +# Active roles map slot. +# Map entries: [0, 0, 0, active_role_index] -> [role_symbol, 0, 0, 0] +const ACTIVE_ROLES_SLOT = word("miden::standards::access::role_based_access_control::active_roles") + +# Per-role config map slot. +# Map entries: [0, 0, 0, role_symbol] -> +# [member_count, admin_role_symbol, active_role_index, 0] +const ROLE_CONFIGS_SLOT = word("miden::standards::access::role_based_access_control::role_config") + +# Per-role member enumeration map slot. +# Map entries: [0, 0, role_symbol, member_index] -> [account_suffix, account_prefix, 0, 0] +const ROLE_MEMBERS_SLOT = word("miden::standards::access::role_based_access_control::role_members") + +# Per-role reverse member index map slot. +# Map entries: [0, role_symbol, account_suffix, account_prefix] -> [is_member, member_index, 0, 0] +const ROLE_MEMBER_INDEX_SLOT = word("miden::standards::access::role_based_access_control::role_member_index") + +# TRANSFER_ROOT_ADMIN LOCALS +const NEW_ROOT_ADMIN_SUFFIX_LOC = 0 +const NEW_ROOT_ADMIN_PREFIX_LOC = 1 +const CURRENT_ROOT_ADMIN_SUFFIX_LOC = 2 +const CURRENT_ROOT_ADMIN_PREFIX_LOC = 3 + +# GRANT_ROLE_INTERNAL & REVOKE_ROLE_INTERNAL LOCALS +const ROLE_SYMBOL_LOC = 0 +const ACCOUNT_SUFFIX_LOC = 1 +const ACCOUNT_PREFIX_LOC = 2 + +# ADD_TO_ROLE_ENUMERATION LOCALS +const MEMBER_COUNT_LOC = 3 + +# REMOVE_FROM_ROLE_ENUMERATION LOCALS +const REMOVE_INDEX_LOC = 3 +const LAST_INDEX_LOC = 4 + +# ERRORS +# ================================================================================================= + +const ERR_SENDER_NOT_ROOT_ADMIN = "note sender is not the root admin" +const ERR_SENDER_NOT_NOMINATED_ROOT_ADMIN = "note sender is not the nominated root admin" +const ERR_NO_NOMINATED_ROOT_ADMIN = "no nominated root admin transfer exists" +const ERR_ROOT_ADMIN_TRANSFER_IN_PROGRESS = "root admin transfer is in progress" +const ERR_SENDER_LACKS_ROLE = "note sender does not hold the required role" +const ERR_SENDER_NOT_ROOT_OR_ROLE_ADMIN = "note sender is not the root admin or a role admin" +const ERR_ROLE_MEMBER_OUT_OF_BOUNDS = "role member index is out of bounds" +const ERR_ACTIVE_ROLE_OUT_OF_BOUNDS = "active role index is out of bounds" +const ERR_ACCOUNT_NOT_IN_ROLE = "account does not hold the role" +const ERR_ROLE_SYMBOL_ZERO = "role symbol is zero" + +# PUBLIC INTERFACE +# ================================================================================================= + +#! Checks that the note sender is the current root admin. +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - the note sender is not the current root admin. +#! +#! Invocation: call +pub proc assert_sender_is_root_admin + exec.assert_sender_is_root_admin_internal + # => [pad(16)] +end + +#! Checks that the note sender holds the given role. +#! +#! Inputs: [role_symbol, pad(15)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - role_symbol is the encoded role symbol. +#! +#! Panics if: +#! - role_symbol is zero. +#! - the note sender does not hold the given role. +#! +#! Invocation: call +pub proc assert_sender_has_role + exec.assert_role_symbol_non_zero + # => [role_symbol, pad(15)] + + exec.is_sender_in_role + # => [has_role, pad(15)] + + assert.err=ERR_SENDER_LACKS_ROLE + # => [pad(16)] +end + +#! Returns the current root admin. +#! +#! Inputs: [pad(16)] +#! Outputs: [root_admin_suffix, root_admin_prefix, pad(14)] +#! +#! Where: +#! - root_admin_{suffix,prefix} are the suffix and prefix felts of the current root admin account ID. +#! +#! Invocation: call +pub proc get_root_admin + exec.get_root_admin_internal + # => [root_admin_suffix, root_admin_prefix, pad(14)] + + movup.2 drop movup.2 drop + # => [root_admin_suffix, root_admin_prefix, pad(14)] +end + +#! Returns the nominated root admin. +#! +#! Inputs: [pad(16)] +#! Outputs: [nominated_root_admin_suffix, nominated_root_admin_prefix, pad(14)] +#! +#! Where: +#! - nominated_root_admin_{suffix,prefix} are the suffix and prefix felts of the nominated root admin +#! account ID. +#! +#! Invocation: call +pub proc get_nominated_root_admin + exec.get_nominated_root_admin_internal + # => [nominated_root_admin_suffix, nominated_root_admin_prefix, pad(14)] + + movup.2 drop movup.2 drop + # => [nominated_root_admin_suffix, nominated_root_admin_prefix, pad(14)] +end + +#! Initiates a two-step root admin transfer by setting the nominated root admin. +#! +#! Inputs: [new_root_admin_suffix, new_root_admin_prefix, pad(14)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - new_root_admin_{suffix,prefix} are the suffix and prefix felts of the new root admin account ID. +#! +#! Panics if: +#! - the note sender is not the current root admin. +#! - the new root admin account ID is invalid. +#! +#! Locals: +#! 0: new_root_admin_suffix +#! 1: new_root_admin_prefix +#! 2: current_root_admin_suffix +#! 3: current_root_admin_prefix +#! +#! Invocation: call +@locals(4) +pub proc transfer_root_admin + exec.assert_sender_is_root_admin_internal + # => [new_root_admin_suffix, new_root_admin_prefix, pad(14)] + + dup.1 dup.1 exec.account_id::validate + # => [new_root_admin_suffix, new_root_admin_prefix, pad(14)] + + loc_store.NEW_ROOT_ADMIN_SUFFIX_LOC + # => [new_root_admin_prefix, pad(14)] + + loc_store.NEW_ROOT_ADMIN_PREFIX_LOC + # => [pad(14)] + + exec.get_root_admin_internal + # => [current_root_admin_suffix, current_root_admin_prefix, pad(14)] + + loc_store.CURRENT_ROOT_ADMIN_SUFFIX_LOC + # => [current_root_admin_prefix, pad(13)] + + loc_store.CURRENT_ROOT_ADMIN_PREFIX_LOC + # => [pad(12)] + + # Check if new_admin == current_admin (cancel case). + loc_load.NEW_ROOT_ADMIN_PREFIX_LOC loc_load.NEW_ROOT_ADMIN_SUFFIX_LOC + # => [new_root_admin_suffix, new_root_admin_prefix, pad(12)] + + loc_load.CURRENT_ROOT_ADMIN_PREFIX_LOC loc_load.CURRENT_ROOT_ADMIN_SUFFIX_LOC + # => [current_root_admin_suffix, current_root_admin_prefix, new_root_admin_suffix, new_root_admin_prefix, pad(12)] + + exec.account_id::is_equal + # => [is_self_transfer, pad(12)] + + if.true + # Cancel root admin transfer and clear the nominated root admin. + # Stack for save: [current_root_admin_suffix, current_root_admin_prefix, 0, 0] + loc_load.CURRENT_ROOT_ADMIN_PREFIX_LOC loc_load.CURRENT_ROOT_ADMIN_SUFFIX_LOC + # => [current_root_admin_suffix, current_root_admin_prefix, pad(12)] + + push.0.0 movup.3 movup.3 + # => [current_root_admin_suffix, current_root_admin_prefix, 0, 0, pad(12)] + else + # Transfer root admin by setting nominated = new_root_admin. + # Stack for save: [current_root_admin_suffix, current_root_admin_prefix, new_root_admin_suffix, new_root_admin_prefix] + loc_load.NEW_ROOT_ADMIN_PREFIX_LOC loc_load.NEW_ROOT_ADMIN_SUFFIX_LOC + # => [new_root_admin_suffix, new_root_admin_prefix, pad(12)] + + loc_load.CURRENT_ROOT_ADMIN_PREFIX_LOC loc_load.CURRENT_ROOT_ADMIN_SUFFIX_LOC + # => [current_root_admin_suffix, current_root_admin_prefix, new_root_admin_suffix, new_root_admin_prefix, pad(12)] + end + + exec.set_root_admin_config + # => [pad(16)] +end + + +#! Accepts the pending root admin transfer. +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - there is no nominated root admin. +#! - the note sender is not the nominated root admin. +#! +#! Invocation: call +pub proc accept_root_admin + exec.get_nominated_root_admin_internal + # => [nominated_root_admin_suffix, nominated_root_admin_prefix, pad(14)] + + dup.1 eq.0 + # => [is_zero_suffix, nominated_root_admin_suffix, nominated_root_admin_prefix, pad(14)] + + dup.1 eq.0 + # => [is_zero_prefix, is_zero_suffix, nominated_root_admin_suffix, nominated_root_admin_prefix, pad(14)] + + and + # => [is_zero_nominated_admin, nominated_root_admin_suffix, nominated_root_admin_prefix, pad(14)] + + assertz.err=ERR_NO_NOMINATED_ROOT_ADMIN + # => [nominated_root_admin_suffix, nominated_root_admin_prefix, pad(14)] + + exec.active_note::get_sender + # => [sender_suffix, sender_prefix, nominated_root_admin_suffix, nominated_root_admin_prefix, pad(12)] + + dup.3 dup.3 + # => [nominated_root_admin_suffix, nominated_root_admin_prefix, sender_suffix, sender_prefix, nominated_root_admin_suffix, nominated_root_admin_prefix, pad(12)] + + exec.account_id::is_equal + # => [is_nominated_admin, nominated_root_admin_suffix, nominated_root_admin_prefix, pad(12)] + + assert.err=ERR_SENDER_NOT_NOMINATED_ROOT_ADMIN + # => [nominated_root_admin_suffix, nominated_root_admin_prefix, pad(14)] + + push.0.0 + # => [0, 0, nominated_root_admin_suffix, nominated_root_admin_prefix, pad(12)] + + movup.3 movup.3 + # => [nominated_root_admin_suffix, nominated_root_admin_prefix, 0, 0, pad(12)] + + exec.set_root_admin_config + # => [pad(16)] +end + + +#! Renounces the root admin role permanently. +#! +#! Inputs: [pad(16)] +#! Outputs: [pad(16)] +#! +#! Panics if: +#! - the note sender is not the current root admin. +#! - a root admin transfer is currently in progress. +#! +#! Invocation: call +pub proc renounce_root_admin + exec.assert_sender_is_root_admin_internal + # => [pad(16)] + + exec.get_nominated_root_admin_internal + # => [nominated_root_admin_suffix, nominated_root_admin_prefix, pad(14)] + + dup.1 eq.0 + # => [is_zero_suffix, nominated_root_admin_suffix, nominated_root_admin_prefix, pad(14)] + + dup.1 eq.0 + # => [is_zero_prefix, is_zero_suffix, nominated_root_admin_suffix, nominated_root_admin_prefix, pad(14)] + + and + # => [is_no_pending_admin, nominated_root_admin_suffix, nominated_root_admin_prefix, pad(14)] + + assert.err=ERR_ROOT_ADMIN_TRANSFER_IN_PROGRESS + # => [nominated_root_admin_suffix, nominated_root_admin_prefix, pad(14)] + + drop drop + # => [pad(16)] + + push.RENOUNCED_ROOT_ADMIN_CONFIG + # => [0, 0, 0, 0, pad(12)] + + exec.set_root_admin_config + # => [pad(16)] +end + + +#! Returns whether a role exists. +#! +#! Inputs: [role_symbol, pad(15)] +#! Outputs: [role_exists, pad(15)] +#! +#! Where: +#! - role_symbol is the encoded role symbol. +#! - role_exists is 1 if the role exists, otherwise 0. +#! +#! Invocation: call +pub proc role_exists + exec.role_exists_internal + # => [role_exists, pad(15)] +end + + +#! Returns the delegated admin role for a role. +#! +#! Inputs: [role_symbol, pad(15)] +#! Outputs: [admin_role_symbol, pad(15)] +#! +#! Where: +#! - role_symbol is the encoded role symbol. +#! - admin_role_symbol is the encoded admin role symbol, or 0 if the role is root-admin-managed. +#! +#! Invocation: call +pub proc get_role_admin + exec.get_role_config + # => [member_count, admin_role_symbol, active_role_index, 0, pad(12)] + + drop movdn.2 drop drop + # => [admin_role_symbol, pad(15)] +end + +#! Returns the number of accounts assigned to a given role. +#! +#! Inputs: [role_symbol, pad(15)] +#! Outputs: [member_count, pad(15)] +#! +#! Where: +#! - role_symbol is the encoded role symbol. +#! - member_count is the number of accounts currently assigned to the given role. +#! +#! Invocation: call +pub proc get_role_member_count + exec.get_role_config + # => [member_count, admin_role_symbol, active_role_index, 0, pad(12)] + + movdn.3 drop drop drop + # => [member_count, pad(15)] +end + +#! Returns whether an account holds a role. +#! +#! Inputs: [role_symbol, account_suffix, account_prefix, pad(13)] +#! Outputs: [has_role, pad(15)] +#! +#! Where: +#! - role_symbol is the encoded role symbol. +#! - account_{suffix,prefix} are the suffix and prefix felts of the account ID. +#! - has_role is 1 if the account holds the role, otherwise 0. +#! +#! Invocation: call +pub proc has_role + exec.has_role_internal + # => [has_role, pad(15)] +end + +#! Returns the account at a given member index for a role. +#! +#! Inputs: [role_symbol, member_index, pad(14)] +#! Outputs: [account_suffix, account_prefix, pad(14)] +#! +#! Where: +#! - role_symbol is the encoded role symbol. +#! - member_index is the member index to look up. +#! - account_{suffix,prefix} are the suffix and prefix felts of the account ID at the index. +#! +#! Panics if: +#! - member_index is out of bounds. +#! +#! Invocation: call +pub proc get_role_member + dup + exec.get_role_config + # => [member_count, admin_role_symbol, active_role_index, 0, + # role_symbol, member_index, pad(14)] + + movdn.3 drop drop drop + # => [member_count, role_symbol, member_index, pad(14)] + + swap movdn.2 + # => [member_count, member_index, role_symbol, pad(14)] + + dup.1 dup.1 lt + # => [is_in_bounds, member_count, member_index, role_symbol, pad(14)] + + assert.err=ERR_ROLE_MEMBER_OUT_OF_BOUNDS + # => [member_count, member_index, role_symbol, pad(14)] + + drop + # => [member_index, role_symbol, pad(14)] + + swap + # => [role_symbol, member_index, pad(14)] + + push.0.0 + # => [0, 0, role_symbol, member_index, pad(14)] + + push.ROLE_MEMBERS_SLOT[0..2] exec.active_account::get_map_item + # => [account_suffix, account_prefix, 0, 0, pad(12)] + + movup.2 drop movup.2 drop + # => [account_suffix, account_prefix, pad(14)] +end + +#! Returns the number of currently active roles. +#! +#! Inputs: [pad(16)] +#! Outputs: [active_role_count, pad(15)] +#! +#! Where: +#! - active_role_count is the number of active roles. +#! +#! Invocation: call +pub proc get_active_role_count + exec.get_active_role_count_internal + # => [active_role_count, pad(15)] + + swap drop + # => [active_role_count, pad(15)] +end + + +#! Returns the role symbol at a given active role index. +#! +#! Inputs: [active_role_index, pad(15)] +#! Outputs: [role_symbol, pad(15)] +#! +#! Where: +#! - active_role_index is the active role index to read. +#! - role_symbol is the encoded role symbol stored at the index. +#! +#! Panics if: +#! - active_role_index is out of bounds. +#! +#! Invocation: call +pub proc get_active_role + exec.get_active_role_count_internal + # => [active_role_count, active_role_index, pad(14)] + + dup.1 dup.1 lt + # => [is_in_bounds, active_role_count, active_role_index, pad(14)] + + assert.err=ERR_ACTIVE_ROLE_OUT_OF_BOUNDS + # => [active_role_count, active_role_index, pad(14)] + + drop + # => [active_role_index, pad(15)] + + push.0.0.0 + # => [0, 0, 0, active_role_index, pad(12)] + + push.ACTIVE_ROLES_SLOT[0..2] exec.active_account::get_map_item + # => [role_symbol, 0, 0, 0, pad(12)] + + movdn.3 drop drop drop + # => [role_symbol, pad(15)] +end + + +#! Sets or updates the delegated admin role for a role. +#! +#! Inputs: [role_symbol, admin_role_symbol, pad(14)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - role_symbol is the encoded role symbol to configure. +#! - admin_role_symbol is the encoded delegated admin role symbol, or 0 for root-admin-only. +#! +#! Panics if: +#! - the note sender is not the current root admin. +#! - role_symbol is zero. +#! +#! Invocation: call +pub proc set_role_admin + exec.assert_sender_is_root_admin_internal + exec.assert_role_symbol_non_zero + # => [role_symbol, admin_role_symbol, pad(14)] + + dup exec.get_role_config + # => [member_count, stored_admin_role_symbol, active_role_index, 0, role_symbol, admin_role_symbol, pad(14)] + + movdn.2 drop swap + # => [member_count, active_role_index, 0, role_symbol, admin_role_symbol, pad(14)] + + movup.3 movup.4 movdn.2 + # => [role_symbol, member_count, admin_role_symbol, active_role_index, 0, pad(14)] + + exec.set_role_config + # => [pad(16)] +end + + +#! Grants a role to an account. +#! +#! Inputs: [role_symbol, account_suffix, account_prefix, pad(13)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - role_symbol is the encoded role symbol to grant. +#! - account_{suffix,prefix} are the suffix and prefix felts of the account ID to grant. +#! +#! Panics if: +#! - role_symbol is zero. +#! - the note sender is neither the current root admin nor a holder of the role's admin role. +#! +#! Invocation: call +pub proc grant_role + exec.assert_role_symbol_non_zero + # => [role_symbol, account_suffix, account_prefix, pad(13)] + + dup exec.assert_sender_is_root_or_role_admin + # => [role_symbol, account_suffix, account_prefix, pad(13)] + + exec.grant_role_internal + # => [pad(16)] +end + +#! Revokes a role from an account. +#! +#! Inputs: [role_symbol, account_suffix, account_prefix, pad(13)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - role_symbol is the encoded role symbol to revoke. +#! - account_{suffix,prefix} are the suffix and prefix felts of the account ID to revoke. +#! +#! Panics if: +#! - role_symbol is zero. +#! - the note sender is neither the current root admin nor a holder of the role's admin role. +#! - the account does not hold the role. +#! +#! Invocation: call +pub proc revoke_role + exec.assert_role_symbol_non_zero + # => [role_symbol, account_suffix, account_prefix, pad(13)] + + dup exec.assert_sender_is_root_or_role_admin + # => [role_symbol, account_suffix, account_prefix, pad(13)] + + exec.revoke_role_internal + # => [pad(16)] +end + +#! Renounces a role held by the note sender. +#! +#! Inputs: [role_symbol, pad(15)] +#! Outputs: [pad(16)] +#! +#! Where: +#! - role_symbol is the encoded role symbol to renounce. +#! +#! Panics if: +#! - role_symbol is zero. +#! - the note sender does not hold the role. +#! - the note sender account ID fails validation. +#! +#! Invocation: call +pub proc renounce_role + exec.assert_role_symbol_non_zero + # => [role_symbol, pad(15)] + + exec.active_note::get_sender + # => [sender_suffix, sender_prefix, role_symbol, pad(15)] + + movup.2 + # => [role_symbol, sender_suffix, sender_prefix, pad(13)] + + exec.revoke_role_internal + # => [pad(16)] +end + +# HELPER PROCEDURES +# ================================================================================================= + +#! Asserts that a role symbol is non-zero. +#! +#! Inputs: [role_symbol] +#! Outputs: [role_symbol] +#! +#! Where: +#! - role_symbol is the encoded role symbol. +#! +#! Panics if: +#! - role_symbol is zero. +#! +#! Invocation: exec +proc assert_role_symbol_non_zero + dup eq.0 + assertz.err=ERR_ROLE_SYMBOL_ZERO + # => [role_symbol] +end + +#! Loads the root admin config word from storage. +#! +#! Inputs: [] +#! Outputs: [root_admin_suffix, root_admin_prefix, nominated_root_admin_suffix, nominated_root_admin_prefix] +#! +#! Where: +#! - root_admin_{suffix,prefix} are the suffix and prefix felts of the current root admin account ID. +#! - nominated_root_admin_{suffix,prefix} are the suffix and prefix felts of the pending root admin account ID. +#! +#! Invocation: exec +proc get_root_admin_config + push.ROOT_ADMIN_CONFIG_SLOT[0..2] exec.active_account::get_item + # => [root_admin_suffix, root_admin_prefix, nominated_root_admin_suffix, nominated_root_admin_prefix] +end + +#! Stores the root admin config word to storage. +#! +#! Inputs: [root_admin_suffix, root_admin_prefix, nominated_root_admin_suffix, nominated_root_admin_prefix] +#! Outputs: [] +#! +#! Where: +#! - root_admin_{suffix,prefix} are the suffix and prefix felts of the current root admin account ID. +#! - nominated_root_admin_{suffix,prefix} are the suffix and prefix felts of the pending root admin account ID. +#! +#! Invocation: exec +proc set_root_admin_config + push.ROOT_ADMIN_CONFIG_SLOT[0..2] exec.native_account::set_item + # => [OLD_ADMIN_CONFIG_WORD] + + dropw + # => [] +end + +#! Returns the current root admin account ID. +#! +#! Inputs: [] +#! Outputs: [root_admin_suffix, root_admin_prefix] +#! +#! Where: +#! - root_admin_{suffix,prefix} are the suffix and prefix felts of the current root admin account ID. +#! +#! Invocation: exec +proc get_root_admin_internal + exec.get_root_admin_config + # => [root_admin_suffix, root_admin_prefix, nominated_root_admin_suffix, nominated_root_admin_prefix] + + movup.2 drop movup.2 drop + # => [root_admin_suffix, root_admin_prefix] +end + +#! Returns the nominated root admin account ID. +#! +#! Inputs: [] +#! Outputs: [nominated_root_admin_suffix, nominated_root_admin_prefix] +#! +#! Where: +#! - nominated_root_admin_{suffix,prefix} are the suffix and prefix felts of the pending root admin account ID. +#! +#! Invocation: exec +proc get_nominated_root_admin_internal + exec.get_root_admin_config + # => [root_admin_suffix, root_admin_prefix, nominated_root_admin_suffix, nominated_root_admin_prefix] + + drop drop + # => [nominated_root_admin_suffix, nominated_root_admin_prefix] +end + +#! Returns whether the note sender is the current root admin. +#! +#! Inputs: [] +#! Outputs: [is_root_admin] +#! +#! Where: +#! - is_root_admin is 1 if the note sender is the current root admin, otherwise 0. +#! +#! Invocation: exec +proc is_sender_root_admin + exec.active_note::get_sender + # => [sender_suffix, sender_prefix] + + exec.get_root_admin_internal + # => [root_admin_suffix, root_admin_prefix, sender_suffix, sender_prefix] + + exec.account_id::is_equal + # => [is_root_admin] +end + +#! Asserts that the note sender is the current root admin. +#! +#! Inputs: [] +#! Outputs: [] +#! +#! Panics if: +#! - the note sender is not the current root admin. +#! +#! Invocation: exec +proc assert_sender_is_root_admin_internal + exec.is_sender_root_admin + # => [is_root_admin] + + assert.err=ERR_SENDER_NOT_ROOT_ADMIN + # => [] +end + + +#! Returns the number of currently active roles. +#! +#! Inputs: [] +#! Outputs: [active_role_count] +#! +#! Where: +#! - active_role_count is the number of roles with at least one member. +#! +#! Invocation: exec +proc get_active_role_count_internal + push.RBAC_STATE_SLOT[0..2] exec.active_account::get_item + # => [active_role_count, 0, 0, 0] + + movdn.3 drop drop drop + # => [active_role_count] +end + +#! Stores the number of currently active roles. +#! +#! Inputs: [active_role_count] +#! Outputs: [] +#! +#! Where: +#! - active_role_count is the number of roles with at least one member. +#! +#! Invocation: exec +proc set_active_role_count_internal + push.0.0.0 movup.3 + # => [active_role_count, 0, 0, 0] + + push.RBAC_STATE_SLOT[0..2] + # => [slot_suffix, slot_prefix, active_role_count, 0, 0, 0] + + exec.native_account::set_item + # => [OLD_RBAC_STATE] + + dropw + # => [] +end + + +#! Returns the config word for a role. +#! +#! Inputs: [role_symbol] +#! Outputs: [member_count, admin_role_symbol, active_role_index, 0] +#! +#! Where: +#! - role_symbol is the encoded role symbol. +#! - member_count is the number of current role members. +#! - admin_role_symbol is the delegated admin role symbol, or 0 for root-admin-managed roles. +#! - active_role_index is the role's index in the active roles map; meaningful only when member_count > 0. +#! +#! Invocation: exec +proc get_role_config + push.0.0.0 + # => [0, 0, 0, role_symbol] + + push.ROLE_CONFIGS_SLOT[0..2] + # => [slot_suffix, slot_prefix, 0, 0, 0, role_symbol] + + exec.active_account::get_map_item + # => [member_count, admin_role_symbol, active_role_index, 0] +end + + +#! Writes the config word for a role. +#! +#! Inputs: [role_symbol, member_count, admin_role_symbol, active_role_index, 0] +#! Outputs: [] +#! +#! Where: +#! - role_symbol is the encoded role symbol. +#! - member_count is the number of current role members. +#! - admin_role_symbol is the delegated admin role symbol, or 0 for root-admin-managed roles. +#! - active_role_index is the role's index in the active roles map; meaningful only when member_count > 0. +#! +#! Invocation: exec +proc set_role_config + push.0.0.0 + # => [0, 0, 0, role_symbol, member_count, admin_role_symbol, active_role_index, 0] + + push.ROLE_CONFIGS_SLOT[0..2] + # => [slot_suffix, slot_prefix, 0, 0, 0, role_symbol, member_count, admin_role_symbol, active_role_index, 0] + + exec.native_account::set_map_item + # => [OLD_ROLE_CONFIG] + + dropw + # => [] +end + + +#! Returns whether a role exists. +#! +#! A role is considered to exist when it has at least one member. +#! +#! Inputs: [role_symbol] +#! Outputs: [role_exists] +#! +#! Where: +#! - role_symbol is the encoded role symbol. +#! - role_exists is 1 if the role has at least one member, otherwise 0. +#! +#! Invocation: exec +proc role_exists_internal + exec.get_role_config + # => [member_count, admin_role_symbol, active_role_index, 0] + + movdn.3 drop drop drop + # => [member_count] + + neq.0 + # => [role_exists] +end + +#! Returns the stored membership entry for a role pair. +#! +#! Inputs: [role_symbol, account_suffix, account_prefix] +#! Outputs: [is_member, member_index] +#! +#! Where: +#! - role_symbol is the encoded role symbol. +#! - account_{suffix,prefix} are the suffix and prefix felts of the account ID. +#! - is_member is 1 if the account holds the role, otherwise 0. +#! - member_index is the stored member index when is_member is 1; unspecified otherwise. +#! +#! Invocation: exec +proc get_role_member_entry + push.0 + # => [0, role_symbol, account_suffix, account_prefix] + + push.ROLE_MEMBER_INDEX_SLOT[0..2] + # => [slot_suffix, slot_prefix, 0, role_symbol, account_suffix, account_prefix] + + exec.active_account::get_map_item + # => [is_member, member_index, 0, 0] + + movup.3 drop movup.2 drop + # => [is_member, member_index] +end + + +#! Returns whether an account holds a role. +#! +#! Inputs: [role_symbol, account_suffix, account_prefix] +#! Outputs: [has_role] +#! +#! Where: +#! - role_symbol is the encoded role symbol. +#! - account_{suffix,prefix} are the suffix and prefix felts of the account ID. +#! - has_role is 1 if the account holds the role, otherwise 0. +#! +#! Invocation: exec +proc has_role_internal + exec.get_role_member_entry + # => [is_member, member_index] + + swap drop + # => [is_member] +end + + +#! Returns whether the note sender holds a role. +#! +#! Inputs: [role_symbol] +#! Outputs: [has_role] +#! +#! Where: +#! - role_symbol is the encoded role symbol. +#! - has_role is 1 if the note sender holds the role, otherwise 0. +#! +#! Invocation: exec +proc is_sender_in_role + exec.active_note::get_sender + # => [sender_suffix, sender_prefix, role_symbol] + + movup.2 + # => [role_symbol, sender_suffix, sender_prefix] + + exec.has_role_internal + # => [has_role] +end + + +#! Asserts that the note sender is the root admin or a member of the role's delegated admin role. +#! +#! Inputs: [role_symbol] +#! Outputs: [] +#! +#! Where: +#! - role_symbol is the encoded role symbol whose admin authority is being checked. +#! +#! Panics if: +#! - the note sender is neither the current root admin nor a holder of the role's delegated admin role. +#! +#! Invocation: exec +proc assert_sender_is_root_or_role_admin + exec.is_sender_root_admin + # => [is_root_admin, role_symbol] + if.true + drop + # => [] + else + exec.get_role_config + # => [member_count, admin_role_symbol, active_role_index, 0] + + drop movdn.2 drop drop + # => [admin_role_symbol] + + dup eq.0 + assertz.err=ERR_SENDER_NOT_ROOT_OR_ROLE_ADMIN + + exec.is_sender_in_role + # => [has_admin_role] + + assert.err=ERR_SENDER_NOT_ROOT_OR_ROLE_ADMIN + end +end + +#! Adds a role to the active role set if it has members and is not already active. +#! +#! Inputs: [role_symbol] +#! Outputs: [] +#! +#! Where: +#! - role_symbol is the encoded role symbol to activate. +#! +#! Invocation: exec +proc add_role_to_active_set + dup exec.get_role_config + # => [member_count, admin_role_symbol, active_role_index, 0, role_symbol] + + dup neq.0 + # => [is_active, member_count, admin_role_symbol, active_role_index, 0, role_symbol] + + if.true + dropw drop + # => [] + else + dup eq.0 + # => [is_empty, member_count, admin_role_symbol, active_role_index, 0, role_symbol] + + if.true + dropw drop + # => [] + else + movup.2 drop + # => [member_count, admin_role_symbol, 0, role_symbol] + + movup.3 + # => [role_symbol, member_count, admin_role_symbol, 0] + + exec.activate_role + # => [] + end + end +end + + +#! Activates a role that is known to have members and not already be active. +#! +#! Inputs: [role_symbol, member_count, admin_role_symbol, 0] +#! Outputs: [] +#! +#! Where: +#! - role_symbol is the encoded role symbol to activate. +#! - member_count is the number of current role members. +#! - admin_role_symbol is the delegated admin role symbol. +#! +#! Invocation: exec +proc activate_role + exec.get_active_role_count_internal + # => [active_role_count, role_symbol, member_count, admin_role_symbol, 0] + + dup push.0.0.0 + # => [0, 0, 0, active_role_count, active_role_count, role_symbol, member_count, admin_role_symbol, 0] + + dup.5 + # => [role_symbol, 0, 0, 0, active_role_count, active_role_count, role_symbol, member_count, admin_role_symbol, 0] + + push.0.0.0 movup.3 swapw + # => [0, 0, 0, active_role_count, role_symbol, 0, 0, 0, active_role_count, role_symbol, member_count, admin_role_symbol, 0] + + push.ACTIVE_ROLES_SLOT[0..2] exec.native_account::set_map_item + # => [OLD_ACTIVE_ROLE_WORD, active_role_count, role_symbol, member_count, admin_role_symbol, 0] + + dropw dup add.1 + # => [active_role_count_plus_one, active_role_count, role_symbol, member_count, admin_role_symbol, 0] + + exec.set_active_role_count_internal + # => [active_role_index, role_symbol, member_count, admin_role_symbol, 0] + + movdn.3 + # => [role_symbol, member_count, admin_role_symbol, active_role_index, 0] + + exec.set_role_config + # => [] +end + + +#! Removes a role from the active role set when it becomes empty. +#! +#! Inputs: [role_symbol] +#! Outputs: [] +#! +#! Where: +#! - role_symbol is the encoded role symbol to deactivate. +#! +#! Invocation: exec +proc remove_role_from_active_set + dup exec.get_role_config + # => [member_count, admin_role_symbol, active_role_index, 0, role_symbol] + + movup.2 movup.4 + # => [role_symbol, active_role_index, member_count, admin_role_symbol, 0] + + movup.4 drop movup.3 drop movup.2 drop + # => [role_symbol, active_role_index] + + exec.deactivate_role + # => [] +end + + + +#! Deactivates a role that is known to be active. +#! +#! Inputs: [role_symbol, active_role_index] +#! Outputs: [] +#! +#! Where: +#! - role_symbol is the encoded role symbol to deactivate. +#! - active_role_index is the role's current index in the active role set. +#! +#! Invocation: exec +proc deactivate_role + exec.get_active_role_count_internal + # => [active_role_count, role_symbol, active_role_index] + + sub.1 + # => [last_index, role_symbol, active_role_index] + + dup.2 dup.1 neq + # => [should_move_last_role, last_index, role_symbol, active_role_index] + + if.true + movup.2 swap + # => [last_index, active_role_index, role_symbol] + + exec.move_last_active_role + # => [role_symbol] + else + movup.2 drop drop + # => [role_symbol] + end + + exec.get_active_role_count_internal + # => [active_role_count, role_symbol] + + sub.1 + # => [last_index, role_symbol] + + padw dup.4 push.0.0.0 + # => [0, 0, 0, last_index, 0, 0, 0, 0, last_index, role_symbol] + + push.ACTIVE_ROLES_SLOT[0..2] exec.native_account::set_map_item + # => [OLD_ACTIVE_ROLE_WORD, last_index, role_symbol] + + dropw + # => [last_index, role_symbol] + + dup exec.set_active_role_count_internal + # => [last_index, role_symbol] + + drop + # => [role_symbol] + + dup exec.get_role_config + # => [member_count, admin_role_symbol, active_role_index, 0, role_symbol] + + movup.2 drop + # => [member_count, admin_role_symbol, 0, role_symbol] + + movup.3 + # => [role_symbol, member_count, admin_role_symbol, 0] + + push.0 movdn.3 + # => [role_symbol, member_count, admin_role_symbol, 0, 0] + + exec.set_role_config + # => [] +end + + +#! Moves the last active role into the removed role's active role slot. +#! +#! Inputs: [source_index, target_index] +#! Outputs: [] +#! +#! Where: +#! - source_index is the current index of the last active role. +#! - target_index is the vacated index that should receive the moved role. +#! +#! Invocation: exec +proc move_last_active_role + dup push.0.0.0 push.ACTIVE_ROLES_SLOT[0..2] + exec.active_account::get_map_item + # => [moved_role_symbol, 0, 0, 0, source_index, target_index] + + movdn.3 drop drop drop + # => [moved_role_symbol, source_index, target_index] + + dup push.0.0.0 movup.3 + # => [moved_role_symbol, 0, 0, 0, moved_role_symbol, source_index, target_index] + + dup.6 push.0.0.0 + # => [0, 0, 0, target_index, moved_role_symbol, 0, 0, 0, moved_role_symbol, source_index, target_index] + + push.ACTIVE_ROLES_SLOT[0..2] exec.native_account::set_map_item + # => [OLD_ACTIVE_ROLE_WORD, moved_role_symbol, source_index, target_index] + + dropw + # => [moved_role_symbol, source_index, target_index] + + dup exec.get_role_config + # => [moved_member_count, moved_admin_role_symbol, moved_active_role_index, moved_0, moved_role_symbol, source_index, target_index] + + movup.2 drop + # => [moved_member_count, moved_admin_role_symbol, moved_0, moved_role_symbol, source_index, target_index] + + dup.5 + # => [moved_active_role_index, moved_member_count, moved_admin_role_symbol, moved_0, moved_role_symbol, source_index, target_index] + + movup.4 swap movdn.3 + # => [moved_role_symbol, moved_member_count, moved_admin_role_symbol, moved_active_role_index, moved_0, source_index, target_index] + + exec.set_role_config + # => [source_index, target_index] + + drop drop + # => [] +end + +#! Adds an account to a role's member enumeration. +#! Assumes the account has already been checked not to hold the role. +#! If the role does not yet exist, it is created first. +#! +#! Inputs: [role_symbol, account_suffix, account_prefix] +#! Outputs: [] +#! +#! Where: +#! - role_symbol is the encoded role symbol to add the account to. +#! - account_{suffix,prefix} are the suffix and prefix felts of the account ID to add. +#! +#! Locals: +#! 0: role_symbol (`ROLE_SYMBOL_LOC`) +#! 1: account_suffix (`ACCOUNT_SUFFIX_LOC`) +#! 2: account_prefix (`ACCOUNT_PREFIX_LOC`) +#! 3: member_count (`MEMBER_COUNT_LOC`) +#! +#! Invocation: exec +@locals(4) +proc add_to_role_enumeration + loc_store.ROLE_SYMBOL_LOC + # => [account_suffix, account_prefix] + + loc_store.ACCOUNT_SUFFIX_LOC + # => [account_prefix] + + loc_store.ACCOUNT_PREFIX_LOC + # => [] + + loc_load.ROLE_SYMBOL_LOC + exec.get_role_config + # => [member_count, admin_role_symbol, active_role_index, 0] + + dup loc_store.MEMBER_COUNT_LOC + # => [member_count, admin_role_symbol, active_role_index, 0] + + # write ROLE_MEMBER_INDEX_SLOT[(role_symbol, account_id)] = [1, member_index, 0, 0] + push.0.0 loc_load.MEMBER_COUNT_LOC push.1 + # => [1, member_index, 0, 0, member_count, admin_role_symbol, active_role_index, 0] + + loc_load.ACCOUNT_PREFIX_LOC loc_load.ACCOUNT_SUFFIX_LOC loc_load.ROLE_SYMBOL_LOC push.0 + # => [0, role_symbol, account_suffix, account_prefix, 1, member_index, 0, 0, + # member_count, admin_role_symbol, active_role_index, 0] + + push.ROLE_MEMBER_INDEX_SLOT[0..2] exec.native_account::set_map_item + # => [OLD_MEMBER_INDEX_WORD, member_count, admin_role_symbol, active_role_index, 0] + + dropw + # => [member_count, admin_role_symbol, active_role_index, 0] + + # write ROLE_MEMBERS_SLOT[(role_symbol, member_index)] = account_id + loc_load.ACCOUNT_PREFIX_LOC loc_load.ACCOUNT_SUFFIX_LOC push.0.0 movup.3 movup.3 + # => [account_suffix, account_prefix, 0, 0, member_count, admin_role_symbol, active_role_index, 0] + + loc_load.MEMBER_COUNT_LOC loc_load.ROLE_SYMBOL_LOC push.0.0 + # => [0, 0, role_symbol, member_index, account_suffix, account_prefix, 0, 0, + # member_count, admin_role_symbol, active_role_index, 0] + + push.ROLE_MEMBERS_SLOT[0..2] exec.native_account::set_map_item + # => [OLD_MEMBER_WORD, member_count, admin_role_symbol, active_role_index, 0] + + dropw + # => [member_count, admin_role_symbol, active_role_index, 0] + + dup eq.0 + # => [is_first_member, member_count, admin_role_symbol, active_role_index, 0] + + if.true + movup.2 drop drop + # => [admin_role_symbol, 0] + + push.1 loc_load.ROLE_SYMBOL_LOC + # => [role_symbol, 1, admin_role_symbol, 0] + + exec.activate_role + # => [] + else + add.1 + # => [member_count_plus_one, admin_role_symbol, active_role_index, 0] + + loc_load.ROLE_SYMBOL_LOC + # => [role_symbol, member_count_plus_one, admin_role_symbol, active_role_index, 0] + + exec.set_role_config + # => [] + end +end + + +#! Internal helper used by `grant_role` to assign a role to an account. +#! +#! Inputs: [role_symbol, account_suffix, account_prefix] +#! Outputs: [] +#! +#! Where: +#! - role_symbol is the encoded role symbol to grant. +#! - account_{suffix,prefix} are the suffix and prefix felts of the account ID to grant. +#! +#! Panics if: +#! - the account ID is invalid. +#! +#! Invocation: exec +proc grant_role_internal + dup.2 dup.2 exec.account_id::validate + # => [role_symbol, account_suffix, account_prefix] + + dup.2 dup.2 dup.2 + # => [role_symbol, account_suffix, account_prefix, role_symbol, account_suffix, account_prefix] + + exec.has_role_internal + # => [has_role, role_symbol, account_suffix, account_prefix] + + if.true + drop drop drop + # => [] + else + exec.add_to_role_enumeration + # => [] + end +end + +#! Removes an account from a role's enumeration. +#! +#! This procedure: +#! - loads the account's current member index in the role. +#! - if the account is not the last member, moves the last member into the removed +#! index and updates that moved account's reverse index. +#! - clears the unused last member entry and the removed account's reverse-index entry. +#! - decrements the role member count. +#! - removes the role from the active role set if it becomes empty. +#! +#! Inputs: [role_symbol, account_suffix, account_prefix] +#! Outputs: [] +#! +#! Where: +#! - role_symbol is the encoded role symbol to remove the account from. +#! - account_{suffix,prefix} are the suffix and prefix felts of the account ID to remove. +#! +#! Panics if: +#! - the account does not hold the role. +#! +#! Locals: +#! 0: role_symbol (`ROLE_SYMBOL_LOC`) +#! 1: account_suffix (`ACCOUNT_SUFFIX_LOC`) +#! 2: account_prefix (`ACCOUNT_PREFIX_LOC`) +#! 3: remove_index (`REMOVE_INDEX_LOC`) +#! 4: last_index (`LAST_INDEX_LOC`) +#! +#! Invocation: exec +@locals(5) +proc remove_from_role_enumeration + loc_store.ROLE_SYMBOL_LOC + # => [account_suffix, account_prefix] + + loc_store.ACCOUNT_SUFFIX_LOC + # => [account_prefix] + + loc_store.ACCOUNT_PREFIX_LOC + # => [] + + loc_load.ACCOUNT_PREFIX_LOC + loc_load.ACCOUNT_SUFFIX_LOC + loc_load.ROLE_SYMBOL_LOC + # => [role_symbol, account_suffix, account_prefix] + + exec.get_role_member_entry + # => [is_member, member_index] + + dup eq.0 + assertz.err=ERR_ACCOUNT_NOT_IN_ROLE + # => [is_member, member_index] + + drop + # => [member_index] + + loc_store.REMOVE_INDEX_LOC + # => [] + + loc_load.ROLE_SYMBOL_LOC + exec.get_role_config + # => [member_count, admin_role_symbol, active_role_index, 0] + + sub.1 + loc_store.LAST_INDEX_LOC + # => [admin_role_symbol, active_role_index, 0] + + drop drop drop + # => [] + + loc_load.REMOVE_INDEX_LOC + loc_load.LAST_INDEX_LOC + neq + # => [should_move_last_member] + + if.true + loc_load.LAST_INDEX_LOC + loc_load.ROLE_SYMBOL_LOC + push.0.0 + # => [0, 0, role_symbol, last_index] + + push.ROLE_MEMBERS_SLOT[0..2] exec.active_account::get_map_item + # => [moved_account_suffix, moved_account_prefix, 0, 0] + + movup.2 drop movup.2 drop + # => [moved_account_suffix, moved_account_prefix] + + push.0.0 movup.3 movup.3 + # => [moved_account_suffix, moved_account_prefix, 0, 0] + + loc_load.REMOVE_INDEX_LOC loc_load.ROLE_SYMBOL_LOC push.0.0 + # => [0, 0, role_symbol, remove_index, moved_account_suffix, moved_account_prefix, 0, 0] + + push.ROLE_MEMBERS_SLOT[0..2] exec.native_account::set_map_item + # => [OLD_MEMBER_WORD] + + dropw + # => [] + + loc_load.REMOVE_INDEX_LOC loc_load.ROLE_SYMBOL_LOC push.0.0 + # => [0, 0, role_symbol, remove_index] + + push.ROLE_MEMBERS_SLOT[0..2] + exec.active_account::get_map_item + # => [moved_account_suffix, moved_account_prefix, 0, 0] + + movup.2 drop movup.2 drop + # => [moved_account_suffix, moved_account_prefix] + + loc_load.ROLE_SYMBOL_LOC push.0 + # => [0, role_symbol, moved_account_suffix, moved_account_prefix] + + push.0.0 loc_load.REMOVE_INDEX_LOC push.1 swapw + # => [0, role_symbol, moved_account_suffix, moved_account_prefix, 1, member_index, 0, 0] + + push.ROLE_MEMBER_INDEX_SLOT[0..2] + exec.native_account::set_map_item + # => [OLD_MEMBER_INDEX_WORD] + + dropw + # => [] + end + + padw loc_load.LAST_INDEX_LOC loc_load.ROLE_SYMBOL_LOC push.0.0 + # => [0, 0, role_symbol, last_index, 0, 0, 0, 0] + + push.ROLE_MEMBERS_SLOT[0..2] exec.native_account::set_map_item + # => [OLD_MEMBER_WORD] + + dropw + # => [] + + padw loc_load.ACCOUNT_PREFIX_LOC loc_load.ACCOUNT_SUFFIX_LOC loc_load.ROLE_SYMBOL_LOC push.0 + # => [0, role_symbol, account_suffix, account_prefix, 0, 0, 0, 0] + + push.ROLE_MEMBER_INDEX_SLOT[0..2] exec.native_account::set_map_item + # => [OLD_MEMBER_INDEX_WORD] + + dropw + # => [] + + loc_load.ROLE_SYMBOL_LOC + exec.get_role_config + # => [member_count, admin_role_symbol, active_role_index, 0] + + dup sub.1 swap drop + # => [member_count_minus_one, admin_role_symbol, active_role_index, 0] + + loc_load.ROLE_SYMBOL_LOC + # => [role_symbol, member_count_minus_one, admin_role_symbol, active_role_index, 0] + + exec.set_role_config + # => [] + + loc_load.LAST_INDEX_LOC eq.0 + # => [is_role_empty] + + if.true + loc_load.ROLE_SYMBOL_LOC + exec.remove_role_from_active_set + # => [] + end +end + + +#! Internal helper used by `revoke_role` to remove a role from an account. +#! +#! Inputs: [role_symbol, account_suffix, account_prefix] +#! Outputs: [] +#! +#! Where: +#! - role_symbol is the encoded role symbol to revoke. +#! - account_{suffix,prefix} are the suffix and prefix felts of the account ID to revoke. +#! +#! Panics if: +#! - the account ID is invalid. +#! - the account does not hold the role. +#! +#! Invocation: exec +proc revoke_role_internal + dup.2 dup.2 exec.account_id::validate + # => [role_symbol, account_suffix, account_prefix] + + exec.remove_from_role_enumeration + # => [] +end diff --git a/crates/miden-standards/src/account/access/mod.rs b/crates/miden-standards/src/account/access/mod.rs index f7c58c875b..6e1850831e 100644 --- a/crates/miden-standards/src/account/access/mod.rs +++ b/crates/miden-standards/src/account/access/mod.rs @@ -1,20 +1,27 @@ use miden_protocol::account::{AccountComponent, AccountId}; pub mod ownable2step; +pub mod role_based_access_control; /// Access control configuration for account components. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AccessControl { /// Uses two-step ownership transfer with the provided initial owner. Ownable2Step { owner: AccountId }, + /// Uses role-based access control with the provided initial root admin. + RoleBasedAccessControl { root_admin: AccountId }, } impl From for AccountComponent { fn from(access_control: AccessControl) -> Self { match access_control { AccessControl::Ownable2Step { owner } => Ownable2Step::new(owner).into(), + AccessControl::RoleBasedAccessControl { root_admin } => { + RoleBasedAccessControl::new(root_admin).into() + }, } } } pub use ownable2step::{Ownable2Step, Ownable2StepError}; +pub use role_based_access_control::{RoleBasedAccessControl, RoleInit}; diff --git a/crates/miden-standards/src/account/access/role_based_access_control.rs b/crates/miden-standards/src/account/access/role_based_access_control.rs new file mode 100644 index 0000000000..2f284f9586 --- /dev/null +++ b/crates/miden-standards/src/account/access/role_based_access_control.rs @@ -0,0 +1,339 @@ +use alloc::collections::{BTreeMap, BTreeSet}; +use alloc::vec::Vec; + +use miden_protocol::account::component::{ + AccountComponentMetadata, + FeltSchema, + SchemaType, + StorageSchema, + StorageSlotSchema, +}; +use miden_protocol::account::{ + AccountComponent, + AccountId, + AccountType, + RoleSymbol, + StorageMap, + StorageMapKey, + StorageSlot, + StorageSlotName, +}; +use miden_protocol::utils::sync::LazyLock; +use miden_protocol::{Felt, Word}; + +use crate::account::components::role_based_access_control_library; + +static ROOT_ADMIN_CONFIG_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::access::role_based_access_control::admin_config") + .expect("storage slot name should be valid") +}); +static RBAC_STATE_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::access::role_based_access_control::state") + .expect("storage slot name should be valid") +}); +static ACTIVE_ROLES_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::access::role_based_access_control::active_roles") + .expect("storage slot name should be valid") +}); +static ROLE_CONFIGS_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::access::role_based_access_control::role_config") + .expect("storage slot name should be valid") +}); +static ROLE_MEMBERS_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::access::role_based_access_control::role_members") + .expect("storage slot name should be valid") +}); +static ROLE_MEMBER_INDEX_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::standards::access::role_based_access_control::role_member_index") + .expect("storage slot name should be valid") +}); + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct RoleInit { + pub admin_role: Option, + pub members: BTreeSet, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RoleBasedAccessControl { + root_admin: AccountId, + roles: BTreeMap, +} + +impl RoleBasedAccessControl { + pub const NAME: &'static str = + "miden::standards::components::access::role_based_access_control"; + + pub fn new(root_admin: AccountId) -> Self { + Self { root_admin, roles: BTreeMap::new() } + } + + pub fn root_admin(&self) -> AccountId { + self.root_admin + } + + pub fn roles(&self) -> &BTreeMap { + &self.roles + } + + pub fn with_role(mut self, role: RoleSymbol) -> Self { + self.roles.entry(role).or_default(); + self + } + + pub fn with_role_admin(mut self, role: RoleSymbol, admin_role: Option) -> Self { + if let Some(admin_role) = admin_role.as_ref() { + self.roles.entry(admin_role.clone()).or_default(); + } + + self.roles.entry(role).or_default().admin_role = admin_role; + self + } + + pub fn with_role_member(mut self, role: RoleSymbol, account_id: AccountId) -> Self { + self.roles.entry(role).or_default().members.insert(account_id); + self + } + + pub fn root_admin_config_slot() -> &'static StorageSlotName { + &ROOT_ADMIN_CONFIG_SLOT_NAME + } + + pub fn state_slot() -> &'static StorageSlotName { + &RBAC_STATE_SLOT_NAME + } + + pub fn active_roles_slot() -> &'static StorageSlotName { + &ACTIVE_ROLES_SLOT_NAME + } + + pub fn role_configs_slot() -> &'static StorageSlotName { + &ROLE_CONFIGS_SLOT_NAME + } + + pub fn role_members_slot() -> &'static StorageSlotName { + &ROLE_MEMBERS_SLOT_NAME + } + + pub fn role_member_index_slot() -> &'static StorageSlotName { + &ROLE_MEMBER_INDEX_SLOT_NAME + } + + pub fn root_admin_config_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + ( + Self::root_admin_config_slot().clone(), + StorageSlotSchema::value( + "RBAC root admin and nominated root admin", + [ + FeltSchema::felt("root_admin_suffix"), + FeltSchema::felt("root_admin_prefix"), + FeltSchema::felt("nominated_root_admin_suffix"), + FeltSchema::felt("nominated_root_admin_prefix"), + ], + ), + ) + } + + pub fn state_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + ( + Self::state_slot().clone(), + StorageSlotSchema::value( + "RBAC global state", + [ + FeltSchema::felt("active_role_count"), + FeltSchema::new_void(), + FeltSchema::new_void(), + FeltSchema::new_void(), + ], + ), + ) + } + + pub fn active_roles_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + ( + Self::active_roles_slot().clone(), + StorageSlotSchema::map( + "Active roles indexed by active role position", + SchemaType::native_felt(), + SchemaType::role_symbol(), + ), + ) + } + + pub fn role_configs_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + ( + Self::role_configs_slot().clone(), + StorageSlotSchema::map( + "Per-role RBAC configuration", + SchemaType::role_symbol(), + SchemaType::native_word(), + ), + ) + } + + pub fn role_members_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + ( + Self::role_members_slot().clone(), + StorageSlotSchema::map( + "Role members indexed by role symbol and member index", + SchemaType::native_word(), + SchemaType::native_word(), + ), + ) + } + + pub fn role_member_index_slot_schema() -> (StorageSlotName, StorageSlotSchema) { + ( + Self::role_member_index_slot().clone(), + StorageSlotSchema::map( + "Role member reverse index lookup", + SchemaType::native_word(), + SchemaType::native_word(), + ), + ) + } + + pub fn component_metadata() -> AccountComponentMetadata { + let storage_schema = StorageSchema::new(vec![ + Self::root_admin_config_slot_schema(), + Self::state_slot_schema(), + Self::active_roles_slot_schema(), + Self::role_configs_slot_schema(), + Self::role_members_slot_schema(), + Self::role_member_index_slot_schema(), + ]) + .expect("storage schema should be valid"); + + AccountComponentMetadata::new(Self::NAME, AccountType::all()) + .with_description("Role-based access control component") + .with_storage_schema(storage_schema) + } +} + +impl From for AccountComponent { + fn from(rbac: RoleBasedAccessControl) -> Self { + let root_admin_config_slot = StorageSlot::with_value( + RoleBasedAccessControl::root_admin_config_slot().clone(), + Word::from([ + rbac.root_admin.suffix(), + rbac.root_admin.prefix().as_felt(), + Felt::ZERO, + Felt::ZERO, + ]), + ); + + let mut active_role_entries = Vec::new(); + let mut role_config_entries = Vec::new(); + let mut role_member_entries = Vec::new(); + let mut role_member_index_entries = Vec::new(); + let mut active_role_count = 0u64; + + for (role_symbol, role_init) in &rbac.roles { + let role_symbol_felt = Felt::from(role_symbol); + let admin_role_felt = + role_init.admin_role.as_ref().map(Felt::from).unwrap_or(Felt::ZERO); + let member_count = role_init.members.len() as u64; + let active_role_index = if member_count > 0 { + let active_index = active_role_count; + active_role_entries.push(( + StorageMapKey::from_raw(Word::from([ + Felt::ZERO, + Felt::ZERO, + Felt::ZERO, + Felt::new(active_index), + ])), + Word::from([role_symbol_felt, Felt::ZERO, Felt::ZERO, Felt::ZERO]), + )); + active_role_count += 1; + Felt::new(active_index) + } else { + Felt::ZERO + }; + + role_config_entries.push(( + StorageMapKey::from_raw(Word::from([ + Felt::ZERO, + Felt::ZERO, + Felt::ZERO, + role_symbol_felt, + ])), + Word::from([ + Felt::new(member_count), + admin_role_felt, + active_role_index, + Felt::ZERO, + ]), + )); + + for (member_index, member) in role_init.members.iter().enumerate() { + role_member_entries.push(( + StorageMapKey::from_raw(Word::from([ + Felt::ZERO, + Felt::ZERO, + role_symbol_felt, + Felt::new(member_index as u64), + ])), + Word::from([ + member.suffix(), + member.prefix().as_felt(), + Felt::ZERO, + Felt::ZERO, + ]), + )); + role_member_index_entries.push(( + StorageMapKey::from_raw(Word::from([ + Felt::ZERO, + role_symbol_felt, + member.suffix(), + member.prefix().as_felt(), + ])), + Word::from([ + Felt::new(1), + Felt::new(member_index as u64), + Felt::ZERO, + Felt::ZERO, + ]), + )); + } + } + + let state_slot = StorageSlot::with_value( + RoleBasedAccessControl::state_slot().clone(), + Word::from([Felt::new(active_role_count), Felt::ZERO, Felt::ZERO, Felt::ZERO]), + ); + let active_roles_slot = StorageSlot::with_map( + RoleBasedAccessControl::active_roles_slot().clone(), + StorageMap::with_entries(active_role_entries) + .expect("active role entries should be unique"), + ); + let role_configs_slot = StorageSlot::with_map( + RoleBasedAccessControl::role_configs_slot().clone(), + StorageMap::with_entries(role_config_entries) + .expect("role config entries should be unique"), + ); + let role_members_slot = StorageSlot::with_map( + RoleBasedAccessControl::role_members_slot().clone(), + StorageMap::with_entries(role_member_entries) + .expect("role member entries should be unique"), + ); + let role_member_index_slot = StorageSlot::with_map( + RoleBasedAccessControl::role_member_index_slot().clone(), + StorageMap::with_entries(role_member_index_entries) + .expect("role member index entries should be unique"), + ); + + AccountComponent::new( + role_based_access_control_library(), + vec![ + root_admin_config_slot, + state_slot, + active_roles_slot, + role_configs_slot, + role_members_slot, + role_member_index_slot, + ], + RoleBasedAccessControl::component_metadata(), + ) + .expect("RBAC component should satisfy the requirements of a valid account component") + } +} diff --git a/crates/miden-standards/src/account/components/mod.rs b/crates/miden-standards/src/account/components/mod.rs index 723cbf5d14..61b8bd9d11 100644 --- a/crates/miden-standards/src/account/components/mod.rs +++ b/crates/miden-standards/src/account/components/mod.rs @@ -34,6 +34,15 @@ static OWNABLE2STEP_LIBRARY: LazyLock = LazyLock::new(|| { Library::read_from_bytes(bytes).expect("Shipped Ownable2Step library is well-formed") }); +// Initialize the RoleBasedAccessControl library only once. +static ROLE_BASED_ACCESS_CONTROL_LIBRARY: LazyLock = LazyLock::new(|| { + let bytes = include_bytes!(concat!( + env!("OUT_DIR"), + "/assets/account_components/access/role_based_access_control.masl" + )); + Library::read_from_bytes(bytes).expect("Shipped RoleBasedAccessControl library is well-formed") +}); + // AUTH LIBRARIES // ================================================================================================ @@ -130,6 +139,11 @@ pub fn ownable2step_library() -> Library { OWNABLE2STEP_LIBRARY.clone() } +/// Returns the RoleBasedAccessControl Library. +pub fn role_based_access_control_library() -> Library { + ROLE_BASED_ACCESS_CONTROL_LIBRARY.clone() +} + /// Returns the Basic Fungible Faucet Library. pub fn basic_fungible_faucet_library() -> Library { BASIC_FUNGIBLE_FAUCET_LIBRARY.clone() diff --git a/crates/miden-testing/tests/scripts/mod.rs b/crates/miden-testing/tests/scripts/mod.rs index 9b8c3e12e5..bc63d82db1 100644 --- a/crates/miden-testing/tests/scripts/mod.rs +++ b/crates/miden-testing/tests/scripts/mod.rs @@ -4,5 +4,6 @@ mod ownable2step; mod p2id; mod p2ide; mod pswap; +mod role_based_access_control; mod send_note; mod swap; diff --git a/crates/miden-testing/tests/scripts/role_based_access_control.rs b/crates/miden-testing/tests/scripts/role_based_access_control.rs new file mode 100644 index 0000000000..d86bbfd83b --- /dev/null +++ b/crates/miden-testing/tests/scripts/role_based_access_control.rs @@ -0,0 +1,1425 @@ +extern crate alloc; + +use alloc::string::String; +use core::slice; + +use anyhow::Context; +use miden_processor::crypto::random::RandomCoin; +use miden_protocol::account::{ + Account, + AccountBuilder, + AccountId, + AccountIdVersion, + AccountStorageMode, + AccountType, + RoleSymbol, +}; +use miden_protocol::errors::AccountIdError; +use miden_protocol::note::{Note, NoteType}; +use miden_protocol::{Felt, Word}; +use miden_standards::account::access::RoleBasedAccessControl; +use miden_standards::errors::standards::{ + ERR_ACCOUNT_NOT_IN_ROLE, + ERR_ACTIVE_ROLE_OUT_OF_BOUNDS, + ERR_ROLE_MEMBER_OUT_OF_BOUNDS, + ERR_ROLE_SYMBOL_ZERO, + ERR_ROOT_ADMIN_TRANSFER_IN_PROGRESS, + ERR_SENDER_NOT_NOMINATED_ROOT_ADMIN, + ERR_SENDER_NOT_ROOT_ADMIN, + ERR_SENDER_NOT_ROOT_OR_ROLE_ADMIN, +}; +use miden_standards::testing::note::NoteBuilder; +use miden_testing::{Auth, MockChain, assert_transaction_executor_error}; + +// HELPERS +// ================================================================================================ + +fn create_rbac_account(rbac: RoleBasedAccessControl) -> anyhow::Result { + let account = AccountBuilder::new([9; 32]) + .storage_mode(AccountStorageMode::Public) + .with_auth_component(Auth::IncrNonce) + .with_component(rbac) + .build_existing()?; + + Ok(account) +} + +fn create_rbac_chain(admin: AccountId) -> anyhow::Result<(Account, MockChain)> { + let account = create_rbac_account(RoleBasedAccessControl::new(admin))?; + let mut builder = MockChain::builder(); + builder.add_account(account.clone())?; + + Ok((account, builder.build()?)) +} + +fn test_account_id(seed: u8) -> AccountId { + AccountId::dummy( + [seed; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ) +} + +fn role(name: &str) -> RoleSymbol { + RoleSymbol::new(name).expect("role symbol should be valid") +} + +fn role_config_key(role: &RoleSymbol) -> Word { + Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::from(role)]) +} + +fn active_role_key(index: u64) -> Word { + Word::from([Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::new(index)]) +} + +fn role_member_key(role: &RoleSymbol, index: u64) -> Word { + Word::from([Felt::ZERO, Felt::ZERO, Felt::from(role), Felt::new(index)]) +} + +fn role_member_index_key(role: &RoleSymbol, account_id: AccountId) -> Word { + Word::from([Felt::ZERO, Felt::from(role), account_id.suffix(), account_id.prefix().as_felt()]) +} + +fn account_id_from_felt_pair( + suffix: Felt, + prefix: Felt, +) -> Result, AccountIdError> { + if suffix == Felt::ZERO && prefix == Felt::ZERO { + Ok(None) + } else { + AccountId::try_from_elements(suffix, prefix).map(Some) + } +} + +fn get_root_admins(account: &Account) -> anyhow::Result<(Option, Option)> { + let word = account.storage().get_item(RoleBasedAccessControl::root_admin_config_slot())?; + + Ok(( + account_id_from_felt_pair(word[0], word[1])?, + account_id_from_felt_pair(word[2], word[3])?, + )) +} + +fn get_role_config(account: &Account, role: &RoleSymbol) -> anyhow::Result { + Ok(account + .storage() + .get_map_item(RoleBasedAccessControl::role_configs_slot(), role_config_key(role))?) +} + +fn get_active_role_count(account: &Account) -> anyhow::Result { + Ok(account.storage().get_item(RoleBasedAccessControl::state_slot())?[0].as_canonical_u64()) +} + +fn get_active_role(account: &Account, index: u64) -> anyhow::Result { + let word = account + .storage() + .get_map_item(RoleBasedAccessControl::active_roles_slot(), active_role_key(index))?; + Ok(RoleSymbol::try_from(word[0])?) +} + +fn get_role_member(account: &Account, role: &RoleSymbol, index: u64) -> anyhow::Result { + let word = account + .storage() + .get_map_item(RoleBasedAccessControl::role_members_slot(), role_member_key(role, index))?; + Ok(AccountId::try_from_elements(word[0], word[1])?) +} + +fn get_role_member_index( + account: &Account, + role: &RoleSymbol, + account_id: AccountId, +) -> anyhow::Result> { + let word = account.storage().get_map_item( + RoleBasedAccessControl::role_member_index_slot(), + role_member_index_key(role, account_id), + )?; + if word[0].as_canonical_u64() == 0 { + Ok(None) + } else { + Ok(Some(word[1].as_canonical_u64())) + } +} + +fn build_note(sender: AccountId, code: impl Into, rng_seed: u32) -> anyhow::Result { + let mut rng = RandomCoin::new([Felt::from(rng_seed); 4].into()); + Ok(NoteBuilder::new(sender, &mut rng) + .note_type(NoteType::Private) + .code(code.into()) + .build()?) +} + +async fn execute_note_and_apply( + mock_chain: &MockChain, + account: &Account, + note: &Note, +) -> anyhow::Result { + let tx = mock_chain + .build_tx_context(account.clone(), &[], slice::from_ref(note))? + .build()?; + let executed = tx.execute().await?; + + let mut updated = account.clone(); + updated.apply_delta(executed.account_delta())?; + + Ok(updated) +} + +// SCRIPTS +// ================================================================================================ + +fn transfer_root_admin_script(new_admin: AccountId) -> String { + format!( + r#" + use miden::standards::access::role_based_access_control + + begin + repeat.14 push.0 end + push.{new_admin_prefix} + push.{new_admin_suffix} + call.role_based_access_control::transfer_root_admin + dropw dropw dropw dropw + end + "#, + new_admin_prefix = new_admin.prefix().as_felt(), + new_admin_suffix = Felt::new(new_admin.suffix().as_canonical_u64()), + ) +} + +fn accept_root_admin_script() -> &'static str { + r#" + use miden::standards::access::role_based_access_control + + begin + repeat.16 push.0 end + call.role_based_access_control::accept_root_admin + dropw dropw dropw dropw + end + "# +} + +fn renounce_root_admin_script() -> &'static str { + r#" + use miden::standards::access::role_based_access_control + + begin + repeat.16 push.0 end + call.role_based_access_control::renounce_root_admin + dropw dropw dropw dropw + end + "# +} + +fn set_role_admin_script(role: &RoleSymbol, admin_role: Option<&RoleSymbol>) -> String { + let admin_role = admin_role.map(Felt::from).unwrap_or(Felt::ZERO); + format!( + r#" + use miden::standards::access::role_based_access_control + + begin + repeat.14 push.0 end + push.{admin_role} + push.{role} + call.role_based_access_control::set_role_admin + dropw dropw dropw dropw + end + "#, + admin_role = admin_role, + role = Felt::from(role), + ) +} + +fn grant_role_script(role: &RoleSymbol, account_id: AccountId) -> String { + format!( + r#" + use miden::standards::access::role_based_access_control + + begin + repeat.13 push.0 end + push.{account_prefix} + push.{account_suffix} + push.{role} + call.role_based_access_control::grant_role + dropw dropw dropw dropw + end + "#, + account_prefix = account_id.prefix().as_felt(), + account_suffix = Felt::new(account_id.suffix().as_canonical_u64()), + role = Felt::from(role), + ) +} + +fn revoke_role_script(role: &RoleSymbol, account_id: AccountId) -> String { + format!( + r#" + use miden::standards::access::role_based_access_control + + begin + repeat.13 push.0 end + push.{account_prefix} + push.{account_suffix} + push.{role} + call.role_based_access_control::revoke_role + dropw dropw dropw dropw + end + "#, + account_prefix = account_id.prefix().as_felt(), + account_suffix = Felt::new(account_id.suffix().as_canonical_u64()), + role = Felt::from(role), + ) +} + +fn renounce_role_script(role: &RoleSymbol) -> String { + format!( + r#" + use miden::standards::access::role_based_access_control + + begin + repeat.15 push.0 end + push.{role} + call.role_based_access_control::renounce_role + dropw dropw dropw dropw + end + "#, + role = Felt::from(role), + ) +} + +fn assert_role_member_count_script(role: &RoleSymbol, expected_count: u64) -> String { + format!( + r#" + use miden::standards::access::role_based_access_control + + begin + repeat.15 push.0 end + push.{role} + call.role_based_access_control::get_role_member_count + eq.{expected_count} assert + dropw dropw dropw + drop drop drop + end + "#, + role = Felt::from(role), + expected_count = expected_count, + ) +} + +fn assert_role_admin_script(role: &RoleSymbol, expected_admin_role: Option<&RoleSymbol>) -> String { + let expected_admin_role = expected_admin_role.map(Felt::from).unwrap_or(Felt::ZERO); + + format!( + r#" + use miden::standards::access::role_based_access_control + + begin + repeat.15 push.0 end + push.{role} + call.role_based_access_control::get_role_admin + eq.{expected_admin_role} assert + dropw dropw dropw + drop drop drop + end + "#, + role = Felt::from(role), + expected_admin_role = expected_admin_role, + ) +} + +fn assert_role_exists_script(role: &RoleSymbol, expected_exists: bool) -> String { + let expected_exists = u8::from(expected_exists); + + format!( + r#" + use miden::standards::access::role_based_access_control + + begin + repeat.15 push.0 end + push.{role} + call.role_based_access_control::role_exists + eq.{expected_exists} assert + dropw dropw dropw + drop drop drop + end + "#, + role = Felt::from(role), + expected_exists = expected_exists, + ) +} + +fn assert_has_role_script( + role: &RoleSymbol, + account_id: AccountId, + expected_has_role: bool, +) -> String { + let expected_has_role = u8::from(expected_has_role); + + format!( + r#" + use miden::standards::access::role_based_access_control + + begin + repeat.13 push.0 end + push.{account_prefix} + push.{account_suffix} + push.{role} + call.role_based_access_control::has_role + eq.{expected_has_role} assert + dropw dropw dropw + drop drop drop + end + "#, + account_prefix = account_id.prefix().as_felt(), + account_suffix = Felt::new(account_id.suffix().as_canonical_u64()), + role = Felt::from(role), + expected_has_role = expected_has_role, + ) +} + +fn assert_root_admin_script(expected_admin: Option) -> String { + let (expected_suffix, expected_prefix) = expected_admin + .map(|account_id| { + (Felt::new(account_id.suffix().as_canonical_u64()), account_id.prefix().as_felt()) + }) + .unwrap_or((Felt::ZERO, Felt::ZERO)); + + format!( + r#" + use miden::standards::access::role_based_access_control + + begin + repeat.16 push.0 end + call.role_based_access_control::get_root_admin + eq.{expected_suffix} assert + eq.{expected_prefix} assert + dropw dropw dropw + drop drop + end + "#, + expected_prefix = expected_prefix, + expected_suffix = expected_suffix, + ) +} + +fn assert_nominated_root_admin_script(expected_admin: Option) -> String { + let (expected_suffix, expected_prefix) = expected_admin + .map(|account_id| { + (Felt::new(account_id.suffix().as_canonical_u64()), account_id.prefix().as_felt()) + }) + .unwrap_or((Felt::ZERO, Felt::ZERO)); + + format!( + r#" + use miden::standards::access::role_based_access_control + + begin + repeat.16 push.0 end + call.role_based_access_control::get_nominated_root_admin + eq.{expected_suffix} assert + eq.{expected_prefix} assert + dropw dropw dropw + drop drop + end + "#, + expected_prefix = expected_prefix, + expected_suffix = expected_suffix, + ) +} + +fn set_role_admin_raw_script(role: Felt, admin_role: Felt) -> String { + format!( + r#" + use miden::standards::access::role_based_access_control + + begin + repeat.14 push.0 end + push.{admin_role} + push.{role} + call.role_based_access_control::set_role_admin + dropw dropw dropw dropw + end + "#, + admin_role = admin_role, + role = role, + ) +} + +fn get_role_member_script(role: &RoleSymbol, index: u64) -> String { + format!( + r#" + use miden::standards::access::role_based_access_control + + begin + repeat.14 push.0 end + push.{index} + push.{role} + call.role_based_access_control::get_role_member + dropw dropw dropw dropw + end + "#, + index = index, + role = Felt::from(role), + ) +} + +fn get_active_role_script(index: u64) -> String { + format!( + r#" + use miden::standards::access::role_based_access_control + + begin + repeat.15 push.0 end + push.{index} + call.role_based_access_control::get_active_role + dropw dropw dropw dropw + end + "#, + index = index, + ) +} + +fn assert_sender_has_role_script(role: &RoleSymbol) -> String { + format!( + r#" + use miden::standards::access::role_based_access_control + + begin + repeat.15 push.0 end + push.{role} + call.role_based_access_control::assert_sender_has_role + dropw dropw dropw dropw + end + "#, + role = Felt::from(role), + ) +} + +// TESTS +// ================================================================================================ + +#[tokio::test] +async fn test_rbac_root_admin_transfer_accept_and_renounce() -> anyhow::Result<()> { + let admin = test_account_id(1); + let new_admin = test_account_id(2); + let outsider = test_account_id(3); + + let account = create_rbac_account(RoleBasedAccessControl::new(admin))?; + let mut builder = MockChain::builder(); + builder.add_account(account.clone())?; + let mock_chain = builder.build()?; + + let transfer_note = build_note(admin, transfer_root_admin_script(new_admin), 101)?; + let tx = mock_chain + .build_tx_context(account.clone(), &[], slice::from_ref(&transfer_note))? + .build()?; + let executed = tx.execute().await?; + + let mut updated = account.clone(); + updated.apply_delta(executed.account_delta())?; + + let (current_admin, nominated_admin) = get_root_admins(&updated)?; + assert_eq!(current_admin, Some(admin)); + assert_eq!(nominated_admin, Some(new_admin)); + + let wrong_accept_note = build_note(outsider, accept_root_admin_script(), 102)?; + let tx = mock_chain + .build_tx_context(updated.clone(), &[], slice::from_ref(&wrong_accept_note))? + .build()?; + let result = tx.execute().await; + assert_transaction_executor_error!(result, ERR_SENDER_NOT_NOMINATED_ROOT_ADMIN); + + let accept_note = build_note(new_admin, accept_root_admin_script(), 103)?; + let tx = mock_chain + .build_tx_context(updated.clone(), &[], slice::from_ref(&accept_note))? + .build()?; + let executed = tx.execute().await?; + + let mut accepted = updated.clone(); + accepted.apply_delta(executed.account_delta())?; + + let (current_admin, nominated_admin) = get_root_admins(&accepted)?; + assert_eq!(current_admin, Some(new_admin)); + assert_eq!(nominated_admin, None); + + let renounce_note = build_note(new_admin, renounce_root_admin_script(), 104)?; + let tx = mock_chain + .build_tx_context(accepted.clone(), &[], slice::from_ref(&renounce_note))? + .build()?; + let executed = tx.execute().await?; + + let mut renounced = accepted.clone(); + renounced.apply_delta(executed.account_delta())?; + + let (current_admin, nominated_admin) = get_root_admins(&renounced)?; + assert_eq!(current_admin, None); + assert_eq!(nominated_admin, None); + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_root_admin_role_management_and_lookup() -> anyhow::Result<()> { + let admin = test_account_id(11); + let member = test_account_id(12); + let minter = role("MINTER"); + let minter_admin = role("MINTER_ADMIN"); + + let account = create_rbac_account(RoleBasedAccessControl::new(admin))?; + let mut builder = MockChain::builder(); + builder.add_account(account.clone())?; + let mock_chain = builder.build()?; + + let set_role_admin_note = + build_note(admin, set_role_admin_script(&minter, Some(&minter_admin)), 201)?; + let tx = mock_chain + .build_tx_context(account.clone(), &[], slice::from_ref(&set_role_admin_note))? + .build()?; + let executed = tx.execute().await?; + + let mut updated = account.clone(); + updated.apply_delta(executed.account_delta())?; + + let minter_config = get_role_config(&updated, &minter)?; + let minter_admin_config = get_role_config(&updated, &minter_admin)?; + assert_eq!(minter_config[0], Felt::ZERO); + assert_eq!(minter_config[1], Felt::from(&minter_admin)); + assert_eq!(minter_config[2], Felt::ZERO); + assert_eq!(minter_config[3], Felt::ZERO); + assert_eq!(minter_admin_config[3], Felt::ZERO); + assert_eq!(get_active_role_count(&updated)?, 0); + + let grant_role_note = build_note(admin, grant_role_script(&minter, member), 202)?; + let tx = mock_chain + .build_tx_context(updated.clone(), &[], slice::from_ref(&grant_role_note))? + .build()?; + let executed = tx.execute().await?; + + let mut granted = updated.clone(); + granted.apply_delta(executed.account_delta())?; + + let minter_config = get_role_config(&granted, &minter)?; + assert_eq!(minter_config[0], Felt::new(1)); + assert_eq!(minter_config[2], Felt::ZERO); + assert_eq!(get_active_role_count(&granted)?, 1); + assert_eq!(get_active_role(&granted, 0)?, minter); + assert_eq!(get_role_member(&granted, &minter, 0)?, member); + assert_eq!(get_role_member_index(&granted, &minter, member)?, Some(0)); + + let revoke_role_note = build_note(admin, revoke_role_script(&minter, member), 203)?; + let tx = mock_chain + .build_tx_context(granted.clone(), &[], slice::from_ref(&revoke_role_note))? + .build()?; + let executed = tx.execute().await?; + + let mut revoked = granted.clone(); + revoked.apply_delta(executed.account_delta())?; + + let minter_config = get_role_config(&revoked, &minter)?; + assert_eq!(minter_config[0], Felt::ZERO); + assert_eq!(minter_config[2], Felt::ZERO); + assert_eq!(minter_config[3], Felt::ZERO); + assert_eq!(get_active_role_count(&revoked)?, 0); + assert_eq!(get_role_member_index(&revoked, &minter, member)?, None); + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_delegated_admin_and_swap_remove() -> anyhow::Result<()> { + let admin = test_account_id(21); + let delegate = test_account_id(22); + let alice = test_account_id(23); + let bob = test_account_id(24); + let burner_holder = test_account_id(25); + + let minter = role("MINTER"); + let minter_admin = role("MINTER_ADMIN"); + let burner = role("BURNER"); + + let account = create_rbac_account(RoleBasedAccessControl::new(admin))?; + let mut builder = MockChain::builder(); + builder.add_account(account.clone())?; + let mock_chain = builder.build()?; + + let set_role_admin_note = + build_note(admin, set_role_admin_script(&minter, Some(&minter_admin)), 304)?; + let tx = mock_chain + .build_tx_context(account.clone(), &[], slice::from_ref(&set_role_admin_note))? + .build()?; + let executed = tx.execute().await.context("set_role_admin for MINTER failed")?; + let mut updated = account.clone(); + updated.apply_delta(executed.account_delta())?; + + let delegate_grant_note = build_note(admin, grant_role_script(&minter_admin, delegate), 305)?; + let tx = mock_chain + .build_tx_context(updated.clone(), &[], slice::from_ref(&delegate_grant_note))? + .build()?; + let executed = tx.execute().await.context("grant MINTER_ADMIN to delegate failed")?; + updated.apply_delta(executed.account_delta())?; + + assert_eq!(get_role_member_index(&updated, &minter_admin, delegate)?, Some(0)); + + let delegated_role_check_note = + build_note(delegate, assert_sender_has_role_script(&minter_admin), 307)?; + let tx = mock_chain + .build_tx_context(updated.clone(), &[], slice::from_ref(&delegated_role_check_note))? + .build()?; + tx.execute() + .await + .context("delegate assert_sender_has_role for MINTER_ADMIN failed")?; + + assert_eq!(get_role_config(&updated, &minter)?[1], Felt::from(&minter_admin)); + + for (seed, target) in [(308, alice), (309, bob)] { + let note = build_note(delegate, grant_role_script(&minter, target), seed)?; + let tx = mock_chain + .build_tx_context(updated.clone(), &[], slice::from_ref(¬e))? + .build()?; + let executed = tx + .execute() + .await + .with_context(|| format!("delegate grant MINTER failed for target {}", target))?; + updated.apply_delta(executed.account_delta())?; + if seed == 308 { + assert_eq!(get_role_config(&updated, &minter)?[1], Felt::from(&minter_admin)); + assert_eq!(get_role_member_index(&updated, &minter_admin, delegate)?, Some(0)); + } + } + + let burner_grant_note = build_note(admin, grant_role_script(&burner, burner_holder), 310)?; + let tx = mock_chain + .build_tx_context(updated.clone(), &[], slice::from_ref(&burner_grant_note))? + .build()?; + let executed = tx.execute().await.context("grant BURNER failed")?; + updated.apply_delta(executed.account_delta())?; + + assert_eq!(get_active_role_count(&updated)?, 3); + assert_eq!(get_active_role(&updated, 0)?, minter_admin); + assert_eq!(get_active_role(&updated, 1)?, minter); + assert_eq!(get_active_role(&updated, 2)?, burner); + assert_eq!(get_role_member(&updated, &minter, 0)?, alice); + assert_eq!(get_role_member(&updated, &minter, 1)?, bob); + + let revoke_alice_note = build_note(delegate, revoke_role_script(&minter, alice), 311)?; + let tx = mock_chain + .build_tx_context(updated.clone(), &[], slice::from_ref(&revoke_alice_note))? + .build()?; + let executed = tx.execute().await.context("delegate revoke MINTER from alice failed")?; + updated.apply_delta(executed.account_delta())?; + + assert_eq!(get_role_member(&updated, &minter, 0)?, bob); + assert_eq!(get_role_member_index(&updated, &minter, alice)?, None); + assert_eq!(get_role_member_index(&updated, &minter, bob)?, Some(0)); + + let revoke_bob_note = build_note(delegate, revoke_role_script(&minter, bob), 312)?; + let tx = mock_chain + .build_tx_context(updated.clone(), &[], slice::from_ref(&revoke_bob_note))? + .build()?; + let executed = tx.execute().await.context("delegate revoke MINTER from bob failed")?; + updated.apply_delta(executed.account_delta())?; + + assert_eq!(get_active_role_count(&updated)?, 2); + assert_eq!(get_active_role(&updated, 0)?, minter_admin); + assert_eq!(get_active_role(&updated, 1)?, burner); + assert_eq!(get_role_config(&updated, &minter)?[0], Felt::ZERO); + assert_eq!(get_role_config(&updated, &burner)?[0], Felt::new(1)); + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_renounce_role_and_permission_checks() -> anyhow::Result<()> { + let admin = test_account_id(31); + let member = test_account_id(32); + let outsider = test_account_id(33); + let pauser = role("PAUSER"); + + let account = create_rbac_account(RoleBasedAccessControl::new(admin))?; + let mut builder = MockChain::builder(); + builder.add_account(account.clone())?; + let mock_chain = builder.build()?; + + let non_admin_grant_note = build_note(outsider, grant_role_script(&pauser, member), 401)?; + let tx = mock_chain + .build_tx_context(account.clone(), &[], slice::from_ref(&non_admin_grant_note))? + .build()?; + let result = tx.execute().await; + assert_transaction_executor_error!(result, ERR_SENDER_NOT_ROOT_OR_ROLE_ADMIN); + + let admin_grant_note = build_note(admin, grant_role_script(&pauser, member), 402)?; + let tx = mock_chain + .build_tx_context(account.clone(), &[], slice::from_ref(&admin_grant_note))? + .build()?; + let executed = tx.execute().await?; + + let mut updated = account.clone(); + updated.apply_delta(executed.account_delta())?; + assert_eq!(get_active_role_count(&updated)?, 1); + + let renounce_note = build_note(member, renounce_role_script(&pauser), 403)?; + let tx = mock_chain + .build_tx_context(updated.clone(), &[], slice::from_ref(&renounce_note))? + .build()?; + let executed = tx.execute().await?; + + let mut renounced = updated.clone(); + renounced.apply_delta(executed.account_delta())?; + assert_eq!(get_active_role_count(&renounced)?, 0); + assert_eq!(get_role_member_index(&renounced, &pauser, member)?, None); + + let bad_revoke_note = build_note(admin, revoke_role_script(&pauser, member), 404)?; + let tx = mock_chain + .build_tx_context(renounced.clone(), &[], slice::from_ref(&bad_revoke_note))? + .build()?; + let result = tx.execute().await; + assert_transaction_executor_error!(result, ERR_ACCOUNT_NOT_IN_ROLE); + + let bad_transfer_note = build_note(outsider, transfer_root_admin_script(member), 405)?; + let tx = mock_chain + .build_tx_context(renounced, &[], slice::from_ref(&bad_transfer_note))? + .build()?; + let result = tx.execute().await; + assert_transaction_executor_error!(result, ERR_SENDER_NOT_ROOT_ADMIN); + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_grant_role_appends_member_and_sets_reverse_index() -> anyhow::Result<()> { + let admin = test_account_id(41); + let member = test_account_id(42); + let minter = role("MINTER"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let grant_note = build_note(admin, grant_role_script(&minter, member), 601)?; + let granted = execute_note_and_apply(&mock_chain, &account, &grant_note).await?; + + assert_eq!(get_role_member(&granted, &minter, 0)?, member); + assert_eq!(get_role_member_index(&granted, &minter, member)?, Some(0)); + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_first_member_activates_role() -> anyhow::Result<()> { + let admin = test_account_id(43); + let member = test_account_id(44); + let burner = role("BURNER"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let grant_note = build_note(admin, grant_role_script(&burner, member), 602)?; + let granted = execute_note_and_apply(&mock_chain, &account, &grant_note).await?; + + let burner_config = get_role_config(&granted, &burner)?; + assert_eq!(burner_config[0], Felt::new(1)); + assert_eq!(burner_config[2], Felt::ZERO); + assert_eq!(get_active_role_count(&granted)?, 1); + assert_eq!(get_active_role(&granted, 0)?, burner); + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_additional_members_do_not_duplicate_active_role() -> anyhow::Result<()> { + let admin = test_account_id(45); + let alice = test_account_id(46); + let bob = test_account_id(47); + let pauser = role("PAUSER"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let first_grant = build_note(admin, grant_role_script(&pauser, alice), 603)?; + let updated = execute_note_and_apply(&mock_chain, &account, &first_grant).await?; + + let second_grant = build_note(admin, grant_role_script(&pauser, bob), 604)?; + let updated = execute_note_and_apply(&mock_chain, &updated, &second_grant).await?; + + let pauser_config = get_role_config(&updated, &pauser)?; + assert_eq!(pauser_config[0], Felt::new(2)); + assert_eq!(get_active_role_count(&updated)?, 1); + assert_eq!(get_active_role(&updated, 0)?, pauser); + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_get_role_member_count_returns_zero_for_missing_role() -> anyhow::Result<()> { + let admin = test_account_id(48); + let missing_role = role("MISSING"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let query_note = build_note(admin, assert_role_member_count_script(&missing_role, 0), 605)?; + let _ = execute_note_and_apply(&mock_chain, &account, &query_note).await?; + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_get_role_admin_returns_zero_when_unset() -> anyhow::Result<()> { + let admin = test_account_id(49); + let root_managed_role = role("ROOT_MANAGED"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let query_note = build_note(admin, assert_role_admin_script(&root_managed_role, None), 606)?; + let _ = execute_note_and_apply(&mock_chain, &account, &query_note).await?; + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_get_role_member_out_of_bounds_fails() -> anyhow::Result<()> { + let admin = test_account_id(50); + let member = test_account_id(51); + let user = role("USER"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let grant_note = build_note(admin, grant_role_script(&user, member), 607)?; + let granted = execute_note_and_apply(&mock_chain, &account, &grant_note).await?; + + let query_note = build_note(admin, get_role_member_script(&user, 1), 608)?; + let tx = mock_chain + .build_tx_context(granted, &[], slice::from_ref(&query_note))? + .build()?; + let result = tx.execute().await; + assert_transaction_executor_error!(result, ERR_ROLE_MEMBER_OUT_OF_BOUNDS); + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_get_active_role_out_of_bounds_fails() -> anyhow::Result<()> { + let admin = test_account_id(52); + let member = test_account_id(53); + let manager = role("MANAGER"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let grant_note = build_note(admin, grant_role_script(&manager, member), 609)?; + let granted = execute_note_and_apply(&mock_chain, &account, &grant_note).await?; + + let query_note = build_note(admin, get_active_role_script(1), 610)?; + let tx = mock_chain + .build_tx_context(granted, &[], slice::from_ref(&query_note))? + .build()?; + let result = tx.execute().await; + assert_transaction_executor_error!(result, ERR_ACTIVE_ROLE_OUT_OF_BOUNDS); + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_non_admin_cannot_revoke_role() -> anyhow::Result<()> { + let admin = test_account_id(54); + let outsider = test_account_id(55); + let member = test_account_id(56); + let minter = role("MINTER"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let grant_note = build_note(admin, grant_role_script(&minter, member), 611)?; + let granted = execute_note_and_apply(&mock_chain, &account, &grant_note).await?; + + let revoke_note = build_note(outsider, revoke_role_script(&minter, member), 612)?; + let tx = mock_chain + .build_tx_context(granted, &[], slice::from_ref(&revoke_note))? + .build()?; + let result = tx.execute().await; + assert_transaction_executor_error!(result, ERR_SENDER_NOT_ROOT_OR_ROLE_ADMIN); + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_non_member_cannot_renounce_role() -> anyhow::Result<()> { + let admin = test_account_id(57); + let outsider = test_account_id(58); + let pauser = role("PAUSER"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let renounce_note = build_note(outsider, renounce_role_script(&pauser), 613)?; + let tx = mock_chain + .build_tx_context(account, &[], slice::from_ref(&renounce_note))? + .build()?; + let result = tx.execute().await; + assert_transaction_executor_error!(result, ERR_ACCOUNT_NOT_IN_ROLE); + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_revoke_role_clears_removed_account_reverse_index() -> anyhow::Result<()> { + let admin = test_account_id(59); + let member = test_account_id(60); + let burner = role("BURNER"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let grant_note = build_note(admin, grant_role_script(&burner, member), 614)?; + let granted = execute_note_and_apply(&mock_chain, &account, &grant_note).await?; + + let revoke_note = build_note(admin, revoke_role_script(&burner, member), 615)?; + let revoked = execute_note_and_apply(&mock_chain, &granted, &revoke_note).await?; + + assert_eq!(get_role_member_index(&revoked, &burner, member)?, None); + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_revoke_non_last_member_moves_last_member_into_removed_index() +-> anyhow::Result<()> { + let admin = test_account_id(61); + let alice = test_account_id(62); + let bob = test_account_id(63); + let minter = role("MINTER"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let grant_alice = build_note(admin, grant_role_script(&minter, alice), 616)?; + let updated = execute_note_and_apply(&mock_chain, &account, &grant_alice).await?; + + let grant_bob = build_note(admin, grant_role_script(&minter, bob), 617)?; + let updated = execute_note_and_apply(&mock_chain, &updated, &grant_bob).await?; + + let revoke_alice = build_note(admin, revoke_role_script(&minter, alice), 618)?; + let updated = execute_note_and_apply(&mock_chain, &updated, &revoke_alice).await?; + + assert_eq!(get_role_member(&updated, &minter, 0)?, bob); + assert_eq!(get_role_member_index(&updated, &minter, alice)?, None); + assert_eq!(get_role_member_index(&updated, &minter, bob)?, Some(0)); + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_revoke_last_member_keeps_remaining_enumeration_consistent() -> anyhow::Result<()> +{ + let admin = test_account_id(64); + let alice = test_account_id(65); + let bob = test_account_id(66); + let burner = role("BURNER"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let grant_alice = build_note(admin, grant_role_script(&burner, alice), 619)?; + let updated = execute_note_and_apply(&mock_chain, &account, &grant_alice).await?; + + let grant_bob = build_note(admin, grant_role_script(&burner, bob), 620)?; + let updated = execute_note_and_apply(&mock_chain, &updated, &grant_bob).await?; + + let revoke_bob = build_note(admin, revoke_role_script(&burner, bob), 621)?; + let updated = execute_note_and_apply(&mock_chain, &updated, &revoke_bob).await?; + + let burner_config = get_role_config(&updated, &burner)?; + assert_eq!(burner_config[0], Felt::new(1)); + assert_eq!(get_role_member(&updated, &burner, 0)?, alice); + assert_eq!(get_role_member_index(&updated, &burner, alice)?, Some(0)); + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_revoke_last_role_member_deactivates_role() -> anyhow::Result<()> { + let admin = test_account_id(67); + let member = test_account_id(68); + let pauser = role("PAUSER"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let grant_note = build_note(admin, grant_role_script(&pauser, member), 622)?; + let granted = execute_note_and_apply(&mock_chain, &account, &grant_note).await?; + + let revoke_note = build_note(admin, revoke_role_script(&pauser, member), 623)?; + let revoked = execute_note_and_apply(&mock_chain, &granted, &revoke_note).await?; + + let pauser_config = get_role_config(&revoked, &pauser)?; + assert_eq!(pauser_config[0], Felt::ZERO); + assert_eq!(pauser_config[2], Felt::ZERO); + assert_eq!(get_active_role_count(&revoked)?, 0); + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_regrant_role_reactivates_role_after_becoming_empty() -> anyhow::Result<()> { + let admin = test_account_id(69); + let member = test_account_id(70); + let user = role("USER"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let grant_note = build_note(admin, grant_role_script(&user, member), 624)?; + let updated = execute_note_and_apply(&mock_chain, &account, &grant_note).await?; + + let revoke_note = build_note(admin, revoke_role_script(&user, member), 625)?; + let updated = execute_note_and_apply(&mock_chain, &updated, &revoke_note).await?; + + let regrant_note = build_note(admin, grant_role_script(&user, member), 626)?; + let updated = execute_note_and_apply(&mock_chain, &updated, ®rant_note).await?; + + let user_config = get_role_config(&updated, &user)?; + assert_eq!(user_config[0], Felt::new(1)); + assert_eq!(user_config[2], Felt::ZERO); + assert_eq!(get_active_role_count(&updated)?, 1); + assert_eq!(get_active_role(&updated, 0)?, user); + assert_eq!(get_role_member_index(&updated, &user, member)?, Some(0)); + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_active_role_slot_is_reused_after_role_deactivation() -> anyhow::Result<()> { + let admin = test_account_id(71); + let alice = test_account_id(72); + let bob = test_account_id(73); + let carol = test_account_id(74); + let minter = role("MINTER"); + let burner = role("BURNER"); + let pauser = role("PAUSER"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let grant_minter = build_note(admin, grant_role_script(&minter, alice), 627)?; + let updated = execute_note_and_apply(&mock_chain, &account, &grant_minter).await?; + + let grant_burner = build_note(admin, grant_role_script(&burner, bob), 628)?; + let updated = execute_note_and_apply(&mock_chain, &updated, &grant_burner).await?; + + let revoke_minter = build_note(admin, revoke_role_script(&minter, alice), 629)?; + let updated = execute_note_and_apply(&mock_chain, &updated, &revoke_minter).await?; + + assert_eq!(get_active_role_count(&updated)?, 1); + assert_eq!(get_active_role(&updated, 0)?, burner); + assert_eq!(get_role_config(&updated, &burner)?[2], Felt::ZERO); + + let grant_pauser = build_note(admin, grant_role_script(&pauser, carol), 630)?; + let updated = execute_note_and_apply(&mock_chain, &updated, &grant_pauser).await?; + + assert_eq!(get_active_role_count(&updated)?, 2); + assert_eq!(get_active_role(&updated, 0)?, burner); + assert_eq!(get_active_role(&updated, 1)?, pauser); + assert_eq!(get_role_config(&updated, &pauser)?[2], Felt::new(1)); + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_get_role_admin_returns_set_role() -> anyhow::Result<()> { + let admin = test_account_id(75); + let minter = role("MINTER"); + let minter_admin = role("MINTER_ADMIN"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let set_role_admin_note = + build_note(admin, set_role_admin_script(&minter, Some(&minter_admin)), 631)?; + let updated = execute_note_and_apply(&mock_chain, &account, &set_role_admin_note).await?; + + let query_note = + build_note(admin, assert_role_admin_script(&minter, Some(&minter_admin)), 632)?; + let _ = execute_note_and_apply(&mock_chain, &updated, &query_note).await?; + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_transfer_admin_to_self_cancels_pending_transfer() -> anyhow::Result<()> { + let admin = test_account_id(76); + let new_admin = test_account_id(77); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let transfer_note = build_note(admin, transfer_root_admin_script(new_admin), 633)?; + let updated = execute_note_and_apply(&mock_chain, &account, &transfer_note).await?; + + let cancel_note = build_note(admin, transfer_root_admin_script(admin), 634)?; + let cancelled = execute_note_and_apply(&mock_chain, &updated, &cancel_note).await?; + + let query_note = build_note(admin, assert_nominated_root_admin_script(None), 635)?; + let _ = execute_note_and_apply(&mock_chain, &cancelled, &query_note).await?; + + let (current_admin, nominated_admin) = get_root_admins(&cancelled)?; + assert_eq!(current_admin, Some(admin)); + assert_eq!(nominated_admin, None); + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_get_admin_returns_zero_when_admin_is_unset() -> anyhow::Result<()> { + let admin = test_account_id(78); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let renounce_note = build_note(admin, renounce_root_admin_script(), 636)?; + let renounced = execute_note_and_apply(&mock_chain, &account, &renounce_note).await?; + + let query_note = build_note(admin, assert_root_admin_script(None), 637)?; + let _ = execute_note_and_apply(&mock_chain, &renounced, &query_note).await?; + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_transfer_admin_fails_when_admin_is_unset() -> anyhow::Result<()> { + let admin = test_account_id(79); + let new_admin = test_account_id(80); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let renounce_note = build_note(admin, renounce_root_admin_script(), 638)?; + let renounced = execute_note_and_apply(&mock_chain, &account, &renounce_note).await?; + + let transfer_note = build_note(admin, transfer_root_admin_script(new_admin), 639)?; + let tx = mock_chain + .build_tx_context(renounced, &[], slice::from_ref(&transfer_note))? + .build()?; + let result = tx.execute().await; + assert_transaction_executor_error!(result, ERR_SENDER_NOT_ROOT_ADMIN); + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_renounce_admin_fails_while_transfer_is_pending() -> anyhow::Result<()> { + let admin = test_account_id(81); + let new_admin = test_account_id(82); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let transfer_note = build_note(admin, transfer_root_admin_script(new_admin), 640)?; + let updated = execute_note_and_apply(&mock_chain, &account, &transfer_note).await?; + + let renounce_note = build_note(admin, renounce_root_admin_script(), 641)?; + let tx = mock_chain + .build_tx_context(updated, &[], slice::from_ref(&renounce_note))? + .build()?; + let result = tx.execute().await; + assert_transaction_executor_error!(result, ERR_ROOT_ADMIN_TRANSFER_IN_PROGRESS); + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_role_admin_can_manage_role_without_root_admin() -> anyhow::Result<()> { + let admin = test_account_id(83); + let manager = test_account_id(84); + let user = test_account_id(85); + let user_role = role("USER"); + let manager_role = role("MANAGER"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let set_role_admin_note = + build_note(admin, set_role_admin_script(&user_role, Some(&manager_role)), 642)?; + let updated = execute_note_and_apply(&mock_chain, &account, &set_role_admin_note).await?; + + let grant_manager_note = build_note(admin, grant_role_script(&manager_role, manager), 643)?; + let updated = execute_note_and_apply(&mock_chain, &updated, &grant_manager_note).await?; + + let renounce_admin_note = build_note(admin, renounce_root_admin_script(), 644)?; + let updated = execute_note_and_apply(&mock_chain, &updated, &renounce_admin_note).await?; + + let grant_user_note = build_note(manager, grant_role_script(&user_role, user), 645)?; + let updated = execute_note_and_apply(&mock_chain, &updated, &grant_user_note).await?; + assert_eq!(get_role_member_index(&updated, &user_role, user)?, Some(0)); + + let revoke_user_note = build_note(manager, revoke_role_script(&user_role, user), 646)?; + let updated = execute_note_and_apply(&mock_chain, &updated, &revoke_user_note).await?; + assert_eq!(get_role_member_index(&updated, &user_role, user)?, None); + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_role_exists_and_has_role_queries() -> anyhow::Result<()> { + let admin = test_account_id(86); + let member = test_account_id(87); + let outsider = test_account_id(88); + let user_role = role("USER"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let role_missing_note = build_note(admin, assert_role_exists_script(&user_role, false), 647)?; + let _ = execute_note_and_apply(&mock_chain, &account, &role_missing_note).await?; + + let non_member_note = + build_note(admin, assert_has_role_script(&user_role, member, false), 648)?; + let _ = execute_note_and_apply(&mock_chain, &account, &non_member_note).await?; + + let grant_note = build_note(admin, grant_role_script(&user_role, member), 649)?; + let updated = execute_note_and_apply(&mock_chain, &account, &grant_note).await?; + + let role_exists_note = build_note(admin, assert_role_exists_script(&user_role, true), 650)?; + let _ = execute_note_and_apply(&mock_chain, &updated, &role_exists_note).await?; + + let member_note = build_note(admin, assert_has_role_script(&user_role, member, true), 651)?; + let _ = execute_note_and_apply(&mock_chain, &updated, &member_note).await?; + + let outsider_note = + build_note(admin, assert_has_role_script(&user_role, outsider, false), 652)?; + let _ = execute_note_and_apply(&mock_chain, &updated, &outsider_note).await?; + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_non_admin_cannot_set_role_admin() -> anyhow::Result<()> { + let admin = test_account_id(89); + let outsider = test_account_id(90); + let user_role = role("USER"); + let manager_role = role("MANAGER"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let note = build_note(outsider, set_role_admin_script(&user_role, Some(&manager_role)), 653)?; + let tx = mock_chain.build_tx_context(account, &[], slice::from_ref(¬e))?.build()?; + let result = tx.execute().await; + assert_transaction_executor_error!(result, ERR_SENDER_NOT_ROOT_ADMIN); + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_set_role_admin_can_clear_delegated_admin_to_root_admin() -> anyhow::Result<()> { + let admin = test_account_id(91); + let user_role = role("USER"); + let manager_role = role("MANAGER"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let set_admin_note = + build_note(admin, set_role_admin_script(&user_role, Some(&manager_role)), 654)?; + let updated = execute_note_and_apply(&mock_chain, &account, &set_admin_note).await?; + + let clear_admin_note = build_note(admin, set_role_admin_script(&user_role, None), 655)?; + let updated = execute_note_and_apply(&mock_chain, &updated, &clear_admin_note).await?; + + let query_note = build_note(admin, assert_role_admin_script(&user_role, None), 656)?; + let _ = execute_note_and_apply(&mock_chain, &updated, &query_note).await?; + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_set_role_admin_rejects_zero_role_symbol() -> anyhow::Result<()> { + let admin = test_account_id(92); + let manager_role = role("MANAGER"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let note = + build_note(admin, set_role_admin_raw_script(Felt::ZERO, Felt::from(&manager_role)), 657)?; + let tx = mock_chain.build_tx_context(account, &[], slice::from_ref(¬e))?.build()?; + let result = tx.execute().await; + assert_transaction_executor_error!(result, ERR_ROLE_SYMBOL_ZERO); + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_set_role_admin_stores_admin_without_activating_roles() -> anyhow::Result<()> { + let admin = test_account_id(93); + let user_role = role("USER"); + let manager_role = role("MANAGER"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let note = build_note(admin, set_role_admin_script(&user_role, Some(&manager_role)), 658)?; + let updated = execute_note_and_apply(&mock_chain, &account, ¬e).await?; + + // set_role_admin stores the admin relationship but does not activate any role. + assert_eq!(get_active_role_count(&updated)?, 0); + assert_eq!(get_role_config(&updated, &user_role)?[0], Felt::ZERO); + assert_eq!(get_role_config(&updated, &user_role)?[1], Felt::from(&manager_role)); + assert_eq!(get_role_config(&updated, &manager_role)?[0], Felt::ZERO); + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_accept_admin_clears_nominated_admin() -> anyhow::Result<()> { + let admin = test_account_id(94); + let new_admin = test_account_id(95); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let transfer_note = build_note(admin, transfer_root_admin_script(new_admin), 659)?; + let updated = execute_note_and_apply(&mock_chain, &account, &transfer_note).await?; + + let accept_note = build_note(new_admin, accept_root_admin_script(), 660)?; + let accepted = execute_note_and_apply(&mock_chain, &updated, &accept_note).await?; + + let query_note = build_note(new_admin, assert_nominated_root_admin_script(None), 661)?; + let _ = execute_note_and_apply(&mock_chain, &accepted, &query_note).await?; + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_granting_admin_role_does_not_change_target_role_admin_config() +-> anyhow::Result<()> { + let admin = test_account_id(96); + let delegate = test_account_id(97); + let user_role = role("USER"); + let manager_role = role("MANAGER"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let set_admin_note = + build_note(admin, set_role_admin_script(&user_role, Some(&manager_role)), 662)?; + let updated = execute_note_and_apply(&mock_chain, &account, &set_admin_note).await?; + assert_eq!(get_role_config(&updated, &user_role)?[1], Felt::from(&manager_role)); + + let grant_manager_note = build_note(admin, grant_role_script(&manager_role, delegate), 663)?; + let updated = execute_note_and_apply(&mock_chain, &updated, &grant_manager_note).await?; + + let user_role_config = get_role_config(&updated, &user_role)?; + assert_eq!(user_role_config[1], Felt::from(&manager_role)); + assert_eq!(user_role_config[0], Felt::ZERO); + + Ok(()) +} + +#[tokio::test] +async fn test_rbac_revoke_non_last_active_role_moves_last_active_role_into_freed_slot() +-> anyhow::Result<()> { + let admin = test_account_id(98); + let alice = test_account_id(99); + let bob = test_account_id(100); + let carol = test_account_id(101); + let minter = role("MINTER"); + let burner = role("BURNER"); + let pauser = role("PAUSER"); + + let (account, mock_chain) = create_rbac_chain(admin)?; + + let grant_minter = build_note(admin, grant_role_script(&minter, alice), 664)?; + let updated = execute_note_and_apply(&mock_chain, &account, &grant_minter).await?; + + let grant_burner = build_note(admin, grant_role_script(&burner, bob), 665)?; + let updated = execute_note_and_apply(&mock_chain, &updated, &grant_burner).await?; + + let grant_pauser = build_note(admin, grant_role_script(&pauser, carol), 666)?; + let updated = execute_note_and_apply(&mock_chain, &updated, &grant_pauser).await?; + + assert_eq!(get_active_role(&updated, 0)?, minter); + assert_eq!(get_active_role(&updated, 1)?, burner); + assert_eq!(get_active_role(&updated, 2)?, pauser); + + let revoke_burner = build_note(admin, revoke_role_script(&burner, bob), 667)?; + let updated = execute_note_and_apply(&mock_chain, &updated, &revoke_burner).await?; + + assert_eq!(get_active_role_count(&updated)?, 2); + assert_eq!(get_active_role(&updated, 0)?, minter); + assert_eq!(get_active_role(&updated, 1)?, pauser); + assert_eq!(get_role_config(&updated, &pauser)?[2], Felt::new(1)); + assert_eq!(get_role_config(&updated, &burner)?[2], Felt::ZERO); + + Ok(()) +}