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