miden_objects/asset/vault/
partial.rs

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