miden_protocol/asset/vault/
partial.rs

1use alloc::string::ToString;
2
3use miden_crypto::merkle::smt::{PartialSmt, SmtLeaf, SmtProof};
4use miden_crypto::merkle::{InnerNodeInfo, MerkleError};
5
6use super::{AssetVault, AssetVaultKey};
7use crate::Word;
8use crate::asset::{Asset, AssetWitness};
9use crate::errors::PartialAssetVaultError;
10use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable};
11
12/// A partial representation of an [`AssetVault`], containing only proofs for a subset of assets.
13///
14/// Partial vault is used to provide verifiable access to specific assets in a vault
15/// without the need to provide the full vault data. It contains all required data for loading
16/// vault data into the transaction kernel for transaction execution.
17#[derive(Clone, Debug, PartialEq, Eq, Default)]
18pub struct PartialVault {
19    /// An SMT with a partial view into an account's full [`AssetVault`].
20    partial_smt: PartialSmt,
21}
22
23impl PartialVault {
24    // CONSTRUCTORS
25    // --------------------------------------------------------------------------------------------
26
27    /// Constructs a [`PartialVault`] from an [`AssetVault`] root.
28    ///
29    /// For conversion from an [`AssetVault`], prefer [`Self::new_minimal`] to be more explicit.
30    pub fn new(root: Word) -> Self {
31        PartialVault { partial_smt: PartialSmt::new(root) }
32    }
33
34    /// Converts an [`AssetVault`] into a partial vault representation.
35    ///
36    /// The resulting [`PartialVault`] will contain the _full_ merkle paths of the original asset
37    /// vault.
38    pub fn new_full(vault: AssetVault) -> Self {
39        let partial_smt = PartialSmt::from(vault.asset_tree);
40
41        PartialVault { partial_smt }
42    }
43
44    /// Converts an [`AssetVault`] into a partial vault representation.
45    ///
46    /// The resulting [`PartialVault`] will represent the root of the asset vault, but not track any
47    /// key-value pairs, which means it is the most _minimal_ representation of the asset vault.
48    pub fn new_minimal(vault: &AssetVault) -> Self {
49        PartialVault::new(vault.root())
50    }
51
52    // ACCESSORS
53    // --------------------------------------------------------------------------------------------
54
55    /// Returns the root of the partial vault.
56    pub fn root(&self) -> Word {
57        self.partial_smt.root()
58    }
59
60    /// Returns an iterator over all inner nodes in the Sparse Merkle Tree proofs.
61    ///
62    /// This is useful for reconstructing parts of the Sparse Merkle Tree or for
63    /// verification purposes.
64    pub fn inner_nodes(&self) -> impl Iterator<Item = InnerNodeInfo> + '_ {
65        self.partial_smt.inner_nodes()
66    }
67
68    /// Returns an iterator over all leaves in the Sparse Merkle Tree proofs.
69    ///
70    /// Each item returned is a tuple containing the leaf index and a reference to the leaf.
71    pub fn leaves(&self) -> impl Iterator<Item = &SmtLeaf> {
72        self.partial_smt.leaves().map(|(_, leaf)| leaf)
73    }
74
75    /// Returns an opening of the leaf associated with `vault_key`.
76    ///
77    /// The `vault_key` can be obtained with [`Asset::vault_key`].
78    ///
79    /// # Errors
80    ///
81    /// Returns an error if:
82    /// - the key is not tracked by this partial vault.
83    pub fn open(&self, vault_key: AssetVaultKey) -> Result<AssetWitness, PartialAssetVaultError> {
84        let smt_proof = self
85            .partial_smt
86            .open(&vault_key.into())
87            .map_err(PartialAssetVaultError::UntrackedAsset)?;
88        // SAFETY: The partial vault should only contain valid assets.
89        Ok(AssetWitness::new_unchecked(smt_proof))
90    }
91
92    /// Returns the [`Asset`] associated with the given `vault_key`.
93    ///
94    /// The return value is `None` if the asset does not exist in the vault.
95    ///
96    /// # Errors
97    ///
98    /// Returns an error if:
99    /// - the key is not tracked by this partial SMT.
100    pub fn get(&self, vault_key: AssetVaultKey) -> Result<Option<Asset>, MerkleError> {
101        self.partial_smt.get_value(&vault_key.into()).map(|word| {
102            if word.is_empty() {
103                None
104            } else {
105                // SAFETY: If this returned a non-empty word, then it should be a valid asset,
106                // because the vault should only track valid ones.
107                Some(Asset::try_from(word).expect("partial vault should only track valid assets"))
108            }
109        })
110    }
111
112    // MUTATORS
113    // --------------------------------------------------------------------------------------------
114
115    /// Adds an [`AssetWitness`] to this [`PartialVault`].
116    ///
117    /// # Errors
118    ///
119    /// Returns an error if:
120    /// - the new root after the insertion of the leaf and the path does not match the existing root
121    ///   (except when the first leaf is added).
122    pub fn add(&mut self, witness: AssetWitness) -> Result<(), PartialAssetVaultError> {
123        let proof = SmtProof::from(witness);
124        self.partial_smt
125            .add_proof(proof)
126            .map_err(PartialAssetVaultError::FailedToAddProof)
127    }
128
129    // HELPER FUNCTIONS
130    // --------------------------------------------------------------------------------------------
131
132    /// Validates that the provided entries are valid vault keys and assets.
133    ///
134    /// For brevity, the error conditions are only mentioned on the public methods that use this
135    /// function.
136    fn validate_entries<'a>(
137        entries: impl IntoIterator<Item = &'a (Word, Word)>,
138    ) -> Result<(), PartialAssetVaultError> {
139        for (vault_key, asset) in entries {
140            let asset = Asset::try_from(asset).map_err(|source| {
141                PartialAssetVaultError::InvalidAssetInSmt { entry: *asset, source }
142            })?;
143
144            if *vault_key != asset.vault_key().into() {
145                return Err(PartialAssetVaultError::AssetVaultKeyMismatch {
146                    expected: asset.vault_key(),
147                    actual: *vault_key,
148                });
149            }
150        }
151
152        Ok(())
153    }
154}
155
156impl TryFrom<PartialSmt> for PartialVault {
157    type Error = PartialAssetVaultError;
158
159    /// Returns a new instance of a partial vault from the provided partial SMT.
160    ///
161    /// # Errors
162    ///
163    /// Returns an error if:
164    /// - the provided SMT does not track only valid [`Asset`]s.
165    /// - the vault key at which the asset is stored does not match the vault key derived from the
166    ///   asset.
167    fn try_from(partial_smt: PartialSmt) -> Result<Self, Self::Error> {
168        Self::validate_entries(partial_smt.entries())?;
169
170        Ok(PartialVault { partial_smt })
171    }
172}
173
174impl Serializable for PartialVault {
175    fn write_into<W: ByteWriter>(&self, target: &mut W) {
176        target.write(&self.partial_smt)
177    }
178}
179
180impl Deserializable for PartialVault {
181    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
182        let partial_smt: PartialSmt = source.read()?;
183
184        PartialVault::try_from(partial_smt)
185            .map_err(|err| DeserializationError::InvalidValue(err.to_string()))
186    }
187}
188
189// TESTS
190// ================================================================================================
191
192#[cfg(test)]
193mod tests {
194    use assert_matches::assert_matches;
195    use miden_crypto::merkle::smt::Smt;
196
197    use super::*;
198    use crate::asset::FungibleAsset;
199
200    #[test]
201    fn partial_vault_ensures_asset_validity() -> anyhow::Result<()> {
202        let invalid_asset = Word::from([0, 0, 0, 5u32]);
203        let smt = Smt::with_entries([(invalid_asset, invalid_asset)])?;
204        let proof = smt.open(&invalid_asset);
205        let partial_smt = PartialSmt::from_proofs([proof.clone()])?;
206
207        let err = PartialVault::try_from(partial_smt).unwrap_err();
208        assert_matches!(err, PartialAssetVaultError::InvalidAssetInSmt { entry, .. } => {
209            assert_eq!(entry, invalid_asset);
210        });
211
212        Ok(())
213    }
214
215    #[test]
216    fn partial_vault_ensures_asset_vault_key_matches() -> anyhow::Result<()> {
217        let asset = FungibleAsset::mock(500);
218        let invalid_vault_key = Word::from([0, 1, 2, 3u32]);
219        let smt = Smt::with_entries([(invalid_vault_key, asset.into())])?;
220        let proof = smt.open(&invalid_vault_key);
221        let partial_smt = PartialSmt::from_proofs([proof.clone()])?;
222
223        let err = PartialVault::try_from(partial_smt).unwrap_err();
224        assert_matches!(err, PartialAssetVaultError::AssetVaultKeyMismatch { expected, actual } => {
225            assert_eq!(actual, invalid_vault_key);
226            assert_eq!(expected, asset.vault_key());
227        });
228
229        Ok(())
230    }
231}