diff --git a/CHANGELOG.md b/CHANGELOG.md index e42fe91417..74ac9cab5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ - Added `CodeBuilder::with_warnings_as_errors()` to promote assembler warning diagnostics to errors ([#2558](https://github.com/0xMiden/protocol/pull/2558)). - Added `MockChain::add_pending_batch()` to allow submitting user batches directly ([#2565](https://github.com/0xMiden/protocol/pull/2565)). - Added `create_fungible_key` for construction of fungible asset keys ([#2575](https://github.com/0xMiden/protocol/pull/2575)). +- Added `InputNoteCommitment::from_parts()` for construction of input note commitments from a nullifier and optional note header ([#2588](https://github.com/0xMiden/protocol/pull/2588)). ### Changes @@ -83,6 +84,7 @@ - [BREAKING] Use `@auth_script` MASM attribute instead of `auth_` prefix to identify authentication procedures in account components ([#2534](https://github.com/0xMiden/protocol/pull/2534)). - [BREAKING] Changed `TransactionId` to include fee asset in hash computation, making it commit to entire `TransactionHeader` contents. - Explicitly use `get_native_account_active_storage_slots_ptr` in `account::set_item` and `account::set_map_item`. +- Added Ownable2Step as an Account Component ([#2572](https://github.com/0xMiden/protocol/pull/2572)) - [BREAKING] Introduced `PrivateNoteHeader` for output notes and removed `RawOutputNote::Header` variant ([#2569](https://github.com/0xMiden/protocol/pull/2569)). ## 0.13.3 (2026-01-27) diff --git a/crates/miden-protocol/src/transaction/proven_tx.rs b/crates/miden-protocol/src/transaction/proven_tx.rs index 1a8cee96ad..5775902cc7 100644 --- a/crates/miden-protocol/src/transaction/proven_tx.rs +++ b/crates/miden-protocol/src/transaction/proven_tx.rs @@ -498,6 +498,16 @@ pub struct InputNoteCommitment { } impl InputNoteCommitment { + /// Returns a new [InputNoteCommitment] instantiated from the provided nullifier and optional + /// note header. + /// + /// Note: this method does not validate that the provided nullifier and header are consistent + /// with each other (i.e., it does not check that the nullifier was derived from the note + /// referenced by the header). + pub fn from_parts_unchecked(nullifier: Nullifier, header: Option) -> Self { + Self { nullifier, header } + } + /// Returns the nullifier of the input note committed to by this commitment. pub fn nullifier(&self) -> Nullifier { self.nullifier diff --git a/crates/miden-standards/asm/account_components/access/ownable2step.masm b/crates/miden-standards/asm/account_components/access/ownable2step.masm new file mode 100644 index 0000000000..0f7b7dd2bd --- /dev/null +++ b/crates/miden-standards/asm/account_components/access/ownable2step.masm @@ -0,0 +1,9 @@ +# The MASM code of the Ownable2Step Account Component. +# +# See the `Ownable2Step` Rust type's documentation for more details. + +pub use ::miden::standards::access::ownable2step::get_owner +pub use ::miden::standards::access::ownable2step::get_nominated_owner +pub use ::miden::standards::access::ownable2step::transfer_ownership +pub use ::miden::standards::access::ownable2step::accept_ownership +pub use ::miden::standards::access::ownable2step::renounce_ownership diff --git a/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm b/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm index 7cded18fb0..604239c7fd 100644 --- a/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm +++ b/crates/miden-standards/asm/account_components/faucets/network_fungible_faucet.masm @@ -4,8 +4,3 @@ pub use ::miden::standards::faucets::network_fungible::distribute pub use ::miden::standards::faucets::network_fungible::burn -pub use ::miden::standards::faucets::network_fungible::get_owner -pub use ::miden::standards::faucets::network_fungible::get_nominated_owner -pub use ::miden::standards::faucets::network_fungible::transfer_ownership -pub use ::miden::standards::faucets::network_fungible::accept_ownership -pub use ::miden::standards::faucets::network_fungible::renounce_ownership diff --git a/crates/miden-standards/asm/standards/faucets/network_fungible.masm b/crates/miden-standards/asm/standards/faucets/network_fungible.masm index 74aed661e2..6d23f55f8b 100644 --- a/crates/miden-standards/asm/standards/faucets/network_fungible.masm +++ b/crates/miden-standards/asm/standards/faucets/network_fungible.masm @@ -4,15 +4,6 @@ use miden::standards::access::ownable2step # PUBLIC INTERFACE # ================================================================================================ -# OWNER MANAGEMENT (re-exported from ownable2step) -# ------------------------------------------------------------------------------------------------ - -pub use ownable2step::get_owner -pub use ownable2step::get_nominated_owner -pub use ownable2step::transfer_ownership -pub use ownable2step::accept_ownership -pub use ownable2step::renounce_ownership - # ASSET DISTRIBUTION # ------------------------------------------------------------------------------------------------ diff --git a/crates/miden-standards/src/account/access/mod.rs b/crates/miden-standards/src/account/access/mod.rs index 6e14ad58bf..f7c58c875b 100644 --- a/crates/miden-standards/src/account/access/mod.rs +++ b/crates/miden-standards/src/account/access/mod.rs @@ -1,3 +1,20 @@ +use miden_protocol::account::{AccountComponent, AccountId}; + pub mod ownable2step; +/// Access control configuration for account components. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AccessControl { + /// Uses two-step ownership transfer with the provided initial owner. + Ownable2Step { owner: AccountId }, +} + +impl From for AccountComponent { + fn from(access_control: AccessControl) -> Self { + match access_control { + AccessControl::Ownable2Step { owner } => Ownable2Step::new(owner).into(), + } + } +} + pub use ownable2step::{Ownable2Step, Ownable2StepError}; diff --git a/crates/miden-standards/src/account/access/ownable2step.rs b/crates/miden-standards/src/account/access/ownable2step.rs index ec3668a7a4..c61f6f9bb8 100644 --- a/crates/miden-standards/src/account/access/ownable2step.rs +++ b/crates/miden-standards/src/account/access/ownable2step.rs @@ -1,9 +1,23 @@ -use miden_protocol::account::component::{FeltSchema, StorageSlotSchema}; -use miden_protocol::account::{AccountId, AccountStorage, StorageSlot, StorageSlotName}; +use miden_protocol::account::component::{ + AccountComponentMetadata, + FeltSchema, + StorageSchema, + StorageSlotSchema, +}; +use miden_protocol::account::{ + AccountComponent, + AccountId, + AccountStorage, + AccountType, + StorageSlot, + StorageSlotName, +}; use miden_protocol::errors::AccountIdError; use miden_protocol::utils::sync::LazyLock; use miden_protocol::{Felt, Word}; +use crate::account::components::ownable2step_library; + static OWNER_CONFIG_SLOT_NAME: LazyLock = LazyLock::new(|| { StorageSlotName::new("miden::standards::access::ownable2step::owner_config") .expect("storage slot name should be valid") @@ -30,6 +44,9 @@ pub struct Ownable2Step { } impl Ownable2Step { + /// The name of the component. + pub const NAME: &'static str = "miden::standards::components::access::ownable2step"; + // CONSTRUCTORS // -------------------------------------------------------------------------------------------- @@ -119,6 +136,23 @@ impl Ownable2Step { } } +impl From for AccountComponent { + fn from(ownership: Ownable2Step) -> Self { + let storage_slot = ownership.to_storage_slot(); + + let storage_schema = StorageSchema::new([Ownable2Step::slot_schema()]) + .expect("storage schema should be valid"); + + let metadata = AccountComponentMetadata::new(Ownable2Step::NAME, AccountType::all()) + .with_description("Two-step ownership management component") + .with_storage_schema(storage_schema); + + AccountComponent::new(ownable2step_library(), vec![storage_slot], metadata).expect( + "Ownable2Step component should satisfy the requirements of a valid account component", + ) + } +} + // OWNABLE2STEP ERROR // ================================================================================================ diff --git a/crates/miden-standards/src/account/components/mod.rs b/crates/miden-standards/src/account/components/mod.rs index f7783a46db..b840ce7ac5 100644 --- a/crates/miden-standards/src/account/components/mod.rs +++ b/crates/miden-standards/src/account/components/mod.rs @@ -22,6 +22,18 @@ static BASIC_WALLET_LIBRARY: LazyLock = LazyLock::new(|| { Library::read_from_bytes(bytes).expect("Shipped Basic Wallet library is well-formed") }); +// ACCESS LIBRARIES +// ================================================================================================ + +// Initialize the Ownable2Step library only once. +static OWNABLE2STEP_LIBRARY: LazyLock = LazyLock::new(|| { + let bytes = include_bytes!(concat!( + env!("OUT_DIR"), + "/assets/account_components/access/ownable2step.masl" + )); + Library::read_from_bytes(bytes).expect("Shipped Ownable2Step library is well-formed") +}); + // AUTH LIBRARIES // ================================================================================================ @@ -102,6 +114,11 @@ pub fn basic_wallet_library() -> Library { BASIC_WALLET_LIBRARY.clone() } +/// Returns the Ownable2Step Library. +pub fn ownable2step_library() -> Library { + OWNABLE2STEP_LIBRARY.clone() +} + /// Returns the Basic Fungible Faucet Library. pub fn basic_fungible_faucet_library() -> Library { BASIC_FUNGIBLE_FAUCET_LIBRARY.clone() diff --git a/crates/miden-standards/src/account/faucets/network_fungible.rs b/crates/miden-standards/src/account/faucets/network_fungible.rs index 8da5d75bb9..86a4144c98 100644 --- a/crates/miden-standards/src/account/faucets/network_fungible.rs +++ b/crates/miden-standards/src/account/faucets/network_fungible.rs @@ -9,7 +9,6 @@ use miden_protocol::account::{ Account, AccountBuilder, AccountComponent, - AccountId, AccountStorage, AccountStorageMode, AccountType, @@ -19,7 +18,7 @@ use miden_protocol::asset::TokenSymbol; use miden_protocol::{Felt, Word}; use super::{FungibleFaucetError, TokenMetadata}; -use crate::account::access::Ownable2Step; +use crate::account::access::AccessControl; use crate::account::auth::NoAuth; use crate::account::components::network_fungible_faucet_library; use crate::account::interface::{AccountComponentInterface, AccountInterface, AccountInterfaceExt}; @@ -60,18 +59,17 @@ procedure_digest!( /// authentication while `burn` does not require authentication and can be called by anyone. /// Thus, this component must be combined with a component providing authentication. /// -/// Ownership is managed via a two-step transfer pattern ([`Ownable2Step`]). The current owner -/// must first nominate a new owner, who then accepts the transfer. +/// This component relies on [`crate::account::access::Ownable2Step`] for ownership checks in +/// `distribute`. When building an account with this component, +/// [`crate::account::access::Ownable2Step`] must also be included. /// /// ## Storage Layout /// /// - [`Self::metadata_slot`]: Fungible faucet metadata. -/// - [`Ownable2Step::slot_name`]: The owner and nominated owner of this network faucet. /// /// [builder]: crate::code_builder::CodeBuilder pub struct NetworkFungibleFaucet { metadata: TokenMetadata, - ownership: Ownable2Step, } impl NetworkFungibleFaucet { @@ -101,20 +99,17 @@ impl NetworkFungibleFaucet { symbol: TokenSymbol, decimals: u8, max_supply: Felt, - owner_account_id: AccountId, ) -> Result { let metadata = TokenMetadata::new(symbol, decimals, max_supply)?; - let ownership = Ownable2Step::new(owner_account_id); - Ok(Self { metadata, ownership }) + Ok(Self { metadata }) } /// Creates a new [`NetworkFungibleFaucet`] component from the given [`TokenMetadata`]. /// /// This is a convenience constructor that allows creating a faucet from pre-validated /// metadata. - pub fn from_metadata(metadata: TokenMetadata, owner_account_id: AccountId) -> Self { - let ownership = Ownable2Step::new(owner_account_id); - Self { metadata, ownership } + pub fn from_metadata(metadata: TokenMetadata) -> Self { + Self { metadata } } /// Attempts to create a new [`NetworkFungibleFaucet`] component from the associated account @@ -145,11 +140,7 @@ impl NetworkFungibleFaucet { // Read token metadata from storage let metadata = TokenMetadata::try_from(storage)?; - // Read ownership data from storage - let ownership = - Ownable2Step::try_from_storage(storage).map_err(FungibleFaucetError::OwnershipError)?; - - Ok(Self { metadata, ownership }) + Ok(Self { metadata }) } // PUBLIC ACCESSORS @@ -207,21 +198,6 @@ impl NetworkFungibleFaucet { self.metadata.token_supply() } - /// Returns the owner account ID of the faucet, or `None` if ownership has been renounced. - pub fn owner_account_id(&self) -> Option { - self.ownership.owner() - } - - /// Returns the nominated owner account ID, or `None` if no transfer is in progress. - pub fn nominated_owner(&self) -> Option { - self.ownership.nominated_owner() - } - - /// Returns the ownership data of the faucet. - pub fn ownership(&self) -> &Ownable2Step { - &self.ownership - } - /// Returns the digest of the `distribute` account procedure. pub fn distribute_digest() -> Word { *NETWORK_FUNGIBLE_FAUCET_DISTRIBUTE @@ -250,13 +226,8 @@ impl NetworkFungibleFaucet { impl From for AccountComponent { fn from(network_faucet: NetworkFungibleFaucet) -> Self { let metadata_slot = network_faucet.metadata.into(); - let owner_slot = network_faucet.ownership.to_storage_slot(); - - let storage_schema = StorageSchema::new([ - NetworkFungibleFaucet::metadata_slot_schema(), - Ownable2Step::slot_schema(), - ]) - .expect("storage schema should be valid"); + let storage_schema = StorageSchema::new([NetworkFungibleFaucet::metadata_slot_schema()]) + .expect("storage schema should be valid"); let metadata = AccountComponentMetadata::new( NetworkFungibleFaucet::NAME, @@ -267,7 +238,7 @@ impl From for AccountComponent { AccountComponent::new( network_fungible_faucet_library(), - vec![metadata_slot, owner_slot], + vec![metadata_slot], metadata, ) .expect("network fungible faucet component should satisfy the requirements of a valid account component") @@ -295,7 +266,7 @@ impl TryFrom<&Account> for NetworkFungibleFaucet { } /// Creates a new faucet account with network fungible faucet interface and provided metadata -/// (token symbol, decimals, max supply, owner account ID). +/// (token symbol, decimals, max supply) and access control. /// /// The network faucet interface exposes two procedures: /// - `distribute`, which mints an assets and create a note for the provided recipient. @@ -309,14 +280,15 @@ impl TryFrom<&Account> for NetworkFungibleFaucet { /// - [`AccountStorageMode::Network`] for storage /// - [`NoAuth`] for authentication /// -/// The storage layout of the faucet account is documented on the [`NetworkFungibleFaucet`] type and -/// contains no additional storage slots for its auth ([`NoAuth`]). +/// The storage layout of the faucet account is documented on the [`NetworkFungibleFaucet`] and +/// [`crate::account::access::Ownable2Step`] types, and contains no additional storage slots for +/// its auth ([`NoAuth`]). pub fn create_network_fungible_faucet( init_seed: [u8; 32], symbol: TokenSymbol, decimals: u8, max_supply: Felt, - owner_account_id: AccountId, + access_control: AccessControl, ) -> Result { let auth_component: AccountComponent = NoAuth::new().into(); @@ -324,9 +296,58 @@ pub fn create_network_fungible_faucet( .account_type(AccountType::FungibleFaucet) .storage_mode(AccountStorageMode::Network) .with_auth_component(auth_component) - .with_component(NetworkFungibleFaucet::new(symbol, decimals, max_supply, owner_account_id)?) + .with_component(NetworkFungibleFaucet::new(symbol, decimals, max_supply)?) + .with_component(access_control) .build() .map_err(FungibleFaucetError::AccountError)?; Ok(account) } + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use miden_protocol::account::{AccountId, AccountIdVersion, AccountStorageMode, AccountType}; + + use super::*; + use crate::account::access::Ownable2Step; + + #[test] + fn test_create_network_fungible_faucet() { + let init_seed = [7u8; 32]; + let symbol = TokenSymbol::new("NET").expect("token symbol should be valid"); + let decimals = 8u8; + let max_supply = Felt::new(1_000); + + let owner = AccountId::dummy( + [1u8; 15], + AccountIdVersion::Version0, + AccountType::RegularAccountImmutableCode, + AccountStorageMode::Private, + ); + + let account = create_network_fungible_faucet( + init_seed, + symbol, + decimals, + max_supply, + AccessControl::Ownable2Step { owner }, + ) + .expect("network faucet creation should succeed"); + + let expected_owner_word = Ownable2Step::new(owner).to_word(); + assert_eq!( + account.storage().get_item(Ownable2Step::slot_name()).unwrap(), + expected_owner_word + ); + + let faucet = NetworkFungibleFaucet::try_from(&account) + .expect("network fungible faucet should be extractable from account"); + assert_eq!(faucet.symbol(), symbol); + assert_eq!(faucet.decimals(), decimals); + assert_eq!(faucet.max_supply(), max_supply); + assert_eq!(faucet.token_supply(), Felt::ZERO); + } +} diff --git a/crates/miden-testing/src/mock_chain/chain_builder.rs b/crates/miden-testing/src/mock_chain/chain_builder.rs index a433e40954..84dc5ba520 100644 --- a/crates/miden-testing/src/mock_chain/chain_builder.rs +++ b/crates/miden-testing/src/mock_chain/chain_builder.rs @@ -46,6 +46,7 @@ use miden_protocol::testing::account_id::ACCOUNT_ID_NATIVE_ASSET_FAUCET; use miden_protocol::testing::random_secret_key::random_secret_key; use miden_protocol::transaction::{OrderedTransactionHeaders, RawOutputNote, TransactionKernel}; use miden_protocol::{Felt, MAX_OUTPUT_NOTES_PER_BATCH, Word}; +use miden_standards::account::access::Ownable2Step; use miden_standards::account::faucets::{BasicFungibleFaucet, NetworkFungibleFaucet}; use miden_standards::account::wallets::BasicWallet; use miden_standards::note::{P2idNote, P2ideNote, P2ideNoteStorage, SwapNote}; @@ -380,18 +381,15 @@ impl MockChainBuilder { let token_symbol = TokenSymbol::new(token_symbol).context("failed to create token symbol")?; - let network_faucet = NetworkFungibleFaucet::new( - token_symbol, - DEFAULT_FAUCET_DECIMALS, - max_supply, - owner_account_id, - ) - .and_then(|fungible_faucet| fungible_faucet.with_token_supply(token_supply)) - .context("failed to create network fungible faucet")?; + let network_faucet = + NetworkFungibleFaucet::new(token_symbol, DEFAULT_FAUCET_DECIMALS, max_supply) + .and_then(|fungible_faucet| fungible_faucet.with_token_supply(token_supply)) + .context("failed to create network fungible faucet")?; let account_builder = AccountBuilder::new(self.rng.random()) .storage_mode(AccountStorageMode::Network) .with_component(network_faucet) + .with_component(Ownable2Step::new(owner_account_id)) .account_type(AccountType::FungibleFaucet); // Network faucets always use IncrNonce auth (no authentication) diff --git a/crates/miden-testing/tests/scripts/faucet.rs b/crates/miden-testing/tests/scripts/faucet.rs index fce161f5af..dd80e4f73e 100644 --- a/crates/miden-testing/tests/scripts/faucet.rs +++ b/crates/miden-testing/tests/scripts/faucet.rs @@ -849,13 +849,13 @@ async fn test_network_faucet_transfer_ownership() -> anyhow::Result<()> { // Step 1: Create transfer_ownership note script to nominate new owner let transfer_note_script_code = format!( r#" - use miden::standards::faucets::network_fungible->network_faucet + use miden::standards::access::ownable2step begin repeat.14 push.0 end push.{new_owner_prefix} push.{new_owner_suffix} - call.network_faucet::transfer_ownership + call.ownable2step::transfer_ownership dropw dropw dropw dropw end "#, @@ -902,11 +902,11 @@ async fn test_network_faucet_transfer_ownership() -> anyhow::Result<()> { // Step 2: Accept ownership as the nominated owner let accept_note_script_code = r#" - use miden::standards::faucets::network_fungible->network_faucet + use miden::standards::access::ownable2step begin repeat.16 push.0 end - call.network_faucet::accept_ownership + call.ownable2step::accept_ownership dropw dropw dropw dropw end "#; @@ -971,13 +971,13 @@ async fn test_network_faucet_only_owner_can_transfer() -> anyhow::Result<()> { // Create transfer ownership note script let transfer_note_script_code = format!( r#" - use miden::standards::faucets::network_fungible->network_faucet + use miden::standards::access::ownable2step begin repeat.14 push.0 end push.{new_owner_prefix} push.{new_owner_suffix} - call.network_faucet::transfer_ownership + call.ownable2step::transfer_ownership dropw dropw dropw dropw end "#, @@ -1035,11 +1035,11 @@ async fn test_network_faucet_renounce_ownership() -> anyhow::Result<()> { // Create renounce_ownership note script let renounce_note_script_code = r#" - use miden::standards::faucets::network_fungible->network_faucet + use miden::standards::access::ownable2step begin repeat.16 push.0 end - call.network_faucet::renounce_ownership + call.ownable2step::renounce_ownership dropw dropw dropw dropw end "#; @@ -1049,13 +1049,13 @@ async fn test_network_faucet_renounce_ownership() -> anyhow::Result<()> { // Create transfer note script (will be used after renounce) let transfer_note_script_code = format!( r#" - use miden::standards::faucets::network_fungible->network_faucet + use miden::standards::access::ownable2step begin repeat.14 push.0 end push.{new_owner_prefix} push.{new_owner_suffix} - call.network_faucet::transfer_ownership + call.ownable2step::transfer_ownership dropw dropw dropw dropw end "#,