From 453dbe65fc40cfc1a87683c370669e9d2e60b4ac Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Fri, 6 Mar 2026 11:24:53 +0100 Subject: [PATCH 01/41] feat: add `AssetCallbacks` and integrate into vault key --- .../src/asset/asset_callbacks.rs | 38 ++++++ crates/miden-protocol/src/asset/mod.rs | 3 + .../miden-protocol/src/asset/nonfungible.rs | 4 +- .../src/asset/vault/vault_key.rs | 116 +++++++++++++++--- crates/miden-protocol/src/errors/mod.rs | 2 + 5 files changed, 142 insertions(+), 21 deletions(-) create mode 100644 crates/miden-protocol/src/asset/asset_callbacks.rs diff --git a/crates/miden-protocol/src/asset/asset_callbacks.rs b/crates/miden-protocol/src/asset/asset_callbacks.rs new file mode 100644 index 0000000000..0024afbd45 --- /dev/null +++ b/crates/miden-protocol/src/asset/asset_callbacks.rs @@ -0,0 +1,38 @@ +use crate::errors::AssetError; + +const CALLBACKS_DISABLED: u8 = 0; +const CALLBACKS_ENABLED: u8 = 1; + +/// Whether callbacks are enabled for assets. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +#[repr(u8)] +pub enum AssetCallbacks { + #[default] + Disabled = CALLBACKS_DISABLED, + + Enabled = CALLBACKS_ENABLED, +} + +impl AssetCallbacks { + /// Encodes the callbacks setting as a `u8`. + pub const fn as_u8(&self) -> u8 { + *self as u8 + } +} + +impl TryFrom for AssetCallbacks { + type Error = AssetError; + + /// Decodes a callbacks setting from a `u8`. + /// + /// # Errors + /// + /// Returns an error if the value is not a valid callbacks encoding. + fn try_from(value: u8) -> Result { + match value { + CALLBACKS_DISABLED => Ok(Self::Disabled), + CALLBACKS_ENABLED => Ok(Self::Enabled), + _ => Err(AssetError::InvalidAssetCallbacks(value)), + } + } +} diff --git a/crates/miden-protocol/src/asset/mod.rs b/crates/miden-protocol/src/asset/mod.rs index 9b9662d950..a8ddabcc2d 100644 --- a/crates/miden-protocol/src/asset/mod.rs +++ b/crates/miden-protocol/src/asset/mod.rs @@ -21,6 +21,9 @@ pub use nonfungible::{NonFungibleAsset, NonFungibleAssetDetails}; mod token_symbol; pub use token_symbol::TokenSymbol; +mod asset_callbacks; +pub use asset_callbacks::AssetCallbacks; + mod vault; pub use vault::{AssetId, AssetVault, AssetVaultKey, AssetWitness, PartialVault}; diff --git a/crates/miden-protocol/src/asset/nonfungible.rs b/crates/miden-protocol/src/asset/nonfungible.rs index dbdaf3c0c5..77333f4fab 100644 --- a/crates/miden-protocol/src/asset/nonfungible.rs +++ b/crates/miden-protocol/src/asset/nonfungible.rs @@ -112,7 +112,7 @@ impl NonFungibleAsset { let asset_id_prefix = self.value[1]; let asset_id = AssetId::new(asset_id_suffix, asset_id_prefix); - AssetVaultKey::new(asset_id, self.faucet_id) + AssetVaultKey::new_native(asset_id, self.faucet_id) .expect("constructors should ensure account ID is of type non-fungible faucet") } @@ -239,7 +239,7 @@ mod tests { #[test] fn fungible_asset_from_key_value_fails_on_invalid_asset_id() -> anyhow::Result<()> { - let invalid_key = AssetVaultKey::new( + let invalid_key = AssetVaultKey::new_native( AssetId::new(Felt::from(1u32), Felt::from(2u32)), ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET.try_into()?, )?; diff --git a/crates/miden-protocol/src/asset/vault/vault_key.rs b/crates/miden-protocol/src/asset/vault/vault_key.rs index 8ccf595d36..6d67b0f476 100644 --- a/crates/miden-protocol/src/asset/vault/vault_key.rs +++ b/crates/miden-protocol/src/asset/vault/vault_key.rs @@ -7,7 +7,7 @@ use miden_crypto::merkle::smt::LeafIndex; use crate::account::AccountId; use crate::account::AccountType::{self}; use crate::asset::vault::AssetId; -use crate::asset::{Asset, FungibleAsset, NonFungibleAsset}; +use crate::asset::{Asset, AssetCallbacks, FungibleAsset, NonFungibleAsset}; use crate::crypto::merkle::smt::SMT_DEPTH; use crate::errors::AssetError; use crate::{Felt, Word}; @@ -19,12 +19,10 @@ use crate::{Felt, Word}; /// [ /// asset_id_suffix (64 bits), /// asset_id_prefix (64 bits), -/// faucet_id_suffix (56 bits), +/// [faucet_id_suffix (56 bits) | 7 zero bits | callbacks_flag (1 bit)], /// faucet_id_prefix (64 bits) /// ] /// ``` -/// -/// See the [`Asset`] documentation for the differences between fungible and non-fungible assets. #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub struct AssetVaultKey { /// The asset ID of the vault key. @@ -32,10 +30,16 @@ pub struct AssetVaultKey { /// The ID of the faucet that issued the asset. faucet_id: AccountId, + + /// Determines whether callbacks are enabled. + callbacks: AssetCallbacks, } impl AssetVaultKey { - /// Creates an [`AssetVaultKey`] from its parts. + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates an [`AssetVaultKey`] for a native asset with callbacks disabled. /// /// # Errors /// @@ -45,7 +49,25 @@ impl AssetVaultKey { /// [`AccountType::NonFungibleFaucet`](crate::account::AccountType::NonFungibleFaucet) /// - the asset ID limbs are not zero when `faucet_id` is of type /// [`AccountType::FungibleFaucet`](crate::account::AccountType::FungibleFaucet). - pub fn new(asset_id: AssetId, faucet_id: AccountId) -> Result { + pub fn new_native(asset_id: AssetId, faucet_id: AccountId) -> Result { + Self::new(asset_id, faucet_id, AssetCallbacks::Disabled) + } + + /// Creates an [`AssetVaultKey`] from its parts with the given [`AssetCallbacks`]. + /// + /// # Errors + /// + /// Returns an error if: + /// - the provided ID is not of type + /// [`AccountType::FungibleFaucet`](crate::account::AccountType::FungibleFaucet) or + /// [`AccountType::NonFungibleFaucet`](crate::account::AccountType::NonFungibleFaucet) + /// - the asset ID limbs are not zero when `faucet_id` is of type + /// [`AccountType::FungibleFaucet`](crate::account::AccountType::FungibleFaucet). + pub fn new( + asset_id: AssetId, + faucet_id: AccountId, + callbacks: AssetCallbacks, + ) -> Result { if !faucet_id.is_faucet() { return Err(AssetError::InvalidFaucetAccountId(Box::from(format!( "expected account ID of type faucet, found account type {}", @@ -57,14 +79,30 @@ impl AssetVaultKey { return Err(AssetError::FungibleAssetIdMustBeZero(asset_id)); } - Ok(Self { asset_id, faucet_id }) + Ok(Self { asset_id, faucet_id, callbacks }) } + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + /// Returns the word representation of the vault key. /// /// See the type-level documentation for details. - pub fn to_word(self) -> Word { - vault_key_to_word(self.asset_id, self.faucet_id) + pub fn to_word(&self) -> Word { + let faucet_suffix = self.faucet_id.suffix().as_canonical_u64(); + // The lower 8 bits of the faucet suffix are guaranteed to be zero and so it is used to + // encode the asset metadata. + debug_assert!(faucet_suffix & 0xff == 0, "lower 8 bits of faucet suffix must be zero"); + let faucet_id_suffix_and_metadata = faucet_suffix | self.callbacks.as_u8() as u64; + let faucet_id_suffix_and_metadata = Felt::try_from(faucet_id_suffix_and_metadata) + .expect("highest bit should still be zero resulting in a valid felt"); + + Word::new([ + self.asset_id.suffix(), + self.asset_id.prefix(), + faucet_id_suffix_and_metadata, + self.faucet_id.prefix().as_felt(), + ]) } /// Returns the [`AssetId`] of the vault key that distinguishes different assets issued by the @@ -78,6 +116,11 @@ impl AssetVaultKey { self.faucet_id } + /// Returns the [`AssetCallbacks`] flag of the vault key. + pub fn callbacks(&self) -> AssetCallbacks { + self.callbacks + } + /// Constructs a fungible asset's key from a faucet ID. /// /// Returns `None` if the provided ID is not of type @@ -86,7 +129,7 @@ impl AssetVaultKey { if matches!(faucet_id.account_type(), AccountType::FungibleFaucet) { let asset_id = AssetId::new(Felt::ZERO, Felt::ZERO); Some( - Self::new(asset_id, faucet_id) + Self::new_native(asset_id, faucet_id) .expect("we should have account type fungible faucet"), ) } else { @@ -136,14 +179,19 @@ impl TryFrom for AssetVaultKey { fn try_from(key: Word) -> Result { let asset_id_suffix = key[0]; let asset_id_prefix = key[1]; - let faucet_id_suffix = key[2]; + let faucet_id_suffix_and_metadata = key[2]; let faucet_id_prefix = key[3]; + let raw = faucet_id_suffix_and_metadata.as_canonical_u64(); + let category = AssetCallbacks::try_from((raw & 0xff) as u8)?; + let faucet_id_suffix = Felt::try_from(raw & 0xffff_ffff_ffff_ff00) + .expect("clearing lower bits should not produce an invalid felt"); + let asset_id = AssetId::new(asset_id_suffix, asset_id_prefix); let faucet_id = AccountId::try_from_elements(faucet_id_suffix, faucet_id_prefix) .map_err(|err| AssetError::InvalidFaucetAccountId(Box::new(err)))?; - Self::new(asset_id, faucet_id) + Self::new(asset_id, faucet_id, category) } } @@ -171,11 +219,41 @@ impl From for AssetVaultKey { } } -fn vault_key_to_word(asset_id: AssetId, faucet_id: AccountId) -> Word { - Word::new([ - asset_id.suffix(), - asset_id.prefix(), - faucet_id.suffix(), - faucet_id.prefix().as_felt(), - ]) +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use super::*; + use crate::asset::AssetCallbacks; + use crate::testing::account_id::{ + ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, + ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET, + }; + + #[test] + fn asset_vault_key_word_roundtrip() { + let fungible_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); + let nonfungible_faucet = + AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET).unwrap(); + + for callbacks in [AssetCallbacks::Disabled, AssetCallbacks::Enabled] { + // Fungible: asset_id must be zero. + let key = AssetVaultKey::new(AssetId::default(), fungible_faucet, callbacks).unwrap(); + + let roundtripped = AssetVaultKey::try_from(key.to_word()).unwrap(); + assert_eq!(key, roundtripped); + + // Non-fungible: asset_id can be non-zero. + let key = AssetVaultKey::new( + AssetId::new(Felt::from(42u32), Felt::from(99u32)), + nonfungible_faucet, + callbacks, + ) + .unwrap(); + + let roundtripped = AssetVaultKey::try_from(key.to_word()).unwrap(); + assert_eq!(key, roundtripped); + } + } } diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index 04ebc56285..88f55b0a13 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -488,6 +488,8 @@ pub enum AssetError { NonFungibleFaucetIdTypeMismatch(AccountId), #[error("smt proof in asset witness contains invalid key or value")] AssetWitnessInvalid(#[source] Box), + #[error("invalid native asset callbacks encoding: {0}")] + InvalidAssetCallbacks(u8), } // TOKEN SYMBOL ERROR From f12f15f37c0c49a0377b84b38e3ffd5c85ae151b Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Fri, 6 Mar 2026 15:34:58 +0100 Subject: [PATCH 02/41] feat: add metadata extraction code in MASM --- .../asm/kernels/transaction/lib/asset.masm | 1 + crates/miden-protocol/asm/protocol/asset.masm | 1 + .../asm/shared_utils/util/asset.masm | 76 ++++++++++++++++++- .../src/kernel_tests/tx/test_asset.rs | 45 ++++++++++- .../src/kernel_tests/tx/test_faucet.rs | 44 ++++++++++- 5 files changed, 163 insertions(+), 4 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/asset.masm b/crates/miden-protocol/asm/kernels/transaction/lib/asset.masm index 193af534b7..b45b093c61 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/asset.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/asset.masm @@ -17,6 +17,7 @@ pub use ::$kernel::util::asset::key_to_faucet_id pub use ::$kernel::util::asset::key_into_faucet_id pub use ::$kernel::util::asset::key_to_asset_id pub use ::$kernel::util::asset::key_into_asset_id +pub use ::$kernel::util::asset::key_to_callbacks_enabled pub use ::$kernel::util::asset::store pub use ::$kernel::util::asset::load diff --git a/crates/miden-protocol/asm/protocol/asset.masm b/crates/miden-protocol/asm/protocol/asset.masm index 04211d61b1..df9032f43e 100644 --- a/crates/miden-protocol/asm/protocol/asset.masm +++ b/crates/miden-protocol/asm/protocol/asset.masm @@ -11,6 +11,7 @@ pub use ::miden::protocol::util::asset::key_to_faucet_id pub use ::miden::protocol::util::asset::key_into_faucet_id pub use ::miden::protocol::util::asset::key_to_asset_id pub use ::miden::protocol::util::asset::key_into_asset_id +pub use ::miden::protocol::util::asset::key_to_callbacks_enabled pub use ::miden::protocol::util::asset::store pub use ::miden::protocol::util::asset::load pub use ::miden::protocol::util::asset::fungible_value_into_amount diff --git a/crates/miden-protocol/asm/shared_utils/util/asset.masm b/crates/miden-protocol/asm/shared_utils/util/asset.masm index 620f70cf0e..f7d2fdab77 100644 --- a/crates/miden-protocol/asm/shared_utils/util/asset.masm +++ b/crates/miden-protocol/asm/shared_utils/util/asset.masm @@ -100,9 +100,12 @@ end #! - faucet_id is the account ID in the vault key. #! - ASSET_KEY is the vault key from which to extract the faucet ID. pub proc key_to_faucet_id - # => [asset_id_suffix, asset_id_prefix, faucet_id_suffix, faucet_id_prefix] + # => [asset_id_suffix, asset_id_prefix, faucet_id_suffix_and_metadata, faucet_id_prefix] dup.3 dup.3 + # => [faucet_id_suffix_and_metadata, faucet_id_prefix, ASSET_KEY] + + exec.split_suffix_and_metadata drop # => [faucet_id_suffix, faucet_id_prefix, ASSET_KEY] end @@ -117,9 +120,12 @@ end #! - faucet_id is the account ID in the vault key. #! - ASSET_KEY is the vault key from which to extract the faucet ID. pub proc key_into_faucet_id - # => [asset_id_suffix, asset_id_prefix, faucet_id_suffix, faucet_id_prefix] + # => [asset_id_suffix, asset_id_prefix, faucet_id_suffix_and_metadata, faucet_id_prefix] drop drop + # => [faucet_id_suffix_and_metadata, faucet_id_prefix] + + exec.split_suffix_and_metadata drop # => [faucet_id_suffix, faucet_id_prefix] end @@ -153,6 +159,27 @@ pub proc key_into_asset_id # => [asset_id_suffix, asset_id_prefix] end +#! Returns the asset callbacks flag from an asset vault key. +#! +#! Inputs: [ASSET_KEY] +#! Outputs: [callbacks_enabled, ASSET_KEY] +#! +#! Where: +#! - ASSET_KEY is the vault key from which to extract the metadata. +#! - callbacks_enabled is 1 if callbacks are enabled and 0 if disabled. +pub proc key_to_callbacks_enabled + # => [asset_id_suffix, asset_id_prefix, faucet_id_suffix_and_metadata, faucet_id_prefix] + + dup.2 + # => [faucet_id_suffix_and_metadata, ASSET_KEY] + + exec.split_suffix_and_metadata swap drop + # => [asset_metadata, ASSET_KEY] + + exec.metadata_into_callbacks_enabled + # => [callbacks_enabled, ASSET_KEY] +end + #! Creates a fungible asset vault key for the specified faucet. #! #! Inputs: [faucet_id_suffix, faucet_id_prefix] @@ -215,3 +242,48 @@ pub proc create_non_fungible_asset_unchecked # => [[hash0, hash1, faucet_id_suffix, faucet_id_prefix], DATA_HASH] # => [ASSET_KEY, ASSET_VALUE] end + +#! Splits the merged faucet ID suffix and the asset metadata. +#! +#! Inputs: [faucet_id_suffix_and_metadata] +#! Outputs: [asset_metadata, faucet_id_suffix] +#! +#! Where: +#! - faucet_id_suffix_and_metadata is the faucet ID suffix merged with the asset metadata. +#! - faucet_id_suffix is the suffix of the account ID. +#! - asset_metadata is the asset metadata. +proc split_suffix_and_metadata + u32split + # => [suffix_metadata_lo, suffix_metadata_hi] + + dup movdn.2 + # => [suffix_metadata_lo, suffix_metadata_hi, suffix_metadata_lo] + + # clear lower 8 bits of the lo part to get the actual ID suffix + u32and.0xffffff00 swap + # => [suffix_metadata_hi, suffix_metadata_lo', suffix_metadata_lo] + + # reassemble the ID suffix by multiplying the hi part with 2^32 and adding the lo part + mul.0x0100000000 add + # => [faucet_id_suffix, suffix_metadata_lo] + + # extract lower 8 bits of the lo part to get the metadata + swap u32and.0x000000ff + # => [asset_metadata, faucet_id_suffix] +end + +#! Extracts the asset callback flag from asset metadata. +#! +#! WARNING: asset_metadata is assumed to be a byte (in particular a valid u32) +#! +#! Inputs: [asset_metadata] +#! Outputs: [callbacks_enabled] +#! +#! Where: +#! - asset_metadata is the asset metadata. +#! - callbacks_enabled is 1 if callbacks are enabled and 0 if disabled. +proc metadata_into_callbacks_enabled + # extract the least significant bit of the metadata + u32and.1 + # => [callbacks_enabled] +end diff --git a/crates/miden-testing/src/kernel_tests/tx/test_asset.rs b/crates/miden-testing/src/kernel_tests/tx/test_asset.rs index bfdddbc0ca..ac1d495503 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_asset.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_asset.rs @@ -1,5 +1,12 @@ use miden_protocol::account::AccountId; -use miden_protocol::asset::{AssetId, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails}; +use miden_protocol::asset::{ + AssetCallbacks, + AssetId, + AssetVaultKey, + FungibleAsset, + NonFungibleAsset, + NonFungibleAssetDetails, +}; use miden_protocol::errors::MasmError; use miden_protocol::errors::tx_kernel::{ ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_AMOUNT, @@ -220,3 +227,39 @@ async fn test_validate_fungible_asset( Ok(()) } + +#[rstest::rstest] +#[case::without_callbacks(AssetCallbacks::Disabled)] +#[case::with_callbacks(AssetCallbacks::Enabled)] +#[tokio::test] +async fn test_key_to_asset_metadata(#[case] callbacks: AssetCallbacks) -> anyhow::Result<()> { + let faucet_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?; + let vault_key = AssetVaultKey::new(AssetId::default(), faucet_id, callbacks)?; + + let code = format!( + " + use $kernel::asset + + begin + push.{ASSET_KEY} + exec.asset::key_to_callbacks_enabled + # => [callbacks_enabled, ASSET_KEY] + + # truncate stack + swapw dropw swap drop + # => [callbacks_enabled] + end + ", + ASSET_KEY = vault_key.to_word(), + ); + + let exec_output = CodeExecutor::with_default_host().run(&code).await?; + + assert_eq!( + exec_output.get_stack_element(0).as_canonical_u64(), + callbacks.as_u8() as u64, + "MASM key_to_asset_category returned wrong value for {callbacks:?}" + ); + + Ok(()) +} diff --git a/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs b/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs index 211a8731eb..3a0a195e79 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs @@ -2,7 +2,13 @@ use alloc::sync::Arc; use miden_protocol::account::{Account, AccountBuilder, AccountComponent, AccountId, AccountType}; use miden_protocol::assembly::DefaultSourceManager; -use miden_protocol::asset::{FungibleAsset, NonFungibleAsset}; +use miden_protocol::asset::{ + AssetCallbacks, + AssetId, + AssetVaultKey, + FungibleAsset, + NonFungibleAsset, +}; use miden_protocol::errors::tx_kernel::{ ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_AMOUNT, ERR_FUNGIBLE_ASSET_FAUCET_IS_NOT_ORIGIN, @@ -294,6 +300,42 @@ async fn mint_non_fungible_asset_fails_on_non_faucet_account() -> anyhow::Result Ok(()) } +/// Tests minting a fungible asset with callbacks enabled. +#[tokio::test] +async fn test_mint_fungible_asset_with_callbacks_enabled() -> anyhow::Result<()> { + let faucet_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); + let asset = FungibleAsset::new(faucet_id, FUNGIBLE_ASSET_AMOUNT)?; + + // Build a vault key with callbacks enabled. + let vault_key = AssetVaultKey::new(AssetId::default(), faucet_id, AssetCallbacks::Enabled)?; + + let code = format!( + r#" + use mock::faucet->mock_faucet + use $kernel::prologue + + begin + exec.prologue::prepare_transaction + + push.{FUNGIBLE_ASSET_VALUE} + push.{FUNGIBLE_ASSET_KEY} + call.mock_faucet::mint + + dropw dropw + end + "#, + FUNGIBLE_ASSET_KEY = vault_key.to_word(), + FUNGIBLE_ASSET_VALUE = asset.to_value_word(), + ); + + TransactionContextBuilder::with_fungible_faucet(faucet_id.into()) + .build()? + .execute_code(&code) + .await?; + + Ok(()) +} + // FUNGIBLE FAUCET BURN TESTS // ================================================================================================ From 8dfb8b616dcafd0b38422b196e1db5a7cd609758 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 9 Mar 2026 13:57:51 +0100 Subject: [PATCH 03/41] feat: implement `on_asset_added_to_account` --- .../asm/kernels/transaction/lib/account.masm | 8 ++ .../kernels/transaction/lib/callbacks.masm | 102 ++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/account.masm b/crates/miden-protocol/asm/kernels/transaction/lib/account.masm index efa5408911..81b5811dc0 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/account.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/account.masm @@ -1,6 +1,7 @@ use $kernel::account_delta use $kernel::account_id use $kernel::asset_vault +use $kernel::callbacks use $kernel::constants::ACCOUNT_PROCEDURE_DATA_LENGTH use $kernel::constants::EMPTY_SMT_ROOT use $kernel::constants::STORAGE_SLOT_TYPE_MAP @@ -666,6 +667,13 @@ end #! added. #! - the vault already contains the same non-fungible asset. pub proc add_asset_to_vault + swapw dupw.1 + # => [ASSET_KEY, ASSET_VALUE, ASSET_KEY] + + exec.callbacks::on_asset_added_to_account + swapw + # => [ASSET_KEY, ASSET_VALUE] + # duplicate the asset for the later event and delta update dupw.1 dupw.1 # => [ASSET_KEY, ASSET_VALUE, ASSET_KEY, ASSET_VALUE] diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm b/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm new file mode 100644 index 0000000000..5f40cac666 --- /dev/null +++ b/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm @@ -0,0 +1,102 @@ +use $kernel::tx +use $kernel::asset +use $kernel::account +use miden::core::word + +# CONSTANTS +# ================================================================================================== + +# The index of the local memory slot that contains the procedure root of the callback. +const CALLBACK_PROC_ROOT_PTR = 0 + +# The name of the storage slot where the procedure root for the on_asset_added_to_account callback +# is stored. +const ON_ASSET_ADDED_TO_ACCOUNT_PROC_ROOT = word("miden::protocol::faucet::callbacks::on_asset_added_to_account") + +# PROCEDURES +# ================================================================================================== + +#! TODO +#! +#! Inputs: [ASSET_KEY, ASSET_VALUE] +#! Outputs: [PROCESSED_ASSET_VALUE] +pub proc on_asset_added_to_account + exec.asset::key_to_callbacks_enabled + # => [callbacks_enabled, ASSET_KEY, ASSET_VALUE] + + if.true + exec.on_asset_added_to_account_raw + # => [PROCESSED_ASSET_VALUE] + else + # drop asset key + dropw + # => [ASSET_VALUE] + end + # => [PROCESSED_ASSET_VALUE] +end + +#! Inputs: [ASSET_KEY, ASSET_VALUE] +#! Outputs: [PROCESSED_ASSET_VALUE] +@locals(4) +proc on_asset_added_to_account_raw + exec.start_foreign_callback_context + # => [native_account_id_suffix, native_account_id_prefix, ASSET_KEY, ASSET_VALUE] + + # get the procedure root of the callback from the faucet's storage + push.ON_ASSET_ADDED_TO_ACCOUNT_PROC_ROOT[0..2] + exec.account::get_item + # => [PROC_ROOT, native_account_id_suffix, native_account_id_prefix, ASSET_KEY, ASSET_VALUE] + + exec.word::testz + # => [is_empty_word, PROC_ROOT, native_account_id_suffix, native_account_id_prefix, ASSET_KEY, ASSET_VALUE] + + # only invoke the callback if it is not the empty word + if.true + # drop proc root, account ID and asset key + dropw drop drop dropw + # => [ASSET_VALUE] + else + # prepare for dyncall by storing procedure root in local memory + loc_storew_le.CALLBACK_PROC_ROOT_PTR dropw + # => [native_account_id_suffix, native_account_id_prefix, ASSET_KEY, ASSET_VALUE] + + # pad the stack to 16 for the call + repeat.6 push.0 movdn.10 end + locaddr.CALLBACK_PROC_ROOT_PTR + # => [callback_proc_root_ptr, native_account_id_suffix, native_account_id_prefix, ASSET_KEY, ASSET_VALUE, pad(6)] + + dyncall + # => [PROCESSED_ASSET_VALUE, pad(12)] + + # truncate the stack after the call + repeat.3 swapw dropw end + # => [PROCESSED_ASSET_VALUE] + end + # => [PROCESSED_ASSET_VALUE] + + exec.tx::end_foreign_context + # => [PROCESSED_ASSET_VALUE] +end + +#! Prepares the invocation of a faucet callback by: +#!- starting a foreign context against the faucet identified by the asset key's faucet ID. +#!- returning the native account's ID that is to be passed as an input to callbacks. +#! +#! Inputs: [ASSET_KEY, ASSET_VALUE] +#! Outputs: [native_account_id_suffix, native_account_id_prefix, ASSET_KEY, ASSET_VALUE] +proc start_foreign_callback_context + exec.asset::key_to_faucet_id + # => [faucet_id_suffix, faucet_id_prefix, ASSET_KEY, ASSET_VALUE] + + # get the ID of the active account that will be passed as an input to the callback + # this must be done before the foreign context is started + exec.account::get_id + # => [native_account_id_suffix, native_account_id_prefix, faucet_id_suffix, faucet_id_prefix, ASSET_KEY, ASSET_VALUE] + + movup.3 movup.3 + # => [faucet_id_suffix, faucet_id_prefix, native_account_id_suffix, native_account_id_prefix, ASSET_KEY, ASSET_VALUE] + + # start a foreign context against the faucet + exec.tx::start_foreign_context + # => [native_account_id_suffix, native_account_id_prefix, ASSET_KEY, ASSET_VALUE] +end From c56074d4e3072973899f9bfbe4cdf288ac0f1926 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 9 Mar 2026 14:19:08 +0100 Subject: [PATCH 04/41] chore: rename `AssetCallbacks` -> `AssetCallbacksFlag` --- .../src/asset/asset_callbacks.rs | 8 ++++---- crates/miden-protocol/src/asset/mod.rs | 2 +- .../src/asset/vault/vault_key.rs | 20 +++++++++---------- crates/miden-protocol/src/errors/mod.rs | 2 +- .../src/kernel_tests/tx/test_asset.rs | 8 ++++---- .../src/kernel_tests/tx/test_faucet.rs | 4 ++-- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/crates/miden-protocol/src/asset/asset_callbacks.rs b/crates/miden-protocol/src/asset/asset_callbacks.rs index 0024afbd45..c9eb19082d 100644 --- a/crates/miden-protocol/src/asset/asset_callbacks.rs +++ b/crates/miden-protocol/src/asset/asset_callbacks.rs @@ -6,21 +6,21 @@ const CALLBACKS_ENABLED: u8 = 1; /// Whether callbacks are enabled for assets. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] #[repr(u8)] -pub enum AssetCallbacks { +pub enum AssetCallbacksFlag { #[default] Disabled = CALLBACKS_DISABLED, Enabled = CALLBACKS_ENABLED, } -impl AssetCallbacks { +impl AssetCallbacksFlag { /// Encodes the callbacks setting as a `u8`. pub const fn as_u8(&self) -> u8 { *self as u8 } } -impl TryFrom for AssetCallbacks { +impl TryFrom for AssetCallbacksFlag { type Error = AssetError; /// Decodes a callbacks setting from a `u8`. @@ -32,7 +32,7 @@ impl TryFrom for AssetCallbacks { match value { CALLBACKS_DISABLED => Ok(Self::Disabled), CALLBACKS_ENABLED => Ok(Self::Enabled), - _ => Err(AssetError::InvalidAssetCallbacks(value)), + _ => Err(AssetError::InvalidAssetCallbacksFlag(value)), } } } diff --git a/crates/miden-protocol/src/asset/mod.rs b/crates/miden-protocol/src/asset/mod.rs index a8ddabcc2d..26b6884fd3 100644 --- a/crates/miden-protocol/src/asset/mod.rs +++ b/crates/miden-protocol/src/asset/mod.rs @@ -22,7 +22,7 @@ mod token_symbol; pub use token_symbol::TokenSymbol; mod asset_callbacks; -pub use asset_callbacks::AssetCallbacks; +pub use asset_callbacks::AssetCallbacksFlag; mod vault; pub use vault::{AssetId, AssetVault, AssetVaultKey, AssetWitness, PartialVault}; diff --git a/crates/miden-protocol/src/asset/vault/vault_key.rs b/crates/miden-protocol/src/asset/vault/vault_key.rs index 6d67b0f476..19714aaf3e 100644 --- a/crates/miden-protocol/src/asset/vault/vault_key.rs +++ b/crates/miden-protocol/src/asset/vault/vault_key.rs @@ -7,7 +7,7 @@ use miden_crypto::merkle::smt::LeafIndex; use crate::account::AccountId; use crate::account::AccountType::{self}; use crate::asset::vault::AssetId; -use crate::asset::{Asset, AssetCallbacks, FungibleAsset, NonFungibleAsset}; +use crate::asset::{Asset, AssetCallbacksFlag, FungibleAsset, NonFungibleAsset}; use crate::crypto::merkle::smt::SMT_DEPTH; use crate::errors::AssetError; use crate::{Felt, Word}; @@ -32,7 +32,7 @@ pub struct AssetVaultKey { faucet_id: AccountId, /// Determines whether callbacks are enabled. - callbacks: AssetCallbacks, + callbacks: AssetCallbacksFlag, } impl AssetVaultKey { @@ -50,10 +50,10 @@ impl AssetVaultKey { /// - the asset ID limbs are not zero when `faucet_id` is of type /// [`AccountType::FungibleFaucet`](crate::account::AccountType::FungibleFaucet). pub fn new_native(asset_id: AssetId, faucet_id: AccountId) -> Result { - Self::new(asset_id, faucet_id, AssetCallbacks::Disabled) + Self::new(asset_id, faucet_id, AssetCallbacksFlag::Disabled) } - /// Creates an [`AssetVaultKey`] from its parts with the given [`AssetCallbacks`]. + /// Creates an [`AssetVaultKey`] from its parts with the given [`AssetCallbacksFlag`]. /// /// # Errors /// @@ -66,7 +66,7 @@ impl AssetVaultKey { pub fn new( asset_id: AssetId, faucet_id: AccountId, - callbacks: AssetCallbacks, + callbacks: AssetCallbacksFlag, ) -> Result { if !faucet_id.is_faucet() { return Err(AssetError::InvalidFaucetAccountId(Box::from(format!( @@ -116,8 +116,8 @@ impl AssetVaultKey { self.faucet_id } - /// Returns the [`AssetCallbacks`] flag of the vault key. - pub fn callbacks(&self) -> AssetCallbacks { + /// Returns the [`AssetCallbacksFlag`] flag of the vault key. + pub fn callbacks(&self) -> AssetCallbacksFlag { self.callbacks } @@ -183,7 +183,7 @@ impl TryFrom for AssetVaultKey { let faucet_id_prefix = key[3]; let raw = faucet_id_suffix_and_metadata.as_canonical_u64(); - let category = AssetCallbacks::try_from((raw & 0xff) as u8)?; + let category = AssetCallbacksFlag::try_from((raw & 0xff) as u8)?; let faucet_id_suffix = Felt::try_from(raw & 0xffff_ffff_ffff_ff00) .expect("clearing lower bits should not produce an invalid felt"); @@ -225,7 +225,7 @@ impl From for AssetVaultKey { #[cfg(test)] mod tests { use super::*; - use crate::asset::AssetCallbacks; + use crate::asset::AssetCallbacksFlag; use crate::testing::account_id::{ ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET, @@ -237,7 +237,7 @@ mod tests { let nonfungible_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET).unwrap(); - for callbacks in [AssetCallbacks::Disabled, AssetCallbacks::Enabled] { + for callbacks in [AssetCallbacksFlag::Disabled, AssetCallbacksFlag::Enabled] { // Fungible: asset_id must be zero. let key = AssetVaultKey::new(AssetId::default(), fungible_faucet, callbacks).unwrap(); diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index 88f55b0a13..047b5c0c78 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -489,7 +489,7 @@ pub enum AssetError { #[error("smt proof in asset witness contains invalid key or value")] AssetWitnessInvalid(#[source] Box), #[error("invalid native asset callbacks encoding: {0}")] - InvalidAssetCallbacks(u8), + InvalidAssetCallbacksFlag(u8), } // TOKEN SYMBOL ERROR diff --git a/crates/miden-testing/src/kernel_tests/tx/test_asset.rs b/crates/miden-testing/src/kernel_tests/tx/test_asset.rs index ac1d495503..5ba4e4f1dc 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_asset.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_asset.rs @@ -1,6 +1,6 @@ use miden_protocol::account::AccountId; use miden_protocol::asset::{ - AssetCallbacks, + AssetCallbacksFlag, AssetId, AssetVaultKey, FungibleAsset, @@ -229,10 +229,10 @@ async fn test_validate_fungible_asset( } #[rstest::rstest] -#[case::without_callbacks(AssetCallbacks::Disabled)] -#[case::with_callbacks(AssetCallbacks::Enabled)] +#[case::without_callbacks(AssetCallbacksFlag::Disabled)] +#[case::with_callbacks(AssetCallbacksFlag::Enabled)] #[tokio::test] -async fn test_key_to_asset_metadata(#[case] callbacks: AssetCallbacks) -> anyhow::Result<()> { +async fn test_key_to_asset_metadata(#[case] callbacks: AssetCallbacksFlag) -> anyhow::Result<()> { let faucet_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?; let vault_key = AssetVaultKey::new(AssetId::default(), faucet_id, callbacks)?; diff --git a/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs b/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs index 3a0a195e79..74829d7a9a 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs @@ -3,7 +3,7 @@ use alloc::sync::Arc; use miden_protocol::account::{Account, AccountBuilder, AccountComponent, AccountId, AccountType}; use miden_protocol::assembly::DefaultSourceManager; use miden_protocol::asset::{ - AssetCallbacks, + AssetCallbacksFlag, AssetId, AssetVaultKey, FungibleAsset, @@ -307,7 +307,7 @@ async fn test_mint_fungible_asset_with_callbacks_enabled() -> anyhow::Result<()> let asset = FungibleAsset::new(faucet_id, FUNGIBLE_ASSET_AMOUNT)?; // Build a vault key with callbacks enabled. - let vault_key = AssetVaultKey::new(AssetId::default(), faucet_id, AssetCallbacks::Enabled)?; + let vault_key = AssetVaultKey::new(AssetId::default(), faucet_id, AssetCallbacksFlag::Enabled)?; let code = format!( r#" From 2cb5e9181f8713a372fdfc1fddf3f175b0838974 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 9 Mar 2026 16:45:38 +0100 Subject: [PATCH 05/41] feat: Add `AssetCallbacks` storage helper struct --- .../src/asset/asset_callbacks.rs | 87 +++++++++++++------ .../src/asset/asset_callbacks_flag.rs | 38 ++++++++ crates/miden-protocol/src/asset/mod.rs | 5 +- 3 files changed, 101 insertions(+), 29 deletions(-) create mode 100644 crates/miden-protocol/src/asset/asset_callbacks_flag.rs diff --git a/crates/miden-protocol/src/asset/asset_callbacks.rs b/crates/miden-protocol/src/asset/asset_callbacks.rs index c9eb19082d..989114e829 100644 --- a/crates/miden-protocol/src/asset/asset_callbacks.rs +++ b/crates/miden-protocol/src/asset/asset_callbacks.rs @@ -1,38 +1,69 @@ -use crate::errors::AssetError; +use alloc::vec::Vec; -const CALLBACKS_DISABLED: u8 = 0; -const CALLBACKS_ENABLED: u8 = 1; +use crate::Word; +use crate::account::{StorageSlot, StorageSlotName}; +use crate::utils::sync::LazyLock; -/// Whether callbacks are enabled for assets. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -#[repr(u8)] -pub enum AssetCallbacksFlag { - #[default] - Disabled = CALLBACKS_DISABLED, +// CONSTANTS +// ================================================================================================ - Enabled = CALLBACKS_ENABLED, +static ON_ASSET_ADDED_TO_ACCOUNT_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::protocol::faucet::callbacks::on_asset_added_to_account") + .expect("storage slot name should be valid") +}); + +// ASSET CALLBACKS +// ================================================================================================ + +/// Configures the callback procedure root for the `on_asset_added_to_account` callback. +/// +/// ## Storage Layout +/// +/// - [`Self::slot`]: Stores the procedure root of the `on_asset_added_to_account` callback. +/// +/// [`AssetCallbacksFlag::Enabled`]: crate::asset::AssetCallbacksFlag::Enabled +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct AssetCallbacks { + on_asset_added_to_account: Option, } -impl AssetCallbacksFlag { - /// Encodes the callbacks setting as a `u8`. - pub const fn as_u8(&self) -> u8 { - *self as u8 +impl AssetCallbacks { + // CONSTRUCTORS + // -------------------------------------------------------------------------------------------- + + /// Creates a new [`AssetCallbacks`] with all callbacks set to `None`. + pub fn new() -> Self { + Self::default() } -} -impl TryFrom for AssetCallbacksFlag { - type Error = AssetError; - - /// Decodes a callbacks setting from a `u8`. - /// - /// # Errors - /// - /// Returns an error if the value is not a valid callbacks encoding. - fn try_from(value: u8) -> Result { - match value { - CALLBACKS_DISABLED => Ok(Self::Disabled), - CALLBACKS_ENABLED => Ok(Self::Enabled), - _ => Err(AssetError::InvalidAssetCallbacksFlag(value)), + pub fn on_asset_added_to_account(mut self, proc_root: Word) -> Self { + self.on_asset_added_to_account = Some(proc_root); + self + } + + // PUBLIC ACCESSORS + // -------------------------------------------------------------------------------------------- + + /// Returns the [`StorageSlotName`] where the callback procedure root is stored. + pub fn on_asset_added_to_account_slot() -> &'static StorageSlotName { + &ON_ASSET_ADDED_TO_ACCOUNT_SLOT_NAME + } + + /// Returns the procedure root of the `on_asset_added_to_account` callback. + pub fn on_asset_added_proc_root(&self) -> Option { + self.on_asset_added_to_account + } + + pub fn into_storage_slots(self) -> Vec { + let mut slots = Vec::new(); + + if let Some(on_asset_added_to_account) = self.on_asset_added_to_account { + slots.push(StorageSlot::with_value( + AssetCallbacks::on_asset_added_to_account_slot().clone(), + on_asset_added_to_account, + )); } + + slots } } diff --git a/crates/miden-protocol/src/asset/asset_callbacks_flag.rs b/crates/miden-protocol/src/asset/asset_callbacks_flag.rs new file mode 100644 index 0000000000..c9eb19082d --- /dev/null +++ b/crates/miden-protocol/src/asset/asset_callbacks_flag.rs @@ -0,0 +1,38 @@ +use crate::errors::AssetError; + +const CALLBACKS_DISABLED: u8 = 0; +const CALLBACKS_ENABLED: u8 = 1; + +/// Whether callbacks are enabled for assets. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +#[repr(u8)] +pub enum AssetCallbacksFlag { + #[default] + Disabled = CALLBACKS_DISABLED, + + Enabled = CALLBACKS_ENABLED, +} + +impl AssetCallbacksFlag { + /// Encodes the callbacks setting as a `u8`. + pub const fn as_u8(&self) -> u8 { + *self as u8 + } +} + +impl TryFrom for AssetCallbacksFlag { + type Error = AssetError; + + /// Decodes a callbacks setting from a `u8`. + /// + /// # Errors + /// + /// Returns an error if the value is not a valid callbacks encoding. + fn try_from(value: u8) -> Result { + match value { + CALLBACKS_DISABLED => Ok(Self::Disabled), + CALLBACKS_ENABLED => Ok(Self::Enabled), + _ => Err(AssetError::InvalidAssetCallbacksFlag(value)), + } + } +} diff --git a/crates/miden-protocol/src/asset/mod.rs b/crates/miden-protocol/src/asset/mod.rs index 26b6884fd3..c2daaa22ce 100644 --- a/crates/miden-protocol/src/asset/mod.rs +++ b/crates/miden-protocol/src/asset/mod.rs @@ -22,7 +22,10 @@ mod token_symbol; pub use token_symbol::TokenSymbol; mod asset_callbacks; -pub use asset_callbacks::AssetCallbacksFlag; +pub use asset_callbacks::AssetCallbacks; + +mod asset_callbacks_flag; +pub use asset_callbacks_flag::AssetCallbacksFlag; mod vault; pub use vault::{AssetId, AssetVault, AssetVaultKey, AssetWitness, PartialVault}; From e5107cd4e56deac28419af56a65f36b0f71c81ad Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 9 Mar 2026 17:29:24 +0100 Subject: [PATCH 06/41] chore: add `BlockList` callback test component --- .../miden-testing/src/kernel_tests/tx/mod.rs | 1 + .../src/kernel_tests/tx/test_callbacks.rs | 229 ++++++++++++++++++ 2 files changed, 230 insertions(+) create mode 100644 crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs diff --git a/crates/miden-testing/src/kernel_tests/tx/mod.rs b/crates/miden-testing/src/kernel_tests/tx/mod.rs index 26d8e10b48..e8fa628883 100644 --- a/crates/miden-testing/src/kernel_tests/tx/mod.rs +++ b/crates/miden-testing/src/kernel_tests/tx/mod.rs @@ -23,6 +23,7 @@ mod test_array; mod test_asset; mod test_asset_vault; mod test_auth; +mod test_callbacks; mod test_epilogue; mod test_faucet; mod test_fee; diff --git a/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs b/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs new file mode 100644 index 0000000000..5e7b36034f --- /dev/null +++ b/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs @@ -0,0 +1,229 @@ +extern crate alloc; + +use alloc::collections::BTreeSet; +use alloc::vec::Vec; + +use miden_protocol::account::component::AccountComponentMetadata; +use miden_protocol::account::{ + AccountBuilder, + AccountComponent, + AccountComponentCode, + AccountId, + AccountStorageMode, + AccountType, + StorageMap, + StorageMapKey, + StorageSlot, + StorageSlotName, +}; +use miden_protocol::asset::{Asset, AssetCallbacks, AssetCallbacksFlag, FungibleAsset}; +use miden_protocol::block::account_tree::AccountIdKey; +use miden_protocol::errors::MasmError; +use miden_protocol::note::NoteType; +use miden_protocol::utils::sync::LazyLock; +use miden_protocol::{Felt, Word}; +use miden_standards::account::faucets::BasicFungibleFaucet; +use miden_standards::code_builder::CodeBuilder; +use miden_standards::procedure_digest; + +use crate::{AccountState, Auth, assert_transaction_executor_error}; + +// CONSTANTS +// ================================================================================================ + +/// MASM code for the BlockList callback component. +/// +/// This procedure checks whether the native account (the one receiving the asset) is in a +/// block list stored in a storage map. If the account is blocked, the callback panics. +const BLOCK_LIST_MASM: &str = r#" +use miden::protocol::active_account +use miden::core::word + +const BLOCK_LIST_MAP_SLOT = word("miden::testing::callbacks::block_list") +const ERR_ACCOUNT_BLOCKED = "the account is blocked and cannot receive this asset" + +#! Callback invoked when an asset with callbacks enabled is added to an account's vault. +#! +#! Checks whether the receiving account is in the block list. If so, panics. +#! +#! Inputs: [native_acct_suffix, native_acct_prefix, ASSET_KEY, ASSET_VALUE, pad(6)] +#! Outputs: [ASSET_VALUE, pad(12)] +#! +#! Invocation: call +pub proc on_asset_added_to_account + # Build account ID map key: [0, 0, suffix, prefix] + push.0.0 + # => [0, 0, native_acct_suffix, native_acct_prefix, ASSET_KEY, ASSET_VALUE, pad(6)] + # => [ACCOUNT_ID_KEY, ASSET_KEY, ASSET_VALUE, pad(6)] + + # Look up in block list storage map + push.BLOCK_LIST_MAP_SLOT[0..2] + exec.active_account::get_map_item + # => [MAP_VALUE, ASSET_KEY, ASSET_VALUE, pad(6)] + + # If value is non-zero, account is blocked. + # testz returns 1 if word is all zeros (not blocked), 0 otherwise (blocked). + # assert fails if top is 0, so blocked accounts cause a panic. + exec.word::testz + assert.err=ERR_ACCOUNT_BLOCKED + # => [ASSET_KEY, ASSET_VALUE, pad(6)] + + # Drop ASSET_KEY, keep ASSET_VALUE on top + dropw + # => [ASSET_VALUE, pad(6)] + + # Pad to 16 elements: need ASSET_VALUE(4) + pad(12), have pad(6), add 6 more + repeat.6 + push.0 + movdn.4 + end + # => [ASSET_VALUE, pad(12)] +end +"#; + +/// The expected error when a blocked account tries to receive an asset with callbacks. +const ERR_ACCOUNT_BLOCKED: MasmError = + MasmError::from_static_str("the account is blocked and cannot receive this asset"); + +// Initialize the Basic Fungible Faucet library only once. +static BLOCK_LIST_COMPONENT_CODE: LazyLock = LazyLock::new(|| { + CodeBuilder::default() + .compile_component_code(BlockList::NAME, BLOCK_LIST_MASM) + .expect("block list library should be valid") +}); + +static BLOCK_LIST_SLOT_NAME: LazyLock = LazyLock::new(|| { + StorageSlotName::new("miden::testing::callbacks::block_list") + .expect("storage slot name should be valid") +}); + +procedure_digest!( + BLOCK_LIST_ON_ASSET_ADDED_TO_ACCOUNT, + BlockList::NAME, + BlockList::ON_ASSET_ADDED_TO_ACCOUNT_PROC_NAME, + || { BLOCK_LIST_COMPONENT_CODE.as_library() } +); + +// BLOCK LIST +// ================================================================================================ + +/// A test component that implements a block list for the `on_asset_added_to_account` callback. +/// +/// When a faucet distributes assets with callbacks enabled, this component checks whether the +/// receiving account is in the block list. If the account is blocked, the transaction fails. +struct BlockList { + blocked_accounts: BTreeSet, +} + +impl BlockList { + const NAME: &str = "miden::testing::callbacks::block_list"; + + const ON_ASSET_ADDED_TO_ACCOUNT_PROC_NAME: &str = "on_asset_added_to_account"; + + /// Creates a new [`BlockList`] with the given set of blocked accounts. + fn new(blocked_accounts: BTreeSet) -> Self { + Self { blocked_accounts } + } + + /// Returns the digest of the `distribute` account procedure. + pub fn on_asset_added_to_account_digest() -> Word { + *BLOCK_LIST_ON_ASSET_ADDED_TO_ACCOUNT + } +} + +impl From for AccountComponent { + fn from(block_list: BlockList) -> Self { + // Build the storage map of blocked accounts + let map_entries: Vec<(StorageMapKey, Word)> = block_list + .blocked_accounts + .iter() + .map(|account_id| { + let map_key = StorageMapKey::new(AccountIdKey::new(*account_id).as_word()); + // Non-zero value means the account is blocked + let map_value = Word::new([Felt::ONE, Felt::ZERO, Felt::ZERO, Felt::ZERO]); + (map_key, map_value) + }) + .collect(); + + let storage_map = StorageMap::with_entries(map_entries) + .expect("btree set should guarantee no duplicates"); + + // Build storage slots: block list map + asset callbacks value slot + let mut storage_slots = + vec![StorageSlot::with_map(BLOCK_LIST_SLOT_NAME.clone(), storage_map)]; + storage_slots.extend( + AssetCallbacks::new() + .on_asset_added_to_account(BlockList::on_asset_added_to_account_digest()) + .into_storage_slots(), + ); + let metadata = + AccountComponentMetadata::new(BlockList::NAME, [AccountType::FungibleFaucet]) + .with_description("block list callback component for testing"); + + AccountComponent::new(BLOCK_LIST_COMPONENT_CODE.clone(), storage_slots, metadata) + .expect("block list should satisfy the requirements of a valid account component") + } +} + +// TESTS +// ================================================================================================ + +/// Tests that a blocked account cannot receive assets with callbacks enabled. +/// +/// Flow: +/// 1. Create a faucet with BasicFungibleFaucet + BlockList components +/// 2. Create a wallet that is in the block list +/// 3. Create a P2ID note with a callbacks-enabled asset from the faucet to the wallet +/// 4. Attempt to consume the note on the blocked wallet +/// 5. Assert that the transaction fails with ERR_ACCOUNT_BLOCKED +#[tokio::test] +async fn test_blocked_account_cannot_receive_asset() -> anyhow::Result<()> { + let mut builder = crate::MockChain::builder(); + + let target_account = builder.add_existing_wallet(Auth::IncrNonce)?; + + let block_list = BlockList::new(BTreeSet::from_iter([target_account.id()])); + let basic_faucet = BasicFungibleFaucet::new("BLK".try_into()?, 8, Felt::new(1_000_000))?; + + let account_builder = AccountBuilder::new([42u8; 32]) + .storage_mode(AccountStorageMode::Public) + .account_type(AccountType::FungibleFaucet) + .with_component(basic_faucet) + .with_component(block_list); + + let faucet = builder.add_account_from_builder( + Auth::BasicAuth { + auth_scheme: miden_protocol::account::auth::AuthScheme::Falcon512Poseidon2, + }, + account_builder, + AccountState::Exists, + )?; + + // Create a P2ID note with a callbacks-enabled asset + let fungible_asset = + FungibleAsset::new(faucet.id(), 100)?.with_callbacks(AssetCallbacksFlag::Enabled); + let note = builder.add_p2id_note( + faucet.id(), + target_account.id(), + &[Asset::Fungible(fungible_asset)], + NoteType::Public, + )?; + + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + // Get foreign account inputs for the faucet so the callback's foreign context can access it + let faucet_inputs = mock_chain.get_foreign_account_inputs(faucet.id())?; + + // Try to consume the note on the blocked wallet - should fail because the callback + // checks the block list and panics. + let consume_tx_context = mock_chain + .build_tx_context(target_account.id(), &[note.id()], &[])? + .foreign_accounts(vec![faucet_inputs]) + .build()?; + let result = consume_tx_context.execute().await; + + assert_transaction_executor_error!(result, ERR_ACCOUNT_BLOCKED); + + Ok(()) +} From 103116221d754c9f87d187307bb1514e20428538 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 9 Mar 2026 17:40:14 +0100 Subject: [PATCH 07/41] chore: define `Asset::is_same` based on vault key --- crates/miden-protocol/src/asset/mod.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/crates/miden-protocol/src/asset/mod.rs b/crates/miden-protocol/src/asset/mod.rs index c2daaa22ce..df4cd19ae5 100644 --- a/crates/miden-protocol/src/asset/mod.rs +++ b/crates/miden-protocol/src/asset/mod.rs @@ -124,16 +124,9 @@ impl Asset { /// Returns true if this asset is the same as the specified asset. /// - /// Two assets are defined to be the same if: - /// - For fungible assets, if they were issued by the same faucet. - /// - For non-fungible assets, if the assets are identical. + /// Two assets are defined to be the same if their vault keys match pub fn is_same(&self, other: &Self) -> bool { - use Asset::*; - match (self, other) { - (Fungible(l), Fungible(r)) => l.is_from_same_faucet(r), - (NonFungible(l), NonFungible(r)) => l == r, - _ => false, - } + self.vault_key() == other.vault_key() } /// Returns true if this asset is a fungible asset. From 2701ded504324e2b48092aeb91185b3ac96aa2b1 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 9 Mar 2026 17:55:21 +0100 Subject: [PATCH 08/41] chore: set callbacks flag in create_fungible_asset --- .../asm/kernels/transaction/lib/epilogue.masm | 5 ++- crates/miden-protocol/asm/protocol/asset.masm | 12 +++--- .../miden-protocol/asm/protocol/faucet.masm | 11 ++++- .../asm/shared_utils/util/asset.masm | 41 +++++++++++++++++-- 4 files changed, 58 insertions(+), 11 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm b/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm index 6b7ba6a647..6229ef8b5a 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm @@ -49,7 +49,7 @@ const SMT_SET_ADDITIONAL_CYCLES=250 # that this includes at least smt::set's best case number of cycles. # This can be _estimated_ using the transaction measurements on ExecutedTransaction and can be set # to the lowest observed value. -const NUM_POST_COMPUTE_FEE_CYCLES=500 +const NUM_POST_COMPUTE_FEE_CYCLES=510 # The number of cycles the epilogue is estimated to take after compute_fee has been executed. const ESTIMATED_AFTER_COMPUTE_FEE_CYCLES=NUM_POST_COMPUTE_FEE_CYCLES+SMT_SET_ADDITIONAL_CYCLES @@ -291,6 +291,9 @@ proc create_native_fee_asset exec.memory::get_native_asset_id # => [native_asset_id_suffix, native_asset_id_prefix, fee_amount] + push.0 + # => [enable_callbacks, native_asset_id_suffix, native_asset_id_prefix, fee_amount] + # SAFETY: native asset ID should be fungible and amount should not be exceeded exec.fungible_asset::create_unchecked # => [FEE_ASSET_KEY, FEE_ASSET_VALUE] diff --git a/crates/miden-protocol/asm/protocol/asset.masm b/crates/miden-protocol/asm/protocol/asset.masm index df9032f43e..6d78ddad69 100644 --- a/crates/miden-protocol/asm/protocol/asset.masm +++ b/crates/miden-protocol/asm/protocol/asset.masm @@ -32,10 +32,11 @@ const ERR_NON_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID="failed to build the #! Creates a fungible asset for the specified fungible faucet and amount. #! -#! Inputs: [faucet_id_suffix, faucet_id_prefix, amount] +#! Inputs: [enable_callbacks, faucet_id_suffix, faucet_id_prefix, amount] #! Outputs: [ASSET_KEY, ASSET_VALUE] #! #! Where: +#! - enable_callbacks is a flag (0 or 1) indicating whether asset callbacks are enabled. #! - faucet_id_{suffix,prefix} are the suffix and prefix felts of the faucet to create the asset #! for. #! - amount is the amount of the asset to create. @@ -45,17 +46,18 @@ const ERR_NON_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID="failed to build the #! Panics if: #! - the provided faucet ID is not a fungible faucet. #! - the provided amount exceeds FUNGIBLE_ASSET_MAX_AMOUNT. +#! - enable_callbacks is not 0 or 1. #! #! Invocation: exec pub proc create_fungible_asset # assert the faucet is a fungible faucet - dup.1 exec.account_id::is_fungible_faucet assert.err=ERR_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID - # => [faucet_id_suffix, faucet_id_prefix, amount] + dup.2 exec.account_id::is_fungible_faucet assert.err=ERR_FUNGIBLE_ASSET_PROVIDED_FAUCET_ID_IS_INVALID + # => [enable_callbacks, faucet_id_suffix, faucet_id_prefix, amount] # assert the amount is valid - dup.2 lte.FUNGIBLE_ASSET_MAX_AMOUNT + dup.3 lte.FUNGIBLE_ASSET_MAX_AMOUNT assert.err=ERR_FUNGIBLE_ASSET_AMOUNT_EXCEEDS_MAX_ALLOWED_AMOUNT - # => [faucet_id_suffix, faucet_id_prefix, amount] + # => [enable_callbacks, faucet_id_suffix, faucet_id_prefix, amount] # SAFETY: faucet ID and amount were validated exec.asset::create_fungible_asset_unchecked diff --git a/crates/miden-protocol/asm/protocol/faucet.masm b/crates/miden-protocol/asm/protocol/faucet.masm index f412026a23..0dd011181c 100644 --- a/crates/miden-protocol/asm/protocol/faucet.masm +++ b/crates/miden-protocol/asm/protocol/faucet.masm @@ -5,22 +5,29 @@ use ::miden::protocol::kernel_proc_offsets::FAUCET_BURN_ASSET_OFFSET #! Creates a fungible asset for the faucet the transaction is being executed against. #! -#! Inputs: [amount] +#! Inputs: [enable_callbacks, amount] #! Outputs: [ASSET_KEY, ASSET_VALUE] #! #! Where: +#! - enable_callbacks is a flag (0 or 1) indicating whether asset callbacks are enabled. #! - amount is the amount of the asset to create. #! - ASSET_KEY is the vault key of the created fungible asset. #! - ASSET_VALUE is the value of the created fungible asset. #! #! Panics if: #! - the active account is not a fungible faucet. +#! - enable_callbacks is not 0 or 1. #! #! Invocation: exec pub proc create_fungible_asset + # => [enable_callbacks, amount] + # fetch the id of the faucet the transaction is being executed against. exec.active_account::get_id - # => [id_suffix, id_prefix, amount] + # => [id_suffix, id_prefix, enable_callbacks, amount] + + movup.2 + # => [enable_callbacks, id_suffix, id_prefix, amount] # create the fungible asset exec.asset::create_fungible_asset diff --git a/crates/miden-protocol/asm/shared_utils/util/asset.masm b/crates/miden-protocol/asm/shared_utils/util/asset.masm index f7d2fdab77..4afc7a431f 100644 --- a/crates/miden-protocol/asm/shared_utils/util/asset.masm +++ b/crates/miden-protocol/asm/shared_utils/util/asset.masm @@ -1,3 +1,8 @@ +# ERRORS +# ================================================================================================= + +const ERR_VAULT_INVALID_ENABLE_CALLBACKS = "enable_callbacks must be 0 or 1" + # CONSTANTS # ================================================================================================= @@ -12,6 +17,9 @@ pub const ASSET_SIZE = 8 # The offset of the asset value in an asset stored in memory. pub const ASSET_VALUE_MEMORY_OFFSET = 4 +pub const CALLBACKS_DISABLED = 0 +pub const CALLBACKS_ENABLED = 1 + # PROCEDURES # ================================================================================================= @@ -180,6 +188,21 @@ pub proc key_to_callbacks_enabled # => [callbacks_enabled, ASSET_KEY] end +#! Creates asset metadata from the provided inputs. +#! +#! Inputs: [enable_callbacks] +#! Outputs: [asset_metadata] +#! +#! Where: +#! - enable_callbacks is a flag (0 or 1) indicating whether the asset callbacks flag should be set. +#! - asset_metadata is the asset metadata. +proc create_metadata + u32assert.err=ERR_VAULT_INVALID_ENABLE_CALLBACKS + dup u32lte.CALLBACKS_ENABLED + assert.err=ERR_VAULT_INVALID_ENABLE_CALLBACKS + # => [asset_metadata] +end + #! Creates a fungible asset vault key for the specified faucet. #! #! Inputs: [faucet_id_suffix, faucet_id_prefix] @@ -198,23 +221,35 @@ end #! Creates a fungible asset for the specified fungible faucet and amount. #! -#! WARNING: Does not validate its inputs. +#! WARNING: Does not validate the faucet ID or amount. #! -#! Inputs: [faucet_id_suffix, faucet_id_prefix, amount] +#! Inputs: [enable_callbacks, faucet_id_suffix, faucet_id_prefix, amount] #! Outputs: [ASSET_KEY, ASSET_VALUE] #! #! Where: +#! - enable_callbacks is a flag (0 or 1) indicating whether asset callbacks are enabled. #! - faucet_id_{suffix,prefix} are the suffix and prefix felts of the faucet to create the asset #! for. #! - amount is the amount of the asset to create. #! - ASSET_KEY is the vault key of the created fungible asset #! - ASSET_VALUE is the value of the created fungible asset. #! +#! Panics if: +#! - enable_callbacks is not 0 or 1. +#! #! Invocation: exec pub proc create_fungible_asset_unchecked + # convert enable_callbacks flag to asset_metadata + exec.create_metadata + # => [asset_metadata, faucet_id_suffix, faucet_id_prefix, amount] + + # merge metadata into lower 8 bits of faucet_id_suffix + add + # => [faucet_id_suffix_and_metadata, faucet_id_prefix, amount] + # create the key and value repeat.3 push.0 movdn.3 end - # => [faucet_id_suffix, faucet_id_prefix, ASSET_VALUE] + # => [faucet_id_suffix_and_metadata, faucet_id_prefix, ASSET_VALUE] exec.create_fungible_key # => [ASSET_KEY, ASSET_VALUE] From eff4cc7679beb6331cb0796494a65792d421ecb4 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 9 Mar 2026 18:35:37 +0100 Subject: [PATCH 09/41] chore: add callbacks flag in `FungibleAsset` --- .../src/asset/asset_callbacks_flag.rs | 29 ++++++++ crates/miden-protocol/src/asset/fungible.rs | 73 +++++++++++++++---- 2 files changed, 86 insertions(+), 16 deletions(-) diff --git a/crates/miden-protocol/src/asset/asset_callbacks_flag.rs b/crates/miden-protocol/src/asset/asset_callbacks_flag.rs index c9eb19082d..d680f6963f 100644 --- a/crates/miden-protocol/src/asset/asset_callbacks_flag.rs +++ b/crates/miden-protocol/src/asset/asset_callbacks_flag.rs @@ -1,4 +1,13 @@ +use std::string::ToString; + use crate::errors::AssetError; +use crate::utils::serde::{ + ByteReader, + ByteWriter, + Deserializable, + DeserializationError, + Serializable, +}; const CALLBACKS_DISABLED: u8 = 0; const CALLBACKS_ENABLED: u8 = 1; @@ -14,6 +23,9 @@ pub enum AssetCallbacksFlag { } impl AssetCallbacksFlag { + /// The serialized size of an [`AssetCallbacksFlag`] in bytes. + pub const SERIALIZED_SIZE: usize = core::mem::size_of::(); + /// Encodes the callbacks setting as a `u8`. pub const fn as_u8(&self) -> u8 { *self as u8 @@ -36,3 +48,20 @@ impl TryFrom for AssetCallbacksFlag { } } } + +impl Serializable for AssetCallbacksFlag { + fn write_into(&self, target: &mut W) { + target.write_u8(self.as_u8()); + } + + fn get_size_hint(&self) -> usize { + AssetCallbacksFlag::SERIALIZED_SIZE + } +} + +impl Deserializable for AssetCallbacksFlag { + fn read_from(source: &mut R) -> Result { + Self::try_from(source.read_u8()?) + .map_err(|err| DeserializationError::InvalidValue(err.to_string())) + } +} diff --git a/crates/miden-protocol/src/asset/fungible.rs b/crates/miden-protocol/src/asset/fungible.rs index ba36e94698..b4818a005d 100644 --- a/crates/miden-protocol/src/asset/fungible.rs +++ b/crates/miden-protocol/src/asset/fungible.rs @@ -2,9 +2,10 @@ use alloc::string::ToString; use core::fmt; use super::vault::AssetVaultKey; -use super::{AccountType, Asset, AssetError, Word}; +use super::{AccountType, Asset, AssetCallbacksFlag, AssetError, Word}; use crate::Felt; use crate::account::AccountId; +use crate::asset::AssetId; use crate::utils::serde::{ ByteReader, ByteWriter, @@ -23,6 +24,7 @@ use crate::utils::serde::{ pub struct FungibleAsset { faucet_id: AccountId, amount: u64, + callbacks: AssetCallbacksFlag, } impl FungibleAsset { @@ -36,8 +38,10 @@ impl FungibleAsset { /// The serialized size of a [`FungibleAsset`] in bytes. /// - /// An account ID (15 bytes) plus an amount (u64). - pub const SERIALIZED_SIZE: usize = AccountId::SERIALIZED_SIZE + core::mem::size_of::(); + /// An account ID (15 bytes) plus an amount (u64) plus a callbacks flag (u8). + pub const SERIALIZED_SIZE: usize = AccountId::SERIALIZED_SIZE + + core::mem::size_of::() + + AssetCallbacksFlag::SERIALIZED_SIZE; // CONSTRUCTOR // -------------------------------------------------------------------------------------------- @@ -58,7 +62,11 @@ impl FungibleAsset { return Err(AssetError::FungibleAssetAmountTooBig(amount)); } - Ok(Self { faucet_id, amount }) + Ok(Self { + faucet_id, + amount, + callbacks: AssetCallbacksFlag::default(), + }) } /// Creates a fungible asset from the provided key and value. @@ -80,7 +88,10 @@ impl FungibleAsset { return Err(AssetError::FungibleAssetValueMostSignificantElementsMustBeZero(value)); } - Self::new(key.faucet_id(), value[0].as_canonical_u64()) + let mut asset = Self::new(key.faucet_id(), value[0].as_canonical_u64())?; + asset.callbacks = key.callbacks(); + + Ok(asset) } /// Creates a fungible asset from the provided key and value. @@ -110,14 +121,26 @@ impl FungibleAsset { self.amount } - /// Returns true if this and the other assets were issued from the same faucet. - pub fn is_from_same_faucet(&self, other: &Self) -> bool { - self.faucet_id == other.faucet_id + /// Returns true if this and the other asset were issued from the same faucet. + pub fn is_same(&self, other: &Self) -> bool { + self.vault_key() == other.vault_key() + } + + /// Returns the [`AssetCallbacksFlag`] of this asset. + pub fn callbacks(&self) -> AssetCallbacksFlag { + self.callbacks + } + + /// Returns a copy of this asset with the given [`AssetCallbacksFlag`]. + pub fn with_callbacks(mut self, callbacks: AssetCallbacksFlag) -> Self { + self.callbacks = callbacks; + self } /// Returns the key which is used to store this asset in the account vault. pub fn vault_key(&self) -> AssetVaultKey { - AssetVaultKey::new_fungible(self.faucet_id).expect("faucet ID should be of type fungible") + AssetVaultKey::new(AssetId::default(), self.faucet_id, self.callbacks) + .expect("faucet ID should be of type fungible") } /// Returns the asset's key encoded to a [`Word`]. @@ -147,7 +170,8 @@ impl FungibleAsset { /// - The total value of assets is greater than or equal to 2^63. #[allow(clippy::should_implement_trait)] pub fn add(self, other: Self) -> Result { - if self.faucet_id != other.faucet_id { + // TODO(callbacks): Return callback flags as well in error. + if !self.is_same(&other) { return Err(AssetError::FungibleAssetInconsistentFaucetIds { original_issuer: self.faucet_id, other_issuer: other.faucet_id, @@ -162,7 +186,11 @@ impl FungibleAsset { return Err(AssetError::FungibleAssetAmountTooBig(amount)); } - Ok(Self { faucet_id: self.faucet_id, amount }) + Ok(Self { + faucet_id: self.faucet_id, + amount, + callbacks: self.callbacks, + }) } /// Subtracts a fungible asset from another and returns the result. @@ -173,7 +201,8 @@ impl FungibleAsset { /// - The final amount would be negative. #[allow(clippy::should_implement_trait)] pub fn sub(self, other: Self) -> Result { - if self.faucet_id != other.faucet_id { + // TODO(callbacks): Return callback flags as well in error. + if !self.is_same(&other) { return Err(AssetError::FungibleAssetInconsistentFaucetIds { original_issuer: self.faucet_id, other_issuer: other.faucet_id, @@ -187,7 +216,11 @@ impl FungibleAsset { }, )?; - Ok(FungibleAsset { faucet_id: self.faucet_id, amount }) + Ok(FungibleAsset { + faucet_id: self.faucet_id, + amount, + callbacks: self.callbacks, + }) } } @@ -213,10 +246,13 @@ impl Serializable for FungibleAsset { // distinguishable during deserialization. target.write(self.faucet_id); target.write(self.amount); + target.write(self.callbacks); } fn get_size_hint(&self) -> usize { - self.faucet_id.get_size_hint() + self.amount.get_size_hint() + self.faucet_id.get_size_hint() + + self.amount.get_size_hint() + + self.callbacks.get_size_hint() } } @@ -235,8 +271,13 @@ impl FungibleAsset { source: &mut R, ) -> Result { let amount: u64 = source.read()?; - FungibleAsset::new(faucet_id, amount) - .map_err(|err| DeserializationError::InvalidValue(err.to_string())) + let callbacks = source.read()?; + + let asset = FungibleAsset::new(faucet_id, amount) + .map_err(|err| DeserializationError::InvalidValue(err.to_string()))? + .with_callbacks(callbacks); + + Ok(asset) } } From c4e14328f0cbbf8d97864d9211132de25e8acabf Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 9 Mar 2026 18:36:50 +0100 Subject: [PATCH 10/41] fix: size hint of fungible asset delta --- crates/miden-protocol/src/account/delta/vault.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/miden-protocol/src/account/delta/vault.rs b/crates/miden-protocol/src/account/delta/vault.rs index 636dfb236b..c27061b21f 100644 --- a/crates/miden-protocol/src/account/delta/vault.rs +++ b/crates/miden-protocol/src/account/delta/vault.rs @@ -352,7 +352,9 @@ impl Serializable for FungibleAssetDelta { } fn get_size_hint(&self) -> usize { - self.0.len().get_size_hint() + self.0.len() * FungibleAsset::SERIALIZED_SIZE + // Each entry is (AccountId, u64 delta) + const ENTRY_SIZE: usize = AccountId::SERIALIZED_SIZE + core::mem::size_of::(); + self.0.len().get_size_hint() + self.0.len() * ENTRY_SIZE } } From c4980545bda673ab6402e6bc1cf68b5b14ad0999 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 10 Mar 2026 11:44:15 +0100 Subject: [PATCH 11/41] feat: add `account::find_storage_slot` --- .../asm/kernels/transaction/lib/account.masm | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/account.masm b/crates/miden-protocol/asm/kernels/transaction/lib/account.masm index 81b5811dc0..c90e56e577 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/account.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/account.masm @@ -426,7 +426,7 @@ pub proc get_item exec.memory::get_account_active_storage_slots_section_ptr # => [acct_storage_slots_section_offset, slot_id_suffix, slot_id_prefix] - exec.find_storage_slot + exec.get_storage_slot # => [slot_ptr] # get the item from storage @@ -452,7 +452,7 @@ pub proc get_typed_item exec.memory::get_account_active_storage_slots_section_ptr # => [acct_storage_slots_section_offset, slot_id_suffix, slot_id_prefix] - exec.find_storage_slot + exec.get_storage_slot # => [slot_ptr] dup add.ACCOUNT_SLOT_TYPE_OFFSET mem_load @@ -480,7 +480,7 @@ pub proc get_initial_item exec.memory::get_account_initial_storage_slots_ptr # => [account_initial_storage_slots_ptr, slot_id_suffix, slot_id_prefix] - exec.find_storage_slot + exec.get_storage_slot # => [slot_ptr] # get the item from initial storage @@ -509,7 +509,7 @@ pub proc set_item exec.memory::get_native_account_active_storage_slots_ptr # => [storage_slots_ptr, slot_id_suffix, slot_id_prefix, VALUE] - exec.find_storage_slot + exec.get_storage_slot # => [slot_ptr, VALUE] # load the slot type @@ -604,7 +604,7 @@ pub proc set_map_item # => [storage_slots_ptr, slot_id_suffix, slot_id_prefix, KEY, NEW_VALUE] # resolve the slot name to its pointer - exec.find_storage_slot + exec.get_storage_slot # => [slot_ptr, KEY, NEW_VALUE] # load the slot type @@ -1589,7 +1589,7 @@ end #! - a slot with the provided slot ID does not exist in account storage. #! - the requested storage slot type is not map. proc get_map_item_raw - exec.find_storage_slot + exec.get_storage_slot # => [slot_ptr, KEY] emit.ACCOUNT_STORAGE_BEFORE_GET_MAP_ITEM_EVENT @@ -1622,19 +1622,17 @@ proc get_map_item_raw end #! Finds the slot identified by the key [slot_id_prefix, slot_id_suffix, 0, 0] (stack order) and -#! returns the pointer to that slot. +#! returns a flag indicating whether the slot was found and the pointer to that slot. #! #! Inputs: [storage_slots_ptr, slot_id_suffix, slot_id_prefix] -#! Outputs: [slot_ptr] +#! Outputs: [is_found, slot_ptr] #! #! Where: #! - storage_slots_ptr is the pointer to the storage slots section. +#! - is_found is 1 if the slot was found, 0 otherwise. #! - slot_ptr is the pointer to the resolved storage slot. #! - slot_id_{suffix, prefix} are the suffix and prefix felts of the slot identifier, which are #! the first two felts of the hashed slot name. -#! -#! Panics if: -#! - a slot with the provided slot ID does not exist in account storage. proc find_storage_slot # construct the start and end pointers of the storage slot section in which we will search dup exec.memory::get_num_storage_slots mul.ACCOUNT_STORAGE_SLOT_DATA_LENGTH add @@ -1648,10 +1646,29 @@ proc find_storage_slot exec.sorted_array::find_half_key_value # => [is_slot_found, slot_ptr, storage_slots_start_ptr, storage_slots_end_ptr] - assert.err=ERR_ACCOUNT_UNKNOWN_STORAGE_SLOT_NAME - # => [slot_ptr, storage_slots_start_ptr, storage_slots_end_ptr] + movup.2 drop movup.2 drop + # => [is_slot_found, slot_ptr] +end - swap.2 drop drop +#! Finds the slot identified by the key [slot_id_prefix, slot_id_suffix, 0, 0] (stack order) and +#! returns the pointer to that slot. +#! +#! Inputs: [storage_slots_ptr, slot_id_suffix, slot_id_prefix] +#! Outputs: [slot_ptr] +#! +#! Where: +#! - storage_slots_ptr is the pointer to the storage slots section. +#! - slot_ptr is the pointer to the resolved storage slot. +#! - slot_id_{suffix, prefix} are the suffix and prefix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. +#! +#! Panics if: +#! - a slot with the provided slot ID does not exist in account storage. +proc get_storage_slot + exec.find_storage_slot + # => [is_found, slot_ptr] + + assert.err=ERR_ACCOUNT_UNKNOWN_STORAGE_SLOT_NAME # => [slot_ptr] end From a9018310dfe5ce89b459b1fac78df6766684770d Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 10 Mar 2026 12:26:18 +0100 Subject: [PATCH 12/41] feat: add `has_callbacks` kernel proc --- .../asm/kernels/transaction/api.masm | 18 ++++++ .../asm/kernels/transaction/lib/account.masm | 51 +++++++++++++++ .../asm/protocol/active_account.masm | 29 +++++++++ .../asm/protocol/kernel_proc_offsets.masm | 49 ++++++++------- .../src/kernel_tests/tx/test_account.rs | 62 ++++++++++++++++++- 5 files changed, 185 insertions(+), 24 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/api.masm b/crates/miden-protocol/asm/kernels/transaction/api.masm index d4020aee1d..d885aa580b 100644 --- a/crates/miden-protocol/asm/kernels/transaction/api.masm +++ b/crates/miden-protocol/asm/kernels/transaction/api.masm @@ -736,6 +736,24 @@ pub proc account_has_procedure # => [is_procedure_available, pad(15)] end +#! Returns whether the active account defines callbacks. +#! +#! Inputs: [pad(16)] +#! Outputs: [has_callbacks, pad(15)] +#! +#! Where: +#! - has_callbacks is 1 if the account defines callbacks, 0 otherwise. +#! +#! Invocation: dynexec +pub proc account_has_callbacks + exec.account::has_callbacks + # => [has_callbacks, pad(16)] + + # truncate the stack + swap drop + # => [has_callbacks, pad(15)] +end + # FAUCET # ------------------------------------------------------------------------------------------------- diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/account.masm b/crates/miden-protocol/asm/kernels/transaction/lib/account.masm index c90e56e577..469a631457 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/account.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/account.masm @@ -18,6 +18,9 @@ use miden::core::word # ERRORS # ================================================================================================= +# The slot ID of the on_asset_added_to_account callback. +const ON_ASSET_ADDED_TO_ACCOUNT_SLOT_ID = word("miden::protocol::faucet::callbacks::on_asset_added_to_account") + const ERR_ACCOUNT_NONCE_CAN_ONLY_BE_INCREMENTED_ONCE="account nonce can only be incremented once" const ERR_ACCOUNT_NONCE_AT_MAX="account nonce is already at its maximum possible value" @@ -2013,6 +2016,54 @@ pub proc has_procedure # => [is_procedure_available'] end +# CALLBACKS +# ------------------------------------------------------------------------------------------------- + +#! Returns whether the active account defines callbacks. +#! +#! Outputs: [has_callbacks] +#! +#! Where: +#! - has_callbacks is 1 if the account defines callbacks, 0 otherwise. +pub proc has_callbacks + # check if the on_asset_added_to_account callback slot exists and is non-empty + push.ON_ASSET_ADDED_TO_ACCOUNT_SLOT_ID[0..2] + exec.has_non_empty_slot + # => [has_callbacks] +end + +#! Checks whether a storage slot with the given slot ID exists in the active account's storage +#! and has a non-empty value. +#! +#! Inputs: [slot_id_suffix, slot_id_prefix] +#! Outputs: [has_non_empty_value] +#! +#! Where: +#! - slot_id_{suffix, prefix} are the suffix and prefix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. +#! - has_non_empty_value is 1 if the slot exists and its value is non-empty, 0 otherwise. +proc has_non_empty_slot + exec.memory::get_account_active_storage_slots_section_ptr + # => [storage_slots_ptr, slot_id_suffix, slot_id_prefix] + + exec.find_storage_slot + # => [is_found, slot_ptr] + + if.true + # read the slot value + exec.get_item_raw + # => [VALUE] + + # check if the value is non-empty + exec.word::eqz not + # => [has_non_empty_value] + else + drop push.0 + # => [0] + end + # => [has_non_empty_value] +end + #! Returns the key built from the provided account ID for use in the advice map or the account #! tree. #! diff --git a/crates/miden-protocol/asm/protocol/active_account.masm b/crates/miden-protocol/asm/protocol/active_account.masm index 0285954f6c..b89dab926d 100644 --- a/crates/miden-protocol/asm/protocol/active_account.masm +++ b/crates/miden-protocol/asm/protocol/active_account.masm @@ -18,6 +18,7 @@ use ::miden::protocol::kernel_proc_offsets::ACCOUNT_GET_INITIAL_ASSET_OFFSET use ::miden::protocol::kernel_proc_offsets::ACCOUNT_GET_NUM_PROCEDURES_OFFSET use ::miden::protocol::kernel_proc_offsets::ACCOUNT_GET_PROCEDURE_ROOT_OFFSET use ::miden::protocol::kernel_proc_offsets::ACCOUNT_HAS_PROCEDURE_OFFSET +use ::miden::protocol::kernel_proc_offsets::ACCOUNT_HAS_CALLBACKS_OFFSET use miden::core::word # ACTIVE ACCOUNT PROCEDURES @@ -671,3 +672,31 @@ pub proc has_procedure swapdw dropw dropw swapw dropw movdn.3 drop drop drop # => [is_procedure_available] end + +# CALLBACKS +# ------------------------------------------------------------------------------------------------- + +#! Returns whether the active account defines callbacks. +#! +#! Inputs: [] +#! Outputs: [has_callbacks] +#! +#! Where: +#! - has_callbacks is 1 if the account defines callbacks, 0 otherwise. +#! +#! Invocation: exec +pub proc has_callbacks + # pad the stack + padw padw padw push.0.0.0 + # => [pad(15)] + + push.ACCOUNT_HAS_CALLBACKS_OFFSET + # => [offset, pad(15)] + + syscall.exec_kernel_proc + # => [has_callbacks, pad(15)] + + # clean the stack + swapdw dropw dropw swapw dropw movdn.3 drop drop drop + # => [has_callbacks] +end diff --git a/crates/miden-protocol/asm/protocol/kernel_proc_offsets.masm b/crates/miden-protocol/asm/protocol/kernel_proc_offsets.masm index 7e65b6b089..15a94cabea 100644 --- a/crates/miden-protocol/asm/protocol/kernel_proc_offsets.masm +++ b/crates/miden-protocol/asm/protocol/kernel_proc_offsets.masm @@ -48,43 +48,46 @@ pub const ACCOUNT_HAS_PROCEDURE_OFFSET=24 pub const FAUCET_MINT_ASSET_OFFSET=25 pub const FAUCET_BURN_ASSET_OFFSET=26 +# Callbacks +pub const ACCOUNT_HAS_CALLBACKS_OFFSET=27 + ### Note ######################################## # input notes -pub const INPUT_NOTE_GET_METADATA_OFFSET=27 -pub const INPUT_NOTE_GET_ASSETS_INFO_OFFSET=28 -pub const INPUT_NOTE_GET_SCRIPT_ROOT_OFFSET=29 -pub const INPUT_NOTE_GET_STORAGE_INFO_OFFSET=30 -pub const INPUT_NOTE_GET_SERIAL_NUMBER_OFFSET=31 -pub const INPUT_NOTE_GET_RECIPIENT_OFFSET=32 +pub const INPUT_NOTE_GET_METADATA_OFFSET=28 +pub const INPUT_NOTE_GET_ASSETS_INFO_OFFSET=29 +pub const INPUT_NOTE_GET_SCRIPT_ROOT_OFFSET=30 +pub const INPUT_NOTE_GET_STORAGE_INFO_OFFSET=31 +pub const INPUT_NOTE_GET_SERIAL_NUMBER_OFFSET=32 +pub const INPUT_NOTE_GET_RECIPIENT_OFFSET=33 # output notes -pub const OUTPUT_NOTE_CREATE_OFFSET=33 -pub const OUTPUT_NOTE_GET_METADATA_OFFSET=34 -pub const OUTPUT_NOTE_GET_ASSETS_INFO_OFFSET=35 -pub const OUTPUT_NOTE_GET_RECIPIENT_OFFSET=36 -pub const OUTPUT_NOTE_ADD_ASSET_OFFSET=37 -pub const OUTPUT_NOTE_SET_ATTACHMENT_OFFSET=38 +pub const OUTPUT_NOTE_CREATE_OFFSET=34 +pub const OUTPUT_NOTE_GET_METADATA_OFFSET=35 +pub const OUTPUT_NOTE_GET_ASSETS_INFO_OFFSET=36 +pub const OUTPUT_NOTE_GET_RECIPIENT_OFFSET=37 +pub const OUTPUT_NOTE_ADD_ASSET_OFFSET=38 +pub const OUTPUT_NOTE_SET_ATTACHMENT_OFFSET=39 ### Tx ########################################## # input notes -pub const TX_GET_NUM_INPUT_NOTES_OFFSET=39 -pub const TX_GET_INPUT_NOTES_COMMITMENT_OFFSET=40 +pub const TX_GET_NUM_INPUT_NOTES_OFFSET=40 +pub const TX_GET_INPUT_NOTES_COMMITMENT_OFFSET=41 # output notes -pub const TX_GET_NUM_OUTPUT_NOTES_OFFSET=41 -pub const TX_GET_OUTPUT_NOTES_COMMITMENT_OFFSET=42 +pub const TX_GET_NUM_OUTPUT_NOTES_OFFSET=42 +pub const TX_GET_OUTPUT_NOTES_COMMITMENT_OFFSET=43 # block info -pub const TX_GET_BLOCK_COMMITMENT_OFFSET=43 -pub const TX_GET_BLOCK_NUMBER_OFFSET=44 -pub const TX_GET_BLOCK_TIMESTAMP_OFFSET=45 +pub const TX_GET_BLOCK_COMMITMENT_OFFSET=44 +pub const TX_GET_BLOCK_NUMBER_OFFSET=45 +pub const TX_GET_BLOCK_TIMESTAMP_OFFSET=46 # foreign context -pub const TX_PREPARE_FPI_OFFSET = 46 -pub const TX_EXEC_FOREIGN_PROC_OFFSET = 47 +pub const TX_PREPARE_FPI_OFFSET = 47 +pub const TX_EXEC_FOREIGN_PROC_OFFSET = 48 # expiration data -pub const TX_GET_EXPIRATION_DELTA_OFFSET=48 # accessor -pub const TX_UPDATE_EXPIRATION_BLOCK_DELTA_OFFSET=49 # mutator +pub const TX_GET_EXPIRATION_DELTA_OFFSET=49 # accessor +pub const TX_UPDATE_EXPIRATION_BLOCK_DELTA_OFFSET=50 # mutator 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 b91a873ce0..f6314b2a3c 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_account.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_account.rs @@ -31,7 +31,7 @@ use miden_protocol::account::{ use miden_protocol::assembly::diagnostics::NamedSource; use miden_protocol::assembly::diagnostics::reporting::PrintDiagnostic; use miden_protocol::assembly::{DefaultSourceManager, Library}; -use miden_protocol::asset::{Asset, FungibleAsset}; +use miden_protocol::asset::{Asset, AssetCallbacks, FungibleAsset}; use miden_protocol::errors::tx_kernel::{ ERR_ACCOUNT_ID_SUFFIX_LEAST_SIGNIFICANT_BYTE_MUST_BE_ZERO, ERR_ACCOUNT_ID_SUFFIX_MOST_SIGNIFICANT_BIT_MUST_BE_ZERO, @@ -54,6 +54,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::faucets::BasicFungibleFaucet; use miden_standards::code_builder::CodeBuilder; use miden_standards::testing::account_component::MockAccountComponent; use miden_standards::testing::mock_account::MockAccountExt; @@ -1677,6 +1678,65 @@ async fn test_has_procedure() -> anyhow::Result<()> { Ok(()) } +/// Tests that the `has_callbacks` kernel procedure correctly reports whether an account defines +/// callbacks. +/// +/// - `with_callbacks`: callback slot has a non-empty value -> returns 1 +/// - `with_empty_callback`: callback slot exists but value is the empty word -> returns 0 +/// - `without_callbacks`: no callback slot at all -> returns 0 +#[rstest::rstest] +#[case::with_callbacks( + vec![StorageSlot::with_value( + AssetCallbacks::on_asset_added_to_account_slot().clone(), + Word::from([1, 2, 3, 4u32]), + )], + true, +)] +#[case::with_empty_callback( + vec![StorageSlot::with_empty_value( + AssetCallbacks::on_asset_added_to_account_slot().clone(), + )], + false, +)] +#[case::without_callbacks(vec![], false)] +#[tokio::test] +async fn test_account_has_callbacks( + #[case] callback_slots: Vec, + #[case] expected_has_callbacks: bool, +) -> anyhow::Result<()> { + let basic_faucet = BasicFungibleFaucet::new("CBK".try_into()?, 8, Felt::new(1_000_000))?; + + let account = AccountBuilder::new([1u8; 32]) + .storage_mode(AccountStorageMode::Public) + .account_type(AccountType::FungibleFaucet) + .with_component(basic_faucet) + .with_component(MockAccountComponent::with_slots(callback_slots)) + .with_auth_component(Auth::IncrNonce) + .build_existing()?; + + let tx_script_code = format!( + r#" + use miden::protocol::active_account + + begin + exec.active_account::has_callbacks + push.{has_callbacks} + assert_eq.err="has_callbacks returned unexpected value" + end + "#, + has_callbacks = u8::from(expected_has_callbacks) + ); + let tx_script = CodeBuilder::default().compile_tx_script(&tx_script_code)?; + + TransactionContextBuilder::new(account) + .tx_script(tx_script) + .build()? + .execute() + .await?; + + Ok(()) +} + // ACCOUNT INITIAL STORAGE TESTS // ================================================================================================ From 02d598c448ef0127f07a498a827201e81fbe84d3 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 10 Mar 2026 13:45:51 +0100 Subject: [PATCH 13/41] feat: pass `enable_callbacks` to `create_fungible_key` --- .../asm/protocol/active_account.masm | 8 ++++++ .../asm/shared_utils/util/asset.masm | 28 +++++++++++-------- .../src/asset/asset_callbacks_flag.rs | 2 +- .../asm/standards/faucets/mod.masm | 4 +++ 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/crates/miden-protocol/asm/protocol/active_account.masm b/crates/miden-protocol/asm/protocol/active_account.masm index b89dab926d..fecaec7bfa 100644 --- a/crates/miden-protocol/asm/protocol/active_account.masm +++ b/crates/miden-protocol/asm/protocol/active_account.masm @@ -513,6 +513,10 @@ pub proc get_balance assert.err=ERR_VAULT_GET_BALANCE_CAN_ONLY_BE_CALLED_ON_FUNGIBLE_ASSET # => [faucet_id_suffix, faucet_id_prefix] + # TODO(callbacks): This should take ASSET_KEY as input to avoid hardcoding the callbacks flag. + push.0 + # => [enable_callbacks = 0, faucet_id_suffix, faucet_id_prefix] + exec.asset::create_fungible_key # => [ASSET_KEY] @@ -545,6 +549,10 @@ pub proc get_initial_balance assert.err=ERR_VAULT_GET_BALANCE_CAN_ONLY_BE_CALLED_ON_FUNGIBLE_ASSET # => [faucet_id_suffix, faucet_id_prefix] + # TODO(callbacks): This should take ASSET_KEY as input to avoid hardcoding the callbacks flag. + push.0 + # => [enable_callbacks = 0, faucet_id_suffix, faucet_id_prefix] + exec.asset::create_fungible_key # => [ASSET_KEY] diff --git a/crates/miden-protocol/asm/shared_utils/util/asset.masm b/crates/miden-protocol/asm/shared_utils/util/asset.masm index 4afc7a431f..9809e240b6 100644 --- a/crates/miden-protocol/asm/shared_utils/util/asset.masm +++ b/crates/miden-protocol/asm/shared_utils/util/asset.masm @@ -205,17 +205,27 @@ end #! Creates a fungible asset vault key for the specified faucet. #! -#! Inputs: [faucet_id_suffix, faucet_id_prefix] +#! Inputs: [enable_callbacks, faucet_id_suffix, faucet_id_prefix] #! Outputs: [ASSET_KEY] #! #! Where: +#! - enable_callbacks is a flag (0 or 1) indicating whether asset callbacks are enabled. #! - faucet_id_{suffix,prefix} are the suffix and prefix felts of the fungible faucet. #! - ASSET_KEY is the vault key for the fungible asset. #! +#! Panics if: +#! - enable_callbacks is not 0 or 1. +#! #! Invocation: exec pub proc create_fungible_key + exec.create_metadata + # => [asset_metadata, faucet_id_suffix, faucet_id_prefix] + + add + # => [faucet_id_suffix_and_metadata, faucet_id_prefix] + push.0.0 - # => [0, 0, faucet_id_suffix, faucet_id_prefix] + # => [0, 0, faucet_id_suffix_and_metadata, faucet_id_prefix] # => [ASSET_KEY] end @@ -239,17 +249,11 @@ end #! #! Invocation: exec pub proc create_fungible_asset_unchecked - # convert enable_callbacks flag to asset_metadata - exec.create_metadata - # => [asset_metadata, faucet_id_suffix, faucet_id_prefix, amount] - - # merge metadata into lower 8 bits of faucet_id_suffix - add - # => [faucet_id_suffix_and_metadata, faucet_id_prefix, amount] + # => [enable_callbacks, faucet_id_suffix, faucet_id_prefix, amount] - # create the key and value - repeat.3 push.0 movdn.3 end - # => [faucet_id_suffix_and_metadata, faucet_id_prefix, ASSET_VALUE] + # pad amount into ASSET_VALUE + repeat.3 push.0 movdn.4 end + # => [enable_callbacks, faucet_id_suffix, faucet_id_prefix, ASSET_VALUE] exec.create_fungible_key # => [ASSET_KEY, ASSET_VALUE] diff --git a/crates/miden-protocol/src/asset/asset_callbacks_flag.rs b/crates/miden-protocol/src/asset/asset_callbacks_flag.rs index d680f6963f..3a80cdd8c8 100644 --- a/crates/miden-protocol/src/asset/asset_callbacks_flag.rs +++ b/crates/miden-protocol/src/asset/asset_callbacks_flag.rs @@ -1,4 +1,4 @@ -use std::string::ToString; +use alloc::string::ToString; use crate::errors::AssetError; use crate::utils::serde::{ diff --git a/crates/miden-standards/asm/standards/faucets/mod.masm b/crates/miden-standards/asm/standards/faucets/mod.masm index bee1948a2c..13f7a513b3 100644 --- a/crates/miden-standards/asm/standards/faucets/mod.masm +++ b/crates/miden-standards/asm/standards/faucets/mod.masm @@ -135,6 +135,10 @@ pub proc distribute # Mint the asset. # --------------------------------------------------------------------------------------------- + # mint the asset with the callbacks flag based on whether the faucet has callbacks defined + exec.active_account::has_callbacks + # => [has_callbacks, amount, note_idx, note_idx] + # creating the asset exec.faucet::create_fungible_asset # => [ASSET_KEY, ASSET_VALUE, note_idx, note_idx] From 773b492f46f0f8e6ed81fbe7ca36374fc905479b Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 10 Mar 2026 14:00:55 +0100 Subject: [PATCH 14/41] chore: rename to `on_before_asset_added_to_account` --- .../asm/kernels/transaction/lib/account.masm | 10 +++--- .../kernels/transaction/lib/callbacks.masm | 12 +++---- .../src/asset/asset_callbacks.rs | 35 ++++++++++--------- .../src/kernel_tests/tx/test_account.rs | 4 +-- .../src/kernel_tests/tx/test_callbacks.rs | 19 +++++----- 5 files changed, 42 insertions(+), 38 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/account.masm b/crates/miden-protocol/asm/kernels/transaction/lib/account.masm index 469a631457..6716f4089b 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/account.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/account.masm @@ -18,8 +18,8 @@ use miden::core::word # ERRORS # ================================================================================================= -# The slot ID of the on_asset_added_to_account callback. -const ON_ASSET_ADDED_TO_ACCOUNT_SLOT_ID = word("miden::protocol::faucet::callbacks::on_asset_added_to_account") +# The slot ID of the on_before_asset_added_to_account callback. +const ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_SLOT_ID = word("miden::protocol::faucet::callback::on_before_asset_added_to_account") const ERR_ACCOUNT_NONCE_CAN_ONLY_BE_INCREMENTED_ONCE="account nonce can only be incremented once" @@ -673,7 +673,7 @@ pub proc add_asset_to_vault swapw dupw.1 # => [ASSET_KEY, ASSET_VALUE, ASSET_KEY] - exec.callbacks::on_asset_added_to_account + exec.callbacks::on_before_asset_added_to_account swapw # => [ASSET_KEY, ASSET_VALUE] @@ -2026,8 +2026,8 @@ end #! Where: #! - has_callbacks is 1 if the account defines callbacks, 0 otherwise. pub proc has_callbacks - # check if the on_asset_added_to_account callback slot exists and is non-empty - push.ON_ASSET_ADDED_TO_ACCOUNT_SLOT_ID[0..2] + # check if the on_before_asset_added_to_account callback slot exists and is non-empty + push.ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_SLOT_ID[0..2] exec.has_non_empty_slot # => [has_callbacks] end diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm b/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm index 5f40cac666..f476a08ffd 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm @@ -9,9 +9,9 @@ use miden::core::word # The index of the local memory slot that contains the procedure root of the callback. const CALLBACK_PROC_ROOT_PTR = 0 -# The name of the storage slot where the procedure root for the on_asset_added_to_account callback +# The name of the storage slot where the procedure root for the on_before_asset_added_to_account callback # is stored. -const ON_ASSET_ADDED_TO_ACCOUNT_PROC_ROOT = word("miden::protocol::faucet::callbacks::on_asset_added_to_account") +const ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_ROOT = word("miden::protocol::faucet::callback::on_before_asset_added_to_account") # PROCEDURES # ================================================================================================== @@ -20,12 +20,12 @@ const ON_ASSET_ADDED_TO_ACCOUNT_PROC_ROOT = word("miden::protocol::faucet::callb #! #! Inputs: [ASSET_KEY, ASSET_VALUE] #! Outputs: [PROCESSED_ASSET_VALUE] -pub proc on_asset_added_to_account +pub proc on_before_asset_added_to_account exec.asset::key_to_callbacks_enabled # => [callbacks_enabled, ASSET_KEY, ASSET_VALUE] if.true - exec.on_asset_added_to_account_raw + exec.on_before_asset_added_to_account_raw # => [PROCESSED_ASSET_VALUE] else # drop asset key @@ -38,12 +38,12 @@ end #! Inputs: [ASSET_KEY, ASSET_VALUE] #! Outputs: [PROCESSED_ASSET_VALUE] @locals(4) -proc on_asset_added_to_account_raw +proc on_before_asset_added_to_account_raw exec.start_foreign_callback_context # => [native_account_id_suffix, native_account_id_prefix, ASSET_KEY, ASSET_VALUE] # get the procedure root of the callback from the faucet's storage - push.ON_ASSET_ADDED_TO_ACCOUNT_PROC_ROOT[0..2] + push.ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_ROOT[0..2] exec.account::get_item # => [PROC_ROOT, native_account_id_suffix, native_account_id_prefix, ASSET_KEY, ASSET_VALUE] diff --git a/crates/miden-protocol/src/asset/asset_callbacks.rs b/crates/miden-protocol/src/asset/asset_callbacks.rs index 989114e829..e957cbea54 100644 --- a/crates/miden-protocol/src/asset/asset_callbacks.rs +++ b/crates/miden-protocol/src/asset/asset_callbacks.rs @@ -7,24 +7,25 @@ use crate::utils::sync::LazyLock; // CONSTANTS // ================================================================================================ -static ON_ASSET_ADDED_TO_ACCOUNT_SLOT_NAME: LazyLock = LazyLock::new(|| { - StorageSlotName::new("miden::protocol::faucet::callbacks::on_asset_added_to_account") - .expect("storage slot name should be valid") -}); +static ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_SLOT_NAME: LazyLock = + LazyLock::new(|| { + StorageSlotName::new("miden::protocol::faucet::callback::on_before_asset_added_to_account") + .expect("storage slot name should be valid") + }); // ASSET CALLBACKS // ================================================================================================ -/// Configures the callback procedure root for the `on_asset_added_to_account` callback. +/// Configures the callback procedure root for the `on_before_asset_added_to_account` callback. /// /// ## Storage Layout /// -/// - [`Self::slot`]: Stores the procedure root of the `on_asset_added_to_account` callback. +/// - [`Self::slot`]: Stores the procedure root of the `on_before_asset_added_to_account` callback. /// /// [`AssetCallbacksFlag::Enabled`]: crate::asset::AssetCallbacksFlag::Enabled #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct AssetCallbacks { - on_asset_added_to_account: Option, + on_before_asset_added_to_account: Option, } impl AssetCallbacks { @@ -36,8 +37,8 @@ impl AssetCallbacks { Self::default() } - pub fn on_asset_added_to_account(mut self, proc_root: Word) -> Self { - self.on_asset_added_to_account = Some(proc_root); + pub fn on_before_asset_added_to_account(mut self, proc_root: Word) -> Self { + self.on_before_asset_added_to_account = Some(proc_root); self } @@ -45,22 +46,22 @@ impl AssetCallbacks { // -------------------------------------------------------------------------------------------- /// Returns the [`StorageSlotName`] where the callback procedure root is stored. - pub fn on_asset_added_to_account_slot() -> &'static StorageSlotName { - &ON_ASSET_ADDED_TO_ACCOUNT_SLOT_NAME + pub fn on_before_asset_added_to_account_slot() -> &'static StorageSlotName { + &ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_SLOT_NAME } - /// Returns the procedure root of the `on_asset_added_to_account` callback. - pub fn on_asset_added_proc_root(&self) -> Option { - self.on_asset_added_to_account + /// Returns the procedure root of the `on_before_asset_added_to_account` callback. + pub fn on_before_asset_added_proc_root(&self) -> Option { + self.on_before_asset_added_to_account } pub fn into_storage_slots(self) -> Vec { let mut slots = Vec::new(); - if let Some(on_asset_added_to_account) = self.on_asset_added_to_account { + if let Some(on_before_asset_added_to_account) = self.on_before_asset_added_to_account { slots.push(StorageSlot::with_value( - AssetCallbacks::on_asset_added_to_account_slot().clone(), - on_asset_added_to_account, + AssetCallbacks::on_before_asset_added_to_account_slot().clone(), + on_before_asset_added_to_account, )); } 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 f6314b2a3c..5c1ad87911 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_account.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_account.rs @@ -1687,14 +1687,14 @@ async fn test_has_procedure() -> anyhow::Result<()> { #[rstest::rstest] #[case::with_callbacks( vec![StorageSlot::with_value( - AssetCallbacks::on_asset_added_to_account_slot().clone(), + AssetCallbacks::on_before_asset_added_to_account_slot().clone(), Word::from([1, 2, 3, 4u32]), )], true, )] #[case::with_empty_callback( vec![StorageSlot::with_empty_value( - AssetCallbacks::on_asset_added_to_account_slot().clone(), + AssetCallbacks::on_before_asset_added_to_account_slot().clone(), )], false, )] 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 5e7b36034f..d06ac16f86 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs @@ -50,7 +50,7 @@ const ERR_ACCOUNT_BLOCKED = "the account is blocked and cannot receive this asse #! Outputs: [ASSET_VALUE, pad(12)] #! #! Invocation: call -pub proc on_asset_added_to_account +pub proc on_before_asset_added_to_account # Build account ID map key: [0, 0, suffix, prefix] push.0.0 # => [0, 0, native_acct_suffix, native_acct_prefix, ASSET_KEY, ASSET_VALUE, pad(6)] @@ -98,16 +98,17 @@ static BLOCK_LIST_SLOT_NAME: LazyLock = LazyLock::new(|| { }); procedure_digest!( - BLOCK_LIST_ON_ASSET_ADDED_TO_ACCOUNT, + BLOCK_LIST_ON_BEFORE_ASSET_ADDED_TO_ACCOUNT, BlockList::NAME, - BlockList::ON_ASSET_ADDED_TO_ACCOUNT_PROC_NAME, + BlockList::ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_NAME, || { BLOCK_LIST_COMPONENT_CODE.as_library() } ); // BLOCK LIST // ================================================================================================ -/// A test component that implements a block list for the `on_asset_added_to_account` callback. +/// A test component that implements a block list for the `on_before_asset_added_to_account` +/// callback. /// /// When a faucet distributes assets with callbacks enabled, this component checks whether the /// receiving account is in the block list. If the account is blocked, the transaction fails. @@ -118,7 +119,7 @@ struct BlockList { impl BlockList { const NAME: &str = "miden::testing::callbacks::block_list"; - const ON_ASSET_ADDED_TO_ACCOUNT_PROC_NAME: &str = "on_asset_added_to_account"; + const ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_NAME: &str = "on_before_asset_added_to_account"; /// Creates a new [`BlockList`] with the given set of blocked accounts. fn new(blocked_accounts: BTreeSet) -> Self { @@ -126,8 +127,8 @@ impl BlockList { } /// Returns the digest of the `distribute` account procedure. - pub fn on_asset_added_to_account_digest() -> Word { - *BLOCK_LIST_ON_ASSET_ADDED_TO_ACCOUNT + pub fn on_before_asset_added_to_account_digest() -> Word { + *BLOCK_LIST_ON_BEFORE_ASSET_ADDED_TO_ACCOUNT } } @@ -153,7 +154,9 @@ impl From for AccountComponent { vec![StorageSlot::with_map(BLOCK_LIST_SLOT_NAME.clone(), storage_map)]; storage_slots.extend( AssetCallbacks::new() - .on_asset_added_to_account(BlockList::on_asset_added_to_account_digest()) + .on_before_asset_added_to_account( + BlockList::on_before_asset_added_to_account_digest(), + ) .into_storage_slots(), ); let metadata = From d9f2329a5467f5a6eb592e96062343db7c531f5d Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 10 Mar 2026 14:07:30 +0100 Subject: [PATCH 15/41] chore: update NUM_POST_COMPUTE_FEE_CYCLES according to benchmark --- bin/bench-transaction/bench-tx.json | 36 +++++++++---------- .../asm/kernels/transaction/lib/epilogue.masm | 2 +- .../src/kernel_tests/tx/test_fee.rs | 2 +- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/bin/bench-transaction/bench-tx.json b/bin/bench-transaction/bench-tx.json index c1dbe2abc3..67dbed5252 100644 --- a/bin/bench-transaction/bench-tx.json +++ b/bin/bench-transaction/bench-tx.json @@ -1,40 +1,40 @@ { "consume single P2ID note": { - "prologue": 3173, - "notes_processing": 1714, + "prologue": 3487, + "notes_processing": 1831, "note_execution": { - "0xa030091e37d38b506d764d5666f3a13af9e5702a0159974a3bc27053d7a55e01": 1674 + "0x1421e92d0f84f11b3e6f84e4e1d193e648eb820666ffb8c50ea818c25a32990c": 1791 }, "tx_script_processing": 42, "epilogue": { - "total": 63977, - "auth_procedure": 62667, - "after_tx_cycles_obtained": 574 + "total": 71195, + "auth_procedure": 69694, + "after_tx_cycles_obtained": 608 } }, "consume two P2ID notes": { - "prologue": 4131, - "notes_processing": 3431, + "prologue": 4509, + "notes_processing": 3668, "note_execution": { - "0x209ecf97790d4328e60a3b15160760934383ecff02550cb5df72e3f6d459fa70": 1708, - "0x4f9da5658d9f717fdcfa674906e92a7424d86da93f3a21fe0362a220f0e457b7": 1674 + "0x702c078c74683d33b507e16d9fc67f0be0cc943cd94c1f652e3a60e0f4164d9f": 1791, + "0x92cc0c8c208e3b8bad970c23b2c4b4c24cc8d42626b3f56363ce1a6bbf4c7ac2": 1828 }, "tx_script_processing": 42, "epilogue": { - "total": 63949, - "auth_procedure": 62653, - "after_tx_cycles_obtained": 574 + "total": 71143, + "auth_procedure": 69668, + "after_tx_cycles_obtained": 608 } }, "create single P2ID note": { - "prologue": 1681, + "prologue": 1766, "notes_processing": 32, "note_execution": {}, - "tx_script_processing": 1497, + "tx_script_processing": 1682, "epilogue": { - "total": 64803, - "auth_procedure": 62899, - "after_tx_cycles_obtained": 574 + "total": 72099, + "auth_procedure": 69906, + "after_tx_cycles_obtained": 608 } } } \ No newline at end of file diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm b/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm index 6229ef8b5a..0c2d953d2a 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm @@ -49,7 +49,7 @@ const SMT_SET_ADDITIONAL_CYCLES=250 # that this includes at least smt::set's best case number of cycles. # This can be _estimated_ using the transaction measurements on ExecutedTransaction and can be set # to the lowest observed value. -const NUM_POST_COMPUTE_FEE_CYCLES=510 +const NUM_POST_COMPUTE_FEE_CYCLES=608 # The number of cycles the epilogue is estimated to take after compute_fee has been executed. const ESTIMATED_AFTER_COMPUTE_FEE_CYCLES=NUM_POST_COMPUTE_FEE_CYCLES+SMT_SET_ADDITIONAL_CYCLES diff --git a/crates/miden-testing/src/kernel_tests/tx/test_fee.rs b/crates/miden-testing/src/kernel_tests/tx/test_fee.rs index c8b7a0dbbf..2256a43ab9 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_fee.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_fee.rs @@ -99,7 +99,7 @@ async fn num_tx_cycles_after_compute_fee_are_less_than_estimated( // These constants should always be updated together with the equivalent constants in // epilogue.masm. const SMT_SET_ADDITIONAL_CYCLES: usize = 250; - const NUM_POST_COMPUTE_FEE_CYCLES: usize = 500; + const NUM_POST_COMPUTE_FEE_CYCLES: usize = 608; assert!( tx.measurements().after_tx_cycles_obtained From 3ffe4ba3b340159ce1394b578b748dcb0fd5569c Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 10 Mar 2026 15:47:32 +0100 Subject: [PATCH 16/41] chore: add test for callback inputs --- .../src/kernel_tests/tx/test_callbacks.rs | 116 +++++++++++++++++- 1 file changed, 115 insertions(+), 1 deletion(-) 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 d06ac16f86..53c19d53f4 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs @@ -3,6 +3,7 @@ extern crate alloc; use alloc::collections::BTreeSet; use alloc::vec::Vec; +use miden_protocol::account::auth::AuthScheme; use miden_protocol::account::component::AccountComponentMetadata; use miden_protocol::account::{ AccountBuilder, @@ -171,6 +172,119 @@ impl From for AccountComponent { // TESTS // ================================================================================================ +/// Tests that the `on_before_asset_added_to_account` callback receives the correct inputs. +#[tokio::test] +async fn test_on_before_asset_added_to_account_callback_receives_correct_inputs() +-> anyhow::Result<()> { + let mut builder = crate::MockChain::builder(); + + // Create wallet first so we know its ID before building the faucet. + let target_account = builder.add_existing_wallet(Auth::IncrNonce)?; + let wallet_id_suffix = target_account.id().suffix().as_canonical_u64(); + let wallet_id_prefix = target_account.id().prefix().as_u64(); + + let amount: u64 = 100; + + // MASM callback that asserts the inputs match expected values. + let component_name = "miden::testing::callbacks::input_validator"; + let proc_name = "on_before_asset_added_to_account"; + let callback_masm = format!( + r#" + const ERR_WRONG_VALUE = "callback received unexpected asset value element" + + #! Inputs: [native_account_suffix, native_account_prefix, ASSET_KEY, ASSET_VALUE, pad(6)] + #! Outputs: [ASSET_VALUE, pad(12)] + pub proc {proc_name} + # Assert native account ID + push.{wallet_id_suffix} assert_eq.err="callback received unexpected native account ID suffix" + push.{wallet_id_prefix} assert_eq.err="callback received unexpected native account ID prefix" + # => [ASSET_KEY, ASSET_VALUE, pad(6)] + + # duplicate the asset value for returning + dupw.1 swapw + # => [ASSET_KEY, ASSET_VALUE, ASSET_VALUE, pad(6)] + + # build the expected asset + push.{amount} + exec.::miden::protocol::active_account::get_id + push.1 + # => [enable_callbacks, active_account_id_suffix, active_account_id_prefix, amount, ASSET_KEY, ASSET_VALUE, pad(6)] + exec.::miden::protocol::asset::create_fungible_asset + # => [EXPECTED_ASSET_KEY, EXPECTED_ASSET_VALUE, ASSET_KEY, ASSET_VALUE, ASSET_VALUE, pad(6)] + + movupw.2 + assert_eqw.err="callback received unexpected asset key" + # => [EXPECTED_ASSET_VALUE, ASSET_VALUE, ASSET_VALUE, pad(6)] + + assert_eqw.err="callback received unexpected asset value" + # => [ASSET_VALUE, pad(12)] + end + "# + ); + + // Compile the callback code and extract the procedure root. + let callback_code = + CodeBuilder::default().compile_component_code(component_name, callback_masm.as_str())?; + + let proc_root = callback_code + .as_library() + .get_procedure_root_by_path(format!("{component_name}::{proc_name}").as_str()) + .expect("callback should contain the procedure"); + + // Build the faucet with BasicFungibleFaucet + callback component. + let basic_faucet = BasicFungibleFaucet::new("CBK".try_into()?, 8, Felt::new(1_000_000))?; + + let callback_storage_slots = AssetCallbacks::new() + .on_before_asset_added_to_account(proc_root) + .into_storage_slots(); + + let callback_metadata = + AccountComponentMetadata::new(component_name, [AccountType::FungibleFaucet]) + .with_description("input validation callback component for testing"); + + let callback_component = + AccountComponent::new(callback_code, callback_storage_slots, callback_metadata)?; + + let account_builder = AccountBuilder::new([43u8; 32]) + .storage_mode(AccountStorageMode::Public) + .account_type(AccountType::FungibleFaucet) + .with_component(basic_faucet) + .with_component(callback_component); + + let faucet = builder.add_account_from_builder( + Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + }, + account_builder, + AccountState::Exists, + )?; + + // Create a P2ID note with a callbacks-enabled fungible asset. + let fungible_asset = + FungibleAsset::new(faucet.id(), amount)?.with_callbacks(AssetCallbacksFlag::Enabled); + let note = builder.add_p2id_note( + faucet.id(), + target_account.id(), + &[Asset::Fungible(fungible_asset)], + NoteType::Public, + )?; + + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + let faucet_inputs = mock_chain.get_foreign_account_inputs(faucet.id())?; + + // Execute the transaction - should succeed because all callback assertions pass. + mock_chain + .build_tx_context(target_account.id(), &[note.id()], &[])? + .foreign_accounts(vec![faucet_inputs]) + .build()? + .execute() + .await?; + + Ok(()) +} + /// Tests that a blocked account cannot receive assets with callbacks enabled. /// /// Flow: @@ -196,7 +310,7 @@ async fn test_blocked_account_cannot_receive_asset() -> anyhow::Result<()> { let faucet = builder.add_account_from_builder( Auth::BasicAuth { - auth_scheme: miden_protocol::account::auth::AuthScheme::Falcon512Poseidon2, + auth_scheme: AuthScheme::Falcon512Poseidon2, }, account_builder, AccountState::Exists, From de06e70421b9d2738c157f41b8f0172ba033071b Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 10 Mar 2026 17:00:59 +0100 Subject: [PATCH 17/41] feat: use `AssetVaultKey` in `FungibleAssetDelta` --- .../transaction/lib/account_delta.masm | 16 ++-- .../miden-protocol/src/account/delta/mod.rs | 4 +- .../miden-protocol/src/account/delta/vault.rs | 91 +++++++++++-------- crates/miden-protocol/src/asset/vault/mod.rs | 9 +- .../block/proposed_block_success.rs | 2 +- crates/miden-tx/src/executor/exec_host.rs | 2 +- 6 files changed, 73 insertions(+), 51 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/account_delta.masm b/crates/miden-protocol/asm/kernels/transaction/lib/account_delta.masm index 7b9f7d6762..f1a02531af 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/account_delta.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/account_delta.masm @@ -338,35 +338,35 @@ proc update_fungible_asset_delta movup.8 loc_store.1 # => [KEY, VALUE0, ...] # this stack state is equivalent to: - # => [[0, 0, faucet_id_suffix, faucet_id_prefix], [delta_amount, 0, 0, 0], ...] + # => [[0, 0, faucet_id_suffix_and_metadata, faucet_id_prefix], [delta_amount, 0, 0, 0], ...] swapw - # => [[delta_amount, 0, 0, 0], [0, 0, faucet_id_suffix, faucet_id_prefix], ...] + # => [[delta_amount, 0, 0, 0], [0, 0, faucet_id_suffix_and_metadata, faucet_id_prefix], ...] # compute the absolute value of delta amount with a flag indicating whether it's positive exec.delta_amount_absolute - # => [is_delta_amount_positive, [delta_amount_abs, 0, 0, 0], [0, 0, faucet_id_suffix, faucet_id_prefix], ...] + # => [is_delta_amount_positive, [delta_amount_abs, 0, 0, 0], [0, 0, faucet_id_suffix_and_metadata, faucet_id_prefix], ...] # define the was_added value as equivalent to is_delta_amount_positive # this value is 1 if the amount was added and 0 if the amount was removed swap.6 drop - # => [[delta_amount_abs, 0, 0, 0], [0, was_added, faucet_id_suffix, faucet_id_prefix], ...] + # => [[delta_amount_abs, 0, 0, 0], [0, was_added, faucet_id_suffix_and_metadata, faucet_id_prefix], ...] dup neq.0 - # => [is_delta_amount_non_zero, [delta_amount_abs, 0, 0, 0], [0, was_added, faucet_id_suffix, faucet_id_prefix], ...] + # => [is_delta_amount_non_zero, [delta_amount_abs, 0, 0, 0], [0, was_added, faucet_id_suffix_and_metadata, faucet_id_prefix], ...] # if delta amount is non-zero, update the hasher if.true push.DOMAIN_ASSET swap.5 drop - # => [[delta_amount_abs, 0, 0, 0], [domain, was_added, faucet_id_suffix, faucet_id_prefix], ...] + # => [[delta_amount_abs, 0, 0, 0], [domain, was_added, faucet_id_suffix_and_metadata, faucet_id_prefix], ...] # swap value and metadata words swapw - # => [[domain, was_added, faucet_id_suffix, faucet_id_prefix], [delta_amount_abs, 0, 0, 0], RATE0, RATE1, CAPACITY] + # => [[domain, was_added, faucet_id_suffix_and_metadata, faucet_id_prefix], [delta_amount_abs, 0, 0, 0], RATE0, RATE1, CAPACITY] # drop previous RATE elements swapdw dropw dropw - # => [[domain, was_added, faucet_id_suffix, faucet_id_prefix], [delta_amount_abs, 0, 0, 0], CAPACITY] + # => [[domain, was_added, faucet_id_suffix_and_metadata, faucet_id_prefix], [delta_amount_abs, 0, 0, 0], CAPACITY] exec.poseidon2::permute # => [RATE0, RATE1, CAPACITY] diff --git a/crates/miden-protocol/src/account/delta/mod.rs b/crates/miden-protocol/src/account/delta/mod.rs index be0ab3512c..5fdddc577b 100644 --- a/crates/miden-protocol/src/account/delta/mod.rs +++ b/crates/miden-protocol/src/account/delta/mod.rs @@ -201,7 +201,9 @@ impl AccountDelta { /// - Fungible Asset Delta /// - For each **updated** fungible asset, sorted by its vault key, whose amount delta is /// **non-zero**: - /// - Append `[domain = 1, was_added, faucet_id_suffix, faucet_id_prefix]`. + /// - Append `[domain = 1, was_added, faucet_id_suffix_and_metadata, faucet_id_prefix]` + /// where `faucet_id_suffix_and_metadata` is the faucet ID suffix with asset metadata + /// (including the callbacks flag) encoded in the lower 8 bits. /// - Append `[amount_delta, 0, 0, 0]` where `amount_delta` is the delta by which the /// fungible asset's amount has changed and `was_added` is a boolean flag indicating /// whether the amount was added (1) or subtracted (0). diff --git a/crates/miden-protocol/src/account/delta/vault.rs b/crates/miden-protocol/src/account/delta/vault.rs index c27061b21f..7f27084331 100644 --- a/crates/miden-protocol/src/account/delta/vault.rs +++ b/crates/miden-protocol/src/account/delta/vault.rs @@ -11,9 +11,9 @@ use super::{ DeserializationError, Serializable, }; -use crate::account::{AccountId, AccountType}; +use crate::account::AccountType; use crate::asset::{Asset, AssetVaultKey, FungibleAsset, NonFungibleAsset}; -use crate::{Felt, ONE, ZERO}; +use crate::{Felt, ONE, Word, ZERO}; // ACCOUNT VAULT DELTA // ================================================================================================ @@ -134,8 +134,12 @@ impl AccountVaultDelta { .0 .iter() .filter(|&(_, &value)| value >= 0) - .map(|(&faucet_id, &diff)| { - Asset::Fungible(FungibleAsset::new(faucet_id, diff.unsigned_abs()).unwrap()) + .map(|(vault_key, &diff)| { + Asset::Fungible( + FungibleAsset::new(vault_key.faucet_id(), diff.unsigned_abs()) + .unwrap() + .with_callbacks(vault_key.callbacks()), + ) }) .chain( self.non_fungible @@ -150,8 +154,12 @@ impl AccountVaultDelta { .0 .iter() .filter(|&(_, &value)| value < 0) - .map(|(&faucet_id, &diff)| { - Asset::Fungible(FungibleAsset::new(faucet_id, diff.unsigned_abs()).unwrap()) + .map(|(vault_key, &diff)| { + Asset::Fungible( + FungibleAsset::new(vault_key.faucet_id(), diff.unsigned_abs()) + .unwrap() + .with_callbacks(vault_key.callbacks()), + ) }) .chain( self.non_fungible @@ -185,15 +193,18 @@ impl Deserializable for AccountVaultDelta { // ================================================================================================ /// A binary tree map of fungible asset balance changes in the account vault. +/// +/// The [`AssetVaultKey`] orders the assets in the same way as the in-kernel account delta which +/// uses a link map. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct FungibleAssetDelta(BTreeMap); +pub struct FungibleAssetDelta(BTreeMap); impl FungibleAssetDelta { /// Validates and creates a new fungible asset delta. /// /// # Errors /// Returns an error if the delta does not pass the validation. - pub fn new(map: BTreeMap) -> Result { + pub fn new(map: BTreeMap) -> Result { let delta = Self(map); delta.validate()?; @@ -206,7 +217,7 @@ impl FungibleAssetDelta { /// Returns an error if the delta would overflow. pub fn add(&mut self, asset: FungibleAsset) -> Result<(), AccountDeltaError> { let amount: i64 = asset.amount().try_into().expect("Amount it too high"); - self.add_delta(asset.faucet_id(), amount) + self.add_delta(asset.vault_key(), amount) } /// Removes a fungible asset from the delta. @@ -215,12 +226,12 @@ impl FungibleAssetDelta { /// Returns an error if the delta would overflow. pub fn remove(&mut self, asset: FungibleAsset) -> Result<(), AccountDeltaError> { let amount: i64 = asset.amount().try_into().expect("Amount it too high"); - self.add_delta(asset.faucet_id(), -amount) + self.add_delta(asset.vault_key(), -amount) } - /// Returns the amount of the fungible asset with the given faucet ID. - pub fn amount(&self, faucet_id: &AccountId) -> Option { - self.0.get(faucet_id).copied() + /// Returns the amount of the fungible asset with the given vault key. + pub fn amount(&self, vault_key: &AssetVaultKey) -> Option { + self.0.get(vault_key).copied() } /// Returns the number of fungible assets affected in the delta. @@ -234,7 +245,7 @@ impl FungibleAssetDelta { } /// Returns an iterator over the (key, value) pairs of the map. - pub fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator { self.0.iter() } @@ -250,8 +261,8 @@ impl FungibleAssetDelta { // Track fungible asset amounts - positive and negative. `i64` is not lossy while // fungibles are restricted to 2^63-1. Overflow is still possible but we check for that. - for (&faucet_id, &amount) in other.0.iter() { - self.add_delta(faucet_id, amount)?; + for (&vault_key, &amount) in other.0.iter() { + self.add_delta(vault_key, amount)?; } Ok(()) @@ -265,8 +276,8 @@ impl FungibleAssetDelta { /// /// # Errors /// Returns an error if the delta would overflow. - fn add_delta(&mut self, faucet_id: AccountId, delta: i64) -> Result<(), AccountDeltaError> { - match self.0.entry(faucet_id) { + fn add_delta(&mut self, vault_key: AssetVaultKey, delta: i64) -> Result<(), AccountDeltaError> { + match self.0.entry(vault_key) { Entry::Vacant(entry) => { // Only track non-zero amounts. if delta != 0 { @@ -277,7 +288,7 @@ impl FungibleAssetDelta { let old = *entry.get(); let new = old.checked_add(delta).ok_or( AccountDeltaError::FungibleAssetDeltaOverflow { - faucet_id, + faucet_id: vault_key.faucet_id(), current: old, delta, }, @@ -299,9 +310,9 @@ impl FungibleAssetDelta { /// # Errors /// Returns an error if one or more fungible assets' faucet IDs are invalid. fn validate(&self) -> Result<(), AccountDeltaError> { - for faucet_id in self.0.keys() { - if !matches!(faucet_id.account_type(), AccountType::FungibleFaucet) { - return Err(AccountDeltaError::NotAFungibleFaucetId(*faucet_id)); + for vault_key in self.0.keys() { + if !matches!(vault_key.faucet_id().account_type(), AccountType::FungibleFaucet) { + return Err(AccountDeltaError::NotAFungibleFaucetId(vault_key.faucet_id())); } } @@ -314,12 +325,12 @@ impl FungibleAssetDelta { /// Note that the order in which elements are appended should be the link map key ordering. This /// is fulfilled here because the link map key's most significant element takes precedence over /// less significant ones. The most significant element in the fungible asset delta is the - /// account ID prefix and the delta happens to be sorted by account IDs. Since the account ID + /// faucet ID prefix and the delta happens to be sorted by vault keys. Since the faucet ID /// prefix is unique, it will always decide on the ordering of a link map key, so less /// significant elements are unimportant. This implicit sort should therefore always match the /// link map key ordering, however this is subtle and fragile. pub(super) fn append_delta_elements(&self, elements: &mut Vec) { - for (faucet_id, amount_delta) in self.iter() { + for (vault_key, amount_delta) in self.iter() { // Note that this iterator is guaranteed to never yield zero amounts, so we don't have // to exclude those explicitly. debug_assert_ne!( @@ -331,11 +342,12 @@ impl FungibleAssetDelta { let amount_delta = Felt::try_from(amount_delta.unsigned_abs()) .expect("amount delta should be less than i64::MAX"); + let key_word = vault_key.to_word(); elements.extend_from_slice(&[ DOMAIN_ASSET, was_added, - faucet_id.suffix(), - faucet_id.prefix().as_felt(), + key_word[2], // faucet_id_suffix_and_metadata + key_word[3], // faucet_id_prefix ]); elements.extend_from_slice(&[amount_delta, ZERO, ZERO, ZERO]); } @@ -348,12 +360,15 @@ impl Serializable for FungibleAssetDelta { // TODO: We save `i64` as `u64` since winter utils only supports unsigned integers for now. // We should update this code (and deserialization as well) once it supports signed // integers. - target.write_many(self.0.iter().map(|(&faucet_id, &delta)| (faucet_id, delta as u64))); + for (vault_key, &delta) in self.0.iter() { + vault_key.to_word().write_into(target); + (delta as u64).write_into(target); + } } fn get_size_hint(&self) -> usize { - // Each entry is (AccountId, u64 delta) - const ENTRY_SIZE: usize = AccountId::SERIALIZED_SIZE + core::mem::size_of::(); + // Each entry is (Word, u64 delta) = 4*8 + 8 = 40 bytes + const ENTRY_SIZE: usize = Word::SERIALIZED_SIZE + core::mem::size_of::(); self.0.len().get_size_hint() + self.0.len() * ENTRY_SIZE } } @@ -362,14 +377,16 @@ impl Deserializable for FungibleAssetDelta { fn read_from(source: &mut R) -> Result { let num_fungible_assets = source.read_usize()?; // TODO: We save `i64` as `u64` since winter utils only supports unsigned integers for now. - // We should update this code (and serialization as well) once it support signeds - // integers. - let map = source - .read_many_iter::<(AccountId, u64)>(num_fungible_assets)? - .map(|result| { - result.map(|(account_id, delta_as_u64)| (account_id, delta_as_u64 as i64)) - }) - .collect::>()?; + // We should update this code (and serialization as well) once it supports signed + // integers. + let mut map = BTreeMap::new(); + for _ in 0..num_fungible_assets { + let word: Word = source.read()?; + let vault_key = AssetVaultKey::try_from(word) + .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?; + let delta_as_u64: u64 = source.read()?; + map.insert(vault_key, delta_as_u64 as i64); + } Self::new(map).map_err(|err| DeserializationError::InvalidValue(err.to_string())) } diff --git a/crates/miden-protocol/src/asset/vault/mod.rs b/crates/miden-protocol/src/asset/vault/mod.rs index ac1d507231..234507530b 100644 --- a/crates/miden-protocol/src/asset/vault/mod.rs +++ b/crates/miden-protocol/src/asset/vault/mod.rs @@ -179,9 +179,12 @@ impl AssetVault { /// - If the delta contains a non-fungible asset addition that is already stored in the vault. /// - The maximum number of leaves per asset is exceeded. pub fn apply_delta(&mut self, delta: &AccountVaultDelta) -> Result<(), AssetVaultError> { - for (&faucet_id, &delta) in delta.fungible().iter() { - let asset = FungibleAsset::new(faucet_id, delta.unsigned_abs()) - .expect("Not a fungible faucet ID or delta is too large"); + for (vault_key, &delta) in delta.fungible().iter() { + // SAFETY: fungible asset delta should only contain fungible faucet IDs and delta amount + // should be in bounds + let asset = FungibleAsset::new(vault_key.faucet_id(), delta.unsigned_abs()) + .expect("fungible asset delta should be valid") + .with_callbacks(vault_key.callbacks()); match delta >= 0 { true => self.add_fungible_asset(asset), false => self.remove_fungible_asset(asset), diff --git a/crates/miden-testing/src/kernel_tests/block/proposed_block_success.rs b/crates/miden-testing/src/kernel_tests/block/proposed_block_success.rs index 29ea41cef6..46cd8eb3a6 100644 --- a/crates/miden-testing/src/kernel_tests/block/proposed_block_success.rs +++ b/crates/miden-testing/src/kernel_tests/block/proposed_block_success.rs @@ -171,7 +171,7 @@ async fn proposed_block_aggregates_account_state_transition() -> anyhow::Result< assert_matches!(account_update.details(), AccountUpdateDetails::Delta(delta) => { assert_eq!(delta.vault().fungible().num_assets(), 1); - assert_eq!(delta.vault().fungible().amount(&asset.unwrap_fungible().faucet_id()).unwrap(), 300); + assert_eq!(delta.vault().fungible().amount(&asset.unwrap_fungible().vault_key()).unwrap(), 300); }); Ok(()) diff --git a/crates/miden-tx/src/executor/exec_host.rs b/crates/miden-tx/src/executor/exec_host.rs index ef525d1682..4d19a2b3bb 100644 --- a/crates/miden-tx/src/executor/exec_host.rs +++ b/crates/miden-tx/src/executor/exec_host.rs @@ -236,7 +236,7 @@ where .account_delta_tracker() .vault_delta() .fungible() - .amount(&initial_fee_asset.faucet_id()) + .amount(&initial_fee_asset.vault_key()) .unwrap_or(0); // SAFETY: Initial native asset faucet ID should be a fungible faucet and amount should From 6316cb891ea93fec9b70e557d8800fe9433b0a7b Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 10 Mar 2026 17:09:27 +0100 Subject: [PATCH 18/41] chore: implement serialization for `AssetVaultKey` --- .../miden-protocol/src/account/delta/vault.rs | 22 ++++--------- .../src/asset/vault/vault_key.rs | 33 +++++++++++++++++++ 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/crates/miden-protocol/src/account/delta/vault.rs b/crates/miden-protocol/src/account/delta/vault.rs index 7f27084331..5dfdc2d875 100644 --- a/crates/miden-protocol/src/account/delta/vault.rs +++ b/crates/miden-protocol/src/account/delta/vault.rs @@ -13,7 +13,7 @@ use super::{ }; use crate::account::AccountType; use crate::asset::{Asset, AssetVaultKey, FungibleAsset, NonFungibleAsset}; -use crate::{Felt, ONE, Word, ZERO}; +use crate::{Felt, ONE, ZERO}; // ACCOUNT VAULT DELTA // ================================================================================================ @@ -360,15 +360,11 @@ impl Serializable for FungibleAssetDelta { // TODO: We save `i64` as `u64` since winter utils only supports unsigned integers for now. // We should update this code (and deserialization as well) once it supports signed // integers. - for (vault_key, &delta) in self.0.iter() { - vault_key.to_word().write_into(target); - (delta as u64).write_into(target); - } + target.write_many(self.0.iter().map(|(vault_key, &delta)| (*vault_key, delta as u64))); } fn get_size_hint(&self) -> usize { - // Each entry is (Word, u64 delta) = 4*8 + 8 = 40 bytes - const ENTRY_SIZE: usize = Word::SERIALIZED_SIZE + core::mem::size_of::(); + const ENTRY_SIZE: usize = AssetVaultKey::SERIALIZED_SIZE + core::mem::size_of::(); self.0.len().get_size_hint() + self.0.len() * ENTRY_SIZE } } @@ -379,14 +375,10 @@ impl Deserializable for FungibleAssetDelta { // TODO: We save `i64` as `u64` since winter utils only supports unsigned integers for now. // We should update this code (and serialization as well) once it supports signed // integers. - let mut map = BTreeMap::new(); - for _ in 0..num_fungible_assets { - let word: Word = source.read()?; - let vault_key = AssetVaultKey::try_from(word) - .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?; - let delta_as_u64: u64 = source.read()?; - map.insert(vault_key, delta_as_u64 as i64); - } + let map = source + .read_many_iter::<(AssetVaultKey, u64)>(num_fungible_assets)? + .map(|result| result.map(|(vault_key, delta_as_u64)| (vault_key, delta_as_u64 as i64))) + .collect::>()?; Self::new(map).map_err(|err| DeserializationError::InvalidValue(err.to_string())) } diff --git a/crates/miden-protocol/src/asset/vault/vault_key.rs b/crates/miden-protocol/src/asset/vault/vault_key.rs index 19714aaf3e..80d03811bd 100644 --- a/crates/miden-protocol/src/asset/vault/vault_key.rs +++ b/crates/miden-protocol/src/asset/vault/vault_key.rs @@ -1,4 +1,5 @@ use alloc::boxed::Box; +use alloc::string::ToString; use core::fmt; use miden_core::LexicographicWord; @@ -10,6 +11,13 @@ use crate::asset::vault::AssetId; use crate::asset::{Asset, AssetCallbacksFlag, FungibleAsset, NonFungibleAsset}; use crate::crypto::merkle::smt::SMT_DEPTH; use crate::errors::AssetError; +use crate::utils::serde::{ + ByteReader, + ByteWriter, + Deserializable, + DeserializationError, + Serializable, +}; use crate::{Felt, Word}; /// The unique identifier of an [`Asset`] in the [`AssetVault`](crate::asset::AssetVault). @@ -36,6 +44,11 @@ pub struct AssetVaultKey { } impl AssetVaultKey { + /// The serialized size of an [`AssetVaultKey`] in bytes. + /// + /// Serialized as its [`Word`] representation (4 field elements). + pub const SERIALIZED_SIZE: usize = Word::SERIALIZED_SIZE; + // CONSTRUCTORS // -------------------------------------------------------------------------------------------- @@ -219,6 +232,26 @@ impl From for AssetVaultKey { } } +// SERIALIZATION +// ================================================================================================ + +impl Serializable for AssetVaultKey { + fn write_into(&self, target: &mut W) { + self.to_word().write_into(target); + } + + fn get_size_hint(&self) -> usize { + Self::SERIALIZED_SIZE + } +} + +impl Deserializable for AssetVaultKey { + fn read_from(source: &mut R) -> Result { + let word: Word = source.read()?; + Self::try_from(word).map_err(|err| DeserializationError::InvalidValue(err.to_string())) + } +} + // TESTS // ================================================================================================ From 7588e080f482e0841cb0d6e034fa7b1fa735fd9e Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 10 Mar 2026 17:17:25 +0100 Subject: [PATCH 19/41] fix: test_create_fungible_asset_succeeds --- crates/miden-testing/src/kernel_tests/tx/test_asset.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/miden-testing/src/kernel_tests/tx/test_asset.rs b/crates/miden-testing/src/kernel_tests/tx/test_asset.rs index 5ba4e4f1dc..227f8d5725 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_asset.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_asset.rs @@ -44,8 +44,8 @@ async fn test_create_fungible_asset_succeeds() -> anyhow::Result<()> { begin exec.prologue::prepare_transaction - # create fungible asset - push.{FUNGIBLE_ASSET_AMOUNT} + # create fungible asset (enable_callbacks = 0) + push.{FUNGIBLE_ASSET_AMOUNT} push.0 exec.faucet::create_fungible_asset # => [ASSET_KEY, ASSET_VALUE] From 82ed18ba6422c0f7925f7adb09beacc99fe48782 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 10 Mar 2026 17:31:50 +0100 Subject: [PATCH 20/41] chore: add missing docs --- .../kernels/transaction/lib/callbacks.masm | 29 ++++++++++++++++--- .../src/asset/asset_callbacks.rs | 4 ++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm b/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm index f476a08ffd..2ad14a8d2b 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm @@ -7,7 +7,7 @@ use miden::core::word # ================================================================================================== # The index of the local memory slot that contains the procedure root of the callback. -const CALLBACK_PROC_ROOT_PTR = 0 +const CALLBACK_PROC_ROOT_LOC = 0 # The name of the storage slot where the procedure root for the on_before_asset_added_to_account callback # is stored. @@ -16,10 +16,21 @@ const ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_ROOT = word("miden::protocol::faucet # PROCEDURES # ================================================================================================== -#! TODO +#! Invokes the `on_before_asset_added_to_account` callback on the faucet that issued the asset, +#! if the asset has callbacks enabled. +#! +#! The callback invocation is skipped in two cases: +#! - If the global callback flag in the asset key is `Disabled`. +#! - If the callback storage slot contains the empty word. #! #! Inputs: [ASSET_KEY, ASSET_VALUE] #! Outputs: [PROCESSED_ASSET_VALUE] +#! +#! Where: +#! - ASSET_KEY is the vault key of the asset being added. +#! - ASSET_VALUE is the value of the asset being added. +#! - PROCESSED_ASSET_VALUE is the asset value returned by the callback, or the original +#! ASSET_VALUE if callbacks are disabled. pub proc on_before_asset_added_to_account exec.asset::key_to_callbacks_enabled # => [callbacks_enabled, ASSET_KEY, ASSET_VALUE] @@ -35,8 +46,18 @@ pub proc on_before_asset_added_to_account # => [PROCESSED_ASSET_VALUE] end +#! Executes the `on_before_asset_added_to_account` callback by starting a foreign context against +#! the faucet, reading the callback procedure root from the faucet's storage, and invoking it via +#! `dyncall`. +#! #! Inputs: [ASSET_KEY, ASSET_VALUE] #! Outputs: [PROCESSED_ASSET_VALUE] +#! +#! Where: +#! - ASSET_KEY is the vault key of the asset being added. +#! - ASSET_VALUE is the value of the asset being added. +#! - PROCESSED_ASSET_VALUE is the asset value returned by the callback, or the original +#! ASSET_VALUE if no callback is configured. @locals(4) proc on_before_asset_added_to_account_raw exec.start_foreign_callback_context @@ -57,12 +78,12 @@ proc on_before_asset_added_to_account_raw # => [ASSET_VALUE] else # prepare for dyncall by storing procedure root in local memory - loc_storew_le.CALLBACK_PROC_ROOT_PTR dropw + loc_storew_le.CALLBACK_PROC_ROOT_LOC dropw # => [native_account_id_suffix, native_account_id_prefix, ASSET_KEY, ASSET_VALUE] # pad the stack to 16 for the call repeat.6 push.0 movdn.10 end - locaddr.CALLBACK_PROC_ROOT_PTR + locaddr.CALLBACK_PROC_ROOT_LOC # => [callback_proc_root_ptr, native_account_id_suffix, native_account_id_prefix, ASSET_KEY, ASSET_VALUE, pad(6)] dyncall diff --git a/crates/miden-protocol/src/asset/asset_callbacks.rs b/crates/miden-protocol/src/asset/asset_callbacks.rs index e957cbea54..cd26d8f2ea 100644 --- a/crates/miden-protocol/src/asset/asset_callbacks.rs +++ b/crates/miden-protocol/src/asset/asset_callbacks.rs @@ -20,7 +20,9 @@ static ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_SLOT_NAME: LazyLock = /// /// ## Storage Layout /// -/// - [`Self::slot`]: Stores the procedure root of the `on_before_asset_added_to_account` callback. +/// - [`Self::on_before_asset_added_to_account_slot()`]: Stores the procedure root of the +/// `on_before_asset_added_to_account` callback. This storage slot is only added if a callback +/// procedure root is set. /// /// [`AssetCallbacksFlag::Enabled`]: crate::asset::AssetCallbacksFlag::Enabled #[derive(Debug, Clone, Default, PartialEq, Eq)] From 301425ee406dd02bf2bfe80df874078fcf015a65 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 10 Mar 2026 17:44:49 +0100 Subject: [PATCH 21/41] chore: split fpi memory clearing out of end_foreign_context --- .../miden-protocol/asm/kernels/transaction/api.masm | 4 ++++ .../asm/kernels/transaction/lib/tx.masm | 11 ++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/api.masm b/crates/miden-protocol/asm/kernels/transaction/api.masm index d885aa580b..601dc2dbbd 100644 --- a/crates/miden-protocol/asm/kernels/transaction/api.masm +++ b/crates/miden-protocol/asm/kernels/transaction/api.masm @@ -1502,6 +1502,10 @@ pub proc tx_exec_foreign_proc # end the foreign context exec.tx::end_foreign_context # => [foreign_procedure_outputs(16)] + + # clear the foreign procedure ID and root in memory + exec.tx::clear_fpi_memory + # => [foreign_procedure_outputs(16)] end #! Updates the transaction expiration block delta. diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/tx.masm b/crates/miden-protocol/asm/kernels/transaction/lib/tx.masm index 63abdb5f63..aaed2d01fc 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/tx.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/tx.masm @@ -205,9 +205,7 @@ end #! Ends a foreign account context. #! -#! This pops the top of the account stack, making the previous account the active account, and -#! resets the foreign procedure info (foreign account ID and foreign procedure root) in the kernel -#! memory. +#! This pops the top of the account stack, making the previous account the active account. #! #! Inputs: [] #! Outputs: [] @@ -219,7 +217,14 @@ end pub proc end_foreign_context exec.memory::pop_ptr_from_account_stack # => [] +end +#! Resets the foreign procedure info (foreign account ID and foreign procedure root) in the kernel +#! memory to zeros. +#! +#! Inputs: [] +#! Outputs: [] +proc clear_fpi_memory # set the upcoming foreign account ID to zero push.0 push.0 exec.memory::set_fpi_account_id # => [] From 5014e9bf9dda7382694b103861acc370e4ad84d7 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 10 Mar 2026 17:45:28 +0100 Subject: [PATCH 22/41] chore: add changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e42fe91417..b26aedcb02 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)). +- Implemented the `on_before_asset_added_to_account` asset callback ([#2571](https://github.com/0xMiden/protocol/pull/2571)). ### Changes @@ -84,6 +85,8 @@ - [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`. - [BREAKING] Introduced `PrivateNoteHeader` for output notes and removed `RawOutputNote::Header` variant ([#2569](https://github.com/0xMiden/protocol/pull/2569)). +- [BREAKING] Changed `asset::create_fungible_asset` and `faucet::create_fungible_asset` signature to take `enable_callbacks` flag ([#2571](https://github.com/0xMiden/protocol/pull/2571)). + ## 0.13.3 (2026-01-27) From 9073f5c1fe2e08582cf094776b241789ba54c67e Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 10 Mar 2026 19:27:25 +0100 Subject: [PATCH 23/41] chore: update protocol library docs --- docs/src/protocol_library.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/src/protocol_library.md b/docs/src/protocol_library.md index 61b3ff2e2a..ff35230383 100644 --- a/docs/src/protocol_library.md +++ b/docs/src/protocol_library.md @@ -54,6 +54,7 @@ Active account procedures can be used to read from storage, fetch or compute com | `get_num_procedures` | Returns the number of procedures in the active account.

