From 2a93cf1e8dc538b0bf27684325376d0291e9eda7 Mon Sep 17 00:00:00 2001 From: onurinanc Date: Mon, 18 May 2026 16:43:18 +0200 Subject: [PATCH 1/3] merge AuthMethod into AccessControl --- .../src/account/access/authority.rs | 17 +- .../miden-standards/src/account/access/mod.rs | 111 ++++++--- .../src/account/faucets/fungible/mod.rs | 231 ++++++++++++++---- .../src/account/faucets/fungible/tests.rs | 189 ++++++++++---- .../src/account/faucets/mod.rs | 6 + .../src/account/policies/manager.rs | 67 +++++ .../src/mock_chain/chain_builder.rs | 48 ++-- crates/miden-testing/tests/scripts/rbac.rs | 7 +- 8 files changed, 543 insertions(+), 133 deletions(-) diff --git a/crates/miden-standards/src/account/access/authority.rs b/crates/miden-standards/src/account/access/authority.rs index 3bca21f2c6..7f11242672 100644 --- a/crates/miden-standards/src/account/access/authority.rs +++ b/crates/miden-standards/src/account/access/authority.rs @@ -48,13 +48,28 @@ const RBAC_CONTROLLED: u8 = 2; /// the MASM helper `authority::assert_authorized`. Installing the [`Authority`] component on an /// account thus selects the gating mode for *all* such procedures in one place. /// +/// # Safety invariant for [`Authority::AuthControlled`] +/// +/// Because `assert_authorized` is a no-op under `AuthControlled`, the account's auth component +/// is the **sole** gate for every authority-gated setter. The auth component MUST therefore +/// authenticate every such setter root, otherwise the setters become permissionless. Factories +/// that compose accounts under this variant enforce this invariant (see +/// [`create_fungible_faucet`][crate::account::faucets::create_fungible_faucet], which rejects +/// [`AuthMethod::NoAuth`][crate::AuthMethod::NoAuth] under +/// [`AccessControl::AuthControlled`][crate::account::access::AccessControl::AuthControlled] and +/// installs [`AuthSingleSigAcl`][crate::account::auth::AuthSingleSigAcl] with the complete +/// authority-gated trigger list for [`AuthMethod::SingleSig`][crate::AuthMethod::SingleSig]). +/// Custom account assemblies that install `Authority::AuthControlled` directly bear the same +/// responsibility. +/// /// Storage layout: `[authority, role_symbol_or_zero, 0, 0]` — single Word. #[repr(u8)] #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] pub enum Authority { /// Authority is the account's auth component; no extra check is performed by - /// `authority::assert_authorized`. + /// `authority::assert_authorized`. See the type-level docs for the safety invariant the + /// auth component must uphold. AuthControlled = AUTH_CONTROLLED, /// Authority is the [`Ownable2Step`][crate::account::access::Ownable2Step] owner; the call /// must be sent by the registered owner. diff --git a/crates/miden-standards/src/account/access/mod.rs b/crates/miden-standards/src/account/access/mod.rs index 085f18d847..37a76a17f2 100644 --- a/crates/miden-standards/src/account/access/mod.rs +++ b/crates/miden-standards/src/account/access/mod.rs @@ -2,51 +2,78 @@ use alloc::vec; use miden_protocol::account::{AccountComponent, AccountId, RoleSymbol}; +use crate::auth_method::AuthMethod; + pub mod authority; pub mod ownable2step; pub mod rbac; /// Access control configuration for account components. /// -/// Each variant expands into the set of [`AccountComponent`]s that implement that access -/// control choice **plus** the matching [`Authority`] component. The [`Authority`] is -/// auto-yielded so callers don't need to remember to install it separately and so that the -/// authority discriminator stays in sync with the chosen access mode. +/// Bundles two related concerns into a single declarative value: +/// +/// 1. **Account-level authentication** ([`AuthMethod`]) — selects the auth component that gates +/// every transaction executed against the account (signature, network-account allowlist, +/// nonce-only, ...). +/// 2. **Setter access gate** — selects the [`Authority`] policy consulted by `set_*` procedures +/// such as `set_max_supply`, `set_mint_policy`, and the token metadata setters. Each variant of +/// this enum picks a different gate; see the variants below. +/// +/// Each variant carries an `auth: AuthMethod` field for concern (1). The variant itself +/// (`AuthControlled` / `Ownable2Step` / `Rbac`) drives concern (2). In `AuthControlled` the two +/// concerns coincide: the auth component **is** the setter gate, so the auth component must +/// authenticate every authority-gated setter (factories enforce this; see +/// [`create_fungible_faucet`][crate::account::faucets::create_fungible_faucet]). /// -/// - [`AccessControl::AuthControlled`] yields just [`Authority::AuthControlled`]. -/// - [`AccessControl::Ownable2Step`] yields [`Ownable2Step`] + [`Authority::OwnerControlled`]. -/// - [`AccessControl::Rbac`] yields [`Ownable2Step`] + [`RoleBasedAccessControl`] + an -/// [`Authority`]. The `authority_role` field selects which authority kind is installed: +/// The variants expand into: +/// - [`AccessControl::AuthControlled`] → [`Authority::AuthControlled`] (only). The setter gate +/// delegates to the auth component. +/// - [`AccessControl::Ownable2Step`] → [`Ownable2Step`] + [`Authority::OwnerControlled`]. The +/// setter gate enforces `sender == owner`. +/// - [`AccessControl::Rbac`] → [`Ownable2Step`] + [`RoleBasedAccessControl`] + an [`Authority`]. +/// The `authority_role` field selects which authority kind is installed: /// - `None` → [`Authority::OwnerControlled`] (the top-level owner gates `set_*` operations). /// - `Some(role)` → [`Authority::RbacControlled { role }`] (any holder of `role` gates `set_*` /// operations). /// -/// Pass to -/// [`AccountBuilder::with_components`][miden_protocol::account::AccountBuilder::with_components] -/// to install the access control components on the account: +/// Note that the auth component is **not** yielded by [`IntoIterator`]; it is constructed +/// separately by factory functions +/// ([`create_fungible_faucet`][crate::account::faucets::create_fungible_faucet]) +/// from [`Self::auth_method`] and installed via +/// [`AccountBuilder::with_auth_component`][miden_protocol::account::AccountBuilder::with_auth_component]. +/// The iterator only yields the setter-gate components. /// /// ```no_run /// use miden_protocol::account::AccountBuilder; +/// use miden_standards::AuthMethod; /// use miden_standards::account::access::AccessControl; /// # let owner: miden_protocol::account::AccountId = unimplemented!(); /// # let init_seed = [0u8; 32]; -/// AccountBuilder::new(init_seed) -/// .with_components(AccessControl::Rbac { owner, authority_role: None }); +/// AccountBuilder::new(init_seed).with_components(AccessControl::Rbac { +/// owner, +/// authority_role: None, +/// auth: AuthMethod::NoAuth, +/// }); /// ``` -/// -/// For accounts that don't use the [`AccessControl`] convenience but want to install the -/// [`Authority`] component directly, the [`Authority`] enum can be passed via -/// [`AccountBuilder::with_component`][miden_protocol::account::AccountBuilder::with_component]. #[derive(Debug, Clone, PartialEq, Eq)] pub enum AccessControl { - /// No external access control component is installed; access decisions are gated solely - /// by the account's auth component. - AuthControlled, - /// Two-step ownership transfer with the provided initial owner. Authority for `set_*` - /// operations is fixed to the registered owner. - Ownable2Step { owner: AccountId }, + /// No external setter-gate component is installed; the account's auth component is the + /// sole gate for both transaction-level authentication and authority-gated setters. + /// + /// Because `Authority::AuthControlled` makes `assert_authorized` a no-op, the auth + /// component **must** authenticate every authority-gated setter root for setters to be + /// safe. Factories that build accounts under this variant reject auth methods that + /// cannot meet that invariant (for example, + /// [`create_fungible_faucet`][crate::account::faucets::create_fungible_faucet] rejects + /// [`AuthMethod::NoAuth`]). + AuthControlled { auth: AuthMethod }, + /// Two-step ownership transfer with the provided initial owner. The setter gate enforces + /// `sender == owner`; the auth component on `auth` only governs the faucet's own + /// transaction authentication. + Ownable2Step { owner: AccountId, auth: AuthMethod }, /// Role-based access control. Includes [`Ownable2Step`] internally; the provided `owner` - /// becomes the top-level RBAC authority (the account's owner). + /// becomes the top-level RBAC authority (the account's owner). `auth` governs the + /// account's own transaction authentication only. /// /// `authority_role` controls which authority is installed alongside RBAC: /// - `None` (default) → [`Authority::OwnerControlled`]: the top-level `owner` is the sole @@ -59,29 +86,51 @@ pub enum AccessControl { Rbac { owner: AccountId, authority_role: Option, + auth: AuthMethod, }, } +impl AccessControl { + /// Returns the [`AuthMethod`] selected for the account's auth component. Factories use + /// this to construct the auth component before installing the setter-gate components + /// yielded by [`IntoIterator`]. + pub fn auth_method(&self) -> &AuthMethod { + match self { + AccessControl::AuthControlled { auth } + | AccessControl::Ownable2Step { auth, .. } + | AccessControl::Rbac { auth, .. } => auth, + } + } +} + impl IntoIterator for AccessControl { type Item = AccountComponent; type IntoIter = alloc::vec::IntoIter; - /// Yields the [`AccountComponent`]s implementing this access control configuration, in the - /// order they must be installed on the account. The matching [`Authority`] component is - /// always included. + /// Yields the [`AccountComponent`]s implementing the **setter-gate** half of this access + /// control configuration. The auth component (concern (1) in the type-level docs) is + /// **not** yielded; callers obtain it from [`Self::auth_method`] and install it + /// separately via + /// [`AccountBuilder::with_auth_component`][miden_protocol::account::AccountBuilder::with_auth_component]. fn into_iter(self) -> Self::IntoIter { match self { - AccessControl::AuthControlled => vec![Authority::AuthControlled.into()].into_iter(), - AccessControl::Ownable2Step { owner } => { + AccessControl::AuthControlled { auth: _ } => { + vec![Authority::AuthControlled.into()].into_iter() + }, + AccessControl::Ownable2Step { owner, auth: _ } => { vec![Ownable2Step::new(owner).into(), Authority::OwnerControlled.into()].into_iter() }, - AccessControl::Rbac { owner, authority_role: None } => vec![ + AccessControl::Rbac { owner, authority_role: None, auth: _ } => vec![ Ownable2Step::new(owner).into(), RoleBasedAccessControl::empty().into(), Authority::OwnerControlled.into(), ] .into_iter(), - AccessControl::Rbac { owner, authority_role: Some(role) } => vec![ + AccessControl::Rbac { + owner, + authority_role: Some(role), + auth: _, + } => vec![ Ownable2Step::new(owner).into(), RoleBasedAccessControl::empty().into(), Authority::RbacControlled { role }.into(), diff --git a/crates/miden-standards/src/account/faucets/fungible/mod.rs b/crates/miden-standards/src/account/faucets/fungible/mod.rs index 65508d0bff..7eeadf0a0e 100644 --- a/crates/miden-standards/src/account/faucets/fungible/mod.rs +++ b/crates/miden-standards/src/account/faucets/fungible/mod.rs @@ -34,7 +34,13 @@ use super::{ }; use crate::account::access::AccessControl; use crate::account::account_component_code; -use crate::account::auth::{AuthNetworkAccount, AuthSingleSigAcl, AuthSingleSigAclConfig, NoAuth}; +use crate::account::auth::{ + AuthNetworkAccount, + AuthSingleSig, + AuthSingleSigAcl, + AuthSingleSigAclConfig, + NoAuth, +}; use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; use crate::account::policies::TokenPolicyManager; use crate::{AuthMethod, procedure_root}; @@ -77,6 +83,34 @@ procedure_root!( FungibleFaucet::code() ); +procedure_root!( + FUNGIBLE_FAUCET_SET_MAX_SUPPLY, + FungibleFaucet::NAME, + FungibleFaucet::SET_MAX_SUPPLY_PROC_NAME, + FungibleFaucet::code() +); + +procedure_root!( + FUNGIBLE_FAUCET_SET_DESCRIPTION, + FungibleFaucet::NAME, + FungibleFaucet::SET_DESCRIPTION_PROC_NAME, + FungibleFaucet::code() +); + +procedure_root!( + FUNGIBLE_FAUCET_SET_LOGO_URI, + FungibleFaucet::NAME, + FungibleFaucet::SET_LOGO_URI_PROC_NAME, + FungibleFaucet::code() +); + +procedure_root!( + FUNGIBLE_FAUCET_SET_EXTERNAL_LINK, + FungibleFaucet::NAME, + FungibleFaucet::SET_EXTERNAL_LINK_PROC_NAME, + FungibleFaucet::code() +); + /// An [`AccountComponent`] implementing a fungible faucet. /// /// This component bundles the asset minting/burning procedures and the token metadata @@ -192,6 +226,10 @@ impl FungibleFaucet { const MINT_PROC_NAME: &'static str = "mint_and_send"; const RECEIVE_AND_BURN_PROC_NAME: &'static str = "receive_and_burn"; + const SET_MAX_SUPPLY_PROC_NAME: &'static str = "set_max_supply"; + const SET_DESCRIPTION_PROC_NAME: &'static str = "set_description"; + const SET_LOGO_URI_PROC_NAME: &'static str = "set_logo_uri"; + const SET_EXTERNAL_LINK_PROC_NAME: &'static str = "set_external_link"; // CONSTRUCTORS // -------------------------------------------------------------------------------------------- @@ -248,6 +286,29 @@ impl FungibleFaucet { *FUNGIBLE_FAUCET_RECEIVE_AND_BURN } + /// Returns the procedure root of the `set_max_supply` account procedure. This is an + /// authority-gated setter; under + /// [`AccessControl::AuthControlled`][crate::account::access::AccessControl::AuthControlled] + /// it must appear in the auth component's trigger procedure list. + pub fn set_max_supply_root() -> AccountProcedureRoot { + *FUNGIBLE_FAUCET_SET_MAX_SUPPLY + } + + /// Returns the procedure root of the `set_description` account procedure. Authority-gated. + pub fn set_description_root() -> AccountProcedureRoot { + *FUNGIBLE_FAUCET_SET_DESCRIPTION + } + + /// Returns the procedure root of the `set_logo_uri` account procedure. Authority-gated. + pub fn set_logo_uri_root() -> AccountProcedureRoot { + *FUNGIBLE_FAUCET_SET_LOGO_URI + } + + /// Returns the procedure root of the `set_external_link` account procedure. Authority-gated. + pub fn set_external_link_root() -> AccountProcedureRoot { + *FUNGIBLE_FAUCET_SET_EXTERNAL_LINK + } + /// Returns the [`StorageSlotName`] holding the token config word /// `[token_supply, max_supply, decimals, token_symbol]`. pub fn token_config_slot() -> &'static StorageSlotName { @@ -497,61 +558,57 @@ impl TryFrom<&Account> for FungibleFaucet { // FACTORY // ================================================================================================ +/// Returns every authority-gated setter procedure root exported by a fungible faucet account. +/// +/// Under [`AccessControl::AuthControlled`] the auth component must authenticate calls to all +/// of these procedures, otherwise the setters become permissionless. This list is the single +/// source of truth used by [`create_fungible_faucet`] when configuring +/// [`AuthSingleSigAcl`]'s trigger procedure list. +/// +/// Includes `mint_and_send` so that minting always requires a signature regardless of access +/// control configuration. `receive_and_burn` is intentionally **excluded**: it is only +/// invoked from a note context (i.e., by an incoming burn note), and faucets accept those +/// without a signature. +fn all_authority_gated_setter_roots() -> Vec { + vec![ + FungibleFaucet::mint_and_send_root(), + FungibleFaucet::set_max_supply_root(), + FungibleFaucet::set_description_root(), + FungibleFaucet::set_logo_uri_root(), + FungibleFaucet::set_external_link_root(), + TokenPolicyManager::set_mint_policy_root(), + TokenPolicyManager::set_burn_policy_root(), + TokenPolicyManager::set_send_policy_root(), + TokenPolicyManager::set_receive_policy_root(), + ] +} + /// Creates a new fungible faucet account by composing the required components. /// -/// The behaviour of the resulting faucet (basic vs network-style) is determined entirely by the -/// combination of arguments passed in: -/// - `storage_mode`: typically [`AccountStorageMode::Public`] for basic or network faucets. -/// - `auth_method`: typically [`AuthMethod::SingleSig`] for basic faucets, or -/// [`AuthMethod::NetworkAccount`] for network-style faucets. [`AuthMethod::NoAuth`] is also -/// accepted for unauthenticated faucets. -/// - `access_control`: [`AccessControl::AuthControlled`] for auth-only faucets, or -/// [`AccessControl::Ownable2Step`] / [`AccessControl::Rbac`] for owner-controlled faucets. -/// - `token_policy_manager`: the unified [`TokenPolicyManager`] holding both mint and burn policy. +/// The behaviour of the resulting faucet is determined entirely by the +/// `access_control` argument, which carries both the setter-access policy and the +/// account-level [`AuthMethod`] used for transaction authentication: +/// - [`AccessControl::AuthControlled { auth }`] — auth-only faucets. +/// - With [`AuthMethod::SingleSig`], an [`AuthSingleSigAcl`] is installed whose trigger procedure +/// list contains **every** authority-gated setter (see [`all_authority_gated_setter_roots`]). +/// - With [`AuthMethod::NetworkAccount`], an [`AuthNetworkAccount`] is installed. The caller is +/// responsible for choosing `allowed_script_roots` that prevent unauthorized setter +/// invocations. +/// - [`AccessControl::Ownable2Step { owner, auth }`] / [`AccessControl::Rbac { .., auth }`] — +/// owner- or role-controlled faucets. The setter gate enforces `sender == owner` or RBAC role +/// membership in-procedure, so the auth component only governs the faucet's own transaction +/// authentication; any [`AuthMethod`] (including [`AuthMethod::NoAuth`]) is permitted. /// /// The faucet itself, including all token metadata, is provided in the `faucet` parameter (see /// [`FungibleFaucet::builder`]). pub fn create_fungible_faucet( init_seed: [u8; 32], faucet: FungibleFaucet, - storage_mode: AccountStorageMode, - auth_method: AuthMethod, access_control: AccessControl, token_policy_manager: TokenPolicyManager, + storage_mode: AccountStorageMode, ) -> Result { - let mint_proc_root = FungibleFaucet::mint_and_send_root(); - - let auth_component: AccountComponent = match auth_method { - AuthMethod::SingleSig { approver: (pub_key, auth_scheme) } => AuthSingleSigAcl::new( - pub_key, - auth_scheme, - AuthSingleSigAclConfig::new() - .with_auth_trigger_procedures(vec![mint_proc_root]) - .with_allow_unauthorized_input_notes(true), - ) - .map_err(FungibleFaucetError::AccountError)? - .into(), - AuthMethod::NoAuth => NoAuth::new().into(), - AuthMethod::NetworkAccount { allowed_script_roots } => { - AuthNetworkAccount::with_allowlist(allowed_script_roots) - .map_err(|err| { - FungibleFaucetError::UnsupportedAuthMethod(alloc::format!( - "invalid network account allowlist: {err}" - )) - })? - .into() - }, - AuthMethod::Unknown => { - return Err(FungibleFaucetError::UnsupportedAuthMethod( - "fungible faucets cannot be created with Unknown authentication method".into(), - )); - }, - AuthMethod::Multisig { .. } => { - return Err(FungibleFaucetError::UnsupportedAuthMethod( - "fungible faucets do not support Multisig authentication".into(), - )); - }, - }; + let auth_component = build_auth_component(&access_control)?; let account = AccountBuilder::new(init_seed) .account_type(AccountType::FungibleFaucet) @@ -565,3 +622,87 @@ pub fn create_fungible_faucet( Ok(account) } + +/// Builds the account-level auth component from the [`AuthMethod`] embedded in +/// `access_control`. The construction is variant-specific: +/// +/// - Under [`AccessControl::AuthControlled`], [`AuthSingleSig`] is wrapped in an +/// [`AuthSingleSigAcl`] whose trigger procedure list contains every authority-gated setter root, +/// ensuring the auth component authenticates every privileged state mutation. +/// - Under [`AccessControl::Ownable2Step`] / [`AccessControl::Rbac`], the setter gate is handled +/// in-procedure by `authority::assert_authorized`, so the auth component only needs to +/// authenticate the account's own transactions. A plain [`AuthSingleSig`] is installed for +/// [`AuthMethod::SingleSig`]. +/// +/// Rejects [`AuthMethod::Multisig`] / [`AuthMethod::Unknown`] for all variants (faucets do +/// not support Multisig today), and rejects [`AuthMethod::NoAuth`] specifically under +/// [`AccessControl::AuthControlled`] because it would leave authority-gated setters +/// permissionless. +fn build_auth_component( + access_control: &AccessControl, +) -> Result { + let auth = access_control.auth_method(); + + if let AccessControl::AuthControlled { .. } = access_control { + // AuthControlled: the auth component is the only setter gate. + return match auth { + AuthMethod::SingleSig { approver: (pub_key, auth_scheme) } => { + let component = AuthSingleSigAcl::new( + *pub_key, + *auth_scheme, + AuthSingleSigAclConfig::new() + .with_auth_trigger_procedures(all_authority_gated_setter_roots()) + .with_allow_unauthorized_input_notes(true), + ) + .map_err(FungibleFaucetError::AccountError)? + .into(); + Ok(component) + }, + AuthMethod::NetworkAccount { allowed_script_roots } => { + let component = AuthNetworkAccount::with_allowlist(allowed_script_roots.clone()) + .map_err(|err| { + FungibleFaucetError::UnsupportedAuthMethod(alloc::format!( + "invalid network account allowlist: {err}" + )) + })? + .into(); + Ok(component) + }, + AuthMethod::NoAuth => Err(FungibleFaucetError::IncompatibleAuthControlledAuth( + "NoAuth cannot authenticate authority-gated setters under AuthControlled; \ + use AccessControl::Ownable2Step or AccessControl::Rbac for owner-gated faucets, \ + or pair AuthControlled with SingleSig / NetworkAccount." + .into(), + )), + AuthMethod::Multisig { .. } => Err(FungibleFaucetError::UnsupportedAuthMethod( + "fungible faucets do not support Multisig authentication".into(), + )), + AuthMethod::Unknown => Err(FungibleFaucetError::UnsupportedAuthMethod( + "fungible faucets cannot be created with Unknown authentication method".into(), + )), + }; + } + + match auth { + AuthMethod::SingleSig { approver: (pub_key, auth_scheme) } => { + Ok(AuthSingleSig::new(*pub_key, *auth_scheme).into()) + }, + AuthMethod::NoAuth => Ok(NoAuth::new().into()), + AuthMethod::NetworkAccount { allowed_script_roots } => { + let component = AuthNetworkAccount::with_allowlist(allowed_script_roots.clone()) + .map_err(|err| { + FungibleFaucetError::UnsupportedAuthMethod(alloc::format!( + "invalid network account allowlist: {err}" + )) + })? + .into(); + Ok(component) + }, + AuthMethod::Multisig { .. } => Err(FungibleFaucetError::UnsupportedAuthMethod( + "fungible faucets do not support Multisig authentication".into(), + )), + AuthMethod::Unknown => Err(FungibleFaucetError::UnsupportedAuthMethod( + "fungible faucets cannot be created with Unknown authentication method".into(), + )), + } +} diff --git a/crates/miden-standards/src/account/faucets/fungible/tests.rs b/crates/miden-standards/src/account/faucets/fungible/tests.rs index a59e9153e6..6dd3f16266 100644 --- a/crates/miden-standards/src/account/faucets/fungible/tests.rs +++ b/crates/miden-standards/src/account/faucets/fungible/tests.rs @@ -1,3 +1,5 @@ +use alloc::collections::BTreeSet; + use assert_matches::assert_matches; use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment}; use miden_protocol::account::{AccountBuilder, AccountStorageMode, AccountType}; @@ -18,10 +20,53 @@ use crate::account::policies::{ }; use crate::account::wallets::BasicWallet; +/// Builds a minimal policy manager with AllowAll on every kind, used by the construction tests. +fn allow_all_policy_manager() -> TokenPolicyManager { + TokenPolicyManager::new() + .with_mint_policy(MintPolicyConfig::AllowAll, PolicyRegistration::Active) + .unwrap() + .with_burn_policy(BurnPolicyConfig::AllowAll, PolicyRegistration::Active) + .unwrap() + .with_send_policy(TransferPolicy::AllowAll, PolicyRegistration::Active) + .unwrap() + .with_receive_policy(TransferPolicy::AllowAll, PolicyRegistration::Active) + .unwrap() +} + +/// Builds a sample `FungibleFaucet` shared by construction tests. +fn sample_faucet() -> FungibleFaucet { + FungibleFaucet::builder() + .name(TokenName::new("polygon").unwrap()) + .symbol(TokenSymbol::try_from("POL").unwrap()) + .decimals(2) + .max_supply(AssetAmount::from(123u32)) + .description(Description::new("A polygon token").unwrap()) + .build() + .unwrap() +} + +/// Reads every trigger-procedure-root map entry from `0..num` and returns the set. +fn read_trigger_procedure_roots( + account: &miden_protocol::account::Account, + num: u32, +) -> BTreeSet { + (0..num) + .map(|i| { + account + .storage() + .get_map_item( + AuthSingleSigAcl::trigger_procedure_roots_slot(), + [Felt::from(i), Felt::ZERO, Felt::ZERO, Felt::ZERO].into(), + ) + .unwrap() + }) + .collect() +} + #[test] fn faucet_contract_creation() { let pub_key_word = Word::new([Felt::ONE; 4]); - let auth_method: AuthMethod = AuthMethod::SingleSig { + let auth = AuthMethod::SingleSig { approver: (pub_key_word.into(), AuthScheme::Falcon512Poseidon2), }; @@ -31,39 +76,18 @@ fn faucet_contract_creation() { 204, 149, 90, 166, 68, 100, 73, 106, 168, 125, 237, 138, 16, ]; - let max_supply = AssetAmount::from(123u32); let token_symbol_string = "POL"; let token_symbol = TokenSymbol::try_from(token_symbol_string).unwrap(); let token_name_string = "polygon"; let description_string = "A polygon token"; - let decimals = 2u8; - let storage_mode = AccountStorageMode::Private; - let token_name = TokenName::new(token_name_string).unwrap(); - let description = Description::new(description_string).unwrap(); - let faucet = FungibleFaucet::builder() - .name(token_name) - .symbol(token_symbol.clone()) - .decimals(decimals) - .max_supply(max_supply) - .description(description) - .build() - .unwrap(); + let faucet = sample_faucet(); let faucet_account = create_fungible_faucet( init_seed, faucet, - storage_mode, - auth_method, - AccessControl::AuthControlled, - TokenPolicyManager::new() - .with_mint_policy(MintPolicyConfig::AllowAll, PolicyRegistration::Active) - .unwrap() - .with_burn_policy(BurnPolicyConfig::AllowAll, PolicyRegistration::Active) - .unwrap() - .with_send_policy(TransferPolicy::AllowAll, PolicyRegistration::Active) - .unwrap() - .with_receive_policy(TransferPolicy::AllowAll, PolicyRegistration::Active) - .unwrap(), + AccessControl::AuthControlled { auth }, + allow_all_policy_manager(), + AccountStorageMode::Private, ) .unwrap(); @@ -76,25 +100,31 @@ fn faucet_contract_creation() { // The config slot of the auth component stores: // [num_trigger_procs, allow_unauthorized_output_notes, allow_unauthorized_input_notes, 0]. // - // With 1 trigger procedure (mint_and_send), allow_unauthorized_output_notes=false, and - // allow_unauthorized_input_notes=true, this should be [1, 0, 1, 0]. + // With 9 authority-gated trigger procedures (mint_and_send + 4 token metadata setters + + // 4 policy setters), allow_unauthorized_output_notes=false, and + // allow_unauthorized_input_notes=true, this should be [9, 0, 1, 0]. assert_eq!( faucet_account.storage().get_item(AuthSingleSigAcl::config_slot()).unwrap(), - [Felt::ONE, Felt::ZERO, Felt::ONE, Felt::ZERO].into() + [Felt::from(9_u32), Felt::ZERO, Felt::ONE, Felt::ZERO].into() ); - // The procedure root map should contain the mint_and_send procedure root. - let mint_root = FungibleFaucet::mint_and_send_root(); - assert_eq!( - faucet_account - .storage() - .get_map_item( - AuthSingleSigAcl::trigger_procedure_roots_slot(), - [Felt::ZERO, Felt::ZERO, Felt::ZERO, Felt::ZERO].into() - ) - .unwrap(), - mint_root.as_word() - ); + // The trigger procedure root map should contain every authority-gated setter root. + let stored_roots = read_trigger_procedure_roots(&faucet_account, 9); + let expected_roots: BTreeSet = [ + FungibleFaucet::mint_and_send_root(), + FungibleFaucet::set_max_supply_root(), + FungibleFaucet::set_description_root(), + FungibleFaucet::set_logo_uri_root(), + FungibleFaucet::set_external_link_root(), + TokenPolicyManager::set_mint_policy_root(), + TokenPolicyManager::set_burn_policy_root(), + TokenPolicyManager::set_send_policy_root(), + TokenPolicyManager::set_receive_policy_root(), + ] + .into_iter() + .map(|root| root.as_word()) + .collect(); + assert_eq!(stored_roots, expected_roots); // Check that faucet metadata was initialized to the given values. // Storage layout: [token_supply, max_supply, decimals, symbol] @@ -122,6 +152,75 @@ fn faucet_contract_creation() { let _faucet_component = FungibleFaucet::try_from(faucet_account.clone()).unwrap(); } +/// `AccessControl::AuthControlled { auth: NoAuth }` must be rejected: under AuthControlled the +/// auth component is the sole gate for authority-gated setters, so a NoAuth pairing would +/// leave them permissionless. +#[test] +fn auth_controlled_rejects_no_auth() { + let err = create_fungible_faucet( + [7u8; 32], + sample_faucet(), + AccessControl::AuthControlled { auth: AuthMethod::NoAuth }, + allow_all_policy_manager(), + AccountStorageMode::Private, + ) + .expect_err("AuthControlled+NoAuth should be rejected"); + assert_matches!(err, FungibleFaucetError::IncompatibleAuthControlledAuth(_)); +} + +/// `AccessControl::AuthControlled { auth: Multisig | Unknown }` must be rejected — both are +/// already unsupported for fungible faucets, and the merge surfaces the rejection at the +/// type level. +#[test] +fn auth_controlled_rejects_multisig_and_unknown() { + let multisig_err = create_fungible_faucet( + [7u8; 32], + sample_faucet(), + AccessControl::AuthControlled { + auth: AuthMethod::Multisig { + threshold: 1, + approvers: alloc::vec::Vec::new(), + }, + }, + allow_all_policy_manager(), + AccountStorageMode::Private, + ) + .expect_err("AuthControlled+Multisig should be rejected"); + assert_matches!(multisig_err, FungibleFaucetError::UnsupportedAuthMethod(_)); + + let unknown_err = create_fungible_faucet( + [7u8; 32], + sample_faucet(), + AccessControl::AuthControlled { auth: AuthMethod::Unknown }, + allow_all_policy_manager(), + AccountStorageMode::Private, + ) + .expect_err("AuthControlled+Unknown should be rejected"); + assert_matches!(unknown_err, FungibleFaucetError::UnsupportedAuthMethod(_)); +} + +/// `Ownable2Step + NoAuth` is a valid configuration: the setter gate is enforced +/// in-procedure (`assert_sender_is_owner`), so the account-level auth can legitimately be +/// NoAuth (typical for network-style faucets driven by allowlisted note scripts). +#[test] +fn ownable2step_with_no_auth_is_accepted() { + use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE; + + let owner = miden_protocol::account::AccountId::try_from( + ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE, + ) + .unwrap(); + + let _account = create_fungible_faucet( + [7u8; 32], + sample_faucet(), + AccessControl::Ownable2Step { owner, auth: AuthMethod::NoAuth }, + allow_all_policy_manager(), + AccountStorageMode::Public, + ) + .expect("Ownable2Step+NoAuth should be accepted"); +} + #[test] fn faucet_create_from_account() { // prepare the test data @@ -168,4 +267,12 @@ fn faucet_create_from_account() { fn get_faucet_procedures() { let _mint_and_send_root = FungibleFaucet::mint_and_send_root(); let _receive_and_burn_root = FungibleFaucet::receive_and_burn_root(); + let _set_max_supply_root = FungibleFaucet::set_max_supply_root(); + let _set_description_root = FungibleFaucet::set_description_root(); + let _set_logo_uri_root = FungibleFaucet::set_logo_uri_root(); + let _set_external_link_root = FungibleFaucet::set_external_link_root(); + let _set_mint_policy_root = TokenPolicyManager::set_mint_policy_root(); + let _set_burn_policy_root = TokenPolicyManager::set_burn_policy_root(); + let _set_send_policy_root = TokenPolicyManager::set_send_policy_root(); + let _set_receive_policy_root = TokenPolicyManager::set_receive_policy_root(); } diff --git a/crates/miden-standards/src/account/faucets/mod.rs b/crates/miden-standards/src/account/faucets/mod.rs index 3622589fcc..ce30510e9a 100644 --- a/crates/miden-standards/src/account/faucets/mod.rs +++ b/crates/miden-standards/src/account/faucets/mod.rs @@ -59,6 +59,12 @@ pub enum FungibleFaucetError { MissingFungibleFaucetInterface, #[error("unsupported authentication method: {0}")] UnsupportedAuthMethod(String), + #[error( + "AccessControl::AuthControlled is incompatible with the chosen auth method: {0}. \ + Under AuthControlled the auth component is the sole gate for authority-protected \ + setters. It must authenticate every authority-gated setter root." + )] + IncompatibleAuthControlledAuth(String), #[error("account creation failed")] AccountError(#[source] AccountError), #[error("account is not a fungible faucet account")] diff --git a/crates/miden-standards/src/account/policies/manager.rs b/crates/miden-standards/src/account/policies/manager.rs index 17cd0df7c2..426c05a518 100644 --- a/crates/miden-standards/src/account/policies/manager.rs +++ b/crates/miden-standards/src/account/policies/manager.rs @@ -37,6 +37,7 @@ use super::burn::BurnPolicyConfig; use super::mint::MintPolicyConfig; use super::transfer::{TransferAllowAll, TransferPolicy}; use crate::account::account_component_code; +use crate::procedure_root; // ERRORS // ================================================================================================ @@ -52,6 +53,44 @@ pub enum TokenPolicyManagerError { account_component_code!(POLICY_MANAGER_CODE, "faucets/policies/policy_manager.masl"); +// PROCEDURE ROOTS +// ================================================================================================ + +/// MASL library namespace that backs the component code installed on faucet accounts. Used +/// for procedure-root lookups. This is `miden::standards::components::*` and is distinct from +/// [`TokenPolicyManager::NAME`], which is a human-readable identifier mirroring the +/// standards-side MASM module path used for documentation and storage slot prefixes. +const POLICY_MANAGER_LIBRARY_PATH: &str = + "miden::standards::components::faucets::policies::policy_manager"; + +procedure_root!( + POLICY_MANAGER_SET_MINT_POLICY, + POLICY_MANAGER_LIBRARY_PATH, + TokenPolicyManager::SET_MINT_POLICY_PROC_NAME, + TokenPolicyManager::code() +); + +procedure_root!( + POLICY_MANAGER_SET_BURN_POLICY, + POLICY_MANAGER_LIBRARY_PATH, + TokenPolicyManager::SET_BURN_POLICY_PROC_NAME, + TokenPolicyManager::code() +); + +procedure_root!( + POLICY_MANAGER_SET_SEND_POLICY, + POLICY_MANAGER_LIBRARY_PATH, + TokenPolicyManager::SET_SEND_POLICY_PROC_NAME, + TokenPolicyManager::code() +); + +procedure_root!( + POLICY_MANAGER_SET_RECEIVE_POLICY, + POLICY_MANAGER_LIBRARY_PATH, + TokenPolicyManager::SET_RECEIVE_POLICY_PROC_NAME, + TokenPolicyManager::code() +); + // STORAGE SLOT NAMES // ================================================================================================ @@ -191,6 +230,11 @@ impl TokenPolicyManager { /// Component description used in [`AccountComponentMetadata`]. pub const DESCRIPTION: &'static str = "Token policy manager for fungible faucets"; + const SET_MINT_POLICY_PROC_NAME: &'static str = "set_mint_policy"; + const SET_BURN_POLICY_PROC_NAME: &'static str = "set_burn_policy"; + const SET_SEND_POLICY_PROC_NAME: &'static str = "set_send_policy"; + const SET_RECEIVE_POLICY_PROC_NAME: &'static str = "set_receive_policy"; + // CONSTRUCTORS // -------------------------------------------------------------------------------------------- @@ -374,6 +418,29 @@ impl TokenPolicyManager { .collect() } + /// Returns the procedure root of the `set_mint_policy` account procedure. This is an + /// authority-gated setter; under + /// [`AccessControl::AuthControlled`][crate::account::access::AccessControl::AuthControlled] + /// it must appear in the auth component's trigger procedure list. + pub fn set_mint_policy_root() -> AccountProcedureRoot { + *POLICY_MANAGER_SET_MINT_POLICY + } + + /// Returns the procedure root of the `set_burn_policy` account procedure. Authority-gated. + pub fn set_burn_policy_root() -> AccountProcedureRoot { + *POLICY_MANAGER_SET_BURN_POLICY + } + + /// Returns the procedure root of the `set_send_policy` account procedure. Authority-gated. + pub fn set_send_policy_root() -> AccountProcedureRoot { + *POLICY_MANAGER_SET_SEND_POLICY + } + + /// Returns the procedure root of the `set_receive_policy` account procedure. Authority-gated. + pub fn set_receive_policy_root() -> AccountProcedureRoot { + *POLICY_MANAGER_SET_RECEIVE_POLICY + } + /// Returns the [`StorageSlotName`] where the active mint policy procedure root is stored. pub fn active_mint_policy_slot() -> &'static StorageSlotName { &ACTIVE_MINT_POLICY_PROC_ROOT_SLOT_NAME diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index 9c9e93c649..3f156f0588 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -47,6 +47,7 @@ use miden_protocol::testing::account_id::ACCOUNT_ID_FEE_FAUCET; use miden_protocol::testing::random_secret_key::random_secret_key; use miden_protocol::transaction::{OrderedTransactionHeaders, RawOutputNote, TransactionKernel}; use miden_protocol::{MAX_OUTPUT_NOTES_PER_BATCH, Word}; +use miden_standards::AuthMethod; use miden_standards::account::access::AccessControl; use miden_standards::account::faucets::{FungibleFaucet, TokenName}; use miden_standards::account::policies::{ @@ -407,11 +408,17 @@ impl MockChainBuilder { .with_send_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)? .with_receive_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)?; + // The `auth` field on AccessControl is a placeholder in chain_builder: the real auth + // component is built from `auth_method: Auth` by `add_account_from_builder` and + // installed via `with_auth_component`. `AccessControl::IntoIterator` only yields the + // setter-gate components, so the placeholder is not used to construct components — it + // only satisfies the type. Tests that exercise factory-level validation should call + // `create_fungible_faucet` directly instead. self.add_existing_fungible_faucet( auth_method, faucet, AccountStorageMode::Public, - AccessControl::AuthControlled, + AccessControl::AuthControlled { auth: AuthMethod::NoAuth }, token_policy_manager, ) } @@ -456,16 +463,22 @@ impl MockChainBuilder { .with_send_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)? .with_receive_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)?; - let allowed_script_roots = allowed_script_roots - .into_iter() - .chain([MintNote::script_root(), BurnNote::script_root()]) - .collect(); + let allowed_script_roots: alloc::collections::BTreeSet = + allowed_script_roots + .into_iter() + .chain([MintNote::script_root(), BurnNote::script_root()]) + .collect(); self.add_existing_fungible_faucet( - Auth::NetworkAccount { allowed_script_roots }, + Auth::NetworkAccount { + allowed_script_roots: allowed_script_roots.clone(), + }, faucet, AccountStorageMode::Public, - AccessControl::Ownable2Step { owner: owner_account_id }, + AccessControl::Ownable2Step { + owner: owner_account_id, + auth: AuthMethod::NetworkAccount { allowed_script_roots }, + }, token_policy_manager, ) } @@ -488,16 +501,22 @@ impl MockChainBuilder { .with_send_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)? .with_receive_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)?; - let allowed_script_roots = allowed_script_roots - .into_iter() - .chain([MintNote::script_root(), BurnNote::script_root()]) - .collect(); + let allowed_script_roots: alloc::collections::BTreeSet = + allowed_script_roots + .into_iter() + .chain([MintNote::script_root(), BurnNote::script_root()]) + .collect(); self.add_existing_fungible_faucet( - Auth::NetworkAccount { allowed_script_roots }, + Auth::NetworkAccount { + allowed_script_roots: allowed_script_roots.clone(), + }, faucet, AccountStorageMode::Public, - AccessControl::Ownable2Step { owner: owner_account_id }, + AccessControl::Ownable2Step { + owner: owner_account_id, + auth: AuthMethod::NetworkAccount { allowed_script_roots }, + }, token_policy_manager, ) } @@ -528,11 +547,12 @@ impl MockChainBuilder { .with_send_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)? .with_receive_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)?; + // See note on `add_existing_basic_faucet` re: the placeholder auth field. self.create_new_fungible_faucet( auth_method, faucet, AccountStorageMode::Public, - AccessControl::AuthControlled, + AccessControl::AuthControlled { auth: AuthMethod::NoAuth }, token_policy_manager, ) } diff --git a/crates/miden-testing/tests/scripts/rbac.rs b/crates/miden-testing/tests/scripts/rbac.rs index 0f952c567c..c4f5bf213d 100644 --- a/crates/miden-testing/tests/scripts/rbac.rs +++ b/crates/miden-testing/tests/scripts/rbac.rs @@ -16,6 +16,7 @@ use miden_protocol::account::{ use miden_protocol::errors::AccountIdError; use miden_protocol::note::{Note, NoteType}; use miden_protocol::{Felt, Word}; +use miden_standards::AuthMethod; use miden_standards::account::access::{AccessControl, Ownable2Step, RoleBasedAccessControl}; use miden_standards::errors::standards::{ ERR_ACCOUNT_NOT_IN_ROLE, @@ -33,7 +34,11 @@ fn create_rbac_account_with_owner(owner: AccountId) -> anyhow::Result { let account = AccountBuilder::new([9; 32]) .storage_mode(AccountStorageMode::Public) .with_auth_component(Auth::IncrNonce) - .with_components(AccessControl::Rbac { owner, authority_role: None }) + .with_components(AccessControl::Rbac { + owner, + authority_role: None, + auth: AuthMethod::NoAuth, + }) .build_existing()?; Ok(account) From 8b2527b04895cf133189ace3a3d106ac07c7cdae Mon Sep 17 00:00:00 2001 From: onurinanc Date: Mon, 18 May 2026 16:52:44 +0200 Subject: [PATCH 2/3] changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f62d53166..4f2090f221 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,10 +59,12 @@ - [BREAKING] `FungibleAsset::amount()` and `AssetVault::get_balance()` now return `AssetAmount` ([#2928](https://github.com/0xMiden/protocol/pull/2928)). - [BREAKING] Upgraded `miden-vm` to v0.23 and `miden-crypto` to v0.25. Notable downstream changes: dropped the immediate form of `adv_push` in kernel and standards MASM, marked cross-module-referenced MASM constants and procedures `pub`, migrated to the split `Host`/`BaseHost` trait surface, renamed `Felt::new` call sites to the preserved-behavior `Felt::new_unchecked`, switched `ecdsa_k256_keccak`/`eddsa_25519_sha512` `SecretKey` references to the new `SigningKey`/`KeyExchangeKey` types, and recomputed the kernel's `EMPTY_SMT_ROOT` constant for the Plonky3-aligned Poseidon2 and domain-separated `SmtLeaf::hash` ([#2931](https://github.com/0xMiden/protocol/pull/2931)). - Derive `Hash` implementation for `StorageMapKey` and `StorageMapKeyHash` to allow using those values as keys in containers ([#2843](https://github.com/0xMiden/protocol/issues/2843)). +- [BREAKING] Merged `AuthMethod` into `AccessControl` so every variant (`AuthControlled`, `Ownable2Step`, `Rbac`) now includes an `auth: AuthMethod` field. ([#2944](https://github.com/0xMiden/protocol/pull/2944)). ### Fixes - Fixed `LocalTransactionProver` accumulating `MastForest` entries across `prove()` calls, causing `capacity_overflow` panics in WASM environments where linear memory fragmentation prevents subsequent allocations ([#2918](https://github.com/0xMiden/protocol/pull/2918)). +- Fixed `create_fungible_faucet` leaving authority-gated setters unauthenticated under `AccessControl::AuthControlled` ([#2943](https://github.com/0xMiden/protocol/issues/2943), [#2944](https://github.com/0xMiden/protocol/pull/2944)). - Made deserialization of `AccountCode` more robust ([#2788](https://github.com/0xMiden/protocol/pull/2788)). - Validated `PartialBlockchain` invariants on deserialization ([#2888](https://github.com/0xMiden/protocol/pull/2888)). - Fixed `output_note::add_asset` and `output_note::set_attachment` to no longer accept invalid note indices ([#2824](https://github.com/0xMiden/protocol/pull/2824)). From 039ced3ebbf3888424a7d12c5b604da391fa8087 Mon Sep 17 00:00:00 2001 From: onurinanc Date: Mon, 18 May 2026 17:02:41 +0200 Subject: [PATCH 3/3] fix documentation --- crates/miden-standards/src/account/faucets/fungible/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/miden-standards/src/account/faucets/fungible/mod.rs b/crates/miden-standards/src/account/faucets/fungible/mod.rs index 7eeadf0a0e..ef97d1c159 100644 --- a/crates/miden-standards/src/account/faucets/fungible/mod.rs +++ b/crates/miden-standards/src/account/faucets/fungible/mod.rs @@ -590,7 +590,10 @@ fn all_authority_gated_setter_roots() -> Vec { /// account-level [`AuthMethod`] used for transaction authentication: /// - [`AccessControl::AuthControlled { auth }`] — auth-only faucets. /// - With [`AuthMethod::SingleSig`], an [`AuthSingleSigAcl`] is installed whose trigger procedure -/// list contains **every** authority-gated setter (see [`all_authority_gated_setter_roots`]). +/// list contains **every** authority-gated setter exported by the faucet and policy-manager +/// components (`mint_and_send`, `set_max_supply`, `set_description`, `set_logo_uri`, +/// `set_external_link`, `set_mint_policy`, `set_burn_policy`, `set_send_policy`, +/// `set_receive_policy`). /// - With [`AuthMethod::NetworkAccount`], an [`AuthNetworkAccount`] is installed. The caller is /// responsible for choosing `allowed_script_roots` that prevent unauthorized setter /// invocations.