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