Skip to main content

miden_protocol/asset/vault/
asset_witness.rs

1use 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/// A witness of an asset in an [`AssetVault`](super::AssetVault).
22///
23/// It proves inclusion of a certain asset in the vault.
24///
25/// ## Guarantees
26///
27/// This type guarantees that the raw ID-value pairs it contains are all present in the
28/// contained SMT proof (under their hashed form). Note that the inverse is not necessarily true:
29/// the proof may contain more entries than the witness because to prove inclusion of a given raw
30/// ID A an [`SmtLeaf::Multiple`](miden_crypto::merkle::smt::SmtLeaf::Multiple) may be present
31/// that contains both SMT keys hash(A) and hash(B). However, B may not be present in the ID-value
32/// pairs and this is a valid state.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct AssetWitness {
35    proof: SmtProof,
36    /// Raw [`AssetId`]s -> asset value words, kept consistent with the proof's leaf entries.
37    entries: BTreeMap<AssetId, Word>,
38}
39
40impl AssetWitness {
41    // CONSTRUCTORS
42    // --------------------------------------------------------------------------------------------
43
44    /// Creates a new [`AssetWitness`] from an SMT proof and a set of raw asset IDs.
45    ///
46    /// For each ID, looks up its hashed form in the proof and records the resulting value.
47    ///
48    /// # Errors
49    ///
50    /// Returns an error if:
51    /// - any ID's hashed form is not present in the proof.
52    /// - any of the resulting `(asset_id, value)` pairs do not form a valid asset.
53    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            // Validate that the (id, value) pair forms a valid asset (and skip empty entries).
65            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    /// Creates a new [`AssetWitness`] from an SMT proof and a set of ID-value pairs without
77    /// validating that the pairs form valid assets.
78    ///
79    /// Prefer [`AssetWitness::new`] whenever possible. See the type-level docs for the invariants
80    /// callers must uphold.
81    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    // PUBLIC ACCESSORS
100    // --------------------------------------------------------------------------------------------
101
102    /// Returns a reference to the underlying [`SmtProof`].
103    pub fn proof(&self) -> &SmtProof {
104        &self.proof
105    }
106
107    /// Returns `true` if this [`AssetWitness`] authenticates the provided [`AssetId`], i.e.
108    /// if its leaf index matches, `false` otherwise.
109    pub fn authenticates_asset_id(&self, asset_id: AssetId) -> bool {
110        self.proof.leaf().index() == asset_id.hash().to_leaf_index()
111    }
112
113    /// Searches for an [`Asset`] in the witness with the given `asset_id`.
114    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    /// Returns an iterator over the [`Asset`]s in this witness.
124    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    /// Returns an iterator over the raw `(asset_id, value)` pairs tracked by this witness.
135    pub(super) fn entries(&self) -> impl Iterator<Item = (&AssetId, &Word)> {
136        self.entries.iter()
137    }
138
139    /// Decomposes the witness into its underlying [`SmtProof`] and the raw `(asset_id, value)`
140    /// entries it tracks.
141    pub(super) fn into_parts(self) -> (SmtProof, BTreeMap<AssetId, Word>) {
142        (self.proof, self.entries)
143    }
144
145    /// Returns an iterator over every inner node of this witness' merkle path.
146    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// TESTS
179// ================================================================================================
180
181#[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    /// Tests that constructing an asset witness fails if the (asset_id, value) pair stored in the
194    /// proof is inconsistent (here: a non-fungible value under a fungible asset ID).
195    #[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        // Manually build a proof at the fungible asset's hashed ID but with a non-fungible value.
201        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    /// Tests that constructing an asset witness fails if the provided raw asset ID is not actually
218    /// present (in hashed form) in the SMT proof.
219    #[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        // The proof was opened at `asset_in_vault`'s hashed ID, so a separate `other_id` won't
230        // be found in it.
231        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}