**Inputs:** `[]`
**Outputs:** `[num_procedures]` | Any | | `get_procedure_root` | Returns the procedure root for the procedure at the specified index.

**Inputs:** `[index]`
**Outputs:** `[PROC_ROOT]` | Any | | `has_procedure` | Returns the binary flag indicating whether the procedure with the provided root is available on the active account.

**Inputs:** `[PROC_ROOT]`
**Outputs:** `[is_procedure_available]` | Any | +| `has_callbacks` | Returns whether the active account defines callbacks.

**Inputs:** `[]`
**Outputs:** `[has_callbacks]` | Any | ## Native account Procedures (`miden::protocol::native_account`) @@ -150,7 +151,7 @@ Faucet procedures allow reading and writing to faucet accounts to mint and burn | Procedure | Description | Context | | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | -| `create_fungible_asset` | Creates a fungible asset for the faucet the transaction is being executed against.

**Inputs:** `[amount]`
**Outputs:** `[ASSET_KEY, ASSET_VALUE]` | Faucet | +| `create_fungible_asset` | Creates a fungible asset for the faucet the transaction is being executed against.

**Inputs:** `[enable_callbacks, amount]`
**Outputs:** `[ASSET_KEY, ASSET_VALUE]` | Faucet | | `create_non_fungible_asset` | Creates a non-fungible asset for the faucet the transaction is being executed against.

