miden_protocol/asset/vault/
asset_witness.rs1use alloc::boxed::Box;
2use alloc::collections::BTreeMap;
3use alloc::string::ToString;
4use alloc::vec::Vec;
5
6use miden_crypto::merkle::InnerNodeInfo;
7use miden_crypto::merkle::smt::SmtProof;
8
9use super::asset_id::AssetId;
10use crate::Word;
11use crate::asset::Asset;
12use crate::errors::AssetError;
13use crate::utils::serde::{
14 ByteReader,
15 ByteWriter,
16 Deserializable,
17 DeserializationError,
18 Serializable,
19};
20
21#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct AssetWitness {
35 proof: SmtProof,
36 entries: BTreeMap<AssetId, Word>,
38}
39
40impl AssetWitness {
41 pub fn new(
54 proof: SmtProof,
55 ids: impl IntoIterator<Item = AssetId>,
56 ) -> Result<Self, AssetError> {
57 let mut entries = BTreeMap::new();
58
59 for id in ids {
60 let value = proof
61 .get(&id.hash().as_word())
62 .ok_or(AssetError::AssetWitnessMissingId { id })?;
63
64 if !value.is_empty() {
66 Asset::new(id, value)
67 .map_err(|err| AssetError::AssetWitnessInvalid(Box::new(err)))?;
68 }
69
70 entries.insert(id, value);
71 }
72
73 Ok(Self { proof, entries })
74 }
75
76 pub fn new_unchecked(
82 proof: SmtProof,
83 id_values: impl IntoIterator<Item = (AssetId, Word)>,
84 ) -> Self {
85 let entries: BTreeMap<AssetId, Word> = id_values.into_iter().collect();
86
87 #[cfg(debug_assertions)]
88 for (id, value) in &entries {
89 debug_assert_eq!(
90 proof.get(&id.hash().as_word()),
91 Some(*value),
92 "AssetWitness::new_unchecked: (id, value) pair does not match the proof",
93 );
94 }
95
96 Self { proof, entries }
97 }
98
99 pub fn proof(&self) -> &SmtProof {
104 &self.proof
105 }
106
107 pub fn authenticates_asset_id(&self, asset_id: AssetId) -> bool {
110 self.proof.leaf().index() == asset_id.hash().to_leaf_index()
111 }
112
113 pub fn find(&self, asset_id: AssetId) -> Option<Asset> {
115 let value = self.entries.get(&asset_id).copied()?;
116 if value.is_empty() {
117 None
118 } else {
119 Some(Asset::new(asset_id, value).expect("asset witness should track valid assets"))
120 }
121 }
122
123 pub fn assets(&self) -> impl Iterator<Item = Asset> + '_ {
125 self.entries.iter().filter_map(|(id, value)| {
126 if value.is_empty() {
127 None
128 } else {
129 Some(Asset::new(*id, *value).expect("asset witness should track valid assets"))
130 }
131 })
132 }
133
134 pub(super) fn entries(&self) -> impl Iterator<Item = (&AssetId, &Word)> {
136 self.entries.iter()
137 }
138
139 pub(super) fn into_parts(self) -> (SmtProof, BTreeMap<AssetId, Word>) {
142 (self.proof, self.entries)
143 }
144
145 pub fn authenticated_nodes(&self) -> impl Iterator<Item = InnerNodeInfo> + '_ {
147 self.proof
148 .path()
149 .authenticated_nodes(self.proof.leaf().index().position(), self.proof.leaf().hash())
150 .expect("leaf index is u64 and should be less than 2^SMT_DEPTH")
151 }
152}
153
154impl From<AssetWitness> for SmtProof {
155 fn from(witness: AssetWitness) -> Self {
156 witness.proof
157 }
158}
159
160impl Serializable for AssetWitness {
161 fn write_into<W: ByteWriter>(&self, target: &mut W) {
162 target.write(&self.proof);
163 target.write_usize(self.entries.len());
164 target.write_many(self.entries.keys());
165 }
166}
167
168impl Deserializable for AssetWitness {
169 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
170 let proof: SmtProof = source.read()?;
171 let num_ids: usize = source.read()?;
172 let ids = source.read_many_iter::<AssetId>(num_ids)?.collect::<Result<Vec<_>, _>>()?;
173
174 Self::new(proof, ids).map_err(|err| DeserializationError::InvalidValue(err.to_string()))
175 }
176}
177
178#[cfg(test)]
182mod tests {
183 use assert_matches::assert_matches;
184 use miden_crypto::merkle::smt::Smt;
185
186 use super::*;
187 use crate::asset::{AssetVault, FungibleAsset, NonFungibleAsset};
188 use crate::testing::account_id::{
189 ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET,
190 ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3,
191 };
192
193 #[test]
196 fn create_asset_witness_fails_on_asset_id_mismatch() -> anyhow::Result<()> {
197 let fungible_asset = FungibleAsset::mock(500);
198 let non_fungible_asset = NonFungibleAsset::mock(&[1]);
199
200 let fungible_id = fungible_asset.id();
202 let inconsistent_smt = Smt::with_entries([(
203 fungible_id.hash().as_word(),
204 non_fungible_asset.to_value_word(),
205 )])?;
206 let proof = inconsistent_smt.open(&fungible_id.hash().as_word());
207
208 let err = AssetWitness::new(proof, [fungible_id]).unwrap_err();
209
210 assert_matches!(err, AssetError::AssetWitnessInvalid(source) => {
211 assert_matches!(*source, AssetError::FungibleAssetValueMostSignificantElementsMustBeZero(_));
212 });
213
214 Ok(())
215 }
216
217 #[test]
220 fn create_asset_witness_fails_on_missing_id() -> anyhow::Result<()> {
221 let asset_in_vault =
222 FungibleAsset::new(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3.try_into()?, 200)?;
223 let other_id =
224 FungibleAsset::new(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into()?, 100)?.id();
225
226 let vault = AssetVault::new(&[asset_in_vault.into()])?;
227 let proof = vault.open(asset_in_vault.id()).proof().clone();
228
229 let err = AssetWitness::new(proof, [other_id]).unwrap_err();
232 assert_matches!(err, AssetError::AssetWitnessMissingId { id } => {
233 assert_eq!(id, other_id);
234 });
235
236 Ok(())
237 }
238
239 #[test]
240 fn asset_witness_authenticates_asset_id() -> anyhow::Result<()> {
241 let fungible_asset0 =
242 FungibleAsset::new(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET_3.try_into()?, 200)?;
243 let fungible_asset1 =
244 FungibleAsset::new(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into()?, 100)?;
245
246 let vault = AssetVault::new(&[fungible_asset0.into()])?;
247 let witness0 = vault.open(fungible_asset0.id());
248
249 assert!(witness0.authenticates_asset_id(fungible_asset0.id()));
250 assert!(!witness0.authenticates_asset_id(fungible_asset1.id()));
251
252 Ok(())
253 }
254}