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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

### Changes

- Skip requests to the `DataStore` for asset vault witnesses which are already in transaction inputs ([#2298](https://github.com/0xMiden/miden-base/pull/2298)).
- [BREAKING] refactored `TransactionAuthenticator::get_public_key()` method to return `Arc<PublicKey> `instead of `&PublicKey` ([#2304](https://github.com/0xMiden/miden-base/pull/2304)).

## 0.13.0 (2026-01-16)
Expand Down
5 changes: 5 additions & 0 deletions crates/miden-protocol/src/asset/vault/vault_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ impl AssetVaultKey {
}
}

/// Returns a reference to the inner [Word] of this key.
pub fn as_word(&self) -> &Word {
&self.0
}

/// Returns `true` if the asset key is for a fungible asset, `false` otherwise.
fn is_fungible(&self) -> bool {
self.0[0].as_int() == 0 && self.0[1].as_int() == 0
Expand Down
2 changes: 1 addition & 1 deletion crates/miden-protocol/src/errors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -734,7 +734,7 @@ pub enum TransactionInputsExtractionError {
MissingMapRoot,
#[error("failed to construct SMT proof")]
SmtProofError(#[from] SmtProofError),
#[error("failed to construct asset witness")]
#[error("failed to construct an asset")]
AssetError(#[from] AssetError),
#[error("failed to handle storage map data")]
StorageMapError(#[from] StorageMapError),
Expand Down
88 changes: 74 additions & 14 deletions crates/miden-protocol/src/transaction/inputs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use alloc::vec::Vec;
use core::fmt::Debug;

use miden_core::utils::{Deserializable, Serializable};
use miden_crypto::merkle::NodeIndex;
use miden_crypto::merkle::smt::{LeafIndex, SmtLeaf, SmtProof};
use miden_crypto::merkle::{MerkleError, NodeIndex};

use super::PartialBlockchain;
use crate::account::{
Expand All @@ -19,7 +19,7 @@ use crate::account::{
StorageSlotId,
StorageSlotName,
};
use crate::asset::{AssetVaultKey, AssetWitness, PartialVault};
use crate::asset::{Asset, AssetVaultKey, AssetWitness, PartialVault};
use crate::block::account_tree::{AccountWitness, account_id_to_smt_index};
use crate::block::{BlockHeader, BlockNumber};
use crate::crypto::merkle::SparseMerklePath;
Expand Down Expand Up @@ -51,8 +51,6 @@ pub struct TransactionInputs {
tx_args: TransactionArgs,
advice_inputs: AdviceInputs,
foreign_account_code: Vec<AccountCode>,
/// Pre-fetched asset witnesses for note assets and the fee asset.
asset_witnesses: Vec<AssetWitness>,
/// Storage slot names for foreign accounts.
foreign_account_slot_names: BTreeMap<StorageSlotId, StorageSlotName>,
}
Expand Down Expand Up @@ -110,14 +108,20 @@ impl TransactionInputs {
tx_args: TransactionArgs::default(),
advice_inputs: AdviceInputs::default(),
foreign_account_code: Vec::new(),
asset_witnesses: Vec::new(),
foreign_account_slot_names: BTreeMap::new(),
})
}

/// Replaces the transaction inputs and assigns the given asset witnesses.
pub fn with_asset_witnesses(mut self, witnesses: Vec<AssetWitness>) -> Self {
self.asset_witnesses = witnesses;
for witness in witnesses {
self.advice_inputs.store.extend(witness.authenticated_nodes());
let smt_proof = SmtProof::from(witness);
self.advice_inputs
.map
.extend([(smt_proof.leaf().hash(), smt_proof.leaf().to_elements())]);
}
Comment on lines 116 to +123
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the approach from #2099 more or less strongly requires that we extend both self.advice_inputs and self.tx_args.advice_inputs. I'm actually not sure how necessary this is (I find this pretty hard to think about now), but it seems it would be more consistent. So I would add the function added in #2274 for this:

/// Extends the advice inputs with the provided inputs.
///
/// This extends both the internal advice inputs and the transaction arguments' advice inputs,
/// ensuring that `self.advice_inputs` is always a subset of `self.tx_args.advice_inputs()`.
(pub) fn extend_advice_inputs(&mut self, advice_inputs: AdviceInputs) {
    self.advice_inputs.extend(advice_inputs.clone());
    self.tx_args.extend_advice_inputs(advice_inputs);
}

And then here do:

let mut tx_adv = TransactionAdviceInputs::default();
witnesses.into_iter().for_each(|witness| tx_adv.add_asset_witness(witness));
self.extend_advice_inputs(tx_adv.into_advice_inputs());

This reuses the existing asset witness -> advice input conversion and we don't duplicate it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the approach from #2099 more or less strongly requires that we extend both self.advice_inputs and self.tx_args.advice_inputs.

I'm not sure this is needed because TransactionInputs::set_advice_inputs() should already take care of this. But I agree the whole structure is confusing. I hope that with the structure I described in #1286 (comment), we'd be able to clarify the structure somewhat.

Copy link
Contributor

@PhilippGackstatter PhilippGackstatter Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure this is needed because TransactionInputs::set_advice_inputs() should already take care of this.

Technically yes, but it's not obvious that set_advice_inputs is called later (only after tx execution) and so this leaves this "invariant" temporarily broken. Not sure if this is a real problem, and I think it's fine to address this in #1286 or #2293.

Nit: I would prefer using TransactionAdviceInputs here so we use the lower-level leaf -> advice inputs conversion implementation (TransactionAdviceInputs::add_asset_witness) rather than move it outside that type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I would prefer using TransactionAdviceInputs here so we use the lower-level leaf -> advice inputs conversion implementation (TransactionAdviceInputs::add_asset_witness) rather than move it outside that type.

I've actually removed TransactionAdviceInputs::add_asset_witness() in this PR since it was no longer needed. I think we can bring it back if needed depending on how we address #1286 and #2293.


self
}

Expand Down Expand Up @@ -210,11 +214,6 @@ impl TransactionInputs {
&self.foreign_account_code
}

/// Returns the pre-fetched witnesses for note and fee assets.
pub fn asset_witnesses(&self) -> &[AssetWitness] {
&self.asset_witnesses
}

/// Returns the foreign account storage slot names.
pub fn foreign_account_slot_names(&self) -> &BTreeMap<StorageSlotId, StorageSlotName> {
&self.foreign_account_slot_names
Expand Down Expand Up @@ -263,6 +262,12 @@ impl TransactionInputs {
}

/// Reads the vault asset witnesses for the given account and vault keys.
///
/// # Errors
/// Returns an error if:
/// - A Merkle tree with the specified root is not present in the advice data of these inputs.
/// - Witnesses for any of the requested assets are not in the specified Merkle tree.
/// - Construction of the Merkle path or the leaf node for the witness fails.
pub fn read_vault_asset_witnesses(
&self,
vault_root: Word,
Expand Down Expand Up @@ -292,6 +297,64 @@ impl TransactionInputs {
Ok(asset_witnesses)
}

/// Returns true if the witness for the specified asset key is present in these inputs.
///
/// Note that this does not verify the witness' validity (i.e., that the witness is for a valid
/// asset).
pub fn has_vault_asset_witness(&self, vault_root: Word, asset_key: &AssetVaultKey) -> bool {
let smt_index: NodeIndex = asset_key.to_leaf_index().into();

// make sure the path is in the Merkle store
if !self.advice_inputs.store.has_path(vault_root, smt_index) {
return false;
}

// make sure the node pre-image is in the Merkle store
match self.advice_inputs.store.get_node(vault_root, smt_index) {
Ok(node) => self.advice_inputs.map.contains_key(&node),
Err(_) => false,
}
}

/// Reads the asset from the specified vault under the specified key; returns `None` if the
/// specified asset is not present in these inputs.
///
/// # Errors
/// Returns an error if:
/// - A Merkle tree with the specified root is not present in the advice data of these inputs.
/// - Construction of the leaf node or the asset fails.
pub fn read_vault_asset(
&self,
vault_root: Word,
asset_key: AssetVaultKey,
) -> Result<Option<Asset>, TransactionInputsExtractionError> {
// Get the node corresponding to the asset_key; if not found return None
let smt_index = asset_key.to_leaf_index();
let merkle_node = match self.advice_inputs.store.get_node(vault_root, smt_index.into()) {
Ok(node) => node,
Err(MerkleError::NodeIndexNotFoundInStore(..)) => return Ok(None),
Err(err) => return Err(err.into()),
};

// Construct SMT leaf for this asset key
let smt_leaf_elements = self
.advice_inputs
.map
.get(&merkle_node)
.ok_or(TransactionInputsExtractionError::MissingVaultRoot)?;
let smt_leaf = smt_leaf_from_elements(smt_leaf_elements, smt_index)?;

// Find the asset in the SMT leaf
let asset = smt_leaf
.entries()
.iter()
.find(|(key, _value)| key == asset_key.as_word())
.map(|(_key, value)| Asset::try_from(value))
.transpose()?;

Ok(asset)
}

/// Reads AccountInputs for a foreign account from the advice inputs.
///
/// This function reverses the process of [`TransactionAdviceInputs::add_foreign_accounts`] by:
Expand Down Expand Up @@ -432,7 +495,6 @@ impl Serializable for TransactionInputs {
self.tx_args.write_into(target);
self.advice_inputs.write_into(target);
self.foreign_account_code.write_into(target);
self.asset_witnesses.write_into(target);
self.foreign_account_slot_names.write_into(target);
}
}
Expand All @@ -448,7 +510,6 @@ impl Deserializable for TransactionInputs {
let tx_args = TransactionArgs::read_from(source)?;
let advice_inputs = AdviceInputs::read_from(source)?;
let foreign_account_code = Vec::<AccountCode>::read_from(source)?;
let asset_witnesses = Vec::<AssetWitness>::read_from(source)?;
let foreign_account_slot_names =
BTreeMap::<StorageSlotId, StorageSlotName>::read_from(source)?;

Expand All @@ -460,7 +521,6 @@ impl Deserializable for TransactionInputs {
tx_args,
advice_inputs,
foreign_account_code,
asset_witnesses,
foreign_account_slot_names,
})
}
Expand Down
4 changes: 0 additions & 4 deletions crates/miden-protocol/src/transaction/inputs/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ fn test_read_foreign_account_inputs_missing_data() {
tx_args: crate::transaction::TransactionArgs::default(),
advice_inputs: crate::vm::AdviceInputs::default(),
foreign_account_code: Vec::new(),
asset_witnesses: Vec::new(),
foreign_account_slot_names: BTreeMap::new(),
};

Expand Down Expand Up @@ -139,7 +138,6 @@ fn test_read_foreign_account_inputs_with_storage_data() {
tx_args: crate::transaction::TransactionArgs::default(),
advice_inputs,
foreign_account_code: vec![code],
asset_witnesses: Vec::new(),
foreign_account_slot_names,
};

Expand Down Expand Up @@ -261,7 +259,6 @@ fn test_read_foreign_account_inputs_with_proper_witness() {
tx_args: crate::transaction::TransactionArgs::default(),
advice_inputs,
foreign_account_code: vec![code],
asset_witnesses: Vec::new(),
foreign_account_slot_names: BTreeMap::new(),
};

Expand Down Expand Up @@ -348,7 +345,6 @@ fn test_transaction_inputs_serialization_with_foreign_slot_names() {
tx_args: crate::transaction::TransactionArgs::default(),
advice_inputs: crate::vm::AdviceInputs::default(),
foreign_account_code: Vec::new(),
asset_witnesses: Vec::new(),
foreign_account_slot_names,
};

Expand Down
14 changes: 0 additions & 14 deletions crates/miden-protocol/src/transaction/kernel/advice_inputs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ use alloc::vec::Vec;
use miden_processor::AdviceMutation;

use crate::account::{AccountHeader, AccountId, PartialAccount};
use crate::asset::AssetWitness;
use crate::block::account_tree::AccountWitness;
use crate::crypto::SequentialCommit;
use crate::crypto::merkle::InnerNodeInfo;
use crate::crypto::merkle::smt::SmtProof;
use crate::note::NoteAttachmentContent;
use crate::transaction::{
AccountInputs,
Expand Down Expand Up @@ -75,10 +73,6 @@ impl TransactionAdviceInputs {
}
}

tx_inputs.asset_witnesses().iter().for_each(|asset_witness| {
inputs.add_asset_witness(asset_witness.clone());
});

// Extend with extra user-supplied advice.
inputs.extend(tx_inputs.tx_args().advice_inputs().clone());

Expand Down Expand Up @@ -309,14 +303,6 @@ impl TransactionAdviceInputs {
self.extend_merkle_store(witness.authenticated_nodes());
}

/// Adds an asset witness to the advice inputs.
fn add_asset_witness(&mut self, witness: AssetWitness) {
self.extend_merkle_store(witness.authenticated_nodes());

let smt_proof = SmtProof::from(witness);
self.extend_map([(smt_proof.leaf().hash(), smt_proof.leaf().to_elements())]);
}

// NOTE INJECTION
// --------------------------------------------------------------------------------------------

Expand Down
3 changes: 3 additions & 0 deletions crates/miden-tx/src/errors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use miden_protocol::errors::{
NoteError,
ProvenTransactionError,
TransactionInputError,
TransactionInputsExtractionError,
TransactionOutputError,
};
use miden_protocol::note::{NoteId, NoteMetadata};
Expand Down Expand Up @@ -72,6 +73,8 @@ impl From<TransactionCheckerError> for TransactionExecutorError {

#[derive(Debug, Error)]
pub enum TransactionExecutorError {
#[error("failed to read fee asset from transaction inputs")]
FeeAssetRetrievalFailed(#[source] TransactionInputsExtractionError),
#[error("failed to fetch transaction inputs from the data store")]
FetchTransactionInputsFailed(#[source] DataStoreError),
#[error("failed to fetch asset witnesses from the data store")]
Expand Down
48 changes: 28 additions & 20 deletions crates/miden-tx/src/executor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,24 +263,34 @@ where
.await
.map_err(TransactionExecutorError::FetchTransactionInputsFailed)?;

// Add the vault key for the fee asset to the list of asset vault keys which will need to be
// accessed at the end of the transaction.
let native_account_vault_root = account.vault().root();
let fee_asset_vault_key =
AssetVaultKey::from_account_id(block_header.fee_parameters().native_asset_id())
.expect("fee asset should be a fungible asset");

let mut tx_inputs = TransactionInputs::new(account, block_header, blockchain, input_notes)
.map_err(TransactionExecutorError::InvalidTransactionInputs)?
.with_tx_args(tx_args);

// Add the vault key for the fee asset to the list of asset vault keys which will need to be
// accessed at the end of the transaction.
asset_vault_keys.insert(fee_asset_vault_key);

// Fetch the witnesses for all asset vault keys.
let asset_witnesses = self
.data_store
.get_vault_asset_witnesses(account_id, account.vault().root(), asset_vault_keys)
.await
.map_err(TransactionExecutorError::FetchAssetWitnessFailed)?;
// filter out any asset vault keys for which we already have witnesses in the advice inputs
asset_vault_keys.retain(|asset_key| {
!tx_inputs.has_vault_asset_witness(native_account_vault_root, asset_key)
});

let tx_inputs = TransactionInputs::new(account, block_header, blockchain, input_notes)
.map_err(TransactionExecutorError::InvalidTransactionInputs)?
.with_tx_args(tx_args)
.with_asset_witnesses(asset_witnesses);
// if any of the witnesses are missing, fetch them from the data store and add to tx_inputs
if !asset_vault_keys.is_empty() {
let asset_witnesses = self
.data_store
.get_vault_asset_witnesses(account_id, native_account_vault_root, asset_vault_keys)
.await
.map_err(TransactionExecutorError::FetchAssetWitnessFailed)?;

tx_inputs = tx_inputs.with_asset_witnesses(asset_witnesses);
}

Ok(tx_inputs)
}
Expand Down Expand Up @@ -318,25 +328,23 @@ where
AccountProcedureIndexMap::new([tx_inputs.account().code()]);

let initial_fee_asset_balance = {
let vault_root = tx_inputs.account().vault().root();
let native_asset_id = tx_inputs.block_header().fee_parameters().native_asset_id();
let fee_asset_vault_key = AssetVaultKey::from_account_id(native_asset_id)
.expect("fee asset should be a fungible asset");

let fee_asset_witness = tx_inputs
.asset_witnesses()
.iter()
.find_map(|witness| witness.find(fee_asset_vault_key));

match fee_asset_witness {
let fee_asset = tx_inputs
.read_vault_asset(vault_root, fee_asset_vault_key)
.map_err(TransactionExecutorError::FeeAssetRetrievalFailed)?;
match fee_asset {
Some(Asset::Fungible(fee_asset)) => fee_asset.amount(),
Some(Asset::NonFungible(_)) => {
return Err(TransactionExecutorError::FeeAssetMustBeFungible);
},
// If the witness does not contain the asset, its balance is zero.
// If the asset was not found, its balance is zero.
None => 0,
}
};

let host = TransactionExecutorHost::new(
tx_inputs.account(),
input_notes.clone(),
Expand Down