Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## 0.14.0 (TBD)

### 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)).

## 0.13.0 (2026-01-16)

### Features
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