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