miden_objects/account/account_id/
mod.rs

1pub(crate) mod v0;
2pub use v0::{AccountIdPrefixV0, AccountIdV0};
3
4mod id_prefix;
5pub use id_prefix::AccountIdPrefix;
6
7mod seed;
8
9mod network_id;
10pub use network_id::NetworkId;
11
12mod account_type;
13pub use account_type::AccountType;
14
15mod storage_mode;
16pub use storage_mode::AccountStorageMode;
17
18mod id_version;
19use alloc::string::{String, ToString};
20use core::fmt;
21
22pub use id_version::AccountIdVersion;
23use miden_core::Felt;
24use miden_core::utils::{ByteReader, Deserializable, Serializable};
25use miden_crypto::utils::hex_to_bytes;
26use miden_processor::DeserializationError;
27
28use crate::errors::AccountIdError;
29use crate::{AccountError, Word};
30
31/// The identifier of an [`Account`](crate::account::Account).
32///
33/// This enum is a wrapper around concrete versions of IDs. The following documents version 0.
34///
35/// # Layout
36///
37/// An `AccountId` consists of two field elements, where the first is called the prefix and the
38/// second is called the suffix. It is laid out as follows:
39///
40/// ```text
41/// prefix: [hash (56 bits) | storage mode (2 bits) | type (2 bits) | version (4 bits)]
42/// suffix: [zero bit | hash (55 bits) | 8 zero bits]
43/// ```
44///
45/// # Generation
46///
47/// An `AccountId` is a commitment to a user-generated seed and the code and storage of an account.
48/// An id is generated by first creating the account's initial storage and code. Then a random seed
49/// is picked and the hash of `(SEED, CODE_COMMITMENT, STORAGE_COMMITMENT, EMPTY_WORD)` is computed.
50/// This process is repeated until the hash's first element has the desired storage mode, account
51/// type and version and the suffix' most significant bit is zero.
52///
53/// The prefix of the ID is exactly the first element of the hash. The suffix of the ID is the
54/// second element of the hash, but its lower 8 bits are zeroed. Thus, the prefix of the ID must
55/// derive exactly from the hash, while only the first 56 bits of the suffix are derived from the
56/// hash.
57///
58/// In total, due to requiring specific bits for storage mode, type, version and the most
59/// significant bit in the suffix, generating an ID requires 9 bits of Proof-of-Work.
60///
61/// # Constraints
62///
63/// Constructors will return an error if:
64///
65/// - The prefix contains account ID metadata (storage mode, type or version) that does not match
66///   any of the known values.
67/// - The most significant bit of the suffix is not zero.
68/// - The lower 8 bits of the suffix are not zero, although [`AccountId::new`] ensures this is the
69///   case rather than return an error.
70///
71/// # Design Rationale
72///
73/// The rationale behind the above layout is as follows.
74///
75/// - The prefix is the output of a hash function so it will be a valid field element without
76///   requiring additional constraints.
77/// - The version is placed at a static offset such that future ID versions which may change the
78///   number of type or storage mode bits will not cause the version to be at a different offset.
79///   This is important so that a parser can always reliably read the version and then parse the
80///   remainder of the ID depending on the version. Having only 4 bits for the version is a trade
81///   off between future proofing to allow introducing more versions and the version requiring Proof
82///   of Work as part of the ID generation.
83/// - The version, type and storage mode are part of the prefix which is included in the
84///   representation of a non-fungible asset. The prefix alone is enough to determine all of these
85///   properties about the ID.
86/// - The most significant bit of the suffix must be zero to ensure the value of the suffix is
87///   always a valid felt, even if the lower 8 bits are all set to `1`. The lower 8 bits of the
88///   suffix may be overwritten when the ID is embedded in other layouts such as the
89///   [`NoteMetadata`](crate::note::NoteMetadata). In that case, it can happen that all lower bits
90///   of the encoded suffix are one, so having the zero bit constraint is important for validity.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92pub enum AccountId {
93    V0(AccountIdV0),
94}
95
96impl AccountId {
97    // CONSTANTS
98    // --------------------------------------------------------------------------------------------
99
100    /// The serialized size of an [`AccountId`] in bytes.
101    pub const SERIALIZED_SIZE: usize = 15;
102
103    // CONSTRUCTORS
104    // --------------------------------------------------------------------------------------------
105
106    /// Creates an [`AccountId`] by hashing the given `seed`, `code_commitment`,
107    /// `storage_commitment` and using the resulting first and second element of the hash as the
108    /// prefix and suffix felts of the ID.
109    ///
110    /// See the documentation of the [`AccountId`] for more details on the generation.
111    ///
112    /// # Errors
113    ///
114    /// Returns an error if any of the ID constraints are not met. See the [constraints
115    /// documentation](AccountId#constraints) for details.
116    pub fn new(
117        seed: Word,
118        version: AccountIdVersion,
119        code_commitment: Word,
120        storage_commitment: Word,
121    ) -> Result<Self, AccountIdError> {
122        match version {
123            AccountIdVersion::Version0 => {
124                AccountIdV0::new(seed, code_commitment, storage_commitment).map(Self::V0)
125            },
126        }
127    }
128
129    /// Creates an [`AccountId`] from the given felts where the felt at index 0 is the prefix
130    /// and the felt at index 1 is the suffix.
131    ///
132    /// # Warning
133    ///
134    /// Validity of the ID must be ensured by the caller. An invalid ID may lead to panics.
135    ///
136    /// # Panics
137    ///
138    /// Panics if the prefix does not contain a known account ID version.
139    ///
140    /// If debug_assertions are enabled (e.g. in debug mode), this function panics if any of the ID
141    /// constraints are not met. See the [constraints documentation](AccountId#constraints) for
142    /// details.
143    pub fn new_unchecked(elements: [Felt; 2]) -> Self {
144        // The prefix contains the metadata.
145        // If we add more versions in the future, we may need to generalize this.
146        match v0::extract_version(elements[0].as_int())
147            .expect("prefix should contain a valid account ID version")
148        {
149            AccountIdVersion::Version0 => Self::V0(AccountIdV0::new_unchecked(elements)),
150        }
151    }
152
153    /// Constructs an [`AccountId`] for testing purposes with the given account type, storage
154    /// mode.
155    ///
156    /// This function does the following:
157    /// - Split the given bytes into a `prefix = bytes[0..8]` and `suffix = bytes[8..]` part to be
158    ///   used for the prefix and suffix felts, respectively.
159    /// - The least significant byte of the prefix is set to the given version, type and storage
160    ///   mode.
161    /// - The 32nd most significant bit in the prefix is cleared to ensure it is a valid felt. The
162    ///   32nd is chosen as it is the lowest bit that we can clear and still ensure felt validity.
163    ///   This leaves the upper 31 bits to be set by the input `bytes` which makes it simpler to
164    ///   create test values which more often need specific values for the most significant end of
165    ///   the ID.
166    /// - In the suffix the most significant bit and the lower 8 bits are cleared.
167    #[cfg(any(feature = "testing", test))]
168    pub fn dummy(
169        bytes: [u8; 15],
170        version: AccountIdVersion,
171        account_type: AccountType,
172        storage_mode: AccountStorageMode,
173    ) -> AccountId {
174        match version {
175            AccountIdVersion::Version0 => {
176                Self::V0(AccountIdV0::dummy(bytes, account_type, storage_mode))
177            },
178        }
179    }
180
181    /// Grinds an account seed until its hash matches the given `account_type`, `storage_mode` and
182    /// `version` and returns it as a [`Word`]. The input to the hash function next to the seed are
183    /// the `code_commitment` and `storage_commitment`.
184    ///
185    /// The grinding process is started from the given `init_seed` which should be a random seed
186    /// generated from a cryptographically secure source.
187    pub fn compute_account_seed(
188        init_seed: [u8; 32],
189        account_type: AccountType,
190        storage_mode: AccountStorageMode,
191        version: AccountIdVersion,
192        code_commitment: Word,
193        storage_commitment: Word,
194    ) -> Result<Word, AccountError> {
195        match version {
196            AccountIdVersion::Version0 => AccountIdV0::compute_account_seed(
197                init_seed,
198                account_type,
199                storage_mode,
200                version,
201                code_commitment,
202                storage_commitment,
203            ),
204        }
205    }
206
207    // PUBLIC ACCESSORS
208    // --------------------------------------------------------------------------------------------
209
210    /// Returns the type of this account ID.
211    pub const fn account_type(&self) -> AccountType {
212        match self {
213            AccountId::V0(account_id) => account_id.account_type(),
214        }
215    }
216
217    /// Returns `true` if an account with this ID is a faucet which can issue assets.
218    pub fn is_faucet(&self) -> bool {
219        self.account_type().is_faucet()
220    }
221
222    /// Returns `true` if an account with this ID is a regular account.
223    pub fn is_regular_account(&self) -> bool {
224        self.account_type().is_regular_account()
225    }
226
227    /// Returns the storage mode of this account ID.
228    pub fn storage_mode(&self) -> AccountStorageMode {
229        match self {
230            AccountId::V0(account_id) => account_id.storage_mode(),
231        }
232    }
233
234    /// Returns `true` if the full state of the account is on chain, i.e. if the modes are
235    /// [`AccountStorageMode::Public`] or [`AccountStorageMode::Network`], `false` otherwise.
236    pub fn is_onchain(&self) -> bool {
237        self.storage_mode().is_onchain()
238    }
239
240    /// Returns `true` if the storage mode is [`AccountStorageMode::Public`], `false` otherwise.
241    pub fn is_public(&self) -> bool {
242        self.storage_mode().is_public()
243    }
244
245    /// Returns `true` if the storage mode is [`AccountStorageMode::Network`], `false` otherwise.
246    pub fn is_network(&self) -> bool {
247        self.storage_mode().is_network()
248    }
249
250    /// Returns `true` if the storage mode is [`AccountStorageMode::Private`], `false` otherwise.
251    pub fn is_private(&self) -> bool {
252        self.storage_mode().is_private()
253    }
254
255    /// Returns the version of this account ID.
256    pub fn version(&self) -> AccountIdVersion {
257        match self {
258            AccountId::V0(_) => AccountIdVersion::Version0,
259        }
260    }
261
262    /// Creates an [`AccountId`] from a hex string. Assumes the string starts with "0x" and
263    /// that the hexadecimal characters are big-endian encoded.
264    pub fn from_hex(hex_str: &str) -> Result<Self, AccountIdError> {
265        hex_to_bytes(hex_str)
266            .map_err(AccountIdError::AccountIdHexParseError)
267            .and_then(AccountId::try_from)
268    }
269
270    /// Returns a big-endian, hex-encoded string of length 32, including the `0x` prefix. This means
271    /// it encodes 15 bytes.
272    pub fn to_hex(self) -> String {
273        match self {
274            AccountId::V0(account_id) => account_id.to_hex(),
275        }
276    }
277
278    /// Returns the [`AccountIdPrefix`] of this ID.
279    ///
280    /// The prefix of an account ID is guaranteed to be unique.
281    pub fn prefix(&self) -> AccountIdPrefix {
282        match self {
283            AccountId::V0(account_id) => AccountIdPrefix::V0(account_id.prefix()),
284        }
285    }
286
287    /// Returns the suffix of this ID as a [`Felt`].
288    pub const fn suffix(&self) -> Felt {
289        match self {
290            AccountId::V0(account_id) => account_id.suffix(),
291        }
292    }
293}
294
295// CONVERSIONS FROM ACCOUNT ID
296// ================================================================================================
297
298impl From<AccountId> for [Felt; 2] {
299    fn from(id: AccountId) -> Self {
300        match id {
301            AccountId::V0(account_id) => account_id.into(),
302        }
303    }
304}
305
306impl From<AccountId> for [u8; 15] {
307    fn from(id: AccountId) -> Self {
308        match id {
309            AccountId::V0(account_id) => account_id.into(),
310        }
311    }
312}
313
314impl From<AccountId> for u128 {
315    fn from(id: AccountId) -> Self {
316        match id {
317            AccountId::V0(account_id) => account_id.into(),
318        }
319    }
320}
321
322// CONVERSIONS TO ACCOUNT ID
323// ================================================================================================
324
325impl From<AccountIdV0> for AccountId {
326    fn from(id: AccountIdV0) -> Self {
327        Self::V0(id)
328    }
329}
330
331impl TryFrom<[Felt; 2]> for AccountId {
332    type Error = AccountIdError;
333
334    /// Returns an [`AccountId`] instantiated with the provided field elements where `elements[0]`
335    /// is taken as the prefix and `elements[1]` is taken as the suffix.
336    ///
337    /// # Errors
338    ///
339    /// Returns an error if any of the ID constraints are not met. See the [constraints
340    /// documentation](AccountId#constraints) for details.
341    fn try_from(elements: [Felt; 2]) -> Result<Self, Self::Error> {
342        // The prefix contains the metadata.
343        // If we add more versions in the future, we may need to generalize this.
344        match v0::extract_version(elements[0].as_int())? {
345            AccountIdVersion::Version0 => AccountIdV0::try_from(elements).map(Self::V0),
346        }
347    }
348}
349
350impl TryFrom<[u8; 15]> for AccountId {
351    type Error = AccountIdError;
352
353    /// Tries to convert a byte array in big-endian order to an [`AccountId`].
354    ///
355    /// # Errors
356    ///
357    /// Returns an error if any of the ID constraints are not met. See the [constraints
358    /// documentation](AccountId#constraints) for details.
359    fn try_from(bytes: [u8; 15]) -> Result<Self, Self::Error> {
360        // The least significant byte of the ID prefix contains the metadata.
361        let metadata_byte = bytes[7];
362        // We only have one supported version for now, so we use the extractor from that version.
363        // If we add more versions in the future, we may need to generalize this.
364        let version = v0::extract_version(metadata_byte as u64)?;
365
366        match version {
367            AccountIdVersion::Version0 => AccountIdV0::try_from(bytes).map(Self::V0),
368        }
369    }
370}
371
372impl TryFrom<u128> for AccountId {
373    type Error = AccountIdError;
374
375    /// Tries to convert a u128 into an [`AccountId`].
376    ///
377    /// # Errors
378    ///
379    /// Returns an error if any of the ID constraints are not met. See the [constraints
380    /// documentation](AccountId#constraints) for details.
381    fn try_from(int: u128) -> Result<Self, Self::Error> {
382        let mut bytes: [u8; 15] = [0; 15];
383        bytes.copy_from_slice(&int.to_be_bytes()[0..15]);
384
385        Self::try_from(bytes)
386    }
387}
388
389// COMMON TRAIT IMPLS
390// ================================================================================================
391
392impl PartialOrd for AccountId {
393    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
394        Some(self.cmp(other))
395    }
396}
397
398impl Ord for AccountId {
399    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
400        u128::from(*self).cmp(&u128::from(*other))
401    }
402}
403
404impl fmt::Display for AccountId {
405    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
406        write!(f, "{}", self.to_hex())
407    }
408}
409
410// SERIALIZATION
411// ================================================================================================
412
413impl Serializable for AccountId {
414    fn write_into<W: miden_core::utils::ByteWriter>(&self, target: &mut W) {
415        match self {
416            AccountId::V0(account_id) => {
417                account_id.write_into(target);
418            },
419        }
420    }
421
422    fn get_size_hint(&self) -> usize {
423        match self {
424            AccountId::V0(account_id) => account_id.get_size_hint(),
425        }
426    }
427}
428
429impl Deserializable for AccountId {
430    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
431        <[u8; 15]>::read_from(source)?
432            .try_into()
433            .map_err(|err: AccountIdError| DeserializationError::InvalidValue(err.to_string()))
434    }
435}
436
437// TESTS
438// ================================================================================================
439
440#[cfg(test)]
441mod tests {
442
443    use super::*;
444    use crate::testing::account_id::{
445        ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET,
446        ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
447        ACCOUNT_ID_PRIVATE_SENDER,
448        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
449        ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
450        ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
451    };
452
453    #[test]
454    fn test_account_id_wrapper_conversion_roundtrip() {
455        for (idx, account_id) in [
456            ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE,
457            ACCOUNT_ID_REGULAR_PRIVATE_ACCOUNT_UPDATABLE_CODE,
458            ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
459            ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
460            ACCOUNT_ID_PRIVATE_SENDER,
461            ACCOUNT_ID_NETWORK_NON_FUNGIBLE_FAUCET,
462        ]
463        .into_iter()
464        .enumerate()
465        {
466            let wrapper = AccountId::try_from(account_id).unwrap();
467            assert_eq!(
468                wrapper,
469                AccountId::read_from_bytes(&wrapper.to_bytes()).unwrap(),
470                "failed in {idx}"
471            );
472        }
473    }
474}