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