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