miden_protocol/account/delta/mod.rs
1use alloc::string::ToString;
2use alloc::vec::Vec;
3
4use crate::account::{
5 Account,
6 AccountCode,
7 AccountId,
8 AccountStorage,
9 StorageSlot,
10 StorageSlotType,
11};
12use crate::asset::AssetVault;
13use crate::crypto::SequentialCommit;
14use crate::errors::{AccountDeltaError, AccountError};
15use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable};
16use crate::{Felt, Word, ZERO};
17
18mod storage;
19pub use storage::{AccountStorageDelta, StorageMapDelta, StorageSlotDelta};
20
21mod vault;
22pub use vault::{
23 AccountVaultDelta,
24 FungibleAssetDelta,
25 NonFungibleAssetDelta,
26 NonFungibleDeltaAction,
27};
28
29// ACCOUNT DELTA
30// ================================================================================================
31
32/// The [`AccountDelta`] stores the differences between two account states, which can result from
33/// one or more transaction.
34///
35/// The differences are represented as follows:
36/// - storage: an [`AccountStorageDelta`] that contains the changes to the account storage.
37/// - vault: an [`AccountVaultDelta`] object that contains the changes to the account vault.
38/// - nonce: if the nonce of the account has changed, the _delta_ of the nonce is stored, i.e. the
39/// value by which the nonce increased.
40/// - code: an [`AccountCode`] for new accounts and `None` for others.
41///
42/// The presence of the code in a delta signals if the delta is a _full state_ or _partial state_
43/// delta. A full state delta must be converted into an [`Account`] object, while a partial state
44/// delta must be applied to an existing [`Account`].
45///
46/// TODO(code_upgrades): The ability to track account code updates is an outstanding feature. For
47/// that reason, the account code is not considered as part of the "nonce must be incremented if
48/// state changed" check.
49#[derive(Clone, Debug, PartialEq, Eq)]
50pub struct AccountDelta {
51 /// The ID of the account to which this delta applies. If the delta is created during
52 /// transaction execution, that is the native account of the transaction.
53 account_id: AccountId,
54 /// The delta of the account's storage.
55 storage: AccountStorageDelta,
56 /// The delta of the account's asset vault.
57 vault: AccountVaultDelta,
58 /// The code of a new account (`Some`) or `None` for existing accounts.
59 code: Option<AccountCode>,
60 /// The value by which the nonce was incremented. Must be greater than zero if storage or vault
61 /// are non-empty.
62 nonce_delta: Felt,
63}
64
65impl AccountDelta {
66 // CONSTRUCTOR
67 // --------------------------------------------------------------------------------------------
68
69 /// Returns new [AccountDelta] instantiated from the provided components.
70 ///
71 /// # Errors
72 ///
73 /// - Returns an error if storage or vault were updated, but the nonce_delta is 0.
74 pub fn new(
75 account_id: AccountId,
76 storage: AccountStorageDelta,
77 vault: AccountVaultDelta,
78 nonce_delta: Felt,
79 ) -> Result<Self, AccountDeltaError> {
80 // nonce must be updated if either account storage or vault were updated
81 validate_nonce(nonce_delta, &storage, &vault)?;
82
83 Ok(Self {
84 account_id,
85 storage,
86 vault,
87 code: None,
88 nonce_delta,
89 })
90 }
91
92 // PUBLIC MUTATORS
93 // --------------------------------------------------------------------------------------------
94
95 /// Merge another [AccountDelta] into this one.
96 pub fn merge(&mut self, other: Self) -> Result<(), AccountDeltaError> {
97 let new_nonce_delta = self.nonce_delta + other.nonce_delta;
98
99 if new_nonce_delta.as_int() < self.nonce_delta.as_int() {
100 return Err(AccountDeltaError::NonceIncrementOverflow {
101 current: self.nonce_delta,
102 increment: other.nonce_delta,
103 new: new_nonce_delta,
104 });
105 }
106
107 // TODO(code_upgrades): This should go away once we have proper account code updates in
108 // deltas. Then, the two code updates can be merged. For now, code cannot be merged
109 // and this should never happen.
110 if self.is_full_state() && other.is_full_state() {
111 return Err(AccountDeltaError::MergingFullStateDeltas);
112 }
113
114 if let Some(code) = other.code {
115 self.code = Some(code);
116 }
117
118 self.nonce_delta = new_nonce_delta;
119
120 self.storage.merge(other.storage)?;
121 self.vault.merge(other.vault)
122 }
123
124 /// Returns a mutable reference to the account vault delta.
125 pub fn vault_mut(&mut self) -> &mut AccountVaultDelta {
126 &mut self.vault
127 }
128
129 /// Sets the [`AccountCode`] of the delta.
130 pub fn with_code(mut self, code: Option<AccountCode>) -> Self {
131 self.code = code;
132 self
133 }
134
135 // PUBLIC ACCESSORS
136 // --------------------------------------------------------------------------------------------
137
138 /// Returns true if this account delta does not contain any vault, storage or nonce updates.
139 pub fn is_empty(&self) -> bool {
140 self.storage.is_empty() && self.vault.is_empty() && self.nonce_delta == ZERO
141 }
142
143 /// Returns `true` if this delta is a "full state" delta, `false` otherwise, i.e. if it is a
144 /// "partial state" delta.
145 ///
146 /// See the type-level docs for more on this distinction.
147 pub fn is_full_state(&self) -> bool {
148 // TODO(code_upgrades): Change this to another detection mechanism once we have code upgrade
149 // support, at which point the presence of code may not be enough of an indication
150 // that a delta can be converted to a full account.
151 self.code.is_some()
152 }
153
154 /// Returns storage updates for this account delta.
155 pub fn storage(&self) -> &AccountStorageDelta {
156 &self.storage
157 }
158
159 /// Returns vault updates for this account delta.
160 pub fn vault(&self) -> &AccountVaultDelta {
161 &self.vault
162 }
163
164 /// Returns the amount by which the nonce was incremented.
165 pub fn nonce_delta(&self) -> Felt {
166 self.nonce_delta
167 }
168
169 /// Returns the account ID to which this delta applies.
170 pub fn id(&self) -> AccountId {
171 self.account_id
172 }
173
174 /// Returns a reference to the account code of this delta, if present.
175 pub fn code(&self) -> Option<&AccountCode> {
176 self.code.as_ref()
177 }
178
179 /// Converts this storage delta into individual delta components.
180 pub fn into_parts(self) -> (AccountStorageDelta, AccountVaultDelta, Option<AccountCode>, Felt) {
181 (self.storage, self.vault, self.code, self.nonce_delta)
182 }
183
184 /// Computes the commitment to the account delta.
185 ///
186 /// ## Computation
187 ///
188 /// The delta is a sequential hash over a vector of field elements which starts out empty and
189 /// is appended to in the following way. Whenever sorting is expected, it is that of a
190 /// [`LexicographicWord`](crate::LexicographicWord). The WORD layout is in memory-order.
191 ///
192 /// - Append `[[nonce_delta, 0, account_id_suffix, account_id_prefix], EMPTY_WORD]`, where
193 /// account_id_{prefix,suffix} are the prefix and suffix felts of the native account id and
194 /// nonce_delta is the value by which the nonce was incremented.
195 /// - Fungible Asset Delta
196 /// - For each **updated** fungible asset, sorted by its vault key, whose amount delta is
197 /// **non-zero**:
198 /// - Append `[domain = 1, was_added, 0, 0]`.
199 /// - Append `[amount, 0, faucet_id_suffix, faucet_id_prefix]` where amount is the delta by
200 /// which the fungible asset's amount has changed and was_added is a boolean flag
201 /// indicating whether the amount was added (1) or subtracted (0).
202 /// - Non-Fungible Asset Delta
203 /// - For each **updated** non-fungible asset, sorted by its vault key:
204 /// - Append `[domain = 1, was_added, 0, 0]` where was_added is a boolean flag indicating
205 /// whether the asset was added (1) or removed (0). Note that the domain is the same for
206 /// assets since `faucet_id_prefix` is at the same position in the layout for both assets,
207 /// and, by design, it is never the same for fungible and non-fungible assets.
208 /// - Append `[hash0, hash1, hash2, faucet_id_prefix]`, i.e. the non-fungible asset.
209 /// - Storage Slots are sorted by slot ID and are iterated in this order. For each slot **whose
210 /// value has changed**, depending on the slot type:
211 /// - Value Slot
212 /// - Append `[[domain = 2, 0, slot_id_suffix, slot_id_prefix], NEW_VALUE]` where
213 /// `NEW_VALUE` is the new value of the slot and `slot_id_{suffix, prefix}` is the
214 /// identifier of the slot.
215 /// - Map Slot
216 /// - For each key-value pair, sorted by key, whose new value is different from the previous
217 /// value in the map:
218 /// - Append `[KEY, NEW_VALUE]`.
219 /// - Append `[[domain = 3, num_changed_entries, slot_id_suffix, slot_id_prefix], 0, 0, 0,
220 /// 0]`, where `slot_id_{suffix, prefix}` are the slot identifiers and
221 /// `num_changed_entries` is the number of changed key-value pairs in the map.
222 /// - For partial state deltas, the map header must only be included if
223 /// `num_changed_entries` is not zero.
224 /// - For full state deltas, the map header must always be included.
225 ///
226 /// ## Rationale
227 ///
228 /// The rationale for this layout is that hashing in the VM should be as efficient as possible
229 /// and minimize the number of branches to be as efficient as possible. Every high-level section
230 /// in this bullet point list should add an even number of words since the hasher operates
231 /// on double words. In the VM, each permutation is done immediately, so adding an uneven
232 /// number of words in a given step will result in more difficulty in the MASM implementation.
233 ///
234 /// ### New Accounts
235 ///
236 /// The delta for new accounts (a full state delta) must commit to all the storage slots of the
237 /// account, even if the storage slots have a default value (e.g. the empty word for value slots
238 /// or an empty storage map). This ensures the full state delta commits to the exact storage
239 /// slots that are contained in the account.
240 ///
241 /// ## Security
242 ///
243 /// The general concern with the commitment is that two distinct deltas must never hash to the
244 /// same commitment. E.g. a commitment of a delta that changes a key-value pair in a storage
245 /// map slot should be different from a delta that adds a non-fungible asset to the vault.
246 /// If not, a delta can be crafted in the VM that sets a map key but a malicious actor
247 /// crafts a delta outside the VM that adds a non-fungible asset. To prevent that, a couple
248 /// of measures are taken.
249 ///
250 /// - Because multiple unrelated contexts (e.g. vaults and storage slots) are hashed in the same
251 /// hasher, domain separators are used to disambiguate. For each changed asset and each
252 /// changed slot in the delta, a domain separator is hashed into the delta. The domain
253 /// separator is always at the same index in each layout so it cannot be maliciously crafted
254 /// (see below for an example).
255 /// - Storage value slots:
256 /// - since only changed value slots are included in the delta, there is no ambiguity between
257 /// a value slot being set to EMPTY_WORD and its value being unchanged.
258 /// - Storage map slots:
259 /// - Map slots append a header which summarizes the changes in the slot, in particular the
260 /// slot ID and number of changed entries.
261 /// - Two distinct storage map slots use the same domain but are disambiguated due to
262 /// inclusion of the slot ID.
263 ///
264 /// ### Domain Separators
265 ///
266 /// As an example for ambiguity, consider these two deltas:
267 ///
268 /// ```text
269 /// [
270 /// ID_AND_NONCE, EMPTY_WORD,
271 /// [/* no fungible asset delta */],
272 /// [[domain = 1, was_added = 0, 0, 0], NON_FUNGIBLE_ASSET],
273 /// [/* no storage delta */]
274 /// ]
275 /// ```
276 ///
277 /// ```text
278 /// [
279 /// ID_AND_NONCE, EMPTY_WORD,
280 /// [/* no fungible asset delta */],
281 /// [/* no non-fungible asset delta */],
282 /// [[domain = 2, 0, slot_id_suffix = 0, slot_id_prefix = 0], NEW_VALUE]
283 /// ]
284 /// ```
285 ///
286 /// `NEW_VALUE` is user-controllable so it can be crafted to match `NON_FUNGIBLE_ASSET`. The
287 /// domain separator is then the only value that differentiates these two deltas. This shows the
288 /// importance of placing the domain separators in the same index within each word's layout
289 /// which makes it easy to see that this value cannot be crafted to be the same.
290 ///
291 /// ### Number of Changed Entries
292 ///
293 /// As an example for ambiguity, consider these two deltas:
294 ///
295 /// ```text
296 /// [
297 /// ID_AND_NONCE, EMPTY_WORD,
298 /// [/* no fungible asset delta */],
299 /// [/* no non-fungible asset delta */],
300 /// [domain = 3, num_changed_entries = 0, slot_id_suffix = 20, slot_id_prefix = 21, 0, 0, 0, 0]
301 /// [domain = 3, num_changed_entries = 0, slot_id_suffix = 42, slot_id_prefix = 43, 0, 0, 0, 0]
302 /// ]
303 /// ```
304 ///
305 /// ```text
306 /// [
307 /// ID_AND_NONCE, EMPTY_WORD,
308 /// [/* no fungible asset delta */],
309 /// [/* no non-fungible asset delta */],
310 /// [KEY0, VALUE0],
311 /// [domain = 3, num_changed_entries = 1, slot_id_suffix = 42, slot_id_prefix = 43, 0, 0, 0, 0]
312 /// ]
313 /// ```
314 ///
315 /// The keys and values of map slots are user-controllable so `KEY0` and `VALUE0` could be
316 /// crafted to match the first map header in the first delta. So, _without_ having
317 /// `num_changed_entries` included in the commitment, these deltas would be ambiguous. A delta
318 /// with two empty maps could have the same commitment as a delta with one map entry where one
319 /// key-value pair has changed.
320 ///
321 /// #### New Accounts
322 ///
323 /// The number of changed entries of a storage map can be validly zero when an empty storage map
324 /// is added to a new account. In such cases, the number of changed key-value pairs is 0, but
325 /// the map must still be committed to, in order to differentiate between a slot being an empty
326 /// map or not being present at all.
327 pub fn to_commitment(&self) -> Word {
328 <Self as SequentialCommit>::to_commitment(self)
329 }
330}
331
332impl TryFrom<&AccountDelta> for Account {
333 type Error = AccountError;
334
335 /// Converts an [`AccountDelta`] into an [`Account`].
336 ///
337 /// Conceptually, this applies the delta onto an empty account.
338 ///
339 /// # Errors
340 ///
341 /// Returns an error if:
342 /// - If the delta is not a full state delta. See [`AccountDelta`] for details.
343 /// - If any vault delta operation removes an asset.
344 /// - If any vault delta operation adds an asset that would overflow the maximum representable
345 /// amount.
346 /// - If any storage delta update violates account storage constraints.
347 fn try_from(delta: &AccountDelta) -> Result<Self, Self::Error> {
348 if !delta.is_full_state() {
349 return Err(AccountError::PartialStateDeltaToAccount);
350 }
351
352 let Some(code) = delta.code().cloned() else {
353 return Err(AccountError::PartialStateDeltaToAccount);
354 };
355
356 let mut vault = AssetVault::default();
357 vault.apply_delta(delta.vault()).map_err(AccountError::AssetVaultUpdateError)?;
358
359 // Once we support addition and removal of storage slots, we may be able to change
360 // this to create an empty account and use `Account::apply_delta` instead.
361 // For now, we need to create the initial storage of the account with the same slot types.
362 let mut empty_storage_slots = Vec::new();
363 for (slot_name, slot_delta) in delta.storage().slots() {
364 let slot = match slot_delta.slot_type() {
365 StorageSlotType::Value => StorageSlot::with_empty_value(slot_name.clone()),
366 StorageSlotType::Map => StorageSlot::with_empty_map(slot_name.clone()),
367 };
368 empty_storage_slots.push(slot);
369 }
370 let mut storage = AccountStorage::new(empty_storage_slots)
371 .expect("storage delta should contain a valid number of slots");
372 storage.apply_delta(delta.storage())?;
373
374 // The nonce of the account is the initial nonce of 0 plus the nonce_delta, so the
375 // nonce_delta itself.
376 let nonce = delta.nonce_delta();
377
378 Account::new(delta.id(), vault, storage, code, nonce, None)
379 }
380}
381
382impl SequentialCommit for AccountDelta {
383 type Commitment = Word;
384
385 /// Reduces the delta to a sequence of field elements.
386 ///
387 /// See [AccountDelta::to_commitment()] for more details.
388 fn to_elements(&self) -> Vec<Felt> {
389 // The commitment to an empty delta is defined as the empty word.
390 if self.is_empty() {
391 return Vec::new();
392 }
393
394 // Minor optimization: At least 24 elements are always added.
395 let mut elements = Vec::with_capacity(24);
396
397 // ID and Nonce
398 elements.extend_from_slice(&[
399 self.nonce_delta,
400 ZERO,
401 self.account_id.suffix(),
402 self.account_id.prefix().as_felt(),
403 ]);
404 elements.extend_from_slice(Word::empty().as_elements());
405
406 // Vault Delta
407 self.vault.append_delta_elements(&mut elements);
408
409 // Storage Delta
410 self.storage.append_delta_elements(&mut elements);
411
412 debug_assert!(
413 elements.len() % (2 * crate::WORD_SIZE) == 0,
414 "expected elements to contain an even number of words, but it contained {} elements",
415 elements.len()
416 );
417
418 elements
419 }
420}
421
422// ACCOUNT UPDATE DETAILS
423// ================================================================================================
424
425/// [`AccountUpdateDetails`] describes the details of one or more transactions executed against an
426/// account.
427///
428/// In particular, private account changes aren't tracked at all; they are represented as
429/// [`AccountUpdateDetails::Private`].
430///
431/// Non-private accounts are tracked as an [`AccountDelta`]. If the account is new, the delta can be
432/// converted into an [`Account`]. If not, the delta can be applied to the existing account using
433/// [`Account::apply_delta`].
434///
435/// Note that these details can represent the changes from one or more transactions in which case
436/// the deltas of each transaction are merged together using [`AccountDelta::merge`].
437#[derive(Clone, Debug, PartialEq, Eq)]
438pub enum AccountUpdateDetails {
439 /// The state update details of a private account is not publicly accessible.
440 Private,
441
442 /// The state update details of non-private accounts.
443 Delta(AccountDelta),
444}
445
446impl AccountUpdateDetails {
447 /// Returns `true` if the account update details are for private account.
448 pub fn is_private(&self) -> bool {
449 matches!(self, Self::Private)
450 }
451
452 /// Merges the `other` update into this one.
453 ///
454 /// This account update is assumed to come before the other.
455 pub fn merge(self, other: AccountUpdateDetails) -> Result<Self, AccountDeltaError> {
456 let merged_update = match (self, other) {
457 (AccountUpdateDetails::Private, AccountUpdateDetails::Private) => {
458 AccountUpdateDetails::Private
459 },
460 (AccountUpdateDetails::Delta(mut delta), AccountUpdateDetails::Delta(new_delta)) => {
461 delta.merge(new_delta)?;
462 AccountUpdateDetails::Delta(delta)
463 },
464 (left, right) => {
465 return Err(AccountDeltaError::IncompatibleAccountUpdates {
466 left_update_type: left.as_tag_str(),
467 right_update_type: right.as_tag_str(),
468 });
469 },
470 };
471
472 Ok(merged_update)
473 }
474
475 /// Returns the tag of the [`AccountUpdateDetails`] as a string for inclusion in error messages.
476 pub(crate) const fn as_tag_str(&self) -> &'static str {
477 match self {
478 AccountUpdateDetails::Private => "private",
479 AccountUpdateDetails::Delta(_) => "delta",
480 }
481 }
482}
483
484// SERIALIZATION
485// ================================================================================================
486
487impl Serializable for AccountDelta {
488 fn write_into<W: ByteWriter>(&self, target: &mut W) {
489 self.account_id.write_into(target);
490 self.storage.write_into(target);
491 self.vault.write_into(target);
492 self.code.write_into(target);
493 self.nonce_delta.write_into(target);
494 }
495
496 fn get_size_hint(&self) -> usize {
497 self.account_id.get_size_hint()
498 + self.storage.get_size_hint()
499 + self.vault.get_size_hint()
500 + self.code.get_size_hint()
501 + self.nonce_delta.get_size_hint()
502 }
503}
504
505impl Deserializable for AccountDelta {
506 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
507 let account_id = AccountId::read_from(source)?;
508 let storage = AccountStorageDelta::read_from(source)?;
509 let vault = AccountVaultDelta::read_from(source)?;
510 let code = <Option<AccountCode>>::read_from(source)?;
511 let nonce_delta = Felt::read_from(source)?;
512
513 validate_nonce(nonce_delta, &storage, &vault)
514 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
515
516 Ok(Self {
517 account_id,
518 storage,
519 vault,
520 code,
521 nonce_delta,
522 })
523 }
524}
525
526impl Serializable for AccountUpdateDetails {
527 fn write_into<W: ByteWriter>(&self, target: &mut W) {
528 match self {
529 AccountUpdateDetails::Private => {
530 0_u8.write_into(target);
531 },
532 AccountUpdateDetails::Delta(delta) => {
533 1_u8.write_into(target);
534 delta.write_into(target);
535 },
536 }
537 }
538
539 fn get_size_hint(&self) -> usize {
540 // Size of the serialized enum tag.
541 let u8_size = 0u8.get_size_hint();
542
543 match self {
544 AccountUpdateDetails::Private => u8_size,
545 AccountUpdateDetails::Delta(account_delta) => u8_size + account_delta.get_size_hint(),
546 }
547 }
548}
549
550impl Deserializable for AccountUpdateDetails {
551 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
552 match u8::read_from(source)? {
553 0 => Ok(Self::Private),
554 1 => Ok(Self::Delta(AccountDelta::read_from(source)?)),
555 variant => Err(DeserializationError::InvalidValue(format!(
556 "Unknown variant {variant} for AccountDetails"
557 ))),
558 }
559 }
560}
561
562// HELPER FUNCTIONS
563// ================================================================================================
564
565/// Checks if the nonce was updated correctly given the provided storage and vault deltas.
566///
567/// # Errors
568///
569/// Returns an error if:
570/// - storage or vault were updated, but the nonce_delta was set to 0.
571fn validate_nonce(
572 nonce_delta: Felt,
573 storage: &AccountStorageDelta,
574 vault: &AccountVaultDelta,
575) -> Result<(), AccountDeltaError> {
576 if (!storage.is_empty() || !vault.is_empty()) && nonce_delta == ZERO {
577 return Err(AccountDeltaError::NonEmptyStorageOrVaultDeltaWithZeroNonceDelta);
578 }
579
580 Ok(())
581}
582
583// TESTS
584// ================================================================================================
585
586#[cfg(test)]
587mod tests {
588
589 use assert_matches::assert_matches;
590 use miden_core::utils::Serializable;
591 use miden_core::{Felt, FieldElement};
592
593 use super::{AccountDelta, AccountStorageDelta, AccountVaultDelta};
594 use crate::account::delta::AccountUpdateDetails;
595 use crate::account::{
596 Account,
597 AccountCode,
598 AccountId,
599 AccountStorage,
600 AccountStorageMode,
601 AccountType,
602 StorageMapDelta,
603 StorageSlotName,
604 };
605 use crate::asset::{
606 Asset,
607 AssetVault,
608 FungibleAsset,
609 NonFungibleAsset,
610 NonFungibleAssetDetails,
611 };
612 use crate::errors::AccountDeltaError;
613 use crate::testing::account_id::{
614 ACCOUNT_ID_PRIVATE_SENDER,
615 ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
616 AccountIdBuilder,
617 };
618 use crate::{ONE, Word, ZERO};
619
620 #[test]
621 fn account_delta_nonce_validation() {
622 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
623 // empty delta
624 let storage_delta = AccountStorageDelta::new();
625 let vault_delta = AccountVaultDelta::default();
626
627 AccountDelta::new(account_id, storage_delta.clone(), vault_delta.clone(), ZERO).unwrap();
628 AccountDelta::new(account_id, storage_delta.clone(), vault_delta.clone(), ONE).unwrap();
629
630 // non-empty delta
631 let storage_delta = AccountStorageDelta::from_iters([StorageSlotName::mock(1)], [], []);
632
633 assert_matches!(
634 AccountDelta::new(account_id, storage_delta.clone(), vault_delta.clone(), ZERO)
635 .unwrap_err(),
636 AccountDeltaError::NonEmptyStorageOrVaultDeltaWithZeroNonceDelta
637 );
638 AccountDelta::new(account_id, storage_delta.clone(), vault_delta.clone(), ONE).unwrap();
639 }
640
641 #[test]
642 fn account_delta_nonce_overflow() {
643 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
644 let storage_delta = AccountStorageDelta::new();
645 let vault_delta = AccountVaultDelta::default();
646
647 let nonce_delta0 = ONE;
648 let nonce_delta1 = Felt::try_from(0xffff_ffff_0000_0000u64).unwrap();
649
650 let mut delta0 =
651 AccountDelta::new(account_id, storage_delta.clone(), vault_delta.clone(), nonce_delta0)
652 .unwrap();
653 let delta1 =
654 AccountDelta::new(account_id, storage_delta, vault_delta, nonce_delta1).unwrap();
655
656 assert_matches!(delta0.merge(delta1).unwrap_err(), AccountDeltaError::NonceIncrementOverflow {
657 current, increment, new
658 } => {
659 assert_eq!(current, nonce_delta0);
660 assert_eq!(increment, nonce_delta1);
661 assert_eq!(new, nonce_delta0 + nonce_delta1);
662 });
663 }
664
665 #[test]
666 fn account_update_details_size_hint() {
667 // AccountDelta
668 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
669 let storage_delta = AccountStorageDelta::new();
670 let vault_delta = AccountVaultDelta::default();
671 assert_eq!(storage_delta.to_bytes().len(), storage_delta.get_size_hint());
672 assert_eq!(vault_delta.to_bytes().len(), vault_delta.get_size_hint());
673
674 let account_delta =
675 AccountDelta::new(account_id, storage_delta, vault_delta, ZERO).unwrap();
676 assert_eq!(account_delta.to_bytes().len(), account_delta.get_size_hint());
677
678 let storage_delta = AccountStorageDelta::from_iters(
679 [StorageSlotName::mock(1)],
680 [
681 (StorageSlotName::mock(2), Word::from([1, 1, 1, 1u32])),
682 (StorageSlotName::mock(3), Word::from([1, 1, 0, 1u32])),
683 ],
684 [(
685 StorageSlotName::mock(4),
686 StorageMapDelta::from_iters(
687 [Word::from([1, 1, 1, 0u32]), Word::from([0, 1, 1, 1u32])],
688 [(Word::from([1, 1, 1, 1u32]), Word::from([1, 1, 1, 1u32]))],
689 ),
690 )],
691 );
692
693 let non_fungible: Asset = NonFungibleAsset::new(
694 &NonFungibleAssetDetails::new(
695 AccountIdBuilder::new()
696 .account_type(AccountType::NonFungibleFaucet)
697 .storage_mode(AccountStorageMode::Public)
698 .build_with_rng(&mut rand::rng())
699 .prefix(),
700 vec![6],
701 )
702 .unwrap(),
703 )
704 .unwrap()
705 .into();
706 let fungible_2: Asset = FungibleAsset::new(
707 AccountIdBuilder::new()
708 .account_type(AccountType::FungibleFaucet)
709 .storage_mode(AccountStorageMode::Public)
710 .build_with_rng(&mut rand::rng()),
711 10,
712 )
713 .unwrap()
714 .into();
715 let vault_delta = AccountVaultDelta::from_iters([non_fungible], [fungible_2]);
716
717 assert_eq!(storage_delta.to_bytes().len(), storage_delta.get_size_hint());
718 assert_eq!(vault_delta.to_bytes().len(), vault_delta.get_size_hint());
719
720 let account_delta = AccountDelta::new(account_id, storage_delta, vault_delta, ONE).unwrap();
721 assert_eq!(account_delta.to_bytes().len(), account_delta.get_size_hint());
722
723 // Account
724
725 let account_id =
726 AccountId::try_from(ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE).unwrap();
727
728 let asset_vault = AssetVault::mock();
729 assert_eq!(asset_vault.to_bytes().len(), asset_vault.get_size_hint());
730
731 let account_storage = AccountStorage::mock();
732 assert_eq!(account_storage.to_bytes().len(), account_storage.get_size_hint());
733
734 let account_code = AccountCode::mock();
735 assert_eq!(account_code.to_bytes().len(), account_code.get_size_hint());
736
737 let account = Account::new_existing(
738 account_id,
739 asset_vault,
740 account_storage,
741 account_code,
742 Felt::ONE,
743 );
744 assert_eq!(account.to_bytes().len(), account.get_size_hint());
745
746 // AccountUpdateDetails
747
748 let update_details_private = AccountUpdateDetails::Private;
749 assert_eq!(update_details_private.to_bytes().len(), update_details_private.get_size_hint());
750
751 let update_details_delta = AccountUpdateDetails::Delta(account_delta);
752 assert_eq!(update_details_delta.to_bytes().len(), update_details_delta.get_size_hint());
753 }
754}