Skip to main content

miden_protocol/account/delta/
vault.rs

1use alloc::collections::BTreeMap;
2use alloc::collections::btree_map::Entry;
3use alloc::string::ToString;
4use alloc::vec::Vec;
5
6use miden_core::Word;
7
8use super::{
9    AccountDeltaError,
10    ByteReader,
11    ByteWriter,
12    Deserializable,
13    DeserializationError,
14    Serializable,
15};
16use crate::Felt;
17use crate::account::delta::AssetDeltaOperation;
18use crate::asset::{Asset, AssetId, FungibleAsset, NonFungibleAsset};
19
20// ACCOUNT VAULT DELTA
21// ================================================================================================
22
23/// [AccountVaultDelta] stores the difference between the initial and final account vault states.
24///
25/// The difference is represented as follows:
26/// - fungible: a binary tree map of fungible asset balance changes in the account vault.
27/// - non_fungible: a binary tree map of non-fungible assets that were added to or removed from the
28///   account vault.
29#[derive(Clone, Debug, Default, PartialEq, Eq)]
30pub struct AccountVaultDelta {
31    fungible: FungibleAssetDelta,
32    non_fungible: NonFungibleAssetDelta,
33}
34
35impl AccountVaultDelta {
36    /// Domain separator for assets in the account delta commitment.
37    pub(in crate::account) const DOMAIN: Felt = Felt::new_unchecked(3);
38
39    /// Validates and creates an [AccountVaultDelta] with the given fungible and non-fungible asset
40    /// deltas.
41    ///
42    /// # Errors
43    /// Returns an error if the delta does not pass the validation.
44    pub const fn new(fungible: FungibleAssetDelta, non_fungible: NonFungibleAssetDelta) -> Self {
45        Self { fungible, non_fungible }
46    }
47
48    /// Returns a reference to the fungible asset delta.
49    pub fn fungible(&self) -> &FungibleAssetDelta {
50        &self.fungible
51    }
52
53    /// Returns a reference to the non-fungible asset delta.
54    pub fn non_fungible(&self) -> &NonFungibleAssetDelta {
55        &self.non_fungible
56    }
57
58    /// Returns true if this vault delta contains no updates.
59    pub fn is_empty(&self) -> bool {
60        self.fungible.is_empty() && self.non_fungible.is_empty()
61    }
62
63    /// Tracks asset addition.
64    pub fn add_asset(&mut self, asset: Asset) -> Result<(), AccountDeltaError> {
65        match asset {
66            Asset::Fungible(asset) => self.fungible.add(asset),
67            Asset::NonFungible(asset) => self.non_fungible.add(asset),
68        }
69    }
70
71    /// Tracks asset removal.
72    pub fn remove_asset(&mut self, asset: Asset) -> Result<(), AccountDeltaError> {
73        match asset {
74            Asset::Fungible(asset) => self.fungible.remove(asset),
75            Asset::NonFungible(asset) => self.non_fungible.remove(asset),
76        }
77    }
78
79    /// Returns an iterator over the added assets in this delta.
80    pub fn added_assets(&self) -> impl Iterator<Item = crate::asset::Asset> + '_ {
81        self.fungible
82            .0
83            .iter()
84            .filter(|&(_, &value)| value >= 0)
85            .map(|(asset_id, &diff)| {
86                Asset::Fungible(
87                    FungibleAsset::new(asset_id.faucet_id(), diff.unsigned_abs()).unwrap(),
88                )
89            })
90            .chain(
91                self.non_fungible
92                    .filter_by_action(NonFungibleDeltaAction::Add)
93                    .map(Asset::NonFungible),
94            )
95    }
96
97    /// Returns an iterator over the removed assets in this delta.
98    pub fn removed_assets(&self) -> impl Iterator<Item = crate::asset::Asset> + '_ {
99        self.fungible
100            .0
101            .iter()
102            .filter(|&(_, &value)| value < 0)
103            .map(|(asset_id, &diff)| {
104                Asset::Fungible(
105                    FungibleAsset::new(asset_id.faucet_id(), diff.unsigned_abs()).unwrap(),
106                )
107            })
108            .chain(
109                self.non_fungible
110                    .filter_by_action(NonFungibleDeltaAction::Remove)
111                    .map(Asset::NonFungible),
112            )
113    }
114
115    /// Appends the vault delta to the given `elements` from which the delta commitment will be
116    /// computed.
117    pub(super) fn append_delta_elements(&self, elements: &mut Vec<Felt>) {
118        // Add added and removed assets to a map to sort by asset ID.
119
120        // TODO(unified_delta): Refactor the internal asset delta structure to match the tx kernel
121        // internals and to make this extra allocation unnecessary.
122        let added_assets = BTreeMap::from_iter(
123            self.added_assets().map(|asset| (asset.id(), asset.to_value_word())),
124        );
125        let removed_assets = BTreeMap::from_iter(
126            self.removed_assets().map(|asset| (asset.id(), asset.to_value_word())),
127        );
128
129        Self::add_asset_section(AssetDeltaOperation::Add, added_assets, elements);
130        Self::add_asset_section(AssetDeltaOperation::Remove, removed_assets, elements);
131    }
132
133    fn add_asset_section(
134        delta_op: AssetDeltaOperation,
135        assets: BTreeMap<AssetId, Word>,
136        elements: &mut Vec<Felt>,
137    ) {
138        let num_changed_assets = assets.len();
139        for (asset_id, asset_value) in assets {
140            elements.extend_from_slice(asset_id.to_word().as_elements());
141            elements.extend_from_slice(asset_value.as_elements());
142        }
143
144        if num_changed_assets != 0 {
145            let num_changed_assets = Felt::try_from(num_changed_assets as u64)
146                .expect("number of changed assets should not exceed max representable felt");
147
148            elements.extend_from_slice(&[
149                Self::DOMAIN,
150                Felt::from(delta_op.as_u8()),
151                num_changed_assets,
152                Felt::ZERO,
153            ]);
154            elements.extend_from_slice(Word::empty().as_elements());
155        }
156    }
157}
158
159#[cfg(any(feature = "testing", test))]
160impl AccountVaultDelta {
161    /// Creates an [AccountVaultDelta] from the given iterators.
162    pub fn from_iters(
163        added_assets: impl IntoIterator<Item = crate::asset::Asset>,
164        removed_assets: impl IntoIterator<Item = crate::asset::Asset>,
165    ) -> Self {
166        let mut fungible = FungibleAssetDelta::default();
167        let mut non_fungible = NonFungibleAssetDelta::default();
168
169        for asset in added_assets {
170            match asset {
171                Asset::Fungible(asset) => {
172                    fungible.add(asset).unwrap();
173                },
174                Asset::NonFungible(asset) => {
175                    non_fungible.add(asset).unwrap();
176                },
177            }
178        }
179
180        for asset in removed_assets {
181            match asset {
182                Asset::Fungible(asset) => {
183                    fungible.remove(asset).unwrap();
184                },
185                Asset::NonFungible(asset) => {
186                    non_fungible.remove(asset).unwrap();
187                },
188            }
189        }
190
191        Self { fungible, non_fungible }
192    }
193}
194
195impl Serializable for AccountVaultDelta {
196    fn write_into<W: ByteWriter>(&self, target: &mut W) {
197        target.write(&self.fungible);
198        target.write(&self.non_fungible);
199    }
200
201    fn get_size_hint(&self) -> usize {
202        self.fungible.get_size_hint() + self.non_fungible.get_size_hint()
203    }
204}
205
206impl Deserializable for AccountVaultDelta {
207    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
208        let fungible = source.read()?;
209        let non_fungible = source.read()?;
210
211        Ok(Self::new(fungible, non_fungible))
212    }
213}
214
215// FUNGIBLE ASSET DELTA
216// ================================================================================================
217
218/// A binary tree map of fungible asset balance changes in the account vault.
219///
220/// The [`AssetId`] orders the assets in the same way as the in-kernel account delta which
221/// uses a link map.
222#[derive(Clone, Debug, Default, PartialEq, Eq)]
223pub struct FungibleAssetDelta(BTreeMap<AssetId, i64>);
224
225impl FungibleAssetDelta {
226    /// Validates and creates a new fungible asset delta.
227    ///
228    /// # Errors
229    /// Returns an error if the delta does not pass the validation.
230    pub fn new(map: BTreeMap<AssetId, i64>) -> Result<Self, AccountDeltaError> {
231        Self::validate(&map)?;
232
233        Ok(Self(map))
234    }
235
236    /// Adds a new fungible asset to the delta.
237    ///
238    /// # Errors
239    /// Returns an error if the delta would overflow.
240    pub fn add(&mut self, asset: FungibleAsset) -> Result<(), AccountDeltaError> {
241        let amount: i64 = asset.amount().as_i64();
242        self.add_delta(asset.id(), amount)
243    }
244
245    /// Removes a fungible asset from the delta.
246    ///
247    /// # Errors
248    /// Returns an error if the delta would overflow.
249    pub fn remove(&mut self, asset: FungibleAsset) -> Result<(), AccountDeltaError> {
250        let amount: i64 = asset.amount().as_i64();
251        self.add_delta(asset.id(), -amount)
252    }
253
254    /// Returns the amount of the fungible asset with the given asset ID.
255    pub fn amount(&self, asset_id: &AssetId) -> Option<i64> {
256        self.0.get(asset_id).copied()
257    }
258
259    /// Returns the number of fungible assets affected in the delta.
260    pub fn num_assets(&self) -> usize {
261        self.0.len()
262    }
263
264    /// Returns true if this vault delta contains no updates.
265    pub fn is_empty(&self) -> bool {
266        self.0.is_empty()
267    }
268
269    /// Returns an iterator over the (key, value) pairs of the map.
270    pub fn iter(&self) -> impl Iterator<Item = (&AssetId, &i64)> {
271        self.0.iter()
272    }
273
274    // HELPER FUNCTIONS
275    // ---------------------------------------------------------------------------------------------
276
277    /// Updates the provided map with the provided key and amount. If the final amount is 0,
278    /// the entry is removed.
279    ///
280    /// # Errors
281    /// Returns an error if the delta would overflow.
282    fn add_delta(&mut self, asset_id: AssetId, delta: i64) -> Result<(), AccountDeltaError> {
283        match self.0.entry(asset_id) {
284            Entry::Vacant(entry) => {
285                // Only track non-zero amounts.
286                if delta != 0 {
287                    entry.insert(delta);
288                }
289            },
290            Entry::Occupied(mut entry) => {
291                let old = *entry.get();
292                let new = old.checked_add(delta).ok_or(
293                    AccountDeltaError::FungibleAssetDeltaOverflow {
294                        faucet_id: asset_id.faucet_id(),
295                        current: old,
296                        delta,
297                    },
298                )?;
299
300                if new == 0 {
301                    entry.remove();
302                } else {
303                    *entry.get_mut() = new;
304                }
305            },
306        }
307
308        Ok(())
309    }
310
311    /// Checks whether this vault delta is valid.
312    ///
313    /// # Errors
314    /// Returns an error if one or more fungible assets' faucet IDs are invalid.
315    fn validate(map: &BTreeMap<AssetId, i64>) -> Result<(), AccountDeltaError> {
316        for asset_id in map.keys() {
317            if !asset_id.composition().is_fungible() {
318                return Err(AccountDeltaError::NotAFungibleFaucetId(asset_id.faucet_id()));
319            }
320        }
321
322        Ok(())
323    }
324}
325
326impl Serializable for FungibleAssetDelta {
327    fn write_into<W: ByteWriter>(&self, target: &mut W) {
328        target.write_usize(self.0.len());
329        // TODO: We save `i64` as `u64` since winter utils only supports unsigned integers for now.
330        //   We should update this code (and deserialization as well) once it supports signed
331        //   integers.
332        target.write_many(self.0.iter().map(|(asset_id, &delta)| (*asset_id, delta as u64)));
333    }
334
335    fn get_size_hint(&self) -> usize {
336        let entries_size: usize = self
337            .0
338            .keys()
339            .map(|id| {
340                // amount is serialized as a u64
341                id.get_size_hint() + core::mem::size_of::<u64>()
342            })
343            .sum();
344
345        self.0.len().get_size_hint() + entries_size
346    }
347}
348
349impl Deserializable for FungibleAssetDelta {
350    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
351        let num_fungible_assets = source.read_usize()?;
352        // TODO: We save `i64` as `u64` since winter utils only supports unsigned integers for now.
353        //   We should update this code (and serialization as well) once it supports signed
354        //   integers.
355        let map = source
356            .read_many_iter::<(AssetId, u64)>(num_fungible_assets)?
357            .map(|result| result.map(|(asset_id, delta_as_u64)| (asset_id, delta_as_u64 as i64)))
358            .collect::<Result<_, _>>()?;
359
360        Self::new(map).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
361    }
362}
363
364// NON-FUNGIBLE ASSET DELTA
365// ================================================================================================
366
367/// A binary tree map of non-fungible asset changes (addition and removal) in the account vault.
368///
369/// The [`AssetId`] orders the assets in the same way as the in-kernel account delta which
370/// uses a link map.
371#[derive(Clone, Debug, Default, PartialEq, Eq)]
372pub struct NonFungibleAssetDelta(BTreeMap<AssetId, (NonFungibleAsset, NonFungibleDeltaAction)>);
373
374impl NonFungibleAssetDelta {
375    /// Creates a new non-fungible asset delta.
376    pub const fn new(map: BTreeMap<AssetId, (NonFungibleAsset, NonFungibleDeltaAction)>) -> Self {
377        Self(map)
378    }
379
380    /// Adds a new non-fungible asset to the delta.
381    ///
382    /// # Errors
383    /// Returns an error if the delta already contains the asset addition.
384    pub fn add(&mut self, asset: NonFungibleAsset) -> Result<(), AccountDeltaError> {
385        self.apply_action(asset, NonFungibleDeltaAction::Add)
386    }
387
388    /// Removes a non-fungible asset from the delta.
389    ///
390    /// # Errors
391    /// Returns an error if the delta already contains the asset removal.
392    pub fn remove(&mut self, asset: NonFungibleAsset) -> Result<(), AccountDeltaError> {
393        self.apply_action(asset, NonFungibleDeltaAction::Remove)
394    }
395
396    /// Returns the number of non-fungible assets affected in the delta.
397    pub fn num_assets(&self) -> usize {
398        self.0.len()
399    }
400
401    /// Returns true if this vault delta contains no updates.
402    pub fn is_empty(&self) -> bool {
403        self.0.is_empty()
404    }
405
406    /// Returns an iterator over the (key, value) pairs of the map.
407    pub fn iter(&self) -> impl Iterator<Item = (&NonFungibleAsset, &NonFungibleDeltaAction)> {
408        self.0
409            .iter()
410            .map(|(_key, (non_fungible_asset, delta_action))| (non_fungible_asset, delta_action))
411    }
412
413    // HELPER FUNCTIONS
414    // ---------------------------------------------------------------------------------------------
415
416    /// Updates the provided map with the provided key and action.
417    /// If the action is the opposite to the previous one, the entry is removed.
418    ///
419    /// # Errors
420    /// Returns an error if the delta already contains the provided key and action.
421    fn apply_action(
422        &mut self,
423        asset: NonFungibleAsset,
424        action: NonFungibleDeltaAction,
425    ) -> Result<(), AccountDeltaError> {
426        match self.0.entry(asset.id()) {
427            Entry::Vacant(entry) => {
428                entry.insert((asset, action));
429            },
430            Entry::Occupied(entry) => {
431                let (_prev_asset, previous_action) = *entry.get();
432                if previous_action == action {
433                    // Asset cannot be added nor removed twice.
434                    return Err(AccountDeltaError::DuplicateNonFungibleVaultUpdate(asset));
435                }
436                // Otherwise they cancel out.
437                entry.remove();
438            },
439        }
440
441        Ok(())
442    }
443
444    /// Returns an iterator over all keys that have the provided action.
445    fn filter_by_action(
446        &self,
447        action: NonFungibleDeltaAction,
448    ) -> impl Iterator<Item = NonFungibleAsset> + '_ {
449        self.0
450            .iter()
451            .filter(move |&(_, (_asset, cur_action))| cur_action == &action)
452            .map(|(_key, (asset, _action))| *asset)
453    }
454}
455
456impl Serializable for NonFungibleAssetDelta {
457    fn write_into<W: ByteWriter>(&self, target: &mut W) {
458        let added: Vec<_> = self.filter_by_action(NonFungibleDeltaAction::Add).collect();
459        let removed: Vec<_> = self.filter_by_action(NonFungibleDeltaAction::Remove).collect();
460
461        target.write_usize(added.len());
462        target.write_many(added.iter());
463
464        target.write_usize(removed.len());
465        target.write_many(removed.iter());
466    }
467
468    fn get_size_hint(&self) -> usize {
469        let added = self.filter_by_action(NonFungibleDeltaAction::Add).count();
470        let removed = self.filter_by_action(NonFungibleDeltaAction::Remove).count();
471
472        added.get_size_hint()
473            + removed.get_size_hint()
474            + added * NonFungibleAsset::SERIALIZED_SIZE
475            + removed * NonFungibleAsset::SERIALIZED_SIZE
476    }
477}
478
479impl Deserializable for NonFungibleAssetDelta {
480    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
481        let mut map = BTreeMap::new();
482
483        let num_added = source.read_usize()?;
484        for _ in 0..num_added {
485            let added_asset: NonFungibleAsset = source.read()?;
486            map.insert(added_asset.id(), (added_asset, NonFungibleDeltaAction::Add));
487        }
488
489        let num_removed = source.read_usize()?;
490        for _ in 0..num_removed {
491            let removed_asset: NonFungibleAsset = source.read()?;
492            map.insert(removed_asset.id(), (removed_asset, NonFungibleDeltaAction::Remove));
493        }
494
495        Ok(Self::new(map))
496    }
497}
498
499#[derive(Clone, Copy, Debug, PartialEq, Eq)]
500pub enum NonFungibleDeltaAction {
501    Add,
502    Remove,
503}
504
505// TESTS
506// ================================================================================================
507
508#[cfg(test)]
509mod tests {
510    use super::{AccountVaultDelta, Deserializable, Serializable};
511    use crate::account::AccountId;
512    use crate::asset::{Asset, FungibleAsset, NonFungibleAsset};
513    use crate::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET;
514
515    #[test]
516    fn test_serde_account_vault() {
517        let asset_0 = FungibleAsset::mock(100);
518        let asset_1 = NonFungibleAsset::mock(&[10, 21, 32, 43]);
519        let delta = AccountVaultDelta::from_iters([asset_0], [asset_1]);
520
521        let serialized = delta.to_bytes();
522        let deserialized = AccountVaultDelta::read_from_bytes(&serialized).unwrap();
523        assert_eq!(deserialized, delta);
524    }
525
526    #[test]
527    fn test_is_empty_account_vault() {
528        let faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
529        let asset: Asset = FungibleAsset::new(faucet, 123).unwrap().into();
530
531        assert!(AccountVaultDelta::default().is_empty());
532        assert!(!AccountVaultDelta::from_iters([asset], []).is_empty());
533        assert!(!AccountVaultDelta::from_iters([], [asset]).is_empty());
534    }
535}