Skip to main content

miden_client/store/
smt_forest.rs

1use alloc::collections::BTreeMap;
2use alloc::format;
3use alloc::vec::Vec;
4
5use miden_protocol::account::{
6    AccountId,
7    AccountStoragePatch,
8    AccountVaultPatch,
9    StorageMapKey,
10    StorageMapPatch,
11    StorageMapWitness,
12    StorageSlot,
13    StorageSlotContent,
14    StorageSlotName,
15};
16use miden_protocol::asset::{Asset, AssetId, AssetWitness};
17use miden_protocol::crypto::merkle::MerkleError;
18use miden_protocol::crypto::merkle::smt::{
19    Backend,
20    BackendReader,
21    LargeSmtForest,
22    LargeSmtForestError,
23    LineageId,
24    SmtForestUpdateBatch,
25    TreeId,
26    VersionId,
27};
28use miden_protocol::utils::serde::Serializable;
29use miden_protocol::{EMPTY_WORD, Hasher, Word};
30
31use super::StoreError;
32
33// LINEAGE DERIVATION
34// ================================================================================================
35
36/// Returns the lineage identifier for an account's asset vault SMT.
37fn vault_lineage_id(account_id: AccountId) -> LineageId {
38    let mut bytes = Vec::new();
39    bytes.extend_from_slice(b"miden-client:vault");
40    bytes.extend_from_slice(&account_id.to_bytes());
41    LineageId::new(Hasher::hash(&bytes).as_bytes())
42}
43
44/// Returns the lineage identifier for an account's storage map SMT in the given slot.
45fn storage_map_lineage_id(account_id: AccountId, slot_name: &StorageSlotName) -> LineageId {
46    let mut bytes = Vec::new();
47    bytes.extend_from_slice(b"miden-client:storage-map");
48    bytes.extend_from_slice(&account_id.to_bytes());
49    // Length-prefix the variable-sized slot name so distinct (id, name) pairs cannot produce
50    // the same preimage. The fixed-width u64 keeps the identifier platform-independent.
51    bytes.extend_from_slice(&(slot_name.as_str().len() as u64).to_le_bytes());
52    bytes.extend_from_slice(slot_name.as_str().as_bytes());
53    LineageId::new(Hasher::hash(&bytes).as_bytes())
54}
55
56// ACCOUNT UPDATE
57// ================================================================================================
58
59/// Changes recorded for one lineage.
60#[derive(Default)]
61struct LineageOps {
62    /// When set, the lineage's computed root must equal this before the update is applied.
63    expect_root: Option<Word>,
64    /// When set, keys absent from `pairs` are removed, so the tree ends up holding exactly the
65    /// recorded pairs.
66    exhaustive: bool,
67    /// Key-value pairs in recording order. An empty-word value is a removal, and a later pair
68    /// for the same key supersedes an earlier one.
69    pairs: Vec<(Word, Word)>,
70}
71
72/// Account SMT changes, applied as a single batch by [`AccountSmtForest::apply`].
73///
74/// Recording is pure bookkeeping: the entries a change implies are worked out when the update is
75/// applied, which is where the forest can be read.
76#[derive(Default)]
77pub struct AccountUpdate {
78    ops: BTreeMap<LineageId, LineageOps>,
79}
80
81impl AccountUpdate {
82    /// Creates an update with no recorded changes.
83    pub fn new() -> Self {
84        Self::default()
85    }
86
87    /// Records an account's vault patch, along with the vault root the transaction produced.
88    ///
89    /// [`apply`] checks the resulting root against `expected_root`. That check is what ties the
90    /// vault tree back to the transaction kernel's result, so a wrong root fails the update
91    /// instead of being persisted.
92    ///
93    /// [`apply`]: AccountSmtForest::apply
94    pub fn vault_patch(
95        &mut self,
96        account_id: AccountId,
97        patch: &AccountVaultPatch,
98        expected_root: Word,
99    ) {
100        let vault = self.entry(vault_lineage_id(account_id));
101        vault.expect_root = Some(expected_root);
102        vault
103            .pairs
104            .extend(patch.updated_assets().map(|a| (a.id().hash().into(), a.to_value_word())));
105        vault
106            .pairs
107            .extend(patch.removed_asset_ids().map(|id| (id.hash().into(), EMPTY_WORD)));
108    }
109
110    /// Records an account's storage patch.
111    ///
112    /// Map slots are layered onto their current tree for `Update` patches and replaced wholesale
113    /// for `Create` and `Remove`. No per-slot root is recorded: the store checks the resulting map
114    /// roots collectively against the transaction's storage commitment, which also catches a tree
115    /// that had drifted from the account tables.
116    pub fn storage_patch(&mut self, account_id: AccountId, patch: &AccountStoragePatch) {
117        for (slot_name, map_patch) in patch.maps() {
118            let ops = self.entry(storage_map_lineage_id(account_id, slot_name));
119            ops.pairs.extend(
120                map_patch
121                    .entries()
122                    .into_iter()
123                    .flat_map(|e| e.as_map().iter())
124                    .map(|(key, value)| (Word::from(key.hash()), *value)),
125            );
126            if matches!(map_patch, StorageMapPatch::Create { .. } | StorageMapPatch::Remove) {
127                ops.exhaustive = true;
128            }
129        }
130    }
131
132    /// Records that an account's vault and map slots hold exactly the provided state.
133    ///
134    /// Slots that the account no longer has are not implied by `slots` and must be named with
135    /// [`Self::clear_map`].
136    pub fn full_state<'a>(
137        &mut self,
138        account_id: AccountId,
139        assets: impl Iterator<Item = Asset>,
140        slots: impl Iterator<Item = &'a StorageSlot>,
141    ) {
142        let vault = self.entry(vault_lineage_id(account_id));
143        vault.exhaustive = true;
144        vault.pairs.extend(assets.map(|a| (a.id().hash().into(), a.to_value_word())));
145
146        for slot in slots {
147            if let StorageSlotContent::Map(map) = slot.content() {
148                let ops = self.entry(storage_map_lineage_id(account_id, slot.name()));
149                ops.exhaustive = true;
150                ops.pairs
151                    .extend(map.entries().map(|(key, value)| (Word::from(key.hash()), *value)));
152            }
153        }
154    }
155
156    /// Records that one of an account's map slots holds nothing.
157    pub fn clear_map(&mut self, account_id: AccountId, slot_name: &StorageSlotName) {
158        self.entry(storage_map_lineage_id(account_id, slot_name)).exhaustive = true;
159    }
160
161    fn entry(&mut self, lineage: LineageId) -> &mut LineageOps {
162        self.ops.entry(lineage).or_default()
163    }
164}
165
166// ACCOUNT SMT FOREST
167// ================================================================================================
168
169/// Account-oriented wrapper around [`LargeSmtForest`].
170///
171/// Account SMTs are tracked as lineages, one per account vault and one per storage map slot,
172/// with identifiers derived deterministically from the account ID (and slot name). Each lineage
173/// evolves through strictly increasing versions supplied by the caller.
174///
175/// Lineage identifiers are an implementation detail: callers address trees by account ID and
176/// slot name, so no store can construct a lineage that diverges from the one this wrapper
177/// derives.
178///
179/// The wrapper is generic over the forest storage [`BackendReader`], so read-only backends can
180/// serve roots and witnesses. Applying updates additionally requires [`Backend`]. Construction
181/// loads the backend's tree metadata.
182pub struct AccountSmtForest<B: BackendReader> {
183    forest: LargeSmtForest<B>,
184}
185
186impl<B: BackendReader> AccountSmtForest<B> {
187    /// Creates a forest over the provided backend, loading tree metadata from it.
188    pub fn new(backend: B) -> Result<Self, StoreError> {
189        Ok(Self {
190            forest: LargeSmtForest::new(backend).map_err(forest_error)?,
191        })
192    }
193
194    // READERS
195    // --------------------------------------------------------------------------------------------
196
197    /// Returns the latest root of the account's asset vault SMT, or `None` if the forest does
198    /// not track the account.
199    pub fn vault_root(&self, account_id: AccountId) -> Option<Word> {
200        self.forest.latest_root(vault_lineage_id(account_id))
201    }
202
203    /// Returns the latest root of the account's storage map SMT in the given slot, or `None` if
204    /// the forest does not track that slot.
205    pub fn map_root(&self, account_id: AccountId, slot_name: &StorageSlotName) -> Option<Word> {
206        self.forest.latest_root(storage_map_lineage_id(account_id, slot_name))
207    }
208
209    /// Retrieves the vault asset and its witness for a specific vault key.
210    ///
211    /// The proof is opened against the latest tree of the account's vault lineage, after
212    /// verifying that its root matches `expected_vault_root` (the root recorded in the account
213    /// tables). A mismatch means forest and account state are out of sync and is reported as a
214    /// conflicting-roots error.
215    pub fn get_asset_and_witness(
216        &self,
217        account_id: AccountId,
218        expected_vault_root: Word,
219        asset_id: AssetId,
220    ) -> Result<(Asset, AssetWitness), StoreError> {
221        let lineage = vault_lineage_id(account_id);
222        let tree = self.verified_latest_tree(lineage, expected_vault_root)?;
223
224        let hashed_key: Word = asset_id.hash().into();
225        let proof = self.forest.open(tree, hashed_key).map_err(forest_error)?;
226        let asset_word = proof
227            .get(&hashed_key)
228            .ok_or(StoreError::VaultKeyNotTracked(asset_id, hashed_key))?;
229        if asset_word == EMPTY_WORD {
230            return Err(StoreError::VaultKeyNotTracked(asset_id, hashed_key));
231        }
232
233        let asset = Asset::from_id_and_value(asset_id, asset_word)?;
234        let witness = AssetWitness::new(proof, [asset_id])?;
235        Ok((asset, witness))
236    }
237
238    /// Retrieves the storage map witness for a specific map item.
239    ///
240    /// The proof is opened against the latest tree of the map's lineage, after verifying that
241    /// its root matches `expected_map_root` (the root recorded in the account tables).
242    pub fn get_storage_map_item_witness(
243        &self,
244        account_id: AccountId,
245        slot_name: &StorageSlotName,
246        expected_map_root: Word,
247        key: StorageMapKey,
248    ) -> Result<StorageMapWitness, StoreError> {
249        let lineage = storage_map_lineage_id(account_id, slot_name);
250        let tree = self.verified_latest_tree(lineage, expected_map_root)?;
251
252        let hashed_key = key.hash();
253        let proof = self.forest.open(tree, Word::from(hashed_key)).map_err(forest_error)?;
254        Ok(StorageMapWitness::new(proof, [key])?)
255    }
256}
257
258// MUTATIONS
259// ================================================================================================
260
261impl<B: Backend> AccountSmtForest<B> {
262    /// Applies a recorded update at the given version.
263    ///
264    /// Lineages unknown to the forest are created from the empty tree; known lineages are
265    /// updated from their latest tree. `new_version` must be strictly greater than the latest
266    /// version of every updated lineage. Resulting roots are read back with [`Self::vault_root`]
267    /// and [`Self::map_root`].
268    ///
269    /// Any root recorded on the update is verified against the computed mutations before they are
270    /// applied, so a mismatch is rejected without modifying the forest.
271    pub fn apply(
272        &mut self,
273        new_version: VersionId,
274        update: AccountUpdate,
275    ) -> Result<(), StoreError> {
276        let mut batch = SmtForestUpdateBatch::empty();
277        let mut expected_roots = Vec::new();
278
279        for (lineage, ops) in update.ops {
280            if let Some(expected_root) = ops.expect_root {
281                expected_roots.push((lineage, expected_root));
282            }
283
284            // Removals are staged as they are seen so a key removed and then re-inserted ends up
285            // inserted, and vice versa: the batch keeps the last operation per key.
286            let stored_keys = if ops.exhaustive {
287                self.lineage_entry_keys(lineage)?
288            } else {
289                Vec::new()
290            };
291            let batch_ops = batch.operations(lineage);
292            let mut target = BTreeMap::new();
293            for (key, value) in ops.pairs {
294                if value == EMPTY_WORD {
295                    target.remove(&key);
296                    batch_ops.add_remove(key);
297                } else {
298                    target.insert(key, value);
299                }
300            }
301            for key in stored_keys {
302                if !target.contains_key(&key) {
303                    batch_ops.add_remove(key);
304                }
305            }
306            for (key, value) in target {
307                batch_ops.add_insert(key, value);
308            }
309        }
310
311        let mutations =
312            self.forest.compute_forest_mutations(new_version, batch).map_err(forest_error)?;
313
314        for (lineage, expected_root) in expected_roots {
315            let actual_root = mutations
316                .roots()
317                .find(|root| root.lineage() == lineage)
318                .map(|root| root.root())
319                .expect("every expected lineage has a computed mutation");
320            if actual_root != expected_root {
321                return Err(StoreError::MerkleStoreError(MerkleError::ConflictingRoots {
322                    expected_root,
323                    actual_root,
324                }));
325            }
326        }
327
328        self.forest.apply_mutations(mutations).map_err(forest_error)?;
329
330        Ok(())
331    }
332}
333
334impl<B: BackendReader> AccountSmtForest<B> {
335    // HELPERS
336    // --------------------------------------------------------------------------------------------
337
338    /// Resolves the latest tree of a lineage and verifies its root against the expected value.
339    fn verified_latest_tree(
340        &self,
341        lineage: LineageId,
342        expected_root: Word,
343    ) -> Result<TreeId, StoreError> {
344        let version = self
345            .forest
346            .latest_version(lineage)
347            .ok_or_else(|| StoreError::DatabaseError(format!("unknown lineage {lineage}")))?;
348        let root = self.forest.latest_root(lineage).expect("lineage has a latest version");
349        if root != expected_root {
350            return Err(StoreError::MerkleStoreError(MerkleError::ConflictingRoots {
351                expected_root,
352                actual_root: root,
353            }));
354        }
355        Ok(TreeId::new(lineage, version))
356    }
357
358    /// Returns the SMT keys currently stored in a lineage, or an empty list if the forest does
359    /// not track it yet.
360    fn lineage_entry_keys(&self, lineage: LineageId) -> Result<Vec<Word>, StoreError> {
361        let Some(version) = self.forest.latest_version(lineage) else {
362            return Ok(Vec::new());
363        };
364
365        let entries = self.forest.entries(TreeId::new(lineage, version)).map_err(forest_error)?;
366        let mut keys = Vec::new();
367        for entry in entries {
368            keys.push(entry.map_err(forest_error)?.key);
369        }
370        Ok(keys)
371    }
372}
373
374// ERROR MAPPING
375// ================================================================================================
376
377/// Maps forest-level errors onto [`StoreError`].
378///
379/// Takes the error by value so it can be used directly with `map_err`.
380#[allow(clippy::needless_pass_by_value)]
381fn forest_error(err: LargeSmtForestError) -> StoreError {
382    StoreError::DatabaseError(format!("smt forest error: {err}"))
383}
384
385// TESTS
386// ================================================================================================
387
388#[cfg(test)]
389mod tests {
390    use miden_protocol::account::StorageMap;
391    use miden_protocol::asset::{AssetVault, FungibleAsset};
392    use miden_protocol::crypto::merkle::smt::ForestInMemoryBackend;
393    use miden_protocol::testing::account_id::{
394        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
395        ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
396    };
397
398    use super::*;
399
400    fn account_a() -> AccountId {
401        AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap()
402    }
403
404    fn account_b() -> AccountId {
405        AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET).unwrap()
406    }
407
408    fn slot(name: &str) -> StorageSlotName {
409        StorageSlotName::new(name).unwrap()
410    }
411
412    fn asset(amount: u64) -> Asset {
413        FungibleAsset::new(account_a(), amount).unwrap().into()
414    }
415
416    fn forest() -> AccountSmtForest<ForestInMemoryBackend> {
417        AccountSmtForest::new(ForestInMemoryBackend::new()).unwrap()
418    }
419
420    fn set_vault(forest: &mut AccountSmtForest<ForestInMemoryBackend>, version: u64, of: &[Asset]) {
421        let mut update = AccountUpdate::new();
422        update.full_state(account_a(), of.iter().copied(), core::iter::empty::<&StorageSlot>());
423        forest.apply(version, update).unwrap();
424    }
425
426    #[test]
427    fn accepts_read_only_backend() {
428        let backend = ForestInMemoryBackend::new();
429        let forest = AccountSmtForest::new(backend.reader().unwrap()).unwrap();
430
431        assert_eq!(forest.vault_root(account_a()), None);
432    }
433
434    /// Colliding lineages would silently serve one account's witnesses from another's tree, so
435    /// the derivation must separate accounts, slots, and the vault/map domains.
436    #[test]
437    fn lineage_ids_are_distinct() {
438        assert_ne!(vault_lineage_id(account_a()), vault_lineage_id(account_b()));
439        assert_ne!(
440            storage_map_lineage_id(account_a(), &slot("miden::test::map_one")),
441            storage_map_lineage_id(account_a(), &slot("miden::test::map_two")),
442        );
443        assert_ne!(
444            storage_map_lineage_id(account_a(), &slot("miden::test::map")),
445            storage_map_lineage_id(account_b(), &slot("miden::test::map")),
446        );
447        assert_ne!(
448            vault_lineage_id(account_a()),
449            storage_map_lineage_id(account_a(), &slot("miden::test::map")),
450        );
451    }
452
453    /// A full-state record is exhaustive: assets missing from it are dropped, not merged.
454    #[test]
455    fn full_state_replaces_previous_entries() {
456        let mut forest = forest();
457        let id = account_a();
458        let (old, new) = (asset(100), asset(250));
459
460        set_vault(&mut forest, 1, &[old]);
461        let (read, _) = forest
462            .get_asset_and_witness(id, forest.vault_root(id).unwrap(), old.id())
463            .unwrap();
464        assert_eq!(read, old);
465
466        set_vault(&mut forest, 2, &[new]);
467        let (read, _) = forest
468            .get_asset_and_witness(id, forest.vault_root(id).unwrap(), new.id())
469            .unwrap();
470        assert_eq!(read, new);
471
472        // Same faucet, so both assets share a vault key; the replacement is visible as the value.
473        assert_ne!(old.to_value_word(), new.to_value_word());
474    }
475
476    /// An empty full state clears the vault rather than leaving the old entries in place.
477    #[test]
478    fn full_state_can_empty_a_vault() {
479        let mut forest = forest();
480        let id = account_a();
481        let held = asset(100);
482
483        set_vault(&mut forest, 1, &[held]);
484        set_vault(&mut forest, 2, &[]);
485
486        let vault_root = forest.vault_root(id).unwrap();
487        assert_eq!(vault_root, StorageMap::default().root());
488        assert!(matches!(
489            forest.get_asset_and_witness(id, vault_root, held.id()),
490            Err(StoreError::VaultKeyNotTracked(..))
491        ));
492    }
493
494    /// Witness reads are the point at which forest/account divergence is caught.
495    #[test]
496    fn witness_reads_reject_mismatched_roots() {
497        let mut forest = forest();
498        let held = asset(100);
499        set_vault(&mut forest, 1, &[held]);
500
501        let result = forest.get_asset_and_witness(account_a(), EMPTY_WORD, held.id());
502        assert!(matches!(
503            result,
504            Err(StoreError::MerkleStoreError(MerkleError::ConflictingRoots { .. }))
505        ));
506    }
507
508    #[test]
509    fn rejected_update_does_not_advance_forest() {
510        let mut forest = forest();
511        let id = account_a();
512        let (old, new) = (asset(100), asset(250));
513        set_vault(&mut forest, 1, &[old]);
514
515        let old_root = forest.vault_root(id).unwrap();
516        let new_root = AssetVault::new(&[new]).unwrap().root();
517        assert_ne!(new_root, old_root);
518
519        let mut rejected = AccountUpdate::new();
520        rejected.vault_patch(id, &AccountVaultPatch::with_assets([new]), old_root);
521        assert!(matches!(
522            forest.apply(2, rejected),
523            Err(StoreError::MerkleStoreError(MerkleError::ConflictingRoots {
524                expected_root,
525                actual_root,
526            })) if expected_root == old_root && actual_root == new_root
527        ));
528        assert_eq!(forest.vault_root(id), Some(old_root));
529
530        let mut accepted = AccountUpdate::new();
531        accepted.vault_patch(id, &AccountVaultPatch::with_assets([new]), new_root);
532        forest.apply(2, accepted).unwrap();
533        assert_eq!(forest.vault_root(id), Some(new_root));
534    }
535}