Skip to main content

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