Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions crates/miden-protocol/src/transaction/proven_tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<NoteHeader>) -> Self {
Self { nullifier, header }
}

/// Returns the nullifier of the input note committed to by this commitment.
pub fn nullifier(&self) -> Nullifier {
self.nullifier
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ------------------------------------------------------------------------------------------------

Expand Down
17 changes: 17 additions & 0 deletions crates/miden-standards/src/account/access/mod.rs
Original file line number Diff line number Diff line change
@@ -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<AccessControl> for AccountComponent {
fn from(access_control: AccessControl) -> Self {
match access_control {
AccessControl::Ownable2Step { owner } => Ownable2Step::new(owner).into(),
}
}
}

pub use ownable2step::{Ownable2Step, Ownable2StepError};
38 changes: 36 additions & 2 deletions crates/miden-standards/src/account/access/ownable2step.rs
Original file line number Diff line number Diff line change
@@ -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<StorageSlotName> = LazyLock::new(|| {
StorageSlotName::new("miden::standards::access::ownable2step::owner_config")
.expect("storage slot name should be valid")
Expand All @@ -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
// --------------------------------------------------------------------------------------------

Expand Down Expand Up @@ -119,6 +136,23 @@ impl Ownable2Step {
}
}

impl From<Ownable2Step> 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
// ================================================================================================

Expand Down
17 changes: 17 additions & 0 deletions crates/miden-standards/src/account/components/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ static BASIC_WALLET_LIBRARY: LazyLock<Library> = 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<Library> = 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
// ================================================================================================

Expand Down Expand Up @@ -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()
Expand Down
111 changes: 66 additions & 45 deletions crates/miden-standards/src/account/faucets/network_fungible.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ use miden_protocol::account::{
Account,
AccountBuilder,
AccountComponent,
AccountId,
AccountStorage,
AccountStorageMode,
AccountType,
Expand All @@ -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};
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -101,20 +99,17 @@ impl NetworkFungibleFaucet {
symbol: TokenSymbol,
decimals: u8,
max_supply: Felt,
owner_account_id: AccountId,
) -> Result<Self, FungibleFaucetError> {
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<AccountId> {
self.ownership.owner()
}

/// Returns the nominated owner account ID, or `None` if no transfer is in progress.
pub fn nominated_owner(&self) -> Option<AccountId> {
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
Expand Down Expand Up @@ -250,13 +226,8 @@ impl NetworkFungibleFaucet {
impl From<NetworkFungibleFaucet> 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,
Expand All @@ -267,7 +238,7 @@ impl From<NetworkFungibleFaucet> 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")
Expand Down Expand Up @@ -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.
Expand All @@ -309,24 +280,74 @@ 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<Account, FungibleFaucetError> {
let auth_component: AccountComponent = NoAuth::new().into();

let account = AccountBuilder::new(init_seed)
.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);
}
}
Loading
Loading