miden_objects/note/
assets.rs

1use alloc::vec::Vec;
2
3use crate::asset::{Asset, FungibleAsset, NonFungibleAsset};
4use crate::errors::NoteError;
5use crate::utils::serde::{
6    ByteReader,
7    ByteWriter,
8    Deserializable,
9    DeserializationError,
10    Serializable,
11};
12use crate::{Felt, Hasher, MAX_ASSETS_PER_NOTE, WORD_SIZE, Word, ZERO};
13
14// NOTE ASSETS
15// ================================================================================================
16/// An asset container for a note.
17///
18/// A note can contain between 0 and 256 assets. No duplicates are allowed, but the order of assets
19/// is unspecified.
20///
21/// All the assets in a note can be reduced to a single commitment which is computed by
22/// sequentially hashing the assets. Note that the same list of assets can result in two different
23/// commitments if the asset ordering is different.
24#[derive(Debug, Default, Clone)]
25pub struct NoteAssets {
26    assets: Vec<Asset>,
27    hash: Word,
28}
29
30impl NoteAssets {
31    // CONSTANTS
32    // --------------------------------------------------------------------------------------------
33
34    /// The maximum number of assets which can be carried by a single note.
35    pub const MAX_NUM_ASSETS: usize = MAX_ASSETS_PER_NOTE;
36
37    // CONSTRUCTOR
38    // --------------------------------------------------------------------------------------------
39
40    /// Returns new [NoteAssets] constructed from the provided list of assets.
41    ///
42    /// # Errors
43    /// Returns an error if:
44    /// - The list contains more than 256 assets.
45    /// - There are duplicate assets in the list.
46    pub fn new(assets: Vec<Asset>) -> Result<Self, NoteError> {
47        if assets.len() > Self::MAX_NUM_ASSETS {
48            return Err(NoteError::TooManyAssets(assets.len()));
49        }
50
51        // make sure all provided assets are unique
52        for (i, asset) in assets.iter().enumerate().skip(1) {
53            // for all assets except the first one, check if the asset is the same as any other
54            // asset in the list, and if so return an error
55            if assets[..i].iter().any(|a| a.is_same(asset)) {
56                return Err(match asset {
57                    Asset::Fungible(asset) => NoteError::DuplicateFungibleAsset(asset.faucet_id()),
58                    Asset::NonFungible(asset) => NoteError::DuplicateNonFungibleAsset(*asset),
59                });
60            }
61        }
62
63        let hash = compute_asset_commitment(&assets);
64        Ok(Self { assets, hash })
65    }
66
67    // PUBLIC ACCESSORS
68    // --------------------------------------------------------------------------------------------
69
70    /// Returns a commitment to the note's assets.
71    pub fn commitment(&self) -> Word {
72        self.hash
73    }
74
75    /// Returns the number of assets.
76    pub fn num_assets(&self) -> usize {
77        self.assets.len()
78    }
79
80    /// Returns true if the number of assets is 0.
81    pub fn is_empty(&self) -> bool {
82        self.assets.is_empty()
83    }
84
85    /// Returns an iterator over all assets.
86    pub fn iter(&self) -> core::slice::Iter<'_, Asset> {
87        self.assets.iter()
88    }
89
90    /// Returns all assets represented as a vector of field elements.
91    ///
92    /// The vector is padded with ZEROs so that its length is a multiple of 8. This is useful
93    /// because hashing the returned elements results in the note asset commitment.
94    pub fn to_padded_assets(&self) -> Vec<Felt> {
95        // if we have an odd number of assets with pad with a single word.
96        let padded_len = if self.assets.len() % 2 == 0 {
97            self.assets.len() * WORD_SIZE
98        } else {
99            (self.assets.len() + 1) * WORD_SIZE
100        };
101
102        // allocate a vector to hold the padded assets
103        let mut padded_assets = Vec::with_capacity(padded_len * WORD_SIZE);
104
105        // populate the vector with the assets
106        padded_assets.extend(self.assets.iter().flat_map(|asset| Word::from(*asset)));
107
108        // pad with an empty word if we have an odd number of assets
109        padded_assets.resize(padded_len, ZERO);
110
111        padded_assets
112    }
113
114    /// Returns an iterator over all [`FungibleAsset`].
115    pub fn iter_fungible(&self) -> impl Iterator<Item = FungibleAsset> {
116        self.assets.iter().filter_map(|asset| match asset {
117            Asset::Fungible(fungible_asset) => Some(*fungible_asset),
118            Asset::NonFungible(_) => None,
119        })
120    }
121
122    /// Returns iterator over all [`NonFungibleAsset`].
123    pub fn iter_non_fungible(&self) -> impl Iterator<Item = NonFungibleAsset> {
124        self.assets.iter().filter_map(|asset| match asset {
125            Asset::Fungible(_) => None,
126            Asset::NonFungible(non_fungible_asset) => Some(*non_fungible_asset),
127        })
128    }
129
130    // STATE MUTATORS
131    // --------------------------------------------------------------------------------------------
132
133    /// Adds the provided asset to this list of note assets.
134    ///
135    /// # Errors
136    /// Returns an error if:
137    /// - The same non-fungible asset is already in the list.
138    /// - A fungible asset issued by the same faucet exists in the list and adding both assets
139    ///   together results in an invalid asset.
140    /// - Adding the asset to the list will push the list beyond the [Self::MAX_NUM_ASSETS] limit.
141    pub fn add_asset(&mut self, asset: Asset) -> Result<(), NoteError> {
142        // check if the asset issued by the faucet as the provided asset already exists in the
143        // list of assets
144        if let Some(own_asset) = self.assets.iter_mut().find(|a| a.is_same(&asset)) {
145            match own_asset {
146                Asset::Fungible(f_own_asset) => {
147                    // if a fungible asset issued by the same faucet is found, try to add the
148                    // the provided asset to it
149                    let new_asset = f_own_asset
150                        .add(asset.unwrap_fungible())
151                        .map_err(NoteError::AddFungibleAssetBalanceError)?;
152                    *own_asset = Asset::Fungible(new_asset);
153                },
154                Asset::NonFungible(nf_asset) => {
155                    return Err(NoteError::DuplicateNonFungibleAsset(*nf_asset));
156                },
157            }
158        } else {
159            // if the asset is not in the list, add it to the list
160            self.assets.push(asset);
161            if self.assets.len() > Self::MAX_NUM_ASSETS {
162                return Err(NoteError::TooManyAssets(self.assets.len()));
163            }
164        }
165
166        self.hash = compute_asset_commitment(&self.assets);
167
168        Ok(())
169    }
170}
171
172impl PartialEq for NoteAssets {
173    fn eq(&self, other: &Self) -> bool {
174        self.assets == other.assets
175    }
176}
177
178impl Eq for NoteAssets {}
179
180// HELPER FUNCTIONS
181// ================================================================================================
182
183/// Returns a commitment to a note's assets.
184///
185/// The commitment is computed as a sequential hash of all assets (each asset represented by 4
186/// field elements), padded to the next multiple of 2. If the asset list is empty, a default digest
187/// is returned.
188fn compute_asset_commitment(assets: &[Asset]) -> Word {
189    if assets.is_empty() {
190        return Word::empty();
191    }
192
193    // If we have an odd number of assets we pad the vector with 4 zero elements. This is to
194    // ensure the number of elements is a multiple of 8 - the size of the hasher rate.
195    let word_capacity = if assets.len() % 2 == 0 {
196        assets.len()
197    } else {
198        assets.len() + 1
199    };
200    let mut asset_elements = Vec::with_capacity(word_capacity * WORD_SIZE);
201
202    for asset in assets.iter() {
203        // convert the asset into field elements and add them to the list elements
204        let asset_word: Word = (*asset).into();
205        asset_elements.extend_from_slice(asset_word.as_elements());
206    }
207
208    // If we have an odd number of assets we pad the vector with 4 zero elements. This is to
209    // ensure the number of elements is a multiple of 8 - the size of the hasher rate. This
210    // simplifies hashing inside of the virtual machine when ingesting assets from a note.
211    if assets.len() % 2 == 1 {
212        asset_elements.extend_from_slice(Word::empty().as_elements());
213    }
214
215    Hasher::hash_elements(&asset_elements)
216}
217
218// SERIALIZATION
219// ================================================================================================
220
221impl Serializable for NoteAssets {
222    fn write_into<W: ByteWriter>(&self, target: &mut W) {
223        const _: () = assert!(NoteAssets::MAX_NUM_ASSETS <= u8::MAX as usize);
224        debug_assert!(self.assets.len() <= NoteAssets::MAX_NUM_ASSETS);
225        target.write_u8(self.assets.len().try_into().expect("Asset number must fit into `u8`"));
226        target.write_many(&self.assets);
227    }
228}
229
230impl Deserializable for NoteAssets {
231    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
232        let count = source.read_u8()?;
233        let assets = source.read_many::<Asset>(count.into())?;
234        Self::new(assets).map_err(|e| DeserializationError::InvalidValue(format!("{e:?}")))
235    }
236}
237
238// TESTS
239// ================================================================================================
240
241#[cfg(test)]
242mod tests {
243    use super::{NoteAssets, compute_asset_commitment};
244    use crate::Word;
245    use crate::account::AccountId;
246    use crate::asset::{Asset, FungibleAsset, NonFungibleAsset, NonFungibleAssetDetails};
247    use crate::testing::account_id::{
248        ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
249        ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET,
250        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
251    };
252
253    #[test]
254    fn add_asset() {
255        let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
256
257        let asset1 = Asset::Fungible(FungibleAsset::new(faucet_id, 100).unwrap());
258        let asset2 = Asset::Fungible(FungibleAsset::new(faucet_id, 50).unwrap());
259
260        // create empty assets
261        let mut assets = NoteAssets::default();
262
263        assert_eq!(assets.hash, Word::empty());
264
265        // add asset1
266        assert!(assets.add_asset(asset1).is_ok());
267        assert_eq!(assets.assets, vec![asset1]);
268        assert_eq!(assets.hash, compute_asset_commitment(&[asset1]));
269
270        // add asset2
271        assert!(assets.add_asset(asset2).is_ok());
272        let expected_asset = Asset::Fungible(FungibleAsset::new(faucet_id, 150).unwrap());
273        assert_eq!(assets.assets, vec![expected_asset]);
274        assert_eq!(assets.hash, compute_asset_commitment(&[expected_asset]));
275    }
276    #[test]
277    fn iter_fungible_asset() {
278        let faucet_id_1 = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
279        let faucet_id_2 = AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap();
280        let account_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_NON_FUNGIBLE_FAUCET).unwrap();
281        let details = NonFungibleAssetDetails::new(account_id.prefix(), vec![1, 2, 3]).unwrap();
282
283        let asset1 = Asset::Fungible(FungibleAsset::new(faucet_id_1, 100).unwrap());
284        let asset2 = Asset::Fungible(FungibleAsset::new(faucet_id_2, 50).unwrap());
285        let non_fungible_asset = Asset::NonFungible(NonFungibleAsset::new(&details).unwrap());
286
287        // Create NoteAsset from assets
288        let assets = NoteAssets::new([asset1, asset2, non_fungible_asset].to_vec()).unwrap();
289
290        let mut fungible_assets = assets.iter_fungible();
291        assert_eq!(fungible_assets.next().unwrap(), asset1.unwrap_fungible());
292        assert_eq!(fungible_assets.next().unwrap(), asset2.unwrap_fungible());
293        assert_eq!(fungible_assets.next(), None);
294    }
295}