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}