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