Skip to main content

miden_protocol/asset/vault/
mod.rs

1use alloc::collections::BTreeMap;
2use alloc::string::ToString;
3use alloc::vec::Vec;
4
5use miden_crypto::merkle::InnerNodeInfo;
6
7use super::{
8    Asset,
9    AssetAmount,
10    AssetComposition,
11    ByteReader,
12    ByteWriter,
13    Deserializable,
14    DeserializationError,
15    FungibleAsset,
16    NonFungibleAsset,
17    Serializable,
18};
19use crate::Word;
20use crate::account::AccountVaultPatch;
21use crate::crypto::merkle::smt::{SMT_DEPTH, Smt};
22use crate::errors::{AssetError, AssetVaultError};
23
24mod partial;
25pub use partial::PartialVault;
26
27mod asset_witness;
28pub use asset_witness::AssetWitness;
29
30mod asset_id;
31pub use asset_id::{AssetId, AssetIdHash};
32
33mod asset_class;
34pub use asset_class::AssetClass;
35
36// ASSET VAULT
37// ================================================================================================
38
39/// A container for an unlimited number of assets.
40///
41/// An asset vault can contain an unlimited number of assets. The assets are stored in a Sparse
42/// Merkle Tree, keyed by the hash of the [`AssetId`] (see [`AssetId::hash`]).
43/// Hashing the raw asset ID gives a uniform leaf distribution: in particular it prevents
44/// non-fungible assets issued by the same faucet from sharing a leaf, which would otherwise happen
45/// because their raw asset IDs share their fourth element (the faucet ID prefix) - the element the
46/// SMT uses to determine leaf membership.
47///
48/// The raw (unhashed) [`AssetId`]s are retained alongside the SMT to allow iteration and
49/// proof reconstruction.
50///
51/// An asset vault can be reduced to a single hash which is the root of the Sparse Merkle Tree.
52#[derive(Debug, Clone, Default, PartialEq, Eq)]
53pub struct AssetVault {
54    /// SMT keyed by hashed [`AssetId`]s.
55    asset_tree: Smt,
56    /// Raw [`AssetId`]s -> asset value words, kept in sync with `asset_tree`.
57    entries: BTreeMap<AssetId, Word>,
58}
59
60impl AssetVault {
61    // CONSTANTS
62    // --------------------------------------------------------------------------------------------
63
64    /// The depth of the SMT that represents the asset vault.
65    pub const DEPTH: u8 = SMT_DEPTH;
66
67    // CONSTRUCTOR
68    // --------------------------------------------------------------------------------------------
69
70    /// Returns a new [AssetVault] initialized with the provided assets.
71    pub fn new(assets: &[Asset]) -> Result<Self, AssetVaultError> {
72        let asset_tree = Smt::with_entries(
73            assets.iter().map(|asset| (asset.id().hash().as_word(), asset.to_value_word())),
74        )
75        .map_err(AssetVaultError::DuplicateAsset)?;
76
77        // Filter empty values so the `entries` map stays in sync with the SMT, which treats
78        // empty values as no-ops. `Smt::with_entries` above already errored on duplicate keys,
79        // so collecting into a `BTreeMap` here cannot silently drop assets.
80        let entries = assets
81            .iter()
82            .filter(|asset| !asset.to_value_word().is_empty())
83            .map(|asset| (asset.id(), asset.to_value_word()))
84            .collect();
85
86        Ok(Self { asset_tree, entries })
87    }
88
89    // PUBLIC ACCESSORS
90    // --------------------------------------------------------------------------------------------
91
92    /// Returns the tree root of this vault.
93    pub fn root(&self) -> Word {
94        self.asset_tree.root()
95    }
96
97    /// Returns the asset corresponding to the provided asset ID, or `None` if the asset
98    /// doesn't exist.
99    pub fn get(&self, asset_id: AssetId) -> Option<Asset> {
100        let asset_value = self.entries.get(&asset_id).copied().unwrap_or_default();
101
102        if asset_value.is_empty() {
103            None
104        } else {
105            Some(
106                Asset::from_id_and_value(asset_id, asset_value)
107                    .expect("asset vault should only store valid assets"),
108            )
109        }
110    }
111
112    /// Returns true if the specified non-fungible asset is stored in this vault.
113    pub fn has_non_fungible_asset(&self, asset: NonFungibleAsset) -> Result<bool, AssetVaultError> {
114        Ok(self.entries.contains_key(&asset.id()))
115    }
116
117    /// Returns the balance of the fungible asset identified by `asset_id`.
118    ///
119    /// If the vault does not contain the asset, zero is returned.
120    ///
121    /// # Errors
122    ///
123    /// Returns an error if `asset_id`'s composition is not [`AssetComposition::Fungible`].
124    pub fn get_balance(&self, asset_id: AssetId) -> Result<AssetAmount, AssetError> {
125        if !asset_id.composition().is_fungible() {
126            return Err(AssetError::AssetCompositionMismatch {
127                faucet_id: asset_id.faucet_id(),
128                expected: AssetComposition::Fungible,
129                actual: asset_id.composition(),
130            });
131        }
132
133        let asset_value = self.entries.get(&asset_id).copied().unwrap_or_default();
134        let asset = FungibleAsset::from_id_and_value(asset_id, asset_value)
135            .expect("asset vault should only store valid assets");
136
137        Ok(asset.amount())
138    }
139
140    /// Returns an iterator over the assets stored in the vault.
141    pub fn assets(&self) -> impl Iterator<Item = Asset> + '_ {
142        // SAFETY: The entries map only tracks valid assets.
143        self.entries.iter().map(|(id, value)| {
144            Asset::from_id_and_value(*id, *value)
145                .expect("asset vault should only store valid assets")
146        })
147    }
148
149    /// Returns an iterator over the inner nodes of the underlying [`Smt`].
150    pub fn inner_nodes(&self) -> impl Iterator<Item = InnerNodeInfo> + '_ {
151        self.asset_tree.inner_nodes()
152    }
153
154    /// Returns an opening of the leaf associated with `asset_id`.
155    ///
156    /// The `asset_id` can be obtained with [`Asset::id`].
157    pub fn open(&self, asset_id: AssetId) -> AssetWitness {
158        let smt_proof = self.asset_tree.open(&asset_id.hash().as_word());
159        let value = self.entries.get(&asset_id).copied().unwrap_or_default();
160
161        // SAFETY: The ID-value pair is guaranteed to be present in the proof since we open its
162        // hashed form, and the asset vault only contains valid assets.
163        AssetWitness::new_unchecked(smt_proof, [(asset_id, value)])
164    }
165
166    /// Returns a bool indicating whether the vault is empty.
167    pub fn is_empty(&self) -> bool {
168        self.asset_tree.is_empty()
169    }
170
171    /// Returns the number of non-empty leaves in the underlying [`Smt`].
172    ///
173    /// Note that this may return a different value from [Self::num_assets()] as a single leaf may
174    /// contain more than one asset.
175    pub fn num_leaves(&self) -> usize {
176        self.asset_tree.num_leaves()
177    }
178
179    /// Returns the number of assets in this vault.
180    ///
181    /// Note that this may return a different value from [Self::num_leaves()] as a single leaf may
182    /// contain more than one asset.
183    pub fn num_assets(&self) -> usize {
184        self.asset_tree.num_entries()
185    }
186
187    // PUBLIC MODIFIERS
188    // --------------------------------------------------------------------------------------------
189
190    /// Applies the specified patch to the asset vault.
191    ///
192    /// This updates each asset that is contained in the patch to its new value.
193    ///
194    /// # Errors
195    ///
196    /// Returns an error if the maximum number of leaves per asset is exceeded.
197    pub fn apply_patch(&mut self, patch: &AccountVaultPatch) -> Result<(), AssetVaultError> {
198        for (&asset_id, &value) in patch.iter() {
199            self.insert_entry(asset_id, value)?;
200        }
201
202        Ok(())
203    }
204
205    // ADD ASSET
206    // --------------------------------------------------------------------------------------------
207
208    /// Inserts the specified asset into the vault, overwriting the asset value at the same asset
209    /// ID. Returns the value of the asset previously.
210    ///
211    /// # Errors
212    /// - The maximum number of leaves per asset is exceeded.
213    pub fn insert_asset(&mut self, asset: Asset) -> Result<Word, AssetVaultError> {
214        self.insert_entry(asset.id(), asset.to_value_word())
215    }
216
217    /// Add the specified asset to the vault.
218    ///
219    /// # Errors
220    /// - If the total value of the added assets is greater than [`FungibleAsset::MAX_AMOUNT`].
221    /// - If the vault already contains the same non-fungible asset.
222    /// - The maximum number of leaves per asset is exceeded.
223    pub fn add_asset(&mut self, asset: Asset) -> Result<Asset, AssetVaultError> {
224        Ok(match asset {
225            Asset::Fungible(asset) => Asset::Fungible(self.add_fungible_asset(asset)?),
226            Asset::NonFungible(asset) => Asset::NonFungible(self.add_non_fungible_asset(asset)?),
227        })
228    }
229
230    /// Add the specified fungible asset to the vault. If the vault already contains an asset
231    /// issued by the same faucet, the amounts are added together.
232    ///
233    /// # Errors
234    /// - If the total value of the added assets is greater than [`FungibleAsset::MAX_AMOUNT`].
235    /// - The maximum number of leaves per asset is exceeded.
236    fn add_fungible_asset(
237        &mut self,
238        other_asset: FungibleAsset,
239    ) -> Result<FungibleAsset, AssetVaultError> {
240        let asset_id = other_asset.id();
241        let current_asset_value = self.entries.get(&asset_id).copied().unwrap_or_default();
242        let current_asset = FungibleAsset::from_id_and_value(asset_id, current_asset_value)
243            .expect("asset vault should store valid assets");
244
245        let new_asset = current_asset
246            .add(other_asset)
247            .map_err(AssetVaultError::AddFungibleAssetBalanceError)?;
248
249        self.insert_entry(new_asset.id(), new_asset.to_value_word())?;
250
251        Ok(new_asset)
252    }
253
254    /// Add the specified non-fungible asset to the vault.
255    ///
256    /// # Errors
257    /// - If the vault already contains the same non-fungible asset.
258    /// - The maximum number of leaves per asset is exceeded.
259    fn add_non_fungible_asset(
260        &mut self,
261        asset: NonFungibleAsset,
262    ) -> Result<NonFungibleAsset, AssetVaultError> {
263        let old = self.insert_entry(asset.id(), asset.to_value_word())?;
264
265        // if the asset already exists, return an error
266        if old != Smt::EMPTY_VALUE {
267            return Err(AssetVaultError::DuplicateNonFungibleAsset(asset));
268        }
269
270        Ok(asset)
271    }
272
273    // REMOVE ASSET
274    // --------------------------------------------------------------------------------------------
275    /// Remove the specified asset from the vault and returns the remaining asset, if any.
276    ///
277    /// - For fungible assets, returns `Some(Asset::Fungible(remaining))` with the remaining balance
278    ///   (which may have amount 0).
279    /// - For non-fungible assets, returns `None` since non-fungible assets are either fully present
280    ///   or absent.
281    ///
282    /// # Errors
283    /// - The fungible asset is not found in the vault.
284    /// - The amount of the fungible asset in the vault is less than the amount to be removed.
285    /// - The non-fungible asset is not found in the vault.
286    pub fn remove_asset(&mut self, asset: Asset) -> Result<Option<Asset>, AssetVaultError> {
287        match asset {
288            Asset::Fungible(asset) => {
289                let remaining = self.remove_fungible_asset(asset)?;
290                Ok(Some(Asset::Fungible(remaining)))
291            },
292            Asset::NonFungible(asset) => {
293                self.remove_non_fungible_asset(asset)?;
294                Ok(None)
295            },
296        }
297    }
298
299    /// Remove the specified fungible asset from the vault and returns the remaining fungible
300    /// asset. If the final amount of the asset is zero, the asset is removed from the vault.
301    ///
302    /// # Errors
303    /// - The asset is not found in the vault.
304    /// - The amount of the asset in the vault is less than the amount to be removed.
305    /// - The maximum number of leaves per asset is exceeded.
306    fn remove_fungible_asset(
307        &mut self,
308        other_asset: FungibleAsset,
309    ) -> Result<FungibleAsset, AssetVaultError> {
310        let asset_id = other_asset.id();
311        let current_asset_value = self.entries.get(&asset_id).copied().unwrap_or_default();
312        let current_asset = FungibleAsset::from_id_and_value(asset_id, current_asset_value)
313            .expect("asset vault should store valid assets");
314
315        // If the asset's amount is 0, we consider it absent from the vault.
316        if current_asset.amount() == AssetAmount::ZERO {
317            return Err(AssetVaultError::FungibleAssetNotFound(other_asset));
318        }
319
320        let new_asset = current_asset
321            .sub(other_asset)
322            .map_err(AssetVaultError::SubtractFungibleAssetBalanceError)?;
323
324        // Note that if new_asset's amount is 0, its value's word representation is equal to
325        // the empty word, which results in the removal of the entire entry from the corresponding
326        // leaf.
327        #[cfg(debug_assertions)]
328        {
329            if new_asset.amount() == AssetAmount::ZERO {
330                assert!(new_asset.to_value_word().is_empty())
331            }
332        }
333
334        self.insert_entry(new_asset.id(), new_asset.to_value_word())?;
335
336        Ok(new_asset)
337    }
338
339    /// Remove the specified non-fungible asset from the vault.
340    ///
341    /// # Errors
342    /// - The non-fungible asset is not found in the vault.
343    /// - The maximum number of leaves per asset is exceeded.
344    fn remove_non_fungible_asset(
345        &mut self,
346        asset: NonFungibleAsset,
347    ) -> Result<(), AssetVaultError> {
348        let old = self.insert_entry(asset.id(), Smt::EMPTY_VALUE)?;
349
350        // return an error if the asset did not exist in the vault.
351        if old == Smt::EMPTY_VALUE {
352            return Err(AssetVaultError::NonFungibleAssetNotFound(asset));
353        }
354
355        Ok(())
356    }
357
358    /// Inserts the given `(asset_id, value)` pair into both the SMT and the raw-entry map.
359    ///
360    /// Returns the previous SMT value at the hashed key (the empty word if no entry existed).
361    fn insert_entry(&mut self, asset_id: AssetId, value: Word) -> Result<Word, AssetVaultError> {
362        // Insert into the SMT first so that `entries` is only mutated once the fallible insert
363        // succeeds; this keeps the two structures in sync even if the insert errors.
364        let old_value = self
365            .asset_tree
366            .insert(asset_id.hash().into(), value)
367            .map_err(AssetVaultError::MaxLeafEntriesExceeded)?;
368
369        if value == Smt::EMPTY_VALUE {
370            self.entries.remove(&asset_id);
371        } else {
372            self.entries.insert(asset_id, value);
373        }
374
375        Ok(old_value)
376    }
377}
378
379// SERIALIZATION
380// ================================================================================================
381
382impl Serializable for AssetVault {
383    fn write_into<W: ByteWriter>(&self, target: &mut W) {
384        let num_assets = self.asset_tree.num_entries();
385        target.write_usize(num_assets);
386        target.write_many(self.assets());
387    }
388
389    fn get_size_hint(&self) -> usize {
390        let mut size = 0;
391        let mut count: usize = 0;
392
393        for asset in self.assets() {
394            size += asset.get_size_hint();
395            count += 1;
396        }
397
398        size += count.get_size_hint();
399
400        size
401    }
402}
403
404impl Deserializable for AssetVault {
405    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
406        let num_assets = source.read_usize()?;
407        let assets = source.read_many_iter::<Asset>(num_assets)?.collect::<Result<Vec<_>, _>>()?;
408        Self::new(&assets).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
409    }
410}
411
412// TESTS
413// ================================================================================================
414
415#[cfg(test)]
416mod tests {
417    use assert_matches::assert_matches;
418
419    use super::*;
420
421    #[test]
422    fn vault_fails_on_absent_fungible_asset() {
423        let mut vault = AssetVault::default();
424        let err = vault.remove_asset(FungibleAsset::mock(50)).unwrap_err();
425        assert_matches!(err, AssetVaultError::FungibleAssetNotFound(_));
426    }
427
428    /// Two non-fungible assets issued by the same faucet share their fourth raw-ID element (the
429    /// faucet ID prefix), which historically caused them to land in the same SMT leaf because the
430    /// SMT uses element 3 for leaf membership. Hashing the asset ID before insertion fixes that:
431    /// the assets must end up in different leaves.
432    ///
433    /// Regression test for <https://github.com/0xMiden/protocol/issues/2518>.
434    #[test]
435    fn two_non_fungible_assets_from_same_faucet_use_different_leaves() -> anyhow::Result<()> {
436        let asset0 = NonFungibleAsset::mock(&[1, 2, 3]);
437        let asset1 = NonFungibleAsset::mock(&[4, 5, 6]);
438
439        // Sanity check: the assets share their faucet but have distinct raw asset IDs (different
440        // asset class).
441        assert_eq!(asset0.id().faucet_id(), asset1.id().faucet_id());
442        assert_ne!(asset0.id(), asset1.id());
443
444        // Without hashing, both raw asset IDs share their two most significant elements (the
445        // faucet ID suffix/metadata in element 2 and the faucet ID prefix in element 3). Element 3
446        // is what the SMT uses for leaf membership, so the two would collide into a single leaf.
447        // Sanity-check that pre-condition.
448        assert_eq!(asset0.id().to_word()[2], asset1.id().to_word()[2]);
449        assert_eq!(asset0.id().to_word()[3], asset1.id().to_word()[3]);
450
451        // With hashing, the hashed leaf indices differ, so they live in different SMT leaves.
452        assert_ne!(asset0.id().hash().to_leaf_index(), asset1.id().hash().to_leaf_index());
453
454        let vault = AssetVault::new(&[asset0, asset1])?;
455        assert_eq!(vault.num_leaves(), 2);
456        assert_eq!(vault.num_assets(), 2);
457
458        Ok(())
459    }
460}