miden_protocol/account/account_id/v1/
mod.rs1mod prefix;
2use alloc::string::{String, ToString};
3use alloc::vec::Vec;
4use core::fmt;
5use core::hash::Hash;
6
7use bech32::Bech32m;
8use bech32::primitives::decode::{ByteIter, CheckedHrpstring};
9use miden_crypto::utils::hex_to_bytes;
10pub use prefix::AccountIdPrefixV1;
11
12use crate::account::account_id::NetworkId;
13use crate::account::{AccountIdVersion, AccountType, AssetCallbackFlag};
14use crate::address::AddressType;
15use crate::errors::{AccountError, AccountIdError, Bech32Error};
16use crate::utils::serde::{
17 ByteReader,
18 ByteWriter,
19 Deserializable,
20 DeserializationError,
21 Serializable,
22};
23use crate::{EMPTY_WORD, Felt, Hasher, Word};
24
25#[derive(Debug, Copy, Clone, Eq, PartialEq)]
32pub struct AccountIdV1 {
33 suffix: Felt,
34 prefix: Felt,
35}
36
37impl Hash for AccountIdV1 {
38 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
39 self.prefix.as_canonical_u64().hash(state);
40 self.suffix.as_canonical_u64().hash(state);
41 }
42}
43
44impl AccountIdV1 {
45 const SERIALIZED_SIZE: usize = 15;
50
51 const VERSION_MASK: u64 = 0b1111;
53
54 pub(crate) const ACCOUNT_TYPE_MASK: u8 = 0b1 << Self::ACCOUNT_TYPE_SHIFT;
57 pub(crate) const ACCOUNT_TYPE_SHIFT: u64 = 4;
58
59 pub(crate) const ASSET_CALLBACK_FLAG_MASK: u8 = 0b1 << Self::ASSET_CALLBACK_FLAG_SHIFT;
62 pub(crate) const ASSET_CALLBACK_FLAG_SHIFT: u64 = 5;
63
64 pub(crate) const SEED_DIGEST_SUFFIX_ELEMENT_IDX: usize = 0;
67 pub(crate) const SEED_DIGEST_PREFIX_ELEMENT_IDX: usize = 1;
69
70 pub fn new(
75 seed: Word,
76 code_commitment: Word,
77 storage_commitment: Word,
78 ) -> Result<Self, AccountIdError> {
79 let seed_digest = compute_digest(seed, code_commitment, storage_commitment);
80
81 let mut suffix = seed_digest[Self::SEED_DIGEST_SUFFIX_ELEMENT_IDX];
84 let prefix = seed_digest[Self::SEED_DIGEST_PREFIX_ELEMENT_IDX];
85
86 suffix = shape_suffix(suffix);
87
88 Self::try_from_elements(suffix, prefix)
89 }
90
91 pub fn new_unchecked(elements: [Felt; 2]) -> Self {
93 let prefix = elements[0];
94 let suffix = elements[1];
95
96 if cfg!(debug_assertions) {
98 validate_prefix(prefix).expect("AccountId::new_unchecked called with invalid prefix");
99 validate_suffix(suffix).expect("AccountId::new_unchecked called with invalid suffix");
100 }
101
102 Self { prefix, suffix }
103 }
104
105 pub fn try_from_elements(suffix: Felt, prefix: Felt) -> Result<Self, AccountIdError> {
107 validate_suffix(suffix)?;
108 validate_prefix(prefix)?;
109
110 Ok(AccountIdV1 { suffix, prefix })
111 }
112
113 #[cfg(any(feature = "testing", test))]
115 pub fn dummy(
116 mut bytes: [u8; 15],
117 account_type: AccountType,
118 asset_callbacks: AssetCallbackFlag,
119 ) -> AccountIdV1 {
120 let version = AccountIdVersion::Version1 as u8;
121 let low_byte = (asset_callbacks.as_u8() << Self::ASSET_CALLBACK_FLAG_SHIFT)
122 | ((account_type as u8) << Self::ACCOUNT_TYPE_SHIFT)
123 | version;
124
125 bytes[7] = low_byte;
127
128 bytes[3] &= 0b1111_1110;
130
131 let prefix_bytes =
132 bytes[0..8].try_into().expect("we should have sliced off exactly 8 bytes");
133 let prefix = Felt::try_from(u64::from_be_bytes(prefix_bytes))
134 .expect("should be a valid felt due to the most significant bit being zero");
135
136 let mut suffix_bytes = [0; 8];
137 suffix_bytes[..7].copy_from_slice(&bytes[8..]);
140
141 let suffix = u64::from_be_bytes(suffix_bytes) & 0x7fff_ffff_ffff_ffff;
143 let mut suffix =
144 Felt::try_from(suffix).expect("no bits were set so felt should still be valid");
145
146 suffix = shape_suffix(suffix);
147
148 let account_id = Self::try_from_elements(suffix, prefix)
149 .expect("we should have shaped the felts to produce a valid id");
150
151 debug_assert_eq!(account_id.account_type(), account_type);
152 debug_assert_eq!(account_id.asset_callback_flag(), asset_callbacks);
153
154 account_id
155 }
156
157 pub fn compute_account_seed(
159 init_seed: [u8; 32],
160 account_type: AccountType,
161 asset_callbacks: AssetCallbackFlag,
162 version: AccountIdVersion,
163 code_commitment: Word,
164 storage_commitment: Word,
165 ) -> Result<Word, AccountError> {
166 crate::account::account_id::seed::compute_account_seed(
167 init_seed,
168 account_type,
169 asset_callbacks,
170 version,
171 code_commitment,
172 storage_commitment,
173 )
174 }
175
176 pub fn account_type(&self) -> AccountType {
181 extract_account_type(self.prefix().as_u64())
182 }
183
184 pub fn asset_callback_flag(&self) -> AssetCallbackFlag {
186 extract_asset_callback_flag(self.prefix().as_u64())
187 }
188
189 pub fn is_public(&self) -> bool {
191 self.account_type() == AccountType::Public
192 }
193
194 pub fn version(&self) -> AccountIdVersion {
196 extract_version(self.prefix().as_u64())
197 .expect("account ID should have been constructed with a valid version")
198 }
199
200 pub fn from_hex(hex_str: &str) -> Result<AccountIdV1, AccountIdError> {
202 hex_to_bytes(hex_str)
203 .map_err(AccountIdError::AccountIdHexParseError)
204 .and_then(AccountIdV1::try_from)
205 }
206
207 pub fn to_hex(self) -> String {
209 let mut hex_string =
213 format!("0x{:016x}{:016x}", self.prefix().as_u64(), self.suffix().as_canonical_u64());
214 hex_string.truncate(32);
215 hex_string
216 }
217
218 pub fn to_bech32(&self, network_id: NetworkId) -> String {
220 let id_bytes: [u8; Self::SERIALIZED_SIZE] = (*self).into();
221
222 let mut data = [0; Self::SERIALIZED_SIZE + 1];
223 data[0] = AddressType::AccountId as u8;
224 data[1..16].copy_from_slice(&id_bytes);
225
226 bech32::encode::<Bech32m>(network_id.into_hrp(), &data)
235 .expect("code length of bech32 should not be exceeded")
236 }
237
238 pub fn from_bech32(bech32_string: &str) -> Result<(NetworkId, Self), AccountIdError> {
240 let checked_string = CheckedHrpstring::new::<Bech32m>(bech32_string).map_err(|source| {
243 AccountIdError::Bech32DecodeError(Bech32Error::DecodeError(source.to_string().into()))
248 })?;
249
250 let hrp = checked_string.hrp();
251 let network_id = NetworkId::from_hrp(hrp);
252
253 let mut byte_iter = checked_string.byte_iter();
254
255 if byte_iter.len() != Self::SERIALIZED_SIZE + 1 {
257 return Err(AccountIdError::Bech32DecodeError(Bech32Error::InvalidDataLength {
258 expected: Self::SERIALIZED_SIZE + 1,
259 actual: byte_iter.len(),
260 }));
261 }
262
263 let address_byte = byte_iter.next().expect("there should be at least one byte");
264 if address_byte != AddressType::AccountId as u8 {
265 return Err(AccountIdError::Bech32DecodeError(Bech32Error::UnknownAddressType(
266 address_byte,
267 )));
268 }
269
270 Self::from_bech32_byte_iter(byte_iter).map(|account_id| (network_id, account_id))
271 }
272
273 pub(crate) fn from_bech32_byte_iter(byte_iter: ByteIter<'_>) -> Result<Self, AccountIdError> {
275 if byte_iter.len() != Self::SERIALIZED_SIZE {
277 return Err(AccountIdError::Bech32DecodeError(Bech32Error::InvalidDataLength {
278 expected: Self::SERIALIZED_SIZE,
279 actual: byte_iter.len(),
280 }));
281 }
282
283 let mut id_bytes = [0_u8; Self::SERIALIZED_SIZE];
286 for (i, byte) in byte_iter.enumerate() {
287 id_bytes[i] = byte;
288 }
289
290 let account_id = Self::try_from(id_bytes)?;
291
292 Ok(account_id)
293 }
294
295 pub fn prefix(&self) -> AccountIdPrefixV1 {
299 AccountIdPrefixV1::new_unchecked(self.prefix)
302 }
303
304 pub const fn suffix(&self) -> Felt {
306 self.suffix
307 }
308}
309
310impl From<AccountIdV1> for [u8; 15] {
314 fn from(id: AccountIdV1) -> Self {
315 let mut result = [0_u8; 15];
316 result[..8].copy_from_slice(&id.prefix().as_u64().to_be_bytes());
317 result[8..].copy_from_slice(&id.suffix().as_canonical_u64().to_be_bytes()[..7]);
319 result
320 }
321}
322
323impl From<AccountIdV1> for u128 {
324 fn from(id: AccountIdV1) -> Self {
325 let mut le_bytes = [0_u8; 16];
326 le_bytes[..8].copy_from_slice(&id.suffix().as_canonical_u64().to_le_bytes());
327 le_bytes[8..].copy_from_slice(&id.prefix().as_u64().to_le_bytes());
328 u128::from_le_bytes(le_bytes)
329 }
330}
331
332impl TryFrom<[u8; 15]> for AccountIdV1 {
336 type Error = AccountIdError;
337
338 fn try_from(mut bytes: [u8; 15]) -> Result<Self, Self::Error> {
341 bytes[..8].reverse();
344 bytes[8..15].reverse();
346
347 let prefix_slice = &bytes[..8];
348 let suffix_slice = &bytes[8..15];
349
350 let mut suffix_bytes = [0; 8];
353 suffix_bytes[1..8].copy_from_slice(suffix_slice);
354
355 let prefix = Felt::try_from(u64::from_le_bytes(
356 prefix_slice.try_into().expect("prefix slice should be 8 bytes"),
357 ))
358 .map_err(|err| {
359 AccountIdError::AccountIdInvalidPrefixFieldElement(DeserializationError::InvalidValue(
360 err.to_string(),
361 ))
362 })?;
363
364 let suffix = Felt::try_from(u64::from_le_bytes(suffix_bytes)).map_err(|err| {
365 AccountIdError::AccountIdInvalidSuffixFieldElement(DeserializationError::InvalidValue(
366 err.to_string(),
367 ))
368 })?;
369
370 Self::try_from_elements(suffix, prefix)
371 }
372}
373
374impl TryFrom<u128> for AccountIdV1 {
375 type Error = AccountIdError;
376
377 fn try_from(int: u128) -> Result<Self, Self::Error> {
380 let mut bytes: [u8; 15] = [0; 15];
381 bytes.copy_from_slice(&int.to_be_bytes()[0..15]);
382
383 Self::try_from(bytes)
384 }
385}
386
387impl Serializable for AccountIdV1 {
391 fn write_into<W: ByteWriter>(&self, target: &mut W) {
392 let bytes: [u8; 15] = (*self).into();
393 bytes.write_into(target);
394 }
395
396 fn get_size_hint(&self) -> usize {
397 Self::SERIALIZED_SIZE
398 }
399}
400
401impl Deserializable for AccountIdV1 {
402 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
403 <[u8; 15]>::read_from(source)?
404 .try_into()
405 .map_err(|err: AccountIdError| DeserializationError::InvalidValue(err.to_string()))
406 }
407}
408
409pub(crate) fn validate_prefix(
414 prefix: Felt,
415) -> Result<(AccountType, AccountIdVersion), AccountIdError> {
416 let prefix = prefix.as_canonical_u64();
417
418 let account_type = extract_account_type(prefix);
419
420 let version = extract_version(prefix)?;
422
423 Ok((account_type, version))
424}
425
426fn validate_suffix(suffix: Felt) -> Result<(), AccountIdError> {
430 let suffix = suffix.as_canonical_u64();
431
432 if suffix >> 63 != 0 {
434 return Err(AccountIdError::AccountIdSuffixMostSignificantBitMustBeZero);
435 }
436
437 if suffix & 0xff != 0 {
439 return Err(AccountIdError::AccountIdSuffixLeastSignificantByteMustBeZero);
440 }
441
442 Ok(())
443}
444
445pub(crate) fn extract_account_type(prefix: u64) -> AccountType {
446 let bits = (prefix & AccountIdV1::ACCOUNT_TYPE_MASK as u64) >> AccountIdV1::ACCOUNT_TYPE_SHIFT;
447 match bits as u8 {
449 AccountType::PRIVATE => AccountType::Private,
450 AccountType::PUBLIC => AccountType::Public,
451 _ => unreachable!("account type mask is 1 bit so every value is covered above"),
452 }
453}
454
455pub(crate) fn extract_asset_callback_flag(prefix: u64) -> AssetCallbackFlag {
456 AssetCallbackFlag::from(prefix & AccountIdV1::ASSET_CALLBACK_FLAG_MASK as u64 != 0)
457}
458
459pub(crate) fn extract_version(prefix: u64) -> Result<AccountIdVersion, AccountIdError> {
460 let version = (prefix & AccountIdV1::VERSION_MASK) as u8;
463 AccountIdVersion::try_from(version)
464}
465
466fn shape_suffix(suffix: Felt) -> Felt {
469 let mut suffix = suffix.as_canonical_u64();
470
471 suffix &= 0xffff_ffff_ffff_ff00;
473
474 Felt::try_from(suffix).expect("no bits were set so felt should still be valid")
476}
477
478impl PartialOrd for AccountIdV1 {
482 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
483 Some(self.cmp(other))
484 }
485}
486
487impl Ord for AccountIdV1 {
488 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
489 u128::from(*self).cmp(&u128::from(*other))
490 }
491}
492
493impl fmt::Display for AccountIdV1 {
494 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
495 write!(f, "{}", self.to_hex())
496 }
497}
498
499pub(crate) fn compute_digest(seed: Word, code_commitment: Word, storage_commitment: Word) -> Word {
502 let mut elements = Vec::with_capacity(16);
503 elements.extend(seed);
504 elements.extend(*code_commitment);
505 elements.extend(*storage_commitment);
506 elements.extend(EMPTY_WORD);
507 Hasher::hash_elements(&elements)
508}
509
510#[cfg(test)]
514mod tests {
515
516 use rstest::rstest;
517
518 use super::*;
519 use crate::account::AccountIdPrefix;
520 use crate::testing::account_id::{
521 ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
522 ACCOUNT_ID_PRIVATE_SENDER,
523 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
524 ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
525 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
526 };
527
528 #[test]
529 fn account_id_from_felts_with_max_pop_count() {
530 let valid_suffix = Felt::try_from(0x7fff_ffff_ffff_ff00u64).unwrap();
531 let valid_prefix = Felt::try_from(0x7fff_ffff_ffff_fff1u64).unwrap();
532
533 let id1 = AccountIdV1::new_unchecked([valid_prefix, valid_suffix]);
534 assert_eq!(id1.account_type(), AccountType::Public);
535 assert_eq!(id1.version(), AccountIdVersion::Version1);
536 }
537
538 #[rstest]
539 fn account_id_serde_roundtrip(
540 #[values([0xff; 15], [0; 15])] input: [u8; 15],
545 #[values(AccountType::Private, AccountType::Public)] account_type: AccountType,
546 #[values(AssetCallbackFlag::Disabled, AssetCallbackFlag::Enabled)]
547 asset_callbacks: AssetCallbackFlag,
548 ) {
549 let id = AccountIdV1::dummy(input, account_type, asset_callbacks);
550 assert_eq!(id.account_type(), account_type);
551 assert_eq!(id.asset_callback_flag(), asset_callbacks);
552 assert_eq!(id.version(), AccountIdVersion::Version1);
553
554 let serialized_id = id.to_bytes();
556 let deserialized_id = AccountIdV1::read_from_bytes(&serialized_id).unwrap();
557 assert_eq!(deserialized_id.asset_callback_flag(), asset_callbacks);
558 assert_eq!(serialized_id.len(), AccountIdV1::SERIALIZED_SIZE);
559
560 let serialized_prefix = id.prefix().to_bytes();
561 let deserialized_prefix = AccountIdPrefix::read_from_bytes(&serialized_prefix).unwrap();
562 assert_eq!(deserialized_prefix.asset_callback_flag(), asset_callbacks);
563 assert_eq!(serialized_prefix.len(), AccountIdPrefix::SERIALIZED_SIZE);
564 }
565
566 #[test]
567 fn account_id_prefix_serialization_compatibility() {
568 let account_id = AccountIdV1::try_from(ACCOUNT_ID_PRIVATE_SENDER).unwrap();
570 let id_bytes = account_id.to_bytes();
571 assert_eq!(account_id.prefix().to_bytes(), id_bytes[..8]);
572
573 let deserialized_prefix = AccountIdPrefix::read_from_bytes(&id_bytes).unwrap();
574 assert_eq!(AccountIdPrefix::V1(account_id.prefix()), deserialized_prefix);
575
576 assert!(account_id.to_hex().starts_with(&account_id.prefix().to_hex()));
578 }
579
580 #[test]
584 fn test_account_id_conversion_roundtrip() {
585 for (idx, account_id) in [
586 ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
587 ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
588 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
589 ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
590 ACCOUNT_ID_PRIVATE_SENDER,
591 ]
592 .into_iter()
593 .enumerate()
594 {
595 let id = AccountIdV1::try_from(account_id).expect("account ID should be valid");
596 assert_eq!(id, AccountIdV1::from_hex(&id.to_hex()).unwrap(), "failed in {idx}");
597 assert_eq!(id, AccountIdV1::try_from(<[u8; 15]>::from(id)).unwrap(), "failed in {idx}");
598 assert_eq!(id, AccountIdV1::try_from(u128::from(id)).unwrap(), "failed in {idx}");
599 assert_eq!(u128::from(id).to_be_bytes()[0..15], <[u8; 15]>::from(id));
602 assert_eq!(account_id, u128::from(id), "failed in {idx}");
603 }
604 }
605
606 #[test]
607 fn test_account_id_accessors() {
608 let account_id = AccountIdV1::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE)
609 .expect("valid account ID");
610 assert!(account_id.is_public());
611
612 let account_id = AccountIdV1::try_from(ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE)
613 .expect("valid account ID");
614 assert!(!account_id.is_public());
615
616 let account_id =
617 AccountIdV1::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).expect("valid account ID");
618 assert!(account_id.is_public());
619
620 let account_id = AccountIdV1::try_from(ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET)
621 .expect("valid account ID");
622 assert!(!account_id.is_public());
623 }
624}