diff --git a/CHANGELOG.md b/CHANGELOG.md index 417f30d9f6..0df16dcd49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,12 @@ - [BREAKING] Refactored `TransferPolicy`, `MintPolicyConfig`, and `BurnPolicyConfig` from enums into structs ([#2974](https://github.com/0xMiden/protocol/pull/2974)). - Added `AccountComponent::has_procedure(root)` helper ([#2974](https://github.com/0xMiden/protocol/pull/2974)). - Optimized protocol MASM stack-cleaning sequences, saving 1 cycle per occurrence across 9 single-element-extraction procedures ([#3041](https://github.com/0xMiden/protocol/pull/3041)). +- [BREAKING] Removed `AuthMethod` enum, `AccountAuthComponent` / `AccountAuthScheme`, and the `AccessControl::AuthControlled` variant. Faucet and wallet factories now take concrete auth-component types so invalid configurations are rejected at compile time ([#2944](https://github.com/0xMiden/protocol/pull/2944)). +- [BREAKING] Split `create_fungible_faucet` into `create_user_fungible_faucet(auth_component: AuthSingleSigAcl, ...)` (installs `Authority::AuthControlled` directly) and the opinionated `create_network_fungible_faucet(access_control, ...)` (always `AccountType::Public`, builds the `AuthNetworkAccount` allowlist internally from `MintNote` + `BurnNote` script roots with an empty tx-script allowlist). Other auth schemes / shapes are no longer supported through these helpers — fall back to `AccountBuilder` directly. A `user_faucet_single_sig_acl` testing helper is provided behind the `testing` feature ([#2944](https://github.com/0xMiden/protocol/pull/2944)). +- Added `create_multisig_wallet` and `create_guarded_wallet` helpers for `BasicWallet` accounts authenticated by `AuthMultisig` and `AuthGuardedMultisig` respectively ([#2944](https://github.com/0xMiden/protocol/pull/2944)). +- [BREAKING] `create_basic_wallet` now takes `AuthSingleSig` directly and returns `AccountError` instead of the removed `BasicWalletError` ([#2944](https://github.com/0xMiden/protocol/pull/2944)). +- [BREAKING] Removed `AccountInterface::auth()` and `AccountComponentInterface::auth_scheme()`. Auth components are now discovered via `AccountInterface::auth_components()`, which iterates `AccountComponentInterface` variants flagged by `is_auth_component()` ([#2944](https://github.com/0xMiden/protocol/pull/2944)). +- [BREAKING] `FungibleFaucet` no longer installs the `is_paused` storage slot itself. Faucet factories (`create_user_fungible_faucet` / `create_network_fungible_faucet`) now bundle the `Pausable` component (slot + `is_paused()` view procedure) alongside `PausableManager`. Callers using `AccountBuilder` directly must also install `Pausable` or the faucet's mint / burn / transfer / metadata-setter procedures will panic at runtime ([#2944](https://github.com/0xMiden/protocol/pull/2944)). - [BREAKING] Refactored `TokenPolicyManager` by adding `invoke_send_policy` / `invoke_receive_policy` wrappers (stored in the protocol reserved asset callback slots) that read the active policy root from the new `active_send_policy_proc_root` / `active_receive_policy_proc_root` storage slots ([#3047](https://github.com/0xMiden/protocol/pull/3047)). - Added a definition of the Miden operator on the architecture overview page and linked it from the note lifecycle ([#3017](https://github.com/0xMiden/protocol/pull/3017)). - Clarified Miden's operational roles on the architecture overview page and linked them from the note lifecycle ([#3017](https://github.com/0xMiden/protocol/pull/3017)). diff --git a/crates/miden-standards/asm/account_components/access/pausable/manager.masm b/crates/miden-standards/asm/account_components/access/pausable/manager.masm index b7e74886f0..ed3d462b92 100644 --- a/crates/miden-standards/asm/account_components/access/pausable/manager.masm +++ b/crates/miden-standards/asm/account_components/access/pausable/manager.masm @@ -4,8 +4,8 @@ # `Authority` component via `exec.authority::assert_authorized`. # # Companion components required: -# - `Authority` (installed via `AccessControl::Ownable2Step` / `AccessControl::Rbac` / -# `AccessControl::AuthControlled`). +# - `Authority` (installed via `AccessControl::Ownable2Step` / `AccessControl::Rbac`, or +# `Authority::AuthControlled` directly by `create_user_fungible_faucet`). # - `Pausable` — provides the `is_paused` storage slot. pub use ::miden::standards::access::pausable::manager::pause diff --git a/crates/miden-standards/src/account/access/authority.rs b/crates/miden-standards/src/account/access/authority.rs index a58c9d874b..e7c75a4f79 100644 --- a/crates/miden-standards/src/account/access/authority.rs +++ b/crates/miden-standards/src/account/access/authority.rs @@ -58,19 +58,13 @@ const RBAC_CONTROLLED: u8 = 2; #[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 is the account's auth component. AuthControlled = AUTH_CONTROLLED, - /// Authority is the [`Ownable2Step`][crate::account::access::Ownable2Step] owner; the call - /// must be sent by the registered owner. + /// Authority is the [`Ownable2Step`][crate::account::access::Ownable2Step] 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 599cd9fbd2..0726a37bde 100644 --- a/crates/miden-standards/src/account/access/mod.rs +++ b/crates/miden-standards/src/account/access/mod.rs @@ -7,52 +7,33 @@ pub mod ownable2step; pub mod pausable; pub mod rbac; -/// Access control configuration for account components. +/// Access control configuration for network-style accounts whose authority-gated setters are +/// gated by an owner / role check rather than by the account's auth component. /// -/// 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. +/// User-account faucets (where the auth component is itself the setter gate) install +/// [`Authority::AuthControlled`] directly via factories like +/// [`create_user_fungible_faucet`][crate::account::faucets::create_user_fungible_faucet]; they +/// do not need this enum. /// -/// - [`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: +/// - [`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: -/// -/// ```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 - /// 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. + /// Two-step ownership transfer with the provided initial owner. The setter gate enforces + /// `sender == 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: /// - `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/revoked but they do not directly gate the + /// RBAC roles can still be granted and 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 @@ -72,7 +53,6 @@ impl IntoIterator for AccessControl { /// always included. fn into_iter(self) -> Self::IntoIter { match self { - AccessControl::AuthControlled => vec![Authority::AuthControlled.into()].into_iter(), AccessControl::Ownable2Step { owner } => { vec![Ownable2Step::new(owner).into(), Authority::OwnerControlled.into()].into_iter() }, diff --git a/crates/miden-standards/src/account/access/pausable/manager.rs b/crates/miden-standards/src/account/access/pausable/manager.rs index ee3ee06c2c..b01c7e2bba 100644 --- a/crates/miden-standards/src/account/access/pausable/manager.rs +++ b/crates/miden-standards/src/account/access/pausable/manager.rs @@ -27,9 +27,9 @@ procedure_root!( /// [`crate::account::access::Authority`] component via `exec.authority::assert_authorized`. /// /// `PausableManager` works uniformly with every standard access scheme: -/// - [`crate::account::access::AccessControl::AuthControlled`] → -/// [`crate::account::access::Authority::AuthControlled`] gates pause / unpause via the account's -/// own auth component. +/// - [`crate::account::access::Authority::AuthControlled`] — installed directly by +/// [`crate::account::faucets::create_user_fungible_faucet`]; gates pause / unpause via the +/// account's own auth component. /// - [`crate::account::access::AccessControl::Ownable2Step`] → /// [`crate::account::access::Authority::OwnerControlled`] requires the Ownable2Step owner. /// - [`crate::account::access::AccessControl::Rbac`] → @@ -39,7 +39,7 @@ procedure_root!( /// /// Companion components required: /// - [`crate::account::access::Authority`] — installed automatically by the -/// [`crate::account::access::AccessControl`] enum. +/// [`crate::account::access::AccessControl`] enum (or directly by user-faucet factories). /// - [`super::Pausable`] — provides the `is_paused` storage slot that pause / unpause write to. #[derive(Debug, Clone, Copy, Default)] pub struct PausableManager; diff --git a/crates/miden-standards/src/account/faucets/fungible/mod.rs b/crates/miden-standards/src/account/faucets/fungible/mod.rs index 671c737b37..ea9fd84809 100644 --- a/crates/miden-standards/src/account/faucets/fungible/mod.rs +++ b/crates/miden-standards/src/account/faucets/fungible/mod.rs @@ -33,11 +33,12 @@ use super::{ TokenMetadataError, TokenName, }; -use crate::account::access::{AccessControl, PausableManager}; +use crate::account::access::{AccessControl, Authority, Pausable, PausableManager}; use crate::account::account_component_code; -use crate::account::auth::{AuthNetworkAccount, AuthSingleSigAcl, AuthSingleSigAclConfig, NoAuth}; +use crate::account::auth::{AuthNetworkAccount, AuthSingleSigAcl}; use crate::account::policies::TokenPolicyManager; -use crate::{AuthMethod, procedure_root}; +use crate::note::{BurnNote, MintNote}; +use crate::procedure_root; #[cfg(test)] mod tests; @@ -283,22 +284,25 @@ impl FungibleFaucet { *FUNGIBLE_FAUCET_RECEIVE_AND_BURN } - /// Returns the procedure root of the `set_max_supply` account procedure. + /// Returns the procedure root of the `set_max_supply` account procedure. This is an + /// authority-gated setter; when paired with `Authority::AuthControlled` (via + /// [`create_user_fungible_faucet`]) 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. + /// 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. + /// 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. + /// 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 } @@ -382,19 +386,11 @@ impl FungibleFaucet { } /// Returns the storage slots produced by this faucet (token config word + name + mutability - /// config + description + logo URI + external link + Pausable's `is_paused` flag). - /// - /// The `is_paused` slot is installed by FungibleFaucet itself (initial value: unpaused, zero - /// word) so that the transversal pause guards baked into `execute_mint_policy`, - /// `execute_burn_policy`, `check_policy` (allow_all / blocklist / allowlist) and the metadata - /// setters can read it without panicking. Pause / unpause administration is exposed by the - /// [`crate::account::access::pausable::PausableManager`] component, which is bundled by - /// [`create_fungible_faucet`] alongside this faucet so the slot is always actionable. + /// config + description + logo URI + external link). pub fn into_storage_slots(self) -> Vec { let mut slots: Vec = Vec::new(); slots.push(self.token_config_slot_value()); slots.extend(self.metadata.into_storage_slots()); - slots.push(crate::account::access::pausable::PausableStorage::default().into_slot()); slots } @@ -556,152 +552,63 @@ impl TryFrom<&Account> for FungibleFaucet { // FACTORY // ================================================================================================ -/// 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(), - 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(), - PausableManager::pause_root(), - PausableManager::unpause_root(), - ] +/// Creates a new **user-account** fungible faucet. The account's auth component is the sole +/// gate for authority-protected setters ([`Authority::AuthControlled`] is installed directly). +/// +/// Caller passes a fully-configured [`AuthSingleSigAcl`] — its trigger procedure list must +/// cover every authority-gated setter on the faucet (`mint_and_send`, the metadata setters, +/// the policy setters, and `pause` / `unpause`), otherwise those procedures become +/// permissionless under [`Authority::AuthControlled`]. +pub fn create_user_fungible_faucet( + init_seed: [u8; 32], + faucet: FungibleFaucet, + auth_component: AuthSingleSigAcl, + token_policy_manager: TokenPolicyManager, + account_type: AccountType, +) -> Result { + AccountBuilder::new(init_seed) + .account_type(account_type) + .with_auth_component(auth_component) + .with_component(faucet) + .with_component(Authority::AuthControlled) + .with_components(token_policy_manager) + .with_component(Pausable::unpaused()) + .with_component(PausableManager) + .build() + .map_err(FungibleFaucetError::AccountError) } -/// Creates a new fungible faucet account by composing the required components. +/// Creates a new **network-style** fungible faucet. The account is always +/// [`AccountType::Public`] (network accounts cannot be private). Setter gating is enforced +/// in-procedure by the owner / role check installed via `access_control` +/// ([`AccessControl::Ownable2Step`] or [`AccessControl::Rbac`]). /// -/// In addition to the explicit parameters, [`PausableManager`] is always bundled so the -/// `is_paused` slot installed by [`FungibleFaucet::into_storage_slots`] is actionable via -/// `pause` / `unpause` admin procedures (gated by the same `Authority` component installed by -/// `access_control`). +/// The factory builds the [`AuthNetworkAccount`] auth component internally with a note +/// allowlist covering the faucet's own [`MintNote`] and [`BurnNote`] scripts and an empty +/// tx-script allowlist (network faucets are consumed via notes, not tx scripts). Callers +/// that need a custom allowlist (additional note scripts or tx scripts) should use +/// [`AccountBuilder`] directly. /// -/// Only specific `(access_control, auth_method)` combinations are supported; everything else -/// is rejected at the factory level. The valid combinations are: -/// -/// - [`AccessControl::AuthControlled`] + [`AuthMethod::SingleSig`] — user-account faucet whose auth -/// component is the sole gate for every authority-protected setter. -/// - [`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 `AuthControlled + -/// NetworkAccount` and for `Ownable2Step`/`Rbac` + `SingleSig`. `Multisig` and `Unknown` -/// remain rejected for every variant via [`FungibleFaucetError::UnsupportedAuthMethod`]. -pub fn create_fungible_faucet( +/// In addition to the explicit parameters, [`Pausable`] (slot + `is_paused` view) and +/// [`PausableManager`] (admin `pause` / `unpause` gated by `access_control`) are bundled. +pub fn create_network_fungible_faucet( init_seed: [u8; 32], faucet: FungibleFaucet, - account_type: AccountType, - auth_method: AuthMethod, access_control: AccessControl, token_policy_manager: TokenPolicyManager, ) -> Result { - let auth_component = build_auth_component(&access_control, auth_method)?; + let note_allowlist = [MintNote::script_root(), BurnNote::script_root()].into_iter().collect(); + let auth_component = AuthNetworkAccount::with_allowed_notes(note_allowlist) + .expect("MintNote + BurnNote allowlist is non-empty"); - let account = AccountBuilder::new(init_seed) - .account_type(account_type) + AccountBuilder::new(init_seed) + .account_type(AccountType::Public) .with_auth_component(auth_component) .with_component(faucet) .with_components(access_control) .with_components(token_policy_manager) + .with_component(Pausable::unpaused()) .with_component(PausableManager) .build() - .map_err(FungibleFaucetError::AccountError)?; - - 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: 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 - // 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, - allowed_tx_script_roots, - }, - ) => Ok(AuthNetworkAccount::with_allowed_notes(allowed_script_roots) - .map_err(|err| { - FungibleFaucetError::UnsupportedAuthMethod(alloc::format!( - "invalid network account allowlist: {err}" - )) - })? - .with_allowed_tx_scripts(allowed_tx_script_roots) - .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(), - )), - } + .map_err(FungibleFaucetError::AccountError) } diff --git a/crates/miden-standards/src/account/faucets/fungible/tests.rs b/crates/miden-standards/src/account/faucets/fungible/tests.rs index c7d1e155d6..41f0882485 100644 --- a/crates/miden-standards/src/account/faucets/fungible/tests.rs +++ b/crates/miden-standards/src/account/faucets/fungible/tests.rs @@ -6,13 +6,13 @@ use miden_protocol::account::{AccountBuilder, AccountType}; use miden_protocol::asset::{AssetAmount, TokenSymbol}; use miden_protocol::{Felt, Word}; -use super::{FungibleFaucet, create_fungible_faucet}; -use crate::AuthMethod; +use super::{FungibleFaucet, create_network_fungible_faucet, create_user_fungible_faucet}; use crate::account::access::{AccessControl, PausableManager}; use crate::account::auth::{AuthSingleSig, AuthSingleSigAcl}; use crate::account::faucets::{Description, FungibleFaucetError, TokenMetadata, TokenName}; use crate::account::policies::{BurnPolicy, MintPolicy, TokenPolicyManager, TransferPolicy}; use crate::account::wallets::BasicWallet; +use crate::testing::faucet::user_faucet_single_sig_acl; /// Builds a minimal policy manager with AllowAll on every kind, used by the construction tests. fn allow_all_policy_manager() -> TokenPolicyManager { @@ -55,13 +55,8 @@ fn read_trigger_procedure_roots( } #[test] -fn faucet_contract_creation() { +fn user_fungible_faucet_with_single_sig_acl() { let pub_key_word = Word::new([Felt::ONE; 4]); - let auth_method = AuthMethod::SingleSig { - approver: (pub_key_word.into(), AuthScheme::Falcon512Poseidon2), - }; - - // we need to use an initial seed to create the wallet account let init_seed: [u8; 32] = [ 90, 110, 209, 94, 84, 105, 250, 242, 223, 203, 216, 124, 22, 159, 14, 132, 215, 85, 183, 204, 149, 90, 166, 68, 100, 73, 106, 168, 125, 237, 138, 16, @@ -72,35 +67,31 @@ fn faucet_contract_creation() { let token_name_string = "polygon"; let description_string = "A polygon token"; - let faucet = sample_faucet(); - let faucet_account = create_fungible_faucet( + let auth_component = + user_faucet_single_sig_acl(pub_key_word.into(), AuthScheme::Falcon512Poseidon2).unwrap(); + + let faucet_account = create_user_fungible_faucet( init_seed, - faucet, - AccountType::Private, - auth_method, - AccessControl::AuthControlled, + sample_faucet(), + auth_component, allow_all_policy_manager(), + AccountType::Private, ) .unwrap(); - // The falcon auth component's public key should be present. + // The auth component's public key should be present. assert_eq!( faucet_account.storage().get_item(AuthSingleSigAcl::public_key_slot()).unwrap(), pub_key_word ); - // The config slot of the auth component stores: - // [num_trigger_procs, allow_unauthorized_output_notes, allow_unauthorized_input_notes, 0]. - // - // With 11 authority-gated trigger procedures (mint_and_send + 4 token metadata setters + - // 4 policy setters + pause + unpause), allow_unauthorized_output_notes=false, and - // allow_unauthorized_input_notes=true, this should be [11, 0, 1, 0]. + // Config slot: 11 trigger procedures (mint_and_send + 4 token metadata setters + 4 policy + // setters + pause + unpause), allow_unauthorized_input_notes=true → [11, 0, 1, 0]. assert_eq!( faucet_account.storage().get_item(AuthSingleSigAcl::config_slot()).unwrap(), [Felt::from(11_u32), Felt::ZERO, Felt::ONE, Felt::ZERO].into() ); - // The trigger procedure root map should contain every authority-gated setter root. let stored_roots = read_trigger_procedure_roots(&faucet_account, 11); let expected_roots: BTreeSet = [ FungibleFaucet::mint_and_send_root(), @@ -120,14 +111,12 @@ fn faucet_contract_creation() { .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] + // Token config slot layout: [token_supply, max_supply, decimals, symbol] assert_eq!( faucet_account.storage().get_item(FungibleFaucet::token_config_slot()).unwrap(), [Felt::ZERO, Felt::from(123_u32), Felt::from(2_u32), token_symbol.into()].into() ); - // Check that name was stored let name_0 = faucet_account.storage().get_item(TokenMetadata::name_chunk_0_slot()).unwrap(); let name_1 = faucet_account.storage().get_item(TokenMetadata::name_chunk_1_slot()).unwrap(); let decoded_name = TokenName::try_from_words(&[name_0, name_1]).unwrap(); @@ -138,77 +127,14 @@ fn faucet_contract_creation() { assert_eq!(chunk, *expected); } - // Verify the faucet component can be extracted let _faucet_component = FungibleFaucet::try_from(faucet_account.clone()).unwrap(); } +/// `create_network_fungible_faucet` with `Ownable2Step` builds a valid account. The factory +/// constructs `AuthNetworkAccount` internally; the setter gate is enforced in-procedure by +/// `assert_sender_is_owner`. #[test] -fn auth_controlled_rejects_no_auth() { - let err = create_fungible_faucet( - [7u8; 32], - sample_faucet(), - AccountType::Private, - AuthMethod::NoAuth, - AccessControl::AuthControlled, - allow_all_policy_manager(), - ) - .expect_err("AuthControlled+NoAuth should be rejected"); - 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(_)); -} - -/// `(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, - allowed_tx_script_roots: BTreeSet::new(), - }, - 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() { +fn network_fungible_faucet_with_ownable2step() { use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_UPDATABLE_CODE; let owner = miden_protocol::account::AccountId::try_from( @@ -216,25 +142,21 @@ fn ownable2step_with_no_auth_is_accepted() { ) .unwrap(); - let _account = create_fungible_faucet( + let _account = create_network_fungible_faucet( [7u8; 32], sample_faucet(), - AccountType::Public, - AuthMethod::NoAuth, AccessControl::Ownable2Step { owner }, allow_all_policy_manager(), ) - .expect("Ownable2Step+NoAuth should be accepted"); + .expect("Ownable2Step network faucet should be accepted"); } #[test] fn faucet_create_from_account() { - // prepare the test data let mock_word = Word::from([0, 1, 2, 3u32]); let mock_public_key = PublicKeyCommitment::from(mock_word); let mock_seed = mock_word.as_bytes(); - // valid account let token_symbol = TokenSymbol::new("POL").expect("invalid token symbol"); let faucet = FungibleFaucet::builder() .name(TokenName::new("POL").unwrap()) @@ -256,7 +178,6 @@ fn faucet_create_from_account() { // invalid account: fungible faucet component is missing let invalid_faucet_account = AccountBuilder::new(mock_seed) .with_auth_component(AuthSingleSig::new(mock_public_key, AuthScheme::Falcon512Poseidon2)) - // we need to add some other component so the builder doesn't fail .with_component(BasicWallet) .build_existing() .expect("failed to create wallet account"); diff --git a/crates/miden-standards/src/account/faucets/mod.rs b/crates/miden-standards/src/account/faucets/mod.rs index 4e8294ded5..d2e3aa7b1d 100644 --- a/crates/miden-standards/src/account/faucets/mod.rs +++ b/crates/miden-standards/src/account/faucets/mod.rs @@ -1,5 +1,3 @@ -use alloc::string::String; - use miden_protocol::account::StorageSlotName; use miden_protocol::errors::{AccountError, TokenSymbolError}; use thiserror::Error; @@ -10,7 +8,12 @@ use crate::utils::FixedWidthStringError; mod fungible; mod token_metadata; -pub use fungible::{FungibleFaucet, FungibleFaucetBuilder, create_fungible_faucet}; +pub use fungible::{ + FungibleFaucet, + FungibleFaucetBuilder, + create_network_fungible_faucet, + create_user_fungible_faucet, +}; pub use token_metadata::{Description, ExternalLink, LogoURI, TokenMetadata, TokenName}; // TOKEN METADATA ERROR @@ -57,12 +60,6 @@ pub enum FungibleFaucetError { "account interface does not have the procedures of the basic fungible faucet component" )] MissingFungibleFaucetInterface, - #[error("unsupported authentication method: {0}")] - 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")] diff --git a/crates/miden-standards/src/account/interface/component.rs b/crates/miden-standards/src/account/interface/component.rs index b7753a7033..18b8179219 100644 --- a/crates/miden-standards/src/account/interface/component.rs +++ b/crates/miden-standards/src/account/interface/component.rs @@ -1,20 +1,7 @@ use alloc::string::{String, ToString}; use alloc::vec::Vec; -use miden_protocol::Word; -use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment}; -use miden_protocol::account::{AccountProcedureRoot, AccountStorage, StorageSlotName}; - -use crate::AuthMethod; -use crate::account::auth::{ - AuthGuardedMultisig, - AuthMultisig, - AuthMultisigSmart, - AuthSingleSig, - AuthSingleSigAcl, - NetworkAccountNoteAllowlist, - NetworkAccountTxScriptAllowlist, -}; +use miden_protocol::account::AccountProcedureRoot; // ACCOUNT COMPONENT INTERFACE // ================================================================================================ @@ -119,141 +106,4 @@ impl AccountComponentInterface { | AccountComponentInterface::AuthNetworkAccount ) } - - /// Returns the authentication schemes associated with this component interface. - pub fn get_auth_methods(&self, storage: &AccountStorage) -> Vec { - match self { - AccountComponentInterface::AuthSingleSig => vec![extract_singlesig_auth_method( - storage, - AuthSingleSig::public_key_slot(), - AuthSingleSig::scheme_id_slot(), - )], - AccountComponentInterface::AuthSingleSigAcl => vec![extract_singlesig_auth_method( - storage, - AuthSingleSigAcl::public_key_slot(), - AuthSingleSigAcl::scheme_id_slot(), - )], - AccountComponentInterface::AuthMultisig => { - vec![extract_multisig_auth_method( - storage, - AuthMultisig::threshold_config_slot(), - AuthMultisig::approver_public_keys_slot(), - AuthMultisig::approver_scheme_ids_slot(), - )] - }, - AccountComponentInterface::AuthGuardedMultisig => { - vec![extract_multisig_auth_method( - storage, - AuthGuardedMultisig::threshold_config_slot(), - AuthGuardedMultisig::approver_public_keys_slot(), - AuthGuardedMultisig::approver_scheme_ids_slot(), - )] - }, - AccountComponentInterface::AuthMultisigSmart => { - vec![extract_multisig_auth_method( - storage, - AuthMultisigSmart::threshold_config_slot(), - AuthMultisigSmart::approver_public_keys_slot(), - AuthMultisigSmart::approver_scheme_ids_slot(), - )] - }, - AccountComponentInterface::AuthNoAuth => vec![AuthMethod::NoAuth], - AccountComponentInterface::AuthNetworkAccount => { - vec![extract_network_account_auth_method(storage)] - }, - _ => vec![], // Non-auth components return empty vector - } - } -} - -// HELPER FUNCTIONS -// ================================================================================================ - -/// Extracts authentication method from a single-signature component. -fn extract_singlesig_auth_method( - storage: &AccountStorage, - public_key_slot: &StorageSlotName, - scheme_id_slot: &StorageSlotName, -) -> AuthMethod { - let pub_key = PublicKeyCommitment::from( - storage - .get_item(public_key_slot) - .expect("invalid storage index of the public key"), - ); - - let scheme_id = storage - .get_item(scheme_id_slot) - .expect("invalid storage index of the scheme id")[0] - .as_canonical_u64() as u8; - - let auth_scheme = - AuthScheme::try_from(scheme_id).expect("invalid auth scheme id in the scheme id slot"); - - AuthMethod::SingleSig { approver: (pub_key, auth_scheme) } -} - -/// Extracts authentication method from a multisig component. -fn extract_multisig_auth_method( - storage: &AccountStorage, - config_slot: &StorageSlotName, - approver_public_keys_slot: &StorageSlotName, - approver_scheme_ids_slot: &StorageSlotName, -) -> AuthMethod { - // Read the multisig configuration from the config slot - // Format: [threshold, num_approvers, 0, 0] - let config = storage - .get_item(config_slot) - .expect("invalid slot name of the multisig configuration"); - - let threshold = config[0].as_canonical_u64() as u32; - let num_approvers = config[1].as_canonical_u64() as u8; - - let mut approvers = Vec::new(); - - // Read each public key from the map - for key_index in 0..num_approvers { - // The multisig component stores keys and scheme IDs using pattern [index, 0, 0, 0] - let map_key = Word::from([key_index as u32, 0, 0, 0]); - - let pub_key_word = - storage.get_map_item(approver_public_keys_slot, map_key).unwrap_or_else(|_| { - panic!( - "Failed to read public key {} from multisig configuration at storage slot {}. \ - Expected key pattern [index, 0, 0, 0].", - key_index, approver_public_keys_slot - ) - }); - - let pub_key = PublicKeyCommitment::from(pub_key_word); - - let scheme_word = storage - .get_map_item(approver_scheme_ids_slot, map_key) - .unwrap_or_else(|_| { - panic!( - "Failed to read scheme id for approver {} from multisig configuration at storage slot {}. \ - Expected key pattern [index, 0, 0, 0].", - key_index, approver_scheme_ids_slot - ) - }); - - let scheme_id = scheme_word[0].as_canonical_u64() as u8; - let auth_scheme = - AuthScheme::try_from(scheme_id).expect("invalid auth scheme id in the scheme id slot"); - approvers.push((pub_key, auth_scheme)); - } - - AuthMethod::Multisig { threshold, approvers } -} - -/// Extracts authentication method from a network-account component. -fn extract_network_account_auth_method(storage: &AccountStorage) -> AuthMethod { - let allowlist = NetworkAccountNoteAllowlist::try_from(storage) - .expect("network account note allowlist slot should be present and valid"); - let tx_script_allowlist = NetworkAccountTxScriptAllowlist::try_from(storage) - .expect("network account tx script allowlist slot should be present and valid"); - - AuthMethod::NetworkAccount { - allowed_script_roots: allowlist.into_allowed_script_roots(), - allowed_tx_script_roots: tx_script_allowlist.into_allowed_script_roots(), - } } diff --git a/crates/miden-standards/src/account/interface/extension.rs b/crates/miden-standards/src/account/interface/extension.rs index 0d3c04b734..aab4ea694e 100644 --- a/crates/miden-standards/src/account/interface/extension.rs +++ b/crates/miden-standards/src/account/interface/extension.rs @@ -3,7 +3,6 @@ use alloc::vec::Vec; use miden_protocol::account::{Account, AccountCode, AccountId, AccountProcedureRoot}; -use crate::AuthMethod; use crate::account::components::StandardAccountComponent; use crate::account::interface::{AccountComponentInterface, AccountInterface}; @@ -12,35 +11,22 @@ use crate::account::interface::{AccountComponentInterface, AccountInterface}; /// An extension for [`AccountInterface`] that allows instantiation from higher-level types. pub trait AccountInterfaceExt { - /// Creates a new [`AccountInterface`] instance from the provided account ID, authentication - /// methods and account code. - fn from_code(account_id: AccountId, auth: Vec, code: &AccountCode) -> Self; + /// Creates a new [`AccountInterface`] instance from the provided account ID and account code. + fn from_code(account_id: AccountId, code: &AccountCode) -> Self; /// Creates a new [`AccountInterface`] instance from the provided [`Account`]. fn from_account(account: &Account) -> Self; } impl AccountInterfaceExt for AccountInterface { - fn from_code(account_id: AccountId, auth: Vec, code: &AccountCode) -> Self { + fn from_code(account_id: AccountId, code: &AccountCode) -> Self { let components = AccountComponentInterface::from_procedures(code.procedures()); - - Self::new(account_id, auth, components) + Self::new(account_id, components) } fn from_account(account: &Account) -> Self { let components = AccountComponentInterface::from_procedures(account.code().procedures()); - let mut auth = Vec::new(); - - // Find the auth component and extract all auth methods from it - // An account should have only one auth component - for component in components.iter() { - if component.is_auth_component() { - auth = component.get_auth_methods(account.storage()); - break; - } - } - - Self::new(account.id(), auth, components) + Self::new(account.id(), components) } } diff --git a/crates/miden-standards/src/account/interface/mod.rs b/crates/miden-standards/src/account/interface/mod.rs index 2564b126a7..f8f7f00f02 100644 --- a/crates/miden-standards/src/account/interface/mod.rs +++ b/crates/miden-standards/src/account/interface/mod.rs @@ -2,8 +2,6 @@ use alloc::vec::Vec; use miden_protocol::account::AccountId; -use crate::AuthMethod; - #[cfg(test)] mod test; @@ -19,7 +17,6 @@ pub use extension::{AccountComponentInterfaceExt, AccountInterfaceExt}; /// An [`AccountInterface`] describes the exported, callable procedures of an account. pub struct AccountInterface { account_id: AccountId, - auth: Vec, components: Vec, } @@ -29,14 +26,10 @@ impl AccountInterface { // CONSTRUCTORS // -------------------------------------------------------------------------------------------- - /// Creates a new [`AccountInterface`] instance from the provided account ID, authentication - /// schemes and account component interfaces. - pub fn new( - account_id: AccountId, - auth: Vec, - components: Vec, - ) -> Self { - Self { account_id, auth, components } + /// Creates a new [`AccountInterface`] instance from the provided account ID and account + /// component interfaces. + pub fn new(account_id: AccountId, components: Vec) -> Self { + Self { account_id, components } } // PUBLIC ACCESSORS @@ -57,13 +50,13 @@ impl AccountInterface { self.account_id.is_public() } - /// Returns a reference to the vector of used authentication methods. - pub fn auth(&self) -> &Vec { - &self.auth - } - /// Returns a reference to the set of used component interfaces. pub fn components(&self) -> &Vec { &self.components } + + /// Returns an iterator over the auth components installed on this account. + pub fn auth_components(&self) -> impl Iterator { + self.components.iter().filter(|c| c.is_auth_component()) + } } diff --git a/crates/miden-standards/src/account/interface/test.rs b/crates/miden-standards/src/account/interface/test.rs index 0ed4606b1f..0a3c3f5dab 100644 --- a/crates/miden-standards/src/account/interface/test.rs +++ b/crates/miden-standards/src/account/interface/test.rs @@ -8,7 +8,6 @@ use miden_protocol::errors::NoteError; use miden_protocol::note::{NoteAttachments, NoteType}; use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE; -use crate::AuthMethod; use crate::account::auth::{AuthMultisig, AuthMultisigConfig, AuthSingleSig, NoAuth}; use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; use crate::account::wallets::BasicWallet; @@ -38,56 +37,17 @@ fn test_required_asset_same_as_offered() { // HELPERS // ================================================================================================ -/// Helper function to create a mock auth component for testing fn get_mock_falcon_auth_component() -> AuthSingleSig { let mock_word = Word::from([0, 1, 2, 3u32]); let mock_public_key = PublicKeyCommitment::from(mock_word); AuthSingleSig::new(mock_public_key, auth::AuthScheme::Falcon512Poseidon2) } -/// Helper function to create a mock Ecdsa auth component for testing -fn get_mock_ecdsa_auth_component() -> AuthSingleSig { - let mock_word = Word::from([0, 1, 2, 3u32]); - let mock_public_key = PublicKeyCommitment::from(mock_word); - AuthSingleSig::new(mock_public_key, auth::AuthScheme::EcdsaK256Keccak) -} - -// GET AUTH SCHEME TESTS +// AUTH COMPONENT IDENTIFICATION TESTS // ================================================================================================ #[test] -fn test_get_auth_scheme_ecdsa_k256_keccak() { - let mock_seed = Word::from([0, 1, 2, 3u32]).as_bytes(); - let wallet_account = AccountBuilder::new(mock_seed) - .with_auth_component(get_mock_ecdsa_auth_component()) - .with_component(BasicWallet) - .build_existing() - .expect("failed to create wallet account"); - - let wallet_account_interface = AccountInterface::from_account(&wallet_account); - - // Find the EcdsaK256Keccak component interface - let ecdsa_k256_keccak_component = wallet_account_interface - .components() - .iter() - .find(|component| matches!(component, AccountComponentInterface::AuthSingleSig)) - .expect("should have EcdsaK256Keccak component"); - - // Test get_auth_methods method - let auth_methods = ecdsa_k256_keccak_component.get_auth_methods(wallet_account.storage()); - assert_eq!(auth_methods.len(), 1); - let auth_method = &auth_methods[0]; - match auth_method { - AuthMethod::SingleSig { approver: (pub_key, auth_scheme) } => { - assert_eq!(*pub_key, PublicKeyCommitment::from(Word::from([0, 1, 2, 3u32]))); - assert_eq!(*auth_scheme, auth::AuthScheme::EcdsaK256Keccak); - }, - _ => panic!("Expected EcdsaK256Keccak auth scheme"), - } -} - -#[test] -fn test_get_auth_scheme_falcon512_poseidon2() { +fn test_account_interface_identifies_single_sig_auth() { let mock_seed = Word::from([0, 1, 2, 3u32]).as_bytes(); let wallet_account = AccountBuilder::new(mock_seed) .with_auth_component(get_mock_falcon_auth_component()) @@ -97,94 +57,14 @@ fn test_get_auth_scheme_falcon512_poseidon2() { let wallet_account_interface = AccountInterface::from_account(&wallet_account); - // Find the single sig component interface - let rpo_falcon_component = wallet_account_interface - .components() - .iter() - .find(|component| matches!(component, AccountComponentInterface::AuthSingleSig)) - .expect("should have single sig component"); - - // Test get_auth_methods method - let auth_methods = rpo_falcon_component.get_auth_methods(wallet_account.storage()); - assert_eq!(auth_methods.len(), 1); - let auth_method = &auth_methods[0]; - match auth_method { - AuthMethod::SingleSig { approver: (pub_key, auth_scheme) } => { - assert_eq!(*pub_key, PublicKeyCommitment::from(Word::from([0, 1, 2, 3u32]))); - assert_eq!(*auth_scheme, auth::AuthScheme::Falcon512Poseidon2); - }, - _ => panic!("Expected Falcon512Poseidon2 auth scheme"), - } -} - -#[test] -fn test_get_auth_scheme_no_auth() { - let mock_seed = Word::from([0, 1, 2, 3u32]).as_bytes(); - let no_auth_account = AccountBuilder::new(mock_seed) - .with_auth_component(NoAuth) - .with_component(BasicWallet) - .build_existing() - .expect("failed to create no-auth account"); - - let no_auth_account_interface = AccountInterface::from_account(&no_auth_account); - - // Find the NoAuth component interface - let no_auth_component = no_auth_account_interface - .components() - .iter() - .find(|component| matches!(component, AccountComponentInterface::AuthNoAuth)) - .expect("should have NoAuth component"); - - // Test get_auth_methods method - let auth_methods = no_auth_component.get_auth_methods(no_auth_account.storage()); - assert_eq!(auth_methods.len(), 1); - let auth_method = &auth_methods[0]; - match auth_method { - AuthMethod::NoAuth => {}, - _ => panic!("Expected NoAuth auth method"), - } -} - -/// Test that non-auth components return None -#[test] -fn test_get_auth_scheme_non_auth_component() { - let basic_wallet_component = AccountComponentInterface::BasicWallet; - let mock_seed = Word::from([0, 1, 2, 3u32]).as_bytes(); - let wallet_account = AccountBuilder::new(mock_seed) - .with_auth_component(get_mock_falcon_auth_component()) - .with_component(BasicWallet) - .build_existing() - .expect("failed to create wallet account"); - - let auth_methods = basic_wallet_component.get_auth_methods(wallet_account.storage()); - assert!(auth_methods.is_empty()); + let auth_components: alloc::vec::Vec<_> = wallet_account_interface.auth_components().collect(); + assert_eq!(auth_components.len(), 1); + assert!(matches!(auth_components[0], AccountComponentInterface::AuthSingleSig)); } -/// Test that the From<&Account> implementation correctly uses get_auth_scheme #[test] -fn test_account_interface_from_account_uses_get_auth_scheme() { +fn test_account_interface_identifies_no_auth() { let mock_seed = Word::from([0, 1, 2, 3u32]).as_bytes(); - let wallet_account = AccountBuilder::new(mock_seed) - .with_auth_component(get_mock_falcon_auth_component()) - .with_component(BasicWallet) - .build_existing() - .expect("failed to create wallet account"); - - let wallet_account_interface = AccountInterface::from_account(&wallet_account); - - // Should have exactly one auth scheme - assert_eq!(wallet_account_interface.auth().len(), 1); - - match &wallet_account_interface.auth()[0] { - AuthMethod::SingleSig { approver: (pub_key, auth_scheme) } => { - let expected_pub_key = PublicKeyCommitment::from(Word::from([0, 1, 2, 3u32])); - assert_eq!(*pub_key, expected_pub_key); - assert_eq!(*auth_scheme, auth::AuthScheme::Falcon512Poseidon2); - }, - _ => panic!("Expected SingleSig auth method"), - } - - // Test with NoAuth let no_auth_account = AccountBuilder::new(mock_seed) .with_auth_component(NoAuth) .with_component(BasicWallet) @@ -193,55 +73,16 @@ fn test_account_interface_from_account_uses_get_auth_scheme() { let no_auth_account_interface = AccountInterface::from_account(&no_auth_account); - // Should have exactly one auth scheme - assert_eq!(no_auth_account_interface.auth().len(), 1); - - match &no_auth_account_interface.auth()[0] { - AuthMethod::NoAuth => {}, - _ => panic!("Expected NoAuth auth method"), - } + let auth_components: alloc::vec::Vec<_> = no_auth_account_interface.auth_components().collect(); + assert_eq!(auth_components.len(), 1); + assert!(matches!(auth_components[0], AccountComponentInterface::AuthNoAuth)); } -/// Test AccountInterface.get_auth_scheme() method with Falcon512Poseidon2 and NoAuth #[test] -fn test_account_interface_get_auth_scheme() { - let mock_seed = Word::from([0, 1, 2, 3u32]).as_bytes(); - let wallet_account = AccountBuilder::new(mock_seed) - .with_auth_component(get_mock_falcon_auth_component()) - .with_component(BasicWallet) - .build_existing() - .expect("failed to create wallet account"); - - let wallet_account_interface = AccountInterface::from_account(&wallet_account); - - // Test that auth() method provides the authentication schemes - assert_eq!(wallet_account_interface.auth().len(), 1); - match &wallet_account_interface.auth()[0] { - AuthMethod::SingleSig { approver: (pub_key, auth_scheme) } => { - assert_eq!(*pub_key, PublicKeyCommitment::from(Word::from([0, 1, 2, 3u32]))); - assert_eq!(*auth_scheme, auth::AuthScheme::Falcon512Poseidon2); - }, - _ => panic!("Expected SingleSig auth method"), - } - - // Test AccountInterface.get_auth_scheme() method with NoAuth - let no_auth_account = AccountBuilder::new(mock_seed) - .with_auth_component(NoAuth) - .with_component(BasicWallet) - .build_existing() - .expect("failed to create no-auth account"); - - let no_auth_account_interface = AccountInterface::from_account(&no_auth_account); - - // Test that auth() method provides the authentication schemes - assert_eq!(no_auth_account_interface.auth().len(), 1); - match &no_auth_account_interface.auth()[0] { - AuthMethod::NoAuth => {}, - _ => panic!("Expected NoAuth auth method"), - } - - // Note: We don't test the case where an account has no auth components because - // accounts are required to have auth components in the current system design +fn test_basic_wallet_is_not_an_auth_component() { + assert!(!AccountComponentInterface::BasicWallet.is_auth_component()); + assert!(AccountComponentInterface::AuthSingleSig.is_auth_component()); + assert!(AccountComponentInterface::AuthNoAuth.is_auth_component()); } #[test] diff --git a/crates/miden-standards/src/account/mod.rs b/crates/miden-standards/src/account/mod.rs index 0dcafe14c3..fb928f2801 100644 --- a/crates/miden-standards/src/account/mod.rs +++ b/crates/miden-standards/src/account/mod.rs @@ -1,5 +1,3 @@ -use super::auth_method::AuthMethod; - pub mod access; pub mod auth; pub mod components; diff --git a/crates/miden-standards/src/account/wallets/mod.rs b/crates/miden-standards/src/account/wallets/mod.rs index 9b9c7a14b7..159b7f2345 100644 --- a/crates/miden-standards/src/account/wallets/mod.rs +++ b/crates/miden-standards/src/account/wallets/mod.rs @@ -1,5 +1,3 @@ -use alloc::string::String; - use miden_protocol::account::component::{AccountComponentCode, AccountComponentMetadata}; use miden_protocol::account::{ Account, @@ -10,11 +8,9 @@ use miden_protocol::account::{ AccountType, }; use miden_protocol::errors::AccountError; -use thiserror::Error; -use super::AuthMethod; use crate::account::account_component_code; -use crate::account::auth::{AuthMultisig, AuthMultisigConfig, AuthSingleSig}; +use crate::account::auth::{AuthGuardedMultisig, AuthMultisig, AuthSingleSig}; use crate::procedure_root; // BASIC WALLET @@ -105,70 +101,54 @@ impl From for AccountComponent { } } -// BASIC WALLET ERROR -// ================================================================================================ - -/// Basic wallet related errors. -#[derive(Debug, Error)] -pub enum BasicWalletError { - #[error("unsupported authentication method: {0}")] - UnsupportedAuthMethod(String), - #[error("account creation failed")] - AccountError(#[source] AccountError), -} - -/// Creates a new account with basic wallet interface, the specified authentication scheme and the -/// account storage type. Basic wallets can be specified to have either mutable or immutable code. +/// Creates a new account with the basic wallet interface authenticated by the provided +/// [`AuthSingleSig`] component. /// -/// The basic wallet interface exposes three procedures: +/// The basic wallet interface exposes two procedures: /// - `receive_asset`, which can be used to add an asset to the account. /// - `move_asset_to_note`, which can be used to remove the specified asset from the account and add /// it to the output note with the specified index. /// -/// All methods require authentication. The authentication procedure is defined by the specified -/// authentication scheme. +/// For multisig-authenticated basic wallets, use [`create_multisig_wallet`] or +/// [`create_guarded_wallet`]. For anything else, use [`AccountBuilder`] directly. pub fn create_basic_wallet( init_seed: [u8; 32], - auth_method: AuthMethod, - account_storage_mode: AccountType, -) -> Result { - let auth_component: AccountComponent = match auth_method { - AuthMethod::SingleSig { approver: (pub_key, auth_scheme) } => { - AuthSingleSig::new(pub_key, auth_scheme).into() - }, - AuthMethod::Multisig { threshold, approvers } => { - let config = AuthMultisigConfig::new(approvers, threshold) - .and_then(|cfg| { - cfg.with_proc_thresholds(vec![(BasicWallet::receive_asset_root(), 1)]) - }) - .map_err(BasicWalletError::AccountError)?; - AuthMultisig::new(config).map_err(BasicWalletError::AccountError)?.into() - }, - AuthMethod::NoAuth => { - return Err(BasicWalletError::UnsupportedAuthMethod( - "basic wallets cannot be created with NoAuth authentication method".into(), - )); - }, - AuthMethod::NetworkAccount { .. } => { - return Err(BasicWalletError::UnsupportedAuthMethod( - "basic wallets cannot be created with NetworkAccount authentication method".into(), - )); - }, - AuthMethod::Unknown => { - return Err(BasicWalletError::UnsupportedAuthMethod( - "basic wallets cannot be created with Unknown authentication method".into(), - )); - }, - }; + auth_component: AuthSingleSig, + account_type: AccountType, +) -> Result { + AccountBuilder::new(init_seed) + .account_type(account_type) + .with_auth_component(auth_component) + .with_component(BasicWallet) + .build() +} - let account = AccountBuilder::new(init_seed) - .account_type(account_storage_mode) +/// Creates a new account with the basic wallet interface authenticated by the provided +/// [`AuthMultisig`] component. Same procedures as [`create_basic_wallet`]. +pub fn create_multisig_wallet( + init_seed: [u8; 32], + auth_component: AuthMultisig, + account_type: AccountType, +) -> Result { + AccountBuilder::new(init_seed) + .account_type(account_type) .with_auth_component(auth_component) .with_component(BasicWallet) .build() - .map_err(BasicWalletError::AccountError)?; +} - Ok(account) +/// Creates a new account with the basic wallet interface authenticated by the provided +/// [`AuthGuardedMultisig`] component. Same procedures as [`create_basic_wallet`]. +pub fn create_guarded_wallet( + init_seed: [u8; 32], + auth_component: AuthGuardedMultisig, + account_type: AccountType, +) -> Result { + AccountBuilder::new(init_seed) + .account_type(account_type) + .with_auth_component(auth_component) + .with_component(BasicWallet) + .build() } // TESTS @@ -180,7 +160,17 @@ mod tests { use miden_protocol::utils::serde::{Deserializable, Serializable}; use miden_protocol::{ONE, Word}; - use super::{Account, AccountType, AuthMethod, create_basic_wallet}; + use super::{ + Account, + AccountType, + AuthGuardedMultisig, + AuthMultisig, + AuthSingleSig, + create_basic_wallet, + create_guarded_wallet, + create_multisig_wallet, + }; + use crate::account::auth::{AuthGuardedMultisigConfig, AuthMultisigConfig, GuardianConfig}; use crate::account::wallets::BasicWallet; #[test] @@ -189,7 +179,7 @@ mod tests { let auth_scheme = auth::AuthScheme::Falcon512Poseidon2; let wallet = create_basic_wallet( [1; 32], - AuthMethod::SingleSig { approver: (pub_key, auth_scheme) }, + AuthSingleSig::new(pub_key, auth_scheme), AccountType::Public, ); @@ -204,7 +194,7 @@ mod tests { let auth_scheme = auth::AuthScheme::EcdsaK256Keccak; let wallet = create_basic_wallet( [1; 32], - AuthMethod::SingleSig { approver: (pub_key, auth_scheme) }, + AuthSingleSig::new(pub_key, auth_scheme), AccountType::Public, ) .unwrap(); @@ -220,4 +210,37 @@ mod tests { let _receive_asset_root = BasicWallet::receive_asset_root(); let _move_asset_to_note_root = BasicWallet::move_asset_to_note_root(); } + + #[test] + fn test_create_multisig_wallet() { + let pub_key_1 = PublicKeyCommitment::from(Word::from([1u32, 0, 0, 0])); + let pub_key_2 = PublicKeyCommitment::from(Word::from([2u32, 0, 0, 0])); + let approvers = vec![ + (pub_key_1, auth::AuthScheme::Falcon512Poseidon2), + (pub_key_2, auth::AuthScheme::Falcon512Poseidon2), + ]; + let config = AuthMultisigConfig::new(approvers, 2).unwrap(); + let auth = AuthMultisig::new(config).unwrap(); + + let wallet = create_multisig_wallet([2; 32], auth, AccountType::Private); + wallet.unwrap_or_else(|err| panic!("{}", err)); + } + + #[test] + fn test_create_guarded_wallet() { + let pub_key_1 = PublicKeyCommitment::from(Word::from([1u32, 0, 0, 0])); + let pub_key_2 = PublicKeyCommitment::from(Word::from([2u32, 0, 0, 0])); + let guardian_key = PublicKeyCommitment::from(Word::from([3u32, 0, 0, 0])); + let approvers = vec![ + (pub_key_1, auth::AuthScheme::Falcon512Poseidon2), + (pub_key_2, auth::AuthScheme::Falcon512Poseidon2), + ]; + let guardian_config = + GuardianConfig::new(guardian_key, auth::AuthScheme::Falcon512Poseidon2); + let config = AuthGuardedMultisigConfig::new(approvers, 2, guardian_config).unwrap(); + let auth = AuthGuardedMultisig::new(config).unwrap(); + + let wallet = create_guarded_wallet([3; 32], auth, AccountType::Private); + wallet.unwrap_or_else(|err| panic!("{}", err)); + } } diff --git a/crates/miden-standards/src/auth_method.rs b/crates/miden-standards/src/auth_method.rs deleted file mode 100644 index 86c0306e2a..0000000000 --- a/crates/miden-standards/src/auth_method.rs +++ /dev/null @@ -1,58 +0,0 @@ -use alloc::collections::BTreeSet; -use alloc::vec::Vec; - -use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment}; -use miden_protocol::note::NoteScriptRoot; -use miden_protocol::transaction::TransactionScriptRoot; - -/// Defines standard authentication methods supported by account auth components. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum AuthMethod { - /// A minimal authentication method that provides no cryptographic authentication. - /// - /// It only increments the nonce if the account state has actually changed during transaction - /// execution, avoiding unnecessary nonce increments for transactions that don't modify the - /// account state. - NoAuth, - /// A single-key authentication method which relies on either ECDSA or Falcon512Poseidon2 - /// signatures. - SingleSig { - approver: (PublicKeyCommitment, AuthScheme), - }, - /// A multi-signature authentication method using either ECDSA or Falcon512Poseidon2 signatures. - /// - /// Requires a threshold number of signatures from the provided public keys. - Multisig { - threshold: u32, - approvers: Vec<(PublicKeyCommitment, AuthScheme)>, - }, - /// An authentication method intended for network-owned accounts. - /// - /// It restricts the account to consuming only notes whose script roots are in - /// `allowed_script_roots` (which must be non-empty), and to executing only transaction scripts - /// whose roots are in `allowed_tx_script_roots`. An empty `allowed_tx_script_roots` permits no - /// transaction scripts. - NetworkAccount { - allowed_script_roots: BTreeSet, - allowed_tx_script_roots: BTreeSet, - }, - /// A non-standard authentication method. - Unknown, -} - -impl AuthMethod { - /// Returns all public key commitments associated with this authentication method. - /// - /// For unknown methods, an empty vector is returned. - pub fn get_public_key_commitments(&self) -> Vec { - match self { - AuthMethod::NoAuth => Vec::new(), - AuthMethod::SingleSig { approver: (pub_key, _) } => vec![*pub_key], - AuthMethod::Multisig { approvers, .. } => { - approvers.iter().map(|(pub_key, _)| *pub_key).collect() - }, - AuthMethod::NetworkAccount { .. } => Vec::new(), - AuthMethod::Unknown => Vec::new(), - } - } -} diff --git a/crates/miden-standards/src/lib.rs b/crates/miden-standards/src/lib.rs index 2ccb68c543..5b9972d5ec 100644 --- a/crates/miden-standards/src/lib.rs +++ b/crates/miden-standards/src/lib.rs @@ -6,9 +6,6 @@ extern crate alloc; #[cfg(feature = "std")] extern crate std; -mod auth_method; -pub use auth_method::AuthMethod; - pub mod account; pub mod code_builder; pub mod errors; diff --git a/crates/miden-standards/src/testing/account_interface.rs b/crates/miden-standards/src/testing/account_interface.rs index 932c03ee4f..49556f5d39 100644 --- a/crates/miden-standards/src/testing/account_interface.rs +++ b/crates/miden-standards/src/testing/account_interface.rs @@ -3,16 +3,72 @@ use alloc::vec::Vec; use miden_protocol::Word; use miden_protocol::account::Account; -use crate::account::interface::{AccountInterface, AccountInterfaceExt}; +use crate::account::auth::{ + AuthGuardedMultisig, + AuthMultisig, + AuthMultisigSmart, + AuthSingleSig, + AuthSingleSigAcl, +}; +use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; -/// Helper function to extract public keys from an account +/// Helper function to extract public key commitments from every standard auth component +/// installed on `account`. Reads storage directly via the component's slot accessors. pub fn get_public_keys_from_account(account: &Account) -> Vec { let interface = AccountInterface::from_account(account); + let storage = account.storage(); - interface - .auth() - .iter() - .flat_map(|auth| auth.get_public_key_commitments()) - .map(Word::from) + let mut keys = Vec::new(); + for component in interface.components() { + match component { + AccountComponentInterface::AuthSingleSig => { + if let Ok(key) = storage.get_item(AuthSingleSig::public_key_slot()) { + keys.push(key); + } + }, + AccountComponentInterface::AuthSingleSigAcl => { + if let Ok(key) = storage.get_item(AuthSingleSigAcl::public_key_slot()) { + keys.push(key); + } + }, + AccountComponentInterface::AuthMultisig => { + keys.extend(read_multisig_public_keys( + storage, + AuthMultisig::threshold_config_slot(), + AuthMultisig::approver_public_keys_slot(), + )); + }, + AccountComponentInterface::AuthGuardedMultisig => { + keys.extend(read_multisig_public_keys( + storage, + AuthGuardedMultisig::threshold_config_slot(), + AuthGuardedMultisig::approver_public_keys_slot(), + )); + }, + AccountComponentInterface::AuthMultisigSmart => { + keys.extend(read_multisig_public_keys( + storage, + AuthMultisigSmart::threshold_config_slot(), + AuthMultisigSmart::approver_public_keys_slot(), + )); + }, + _ => {}, + } + } + keys +} + +fn read_multisig_public_keys( + storage: &miden_protocol::account::AccountStorage, + config_slot: &miden_protocol::account::StorageSlotName, + keys_slot: &miden_protocol::account::StorageSlotName, +) -> Vec { + let config = match storage.get_item(config_slot) { + Ok(c) => c, + Err(_) => return Vec::new(), + }; + let num_approvers = config[1].as_canonical_u64() as u32; + (0..num_approvers) + .filter_map(|i| storage.get_map_item(keys_slot, Word::from([i, 0u32, 0, 0])).ok()) .collect() } diff --git a/crates/miden-standards/src/testing/faucet.rs b/crates/miden-standards/src/testing/faucet.rs new file mode 100644 index 0000000000..ead462d191 --- /dev/null +++ b/crates/miden-standards/src/testing/faucet.rs @@ -0,0 +1,53 @@ +use alloc::vec; +use alloc::vec::Vec; + +use miden_protocol::account::AccountProcedureRoot; +use miden_protocol::account::auth::{AuthScheme, PublicKeyCommitment}; +use miden_protocol::errors::AccountError; + +use crate::account::access::PausableManager; +use crate::account::auth::{AuthSingleSigAcl, AuthSingleSigAclConfig}; +use crate::account::faucets::FungibleFaucet; +use crate::account::policies::TokenPolicyManager; + +/// Returns every authority-gated setter procedure root exported by a fungible faucet account +/// (`mint_and_send`, the metadata setters, the policy setters, and `pause` / `unpause`). +/// +/// Under `Authority::AuthControlled` the auth component must authenticate calls to every +/// procedure in this list, otherwise the setters become permissionless. Use this when +/// constructing a custom [`AuthSingleSigAcl`] trigger procedure list; for the canonical +/// configuration, prefer [`user_faucet_single_sig_acl`]. +pub 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(), + PausableManager::pause_root(), + PausableManager::unpause_root(), + ] +} + +/// Convenience constructor for the typical user-account fungible faucet auth component: an +/// [`AuthSingleSigAcl`] with the trigger procedure list set to +/// [`all_authority_gated_setter_roots`] and `allow_unauthorized_input_notes=true`. +/// +/// Production callers that need a different ACL shape should construct [`AuthSingleSigAcl`] +/// directly, optionally seeding the trigger list with [`all_authority_gated_setter_roots`]. +pub fn user_faucet_single_sig_acl( + pub_key: PublicKeyCommitment, + scheme: AuthScheme, +) -> Result { + AuthSingleSigAcl::new( + pub_key, + scheme, + AuthSingleSigAclConfig::new() + .with_auth_trigger_procedures(all_authority_gated_setter_roots()) + .with_allow_unauthorized_input_notes(true), + ) +} diff --git a/crates/miden-standards/src/testing/mod.rs b/crates/miden-standards/src/testing/mod.rs index 01cf73f63c..bf5cbcc069 100644 --- a/crates/miden-standards/src/testing/mod.rs +++ b/crates/miden-standards/src/testing/mod.rs @@ -1,6 +1,8 @@ pub mod account_component; pub mod account_interface; +pub mod faucet; + pub mod mock_account; pub mod mock_account_code; diff --git a/crates/miden-testing/src/kernel_tests/tx/test_account.rs b/crates/miden-testing/src/kernel_tests/tx/test_account.rs index 6b5029efa5..6610abc3e7 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_account.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_account.rs @@ -59,6 +59,7 @@ use miden_protocol::testing::account_id::{ use miden_protocol::testing::storage::{MOCK_MAP_SLOT, MOCK_VALUE_SLOT0, MOCK_VALUE_SLOT1}; use miden_protocol::transaction::{RawOutputNote, TransactionKernel}; use miden_protocol::utils::sync::LazyLock; +use miden_standards::account::access::Pausable; use miden_standards::account::faucets::{FungibleFaucet, TokenName}; use miden_standards::code_builder::CodeBuilder; use miden_standards::testing::account_component::MockAccountComponent; @@ -1647,6 +1648,7 @@ async fn test_faucet_has_callbacks( .account_type(AccountType::Public) .with_component(faucet) .with_component(MockAccountComponent::with_slots(callback_slots)) + .with_component(Pausable::unpaused()) .with_auth_component(Auth::IncrNonce) .build_existing()?; diff --git a/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs b/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs index 9f469d94da..4fc5f676da 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs @@ -33,7 +33,7 @@ use miden_protocol::errors::MasmError; use miden_protocol::note::{NoteTag, NoteType}; use miden_protocol::utils::sync::LazyLock; use miden_protocol::{Felt, Word}; -use miden_standards::account::access::Authority; +use miden_standards::account::access::{Authority, Pausable}; use miden_standards::account::faucets::{FungibleFaucet, TokenName}; use miden_standards::account::policies::{BurnPolicy, MintPolicy, TokenPolicyManager}; use miden_standards::code_builder::CodeBuilder; @@ -773,6 +773,7 @@ fn add_faucet_with_callbacks( .active_burn_policy(BurnPolicy::allow_all()) .build(), ) + .with_component(Pausable::unpaused()) .with_component(callback_component); builder.add_account_from_builder( diff --git a/crates/miden-testing/src/kernel_tests/tx/test_tx.rs b/crates/miden-testing/src/kernel_tests/tx/test_tx.rs index 80dfc69898..8c25961496 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_tx.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_tx.rs @@ -56,12 +56,16 @@ use miden_protocol::transaction::{ TransactionSummary, }; use miden_protocol::{Felt, Hasher, ONE, Word}; -use miden_standards::AuthMethod; -use miden_standards::account::interface::{AccountInterface, AccountInterfaceExt}; +use miden_standards::account::interface::{ + AccountComponentInterface, + AccountInterface, + AccountInterfaceExt, +}; use miden_standards::account::wallets::BasicWallet; use miden_standards::code_builder::CodeBuilder; use miden_standards::note::P2idNote; use miden_standards::testing::account_component::IncrNonceAuthComponent; +use miden_standards::testing::account_interface::get_public_keys_from_account; use miden_standards::testing::mock_account::MockAccountExt; use miden_standards::tx_script::SendNotesTransactionScript; use miden_tx::auth::UnreachableAuth; @@ -556,21 +560,16 @@ async fn tx_summary_commitment_is_signed_by_falcon_auth() -> anyhow::Result<()> let summary_commitment = summary.to_commitment(); let account_interface = AccountInterface::from_account(&account); - let pub_key = match account_interface.auth().first().unwrap() { - AuthMethod::SingleSig { approver: (pub_key, _) } => pub_key, - AuthMethod::NoAuth => panic!("Expected SingleSig auth scheme, got NoAuth"), - AuthMethod::Multisig { .. } => { - panic!("Expected SingleSig auth scheme, got Multisig") - }, - AuthMethod::NetworkAccount { .. } => { - panic!("Expected SingleSig auth scheme, got NetworkAccount") - }, - AuthMethod::Unknown => panic!("Expected SingleSig auth scheme, got Unknown"), - }; + assert!(matches!( + account_interface.auth_components().next(), + Some(AccountComponentInterface::AuthSingleSig) + )); + let pub_keys = get_public_keys_from_account(&account); + let pub_key = pub_keys.first().expect("expected at least one public key"); // This is in an internal detail of the tx executor host, but this is the easiest way to check // for the presence of the signature in the advice map. - let signature_key = Hasher::merge(&[Word::from(*pub_key), summary_commitment]); + let signature_key = Hasher::merge(&[*pub_key, summary_commitment]); // The summary commitment should have been signed as part of transaction execution and inserted // into the advice map. @@ -618,21 +617,16 @@ async fn tx_summary_commitment_is_signed_by_ecdsa_auth() -> anyhow::Result<()> { let summary_commitment = summary.to_commitment(); let account_interface = AccountInterface::from_account(&account); - let pub_key = match account_interface.auth().first().unwrap() { - AuthMethod::SingleSig { approver: (pub_key, _) } => pub_key, - AuthMethod::NoAuth => panic!("Expected SingleSig auth scheme, got NoAuth"), - AuthMethod::Multisig { .. } => { - panic!("Expected SingleSig auth scheme, got Multisig") - }, - AuthMethod::NetworkAccount { .. } => { - panic!("Expected SingleSig auth scheme, got NetworkAccount") - }, - AuthMethod::Unknown => panic!("Expected SingleSig auth scheme, got Unknown"), - }; + assert!(matches!( + account_interface.auth_components().next(), + Some(AccountComponentInterface::AuthSingleSig) + )); + let pub_keys = get_public_keys_from_account(&account); + let pub_key = pub_keys.first().expect("expected at least one public key"); // This is in an internal detail of the tx executor host, but this is the easiest way to check // for the presence of the signature in the advice map. - let signature_key = Hasher::merge(&[Word::from(*pub_key), summary_commitment]); + let signature_key = Hasher::merge(&[*pub_key, summary_commitment]); // The summary commitment should have been signed as part of transaction execution and inserted // into the advice map. diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index e3202a0b02..1296dce637 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -46,7 +46,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::account::access::AccessControl; +use miden_standards::account::access::{AccessControl, Authority, Pausable, PausableManager}; use miden_standards::account::faucets::{FungibleFaucet, TokenName}; use miden_standards::account::policies::{ BurnPolicy, @@ -315,43 +315,9 @@ impl MockChainBuilder { self.add_account_from_builder(auth_method, account_builder, AccountState::Exists) } - /// Creates a new public [`FungibleFaucet`] account and registers the authenticator (if - /// any) for it. - /// - /// This does not add the account to the chain state, but it can still be used to call - /// [`MockChain::build_tx_context`] to automatically add the authenticator. - fn create_new_fungible_faucet( - &mut self, - auth_method: Auth, - faucet: FungibleFaucet, - account_type: AccountType, - access_control: AccessControl, - token_policy_manager: TokenPolicyManager, - ) -> anyhow::Result { - let account_builder = AccountBuilder::new(self.rng.random()) - .account_type(account_type) - .with_component(faucet) - .with_components(access_control) - .with_components(token_policy_manager); - - self.add_account_from_builder(auth_method, account_builder, AccountState::New) - } - - /// Adds an existing fungible faucet account to the initial chain state and registers the - /// authenticator (if any). - /// - /// The behaviour of the faucet (basic vs network-style) is determined entirely by the - /// combination of arguments: - /// - `account_type`: [`AccountType::Public`] for basic faucets, or [`AccountType::Private`] for - /// off-chain accounts. - /// - `auth_method`: typically a [`Auth::BasicAuth`] for basic faucets, or [`Auth::IncrNonce`] - /// for network-style faucets. - /// - `access_control`: [`AccessControl::AuthControlled`] for basic faucets; - /// [`AccessControl::Ownable2Step`] / [`AccessControl::Rbac`] for owner-controlled faucets. - /// The matching `Authority` component is auto-installed by `AccessControl`. - /// - `token_policy_manager`: the unified [`TokenPolicyManager`] holding both mint and burn - /// policy. - fn add_existing_fungible_faucet( + /// Internal helper: adds an existing network-style fungible faucet (Ownable2Step / Rbac). + /// Bundles [`PausableManager`] to match the `create_network_fungible_faucet` factory. + fn add_existing_network_fungible_faucet( &mut self, auth_method: Auth, faucet: FungibleFaucet, @@ -363,17 +329,19 @@ impl MockChainBuilder { .account_type(account_type) .with_component(faucet) .with_components(access_control) - .with_components(token_policy_manager); + .with_components(token_policy_manager) + .with_component(Pausable::unpaused()) + .with_component(PausableManager); self.add_account_from_builder(auth_method, account_builder, AccountState::Exists) } /// Convenience: builds a basic auth-controlled fungible faucet from a token-symbol shorthand - /// using default decimals and `AllowAll` policies, then adds it via - /// `Self::add_existing_fungible_faucet`. + /// using default decimals and `AllowAll` policies, then adds it as an existing account with + /// [`Authority::AuthControlled`]. /// /// For full control over the faucet's metadata, decimals, and policies, construct a - /// [`FungibleFaucet`] manually and call `Self::add_existing_fungible_faucet`. + /// [`FungibleFaucet`] manually and use [`AccountBuilder`] directly. pub fn add_existing_basic_faucet( &mut self, auth_method: Auth, @@ -403,13 +371,15 @@ impl MockChainBuilder { .active_receive_policy(TransferPolicy::allow_all()) .build(); - self.add_existing_fungible_faucet( - auth_method, - faucet, - AccountType::Public, - AccessControl::AuthControlled, - token_policy_manager, - ) + let account_builder = AccountBuilder::new(self.rng.random()) + .account_type(AccountType::Public) + .with_component(faucet) + .with_component(Authority::AuthControlled) + .with_components(token_policy_manager) + .with_component(Pausable::unpaused()) + .with_component(PausableManager); + + self.add_account_from_builder(auth_method, account_builder, AccountState::Exists) } /// Convenience: builds an owner-controlled (network-style) fungible faucet from a @@ -453,12 +423,12 @@ impl MockChainBuilder { .active_receive_policy(TransferPolicy::allow_all()) .build(); - let allowed_script_roots = allowed_script_roots + let allowed_script_roots: BTreeSet = allowed_script_roots .into_iter() .chain([MintNote::script_root(), BurnNote::script_root()]) .collect(); - self.add_existing_fungible_faucet( + self.add_existing_network_fungible_faucet( Auth::NetworkAccount { allowed_script_roots, allowed_tx_script_roots: BTreeSet::new(), @@ -489,12 +459,12 @@ impl MockChainBuilder { .active_receive_policy(TransferPolicy::allow_all()) .build(); - let allowed_script_roots = allowed_script_roots + let allowed_script_roots: BTreeSet = allowed_script_roots .into_iter() .chain([MintNote::script_root(), BurnNote::script_root()]) .collect(); - self.add_existing_fungible_faucet( + self.add_existing_network_fungible_faucet( Auth::NetworkAccount { allowed_script_roots, allowed_tx_script_roots: BTreeSet::new(), @@ -533,13 +503,15 @@ impl MockChainBuilder { .active_receive_policy(TransferPolicy::allow_all()) .build(); - self.create_new_fungible_faucet( - auth_method, - faucet, - AccountType::Public, - AccessControl::AuthControlled, - token_policy_manager, - ) + let account_builder = AccountBuilder::new(self.rng.random()) + .account_type(AccountType::Public) + .with_component(faucet) + .with_component(Authority::AuthControlled) + .with_components(token_policy_manager) + .with_component(Pausable::unpaused()) + .with_component(PausableManager); + + self.add_account_from_builder(auth_method, account_builder, AccountState::New) } /// Creates a new public account with an [`MockAccountComponent`] and registers the diff --git a/crates/miden-testing/src/standards/token_metadata.rs b/crates/miden-testing/src/standards/token_metadata.rs index 04427c31a6..bada9ef71c 100644 --- a/crates/miden-testing/src/standards/token_metadata.rs +++ b/crates/miden-testing/src/standards/token_metadata.rs @@ -21,6 +21,7 @@ use miden_protocol::asset::{AssetAmount, TokenSymbol}; use miden_protocol::errors::MasmError; use miden_protocol::note::{NoteTag, NoteType}; use miden_protocol::{Felt, Word}; +use miden_standards::account::access::Pausable; use miden_standards::account::auth::NoAuth; use miden_standards::account::faucets::{ Description, @@ -214,6 +215,7 @@ async fn get_name_from_masm() -> anyhow::Result<()> { let account = AccountBuilder::new([1u8; 32]) .with_auth_component(NoAuth) .with_component(faucet) + .with_component(Pausable::unpaused()) .build()?; execute_tx_script( @@ -249,6 +251,7 @@ async fn get_name_zeros_returns_empty() -> anyhow::Result<()> { let account = AccountBuilder::new([1u8; 32]) .with_auth_component(NoAuth) .with_component(faucet) + .with_component(Pausable::unpaused()) .build()?; execute_tx_script( @@ -406,6 +409,7 @@ async fn get_mutability_config() -> anyhow::Result<()> { let account = AccountBuilder::new([1u8; 32]) .with_auth_component(NoAuth) .with_component(faucet) + .with_component(Pausable::unpaused()) .build()?; execute_tx_script( @@ -448,6 +452,7 @@ async fn is_field_mutable_checks( let account = AccountBuilder::new([1u8; 32]) .with_auth_component(NoAuth) .with_component(faucet) + .with_component(Pausable::unpaused()) .build()?; execute_tx_script( @@ -525,7 +530,8 @@ fn verify_faucet_with_max_name_and_description( let mut builder = AccountBuilder::new(seed) .account_type(account_type) .with_auth_component(NoAuth) - .with_component(faucet); + .with_component(faucet) + .with_component(Pausable::unpaused()); for comp in extra_components { builder = builder.with_component(comp); diff --git a/crates/miden-testing/tests/scripts/allowlist.rs b/crates/miden-testing/tests/scripts/allowlist.rs index 8618ca817a..9de6a82fc8 100644 --- a/crates/miden-testing/tests/scripts/allowlist.rs +++ b/crates/miden-testing/tests/scripts/allowlist.rs @@ -16,7 +16,7 @@ use miden_protocol::asset::{Asset, AssetAmount, AssetCallbackFlag, FungibleAsset use miden_protocol::note::{Note, NoteTag, NoteType}; use miden_protocol::transaction::RawOutputNote; use miden_protocol::{Felt, Word}; -use miden_standards::account::access::{Authority, Ownable2Step}; +use miden_standards::account::access::{Authority, Ownable2Step, Pausable}; use miden_standards::account::faucets::{FungibleFaucet, TokenName}; use miden_standards::account::policies::{ AllowlistOwnerControlled, @@ -86,6 +86,7 @@ fn add_faucet_with_owner_allowlist_transfer_initialized( .active_receive_policy(TransferPolicy::with_basic_allowlist(allow_list)) .build(), ) + .with_component(Pausable::unpaused()) .with_component(AllowlistOwnerControlled); builder.add_account_from_builder( diff --git a/crates/miden-testing/tests/scripts/blocklist.rs b/crates/miden-testing/tests/scripts/blocklist.rs index 59de9cae13..69082561c6 100644 --- a/crates/miden-testing/tests/scripts/blocklist.rs +++ b/crates/miden-testing/tests/scripts/blocklist.rs @@ -16,7 +16,7 @@ use miden_protocol::asset::{Asset, AssetAmount, AssetCallbackFlag, FungibleAsset use miden_protocol::note::{Note, NoteTag, NoteType}; use miden_protocol::transaction::RawOutputNote; use miden_protocol::{Felt, Word}; -use miden_standards::account::access::{Authority, Ownable2Step}; +use miden_standards::account::access::{Authority, Ownable2Step, Pausable}; use miden_standards::account::faucets::{FungibleFaucet, TokenName}; use miden_standards::account::policies::{ BlocklistOwnerControlled, @@ -84,6 +84,7 @@ fn add_faucet_with_owner_blocklist_transfer_initialized( .active_receive_policy(TransferPolicy::empty_basic_blocklist()) .build(), ) + .with_component(Pausable::unpaused()) .with_component(BlocklistOwnerControlled); builder.add_account_from_builder( diff --git a/crates/miden-testing/tests/scripts/faucet.rs b/crates/miden-testing/tests/scripts/faucet.rs index 06d0313ab1..f9723dea66 100644 --- a/crates/miden-testing/tests/scripts/faucet.rs +++ b/crates/miden-testing/tests/scripts/faucet.rs @@ -26,7 +26,7 @@ use miden_protocol::note::{ use miden_protocol::testing::account_id::ACCOUNT_ID_PRIVATE_SENDER; use miden_protocol::transaction::{ExecutedTransaction, RawOutputNote}; use miden_protocol::{Felt, Word}; -use miden_standards::account::access::{Authority, Ownable2Step}; +use miden_standards::account::access::{Authority, Ownable2Step, Pausable}; use miden_standards::account::faucets::{FungibleFaucet, TokenName}; use miden_standards::account::policies::{ BurnAllowAll, @@ -238,7 +238,8 @@ fn build_network_faucet_with_burn_switching( .with_component(faucet) .with_component(Ownable2Step::new(owner)) .with_component(Authority::OwnerControlled) - .with_components(token_policy_manager); + .with_components(token_policy_manager) + .with_component(Pausable::unpaused()); builder.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists) } @@ -280,7 +281,8 @@ fn build_network_faucet_with_min_burn_amount( .with_component(faucet) .with_component(Ownable2Step::new(owner)) .with_component(Authority::OwnerControlled) - .with_components(token_policy_manager); + .with_components(token_policy_manager) + .with_component(Pausable::unpaused()); builder.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists) } @@ -2104,7 +2106,8 @@ fn build_network_faucet_with_blocklist_transfer( .with_component(faucet) .with_component(Ownable2Step::new(owner)) .with_component(Authority::OwnerControlled) - .with_components(token_policy_manager); + .with_components(token_policy_manager) + .with_component(Pausable::unpaused()); builder.add_account_from_builder( Auth::NetworkAccount { diff --git a/crates/miden-testing/tests/scripts/pausable.rs b/crates/miden-testing/tests/scripts/pausable.rs index ef8cf4b217..3df55e04fd 100644 --- a/crates/miden-testing/tests/scripts/pausable.rs +++ b/crates/miden-testing/tests/scripts/pausable.rs @@ -16,8 +16,8 @@ use miden_protocol::errors::MasmError; use miden_protocol::note::{Note, NoteTag, NoteType}; use miden_protocol::transaction::RawOutputNote; use miden_protocol::utils::sync::LazyLock; -use miden_standards::account::access::AccessControl; -use miden_standards::account::access::pausable::{PausableManager, PausableStorage}; +use miden_standards::account::access::pausable::{Pausable, PausableManager, PausableStorage}; +use miden_standards::account::access::{AccessControl, Authority}; use miden_standards::account::faucets::{FungibleFaucet, TokenName}; use miden_standards::account::policies::{ BurnPolicy, @@ -66,6 +66,7 @@ fn add_faucet_with_pause( .account_type(AccountType::Public) .with_component(faucet) .with_components(AccessControl::Ownable2Step { owner }) + .with_component(Pausable::unpaused()) .with_component(PausableManager); builder.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists) @@ -293,6 +294,7 @@ fn add_faucet_with_pause_and_policies( .account_type(AccountType::Public) .with_component(faucet) .with_components(AccessControl::Ownable2Step { owner }) + .with_component(Pausable::unpaused()) .with_component(PausableManager) .with_components( TokenPolicyManager::builder() @@ -523,6 +525,7 @@ fn add_faucet_mutable_max_supply_with_pause( .account_type(AccountType::Public) .with_component(faucet) .with_components(AccessControl::Ownable2Step { owner }) + .with_component(Pausable::unpaused()) .with_component(PausableManager); builder.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists) @@ -579,7 +582,7 @@ async fn pausable_set_max_supply_fails_when_paused() -> anyhow::Result<()> { // TESTS — PAUSABLE MANAGER WITH AUTH-CONTROLLED ACCESS CONTROL // ================================================================================================ -/// Same as `add_faucet_with_pause` but uses `AccessControl::AuthControlled`. +/// Same as `add_faucet_with_pause` but uses `Authority::AuthControlled` directly. fn add_faucet_with_pause_auth_controlled( builder: &mut MockChainBuilder, ) -> anyhow::Result { @@ -593,7 +596,8 @@ fn add_faucet_with_pause_auth_controlled( let account_builder = AccountBuilder::new([46u8; 32]) .account_type(AccountType::Public) .with_component(faucet) - .with_components(AccessControl::AuthControlled) + .with_component(Authority::AuthControlled) + .with_component(Pausable::unpaused()) .with_component(PausableManager); builder.add_account_from_builder(Auth::IncrNonce, account_builder, AccountState::Exists) diff --git a/crates/miden-testing/tests/wallet/mod.rs b/crates/miden-testing/tests/wallet/mod.rs index f2a71098f6..452405f7bf 100644 --- a/crates/miden-testing/tests/wallet/mod.rs +++ b/crates/miden-testing/tests/wallet/mod.rs @@ -1,6 +1,5 @@ use miden_protocol::Word; use miden_protocol::account::auth::AuthSecretKey; -use miden_standards::AuthMethod; use miden_standards::account::wallets::create_basic_wallet; use rand_chacha::ChaCha20Rng; use rand_chacha::rand_core::SeedableRng; @@ -19,7 +18,7 @@ fn wallet_creation() { let sec_key = AuthSecretKey::new_falcon512_poseidon2_with_rng(&mut rng); let auth_scheme = auth::AuthScheme::Falcon512Poseidon2; let pub_key = sec_key.public_key().to_commitment(); - let auth_method: AuthMethod = AuthMethod::SingleSig { approver: (pub_key, auth_scheme) }; + let auth_component = AuthSingleSig::new(pub_key, auth_scheme); // we need to use an initial seed to create the wallet account let init_seed: [u8; 32] = [ @@ -29,7 +28,7 @@ fn wallet_creation() { let account_type = AccountType::Private; - let wallet = create_basic_wallet(init_seed, auth_method, account_type).unwrap(); + let wallet = create_basic_wallet(init_seed, auth_component, account_type).unwrap(); let expected_code = AccountCode::from_components(&[ AuthSingleSig::new(pub_key, auth_scheme).into(), @@ -58,7 +57,7 @@ fn wallet_creation_2() { let sec_key = AuthSecretKey::new_ecdsa_k256_keccak_with_rng(&mut rng); let auth_scheme = auth::AuthScheme::EcdsaK256Keccak; let pub_key = sec_key.public_key().to_commitment(); - let auth_method: AuthMethod = AuthMethod::SingleSig { approver: (pub_key, auth_scheme) }; + let auth_component = AuthSingleSig::new(pub_key, auth_scheme); // we need to use an initial seed to create the wallet account let init_seed: [u8; 32] = [ @@ -68,7 +67,7 @@ fn wallet_creation_2() { let account_type = AccountType::Private; - let wallet = create_basic_wallet(init_seed, auth_method, account_type).unwrap(); + let wallet = create_basic_wallet(init_seed, auth_component, account_type).unwrap(); let expected_code = AccountCode::from_components(&[ AuthSingleSig::new(pub_key, auth_scheme).into(),