Skip to main content

miden_protocol/account/patch/
mod.rs

1mod code;
2mod vault;
3
4mod storage;
5mod update_details;
6use alloc::string::ToString;
7use alloc::vec::Vec;
8
9pub use code::AccountCodePatch;
10pub use storage::{
11    AccountStoragePatch,
12    StorageMapPatch,
13    StorageMapPatchEntries,
14    StoragePatchOperation,
15    StorageSlotPatch,
16    StorageValuePatch,
17};
18pub use update_details::AccountUpdateDetails;
19pub(crate) use update_details::validate_new_public_account;
20pub use vault::AccountVaultPatch;
21
22use crate::account::{Account, AccountId, AccountStorage};
23use crate::asset::AssetVault;
24use crate::crypto::SequentialCommit;
25use crate::errors::{AccountError, AccountPatchError};
26use crate::utils::serde::{
27    ByteReader,
28    ByteWriter,
29    Deserializable,
30    DeserializationError,
31    Serializable,
32};
33use crate::{Felt, Hasher, Word};
34
35/// An [`AccountPatch`] describes the new absolute state of an account after one or more
36/// transactions, in contrast to an [`AccountDelta`](crate::account::AccountDelta), which describes
37/// the relative change.
38///
39/// For example, where a delta might say "remove 50 USDC from the vault", a patch says "the new
40/// USDC balance is 100". This means a patch can be applied to compute the new account state
41/// without loading the previous state and without invoking any custom asset compose logic (e.g.
42/// merge/split procedures defined by the issuing faucet).
43///
44/// [`Account::apply_patch`]: crate::account::Account::apply_patch
45///
46/// The patch represents updates to the account as follows:
47/// - storage: an [`AccountStoragePatch`] containing the new values of changed storage slots and map
48///   entries. Storage updates are already absolute per changed entry, so no dedicated patch type is
49///   required for storage.
50/// - vault: an [`AccountVaultPatch`] containing the new values of changed vault entries.
51/// - nonce: the new (absolute) nonce of the account, in contrast to
52///   [`AccountDelta::nonce_delta`](crate::account::AccountDelta::nonce_delta) which stores the
53///   increment.
54/// - code: an [`AccountCodePatch`] containing the code of a new or upgraded account.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct AccountPatch {
57    /// The ID of the account to which this patch applies.
58    account_id: AccountId,
59    /// The new values of changed storage slots and map entries.
60    storage: AccountStoragePatch,
61    /// The new values of changed vault entries.
62    vault: AccountVaultPatch,
63    /// The code of a new or upgraded account.
64    code: AccountCodePatch,
65    /// The new (absolute) nonce of the account.
66    ///
67    /// Should be set to `None` if the nonce wasn't updated.
68    final_nonce: Option<Felt>,
69}
70
71impl AccountPatch {
72    // CONSTANTS
73    // --------------------------------------------------------------------------------------------
74
75    /// Domain separator for the account patch commitment.
76    ///
77    /// See [`AccountDelta::DOMAIN`](crate::account::AccountDelta) for why it lives in the capacity
78    /// word and where the value is allocated from.
79    const DOMAIN: Felt = Felt::new_unchecked(0x02_0000);
80
81    /// Version 1 of the account patch commitment layout.
82    ///
83    /// The version occupies the first element of the commitment header, so a reader can get it
84    /// before it interprets the rest of the commitment. Version 0 is unused, which means an
85    /// all-zero word is never a valid header.
86    const VERSION_1: u8 = 1;
87
88    // CONSTRUCTOR
89    // --------------------------------------------------------------------------------------------
90
91    /// Returns a new [`AccountPatch`] instantiated from the provided components.
92    ///
93    /// `final_nonce` must be `Some(non_zero_nonce)` if `storage`, `vault` or `code` contain any
94    /// updates, and can be `None` only for empty patches.
95    ///
96    /// # Errors
97    ///
98    /// Returns an error if:
99    /// - `final_nonce` is `Some(Felt::ZERO)`. The tx kernel guarantees that an updated nonce is at
100    ///   least one, so a zero nonce is never a valid post-tx-state. Empty patches must be
101    ///   constructed with `None` instead.
102    /// - `storage` or `vault` contain updates or code is present but `final_nonce` is `None`. The
103    ///   tx kernel mandates that the nonce is incremented whenever account state changes.
104    /// - `final_nonce` is 1 but `code` is not `Some`. Such a patch describes a new account and
105    ///   should be convertible into a full [`Account`](crate::account::Account), so account code is
106    ///   required.
107    pub fn new(
108        account_id: AccountId,
109        storage: AccountStoragePatch,
110        vault: AccountVaultPatch,
111        code: AccountCodePatch,
112        final_nonce: Option<Felt>,
113    ) -> Result<Self, AccountPatchError> {
114        // New nonce should never be zero as the tx kernel requires that the nonce must be
115        // incremented to at least 1 in the account-creating transaction.
116        // Patches that do not change the account (and the nonce) should pass `None`.
117        if final_nonce.is_some_and(|final_nonce| final_nonce == Felt::ZERO) {
118            return Err(AccountPatchError::FinalNonceIsZero);
119        }
120
121        // If account storage or vault were updated or code is present, the patch represents a state
122        // change and so the nonce cannot be zero. The tx kernel mandates this.
123        if (!storage.is_empty() || !vault.is_empty() || !code.is_empty()) && final_nonce.is_none() {
124            return Err(AccountPatchError::StateChangeRequiresNonceUpdate);
125        }
126
127        // Code must be provided for new accounts to be able to reconstruct the full Account.
128        // New accounts are defined with nonce 0, but here we have the post-creation
129        // final nonce, so we define new accounts as having final_nonce = 1.
130        if final_nonce.is_some_and(|final_nonce| final_nonce == Felt::ONE) && code.is_empty() {
131            return Err(AccountPatchError::CodeMustBeProvidedForNewAccounts);
132        }
133
134        Ok(Self {
135            account_id,
136            storage,
137            vault,
138            code,
139            final_nonce,
140        })
141    }
142
143    /// Returns an empty patch for the provided account ID.
144    pub fn empty(account_id: AccountId) -> Self {
145        AccountPatch::new(
146            account_id,
147            AccountStoragePatch::default(),
148            AccountVaultPatch::default(),
149            AccountCodePatch::default(),
150            None,
151        )
152        .expect("empty patch should be valid")
153    }
154
155    // PUBLIC MUTATORS
156    // --------------------------------------------------------------------------------------------
157
158    /// Merges the `other` [`AccountPatch`] into this one with patch semantics: entries present in
159    /// `other` overwrite their counterparts in `self`, and `other.final_nonce`, if present,
160    /// becomes the new final nonce.
161    ///
162    /// The merge is not commutative: `self` must describe the earlier and `other` the later
163    /// state, so `a.merge(b)` and `b.merge(a)` generally give different results.
164    ///
165    /// Both patches must apply to the same account, and `other.final_nonce` must be greater than
166    /// `self.final_nonce` whenever both are set. `other` can be an aggregate of multiple
167    /// transactions (e.g. of a batch), so the nonce can grow by more than one. Continuity of the
168    /// merged patches is not and cannot be checked here.
169    ///
170    /// If `other` carries code, it replaces the code of `self`, since `other` describes the later
171    /// state.
172    ///
173    /// Empty patches are neutral: merging into an empty `self` adopts `other`, and merging an empty
174    /// `other` is a no-op.
175    ///
176    /// # Errors
177    ///
178    /// Returns an error if:
179    /// - the two patches apply to different accounts.
180    /// - both patches carry a final nonce and the nonce in `other` is not greater than the nonce in
181    ///   `self`.
182    /// - a storage slot is used as different slot types in the two patches.
183    pub fn merge(&mut self, other: Self) -> Result<(), AccountPatchError> {
184        if self.account_id != other.account_id {
185            return Err(AccountPatchError::AccountIdMismatch {
186                expected: self.account_id,
187                actual: other.account_id,
188            });
189        }
190
191        match (self.final_nonce, other.final_nonce) {
192            // Both patches are empty, nothing to merge.
193            (None, None) => return Ok(()),
194
195            // `self` is empty, so `other` becomes the merged result.
196            (None, Some(_)) => {
197                *self = other;
198                return Ok(());
199            },
200
201            // `other` is empty, nothing to merge.
202            (Some(_), None) => return Ok(()),
203
204            (Some(current), Some(new)) => {
205                if new <= current {
206                    return Err(AccountPatchError::NonceMustIncrease { current, new });
207                }
208                self.final_nonce = Some(new);
209            },
210        }
211
212        self.storage.merge(other.storage)?;
213        self.vault.merge(other.vault);
214        self.code.merge(other.code);
215
216        Ok(())
217    }
218
219    // PUBLIC ACCESSORS
220    // --------------------------------------------------------------------------------------------
221
222    /// Returns the account ID to which this patch applies.
223    pub fn id(&self) -> AccountId {
224        self.account_id
225    }
226
227    /// Returns the storage updates of this patch.
228    pub fn storage(&self) -> &AccountStoragePatch {
229        &self.storage
230    }
231
232    /// Returns the vault updates of this patch.
233    pub fn vault(&self) -> &AccountVaultPatch {
234        &self.vault
235    }
236
237    /// Returns the code updates of this patch.
238    pub fn code(&self) -> &AccountCodePatch {
239        &self.code
240    }
241
242    /// Returns the new (absolute) nonce of the account after this patch is applied, or `None` if
243    /// the nonce wasn't updated.
244    pub fn final_nonce(&self) -> Option<Felt> {
245        self.final_nonce
246    }
247
248    /// Returns true if this account patch does not contain any vault, storage or code updates and
249    /// the nonce wasn't updated.
250    pub fn is_empty(&self) -> bool {
251        // A nonce that wasn't updated means the patch is empty, since the constructor validates
252        // that non-empty storage, vault or code updates must increment the nonce.
253        self.final_nonce.is_none()
254    }
255
256    /// Computes the commitment to the account patch.
257    ///
258    /// This is very similar to
259    /// [`AccountDelta::to_commitment`](crate::account::AccountDelta::to_commitment). See its docs
260    /// for the rationale, security aspects, and other details. The only differences between
261    /// these are:
262    /// - the patch includes the new nonce rather than the nonce delta.
263    /// - The patch includes the new absolute asset values ([`AccountVaultPatch`]) while the delta
264    ///   includes the relative asset changes
265    ///   ([`AccountVaultDelta`](crate::account::AccountVaultDelta)).
266    ///
267    /// ## Computation
268    ///
269    /// The patch commitment is a sequential hash over a vector of field elements which starts out
270    /// empty and is appended to in the following way. If no asset, storage or code elements were
271    /// appended, the commitment is defined as the empty word. Whenever sorting is expected, it is
272    /// that of a [`Word`]. The hash is domain-separated by the patch's `DOMAIN`, which is
273    /// placed in the capacity word of the hasher. This is what distinguishes a patch commitment
274    /// from a delta commitment, whose headers are otherwise identically shaped.
275    ///
276    /// - Append `[[version = 1, final_nonce, account_id_suffix, account_id_prefix], EMPTY_WORD]`,
277    ///   where `account_id_{prefix,suffix}` are the prefix and suffix felts of the native account
278    ///   id, `final_nonce` is the new nonce of the account, and `version` is the version of this
279    ///   layout.
280    /// - Asset Patch
281    ///   - For each asset whose value has changed compared to the initial state of the transaction,
282    ///     including if it was removed, sorted by its asset ID:
283    ///     - Append `[ASSET_ID, ASSET_VALUE_OR_EMPTY_WORD]` which are the key and either the value
284    ///       of the asset (for updates) or the empty word (for removals).
285    ///     - Append `[[domain = 1, num_changed_assets, 0, 0], 0, 0, 0, 0]`, where
286    ///       `num_changed_assets` is the number of assets that were appended. This is the same
287    ///       domain as the delta asset section uses, since the capacity domain already prevents an
288    ///       asset delta and an asset patch from producing the same commitment.
289    /// - Storage Slots are sorted by slot ID and are iterated in this order. `patch_op` is the
290    ///   [`StoragePatchOperation`](crate::account::StoragePatchOperation) of the slot patch and
291    ///   `slot_id_{suffix, prefix}` is the identifier of the slot. For each slot, depending on its
292    ///   slot type:
293    ///   - Value Slot
294    ///     - Append `[[domain = 2, patch_op, slot_id_suffix, slot_id_prefix], NEW_VALUE]` where
295    ///       `NEW_VALUE` is the new value of the slot.
296    ///   - Map Slot
297    ///     - For each key-value pair, sorted by key, whose new value is different from the previous
298    ///       value in the map:
299    ///       - Append `[KEY, NEW_VALUE]`.
300    ///     - The map trailer is constructed as `[[domain = 3, patch_op, slot_id_suffix,
301    ///       slot_id_prefix], [num_changed_entries, 0, 0, 0]]`, where `num_changed_entries` is the
302    ///       number of key-value pairs appended above. Whether the trailer is included depends on
303    ///       `patch_op`:
304    ///         - For
305    ///           [`StoragePatchOperation::Create`](crate::account::StoragePatchOperation::Create),
306    ///           the trailer is always included, since the slot's creation must be committed to even
307    ///           when the map is created empty (`num_changed_entries == 0`).
308    ///         - For
309    ///           [`StoragePatchOperation::Update`](crate::account::StoragePatchOperation::Update),
310    ///           the trailer is included only if `num_changed_entries != 0`. An update that changes
311    ///           no entries is a no-op and is omitted entirely.
312    ///         - For
313    ///           [`StoragePatchOperation::Remove`](crate::account::StoragePatchOperation::Remove),
314    ///           the trailer is always included with `num_changed_entries` set to zero, since the
315    ///           number of removed entries is unknown.
316    /// - If the account is new or its code was upgraded, append `[[domain = 4, 0, 0, 0],
317    ///   CODE_COMMITMENT]`, where `CODE_COMMITMENT` is the commitment of the account code.
318    ///
319    /// Headers for storage map slots and asset patches are appended rather than prepended since the
320    /// tx kernel cannot efficiently get the number of changed entries before the iteration.
321    pub fn to_commitment(&self) -> Word {
322        <Self as SequentialCommit>::to_commitment(self)
323    }
324
325    /// Returns the new [`Account`] created by this patch.
326    ///
327    /// Conceptually, this applies the patch onto an empty account.
328    ///
329    /// # Warning
330    ///
331    /// This method only results in a semantically correct account if the caller knows that the
332    /// patch is from an account-creating transaction itself or resulted from merging patches
333    /// onto the account-creating patch. The method can also succeed on patches coming from
334    /// code-upgrading transactions, but the result will not correspond to a meaningful
335    /// account state. Prefer applying the patch onto the new account against which the
336    /// account-creating transaction was executed.
337    ///
338    /// # Errors
339    ///
340    /// Returns an error if:
341    /// - the patch does not carry account code or a final nonce.
342    /// - the storage patch contains an `Update` or `Remove` operation, which cannot be applied to
343    ///   the empty storage of a new account.
344    /// - applying the vault patch to an empty vault fails.
345    /// - applying the storage patch to empty storage fails.
346    /// - [`Account::new`] fails on the resulting components.
347    pub fn try_to_new_account(&self) -> Result<Account, AccountError> {
348        let (Some(code), Some(nonce)) = (self.code.as_code(), self.final_nonce) else {
349            return Err(AccountError::NewAccountRequiresCodeAndNonce);
350        };
351
352        if self.storage.contains_non_create_ops() {
353            return Err(AccountError::NewAccountStorageRequiresCreateOps);
354        }
355
356        let mut vault = AssetVault::default();
357        vault.apply_patch(&self.vault).map_err(AccountError::AssetVaultUpdateError)?;
358
359        let mut storage = AccountStorage::default();
360        storage.apply_patch(&self.storage)?;
361
362        Account::new(self.account_id, vault, storage, code.clone(), nonce, None)
363    }
364}
365
366impl SequentialCommit for AccountPatch {
367    type Commitment = Word;
368
369    /// Computes the commitment to the patch, domain-separated by its `DOMAIN`.
370    ///
371    /// See [AccountPatch::to_commitment()] for more details.
372    fn to_commitment(&self) -> Word {
373        let elements = self.to_elements();
374
375        // An empty patch produces no elements and its commitment is defined as the empty word.
376        if elements.is_empty() {
377            return Word::empty();
378        }
379
380        Hasher::hash_elements_in_domain(&elements, Self::DOMAIN)
381    }
382
383    /// Reduces the patch to a sequence of field elements.
384    ///
385    /// See [AccountPatch::to_commitment()] for more details.
386    fn to_elements(&self) -> Vec<Felt> {
387        // The commitment to an empty patch is defined as the empty word.
388        if self.is_empty() {
389            return Vec::new();
390        }
391
392        // Minor optimization: At least 8 elements are always added.
393        let mut elements = Vec::with_capacity(8);
394
395        // Metadata
396        let final_nonce = self.final_nonce.expect("non-empty patches should have a new nonce set");
397        elements.extend_from_slice(&[
398            Felt::from(Self::VERSION_1),
399            final_nonce,
400            self.account_id.suffix(),
401            self.account_id.prefix().as_felt(),
402        ]);
403        elements.extend_from_slice(Word::empty().as_elements());
404
405        // Vault patch
406        self.vault.append_patch_elements(&mut elements);
407
408        // Storage Patch
409        self.storage.append_patch_elements(&mut elements);
410
411        // Code
412        self.code.append_patch_elements(&mut elements);
413
414        debug_assert!(
415            elements.len() % (2 * crate::WORD_SIZE) == 0,
416            "expected elements to contain an even number of words, but it contained {} elements",
417            elements.len()
418        );
419
420        elements
421    }
422}
423
424impl Serializable for AccountPatch {
425    fn write_into<W: ByteWriter>(&self, target: &mut W) {
426        self.account_id.write_into(target);
427        self.storage.write_into(target);
428        self.vault.write_into(target);
429        self.code.write_into(target);
430        self.final_nonce.write_into(target);
431    }
432
433    fn get_size_hint(&self) -> usize {
434        self.account_id.get_size_hint()
435            + self.storage.get_size_hint()
436            + self.vault.get_size_hint()
437            + self.code.get_size_hint()
438            + self.final_nonce.get_size_hint()
439    }
440}
441
442impl Deserializable for AccountPatch {
443    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
444        let account_id = AccountId::read_from(source)?;
445        let storage = AccountStoragePatch::read_from(source)?;
446        let vault = AccountVaultPatch::read_from(source)?;
447        let code = AccountCodePatch::read_from(source)?;
448        let final_nonce = <Option<Felt>>::read_from(source)?;
449
450        Self::new(account_id, storage, vault, code, final_nonce)
451            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
452    }
453}
454
455// TESTS
456// ================================================================================================
457
458#[cfg(test)]
459mod tests {
460    use assert_matches::assert_matches;
461    use miden_core::serde::Deserializable;
462    use rstest::rstest;
463
464    use super::{AccountCodePatch, AccountPatch, AccountVaultPatch};
465    use crate::account::{
466        AccountCode,
467        AccountId,
468        AccountStoragePatch,
469        StorageMapKey,
470        StorageMapPatch,
471        StorageSlotName,
472        StorageSlotPatch,
473        StorageValuePatch,
474    };
475    use crate::asset::{Asset, FungibleAsset, NonFungibleAsset};
476    use crate::errors::{AccountError, AccountPatchError};
477    use crate::testing::account_id::{
478        ACCOUNT_ID_PRIVATE_SENDER,
479        ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
480    };
481    use crate::testing::add_component::AddComponent;
482    use crate::testing::noop_auth_component::NoopAuthComponent;
483    use crate::utils::serde::Serializable;
484    use crate::{Felt, Word};
485
486    #[test]
487    fn account_patch_serde() -> anyhow::Result<()> {
488        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
489        let asset_0 = FungibleAsset::mock(100);
490        let asset_1 = FungibleAsset::new(ACCOUNT_ID_PRIVATE_SENDER.try_into()?, 500_000)?.into();
491        let asset_2 = NonFungibleAsset::mock(&[10]);
492        let asset_3 = NonFungibleAsset::mock(&[20]);
493        let vault_patch = AccountVaultPatch::with_assets([asset_0, asset_1, asset_2, asset_3]);
494
495        let storage_patch = AccountStoragePatch::from_iters(
496            [StorageSlotName::mock(1)],
497            [
498                (StorageSlotName::mock(2), Word::from([1, 1, 1, 1u32])),
499                (StorageSlotName::mock(3), Word::from([1, 1, 0, 1u32])),
500            ],
501            [(
502                StorageSlotName::mock(4),
503                StorageMapPatch::from_iters(
504                    [
505                        StorageMapKey::from_array([1, 1, 1, 0]),
506                        StorageMapKey::from_array([0, 1, 1, 1]),
507                    ],
508                    [(StorageMapKey::from_array([1, 1, 1, 1]), Word::from([1, 1, 1, 1u32]))],
509                ),
510            )],
511        );
512
513        assert_eq!(storage_patch.to_bytes().len(), storage_patch.get_size_hint());
514        assert_eq!(vault_patch.to_bytes().len(), vault_patch.get_size_hint());
515
516        let account_patch = AccountPatch::new(
517            account_id,
518            storage_patch,
519            vault_patch,
520            AccountCodePatch::default(),
521            Some(Felt::from(5u8)),
522        )?;
523        assert_eq!(AccountPatch::read_from_bytes(&account_patch.to_bytes())?, account_patch);
524        assert_eq!(account_patch.to_bytes().len(), account_patch.get_size_hint());
525
526        Ok(())
527    }
528
529    /// A `final_nonce` set to `Some(Felt::ZERO)` is rejected: the tx kernel guarantees the nonce of
530    /// an updated account is at least one, so empty patches must pass `None` instead.
531    #[test]
532    fn account_patch_final_nonce_is_zero() -> anyhow::Result<()> {
533        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
534
535        let error = AccountPatch::new(
536            account_id,
537            AccountStoragePatch::new(),
538            AccountVaultPatch::default(),
539            AccountCodePatch::default(),
540            Some(Felt::ZERO),
541        )
542        .unwrap_err();
543
544        assert_matches!(error, AccountPatchError::FinalNonceIsZero);
545
546        Ok(())
547    }
548
549    /// A patch that updates storage, the vault, or carries code but leaves `final_nonce` as `None`
550    /// is rejected, since any account state change requires the nonce to be incremented.
551    #[rstest::rstest]
552    #[case::non_empty_storage(
553        AccountStoragePatch::from_iters([StorageSlotName::mock(1)], [], []),
554        AccountVaultPatch::default(),
555        AccountCodePatch::default(),
556    )]
557    #[case::non_empty_vault(
558        AccountStoragePatch::new(),
559        AccountVaultPatch::with_assets([FungibleAsset::mock(100)]),
560        AccountCodePatch::default(),
561    )]
562    #[case::non_empty_code(
563        AccountStoragePatch::new(),
564        AccountVaultPatch::default(),
565        AccountCodePatch::new(Some(AccountCode::mock()))
566    )]
567    #[test]
568    fn account_patch_with_state_change_requires_nonce_update(
569        #[case] storage: AccountStoragePatch,
570        #[case] vault: AccountVaultPatch,
571        #[case] code: AccountCodePatch,
572    ) -> anyhow::Result<()> {
573        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
574
575        let error = AccountPatch::new(account_id, storage, vault, code, None).unwrap_err();
576        assert_matches!(error, AccountPatchError::StateChangeRequiresNonceUpdate);
577
578        Ok(())
579    }
580
581    /// A patch for a newly created account (`final_nonce = Some(Felt::ONE)`) must include the
582    /// account code, since otherwise the full account cannot be reconstructed from the patch.
583    #[test]
584    fn account_patch_new_account_requires_code() -> anyhow::Result<()> {
585        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
586
587        let error = AccountPatch::new(
588            account_id,
589            AccountStoragePatch::new(),
590            AccountVaultPatch::default(),
591            AccountCodePatch::default(),
592            Some(Felt::ONE),
593        )
594        .unwrap_err();
595        assert_matches!(error, AccountPatchError::CodeMustBeProvidedForNewAccounts);
596
597        // With the code provided, the same patch should succeed.
598        AccountPatch::new(
599            account_id,
600            AccountStoragePatch::new(),
601            AccountVaultPatch::default(),
602            AccountCodePatch::new(Some(AccountCode::mock())),
603            Some(Felt::ONE),
604        )?;
605
606        Ok(())
607    }
608
609    /// A patch carrying account code and a final nonce can be converted to an [`Account`] and back,
610    /// preserving all components.
611    #[test]
612    fn account_patch_roundtrip() -> anyhow::Result<()> {
613        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
614        let code = AccountCode::mock();
615        let asset = FungibleAsset::mock(42);
616
617        let slot_name = StorageSlotName::mock(4);
618        let slot_value = Word::from([1, 2, 3, 4u32]);
619
620        // A creation patch is composed of `Create` slot patches.
621        let storage_patch = AccountStoragePatch::from_entries([(
622            slot_name.clone(),
623            StorageSlotPatch::Value(StorageValuePatch::Create { value: slot_value }),
624        )])?;
625
626        let patch = AccountPatch::new(
627            account_id,
628            storage_patch,
629            AccountVaultPatch::with_assets([asset]),
630            AccountCodePatch::new(Some(code.clone())),
631            Some(Felt::ONE),
632        )?;
633
634        let account = patch.try_to_new_account()?;
635
636        assert_eq!(account.id(), account_id);
637        assert_eq!(account.code(), &code);
638        assert_eq!(account.nonce(), Felt::ONE);
639        assert_eq!(account.storage().get_item(&slot_name)?, slot_value);
640        assert_eq!(account.vault().get(asset.id()), Some(asset));
641
642        // Roundtrip back to a patch should reproduce the original.
643        let roundtripped_patch = AccountPatch::try_from(account)?;
644        assert_eq!(roundtripped_patch, patch);
645
646        Ok(())
647    }
648
649    /// A patch lacking code cannot be converted to an [`Account`], whether or not a final nonce
650    /// is present.
651    #[rstest::rstest]
652    #[case::missing_code(Some(Felt::from(2_u32)))]
653    #[case::empty_patch(None)]
654    #[test]
655    fn account_patch_try_to_new_account_requires_code(
656        #[case] final_nonce: Option<Felt>,
657    ) -> anyhow::Result<()> {
658        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
659
660        let patch = AccountPatch::new(
661            account_id,
662            AccountStoragePatch::new(),
663            AccountVaultPatch::default(),
664            AccountCodePatch::default(),
665            final_nonce,
666        )?;
667        assert_matches!(
668            patch.try_to_new_account().unwrap_err(),
669            AccountError::NewAccountRequiresCodeAndNonce
670        );
671
672        Ok(())
673    }
674
675    /// A patch carrying code may contain `Update` or `Remove` storage ops, since it can describe a
676    /// code upgrade. Such a patch cannot create an account, since the ops cannot be applied to the
677    /// empty storage of a new account.
678    #[rstest]
679    #[case::update(
680        AccountStoragePatch::builder().update_value(StorageSlotName::mock(1), Word::empty()).build()
681    )]
682    #[case::remove(
683        AccountStoragePatch::builder().remove_value(StorageSlotName::mock(1)).build()
684    )]
685    fn account_patch_try_to_new_account_rejects_non_create_op(
686        #[case] storage: AccountStoragePatch,
687    ) -> anyhow::Result<()> {
688        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
689
690        let patch = AccountPatch::new(
691            account_id,
692            storage,
693            AccountVaultPatch::default(),
694            AccountCodePatch::new(Some(AccountCode::mock())),
695            Some(Felt::from(2u32)),
696        )?;
697        assert_matches!(
698            patch.try_to_new_account().unwrap_err(),
699            AccountError::NewAccountStorageRequiresCreateOps
700        );
701
702        Ok(())
703    }
704
705    /// A patch whose storage only creates slots can be reconstructed into an account.
706    #[test]
707    fn account_patch_try_to_new_account_with_create_reconstructs() -> anyhow::Result<()> {
708        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
709        let code = AccountCode::mock();
710        let created_slot = StorageSlotName::mock(1);
711        let created_value = Word::from([7u32, 0, 0, 0]);
712
713        let storage = AccountStoragePatch::builder()
714            .create_value(created_slot.clone(), created_value)
715            .build();
716
717        let patch = AccountPatch::new(
718            account_id,
719            storage,
720            AccountVaultPatch::default(),
721            AccountCodePatch::new(Some(code.clone())),
722            Some(Felt::ONE),
723        )?;
724
725        let account = patch.try_to_new_account()?;
726        assert_eq!(account.code(), &code);
727        assert_eq!(account.storage().get_item(&created_slot)?, created_value);
728
729        Ok(())
730    }
731
732    // MERGE TESTS
733    // ============================================================================================
734
735    /// Returns account code that differs from [`AccountCode::mock`].
736    fn upgraded_code() -> anyhow::Result<AccountCode> {
737        Ok(AccountCode::from_components(&[NoopAuthComponent.into(), AddComponent.into()])?)
738    }
739
740    /// Returns a patch with a single updated value slot and the provided final nonce.
741    fn update_patch(account_id: AccountId, final_nonce: u32) -> anyhow::Result<AccountPatch> {
742        let storage = AccountStoragePatch::from_iters(
743            [],
744            [(StorageSlotName::mock(1), Word::from([1u32, 0, 0, 0]))],
745            [],
746        );
747        AccountPatch::new(
748            account_id,
749            storage,
750            AccountVaultPatch::default(),
751            AccountCodePatch::default(),
752            Some(Felt::from(final_nonce)),
753        )
754        .map_err(Into::into)
755    }
756
757    #[test]
758    fn account_patch_merge_rejects_id_mismatch() -> anyhow::Result<()> {
759        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
760        let other_account_id =
761            AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE)?;
762
763        let mut patch = update_patch(account_id, 2)?;
764        let other = update_patch(other_account_id, 3)?;
765
766        assert_matches!(
767            patch.merge(other).unwrap_err(),
768            AccountPatchError::AccountIdMismatch { expected, actual } => {
769                assert_eq!(expected, account_id);
770                assert_eq!(actual, other_account_id);
771            }
772        );
773
774        Ok(())
775    }
776
777    /// Merging a patch that upgrades the code replaces the code of the base and keeps the storage
778    /// updates of both patches.
779    #[test]
780    fn account_patch_merge_replaces_code() -> anyhow::Result<()> {
781        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
782        let code = upgraded_code()?;
783        let updated_slot = StorageSlotName::mock(2);
784        let updated_value = Word::from([2u32, 0, 0, 0]);
785
786        let mut patch = update_patch(account_id, 2)?;
787        let other = AccountPatch::new(
788            account_id,
789            AccountStoragePatch::builder()
790                .update_value(updated_slot.clone(), updated_value)
791                .build(),
792            AccountVaultPatch::default(),
793            AccountCodePatch::new(Some(code.clone())),
794            Some(Felt::from(3u32)),
795        )?;
796
797        patch.merge(other)?;
798
799        assert_eq!(patch.code().as_code(), Some(&code));
800        assert_eq!(patch.final_nonce(), Some(Felt::from(3u32)));
801        assert_eq!(patch.storage().num_slots(), 2);
802        assert_eq!(patch.storage().updated_value(&updated_slot), Some(updated_value));
803
804        Ok(())
805    }
806
807    #[rstest::rstest]
808    #[case::equal(3, 3)]
809    #[case::smaller(3, 2)]
810    fn account_patch_merge_rejects_non_increasing_nonce(
811        #[case] self_nonce: u32,
812        #[case] other_nonce: u32,
813    ) -> anyhow::Result<()> {
814        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
815        let mut patch = update_patch(account_id, self_nonce)?;
816        let other = update_patch(account_id, other_nonce)?;
817
818        assert_matches!(
819            patch.merge(other).unwrap_err(),
820            AccountPatchError::NonceMustIncrease { current, new } => {
821                assert_eq!(current, Felt::from(self_nonce));
822                assert_eq!(new, Felt::from(other_nonce));
823            }
824        );
825
826        Ok(())
827    }
828
829    #[test]
830    fn account_patch_merge_rejects_storage_slot_type_conflict() -> anyhow::Result<()> {
831        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
832        let shared_slot = StorageSlotName::mock(7);
833
834        let value_storage = AccountStoragePatch::from_iters(
835            [],
836            [(shared_slot.clone(), Word::from([9u32, 0, 0, 0]))],
837            [],
838        );
839        let map_storage = AccountStoragePatch::from_iters(
840            [],
841            [],
842            [(shared_slot.clone(), StorageMapPatch::from_iters([], []))],
843        );
844
845        let mut patch = AccountPatch::new(
846            account_id,
847            value_storage,
848            AccountVaultPatch::default(),
849            AccountCodePatch::default(),
850            Some(Felt::from(2u32)),
851        )?;
852        let other = AccountPatch::new(
853            account_id,
854            map_storage,
855            AccountVaultPatch::default(),
856            AccountCodePatch::default(),
857            Some(Felt::from(3u32)),
858        )?;
859
860        assert_matches!(
861            patch.merge(other).unwrap_err(),
862            AccountPatchError::StorageSlotUsedAsDifferentTypes(slot) => {
863                assert_eq!(slot, shared_slot);
864            }
865        );
866
867        Ok(())
868    }
869
870    #[test]
871    fn account_patch_merge_overrides_vault_entry() -> anyhow::Result<()> {
872        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
873        let asset_initial: Asset = FungibleAsset::mock(100);
874        let asset_updated: Asset = FungibleAsset::mock(250);
875        assert_eq!(asset_initial.id(), asset_updated.id());
876
877        let mut patch = AccountPatch::new(
878            account_id,
879            AccountStoragePatch::new(),
880            AccountVaultPatch::with_assets([asset_initial]),
881            AccountCodePatch::default(),
882            Some(Felt::from(2u32)),
883        )?;
884        let other = AccountPatch::new(
885            account_id,
886            AccountStoragePatch::new(),
887            AccountVaultPatch::with_assets([asset_updated]),
888            AccountCodePatch::default(),
889            Some(Felt::from(3u32)),
890        )?;
891
892        patch.merge(other)?;
893
894        assert_eq!(patch.vault().num_assets(), 1);
895        assert_eq!(
896            patch.vault().as_map().get(&asset_updated.id()).copied(),
897            Some(asset_updated.to_value_word())
898        );
899
900        Ok(())
901    }
902
903    #[test]
904    fn account_patch_merge_overrides_storage_value() -> anyhow::Result<()> {
905        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
906        let slot_name = StorageSlotName::mock(1);
907        let initial_value = Word::from([1u32, 0, 0, 0]);
908        let updated_value = Word::from([2u32, 0, 0, 0]);
909
910        let mut patch = AccountPatch::new(
911            account_id,
912            AccountStoragePatch::from_iters([], [(slot_name.clone(), initial_value)], []),
913            AccountVaultPatch::default(),
914            AccountCodePatch::default(),
915            Some(Felt::from(2u32)),
916        )?;
917        let other = AccountPatch::new(
918            account_id,
919            AccountStoragePatch::from_iters([], [(slot_name.clone(), updated_value)], []),
920            AccountVaultPatch::default(),
921            AccountCodePatch::default(),
922            Some(Felt::from(3u32)),
923        )?;
924
925        patch.merge(other)?;
926
927        assert_eq!(patch.storage().num_slots(), 1);
928        assert_eq!(patch.storage().updated_value(&slot_name), Some(updated_value));
929
930        Ok(())
931    }
932
933    #[test]
934    fn account_patch_merge_extends_storage_map() -> anyhow::Result<()> {
935        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
936        let map_slot = StorageSlotName::mock(1);
937        let key_self = StorageMapKey::from_array([1, 0, 0, 0]);
938        let value_self = Word::from([10u32, 0, 0, 0]);
939        let key_other = StorageMapKey::from_array([2, 0, 0, 0]);
940        let value_other = Word::from([20u32, 0, 0, 0]);
941
942        let mut patch = AccountPatch::new(
943            account_id,
944            AccountStoragePatch::from_iters(
945                [],
946                [],
947                [(map_slot.clone(), StorageMapPatch::from_iters([], [(key_self, value_self)]))],
948            ),
949            AccountVaultPatch::default(),
950            AccountCodePatch::default(),
951            Some(Felt::from(2u32)),
952        )?;
953        let other = AccountPatch::new(
954            account_id,
955            AccountStoragePatch::from_iters(
956                [],
957                [],
958                [(map_slot.clone(), StorageMapPatch::from_iters([], [(key_other, value_other)]))],
959            ),
960            AccountVaultPatch::default(),
961            AccountCodePatch::default(),
962            Some(Felt::from(3u32)),
963        )?;
964
965        patch.merge(other)?;
966
967        assert_eq!(patch.storage().num_slots(), 1);
968        assert_eq!(patch.storage().updated_map(&map_slot).unwrap().num_entries(), 2);
969        assert_eq!(patch.storage().updated_map_item(&map_slot, &key_self), Some(value_self));
970        assert_eq!(patch.storage().updated_map_item(&map_slot, &key_other), Some(value_other));
971
972        Ok(())
973    }
974
975    /// A creation patch as the merge base, with a patch updating one of its created slots, stays a
976    /// creation patch carrying only `Create` ops.
977    #[test]
978    fn account_patch_merge_creation_base_stays_creation_patch() -> anyhow::Result<()> {
979        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
980        let code = AccountCode::mock();
981        let slot_name = StorageSlotName::mock(1);
982        let created_value = Word::from([1u32, 0, 0, 0]);
983        let updated_value = Word::from([2u32, 0, 0, 0]);
984
985        // Creation base: a created value slot, code and the account-creation nonce of 1.
986        let mut patch = AccountPatch::new(
987            account_id,
988            AccountStoragePatch::builder()
989                .create_value(slot_name.clone(), created_value)
990                .build(),
991            AccountVaultPatch::default(),
992            AccountCodePatch::new(Some(code.clone())),
993            Some(Felt::ONE),
994        )?;
995
996        // Patch updating the same slot in the next transaction.
997        let other = AccountPatch::new(
998            account_id,
999            AccountStoragePatch::builder()
1000                .update_value(slot_name.clone(), updated_value)
1001                .build(),
1002            AccountVaultPatch::default(),
1003            AccountCodePatch::default(),
1004            Some(Felt::from(2u32)),
1005        )?;
1006
1007        patch.merge(other)?;
1008
1009        assert_eq!(patch.code().as_code(), Some(&code));
1010        assert_eq!(patch.final_nonce(), Some(Felt::from(2u32)));
1011        assert!(!patch.storage().contains_non_create_ops());
1012        assert_eq!(patch.storage().created_value(&slot_name), Some(updated_value));
1013
1014        Ok(())
1015    }
1016
1017    /// A + B_empty = A
1018    #[test]
1019    fn account_patch_merge_empty_other_is_noop() -> anyhow::Result<()> {
1020        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
1021        let mut patch = update_patch(account_id, 4)?;
1022        let snapshot = patch.clone();
1023
1024        let empty = AccountPatch::empty(account_id);
1025
1026        patch.merge(empty)?;
1027        assert_eq!(patch, snapshot);
1028
1029        Ok(())
1030    }
1031
1032    /// A_empty + B = B
1033    #[test]
1034    fn account_patch_merge_empty_self_adopts_other() -> anyhow::Result<()> {
1035        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
1036        let mut empty = AccountPatch::empty(account_id);
1037        let other = update_patch(account_id, 7)?;
1038        let expected = other.clone();
1039
1040        empty.merge(other)?;
1041        assert_eq!(empty, expected);
1042
1043        Ok(())
1044    }
1045}