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