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