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