miden_protocol/asset/vault/
partial.rs1use 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#[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 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 Asset::from_id_and_value(id, value).map_err(|source| {
120 PartialAssetVaultError::InvalidAssetForId { id, value, source }
121 })?;
122
123 if !value.is_empty() {
126 entries.insert(id, value);
127 }
128 }
129
130 Ok(Self { partial_smt, entries })
131 }
132
133 pub fn root(&self) -> Word {
138 self.partial_smt.root()
139 }
140
141 pub fn inner_nodes(&self) -> impl Iterator<Item = InnerNodeInfo> + '_ {
146 self.partial_smt.inner_nodes()
147 }
148
149 pub fn leaves(&self) -> impl Iterator<Item = &SmtLeaf> {
151 self.partial_smt.leaves().map(|(_, leaf)| leaf)
152 }
153
154 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 #[cfg(test)]
164 pub(super) fn entries(&self) -> impl Iterator<Item = (&AssetId, &Word)> {
165 self.entries.iter()
166 }
167
168 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 Ok(AssetWitness::new_unchecked(smt_proof, [(asset_id, value)]))
186 }
187
188 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 pub fn add(&mut self, witness: AssetWitness) -> Result<(), PartialAssetVaultError> {
219 let (proof, new_entries) = witness.into_parts();
223 self.partial_smt
224 .add_proof(proof)
225 .map_err(PartialAssetVaultError::FailedToAddProof)?;
226 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#[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 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 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 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 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 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 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}