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 super::{
7    AccountDeltaError,
8    ByteReader,
9    ByteWriter,
10    Deserializable,
11    DeserializationError,
12    Serializable,
13};
14use crate::account::delta::AssetDeltaOperation;
15use crate::asset::{Asset, AssetId};
16use crate::{Felt, Word};
17
18// ASSET DELTA
19// ================================================================================================
20
21/// The change of a single asset in an [`AccountVaultDelta`].
22///
23/// The asset is the magnitude of the change while the operation gives its direction.
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub struct AssetDelta {
26    delta_op: AssetDeltaOperation,
27    asset: Asset,
28}
29
30impl AssetDelta {
31    /// Creates a new [`AssetDelta`] by which the vault changed under the given operation.
32    pub fn new(delta_op: AssetDeltaOperation, asset: Asset) -> Self {
33        Self { delta_op, asset }
34    }
35
36    /// Returns the operation of this delta.
37    pub fn delta_op(&self) -> AssetDeltaOperation {
38        self.delta_op
39    }
40
41    /// Returns the asset by which the vault changed.
42    pub fn asset(&self) -> Asset {
43        self.asset
44    }
45
46    /// Returns the ID of the asset by which the vault changed.
47    pub fn asset_id(&self) -> AssetId {
48        self.asset.id()
49    }
50}
51
52// ACCOUNT VAULT DELTA
53// ================================================================================================
54
55/// [`AccountVaultDelta`] stores the difference between the initial and final account vault states.
56///
57/// The difference is represented as a map of [`AssetDelta`]s keyed by the ID of the asset they
58/// change. The [`AssetId`] orders the assets in the same way as the in-kernel account delta.
59///
60/// ## Purpose
61///
62/// The purpose of a vault delta is to represent the changes to the vault that a transaction results
63/// in and provide a way to commit to and sign these changes. Unlike an
64/// [`AccountVaultPatch`](crate::account::AccountVaultPatch), a delta cannot be applied to an
65/// account and multiple deltas cannot be merged, since that isn't necessary for signing.
66///
67/// ## Limitations
68///
69/// The delta does not include the functionality to merge or split assets. This would mainly be
70/// needed to merge deltas, which isn't supported. Additionally, once custom assets are supported,
71/// their merge and split logic will be defined in the issuing faucet, and the delta would not be
72/// able to (easily) invoke this logic.
73#[derive(Clone, Debug, Default, PartialEq, Eq)]
74pub struct AccountVaultDelta {
75    delta: BTreeMap<AssetId, AssetDelta>,
76}
77
78impl AccountVaultDelta {
79    /// Domain separator for assets in delta and patch commitments.
80    pub(in crate::account) const DOMAIN: Felt = Felt::new_unchecked(1);
81
82    /// Maximum number of added or removed assets in a vault delta.
83    pub const MAX_ASSETS_PER_DELTA_OP: u16 = 1024;
84
85    /// Validates and creates an [`AccountVaultDelta`] from the given asset deltas.
86    ///
87    /// # Errors
88    ///
89    /// Returns an error if:
90    /// - the same asset is changed by more than one delta.
91    /// - the number of added or removed assets exceeds [`Self::MAX_ASSETS_PER_DELTA_OP`].
92    pub fn new(
93        asset_deltas: impl IntoIterator<Item = AssetDelta>,
94    ) -> Result<Self, AccountDeltaError> {
95        let mut delta = BTreeMap::new();
96        let mut num_added_assets = 0usize;
97        let mut num_removed_assets = 0usize;
98
99        for asset_delta in asset_deltas {
100            match asset_delta.delta_op() {
101                AssetDeltaOperation::Add => num_added_assets += 1,
102                AssetDeltaOperation::Remove => num_removed_assets += 1,
103            }
104
105            match delta.entry(asset_delta.asset_id()) {
106                Entry::Vacant(entry) => {
107                    entry.insert(asset_delta);
108                },
109                Entry::Occupied(_) => {
110                    return Err(AccountDeltaError::DuplicateAssetDelta(asset_delta.asset_id()));
111                },
112            }
113        }
114
115        Self::validate_asset_count(AssetDeltaOperation::Add, num_added_assets)?;
116        Self::validate_asset_count(AssetDeltaOperation::Remove, num_removed_assets)?;
117
118        Ok(Self { delta })
119    }
120
121    /// Returns true if this vault delta contains no updates.
122    pub fn is_empty(&self) -> bool {
123        self.delta.is_empty()
124    }
125
126    /// Returns the number of assets changed in this delta.
127    pub fn num_assets(&self) -> usize {
128        self.delta.len()
129    }
130
131    /// Returns an iterator over the asset deltas, sorted by asset ID.
132    pub fn iter(&self) -> impl Iterator<Item = &AssetDelta> {
133        self.delta.values()
134    }
135
136    /// Returns an iterator over the added assets in this delta.
137    pub fn added_assets(&self) -> impl Iterator<Item = Asset> + '_ {
138        self.filter_by_op(AssetDeltaOperation::Add)
139    }
140
141    /// Returns an iterator over the removed assets in this delta.
142    pub fn removed_assets(&self) -> impl Iterator<Item = Asset> + '_ {
143        self.filter_by_op(AssetDeltaOperation::Remove)
144    }
145
146    /// Appends the vault delta to the given `elements` from which the delta commitment will be
147    /// computed.
148    pub(super) fn append_delta_elements(&self, elements: &mut Vec<Felt>) {
149        self.append_asset_section(AssetDeltaOperation::Add, elements);
150        self.append_asset_section(AssetDeltaOperation::Remove, elements);
151    }
152
153    // HELPER FUNCTIONS
154    // ---------------------------------------------------------------------------------------------
155
156    /// Returns the number of assets changed by the given operation.
157    ///
158    /// The count fits in a `u16` since it is bounded by [`Self::MAX_ASSETS_PER_DELTA_OP`].
159    fn num_assets_by_op(&self, delta_op: AssetDeltaOperation) -> u16 {
160        let num_assets = self.filter_by_op(delta_op).count();
161        u16::try_from(num_assets).expect("number of changed assets is validated on construction")
162    }
163
164    /// Counts the number of added assets.
165    fn num_added_assets(&self) -> u16 {
166        self.num_assets_by_op(AssetDeltaOperation::Add)
167    }
168
169    /// Counts the number of removed assets.
170    fn num_removed_assets(&self) -> u16 {
171        self.num_assets_by_op(AssetDeltaOperation::Remove)
172    }
173
174    /// Returns an error if the given number of assets exceeds
175    /// [`Self::MAX_ASSETS_PER_DELTA_OP`].
176    fn validate_asset_count(
177        delta_op: AssetDeltaOperation,
178        num_ops: usize,
179    ) -> Result<(), AccountDeltaError> {
180        if num_ops > usize::from(Self::MAX_ASSETS_PER_DELTA_OP) {
181            return Err(AccountDeltaError::TooManyVaultAssetDeltas { delta_op, num_ops });
182        }
183
184        Ok(())
185    }
186
187    /// Returns an iterator over all assets that were changed by the provided operation.
188    fn filter_by_op(&self, delta_op: AssetDeltaOperation) -> impl Iterator<Item = Asset> + '_ {
189        self.delta
190            .values()
191            .filter(move |asset_delta| asset_delta.delta_op() == delta_op)
192            .map(AssetDelta::asset)
193    }
194
195    /// Appends the assets changed by the provided operation, followed by the section's trailer.
196    ///
197    /// The trailer is omitted if the operation did not change any asset.
198    fn append_asset_section(&self, delta_op: AssetDeltaOperation, elements: &mut Vec<Felt>) {
199        let mut num_changed_assets = 0;
200        for asset in self.filter_by_op(delta_op) {
201            elements.extend_from_slice(&asset.as_elements());
202            num_changed_assets += 1;
203        }
204
205        if num_changed_assets != 0 {
206            let num_changed_assets = Felt::try_from(num_changed_assets as u64)
207                .expect("number of changed assets should not exceed max representable felt");
208
209            elements.extend_from_slice(&[
210                Self::DOMAIN,
211                Felt::from(delta_op.as_u8()),
212                num_changed_assets,
213                Felt::ZERO,
214            ]);
215            elements.extend_from_slice(Word::empty().as_elements());
216        }
217    }
218}
219
220impl Serializable for AccountVaultDelta {
221    fn write_into<W: ByteWriter>(&self, target: &mut W) {
222        target.write(self.num_added_assets());
223        target.write_many(self.added_assets());
224
225        target.write(self.num_removed_assets());
226        target.write_many(self.removed_assets());
227    }
228
229    fn get_size_hint(&self) -> usize {
230        let added_size: usize = self.added_assets().map(|asset| asset.get_size_hint()).sum();
231        let removed_size: usize = self.removed_assets().map(|asset| asset.get_size_hint()).sum();
232
233        2 * 0u16.get_size_hint() + added_size + removed_size
234    }
235}
236
237impl Deserializable for AccountVaultDelta {
238    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
239        let num_added_assets: u16 = source.read()?;
240        if num_added_assets > Self::MAX_ASSETS_PER_DELTA_OP {
241            return Err(DeserializationError::InvalidValue(
242                AccountDeltaError::TooManyVaultAssetDeltas {
243                    delta_op: AssetDeltaOperation::Add,
244                    num_ops: usize::from(num_added_assets),
245                }
246                .to_string(),
247            ));
248        }
249
250        // The capacity is not reserved upfront since the number of assets is not yet validated
251        // against the remaining bytes at this point.
252        let mut asset_deltas = Vec::new();
253        for asset in source.read_many_iter::<Asset>(usize::from(num_added_assets))? {
254            asset_deltas.push(AssetDelta::new(AssetDeltaOperation::Add, asset?));
255        }
256
257        let num_removed_assets: u16 = source.read()?;
258        if num_removed_assets > Self::MAX_ASSETS_PER_DELTA_OP {
259            return Err(DeserializationError::InvalidValue(
260                AccountDeltaError::TooManyVaultAssetDeltas {
261                    delta_op: AssetDeltaOperation::Remove,
262                    num_ops: usize::from(num_removed_assets),
263                }
264                .to_string(),
265            ));
266        }
267        for asset in source.read_many_iter::<Asset>(usize::from(num_removed_assets))? {
268            asset_deltas.push(AssetDelta::new(AssetDeltaOperation::Remove, asset?));
269        }
270
271        Self::new(asset_deltas).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
272    }
273}
274
275// TESTS
276// ================================================================================================
277
278#[cfg(test)]
279mod tests {
280    use alloc::string::ToString;
281    use alloc::vec::Vec;
282
283    use assert_matches::assert_matches;
284    use rstest::rstest;
285
286    use super::{AccountVaultDelta, Deserializable, DeserializationError, Serializable};
287    use crate::account::delta::AssetDeltaOperation;
288    use crate::account::{AccountId, AssetDelta};
289    use crate::asset::{Asset, FungibleAsset, NonFungibleAsset};
290    use crate::errors::AccountDeltaError;
291    use crate::utils::serde::ByteWriter;
292
293    #[test]
294    fn account_vault_delta_serde() -> anyhow::Result<()> {
295        let empty_delta = AccountVaultDelta::default();
296        assert!(empty_delta.is_empty());
297        let serialized = empty_delta.to_bytes();
298        assert_eq!(AccountVaultDelta::read_from_bytes(&serialized)?, empty_delta);
299        assert_eq!(empty_delta.get_size_hint(), serialized.len());
300
301        let delta = AccountVaultDelta::from_iters(
302            [FungibleAsset::mock(100), NonFungibleAsset::mock(&[10, 21, 32, 43])],
303            [NonFungibleAsset::mock(&[54, 65])],
304        );
305        assert!(!delta.is_empty());
306
307        let serialized = delta.to_bytes();
308        assert_eq!(AccountVaultDelta::read_from_bytes(&serialized)?, delta);
309        assert_eq!(delta.get_size_hint(), serialized.len());
310
311        Ok(())
312    }
313
314    fn generate_asset_deltas(delta_op: AssetDeltaOperation, num_deltas: usize) -> Vec<AssetDelta> {
315        (0..num_deltas)
316            .map(|_| {
317                let asset =
318                    FungibleAsset::new(AccountId::builder().build_with_seed(rand::random()), 42)
319                        .unwrap();
320                AssetDelta::new(delta_op, Asset::from(asset))
321            })
322            .collect::<Vec<_>>()
323    }
324
325    #[rstest]
326    #[case::add(AssetDeltaOperation::Add)]
327    #[case::remove(AssetDeltaOperation::Remove)]
328    fn account_vault_delta_accepts_max_num_changed_assets(
329        #[case] expected_delta_op: AssetDeltaOperation,
330    ) -> anyhow::Result<()> {
331        let asset_deltas = generate_asset_deltas(
332            expected_delta_op,
333            usize::from(AccountVaultDelta::MAX_ASSETS_PER_DELTA_OP),
334        );
335
336        AccountVaultDelta::new(asset_deltas)?;
337
338        Ok(())
339    }
340
341    #[rstest]
342    #[case::add(AssetDeltaOperation::Add)]
343    #[case::remove(AssetDeltaOperation::Remove)]
344    fn account_vault_delta_rejects_more_than_max_num_changed_assets(
345        #[case] expected_delta_op: AssetDeltaOperation,
346    ) -> anyhow::Result<()> {
347        let expected_num_ops = usize::from(AccountVaultDelta::MAX_ASSETS_PER_DELTA_OP) + 1;
348        let asset_deltas = generate_asset_deltas(expected_delta_op, expected_num_ops);
349
350        let err = AccountVaultDelta::new(asset_deltas).unwrap_err();
351        assert_matches!(err, AccountDeltaError::TooManyVaultAssetDeltas { delta_op, num_ops } => {
352            assert_eq!(delta_op, expected_delta_op);
353            assert_eq!(num_ops, expected_num_ops);
354        });
355
356        Ok(())
357    }
358
359    /// The same asset must not be changed by two deltas, since the delta could not represent both.
360    #[test]
361    fn account_vault_delta_rejects_duplicate_asset() -> anyhow::Result<()> {
362        let asset = NonFungibleAsset::mock(&[10, 21, 32, 43]);
363        let asset_deltas = [
364            AssetDelta::new(AssetDeltaOperation::Add, asset),
365            AssetDelta::new(AssetDeltaOperation::Remove, asset),
366        ];
367
368        let err = AccountVaultDelta::new(asset_deltas).unwrap_err();
369        assert_matches!(err, AccountDeltaError::DuplicateAssetDelta(asset_id) => {
370            assert_eq!(asset_id, asset.id());
371        });
372
373        Ok(())
374    }
375
376    /// A crafted byte stream that changes the same asset in both the added and the removed section
377    /// must be rejected rather than silently collapsing into a single entry.
378    #[test]
379    fn account_vault_delta_deserialization_rejects_duplicate_asset() -> anyhow::Result<()> {
380        let asset = NonFungibleAsset::mock(&[10, 21, 32, 43]);
381
382        let mut bytes = Vec::new();
383        bytes.write(1u16);
384        bytes.write(asset);
385        bytes.write(1u16);
386        bytes.write(asset);
387
388        let error = AccountVaultDelta::read_from_bytes(&bytes)
389            .expect_err("delta with a duplicate asset should not deserialize");
390
391        let expected = AccountDeltaError::DuplicateAssetDelta(asset.id()).to_string();
392        assert_matches!(error, DeserializationError::InvalidValue(message) if message == expected);
393
394        Ok(())
395    }
396}