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,
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 // STATE MUTATORS
111 // --------------------------------------------------------------------------------------------
112
113 /// Adds the provided asset to this list of note assets.
114 ///
115 /// # Errors
116 /// Returns an error if:
117 /// - The same non-fungible asset is already in the list.
118 /// - A fungible asset issued by the same faucet exists in the list and adding both assets
119 /// together results in an invalid asset.
120 /// - Adding the asset to the list will push the list beyond the [Self::MAX_NUM_ASSETS] limit.
121 pub fn add_asset(&mut self, asset: Asset) -> Result<(), NoteError> {
122 // check if the asset issued by the faucet as the provided asset already exists in the
123 // list of assets
124 if let Some(own_asset) = self.assets.iter_mut().find(|a| a.is_same(&asset)) {
125 match own_asset {
126 Asset::Fungible(f_own_asset) => {
127 // if a fungible asset issued by the same faucet is found, try to add the
128 // the provided asset to it
129 let new_asset = f_own_asset
130 .add(asset.unwrap_fungible())
131 .map_err(NoteError::AddFungibleAssetBalanceError)?;
132 *own_asset = Asset::Fungible(new_asset);
133 },
134 Asset::NonFungible(nf_asset) => {
135 return Err(NoteError::DuplicateNonFungibleAsset(*nf_asset));
136 },
137 }
138 } else {
139 // if the asset is not in the list, add it to the list
140 self.assets.push(asset);
141 if self.assets.len() > Self::MAX_NUM_ASSETS {
142 return Err(NoteError::TooManyAssets(self.assets.len()));
143 }
144 }
145
146 self.hash = compute_asset_commitment(&self.assets);
147
148 Ok(())
149 }
150}
151
152impl PartialEq for NoteAssets {
153 fn eq(&self, other: &Self) -> bool {
154 self.assets == other.assets
155 }
156}
157
158impl Eq for NoteAssets {}
159
160// HELPER FUNCTIONS
161// ================================================================================================
162
163/// Returns a commitment to a note's assets.
164///
165/// The commitment is computed as a sequential hash of all assets (each asset represented by 4
166/// field elements), padded to the next multiple of 2. If the asset list is empty, a default digest
167/// is returned.
168fn compute_asset_commitment(assets: &[Asset]) -> Digest {
169 if assets.is_empty() {
170 return Digest::default();
171 }
172
173 // If we have an odd number of assets we pad the vector with 4 zero elements. This is to
174 // ensure the number of elements is a multiple of 8 - the size of the hasher rate.
175 let word_capacity = if assets.len() % 2 == 0 {
176 assets.len()
177 } else {
178 assets.len() + 1
179 };
180 let mut asset_elements = Vec::with_capacity(word_capacity * WORD_SIZE);
181
182 for asset in assets.iter() {
183 // convert the asset into field elements and add them to the list elements
184 let asset_word: Word = (*asset).into();
185 asset_elements.extend_from_slice(&asset_word);
186 }
187
188 // If we have an odd number of assets we pad the vector with 4 zero elements. This is to
189 // ensure the number of elements is a multiple of 8 - the size of the hasher rate. This
190 // simplifies hashing inside of the virtual machine when ingesting assets from a note.
191 if assets.len() % 2 == 1 {
192 asset_elements.extend_from_slice(&Word::default());
193 }
194
195 Hasher::hash_elements(&asset_elements)
196}
197
198// SERIALIZATION
199// ================================================================================================
200
201impl Serializable for NoteAssets {
202 fn write_into<W: ByteWriter>(&self, target: &mut W) {
203 const _: () = assert!(NoteAssets::MAX_NUM_ASSETS <= u8::MAX as usize);
204 debug_assert!(self.assets.len() <= NoteAssets::MAX_NUM_ASSETS);
205 target.write_u8(self.assets.len().try_into().expect("Asset number must fit into `u8`"));
206 target.write_many(&self.assets);
207 }
208}
209
210impl Deserializable for NoteAssets {
211 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
212 let count = source.read_u8()?;
213 let assets = source.read_many::<Asset>(count.into())?;
214 Self::new(assets).map_err(|e| DeserializationError::InvalidValue(format!("{e:?}")))
215 }
216}
217
218// TESTS
219// ================================================================================================
220
221#[cfg(test)]
222mod tests {
223 use super::{NoteAssets, compute_asset_commitment};
224 use crate::{
225 Digest,
226 account::AccountId,
227 asset::{Asset, FungibleAsset},
228 testing::account_id::ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
229 };
230
231 #[test]
232 fn add_asset() {
233 let faucet_id = AccountId::try_from(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET).unwrap();
234
235 let asset1 = Asset::Fungible(FungibleAsset::new(faucet_id, 100).unwrap());
236 let asset2 = Asset::Fungible(FungibleAsset::new(faucet_id, 50).unwrap());
237
238 // create empty assets
239 let mut assets = NoteAssets::default();
240 assert_eq!(assets.hash, Digest::default());
241
242 // add asset1
243 assert!(assets.add_asset(asset1).is_ok());
244 assert_eq!(assets.assets, vec![asset1]);
245 assert_eq!(assets.hash, compute_asset_commitment(&[asset1]));
246
247 // add asset2
248 assert!(assets.add_asset(asset2).is_ok());
249 let expected_asset = Asset::Fungible(FungibleAsset::new(faucet_id, 150).unwrap());
250 assert_eq!(assets.assets, vec![expected_asset]);
251 assert_eq!(assets.hash, compute_asset_commitment(&[expected_asset]));
252 }
253}