miden_objects/account/account_id/
mod.rs

1mod id_anchor;
2pub use id_anchor::AccountIdAnchor;
3
4pub(crate) mod v0;
5pub use v0::{AccountIdPrefixV0, AccountIdV0};
6
7mod id_prefix;
8pub use id_prefix::AccountIdPrefix;
9
10mod seed;
11
12mod network_id;
13pub use network_id::NetworkId;
14
15mod address_type;
16pub use address_type::AddressType;
17
18mod account_type;
19pub use account_type::AccountType;
20
21mod storage_mode;
22pub use storage_mode::AccountStorageMode;
23
24mod id_version;
25use alloc::string::{String, ToString};
26use core::fmt;
27
28pub use id_version::AccountIdVersion;
29use miden_crypto::utils::hex_to_bytes;
30use vm_core::{
31    Felt, Word,
32    utils::{ByteReader, Deserializable, Serializable},
33};
34use vm_processor::{DeserializationError, Digest};
35
36use crate::{AccountError, errors::AccountIdError};
37
38/// The identifier of an [`Account`](crate::account::Account).
39///
40/// This enum is a wrapper around concrete versions of IDs. The following documents version 0.
41///
42/// # Layout
43///
44/// An `AccountId` consists of two field elements, where the first is called the prefix and the
45/// second is called the suffix. It is layed out as follows:
46///
47/// ```text
48/// prefix: [hash (56 bits) | storage mode (2 bits) | type (2 bits) | version (4 bits)]
49/// suffix: [anchor_epoch (16 bits) | hash (40 bits) | 8 zero bits]
50/// ```
51///
52/// # Generation
53///
54/// An `AccountId` is a commitment to a user-generated seed, the code and storage of an account and
55/// to a certain hash of an epoch block of the blockchain. An id is generated by picking an epoch
56/// block as an anchor - which is why it is also referred to as the anchor block - and creating the
57/// account's initial storage and code. Then a random seed is picked and the hash of `(SEED,
58/// CODE_COMMITMENT, STORAGE_COMMITMENT, ANCHOR_BLOCK_COMMITMENT)` is computed. If the hash's first
59/// element has the desired storage mode, account type and version, the computation part of the ID
60/// generation is done. If not, another random seed is picked and the process is repeated. The first
61/// felt of the ID is then the first element of the hash.
62///
63/// The suffix of the ID is the second element of the hash. Its upper 16 bits are overwritten
64/// with the epoch in which the ID is anchored and the lower 8 bits are zeroed. Thus, the prefix
65/// of the ID must derive exactly from the hash, while only part of the suffix is derived from
66/// the hash.
67///
68/// # Constraints
69///
70/// Constructors will return an error if:
71///
72/// - The prefix contains account ID metadata (storage mode, type or version) that does not match
73///   any of the known values.
74/// - The anchor epoch in the suffix is equal to [`u16::MAX`].
75/// - The lower 8 bits of the suffix are not zero, although [`AccountId::new`] ensures this is the
76///   case rather than return an error.
77///
78/// # Design Rationale
79///
80/// The rationale behind the above layout is as follows.
81///
82/// - The prefix is the output of a hash function so it will be a valid field element without
83///   requiring additional constraints.
84/// - The version is placed at a static offset such that future ID versions which may change the
85///   number of type or storage mode bits will not cause the version to be at a different offset.
86///   This is important so that a parser can always reliably read the version and then parse the
87///   remainder of the ID depending on the version. Having only 4 bits for the version is a trade
88///   off between future proofing to be able to introduce more versions and the version requiring
89///   Proof of Work as part of the ID generation.
90/// - The version, type and storage mode are part of the prefix which is included in the
91///   representation of a non-fungible asset. The prefix alone is enough to determine all of these
92///   properties about the ID.
93///     - The anchor epoch is not important beyond the creation process, so placing it in the second
94///       felt is fine. Moreover, all properties of the prefix must be derived from the seed, so
95///       they add to the proof of work difficulty. Adding 16 bits of PoW for the epoch would be
96///       significant.
97/// - The anchor epoch is placed at the most significant end of the suffix. Its value must be less
98///   than [`u16::MAX`] so that at least one of the upper 16 bits is always zero. This ensures that
99///   the entire suffix is valid even if the remaining bits of the felt are one.
100/// - The lower 8 bits of the suffix may be overwritten when the ID is encoded in other layouts such
101///   as the [`NoteMetadata`](crate::note::NoteMetadata). In such cases, it can happen that all bits
102///   of the encoded suffix would be one, so having the epoch constraint is important.
103/// - The ID is dependent on the hash of an epoch block. This is a block whose number is a multiple
104///   of 2^[`BlockNumber::EPOCH_LENGTH_EXPONENT`][epoch_len_exp], e.g. `0`, `65536`, `131072`, ...
105///   These are the first blocks of epoch 0, 1, 2, ... We call this dependence _anchoring_ because
106///   the ID is anchored to that epoch block's commitment. Anchoring makes it practically impossible
107///   for an attacker to construct a rainbow table of account IDs whose epoch is X, if the block for
108///   epoch X has not been constructed yet because its hash is then unknown. Therefore, picking a
109///   recent anchor block when generating a new ID makes it extremely unlikely that an attacker can
110///   highjack this ID because the hash of that block has only been known for a short period of
111///   time.
112///     - An ID highjack refers to an attack where a user generates an ID and lets someone else send
113///       assets to it. At this point the user has not registered the ID on-chain yet, likely
114///       because they need the funds in the asset to pay for their first transaction where the
115///       account would be registered. Until the ID is registered on chain, an attacker with a
116///       rainbow table who happens to have a seed, code and storage commitment combination that
117///       hashes to the user's ID can claim the assets sent to the user's ID. Adding the anchor
118///       block commitment to the ID generation process makes this attack practically impossible.
119///
120/// [epoch_len_exp]: crate::block::BlockNumber::EPOCH_LENGTH_EXPONENT
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum AccountId {
123    V0(AccountIdV0),
124}
125
126impl AccountId {
127    // CONSTANTS
128    // --------------------------------------------------------------------------------------------
129
130    /// The serialized size of an [`AccountId`] in bytes.
131    pub const SERIALIZED_SIZE: usize = 15;
132
133    // CONSTRUCTORS
134    // --------------------------------------------------------------------------------------------
135
136    /// Creates an [`AccountId`] by hashing the given `seed`, `code_commitment`,
137    /// `storage_commitment` and [`AccountIdAnchor::block_commitment`] from the `anchor` and using
138    /// the resulting first and second element of the hash as the prefix and suffix felts of the
139    /// ID.
140    ///
141    /// The [`AccountIdAnchor::epoch`] from the `anchor` overwrites part of the suffix.
142    ///
143    /// Note that the `anchor` must correspond to a valid block in the chain for the ID to be deemed
144    /// valid during creation.
145    ///
146    /// See the documentation of the [`AccountId`] for more details on the generation.
147    ///
148    /// # Errors
149    ///
150    /// Returns an error if any of the ID constraints are not met. See the [constraints
151    /// documentation](AccountId#constraints) for details.
152    pub fn new(
153        seed: Word,
154        anchor: AccountIdAnchor,
155        version: AccountIdVersion,
156        code_commitment: Digest,
157        storage_commitment: Digest,
158    ) -> Result<Self, AccountIdError> {
159        match version {
160            AccountIdVersion::Version0 => {
161                AccountIdV0::new(seed, anchor, code_commitment, storage_commitment).map(Self::V0)
162            },
163        }
164    }
165
166    /// Creates an [`AccountId`] from the given felts where the felt at index 0 is the prefix
167    /// and the felt at index 2 is the suffix.
168    ///
169    /// # Warning
170    ///
171    /// Validity of the ID must be ensured by the caller. An invalid ID may lead to panics.
172    ///
173    /// # Panics
174    ///
175    /// Panics if the prefix does not contain a known account ID version.
176    ///
177    /// If debug_assertions are enabled (e.g. in debug mode), this function panics if any of the ID
178    /// constraints are not met. See the [constraints documentation](AccountId#constraints) for
179    /// details.
180    pub fn new_unchecked(elements: [Felt; 2]) -> Self {
181        // The prefix contains the metadata.
182        // If we add more versions in the future, we may need to generalize this.
183        match v0::extract_version(elements[0].as_int())
184            .expect("prefix should contain a valid account ID version")
185        {
186            AccountIdVersion::Version0 => Self::V0(AccountIdV0::new_unchecked(elements)),
187        }
188    }
189
190    /// Constructs an [`AccountId`] for testing purposes with the given account type, storage
191    /// mode.
192    ///
193    /// This function does the following:
194    /// - Split the given bytes into a `prefix = bytes[0..8]` and `suffix = bytes[8..]` part to be
195    ///   used for the prefix and suffix felts, respectively.
196    /// - The least significant byte of the prefix is set to the given version, type and storage
197    ///   mode.
198    /// - The 32nd most significant bit in the prefix is cleared to ensure it is a valid felt. The
199    ///   32nd is chosen as it is the lowest bit that we can clear and still ensure felt validity.
200    ///   This leaves the upper 31 bits to be set by the input `bytes` which makes it simpler to
201    ///   create test values which more often need specific values for the most significant end of
202    ///   the ID.
203    /// - In the suffix the anchor epoch is set to 0 and the lower 8 bits are cleared.
204    #[cfg(any(feature = "testing", test))]
205    pub fn dummy(
206        bytes: [u8; 15],
207        version: AccountIdVersion,
208        account_type: AccountType,
209        storage_mode: AccountStorageMode,
210    ) -> AccountId {
211        match version {
212            AccountIdVersion::Version0 => {
213                Self::V0(AccountIdV0::dummy(bytes, account_type, storage_mode))
214            },
215        }
216    }
217
218    /// Grinds an account seed until its hash matches the given `account_type`, `storage_mode` and
219    /// `version` and returns it as a [`Word`]. The input to the hash function next to the seed are
220    /// the `code_commitment`, `storage_commitment` and `anchor_block_commitment`.
221    ///
222    /// The grinding process is started from the given `init_seed` which should be a random seed
223    /// generated from a cryptographically secure source.
224    pub fn compute_account_seed(
225        init_seed: [u8; 32],
226        account_type: AccountType,
227        storage_mode: AccountStorageMode,
228        version: AccountIdVersion,
229        code_commitment: Digest,
230        storage_commitment: Digest,
231        anchor_block_commitment: Digest,
232    ) -> Result<Word, AccountError> {
233        match version {
234            AccountIdVersion::Version0 => AccountIdV0::compute_account_seed(
235                init_seed,
236                account_type,
237                storage_mode,
238                version,
239                code_commitment,
240                storage_commitment,
241                anchor_block_commitment,
242            ),
243        }
244    }
245
246    // PUBLIC ACCESSORS
247    // --------------------------------------------------------------------------------------------
248
249    /// Returns the type of this account ID.
250    pub const fn account_type(&self) -> AccountType {
251        match self {
252            AccountId::V0(account_id) => account_id.account_type(),
253        }
254    }
255
256    /// Returns `true` if an account with this ID is a faucet which can issue assets.
257    pub fn is_faucet(&self) -> bool {
258        self.account_type().is_faucet()
259    }
260
261    /// Returns `true` if an account with this ID is a regular account.
262    pub fn is_regular_account(&self) -> bool {
263        self.account_type().is_regular_account()
264    }
265
266    /// Returns the storage mode of this account ID.
267    pub fn storage_mode(&self) -> AccountStorageMode {
268        match self {
269            AccountId::V0(account_id) => account_id.storage_mode(),
270        }
271    }
272
273    /// Returns `true` if the full state of the account is on chain, i.e. if the modes are
274    /// [`AccountStorageMode::Public`] or [`AccountStorageMode::Network`], `false` otherwise.
275    pub fn is_onchain(&self) -> bool {
276        self.storage_mode().is_onchain()
277    }
278
279    /// Returns `true` if the storage mode is [`AccountStorageMode::Public`], `false` otherwise.
280    pub fn is_public(&self) -> bool {
281        self.storage_mode().is_public()
282    }
283
284    /// Returns `true` if the storage mode is [`AccountStorageMode::Network`], `false` otherwise.
285    pub fn is_network(&self) -> bool {
286        self.storage_mode().is_network()
287    }
288
289    /// Returns `true` if the storage mode is [`AccountStorageMode::Private`], `false` otherwise.
290    pub fn is_private(&self) -> bool {
291        self.storage_mode().is_private()
292    }
293
294    /// Returns the version of this account ID.
295    pub fn version(&self) -> AccountIdVersion {
296        match self {
297            AccountId::V0(_) => AccountIdVersion::Version0,
298        }
299    }
300
301    /// Returns the anchor epoch of this account ID.
302    ///
303    /// This is the epoch to which this ID is anchored. The hash of this epoch block is used in the
304    /// generation of the ID.
305    pub fn anchor_epoch(&self) -> u16 {
306        match self {
307            AccountId::V0(account_id) => account_id.anchor_epoch(),
308        }
309    }
310
311    /// Creates an [`AccountId`] from a hex string. Assumes the string starts with "0x" and
312    /// that the hexadecimal characters are big-endian encoded.
313    pub fn from_hex(hex_str: &str) -> Result<Self, AccountIdError> {
314        hex_to_bytes(hex_str)
315            .map_err(AccountIdError::AccountIdHexParseError)
316            .and_then(AccountId::try_from)
317    }
318
319    /// Returns a big-endian, hex-encoded string of length 32, including the `0x` prefix. This means
320    /// it encodes 15 bytes.
321    pub fn to_hex(self) -> String {
322        match self {
323            AccountId::V0(account_id) => account_id.to_hex(),
324        }
325    }
326
327    /// Encodes the [`AccountId`] into a [bech32](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) string.
328    ///
329    /// # Encoding
330    ///
331    /// The encoding of an account ID into bech32 is done as follows:
332    /// - Convert the account ID into its `[u8; 15]` data format.
333    /// - Insert the address type [`AddressType::AccountId`] byte at index 0, shifting all other
334    ///   elements to the right.
335    /// - Choose an HRP, defined as a [`NetworkId`], for example [`NetworkId::Mainnet`] whose string
336    ///   representation is `mm`.
337    /// - Encode the resulting HRP together with the data into a bech32 string using the
338    ///   [`bech32::Bech32m`] checksum algorithm.
339    ///
340    /// This is an example of an account ID in hex and bech32 representations:
341    ///
342    /// ```text
343    /// hex:    0x140fa04a1e61fc100000126ef8f1d6
344    /// bech32: mm1qq2qlgz2reslcyqqqqfxa7836chrjcvk
345    /// ```
346    ///
347    /// ## Rationale
348    ///
349    /// Having the address type at the very beginning is so that it can be decoded to detect the
350    /// type of the address without having to decode the entire data. Moreover, choosing the
351    /// address type as a multiple of 8 means the first character of the bech32 string after the
352    /// `1` separator will be different for every address type. This makes the type of the address
353    /// conveniently human-readable.
354    ///
355    /// The only allowed checksum algorithm is [`Bech32m`](bech32::Bech32m) due to being the best
356    /// available checksum algorithm with no known weaknesses (unlike [`Bech32`](bech32::Bech32)).
357    /// No checksum is also not allowed since the intended use of bech32 is to have error
358    /// detection capabilities.
359    pub fn to_bech32(&self, network_id: NetworkId) -> String {
360        match self {
361            AccountId::V0(account_id_v0) => account_id_v0.to_bech32(network_id),
362        }
363    }
364
365    /// Decodes a [bech32](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki) string into an [`AccountId`].
366    ///
367    /// See [`AccountId::to_bech32`] for details on the format. The procedure for decoding the
368    /// bech32 data into the ID consists of the inverse operations of encoding.
369    pub fn from_bech32(bech32_string: &str) -> Result<(NetworkId, Self), AccountIdError> {
370        AccountIdV0::from_bech32(bech32_string)
371            .map(|(network_id, account_id)| (network_id, AccountId::V0(account_id)))
372    }
373
374    /// Returns the [`AccountIdPrefix`] of this ID.
375    ///
376    /// The prefix of an account ID is guaranteed to be unique.
377    pub fn prefix(&self) -> AccountIdPrefix {
378        match self {
379            AccountId::V0(account_id) => AccountIdPrefix::V0(account_id.prefix()),
380        }
381    }
382
383    /// Returns the suffix of this ID as a [`Felt`].
384    pub const fn suffix(&self) -> Felt {
385        match self {
386            AccountId::V0(account_id) => account_id.suffix(),
387        }
388    }
389}
390
391// CONVERSIONS FROM ACCOUNT ID
392// ================================================================================================
393
394impl From<AccountId> for [Felt; 2] {
395    fn from(id: AccountId) -> Self {
396        match id {
397            AccountId::V0(account_id) => account_id.into(),
398        }
399    }
400}
401
402impl From<AccountId> for [u8; 15] {
403    fn from(id: AccountId) -> Self {
404        match id {
405            AccountId::V0(account_id) => account_id.into(),
406        }
407    }
408}
409
410impl From<AccountId> for u128 {
411    fn from(id: AccountId) -> Self {
412        match id {
413            AccountId::V0(account_id) => account_id.into(),
414        }
415    }
416}
417
418// CONVERSIONS TO ACCOUNT ID
419// ================================================================================================
420
421impl From<AccountIdV0> for AccountId {
422    fn from(id: AccountIdV0) -> Self {
423        Self::V0(id)
424    }
425}
426
427impl TryFrom<[Felt; 2]> for AccountId {
428    type Error = AccountIdError;
429
430    /// Returns an [`AccountId`] instantiated with the provided field elements where `elements[0]`
431    /// is taken as the prefix and `elements[1]` is taken as the suffix.
432    ///
433    /// # Errors
434    ///
435    /// Returns an error if any of the ID constraints are not met. See the [constraints
436    /// documentation](AccountId#constraints) for details.
437    fn try_from(elements: [Felt; 2]) -> Result<Self, Self::Error> {
438        // The prefix contains the metadata.
439        // If we add more versions in the future, we may need to generalize this.
440        match v0::extract_version(elements[0].as_int())? {
441            AccountIdVersion::Version0 => AccountIdV0::try_from(elements).map(Self::V0),
442        }
443    }
444}
445
446impl TryFrom<[u8; 15]> for AccountId {
447    type Error = AccountIdError;
448
449    /// Tries to convert a byte array in big-endian order to an [`AccountId`].
450    ///
451    /// # Errors
452    ///
453    /// Returns an error if any of the ID constraints are not met. See the [constraints
454    /// documentation](AccountId#constraints) for details.
455    fn try_from(bytes: [u8; 15]) -> Result<Self, Self::Error> {
456        // The least significant byte of the ID prefix contains the metadata.
457        let metadata_byte = bytes[7];
458        // We only have one supported version for now, so we use the extractor from that version.
459        // If we add more versions in the future, we may need to generalize this.
460        let version = v0::extract_version(metadata_byte as u64)?;
461
462        match version {
463            AccountIdVersion::Version0 => AccountIdV0::try_from(bytes).map(Self::V0),
464        }
465    }
466}
467
468impl TryFrom<u128> for AccountId {
469    type Error = AccountIdError;
470
471    /// Tries to convert a u128 into an [`AccountId`].
472    ///
473    /// # Errors
474    ///
475    /// Returns an error if any of the ID constraints are not met. See the [constraints
476    /// documentation](AccountId#constraints) for details.
477    fn try_from(int: u128) -> Result<Self, Self::Error> {
478        let mut bytes: [u8; 15] = [0; 15];
479        bytes.copy_from_slice(&int.to_be_bytes()[0..15]);
480
481        Self::try_from(bytes)
482    }
483}
484
485// COMMON TRAIT IMPLS
486// ================================================================================================
487
488impl PartialOrd for AccountId {
489    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
490        Some(self.cmp(other))
491    }
492}
493
494impl Ord for AccountId {
495    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
496        u128::from(*self).cmp(&u128::from(*other))
497    }
498}
499
500impl fmt::Display for AccountId {
501    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
502        write!(f, "{}", self.to_hex())
503    }
504}
505
506// SERIALIZATION
507// ================================================================================================
508
509impl Serializable for AccountId {
510    fn write_into<W: miden_crypto::utils::ByteWriter>(&self, target: &mut W) {
511        match self {
512            AccountId::V0(account_id) => {
513                account_id.write_into(target);
514            },
515        }
516    }
517
518    fn get_size_hint(&self) -> usize {
519        match self {
520            AccountId::V0(account_id) => account_id.get_size_hint(),
521        }
522    }
523}
524
525impl Deserializable for AccountId {
526    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
527        <[u8; 15]>::read_from(source)?
528            .try_into()
529            .map_err(|err: AccountIdError| DeserializationError::InvalidValue(err.to_string()))
530    }
531}
532
533// TESTS
534// ================================================================================================
535
536#[cfg(test)]
537mod tests {
538    use assert_matches::assert_matches;
539    use bech32::{Bech32, Bech32m, Hrp, NoChecksum};
540
541    use super::*;
542    use crate::{
543        account::account_id::{
544            address_type::AddressType,
545            v0::{extract_storage_mode, extract_type, extract_version},
546        },
547        errors::Bech32Error,
548        testing::account_id::{
549            ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET, ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
550            ACCOUNT_ID_PRIVATE_SENDER, ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
551            ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
552            ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
553        },
554    };
555
556    #[test]
557    fn test_account_id_wrapper_conversion_roundtrip() {
558        for (idx, account_id) in [
559            ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
560            ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
561            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
562            ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
563            ACCOUNT_ID_PRIVATE_SENDER,
564            ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET,
565        ]
566        .into_iter()
567        .enumerate()
568        {
569            let wrapper = AccountId::try_from(account_id).unwrap();
570            assert_eq!(
571                wrapper,
572                AccountId::read_from_bytes(&wrapper.to_bytes()).unwrap(),
573                "failed in {idx}"
574            );
575        }
576    }
577
578    #[test]
579    fn bech32_encode_decode_roundtrip() {
580        // We use this to check that encoding does not panic even when using the longest possible
581        // HRP.
582        let longest_possible_hrp =
583            "01234567890123456789012345678901234567890123456789012345678901234567890123456789012";
584        assert_eq!(longest_possible_hrp.len(), 83);
585
586        for network_id in [
587            NetworkId::Mainnet,
588            NetworkId::Custom(Hrp::parse("custom").unwrap()),
589            NetworkId::Custom(Hrp::parse(longest_possible_hrp).unwrap()),
590        ] {
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 account_id = AccountId::try_from(account_id).unwrap();
602
603                let bech32_string = account_id.to_bech32(network_id);
604                let (decoded_network_id, decoded_account_id) =
605                    AccountId::from_bech32(&bech32_string).unwrap();
606
607                assert_eq!(network_id, decoded_network_id, "network id failed in {idx}");
608                assert_eq!(account_id, decoded_account_id, "account id failed in {idx}");
609
610                let (_, data) = bech32::decode(&bech32_string).unwrap();
611
612                // Raw bech32 data should contain the address type as the first byte.
613                assert_eq!(data[0], AddressType::AccountId as u8);
614
615                // Raw bech32 data should contain the metadata byte at index 8.
616                assert_eq!(extract_version(data[8] as u64).unwrap(), account_id.version());
617                assert_eq!(extract_type(data[8] as u64), account_id.account_type());
618                assert_eq!(
619                    extract_storage_mode(data[8] as u64).unwrap(),
620                    account_id.storage_mode()
621                );
622            }
623        }
624    }
625
626    #[test]
627    fn bech32_invalid_checksum() {
628        let network_id = NetworkId::Mainnet;
629        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
630
631        let bech32_string = account_id.to_bech32(network_id);
632        let mut invalid_bech32_1 = bech32_string.clone();
633        invalid_bech32_1.remove(0);
634        let mut invalid_bech32_2 = bech32_string.clone();
635        invalid_bech32_2.remove(7);
636
637        let error = AccountId::from_bech32(&invalid_bech32_1).unwrap_err();
638        assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_)));
639
640        let error = AccountId::from_bech32(&invalid_bech32_2).unwrap_err();
641        assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_)));
642    }
643
644    #[test]
645    fn bech32_invalid_address_type() {
646        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
647        let mut id_bytes = account_id.to_bytes();
648
649        // Set invalid address type.
650        id_bytes.insert(0, 16);
651
652        let invalid_bech32 =
653            bech32::encode::<Bech32m>(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap();
654
655        let error = AccountId::from_bech32(&invalid_bech32).unwrap_err();
656        assert_matches!(
657            error,
658            AccountIdError::Bech32DecodeError(Bech32Error::UnknownAddressType(16))
659        );
660    }
661
662    #[test]
663    fn bech32_invalid_other_checksum() {
664        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
665        let mut id_bytes = account_id.to_bytes();
666        id_bytes.insert(0, AddressType::AccountId as u8);
667
668        // Use Bech32 instead of Bech32m which is disallowed.
669        let invalid_bech32_regular =
670            bech32::encode::<Bech32>(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap();
671        let error = AccountId::from_bech32(&invalid_bech32_regular).unwrap_err();
672        assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_)));
673
674        // Use no checksum instead of Bech32m which is disallowed.
675        let invalid_bech32_no_checksum =
676            bech32::encode::<NoChecksum>(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap();
677        let error = AccountId::from_bech32(&invalid_bech32_no_checksum).unwrap_err();
678        assert_matches!(error, AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(_)));
679    }
680
681    #[test]
682    fn bech32_invalid_length() {
683        let account_id = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
684        let mut id_bytes = account_id.to_bytes();
685        id_bytes.insert(0, AddressType::AccountId as u8);
686        // Add one byte to make the length invalid.
687        id_bytes.push(5);
688
689        let invalid_bech32 =
690            bech32::encode::<Bech32m>(NetworkId::Mainnet.into_hrp(), &id_bytes).unwrap();
691
692        let error = AccountId::from_bech32(&invalid_bech32).unwrap_err();
693        assert_matches!(
694            error,
695            AccountIdError::Bech32DecodeError(Bech32Error::InvalidDataLength { .. })
696        );
697    }
698}