Skip to main content

miden_protocol/account/delta/
mod.rs

1use alloc::string::ToString;
2use alloc::vec::Vec;
3
4use crate::account::{
5    Account,
6    AccountCode,
7    AccountId,
8    AccountStorage,
9    StorageSlot,
10    StorageSlotType,
11};
12use crate::asset::AssetVault;
13use crate::crypto::SequentialCommit;
14use crate::errors::{AccountDeltaError, AccountError};
15use crate::utils::serde::{
16    ByteReader,
17    ByteWriter,
18    Deserializable,
19    DeserializationError,
20    Serializable,
21};
22use crate::{Felt, Word, ZERO};
23
24mod storage;
25pub use storage::{AccountStorageDelta, StorageMapDelta, StorageSlotDelta};
26
27mod vault;
28pub use vault::{
29    AccountVaultDelta,
30    FungibleAssetDelta,
31    NonFungibleAssetDelta,
32    NonFungibleDeltaAction,
33};
34
35// ACCOUNT DELTA
36// ================================================================================================
37
38/// The [`AccountDelta`] stores the differences between two account states, which can result from
39/// one or more transaction.
40///
41/// The differences are represented as follows:
42/// - storage: an [`AccountStorageDelta`] that contains the changes to the account storage.
43/// - vault: an [`AccountVaultDelta`] object that contains the changes to the account vault.
44/// - nonce: if the nonce of the account has changed, the _delta_ of the nonce is stored, i.e. the
45///   value by which the nonce increased.
46/// - code: an [`AccountCode`] for new accounts and `None` for others.
47///
48/// The presence of the code in a delta signals if the delta is a _full state_ or _partial state_
49/// delta. A full state delta must be converted into an [`Account`] object, while a partial state
50/// delta must be applied to an existing [`Account`].
51///
52/// TODO(code_upgrades): The ability to track account code updates is an outstanding feature. For
53/// that reason, the account code is not considered as part of the "nonce must be incremented if
54/// state changed" check.
55#[derive(Clone, Debug, PartialEq, Eq)]
56pub struct AccountDelta {
57    /// The ID of the account to which this delta applies. If the delta is created during
58    /// transaction execution, that is the native account of the transaction.
59    account_id: AccountId,
60    /// The delta of the account's storage.
61    storage: AccountStorageDelta,
62    /// The delta of the account's asset vault.
63    vault: AccountVaultDelta,
64    /// The code of a new account (`Some`) or `None` for existing accounts.
65    code: Option<AccountCode>,
66    /// The value by which the nonce was incremented. Must be greater than zero if storage or vault
67    /// are non-empty.
68    nonce_delta: Felt,
69}
70
71impl AccountDelta {
72    // CONSTRUCTOR
73    // --------------------------------------------------------------------------------------------
74
75    /// Returns new [AccountDelta] instantiated from the provided components.
76    ///
77    /// # Errors
78    ///
79    /// - Returns an error if storage or vault were updated, but the nonce_delta is 0.
80    pub fn new(
81        account_id: AccountId,
82        storage: AccountStorageDelta,
83        vault: AccountVaultDelta,
84        nonce_delta: Felt,
85    ) -> Result<Self, AccountDeltaError> {
86        // nonce must be updated if either account storage or vault were updated
87        validate_nonce(nonce_delta, &storage, &vault)?;
88
89        Ok(Self {
90            account_id,
91            storage,
92            vault,
93            code: None,
94            nonce_delta,
95        })
96    }
97
98    // PUBLIC MUTATORS
99    // --------------------------------------------------------------------------------------------
100
101    /// Merge another [AccountDelta] into this one.
102    pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> {
103        let new_nonce_delta = self.nonce_delta + other.nonce_delta;
104
105        if new_nonce_delta.as_canonical_u64() < self.nonce_delta.as_canonical_u64() {
106            return Err(AccountDeltaError::NonceIncrementOverflow {
107                current: self.nonce_delta,
108                increment: other.nonce_delta,
109                new: new_nonce_delta,
110            });
111        }
112
113        // TODO(code_upgrades): This should go away once we have proper account code updates in
114        // deltas. Then, the two code updates can be merged. For now, code cannot be merged
115        // and this should never happen.
116        if self.is_full_state() && other.is_full_state() {
117            return Err(AccountDeltaError::MergingFullStateDeltas);
118        }
119
120        if let Some(code) = other.code {
121            self.code = Some(code);
122        }
123
124        self.nonce_delta = new_nonce_delta;
125
126        self.storage.merge(other.storage)?;
127        self.vault.merge(other.vault)
128    }
129
130    /// Returns a mutable reference to the account vault delta.
131    pub fn vault_mut(&mut self) -> &mut AccountVaultDelta {
132        &mut self.vault
133    }
134
135    /// Sets the [`AccountCode`] of the delta.
136    pub fn with_code(mut self, code: Option<AccountCode>) -> Self {
137        self.code = code;
138        self
139    }
140
141    // PUBLIC ACCESSORS
142    // --------------------------------------------------------------------------------------------
143
144    /// Returns true if this account delta does not contain any vault, storage or nonce updates.
145    pub fn is_empty(&self) -> bool {
146        self.storage.is_empty() && self.vault.is_empty() && self.nonce_delta == ZERO
147    }
148
149    /// Returns `true` if this delta is a "full state" delta, `false` otherwise, i.e. if it is a
150    /// "partial state" delta.
151    ///
152    /// See the type-level docs for more on this distinction.
153    pub fn is_full_state(&self) -> bool {
154        // TODO(code_upgrades): Change this to another detection mechanism once we have code upgrade
155        // support, at which point the presence of code may not be enough of an indication
156        // that a delta can be converted to a full account.
157        self.code.is_some()
158    }
159
160    /// Returns storage updates for this account delta.
161    pub fn storage(&self) -> &AccountStorageDelta {
162        &self.storage
163    }
164
165    /// Returns vault updates for this account delta.
166    pub fn vault(&self) -> &AccountVaultDelta {
167        &self.vault
168    }
169
170    /// Returns the amount by which the nonce was incremented.
171    pub fn nonce_delta(&self) -> Felt {
172        self.nonce_delta
173    }
174
175    /// Returns the account ID to which this delta applies.
176    pub fn id(&self) -> AccountId {
177        self.account_id
178    }
179
180    /// Returns a reference to the account code of this delta, if present.
181    pub fn code(&self) -> Option<&AccountCode> {
182        self.code.as_ref()
183    }
184
185    /// Converts this storage delta into individual delta components.
186    pub fn into_parts(self) -> (AccountStorageDelta, AccountVaultDelta, Option<AccountCode>, Felt) {
187        (self.storage, self.vault, self.code, self.nonce_delta)
188    }
189
190    /// Computes the commitment to the account delta.
191    ///
192    /// ## Computation
193    ///
194    /// The delta is a sequential hash over a vector of field elements which starts out empty and
195    /// is appended to in the following way. Whenever sorting is expected, it is that of a [`Word`].
196    ///
197    /// - Append `[[nonce_delta, 0, account_id_suffix, account_id_prefix], EMPTY_WORD]`, where
198    ///   `account_id_{prefix,suffix}` are the prefix and suffix felts of the native account id and
199    ///   `nonce_delta` is the value by which the nonce was incremented.
200    /// - Fungible Asset Delta
201    ///   - For each **updated** fungible asset, sorted by its vault key, whose amount delta is
202    ///     **non-zero**:
203    ///     - Append `[domain = 1, was_added, faucet_id_suffix_and_metadata, faucet_id_prefix]`
204    ///       where `faucet_id_suffix_and_metadata` is the faucet ID suffix with asset metadata
205    ///       (including the callbacks flag) encoded in the lower 8 bits.
206    ///     - Append `[amount_delta, 0, 0, 0]` where `amount_delta` is the delta by which the
207    ///       fungible asset's amount has changed and `was_added` is a boolean flag indicating
208    ///       whether the amount was added (1) or subtracted (0).
209    /// - Non-Fungible Asset Delta
210    ///   - For each **updated** non-fungible asset, sorted by its vault key:
211    ///     - Append `[domain = 1, was_added, faucet_id_suffix, faucet_id_prefix]` where `was_added`
212    ///       is a boolean flag indicating whether the asset was added (1) or removed (0). Note that
213    ///       the domain is the same for assets since `faucet_id_suffix` and `faucet_id_prefix` are
214    ///       at the same position in the layout for both assets, and, by design, they are never the
215    ///       same for fungible and non-fungible assets.
216    ///     - Append `[hash0, hash1, hash2, hash3]`, i.e. the non-fungible asset.
217    /// - Storage Slots are sorted by slot ID and are iterated in this order. For each slot **whose
218    ///   value has changed**, depending on the slot type:
219    ///   - Value Slot
220    ///     - Append `[[domain = 2, 0, slot_id_suffix, slot_id_prefix], NEW_VALUE]` where
221    ///       `NEW_VALUE` is the new value of the slot and `slot_id_{suffix, prefix}` is the
222    ///       identifier of the slot.
223    ///   - Map Slot
224    ///     - For each key-value pair, sorted by key, whose new value is different from the previous
225    ///       value in the map:
226    ///       - Append `[KEY, NEW_VALUE]`.
227    ///     - Append `[[domain = 3, num_changed_entries, slot_id_suffix, slot_id_prefix], 0, 0, 0,
228    ///       0]`, where `slot_id_{suffix, prefix}` are the slot identifiers and
229    ///       `num_changed_entries` is the number of changed key-value pairs in the map.
230    ///         - For partial state deltas, the map header must only be included if
231    ///           `num_changed_entries` is not zero.
232    ///         - For full state deltas, the map header must always be included.
233    ///
234    /// ## Rationale
235    ///
236    /// The rationale for this layout is that hashing in the VM should be as efficient as possible
237    /// and minimize the number of branches to be as efficient as possible. Every high-level section
238    /// in this bullet point list should add an even number of words since the hasher operates
239    /// on double words. In the VM, each permutation is done immediately, so adding an uneven
240    /// number of words in a given step will result in more difficulty in the MASM implementation.
241    ///
242    /// ### New Accounts
243    ///
244    /// The delta for new accounts (a full state delta) must commit to all the storage slots of the
245    /// account, even if the storage slots have a default value (e.g. the empty word for value slots
246    /// or an empty storage map). This ensures the full state delta commits to the exact storage
247    /// slots that are contained in the account.
248    ///
249    /// ## Security
250    ///
251    /// The general concern with the commitment is that two distinct deltas must never hash to the
252    /// same commitment. E.g. a commitment of a delta that changes a key-value pair in a storage
253    /// map slot should be different from a delta that adds a non-fungible asset to the vault.
254    /// If not, a delta can be crafted in the VM that sets a map key but a malicious actor
255    /// crafts a delta outside the VM that adds a non-fungible asset. To prevent that, a couple
256    /// of measures are taken.
257    ///
258    /// - Because multiple unrelated contexts (e.g. vaults and storage slots) are hashed in the same
259    ///   hasher, domain separators are used to disambiguate. For each changed asset and each
260    ///   changed slot in the delta, a domain separator is hashed into the delta. The domain
261    ///   separator is always at the same index in each layout so it cannot be maliciously crafted
262    ///   (see below for an example).
263    /// - Storage value slots:
264    ///   - since only changed value slots are included in the delta, there is no ambiguity between
265    ///     a value slot being set to EMPTY_WORD and its value being unchanged.
266    /// - Storage map slots:
267    ///   - Map slots append a header which summarizes the changes in the slot, in particular the
268    ///     slot ID and number of changed entries.
269    ///   - Two distinct storage map slots use the same domain but are disambiguated due to
270    ///     inclusion of the slot ID.
271    ///
272    /// ### Domain Separators
273    ///
274    /// As an example for ambiguity, consider these two deltas:
275    ///
276    /// ```text
277    /// [
278    ///   ID_AND_NONCE, EMPTY_WORD,
279    ///   [/* no fungible asset delta */],
280    ///   [[domain = 1, was_added = 0, faucet_id_suffix, faucet_id_prefix], NON_FUNGIBLE_ASSET],
281    ///   [/* no storage delta */]
282    /// ]
283    /// ```
284    ///
285    /// ```text
286    /// [
287    ///   ID_AND_NONCE, EMPTY_WORD,
288    ///   [/* no fungible asset delta */],
289    ///   [/* no non-fungible asset delta */],
290    ///   [[domain = 2, 0, slot_id_suffix = faucet_id_suffix, slot_id_prefix = faucet_id_prefix], NEW_VALUE]
291    /// ]
292    /// ```
293    ///
294    /// `NEW_VALUE` is user-controllable so it can be crafted to match `NON_FUNGIBLE_ASSET`. Users
295    /// would have to choose a slot ID (at account creation time) that is equal to the faucet ID.
296    /// The domain separator is then the only value that differentiates these two deltas. This shows
297    /// the importance of placing the domain separators in the same index within each word's layout
298    /// to ensure users cannot craft an ambiguous delta.
299    ///
300    /// ### Number of Changed Entries
301    ///
302    /// As an example for ambiguity, consider these two deltas:
303    ///
304    /// ```text
305    /// [
306    ///   ID_AND_NONCE, EMPTY_WORD,
307    ///   [/* no fungible asset delta */],
308    ///   [/* no non-fungible asset delta */],
309    ///   [domain = 3, num_changed_entries = 0, slot_id_suffix = 20, slot_id_prefix = 21, 0, 0, 0, 0]
310    ///   [domain = 3, num_changed_entries = 0, slot_id_suffix = 42, slot_id_prefix = 43, 0, 0, 0, 0]
311    /// ]
312    /// ```
313    ///
314    /// ```text
315    /// [
316    ///   ID_AND_NONCE, EMPTY_WORD,
317    ///   [/* no fungible asset delta */],
318    ///   [/* no non-fungible asset delta */],
319    ///   [KEY0, VALUE0],
320    ///   [domain = 3, num_changed_entries = 1, slot_id_suffix = 42, slot_id_prefix = 43, 0, 0, 0, 0]
321    /// ]
322    /// ```
323    ///
324    /// The keys and values of map slots are user-controllable so `KEY0` and `VALUE0` could be
325    /// crafted to match the first map header in the first delta. So, _without_ having
326    /// `num_changed_entries` included in the commitment, these deltas would be ambiguous. A delta
327    /// with two empty maps could have the same commitment as a delta with one map entry where one
328    /// key-value pair has changed.
329    ///
330    /// #### New Accounts
331    ///
332    /// The number of changed entries of a storage map can be validly zero when an empty storage map
333    /// is added to a new account. In such cases, the number of changed key-value pairs is 0, but
334    /// the map must still be committed to, in order to differentiate between a slot being an empty
335    /// map or not being present at all.
336    pub fn to_commitment(&self) -> Word {
337        <Self as SequentialCommit>::to_commitment(self)
338    }
339}
340
341impl TryFrom<&AccountDelta> for Account {
342    type Error = AccountError;
343
344    /// Converts an [`AccountDelta`] into an [`Account`].
345    ///
346    /// Conceptually, this applies the delta onto an empty account.
347    ///
348    /// # Errors
349    ///
350    /// Returns an error if:
351    /// - If the delta is not a full state delta. See [`AccountDelta`] for details.
352    /// - If any vault delta operation removes an asset.
353    /// - If any vault delta operation adds an asset that would overflow the maximum representable
354    ///   amount.
355    /// - If any storage delta update violates account storage constraints.
356    fn try_from(delta: &AccountDelta) -> Result<Self, Self::Error> {
357        if !delta.is_full_state() {
358            return Err(AccountError::PartialStateDeltaToAccount);
359        }
360
361        let Some(code) = delta.code().cloned() else {
362            return Err(AccountError::PartialStateDeltaToAccount);
363        };
364
365        let mut vault = AssetVault::default();
366        vault.apply_delta(delta.vault()).map_err(AccountError::AssetVaultUpdateError)?;
367
368        // Once we support addition and removal of storage slots, we may be able to change
369        // this to create an empty account and use `Account::apply_delta` instead.
370        // For now, we need to create the initial storage of the account with the same slot types.
371        let mut empty_storage_slots = Vec::new();
372        for (slot_name, slot_delta) in delta.storage().slots() {
373            let slot = match slot_delta.slot_type() {
374                StorageSlotType::Value => StorageSlot::with_empty_value(slot_name.clone()),
375                StorageSlotType::Map => StorageSlot::with_empty_map(slot_name.clone()),
376            };
377            empty_storage_slots.push(slot);
378        }
379        let mut storage = AccountStorage::new(empty_storage_slots)
380            .expect("storage delta should contain a valid number of slots");
381        storage.apply_delta(delta.storage())?;
382
383        // The nonce of the account is the initial nonce of 0 plus the nonce_delta, so the
384        // nonce_delta itself.
385        let nonce = delta.nonce_delta();
386
387        Account::new(delta.id(), vault, storage, code, nonce, None)
388    }
389}
390
391impl SequentialCommit for AccountDelta {
392    type Commitment = Word;
393
394    /// Reduces the delta to a sequence of field elements.
395    ///
396    /// See [AccountDelta::to_commitment()] for more details.
397    fn to_elements(&self) -> Vec<Felt> {
398        // The commitment to an empty delta is defined as the empty word.
399        if self.is_empty() {
400            return Vec::new();
401        }
402
403        // Minor optimization: At least 24 elements are always added.
404        let mut elements = Vec::with_capacity(24);
405
406        // ID and Nonce
407        elements.extend_from_slice(&[
408            self.nonce_delta,
409            ZERO,
410            self.account_id.suffix(),
411            self.account_id.prefix().as_felt(),
412        ]);
413        elements.extend_from_slice(Word::empty().as_elements());
414
415        // Vault Delta
416        self.vault.append_delta_elements(&mut elements);
417
418        // Storage Delta
419        self.storage.append_delta_elements(&mut elements);
420
421        debug_assert!(
422            elements.len() % (2 * crate::WORD_SIZE) == 0,
423            "expected elements to contain an even number of words, but it contained {} elements",
424            elements.len()
425        );
426
427        elements
428    }
429}
430
431// ACCOUNT UPDATE DETAILS
432// ================================================================================================
433
434/// [`AccountUpdateDetails`] describes the details of one or more transactions executed against an
435/// account.
436///
437/// In particular, private account changes aren't tracked at all; they are represented as
438/// [`AccountUpdateDetails::Private`].
439///
440/// Non-private accounts are tracked as an [`AccountDelta`]. If the account is new, the delta can be
441/// converted into an [`Account`]. If not, the delta can be applied to the existing account using
442/// [`Account::apply_delta`].
443///
444/// Note that these details can represent the changes from one or more transactions in which case
445/// the deltas of each transaction are merged together using [`AccountDelta::merge`].
446#[derive(Clone, Debug, PartialEq, Eq)]
447pub enum AccountUpdateDetails {
448    /// The state update details of a private account is not publicly accessible.
449    Private,
450
451    /// The state update details of non-private accounts.
452    Delta(AccountDelta),
453}
454
455impl AccountUpdateDetails {
456    /// Returns `true` if the account update details are for private account.
457    pub fn is_private(&self) -> bool {
458        matches!(self, Self::Private)
459    }
460
461    /// Merges the `other` update into this one.
462    ///
463    /// This account update is assumed to come before the other.
464    pub fn merge(self, other: AccountUpdateDetails) -> Result<Self, AccountDeltaError> {
465        let merged_update = match (self, other) {
466            (AccountUpdateDetails::Private, AccountUpdateDetails::Private) => {
467                AccountUpdateDetails::Private
468            },
469            (AccountUpdateDetails::Delta(mut delta), AccountUpdateDetails::Delta(new_delta)) => {
470                delta.merge(new_delta)?;
471                AccountUpdateDetails::Delta(delta)
472            },
473            (left, right) => {
474                return Err(AccountDeltaError::IncompatibleAccountUpdates {
475                    left_update_type: left.as_tag_str(),
476                    right_update_type: right.as_tag_str(),
477                });
478            },
479        };
480
481        Ok(merged_update)
482    }
483
484    /// Returns the tag of the [`AccountUpdateDetails`] as a string for inclusion in error messages.
485    pub(crate) const fn as_tag_str(&self) -> &'static str {
486        match self {
487            AccountUpdateDetails::Private => "private",
488            AccountUpdateDetails::Delta(_) => "delta",
489        }
490    }
491}
492
493// SERIALIZATION
494// ================================================================================================
495
496impl Serializable for AccountDelta {
497    fn write_into<W: ByteWriter>(&self, target: &mut W) {
498        self.account_id.write_into(target);
499        self.storage.write_into(target);
500        self.vault.write_into(target);
501        self.code.write_into(target);
502        self.nonce_delta.write_into(target);
503    }
504
505    fn get_size_hint(&self) -> usize {
506        self.account_id.get_size_hint()
507            + self.storage.get_size_hint()
508            + self.vault.get_size_hint()
509            + self.code.get_size_hint()
510            + self.nonce_delta.get_size_hint()
511    }
512}
513
514impl Deserializable for AccountDelta {
515    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
516        let account_id = AccountId::read_from(source)?;
517        let storage = AccountStorageDelta::read_from(source)?;
518        let vault = AccountVaultDelta::read_from(source)?;
519        let code = <Option<AccountCode>>::read_from(source)?;
520        let nonce_delta = Felt::read_from(source)?;
521
522        validate_nonce(nonce_delta, &storage, &vault)
523            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
524
525        Ok(Self {
526            account_id,
527            storage,
528            vault,
529            code,
530            nonce_delta,
531        })
532    }
533}
534
535impl Serializable for AccountUpdateDetails {
536    fn write_into<W: ByteWriter>(&self, target: &mut W) {
537        match self {
538            AccountUpdateDetails::Private => {
539                0_u8.write_into(target);
540            },
541            AccountUpdateDetails::Delta(delta) => {
542                1_u8.write_into(target);
543                delta.write_into(target);
544            },
545        }
546    }
547
548    fn get_size_hint(&self) -> usize {
549        // Size of the serialized enum tag.
550        let u8_size = 0u8.get_size_hint();
551
552        match self {
553            AccountUpdateDetails::Private => u8_size,
554            AccountUpdateDetails::Delta(account_delta) => u8_size + account_delta.get_size_hint(),
555        }
556    }
557}
558
559impl Deserializable for AccountUpdateDetails {
560    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
561        match u8::read_from(source)? {
562            0 => Ok(Self::Private),
563            1 => Ok(Self::Delta(AccountDelta::read_from(source)?)),
564            variant => Err(DeserializationError::InvalidValue(format!(
565                "Unknown variant {variant} for AccountDetails"
566            ))),
567        }
568    }
569}
570
571// HELPER FUNCTIONS
572// ================================================================================================
573
574/// Checks if the nonce was updated correctly given the provided storage and vault deltas.
575///
576/// # Errors
577///
578/// Returns an error if:
579/// - storage or vault were updated, but the nonce_delta was set to 0.
580fn validate_nonce(
581    nonce_delta: Felt,
582    storage: &AccountStorageDelta,
583    vault: &AccountVaultDelta,
584) -> Result<(), AccountDeltaError> {
585    if (!storage.is_empty() || !vault.is_empty()) && nonce_delta == ZERO {
586        return Err(AccountDeltaError::NonEmptyStorageOrVaultDeltaWithZeroNonceDelta);
587    }
588
589    Ok(())
590}
591
592// TESTS
593// ================================================================================================
594
595#[cfg(test)]
596mod tests {
597
598    use assert_matches::assert_matches;
599    use miden_core::Felt;
600
601    use super::{AccountDelta, AccountStorageDelta, AccountVaultDelta};
602    use crate::account::delta::AccountUpdateDetails;
603    use crate::account::{
604        Account,
605        AccountCode,
606        AccountId,
607        AccountStorage,
608        AccountStorageMode,
609        AccountType,
610        StorageMapDelta,
611        StorageMapKey,
612        StorageSlotName,
613    };
614    use crate::asset::{
615        Asset,
616        AssetVault,
617        FungibleAsset,
618        NonFungibleAsset,
619        NonFungibleAssetDetails,
620    };
621    use crate::errors::AccountDeltaError;
622    use crate::testing::account_id::{
623        ACCOUNT_ID_PRIVATE_SENDER,
624        ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
625        AccountIdBuilder,
626    };
627    use crate::utils::serde::Serializable;
628    use crate::{ONE, Word, ZERO};
629
630    #[test]
631    fn account_delta_nonce_validation() {
632        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
633        // empty delta
634        let storage_delta = AccountStorageDelta::new();
635        let vault_delta = AccountVaultDelta::default();
636
637        AccountDelta::new(account_id, storage_delta.clone(), vault_delta.clone(), ZERO).unwrap();
638        AccountDelta::new(account_id, storage_delta.clone(), vault_delta.clone(), ONE).unwrap();
639
640        // non-empty delta
641        let storage_delta = AccountStorageDelta::from_iters([StorageSlotName::mock(1)], [], []);
642
643        assert_matches!(
644            AccountDelta::new(account_id, storage_delta.clone(), vault_delta.clone(), ZERO)
645                .unwrap_err(),
646            AccountDeltaError::NonEmptyStorageOrVaultDeltaWithZeroNonceDelta
647        );
648        AccountDelta::new(account_id, storage_delta.clone(), vault_delta.clone(), ONE).unwrap();
649    }
650
651    #[test]
652    fn account_delta_nonce_overflow() {
653        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
654        let storage_delta = AccountStorageDelta::new();
655        let vault_delta = AccountVaultDelta::default();
656
657        let nonce_delta0 = ONE;
658        let nonce_delta1 = Felt::try_from(0xffff_ffff_0000_0000u64).unwrap();
659
660        let mut delta0 =
661            AccountDelta::new(account_id, storage_delta.clone(), vault_delta.clone(), nonce_delta0)
662                .unwrap();
663        let delta1 =
664            AccountDelta::new(account_id, storage_delta, vault_delta, nonce_delta1).unwrap();
665
666        assert_matches!(delta0.merge(delta1).unwrap_err(), AccountDeltaError::NonceIncrementOverflow {
667          current, increment, new
668        } => {
669            assert_eq!(current, nonce_delta0);
670            assert_eq!(increment, nonce_delta1);
671            assert_eq!(new, nonce_delta0 + nonce_delta1);
672        });
673    }
674
675    #[test]
676    fn account_update_details_size_hint() {
677        // AccountDelta
678        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
679        let storage_delta = AccountStorageDelta::new();
680        let vault_delta = AccountVaultDelta::default();
681        assert_eq!(storage_delta.to_bytes().len(), storage_delta.get_size_hint());
682        assert_eq!(vault_delta.to_bytes().len(), vault_delta.get_size_hint());
683
684        let account_delta =
685            AccountDelta::new(account_id, storage_delta, vault_delta, ZERO).unwrap();
686        assert_eq!(account_delta.to_bytes().len(), account_delta.get_size_hint());
687
688        let storage_delta = AccountStorageDelta::from_iters(
689            [StorageSlotName::mock(1)],
690            [
691                (StorageSlotName::mock(2), Word::from([1, 1, 1, 1u32])),
692                (StorageSlotName::mock(3), Word::from([1, 1, 0, 1u32])),
693            ],
694            [(
695                StorageSlotName::mock(4),
696                StorageMapDelta::from_iters(
697                    [
698                        StorageMapKey::from_array([1, 1, 1, 0]),
699                        StorageMapKey::from_array([0, 1, 1, 1]),
700                    ],
701                    [(StorageMapKey::from_array([1, 1, 1, 1]), Word::from([1, 1, 1, 1u32]))],
702                ),
703            )],
704        );
705
706        let non_fungible: Asset = NonFungibleAsset::new(
707            &NonFungibleAssetDetails::new(
708                AccountIdBuilder::new()
709                    .account_type(AccountType::NonFungibleFaucet)
710                    .storage_mode(AccountStorageMode::Public)
711                    .build_with_rng(&mut rand::rng()),
712                vec![6],
713            )
714            .unwrap(),
715        )
716        .unwrap()
717        .into();
718        let fungible_2: Asset = FungibleAsset::new(
719            AccountIdBuilder::new()
720                .account_type(AccountType::FungibleFaucet)
721                .storage_mode(AccountStorageMode::Public)
722                .build_with_rng(&mut rand::rng()),
723            10,
724        )
725        .unwrap()
726        .into();
727        let vault_delta = AccountVaultDelta::from_iters([non_fungible], [fungible_2]);
728
729        assert_eq!(storage_delta.to_bytes().len(), storage_delta.get_size_hint());
730        assert_eq!(vault_delta.to_bytes().len(), vault_delta.get_size_hint());
731
732        let account_delta = AccountDelta::new(account_id, storage_delta, vault_delta, ONE).unwrap();
733        assert_eq!(account_delta.to_bytes().len(), account_delta.get_size_hint());
734
735        // Account
736
737        let account_id =
738            AccountId::try_from(ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE).unwrap();
739
740        let asset_vault = AssetVault::mock();
741        assert_eq!(asset_vault.to_bytes().len(), asset_vault.get_size_hint());
742
743        let account_storage = AccountStorage::mock();
744        assert_eq!(account_storage.to_bytes().len(), account_storage.get_size_hint());
745
746        let account_code = AccountCode::mock();
747        assert_eq!(account_code.to_bytes().len(), account_code.get_size_hint());
748
749        let account =
750            Account::new_existing(account_id, asset_vault, account_storage, account_code, ONE);
751        assert_eq!(account.to_bytes().len(), account.get_size_hint());
752
753        // AccountUpdateDetails
754
755        let update_details_private = AccountUpdateDetails::Private;
756        assert_eq!(update_details_private.to_bytes().len(), update_details_private.get_size_hint());
757
758        let update_details_delta = AccountUpdateDetails::Delta(account_delta);
759        assert_eq!(update_details_delta.to_bytes().len(), update_details_delta.get_size_hint());
760    }
761}