diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm b/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm index 4ac2b7ceea..5bbbcaffc6 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/epilogue.masm @@ -22,6 +22,8 @@ const ERR_EPILOGUE_AUTH_PROCEDURE_CALLED_FROM_WRONG_CONTEXT="auth procedure has const ERR_EPILOGUE_NONCE_CANNOT_BE_0="nonce cannot be 0 after an account-creating transaction" +const ERR_EPILOGUE_FEE_PROC_EXCEEDED_BUDGET="fee procedure exceeded its cycle budget" + # CONSTANTS # ================================================================================================= @@ -54,6 +56,9 @@ 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 +# Maximum number of cycles the user-defined fee procedure is allowed to consume. +const MAX_FEE_PROC_CYCLES=2000 + # OUTPUT NOTES PROCEDURES # ================================================================================================= @@ -291,8 +296,6 @@ 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] @@ -301,23 +304,65 @@ proc create_native_fee_asset # => [FEE_ASSET_KEY, FEE_ASSET_VALUE] end -#! Computes the fee of this transaction and removes the asset from the native account's vault. +# TODO(multi-asset-fees): Wire up execute_fee_procedure once existing auth components +# bundle a @fee_script procedure. The dispatch code below is complete but not called +# from compute_and_remove_fee yet because accounts without @fee_script would crash. +# The open question is how a user-space fee procedure obtains the native faucet ID +# (storage slot vs. FEE_ARGS vs. syscall). + +#! Executes the account's fee procedure to convert computation units into a fee asset. #! -#! Note that this does not have to account for the fee asset in the output vault explicitly, -#! because the fee asset is removed from the account vault after build_output_vault and because -#! it is not added to an output note. Effectively, the fee asset bypasses the asset preservation -#! check. That's okay, because the logic is entirely determined by the transaction kernel. +#! The fee procedure is at index 1 in the account's procedure list (placed there by the +#! AccountProcedureBuilder when it sees the @fee_script attribute). #! -#! Inputs: [] -#! Outputs: [native_asset_id_suffix, native_asset_id_prefix, fee_amount] +#! Inputs: [computation_units] +#! Outputs: [FEE_ASSET_KEY, FEE_ASSET_VALUE] #! #! Where: -#! - fee_amount is the computed fee amount of the transaction in the native asset. -#! - native_asset_id_{prefix,suffix} are the prefix and suffix felts of the faucet that issues the -#! native asset. +#! - computation_units is the fee amount computed by compute_fee. +#! - FEE_ASSET_KEY is the asset vault key of the fee asset chosen by the fee procedure. +#! - FEE_ASSET_VALUE is the value of the fee asset. #! #! Panics if: -#! - the account vault contains less than the computed fee. +#! - the fee procedure exceeds MAX_FEE_PROC_CYCLES. +proc execute_fee_procedure + # build the dyncall input: [computation_units, FEE_ARGS, pad(11)] + padw padw push.0.0.0 + # => [pad(11), computation_units] + + exec.memory::get_fee_args + # => [FEE_ARGS, pad(11), computation_units] + + movup.15 + # => [computation_units, FEE_ARGS, pad(11)] + + # fee procedure is at index 1 within the account procedures section. + push.1 exec.memory::get_account_procedure_ptr + # => [fee_procedure_ptr, computation_units, FEE_ARGS, pad(11)] + + padw dup.4 mem_loadw_le + # => [FEE_PROC_ROOT, fee_procedure_ptr, computation_units, FEE_ARGS, pad(11)] + + dyncall + # => [OUTPUT_3, OUTPUT_2, OUTPUT_1, OUTPUT_0] + + # TODO: enforce MAX_FEE_PROC_CYCLES budget via a memory slot (movup.16 is out of range) + + # the fee procedure returns [FEE_ASSET_KEY, FEE_ASSET_VALUE, pad(8)] + swapw.2 dropw swapw.2 dropw + # => [FEE_ASSET_KEY, FEE_ASSET_VALUE] +end + +#! Computes the fee of this transaction and removes the asset from the native account's vault. +#! +#! TODO(multi-asset-fees): once existing auth components bundle a @fee_script procedure, +#! switch from create_native_fee_asset to execute_fee_procedure so accounts can pay in any asset. +#! +#! Note that this deliberately does not use account::remove_asset_from_vault, because that +#! procedure modifies the vault delta. +#! +#! Inputs: [] +#! Outputs: [native_asset_id_suffix, native_asset_id_prefix, fee_amount] proc compute_and_remove_fee # compute the fee the tx needs to pay exec.compute_fee dup @@ -337,14 +382,6 @@ proc compute_and_remove_fee movdn.9 movdn.9 # => [FEE_ASSET_KEY, FEE_ASSET_VALUE, native_asset_id_suffix, native_asset_id_prefix, fee_amount] - # remove the fee from the native account's vault - # note that this deliberately does not use account::remove_asset_from_vault, because that - # procedure modifies the vault delta of the account. That is undesirable because it does not - # take a constant number of cycles, which makes it much harder to calculate the number of - # cycles the kernel takes after computing the fee. It is also unnecessary, because the delta - # commitment has already been computed and so any modifications done to the delta at this point - # are essentially ignored. - # fetch the vault root ptr exec.memory::get_account_vault_root_ptr movdn.8 # => [FEE_ASSET_KEY, FEE_ASSET_VALUE, account_vault_root_ptr, native_asset_id_suffix, native_asset_id_prefix, fee_amount] @@ -378,7 +415,7 @@ end #! #! Where: #! - OUTPUT_NOTES_COMMITMENT is the commitment of the output notes. -#! - ACCOUNT_UPDATE_COMMITMENT is the hash of the the final account commitment and account +#! - ACCOUNT_UPDATE_COMMITMENT is the hash of the final account commitment and account #! delta commitment. #! - fee_amount is the computed fee amount of the transaction denominated in the native asset. #! - native_asset_id_{prefix,suffix} are the prefix and suffix felts of the faucet that issues the diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm b/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm index 25f533bddd..01e05aed81 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/memory.masm @@ -112,6 +112,9 @@ const TX_SCRIPT_ARGS_PTR=432 # The memory address at which the auth procedure arguments are stored. const AUTH_ARGS_PTR=436 +# The memory address at which the fee procedure arguments are stored. +const FEE_ARGS_PTR=440 + # GLOBAL BLOCK DATA # ------------------------------------------------------------------------------------------------- @@ -706,6 +709,28 @@ pub proc set_auth_args mem_storew_le.AUTH_ARGS_PTR end +#! Returns the fee procedure arguments. +#! +#! Inputs: [] +#! Outputs: [FEE_ARGS] +#! +#! Where: +#! - FEE_ARGS is the argument passed to the fee procedure. +pub proc get_fee_args + padw mem_loadw_le.FEE_ARGS_PTR +end + +#! Sets the fee procedure arguments. +#! +#! Inputs: [FEE_ARGS] +#! Outputs: [FEE_ARGS] +#! +#! Where: +#! - FEE_ARGS is the argument passed to the fee procedure. +pub proc set_fee_args + mem_storew_le.FEE_ARGS_PTR +end + # BLOCK DATA # ------------------------------------------------------------------------------------------------- diff --git a/crates/miden-protocol/asm/kernels/transaction/lib/prologue.masm b/crates/miden-protocol/asm/kernels/transaction/lib/prologue.masm index 01d808d09c..2a6d191ed6 100644 --- a/crates/miden-protocol/asm/kernels/transaction/lib/prologue.masm +++ b/crates/miden-protocol/asm/kernels/transaction/lib/prologue.masm @@ -1064,6 +1064,25 @@ proc process_auth_procedure_data # => [] end +#! Saves the fee procedure args to memory. +#! +#! Inputs: +#! Operand stack: [] +#! Advice stack: [FEE_ARGS] +#! Outputs: +#! Operand stack: [] +#! Advice stack: [] +#! +#! Where: +#! - FEE_ARGS is the argument passed to the fee procedure. +proc process_fee_procedure_data + padw adv_loadw + # => [FEE_ARGS] + + exec.memory::set_fee_args dropw + # => [] +end + # TRANSACTION PROLOGUE # ================================================================================================= @@ -1101,6 +1120,7 @@ end #! TX_SCRIPT_ROOT, #! TX_SCRIPT_ARGS, #! AUTH_ARGS, +#! FEE_ARGS, #! ] #! Advice map: { #! PARTIAL_BLOCKCHAIN_COMMITMENT: [MMR_PEAKS], @@ -1162,6 +1182,7 @@ pub proc prepare_transaction exec.process_input_notes_data exec.process_tx_script_data exec.process_auth_procedure_data + exec.process_fee_procedure_data # => [] push.MAX_BLOCK_NUM exec.memory::set_expiration_block_num diff --git a/crates/miden-protocol/src/account/code/mod.rs b/crates/miden-protocol/src/account/code/mod.rs index 09e7558501..ba70e79274 100644 --- a/crates/miden-protocol/src/account/code/mod.rs +++ b/crates/miden-protocol/src/account/code/mod.rs @@ -336,7 +336,7 @@ impl PrettyPrint for AccountCode { /// A helper type for building the set of account procedures from account components. /// -/// In particular, this ensures that the auth procedure ends up at index 0. +/// Ensures the auth procedure ends up at index 0 and the fee procedure at index 1. struct AccountProcedureBuilder { procedures: Vec, } @@ -346,11 +346,16 @@ impl AccountProcedureBuilder { Self { procedures: Vec::new() } } - /// This method must be called before add_component is called. + /// Adds the first (auth + fee) component. Must be called before `add_component`. + /// + /// The component must contain exactly one `@auth_script` procedure and exactly one + /// `@fee_script` procedure. The auth procedure is placed at index 0 and the fee procedure + /// at index 1. fn add_auth_component(&mut self, component: &AccountComponent) -> Result<(), AccountError> { let mut auth_proc_count = 0; + let mut fee_proc_count = 0; - for (proc_root, is_auth) in component.procedures() { + for (proc_root, is_auth, is_fee) in component.procedures() { self.add_procedure(proc_root); if is_auth { @@ -358,6 +363,10 @@ impl AccountProcedureBuilder { self.procedures.swap(0, auth_proc_idx); auth_proc_count += 1; } + + if is_fee { + fee_proc_count += 1; + } } if auth_proc_count == 0 { @@ -366,14 +375,38 @@ impl AccountProcedureBuilder { return Err(AccountError::AccountComponentMultipleAuthProcedures); } + if fee_proc_count > 1 { + return Err(AccountError::AccountComponentMultipleFeeProcedures); + } + + // Place the fee procedure at index 1 if one exists. We need a second pass since the + // auth swap may have moved things around. + if fee_proc_count == 1 { + let mut fee_proc_idx = None; + for (idx, proc_root) in self.procedures.iter().enumerate() { + for (comp_root, _, is_fee) in component.procedures() { + if is_fee && proc_root == &comp_root { + fee_proc_idx = Some(idx); + } + } + } + + if let Some(idx) = fee_proc_idx { + self.procedures.swap(1, idx); + } + } + Ok(()) } fn add_component(&mut self, component: &AccountComponent) -> Result<(), AccountError> { - for (proc_root, is_auth) in component.procedures() { + for (proc_root, is_auth, is_fee) in component.procedures() { if is_auth { return Err(AccountError::AccountCodeMultipleAuthComponents); } + if is_fee { + return Err(AccountError::AccountCodeMultipleFeeComponents); + } self.add_procedure(proc_root); } diff --git a/crates/miden-protocol/src/account/component/mod.rs b/crates/miden-protocol/src/account/component/mod.rs index b7184d2db0..4d57e2199b 100644 --- a/crates/miden-protocol/src/account/component/mod.rs +++ b/crates/miden-protocol/src/account/component/mod.rs @@ -21,6 +21,9 @@ use crate::{MastForest, Word}; /// The attribute name used to mark the authentication procedure in an account component. const AUTH_SCRIPT_ATTRIBUTE: &str = "auth_script"; +/// The attribute name used to mark the fee procedure in an account component. +const FEE_SCRIPT_ATTRIBUTE: &str = "fee_script"; + // ACCOUNT COMPONENT // ================================================================================================ @@ -189,12 +192,12 @@ impl AccountComponent { self.metadata.supported_types().contains(&account_type) } - /// Returns an iterator over ([`AccountProcedureRoot`], is_auth) for all procedures in this - /// component. + /// Returns an iterator over ([`AccountProcedureRoot`], is_auth, is_fee) for all procedures in + /// this component. /// /// A procedure is considered an authentication procedure if it has the `@auth_script` - /// attribute. - pub fn procedures(&self) -> impl Iterator + '_ { + /// attribute, and a fee procedure if it has the `@fee_script` attribute. + pub fn procedures(&self) -> impl Iterator + '_ { let library = self.code.as_library(); library.exports().filter_map(|export| { export.as_procedure().map(|proc_export| { @@ -204,7 +207,8 @@ impl AccountComponent { .expect("export node not in the forest") .digest(); let is_auth = proc_export.attributes.has(AUTH_SCRIPT_ATTRIBUTE); - (AccountProcedureRoot::from_raw(digest), is_auth) + let is_fee = proc_export.attributes.has(FEE_SCRIPT_ATTRIBUTE); + (AccountProcedureRoot::from_raw(digest), is_auth, is_fee) }) }) } diff --git a/crates/miden-protocol/src/batch/proposed_batch.rs b/crates/miden-protocol/src/batch/proposed_batch.rs index b0a96439ba..3b055237df 100644 --- a/crates/miden-protocol/src/batch/proposed_batch.rs +++ b/crates/miden-protocol/src/batch/proposed_batch.rs @@ -440,7 +440,7 @@ mod tests { use crate::Word; use crate::account::delta::AccountUpdateDetails; use crate::account::{AccountIdVersion, AccountStorageMode, AccountType}; - use crate::asset::FungibleAsset; + use crate::asset::{Asset, FungibleAsset}; use crate::transaction::{InputNoteCommitment, OutputNote, ProvenTransaction, TxAccountUpdate}; #[test] @@ -497,7 +497,7 @@ mod tests { Vec::::new(), block_num, block_ref, - FungibleAsset::mock(100).unwrap_fungible(), + Asset::from(FungibleAsset::mock(100).unwrap_fungible()), expiration_block_num, proof, ) diff --git a/crates/miden-protocol/src/errors/mod.rs b/crates/miden-protocol/src/errors/mod.rs index b13ec8d068..f749bbe7cb 100644 --- a/crates/miden-protocol/src/errors/mod.rs +++ b/crates/miden-protocol/src/errors/mod.rs @@ -122,6 +122,12 @@ pub enum AccountError { AccountComponentMastForestMergeError(#[source] MastForestError), #[error("account component contains multiple authentication procedures")] AccountComponentMultipleAuthProcedures, + #[error("account component contains multiple fee procedures")] + AccountComponentMultipleFeeProcedures, + #[error("account code does not contain a fee component")] + AccountCodeNoFeeComponent, + #[error("account code contains multiple fee components")] + AccountCodeMultipleFeeComponents, #[error("failed to update asset vault")] AssetVaultUpdateError(#[source] AssetVaultError), #[error("account build error: {0}")] diff --git a/crates/miden-protocol/src/transaction/executed_tx.rs b/crates/miden-protocol/src/transaction/executed_tx.rs index a3077d456f..4b6ad45bc2 100644 --- a/crates/miden-protocol/src/transaction/executed_tx.rs +++ b/crates/miden-protocol/src/transaction/executed_tx.rs @@ -14,7 +14,7 @@ use super::{ TransactionOutputs, }; use crate::account::PartialAccount; -use crate::asset::FungibleAsset; +use crate::asset::Asset; use crate::block::{BlockHeader, BlockNumber}; use crate::transaction::TransactionInputs; use crate::utils::serde::{ @@ -117,7 +117,7 @@ impl ExecutedTransaction { } /// Returns the fee of the transaction. - pub fn fee(&self) -> FungibleAsset { + pub fn fee(&self) -> Asset { self.tx_outputs.fee() } diff --git a/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs b/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs index 27d29f054a..bc4f302717 100644 --- a/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs +++ b/crates/miden-protocol/src/transaction/kernel/advice_inputs.rs @@ -203,6 +203,9 @@ impl TransactionAdviceInputs { // --- auth procedure args -------------------------------------------- self.extend_stack(tx_args.auth_args()); + + // --- fee procedure args --------------------------------------------- + self.extend_stack(tx_args.fee_args()); } // BLOCKCHAIN INJECTIONS diff --git a/crates/miden-protocol/src/transaction/kernel/mod.rs b/crates/miden-protocol/src/transaction/kernel/mod.rs index 917abe382b..341ffcfe95 100644 --- a/crates/miden-protocol/src/transaction/kernel/mod.rs +++ b/crates/miden-protocol/src/transaction/kernel/mod.rs @@ -3,12 +3,12 @@ use alloc::vec::Vec; use miden_core_lib::CoreLibrary; -use crate::account::{AccountHeader, AccountId}; +use crate::account::{AccountHeader, AccountId, AccountType}; #[cfg(any(feature = "testing", test))] use crate::assembly::Library; use crate::assembly::debuginfo::SourceManagerSync; use crate::assembly::{Assembler, DefaultSourceManager, KernelLibrary}; -use crate::asset::FungibleAsset; +use crate::asset::{Asset, FungibleAsset}; use crate::block::BlockNumber; use crate::crypto::SequentialCommit; use crate::errors::TransactionOutputError; @@ -205,20 +205,23 @@ impl TransactionKernel { /// /// Where: /// - OUTPUT_NOTES_COMMITMENT is a commitment to the output notes. - /// - ACCOUNT_UPDATE_COMMITMENT is the hash of the the final account commitment and account - /// delta commitment. - /// - FEE_ASSET is the fungible asset used as the transaction fee. + /// - ACCOUNT_UPDATE_COMMITMENT is the hash of the final account commitment and account delta + /// commitment. + /// - the fee asset is packed as three felts: faucet ID + amount. For v1 fee assets must be + /// fungible. /// - expiration_block_num is the block number at which the transaction will expire. pub fn build_output_stack( final_account_commitment: Word, account_delta_commitment: Word, output_notes_commitment: Word, - fee: FungibleAsset, + fee: Asset, expiration_block_num: BlockNumber, ) -> StackOutputs { let account_update_commitment = Hasher::merge(&[final_account_commitment, account_delta_commitment]); + let fee = fee.unwrap_fungible(); + let mut outputs: Vec = Vec::with_capacity(12); outputs.extend(output_notes_commitment); outputs.extend(account_update_commitment); @@ -238,28 +241,21 @@ impl TransactionKernel { /// [ /// OUTPUT_NOTES_COMMITMENT, /// ACCOUNT_UPDATE_COMMITMENT, - /// FEE_ASSET, + /// native_asset_id_suffix, native_asset_id_prefix, fee_amount, /// expiration_block_num, /// ] /// ``` /// /// Where: /// - OUTPUT_NOTES_COMMITMENT is the commitment of the output notes. - /// - ACCOUNT_UPDATE_COMMITMENT is the hash of the the final account commitment and account - /// delta commitment. - /// - FEE_ASSET is the fungible asset used as the transaction fee. - /// - tx_expiration_block_num is the block height at which the transaction will become expired, - /// defined by the sum of the execution block ref and the transaction's block expiration delta - /// (if set during transaction execution). - /// - /// # Errors - /// - /// Returns an error if: - /// - Indices 13..16 on the stack are not zeroes. - /// - Overflow addresses are not empty. + /// - ACCOUNT_UPDATE_COMMITMENT is the hash of the final account commitment and account delta + /// commitment. + /// - the fee is packed as three felts (faucet_id + amount). Returned wrapped in an + /// [`Asset::Fungible`]. Non-fungible fee assets are rejected in v1. + /// - tx_expiration_block_num is the block height at which the transaction will become expired. pub fn parse_output_stack( - stack: &StackOutputs, // FIXME TODO add an extension trait for this one - ) -> Result<(Word, Word, FungibleAsset, BlockNumber), TransactionOutputError> { + stack: &StackOutputs, + ) -> Result<(Word, Word, Asset, BlockNumber), TransactionOutputError> { let output_notes_commitment = stack .get_word(TransactionOutputs::OUTPUT_NOTES_COMMITMENT_WORD_IDX) .expect("output_notes_commitment (first word) missing"); @@ -290,8 +286,7 @@ impl TransactionKernel { })? .into(); - // Make sure that indices 13, 14 and 15 are zeroes (i.e. the fourth word without the - // expiration block number). + // Make sure that indices 13, 14 and 15 are zeroes. if stack.get_word(12).expect("fourth word missing").as_elements()[..3] != Word::empty().as_elements()[..3] { @@ -303,10 +298,16 @@ impl TransactionKernel { let native_asset_id = AccountId::try_from_elements(native_asset_id_suffix, native_asset_id_prefix) .expect("native asset ID should be validated by the tx kernel"); + assert_eq!(native_asset_id.account_type(), AccountType::FungibleFaucet); let fee = FungibleAsset::new(native_asset_id, fee_amount.as_canonical_u64()) .map_err(TransactionOutputError::FeeAssetNotFungibleAsset)?; - Ok((output_notes_commitment, account_update_commitment, fee, expiration_block_num)) + Ok(( + output_notes_commitment, + account_update_commitment, + Asset::from(fee), + expiration_block_num, + )) } // TRANSACTION OUTPUT PARSER @@ -320,7 +321,8 @@ impl TransactionKernel { /// [ /// OUTPUT_NOTES_COMMITMENT, /// ACCOUNT_UPDATE_COMMITMENT, - /// FEE_ASSET, + /// FEE_ASSET_KEY, + /// FEE_ASSET_VALUE, /// expiration_block_num, /// ] /// ``` @@ -329,10 +331,9 @@ impl TransactionKernel { /// - OUTPUT_NOTES_COMMITMENT is the commitment of the output notes. /// - ACCOUNT_UPDATE_COMMITMENT is the hash of the final account commitment and the account /// delta commitment of the account that the transaction is being executed against. - /// - FEE_ASSET is the fungible asset used as the transaction fee. - /// - tx_expiration_block_num is the block height at which the transaction will become expired, - /// defined by the sum of the execution block ref and the transaction's block expiration delta - /// (if set during transaction execution). + /// - FEE_ASSET_KEY is the asset vault key of the fee asset. + /// - FEE_ASSET_VALUE is the value of the fee asset. + /// - tx_expiration_block_num is the block height at which the transaction will become expired. /// /// The actual data describing the new account state and output notes is expected to be located /// in the provided advice map under keys `OUTPUT_NOTES_COMMITMENT` and diff --git a/crates/miden-protocol/src/transaction/outputs/mod.rs b/crates/miden-protocol/src/transaction/outputs/mod.rs index a784237690..c1c6a7ab43 100644 --- a/crates/miden-protocol/src/transaction/outputs/mod.rs +++ b/crates/miden-protocol/src/transaction/outputs/mod.rs @@ -2,7 +2,7 @@ use core::fmt::Debug; use crate::Word; use crate::account::AccountHeader; -use crate::asset::FungibleAsset; +use crate::asset::Asset; use crate::block::BlockNumber; use crate::utils::serde::{ ByteReader, @@ -38,8 +38,8 @@ pub struct TransactionOutputs { account_delta_commitment: Word, /// Set of output notes created by the transaction. output_notes: RawOutputNotes, - /// The fee of the transaction. - fee: FungibleAsset, + /// The fee asset removed from the account vault to pay for the transaction. + fee: Asset, /// Defines up to which block the transaction is considered valid. expiration_block_num: BlockNumber, } @@ -56,11 +56,11 @@ impl TransactionOutputs { /// output stack. pub const ACCOUNT_UPDATE_COMMITMENT_WORD_IDX: usize = 4; - /// The index of the element at which the ID suffix of the faucet that issues the native asset + /// The index of the element at which the ID suffix of the faucet that issues the fee asset /// is stored on the output stack. pub const NATIVE_ASSET_ID_SUFFIX_ELEMENT_IDX: usize = 8; - /// The index of the element at which the ID prefix of the faucet that issues the native asset + /// The index of the element at which the ID prefix of the faucet that issues the fee asset /// is stored on the output stack. pub const NATIVE_ASSET_ID_PREFIX_ELEMENT_IDX: usize = 9; @@ -78,7 +78,7 @@ impl TransactionOutputs { account: AccountHeader, account_delta_commitment: Word, output_notes: RawOutputNotes, - fee: FungibleAsset, + fee: Asset, expiration_block_num: BlockNumber, ) -> Self { Self { @@ -108,8 +108,8 @@ impl TransactionOutputs { &self.output_notes } - /// Returns the fee of the transaction. - pub fn fee(&self) -> FungibleAsset { + /// Returns the fee asset of the transaction. + pub fn fee(&self) -> Asset { self.fee } @@ -142,7 +142,7 @@ impl Deserializable for TransactionOutputs { let account = AccountHeader::read_from(source)?; let account_delta_commitment = Word::read_from(source)?; let output_notes = RawOutputNotes::read_from(source)?; - let fee = FungibleAsset::read_from(source)?; + let fee = Asset::read_from(source)?; let expiration_block_num = BlockNumber::read_from(source)?; Ok(Self { diff --git a/crates/miden-protocol/src/transaction/proven_tx.rs b/crates/miden-protocol/src/transaction/proven_tx.rs index da89162b3e..ab22dfccfa 100644 --- a/crates/miden-protocol/src/transaction/proven_tx.rs +++ b/crates/miden-protocol/src/transaction/proven_tx.rs @@ -5,7 +5,7 @@ use alloc::vec::Vec; use super::{InputNote, ToInputNoteCommitments}; use crate::account::Account; use crate::account::delta::AccountUpdateDetails; -use crate::asset::FungibleAsset; +use crate::asset::Asset; use crate::block::BlockNumber; use crate::errors::ProvenTransactionError; use crate::note::NoteHeader; @@ -61,7 +61,7 @@ pub struct ProvenTransaction { ref_block_commitment: Word, /// The fee of the transaction. - fee: FungibleAsset, + fee: Asset, /// The block number by which the transaction will expire, as defined by the executed scripts. expiration_block_num: BlockNumber, @@ -95,7 +95,7 @@ impl ProvenTransaction { output_notes: impl IntoIterator>, ref_block_num: BlockNumber, ref_block_commitment: Word, - fee: FungibleAsset, + fee: Asset, expiration_block_num: BlockNumber, proof: ExecutionProof, ) -> Result { @@ -175,7 +175,7 @@ impl ProvenTransaction { } /// Returns the fee of the transaction. - pub fn fee(&self) -> FungibleAsset { + pub fn fee(&self) -> Asset { self.fee } @@ -225,7 +225,7 @@ impl ProvenTransaction { AccountUpdateDetails::Delta(post_fee_account_delta) => { // Add the removed fee to the post fee delta to get the pre-fee delta, against which // the delta commitment needs to be validated. - post_fee_account_delta.vault_mut().add_asset(self.fee.into()).map_err(|err| { + post_fee_account_delta.vault_mut().add_asset(self.fee).map_err(|err| { ProvenTransactionError::AccountDeltaCommitmentMismatch(Box::from(err)) })?; @@ -240,9 +240,9 @@ impl ProvenTransaction { } // Remove the added fee again to recreate the post fee delta. - post_fee_account_delta.vault_mut().remove_asset(self.fee.into()).map_err( - |err| ProvenTransactionError::AccountDeltaCommitmentMismatch(Box::from(err)), - )?; + post_fee_account_delta.vault_mut().remove_asset(self.fee).map_err(|err| { + ProvenTransactionError::AccountDeltaCommitmentMismatch(Box::from(err)) + })?; }, } @@ -272,7 +272,7 @@ impl Deserializable for ProvenTransaction { let ref_block_num = BlockNumber::read_from(source)?; let ref_block_commitment = Word::read_from(source)?; - let fee = FungibleAsset::read_from(source)?; + let fee = Asset::read_from(source)?; let expiration_block_num = BlockNumber::read_from(source)?; let proof = ExecutionProof::read_from(source)?; @@ -614,7 +614,7 @@ mod tests { StorageMapKey, StorageSlotName, }; - use crate::asset::FungibleAsset; + use crate::asset::{Asset, FungibleAsset}; use crate::block::BlockNumber; use crate::errors::ProvenTransactionError; use crate::testing::account_id::{ @@ -737,7 +737,7 @@ mod tests { Vec::::new(), ref_block_num, ref_block_commitment, - FungibleAsset::mock(42).unwrap_fungible(), + Asset::from(FungibleAsset::mock(42).unwrap_fungible()), expiration_block_num, proof, ) diff --git a/crates/miden-protocol/src/transaction/transaction_id.rs b/crates/miden-protocol/src/transaction/transaction_id.rs index 4313e6c465..5ac21de8e4 100644 --- a/crates/miden-protocol/src/transaction/transaction_id.rs +++ b/crates/miden-protocol/src/transaction/transaction_id.rs @@ -4,7 +4,7 @@ use core::fmt::{Debug, Display}; use miden_crypto_derive::WordWrapper; use super::{Felt, Hasher, ProvenTransaction, WORD_SIZE, Word, ZERO}; -use crate::asset::{Asset, FungibleAsset}; +use crate::asset::Asset; use crate::utils::serde::{ ByteReader, ByteWriter, @@ -41,14 +41,14 @@ impl TransactionId { final_account_commitment: Word, input_notes_commitment: Word, output_notes_commitment: Word, - fee_asset: FungibleAsset, + fee_asset: Asset, ) -> Self { let mut elements = [ZERO; 6 * WORD_SIZE]; elements[..4].copy_from_slice(init_account_commitment.as_elements()); elements[4..8].copy_from_slice(final_account_commitment.as_elements()); elements[8..12].copy_from_slice(input_notes_commitment.as_elements()); elements[12..16].copy_from_slice(output_notes_commitment.as_elements()); - elements[16..].copy_from_slice(&Asset::from(fee_asset).as_elements()); + elements[16..].copy_from_slice(&fee_asset.as_elements()); Self(Hasher::hash_elements(&elements)) } } diff --git a/crates/miden-protocol/src/transaction/tx_args.rs b/crates/miden-protocol/src/transaction/tx_args.rs index 1e6657bfaf..3de87dc719 100644 --- a/crates/miden-protocol/src/transaction/tx_args.rs +++ b/crates/miden-protocol/src/transaction/tx_args.rs @@ -39,6 +39,9 @@ use crate::{EMPTY_WORD, MastForest, MastNodeId}; /// this argument is not specified, the [`EMPTY_WORD`] would be used as a default value. If the /// [AdviceInputs] are propagated with some user defined map entries, this argument could be used /// as a key to access the corresponding value. +/// - Fee arguments: data put onto the stack right before fee procedure execution. Used to configure +/// how the account pays for transaction fees (e.g., which asset to use, exchange rate). If not +/// specified, [`EMPTY_WORD`] is used. Can also be used as a key into the advice map. #[derive(Clone, Debug, PartialEq, Eq)] pub struct TransactionArgs { tx_script: Option, @@ -46,6 +49,7 @@ pub struct TransactionArgs { note_args: BTreeMap, advice_inputs: AdviceInputs, auth_args: Word, + fee_args: Word, } impl TransactionArgs { @@ -63,6 +67,7 @@ impl TransactionArgs { note_args: Default::default(), advice_inputs, auth_args: EMPTY_WORD, + fee_args: EMPTY_WORD, } } @@ -109,6 +114,16 @@ impl TransactionArgs { self } + /// Returns new [TransactionArgs] instantiated with the provided fee arguments. + /// + /// Fee arguments are passed to the account's fee procedure, which uses them to determine + /// which asset to pay fees in and at what rate. + #[must_use] + pub fn with_fee_args(mut self, fee_args: Word) -> Self { + self.fee_args = fee_args; + self + } + // PUBLIC ACCESSORS // -------------------------------------------------------------------------------------------- @@ -149,6 +164,11 @@ impl TransactionArgs { self.auth_args } + /// Returns the fee procedure argument, or [`EMPTY_WORD`] if not specified. + pub fn fee_args(&self) -> Word { + self.fee_args + } + // STATE MUTATORS // -------------------------------------------------------------------------------------------- @@ -252,6 +272,7 @@ impl Serializable for TransactionArgs { self.note_args.write_into(target); self.advice_inputs.write_into(target); self.auth_args.write_into(target); + self.fee_args.write_into(target); } } @@ -262,6 +283,7 @@ impl Deserializable for TransactionArgs { let note_args = BTreeMap::::read_from(source)?; let advice_inputs = AdviceInputs::read_from(source)?; let auth_args = Word::read_from(source)?; + let fee_args = Word::read_from(source)?; Ok(Self { tx_script, @@ -269,6 +291,7 @@ impl Deserializable for TransactionArgs { note_args, advice_inputs, auth_args, + fee_args, }) } } diff --git a/crates/miden-protocol/src/transaction/tx_header.rs b/crates/miden-protocol/src/transaction/tx_header.rs index 23a2721e88..1c95cea8d2 100644 --- a/crates/miden-protocol/src/transaction/tx_header.rs +++ b/crates/miden-protocol/src/transaction/tx_header.rs @@ -1,7 +1,7 @@ use alloc::vec::Vec; use crate::Word; -use crate::asset::FungibleAsset; +use crate::asset::Asset; use crate::note::NoteHeader; use crate::transaction::{ AccountId, @@ -36,7 +36,7 @@ pub struct TransactionHeader { final_state_commitment: Word, input_notes: InputNotes, output_notes: Vec, - fee: FungibleAsset, + fee: Asset, } impl TransactionHeader { @@ -59,7 +59,7 @@ impl TransactionHeader { final_state_commitment: Word, input_notes: InputNotes, output_notes: Vec, - fee: FungibleAsset, + fee: Asset, ) -> Self { let input_notes_commitment = input_notes.commitment(); let output_notes_commitment = RawOutputNotes::compute_commitment(output_notes.iter()); @@ -96,7 +96,7 @@ impl TransactionHeader { final_state_commitment: Word, input_notes: InputNotes, output_notes: Vec, - fee: FungibleAsset, + fee: Asset, ) -> Self { Self { id, @@ -157,7 +157,7 @@ impl TransactionHeader { } /// Returns the fee paid by this transaction. - pub fn fee(&self) -> FungibleAsset { + pub fn fee(&self) -> Asset { self.fee } } @@ -225,7 +225,7 @@ impl Deserializable for TransactionHeader { let final_state_commitment = ::read_from(source)?; let input_notes = >::read_from(source)?; let output_notes = >::read_from(source)?; - let fee = FungibleAsset::read_from(source)?; + let fee = Asset::read_from(source)?; let tx_header = Self::new( account_id, diff --git a/crates/miden-standards/asm/account_components/fees/native_fee.masm b/crates/miden-standards/asm/account_components/fees/native_fee.masm new file mode 100644 index 0000000000..604f5a586d --- /dev/null +++ b/crates/miden-standards/asm/account_components/fees/native_fee.masm @@ -0,0 +1,68 @@ +# Default fee procedure that pays fees in the native asset. +# +# The native faucet ID is passed via FEE_ARGS as: +# FEE_ARGS = [faucet_id_suffix, faucet_id_prefix, enable_callbacks, 0] +# +# The fee procedure constructs a fungible asset from the computation_units (as the amount) +# and the faucet ID from FEE_ARGS, then returns the asset key and value words. +# +# For accounts using the default fee path, the client populates FEE_ARGS with the native +# faucet ID from the reference block. + +use miden::protocol::active_account + +const FAUCET_ID_SLOT = word("miden::standards::fees::native_fee::faucet_id") + +#! Default fee procedure: returns a native fungible asset for the given computation units. +#! +#! The faucet ID is read from the account's storage slot configured at account creation. +#! +#! Inputs: [computation_units, FEE_ARGS, pad(11)] +#! Outputs: [ASSET_KEY, ASSET_VALUE, pad(8)] +@fee_script +pub proc create_fee + # drop everything except computation_units + # stack: [computation_units, FEE_ARGS(4), pad(11)] + swap drop swap drop swap drop swap drop + # => [computation_units, pad(11)] + + # drop remaining padding + swap drop swap drop swap drop swap drop + swap drop swap drop swap drop + # => [computation_units] + + # read the faucet ID from account storage + push.FAUCET_ID_SLOT[0..2] exec.active_account::get_item + # => [faucet_id_suffix, faucet_id_prefix, 0, 0, computation_units] + + # construct ASSET_KEY = [faucet_id_suffix, faucet_id_prefix, 0, 0] (already on stack) + # construct ASSET_VALUE = [0, 0, computation_units, 0] + + movup.4 + # => [computation_units, faucet_id_suffix, faucet_id_prefix, 0, 0] + + push.0.0.0 + # => [0, 0, 0, computation_units, faucet_id_suffix, faucet_id_prefix, 0, 0] + + movup.3 + # => [computation_units, 0, 0, 0, faucet_id_suffix, faucet_id_prefix, 0, 0] + + # reorder to ASSET_VALUE = [0, 0, computation_units, 0] + # Currently: [computation_units, 0, 0, 0, ...] + # Need: [0, 0, computation_units, 0, ...] + swap movdn.2 + # => [0, 0, computation_units, 0, faucet_id_suffix, faucet_id_prefix, 0, 0] + + # swap so ASSET_KEY is on top + swapw + # => [faucet_id_suffix, faucet_id_prefix, 0, 0, 0, 0, computation_units, 0] + + # pad output to 16 elements + padw padw + # => [pad(8), faucet_id_suffix, faucet_id_prefix, 0, 0, 0, 0, computation_units, 0] + + swapw.2 swapw + # => [faucet_id_suffix, faucet_id_prefix, 0, 0, 0, 0, computation_units, 0, pad(8)] + + # => [ASSET_KEY, ASSET_VALUE, pad(8)] +end diff --git a/crates/miden-standards/src/account/components/mod.rs b/crates/miden-standards/src/account/components/mod.rs index 723cbf5d14..fe31fd73a3 100644 --- a/crates/miden-standards/src/account/components/mod.rs +++ b/crates/miden-standards/src/account/components/mod.rs @@ -76,6 +76,15 @@ static NO_AUTH_LIBRARY: LazyLock = LazyLock::new(|| { Library::read_from_bytes(bytes).expect("Shipped NoAuth library is well-formed") }); +// FEE LIBRARIES +// ================================================================================================ + +static NATIVE_FEE_LIBRARY: LazyLock = LazyLock::new(|| { + let bytes = + include_bytes!(concat!(env!("OUT_DIR"), "/assets/account_components/fees/native_fee.masl")); + Library::read_from_bytes(bytes).expect("Shipped NativeFee library is well-formed") +}); + // FAUCET LIBRARIES // ================================================================================================ @@ -175,6 +184,11 @@ pub fn no_auth_library() -> Library { NO_AUTH_LIBRARY.clone() } +/// Returns the NativeFee Library. +pub fn native_fee_library() -> Library { + NATIVE_FEE_LIBRARY.clone() +} + // STANDARD ACCOUNT COMPONENTS // ================================================================================================ diff --git a/crates/miden-standards/src/account/fees/mod.rs b/crates/miden-standards/src/account/fees/mod.rs new file mode 100644 index 0000000000..3b559e5e05 --- /dev/null +++ b/crates/miden-standards/src/account/fees/mod.rs @@ -0,0 +1,43 @@ +use miden_protocol::account::component::AccountComponentMetadata; +use miden_protocol::account::{AccountComponent, AccountType}; + +use crate::account::components::native_fee_library; + +/// Default fee component that pays transaction fees in the native asset. +/// +/// This component provides a `@fee_script` procedure that converts computation units +/// into the native fungible asset at a 1:1 rate. FEE_ARGS are ignored. +/// +/// Accounts using this component pay fees exactly as they do today: in the native asset, +/// proportional to computation cost. For accounts that want to pay in a different asset, +/// a custom fee component can be provided instead. +pub struct NativeFee; + +impl NativeFee { + pub const NAME: &'static str = "miden::standards::components::fees::native_fee"; + + pub fn new() -> Self { + Self + } + + pub fn component_metadata() -> AccountComponentMetadata { + AccountComponentMetadata::new(Self::NAME, AccountType::all()) + .with_description("Default fee component paying in the native asset") + } +} + +impl Default for NativeFee { + fn default() -> Self { + Self::new() + } +} + +impl From for AccountComponent { + fn from(_: NativeFee) -> Self { + let metadata = NativeFee::component_metadata(); + + AccountComponent::new(native_fee_library(), vec![], metadata).expect( + "NativeFee component should satisfy the requirements of a valid account component", + ) + } +} diff --git a/crates/miden-standards/src/account/mod.rs b/crates/miden-standards/src/account/mod.rs index 9580c185f8..5f20bc608d 100644 --- a/crates/miden-standards/src/account/mod.rs +++ b/crates/miden-standards/src/account/mod.rs @@ -4,6 +4,7 @@ pub mod access; pub mod auth; pub mod components; pub mod faucets; +pub mod fees; pub mod interface; pub mod metadata; pub mod mint_policies; diff --git a/crates/miden-testing/src/kernel_tests/batch/proven_tx_builder.rs b/crates/miden-testing/src/kernel_tests/batch/proven_tx_builder.rs index abf6b8809e..5420913ee2 100644 --- a/crates/miden-testing/src/kernel_tests/batch/proven_tx_builder.rs +++ b/crates/miden-testing/src/kernel_tests/batch/proven_tx_builder.rs @@ -4,7 +4,7 @@ use anyhow::Context; use miden_protocol::Word; use miden_protocol::account::AccountId; use miden_protocol::account::delta::AccountUpdateDetails; -use miden_protocol::asset::FungibleAsset; +use miden_protocol::asset::{Asset, FungibleAsset}; use miden_protocol::block::BlockNumber; use miden_protocol::crypto::merkle::SparseMerklePath; use miden_protocol::note::{Note, NoteInclusionProof, Nullifier}; @@ -130,7 +130,7 @@ impl MockProvenTxBuilder { self.output_notes.unwrap_or_default(), BlockNumber::from(0), self.ref_block_commitment.unwrap_or_default(), - self.fee, + Asset::from(self.fee), self.expiration_block_num, ExecutionProof::new_dummy(), ) diff --git a/crates/miden-testing/src/kernel_tests/block/header_errors.rs b/crates/miden-testing/src/kernel_tests/block/header_errors.rs index ffc2e1d2c6..fc76e3d14c 100644 --- a/crates/miden-testing/src/kernel_tests/block/header_errors.rs +++ b/crates/miden-testing/src/kernel_tests/block/header_errors.rs @@ -12,7 +12,7 @@ use miden_protocol::account::{ StorageSlot, StorageSlotName, }; -use miden_protocol::asset::FungibleAsset; +use miden_protocol::asset::{Asset, FungibleAsset}; use miden_protocol::batch::ProvenBatch; use miden_protocol::block::{BlockInputs, BlockNumber, ProposedBlock}; use miden_protocol::errors::{AccountTreeError, NullifierTreeError, ProposedBlockError}; @@ -403,7 +403,7 @@ async fn block_building_fails_on_creating_account_with_duplicate_account_id_pref Vec::::new(), genesis_block.block_num(), genesis_block.commitment(), - FungibleAsset::mock(500).unwrap_fungible(), + Asset::from(FungibleAsset::mock(500).unwrap_fungible()), BlockNumber::from(u32::MAX), ExecutionProof::new_dummy(), ) 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 2256a43ab9..3afdb4c4f5 100644 --- a/crates/miden-testing/src/kernel_tests/tx/test_fee.rs +++ b/crates/miden-testing/src/kernel_tests/tx/test_fee.rs @@ -33,10 +33,12 @@ async fn create_account_with_fees() -> anyhow::Result<()> { .context("failed to execute account-creating transaction")?; let expected_fee = tx.compute_fee(); - assert_eq!(expected_fee, tx.fee().amount()); + let fee_fungible = tx.fee().unwrap_fungible(); + assert_eq!(expected_fee, fee_fungible.amount()); // We expect that the new account contains the note_amount minus the paid fee. - let added_asset = FungibleAsset::new(chain.native_asset_id(), note_amount)?.sub(tx.fee())?; + let added_asset = + FungibleAsset::new(chain.native_asset_id(), note_amount)?.sub(fee_fungible)?; assert_eq!(tx.account_delta().nonce_delta(), Felt::new(1)); // except for the nonce, the storage delta should be empty diff --git a/crates/miden-testing/tests/scripts/fee.rs b/crates/miden-testing/tests/scripts/fee.rs index 144f445d08..4c00015426 100644 --- a/crates/miden-testing/tests/scripts/fee.rs +++ b/crates/miden-testing/tests/scripts/fee.rs @@ -30,10 +30,11 @@ async fn prove_account_creation_with_fees() -> anyhow::Result<()> { .context("failed to execute account-creating transaction")?; let expected_fee = tx.compute_fee(); - assert_eq!(expected_fee, tx.fee().amount()); + let fee_fungible = tx.fee().unwrap_fungible(); + assert_eq!(expected_fee, fee_fungible.amount()); // We expect that the new account contains the amount minus the paid fee. - let added_asset = FungibleAsset::new(chain.native_asset_id(), amount)?.sub(tx.fee())?; + let added_asset = FungibleAsset::new(chain.native_asset_id(), amount)?.sub(fee_fungible)?; assert_eq!(tx.account_delta().nonce_delta(), Felt::new(1)); // except for the nonce, the storage delta should be empty diff --git a/crates/miden-tx/src/errors/mod.rs b/crates/miden-tx/src/errors/mod.rs index 4013fc52fe..1452d17870 100644 --- a/crates/miden-tx/src/errors/mod.rs +++ b/crates/miden-tx/src/errors/mod.rs @@ -309,6 +309,8 @@ pub enum TransactionKernelError { "native asset amount {account_balance} in the account vault is not sufficient to cover the transaction fee of {tx_fee}" )] InsufficientFee { account_balance: u64, tx_fee: u64 }, + #[error("non-fungible fee assets are not supported")] + NonFungibleFeeAssetNotSupported, /// This variant signals that a signature over the contained commitments is required, but /// missing. #[error("transaction requires a signature")] diff --git a/crates/miden-tx/src/executor/exec_host.rs b/crates/miden-tx/src/executor/exec_host.rs index a277eb8f11..1ac0f302aa 100644 --- a/crates/miden-tx/src/executor/exec_host.rs +++ b/crates/miden-tx/src/executor/exec_host.rs @@ -19,7 +19,7 @@ use miden_protocol::account::{ }; use miden_protocol::assembly::debuginfo::Location; use miden_protocol::assembly::{SourceFile, SourceManagerSync, SourceSpan}; -use miden_protocol::asset::{AssetVaultKey, AssetWitness, FungibleAsset}; +use miden_protocol::asset::{Asset, AssetVaultKey, AssetWitness, FungibleAsset}; use miden_protocol::block::BlockNumber; use miden_protocol::crypto::merkle::smt::SmtProof; use miden_protocol::note::{NoteMetadata, NoteRecipient, NoteScript, NoteStorage}; @@ -219,56 +219,63 @@ where /// Handles the [`TransactionEvent::EpilogueBeforeTxFeeRemovedFromAccount`] and returns an error /// if the account cannot pay the fee. + /// + /// The fee procedure can return any asset type. For now we only support fungible fee assets; + /// non-fungible fee assets are rejected. The node uses this check as the accept/reject point + /// for the fee asset: if the asset type or amount is unacceptable, the transaction is aborted. async fn on_before_tx_fee_removed_from_account( &self, - fee_asset: FungibleAsset, + fee_asset: Asset, ) -> Result, TransactionKernelError> { - // Construct initial fee asset. - let initial_fee_asset = - FungibleAsset::new(fee_asset.faucet_id(), self.initial_fee_asset_balance) - .expect("fungible asset created from fee asset should be valid"); - - // Compute the current balance of the native asset in the account based on the initial value - // and the delta. - let current_fee_asset = { - let fee_asset_amount_delta = self - .base_host - .account_delta_tracker() - .vault_delta() - .fungible() - .amount(&initial_fee_asset.vault_key()) - .unwrap_or(0); - - // SAFETY: Initial native asset faucet ID should be a fungible faucet and amount should - // be less than MAX_AMOUNT as checked by the account delta. - let fee_asset_delta = FungibleAsset::new( - initial_fee_asset.faucet_id(), - fee_asset_amount_delta.unsigned_abs(), - ) - .expect("faucet ID and amount should be valid"); - - // SAFETY: These computations are essentially the same as the ones executed by the - // transaction kernel, which should have aborted if they weren't valid. - if fee_asset_amount_delta > 0 { - initial_fee_asset - .add(fee_asset_delta) - .expect("transaction kernel should ensure amounts do not exceed MAX_AMOUNT") - } else { - initial_fee_asset - .sub(fee_asset_delta) - .expect("transaction kernel should ensure amount is not negative") - } - }; + match fee_asset { + Asset::Fungible(fee_asset) => { + // Construct initial fee asset. + let initial_fee_asset = + FungibleAsset::new(fee_asset.faucet_id(), self.initial_fee_asset_balance) + .expect("fungible asset created from fee asset should be valid"); + + // Compute the current balance of the fee asset based on the initial value and + // delta. + let current_fee_asset = { + let fee_asset_amount_delta = self + .base_host + .account_delta_tracker() + .vault_delta() + .fungible() + .amount(&initial_fee_asset.vault_key()) + .unwrap_or(0); + + let fee_asset_delta = FungibleAsset::new( + initial_fee_asset.faucet_id(), + fee_asset_amount_delta.unsigned_abs(), + ) + .expect("faucet ID and amount should be valid"); - // Return an error if the balance in the account does not cover the fee. - if current_fee_asset.amount() < fee_asset.amount() { - return Err(TransactionKernelError::InsufficientFee { - account_balance: current_fee_asset.amount(), - tx_fee: fee_asset.amount(), - }); - } + if fee_asset_amount_delta > 0 { + initial_fee_asset.add(fee_asset_delta).expect( + "transaction kernel should ensure amounts do not exceed MAX_AMOUNT", + ) + } else { + initial_fee_asset + .sub(fee_asset_delta) + .expect("transaction kernel should ensure amount is not negative") + } + }; + + if current_fee_asset.amount() < fee_asset.amount() { + return Err(TransactionKernelError::InsufficientFee { + account_balance: current_fee_asset.amount(), + tx_fee: fee_asset.amount(), + }); + } - Ok(Vec::new()) + Ok(Vec::new()) + }, + Asset::NonFungible(_) => { + // Non-fungible fee assets are not supported in v1. + Err(TransactionKernelError::NonFungibleFeeAssetNotSupported) + }, + } } /// Handles a request for a storage map witness by querying the data store for a merkle path. diff --git a/crates/miden-tx/src/executor/mod.rs b/crates/miden-tx/src/executor/mod.rs index eb2541faf5..17d6387529 100644 --- a/crates/miden-tx/src/executor/mod.rs +++ b/crates/miden-tx/src/executor/mod.rs @@ -437,7 +437,7 @@ fn build_executed_transaction