use alloc::collections::BTreeMap;
use alloc::format;
use alloc::vec::Vec;
use miden_protocol::account::{
AccountId,
AccountStoragePatch,
AccountVaultPatch,
StorageMapKey,
StorageMapPatch,
StorageMapWitness,
StorageSlot,
StorageSlotContent,
StorageSlotName,
};
use miden_protocol::asset::{Asset, AssetId, AssetWitness};
use miden_protocol::crypto::merkle::MerkleError;
use miden_protocol::crypto::merkle::smt::{
Backend,
BackendReader,
LargeSmtForest,
LargeSmtForestError,
LineageId,
SmtForestUpdateBatch,
TreeId,
VersionId,
};
use miden_protocol::utils::serde::Serializable;
use miden_protocol::{EMPTY_WORD, Hasher, Word};
use super::StoreError;
fn vault_lineage_id(account_id: AccountId) -> LineageId {
let mut bytes = Vec::new();
bytes.extend_from_slice(b"miden-client:vault");
bytes.extend_from_slice(&account_id.to_bytes());
LineageId::new(Hasher::hash(&bytes).as_bytes())
}
fn storage_map_lineage_id(account_id: AccountId, slot_name: &StorageSlotName) -> LineageId {
let mut bytes = Vec::new();
bytes.extend_from_slice(b"miden-client:storage-map");
bytes.extend_from_slice(&account_id.to_bytes());
bytes.extend_from_slice(&(slot_name.as_str().len() as u64).to_le_bytes());
bytes.extend_from_slice(slot_name.as_str().as_bytes());
LineageId::new(Hasher::hash(&bytes).as_bytes())
}
#[derive(Default)]
struct LineageOps {
expect_root: Option<Word>,
exhaustive: bool,
pairs: Vec<(Word, Word)>,
}
#[derive(Default)]
pub struct AccountUpdate {
ops: BTreeMap<LineageId, LineageOps>,
}
impl AccountUpdate {
pub fn new() -> Self {
Self::default()
}
pub fn vault_patch(
&mut self,
account_id: AccountId,
patch: &AccountVaultPatch,
expected_root: Word,
) {
let vault = self.entry(vault_lineage_id(account_id));
vault.expect_root = Some(expected_root);
vault
.pairs
.extend(patch.updated_assets().map(|a| (a.id().hash().into(), a.to_value_word())));
vault
.pairs
.extend(patch.removed_asset_ids().map(|id| (id.hash().into(), EMPTY_WORD)));
}
pub fn storage_patch(&mut self, account_id: AccountId, patch: &AccountStoragePatch) {
for (slot_name, map_patch) in patch.maps() {
let ops = self.entry(storage_map_lineage_id(account_id, slot_name));
ops.pairs.extend(
map_patch
.entries()
.into_iter()
.flat_map(|e| e.as_map().iter())
.map(|(key, value)| (Word::from(key.hash()), *value)),
);
if matches!(map_patch, StorageMapPatch::Create { .. } | StorageMapPatch::Remove) {
ops.exhaustive = true;
}
}
}
pub fn full_state<'a>(
&mut self,
account_id: AccountId,
assets: impl Iterator<Item = Asset>,
slots: impl Iterator<Item = &'a StorageSlot>,
) {
let vault = self.entry(vault_lineage_id(account_id));
vault.exhaustive = true;
vault.pairs.extend(assets.map(|a| (a.id().hash().into(), a.to_value_word())));
for slot in slots {
if let StorageSlotContent::Map(map) = slot.content() {
let ops = self.entry(storage_map_lineage_id(account_id, slot.name()));
ops.exhaustive = true;
ops.pairs
.extend(map.entries().map(|(key, value)| (Word::from(key.hash()), *value)));
}
}
}
pub fn clear_map(&mut self, account_id: AccountId, slot_name: &StorageSlotName) {
self.entry(storage_map_lineage_id(account_id, slot_name)).exhaustive = true;
}
fn entry(&mut self, lineage: LineageId) -> &mut LineageOps {
self.ops.entry(lineage).or_default()
}
}
pub struct AccountSmtForest<B: BackendReader> {
forest: LargeSmtForest<B>,
}
impl<B: BackendReader> AccountSmtForest<B> {
pub fn new(backend: B) -> Result<Self, StoreError> {
Ok(Self {
forest: LargeSmtForest::new(backend).map_err(forest_error)?,
})
}
pub fn vault_root(&self, account_id: AccountId) -> Option<Word> {
self.forest.latest_root(vault_lineage_id(account_id))
}
pub fn map_root(&self, account_id: AccountId, slot_name: &StorageSlotName) -> Option<Word> {
self.forest.latest_root(storage_map_lineage_id(account_id, slot_name))
}
pub fn get_asset_and_witness(
&self,
account_id: AccountId,
expected_vault_root: Word,
asset_id: AssetId,
) -> Result<(Asset, AssetWitness), StoreError> {
let lineage = vault_lineage_id(account_id);
let tree = self.verified_latest_tree(lineage, expected_vault_root)?;
let hashed_key: Word = asset_id.hash().into();
let proof = self.forest.open(tree, hashed_key).map_err(forest_error)?;
let asset_word = proof
.get(&hashed_key)
.ok_or(StoreError::VaultKeyNotTracked(asset_id, hashed_key))?;
if asset_word == EMPTY_WORD {
return Err(StoreError::VaultKeyNotTracked(asset_id, hashed_key));
}
let asset = Asset::from_id_and_value(asset_id, asset_word)?;
let witness = AssetWitness::new(proof, [asset_id])?;
Ok((asset, witness))
}
pub fn get_storage_map_item_witness(
&self,
account_id: AccountId,
slot_name: &StorageSlotName,
expected_map_root: Word,
key: StorageMapKey,
) -> Result<StorageMapWitness, StoreError> {
let lineage = storage_map_lineage_id(account_id, slot_name);
let tree = self.verified_latest_tree(lineage, expected_map_root)?;
let hashed_key = key.hash();
let proof = self.forest.open(tree, Word::from(hashed_key)).map_err(forest_error)?;
Ok(StorageMapWitness::new(proof, [key])?)
}
}
impl<B: Backend> AccountSmtForest<B> {
pub fn apply(
&mut self,
new_version: VersionId,
update: AccountUpdate,
) -> Result<(), StoreError> {
let mut batch = SmtForestUpdateBatch::empty();
let mut expected_roots = Vec::new();
for (lineage, ops) in update.ops {
if let Some(expected_root) = ops.expect_root {
expected_roots.push((lineage, expected_root));
}
let stored_keys = if ops.exhaustive {
self.lineage_entry_keys(lineage)?
} else {
Vec::new()
};
let batch_ops = batch.operations(lineage);
let mut target = BTreeMap::new();
for (key, value) in ops.pairs {
if value == EMPTY_WORD {
target.remove(&key);
batch_ops.add_remove(key);
} else {
target.insert(key, value);
}
}
for key in stored_keys {
if !target.contains_key(&key) {
batch_ops.add_remove(key);
}
}
for (key, value) in target {
batch_ops.add_insert(key, value);
}
}
let mutations =
self.forest.compute_forest_mutations(new_version, batch).map_err(forest_error)?;
for (lineage, expected_root) in expected_roots {
let actual_root = mutations
.roots()
.find(|root| root.lineage() == lineage)
.map(|root| root.root())
.expect("every expected lineage has a computed mutation");
if actual_root != expected_root {
return Err(StoreError::MerkleStoreError(MerkleError::ConflictingRoots {
expected_root,
actual_root,
}));
}
}
self.forest.apply_mutations(mutations).map_err(forest_error)?;
Ok(())
}
}
impl<B: BackendReader> AccountSmtForest<B> {
fn verified_latest_tree(
&self,
lineage: LineageId,
expected_root: Word,
) -> Result<TreeId, StoreError> {
let version = self
.forest
.latest_version(lineage)
.ok_or_else(|| StoreError::DatabaseError(format!("unknown lineage {lineage}")))?;
let root = self.forest.latest_root(lineage).expect("lineage has a latest version");
if root != expected_root {
return Err(StoreError::MerkleStoreError(MerkleError::ConflictingRoots {
expected_root,
actual_root: root,
}));
}
Ok(TreeId::new(lineage, version))
}
fn lineage_entry_keys(&self, lineage: LineageId) -> Result<Vec<Word>, StoreError> {
let Some(version) = self.forest.latest_version(lineage) else {
return Ok(Vec::new());
};
let entries = self.forest.entries(TreeId::new(lineage, version)).map_err(forest_error)?;
let mut keys = Vec::new();
for entry in entries {
keys.push(entry.map_err(forest_error)?.key);
}
Ok(keys)
}
}
#[allow(clippy::needless_pass_by_value)]
fn forest_error(err: LargeSmtForestError) -> StoreError {
StoreError::DatabaseError(format!("smt forest error: {err}"))
}
#[cfg(test)]
mod tests {
use miden_protocol::account::StorageMap;
use miden_protocol::asset::{AssetVault, FungibleAsset};
use miden_protocol::crypto::merkle::smt::ForestInMemoryBackend;
use miden_protocol::testing::account_id::{
ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET,
ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET,
};
use super::*;
fn account_a() -> AccountId {
AccountId::try_from(ACCOUNT_ID_PUBLIC_FUNGIBLE_FAUCET).unwrap()
}
fn account_b() -> AccountId {
AccountId::try_from(ACCOUNT_ID_PUBLIC_NON_FUNGIBLE_FAUCET).unwrap()
}
fn slot(name: &str) -> StorageSlotName {
StorageSlotName::new(name).unwrap()
}
fn asset(amount: u64) -> Asset {
FungibleAsset::new(account_a(), amount).unwrap().into()
}
fn forest() -> AccountSmtForest<ForestInMemoryBackend> {
AccountSmtForest::new(ForestInMemoryBackend::new()).unwrap()
}
fn set_vault(forest: &mut AccountSmtForest<ForestInMemoryBackend>, version: u64, of: &[Asset]) {
let mut update = AccountUpdate::new();
update.full_state(account_a(), of.iter().copied(), core::iter::empty::<&StorageSlot>());
forest.apply(version, update).unwrap();
}
#[test]
fn accepts_read_only_backend() {
let backend = ForestInMemoryBackend::new();
let forest = AccountSmtForest::new(backend.reader().unwrap()).unwrap();
assert_eq!(forest.vault_root(account_a()), None);
}
#[test]
fn lineage_ids_are_distinct() {
assert_ne!(vault_lineage_id(account_a()), vault_lineage_id(account_b()));
assert_ne!(
storage_map_lineage_id(account_a(), &slot("miden::test::map_one")),
storage_map_lineage_id(account_a(), &slot("miden::test::map_two")),
);
assert_ne!(
storage_map_lineage_id(account_a(), &slot("miden::test::map")),
storage_map_lineage_id(account_b(), &slot("miden::test::map")),
);
assert_ne!(
vault_lineage_id(account_a()),
storage_map_lineage_id(account_a(), &slot("miden::test::map")),
);
}
#[test]
fn full_state_replaces_previous_entries() {
let mut forest = forest();
let id = account_a();
let (old, new) = (asset(100), asset(250));
set_vault(&mut forest, 1, &[old]);
let (read, _) = forest
.get_asset_and_witness(id, forest.vault_root(id).unwrap(), old.id())
.unwrap();
assert_eq!(read, old);
set_vault(&mut forest, 2, &[new]);
let (read, _) = forest
.get_asset_and_witness(id, forest.vault_root(id).unwrap(), new.id())
.unwrap();
assert_eq!(read, new);
assert_ne!(old.to_value_word(), new.to_value_word());
}
#[test]
fn full_state_can_empty_a_vault() {
let mut forest = forest();
let id = account_a();
let held = asset(100);
set_vault(&mut forest, 1, &[held]);
set_vault(&mut forest, 2, &[]);
let vault_root = forest.vault_root(id).unwrap();
assert_eq!(vault_root, StorageMap::default().root());
assert!(matches!(
forest.get_asset_and_witness(id, vault_root, held.id()),
Err(StoreError::VaultKeyNotTracked(..))
));
}
#[test]
fn witness_reads_reject_mismatched_roots() {
let mut forest = forest();
let held = asset(100);
set_vault(&mut forest, 1, &[held]);
let result = forest.get_asset_and_witness(account_a(), EMPTY_WORD, held.id());
assert!(matches!(
result,
Err(StoreError::MerkleStoreError(MerkleError::ConflictingRoots { .. }))
));
}
#[test]
fn rejected_update_does_not_advance_forest() {
let mut forest = forest();
let id = account_a();
let (old, new) = (asset(100), asset(250));
set_vault(&mut forest, 1, &[old]);
let old_root = forest.vault_root(id).unwrap();
let new_root = AssetVault::new(&[new]).unwrap().root();
assert_ne!(new_root, old_root);
let mut rejected = AccountUpdate::new();
rejected.vault_patch(id, &AccountVaultPatch::with_assets([new]), old_root);
assert!(matches!(
forest.apply(2, rejected),
Err(StoreError::MerkleStoreError(MerkleError::ConflictingRoots {
expected_root,
actual_root,
})) if expected_root == old_root && actual_root == new_root
));
assert_eq!(forest.vault_root(id), Some(old_root));
let mut accepted = AccountUpdate::new();
accepted.vault_patch(id, &AccountVaultPatch::with_assets([new]), new_root);
forest.apply(2, accepted).unwrap();
assert_eq!(forest.vault_root(id), Some(new_root));
}
}