miden_protocol/asset/vault/
partial.rs1use alloc::collections::{BTreeMap, BTreeSet};
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#[derive(Clone, Debug, PartialEq, Eq, Default)]
34pub struct PartialVault {
35 partial_smt: PartialSmt,
38 entries: BTreeMap<AssetId, Word>,
40}
41
42impl PartialVault {
43 pub fn new(root: Word) -> Self {
50 PartialVault {
51 partial_smt: PartialSmt::new(root),
52 entries: BTreeMap::new(),
53 }
54 }
55
56 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 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 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 pub fn new_minimal(vault: &AssetVault) -> Self {
94 PartialVault::new(vault.root())
95 }
96
97 pub fn try_from_parts(
106 partial_smt: PartialSmt,
107 ids: impl IntoIterator<Item = AssetId>,
108 ) -> Result<Self, PartialAssetVaultError> {
109 let mut entries = BTreeMap::new();
110 let mut seen_ids = BTreeSet::new();
111
112 for id in ids {
113 if !seen_ids.insert(id) {
114 return Err(PartialAssetVaultError::DuplicateAssetId(id));
115 }
116
117 let value = partial_smt
118 .get_value(&id.hash().as_word())
119 .map_err(PartialAssetVaultError::UntrackedAsset)?;
120
121 Asset::new(id, value).map_err(|source| PartialAssetVaultError::InvalidAssetForId {
125 id,
126 value,
127 source,
128 })?;
129
130 if !value.is_empty() {
133 entries.insert(id, value);
134 }
135 }
136
137 Ok(Self { partial_smt, entries })
138 }
139
140 pub fn root(&self) -> Word {
145 self.partial_smt.root()
146 }
147
148 pub fn partial_smt(&self) -> &PartialSmt {
150 &self.partial_smt
151 }
152
153 pub fn inner_nodes(&self) -> impl Iterator<Item = InnerNodeInfo> + '_ {
158 self.partial_smt.inner_nodes()
159 }
160
161 pub fn leaves(&self) -> impl Iterator<Item = &SmtLeaf> {
163 self.partial_smt.leaves().map(|(_, leaf)| leaf)
164 }
165
166 pub fn assets(&self) -> impl Iterator<Item = Asset> + '_ {
168 self.entries.iter().map(|(id, value)| {
169 Asset::new(*id, *value).expect("partial vault should only track valid assets")
170 })
171 }
172
173 pub fn asset_ids(&self) -> impl Iterator<Item = AssetId> + '_ {
175 self.entries.keys().copied()
176 }
177
178 #[cfg(test)]
180 pub(super) fn entries(&self) -> impl Iterator<Item = (&AssetId, &Word)> {
181 self.entries.iter()
182 }
183
184 pub fn open(&self, asset_id: AssetId) -> Result<AssetWitness, PartialAssetVaultError> {
193 let smt_proof = self
194 .partial_smt
195 .open(&asset_id.hash().as_word())
196 .map_err(PartialAssetVaultError::UntrackedAsset)?;
197 let value = self.entries.get(&asset_id).copied().unwrap_or_default();
198
199 Ok(AssetWitness::new_unchecked(smt_proof, [(asset_id, value)]))
202 }
203
204 pub fn get(&self, asset_id: AssetId) -> Result<Option<Asset>, MerkleError> {
213 let value = self.partial_smt.get_value(&asset_id.hash().as_word())?;
214 if value.is_empty() {
215 Ok(None)
216 } else {
217 Ok(Some(
218 Asset::new(asset_id, value).expect("partial vault should only track valid assets"),
219 ))
220 }
221 }
222
223 pub fn add(&mut self, witness: AssetWitness) -> Result<(), PartialAssetVaultError> {
234 let (proof, new_entries) = witness.into_parts();
238 self.partial_smt
239 .add_proof(proof)
240 .map_err(PartialAssetVaultError::FailedToAddProof)?;
241 self.entries
243 .extend(new_entries.into_iter().filter(|(_, value)| !value.is_empty()));
244 Ok(())
245 }
246}
247
248impl Serializable for PartialVault {
249 fn write_into<W: ByteWriter>(&self, target: &mut W) {
250 target.write(&self.partial_smt);
251 target.write_usize(self.entries.len());
252 target.write_many(self.entries.keys());
253 }
254}
255
256impl Deserializable for PartialVault {
257 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
258 let partial_smt: PartialSmt = source.read()?;
259 let num_entries: usize = source.read()?;
260 let ids = source.read_many_iter::<AssetId>(num_entries)?.collect::<Result<Vec<_>, _>>()?;
261
262 Self::try_from_parts(partial_smt, ids)
263 .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
264 }
265}
266
267#[cfg(test)]
271mod tests {
272 use alloc::vec::Vec;
273
274 use assert_matches::assert_matches;
275 use miden_crypto::merkle::smt::Smt;
276
277 use super::*;
278 use crate::asset::{FungibleAsset, NonFungibleAsset};
279 use crate::testing::account_id::ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET;
280
281 #[test]
282 fn partial_smt_accessor_returns_vault_smt() {
283 let root = Word::from([1_u32, 2, 3, 4]);
284 let vault = PartialVault::new(root);
285
286 assert_eq!(vault.partial_smt().root(), root);
287 }
288
289 #[test]
290 fn partial_vault_open_returns_correct_asset_after_full_conversion() -> anyhow::Result<()> {
291 let asset = FungibleAsset::mock(500);
292 let vault = AssetVault::new(&[asset])?;
293 let partial = PartialVault::new_full(vault.clone());
294
295 let id = asset.id();
296 let witness = partial.open(id)?;
297
298 assert!(witness.authenticates_asset_id(id));
299 assert_eq!(witness.find(id), Some(asset));
300 assert_eq!(partial.root(), vault.root());
301
302 Ok(())
303 }
304
305 #[test]
306 fn partial_vault_open_fails_for_untracked_id() -> anyhow::Result<()> {
307 let asset = FungibleAsset::mock(500);
308 let vault = AssetVault::new(&[asset])?;
309 let partial = PartialVault::new_minimal(&vault);
311
312 let err = partial.open(asset.id()).unwrap_err();
313 assert_matches!(err, PartialAssetVaultError::UntrackedAsset(_));
314
315 Ok(())
316 }
317
318 #[test]
319 fn partial_vault_with_witnesses_round_trips() -> anyhow::Result<()> {
320 let fungible = FungibleAsset::mock(500);
321 let non_fungible = NonFungibleAsset::mock(&[1, 2, 3]);
322 let vault = AssetVault::new(&[fungible, non_fungible])?;
323
324 let witnesses = [vault.open(fungible.id()), vault.open(non_fungible.id())];
325 let partial = PartialVault::with_witnesses(witnesses)?;
326
327 assert_eq!(partial.root(), vault.root());
328 assert_eq!(partial.entries().count(), 2);
329
330 let bytes = partial.to_bytes();
332 let roundtripped = PartialVault::read_from_bytes(&bytes)?;
333 assert_eq!(partial, roundtripped);
334
335 Ok(())
336 }
337
338 #[test]
339 fn partial_vault_with_witnesses_fails_on_root_mismatch() -> anyhow::Result<()> {
340 let asset_a = FungibleAsset::mock(500);
342 let asset_b: Asset =
343 FungibleAsset::new(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into()?, 100)?.into();
344 let vault_a = AssetVault::new(&[asset_a])?;
345 let vault_b = AssetVault::new(&[asset_b])?;
346 assert_ne!(vault_a.root(), vault_b.root());
347
348 let witness_a = vault_a.open(asset_a.id());
349 let witness_b = vault_b.open(asset_b.id());
350
351 let err = PartialVault::with_witnesses([witness_a, witness_b]).unwrap_err();
352 assert_matches!(err, PartialAssetVaultError::FailedToAddProof(_));
353
354 Ok(())
355 }
356
357 #[test]
358 fn partial_vault_add_extends_with_new_witness() -> anyhow::Result<()> {
359 let fungible = FungibleAsset::mock(500);
360 let non_fungible = NonFungibleAsset::mock(&[7, 8, 9]);
361 let vault = AssetVault::new(&[fungible, non_fungible])?;
362
363 let mut partial = PartialVault::with_witnesses([vault.open(fungible.id())])?;
364 assert_eq!(partial.entries().count(), 1);
365
366 partial.add(vault.open(non_fungible.id()))?;
367
368 assert_eq!(partial.root(), vault.root());
369 assert_eq!(partial.entries().count(), 2);
370 assert_eq!(partial.open(fungible.id())?.find(fungible.id()), Some(fungible));
371 assert_eq!(partial.open(non_fungible.id())?.find(non_fungible.id()), Some(non_fungible),);
372
373 Ok(())
374 }
375
376 #[test]
377 fn partial_vault_add_is_atomic_on_failure() -> anyhow::Result<()> {
378 let asset_a = FungibleAsset::mock(500);
380 let asset_b: Asset =
381 FungibleAsset::new(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into()?, 100)?.into();
382 let vault_a = AssetVault::new(&[asset_a])?;
383 let vault_b = AssetVault::new(&[asset_b])?;
384
385 let mut partial = PartialVault::with_witnesses([vault_a.open(asset_a.id())])?;
386 let entries_before: Vec<_> = partial.entries().map(|(k, v)| (*k, *v)).collect();
387 let root_before = partial.root();
388
389 let err = partial.add(vault_b.open(asset_b.id())).unwrap_err();
390 assert_matches!(err, PartialAssetVaultError::FailedToAddProof(_));
391
392 let entries_after: Vec<_> = partial.entries().map(|(k, v)| (*k, *v)).collect();
394 assert_eq!(entries_before, entries_after);
395 assert_eq!(partial.root(), root_before);
396
397 Ok(())
398 }
399
400 #[test]
401 fn try_from_parts_rejects_inconsistent_asset() -> anyhow::Result<()> {
402 let fungible = FungibleAsset::mock(500);
403 let non_fungible = NonFungibleAsset::mock(&[4, 5, 6]);
404
405 let fungible_id = fungible.id();
408 let inconsistent_smt =
409 Smt::with_entries([(fungible_id.hash().as_word(), non_fungible.to_value_word())])?;
410 let proof = inconsistent_smt.open(&fungible_id.hash().as_word());
411 let partial_smt = PartialSmt::from_proofs([proof])?;
412
413 let err = PartialVault::try_from_parts(partial_smt, [fungible_id]).unwrap_err();
414 assert_matches!(err, PartialAssetVaultError::InvalidAssetForId { .. });
415
416 Ok(())
417 }
418
419 #[test]
420 fn try_from_parts_preserves_unrelated_partial_smt_material() -> anyhow::Result<()> {
421 let tracked_asset = FungibleAsset::mock(500);
422 let extra_asset = NonFungibleAsset::mock(&[1, 2, 3]);
423 let vault = AssetVault::new(&[tracked_asset, extra_asset])?;
424 let partial_smt = PartialSmt::from_proofs([
425 vault.open(tracked_asset.id()).into(),
426 vault.open(extra_asset.id()).into(),
427 ])?;
428
429 let partial_vault = PartialVault::try_from_parts(partial_smt, [tracked_asset.id()])?;
430
431 assert_eq!(partial_vault.asset_ids().collect::<Vec<_>>(), [tracked_asset.id()]);
432 assert_eq!(partial_vault.get(extra_asset.id())?, Some(extra_asset));
433
434 Ok(())
435 }
436
437 #[test]
438 fn try_from_parts_rejects_duplicate_asset_ids() -> anyhow::Result<()> {
439 let asset = FungibleAsset::mock(500);
440 let vault = AssetVault::new(&[asset])?;
441 let partial_smt = PartialSmt::from_proofs([vault.open(asset.id()).into()])?;
442
443 let result = PartialVault::try_from_parts(partial_smt, [asset.id(), asset.id()]);
444
445 assert_matches!(result, Err(PartialAssetVaultError::DuplicateAssetId(id)) if id == asset.id());
446
447 Ok(())
448 }
449
450 #[test]
451 fn try_from_parts_rejects_untracked_asset_ids() {
452 let asset_id = FungibleAsset::mock(500).id();
453 let result = PartialVault::try_from_parts(PartialSmt::new(Word::empty()), [asset_id]);
454
455 assert_matches!(
456 result,
457 Err(PartialAssetVaultError::UntrackedAsset(MerkleError::UntrackedKey(hashed_id)))
458 if hashed_id == asset_id.hash().as_word()
459 );
460 }
461}