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
33fn 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
44fn 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 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#[derive(Default)]
61struct LineageOps {
62 expect_root: Option<Word>,
64 exhaustive: bool,
67 pairs: Vec<(Word, Word)>,
70}
71
72#[derive(Default)]
77pub struct AccountUpdate {
78 ops: BTreeMap<LineageId, LineageOps>,
79}
80
81impl AccountUpdate {
82 pub fn new() -> Self {
84 Self::default()
85 }
86
87 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 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 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 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
166pub struct AccountSmtForest<B: BackendReader> {
183 forest: LargeSmtForest<B>,
184}
185
186impl<B: BackendReader> AccountSmtForest<B> {
187 pub fn new(backend: B) -> Result<Self, StoreError> {
189 Ok(Self {
190 forest: LargeSmtForest::new(backend).map_err(forest_error)?,
191 })
192 }
193
194 pub fn vault_root(&self, account_id: AccountId) -> Option<Word> {
200 self.forest.latest_root(vault_lineage_id(account_id))
201 }
202
203 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 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 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
258impl<B: Backend> AccountSmtForest<B> {
262 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 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 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 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#[allow(clippy::needless_pass_by_value)]
381fn forest_error(err: LargeSmtForestError) -> StoreError {
382 StoreError::DatabaseError(format!("smt forest error: {err}"))
383}
384
385#[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 #[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 #[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 assert_ne!(old.to_value_word(), new.to_value_word());
474 }
475
476 #[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 #[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}