miden_objects/asset/
nonfungible.rs

1use alloc::{boxed::Box, string::ToString, vec::Vec};
2use core::fmt;
3
4use vm_core::{FieldElement, WORD_SIZE};
5
6use super::{AccountIdPrefix, AccountType, Asset, AssetError, Felt, Hasher, Word};
7use crate::{
8    utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
9    Digest,
10};
11
12/// Position of the faucet_id inside the [`NonFungibleAsset`] word.
13const FAUCET_ID_POS: usize = 3;
14
15// NON-FUNGIBLE ASSET
16// ================================================================================================
17
18/// A commitment to a non-fungible asset.
19///
20/// The commitment is constructed as follows:
21///
22/// - Hash the asset data producing `[hash0, hash1, hash2, hash3]`.
23/// - Replace the value of `hash3` with the prefix of the faucet id (`faucet_id_prefix`) producing
24///   `[hash0, hash1, hash2, faucet_id_prefix]`.
25/// - This layout ensures that fungible and non-fungible assets are distinguishable by interpreting
26///   the 3rd element of an asset as an [`AccountIdPrefix`] and checking its type.
27///
28/// [`NonFungibleAsset`] itself does not contain the actual asset data. The container for this data
29/// is [`NonFungibleAssetDetails`].
30#[derive(Debug, Copy, Clone, PartialEq, Eq)]
31pub struct NonFungibleAsset(Word);
32
33impl PartialOrd for NonFungibleAsset {
34    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
35        Some(self.cmp(other))
36    }
37}
38
39impl Ord for NonFungibleAsset {
40    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
41        Digest::from(self.0).cmp(&Digest::from(other.0))
42    }
43}
44
45impl NonFungibleAsset {
46    // CONSTANTS
47    // --------------------------------------------------------------------------------------------
48
49    /// The serialized size of a [`NonFungibleAsset`] in bytes.
50    ///
51    /// Currently represented as a word.
52    pub const SERIALIZED_SIZE: usize = Felt::ELEMENT_BYTES * WORD_SIZE;
53
54    // CONSTRUCTORS
55    // --------------------------------------------------------------------------------------------
56
57    /// Returns a non-fungible asset created from the specified asset details.
58    ///
59    /// # Errors
60    /// Returns an error if the provided faucet ID is not for a non-fungible asset faucet.
61    pub fn new(details: &NonFungibleAssetDetails) -> Result<Self, AssetError> {
62        let data_hash = Hasher::hash(details.asset_data());
63        Self::from_parts(details.faucet_id(), data_hash.into())
64    }
65
66    /// Return a non-fungible asset created from the specified faucet and using the provided
67    /// hash of the asset's data.
68    ///
69    /// Hash of the asset's data is expected to be computed from the binary representation of the
70    /// asset's data.
71    ///
72    /// # Errors
73    /// Returns an error if the provided faucet ID is not for a non-fungible asset faucet.
74    pub fn from_parts(faucet_id: AccountIdPrefix, mut data_hash: Word) -> Result<Self, AssetError> {
75        if !matches!(faucet_id.account_type(), AccountType::NonFungibleFaucet) {
76            return Err(AssetError::NonFungibleFaucetIdTypeMismatch(faucet_id));
77        }
78
79        data_hash[FAUCET_ID_POS] = Felt::from(faucet_id);
80
81        Ok(Self(data_hash))
82    }
83
84    /// Creates a new [NonFungibleAsset] without checking its validity.
85    ///
86    /// # Safety
87    /// This function required that the provided value is a valid word representation of a
88    /// [NonFungibleAsset].
89    pub unsafe fn new_unchecked(value: Word) -> NonFungibleAsset {
90        NonFungibleAsset(value)
91    }
92
93    // ACCESSORS
94    // --------------------------------------------------------------------------------------------
95
96    /// Returns the vault key of the [`NonFungibleAsset`].
97    ///
98    /// This is the same as the asset with the following modifications, in this order:
99    /// - Swaps the faucet ID at index 0 and `hash0` at index 3.
100    /// - Sets the fungible bit for `hash0` to `0`.
101    ///
102    /// # Rationale
103    ///
104    /// This means `hash0` will be used as the leaf index in the asset SMT which ensures that a
105    /// non-fungible faucet's assets generally end up in different leaves as the key is not based on
106    /// the faucet ID.
107    ///
108    /// It also ensures that there is never any collision in the leaf index between a non-fungible
109    /// asset and a fungible asset, as the former's vault key always has the fungible bit set to `0`
110    /// and the latter's vault key always has the bit set to `1`.
111    pub fn vault_key(&self) -> Word {
112        let mut vault_key = self.0;
113
114        // Swap prefix of faucet ID with hash0.
115        vault_key.swap(0, FAUCET_ID_POS);
116
117        // Set the fungible bit to zero.
118        vault_key[3] =
119            AccountIdPrefix::clear_fungible_bit(self.faucet_id_prefix().version(), vault_key[3]);
120
121        vault_key
122    }
123
124    /// Return ID prefix of the faucet which issued this asset.
125    pub fn faucet_id_prefix(&self) -> AccountIdPrefix {
126        AccountIdPrefix::new_unchecked(self.0[FAUCET_ID_POS])
127    }
128
129    // HELPER FUNCTIONS
130    // --------------------------------------------------------------------------------------------
131
132    /// Validates this non-fungible asset.
133    /// # Errors
134    /// Returns an error if:
135    /// - The faucet_id is not a valid non-fungible faucet ID.
136    /// - The most significant bit of the asset is not ZERO.
137    fn validate(&self) -> Result<(), AssetError> {
138        let faucet_id = AccountIdPrefix::try_from(self.0[FAUCET_ID_POS])
139            .map_err(|err| AssetError::InvalidFaucetAccountId(Box::new(err)))?;
140
141        let account_type = faucet_id.account_type();
142        if !matches!(account_type, AccountType::NonFungibleFaucet) {
143            return Err(AssetError::NonFungibleFaucetIdTypeMismatch(faucet_id));
144        }
145
146        Ok(())
147    }
148}
149
150impl From<NonFungibleAsset> for Word {
151    fn from(asset: NonFungibleAsset) -> Self {
152        asset.0
153    }
154}
155
156impl From<NonFungibleAsset> for Asset {
157    fn from(asset: NonFungibleAsset) -> Self {
158        Asset::NonFungible(asset)
159    }
160}
161
162impl TryFrom<Word> for NonFungibleAsset {
163    type Error = AssetError;
164
165    fn try_from(value: Word) -> Result<Self, Self::Error> {
166        let asset = Self(value);
167        asset.validate()?;
168        Ok(asset)
169    }
170}
171
172impl fmt::Display for NonFungibleAsset {
173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174        write!(f, "{:?}", self)
175    }
176}
177
178// SERIALIZATION
179// ================================================================================================
180
181impl Serializable for NonFungibleAsset {
182    fn write_into<W: ByteWriter>(&self, target: &mut W) {
183        // All assets should serialize their faucet ID at the first position to allow them to be
184        // easily distinguishable during deserialization.
185        target.write(self.faucet_id_prefix());
186        target.write(self.0[2]);
187        target.write(self.0[1]);
188        target.write(self.0[0]);
189    }
190
191    fn get_size_hint(&self) -> usize {
192        Self::SERIALIZED_SIZE
193    }
194}
195
196impl Deserializable for NonFungibleAsset {
197    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
198        let faucet_id_prefix: AccountIdPrefix = source.read()?;
199
200        Self::deserialize_with_faucet_id_prefix(faucet_id_prefix, source)
201            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
202    }
203}
204
205impl NonFungibleAsset {
206    /// Deserializes a [`NonFungibleAsset`] from an [`AccountIdPrefix`] and the remaining data from
207    /// the given `source`.
208    pub(super) fn deserialize_with_faucet_id_prefix<R: ByteReader>(
209        faucet_id_prefix: AccountIdPrefix,
210        source: &mut R,
211    ) -> Result<Self, DeserializationError> {
212        let hash_2: Felt = source.read()?;
213        let hash_1: Felt = source.read()?;
214        let hash_0: Felt = source.read()?;
215
216        // The last felt in the data_hash will be replaced by the faucet id, so we can set it to
217        // zero here.
218        NonFungibleAsset::from_parts(faucet_id_prefix, [hash_0, hash_1, hash_2, Felt::ZERO])
219            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
220    }
221}
222
223// NON-FUNGIBLE ASSET DETAILS
224// ================================================================================================
225
226/// Details about a non-fungible asset.
227///
228/// Unlike [NonFungibleAsset] struct, this struct contains full details of a non-fungible asset.
229#[derive(Debug, Clone, PartialEq, Eq)]
230pub struct NonFungibleAssetDetails {
231    faucet_id: AccountIdPrefix,
232    asset_data: Vec<u8>,
233}
234
235impl NonFungibleAssetDetails {
236    /// Returns asset details instantiated from the specified faucet ID and asset data.
237    ///
238    /// # Errors
239    /// Returns an error if the provided faucet ID is not for a non-fungible asset faucet.
240    pub fn new(faucet_id: AccountIdPrefix, asset_data: Vec<u8>) -> Result<Self, AssetError> {
241        if !matches!(faucet_id.account_type(), AccountType::NonFungibleFaucet) {
242            return Err(AssetError::NonFungibleFaucetIdTypeMismatch(faucet_id));
243        }
244
245        Ok(Self { faucet_id, asset_data })
246    }
247
248    /// Returns ID of the faucet which issued this asset.
249    pub fn faucet_id(&self) -> AccountIdPrefix {
250        self.faucet_id
251    }
252
253    /// Returns asset data in binary format.
254    pub fn asset_data(&self) -> &[u8] {
255        &self.asset_data
256    }
257}
258
259// TESTS
260// ================================================================================================
261
262#[cfg(test)]
263mod tests {
264    use assert_matches::assert_matches;
265
266    use super::*;
267    use crate::{
268        account::AccountId,
269        testing::account_id::{
270            ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN,
271            ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN, ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN_1,
272        },
273    };
274
275    #[test]
276    fn test_non_fungible_asset_serde() {
277        for non_fungible_account_id in [
278            ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN,
279            ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN,
280            ACCOUNT_ID_NON_FUNGIBLE_FAUCET_ON_CHAIN_1,
281        ] {
282            let account_id = AccountId::try_from(non_fungible_account_id).unwrap();
283            let details = NonFungibleAssetDetails::new(account_id.prefix(), vec![1, 2, 3]).unwrap();
284            let non_fungible_asset = NonFungibleAsset::new(&details).unwrap();
285            assert_eq!(
286                non_fungible_asset,
287                NonFungibleAsset::read_from_bytes(&non_fungible_asset.to_bytes()).unwrap()
288            );
289        }
290
291        let account = AccountId::try_from(ACCOUNT_ID_NON_FUNGIBLE_FAUCET_OFF_CHAIN).unwrap();
292        let details = NonFungibleAssetDetails::new(account.prefix(), vec![4, 5, 6, 7]).unwrap();
293        let asset = NonFungibleAsset::new(&details).unwrap();
294        let mut asset_bytes = asset.to_bytes();
295
296        let fungible_faucet_id = AccountId::try_from(ACCOUNT_ID_FUNGIBLE_FAUCET_OFF_CHAIN).unwrap();
297
298        // Set invalid Faucet ID Prefix.
299        asset_bytes[0..8].copy_from_slice(&fungible_faucet_id.prefix().to_bytes());
300
301        let err = NonFungibleAsset::read_from_bytes(&asset_bytes).unwrap_err();
302        assert_matches!(err, DeserializationError::InvalidValue(msg) if msg.contains("must be of type NonFungibleFaucet"));
303    }
304}