miden_objects/account/account_id/v0/
mod.rs

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