miden_protocol/account/delta/mod.rs
1use alloc::string::ToString;
2use alloc::vec::Vec;
3
4use crate::account::{Account, AccountCode, 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, Word, ZERO};
16
17mod delta_op;
18pub use delta_op::AssetDeltaOperation;
19
20mod vault;
21pub use vault::{
22 AccountVaultDelta,
23 FungibleAssetDelta,
24 NonFungibleAssetDelta,
25 NonFungibleDeltaAction,
26};
27
28// ACCOUNT DELTA
29// ================================================================================================
30
31/// The [`AccountDelta`] stores the differences between two account states, which can result from
32/// one or more transaction.
33///
34/// The differences are represented as follows:
35/// - storage: an [`AccountStoragePatch`] that contains the changes to the account storage.
36/// - vault: an [`AccountVaultDelta`] object that contains the changes to the account vault.
37/// - nonce: if the nonce of the account has changed, the _delta_ of the nonce is stored, i.e. the
38/// value by which the nonce increased.
39/// - code: an [`AccountCode`] for new accounts and `None` for others.
40///
41/// The presence of the code in a delta signals if the delta is a _full state_ or _partial state_
42/// delta. A full state delta must be converted into an [`Account`] object, while a partial state
43/// delta must be applied to an existing [`Account`]. Because a full state delta reconstructs the
44/// account from empty storage, its storage patch may only create slots, never update or remove
45/// them; [`AccountDelta::new`] enforces this.
46///
47/// TODO(code_upgrades): The ability to track account code updates is an outstanding feature. For
48/// that reason, the account code is not considered as part of the "nonce must be incremented if
49/// state changed" check.
50#[derive(Clone, Debug, PartialEq, Eq)]
51pub struct AccountDelta {
52 /// The ID of the account to which this delta applies. If the delta is created during
53 /// transaction execution, that is the native account of the transaction.
54 account_id: AccountId,
55 /// The patch of the account's storage.
56 storage: AccountStoragePatch,
57 /// The delta of the account's asset vault.
58 vault: AccountVaultDelta,
59 /// The code of a new account (`Some`) or `None` for existing accounts.
60 code: Option<AccountCode>,
61 /// The value by which the nonce was incremented. Must be greater than zero if storage or vault
62 /// are non-empty.
63 nonce_delta: Felt,
64}
65
66impl AccountDelta {
67 // CONSTANTS
68 // --------------------------------------------------------------------------------------------
69
70 /// Domain separator for the account delta commitment header.
71 const DOMAIN: Felt = Felt::new_unchecked(1);
72
73 // CONSTRUCTOR
74 // --------------------------------------------------------------------------------------------
75
76 /// Returns new [AccountDelta] instantiated from the provided components.
77 ///
78 /// `code` is `Some` for a full state delta (a new account) and `None` otherwise.
79 ///
80 /// # Errors
81 ///
82 /// - Returns an error if storage or vault were updated, but the nonce_delta is 0.
83 /// - Returns an error if `code` is provided but the storage patch contains an `Update` or
84 /// `Remove` operation. A full state delta must reconstruct the account from empty storage, so
85 /// it may only create slots.
86 pub fn new(
87 account_id: AccountId,
88 storage: AccountStoragePatch,
89 vault: AccountVaultDelta,
90 code: Option<AccountCode>,
91 nonce_delta: Felt,
92 ) -> Result<Self, AccountDeltaError> {
93 // nonce must be updated if either account storage or vault were updated
94 validate_nonce(nonce_delta, &storage, &vault)?;
95
96 // A full state delta (carrying code) must reconstruct the account from empty storage, so it
97 // may only create slots. An `Update` or `Remove` assumes the slot already exists and would
98 // make reconstruction impossible.
99 if code.is_some() && storage.contains_non_create_ops() {
100 return Err(AccountDeltaError::FullStateDeltaContainsNonCreateOp);
101 }
102
103 Ok(Self {
104 account_id,
105 storage,
106 vault,
107 code,
108 nonce_delta,
109 })
110 }
111
112 // PUBLIC MUTATORS
113 // --------------------------------------------------------------------------------------------
114
115 /// Returns a mutable reference to the account vault delta.
116 pub fn vault_mut(&mut self) -> &mut AccountVaultDelta {
117 &mut self.vault
118 }
119
120 // PUBLIC ACCESSORS
121 // --------------------------------------------------------------------------------------------
122
123 /// Returns true if this account delta does not contain any vault, storage or nonce updates.
124 pub fn is_empty(&self) -> bool {
125 self.storage.is_empty() && self.vault.is_empty() && self.nonce_delta == ZERO
126 }
127
128 /// Returns `true` if this delta is a "full state" delta, `false` otherwise, i.e. if it is a
129 /// "partial state" delta.
130 ///
131 /// See the type-level docs for more on this distinction.
132 pub fn is_full_state(&self) -> bool {
133 // TODO(code_upgrades): Change this to another detection mechanism once we have code upgrade
134 // support, at which point the presence of code may not be enough of an indication
135 // that a delta can be converted to a full account.
136 //
137 // The presence of code alone is sufficient to identify a full state delta: the constructor
138 // enforces that a code-carrying delta's storage patch contains only `Create` ops, so it
139 // always reconstructs a full account.
140 self.code.is_some()
141 }
142
143 /// Returns storage updates for this account delta.
144 pub fn storage(&self) -> &AccountStoragePatch {
145 &self.storage
146 }
147
148 /// Returns vault updates for this account delta.
149 pub fn vault(&self) -> &AccountVaultDelta {
150 &self.vault
151 }
152
153 /// Returns the amount by which the nonce was incremented.
154 pub fn nonce_delta(&self) -> Felt {
155 self.nonce_delta
156 }
157
158 /// Returns the account ID to which this delta applies.
159 pub fn id(&self) -> AccountId {
160 self.account_id
161 }
162
163 /// Returns a reference to the account code of this delta, if present.
164 pub fn code(&self) -> Option<&AccountCode> {
165 self.code.as_ref()
166 }
167
168 /// Converts this delta into its individual components.
169 pub fn into_parts(self) -> (AccountStoragePatch, AccountVaultDelta, Option<AccountCode>, Felt) {
170 (self.storage, self.vault, self.code, self.nonce_delta)
171 }
172
173 /// Computes the commitment to the account delta.
174 ///
175 /// ## Computation
176 ///
177 /// The delta commitment is a sequential hash over a vector of field elements which starts out
178 /// empty and is appended to in the following way. Whenever sorting is expected, it is that
179 /// of a [`Word`].
180 ///
181 /// - Append `[[domain = 1, nonce_delta, account_id_suffix, account_id_prefix], EMPTY_WORD]`,
182 /// where `account_id_{prefix,suffix}` are the prefix and suffix felts of the native account
183 /// id, `nonce_delta` is the value by which the nonce was incremented, and `domain = 1`
184 /// identifies the header as the start of an account delta commitment.
185 /// - Asset Delta
186 /// - For each **added** asset, sorted by its asset ID:
187 /// - Append `[ASSET_ID, ASSET_VALUE]`.
188 /// - Append `[domain = 3, delta_op = 1, num_added_assets, 0]` if `num_added_assets != 0`
189 /// where `num_added_assets` is the number of added assets and `delta_op` is set to `1`
190 /// indicating asset addition.
191 /// - For each **removed** asset, sorted by its asset ID:
192 /// - Append `[ASSET_ID, ASSET_VALUE]`.
193 /// - Append `[domain = 3, delta_op = 2, num_removed_assets, 0]` if `num_removed_assets != 0`
194 /// where `num_removed_assets` is the number of removed assets and `delta_op` is set to `2`
195 /// indicating asset removal.
196 /// - Note that the domain is the same independent of asset addition or removal, since the
197 /// `delta_op` sufficiently distinguishes the two domains.
198 /// - Storage Slots are sorted by slot ID and are iterated in this order. `patch_op` is the
199 /// [`StoragePatchOperation`](crate::account::StoragePatchOperation) of the slot patch and
200 /// `slot_id_{suffix, prefix}` is the identifier of the slot. For each slot, depending on its
201 /// slot type:
202 /// - Value Slot
203 /// - Append `[[domain = 5, patch_op, slot_id_suffix, slot_id_prefix], NEW_VALUE]` where
204 /// `NEW_VALUE` is the new value of the slot.
205 /// - Map Slot
206 /// - For each key-value pair, sorted by key, whose new value is different from the previous
207 /// value in the map:
208 /// - Append `[KEY, NEW_VALUE]`.
209 /// - The map trailer is constructed as `[[domain = 6, patch_op, slot_id_suffix,
210 /// slot_id_prefix], [num_changed_entries, 0, 0, 0]]`, where `num_changed_entries` is the
211 /// number of key-value pairs appended above. Whether the trailer is included depends on
212 /// `patch_op`:
213 /// - For
214 /// [`StoragePatchOperation::Create`](crate::account::StoragePatchOperation::Create),
215 /// the trailer is always included, since the slot's creation must be committed to even
216 /// when the map is created empty (`num_changed_entries == 0`).
217 /// - For
218 /// [`StoragePatchOperation::Update`](crate::account::StoragePatchOperation::Update),
219 /// the trailer is included only if `num_changed_entries != 0`. An update that changes
220 /// no entries is a no-op and is omitted entirely.
221 /// - For
222 /// [`StoragePatchOperation::Remove`](crate::account::StoragePatchOperation::Remove),
223 /// the trailer is always included with `num_changed_entries` set to zero, since the
224 /// number of removed entries is unknown.
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 created storage slots
237 /// of the account, even if these slots contain the default value (e.g. the empty word for value
238 /// slots or an empty storage map). This ensures the full state delta commits to the exact
239 /// storage 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 domains (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 value slots are only included in the patch if their value has changed when the
257 /// operation is `Update`, there is no ambiguity between a value slot being set to
258 /// EMPTY_WORD and its value being unchanged.
259 /// - Storage map slots:
260 /// - Map slots append a header which summarizes the changes in the slot, in particular the
261 /// slot ID and number of changed entries.
262 /// - Two distinct storage map slots use the same domain but are disambiguated due to
263 /// inclusion of the slot ID.
264 ///
265 /// ### Domain Separators
266 ///
267 /// As an example for ambiguity, consider these two deltas:
268 ///
269 /// ```text
270 /// [
271 /// ID_AND_NONCE, EMPTY_WORD,
272 /// [ASSET_ID, ASSET_VALUE],
273 /// [[domain = 3, delta_op = 1, num_added_assets = 1, 0], EMPTY_WORD],
274 /// [/* no removed assets delta */],
275 /// [/* no storage patch */]
276 /// ]
277 /// ```
278 ///
279 /// ```text
280 /// [
281 /// ID_AND_NONCE, EMPTY_WORD,
282 /// [/* no asset delta */],
283 /// [[domain = 5, patch_op, slot_id_suffix0, slot_id_prefix0], NEW_VALUE]
284 /// [[domain = 5, patch_op, slot_id_suffix1, slot_id_prefix1], NEW_VALUE]
285 /// ]
286 /// ```
287 ///
288 /// - `NEW_VALUE` is user-controlled and can be crafted to match `ASSET_VALUE` or `EMPTY_WORD`.
289 /// - Slot IDs are user-controlled and can be crafted to match the two most significant elements
290 /// in the asset ID or `num_added_assets` and the fixed 0.
291 /// - This leaves only the domain separator and the patch_op to differentiate these two deltas.
292 ///
293 /// The delta and patch headers further use distinct domain separators (1 and 2 respectively),
294 /// so a delta and a patch with otherwise identical bodies can never collide.
295 ///
296 /// ### Number of Changed Entries
297 ///
298 /// As an example for ambiguity, consider these two deltas:
299 ///
300 /// ```text
301 /// [
302 /// ID_AND_NONCE, EMPTY_WORD,
303 /// [/* no asset delta */],
304 /// [domain = 6, patch_op, slot_id_suffix = 20, slot_id_prefix = 21, num_changed_entries = 0, 0, 0, 0]
305 /// [domain = 6, patch_op, slot_id_suffix = 42, slot_id_prefix = 43, num_changed_entries = 0, 0, 0, 0]
306 /// ]
307 /// ```
308 ///
309 /// ```text
310 /// [
311 /// ID_AND_NONCE, EMPTY_WORD,
312 /// [/* no asset delta */],
313 /// [KEY0, VALUE0],
314 /// [domain = 6, patch_op, slot_id_suffix = 42, slot_id_prefix = 43, num_changed_entries = 1, 0, 0, 0]
315 /// ]
316 /// ```
317 ///
318 /// The keys and values of map slots are user-controllable so `KEY0` and `VALUE0` could be
319 /// crafted to match the first map header in the first delta. So, _without_ having
320 /// `num_changed_entries` included in the commitment, these deltas would be ambiguous. A delta
321 /// with two empty maps could have the same commitment as a delta with one map entry where one
322 /// key-value pair has changed.
323 ///
324 /// #### New Accounts
325 ///
326 /// The number of changed entries of a storage map can be validly zero when an empty storage map
327 /// is created in account (e.g. at account creation time). In such cases, the number of changed
328 /// key-value pairs is 0, but the map must still be committed to, in order to differentiate
329 /// between a slot being created as an empty map or not being created at all.
330 pub fn to_commitment(&self) -> Word {
331 <Self as SequentialCommit>::to_commitment(self)
332 }
333}
334
335impl TryFrom<&AccountDelta> for Account {
336 type Error = AccountError;
337
338 /// Converts an [`AccountDelta`] into an [`Account`].
339 ///
340 /// Conceptually, this applies the delta onto an empty account.
341 ///
342 /// # Errors
343 ///
344 /// Returns an error if:
345 /// - If the delta is not a full state delta. See [`AccountDelta`] for details.
346 /// - If any vault delta operation removes an asset.
347 /// - If any vault delta operation adds an asset that would overflow the maximum representable
348 /// amount.
349 /// - If any storage patch update violates account storage constraints.
350 fn try_from(delta: &AccountDelta) -> Result<Self, Self::Error> {
351 if !delta.is_full_state() {
352 return Err(AccountError::PartialStateDeltaToAccount);
353 }
354
355 let Some(code) = delta.code().cloned() else {
356 return Err(AccountError::PartialStateDeltaToAccount);
357 };
358
359 // The asset vault of a new account is empty, so if the delta contains removed assets, the
360 // delta is invalid.
361 if delta.vault().removed_assets().count() != 0 {
362 return Err(AccountError::AssetsRemovedFromNewAccount);
363 }
364
365 let mut vault = AssetVault::default();
366 for added_asset in delta.vault().added_assets() {
367 vault.insert_asset(added_asset).map_err(AccountError::AssetVaultUpdateError)?;
368 }
369
370 // A full state delta consists of `Create` slot patches, so applying it to empty storage
371 // reconstructs the account's full storage.
372 let mut storage = AccountStorage::default();
373 storage.apply_patch(delta.storage())?;
374
375 // The nonce of the account is the initial nonce of 0 plus the nonce_delta, so the
376 // nonce_delta itself.
377 let nonce = delta.nonce_delta();
378
379 Account::new(delta.id(), vault, storage, code, nonce, None)
380 }
381}
382
383impl SequentialCommit for AccountDelta {
384 type Commitment = Word;
385
386 /// Reduces the delta to a sequence of field elements.
387 ///
388 /// See [AccountDelta::to_commitment()] for more details.
389 fn to_elements(&self) -> Vec<Felt> {
390 // The commitment to an empty delta is defined as the empty word.
391 if self.is_empty() {
392 return Vec::new();
393 }
394
395 // Minor optimization: At least 24 elements are always added.
396 let mut elements = Vec::with_capacity(24);
397
398 // ID and Nonce
399 elements.extend_from_slice(&[
400 Self::DOMAIN,
401 self.nonce_delta,
402 self.account_id.suffix(),
403 self.account_id.prefix().as_felt(),
404 ]);
405 elements.extend_from_slice(Word::empty().as_elements());
406
407 // Vault Delta
408 self.vault.append_delta_elements(&mut elements);
409
410 // Storage Patch
411 self.storage.append_patch_elements(&mut elements);
412
413 debug_assert!(
414 elements.len() % (2 * crate::WORD_SIZE) == 0,
415 "expected elements to contain an even number of words, but it contained {} elements",
416 elements.len()
417 );
418
419 elements
420 }
421}
422
423// SERIALIZATION
424// ================================================================================================
425
426impl Serializable for AccountDelta {
427 fn write_into<W: ByteWriter>(&self, target: &mut W) {
428 self.account_id.write_into(target);
429 self.storage.write_into(target);
430 self.vault.write_into(target);
431 self.code.write_into(target);
432 self.nonce_delta.write_into(target);
433 }
434
435 fn get_size_hint(&self) -> usize {
436 self.account_id.get_size_hint()
437 + self.storage.get_size_hint()
438 + self.vault.get_size_hint()
439 + self.code.get_size_hint()
440 + self.nonce_delta.get_size_hint()
441 }
442}
443
444impl Deserializable for AccountDelta {
445 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
446 let account_id = AccountId::read_from(source)?;
447 let storage = AccountStoragePatch::read_from(source)?;
448 let vault = AccountVaultDelta::read_from(source)?;
449 let code = <Option<AccountCode>>::read_from(source)?;
450 let nonce_delta = Felt::read_from(source)?;
451
452 validate_nonce(nonce_delta, &storage, &vault)
453 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?;
454
455 Ok(Self {
456 account_id,
457 storage,
458 vault,
459 code,
460 nonce_delta,
461 })
462 }
463}
464
465// HELPER FUNCTIONS
466// ================================================================================================
467
468/// Checks if the nonce was updated correctly given the provided storage and vault deltas.
469///
470/// # Errors
471///
472/// Returns an error if:
473/// - storage or vault were updated, but the nonce_delta was set to 0.
474fn validate_nonce(
475 nonce_delta: Felt,
476 storage: &AccountStoragePatch,
477 vault: &AccountVaultDelta,
478) -> Result<(), AccountDeltaError> {
479 if (!storage.is_empty() || !vault.is_empty()) && nonce_delta == ZERO {
480 return Err(AccountDeltaError::NonEmptyStorageOrVaultDeltaWithZeroNonceDelta);
481 }
482
483 Ok(())
484}
485
486// TESTS
487// ================================================================================================
488
489#[cfg(test)]
490mod tests {
491
492 use assert_matches::assert_matches;
493 use rstest::rstest;
494
495 use super::{AccountDelta, AccountStoragePatch, AccountVaultDelta};
496 use crate::account::{
497 Account,
498 AccountCode,
499 AccountId,
500 AccountStorage,
501 AccountType,
502 StorageMapKey,
503 StorageMapPatch,
504 StorageSlotName,
505 };
506 use crate::asset::{
507 Asset,
508 AssetVault,
509 FungibleAsset,
510 NonFungibleAsset,
511 NonFungibleAssetDetails,
512 };
513 use crate::errors::AccountDeltaError;
514 use crate::testing::account_id::{
515 ACCOUNT_ID_PRIVATE_SENDER,
516 ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
517 AccountIdBuilder,
518 };
519 use crate::utils::serde::Serializable;
520 use crate::{ONE, Word, ZERO};
521
522 #[test]
523 fn account_delta_nonce_validation() {
524 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
525 // empty delta
526 let storage_patch = AccountStoragePatch::new();
527 let vault_delta = AccountVaultDelta::default();
528
529 AccountDelta::new(account_id, storage_patch.clone(), vault_delta.clone(), None, ZERO)
530 .unwrap();
531 AccountDelta::new(account_id, storage_patch.clone(), vault_delta.clone(), None, ONE)
532 .unwrap();
533
534 // non-empty delta
535 let storage_patch = AccountStoragePatch::from_iters([StorageSlotName::mock(1)], [], []);
536
537 assert_matches!(
538 AccountDelta::new(account_id, storage_patch.clone(), vault_delta.clone(), None, ZERO)
539 .unwrap_err(),
540 AccountDeltaError::NonEmptyStorageOrVaultDeltaWithZeroNonceDelta
541 );
542 AccountDelta::new(account_id, storage_patch.clone(), vault_delta.clone(), None, ONE)
543 .unwrap();
544 }
545
546 /// A full state delta (carrying code) must only contain `Create` storage ops, since an `Update`
547 /// or `Remove` could not be applied to the empty storage of a new account.
548 #[rstest]
549 #[case::update(
550 AccountStoragePatch::builder().update_value(StorageSlotName::mock(1), Word::empty()).build()
551 )]
552 #[case::remove(
553 AccountStoragePatch::builder().remove_value(StorageSlotName::mock(1)).build()
554 )]
555 fn account_delta_new_rejects_full_state_with_non_create_op(
556 #[case] storage: AccountStoragePatch,
557 ) -> anyhow::Result<()> {
558 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
559
560 let error = AccountDelta::new(
561 account_id,
562 storage,
563 AccountVaultDelta::default(),
564 Some(AccountCode::mock()),
565 ONE,
566 )
567 .unwrap_err();
568 assert_matches!(error, AccountDeltaError::FullStateDeltaContainsNonCreateOp);
569
570 Ok(())
571 }
572
573 /// A full state delta whose storage only creates slots can be reconstructed into an account.
574 #[test]
575 fn account_delta_full_state_with_create_reconstructs() -> anyhow::Result<()> {
576 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER)?;
577 let code = AccountCode::mock();
578 let created_slot = StorageSlotName::mock(1);
579 let created_value = Word::from([7u32, 0, 0, 0]);
580
581 let storage = AccountStoragePatch::builder()
582 .create_value(created_slot.clone(), created_value)
583 .build();
584
585 let delta = AccountDelta::new(
586 account_id,
587 storage,
588 AccountVaultDelta::default(),
589 Some(code.clone()),
590 ONE,
591 )?;
592 assert!(delta.is_full_state());
593
594 let account = Account::try_from(&delta)?;
595 assert_eq!(account.code(), &code);
596 assert_eq!(account.storage().get_item(&created_slot)?, created_value);
597
598 Ok(())
599 }
600
601 #[test]
602 fn account_delta_size_hint() {
603 // AccountDelta
604 let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
605 let storage_patch = AccountStoragePatch::new();
606 let vault_delta = AccountVaultDelta::default();
607 assert_eq!(storage_patch.to_bytes().len(), storage_patch.get_size_hint());
608 assert_eq!(vault_delta.to_bytes().len(), vault_delta.get_size_hint());
609
610 let account_delta =
611 AccountDelta::new(account_id, storage_patch, vault_delta, None, ZERO).unwrap();
612 assert_eq!(account_delta.to_bytes().len(), account_delta.get_size_hint());
613
614 let storage_patch = AccountStoragePatch::from_iters(
615 [StorageSlotName::mock(1)],
616 [
617 (StorageSlotName::mock(2), Word::from([1, 1, 1, 1u32])),
618 (StorageSlotName::mock(3), Word::from([1, 1, 0, 1u32])),
619 ],
620 [(
621 StorageSlotName::mock(4),
622 StorageMapPatch::from_iters(
623 [
624 StorageMapKey::from_array([1, 1, 1, 0]),
625 StorageMapKey::from_array([0, 1, 1, 1]),
626 ],
627 [(StorageMapKey::from_array([1, 1, 1, 1]), Word::from([1, 1, 1, 1u32]))],
628 ),
629 )],
630 );
631
632 let non_fungible: Asset = NonFungibleAsset::new(&NonFungibleAssetDetails::new(
633 AccountIdBuilder::new()
634 .account_type(AccountType::Public)
635 .build_with_rng(&mut rand::rng()),
636 vec![6],
637 ))
638 .into();
639 let fungible_2: Asset = FungibleAsset::new(
640 AccountIdBuilder::new()
641 .account_type(AccountType::Public)
642 .build_with_rng(&mut rand::rng()),
643 10,
644 )
645 .unwrap()
646 .into();
647 let vault_delta = AccountVaultDelta::from_iters([non_fungible], [fungible_2]);
648
649 assert_eq!(storage_patch.to_bytes().len(), storage_patch.get_size_hint());
650 assert_eq!(vault_delta.to_bytes().len(), vault_delta.get_size_hint());
651
652 let account_delta =
653 AccountDelta::new(account_id, storage_patch, vault_delta, None, ONE).unwrap();
654 assert_eq!(account_delta.to_bytes().len(), account_delta.get_size_hint());
655
656 // Account
657
658 let account_id =
659 AccountId::try_from(ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE).unwrap();
660
661 let asset_vault = AssetVault::mock();
662 assert_eq!(asset_vault.to_bytes().len(), asset_vault.get_size_hint());
663
664 let account_storage = AccountStorage::mock();
665 assert_eq!(account_storage.to_bytes().len(), account_storage.get_size_hint());
666
667 let account_code = AccountCode::mock();
668 assert_eq!(account_code.to_bytes().len(), account_code.get_size_hint());
669
670 let account =
671 Account::new_existing(account_id, asset_vault, account_storage, account_code, ONE);
672 assert_eq!(account.to_bytes().len(), account.get_size_hint());
673 }
674}