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        // TODO: If we keep this code, optimize by not serializing asset class (which is always 0).
333        target.write_many(self.0.iter().map(|(asset_id, &delta)| (*asset_id, delta as u64)));
334    }
335
336    fn get_size_hint(&self) -> usize {
337        const ENTRY_SIZE: usize = AssetId::SERIALIZED_SIZE + core::mem::size_of::<u64>();
338        self.0.len().get_size_hint() + self.0.len() * ENTRY_SIZE
339    }
340}
341
342impl Deserializable for FungibleAssetDelta {
343    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
344        let num_fungible_assets = source.read_usize()?;
345        // TODO: We save `i64` as `u64` since winter utils only supports unsigned integers for now.
346        //   We should update this code (and serialization as well) once it supports signed
347        //   integers.
348        let map = source
349            .read_many_iter::<(AssetId, u64)>(num_fungible_assets)?
350            .map(|result| result.map(|(asset_id, delta_as_u64)| (asset_id, delta_as_u64 as i64)))
351            .collect::<Result<_, _>>()?;
352
353        Self::new(map).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
354    }
355}
356
357// NON-FUNGIBLE ASSET DELTA
358// ================================================================================================
359
360/// A binary tree map of non-fungible asset changes (addition and removal) in the account vault.
361///
362/// The [`AssetId`] orders the assets in the same way as the in-kernel account delta which
363/// uses a link map.
364#[derive(Clone, Debug, Default, PartialEq, Eq)]
365pub struct NonFungibleAssetDelta(BTreeMap<AssetId, (NonFungibleAsset, NonFungibleDeltaAction)>);
366
367impl NonFungibleAssetDelta {
368    /// Creates a new non-fungible asset delta.
369    pub const fn new(map: BTreeMap<AssetId, (NonFungibleAsset, NonFungibleDeltaAction)>) -> Self {
370        Self(map)
371    }
372
373    /// Adds a new non-fungible asset to the delta.
374    ///
375    /// # Errors
376    /// Returns an error if the delta already contains the asset addition.
377    pub fn add(&mut self, asset: NonFungibleAsset) -> Result<(), AccountDeltaError> {
378        self.apply_action(asset, NonFungibleDeltaAction::Add)
379    }
380
381    /// Removes a non-fungible asset from the delta.
382    ///
383    /// # Errors
384    /// Returns an error if the delta already contains the asset removal.
385    pub fn remove(&mut self, asset: NonFungibleAsset) -> Result<(), AccountDeltaError> {
386        self.apply_action(asset, NonFungibleDeltaAction::Remove)
387    }
388
389    /// Returns the number of non-fungible assets affected in the delta.
390    pub fn num_assets(&self) -> usize {
391        self.0.len()
392    }
393
394    /// Returns true if this vault delta contains no updates.
395    pub fn is_empty(&self) -> bool {
396        self.0.is_empty()
397    }
398
399    /// Returns an iterator over the (key, value) pairs of the map.
400    pub fn iter(&self) -> impl Iterator<Item = (&NonFungibleAsset, &NonFungibleDeltaAction)> {
401        self.0
402            .iter()
403            .map(|(_key, (non_fungible_asset, delta_action))| (non_fungible_asset, delta_action))
404    }
405
406    // HELPER FUNCTIONS
407    // ---------------------------------------------------------------------------------------------
408
409    /// Updates the provided map with the provided key and action.
410    /// If the action is the opposite to the previous one, the entry is removed.
411    ///
412    /// # Errors
413    /// Returns an error if the delta already contains the provided key and action.
414    fn apply_action(
415        &mut self,
416        asset: NonFungibleAsset,
417        action: NonFungibleDeltaAction,
418    ) -> Result<(), AccountDeltaError> {
419        match self.0.entry(asset.id()) {
420            Entry::Vacant(entry) => {
421                entry.insert((asset, action));
422            },
423            Entry::Occupied(entry) => {
424                let (_prev_asset, previous_action) = *entry.get();
425                if previous_action == action {
426                    // Asset cannot be added nor removed twice.
427                    return Err(AccountDeltaError::DuplicateNonFungibleVaultUpdate(asset));
428                }
429                // Otherwise they cancel out.
430                entry.remove();
431            },
432        }
433
434        Ok(())
435    }
436
437    /// Returns an iterator over all keys that have the provided action.
438    fn filter_by_action(
439        &self,
440        action: NonFungibleDeltaAction,
441    ) -> impl Iterator<Item = NonFungibleAsset> + '_ {
442        self.0
443            .iter()
444            .filter(move |&(_, (_asset, cur_action))| cur_action == &action)
445            .map(|(_key, (asset, _action))| *asset)
446    }
447}
448
449impl Serializable for NonFungibleAssetDelta {
450    fn write_into<W: ByteWriter>(&self, target: &mut W) {
451        let added: Vec<_> = self.filter_by_action(NonFungibleDeltaAction::Add).collect();
452        let removed: Vec<_> = self.filter_by_action(NonFungibleDeltaAction::Remove).collect();
453
454        target.write_usize(added.len());
455        target.write_many(added.iter());
456
457        target.write_usize(removed.len());
458        target.write_many(removed.iter());
459    }
460
461    fn get_size_hint(&self) -> usize {
462        let added = self.filter_by_action(NonFungibleDeltaAction::Add).count();
463        let removed = self.filter_by_action(NonFungibleDeltaAction::Remove).count();
464
465        added.get_size_hint()
466            + removed.get_size_hint()
467            + added * NonFungibleAsset::SERIALIZED_SIZE
468            + removed * NonFungibleAsset::SERIALIZED_SIZE
469    }
470}
471
472impl Deserializable for NonFungibleAssetDelta {
473    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
474        let mut map = BTreeMap::new();
475
476        let num_added = source.read_usize()?;
477        for _ in 0..num_added {
478            let added_asset: NonFungibleAsset = source.read()?;
479            map.insert(added_asset.id(), (added_asset, NonFungibleDeltaAction::Add));
480        }
481
482        let num_removed = source.read_usize()?;
483        for _ in 0..num_removed {
484            let removed_asset: NonFungibleAsset = source.read()?;
485            map.insert(removed_asset.id(), (removed_asset, NonFungibleDeltaAction::Remove));
486        }
487
488        Ok(Self::new(map))
489    }
490}
491
492#[derive(Clone, Copy, Debug, PartialEq, Eq)]
493pub enum NonFungibleDeltaAction {
494    Add,
495    Remove,
496}
497
498// TESTS
499// ================================================================================================
500
501#[cfg(test)]
502mod tests {
503    use super::{AccountVaultDelta, Deserializable, Serializable};
504    use crate::account::AccountId;
505    use crate::asset::{Asset, FungibleAsset, NonFungibleAsset};
506    use crate::testing::account_id::ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET;
507
508    #[test]
509    fn test_serde_account_vault() {
510        let asset_0 = FungibleAsset::mock(100);
511        let asset_1 = NonFungibleAsset::mock(&[10, 21, 32, 43]);
512        let delta = AccountVaultDelta::from_iters([asset_0], [asset_1]);
513
514        let serialized = delta.to_bytes();
515        let deserialized = AccountVaultDelta::read_from_bytes(&serialized).unwrap();
516        assert_eq!(deserialized, delta);
517    }
518
519    #[test]
520    fn test_is_empty_account_vault() {
521        let faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
522        let asset: Asset = FungibleAsset::new(faucet, 123).unwrap().into();
523
524        assert!(AccountVaultDelta::default().is_empty());
525        assert!(!AccountVaultDelta::from_iters([asset], []).is_empty());
526        assert!(!AccountVaultDelta::from_iters([], [asset]).is_empty());
527    }
528}