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 vault asset witnesses for the given vault keys.
239    ///
240    /// Unlike [`Self::get_asset_and_witness`], keys absent from the vault are served too: their
241    /// witness is an emptiness proof, which the executor needs when an asset is being added to
242    /// the vault.
243    ///
244    /// The proofs are opened against the latest tree of the account's vault lineage, after
245    /// verifying that its root matches `expected_vault_root`.
246    pub fn open_vault_asset_witnesses(
247        &self,
248        account_id: AccountId,
249        expected_vault_root: Word,
250        asset_ids: impl IntoIterator<Item = AssetId>,
251    ) -> Result<Vec<AssetWitness>, StoreError> {
252        let lineage = vault_lineage_id(account_id);
253        let tree = self.verified_latest_tree(lineage, expected_vault_root)?;
254
255        asset_ids
256            .into_iter()
257            .map(|asset_id| {
258                let proof = self.forest.open(tree, asset_id.hash().into()).map_err(forest_error)?;
259                Ok(AssetWitness::new(proof, [asset_id])?)
260            })
261            .collect()
262    }
263
264    /// Retrieves the storage map witness for a specific map item.
265    ///
266    /// The proof is opened against the latest tree of the map's lineage, after verifying that
267    /// its root matches `expected_map_root` (the root recorded in the account tables).
268    pub fn get_storage_map_item_witness(
269        &self,
270        account_id: AccountId,
271        slot_name: &StorageSlotName,
272        expected_map_root: Word,
273        key: StorageMapKey,
274    ) -> Result<StorageMapWitness, StoreError> {
275        let lineage = storage_map_lineage_id(account_id, slot_name);
276        let tree = self.verified_latest_tree(lineage, expected_map_root)?;
277
278        let hashed_key = key.hash();
279        let proof = self.forest.open(tree, Word::from(hashed_key)).map_err(forest_error)?;
280        Ok(StorageMapWitness::new(proof, [key])?)
281    }
282}
283
284// MUTATIONS
285// ================================================================================================
286
287impl<B: Backend> AccountSmtForest<B> {
288    /// Applies a recorded update at the given version.
289    ///
290    /// Lineages unknown to the forest are created from the empty tree; known lineages are
291    /// updated from their latest tree. `new_version` must be strictly greater than the latest
292    /// version of every updated lineage. Resulting roots are read back with [`Self::vault_root`]
293    /// and [`Self::map_root`].
294    ///
295    /// Any root recorded on the update is verified against the computed mutations before they are
296    /// applied, so a mismatch is rejected without modifying the forest.
297    pub fn apply(
298        &mut self,
299        new_version: VersionId,
300        update: AccountUpdate,
301    ) -> Result<(), StoreError> {
302        let mut batch = SmtForestUpdateBatch::empty();
303        let mut expected_roots = Vec::new();
304
305        for (lineage, ops) in update.ops {
306            if let Some(expected_root) = ops.expect_root {
307                expected_roots.push((lineage, expected_root));
308            }
309
310            // Removals are staged as they are seen so a key removed and then re-inserted ends up
311            // inserted, and vice versa: the batch keeps the last operation per key.
312            let stored_keys = if ops.exhaustive {
313                self.lineage_entry_keys(lineage)?
314            } else {
315                Vec::new()
316            };
317            let batch_ops = batch.operations(lineage);
318            let mut target = BTreeMap::new();
319            for (key, value) in ops.pairs {
320                if value == EMPTY_WORD {
321                    target.remove(&key);
322                    batch_ops.add_remove(key);
323                } else {
324                    target.insert(key, value);
325                }
326            }
327            for key in stored_keys {
328                if !target.contains_key(&key) {
329                    batch_ops.add_remove(key);
330                }
331            }
332            for (key, value) in target {
333                batch_ops.add_insert(key, value);
334            }
335        }
336
337        let mutations =
338            self.forest.compute_forest_mutations(new_version, batch).map_err(forest_error)?;
339
340        for (lineage, expected_root) in expected_roots {
341            let actual_root = mutations
342                .roots()
343                .find(|root| root.lineage() == lineage)
344                .map(|root| root.root())
345                .expect("every expected lineage has a computed mutation");
346            if actual_root != expected_root {
347                return Err(StoreError::MerkleStoreError(MerkleError::ConflictingRoots {
348                    expected_root,
349                    actual_root,
350                }));
351            }
352        }
353
354        self.forest.apply_mutations(mutations).map_err(forest_error)?;
355
356        Ok(())
357    }
358}
359
360impl<B: BackendReader> AccountSmtForest<B> {
361    // HELPERS
362    // --------------------------------------------------------------------------------------------
363
364    /// Resolves the latest tree of a lineage and verifies its root against the expected value.
365    fn verified_latest_tree(
366        &self,
367        lineage: LineageId,
368        expected_root: Word,
369    ) -> Result<TreeId, StoreError> {
370        let version = self
371            .forest
372            .latest_version(lineage)
373            .ok_or_else(|| StoreError::DatabaseError(format!("unknown lineage {lineage}")))?;
374        let root = self.forest.latest_root(lineage).expect("lineage has a latest version");
375        if root != expected_root {
376            return Err(StoreError::MerkleStoreError(MerkleError::ConflictingRoots {
377                expected_root,
378                actual_root: root,
379            }));
380        }
381        Ok(TreeId::new(lineage, version))
382    }
383
384    /// Returns the SMT keys currently stored in a lineage, or an empty list if the forest does
385    /// not track it yet.
386    fn lineage_entry_keys(&self, lineage: LineageId) -> Result<Vec<Word>, StoreError> {
387        let Some(version) = self.forest.latest_version(lineage) else {
388            return Ok(Vec::new());
389        };
390
391        let entries = self.forest.entries(TreeId::new(lineage, version)).map_err(forest_error)?;
392        let mut keys = Vec::new();
393        for entry in entries {
394            keys.push(entry.map_err(forest_error)?.key);
395        }
396        Ok(keys)
397    }
398}
399
400// ERROR MAPPING
401// ================================================================================================
402
403/// Maps forest-level errors onto [`StoreError`].
404///
405/// Takes the error by value so it can be used directly with `map_err`.
406#[allow(clippy::needless_pass_by_value)]
407fn forest_error(err: LargeSmtForestError) -> StoreError {
408    StoreError::DatabaseError(format!("smt forest error: {err}"))
409}
410
411// TESTS
412// ================================================================================================
413
414#[cfg(test)]
415mod tests {
416    use miden_protocol::account::StorageMap;
417    use miden_protocol::asset::{AssetVault, FungibleAsset};
418    use miden_protocol::crypto::merkle::smt::ForestInMemoryBackend;
419    use miden_protocol::testing::account_id::{
420        ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
421        ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
422    };
423
424    use super::*;
425
426    fn account_a() -> AccountId {
427        AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap()
428    }
429
430    fn account_b() -> AccountId {
431        AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET).unwrap()
432    }
433
434    fn slot(name: &str) -> StorageSlotName {
435        StorageSlotName::new(name).unwrap()
436    }
437
438    fn asset(amount: u64) -> Asset {
439        FungibleAsset::new(account_a(), amount).unwrap().into()
440    }
441
442    fn forest() -> AccountSmtForest<ForestInMemoryBackend> {
443        AccountSmtForest::new(ForestInMemoryBackend::new()).unwrap()
444    }
445
446    fn set_vault(forest: &mut AccountSmtForest<ForestInMemoryBackend>, version: u64, of: &[Asset]) {
447        let mut update = AccountUpdate::new();
448        update.full_state(account_a(), of.iter().copied(), core::iter::empty::<&StorageSlot>());
449        forest.apply(version, update).unwrap();
450    }
451
452    #[test]
453    fn accepts_read_only_backend() {
454        let backend = ForestInMemoryBackend::new();
455        let forest = AccountSmtForest::new(backend.reader().unwrap()).unwrap();
456
457        assert_eq!(forest.vault_root(account_a()), None);
458    }
459
460    /// Colliding lineages would silently serve one account's witnesses from another's tree, so
461    /// the derivation must separate accounts, slots, and the vault/map domains.
462    #[test]
463    fn lineage_ids_are_distinct() {
464        assert_ne!(vault_lineage_id(account_a()), vault_lineage_id(account_b()));
465        assert_ne!(
466            storage_map_lineage_id(account_a(), &slot("miden::test::map_one")),
467            storage_map_lineage_id(account_a(), &slot("miden::test::map_two")),
468        );
469        assert_ne!(
470            storage_map_lineage_id(account_a(), &slot("miden::test::map")),
471            storage_map_lineage_id(account_b(), &slot("miden::test::map")),
472        );
473        assert_ne!(
474            vault_lineage_id(account_a()),
475            storage_map_lineage_id(account_a(), &slot("miden::test::map")),
476        );
477    }
478
479    /// A full-state record is exhaustive: assets missing from it are dropped, not merged.
480    #[test]
481    fn full_state_replaces_previous_entries() {
482        let mut forest = forest();
483        let id = account_a();
484        let (old, new) = (asset(100), asset(250));
485
486        set_vault(&mut forest, 1, &[old]);
487        let (read, _) = forest
488            .get_asset_and_witness(id, forest.vault_root(id).unwrap(), old.id())
489            .unwrap();
490        assert_eq!(read, old);
491
492        set_vault(&mut forest, 2, &[new]);
493        let (read, _) = forest
494            .get_asset_and_witness(id, forest.vault_root(id).unwrap(), new.id())
495            .unwrap();
496        assert_eq!(read, new);
497
498        // Same faucet, so both assets share a vault key; the replacement is visible as the value.
499        assert_ne!(old.to_value_word(), new.to_value_word());
500    }
501
502    /// An empty full state clears the vault rather than leaving the old entries in place.
503    #[test]
504    fn full_state_can_empty_a_vault() {
505        let mut forest = forest();
506        let id = account_a();
507        let held = asset(100);
508
509        set_vault(&mut forest, 1, &[held]);
510        set_vault(&mut forest, 2, &[]);
511
512        let vault_root = forest.vault_root(id).unwrap();
513        assert_eq!(vault_root, StorageMap::default().root());
514        assert!(matches!(
515            forest.get_asset_and_witness(id, vault_root, held.id()),
516            Err(StoreError::VaultKeyNotTracked(..))
517        ));
518    }
519
520    /// Witness reads are the point at which forest/account divergence is caught.
521    #[test]
522    fn witness_reads_reject_mismatched_roots() {
523        let mut forest = forest();
524        let held = asset(100);
525        set_vault(&mut forest, 1, &[held]);
526
527        let result = forest.get_asset_and_witness(account_a(), EMPTY_WORD, held.id());
528        assert!(matches!(
529            result,
530            Err(StoreError::MerkleStoreError(MerkleError::ConflictingRoots { .. }))
531        ));
532    }
533
534    #[test]
535    fn rejected_update_does_not_advance_forest() {
536        let mut forest = forest();
537        let id = account_a();
538        let (old, new) = (asset(100), asset(250));
539        set_vault(&mut forest, 1, &[old]);
540
541        let old_root = forest.vault_root(id).unwrap();
542        let new_root = AssetVault::new(&[new]).unwrap().root();
543        assert_ne!(new_root, old_root);
544
545        let mut rejected = AccountUpdate::new();
546        rejected.vault_patch(id, &AccountVaultPatch::with_assets([new]), old_root);
547        assert!(matches!(
548            forest.apply(2, rejected),
549            Err(StoreError::MerkleStoreError(MerkleError::ConflictingRoots {
550                expected_root,
551                actual_root,
552            })) if expected_root == old_root && actual_root == new_root
553        ));
554        assert_eq!(forest.vault_root(id), Some(old_root));
555
556        let mut accepted = AccountUpdate::new();
557        accepted.vault_patch(id, &AccountVaultPatch::with_assets([new]), new_root);
558        forest.apply(2, accepted).unwrap();
559        assert_eq!(forest.vault_root(id), Some(new_root));
560    }
561}