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