Skip to main content

miden_protocol/account/delta/
mod.rs

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