Skip to main content

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