miden_objects/account/account_id/v0/
mod.rs

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