use alloc::collections::BTreeMap;
use alloc::string::ToString;
use alloc::vec::Vec;
use miden_crypto::merkle::smt::{PartialSmt, SmtLeaf, SmtProof};
use miden_crypto::merkle::{InnerNodeInfo, MerkleError};
use super::{AssetId, AssetVault};
use crate::Word;
use crate::asset::{Asset, AssetWitness};
use crate::errors::PartialAssetVaultError;
use crate::utils::serde::{
ByteReader,
ByteWriter,
Deserializable,
DeserializationError,
Serializable,
};
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct PartialVault {
partial_smt: PartialSmt,
entries: BTreeMap<AssetId, Word>,
}
impl PartialVault {
pub fn new(root: Word) -> Self {
PartialVault {
partial_smt: PartialSmt::new(root),
entries: BTreeMap::new(),
}
}
pub fn with_witnesses(
witnesses: impl IntoIterator<Item = AssetWitness>,
) -> Result<Self, PartialAssetVaultError> {
let mut entries = BTreeMap::new();
let partial_smt = PartialSmt::from_proofs(witnesses.into_iter().map(|witness| {
entries.extend(
witness
.entries()
.filter(|(_, value)| !value.is_empty())
.map(|(id, value)| (*id, *value)),
);
SmtProof::from(witness)
}))
.map_err(PartialAssetVaultError::FailedToAddProof)?;
Ok(PartialVault { partial_smt, entries })
}
pub fn new_full(vault: AssetVault) -> Self {
let partial_smt = PartialSmt::from(vault.asset_tree);
let entries = vault.entries;
PartialVault { partial_smt, entries }
}
pub fn new_minimal(vault: &AssetVault) -> Self {
PartialVault::new(vault.root())
}
fn from_partial_smt_and_ids(
partial_smt: PartialSmt,
ids: impl IntoIterator<Item = AssetId>,
) -> Result<Self, PartialAssetVaultError> {
let mut entries = BTreeMap::new();
for id in ids {
let value = partial_smt
.get_value(&id.hash().as_word())
.map_err(PartialAssetVaultError::UntrackedAsset)?;
Asset::from_id_and_value(id, value).map_err(|source| {
PartialAssetVaultError::InvalidAssetForId { id, value, source }
})?;
if !value.is_empty() {
entries.insert(id, value);
}
}
Ok(Self { partial_smt, entries })
}
pub fn root(&self) -> Word {
self.partial_smt.root()
}
pub fn inner_nodes(&self) -> impl Iterator<Item = InnerNodeInfo> + '_ {
self.partial_smt.inner_nodes()
}
pub fn leaves(&self) -> impl Iterator<Item = &SmtLeaf> {
self.partial_smt.leaves().map(|(_, leaf)| leaf)
}
pub fn assets(&self) -> impl Iterator<Item = Asset> + '_ {
self.entries.iter().map(|(id, value)| {
Asset::from_id_and_value(*id, *value)
.expect("partial vault should only track valid assets")
})
}
#[cfg(test)]
pub(super) fn entries(&self) -> impl Iterator<Item = (&AssetId, &Word)> {
self.entries.iter()
}
pub fn open(&self, asset_id: AssetId) -> Result<AssetWitness, PartialAssetVaultError> {
let smt_proof = self
.partial_smt
.open(&asset_id.hash().as_word())
.map_err(PartialAssetVaultError::UntrackedAsset)?;
let value = self.entries.get(&asset_id).copied().unwrap_or_default();
Ok(AssetWitness::new_unchecked(smt_proof, [(asset_id, value)]))
}
pub fn get(&self, asset_id: AssetId) -> Result<Option<Asset>, MerkleError> {
let value = self.partial_smt.get_value(&asset_id.hash().as_word())?;
if value.is_empty() {
Ok(None)
} else {
Ok(Some(
Asset::from_id_and_value(asset_id, value)
.expect("partial vault should only track valid assets"),
))
}
}
pub fn add(&mut self, witness: AssetWitness) -> Result<(), PartialAssetVaultError> {
let (proof, new_entries) = witness.into_parts();
self.partial_smt
.add_proof(proof)
.map_err(PartialAssetVaultError::FailedToAddProof)?;
self.entries
.extend(new_entries.into_iter().filter(|(_, value)| !value.is_empty()));
Ok(())
}
}
impl Serializable for PartialVault {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
target.write(&self.partial_smt);
target.write_usize(self.entries.len());
target.write_many(self.entries.keys());
}
}
impl Deserializable for PartialVault {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let partial_smt: PartialSmt = source.read()?;
let num_entries: usize = source.read()?;
let ids = source.read_many_iter::<AssetId>(num_entries)?.collect::<Result<Vec<_>, _>>()?;
Self::from_partial_smt_and_ids(partial_smt, ids)
.map_err(|err| DeserializationError::InvalidValue(err.to_string()))
}
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use assert_matches::assert_matches;
use miden_crypto::merkle::smt::Smt;
use super::*;
use crate::asset::{FungibleAsset, NonFungibleAsset};
use crate::testing::account_id::ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET;
#[test]
fn partial_vault_open_returns_correct_asset_after_full_conversion() -> anyhow::Result<()> {
let asset = FungibleAsset::mock(500);
let vault = AssetVault::new(&[asset])?;
let partial = PartialVault::new_full(vault.clone());
let id = asset.id();
let witness = partial.open(id)?;
assert!(witness.authenticates_asset_id(id));
assert_eq!(witness.find(id), Some(asset));
assert_eq!(partial.root(), vault.root());
Ok(())
}
#[test]
fn partial_vault_open_fails_for_untracked_id() -> anyhow::Result<()> {
let asset = FungibleAsset::mock(500);
let vault = AssetVault::new(&[asset])?;
let partial = PartialVault::new_minimal(&vault);
let err = partial.open(asset.id()).unwrap_err();
assert_matches!(err, PartialAssetVaultError::UntrackedAsset(_));
Ok(())
}
#[test]
fn partial_vault_with_witnesses_round_trips() -> anyhow::Result<()> {
let fungible = FungibleAsset::mock(500);
let non_fungible = NonFungibleAsset::mock(&[1, 2, 3]);
let vault = AssetVault::new(&[fungible, non_fungible])?;
let witnesses = [vault.open(fungible.id()), vault.open(non_fungible.id())];
let partial = PartialVault::with_witnesses(witnesses)?;
assert_eq!(partial.root(), vault.root());
assert_eq!(partial.entries().count(), 2);
let bytes = partial.to_bytes();
let roundtripped = PartialVault::read_from_bytes(&bytes)?;
assert_eq!(partial, roundtripped);
Ok(())
}
#[test]
fn partial_vault_with_witnesses_fails_on_root_mismatch() -> anyhow::Result<()> {
let asset_a = FungibleAsset::mock(500);
let asset_b: Asset =
FungibleAsset::new(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into()?, 100)?.into();
let vault_a = AssetVault::new(&[asset_a])?;
let vault_b = AssetVault::new(&[asset_b])?;
assert_ne!(vault_a.root(), vault_b.root());
let witness_a = vault_a.open(asset_a.id());
let witness_b = vault_b.open(asset_b.id());
let err = PartialVault::with_witnesses([witness_a, witness_b]).unwrap_err();
assert_matches!(err, PartialAssetVaultError::FailedToAddProof(_));
Ok(())
}
#[test]
fn partial_vault_add_extends_with_new_witness() -> anyhow::Result<()> {
let fungible = FungibleAsset::mock(500);
let non_fungible = NonFungibleAsset::mock(&[7, 8, 9]);
let vault = AssetVault::new(&[fungible, non_fungible])?;
let mut partial = PartialVault::with_witnesses([vault.open(fungible.id())])?;
assert_eq!(partial.entries().count(), 1);
partial.add(vault.open(non_fungible.id()))?;
assert_eq!(partial.root(), vault.root());
assert_eq!(partial.entries().count(), 2);
assert_eq!(partial.open(fungible.id())?.find(fungible.id()), Some(fungible));
assert_eq!(partial.open(non_fungible.id())?.find(non_fungible.id()), Some(non_fungible),);
Ok(())
}
#[test]
fn partial_vault_add_is_atomic_on_failure() -> anyhow::Result<()> {
let asset_a = FungibleAsset::mock(500);
let asset_b: Asset =
FungibleAsset::new(ACCOUNT_ID_PRIVATE_FUNGIBLE_FAUCET.try_into()?, 100)?.into();
let vault_a = AssetVault::new(&[asset_a])?;
let vault_b = AssetVault::new(&[asset_b])?;
let mut partial = PartialVault::with_witnesses([vault_a.open(asset_a.id())])?;
let entries_before: Vec<_> = partial.entries().map(|(k, v)| (*k, *v)).collect();
let root_before = partial.root();
let err = partial.add(vault_b.open(asset_b.id())).unwrap_err();
assert_matches!(err, PartialAssetVaultError::FailedToAddProof(_));
let entries_after: Vec<_> = partial.entries().map(|(k, v)| (*k, *v)).collect();
assert_eq!(entries_before, entries_after);
assert_eq!(partial.root(), root_before);
Ok(())
}
#[test]
fn from_partial_smt_and_ids_rejects_inconsistent_asset() -> anyhow::Result<()> {
let fungible = FungibleAsset::mock(500);
let non_fungible = NonFungibleAsset::mock(&[4, 5, 6]);
let fungible_id = fungible.id();
let inconsistent_smt =
Smt::with_entries([(fungible_id.hash().as_word(), non_fungible.to_value_word())])?;
let proof = inconsistent_smt.open(&fungible_id.hash().as_word());
let partial_smt = PartialSmt::from_proofs([proof])?;
let err = PartialVault::from_partial_smt_and_ids(partial_smt, [fungible_id]).unwrap_err();
assert_matches!(err, PartialAssetVaultError::InvalidAssetForId { .. });
Ok(())
}
}