**Inputs:** `[DATA_HASH]`
**Outputs:** `[ASSET_KEY, ASSET_VALUE]` | Faucet | | `mint` | Mint an asset from the faucet the transaction is being executed against.

**Inputs:** `[ASSET_KEY, ASSET_VALUE]`
**Outputs:** `[NEW_ASSET_VALUE]` | Native & Account & Faucet | | `burn` | Burn an asset from the faucet the transaction is being executed against.

**Inputs:** `[ASSET_KEY, ASSET_VALUE]`
**Outputs:** `[ASSET_VALUE]` | Native & Account & Faucet | @@ -161,5 +162,5 @@ Asset procedures provide utilities for creating fungible and non-fungible assets | Procedure | Description | Context | | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -| `create_fungible_asset` | Builds a fungible asset for the specified fungible faucet and amount.

**Inputs:** `[faucet_id_suffix, faucet_id_prefix, amount]`
**Outputs:** `[ASSET_KEY, ASSET_VALUE]` | Any | +| `create_fungible_asset` | Builds a fungible asset for the specified fungible faucet and amount.

**Inputs:** `[enable_callbacks, faucet_id_suffix, faucet_id_prefix, amount]`
**Outputs:** `[ASSET_KEY, ASSET_VALUE]` | Any | | `create_non_fungible_asset` | Builds a non-fungible asset for the specified non-fungible faucet and data hash.

