From 2a93cf1e8dc538b0bf27684325376d0291e9eda7 Mon Sep 17 00:00:00 2001 From: onurinanc Date: Mon, 18 May 2026 16:43:18 +0200 Subject: [PATCH 1/8] 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/8] 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/8] 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. From 9c683be82b0378a288102ab29b0d0b351ae51df1 Mon Sep 17 00:00:00 2001 From: onurinanc Date: Wed, 20 May 2026 12:41:01 +0200 Subject: [PATCH 4/8] fix --- CHANGELOG.md | 3 +- .../miden-standards/src/account/access/mod.rs | 62 +++---- .../src/account/faucets/fungible/mod.rs | 151 ++++++++---------- .../src/account/faucets/fungible/tests.rs | 52 ++---- .../src/mock_chain/chain_builder.rs | 48 ++---- crates/miden-testing/tests/scripts/rbac.rs | 7 +- 6 files changed, 111 insertions(+), 212 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ee26b0f48..21e1168f41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,13 +63,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)). - [BREAKING] Removed `AccountType` and renamed `AccountStorageMode` to `AccountType` ([#2939](https://github.com/0xMiden/protocol/pull/2939), [#2942](https://github.com/0xMiden/protocol/pull/2942)). ### 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)). +- Fixed `create_fungible_faucet` leaving authority-gated setters unauthenticated under `AccessControl::AuthControlled`: the `AuthSingleSigAcl` trigger list now contains every authority-gated setter root (`set_max_supply`, `set_description`, `set_logo_uri`, `set_external_link`, `set_mint_policy`, `set_burn_policy`, `set_send_policy`, `set_receive_policy`) in addition to `mint_and_send`. - 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)). diff --git a/crates/miden-standards/src/account/access/mod.rs b/crates/miden-standards/src/account/access/mod.rs index b0f6b0eb48..98af226d2d 100644 --- a/crates/miden-standards/src/account/access/mod.rs +++ b/crates/miden-standards/src/account/access/mod.rs @@ -2,41 +2,34 @@ 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. /// -/// - [`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: +/// - [`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: /// - `None` → [`Authority::OwnerControlled`] (the top-level owner gates `set_*` operations). /// - `Some(role)` → [`Authority::RbacControlled { role }`] (any holder of `role` gates `set_*` /// operations). -/// -/// Note that the auth component is **not** yielded by [`IntoIterator`]. #[derive(Debug, Clone, PartialEq, Eq)] pub enum AccessControl { - /// The account's auth component is used for both transaction-level authentication - /// and authority-gated setters. - AuthControlled { auth: AuthMethod }, - /// Two-step ownership transfer with the provided initial owner. The setter gate enforces - /// `sender == owner`. - 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). `auth` governs the - /// account's own transaction authentication only. + /// 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 }, + /// Role-based access control. Includes [`Ownable2Step`] internally, the provided `owner` + /// becomes the top-level RBAC authority (the account's owner). /// /// `authority_role` controls which authority is installed alongside RBAC: /// - `None` (default) → [`Authority::OwnerControlled`]: the top-level `owner` is the sole /// authority for `set_*` operations (`set_mint_policy`, `set_burn_policy`, metadata setters). - /// RBAC roles can still be granted and revoked but they do not directly gate the + /// RBAC roles can still be granted/revoked but they do not directly gate the /// authority-protected procedures. /// - `Some(role)` → [`Authority::RbacControlled { role }`]: any account holding `role` becomes /// a valid authority for `set_*` operations. Role membership is managed through the standard @@ -44,44 +37,29 @@ pub enum AccessControl { Rbac { owner: AccountId, authority_role: Option, - auth: AuthMethod, }, } -impl AccessControl { - /// Returns the [`AuthMethod`] selected for the account's auth component. - 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. fn into_iter(self) -> Self::IntoIter { match self { - AccessControl::AuthControlled { auth: _ } => { - vec![Authority::AuthControlled.into()].into_iter() - }, - AccessControl::Ownable2Step { owner, auth: _ } => { + AccessControl::AuthControlled => vec![Authority::AuthControlled.into()].into_iter(), + AccessControl::Ownable2Step { owner } => { vec![Ownable2Step::new(owner).into(), Authority::OwnerControlled.into()].into_iter() }, - AccessControl::Rbac { owner, authority_role: None, auth: _ } => vec![ + AccessControl::Rbac { owner, authority_role: None } => vec![ Ownable2Step::new(owner).into(), RoleBasedAccessControl::empty().into(), Authority::OwnerControlled.into(), ] .into_iter(), - AccessControl::Rbac { - owner, - authority_role: Some(role), - auth: _, - } => vec![ + AccessControl::Rbac { owner, authority_role: Some(role) } => 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 c1938e92e8..ea8b34851f 100644 --- a/crates/miden-standards/src/account/faucets/fungible/mod.rs +++ b/crates/miden-standards/src/account/faucets/fungible/mod.rs @@ -34,13 +34,7 @@ use super::{ }; use crate::account::access::AccessControl; use crate::account::account_component_code; -use crate::account::auth::{ - AuthNetworkAccount, - AuthSingleSig, - AuthSingleSigAcl, - AuthSingleSigAclConfig, - NoAuth, -}; +use crate::account::auth::{AuthNetworkAccount, AuthSingleSigAcl, AuthSingleSigAclConfig, NoAuth}; use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; use crate::account::policies::TokenPolicyManager; use crate::{AuthMethod, procedure_root}; @@ -586,26 +580,78 @@ fn all_authority_gated_setter_roots() -> Vec { ] } -/// Creates a new fungible faucet account. -/// -/// `access_control` carries both the setter access gate and the account's [`AuthMethod`]: +/// Creates a new fungible faucet account by composing the required components. /// -/// - [`AccessControl::AuthControlled { auth }`] — `auth` is the sole gate for authority-gated -/// setters. For [`AuthMethod::NetworkAccount`], the caller is responsible for choosing -/// `allowed_script_roots` that exclude unauthorized setter invocations. -/// - [`AccessControl::Ownable2Step`] / [`AccessControl::Rbac`] — setter gate is enforced -/// in-procedure (owner / role check), so any [`AuthMethod`] is accepted. +/// The behaviour of the resulting faucet (basic vs network-style) is determined entirely by the +/// combination of arguments passed in: +/// - `account_type`: typically [`AccountType::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 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, + account_type: AccountType, + auth_method: AuthMethod, access_control: AccessControl, token_policy_manager: TokenPolicyManager, - account_type: AccountType, ) -> Result { - let auth_component = build_auth_component(&access_control)?; + let is_auth_controlled = matches!(access_control, AccessControl::AuthControlled); + + let auth_component: AccountComponent = match auth_method { + AuthMethod::SingleSig { approver: (pub_key, auth_scheme) } => { + let trigger_procedures = if is_auth_controlled { + all_authority_gated_setter_roots() + } else { + vec![FungibleFaucet::mint_and_send_root()] + }; + AuthSingleSigAcl::new( + pub_key, + auth_scheme, + AuthSingleSigAclConfig::new() + .with_auth_trigger_procedures(trigger_procedures) + .with_allow_unauthorized_input_notes(true), + ) + .map_err(FungibleFaucetError::AccountError)? + .into() + }, + AuthMethod::NoAuth => { + if is_auth_controlled { + return 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(), + )); + } + 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 account = AccountBuilder::new(init_seed) .account_type(account_type) @@ -618,74 +664,3 @@ pub fn create_fungible_faucet( Ok(account) } - -/// Builds the account-level auth component from the [`AuthMethod`] embedded in -/// `access_control`. -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 211303cf22..7151277b52 100644 --- a/crates/miden-standards/src/account/faucets/fungible/tests.rs +++ b/crates/miden-standards/src/account/faucets/fungible/tests.rs @@ -66,7 +66,7 @@ fn read_trigger_procedure_roots( #[test] fn faucet_contract_creation() { let pub_key_word = Word::new([Felt::ONE; 4]); - let auth = AuthMethod::SingleSig { + let auth_method = AuthMethod::SingleSig { approver: (pub_key_word.into(), AuthScheme::Falcon512Poseidon2), }; @@ -85,9 +85,10 @@ fn faucet_contract_creation() { let faucet_account = create_fungible_faucet( init_seed, faucet, - AccessControl::AuthControlled { auth }, - allow_all_policy_manager(), AccountType::Private, + auth_method, + AccessControl::AuthControlled, + allow_all_policy_manager(), ) .unwrap(); @@ -148,53 +149,23 @@ 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 +/// `(AccessControl::AuthControlled, AuthMethod::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(), AccountType::Private, + AuthMethod::NoAuth, + AccessControl::AuthControlled, + allow_all_policy_manager(), ) .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(), - AccountType::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(), - AccountType::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). @@ -210,9 +181,10 @@ fn ownable2step_with_no_auth_is_accepted() { let _account = create_fungible_faucet( [7u8; 32], sample_faucet(), - AccessControl::Ownable2Step { owner, auth: AuthMethod::NoAuth }, - allow_all_policy_manager(), AccountType::Public, + AuthMethod::NoAuth, + AccessControl::Ownable2Step { owner }, + allow_all_policy_manager(), ) .expect("Ownable2Step+NoAuth should be accepted"); } diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index 8183501308..4f5aa42276 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -46,7 +46,6 @@ 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::{ @@ -404,17 +403,11 @@ 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, AccountType::Public, - AccessControl::AuthControlled { auth: AuthMethod::NoAuth }, + AccessControl::AuthControlled, token_policy_manager, ) } @@ -459,22 +452,16 @@ impl MockChainBuilder { .with_send_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)? .with_receive_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)?; - let allowed_script_roots: alloc::collections::BTreeSet = - allowed_script_roots - .into_iter() - .chain([MintNote::script_root(), BurnNote::script_root()]) - .collect(); + let allowed_script_roots = allowed_script_roots + .into_iter() + .chain([MintNote::script_root(), BurnNote::script_root()]) + .collect(); self.add_existing_fungible_faucet( - Auth::NetworkAccount { - allowed_script_roots: allowed_script_roots.clone(), - }, + Auth::NetworkAccount { allowed_script_roots }, faucet, AccountType::Public, - AccessControl::Ownable2Step { - owner: owner_account_id, - auth: AuthMethod::NetworkAccount { allowed_script_roots }, - }, + AccessControl::Ownable2Step { owner: owner_account_id }, token_policy_manager, ) } @@ -497,22 +484,16 @@ impl MockChainBuilder { .with_send_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)? .with_receive_policy(TransferPolicy::AllowAll, PolicyRegistration::Active)?; - let allowed_script_roots: alloc::collections::BTreeSet = - allowed_script_roots - .into_iter() - .chain([MintNote::script_root(), BurnNote::script_root()]) - .collect(); + let allowed_script_roots = allowed_script_roots + .into_iter() + .chain([MintNote::script_root(), BurnNote::script_root()]) + .collect(); self.add_existing_fungible_faucet( - Auth::NetworkAccount { - allowed_script_roots: allowed_script_roots.clone(), - }, + Auth::NetworkAccount { allowed_script_roots }, faucet, AccountType::Public, - AccessControl::Ownable2Step { - owner: owner_account_id, - auth: AuthMethod::NetworkAccount { allowed_script_roots }, - }, + AccessControl::Ownable2Step { owner: owner_account_id }, token_policy_manager, ) } @@ -543,12 +524,11 @@ 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, AccountType::Public, - AccessControl::AuthControlled { auth: AuthMethod::NoAuth }, + AccessControl::AuthControlled, token_policy_manager, ) } diff --git a/crates/miden-testing/tests/scripts/rbac.rs b/crates/miden-testing/tests/scripts/rbac.rs index 5b29c60287..6ba7cc2829 100644 --- a/crates/miden-testing/tests/scripts/rbac.rs +++ b/crates/miden-testing/tests/scripts/rbac.rs @@ -15,7 +15,6 @@ 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,11 +32,7 @@ fn create_rbac_account_with_owner(owner: AccountId) -> anyhow::Result { let account = AccountBuilder::new([9; 32]) .account_type(AccountType::Public) .with_auth_component(Auth::IncrNonce) - .with_components(AccessControl::Rbac { - owner, - authority_role: None, - auth: AuthMethod::NoAuth, - }) + .with_components(AccessControl::Rbac { owner, authority_role: None }) .build_existing()?; Ok(account) From 3c775d251c20f4a8d88fa81c688b96a9703cd03e Mon Sep 17 00:00:00 2001 From: onurinanc Date: Wed, 20 May 2026 12:46:52 +0200 Subject: [PATCH 5/8] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21e1168f41..dcb300c863 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,7 +68,7 @@ ### 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`: the `AuthSingleSigAcl` trigger list now contains every authority-gated setter root (`set_max_supply`, `set_description`, `set_logo_uri`, `set_external_link`, `set_mint_policy`, `set_burn_policy`, `set_send_policy`, `set_receive_policy`) in addition to `mint_and_send`. +- Fixed `create_fungible_faucet` leaving authority-gated setters unauthenticated under `AccessControl::AuthControlled`: the `AuthSingleSigAcl` trigger list now contains every authority-gated setter root (`set_max_supply`, `set_description`, `set_logo_uri`, `set_external_link`, `set_mint_policy`, `set_burn_policy`, `set_send_policy`, `set_receive_policy`) in addition to `mint_and_send`. ([#2958](https://github.com/0xMiden/protocol/pull/2958)). - 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 1402b3319e3d29c6d887f60537c2e574fdc5513f Mon Sep 17 00:00:00 2001 From: onurinanc Date: Wed, 20 May 2026 13:39:12 +0200 Subject: [PATCH 6/8] fix comments --- .../src/account/access/authority.rs | 10 ++++-- .../miden-standards/src/account/access/mod.rs | 24 +++++++++++++- .../src/account/faucets/fungible/mod.rs | 31 ++++++------------- .../src/account/faucets/fungible/tests.rs | 6 ---- .../src/account/faucets/mod.rs | 6 +--- .../src/account/policies/manager.rs | 17 ++++------ 6 files changed, 47 insertions(+), 47 deletions(-) diff --git a/crates/miden-standards/src/account/access/authority.rs b/crates/miden-standards/src/account/access/authority.rs index e7c75a4f79..a58c9d874b 100644 --- a/crates/miden-standards/src/account/access/authority.rs +++ b/crates/miden-standards/src/account/access/authority.rs @@ -58,13 +58,19 @@ const RBAC_CONTROLLED: u8 = 2; #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] pub enum Authority { - /// Authority is the account's auth component. + /// Authority is the account's auth component; no extra check is performed by + /// `authority::assert_authorized`. AuthControlled = AUTH_CONTROLLED, - /// Authority is the [`Ownable2Step`][crate::account::access::Ownable2Step] owner. + /// Authority is the [`Ownable2Step`][crate::account::access::Ownable2Step] owner; the call + /// must be sent by the registered owner. OwnerControlled = OWNER_CONTROLLED, /// Authority is membership in a specific RBAC role. The call must be sent by an account that /// holds `role` in the /// [`RoleBasedAccessControl`][crate::account::access::RoleBasedAccessControl] component. + /// + /// Requires the [`RoleBasedAccessControl`][crate::account::access::RoleBasedAccessControl] + /// component to be installed on the account; the MASM helper calls into + /// `rbac::assert_sender_has_role` and will fail to link otherwise. RbacControlled { role: RoleSymbol } = RBAC_CONTROLLED, } diff --git a/crates/miden-standards/src/account/access/mod.rs b/crates/miden-standards/src/account/access/mod.rs index 98af226d2d..085f18d847 100644 --- a/crates/miden-standards/src/account/access/mod.rs +++ b/crates/miden-standards/src/account/access/mod.rs @@ -8,6 +8,11 @@ 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. +/// /// - [`AccessControl::AuthControlled`] yields just [`Authority::AuthControlled`]. /// - [`AccessControl::Ownable2Step`] yields [`Ownable2Step`] + [`Authority::OwnerControlled`]. /// - [`AccessControl::Rbac`] yields [`Ownable2Step`] + [`RoleBasedAccessControl`] + an @@ -15,6 +20,23 @@ pub mod rbac; /// - `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: +/// +/// ```no_run +/// use miden_protocol::account::AccountBuilder; +/// 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 }); +/// ``` +/// +/// 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 @@ -23,7 +45,7 @@ pub enum AccessControl { /// Two-step ownership transfer with the provided initial owner. Authority for `set_*` /// operations is fixed to the registered owner. Ownable2Step { owner: AccountId }, - /// Role-based access control. Includes [`Ownable2Step`] internally, the provided `owner` + /// Role-based access control. Includes [`Ownable2Step`] internally; the provided `owner` /// becomes the top-level RBAC authority (the account's owner). /// /// `authority_role` controls which authority is installed alongside RBAC: diff --git a/crates/miden-standards/src/account/faucets/fungible/mod.rs b/crates/miden-standards/src/account/faucets/fungible/mod.rs index ea8b34851f..5ef30116be 100644 --- a/crates/miden-standards/src/account/faucets/fungible/mod.rs +++ b/crates/miden-standards/src/account/faucets/fungible/mod.rs @@ -283,25 +283,22 @@ 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. + /// Returns the procedure root of the `set_max_supply` account procedure. pub fn set_max_supply_root() -> AccountProcedureRoot { *FUNGIBLE_FAUCET_SET_MAX_SUPPLY } - /// Returns the procedure root of the `set_description` account procedure. Authority-gated. + /// Returns the procedure root of the `set_description` account procedure. pub fn set_description_root() -> AccountProcedureRoot { *FUNGIBLE_FAUCET_SET_DESCRIPTION } - /// Returns the procedure root of the `set_logo_uri` account procedure. Authority-gated. + /// Returns the procedure root of the `set_logo_uri` account procedure. 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. + /// Returns the procedure root of the `set_external_link` account procedure. pub fn set_external_link_root() -> AccountProcedureRoot { *FUNGIBLE_FAUCET_SET_EXTERNAL_LINK } @@ -555,17 +552,10 @@ 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. +/// Every authority-gated procedure root that must require a signature when +/// [`AccessControl::AuthControlled`] is paired with [`AuthMethod::SingleSig`]. Includes +/// `mint_and_send` so that minting always requires a signature regardless of the access +/// control variant. fn all_authority_gated_setter_roots() -> Vec { vec![ FungibleFaucet::mint_and_send_root(), @@ -624,10 +614,7 @@ pub fn create_fungible_faucet( AuthMethod::NoAuth => { if is_auth_controlled { return 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(), + "NoAuth cannot authenticate authority-gated setters".into(), )); } NoAuth::new().into() diff --git a/crates/miden-standards/src/account/faucets/fungible/tests.rs b/crates/miden-standards/src/account/faucets/fungible/tests.rs index 7151277b52..42855d7ce5 100644 --- a/crates/miden-standards/src/account/faucets/fungible/tests.rs +++ b/crates/miden-standards/src/account/faucets/fungible/tests.rs @@ -149,9 +149,6 @@ fn faucet_contract_creation() { let _faucet_component = FungibleFaucet::try_from(faucet_account.clone()).unwrap(); } -/// `(AccessControl::AuthControlled, AuthMethod::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( @@ -166,9 +163,6 @@ fn auth_controlled_rejects_no_auth() { assert_matches!(err, FungibleFaucetError::IncompatibleAuthControlledAuth(_)); } -/// `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; diff --git a/crates/miden-standards/src/account/faucets/mod.rs b/crates/miden-standards/src/account/faucets/mod.rs index ce30510e9a..1c56ef74d3 100644 --- a/crates/miden-standards/src/account/faucets/mod.rs +++ b/crates/miden-standards/src/account/faucets/mod.rs @@ -59,11 +59,7 @@ 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." - )] + #[error("AccessControl::AuthControlled is incompatible with the chosen auth method: {0}")] IncompatibleAuthControlledAuth(String), #[error("account creation failed")] AccountError(#[source] AccountError), diff --git a/crates/miden-standards/src/account/policies/manager.rs b/crates/miden-standards/src/account/policies/manager.rs index 022d20dfa2..0424b327aa 100644 --- a/crates/miden-standards/src/account/policies/manager.rs +++ b/crates/miden-standards/src/account/policies/manager.rs @@ -56,10 +56,8 @@ account_component_code!(POLICY_MANAGER_CODE, "faucets/policies/policy_manager.ma // 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. +/// MASL library namespace used for procedure-root lookups. Distinct from +/// [`TokenPolicyManager::NAME`], which mirrors the standards-side MASM module path. const POLICY_MANAGER_LIBRARY_PATH: &str = "miden::standards::components::faucets::policies::policy_manager"; @@ -423,25 +421,22 @@ 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. + /// Returns the procedure root of the `set_mint_policy` account procedure. 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. + /// Returns the procedure root of the `set_burn_policy` account procedure. 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. + /// Returns the procedure root of the `set_send_policy` account procedure. 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. + /// Returns the procedure root of the `set_receive_policy` account procedure. pub fn set_receive_policy_root() -> AccountProcedureRoot { *POLICY_MANAGER_SET_RECEIVE_POLICY } From 5736d2c426c025957e79e307c82769255e633f55 Mon Sep 17 00:00:00 2001 From: onurinanc Date: Thu, 21 May 2026 13:59:41 +0200 Subject: [PATCH 7/8] add build_auth_component --- .../src/account/faucets/fungible/mod.rs | 159 +++++++++++------- .../src/account/faucets/fungible/tests.rs | 27 +++ .../src/account/faucets/mod.rs | 2 + 3 files changed, 130 insertions(+), 58 deletions(-) diff --git a/crates/miden-standards/src/account/faucets/fungible/mod.rs b/crates/miden-standards/src/account/faucets/fungible/mod.rs index 5ef30116be..76fe9cafe6 100644 --- a/crates/miden-standards/src/account/faucets/fungible/mod.rs +++ b/crates/miden-standards/src/account/faucets/fungible/mod.rs @@ -572,18 +572,22 @@ fn all_authority_gated_setter_roots() -> Vec { /// 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: -/// - `account_type`: typically [`AccountType::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. +/// Only specific `(access_control, auth_method)` combinations are supported; everything else +/// is rejected at the factory level. The valid combinations are: /// -/// The faucet itself, including all token metadata, is provided in the `faucet` parameter (see -/// [`FungibleFaucet::builder`]). +/// - [`AccessControl::AuthControlled`] + [`AuthMethod::SingleSig`] — user-account faucet whose auth +/// component is the sole gate for every authority-protected setter. +/// - [`AccessControl::AuthControlled`] + [`AuthMethod::NetworkAccount`] — the caller is responsible +/// for choosing `allowed_script_roots` that prevent unauthorized setter invocations. +/// - [`AccessControl::Ownable2Step`] / [`AccessControl::Rbac`] + [`AuthMethod::NetworkAccount`] or +/// [`AuthMethod::NoAuth`] — network-style faucet whose setter gate is enforced in-procedure by +/// the owner/role check. +/// +/// All other pairings return a typed error: +/// [`FungibleFaucetError::IncompatibleAuthControlledAuth`] for `AuthControlled + NoAuth` and +/// [`FungibleFaucetError::UnsupportedAccessControlAuthCombination`] for +/// `Ownable2Step`/`Rbac` + `SingleSig`. `Multisig` and `Unknown` remain rejected for every +/// variant via [`FungibleFaucetError::UnsupportedAuthMethod`]. pub fn create_fungible_faucet( init_seed: [u8; 32], faucet: FungibleFaucet, @@ -592,53 +596,7 @@ pub fn create_fungible_faucet( access_control: AccessControl, token_policy_manager: TokenPolicyManager, ) -> Result { - let is_auth_controlled = matches!(access_control, AccessControl::AuthControlled); - - let auth_component: AccountComponent = match auth_method { - AuthMethod::SingleSig { approver: (pub_key, auth_scheme) } => { - let trigger_procedures = if is_auth_controlled { - all_authority_gated_setter_roots() - } else { - vec![FungibleFaucet::mint_and_send_root()] - }; - AuthSingleSigAcl::new( - pub_key, - auth_scheme, - AuthSingleSigAclConfig::new() - .with_auth_trigger_procedures(trigger_procedures) - .with_allow_unauthorized_input_notes(true), - ) - .map_err(FungibleFaucetError::AccountError)? - .into() - }, - AuthMethod::NoAuth => { - if is_auth_controlled { - return Err(FungibleFaucetError::IncompatibleAuthControlledAuth( - "NoAuth cannot authenticate authority-gated setters".into(), - )); - } - 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, auth_method)?; let account = AccountBuilder::new(init_seed) .account_type(account_type) @@ -651,3 +609,88 @@ pub fn create_fungible_faucet( Ok(account) } + +/// Builds the account-level auth component, validating the `(access_control, auth_method)` +/// pair. See [`create_fungible_faucet`] for the list of supported combinations. +fn build_auth_component( + access_control: &AccessControl, + auth_method: AuthMethod, +) -> Result { + match (access_control, auth_method) { + // AuthControlled + SingleSig: the auth component is the sole setter gate, so it + // must authenticate every authority-gated setter root. + ( + AccessControl::AuthControlled, + AuthMethod::SingleSig { approver: (pub_key, auth_scheme) }, + ) => Ok(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()), + + // AuthControlled + NetworkAccount: accepted; allowed_script_roots is the caller's + // responsibility (must not include scripts that can invoke authority-gated setters). + (AccessControl::AuthControlled, AuthMethod::NetworkAccount { allowed_script_roots }) => { + Ok(AuthNetworkAccount::with_allowlist(allowed_script_roots) + .map_err(|err| { + FungibleFaucetError::UnsupportedAuthMethod(alloc::format!( + "invalid network account allowlist: {err}" + )) + })? + .into()) + }, + + // AuthControlled + NoAuth: rejected. NoAuth cannot authenticate setters; under + // AuthControlled the auth component is the sole gate, so this would leave every + // authority-gated setter permissionless. + (AccessControl::AuthControlled, AuthMethod::NoAuth) => { + Err(FungibleFaucetError::IncompatibleAuthControlledAuth( + "NoAuth cannot authenticate authority-gated setters".into(), + )) + }, + + // Ownable2Step / Rbac + NetworkAccount: typical network-style faucet. Setter gating + // is enforced in-procedure; the auth component restricts which note scripts can be + // consumed against the faucet. + ( + AccessControl::Ownable2Step { .. } | AccessControl::Rbac { .. }, + AuthMethod::NetworkAccount { allowed_script_roots }, + ) => Ok(AuthNetworkAccount::with_allowlist(allowed_script_roots) + .map_err(|err| { + FungibleFaucetError::UnsupportedAuthMethod(alloc::format!( + "invalid network account allowlist: {err}" + )) + })? + .into()), + + // Ownable2Step / Rbac + NoAuth: valid; the setter gate is the in-procedure owner / + // role check, so the account-level auth can legitimately be NoAuth. + (AccessControl::Ownable2Step { .. } | AccessControl::Rbac { .. }, AuthMethod::NoAuth) => { + Ok(NoAuth::new().into()) + }, + + // Ownable2Step / Rbac + SingleSig: rejected. SingleSig is for user-account faucets + // (AuthControlled); under owner/role-gated faucets it duplicates the setter check + // with a per-tx signature that doesn't add security. + ( + AccessControl::Ownable2Step { .. } | AccessControl::Rbac { .. }, + AuthMethod::SingleSig { .. }, + ) => Err(FungibleFaucetError::UnsupportedAccessControlAuthCombination( + "SingleSig is only supported with AccessControl::AuthControlled; pair \ + Ownable2Step / Rbac with NetworkAccount or NoAuth instead" + .into(), + )), + + // Multisig and Unknown are not supported for any access control variant. + (_, 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 42855d7ce5..3ab8e5294f 100644 --- a/crates/miden-standards/src/account/faucets/fungible/tests.rs +++ b/crates/miden-standards/src/account/faucets/fungible/tests.rs @@ -163,6 +163,33 @@ fn auth_controlled_rejects_no_auth() { assert_matches!(err, FungibleFaucetError::IncompatibleAuthControlledAuth(_)); } +/// `(Ownable2Step / Rbac, SingleSig)` must be rejected: SingleSig is intended for +/// user-account faucets gated by `AuthControlled`; under owner/role-gated faucets it +/// duplicates the setter check with a per-tx signature that doesn't add security. +#[test] +fn ownable2step_rejects_single_sig() { + 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 auth_method = AuthMethod::SingleSig { + approver: (Word::new([Felt::ONE; 4]).into(), AuthScheme::Falcon512Poseidon2), + }; + + let err = create_fungible_faucet( + [7u8; 32], + sample_faucet(), + AccountType::Public, + auth_method, + AccessControl::Ownable2Step { owner }, + allow_all_policy_manager(), + ) + .expect_err("Ownable2Step+SingleSig should be rejected"); + assert_matches!(err, FungibleFaucetError::UnsupportedAccessControlAuthCombination(_)); +} + #[test] fn ownable2step_with_no_auth_is_accepted() { use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE; diff --git a/crates/miden-standards/src/account/faucets/mod.rs b/crates/miden-standards/src/account/faucets/mod.rs index 1c56ef74d3..4e8294ded5 100644 --- a/crates/miden-standards/src/account/faucets/mod.rs +++ b/crates/miden-standards/src/account/faucets/mod.rs @@ -61,6 +61,8 @@ pub enum FungibleFaucetError { UnsupportedAuthMethod(String), #[error("AccessControl::AuthControlled is incompatible with the chosen auth method: {0}")] IncompatibleAuthControlledAuth(String), + #[error("unsupported combination of AccessControl and AuthMethod: {0}")] + UnsupportedAccessControlAuthCombination(String), #[error("account creation failed")] AccountError(#[source] AccountError), #[error("account is not a fungible faucet account")] From f5066efa0c30ecbde9cf44792e1471543d7fc5b8 Mon Sep 17 00:00:00 2001 From: onurinanc Date: Fri, 22 May 2026 09:17:49 +0200 Subject: [PATCH 8/8] fix invalid combination --- .../src/account/faucets/fungible/mod.rs | 27 ++++++++----------- .../src/account/faucets/fungible/tests.rs | 21 +++++++++++++++ 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/crates/miden-standards/src/account/faucets/fungible/mod.rs b/crates/miden-standards/src/account/faucets/fungible/mod.rs index 76fe9cafe6..91f5f1e02e 100644 --- a/crates/miden-standards/src/account/faucets/fungible/mod.rs +++ b/crates/miden-standards/src/account/faucets/fungible/mod.rs @@ -577,17 +577,15 @@ fn all_authority_gated_setter_roots() -> Vec { /// /// - [`AccessControl::AuthControlled`] + [`AuthMethod::SingleSig`] — user-account faucet whose auth /// component is the sole gate for every authority-protected setter. -/// - [`AccessControl::AuthControlled`] + [`AuthMethod::NetworkAccount`] — the caller is responsible -/// for choosing `allowed_script_roots` that prevent unauthorized setter invocations. /// - [`AccessControl::Ownable2Step`] / [`AccessControl::Rbac`] + [`AuthMethod::NetworkAccount`] or /// [`AuthMethod::NoAuth`] — network-style faucet whose setter gate is enforced in-procedure by /// the owner/role check. /// /// All other pairings return a typed error: -/// [`FungibleFaucetError::IncompatibleAuthControlledAuth`] for `AuthControlled + NoAuth` and -/// [`FungibleFaucetError::UnsupportedAccessControlAuthCombination`] for -/// `Ownable2Step`/`Rbac` + `SingleSig`. `Multisig` and `Unknown` remain rejected for every -/// variant via [`FungibleFaucetError::UnsupportedAuthMethod`]. +/// [`FungibleFaucetError::IncompatibleAuthControlledAuth`] for `AuthControlled + NoAuth`, and +/// [`FungibleFaucetError::UnsupportedAccessControlAuthCombination`] for `AuthControlled + +/// NetworkAccount` and for `Ownable2Step`/`Rbac` + `SingleSig`. `Multisig` and `Unknown` +/// remain rejected for every variant via [`FungibleFaucetError::UnsupportedAuthMethod`]. pub fn create_fungible_faucet( init_seed: [u8; 32], faucet: FungibleFaucet, @@ -632,16 +630,13 @@ fn build_auth_component( .map_err(FungibleFaucetError::AccountError)? .into()), - // AuthControlled + NetworkAccount: accepted; allowed_script_roots is the caller's - // responsibility (must not include scripts that can invoke authority-gated setters). - (AccessControl::AuthControlled, AuthMethod::NetworkAccount { allowed_script_roots }) => { - Ok(AuthNetworkAccount::with_allowlist(allowed_script_roots) - .map_err(|err| { - FungibleFaucetError::UnsupportedAuthMethod(alloc::format!( - "invalid network account allowlist: {err}" - )) - })? - .into()) + // AuthControlled + NetworkAccount: rejected. + (AccessControl::AuthControlled, AuthMethod::NetworkAccount { .. }) => { + Err(FungibleFaucetError::UnsupportedAccessControlAuthCombination( + "NetworkAccount is only supported with AccessControl::Ownable2Step or \ + AccessControl::Rbac (network-style faucets)" + .into(), + )) }, // AuthControlled + NoAuth: rejected. NoAuth cannot authenticate setters; under diff --git a/crates/miden-standards/src/account/faucets/fungible/tests.rs b/crates/miden-standards/src/account/faucets/fungible/tests.rs index 3ab8e5294f..07d2714871 100644 --- a/crates/miden-standards/src/account/faucets/fungible/tests.rs +++ b/crates/miden-standards/src/account/faucets/fungible/tests.rs @@ -190,6 +190,27 @@ fn ownable2step_rejects_single_sig() { assert_matches!(err, FungibleFaucetError::UnsupportedAccessControlAuthCombination(_)); } +/// `(AuthControlled, NetworkAccount)` must be rejected: `NetworkAccount` is the auth scheme +/// for network-style faucets, which pair with owner / role-based setter gating +/// (`Ownable2Step` / `Rbac`), not the auth-component-as-gate model of `AuthControlled`. +#[test] +fn auth_controlled_rejects_network_account() { + use alloc::collections::BTreeSet; + + let allowed_script_roots: BTreeSet = BTreeSet::new(); + + let err = create_fungible_faucet( + [7u8; 32], + sample_faucet(), + AccountType::Private, + AuthMethod::NetworkAccount { allowed_script_roots }, + AccessControl::AuthControlled, + allow_all_policy_manager(), + ) + .expect_err("AuthControlled+NetworkAccount should be rejected"); + assert_matches!(err, FungibleFaucetError::UnsupportedAccessControlAuthCombination(_)); +} + #[test] fn ownable2step_with_no_auth_is_accepted() { use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE;