Skip to main content

miden_protocol/asset/vault/
vault_key.rs

1use alloc::boxed::Box;
2use alloc::string::ToString;
3use core::fmt;
4
5use miden_crypto::merkle::smt::LeafIndex;
6
7use crate::account::AccountId;
8use crate::asset::vault::AssetId;
9use crate::asset::{Asset, AssetCallbackFlag, AssetComposition, FungibleAsset, NonFungibleAsset};
10use crate::crypto::merkle::smt::SMT_DEPTH;
11use crate::errors::AssetError;
12use crate::utils::serde::{
13    ByteReader,
14    ByteWriter,
15    Deserializable,
16    DeserializationError,
17    Serializable,
18};
19use crate::{Felt, Word};
20
21/// The unique identifier of an [`Asset`] in the [`AssetVault`](crate::asset::AssetVault).
22///
23/// Its [`Word`] layout is:
24/// ```text
25/// [
26///   asset_id_suffix (64 bits),
27///   asset_id_prefix (64 bits),
28///   [faucet_id_suffix (56 bits) | reserved (5 bits) | callback_flag (1 bit) | composition (2 bits)],
29///   faucet_id_prefix (64 bits)
30/// ]
31/// ```
32///
33/// The composition is the discriminator between assets and so it is placed at a static offset much
34/// like the version in an account ID. This makes it slightly easier to change the asset metadata in
35/// the future without affecting identification of previous assets.
36#[derive(Debug, PartialEq, Eq, Clone, Copy)]
37pub struct AssetVaultKey {
38    /// The asset ID of the vault key.
39    asset_id: AssetId,
40
41    /// The ID of the faucet that issued the asset.
42    faucet_id: AccountId,
43
44    /// The composition of the asset.
45    composition: AssetComposition,
46
47    /// Determines whether callbacks are enabled.
48    callback_flag: AssetCallbackFlag,
49}
50
51impl AssetVaultKey {
52    /// The serialized size of an [`AssetVaultKey`] in bytes.
53    ///
54    /// Serialized as its [`Word`] representation (4 field elements).
55    pub const SERIALIZED_SIZE: usize = Word::SERIALIZED_SIZE;
56
57    // BIT LAYOUT CONSTANTS
58    // --------------------------------------------------------------------------------------------
59
60    /// The metadata byte occupies the lower 8 bits of the third element of the key word.
61    pub(in crate::asset) const METADATA_BYTE_MASK: u8 = 0xff;
62
63    /// Bits 0-1 of the metadata byte encode the [`AssetComposition`]. The composition occupies
64    /// the lowest bits so its position remains stable as new metadata bits are added, since it
65    /// identifies the asset's type.
66    pub(in crate::asset) const COMPOSITION_MASK: u8 = 0b11;
67
68    /// Bit 2 of the metadata byte encodes the [`AssetCallbackFlag`].
69    pub(in crate::asset) const CALLBACK_FLAG_MASK: u8 = 0b1 << Self::CALLBACK_FLAG_SHIFT;
70    pub(in crate::asset) const CALLBACK_FLAG_SHIFT: u8 = 2;
71
72    /// Bits 3-7 of the metadata byte are reserved and must be zero.
73    pub(in crate::asset) const METADATA_RESERVED_MASK: u8 = 0b1111_1000;
74
75    // CONSTRUCTORS
76    // --------------------------------------------------------------------------------------------
77
78    /// Creates an [`AssetVaultKey`] from its parts with the given [`AssetComposition`] and
79    /// [`AssetCallbackFlag`].
80    ///
81    /// # Errors
82    ///
83    /// Returns an error if:
84    /// - the asset ID limbs are not zero when `composition` is [`AssetComposition::Fungible`].
85    /// - the composition is [`AssetComposition::Custom`], which is disallowed until its support is
86    ///   enabled in the tx kernel.
87    pub fn new(
88        asset_id: AssetId,
89        faucet_id: AccountId,
90        composition: AssetComposition,
91        callback_flag: AssetCallbackFlag,
92    ) -> Result<Self, AssetError> {
93        // For now, reject custom composition.
94        if composition.is_custom() {
95            return Err(AssetError::UnsupportedAssetComposition(AssetComposition::Custom));
96        }
97
98        if composition.is_fungible() && !asset_id.is_empty() {
99            return Err(AssetError::FungibleAssetIdMustBeZero(asset_id));
100        }
101
102        Ok(Self {
103            asset_id,
104            faucet_id,
105            composition,
106            callback_flag,
107        })
108    }
109
110    /// Constructs a fungible asset's key from a faucet ID.
111    pub fn new_fungible(faucet_id: AccountId, callback_flag: AssetCallbackFlag) -> Self {
112        Self::new(AssetId::default(), faucet_id, AssetComposition::Fungible, callback_flag).expect(
113            "passing AssetComposition::Fungible together with AssetId::default should be valid",
114        )
115    }
116
117    // PUBLIC ACCESSORS
118    // --------------------------------------------------------------------------------------------
119
120    /// Returns the word representation of the vault key.
121    ///
122    /// See the type-level documentation for details.
123    pub fn to_word(&self) -> Word {
124        let faucet_suffix = self.faucet_id.suffix().as_canonical_u64();
125        // The lower 8 bits of the faucet suffix are guaranteed to be zero and so it is used to
126        // encode the asset metadata.
127        debug_assert!(
128            faucet_suffix & Self::METADATA_BYTE_MASK as u64 == 0,
129            "lower 8 bits of faucet suffix must be zero",
130        );
131        let metadata_byte =
132            self.composition.as_u8() | (self.callback_flag.as_u8() << Self::CALLBACK_FLAG_SHIFT);
133        let faucet_id_suffix_and_metadata = faucet_suffix | metadata_byte as u64;
134        let faucet_id_suffix_and_metadata = Felt::try_from(faucet_id_suffix_and_metadata)
135            .expect("highest bit should still be zero resulting in a valid felt");
136
137        Word::new([
138            self.asset_id.suffix(),
139            self.asset_id.prefix(),
140            faucet_id_suffix_and_metadata,
141            self.faucet_id.prefix().as_felt(),
142        ])
143    }
144
145    /// Returns the [`AssetId`] of the vault key that distinguishes different assets issued by the
146    /// same faucet.
147    pub fn asset_id(&self) -> AssetId {
148        self.asset_id
149    }
150
151    /// Returns the [`AccountId`] of the faucet that issued the asset.
152    pub fn faucet_id(&self) -> AccountId {
153        self.faucet_id
154    }
155
156    /// Returns the [`AssetCallbackFlag`] flag of the vault key.
157    pub fn callback_flag(&self) -> AssetCallbackFlag {
158        self.callback_flag
159    }
160
161    /// Returns the [`AssetComposition`] of the vault key.
162    pub fn composition(&self) -> AssetComposition {
163        self.composition
164    }
165
166    /// Returns the leaf index of a vault key.
167    pub fn to_leaf_index(&self) -> LeafIndex<SMT_DEPTH> {
168        LeafIndex::<SMT_DEPTH>::from(self.to_word())
169    }
170}
171
172// CONVERSIONS
173// ================================================================================================
174
175impl From<AssetVaultKey> for Word {
176    fn from(vault_key: AssetVaultKey) -> Self {
177        vault_key.to_word()
178    }
179}
180
181impl Ord for AssetVaultKey {
182    /// Implements comparison based on the [`Word`] representation.
183    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
184        self.to_word().cmp(&other.to_word())
185    }
186}
187
188impl PartialOrd for AssetVaultKey {
189    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
190        Some(self.cmp(other))
191    }
192}
193
194impl TryFrom<Word> for AssetVaultKey {
195    type Error = AssetError;
196
197    /// Attempts to convert the provided [`Word`] into an [`AssetVaultKey`].
198    ///
199    /// # Errors
200    ///
201    /// Returns an error if:
202    /// - the asset ID limbs are not zero when asset composition is [`AssetComposition::Fungible`].
203    /// - the metadata byte has reserved bits set.
204    /// - the composition encoded in the metadata byte is invalid.
205    fn try_from(key: Word) -> Result<Self, Self::Error> {
206        let asset_id_suffix = key[0];
207        let asset_id_prefix = key[1];
208        let faucet_id_suffix_and_metadata = key[2];
209        let faucet_id_prefix = key[3];
210
211        let raw = faucet_id_suffix_and_metadata.as_canonical_u64();
212        let metadata_byte = (raw & Self::METADATA_BYTE_MASK as u64) as u8;
213
214        // Make sure the reserved bits of the metadata are zero.
215        if metadata_byte & Self::METADATA_RESERVED_MASK != 0 {
216            return Err(AssetError::ReservedAssetMetadata(metadata_byte));
217        }
218
219        let callback_flag = AssetCallbackFlag::try_from(
220            (metadata_byte & Self::CALLBACK_FLAG_MASK) >> Self::CALLBACK_FLAG_SHIFT,
221        )?;
222        let composition = AssetComposition::try_from(metadata_byte & Self::COMPOSITION_MASK)?;
223
224        let faucet_id_suffix = Felt::try_from(raw & !(Self::METADATA_BYTE_MASK as u64))
225            .expect("clearing lower bits should not produce an invalid felt");
226
227        let asset_id = AssetId::new(asset_id_suffix, asset_id_prefix);
228        let faucet_id = AccountId::try_from_elements(faucet_id_suffix, faucet_id_prefix)
229            .map_err(|err| AssetError::InvalidFaucetAccountId(Box::new(err)))?;
230
231        Self::new(asset_id, faucet_id, composition, callback_flag)
232    }
233}
234
235impl fmt::Display for AssetVaultKey {
236    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237        f.write_str(&self.to_word().to_hex())
238    }
239}
240
241impl From<Asset> for AssetVaultKey {
242    fn from(asset: Asset) -> Self {
243        asset.vault_key()
244    }
245}
246
247impl From<FungibleAsset> for AssetVaultKey {
248    fn from(fungible_asset: FungibleAsset) -> Self {
249        fungible_asset.vault_key()
250    }
251}
252
253impl From<NonFungibleAsset> for AssetVaultKey {
254    fn from(non_fungible_asset: NonFungibleAsset) -> Self {
255        non_fungible_asset.vault_key()
256    }
257}
258
259// SERIALIZATION
260// ================================================================================================
261
262impl Serializable for AssetVaultKey {
263    fn write_into<W: ByteWriter>(&self, target: &mut W) {
264        self.to_word().write_into(target);
265    }
266
267    fn get_size_hint(&self) -> usize {
268        Self::SERIALIZED_SIZE
269    }
270}
271
272impl Deserializable for AssetVaultKey {
273    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
274        let word: Word = source.read()?;
275        Self::try_from(word).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
276    }
277}
278
279// TESTS
280// ================================================================================================
281
282#[cfg(test)]
283mod tests {
284    use assert_matches::assert_matches;
285    use rstest::rstest;
286
287    use super::*;
288    use crate::asset::tests::{asset_metadata, set_asset_metadata};
289    use crate::asset::{AssetCallbackFlag, AssetComposition};
290    use crate::testing::account_id::{
291        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
292        ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
293    };
294
295    #[rstest]
296    fn asset_vault_key_word_roundtrip(
297        #[values(AssetCallbackFlag::Disabled, AssetCallbackFlag::Enabled)]
298        callback_flag: AssetCallbackFlag,
299    ) -> anyhow::Result<()> {
300        let fungible_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET)?;
301        let nonfungible_faucet = AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET)?;
302
303        // Fungible: asset_id must be zero.
304        let key = AssetVaultKey::new(
305            AssetId::default(),
306            fungible_faucet,
307            AssetComposition::Fungible,
308            callback_flag,
309        )?;
310        assert_eq!(key.composition(), AssetComposition::Fungible);
311        let roundtripped = AssetVaultKey::try_from(key.to_word())?;
312        assert_eq!(key, roundtripped);
313        assert_eq!(key, AssetVaultKey::read_from_bytes(&key.to_bytes())?);
314
315        // Non-fungible: asset_id can be non-zero.
316        let key = AssetVaultKey::new(
317            AssetId::new(Felt::from(42u32), Felt::from(99u32)),
318            nonfungible_faucet,
319            AssetComposition::None,
320            callback_flag,
321        )?;
322        assert_eq!(key.composition(), AssetComposition::None);
323        let roundtripped = AssetVaultKey::try_from(key.to_word())?;
324        assert_eq!(key, roundtripped);
325        assert_eq!(key, AssetVaultKey::read_from_bytes(&key.to_bytes())?);
326
327        Ok(())
328    }
329
330    #[test]
331    fn decoding_word_with_reserved_bits_set_fails() -> anyhow::Result<()> {
332        let key = FungibleAsset::mock(42).vault_key();
333        let valid_metadata = asset_metadata(key);
334        // Set the reserved bits so the reserved-bits check fires.
335        let word = set_asset_metadata(key, valid_metadata | AssetVaultKey::METADATA_RESERVED_MASK);
336
337        let err = AssetVaultKey::try_from(word).unwrap_err();
338        assert_matches!(err, AssetError::ReservedAssetMetadata(_));
339
340        Ok(())
341    }
342
343    #[test]
344    fn decoding_word_with_invalid_composition_value_fails() -> anyhow::Result<()> {
345        let key = FungibleAsset::mock(42).vault_key();
346        // Set all composition bits — value 3 is the invalid bit pattern within the 2-bit field.
347        let invalid_metadata = AssetVaultKey::COMPOSITION_MASK;
348        let word = set_asset_metadata(key, invalid_metadata);
349
350        let err = AssetVaultKey::try_from(word).unwrap_err();
351        assert_matches!(err, AssetError::UnknownAssetComposition(_));
352
353        Ok(())
354    }
355}