**Inputs:** `[faucet_id_suffix, faucet_id_prefix, DATA_HASH]`
**Outputs:** `[ASSET_KEY, ASSET_VALUE]` | Any | From b0e8e1679e0791836fddf23f64e21d999b3c2e9e Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 10 Mar 2026 19:42:32 +0100 Subject: [PATCH 24/41] chore: deduplicate slot ID definition --- .../asm/kernels/transaction/lib/account.masm | 32 +++++++++---------- .../kernels/transaction/lib/callbacks.masm | 9 +++--- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/account.masm b/crates/miden-protocol/asm/kernels/transaction/lib/account.masm index 6716f4089b..99edd790bb 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/account.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/account.masm @@ -2,6 +2,7 @@ use $kernel::account_delta use $kernel::account_id use $kernel::asset_vault use $kernel::callbacks +use $kernel::callbacks::ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_ROOT_SLOT use $kernel::constants::ACCOUNT_PROCEDURE_DATA_LENGTH use $kernel::constants::EMPTY_SMT_ROOT use $kernel::constants::STORAGE_SLOT_TYPE_MAP @@ -18,9 +19,6 @@ use miden::core::word # ERRORS # ================================================================================================= -# The slot ID of the on_before_asset_added_to_account callback. -const ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_SLOT_ID = word("miden::protocol::faucet::callback::on_before_asset_added_to_account") - const ERR_ACCOUNT_NONCE_CAN_ONLY_BE_INCREMENTED_ONCE="account nonce can only be incremented once" const ERR_ACCOUNT_NONCE_AT_MAX="account nonce is already at its maximum possible value" @@ -675,33 +673,33 @@ pub proc add_asset_to_vault exec.callbacks::on_before_asset_added_to_account swapw - # => [ASSET_KEY, ASSET_VALUE] + # => [ASSET_KEY, PROCESSED_ASSET_VALUE] # duplicate the asset for the later event and delta update dupw.1 dupw.1 - # => [ASSET_KEY, ASSET_VALUE, ASSET_KEY, ASSET_VALUE] + # => [ASSET_KEY, PROCESSED_ASSET_VALUE, ASSET_KEY, PROCESSED_ASSET_VALUE] # push the account vault root ptr exec.memory::get_account_vault_root_ptr movdn.8 - # => [ASSET_KEY, ASSET_VALUE, account_vault_root_ptr, ASSET_KEY, ASSET_VALUE] + # => [ASSET_KEY, PROCESSED_ASSET_VALUE, account_vault_root_ptr, ASSET_KEY, PROCESSED_ASSET_VALUE] # emit event to signal that an asset is going to be added to the account vault emit.ACCOUNT_VAULT_BEFORE_ADD_ASSET_EVENT - # => [ASSET_KEY, ASSET_VALUE, account_vault_root_ptr, ASSET_KEY, ASSET_VALUE] + # => [ASSET_KEY, PROCESSED_ASSET_VALUE, account_vault_root_ptr, ASSET_KEY, PROCESSED_ASSET_VALUE] # add the asset to the account vault exec.asset_vault::add_asset - # => [ASSET_VALUE', ASSET_KEY, ASSET_VALUE] + # => [PROCESSED_ASSET_VALUE', ASSET_KEY, PROCESSED_ASSET_VALUE] movdnw.2 - # => [ASSET_KEY, ASSET_VALUE, ASSET_VALUE'] + # => [ASSET_KEY, PROCESSED_ASSET_VALUE, PROCESSED_ASSET_VALUE'] # emit event to signal that an asset is being added to the account vault emit.ACCOUNT_VAULT_AFTER_ADD_ASSET_EVENT - # => [ASSET_KEY, ASSET_VALUE, ASSET_VALUE'] + # => [ASSET_KEY, PROCESSED_ASSET_VALUE, PROCESSED_ASSET_VALUE'] exec.account_delta::add_asset - # => [ASSET_VALUE'] + # => [PROCESSED_ASSET_VALUE'] end #! Removes the specified asset from the account vault. @@ -1624,8 +1622,8 @@ proc get_map_item_raw # => [VALUE] end -#! Finds the slot identified by the key [slot_id_prefix, slot_id_suffix, 0, 0] (stack order) and -#! returns a flag indicating whether the slot was found and the pointer to that slot. +#! Finds the slot identified by the key [_, _, slot_id_suffix, slot_id_prefix] and returns a flag +#! indicating whether the slot was found and the pointer to that slot. #! #! Inputs: [storage_slots_ptr, slot_id_suffix, slot_id_prefix] #! Outputs: [is_found, slot_ptr] @@ -1644,7 +1642,6 @@ proc find_storage_slot swap movup.3 movup.3 # => [slot_id_suffix, slot_id_prefix, storage_slots_start_ptr, storage_slots_end_ptr] - # find the slot whose slot key matches [_, _, slot_id_suffix, slot_id_prefix] # if the slot key does not exist, this procedure will validate its absence exec.sorted_array::find_half_key_value # => [is_slot_found, slot_ptr, storage_slots_start_ptr, storage_slots_end_ptr] @@ -1653,8 +1650,8 @@ proc find_storage_slot # => [is_slot_found, slot_ptr] end -#! Finds the slot identified by the key [slot_id_prefix, slot_id_suffix, 0, 0] (stack order) and -#! returns the pointer to that slot. +#! Finds the slot identified by the key [_, _, slot_id_suffix, slot_id_prefix] and returns the +#! pointer to that slot. #! #! Inputs: [storage_slots_ptr, slot_id_suffix, slot_id_prefix] #! Outputs: [slot_ptr] @@ -2021,13 +2018,14 @@ end #! Returns whether the active account defines callbacks. #! +#! Inputs: [] #! Outputs: [has_callbacks] #! #! Where: #! - has_callbacks is 1 if the account defines callbacks, 0 otherwise. pub proc has_callbacks # check if the on_before_asset_added_to_account callback slot exists and is non-empty - push.ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_SLOT_ID[0..2] + push.ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_ROOT_SLOT[0..2] exec.has_non_empty_slot # => [has_callbacks] end diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm b/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm index 2ad14a8d2b..06560e27f1 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm @@ -11,7 +11,7 @@ const CALLBACK_PROC_ROOT_LOC = 0 # The name of the storage slot where the procedure root for the on_before_asset_added_to_account callback # is stored. -const ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_ROOT = word("miden::protocol::faucet::callback::on_before_asset_added_to_account") +pub const ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_ROOT_SLOT = word("miden::protocol::faucet::callback::on_before_asset_added_to_account") # PROCEDURES # ================================================================================================== @@ -64,7 +64,7 @@ proc on_before_asset_added_to_account_raw # => [native_account_id_suffix, native_account_id_prefix, ASSET_KEY, ASSET_VALUE] # get the procedure root of the callback from the faucet's storage - push.ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_ROOT[0..2] + push.ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_ROOT_SLOT[0..2] exec.account::get_item # => [PROC_ROOT, native_account_id_suffix, native_account_id_prefix, ASSET_KEY, ASSET_VALUE] @@ -81,16 +81,17 @@ proc on_before_asset_added_to_account_raw loc_storew_le.CALLBACK_PROC_ROOT_LOC dropw # => [native_account_id_suffix, native_account_id_prefix, ASSET_KEY, ASSET_VALUE] - # pad the stack to 16 for the call + # pad the stack to 16 for the call (excluding the proc root ptr) repeat.6 push.0 movdn.10 end locaddr.CALLBACK_PROC_ROOT_LOC # => [callback_proc_root_ptr, native_account_id_suffix, native_account_id_prefix, ASSET_KEY, ASSET_VALUE, pad(6)] + # invoke the callback dyncall # => [PROCESSED_ASSET_VALUE, pad(12)] # truncate the stack after the call - repeat.3 swapw dropw end + swapdw dropw dropw swapw dropw # => [PROCESSED_ASSET_VALUE] end # => [PROCESSED_ASSET_VALUE] From dd751281dcf8b126c6510c6c92678eb755167bfe Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 10 Mar 2026 19:56:23 +0100 Subject: [PATCH 25/41] chore: use empty word for detection in `AssetCallbacks` --- .../asm/kernels/transaction/lib/epilogue.masm | 2 ++ .../asm/protocol/active_account.masm | 3 +++ crates/miden-protocol/asm/protocol/faucet.masm | 2 -- .../miden-protocol/src/asset/asset_callbacks.rs | 16 ++++++++-------- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm b/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm index 0c2d953d2a..4ac2b7ceea 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm @@ -291,6 +291,8 @@ proc create_native_fee_asset exec.memory::get_native_asset_id # => [native_asset_id_suffix, native_asset_id_prefix, fee_amount] + # assume the fee asset does not have callbacks + # this should be addressed more holistically with a fee construction refactor push.0 # => [enable_callbacks, native_asset_id_suffix, native_asset_id_prefix, fee_amount] diff --git a/crates/miden-protocol/asm/protocol/active_account.masm b/crates/miden-protocol/asm/protocol/active_account.masm index fecaec7bfa..f374d738b6 100644 --- a/crates/miden-protocol/asm/protocol/active_account.masm +++ b/crates/miden-protocol/asm/protocol/active_account.masm @@ -686,6 +686,9 @@ end #! Returns whether the active account defines callbacks. #! +#! The account defines callbacks if any callback storage slot is present and it contains not the +#! empty word. +#! #! Inputs: [] #! Outputs: [has_callbacks] #! diff --git a/crates/miden-protocol/asm/protocol/faucet.masm b/crates/miden-protocol/asm/protocol/faucet.masm index 0dd011181c..55e05aa2fa 100644 --- a/crates/miden-protocol/asm/protocol/faucet.masm +++ b/crates/miden-protocol/asm/protocol/faucet.masm @@ -20,8 +20,6 @@ use ::miden::protocol::kernel_proc_offsets::FAUCET_BURN_ASSET_OFFSET #! #! Invocation: exec pub proc create_fungible_asset - # => [enable_callbacks, amount] - # fetch the id of the faucet the transaction is being executed against. exec.active_account::get_id # => [id_suffix, id_prefix, enable_callbacks, amount] diff --git a/crates/miden-protocol/src/asset/asset_callbacks.rs b/crates/miden-protocol/src/asset/asset_callbacks.rs index cd26d8f2ea..7ee13722ee 100644 --- a/crates/miden-protocol/src/asset/asset_callbacks.rs +++ b/crates/miden-protocol/src/asset/asset_callbacks.rs @@ -21,26 +21,26 @@ static ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_SLOT_NAME: LazyLock = /// ## Storage Layout /// /// - [`Self::on_before_asset_added_to_account_slot()`]: Stores the procedure root of the -/// `on_before_asset_added_to_account` callback. This storage slot is only added if a callback -/// procedure root is set. +/// `on_before_asset_added_to_account` callback. This storage slot is only added if the callback +/// procedure root is not the empty word. /// /// [`AssetCallbacksFlag::Enabled`]: crate::asset::AssetCallbacksFlag::Enabled #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct AssetCallbacks { - on_before_asset_added_to_account: Option, + on_before_asset_added_to_account: Word, } impl AssetCallbacks { // CONSTRUCTORS // -------------------------------------------------------------------------------------------- - /// Creates a new [`AssetCallbacks`] with all callbacks set to `None`. + /// Creates a new [`AssetCallbacks`] with all callbacks set to the empty word. pub fn new() -> Self { Self::default() } pub fn on_before_asset_added_to_account(mut self, proc_root: Word) -> Self { - self.on_before_asset_added_to_account = Some(proc_root); + self.on_before_asset_added_to_account = proc_root; self } @@ -53,17 +53,17 @@ impl AssetCallbacks { } /// Returns the procedure root of the `on_before_asset_added_to_account` callback. - pub fn on_before_asset_added_proc_root(&self) -> Option { + pub fn on_before_asset_added_proc_root(&self) -> Word { self.on_before_asset_added_to_account } pub fn into_storage_slots(self) -> Vec { let mut slots = Vec::new(); - if let Some(on_before_asset_added_to_account) = self.on_before_asset_added_to_account { + if !self.on_before_asset_added_to_account.is_empty() { slots.push(StorageSlot::with_value( AssetCallbacks::on_before_asset_added_to_account_slot().clone(), - on_before_asset_added_to_account, + self.on_before_asset_added_to_account, )); } From 08d5582a79f5a1b6304389f94f5ad83c762c2445 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 10 Mar 2026 20:04:30 +0100 Subject: [PATCH 26/41] chore: return vault key in fungible asset error --- crates/miden-protocol/src/asset/fungible.rs | 18 ++++++++---------- crates/miden-protocol/src/errors/mod.rs | 10 +++++----- .../src/kernel_tests/tx/test_asset_vault.rs | 2 +- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/crates/miden-protocol/src/asset/fungible.rs b/crates/miden-protocol/src/asset/fungible.rs index b4818a005d..93eab15ebc 100644 --- a/crates/miden-protocol/src/asset/fungible.rs +++ b/crates/miden-protocol/src/asset/fungible.rs @@ -166,15 +166,14 @@ impl FungibleAsset { /// /// # Errors /// Returns an error if: - /// - The assets were not issued by the same faucet. + /// - The assets do not have the same vault key (i.e. different faucet or callback flags). /// - The total value of assets is greater than or equal to 2^63. #[allow(clippy::should_implement_trait)] pub fn add(self, other: Self) -> Result { - // TODO(callbacks): Return callback flags as well in error. if !self.is_same(&other) { - return Err(AssetError::FungibleAssetInconsistentFaucetIds { - original_issuer: self.faucet_id, - other_issuer: other.faucet_id, + return Err(AssetError::FungibleAssetInconsistentVaultKeys { + original_key: self.vault_key(), + other_key: other.vault_key(), }); } @@ -197,15 +196,14 @@ impl FungibleAsset { /// /// # Errors /// Returns an error if: - /// - The assets were not issued by the same faucet. + /// - The assets do not have the same vault key (i.e. different faucet or callback flags). /// - The final amount would be negative. #[allow(clippy::should_implement_trait)] pub fn sub(self, other: Self) -> Result { - // TODO(callbacks): Return callback flags as well in error. if !self.is_same(&other) { - return Err(AssetError::FungibleAssetInconsistentFaucetIds { - original_issuer: self.faucet_id, - other_issuer: other.faucet_id, + return Err(AssetError::FungibleAssetInconsistentVaultKeys { + original_key: self.vault_key(), + other_key: other.vault_key(), }); } diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index 047b5c0c78..bfd1b7b41f 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -13,7 +13,7 @@ use miden_crypto::utils::HexParseError; use thiserror::Error; use super::account::AccountId; -use super::asset::{FungibleAsset, NonFungibleAsset, TokenSymbol}; +use super::asset::{AssetVaultKey, FungibleAsset, NonFungibleAsset, TokenSymbol}; use super::crypto::merkle::MerkleError; use super::note::NoteId; use super::{MAX_BATCHES_PER_BLOCK, MAX_OUTPUT_NOTES_PER_BATCH, Word}; @@ -456,11 +456,11 @@ pub enum AssetError { #[error("subtracting {subtrahend} from fungible asset amount {minuend} would underflow")] FungibleAssetAmountNotSufficient { minuend: u64, subtrahend: u64 }, #[error( - "cannot add fungible asset with issuer {other_issuer} to fungible asset with issuer {original_issuer}" + "cannot combine fungible assets with different vault keys: {original_key} and {other_key}" )] - FungibleAssetInconsistentFaucetIds { - original_issuer: AccountId, - other_issuer: AccountId, + FungibleAssetInconsistentVaultKeys { + original_key: AssetVaultKey, + other_key: AssetVaultKey, }, #[error("faucet account ID in asset is invalid")] InvalidFaucetAccountId(#[source] Box), diff --git a/crates/miden-testing/src/kernel_tests/tx/test_asset_vault.rs b/crates/miden-testing/src/kernel_tests/tx/test_asset_vault.rs index 411c06145f..f551c63db1 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_asset_vault.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_asset_vault.rs @@ -714,7 +714,7 @@ async fn test_merge_different_fungible_assets_fails() -> anyhow::Result<()> { // Sanity check that the Rust implementation errors when adding assets from different faucets. assert_matches!( asset0.add(asset1).unwrap_err(), - AssetError::FungibleAssetInconsistentFaucetIds { .. } + AssetError::FungibleAssetInconsistentVaultKeys { .. } ); Ok(()) From 4b61a4f15383d70fafde8b5b970c895e748fcaca Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 10 Mar 2026 20:15:11 +0100 Subject: [PATCH 27/41] chore: rename account_has_callbacks -> faucet_has_callbacks --- .../asm/kernels/transaction/api.masm | 36 +++++++++--------- .../asm/protocol/active_account.masm | 31 --------------- .../miden-protocol/asm/protocol/faucet.masm | 29 ++++++++++++++ .../asm/protocol/kernel_proc_offsets.masm | 4 +- .../asm/shared_utils/util/asset.masm | 38 +++++++++++-------- .../asm/standards/faucets/mod.masm | 2 +- .../src/kernel_tests/tx/test_account.rs | 8 ++-- docs/src/protocol_library.md | 2 +- 8 files changed, 77 insertions(+), 73 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/api.masm b/crates/miden-protocol/asm/kernels/transaction/api.masm index 601dc2dbbd..19c75a36c9 100644 --- a/crates/miden-protocol/asm/kernels/transaction/api.masm +++ b/crates/miden-protocol/asm/kernels/transaction/api.masm @@ -736,24 +736,6 @@ pub proc account_has_procedure # => [is_procedure_available, pad(15)] end -#! Returns whether the active account defines callbacks. -#! -#! Inputs: [pad(16)] -#! Outputs: [has_callbacks, pad(15)] -#! -#! Where: -#! - has_callbacks is 1 if the account defines callbacks, 0 otherwise. -#! -#! Invocation: dynexec -pub proc account_has_callbacks - exec.account::has_callbacks - # => [has_callbacks, pad(16)] - - # truncate the stack - swap drop - # => [has_callbacks, pad(15)] -end - # FAUCET # ------------------------------------------------------------------------------------------------- @@ -831,6 +813,24 @@ pub proc faucet_burn_asset # => [ASSET_VALUE, pad(12)] end +#! Returns whether the active account defines callbacks. +#! +#! Inputs: [pad(16)] +#! Outputs: [has_callbacks, pad(15)] +#! +#! Where: +#! - has_callbacks is 1 if the account defines callbacks, 0 otherwise. +#! +#! Invocation: dynexec +pub proc faucet_has_callbacks + exec.account::has_callbacks + # => [has_callbacks, pad(16)] + + # truncate the stack + swap drop + # => [has_callbacks, pad(15)] +end + # INPUT NOTE # ------------------------------------------------------------------------------------------------- diff --git a/crates/miden-protocol/asm/protocol/active_account.masm b/crates/miden-protocol/asm/protocol/active_account.masm index f374d738b6..724700ece3 100644 --- a/crates/miden-protocol/asm/protocol/active_account.masm +++ b/crates/miden-protocol/asm/protocol/active_account.masm @@ -18,7 +18,6 @@ use ::miden::protocol::kernel_proc_offsets::ACCOUNT_GET_INITIAL_ASSET_OFFSET use ::miden::protocol::kernel_proc_offsets::ACCOUNT_GET_NUM_PROCEDURES_OFFSET use ::miden::protocol::kernel_proc_offsets::ACCOUNT_GET_PROCEDURE_ROOT_OFFSET use ::miden::protocol::kernel_proc_offsets::ACCOUNT_HAS_PROCEDURE_OFFSET -use ::miden::protocol::kernel_proc_offsets::ACCOUNT_HAS_CALLBACKS_OFFSET use miden::core::word # ACTIVE ACCOUNT PROCEDURES @@ -681,33 +680,3 @@ pub proc has_procedure # => [is_procedure_available] end -# CALLBACKS -# ------------------------------------------------------------------------------------------------- - -#! Returns whether the active account defines callbacks. -#! -#! The account defines callbacks if any callback storage slot is present and it contains not the -#! empty word. -#! -#! Inputs: [] -#! Outputs: [has_callbacks] -#! -#! Where: -#! - has_callbacks is 1 if the account defines callbacks, 0 otherwise. -#! -#! Invocation: exec -pub proc has_callbacks - # pad the stack - padw padw padw push.0.0.0 - # => [pad(15)] - - push.ACCOUNT_HAS_CALLBACKS_OFFSET - # => [offset, pad(15)] - - syscall.exec_kernel_proc - # => [has_callbacks, pad(15)] - - # clean the stack - swapdw dropw dropw swapw dropw movdn.3 drop drop drop - # => [has_callbacks] -end diff --git a/crates/miden-protocol/asm/protocol/faucet.masm b/crates/miden-protocol/asm/protocol/faucet.masm index 55e05aa2fa..da560106d6 100644 --- a/crates/miden-protocol/asm/protocol/faucet.masm +++ b/crates/miden-protocol/asm/protocol/faucet.masm @@ -2,6 +2,7 @@ use miden::protocol::asset use miden::protocol::active_account use ::miden::protocol::kernel_proc_offsets::FAUCET_MINT_ASSET_OFFSET use ::miden::protocol::kernel_proc_offsets::FAUCET_BURN_ASSET_OFFSET +use ::miden::protocol::kernel_proc_offsets::FAUCET_HAS_CALLBACKS_OFFSET #! Creates a fungible asset for the faucet the transaction is being executed against. #! @@ -129,3 +130,31 @@ pub proc burn swapdw dropw dropw swapw dropw # => [ASSET_VALUE] end + +#! Returns whether the active account defines callbacks. +#! +#! The account defines callbacks if any callback storage slot is present and it contains not the +#! empty word. +#! +#! Inputs: [] +#! Outputs: [has_callbacks] +#! +#! Where: +#! - has_callbacks is 1 if the account defines callbacks, 0 otherwise. +#! +#! Invocation: exec +pub proc has_callbacks + # pad the stack + padw padw padw push.0.0.0 + # => [pad(15)] + + push.FAUCET_HAS_CALLBACKS_OFFSET + # => [offset, pad(15)] + + syscall.exec_kernel_proc + # => [has_callbacks, pad(15)] + + # clean the stack + swapdw dropw dropw swapw dropw movdn.3 drop drop drop + # => [has_callbacks] +end diff --git a/crates/miden-protocol/asm/protocol/kernel_proc_offsets.masm b/crates/miden-protocol/asm/protocol/kernel_proc_offsets.masm index 15a94cabea..eeb370179c 100644 --- a/crates/miden-protocol/asm/protocol/kernel_proc_offsets.masm +++ b/crates/miden-protocol/asm/protocol/kernel_proc_offsets.masm @@ -47,9 +47,7 @@ pub const ACCOUNT_HAS_PROCEDURE_OFFSET=24 ### Faucet ###################################### pub const FAUCET_MINT_ASSET_OFFSET=25 pub const FAUCET_BURN_ASSET_OFFSET=26 - -# Callbacks -pub const ACCOUNT_HAS_CALLBACKS_OFFSET=27 +pub const FAUCET_HAS_CALLBACKS_OFFSET=27 ### Note ######################################## diff --git a/crates/miden-protocol/asm/shared_utils/util/asset.masm b/crates/miden-protocol/asm/shared_utils/util/asset.masm index 9809e240b6..bca6f84ac6 100644 --- a/crates/miden-protocol/asm/shared_utils/util/asset.masm +++ b/crates/miden-protocol/asm/shared_utils/util/asset.masm @@ -17,7 +17,10 @@ pub const ASSET_SIZE = 8 # The offset of the asset value in an asset stored in memory. pub const ASSET_VALUE_MEMORY_OFFSET = 4 +# The flag representing disabled callbacks. pub const CALLBACKS_DISABLED = 0 + +# The flag representing enabled callbacks. pub const CALLBACKS_ENABLED = 1 # PROCEDURES @@ -188,21 +191,6 @@ pub proc key_to_callbacks_enabled # => [callbacks_enabled, ASSET_KEY] end -#! Creates asset metadata from the provided inputs. -#! -#! Inputs: [enable_callbacks] -#! Outputs: [asset_metadata] -#! -#! Where: -#! - enable_callbacks is a flag (0 or 1) indicating whether the asset callbacks flag should be set. -#! - asset_metadata is the asset metadata. -proc create_metadata - u32assert.err=ERR_VAULT_INVALID_ENABLE_CALLBACKS - dup u32lte.CALLBACKS_ENABLED - assert.err=ERR_VAULT_INVALID_ENABLE_CALLBACKS - # => [asset_metadata] -end - #! Creates a fungible asset vault key for the specified faucet. #! #! Inputs: [enable_callbacks, faucet_id_suffix, faucet_id_prefix] @@ -221,6 +209,8 @@ pub proc create_fungible_key exec.create_metadata # => [asset_metadata, faucet_id_suffix, faucet_id_prefix] + # merge the asset metadata into the lower 8 bits of the suffix + # this is safe since create_metadata builds only valid metadata add # => [faucet_id_suffix_and_metadata, faucet_id_prefix] @@ -311,6 +301,24 @@ proc split_suffix_and_metadata # => [asset_metadata, faucet_id_suffix] end +#! Creates asset metadata from the provided inputs. +#! +#! Inputs: [enable_callbacks] +#! Outputs: [asset_metadata] +#! +#! Where: +#! - enable_callbacks is a flag (0 or 1) indicating whether the asset callbacks flag should be set. +#! - asset_metadata is the asset metadata. +#! +#! Panics if: +#! - enable_callbacks is not 0 or 1. +proc create_metadata + u32assert.err=ERR_VAULT_INVALID_ENABLE_CALLBACKS + dup u32lte.CALLBACKS_ENABLED + assert.err=ERR_VAULT_INVALID_ENABLE_CALLBACKS + # => [asset_metadata] +end + #! Extracts the asset callback flag from asset metadata. #! #! WARNING: asset_metadata is assumed to be a byte (in particular a valid u32) diff --git a/crates/miden-standards/asm/standards/faucets/mod.masm b/crates/miden-standards/asm/standards/faucets/mod.masm index 13f7a513b3..c9ae4b2576 100644 --- a/crates/miden-standards/asm/standards/faucets/mod.masm +++ b/crates/miden-standards/asm/standards/faucets/mod.masm @@ -136,7 +136,7 @@ pub proc distribute # --------------------------------------------------------------------------------------------- # mint the asset with the callbacks flag based on whether the faucet has callbacks defined - exec.active_account::has_callbacks + exec.faucet::has_callbacks # => [has_callbacks, amount, note_idx, note_idx] # creating the asset 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 5c1ad87911..58a8d2b725 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_account.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_account.rs @@ -1678,7 +1678,7 @@ async fn test_has_procedure() -> anyhow::Result<()> { Ok(()) } -/// Tests that the `has_callbacks` kernel procedure correctly reports whether an account defines +/// Tests that the `has_callbacks` faucet procedure correctly reports whether a faucet defines /// callbacks. /// /// - `with_callbacks`: callback slot has a non-empty value -> returns 1 @@ -1700,7 +1700,7 @@ async fn test_has_procedure() -> anyhow::Result<()> { )] #[case::without_callbacks(vec![], false)] #[tokio::test] -async fn test_account_has_callbacks( +async fn test_faucet_has_callbacks( #[case] callback_slots: Vec, #[case] expected_has_callbacks: bool, ) -> anyhow::Result<()> { @@ -1716,10 +1716,10 @@ async fn test_account_has_callbacks( let tx_script_code = format!( r#" - use miden::protocol::active_account + use miden::protocol::faucet begin - exec.active_account::has_callbacks + exec.faucet::has_callbacks push.{has_callbacks} assert_eq.err="has_callbacks returned unexpected value" end diff --git a/docs/src/protocol_library.md b/docs/src/protocol_library.md index ff35230383..ec9bafe56f 100644 --- a/docs/src/protocol_library.md +++ b/docs/src/protocol_library.md @@ -54,7 +54,6 @@ Active account procedures can be used to read from storage, fetch or compute com | `get_num_procedures` | Returns the number of procedures in the active account.

