miden_objects/note/
assets.rs

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