miden_objects/note/
assets.rs

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