**Inputs:** `[]`
**Outputs:** `[num_procedures]` | Any | | `get_procedure_root` | Returns the procedure root for the procedure at the specified index.

**Inputs:** `[index]`
**Outputs:** `[PROC_ROOT]` | Any | | `has_procedure` | Returns the binary flag indicating whether the procedure with the provided root is available on the active account.

**Inputs:** `[PROC_ROOT]`
**Outputs:** `[is_procedure_available]` | Any | -| `has_callbacks` | Returns whether the active account defines callbacks.

**Inputs:** `[]`
**Outputs:** `[has_callbacks]` | Any | ## Native account Procedures (`miden::protocol::native_account`) @@ -155,6 +154,7 @@ Faucet procedures allow reading and writing to faucet accounts to mint and burn | `create_non_fungible_asset` | Creates a non-fungible asset for the faucet the transaction is being executed against.

**Inputs:** `[DATA_HASH]`
**Outputs:** `[ASSET_KEY, ASSET_VALUE]` | Faucet | | `mint` | Mint an asset from the faucet the transaction is being executed against.

**Inputs:** `[ASSET_KEY, ASSET_VALUE]`
**Outputs:** `[NEW_ASSET_VALUE]` | Native & Account & Faucet | | `burn` | Burn an asset from the faucet the transaction is being executed against.

