miden_objects/account/account_id/v0/
mod.rs

1mod prefix;
2use alloc::string::{String, ToString};
3use alloc::vec::Vec;
4use core::fmt;
5use core::hash::Hash;
6
7use bech32::Bech32m;
8use bech32::primitives::decode::{ByteIter, CheckedHrpstring};
9use miden_crypto::utils::hex_to_bytes;
10pub use prefix::AccountIdPrefixV0;
11
12use crate::account::account_id::NetworkId;
13use crate::account::account_id::account_type::{
14    FUNGIBLE_FAUCET,
15    NON_FUNGIBLE_FAUCET,
16    REGULAR_ACCOUNT_IMMUTABLE_CODE,
17    REGULAR_ACCOUNT_UPDATABLE_CODE,
18};
19use crate::account::account_id::storage_mode::{NETWORK, PRIVATE, PUBLIC};
20use crate::account::{AccountIdVersion, AccountStorageMode, AccountType};
21use crate::address::AddressType;
22use crate::errors::{AccountIdError, Bech32Error};
23use crate::utils::{ByteReader, Deserializable, DeserializationError, Serializable};
24use crate::{AccountError, EMPTY_WORD, Felt, Hasher, Word};
25
26// ACCOUNT ID VERSION 0
27// ================================================================================================
28
29/// Version 0 of the [`Account`](crate::account::Account) identifier.
30///
31/// See the [`AccountId`](super::AccountId) type's documentation for details.
32#[derive(Debug, Copy, Clone, Eq, PartialEq)]
33pub struct AccountIdV0 {
34    prefix: Felt,
35    suffix: Felt,
36}
37
38impl Hash for AccountIdV0 {
39    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
40        self.prefix.inner().hash(state);
41        self.suffix.inner().hash(state);
42    }
43}
44
45impl AccountIdV0 {
46    // CONSTANTS
47    // --------------------------------------------------------------------------------------------
48
49    /// The serialized size of an [`AccountIdV0`] in bytes.
50    const SERIALIZED_SIZE: usize = 15;
51
52    /// The lower two bits of the second least significant nibble encode the account type.
53    pub(crate) const TYPE_MASK: u8 = 0b11 << Self::TYPE_SHIFT;
54    pub(crate) const TYPE_SHIFT: u64 = 4;
55
56    /// The least significant nibble determines the account version.
57    const VERSION_MASK: u64 = 0b1111;
58
59    /// The higher two bits of the second least significant nibble encode the account storage
60    /// mode.
61    pub(crate) const STORAGE_MODE_MASK: u8 = 0b11 << Self::STORAGE_MODE_SHIFT;
62    pub(crate) const STORAGE_MODE_SHIFT: u64 = 6;
63
64    /// The bit at index 5 of the prefix encodes whether the account is a faucet.
65    pub(crate) const IS_FAUCET_MASK: u64 = 0b10 << Self::TYPE_SHIFT;
66
67    // CONSTRUCTORS
68    // --------------------------------------------------------------------------------------------
69
70    /// See [`AccountId::new`](super::AccountId::new) for details.
71    pub fn new(
72        seed: Word,
73        code_commitment: Word,
74        storage_commitment: Word,
75    ) -> Result<Self, AccountIdError> {
76        let seed_digest = compute_digest(seed, code_commitment, storage_commitment);
77
78        let mut felts: [Felt; 2] = seed_digest.as_elements()[0..2]
79            .try_into()
80            .expect("we should have sliced off 2 elements");
81
82        felts[1] = shape_suffix(felts[1]);
83
84        account_id_from_felts(felts)
85    }
86
87    /// See [`AccountId::new_unchecked`](super::AccountId::new_unchecked) for details.
88    pub fn new_unchecked(elements: [Felt; 2]) -> Self {
89        let prefix = elements[0];
90        let suffix = elements[1];
91
92        // Panic on invalid felts in debug mode.
93        if cfg!(debug_assertions) {
94            validate_prefix(prefix).expect("AccountId::new_unchecked called with invalid prefix");
95            validate_suffix(suffix).expect("AccountId::new_unchecked called with invalid suffix");
96        }
97
98        Self { prefix, suffix }
99    }
100
101    /// See [`AccountId::dummy`](super::AccountId::dummy) for details.
102    #[cfg(any(feature = "testing", test))]
103    pub fn dummy(
104        mut bytes: [u8; 15],
105        account_type: AccountType,
106        storage_mode: AccountStorageMode,
107    ) -> AccountIdV0 {
108        let version = AccountIdVersion::Version0 as u8;
109        let low_nibble = ((storage_mode as u8) << Self::STORAGE_MODE_SHIFT)
110            | ((account_type as u8) << Self::TYPE_SHIFT)
111            | version;
112
113        // Set least significant byte.
114        bytes[7] = low_nibble;
115
116        // Clear the 32nd most significant bit.
117        bytes[3] &= 0b1111_1110;
118
119        let prefix_bytes =
120            bytes[0..8].try_into().expect("we should have sliced off exactly 8 bytes");
121        let prefix = Felt::try_from(u64::from_be_bytes(prefix_bytes))
122            .expect("should be a valid felt due to the most significant bit being zero");
123
124        let mut suffix_bytes = [0; 8];
125        // Overwrite first 7 bytes, leaving the 8th byte 0 (which will be cleared by
126        // shape_suffix anyway).
127        suffix_bytes[..7].copy_from_slice(&bytes[8..]);
128
129        // If the value is too large modular reduction is performed, which is fine here.
130        let mut suffix = Felt::new(u64::from_be_bytes(suffix_bytes));
131
132        // Clear the most significant bit of the suffix.
133        suffix = Felt::try_from(suffix.as_int() & 0x7fff_ffff_ffff_ffff)
134            .expect("no bits were set so felt should still be valid");
135
136        suffix = shape_suffix(suffix);
137
138        let account_id = account_id_from_felts([prefix, suffix])
139            .expect("we should have shaped the felts to produce a valid id");
140
141        debug_assert_eq!(account_id.account_type(), account_type);
142        debug_assert_eq!(account_id.storage_mode(), storage_mode);
143
144        account_id
145    }
146
147    /// See [`AccountId::compute_account_seed`](super::AccountId::compute_account_seed) for details.
148    pub fn compute_account_seed(
149        init_seed: [u8; 32],
150        account_type: AccountType,
151        storage_mode: AccountStorageMode,
152        version: AccountIdVersion,
153        code_commitment: Word,
154        storage_commitment: Word,
155    ) -> Result<Word, AccountError> {
156        crate::account::account_id::seed::compute_account_seed(
157            init_seed,
158            account_type,
159            storage_mode,
160            version,
161            code_commitment,
162            storage_commitment,
163        )
164    }
165
166    // PUBLIC ACCESSORS
167    // --------------------------------------------------------------------------------------------
168
169    /// See [`AccountId::account_type`](super::AccountId::account_type) for details.
170    pub const fn account_type(&self) -> AccountType {
171        extract_type(self.prefix.as_int())
172    }
173
174    /// See [`AccountId::is_faucet`](super::AccountId::is_faucet) for details.
175    pub fn is_faucet(&self) -> bool {
176        self.account_type().is_faucet()
177    }
178
179    /// See [`AccountId::is_regular_account`](super::AccountId::is_regular_account) for details.
180    pub fn is_regular_account(&self) -> bool {
181        self.account_type().is_regular_account()
182    }
183
184    /// See [`AccountId::storage_mode`](super::AccountId::storage_mode) for details.
185    pub fn storage_mode(&self) -> AccountStorageMode {
186        extract_storage_mode(self.prefix().as_u64())
187            .expect("account ID should have been constructed with a valid storage mode")
188    }
189
190    /// See [`AccountId::is_public`](super::AccountId::is_public) for details.
191    pub fn is_public(&self) -> bool {
192        self.storage_mode() == AccountStorageMode::Public
193    }
194
195    /// See [`AccountId::version`](super::AccountId::version) for details.
196    pub fn version(&self) -> AccountIdVersion {
197        extract_version(self.prefix().as_u64())
198            .expect("account ID should have been constructed with a valid version")
199    }
200
201    /// See [`AccountId::from_hex`](super::AccountId::from_hex) for details.
202    pub fn from_hex(hex_str: &str) -> Result<AccountIdV0, AccountIdError> {
203        hex_to_bytes(hex_str)
204            .map_err(AccountIdError::AccountIdHexParseError)
205            .and_then(AccountIdV0::try_from)
206    }
207
208    /// See [`AccountId::to_hex`](super::AccountId::to_hex) for details.
209    pub fn to_hex(self) -> String {
210        // We need to pad the suffix with 16 zeroes so it produces a correctly padded 8 byte
211        // big-endian hex string. Only then can we cut off the last zero byte by truncating. We
212        // cannot use `:014x` padding.
213        let mut hex_string =
214            format!("0x{:016x}{:016x}", self.prefix().as_u64(), self.suffix().as_int());
215        hex_string.truncate(32);
216        hex_string
217    }
218
219    /// See [`AccountId::to_bech32`](super::AccountId::to_bech32) for details.
220    pub fn to_bech32(&self, network_id: NetworkId) -> String {
221        let id_bytes: [u8; Self::SERIALIZED_SIZE] = (*self).into();
222
223        let mut data = [0; Self::SERIALIZED_SIZE + 1];
224        data[0] = AddressType::AccountId as u8;
225        data[1..16].copy_from_slice(&id_bytes);
226
227        // SAFETY: Encoding only panics if the total length of the hrp, data (in GF(32)), separator
228        // and checksum exceeds Bech32m::CODE_LENGTH, which is 1023. Since the data is 26 bytes in
229        // that field and the hrp is at most 83 in size we are way below the limit.
230        //
231        // The only allowed checksum algorithm is [`Bech32m`](bech32::Bech32m) due to being the
232        // best available checksum algorithm with no known weaknesses (unlike
233        // [`Bech32`](bech32::Bech32)). No checksum is also not allowed since the intended
234        // use of bech32 is to have error detection capabilities.
235        bech32::encode::<Bech32m>(network_id.into_hrp(), &data)
236            .expect("code length of bech32 should not be exceeded")
237    }
238
239    /// See [`AccountId::from_bech32`](super::AccountId::from_bech32) for details.
240    pub fn from_bech32(bech32_string: &str) -> Result<(NetworkId, Self), AccountIdError> {
241        // We use CheckedHrpString instead of bech32::decode with an explicit checksum algorithm so
242        // we don't allow the `Bech32` or `NoChecksum` algorithms.
243        let checked_string = CheckedHrpstring::new::<Bech32m>(bech32_string).map_err(|source| {
244            // The CheckedHrpStringError does not implement core::error::Error, only
245            // std::error::Error, so for now we convert it to a String. Even if it will
246            // implement the trait in the future, we should include it as an opaque
247            // error since the crate does not have a stable release yet.
248            AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(source.to_string().into()))
249        })?;
250
251        let hrp = checked_string.hrp();
252        let network_id = NetworkId::from_hrp(hrp);
253
254        let mut byte_iter = checked_string.byte_iter();
255
256        // The length must be the serialized size of the account ID plus the address byte.
257        if byte_iter.len() != Self::SERIALIZED_SIZE + 1 {
258            return Err(AccountIdError::Bech32DecodeError(Bech32Error::InvalidDataLength {
259                expected: Self::SERIALIZED_SIZE + 1,
260                actual: byte_iter.len(),
261            }));
262        }
263
264        let address_byte = byte_iter.next().expect("there should be at least one byte");
265        if address_byte != AddressType::AccountId as u8 {
266            return Err(AccountIdError::Bech32DecodeError(Bech32Error::UnknownAddressType(
267                address_byte,
268            )));
269        }
270
271        Self::from_bech32_byte_iter(byte_iter).map(|account_id| (network_id, account_id))
272    }
273
274    /// Decodes the data from the bech32 byte iterator into an [`AccountId`].
275    pub(crate) fn from_bech32_byte_iter(byte_iter: ByteIter<'_>) -> Result<Self, AccountIdError> {
276        // The _remaining_ length of the iterator must be the serialized size of the account ID.
277        if byte_iter.len() != Self::SERIALIZED_SIZE {
278            return Err(AccountIdError::Bech32DecodeError(Bech32Error::InvalidDataLength {
279                expected: Self::SERIALIZED_SIZE,
280                actual: byte_iter.len(),
281            }));
282        }
283
284        // Every byte is guaranteed to be overwritten since we've checked the length of the
285        // iterator.
286        let mut id_bytes = [0_u8; Self::SERIALIZED_SIZE];
287        for (i, byte) in byte_iter.enumerate() {
288            id_bytes[i] = byte;
289        }
290
291        let account_id = Self::try_from(id_bytes)?;
292
293        Ok(account_id)
294    }
295
296    /// Returns the [`AccountIdPrefixV0`] of this account ID.
297    ///
298    /// See also [`AccountId::prefix`](super::AccountId::prefix) for details.
299    pub fn prefix(&self) -> AccountIdPrefixV0 {
300        // SAFETY: We only construct account IDs with valid prefixes, so we don't have to validate
301        // it again.
302        AccountIdPrefixV0::new_unchecked(self.prefix)
303    }
304
305    /// See [`AccountId::suffix`](super::AccountId::suffix) for details.
306    pub const fn suffix(&self) -> Felt {
307        self.suffix
308    }
309}
310
311// CONVERSIONS FROM ACCOUNT ID
312// ================================================================================================
313
314impl From<AccountIdV0> for [Felt; 2] {
315    fn from(id: AccountIdV0) -> Self {
316        [id.prefix, id.suffix]
317    }
318}
319
320impl From<AccountIdV0> for [u8; 15] {
321    fn from(id: AccountIdV0) -> Self {
322        let mut result = [0_u8; 15];
323        result[..8].copy_from_slice(&id.prefix().as_u64().to_be_bytes());
324        // The last byte of the suffix is always zero so we skip it here.
325        result[8..].copy_from_slice(&id.suffix().as_int().to_be_bytes()[..7]);
326        result
327    }
328}
329
330impl From<AccountIdV0> for u128 {
331    fn from(id: AccountIdV0) -> Self {
332        let mut le_bytes = [0_u8; 16];
333        le_bytes[..8].copy_from_slice(&id.suffix().as_int().to_le_bytes());
334        le_bytes[8..].copy_from_slice(&id.prefix().as_u64().to_le_bytes());
335        u128::from_le_bytes(le_bytes)
336    }
337}
338
339// CONVERSIONS TO ACCOUNT ID
340// ================================================================================================
341
342impl TryFrom<[Felt; 2]> for AccountIdV0 {
343    type Error = AccountIdError;
344
345    /// See [`TryFrom<[Felt; 2]> for
346    /// AccountId`](super::AccountId#impl-TryFrom<%5BFelt;+2%5D>-for-AccountId) for details.
347    fn try_from(elements: [Felt; 2]) -> Result<Self, Self::Error> {
348        account_id_from_felts(elements)
349    }
350}
351
352impl TryFrom<[u8; 15]> for AccountIdV0 {
353    type Error = AccountIdError;
354
355    /// See [`TryFrom<[u8; 15]> for
356    /// AccountId`](super::AccountId#impl-TryFrom<%5Bu8;+15%5D>-for-AccountId) for details.
357    fn try_from(mut bytes: [u8; 15]) -> Result<Self, Self::Error> {
358        // Felt::try_from expects little-endian order, so reverse the individual felt slices.
359        // This prefix slice has 8 bytes.
360        bytes[..8].reverse();
361        // The suffix slice has 7 bytes, since the 8th byte will always be zero.
362        bytes[8..15].reverse();
363
364        let prefix_slice = &bytes[..8];
365        let suffix_slice = &bytes[8..15];
366
367        // The byte order is little-endian here, so we prepend a 0 to set the least significant
368        // byte.
369        let mut suffix_bytes = [0; 8];
370        suffix_bytes[1..8].copy_from_slice(suffix_slice);
371
372        let prefix = Felt::try_from(prefix_slice)
373            .map_err(AccountIdError::AccountIdInvalidPrefixFieldElement)?;
374
375        let suffix = Felt::try_from(suffix_bytes.as_slice())
376            .map_err(AccountIdError::AccountIdInvalidSuffixFieldElement)?;
377
378        Self::try_from([prefix, suffix])
379    }
380}
381
382impl TryFrom<u128> for AccountIdV0 {
383    type Error = AccountIdError;
384
385    /// See [`TryFrom<u128> for AccountId`](super::AccountId#impl-TryFrom<u128>-for-AccountId) for
386    /// details.
387    fn try_from(int: u128) -> Result<Self, Self::Error> {
388        let mut bytes: [u8; 15] = [0; 15];
389        bytes.copy_from_slice(&int.to_be_bytes()[0..15]);
390
391        Self::try_from(bytes)
392    }
393}
394
395// SERIALIZATION
396// ================================================================================================
397
398impl Serializable for AccountIdV0 {
399    fn write_into<W: miden_core::utils::ByteWriter>(&self, target: &mut W) {
400        let bytes: [u8; 15] = (*self).into();
401        bytes.write_into(target);
402    }
403
404    fn get_size_hint(&self) -> usize {
405        Self::SERIALIZED_SIZE
406    }
407}
408
409impl Deserializable for AccountIdV0 {
410    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
411        <[u8; 15]>::read_from(source)?
412            .try_into()
413            .map_err(|err: AccountIdError| DeserializationError::InvalidValue(err.to_string()))
414    }
415}
416
417// HELPER FUNCTIONS
418// ================================================================================================
419
420/// Returns an [AccountId] instantiated with the provided field elements.
421///
422/// # Errors
423///
424/// Returns an error if any of the ID constraints are not met. See the [constraints
425/// documentation](AccountId#constraints) for details.
426fn account_id_from_felts(elements: [Felt; 2]) -> Result<AccountIdV0, AccountIdError> {
427    validate_prefix(elements[0])?;
428    validate_suffix(elements[1])?;
429
430    Ok(AccountIdV0 { prefix: elements[0], suffix: elements[1] })
431}
432
433/// Checks that the prefix:
434/// - has known values for metadata (storage mode, type and version).
435pub(crate) fn validate_prefix(
436    prefix: Felt,
437) -> Result<(AccountType, AccountStorageMode, AccountIdVersion), AccountIdError> {
438    let prefix = prefix.as_int();
439
440    // Validate storage bits.
441    let storage_mode = extract_storage_mode(prefix)?;
442
443    // Validate version bits.
444    let version = extract_version(prefix)?;
445
446    let account_type = extract_type(prefix);
447
448    Ok((account_type, storage_mode, version))
449}
450
451/// Checks that the suffix:
452/// - has its most significant bit set to zero.
453/// - has its lower 8 bits set to zero.
454const fn validate_suffix(suffix: Felt) -> Result<(), AccountIdError> {
455    let suffix = suffix.as_int();
456
457    // Validate most significant bit is zero.
458    if suffix >> 63 != 0 {
459        return Err(AccountIdError::AccountIdSuffixMostSignificantBitMustBeZero);
460    }
461
462    // Validate lower 8 bits of second felt are zero.
463    if suffix & 0xff != 0 {
464        return Err(AccountIdError::AccountIdSuffixLeastSignificantByteMustBeZero);
465    }
466
467    Ok(())
468}
469
470pub(crate) fn extract_storage_mode(prefix: u64) -> Result<AccountStorageMode, AccountIdError> {
471    let bits = (prefix & AccountIdV0::STORAGE_MODE_MASK as u64) >> AccountIdV0::STORAGE_MODE_SHIFT;
472    // SAFETY: `STORAGE_MODE_MASK` is u8 so casting bits is lossless
473    match bits as u8 {
474        PUBLIC => Ok(AccountStorageMode::Public),
475        NETWORK => Ok(AccountStorageMode::Network),
476        PRIVATE => Ok(AccountStorageMode::Private),
477        _ => Err(AccountIdError::UnknownAccountStorageMode(format!("0b{bits:b}").into())),
478    }
479}
480
481pub(crate) fn extract_version(prefix: u64) -> Result<AccountIdVersion, AccountIdError> {
482    // SAFETY: The mask guarantees that we only mask out the least significant nibble, so casting to
483    // u8 is safe.
484    let version = (prefix & AccountIdV0::VERSION_MASK) as u8;
485    AccountIdVersion::try_from(version)
486}
487
488pub(crate) const fn extract_type(prefix: u64) -> AccountType {
489    let bits = (prefix & (AccountIdV0::TYPE_MASK as u64)) >> AccountIdV0::TYPE_SHIFT;
490    // SAFETY: `TYPE_MASK` is u8 so casting bits is lossless
491    match bits as u8 {
492        REGULAR_ACCOUNT_UPDATABLE_CODE => AccountType::RegularAccountUpdatableCode,
493        REGULAR_ACCOUNT_IMMUTABLE_CODE => AccountType::RegularAccountImmutableCode,
494        FUNGIBLE_FAUCET => AccountType::FungibleFaucet,
495        NON_FUNGIBLE_FAUCET => AccountType::NonFungibleFaucet,
496        _ => {
497            // SAFETY: type mask contains only 2 bits and we've covered all 4 possible options.
498            panic!("type mask contains only 2 bits and we've covered all 4 possible options")
499        },
500    }
501}
502
503/// Shapes the suffix so it meets the requirements of the account ID, by setting the lower 8 bits to
504/// zero.
505fn shape_suffix(suffix: Felt) -> Felt {
506    let mut suffix = suffix.as_int();
507
508    // Clear the lower 8 bits.
509    suffix &= 0xffff_ffff_ffff_ff00;
510
511    // SAFETY: Felt was previously valid and we only cleared bits, so it must still be valid.
512    Felt::try_from(suffix).expect("no bits were set so felt should still be valid")
513}
514
515// COMMON TRAIT IMPLS
516// ================================================================================================
517
518impl PartialOrd for AccountIdV0 {
519    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
520        Some(self.cmp(other))
521    }
522}
523
524impl Ord for AccountIdV0 {
525    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
526        u128::from(*self).cmp(&u128::from(*other))
527    }
528}
529
530impl fmt::Display for AccountIdV0 {
531    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
532        write!(f, "{}", self.to_hex())
533    }
534}
535
536/// Returns the digest of two hashing permutations over the seed, code commitment, storage
537/// commitment and padding.
538pub(crate) fn compute_digest(seed: Word, code_commitment: Word, storage_commitment: Word) -> Word {
539    let mut elements = Vec::with_capacity(16);
540    elements.extend(seed);
541    elements.extend(*code_commitment);
542    elements.extend(*storage_commitment);
543    elements.extend(EMPTY_WORD);
544    Hasher::hash_elements(&elements)
545}
546
547// TESTS
548// ================================================================================================
549
550#[cfg(test)]
551mod tests {
552
553    use super::*;
554    use crate::account::AccountIdPrefix;
555    use crate::testing::account_id::{
556        ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
557        ACCOUNT_ID_PRIVATE_SENDER,
558        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
559        ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
560        ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
561    };
562
563    #[test]
564    fn account_id_from_felts_with_max_pop_count() {
565        let valid_suffix = Felt::try_from(0x7fff_ffff_ffff_ff00u64).unwrap();
566        let valid_prefix = Felt::try_from(0x7fff_ffff_ffff_ff70u64).unwrap();
567
568        let id1 = AccountIdV0::new_unchecked([valid_prefix, valid_suffix]);
569        assert_eq!(id1.account_type(), AccountType::NonFungibleFaucet);
570        assert_eq!(id1.storage_mode(), AccountStorageMode::Network);
571        assert_eq!(id1.version(), AccountIdVersion::Version0);
572    }
573
574    #[test]
575    fn account_id_dummy_construction() {
576        // Use the highest possible input to check if the constructed id is a valid Felt in that
577        // scenario.
578        // Use the lowest possible input to check whether the constructor produces valid IDs with
579        // all-zeroes input.
580        for input in [[0xff; 15], [0; 15]] {
581            for account_type in [
582                AccountType::FungibleFaucet,
583                AccountType::NonFungibleFaucet,
584                AccountType::RegularAccountImmutableCode,
585                AccountType::RegularAccountUpdatableCode,
586            ] {
587                for storage_mode in [AccountStorageMode::Private, AccountStorageMode::Public] {
588                    let id = AccountIdV0::dummy(input, account_type, storage_mode);
589                    assert_eq!(id.account_type(), account_type);
590                    assert_eq!(id.storage_mode(), storage_mode);
591                    assert_eq!(id.version(), AccountIdVersion::Version0);
592
593                    // Do a serialization roundtrip to ensure validity.
594                    let serialized_id = id.to_bytes();
595                    AccountIdV0::read_from_bytes(&serialized_id).unwrap();
596                    assert_eq!(serialized_id.len(), AccountIdV0::SERIALIZED_SIZE);
597                }
598            }
599        }
600    }
601
602    #[test]
603    fn account_id_prefix_serialization_compatibility() {
604        // Ensure that an AccountIdPrefix can be read from the serialized bytes of an AccountId.
605        let account_id = AccountIdV0::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
606        let id_bytes = account_id.to_bytes();
607        assert_eq!(account_id.prefix().to_bytes(), id_bytes[..8]);
608
609        let deserialized_prefix = AccountIdPrefix::read_from_bytes(&id_bytes).unwrap();
610        assert_eq!(AccountIdPrefix::V0(account_id.prefix()), deserialized_prefix);
611
612        // Ensure AccountId and AccountIdPrefix's hex representation are compatible.
613        assert!(account_id.to_hex().starts_with(&account_id.prefix().to_hex()));
614    }
615
616    // CONVERSION TESTS
617    // ================================================================================================
618
619    #[test]
620    fn test_account_id_conversion_roundtrip() {
621        for (idx, account_id) in [
622            ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
623            ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
624            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
625            ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
626            ACCOUNT_ID_PRIVATE_SENDER,
627        ]
628        .into_iter()
629        .enumerate()
630        {
631            let id = AccountIdV0::try_from(account_id).expect("account ID should be valid");
632            assert_eq!(id, AccountIdV0::from_hex(&id.to_hex()).unwrap(), "failed in {idx}");
633            assert_eq!(id, AccountIdV0::try_from(<[u8; 15]>::from(id)).unwrap(), "failed in {idx}");
634            assert_eq!(id, AccountIdV0::try_from(u128::from(id)).unwrap(), "failed in {idx}");
635            // The u128 big-endian representation without the least significant byte and the
636            // [u8; 15] representations should be equivalent.
637            assert_eq!(u128::from(id).to_be_bytes()[0..15], <[u8; 15]>::from(id));
638            assert_eq!(account_id, u128::from(id), "failed in {idx}");
639        }
640    }
641
642    #[test]
643    fn test_account_id_tag_identifiers() {
644        let account_id = AccountIdV0::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE)
645            .expect("valid account ID");
646        assert!(account_id.is_regular_account());
647        assert_eq!(account_id.account_type(), AccountType::RegularAccountImmutableCode);
648        assert!(account_id.is_public());
649
650        let account_id = AccountIdV0::try_from(ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE)
651            .expect("valid account ID");
652        assert!(account_id.is_regular_account());
653        assert_eq!(account_id.account_type(), AccountType::RegularAccountUpdatableCode);
654        assert!(!account_id.is_public());
655
656        let account_id =
657            AccountIdV0::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).expect("valid account ID");
658        assert!(account_id.is_faucet());
659        assert_eq!(account_id.account_type(), AccountType::FungibleFaucet);
660        assert!(account_id.is_public());
661
662        let account_id = AccountIdV0::try_from(ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET)
663            .expect("valid account ID");
664        assert!(account_id.is_faucet());
665        assert_eq!(account_id.account_type(), AccountType::NonFungibleFaucet);
666        assert!(!account_id.is_public());
667    }
668}