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}