**Inputs:** `[ASSET_KEY, ASSET_VALUE]`
**Outputs:** `[ASSET_VALUE]` | Native & Account & Faucet | +| `has_callbacks` | Returns whether the active account defines callbacks.

**Inputs:** `[]`
**Outputs:** `[has_callbacks]` | Any | ## Asset Procedures (`miden::protocol::asset`) From 2b5ca9861e213478e86cfdc1c438ef63cd933d76 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 10 Mar 2026 20:21:21 +0100 Subject: [PATCH 28/41] chore: AssetCallbacksFlag -> AssetCallbackFlag --- .../miden-protocol/src/account/delta/vault.rs | 4 +-- .../src/asset/asset_callbacks.rs | 2 +- .../src/asset/asset_callbacks_flag.rs | 21 ++++++------ crates/miden-protocol/src/asset/fungible.rs | 18 +++++----- crates/miden-protocol/src/asset/mod.rs | 2 +- crates/miden-protocol/src/asset/vault/mod.rs | 2 +- .../src/asset/vault/vault_key.rs | 33 ++++++++++--------- crates/miden-protocol/src/errors/mod.rs | 2 +- .../src/kernel_tests/tx/test_asset.rs | 8 ++--- .../src/kernel_tests/tx/test_callbacks.rs | 6 ++-- .../src/kernel_tests/tx/test_faucet.rs | 4 +-- 11 files changed, 52 insertions(+), 50 deletions(-) diff --git a/crates/miden-protocol/src/account/delta/vault.rs b/crates/miden-protocol/src/account/delta/vault.rs index 5dfdc2d875..2823a780a8 100644 --- a/crates/miden-protocol/src/account/delta/vault.rs +++ b/crates/miden-protocol/src/account/delta/vault.rs @@ -138,7 +138,7 @@ impl AccountVaultDelta { Asset::Fungible( FungibleAsset::new(vault_key.faucet_id(), diff.unsigned_abs()) .unwrap() - .with_callbacks(vault_key.callbacks()), + .with_callbacks(vault_key.callback_flag()), ) }) .chain( @@ -158,7 +158,7 @@ impl AccountVaultDelta { Asset::Fungible( FungibleAsset::new(vault_key.faucet_id(), diff.unsigned_abs()) .unwrap() - .with_callbacks(vault_key.callbacks()), + .with_callbacks(vault_key.callback_flag()), ) }) .chain( diff --git a/crates/miden-protocol/src/asset/asset_callbacks.rs b/crates/miden-protocol/src/asset/asset_callbacks.rs index 7ee13722ee..992f51dabc 100644 --- a/crates/miden-protocol/src/asset/asset_callbacks.rs +++ b/crates/miden-protocol/src/asset/asset_callbacks.rs @@ -24,7 +24,7 @@ static ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_SLOT_NAME: LazyLock = /// `on_before_asset_added_to_account` callback. This storage slot is only added if the callback /// procedure root is not the empty word. /// -/// [`AssetCallbacksFlag::Enabled`]: crate::asset::AssetCallbacksFlag::Enabled +/// [`AssetCallbackFlag::Enabled`]: crate::asset::AssetCallbackFlag::Enabled #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct AssetCallbacks { on_before_asset_added_to_account: Word, diff --git a/crates/miden-protocol/src/asset/asset_callbacks_flag.rs b/crates/miden-protocol/src/asset/asset_callbacks_flag.rs index 3a80cdd8c8..6b0a06c7bb 100644 --- a/crates/miden-protocol/src/asset/asset_callbacks_flag.rs +++ b/crates/miden-protocol/src/asset/asset_callbacks_flag.rs @@ -12,19 +12,20 @@ use crate::utils::serde::{ const CALLBACKS_DISABLED: u8 = 0; const CALLBACKS_ENABLED: u8 = 1; -/// Whether callbacks are enabled for assets. +/// The flag in an [`AssetVaultKey`](super::AssetVaultKey) that indicates whether +/// [`AssetCallbacks`](super::AssetCallbacks) are enabled for this asset. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] #[repr(u8)] -pub enum AssetCallbacksFlag { +pub enum AssetCallbackFlag { #[default] Disabled = CALLBACKS_DISABLED, Enabled = CALLBACKS_ENABLED, } -impl AssetCallbacksFlag { - /// The serialized size of an [`AssetCallbacksFlag`] in bytes. - pub const SERIALIZED_SIZE: usize = core::mem::size_of::(); +impl AssetCallbackFlag { + /// The serialized size of an [`AssetCallbackFlag`] in bytes. + pub const SERIALIZED_SIZE: usize = core::mem::size_of::(); /// Encodes the callbacks setting as a `u8`. pub const fn as_u8(&self) -> u8 { @@ -32,7 +33,7 @@ impl AssetCallbacksFlag { } } -impl TryFrom for AssetCallbacksFlag { +impl TryFrom for AssetCallbackFlag { type Error = AssetError; /// Decodes a callbacks setting from a `u8`. @@ -44,22 +45,22 @@ impl TryFrom for AssetCallbacksFlag { match value { CALLBACKS_DISABLED => Ok(Self::Disabled), CALLBACKS_ENABLED => Ok(Self::Enabled), - _ => Err(AssetError::InvalidAssetCallbacksFlag(value)), + _ => Err(AssetError::InvalidAssetCallbackFlag(value)), } } } -impl Serializable for AssetCallbacksFlag { +impl Serializable for AssetCallbackFlag { fn write_into(&self, target: &mut W) { target.write_u8(self.as_u8()); } fn get_size_hint(&self) -> usize { - AssetCallbacksFlag::SERIALIZED_SIZE + AssetCallbackFlag::SERIALIZED_SIZE } } -impl Deserializable for AssetCallbacksFlag { +impl Deserializable for AssetCallbackFlag { fn read_from(source: &mut R) -> Result { Self::try_from(source.read_u8()?) .map_err(|err| DeserializationError::InvalidValue(err.to_string())) diff --git a/crates/miden-protocol/src/asset/fungible.rs b/crates/miden-protocol/src/asset/fungible.rs index 93eab15ebc..c7cdd04448 100644 --- a/crates/miden-protocol/src/asset/fungible.rs +++ b/crates/miden-protocol/src/asset/fungible.rs @@ -2,7 +2,7 @@ use alloc::string::ToString; use core::fmt; use super::vault::AssetVaultKey; -use super::{AccountType, Asset, AssetCallbacksFlag, AssetError, Word}; +use super::{AccountType, Asset, AssetCallbackFlag, AssetError, Word}; use crate::Felt; use crate::account::AccountId; use crate::asset::AssetId; @@ -24,7 +24,7 @@ use crate::utils::serde::{ pub struct FungibleAsset { faucet_id: AccountId, amount: u64, - callbacks: AssetCallbacksFlag, + callbacks: AssetCallbackFlag, } impl FungibleAsset { @@ -41,7 +41,7 @@ impl FungibleAsset { /// An account ID (15 bytes) plus an amount (u64) plus a callbacks flag (u8). pub const SERIALIZED_SIZE: usize = AccountId::SERIALIZED_SIZE + core::mem::size_of::() - + AssetCallbacksFlag::SERIALIZED_SIZE; + + AssetCallbackFlag::SERIALIZED_SIZE; // CONSTRUCTOR // -------------------------------------------------------------------------------------------- @@ -65,7 +65,7 @@ impl FungibleAsset { Ok(Self { faucet_id, amount, - callbacks: AssetCallbacksFlag::default(), + callbacks: AssetCallbackFlag::default(), }) } @@ -89,7 +89,7 @@ impl FungibleAsset { } let mut asset = Self::new(key.faucet_id(), value[0].as_canonical_u64())?; - asset.callbacks = key.callbacks(); + asset.callbacks = key.callback_flag(); Ok(asset) } @@ -126,13 +126,13 @@ impl FungibleAsset { self.vault_key() == other.vault_key() } - /// Returns the [`AssetCallbacksFlag`] of this asset. - pub fn callbacks(&self) -> AssetCallbacksFlag { + /// Returns the [`AssetCallbackFlag`] of this asset. + pub fn callbacks(&self) -> AssetCallbackFlag { self.callbacks } - /// Returns a copy of this asset with the given [`AssetCallbacksFlag`]. - pub fn with_callbacks(mut self, callbacks: AssetCallbacksFlag) -> Self { + /// Returns a copy of this asset with the given [`AssetCallbackFlag`]. + pub fn with_callbacks(mut self, callbacks: AssetCallbackFlag) -> Self { self.callbacks = callbacks; self } diff --git a/crates/miden-protocol/src/asset/mod.rs b/crates/miden-protocol/src/asset/mod.rs index df4cd19ae5..70d50f3532 100644 --- a/crates/miden-protocol/src/asset/mod.rs +++ b/crates/miden-protocol/src/asset/mod.rs @@ -25,7 +25,7 @@ mod asset_callbacks; pub use asset_callbacks::AssetCallbacks; mod asset_callbacks_flag; -pub use asset_callbacks_flag::AssetCallbacksFlag; +pub use asset_callbacks_flag::AssetCallbackFlag; mod vault; pub use vault::{AssetId, AssetVault, AssetVaultKey, AssetWitness, PartialVault}; diff --git a/crates/miden-protocol/src/asset/vault/mod.rs b/crates/miden-protocol/src/asset/vault/mod.rs index 234507530b..35c7f1c643 100644 --- a/crates/miden-protocol/src/asset/vault/mod.rs +++ b/crates/miden-protocol/src/asset/vault/mod.rs @@ -184,7 +184,7 @@ impl AssetVault { // should be in bounds let asset = FungibleAsset::new(vault_key.faucet_id(), delta.unsigned_abs()) .expect("fungible asset delta should be valid") - .with_callbacks(vault_key.callbacks()); + .with_callbacks(vault_key.callback_flag()); match delta >= 0 { true => self.add_fungible_asset(asset), false => self.remove_fungible_asset(asset), diff --git a/crates/miden-protocol/src/asset/vault/vault_key.rs b/crates/miden-protocol/src/asset/vault/vault_key.rs index 80d03811bd..c8a84adb4f 100644 --- a/crates/miden-protocol/src/asset/vault/vault_key.rs +++ b/crates/miden-protocol/src/asset/vault/vault_key.rs @@ -8,7 +8,7 @@ use miden_crypto::merkle::smt::LeafIndex; use crate::account::AccountId; use crate::account::AccountType::{self}; use crate::asset::vault::AssetId; -use crate::asset::{Asset, AssetCallbacksFlag, FungibleAsset, NonFungibleAsset}; +use crate::asset::{Asset, AssetCallbackFlag, FungibleAsset, NonFungibleAsset}; use crate::crypto::merkle::smt::SMT_DEPTH; use crate::errors::AssetError; use crate::utils::serde::{ @@ -27,7 +27,7 @@ use crate::{Felt, Word}; /// [ /// asset_id_suffix (64 bits), /// asset_id_prefix (64 bits), -/// [faucet_id_suffix (56 bits) | 7 zero bits | callbacks_flag (1 bit)], +/// [faucet_id_suffix (56 bits) | 7 zero bits | callbacks_enabled (1 bit)], /// faucet_id_prefix (64 bits) /// ] /// ``` @@ -40,7 +40,7 @@ pub struct AssetVaultKey { faucet_id: AccountId, /// Determines whether callbacks are enabled. - callbacks: AssetCallbacksFlag, + callback_flag: AssetCallbackFlag, } impl AssetVaultKey { @@ -63,10 +63,10 @@ impl AssetVaultKey { /// - the asset ID limbs are not zero when `faucet_id` is of type /// [`AccountType::FungibleFaucet`](crate::account::AccountType::FungibleFaucet). pub fn new_native(asset_id: AssetId, faucet_id: AccountId) -> Result { - Self::new(asset_id, faucet_id, AssetCallbacksFlag::Disabled) + Self::new(asset_id, faucet_id, AssetCallbackFlag::Disabled) } - /// Creates an [`AssetVaultKey`] from its parts with the given [`AssetCallbacksFlag`]. + /// Creates an [`AssetVaultKey`] from its parts with the given [`AssetCallbackFlag`]. /// /// # Errors /// @@ -79,7 +79,7 @@ impl AssetVaultKey { pub fn new( asset_id: AssetId, faucet_id: AccountId, - callbacks: AssetCallbacksFlag, + callback_flag: AssetCallbackFlag, ) -> Result { if !faucet_id.is_faucet() { return Err(AssetError::InvalidFaucetAccountId(Box::from(format!( @@ -92,7 +92,7 @@ impl AssetVaultKey { return Err(AssetError::FungibleAssetIdMustBeZero(asset_id)); } - Ok(Self { asset_id, faucet_id, callbacks }) + Ok(Self { asset_id, faucet_id, callback_flag }) } // PUBLIC ACCESSORS @@ -106,7 +106,7 @@ impl AssetVaultKey { // The lower 8 bits of the faucet suffix are guaranteed to be zero and so it is used to // encode the asset metadata. debug_assert!(faucet_suffix & 0xff == 0, "lower 8 bits of faucet suffix must be zero"); - let faucet_id_suffix_and_metadata = faucet_suffix | self.callbacks.as_u8() as u64; + let faucet_id_suffix_and_metadata = faucet_suffix | self.callback_flag.as_u8() as u64; let faucet_id_suffix_and_metadata = Felt::try_from(faucet_id_suffix_and_metadata) .expect("highest bit should still be zero resulting in a valid felt"); @@ -129,9 +129,9 @@ impl AssetVaultKey { self.faucet_id } - /// Returns the [`AssetCallbacksFlag`] flag of the vault key. - pub fn callbacks(&self) -> AssetCallbacksFlag { - self.callbacks + /// Returns the [`AssetCallbackFlag`] flag of the vault key. + pub fn callback_flag(&self) -> AssetCallbackFlag { + self.callback_flag } /// Constructs a fungible asset's key from a faucet ID. @@ -196,7 +196,7 @@ impl TryFrom for AssetVaultKey { let faucet_id_prefix = key[3]; let raw = faucet_id_suffix_and_metadata.as_canonical_u64(); - let category = AssetCallbacksFlag::try_from((raw & 0xff) as u8)?; + let category = AssetCallbackFlag::try_from((raw & 0xff) as u8)?; let faucet_id_suffix = Felt::try_from(raw & 0xffff_ffff_ffff_ff00) .expect("clearing lower bits should not produce an invalid felt"); @@ -258,7 +258,7 @@ impl Deserializable for AssetVaultKey { #[cfg(test)] mod tests { use super::*; - use crate::asset::AssetCallbacksFlag; + use crate::asset::AssetCallbackFlag; use crate::testing::account_id::{ ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET, ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET, @@ -270,9 +270,10 @@ mod tests { let nonfungible_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET).unwrap(); - for callbacks in [AssetCallbacksFlag::Disabled, AssetCallbacksFlag::Enabled] { + for callback_flag in [AssetCallbackFlag::Disabled, AssetCallbackFlag::Enabled] { // Fungible: asset_id must be zero. - let key = AssetVaultKey::new(AssetId::default(), fungible_faucet, callbacks).unwrap(); + let key = + AssetVaultKey::new(AssetId::default(), fungible_faucet, callback_flag).unwrap(); let roundtripped = AssetVaultKey::try_from(key.to_word()).unwrap(); assert_eq!(key, roundtripped); @@ -281,7 +282,7 @@ mod tests { let key = AssetVaultKey::new( AssetId::new(Felt::from(42u32), Felt::from(99u32)), nonfungible_faucet, - callbacks, + callback_flag, ) .unwrap(); diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index bfd1b7b41f..96affb9a24 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -489,7 +489,7 @@ pub enum AssetError { #[error("smt proof in asset witness contains invalid key or value")] AssetWitnessInvalid(#[source] Box), #[error("invalid native asset callbacks encoding: {0}")] - InvalidAssetCallbacksFlag(u8), + InvalidAssetCallbackFlag(u8), } // TOKEN SYMBOL ERROR diff --git a/crates/miden-testing/src/kernel_tests/tx/test_asset.rs b/crates/miden-testing/src/kernel_tests/tx/test_asset.rs index 227f8d5725..6cd653b192 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_asset.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_asset.rs @@ -1,6 +1,6 @@ use miden_protocol::account::AccountId; use miden_protocol::asset::{ - AssetCallbacksFlag, + AssetCallbackFlag, AssetId, AssetVaultKey, FungibleAsset, @@ -229,10 +229,10 @@ async fn test_validate_fungible_asset( } #[rstest::rstest] -#[case::without_callbacks(AssetCallbacksFlag::Disabled)] -#[case::with_callbacks(AssetCallbacksFlag::Enabled)] +#[case::without_callbacks(AssetCallbackFlag::Disabled)] +#[case::with_callbacks(AssetCallbackFlag::Enabled)] #[tokio::test] -async fn test_key_to_asset_metadata(#[case] callbacks: AssetCallbacksFlag) -> anyhow::Result<()> { +async fn test_key_to_asset_metadata(#[case] callbacks: AssetCallbackFlag) -> anyhow::Result<()> { let faucet_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?; let vault_key = AssetVaultKey::new(AssetId::default(), faucet_id, callbacks)?; 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 53c19d53f4..b32d274f70 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs @@ -17,7 +17,7 @@ use miden_protocol::account::{ StorageSlot, StorageSlotName, }; -use miden_protocol::asset::{Asset, AssetCallbacks, AssetCallbacksFlag, FungibleAsset}; +use miden_protocol::asset::{Asset, AssetCallbackFlag, AssetCallbacks, FungibleAsset}; use miden_protocol::block::account_tree::AccountIdKey; use miden_protocol::errors::MasmError; use miden_protocol::note::NoteType; @@ -261,7 +261,7 @@ async fn test_on_before_asset_added_to_account_callback_receives_correct_inputs( // Create a P2ID note with a callbacks-enabled fungible asset. let fungible_asset = - FungibleAsset::new(faucet.id(), amount)?.with_callbacks(AssetCallbacksFlag::Enabled); + FungibleAsset::new(faucet.id(), amount)?.with_callbacks(AssetCallbackFlag::Enabled); let note = builder.add_p2id_note( faucet.id(), target_account.id(), @@ -318,7 +318,7 @@ async fn test_blocked_account_cannot_receive_asset() -> anyhow::Result<()> { // Create a P2ID note with a callbacks-enabled asset let fungible_asset = - FungibleAsset::new(faucet.id(), 100)?.with_callbacks(AssetCallbacksFlag::Enabled); + FungibleAsset::new(faucet.id(), 100)?.with_callbacks(AssetCallbackFlag::Enabled); let note = builder.add_p2id_note( faucet.id(), target_account.id(), diff --git a/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs b/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs index 74829d7a9a..40425fdf7f 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_faucet.rs @@ -3,7 +3,7 @@ use alloc::sync::Arc; use miden_protocol::account::{Account, AccountBuilder, AccountComponent, AccountId, AccountType}; use miden_protocol::assembly::DefaultSourceManager; use miden_protocol::asset::{ - AssetCallbacksFlag, + AssetCallbackFlag, AssetId, AssetVaultKey, FungibleAsset, @@ -307,7 +307,7 @@ async fn test_mint_fungible_asset_with_callbacks_enabled() -> anyhow::Result<()> let asset = FungibleAsset::new(faucet_id, FUNGIBLE_ASSET_AMOUNT)?; // Build a vault key with callbacks enabled. - let vault_key = AssetVaultKey::new(AssetId::default(), faucet_id, AssetCallbacksFlag::Enabled)?; + let vault_key = AssetVaultKey::new(AssetId::default(), faucet_id, AssetCallbackFlag::Enabled)?; let code = format!( r#" From bcc14a1b0cb929d1fb114c1551016fe2af32728e Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Tue, 10 Mar 2026 20:34:34 +0100 Subject: [PATCH 29/41] chore: simplify block list; minor fixes --- .../asm/shared_utils/util/asset.masm | 2 +- crates/miden-protocol/src/asset/mod.rs | 2 +- .../src/asset/vault/vault_key.rs | 21 ++++++++------- .../src/kernel_tests/tx/test_callbacks.rs | 26 +++++++------------ 4 files changed, 22 insertions(+), 29 deletions(-) diff --git a/crates/miden-protocol/asm/shared_utils/util/asset.masm b/crates/miden-protocol/asm/shared_utils/util/asset.masm index bca6f84ac6..fcf1af1421 100644 --- a/crates/miden-protocol/asm/shared_utils/util/asset.masm +++ b/crates/miden-protocol/asm/shared_utils/util/asset.masm @@ -297,7 +297,7 @@ proc split_suffix_and_metadata # => [faucet_id_suffix, suffix_metadata_lo] # extract lower 8 bits of the lo part to get the metadata - swap u32and.0x000000ff + swap u32and.0xff # => [asset_metadata, faucet_id_suffix] end diff --git a/crates/miden-protocol/src/asset/mod.rs b/crates/miden-protocol/src/asset/mod.rs index 70d50f3532..4d52e6ab1b 100644 --- a/crates/miden-protocol/src/asset/mod.rs +++ b/crates/miden-protocol/src/asset/mod.rs @@ -124,7 +124,7 @@ impl Asset { /// Returns true if this asset is the same as the specified asset. /// - /// Two assets are defined to be the same if their vault keys match + /// Two assets are defined to be the same if their vault keys match. pub fn is_same(&self, other: &Self) -> bool { self.vault_key() == other.vault_key() } diff --git a/crates/miden-protocol/src/asset/vault/vault_key.rs b/crates/miden-protocol/src/asset/vault/vault_key.rs index c8a84adb4f..3e0c2003b5 100644 --- a/crates/miden-protocol/src/asset/vault/vault_key.rs +++ b/crates/miden-protocol/src/asset/vault/vault_key.rs @@ -265,29 +265,30 @@ mod tests { }; #[test] - fn asset_vault_key_word_roundtrip() { - let fungible_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap(); - let nonfungible_faucet = - AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET).unwrap(); + fn asset_vault_key_word_roundtrip() -> anyhow::Result<()> { + let fungible_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?; + let nonfungible_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET)?; for callback_flag in [AssetCallbackFlag::Disabled, AssetCallbackFlag::Enabled] { // Fungible: asset_id must be zero. - let key = - AssetVaultKey::new(AssetId::default(), fungible_faucet, callback_flag).unwrap(); + let key = AssetVaultKey::new(AssetId::default(), fungible_faucet, callback_flag)?; - let roundtripped = AssetVaultKey::try_from(key.to_word()).unwrap(); + let roundtripped = AssetVaultKey::try_from(key.to_word())?; assert_eq!(key, roundtripped); + assert_eq!(key, AssetVaultKey::read_from_bytes(&key.to_bytes())?); // Non-fungible: asset_id can be non-zero. let key = AssetVaultKey::new( AssetId::new(Felt::from(42u32), Felt::from(99u32)), nonfungible_faucet, callback_flag, - ) - .unwrap(); + )?; - let roundtripped = AssetVaultKey::try_from(key.to_word()).unwrap(); + let roundtripped = AssetVaultKey::try_from(key.to_word())?; assert_eq!(key, roundtripped); + assert_eq!(key, AssetVaultKey::read_from_bytes(&key.to_bytes())?); } + + Ok(()) } } 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 b32d274f70..81dafa9fbc 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs @@ -60,24 +60,15 @@ pub proc on_before_asset_added_to_account # Look up in block list storage map push.BLOCK_LIST_MAP_SLOT[0..2] exec.active_account::get_map_item - # => [MAP_VALUE, ASSET_KEY, ASSET_VALUE, pad(6)] + # => [IS_BLOCKED, ASSET_KEY, ASSET_VALUE, pad(6)] - # If value is non-zero, account is blocked. - # testz returns 1 if word is all zeros (not blocked), 0 otherwise (blocked). - # assert fails if top is 0, so blocked accounts cause a panic. - exec.word::testz + # If IS_BLOCKED is non-zero, account is blocked. + exec.word::eqz assert.err=ERR_ACCOUNT_BLOCKED - # => [ASSET_KEY, ASSET_VALUE, pad(6)] + # => [ASSET_KEY, ASSET_VALUE, pad(8)] - # Drop ASSET_KEY, keep ASSET_VALUE on top + # drop unused asset key - auto pads to 12 dropw - # => [ASSET_VALUE, pad(6)] - - # Pad to 16 elements: need ASSET_VALUE(4) + pad(12), have pad(6), add 6 more - repeat.6 - push.0 - movdn.4 - end # => [ASSET_VALUE, pad(12)] end "#; @@ -334,11 +325,12 @@ async fn test_blocked_account_cannot_receive_asset() -> anyhow::Result<()> { // Try to consume the note on the blocked wallet - should fail because the callback // checks the block list and panics. - let consume_tx_context = mock_chain + let result = mock_chain .build_tx_context(target_account.id(), &[note.id()], &[])? .foreign_accounts(vec![faucet_inputs]) - .build()?; - let result = consume_tx_context.execute().await; + .build()? + .execute() + .await; assert_transaction_executor_error!(result, ERR_ACCOUNT_BLOCKED); From 4c37b06f1d1bd899aa49af4a39ac1ac1f1a9a7fa Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Wed, 11 Mar 2026 14:15:36 +0100 Subject: [PATCH 30/41] chore: do not pass native account ID to callback --- .../kernels/transaction/lib/callbacks.masm | 33 +++++++------------ .../src/kernel_tests/tx/test_callbacks.rs | 33 +++++++++++-------- 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm b/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm index 06560e27f1..22862eac7c 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm @@ -61,30 +61,30 @@ end @locals(4) proc on_before_asset_added_to_account_raw exec.start_foreign_callback_context - # => [native_account_id_suffix, native_account_id_prefix, ASSET_KEY, ASSET_VALUE] + # => [ASSET_KEY, ASSET_VALUE] # get the procedure root of the callback from the faucet's storage push.ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_ROOT_SLOT[0..2] exec.account::get_item - # => [PROC_ROOT, native_account_id_suffix, native_account_id_prefix, ASSET_KEY, ASSET_VALUE] + # => [PROC_ROOT, ASSET_KEY, ASSET_VALUE] exec.word::testz - # => [is_empty_word, PROC_ROOT, native_account_id_suffix, native_account_id_prefix, ASSET_KEY, ASSET_VALUE] + # => [is_empty_word, PROC_ROOT, ASSET_KEY, ASSET_VALUE] # only invoke the callback if it is not the empty word if.true - # drop proc root, account ID and asset key - dropw drop drop dropw + # drop proc root and asset key + dropw dropw # => [ASSET_VALUE] else # prepare for dyncall by storing procedure root in local memory loc_storew_le.CALLBACK_PROC_ROOT_LOC dropw - # => [native_account_id_suffix, native_account_id_prefix, ASSET_KEY, ASSET_VALUE] + # => [ASSET_KEY, ASSET_VALUE] # pad the stack to 16 for the call (excluding the proc root ptr) - repeat.6 push.0 movdn.10 end + repeat.8 push.0 movdn.8 end locaddr.CALLBACK_PROC_ROOT_LOC - # => [callback_proc_root_ptr, native_account_id_suffix, native_account_id_prefix, ASSET_KEY, ASSET_VALUE, pad(6)] + # => [callback_proc_root_ptr, ASSET_KEY, ASSET_VALUE, pad(8)] # invoke the callback dyncall @@ -100,25 +100,16 @@ proc on_before_asset_added_to_account_raw # => [PROCESSED_ASSET_VALUE] end -#! Prepares the invocation of a faucet callback by: -#!- starting a foreign context against the faucet identified by the asset key's faucet ID. -#!- returning the native account's ID that is to be passed as an input to callbacks. +#! Prepares the invocation of a faucet callback by starting a foreign context against the faucet +#! identified by the asset key's faucet ID. #! #! Inputs: [ASSET_KEY, ASSET_VALUE] -#! Outputs: [native_account_id_suffix, native_account_id_prefix, ASSET_KEY, ASSET_VALUE] +#! Outputs: [ASSET_KEY, ASSET_VALUE] proc start_foreign_callback_context exec.asset::key_to_faucet_id # => [faucet_id_suffix, faucet_id_prefix, ASSET_KEY, ASSET_VALUE] - # get the ID of the active account that will be passed as an input to the callback - # this must be done before the foreign context is started - exec.account::get_id - # => [native_account_id_suffix, native_account_id_prefix, faucet_id_suffix, faucet_id_prefix, ASSET_KEY, ASSET_VALUE] - - movup.3 movup.3 - # => [faucet_id_suffix, faucet_id_prefix, native_account_id_suffix, native_account_id_prefix, ASSET_KEY, ASSET_VALUE] - # start a foreign context against the faucet exec.tx::start_foreign_context - # => [native_account_id_suffix, native_account_id_prefix, ASSET_KEY, ASSET_VALUE] + # => [ASSET_KEY, ASSET_VALUE] end 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 81dafa9fbc..6dae9c68c6 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs @@ -38,6 +38,7 @@ use crate::{AccountState, Auth, assert_transaction_executor_error}; /// block list stored in a storage map. If the account is blocked, the callback panics. const BLOCK_LIST_MASM: &str = r#" use miden::protocol::active_account +use miden::protocol::native_account use miden::core::word const BLOCK_LIST_MAP_SLOT = word("miden::testing::callbacks::block_list") @@ -47,27 +48,31 @@ const ERR_ACCOUNT_BLOCKED = "the account is blocked and cannot receive this asse #! #! Checks whether the receiving account is in the block list. If so, panics. #! -#! Inputs: [native_acct_suffix, native_acct_prefix, ASSET_KEY, ASSET_VALUE, pad(6)] +#! Inputs: [ASSET_KEY, ASSET_VALUE, pad(8)] #! Outputs: [ASSET_VALUE, pad(12)] #! #! Invocation: call pub proc on_before_asset_added_to_account + # Get the native account ID (the account receiving the asset) + exec.native_account::get_id + # => [native_acct_suffix, native_acct_prefix, ASSET_KEY, ASSET_VALUE, pad(8)] + # Build account ID map key: [0, 0, suffix, prefix] push.0.0 - # => [0, 0, native_acct_suffix, native_acct_prefix, ASSET_KEY, ASSET_VALUE, pad(6)] - # => [ACCOUNT_ID_KEY, ASSET_KEY, ASSET_VALUE, pad(6)] + # => [0, 0, native_acct_suffix, native_acct_prefix, ASSET_KEY, ASSET_VALUE, pad(8)] + # => [ACCOUNT_ID_KEY, ASSET_KEY, ASSET_VALUE, pad(8)] # Look up in block list storage map push.BLOCK_LIST_MAP_SLOT[0..2] exec.active_account::get_map_item - # => [IS_BLOCKED, ASSET_KEY, ASSET_VALUE, pad(6)] + # => [IS_BLOCKED, ASSET_KEY, ASSET_VALUE, pad(8)] # If IS_BLOCKED is non-zero, account is blocked. exec.word::eqz assert.err=ERR_ACCOUNT_BLOCKED - # => [ASSET_KEY, ASSET_VALUE, pad(8)] + # => [ASSET_KEY, ASSET_VALUE, pad(10)] - # drop unused asset key - auto pads to 12 + # drop unused asset key dropw # => [ASSET_VALUE, pad(12)] end @@ -183,29 +188,31 @@ async fn test_on_before_asset_added_to_account_callback_receives_correct_inputs( r#" const ERR_WRONG_VALUE = "callback received unexpected asset value element" - #! Inputs: [native_account_suffix, native_account_prefix, ASSET_KEY, ASSET_VALUE, pad(6)] + #! Inputs: [ASSET_KEY, ASSET_VALUE, pad(8)] #! Outputs: [ASSET_VALUE, pad(12)] pub proc {proc_name} - # Assert native account ID + # Assert native account ID can be retrieved via native_account::get_id + exec.::miden::protocol::native_account::get_id + # => [native_account_suffix, native_account_prefix, ASSET_KEY, ASSET_VALUE, pad(8)] push.{wallet_id_suffix} assert_eq.err="callback received unexpected native account ID suffix" push.{wallet_id_prefix} assert_eq.err="callback received unexpected native account ID prefix" - # => [ASSET_KEY, ASSET_VALUE, pad(6)] + # => [ASSET_KEY, ASSET_VALUE, pad(8)] # duplicate the asset value for returning dupw.1 swapw - # => [ASSET_KEY, ASSET_VALUE, ASSET_VALUE, pad(6)] + # => [ASSET_KEY, ASSET_VALUE, ASSET_VALUE, pad(8)] # build the expected asset push.{amount} exec.::miden::protocol::active_account::get_id push.1 - # => [enable_callbacks, active_account_id_suffix, active_account_id_prefix, amount, ASSET_KEY, ASSET_VALUE, pad(6)] + # => [enable_callbacks, active_account_id_suffix, active_account_id_prefix, amount, ASSET_KEY, ASSET_VALUE, ASSET_VALUE, pad(8)] exec.::miden::protocol::asset::create_fungible_asset - # => [EXPECTED_ASSET_KEY, EXPECTED_ASSET_VALUE, ASSET_KEY, ASSET_VALUE, ASSET_VALUE, pad(6)] + # => [EXPECTED_ASSET_KEY, EXPECTED_ASSET_VALUE, ASSET_KEY, ASSET_VALUE, ASSET_VALUE, pad(8)] movupw.2 assert_eqw.err="callback received unexpected asset key" - # => [EXPECTED_ASSET_VALUE, ASSET_VALUE, ASSET_VALUE, pad(6)] + # => [EXPECTED_ASSET_VALUE, ASSET_VALUE, ASSET_VALUE, pad(8)] assert_eqw.err="callback received unexpected asset value" # => [ASSET_VALUE, pad(12)] From 0a1a7573428e4df99d5af3d09e64225b6132eb94 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Wed, 11 Mar 2026 16:24:40 +0100 Subject: [PATCH 31/41] feat: handle missing callback slot in kernel event handler --- .../asm/kernels/transaction/lib/account.masm | 37 +++++++++++++ .../kernels/transaction/lib/callbacks.masm | 30 +++++++---- .../src/kernel_tests/tx/test_callbacks.rs | 52 +++++++++++++++++++ 3 files changed, 108 insertions(+), 11 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/account.masm b/crates/miden-protocol/asm/kernels/transaction/lib/account.masm index 99edd790bb..d819b41adf 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/account.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/account.masm @@ -435,6 +435,43 @@ pub proc get_item # => [VALUE] end +#! Finds an item in the active account's storage by slot ID, returning whether the slot was found +#! along with its value. +#! +#! Unlike `get_item`, this procedure does not panic if the slot does not exist. Instead, it +#! returns `is_found = 0` and the empty word. +#! +#! Inputs: [slot_id_suffix, slot_id_prefix] +#! Outputs: [is_found, VALUE] +#! +#! Where: +#! - slot_id_{suffix, prefix} are the suffix and prefix felts of the slot identifier, which are +#! the first two felts of the hashed slot name. +#! - is_found is 1 if the slot was found, 0 otherwise. +#! - VALUE is the value of the item, or the empty word if the slot was not found. +pub proc find_item + # get account storage slots section offset + exec.memory::get_account_active_storage_slots_section_ptr + # => [acct_storage_slots_section_offset, slot_id_suffix, slot_id_prefix] + + exec.find_storage_slot + # => [is_found, slot_ptr] + + if.true + # slot was found, read its value + exec.get_item_raw + # => [VALUE] + + push.1 + # => [is_found = 1, VALUE] + else + # slot was not found, drop slot_ptr and return empty word + drop padw push.0 + # => [is_found = 0, EMPTY_WORD] + end + # => [is_found, VALUE] +end + #! Gets an item and its slot type from the account storage. #! #! Inputs: [slot_id_suffix, slot_id_prefix] diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm b/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm index 22862eac7c..e74ea30284 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm @@ -19,8 +19,9 @@ pub const ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_ROOT_SLOT = word("miden::protoco #! Invokes the `on_before_asset_added_to_account` callback on the faucet that issued the asset, #! if the asset has callbacks enabled. #! -#! The callback invocation is skipped in two cases: +#! The callback invocation is skipped in these cases: #! - If the global callback flag in the asset key is `Disabled`. +#! - If the faucet does not have the callback storage slot. #! - If the callback storage slot contains the empty word. #! #! Inputs: [ASSET_KEY, ASSET_VALUE] @@ -50,6 +51,9 @@ end #! the faucet, reading the callback procedure root from the faucet's storage, and invoking it via #! `dyncall`. #! +#! If the faucet does not have the callback storage slot, or if the slot contains the empty word, +#! the callback is skipped and the original ASSET_VALUE is returned. +#! #! Inputs: [ASSET_KEY, ASSET_VALUE] #! Outputs: [PROCESSED_ASSET_VALUE] #! @@ -63,20 +67,20 @@ proc on_before_asset_added_to_account_raw exec.start_foreign_callback_context # => [ASSET_KEY, ASSET_VALUE] - # get the procedure root of the callback from the faucet's storage + # try to find the callback procedure root in the faucet's storage push.ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_PROC_ROOT_SLOT[0..2] - exec.account::get_item - # => [PROC_ROOT, ASSET_KEY, ASSET_VALUE] + exec.account::find_item + # => [is_found, PROC_ROOT, ASSET_KEY, ASSET_VALUE] + + movdn.4 exec.word::testz not + # => [is_non_empty_word, PROC_ROOT, is_found, ASSET_KEY, ASSET_VALUE] - exec.word::testz - # => [is_empty_word, PROC_ROOT, ASSET_KEY, ASSET_VALUE] + # invoke the callback if is_found && is_non_empty_word + movup.5 and + # => [should_invoke, PROC_ROOT, ASSET_KEY, ASSET_VALUE] - # only invoke the callback if it is not the empty word + # only invoke the callback if the procedure root is not the empty word if.true - # drop proc root and asset key - dropw dropw - # => [ASSET_VALUE] - else # prepare for dyncall by storing procedure root in local memory loc_storew_le.CALLBACK_PROC_ROOT_LOC dropw # => [ASSET_KEY, ASSET_VALUE] @@ -93,6 +97,10 @@ proc on_before_asset_added_to_account_raw # truncate the stack after the call swapdw dropw dropw swapw dropw # => [PROCESSED_ASSET_VALUE] + else + # drop proc root and asset key + dropw dropw + # => [ASSET_VALUE] end # => [PROCESSED_ASSET_VALUE] 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 6dae9c68c6..f9f606004b 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_callbacks.rs @@ -343,3 +343,55 @@ async fn test_blocked_account_cannot_receive_asset() -> anyhow::Result<()> { Ok(()) } + +/// Tests that consuming a callbacks-enabled asset succeeds even when the issuing faucet does not +/// have the callback storage slot. +#[tokio::test] +async fn test_faucet_without_callback_slot_skips_callback() -> anyhow::Result<()> { + let mut builder = crate::MockChain::builder(); + + let target_account = builder.add_existing_wallet(Auth::IncrNonce)?; + + let basic_faucet = BasicFungibleFaucet::new("NCB".try_into()?, 8, Felt::new(1_000_000))?; + + // Create a faucet WITHOUT any AssetCallbacks component. + let account_builder = AccountBuilder::new([45u8; 32]) + .storage_mode(AccountStorageMode::Public) + .account_type(AccountType::FungibleFaucet) + .with_component(basic_faucet); + + let faucet = builder.add_account_from_builder( + Auth::BasicAuth { + auth_scheme: AuthScheme::Falcon512Poseidon2, + }, + account_builder, + AccountState::Exists, + )?; + + // Create a P2ID note with a callbacks-enabled asset from this faucet. + // The faucet does not have the callback slot, but the asset has callbacks enabled. + let fungible_asset = + FungibleAsset::new(faucet.id(), 100)?.with_callbacks(AssetCallbackFlag::Enabled); + let note = builder.add_p2id_note( + faucet.id(), + target_account.id(), + &[Asset::Fungible(fungible_asset)], + NoteType::Public, + )?; + + let mut mock_chain = builder.build()?; + mock_chain.prove_next_block()?; + + let faucet_inputs = mock_chain.get_foreign_account_inputs(faucet.id())?; + + // Consuming the note should succeed: the callback is gracefully skipped because the + // faucet does not define the callback storage slot. + mock_chain + .build_tx_context(target_account.id(), &[note.id()], &[])? + .foreign_accounts(vec![faucet_inputs]) + .build()? + .execute() + .await?; + + Ok(()) +} From a72d12bf3e06c6cdc03a4bc5233e13fb43d07a29 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Wed, 11 Mar 2026 16:27:30 +0100 Subject: [PATCH 32/41] chore: simplify has_non_empty_slot --- .../asm/kernels/transaction/lib/account.masm | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/account.masm b/crates/miden-protocol/asm/kernels/transaction/lib/account.masm index d819b41adf..4d529753b2 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/account.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/account.masm @@ -2078,24 +2078,14 @@ end #! the first two felts of the hashed slot name. #! - has_non_empty_value is 1 if the slot exists and its value is non-empty, 0 otherwise. proc has_non_empty_slot - exec.memory::get_account_active_storage_slots_section_ptr - # => [storage_slots_ptr, slot_id_suffix, slot_id_prefix] - - exec.find_storage_slot - # => [is_found, slot_ptr] + exec.find_item + # => [is_found, VALUE] - if.true - # read the slot value - exec.get_item_raw - # => [VALUE] + # check if is_found && value is non-empty + movdn.4 exec.word::eqz not + # => [is_non_empty_value, is_found] - # check if the value is non-empty - exec.word::eqz not - # => [has_non_empty_value] - else - drop push.0 - # => [0] - end + and # => [has_non_empty_value] end From 3418a74d02a71ef66d95ff89c29e171d345f3361 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Thu, 12 Mar 2026 10:39:13 +0100 Subject: [PATCH 33/41] chore: use `has_callbacks` in `faucet::create_fungible_asset` --- crates/miden-protocol/asm/protocol/faucet.masm | 11 +++++------ crates/miden-standards/asm/standards/faucets/mod.masm | 4 ---- .../miden-testing/src/kernel_tests/tx/test_asset.rs | 4 ++-- docs/src/protocol_library.md | 2 +- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/crates/miden-protocol/asm/protocol/faucet.masm b/crates/miden-protocol/asm/protocol/faucet.masm index da560106d6..ea1135d036 100644 --- a/crates/miden-protocol/asm/protocol/faucet.masm +++ b/crates/miden-protocol/asm/protocol/faucet.masm @@ -6,27 +6,26 @@ use ::miden::protocol::kernel_proc_offsets::FAUCET_HAS_CALLBACKS_OFFSET #! Creates a fungible asset for the faucet the transaction is being executed against. #! -#! Inputs: [enable_callbacks, amount] +#! Inputs: [amount] #! Outputs: [ASSET_KEY, ASSET_VALUE] #! #! Where: -#! - enable_callbacks is a flag (0 or 1) indicating whether asset callbacks are enabled. #! - amount is the amount of the asset to create. #! - ASSET_KEY is the vault key of the created fungible asset. #! - ASSET_VALUE is the value of the created fungible asset. #! #! Panics if: #! - the active account is not a fungible faucet. -#! - enable_callbacks is not 0 or 1. #! #! Invocation: exec pub proc create_fungible_asset # fetch the id of the faucet the transaction is being executed against. exec.active_account::get_id - # => [id_suffix, id_prefix, enable_callbacks, amount] + # => [id_suffix, id_prefix, amount] - movup.2 - # => [enable_callbacks, id_suffix, id_prefix, amount] + # check whether the faucet has callbacks defined + exec.has_callbacks + # => [has_callbacks, id_suffix, id_prefix, amount] # create the fungible asset exec.asset::create_fungible_asset diff --git a/crates/miden-standards/asm/standards/faucets/mod.masm b/crates/miden-standards/asm/standards/faucets/mod.masm index c9ae4b2576..bee1948a2c 100644 --- a/crates/miden-standards/asm/standards/faucets/mod.masm +++ b/crates/miden-standards/asm/standards/faucets/mod.masm @@ -135,10 +135,6 @@ pub proc distribute # Mint the asset. # --------------------------------------------------------------------------------------------- - # mint the asset with the callbacks flag based on whether the faucet has callbacks defined - exec.faucet::has_callbacks - # => [has_callbacks, amount, note_idx, note_idx] - # creating the asset exec.faucet::create_fungible_asset # => [ASSET_KEY, ASSET_VALUE, note_idx, note_idx] diff --git a/crates/miden-testing/src/kernel_tests/tx/test_asset.rs b/crates/miden-testing/src/kernel_tests/tx/test_asset.rs index 6cd653b192..15e49bf6cd 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_asset.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_asset.rs @@ -44,8 +44,8 @@ async fn test_create_fungible_asset_succeeds() -> anyhow::Result<()> { begin exec.prologue::prepare_transaction - # create fungible asset (enable_callbacks = 0) - push.{FUNGIBLE_ASSET_AMOUNT} push.0 + # create fungible asset + push.{FUNGIBLE_ASSET_AMOUNT} exec.faucet::create_fungible_asset # => [ASSET_KEY, ASSET_VALUE] diff --git a/docs/src/protocol_library.md b/docs/src/protocol_library.md index ec9bafe56f..3076434d5c 100644 --- a/docs/src/protocol_library.md +++ b/docs/src/protocol_library.md @@ -150,7 +150,7 @@ Faucet procedures allow reading and writing to faucet accounts to mint and burn | Procedure | Description | Context | | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | -| `create_fungible_asset` | Creates a fungible asset for the faucet the transaction is being executed against.

