Skip to main content

miden_protocol/asset/vault/
partial.rs

1use alloc::collections::BTreeMap;
2use alloc::string::ToString;
3use alloc::vec::Vec;
4
5use miden_crypto::merkle::smt::{PartialSmt, SmtLeaf, SmtProof};
6use miden_crypto::merkle::{InnerNodeInfo, MerkleError};
7
8use super::{AssetId, AssetVault};
9use crate::Word;
10use crate::asset::{Asset, AssetWitness};
11use crate::errors::PartialAssetVaultError;
12use crate::utils::serde::{
13    ByteReader,
14    ByteWriter,
15    Deserializable,
16    DeserializationError,
17    Serializable,
18};
19
20/// A partial representation of an [`AssetVault`], containing only proofs for a subset of assets.
21///
22/// Partial vault is used to provide verifiable access to specific assets in a vault
23/// without the need to provide the full vault data. It contains all required data for loading
24/// vault data into the transaction kernel for transaction execution.
25///
26/// ## Guarantees
27///
28/// This type guarantees that the raw ID-value pairs it contains are all present in the contained
29/// partial SMT (under their hashed form). Note that the inverse is not necessarily true: the SMT
30/// may contain more entries than the map because to prove inclusion of a given raw ID A an
31/// [`SmtLeaf::Multiple`] may be present that contains both SMT keys hash(A) and hash(B). However, B
32/// may not be present in the ID-value pairs and this is a valid state.
33#[derive(Clone, Debug, PartialEq, Eq, Default)]
34pub struct PartialVault {
35    /// An SMT with a partial view into an account's full [`AssetVault`], keyed by hashed
36    /// [`AssetId`]s.
37    partial_smt: PartialSmt,
38    /// Raw [`AssetId`]s -> asset value words, kept consistent with `partial_smt`.
39    entries: BTreeMap<AssetId, Word>,
40}
41
42impl PartialVault {
43    // CONSTRUCTORS
44    // --------------------------------------------------------------------------------------------
45
46    /// Constructs a [`PartialVault`] from an [`AssetVault`] root.
47    ///
48    /// For conversion from an [`AssetVault`], prefer [`Self::new_minimal`] to be more explicit.
49    pub fn new(root: Word) -> Self {
50        PartialVault {
51            partial_smt: PartialSmt::new(root),
52            entries: BTreeMap::new(),
53        }
54    }
55
56    /// Returns a new [`PartialVault`] with all provided witnesses added to it.
57    pub fn with_witnesses(
58        witnesses: impl IntoIterator<Item = AssetWitness>,
59    ) -> Result<Self, PartialAssetVaultError> {
60        let mut entries = BTreeMap::new();
61
62        let partial_smt = PartialSmt::from_proofs(witnesses.into_iter().map(|witness| {
63            // Skip empty values so `entries` only ever tracks valid assets (mirrors
64            // `AssetVault::new`).
65            entries.extend(
66                witness
67                    .entries()
68                    .filter(|(_, value)| !value.is_empty())
69                    .map(|(id, value)| (*id, *value)),
70            );
71            SmtProof::from(witness)
72        }))
73        .map_err(PartialAssetVaultError::FailedToAddProof)?;
74
75        Ok(PartialVault { partial_smt, entries })
76    }
77
78    /// Converts an [`AssetVault`] into a partial vault representation.
79    ///
80    /// The resulting [`PartialVault`] will contain the _full_ merkle paths and entries of the
81    /// original asset vault.
82    pub fn new_full(vault: AssetVault) -> Self {
83        let partial_smt = PartialSmt::from(vault.asset_tree);
84        let entries = vault.entries;
85
86        PartialVault { partial_smt, entries }
87    }
88
89    /// Converts an [`AssetVault`] into a partial vault representation.
90    ///
91    /// The resulting [`PartialVault`] will represent the root of the asset vault, but not track any
92    /// ID-value pairs, which means it is the most _minimal_ representation of the asset vault.
93    pub fn new_minimal(vault: &AssetVault) -> Self {
94        PartialVault::new(vault.root())
95    }
96
97    /// Constructs a [`PartialVault`] from a [`PartialSmt`] and the raw [`AssetId`]s whose
98    /// values are looked up from the SMT.
99    ///
100    /// # Errors
101    ///
102    /// Returns an error if:
103    /// - any ID's hashed form is not present in the partial SMT.
104    /// - any of the resulting `(asset_id, value)` pairs does not form a valid asset.
105    fn from_partial_smt_and_ids(
106        partial_smt: PartialSmt,
107        ids: impl IntoIterator<Item = AssetId>,
108    ) -> Result<Self, PartialAssetVaultError> {
109        let mut entries = BTreeMap::new();
110
111        for id in ids {
112            let value = partial_smt
113                .get_value(&id.hash().as_word())
114                .map_err(PartialAssetVaultError::UntrackedAsset)?;
115
116            // Validate that the (id, value) pair forms a valid asset, even when the value is
117            // empty: an empty value paired with e.g. a non-fungible ID carrying a non-zero asset
118            // class is malformed and must be rejected rather than silently tracked.
119            Asset::from_id_and_value(id, value).map_err(|source| {
120                PartialAssetVaultError::InvalidAssetForId { id, value, source }
121            })?;
122
123            // Skip empty values so `entries` stays in sync with the SMT, which treats empty values
124            // as no-ops (mirrors `AssetVault::new`).
125            if !value.is_empty() {
126                entries.insert(id, value);
127            }
128        }
129
130        Ok(Self { partial_smt, entries })
131    }
132
133    // ACCESSORS
134    // --------------------------------------------------------------------------------------------
135
136    /// Returns the root of the partial vault.
137    pub fn root(&self) -> Word {
138        self.partial_smt.root()
139    }
140
141    /// Returns an iterator over all inner nodes in the Sparse Merkle Tree proofs.
142    ///
143    /// This is useful for reconstructing parts of the Sparse Merkle Tree or for
144    /// verification purposes.
145    pub fn inner_nodes(&self) -> impl Iterator<Item = InnerNodeInfo> + '_ {
146        self.partial_smt.inner_nodes()
147    }
148
149    /// Returns an iterator over all leaves of the underlying [`PartialSmt`].
150    pub fn leaves(&self) -> impl Iterator<Item = &SmtLeaf> {
151        self.partial_smt.leaves().map(|(_, leaf)| leaf)
152    }
153
154    /// Returns an iterator over the [`Asset`]s tracked by this partial vault.
155    pub fn assets(&self) -> impl Iterator<Item = Asset> + '_ {
156        self.entries.iter().map(|(id, value)| {
157            Asset::from_id_and_value(*id, *value)
158                .expect("partial vault should only track valid assets")
159        })
160    }
161
162    /// Returns an iterator over the raw `(asset_id, value)` pairs tracked by this partial vault.
163    #[cfg(test)]
164    pub(super) fn entries(&self) -> impl Iterator<Item = (&AssetId, &Word)> {
165        self.entries.iter()
166    }
167
168    /// Returns an opening of the leaf associated with `asset_id`.
169    ///
170    /// The `asset_id` can be obtained with [`Asset::id`].
171    ///
172    /// # Errors
173    ///
174    /// Returns an error if:
175    /// - the asset ID is not tracked by this partial vault.
176    pub fn open(&self, asset_id: AssetId) -> Result<AssetWitness, PartialAssetVaultError> {
177        let smt_proof = self
178            .partial_smt
179            .open(&asset_id.hash().as_word())
180            .map_err(PartialAssetVaultError::UntrackedAsset)?;
181        let value = self.entries.get(&asset_id).copied().unwrap_or_default();
182
183        // SAFETY: The ID-value pair is guaranteed to be present in the proof since we open its
184        // hashed form, and the partial vault only tracks valid assets.
185        Ok(AssetWitness::new_unchecked(smt_proof, [(asset_id, value)]))
186    }
187
188    /// Returns the [`Asset`] associated with the given `asset_id`.
189    ///
190    /// The return value is `None` if the asset does not exist in the vault.
191    ///
192    /// # Errors
193    ///
194    /// Returns an error if:
195    /// - the asset ID is not tracked by this partial SMT.
196    pub fn get(&self, asset_id: AssetId) -> Result<Option<Asset>, MerkleError> {
197        let value = self.partial_smt.get_value(&asset_id.hash().as_word())?;
198        if value.is_empty() {
199            Ok(None)
200        } else {
201            Ok(Some(
202                Asset::from_id_and_value(asset_id, value)
203                    .expect("partial vault should only track valid assets"),
204            ))
205        }
206    }
207
208    // MUTATORS
209    // --------------------------------------------------------------------------------------------
210
211    /// Adds an [`AssetWitness`] to this [`PartialVault`].
212    ///
213    /// # Errors
214    ///
215    /// Returns an error if:
216    /// - the new root after the insertion of the leaf and the path does not match the existing root
217    ///   (except when the first leaf is added).
218    pub fn add(&mut self, witness: AssetWitness) -> Result<(), PartialAssetVaultError> {
219        // Take ownership of the witness' entries up front so that, if `add_proof` fails, no
220        // partial state escapes into `self.entries`. The type-level guarantee (entries are a
221        // subset of partial_smt) must hold even after an error.
222        let (proof, new_entries) = witness.into_parts();
223        self.partial_smt
224            .add_proof(proof)
225            .map_err(PartialAssetVaultError::FailedToAddProof)?;
226        // Skip empty values so `entries` only ever tracks valid assets (mirrors `AssetVault::new`).
227        self.entries
228            .extend(new_entries.into_iter().filter(|(_, value)| !value.is_empty()));
229        Ok(())
230    }
231}
232
233impl Serializable for PartialVault {
234    fn write_into<W: ByteWriter>(&self, target: &mut W) {
235        target.write(&self.partial_smt);
236        target.write_usize(self.entries.len());
237        target.write_many(self.entries.keys());
238    }
239}
240
241impl Deserializable for PartialVault {
242    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
243        let partial_smt: PartialSmt = source.read()?;
244        let num_entries: usize = source.read()?;
245        let ids = source.read_many_iter::<AssetId>(num_entries)?.collect::<Result<Vec<_>, _>>()?;
246
247        Self::from_partial_smt_and_ids(partial_smt, ids)
248            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
249    }
250}
251
252// TESTS
253// ================================================================================================
254
255#[cfg(test)]
256mod tests {
257    use alloc::vec::Vec;
258
259    use assert_matches::assert_matches;
260    use miden_crypto::merkle::smt::Smt;
261
262    use super::*;
263    use crate::asset::{FungibleAsset, NonFungibleAsset};
264    use crate::testing::account_id::ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET;
265
266    #[test]
267    fn partial_vault_open_returns_correct_asset_after_full_conversion() -> anyhow::Result<()> {
268        let asset = FungibleAsset::mock(500);
269        let vault = AssetVault::new(&[asset])?;
270        let partial = PartialVault::new_full(vault.clone());
271
272        let id = asset.id();
273        let witness = partial.open(id)?;
274
275        assert!(witness.authenticates_asset_id(id));
276        assert_eq!(witness.find(id), Some(asset));
277        assert_eq!(partial.root(), vault.root());
278
279        Ok(())
280    }
281
282    #[test]
283    fn partial_vault_open_fails_for_untracked_id() -> anyhow::Result<()> {
284        let asset = FungibleAsset::mock(500);
285        let vault = AssetVault::new(&[asset])?;
286        // `new_minimal` carries the root but no entries.
287        let partial = PartialVault::new_minimal(&vault);
288
289        let err = partial.open(asset.id()).unwrap_err();
290        assert_matches!(err, PartialAssetVaultError::UntrackedAsset(_));
291
292        Ok(())
293    }
294
295    #[test]
296    fn partial_vault_with_witnesses_round_trips() -> anyhow::Result<()> {
297        let fungible = FungibleAsset::mock(500);
298        let non_fungible = NonFungibleAsset::mock(&[1, 2, 3]);
299        let vault = AssetVault::new(&[fungible, non_fungible])?;
300
301        let witnesses = [vault.open(fungible.id()), vault.open(non_fungible.id())];
302        let partial = PartialVault::with_witnesses(witnesses)?;
303
304        assert_eq!(partial.root(), vault.root());
305        assert_eq!(partial.entries().count(), 2);
306
307        // Round-trip serialization preserves equality.
308        let bytes = partial.to_bytes();
309        let roundtripped = PartialVault::read_from_bytes(&bytes)?;
310        assert_eq!(partial, roundtripped);
311
312        Ok(())
313    }
314
315    #[test]
316    fn partial_vault_with_witnesses_fails_on_root_mismatch() -> anyhow::Result<()> {
317        // Two single-asset vaults rooted at different SMT roots.
318        let asset_a = FungibleAsset::mock(500);
319        let asset_b: Asset =
320            FungibleAsset::new(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into()?, 100)?.into();
321        let vault_a = AssetVault::new(&[asset_a])?;
322        let vault_b = AssetVault::new(&[asset_b])?;
323        assert_ne!(vault_a.root(), vault_b.root());
324
325        let witness_a = vault_a.open(asset_a.id());
326        let witness_b = vault_b.open(asset_b.id());
327
328        let err = PartialVault::with_witnesses([witness_a, witness_b]).unwrap_err();
329        assert_matches!(err, PartialAssetVaultError::FailedToAddProof(_));
330
331        Ok(())
332    }
333
334    #[test]
335    fn partial_vault_add_extends_with_new_witness() -> anyhow::Result<()> {
336        let fungible = FungibleAsset::mock(500);
337        let non_fungible = NonFungibleAsset::mock(&[7, 8, 9]);
338        let vault = AssetVault::new(&[fungible, non_fungible])?;
339
340        let mut partial = PartialVault::with_witnesses([vault.open(fungible.id())])?;
341        assert_eq!(partial.entries().count(), 1);
342
343        partial.add(vault.open(non_fungible.id()))?;
344
345        assert_eq!(partial.root(), vault.root());
346        assert_eq!(partial.entries().count(), 2);
347        assert_eq!(partial.open(fungible.id())?.find(fungible.id()), Some(fungible));
348        assert_eq!(partial.open(non_fungible.id())?.find(non_fungible.id()), Some(non_fungible),);
349
350        Ok(())
351    }
352
353    #[test]
354    fn partial_vault_add_is_atomic_on_failure() -> anyhow::Result<()> {
355        // Build two distinct vaults so the second witness's root disagrees with the first.
356        let asset_a = FungibleAsset::mock(500);
357        let asset_b: Asset =
358            FungibleAsset::new(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into()?, 100)?.into();
359        let vault_a = AssetVault::new(&[asset_a])?;
360        let vault_b = AssetVault::new(&[asset_b])?;
361
362        let mut partial = PartialVault::with_witnesses([vault_a.open(asset_a.id())])?;
363        let entries_before: Vec<_> = partial.entries().map(|(k, v)| (*k, *v)).collect();
364        let root_before = partial.root();
365
366        let err = partial.add(vault_b.open(asset_b.id())).unwrap_err();
367        assert_matches!(err, PartialAssetVaultError::FailedToAddProof(_));
368
369        // Atomicity: failed `add` must not leak entries or shift the root.
370        let entries_after: Vec<_> = partial.entries().map(|(k, v)| (*k, *v)).collect();
371        assert_eq!(entries_before, entries_after);
372        assert_eq!(partial.root(), root_before);
373
374        Ok(())
375    }
376
377    #[test]
378    fn from_partial_smt_and_ids_rejects_inconsistent_asset() -> anyhow::Result<()> {
379        let fungible = FungibleAsset::mock(500);
380        let non_fungible = NonFungibleAsset::mock(&[4, 5, 6]);
381
382        // Build an SMT that stores a non-fungible value under a fungible ID's hashed slot, then
383        // wrap it in a partial SMT covering that ID.
384        let fungible_id = fungible.id();
385        let inconsistent_smt =
386            Smt::with_entries([(fungible_id.hash().as_word(), non_fungible.to_value_word())])?;
387        let proof = inconsistent_smt.open(&fungible_id.hash().as_word());
388        let partial_smt = PartialSmt::from_proofs([proof])?;
389
390        let err = PartialVault::from_partial_smt_and_ids(partial_smt, [fungible_id]).unwrap_err();
391        assert_matches!(err, PartialAssetVaultError::InvalidAssetForId { .. });
392
393        Ok(())
394    }
395}