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, Hasher, Word, ZERO};
16
17mod delta_op;
18pub use delta_op::AssetDeltaOperation;
19
20mod vault;
21pub use vault::{AccountVaultDelta, AssetDelta};
22
23// ACCOUNT DELTA
24// ================================================================================================
25
26/// The [`AccountDelta`] stores the differences between two account states, which can result from
27/// one or more transaction.
28///
29/// The differences are represented as follows:
30/// - storage: an [`AccountStoragePatch`] that contains the changes to the account storage.
31/// - vault: an [`AccountVaultDelta`] object that contains the changes to the account vault.
32/// - nonce: if the nonce of the account has changed, the _delta_ of the nonce is stored, i.e. the
33///   value by which the nonce increased.
34/// - code: an [`AccountCode`] for new accounts and `None` for others.
35///
36/// The presence of the code in a delta signals if the delta is a _full state_ or _partial state_
37/// delta. A full state delta must be converted into an [`Account`] object, while a partial state
38/// delta must be applied to an existing [`Account`]. Because a full state delta reconstructs the
39/// account from empty storage, its storage patch may only create slots, never update or remove
40/// them; [`AccountDelta::new`] enforces this.
41///
42/// TODO(code_upgrades): The ability to track account code updates is an outstanding feature. For
43/// that reason, the account code is not considered as part of the "nonce must be incremented if
44/// state changed" check.
45#[derive(Clone, Debug, PartialEq, Eq)]
46pub struct AccountDelta {
47    /// The ID of the account to which this delta applies. If the delta is created during
48    /// transaction execution, that is the native account of the transaction.
49    account_id: AccountId,
50    /// The patch of the account's storage.
51    storage: AccountStoragePatch,
52    /// The delta of the account's asset vault.
53    vault: AccountVaultDelta,
54    /// The code of a new account (`Some`) or `None` for existing accounts.
55    code: Option<AccountCode>,
56    /// The value by which the nonce was incremented. Must be greater than zero if storage or vault
57    /// are non-empty.
58    nonce_delta: Felt,
59}
60
61impl AccountDelta {
62    // CONSTANTS
63    // --------------------------------------------------------------------------------------------
64
65    /// Domain separator for the account delta commitment.
66    ///
67    /// It is placed in the capacity word of the hasher rather than in the hashed elements, so that
68    /// it stays fixed even as the layout of those elements evolves across versions. The value is
69    /// allocated from the range that the [Poseidon2 domain registry][registry] delegates to this
70    /// repository.
71    ///
72    /// [registry]: https://github.com/0xMiden/crypto/blob/main/docs/registry/poseidon2-domains.toml
73    const DOMAIN: Felt = Felt::new_unchecked(0x02_0001);
74
75    /// Version 1 of the account delta commitment layout.
76    ///
77    /// The version occupies the first element of the commitment header, so a reader can get it
78    /// before it interprets the rest of the commitment.
79    const VERSION_1: u8 = 1;
80
81    // CONSTRUCTOR
82    // --------------------------------------------------------------------------------------------
83
84    /// Returns new [AccountDelta] instantiated from the provided components.
85    ///
86    /// `code` is `Some` for a full state delta (a new account) and `None` otherwise.
87    ///
88    /// # Errors
89    ///
90    /// - Returns an error if storage or vault were updated, but the nonce_delta is 0.
91    /// - Returns an error if `code` is provided but the storage patch contains an `Update` or
92    ///   `Remove` operation. A full state delta must reconstruct the account from empty storage, so
93    ///   it may only create slots.
94    pub fn new(
95        account_id: AccountId,
96        storage: AccountStoragePatch,
97        vault: AccountVaultDelta,
98        code: Option<AccountCode>,
99        nonce_delta: Felt,
100    ) -> Result<Self, AccountDeltaError> {
101        // nonce must be updated if either account storage or vault were updated
102        validate_nonce(nonce_delta, &storage, &vault)?;
103
104        // A full state delta (carrying code) must reconstruct the account from empty storage, so it
105        // may only create slots. An `Update` or `Remove` assumes the slot already exists and would
106        // make reconstruction impossible.
107        if code.is_some() && storage.contains_non_create_ops() {
108            return Err(AccountDeltaError::FullStateDeltaContainsNonCreateOp);
109        }
110
111        Ok(Self {
112            account_id,
113            storage,
114            vault,
115            code,
116            nonce_delta,
117        })
118    }
119
120    // PUBLIC MUTATORS
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    // PUBLIC ACCESSORS
129    // --------------------------------------------------------------------------------------------
130
131    /// Returns true if this account delta does not contain any vault, storage or nonce updates.
132    pub fn is_empty(&self) -> bool {
133        self.storage.is_empty() && self.vault.is_empty() && self.nonce_delta == ZERO
134    }
135
136    /// Returns `true` if this delta is a "full state" delta, `false` otherwise, i.e. if it is a
137    /// "partial state" delta.
138    ///
139    /// See the type-level docs for more on this distinction.
140    pub fn is_full_state(&self) -> bool {
141        // TODO(code_upgrades): Change this to another detection mechanism once we have code upgrade
142        // support, at which point the presence of code may not be enough of an indication
143        // that a delta can be converted to a full account.
144        //
145        // The presence of code alone is sufficient to identify a full state delta: the constructor
146        // enforces that a code-carrying delta's storage patch contains only `Create` ops, so it
147        // always reconstructs a full account.
148        self.code.is_some()
149    }
150
151    /// Returns storage updates for this account delta.
152    pub fn storage(&self) -> &AccountStoragePatch {
153        &self.storage
154    }
155
156    /// Returns vault updates for this account delta.
157    pub fn vault(&self) -> &AccountVaultDelta {
158        &self.vault
159    }
160
161    /// Returns the amount by which the nonce was incremented.
162    pub fn nonce_delta(&self) -> Felt {
163        self.nonce_delta
164    }
165
166    /// Returns the account ID to which this delta applies.
167    pub fn id(&self) -> AccountId {
168        self.account_id
169    }
170
171    /// Returns a reference to the account code of this delta, if present.
172    pub fn code(&self) -> Option<&AccountCode> {
173        self.code.as_ref()
174    }
175
176    /// Converts this delta into its individual components.
177    pub fn into_parts(self) -> (AccountStoragePatch, AccountVaultDelta, Option<AccountCode>, Felt) {
178        (self.storage, self.vault, self.code, self.nonce_delta)
179    }
180
181    /// Computes the commitment to the account delta.
182    ///
183    /// ## Computation
184    ///
185    /// The delta commitment is a sequential hash over a vector of field elements which starts out
186    /// empty and is appended to in the following way. If no asset or storage elements were
187    /// appended, the commitment is defined as the empty word. Whenever sorting is expected, it
188    /// is that of a [`Word`]. The hash is domain-separated by the delta's `DOMAIN`, which is
189    /// placed in the capacity word of the hasher.
190    ///
191    /// - Append `[[version = 1, nonce_delta, account_id_suffix, account_id_prefix], EMPTY_WORD]`,
192    ///   where `account_id_{prefix,suffix}` are the prefix and suffix felts of the native account
193    ///   id, `nonce_delta` is the value by which the nonce was incremented, and `version` is the
194    ///   version of this layout.
195    /// - Asset Delta
196    ///   - For each **added** asset, sorted by its asset ID:
197    ///     - Append `[ASSET_ID, ASSET_VALUE]`.
198    ///   - Append `[domain = 1, delta_op = 1, num_added_assets, 0]` if `num_added_assets != 0`
199    ///     where `num_added_assets` is the number of added assets and `delta_op` is set to `1`
200    ///     indicating asset addition.
201    ///   - For each **removed** asset, sorted by its asset ID:
202    ///     - Append `[ASSET_ID, ASSET_VALUE]`.
203    ///   - Append `[domain = 1, delta_op = 2, num_removed_assets, 0]` if `num_removed_assets != 0`
204    ///     where `num_removed_assets` is the number of removed assets and `delta_op` is set to `2`
205    ///     indicating asset removal.
206    ///   - Note that the domain is the same independent of asset addition or removal, since the
207    ///     `delta_op` sufficiently distinguishes the two domains.
208    /// - Storage Slots are sorted by slot ID and are iterated in this order. `patch_op` is the
209    ///   [`StoragePatchOperation`](crate::account::StoragePatchOperation) of the slot patch and
210    ///   `slot_id_{suffix, prefix}` is the identifier of the slot. For each slot, depending on its
211    ///   slot type:
212    ///   - Value Slot
213    ///     - Append `[[domain = 2, patch_op, slot_id_suffix, slot_id_prefix], NEW_VALUE]` where
214    ///       `NEW_VALUE` is the new value 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    ///     - The map trailer is constructed as `[[domain = 3, patch_op, slot_id_suffix,
220    ///       slot_id_prefix], [num_changed_entries, 0, 0, 0]]`, where `num_changed_entries` is the
221    ///       number of key-value pairs appended above. Whether the trailer is included depends on
222    ///       `patch_op`:
223    ///         - For
224    ///           [`StoragePatchOperation::Create`](crate::account::StoragePatchOperation::Create),
225    ///           the trailer is always included, since the slot's creation must be committed to even
226    ///           when the map is created empty (`num_changed_entries == 0`).
227    ///         - For
228    ///           [`StoragePatchOperation::Update`](crate::account::StoragePatchOperation::Update),
229    ///           the trailer is included only if `num_changed_entries != 0`. An update that changes
230    ///           no entries is a no-op and is omitted entirely.
231    ///         - For
232    ///           [`StoragePatchOperation::Remove`](crate::account::StoragePatchOperation::Remove),
233    ///           the trailer is always included with `num_changed_entries` set to zero, since the
234    ///           number of removed entries is unknown.
235    ///
236    /// ## Rationale
237    ///
238    /// The rationale for this layout is that hashing in the VM should be as efficient as possible
239    /// and minimize the number of branches to be as efficient as possible. Every high-level section
240    /// in this bullet point list should add an even number of words since the hasher operates
241    /// on double words. In the VM, each permutation is done immediately, so adding an uneven
242    /// number of words in a given step will result in more difficulty in the MASM implementation.
243    ///
244    /// ### New Accounts
245    ///
246    /// The delta for new accounts (a full state delta) must commit to all the created storage slots
247    /// of the account, even if these slots contain the default value (e.g. the empty word for value
248    /// slots or an empty storage map). This ensures the full state delta commits to the exact
249    /// storage slots that are contained in the account.
250    ///
251    /// ## Security
252    ///
253    /// The general concern with the commitment is that two distinct deltas must never hash to the
254    /// same commitment. E.g. a commitment of a delta that changes a key-value pair in a storage
255    /// map slot should be different from a delta that adds a non-fungible asset to the vault.
256    /// If not, a delta can be crafted in the VM that sets a map key but a malicious actor
257    /// crafts a delta outside the VM that adds a non-fungible asset. To prevent that, a couple
258    /// of measures are taken.
259    ///
260    /// - Because multiple unrelated domains (e.g. vaults and storage slots) are hashed in the same
261    ///   hasher, domain separators are used to disambiguate. For each changed asset and each
262    ///   changed slot in the delta, a domain separator is hashed into the delta. The domain
263    ///   separator is always at the same index in each layout so it cannot be maliciously crafted
264    ///   (see below for an example). These separators only need to be unique _within_ a delta or
265    ///   patch, since the `DOMAIN` of a delta and of a patch already separate the two objects.
266    /// - Storage value slots:
267    ///   - since value slots are only included in the patch if their value has changed when the
268    ///     operation is `Update`, there is no ambiguity between a value slot being set to
269    ///     EMPTY_WORD and its value being unchanged.
270    /// - Storage map slots:
271    ///   - Map slots append a header which summarizes the changes in the slot, in particular the
272    ///     slot ID and number of changed entries.
273    ///   - Two distinct storage map slots use the same domain but are disambiguated due to
274    ///     inclusion of the slot ID.
275    ///
276    /// ### Domain Separators
277    ///
278    /// As an example for ambiguity, consider these two deltas:
279    ///
280    /// ```text
281    /// [
282    ///   METADATA, EMPTY_WORD,
283    ///   [ASSET_ID, ASSET_VALUE],
284    ///   [[domain = 1, delta_op = 1, num_added_assets = 1, 0], EMPTY_WORD],
285    ///   [/* no removed assets delta */],
286    ///   [/* no storage patch */]
287    /// ]
288    /// ```
289    ///
290    /// ```text
291    /// [
292    ///   METADATA, EMPTY_WORD,
293    ///   [/* no asset delta */],
294    ///   [[domain = 2, patch_op, slot_id_suffix0, slot_id_prefix0], NEW_VALUE]
295    ///   [[domain = 2, patch_op, slot_id_suffix1, slot_id_prefix1], NEW_VALUE]
296    /// ]
297    /// ```
298    ///
299    /// - `NEW_VALUE` is user-controlled and can be crafted to match `ASSET_VALUE` or `EMPTY_WORD`.
300    /// - Slot IDs are user-controlled and can be crafted to match the two most significant elements
301    ///   in the asset ID or `num_added_assets` and the fixed 0.
302    /// - This leaves only the domain separator and the patch_op to differentiate these two deltas.
303    ///
304    /// A delta and a patch have identically shaped headers, so their element sequences can be made
305    /// to match. They cannot collide because the delta and the patch commitment use distinct hasher
306    /// capacity domains.
307    ///
308    /// ### Number of Changed Entries
309    ///
310    /// As an example for ambiguity, consider these two deltas:
311    ///
312    /// ```text
313    /// [
314    ///   METADATA, EMPTY_WORD,
315    ///   [/* no asset delta */],
316    ///   [domain = 3, patch_op, slot_id_suffix = 20, slot_id_prefix = 21, num_changed_entries = 0, 0, 0, 0]
317    ///   [domain = 3, patch_op, slot_id_suffix = 42, slot_id_prefix = 43, num_changed_entries = 0, 0, 0, 0]
318    /// ]
319    /// ```
320    ///
321    /// ```text
322    /// [
323    ///   METADATA, EMPTY_WORD,
324    ///   [/* no asset delta */],
325    ///   [KEY0, VALUE0],
326    ///   [domain = 3, patch_op, slot_id_suffix = 42, slot_id_prefix = 43, num_changed_entries = 1, 0, 0, 0]
327    /// ]
328    /// ```
329    ///
330    /// The keys and values of map slots are user-controllable so `KEY0` and `VALUE0` could be
331    /// crafted to match the first map header in the first delta. So, _without_ having
332    /// `num_changed_entries` included in the commitment, these deltas would be ambiguous. A delta
333    /// with two empty maps could have the same commitment as a delta with one map entry where one
334    /// key-value pair has changed.
335    ///
336    /// #### New Accounts
337    ///
338    /// The number of changed entries of a storage map can be validly zero when an empty storage map
339    /// is created in account (e.g. at account creation time). In such cases, the number of changed
340    /// key-value pairs is 0, but the map must still be committed to, in order to differentiate
341    /// between a slot being created as an empty map or not being created at all.
342    pub fn to_commitment(&self) -> Word {
343        <Self as SequentialCommit>::to_commitment(self)
344    }
345}
346
347impl TryFrom<&AccountDelta> for Account {
348    type Error = AccountError;
349
350    /// Converts an [`AccountDelta`] into an [`Account`].
351    ///
352    /// Conceptually, this applies the delta onto an empty account.
353    ///
354    /// # Errors
355    ///
356    /// Returns an error if:
357    /// - If the delta is not a full state delta. See [`AccountDelta`] for details.
358    /// - If any vault delta operation removes an asset.
359    /// - If any vault delta operation adds an asset that would overflow the maximum representable
360    ///   amount.
361    /// - If any storage patch update violates account storage constraints.
362    fn try_from(delta: &AccountDelta) -> Result<Self, Self::Error> {
363        if !delta.is_full_state() {
364            return Err(AccountError::PartialStateDeltaToAccount);
365        }
366
367        let Some(code) = delta.code().cloned() else {
368            return Err(AccountError::PartialStateDeltaToAccount);
369        };
370
371        // The asset vault of a new account is empty, so if the delta contains removed assets, the
372        // delta is invalid.
373        if delta.vault().removed_assets().count() != 0 {
374            return Err(AccountError::AssetsRemovedFromNewAccount);
375        }
376
377        let mut vault = AssetVault::default();
378        for added_asset in delta.vault().added_assets() {
379            vault.insert_asset(added_asset).map_err(AccountError::AssetVaultUpdateError)?;
380        }
381
382        // A full state delta consists of `Create` slot patches, so applying it to empty storage
383        // reconstructs the account's full storage.
384        let mut storage = AccountStorage::default();
385        storage.apply_patch(delta.storage())?;
386
387        // The nonce of the account is the initial nonce of 0 plus the nonce_delta, so the
388        // nonce_delta itself.
389        let nonce = delta.nonce_delta();
390
391        Account::new(delta.id(), vault, storage, code, nonce, None)
392    }
393}
394
395impl SequentialCommit for AccountDelta {
396    type Commitment = Word;
397
398    /// Computes the commitment to the delta, domain-separated by its `DOMAIN`.
399    ///
400    /// See [AccountDelta::to_commitment()] for more details.
401    fn to_commitment(&self) -> Word {
402        let elements = self.to_elements();
403
404        // An empty delta produces no elements and its commitment is defined as the empty word.
405        if elements.is_empty() {
406            return Word::empty();
407        }
408
409        Hasher::hash_elements_in_domain(&elements, Self::DOMAIN)
410    }
411
412    /// Reduces the delta to a sequence of field elements.
413    ///
414    /// See [AccountDelta::to_commitment()] for more details.
415    fn to_elements(&self) -> Vec<Felt> {
416        // The commitment to an empty delta is defined as the empty word.
417        if self.is_empty() {
418            return Vec::new();
419        }
420
421        // Minor optimization: At least 24 elements are always added.
422        let mut elements = Vec::with_capacity(24);
423
424        // Metadata
425        elements.extend_from_slice(&[
426            Felt::from(Self::VERSION_1),
427            self.nonce_delta,
428            self.account_id.suffix(),
429            self.account_id.prefix().as_felt(),
430        ]);
431        elements.extend_from_slice(Word::empty().as_elements());
432
433        // Vault Delta
434        self.vault.append_delta_elements(&mut elements);
435
436        // Storage Patch
437        self.storage.append_patch_elements(&mut elements);
438
439        debug_assert!(
440            elements.len() % (2 * crate::WORD_SIZE) == 0,
441            "expected elements to contain an even number of words, but it contained {} elements",
442            elements.len()
443        );
444
445        elements
446    }
447}
448
449// SERIALIZATION
450// ================================================================================================
451
452impl Serializable for AccountDelta {
453    fn write_into<W: ByteWriter>(&self, target: &mut W) {
454        self.account_id.write_into(target);
455        self.storage.write_into(target);
456        self.vault.write_into(target);
457        self.code.write_into(target);
458        self.nonce_delta.write_into(target);
459    }
460
461    fn get_size_hint(&self) -> usize {
462        self.account_id.get_size_hint()
463            + self.storage.get_size_hint()
464            + self.vault.get_size_hint()
465            + self.code.get_size_hint()
466            + self.nonce_delta.get_size_hint()
467    }
468}
469
470impl Deserializable for AccountDelta {
471    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
472        let account_id = AccountId::read_from(source)?;
473        let storage = AccountStoragePatch::read_from(source)?;
474        let vault = AccountVaultDelta::read_from(source)?;
475        let code = <Option<AccountCode>>::read_from(source)?;
476        let nonce_delta = Felt::read_from(source)?;
477
478        validate_nonce(nonce_delta, &storage, &vault)
479            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
480
481        Ok(Self {
482            account_id,
483            storage,
484            vault,
485            code,
486            nonce_delta,
487        })
488    }
489}
490
491// HELPER FUNCTIONS
492// ================================================================================================
493
494/// Checks if the nonce was updated correctly given the provided storage and vault deltas.
495///
496/// # Errors
497///
498/// Returns an error if:
499/// - storage or vault were updated, but the nonce_delta was set to 0.
500fn validate_nonce(
501    nonce_delta: Felt,
502    storage: &AccountStoragePatch,
503    vault: &AccountVaultDelta,
504) -> Result<(), AccountDeltaError> {
505    if (!storage.is_empty() || !vault.is_empty()) && nonce_delta == ZERO {
506        return Err(AccountDeltaError::NonEmptyStorageOrVaultDeltaWithZeroNonceDelta);
507    }
508
509    Ok(())
510}
511
512// TESTS
513// ================================================================================================
514
515#[cfg(test)]
516mod tests {
517
518    use assert_matches::assert_matches;
519    use rstest::rstest;
520
521    use super::{AccountDelta, AccountStoragePatch, AccountVaultDelta};
522    use crate::account::{
523        Account,
524        AccountCode,
525        AccountId,
526        AccountPatch,
527        AccountStorage,
528        AccountType,
529        AccountVaultPatch,
530        StorageMapKey,
531        StorageMapPatch,
532        StorageSlotName,
533    };
534    use crate::asset::{
535        Asset,
536        AssetVault,
537        FungibleAsset,
538        NonFungibleAsset,
539        NonFungibleAssetDetails,
540    };
541    use crate::crypto::SequentialCommit;
542    use crate::errors::AccountDeltaError;
543    use crate::testing::account_id::{
544        ACCOUNT_ID_PRIVATE_SENDER,
545        ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
546        AccountIdBuilder,
547    };
548    use crate::utils::serde::Serializable;
549    use crate::{Felt, ONE, Word, ZERO};
550
551    #[test]
552    fn empty_account_delta_commitment_is_empty_word() -> anyhow::Result<()> {
553        let empty_delta = AccountDelta::new(
554            AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?,
555            AccountStoragePatch::new(),
556            AccountVaultDelta::default(),
557            None,
558            ZERO,
559        )?;
560        assert_eq!(empty_delta.to_commitment(), Word::empty());
561
562        Ok(())
563    }
564
565    /// A delta and a patch that reduce to identical element sequences still commit to different
566    /// words, because they use distinct hasher domains.
567    #[test]
568    fn account_delta_commitment_domain_separation() -> anyhow::Result<()> {
569        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
570        let nonce = Felt::from(2u8);
571
572        let delta = AccountDelta::new(
573            account_id,
574            AccountStoragePatch::new(),
575            AccountVaultDelta::default(),
576            None,
577            nonce,
578        )?;
579        let patch = AccountPatch::new(
580            account_id,
581            AccountStoragePatch::new(),
582            AccountVaultPatch::default(),
583            None,
584            Some(nonce),
585        )?;
586
587        assert_eq!(delta.to_elements(), patch.to_elements());
588        assert_ne!(delta.to_commitment(), Word::empty());
589        assert_ne!(delta.to_commitment(), patch.to_commitment());
590
591        Ok(())
592    }
593
594    #[test]
595    fn account_delta_nonce_validation() {
596        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
597        // empty delta
598        let storage_patch = AccountStoragePatch::new();
599        let vault_delta = AccountVaultDelta::default();
600
601        AccountDelta::new(account_id, storage_patch.clone(), vault_delta.clone(), None, ZERO)
602            .unwrap();
603        AccountDelta::new(account_id, storage_patch.clone(), vault_delta.clone(), None, ONE)
604            .unwrap();
605
606        // non-empty delta
607        let storage_patch = AccountStoragePatch::from_iters([StorageSlotName::mock(1)], [], []);
608
609        assert_matches!(
610            AccountDelta::new(account_id, storage_patch.clone(), vault_delta.clone(), None, ZERO)
611                .unwrap_err(),
612            AccountDeltaError::NonEmptyStorageOrVaultDeltaWithZeroNonceDelta
613        );
614        AccountDelta::new(account_id, storage_patch.clone(), vault_delta.clone(), None, ONE)
615            .unwrap();
616    }
617
618    /// A full state delta (carrying code) must only contain `Create` storage ops, since an `Update`
619    /// or `Remove` could not be applied to the empty storage of a new account.
620    #[rstest]
621    #[case::update(
622        AccountStoragePatch::builder().update_value(StorageSlotName::mock(1), Word::empty()).build()
623    )]
624    #[case::remove(
625        AccountStoragePatch::builder().remove_value(StorageSlotName::mock(1)).build()
626    )]
627    fn account_delta_new_rejects_full_state_with_non_create_op(
628        #[case] storage: AccountStoragePatch,
629    ) -> anyhow::Result<()> {
630        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
631
632        let error = AccountDelta::new(
633            account_id,
634            storage,
635            AccountVaultDelta::default(),
636            Some(AccountCode::mock()),
637            ONE,
638        )
639        .unwrap_err();
640        assert_matches!(error, AccountDeltaError::FullStateDeltaContainsNonCreateOp);
641
642        Ok(())
643    }
644
645    /// A full state delta whose storage only creates slots can be reconstructed into an account.
646    #[test]
647    fn account_delta_full_state_with_create_reconstructs() -> anyhow::Result<()> {
648        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
649        let code = AccountCode::mock();
650        let created_slot = StorageSlotName::mock(1);
651        let created_value = Word::from([7u32, 0, 0, 0]);
652
653        let storage = AccountStoragePatch::builder()
654            .create_value(created_slot.clone(), created_value)
655            .build();
656
657        let delta = AccountDelta::new(
658            account_id,
659            storage,
660            AccountVaultDelta::default(),
661            Some(code.clone()),
662            ONE,
663        )?;
664        assert!(delta.is_full_state());
665
666        let account = Account::try_from(&delta)?;
667        assert_eq!(account.code(), &code);
668        assert_eq!(account.storage().get_item(&created_slot)?, created_value);
669
670        Ok(())
671    }
672
673    #[test]
674    fn account_delta_size_hint() {
675        // AccountDelta
676        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
677        let storage_patch = AccountStoragePatch::new();
678        let vault_delta = AccountVaultDelta::default();
679        assert_eq!(storage_patch.to_bytes().len(), storage_patch.get_size_hint());
680        assert_eq!(vault_delta.to_bytes().len(), vault_delta.get_size_hint());
681
682        let account_delta =
683            AccountDelta::new(account_id, storage_patch, vault_delta, None, ZERO).unwrap();
684        assert_eq!(account_delta.to_bytes().len(), account_delta.get_size_hint());
685
686        let storage_patch = AccountStoragePatch::from_iters(
687            [StorageSlotName::mock(1)],
688            [
689                (StorageSlotName::mock(2), Word::from([1, 1, 1, 1u32])),
690                (StorageSlotName::mock(3), Word::from([1, 1, 0, 1u32])),
691            ],
692            [(
693                StorageSlotName::mock(4),
694                StorageMapPatch::from_iters(
695                    [
696                        StorageMapKey::from_array([1, 1, 1, 0]),
697                        StorageMapKey::from_array([0, 1, 1, 1]),
698                    ],
699                    [(StorageMapKey::from_array([1, 1, 1, 1]), Word::from([1, 1, 1, 1u32]))],
700                ),
701            )],
702        );
703
704        let non_fungible: Asset = NonFungibleAsset::new(&NonFungibleAssetDetails::new(
705            AccountIdBuilder::new()
706                .account_type(AccountType::Public)
707                .build_with_rng(&mut rand::rng()),
708            vec![6],
709        ))
710        .into();
711        let fungible_2: Asset = FungibleAsset::new(
712            AccountIdBuilder::new()
713                .account_type(AccountType::Public)
714                .build_with_rng(&mut rand::rng()),
715            10,
716        )
717        .unwrap()
718        .into();
719        let vault_delta = AccountVaultDelta::from_iters([non_fungible], [fungible_2]);
720
721        assert_eq!(storage_patch.to_bytes().len(), storage_patch.get_size_hint());
722        assert_eq!(vault_delta.to_bytes().len(), vault_delta.get_size_hint());
723
724        let account_delta =
725            AccountDelta::new(account_id, storage_patch, vault_delta, None, ONE).unwrap();
726        assert_eq!(account_delta.to_bytes().len(), account_delta.get_size_hint());
727
728        // Account
729
730        let account_id =
731            AccountId::try_from(ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE).unwrap();
732
733        let asset_vault = AssetVault::mock();
734        assert_eq!(asset_vault.to_bytes().len(), asset_vault.get_size_hint());
735
736        let account_storage = AccountStorage::mock();
737        assert_eq!(account_storage.to_bytes().len(), account_storage.get_size_hint());
738
739        let account_code = AccountCode::mock();
740        assert_eq!(account_code.to_bytes().len(), account_code.get_size_hint());
741
742        let account =
743            Account::new_existing(account_id, asset_vault, account_storage, account_code, ONE);
744        assert_eq!(account.to_bytes().len(), account.get_size_hint());
745    }
746}