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