Skip to main content

miden_protocol/account/delta/
mod.rs

1use alloc::string::ToString;
2use alloc::vec::Vec;
3
4use crate::account::{Account, AccountCode, AccountId, AccountStorage, AccountStoragePatch};
5use crate::asset::AssetVault;
6use crate::crypto::SequentialCommit;
7use crate::errors::{AccountDeltaError, AccountError};
8use crate::utils::serde::{
9    ByteReader,
10    ByteWriter,
11    Deserializable,
12    DeserializationError,
13    Serializable,
14};
15use crate::{Felt, Word, ZERO};
16
17mod delta_op;
18pub use delta_op::AssetDeltaOperation;
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 [`AccountStoragePatch`] 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`]. Because a full state delta reconstructs the
44/// account from empty storage, its storage patch may only create slots, never update or remove
45/// them; [`AccountDelta::new`] enforces this.
46///
47/// TODO(code_upgrades): The ability to track account code updates is an outstanding feature. For
48/// that reason, the account code is not considered as part of the "nonce must be incremented if
49/// state changed" check.
50#[derive(Clone, Debug, PartialEq, Eq)]
51pub struct AccountDelta {
52    /// The ID of the account to which this delta applies. If the delta is created during
53    /// transaction execution, that is the native account of the transaction.
54    account_id: AccountId,
55    /// The patch of the account's storage.
56    storage: AccountStoragePatch,
57    /// The delta of the account's asset vault.
58    vault: AccountVaultDelta,
59    /// The code of a new account (`Some`) or `None` for existing accounts.
60    code: Option<AccountCode>,
61    /// The value by which the nonce was incremented. Must be greater than zero if storage or vault
62    /// are non-empty.
63    nonce_delta: Felt,
64}
65
66impl AccountDelta {
67    // CONSTANTS
68    // --------------------------------------------------------------------------------------------
69
70    /// Domain separator for the account delta commitment header.
71    const DOMAIN: Felt = Felt::new_unchecked(1);
72
73    // CONSTRUCTOR
74    // --------------------------------------------------------------------------------------------
75
76    /// Returns new [AccountDelta] instantiated from the provided components.
77    ///
78    /// `code` is `Some` for a full state delta (a new account) and `None` otherwise.
79    ///
80    /// # Errors
81    ///
82    /// - Returns an error if storage or vault were updated, but the nonce_delta is 0.
83    /// - Returns an error if `code` is provided but the storage patch contains an `Update` or
84    ///   `Remove` operation. A full state delta must reconstruct the account from empty storage, so
85    ///   it may only create slots.
86    pub fn new(
87        account_id: AccountId,
88        storage: AccountStoragePatch,
89        vault: AccountVaultDelta,
90        code: Option<AccountCode>,
91        nonce_delta: Felt,
92    ) -> Result<Self, AccountDeltaError> {
93        // nonce must be updated if either account storage or vault were updated
94        validate_nonce(nonce_delta, &storage, &vault)?;
95
96        // A full state delta (carrying code) must reconstruct the account from empty storage, so it
97        // may only create slots. An `Update` or `Remove` assumes the slot already exists and would
98        // make reconstruction impossible.
99        if code.is_some() && storage.contains_non_create_ops() {
100            return Err(AccountDeltaError::FullStateDeltaContainsNonCreateOp);
101        }
102
103        Ok(Self {
104            account_id,
105            storage,
106            vault,
107            code,
108            nonce_delta,
109        })
110    }
111
112    // PUBLIC MUTATORS
113    // --------------------------------------------------------------------------------------------
114
115    /// Returns a mutable reference to the account vault delta.
116    pub fn vault_mut(&mut self) -> &mut AccountVaultDelta {
117        &mut self.vault
118    }
119
120    // PUBLIC ACCESSORS
121    // --------------------------------------------------------------------------------------------
122
123    /// Returns true if this account delta does not contain any vault, storage or nonce updates.
124    pub fn is_empty(&self) -> bool {
125        self.storage.is_empty() && self.vault.is_empty() && self.nonce_delta == ZERO
126    }
127
128    /// Returns `true` if this delta is a "full state" delta, `false` otherwise, i.e. if it is a
129    /// "partial state" delta.
130    ///
131    /// See the type-level docs for more on this distinction.
132    pub fn is_full_state(&self) -> bool {
133        // TODO(code_upgrades): Change this to another detection mechanism once we have code upgrade
134        // support, at which point the presence of code may not be enough of an indication
135        // that a delta can be converted to a full account.
136        //
137        // The presence of code alone is sufficient to identify a full state delta: the constructor
138        // enforces that a code-carrying delta's storage patch contains only `Create` ops, so it
139        // always reconstructs a full account.
140        self.code.is_some()
141    }
142
143    /// Returns storage updates for this account delta.
144    pub fn storage(&self) -> &AccountStoragePatch {
145        &self.storage
146    }
147
148    /// Returns vault updates for this account delta.
149    pub fn vault(&self) -> &AccountVaultDelta {
150        &self.vault
151    }
152
153    /// Returns the amount by which the nonce was incremented.
154    pub fn nonce_delta(&self) -> Felt {
155        self.nonce_delta
156    }
157
158    /// Returns the account ID to which this delta applies.
159    pub fn id(&self) -> AccountId {
160        self.account_id
161    }
162
163    /// Returns a reference to the account code of this delta, if present.
164    pub fn code(&self) -> Option<&AccountCode> {
165        self.code.as_ref()
166    }
167
168    /// Converts this delta into its individual components.
169    pub fn into_parts(self) -> (AccountStoragePatch, AccountVaultDelta, Option<AccountCode>, Felt) {
170        (self.storage, self.vault, self.code, self.nonce_delta)
171    }
172
173    /// Computes the commitment to the account delta.
174    ///
175    /// ## Computation
176    ///
177    /// The delta commitment is a sequential hash over a vector of field elements which starts out
178    /// empty and is appended to in the following way. Whenever sorting is expected, it is that
179    /// of a [`Word`].
180    ///
181    /// - Append `[[domain = 1, nonce_delta, account_id_suffix, account_id_prefix], EMPTY_WORD]`,
182    ///   where `account_id_{prefix,suffix}` are the prefix and suffix felts of the native account
183    ///   id, `nonce_delta` is the value by which the nonce was incremented, and `domain = 1`
184    ///   identifies the header as the start of an account delta commitment.
185    /// - Asset Delta
186    ///   - For each **added** asset, sorted by its asset ID:
187    ///     - Append `[ASSET_ID, ASSET_VALUE]`.
188    ///   - Append `[domain = 3, delta_op = 1, num_added_assets, 0]` if `num_added_assets != 0`
189    ///     where `num_added_assets` is the number of added assets and `delta_op` is set to `1`
190    ///     indicating asset addition.
191    ///   - For each **removed** asset, sorted by its asset ID:
192    ///     - Append `[ASSET_ID, ASSET_VALUE]`.
193    ///   - Append `[domain = 3, delta_op = 2, num_removed_assets, 0]` if `num_removed_assets != 0`
194    ///     where `num_removed_assets` is the number of removed assets and `delta_op` is set to `2`
195    ///     indicating asset removal.
196    ///   - Note that the domain is the same independent of asset addition or removal, since the
197    ///     `delta_op` sufficiently distinguishes the two domains.
198    /// - Storage Slots are sorted by slot ID and are iterated in this order. `patch_op` is the
199    ///   [`StoragePatchOperation`](crate::account::StoragePatchOperation) of the slot patch and
200    ///   `slot_id_{suffix, prefix}` is the identifier of the slot. For each slot, depending on its
201    ///   slot type:
202    ///   - Value Slot
203    ///     - Append `[[domain = 5, patch_op, slot_id_suffix, slot_id_prefix], NEW_VALUE]` where
204    ///       `NEW_VALUE` is the new value of the slot.
205    ///   - Map Slot
206    ///     - For each key-value pair, sorted by key, whose new value is different from the previous
207    ///       value in the map:
208    ///       - Append `[KEY, NEW_VALUE]`.
209    ///     - The map trailer is constructed as `[[domain = 6, patch_op, slot_id_suffix,
210    ///       slot_id_prefix], [num_changed_entries, 0, 0, 0]]`, where `num_changed_entries` is the
211    ///       number of key-value pairs appended above. Whether the trailer is included depends on
212    ///       `patch_op`:
213    ///         - For
214    ///           [`StoragePatchOperation::Create`](crate::account::StoragePatchOperation::Create),
215    ///           the trailer is always included, since the slot's creation must be committed to even
216    ///           when the map is created empty (`num_changed_entries == 0`).
217    ///         - For
218    ///           [`StoragePatchOperation::Update`](crate::account::StoragePatchOperation::Update),
219    ///           the trailer is included only if `num_changed_entries != 0`. An update that changes
220    ///           no entries is a no-op and is omitted entirely.
221    ///         - For
222    ///           [`StoragePatchOperation::Remove`](crate::account::StoragePatchOperation::Remove),
223    ///           the trailer is always included with `num_changed_entries` set to zero, since the
224    ///           number of removed entries is unknown.
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 created storage slots
237    /// of the account, even if these slots contain the default value (e.g. the empty word for value
238    /// slots or an empty storage map). This ensures the full state delta commits to the exact
239    /// storage 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 domains (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 value slots are only included in the patch if their value has changed when the
257    ///     operation is `Update`, there is no ambiguity between a value slot being set to
258    ///     EMPTY_WORD and its value being unchanged.
259    /// - Storage map slots:
260    ///   - Map slots append a header which summarizes the changes in the slot, in particular the
261    ///     slot ID and number of changed entries.
262    ///   - Two distinct storage map slots use the same domain but are disambiguated due to
263    ///     inclusion of the slot ID.
264    ///
265    /// ### Domain Separators
266    ///
267    /// As an example for ambiguity, consider these two deltas:
268    ///
269    /// ```text
270    /// [
271    ///   ID_AND_NONCE, EMPTY_WORD,
272    ///   [ASSET_ID, ASSET_VALUE],
273    ///   [[domain = 3, delta_op = 1, num_added_assets = 1, 0], EMPTY_WORD],
274    ///   [/* no removed assets delta */],
275    ///   [/* no storage patch */]
276    /// ]
277    /// ```
278    ///
279    /// ```text
280    /// [
281    ///   ID_AND_NONCE, EMPTY_WORD,
282    ///   [/* no asset delta */],
283    ///   [[domain = 5, patch_op, slot_id_suffix0, slot_id_prefix0], NEW_VALUE]
284    ///   [[domain = 5, patch_op, slot_id_suffix1, slot_id_prefix1], NEW_VALUE]
285    /// ]
286    /// ```
287    ///
288    /// - `NEW_VALUE` is user-controlled and can be crafted to match `ASSET_VALUE` or `EMPTY_WORD`.
289    /// - Slot IDs are user-controlled and can be crafted to match the two most significant elements
290    ///   in the asset ID or `num_added_assets` and the fixed 0.
291    /// - This leaves only the domain separator and the patch_op to differentiate these two deltas.
292    ///
293    /// The delta and patch headers further use distinct domain separators (1 and 2 respectively),
294    /// so a delta and a patch with otherwise identical bodies can never collide.
295    ///
296    /// ### Number of Changed Entries
297    ///
298    /// As an example for ambiguity, consider these two deltas:
299    ///
300    /// ```text
301    /// [
302    ///   ID_AND_NONCE, EMPTY_WORD,
303    ///   [/* no asset delta */],
304    ///   [domain = 6, patch_op, slot_id_suffix = 20, slot_id_prefix = 21, num_changed_entries = 0, 0, 0, 0]
305    ///   [domain = 6, patch_op, slot_id_suffix = 42, slot_id_prefix = 43, num_changed_entries = 0, 0, 0, 0]
306    /// ]
307    /// ```
308    ///
309    /// ```text
310    /// [
311    ///   ID_AND_NONCE, EMPTY_WORD,
312    ///   [/* no asset delta */],
313    ///   [KEY0, VALUE0],
314    ///   [domain = 6, patch_op, slot_id_suffix = 42, slot_id_prefix = 43, num_changed_entries = 1, 0, 0, 0]
315    /// ]
316    /// ```
317    ///
318    /// The keys and values of map slots are user-controllable so `KEY0` and `VALUE0` could be
319    /// crafted to match the first map header in the first delta. So, _without_ having
320    /// `num_changed_entries` included in the commitment, these deltas would be ambiguous. A delta
321    /// with two empty maps could have the same commitment as a delta with one map entry where one
322    /// key-value pair has changed.
323    ///
324    /// #### New Accounts
325    ///
326    /// The number of changed entries of a storage map can be validly zero when an empty storage map
327    /// is created in account (e.g. at account creation time). In such cases, the number of changed
328    /// key-value pairs is 0, but the map must still be committed to, in order to differentiate
329    /// between a slot being created as an empty map or not being created at all.
330    pub fn to_commitment(&self) -> Word {
331        <Self as SequentialCommit>::to_commitment(self)
332    }
333}
334
335impl TryFrom<&AccountDelta> for Account {
336    type Error = AccountError;
337
338    /// Converts an [`AccountDelta`] into an [`Account`].
339    ///
340    /// Conceptually, this applies the delta onto an empty account.
341    ///
342    /// # Errors
343    ///
344    /// Returns an error if:
345    /// - If the delta is not a full state delta. See [`AccountDelta`] for details.
346    /// - If any vault delta operation removes an asset.
347    /// - If any vault delta operation adds an asset that would overflow the maximum representable
348    ///   amount.
349    /// - If any storage patch update violates account storage constraints.
350    fn try_from(delta: &AccountDelta) -> Result<Self, Self::Error> {
351        if !delta.is_full_state() {
352            return Err(AccountError::PartialStateDeltaToAccount);
353        }
354
355        let Some(code) = delta.code().cloned() else {
356            return Err(AccountError::PartialStateDeltaToAccount);
357        };
358
359        // The asset vault of a new account is empty, so if the delta contains removed assets, the
360        // delta is invalid.
361        if delta.vault().removed_assets().count() != 0 {
362            return Err(AccountError::AssetsRemovedFromNewAccount);
363        }
364
365        let mut vault = AssetVault::default();
366        for added_asset in delta.vault().added_assets() {
367            vault.insert_asset(added_asset).map_err(AccountError::AssetVaultUpdateError)?;
368        }
369
370        // A full state delta consists of `Create` slot patches, so applying it to empty storage
371        // reconstructs the account's full storage.
372        let mut storage = AccountStorage::default();
373        storage.apply_patch(delta.storage())?;
374
375        // The nonce of the account is the initial nonce of 0 plus the nonce_delta, so the
376        // nonce_delta itself.
377        let nonce = delta.nonce_delta();
378
379        Account::new(delta.id(), vault, storage, code, nonce, None)
380    }
381}
382
383impl SequentialCommit for AccountDelta {
384    type Commitment = Word;
385
386    /// Reduces the delta to a sequence of field elements.
387    ///
388    /// See [AccountDelta::to_commitment()] for more details.
389    fn to_elements(&self) -> Vec<Felt> {
390        // The commitment to an empty delta is defined as the empty word.
391        if self.is_empty() {
392            return Vec::new();
393        }
394
395        // Minor optimization: At least 24 elements are always added.
396        let mut elements = Vec::with_capacity(24);
397
398        // ID and Nonce
399        elements.extend_from_slice(&[
400            Self::DOMAIN,
401            self.nonce_delta,
402            self.account_id.suffix(),
403            self.account_id.prefix().as_felt(),
404        ]);
405        elements.extend_from_slice(Word::empty().as_elements());
406
407        // Vault Delta
408        self.vault.append_delta_elements(&mut elements);
409
410        // Storage Patch
411        self.storage.append_patch_elements(&mut elements);
412
413        debug_assert!(
414            elements.len() % (2 * crate::WORD_SIZE) == 0,
415            "expected elements to contain an even number of words, but it contained {} elements",
416            elements.len()
417        );
418
419        elements
420    }
421}
422
423// SERIALIZATION
424// ================================================================================================
425
426impl Serializable for AccountDelta {
427    fn write_into<W: ByteWriter>(&self, target: &mut W) {
428        self.account_id.write_into(target);
429        self.storage.write_into(target);
430        self.vault.write_into(target);
431        self.code.write_into(target);
432        self.nonce_delta.write_into(target);
433    }
434
435    fn get_size_hint(&self) -> usize {
436        self.account_id.get_size_hint()
437            + self.storage.get_size_hint()
438            + self.vault.get_size_hint()
439            + self.code.get_size_hint()
440            + self.nonce_delta.get_size_hint()
441    }
442}
443
444impl Deserializable for AccountDelta {
445    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
446        let account_id = AccountId::read_from(source)?;
447        let storage = AccountStoragePatch::read_from(source)?;
448        let vault = AccountVaultDelta::read_from(source)?;
449        let code = <Option<AccountCode>>::read_from(source)?;
450        let nonce_delta = Felt::read_from(source)?;
451
452        validate_nonce(nonce_delta, &storage, &vault)
453            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
454
455        Ok(Self {
456            account_id,
457            storage,
458            vault,
459            code,
460            nonce_delta,
461        })
462    }
463}
464
465// HELPER FUNCTIONS
466// ================================================================================================
467
468/// Checks if the nonce was updated correctly given the provided storage and vault deltas.
469///
470/// # Errors
471///
472/// Returns an error if:
473/// - storage or vault were updated, but the nonce_delta was set to 0.
474fn validate_nonce(
475    nonce_delta: Felt,
476    storage: &AccountStoragePatch,
477    vault: &AccountVaultDelta,
478) -> Result<(), AccountDeltaError> {
479    if (!storage.is_empty() || !vault.is_empty()) && nonce_delta == ZERO {
480        return Err(AccountDeltaError::NonEmptyStorageOrVaultDeltaWithZeroNonceDelta);
481    }
482
483    Ok(())
484}
485
486// TESTS
487// ================================================================================================
488
489#[cfg(test)]
490mod tests {
491
492    use assert_matches::assert_matches;
493    use rstest::rstest;
494
495    use super::{AccountDelta, AccountStoragePatch, AccountVaultDelta};
496    use crate::account::{
497        Account,
498        AccountCode,
499        AccountId,
500        AccountStorage,
501        AccountType,
502        StorageMapKey,
503        StorageMapPatch,
504        StorageSlotName,
505    };
506    use crate::asset::{
507        Asset,
508        AssetVault,
509        FungibleAsset,
510        NonFungibleAsset,
511        NonFungibleAssetDetails,
512    };
513    use crate::errors::AccountDeltaError;
514    use crate::testing::account_id::{
515        ACCOUNT_ID_PRIVATE_SENDER,
516        ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
517        AccountIdBuilder,
518    };
519    use crate::utils::serde::Serializable;
520    use crate::{ONE, Word, ZERO};
521
522    #[test]
523    fn account_delta_nonce_validation() {
524        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
525        // empty delta
526        let storage_patch = AccountStoragePatch::new();
527        let vault_delta = AccountVaultDelta::default();
528
529        AccountDelta::new(account_id, storage_patch.clone(), vault_delta.clone(), None, ZERO)
530            .unwrap();
531        AccountDelta::new(account_id, storage_patch.clone(), vault_delta.clone(), None, ONE)
532            .unwrap();
533
534        // non-empty delta
535        let storage_patch = AccountStoragePatch::from_iters([StorageSlotName::mock(1)], [], []);
536
537        assert_matches!(
538            AccountDelta::new(account_id, storage_patch.clone(), vault_delta.clone(), None, ZERO)
539                .unwrap_err(),
540            AccountDeltaError::NonEmptyStorageOrVaultDeltaWithZeroNonceDelta
541        );
542        AccountDelta::new(account_id, storage_patch.clone(), vault_delta.clone(), None, ONE)
543            .unwrap();
544    }
545
546    /// A full state delta (carrying code) must only contain `Create` storage ops, since an `Update`
547    /// or `Remove` could not be applied to the empty storage of a new account.
548    #[rstest]
549    #[case::update(
550        AccountStoragePatch::builder().update_value(StorageSlotName::mock(1), Word::empty()).build()
551    )]
552    #[case::remove(
553        AccountStoragePatch::builder().remove_value(StorageSlotName::mock(1)).build()
554    )]
555    fn account_delta_new_rejects_full_state_with_non_create_op(
556        #[case] storage: AccountStoragePatch,
557    ) -> anyhow::Result<()> {
558        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
559
560        let error = AccountDelta::new(
561            account_id,
562            storage,
563            AccountVaultDelta::default(),
564            Some(AccountCode::mock()),
565            ONE,
566        )
567        .unwrap_err();
568        assert_matches!(error, AccountDeltaError::FullStateDeltaContainsNonCreateOp);
569
570        Ok(())
571    }
572
573    /// A full state delta whose storage only creates slots can be reconstructed into an account.
574    #[test]
575    fn account_delta_full_state_with_create_reconstructs() -> anyhow::Result<()> {
576        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
577        let code = AccountCode::mock();
578        let created_slot = StorageSlotName::mock(1);
579        let created_value = Word::from([7u32, 0, 0, 0]);
580
581        let storage = AccountStoragePatch::builder()
582            .create_value(created_slot.clone(), created_value)
583            .build();
584
585        let delta = AccountDelta::new(
586            account_id,
587            storage,
588            AccountVaultDelta::default(),
589            Some(code.clone()),
590            ONE,
591        )?;
592        assert!(delta.is_full_state());
593
594        let account = Account::try_from(&delta)?;
595        assert_eq!(account.code(), &code);
596        assert_eq!(account.storage().get_item(&created_slot)?, created_value);
597
598        Ok(())
599    }
600
601    #[test]
602    fn account_delta_size_hint() {
603        // AccountDelta
604        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
605        let storage_patch = AccountStoragePatch::new();
606        let vault_delta = AccountVaultDelta::default();
607        assert_eq!(storage_patch.to_bytes().len(), storage_patch.get_size_hint());
608        assert_eq!(vault_delta.to_bytes().len(), vault_delta.get_size_hint());
609
610        let account_delta =
611            AccountDelta::new(account_id, storage_patch, vault_delta, None, ZERO).unwrap();
612        assert_eq!(account_delta.to_bytes().len(), account_delta.get_size_hint());
613
614        let storage_patch = AccountStoragePatch::from_iters(
615            [StorageSlotName::mock(1)],
616            [
617                (StorageSlotName::mock(2), Word::from([1, 1, 1, 1u32])),
618                (StorageSlotName::mock(3), Word::from([1, 1, 0, 1u32])),
619            ],
620            [(
621                StorageSlotName::mock(4),
622                StorageMapPatch::from_iters(
623                    [
624                        StorageMapKey::from_array([1, 1, 1, 0]),
625                        StorageMapKey::from_array([0, 1, 1, 1]),
626                    ],
627                    [(StorageMapKey::from_array([1, 1, 1, 1]), Word::from([1, 1, 1, 1u32]))],
628                ),
629            )],
630        );
631
632        let non_fungible: Asset = NonFungibleAsset::new(&NonFungibleAssetDetails::new(
633            AccountIdBuilder::new()
634                .account_type(AccountType::Public)
635                .build_with_rng(&mut rand::rng()),
636            vec![6],
637        ))
638        .into();
639        let fungible_2: Asset = FungibleAsset::new(
640            AccountIdBuilder::new()
641                .account_type(AccountType::Public)
642                .build_with_rng(&mut rand::rng()),
643            10,
644        )
645        .unwrap()
646        .into();
647        let vault_delta = AccountVaultDelta::from_iters([non_fungible], [fungible_2]);
648
649        assert_eq!(storage_patch.to_bytes().len(), storage_patch.get_size_hint());
650        assert_eq!(vault_delta.to_bytes().len(), vault_delta.get_size_hint());
651
652        let account_delta =
653            AccountDelta::new(account_id, storage_patch, vault_delta, None, ONE).unwrap();
654        assert_eq!(account_delta.to_bytes().len(), account_delta.get_size_hint());
655
656        // Account
657
658        let account_id =
659            AccountId::try_from(ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE).unwrap();
660
661        let asset_vault = AssetVault::mock();
662        assert_eq!(asset_vault.to_bytes().len(), asset_vault.get_size_hint());
663
664        let account_storage = AccountStorage::mock();
665        assert_eq!(account_storage.to_bytes().len(), account_storage.get_size_hint());
666
667        let account_code = AccountCode::mock();
668        assert_eq!(account_code.to_bytes().len(), account_code.get_size_hint());
669
670        let account =
671            Account::new_existing(account_id, asset_vault, account_storage, account_code, ONE);
672        assert_eq!(account.to_bytes().len(), account.get_size_hint());
673    }
674}