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