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}