**Inputs:** `[enable_callbacks, amount]`
**Outputs:** `[ASSET_KEY, ASSET_VALUE]` | Faucet | +| `create_fungible_asset` | Creates a fungible asset for the faucet the transaction is being executed against.

**Inputs:** `[amount]`
**Outputs:** `[ASSET_KEY, ASSET_VALUE]` | Faucet | | `create_non_fungible_asset` | Creates a non-fungible asset for the faucet the transaction is being executed against.

**Inputs:** `[DATA_HASH]`
**Outputs:** `[ASSET_KEY, ASSET_VALUE]` | Faucet | | `mint` | Mint an asset from the faucet the transaction is being executed against.

**Inputs:** `[ASSET_KEY, ASSET_VALUE]`
**Outputs:** `[NEW_ASSET_VALUE]` | Native & Account & Faucet | | `burn` | Burn an asset from the faucet the transaction is being executed against.

**Inputs:** `[ASSET_KEY, ASSET_VALUE]`
**Outputs:** `[ASSET_VALUE]` | Native & Account & Faucet | From 40f401a902b1c782b8117c0e24799119662169c6 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Thu, 12 Mar 2026 10:40:47 +0100 Subject: [PATCH 34/41] chore: use `padw padw swapdw` for padding --- .../miden-protocol/asm/kernels/transaction/lib/callbacks.masm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm b/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm index e74ea30284..8739235d3b 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/callbacks.masm @@ -86,7 +86,7 @@ proc on_before_asset_added_to_account_raw # => [ASSET_KEY, ASSET_VALUE] # pad the stack to 16 for the call (excluding the proc root ptr) - repeat.8 push.0 movdn.8 end + padw padw swapdw locaddr.CALLBACK_PROC_ROOT_LOC # => [callback_proc_root_ptr, ASSET_KEY, ASSET_VALUE, pad(8)] From b3ff0a0a33ae9c6bd25028d1f29f63626cde3893 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Thu, 12 Mar 2026 10:41:00 +0100 Subject: [PATCH 35/41] chore: `get_storage_slot` -> `get_storage_slot_ptr` --- .../asm/kernels/transaction/lib/account.masm | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/account.masm b/crates/miden-protocol/asm/kernels/transaction/lib/account.masm index 4d529753b2..a14c436e33 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/account.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/account.masm @@ -427,7 +427,7 @@ pub proc get_item exec.memory::get_account_active_storage_slots_section_ptr # => [acct_storage_slots_section_offset, slot_id_suffix, slot_id_prefix] - exec.get_storage_slot + exec.get_storage_slot_ptr # => [slot_ptr] # get the item from storage @@ -490,7 +490,7 @@ pub proc get_typed_item exec.memory::get_account_active_storage_slots_section_ptr # => [acct_storage_slots_section_offset, slot_id_suffix, slot_id_prefix] - exec.get_storage_slot + exec.get_storage_slot_ptr # => [slot_ptr] dup add.ACCOUNT_SLOT_TYPE_OFFSET mem_load @@ -518,7 +518,7 @@ pub proc get_initial_item exec.memory::get_account_initial_storage_slots_ptr # => [account_initial_storage_slots_ptr, slot_id_suffix, slot_id_prefix] - exec.get_storage_slot + exec.get_storage_slot_ptr # => [slot_ptr] # get the item from initial storage @@ -547,7 +547,7 @@ pub proc set_item exec.memory::get_native_account_active_storage_slots_ptr # => [storage_slots_ptr, slot_id_suffix, slot_id_prefix, VALUE] - exec.get_storage_slot + exec.get_storage_slot_ptr # => [slot_ptr, VALUE] # load the slot type @@ -642,7 +642,7 @@ pub proc set_map_item # => [storage_slots_ptr, slot_id_suffix, slot_id_prefix, KEY, NEW_VALUE] # resolve the slot name to its pointer - exec.get_storage_slot + exec.get_storage_slot_ptr # => [slot_ptr, KEY, NEW_VALUE] # load the slot type @@ -1627,7 +1627,7 @@ end #! - a slot with the provided slot ID does not exist in account storage. #! - the requested storage slot type is not map. proc get_map_item_raw - exec.get_storage_slot + exec.get_storage_slot_ptr # => [slot_ptr, KEY] emit.ACCOUNT_STORAGE_BEFORE_GET_MAP_ITEM_EVENT @@ -1701,7 +1701,7 @@ end #! #! Panics if: #! - a slot with the provided slot ID does not exist in account storage. -proc get_storage_slot +proc get_storage_slot_ptr exec.find_storage_slot # => [is_found, slot_ptr] From adfba4f916fb641658c1f4df4b15cce43c9b2e6e Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Thu, 12 Mar 2026 10:43:51 +0100 Subject: [PATCH 36/41] chore: make flag constants associated --- .../src/asset/asset_callbacks_flag.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/miden-protocol/src/asset/asset_callbacks_flag.rs b/crates/miden-protocol/src/asset/asset_callbacks_flag.rs index 6b0a06c7bb..c5dfa620e4 100644 --- a/crates/miden-protocol/src/asset/asset_callbacks_flag.rs +++ b/crates/miden-protocol/src/asset/asset_callbacks_flag.rs @@ -9,21 +9,21 @@ use crate::utils::serde::{ Serializable, }; -const CALLBACKS_DISABLED: u8 = 0; -const CALLBACKS_ENABLED: u8 = 1; - /// The flag in an [`AssetVaultKey`](super::AssetVaultKey) that indicates whether /// [`AssetCallbacks`](super::AssetCallbacks) are enabled for this asset. #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] #[repr(u8)] pub enum AssetCallbackFlag { #[default] - Disabled = CALLBACKS_DISABLED, + Disabled = Self::DISABLED, - Enabled = CALLBACKS_ENABLED, + Enabled = Self::ENABLED, } impl AssetCallbackFlag { + const DISABLED: u8 = 0; + const ENABLED: u8 = 1; + /// The serialized size of an [`AssetCallbackFlag`] in bytes. pub const SERIALIZED_SIZE: usize = core::mem::size_of::(); @@ -43,8 +43,8 @@ impl TryFrom for AssetCallbackFlag { /// Returns an error if the value is not a valid callbacks encoding. fn try_from(value: u8) -> Result { match value { - CALLBACKS_DISABLED => Ok(Self::Disabled), - CALLBACKS_ENABLED => Ok(Self::Enabled), + Self::DISABLED => Ok(Self::Disabled), + Self::ENABLED => Ok(Self::Enabled), _ => Err(AssetError::InvalidAssetCallbackFlag(value)), } } From 78db024513fed9953238d9fd18696f19dad234b9 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Thu, 12 Mar 2026 10:46:26 +0100 Subject: [PATCH 37/41] chore: move with_callbacks to constructor section --- crates/miden-protocol/src/asset/fungible.rs | 12 ++++++------ crates/miden-protocol/src/asset/vault/vault_key.rs | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/miden-protocol/src/asset/fungible.rs b/crates/miden-protocol/src/asset/fungible.rs index c7cdd04448..9fcf28a7c4 100644 --- a/crates/miden-protocol/src/asset/fungible.rs +++ b/crates/miden-protocol/src/asset/fungible.rs @@ -108,6 +108,12 @@ impl FungibleAsset { Self::from_key_value(vault_key, value) } + /// Returns a copy of this asset with the given [`AssetCallbackFlag`]. + pub fn with_callbacks(mut self, callbacks: AssetCallbackFlag) -> Self { + self.callbacks = callbacks; + self + } + // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- @@ -131,12 +137,6 @@ impl FungibleAsset { self.callbacks } - /// Returns a copy of this asset with the given [`AssetCallbackFlag`]. - pub fn with_callbacks(mut self, callbacks: AssetCallbackFlag) -> Self { - self.callbacks = callbacks; - self - } - /// Returns the key which is used to store this asset in the account vault. pub fn vault_key(&self) -> AssetVaultKey { AssetVaultKey::new(AssetId::default(), self.faucet_id, self.callbacks) diff --git a/crates/miden-protocol/src/asset/vault/vault_key.rs b/crates/miden-protocol/src/asset/vault/vault_key.rs index 3e0c2003b5..290d7d6ba3 100644 --- a/crates/miden-protocol/src/asset/vault/vault_key.rs +++ b/crates/miden-protocol/src/asset/vault/vault_key.rs @@ -196,7 +196,7 @@ impl TryFrom for AssetVaultKey { let faucet_id_prefix = key[3]; let raw = faucet_id_suffix_and_metadata.as_canonical_u64(); - let category = AssetCallbackFlag::try_from((raw & 0xff) as u8)?; + let callback_flag = AssetCallbackFlag::try_from((raw & 0xff) as u8)?; let faucet_id_suffix = Felt::try_from(raw & 0xffff_ffff_ffff_ff00) .expect("clearing lower bits should not produce an invalid felt"); @@ -204,7 +204,7 @@ impl TryFrom for AssetVaultKey { let faucet_id = AccountId::try_from_elements(faucet_id_suffix, faucet_id_prefix) .map_err(|err| AssetError::InvalidFaucetAccountId(Box::new(err)))?; - Self::new(asset_id, faucet_id, category) + Self::new(asset_id, faucet_id, callback_flag) } } From 73c2dee3eb97fa645bd3a6bfe07cf9c4ba8c3b2e Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Thu, 12 Mar 2026 10:48:04 +0100 Subject: [PATCH 38/41] chore: mention callbacks in fungible asset docs --- crates/miden-protocol/src/asset/fungible.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/miden-protocol/src/asset/fungible.rs b/crates/miden-protocol/src/asset/fungible.rs index 9fcf28a7c4..58b5754663 100644 --- a/crates/miden-protocol/src/asset/fungible.rs +++ b/crates/miden-protocol/src/asset/fungible.rs @@ -20,6 +20,9 @@ use crate::utils::serde::{ /// /// A fungible asset consists of a faucet ID of the faucet which issued the asset as well as the /// asset amount. Asset amount is guaranteed to be 2^63 - 1 or smaller. +/// +/// The fungible asset can have callbacks to the faucet enabled or disabled, depending on +/// [`AssetCallbackFlag`]. See [`AssetCallbacks`](crate::asset::AssetCallbacks) for more details. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct FungibleAsset { faucet_id: AccountId, From ea5c07476171d5f42940dc503ccb4fe3b7eb9763 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Thu, 12 Mar 2026 13:43:08 +0100 Subject: [PATCH 39/41] chore: add missing docs --- crates/miden-protocol/src/asset/asset_callbacks.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/miden-protocol/src/asset/asset_callbacks.rs b/crates/miden-protocol/src/asset/asset_callbacks.rs index 992f51dabc..06600c427d 100644 --- a/crates/miden-protocol/src/asset/asset_callbacks.rs +++ b/crates/miden-protocol/src/asset/asset_callbacks.rs @@ -39,6 +39,7 @@ impl AssetCallbacks { Self::default() } + /// Sets the `on_before_asset_added_to_account` callback procedure root. pub fn on_before_asset_added_to_account(mut self, proc_root: Word) -> Self { self.on_before_asset_added_to_account = proc_root; self @@ -53,7 +54,7 @@ impl AssetCallbacks { } /// Returns the procedure root of the `on_before_asset_added_to_account` callback. - pub fn on_before_asset_added_proc_root(&self) -> Word { + pub fn on_before_asset_added_to_account_proc_root(&self) -> Word { self.on_before_asset_added_to_account } From aa9173b1cffc8cfe60bd4780133640193a3ccfab Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Thu, 12 Mar 2026 13:44:54 +0100 Subject: [PATCH 40/41] chore: remove unused doc link --- crates/miden-protocol/src/asset/asset_callbacks.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/miden-protocol/src/asset/asset_callbacks.rs b/crates/miden-protocol/src/asset/asset_callbacks.rs index 06600c427d..21c3e05ea9 100644 --- a/crates/miden-protocol/src/asset/asset_callbacks.rs +++ b/crates/miden-protocol/src/asset/asset_callbacks.rs @@ -23,8 +23,6 @@ static ON_BEFORE_ASSET_ADDED_TO_ACCOUNT_SLOT_NAME: LazyLock = /// - [`Self::on_before_asset_added_to_account_slot()`]: Stores the procedure root of the /// `on_before_asset_added_to_account` callback. This storage slot is only added if the callback /// procedure root is not the empty word. -/// -/// [`AssetCallbackFlag::Enabled`]: crate::asset::AssetCallbackFlag::Enabled #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct AssetCallbacks { on_before_asset_added_to_account: Word, From be3c98b20914336dcf193dae7bd58c4c6811c550 Mon Sep 17 00:00:00 2001 From: Philipp Gackstatter Date: Mon, 16 Mar 2026 09:21:57 +0100 Subject: [PATCH 41/41] chore: add potential future fungible asset delta todo --- crates/miden-protocol/src/account/delta/vault.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/miden-protocol/src/account/delta/vault.rs b/crates/miden-protocol/src/account/delta/vault.rs index 2823a780a8..d6e466e64d 100644 --- a/crates/miden-protocol/src/account/delta/vault.rs +++ b/crates/miden-protocol/src/account/delta/vault.rs @@ -360,6 +360,7 @@ impl Serializable for FungibleAssetDelta { // TODO: We save `i64` as `u64` since winter utils only supports unsigned integers for now. // We should update this code (and deserialization as well) once it supports signed // integers. + // TODO: If we keep this code, optimize by not serializing asset ID (which is always 0). target.write_many(self.0.iter().map(|(vault_key, &delta)| (*vault_key, delta as u64))); }