Skip to main content

miden_protocol/account/patch/
vault.rs

1use alloc::collections::BTreeMap;
2use alloc::string::ToString;
3use alloc::vec::Vec;
4
5use crate::asset::{Asset, AssetId};
6use crate::errors::AssetError;
7use crate::utils::serde::{
8    ByteReader,
9    ByteWriter,
10    Deserializable,
11    DeserializationError,
12    Serializable,
13};
14use crate::{Felt, Word};
15
16/// Describes the updates to an [`AssetVault`](crate::account::AssetVault) after a transaction.
17///
18/// The patch entries map an [`AssetId`] to the final [`Word`] value of the asset after the
19/// update. If the asset was removed, the value is [`Word::empty`].
20#[derive(Debug, Clone, Default, PartialEq, Eq)]
21pub struct AccountVaultPatch {
22    entries: BTreeMap<AssetId, Word>,
23}
24
25impl AccountVaultPatch {
26    /// Domain separator for assets in the account patch commitment.
27    const DOMAIN: Felt = Felt::new_unchecked(3);
28
29    /// Creates a new vault patch directly from its raw key/value entries.
30    ///
31    /// # Errors
32    ///
33    /// Returns an error if the provided entries are not valid assets, unless the value is
34    /// [`Word::empty`].
35    pub fn new(entries: BTreeMap<AssetId, Word>) -> Result<Self, AssetError> {
36        for (key, value) in entries.iter() {
37            // If the asset was not removed (final value != Word::empty), ensure the provided entry
38            // is a valid asset.
39            if !value.is_empty() {
40                Asset::from_id_and_value(*key, *value)?;
41            }
42        }
43
44        Ok(Self { entries })
45    }
46
47    /// Inserts an asset into the patch, overwriting the previous value.
48    pub fn insert_asset(&mut self, asset: Asset) {
49        self.entries.insert(asset.id(), asset.to_value_word());
50    }
51
52    /// Marks an asset as removed by inserting [`Word::empty`] into the patch.
53    pub fn remove_asset(&mut self, asset_id: AssetId) {
54        self.entries.insert(asset_id, Word::empty());
55    }
56
57    /// Returns the number of assets being patched.
58    pub fn num_assets(&self) -> usize {
59        self.entries.len()
60    }
61
62    /// Returns a reference to the underlying map of the vault patch.
63    pub fn as_map(&self) -> &BTreeMap<AssetId, Word> {
64        &self.entries
65    }
66
67    /// Consumes self and returns the underlying map of the vault patch.
68    pub fn into_map(self) -> BTreeMap<AssetId, Word> {
69        self.entries
70    }
71
72    /// Returns an iterator over the assets contained in this patch, sorted by ID.
73    pub fn iter(&self) -> impl Iterator<Item = (&AssetId, &Word)> {
74        self.entries.iter()
75    }
76
77    /// Returns `true` if this vault patch contains no entries.
78    pub fn is_empty(&self) -> bool {
79        self.entries.is_empty()
80    }
81
82    /// Merges another vault patch into this one. Entries from `other` overwrite any existing
83    /// entries in `self` for the same [`AssetId`].
84    pub fn merge(&mut self, other: Self) {
85        self.entries.extend(other.entries);
86    }
87
88    /// Appends the vault patch to the given `elements` from which the patch commitment will be
89    /// computed.
90    pub(super) fn append_patch_elements(&self, elements: &mut Vec<Felt>) {
91        for (asset_id, asset_value_or_empty_word) in self.entries.iter() {
92            elements.extend_from_slice(asset_id.to_word().as_elements());
93            elements.extend_from_slice(asset_value_or_empty_word.as_elements());
94        }
95
96        let num_changed_assets = self.entries.len();
97        if num_changed_assets != 0 {
98            let num_changed_assets = Felt::try_from(num_changed_assets as u64)
99                .expect("number of assets should not exceed max representable felt");
100
101            elements.extend_from_slice(&[Self::DOMAIN, num_changed_assets, Felt::ZERO, Felt::ZERO]);
102            elements.extend_from_slice(Word::empty().as_elements());
103        }
104    }
105
106    /// Returns an iterator over the keys of assets that were removed (i.e. whose value is
107    /// [`Word::empty`]).
108    pub fn removed_asset_ids(&self) -> impl Iterator<Item = &AssetId> {
109        self.entries
110            .iter()
111            .filter(|(_key, value)| value.is_empty())
112            .map(|(key, _value)| key)
113    }
114
115    /// Returns an iterator over the assets that were added or updated (i.e. whose value is not
116    /// [`Word::empty`]).
117    pub fn updated_assets(&self) -> impl Iterator<Item = Asset> {
118        self.entries
119            .iter()
120            .filter(|(_key, value)| !value.is_empty())
121            .map(|(key, value)| {
122                Asset::from_id_and_value(*key, *value).expect("patch should track valid assets")
123            })
124    }
125}
126
127impl Serializable for AccountVaultPatch {
128    fn write_into<W: ByteWriter>(&self, target: &mut W) {
129        target.write_usize(self.removed_asset_ids().count());
130        target.write_many(self.removed_asset_ids());
131
132        target.write_usize(self.updated_assets().count());
133        target.write_many(self.updated_assets());
134    }
135
136    fn get_size_hint(&self) -> usize {
137        let removed_size = AssetId::SERIALIZED_SIZE * self.removed_asset_ids().count();
138        let updated_size: usize = self.updated_assets().map(|asset| asset.get_size_hint()).sum();
139
140        2 * 0usize.get_size_hint() + removed_size + updated_size
141    }
142}
143
144impl Deserializable for AccountVaultPatch {
145    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
146        let num_removed_assets = source.read_usize()?;
147        let mut entries: BTreeMap<AssetId, Word> = source
148            .read_many_iter::<AssetId>(num_removed_assets)?
149            .map(|result| result.map(|id| (id, Word::empty())))
150            .collect::<Result<_, _>>()?;
151
152        let num_added_assets = source.read_usize()?;
153        for result in source.read_many_iter::<Asset>(num_added_assets)? {
154            let asset = result?;
155            entries.insert(asset.id(), asset.to_value_word());
156        }
157
158        Self::new(entries).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use crate::asset::{FungibleAsset, NonFungibleAsset};
166    use crate::testing::account_id::ACCOUNT_ID_PRIVATE_SENDER;
167
168    #[test]
169    fn account_vault_patch_serde() -> anyhow::Result<()> {
170        let empty_patch = AccountVaultPatch::default();
171        let serialized = empty_patch.to_bytes();
172        let deserialized = AccountVaultPatch::read_from_bytes(&serialized)?;
173        assert_eq!(empty_patch, deserialized);
174        assert_eq!(empty_patch.get_size_hint(), serialized.len());
175
176        let asset_0: Asset = FungibleAsset::mock(100);
177        let asset_1: Asset =
178            FungibleAsset::new(ACCOUNT_ID_PRIVATE_SENDER.try_into()?, 500_000)?.into();
179        let asset_2: Asset = NonFungibleAsset::mock(&[10]);
180        let asset_3: Asset = NonFungibleAsset::mock(&[20]);
181        let patch = AccountVaultPatch::from_iters([asset_0, asset_1, asset_2], [asset_3]);
182
183        let serialized = patch.to_bytes();
184        let deserialized = AccountVaultPatch::read_from_bytes(&serialized)?;
185        assert_eq!(deserialized, patch);
186        assert_eq!(patch.get_size_hint(), serialized.len());
187
188        Ok(())
189    }
190}