use std::collections::BTreeMap;
use std::num::NonZeroUsize;
use miden_crypto::hash::rpo::Rpo256;
#[cfg(feature = "rocksdb")]
use miden_crypto::merkle::smt::ForestPersistentBackend;
use miden_crypto::merkle::smt::{Backend, BackendReader, ForestInMemoryBackend};
use miden_node_proto::domain::account::{
AccountStorageMapDetails,
AccountVaultDetails,
StorageMapEntries,
};
use miden_node_utils::ErrorReport;
use miden_node_utils::lru_cache::LruCache;
use miden_node_utils::tracing::miden_instrument;
use miden_protocol::account::{
AccountId,
AccountPatch,
StorageMapKey,
StorageMapKeyHash,
StoragePatchOperation,
StorageSlotName,
};
use miden_protocol::asset::{Asset, AssetId, AssetIdHash};
use miden_protocol::block::BlockNumber;
use miden_protocol::crypto::merkle::smt::{
LargeSmtForest,
LargeSmtForestError,
LineageId,
RootInfo,
SMT_DEPTH,
SmtForestMutationSet,
SmtForestOperation,
SmtForestUpdateBatch,
SmtUpdateBatch,
TreeId,
};
use miden_protocol::crypto::merkle::{EmptySubtreeRoots, MerkleError};
use miden_protocol::errors::{AssetError, StorageMapError};
use miden_protocol::utils::serde::Serializable;
use miden_protocol::{EMPTY_WORD, Word};
use thiserror::Error;
use crate::COMPONENT;
pub use crate::db::models::queries::HISTORICAL_BLOCK_RETENTION;
use crate::db::models::queries::{PrecomputedPublicAccountState, PrecomputedPublicAccountStates};
use crate::errors::AccountStateForestUpdateError;
#[cfg(test)]
mod tests;
const HASHED_STORAGE_MAP_KEY_CACHE_CAPACITY: usize = 65_536;
const HASHED_VAULT_KEY_CACHE_CAPACITY: usize = 65_536;
#[derive(Debug, Error)]
pub enum WitnessError {
#[error("root not found")]
RootNotFound,
#[error("merkle error")]
MerkleError(#[from] MerkleError),
#[error("storage map error")]
StorageMapError(#[from] StorageMapError),
#[error("failed to construct asset")]
AssetError(#[from] AssetError),
}
#[cfg(feature = "rocksdb")]
pub(crate) type AccountStateForestBackend = ForestPersistentBackend;
#[cfg(not(feature = "rocksdb"))]
pub(crate) type AccountStateForestBackend = ForestInMemoryBackend;
pub(crate) type AccountStateForestBackendReader = <AccountStateForestBackend as Backend>::Reader;
const fn empty_smt_root() -> Word {
*EmptySubtreeRoots::entry(SMT_DEPTH, 0)
}
#[derive(Debug, PartialEq)]
pub enum AccountStorageMapResult {
NotFound,
CannotReconstructKeysFromCache,
Details(AccountStorageMapDetails),
}
pub(crate) struct AccountStateForest<B: BackendReader = ForestInMemoryBackend> {
forest: LargeSmtForest<B>,
storage_map_key_cache: LruCache<StorageMapKeyHash, StorageMapKey>,
pub(crate) vault_key_cache: LruCache<AssetIdHash, AssetId>,
}
pub(crate) struct PreparedAccountStateForestBlockUpdate<B: Backend = ForestInMemoryBackend> {
pub(crate) account_states: PrecomputedPublicAccountStates,
mutations: SmtForestMutationSet<B>,
account_patches: Vec<AccountPatch>,
}
#[derive(Default)]
struct AccountUpdateForestLineages {
vault: BTreeMap<AccountId, LineageId>,
storage: BTreeMap<AccountId, BTreeMap<StorageSlotName, LineageId>>,
}
#[cfg(test)]
impl AccountStateForest<ForestInMemoryBackend> {
pub(crate) fn new() -> Self {
Self {
forest: Self::create_forest(),
storage_map_key_cache: LruCache::new(
NonZeroUsize::new(HASHED_STORAGE_MAP_KEY_CACHE_CAPACITY)
.expect("storage map key cache capacity must be non-zero"),
),
vault_key_cache: LruCache::new(
NonZeroUsize::new(HASHED_VAULT_KEY_CACHE_CAPACITY)
.expect("vault key cache capacity must be non-zero"),
),
}
}
pub(crate) const fn empty_smt_root() -> Word {
empty_smt_root()
}
fn create_forest() -> LargeSmtForest<ForestInMemoryBackend> {
let backend = ForestInMemoryBackend::new();
LargeSmtForest::new(backend).expect("in-memory backend should initialize")
}
}
impl<B: BackendReader> AccountStateForest<B> {
pub(crate) fn from_backend(backend: B) -> Result<Self, LargeSmtForestError> {
Ok(Self {
forest: LargeSmtForest::new(backend)?,
storage_map_key_cache: LruCache::new(
NonZeroUsize::new(HASHED_STORAGE_MAP_KEY_CACHE_CAPACITY)
.expect("storage map key cache capacity must be non-zero"),
),
vault_key_cache: LruCache::new(
NonZeroUsize::new(HASHED_VAULT_KEY_CACHE_CAPACITY)
.expect("vault key cache capacity must be non-zero"),
),
})
}
#[cfg(feature = "rocksdb")]
pub(crate) fn lineage_count(&self) -> usize {
self.forest.lineage_count()
}
#[cfg(test)]
fn tree_id_for_root(
&self,
account_id: AccountId,
slot_name: &StorageSlotName,
block_num: BlockNumber,
) -> TreeId {
let lineage = Self::storage_lineage_id(account_id, slot_name);
self.lookup_tree_id(lineage, block_num)
}
#[cfg(test)]
fn tree_id_for_vault_root(&self, account_id: AccountId, block_num: BlockNumber) -> TreeId {
let lineage = Self::vault_lineage_id(account_id);
self.lookup_tree_id(lineage, block_num)
}
#[expect(clippy::unused_self)]
fn lookup_tree_id(&self, lineage: LineageId, block_num: BlockNumber) -> TreeId {
TreeId::new(lineage, block_num.as_u64())
}
fn storage_lineage_id(account_id: AccountId, slot_name: &StorageSlotName) -> LineageId {
let mut bytes = Vec::new();
bytes.extend_from_slice(&account_id.to_bytes());
bytes.extend_from_slice(slot_name.as_str().as_bytes());
LineageId::new(Rpo256::hash(&bytes).as_bytes())
}
fn vault_lineage_id(account_id: AccountId) -> LineageId {
LineageId::new(Rpo256::hash(&account_id.to_bytes()).as_bytes())
}
fn build_forest_operations(
entries: impl IntoIterator<Item = (Word, Word)>,
) -> Vec<SmtForestOperation> {
entries
.into_iter()
.map(|(key, value)| {
if value == EMPTY_WORD {
SmtForestOperation::remove(key)
} else {
SmtForestOperation::insert(key, value)
}
})
.collect()
}
fn update_batch_from_operations(operations: Vec<SmtForestOperation>) -> SmtUpdateBatch {
if operations.is_empty() {
SmtUpdateBatch::empty()
} else {
SmtUpdateBatch::new(operations.into_iter())
}
}
fn build_current_tree_removal_operations(
&self,
lineage: LineageId,
) -> Result<Vec<SmtForestOperation>, LargeSmtForestError> {
let Some(version) = self.forest.latest_version(lineage) else {
return Ok(Vec::new());
};
let tree = TreeId::new(lineage, version);
self.forest
.entries(tree)?
.map(|entry| entry.map(|entry| SmtForestOperation::remove(entry.key)))
.collect()
}
fn add_vault_updates(
&self,
batch: &mut SmtForestUpdateBatch,
lineages: &mut AccountUpdateForestLineages,
patch: &AccountPatch,
) -> Result<(), AccountStateForestUpdateError> {
let account_id = patch.id();
if !patch.is_full_state() && patch.vault().is_empty() {
return Ok(());
}
let lineage = Self::vault_lineage_id(account_id);
if patch.is_full_state() && self.forest.latest_version(lineage).is_some() {
return Err(AccountStateForestUpdateError::VaultLineageAlreadyExists { account_id });
}
let operations = Self::build_forest_operations(
patch.vault().iter().map(|(key, value)| (key.hash().as_word(), *value)),
);
let updates = Self::update_batch_from_operations(operations);
batch.operations(lineage).add_operations(updates.into_iter());
lineages.vault.insert(account_id, lineage);
Ok(())
}
fn add_full_state_storage_updates(
&self,
batch: &mut SmtForestUpdateBatch,
lineages: &mut AccountUpdateForestLineages,
patch: &AccountPatch,
) -> Result<(), AccountStateForestUpdateError> {
let account_id = patch.id();
for (slot_name, map_patch) in patch.storage().maps() {
let raw_map_entries = Vec::from_iter(
map_patch
.entries()
.into_iter()
.flat_map(|entries| entries.as_map().iter())
.filter_map(
|(&key, &value)| {
if value == EMPTY_WORD { None } else { Some((key, value)) }
},
),
);
let operations = Self::build_forest_operations(
raw_map_entries.iter().map(|(raw_key, value)| (raw_key.hash().into(), *value)),
);
let lineage = Self::storage_lineage_id(account_id, slot_name);
if self.forest.latest_version(lineage).is_some() {
return Err(AccountStateForestUpdateError::StorageLineageAlreadyExists {
account_id,
slot_name: slot_name.clone(),
});
}
let updates = Self::update_batch_from_operations(operations);
batch.operations(lineage).add_operations(updates.into_iter());
lineages
.storage
.entry(account_id)
.or_default()
.insert(slot_name.clone(), lineage);
}
Ok(())
}
fn add_partial_storage_updates(
&self,
batch: &mut SmtForestUpdateBatch,
lineages: &mut AccountUpdateForestLineages,
patch: &AccountPatch,
) -> Result<(), LargeSmtForestError> {
if patch.storage().is_empty() {
return Ok(());
}
let account_id = patch.id();
for (slot_name, map_patch) in patch.storage().maps() {
let lineage = Self::storage_lineage_id(account_id, slot_name);
let Some(entries) = map_patch.entries() else {
continue;
};
if entries.is_empty() && map_patch.patch_op() != StoragePatchOperation::Create {
continue;
}
let mut operations = if map_patch.patch_op() == StoragePatchOperation::Create {
self.build_current_tree_removal_operations(lineage)?
} else {
Vec::new()
};
operations.extend(Self::build_forest_operations(
entries.as_map().iter().map(|(key, value)| (key.hash().into(), *value)),
));
if operations.is_empty() && self.forest.latest_version(lineage).is_none() {
if map_patch.patch_op() != StoragePatchOperation::Create {
continue;
}
}
let updates = Self::update_batch_from_operations(operations);
batch.operations(lineage).add_operations(updates.into_iter());
lineages
.storage
.entry(account_id)
.or_default()
.insert(slot_name.clone(), lineage);
}
Ok(())
}
fn prepare_block_update_batch(
&self,
account_patches: &[AccountPatch],
) -> Result<(SmtForestUpdateBatch, AccountUpdateForestLineages), AccountStateForestUpdateError>
{
let mut batch = SmtForestUpdateBatch::empty();
let mut lineages = AccountUpdateForestLineages::default();
for patch in account_patches {
self.add_vault_updates(&mut batch, &mut lineages, patch)?;
if patch.is_full_state() {
self.add_full_state_storage_updates(&mut batch, &mut lineages, patch)?;
} else {
self.add_partial_storage_updates(&mut batch, &mut lineages, patch)?;
}
}
Ok((batch, lineages))
}
fn precomputed_account_states_from_mutations<B2: Backend>(
&self,
account_patches: &[AccountPatch],
lineages: &AccountUpdateForestLineages,
mutations: &SmtForestMutationSet<B2>,
) -> Result<PrecomputedPublicAccountStates, AccountStateForestUpdateError> {
let mut account_states = PrecomputedPublicAccountStates::new();
let roots = mutations
.roots()
.map(|root| (root.lineage(), root.root()))
.collect::<BTreeMap<_, _>>();
for patch in account_patches {
let account_id = patch.id();
let vault_root = match lineages.vault.get(&account_id) {
Some(lineage) => roots.get(lineage).copied().ok_or(
AccountStateForestUpdateError::MissingComputedRoot { lineage: *lineage },
)?,
None => self.get_latest_vault_root(account_id),
};
let mut storage_map_roots = BTreeMap::new();
if let Some(account_lineages) = lineages.storage.get(&account_id) {
for (slot_name, lineage) in account_lineages {
let root = roots.get(lineage).copied().ok_or(
AccountStateForestUpdateError::MissingComputedRoot { lineage: *lineage },
)?;
storage_map_roots.insert(slot_name.clone(), root);
}
}
account_states.insert(
account_id,
PrecomputedPublicAccountState { vault_root, storage_map_roots },
);
}
Ok(account_states)
}
fn cache_hashed_keys_from_patch(&mut self, patch: &AccountPatch) {
let raw_keys = patch.storage().maps().flat_map(|(_slot_name, map_patch)| {
map_patch.entries().into_iter().flat_map(|e| e.as_map().keys().copied())
});
self.cache_storage_map_keys(raw_keys);
let raw_keys = patch.vault().iter().map(|(vault_key, _)| *vault_key);
self.vault_key_cache
.put_many(raw_keys.into_iter().map(|raw_key| (raw_key.hash(), raw_key)));
}
pub(crate) fn cache_storage_map_keys(&self, raw_keys: impl IntoIterator<Item = StorageMapKey>) {
self.storage_map_key_cache
.put_many(raw_keys.into_iter().map(|raw_key| (raw_key.hash(), raw_key)));
}
#[cfg(test)]
fn clear_storage_map_key_cache(&self) {
self.storage_map_key_cache.clear();
}
fn map_forest_error(error: LargeSmtForestError) -> MerkleError {
match error {
LargeSmtForestError::Merkle(merkle) => merkle,
other => MerkleError::InternalError(other.as_report()),
}
}
fn map_forest_error_to_witness(error: LargeSmtForestError) -> WitnessError {
match error {
LargeSmtForestError::Merkle(merkle) => WitnessError::MerkleError(merkle),
other => WitnessError::MerkleError(MerkleError::InternalError(other.as_report())),
}
}
fn get_tree_id(&self, lineage: LineageId, block_num: BlockNumber) -> Option<TreeId> {
let tree = self.lookup_tree_id(lineage, block_num);
match self.forest.root_info(tree) {
RootInfo::LatestVersion(_) | RootInfo::HistoricalVersion(_) => Some(tree),
RootInfo::Missing => {
let latest_version = self.forest.latest_version(lineage)?;
if latest_version <= block_num.as_u64() {
Some(TreeId::new(lineage, latest_version))
} else {
None
}
},
}
}
#[cfg(test)]
fn get_tree_root(&self, lineage: LineageId, block_num: BlockNumber) -> Option<Word> {
let tree = self.get_tree_id(lineage, block_num)?;
match self.forest.root_info(tree) {
RootInfo::LatestVersion(root) | RootInfo::HistoricalVersion(root) => Some(root),
RootInfo::Missing => None,
}
}
#[cfg(test)]
pub(crate) fn get_vault_root(
&self,
account_id: AccountId,
block_num: BlockNumber,
) -> Option<Word> {
let lineage = Self::vault_lineage_id(account_id);
self.get_tree_root(lineage, block_num)
}
#[cfg(test)]
pub(crate) fn get_storage_map_root(
&self,
account_id: AccountId,
slot_name: &StorageSlotName,
block_num: BlockNumber,
) -> Option<Word> {
let lineage = Self::storage_lineage_id(account_id, slot_name);
self.get_tree_root(lineage, block_num)
}
#[miden_instrument(
target = COMPONENT,
)]
pub(crate) fn get_vault_details(
&self,
account_id: AccountId,
block_num: BlockNumber,
) -> Result<Option<AccountVaultDetails>, WitnessError> {
let lineage = Self::vault_lineage_id(account_id);
let tree = self.get_tree_id(lineage, block_num).ok_or(WitnessError::RootNotFound)?;
let num_entries =
self.forest.entry_count(tree).map_err(Self::map_forest_error_to_witness)?;
if num_entries > AccountVaultDetails::MAX_RETURN_ENTRIES {
return Ok(Some(AccountVaultDetails::LimitExceeded));
}
let entries = self.forest.entries(tree).map_err(Self::map_forest_error_to_witness)?;
let hashed_entries = entries
.map(|entry| {
let entry = entry.map_err(Self::map_forest_error_to_witness)?;
Ok((AssetIdHash::from_raw(entry.key), entry.value))
})
.collect::<Result<Vec<_>, WitnessError>>()?;
let raw_keys = self
.vault_key_cache
.get_many(hashed_entries.iter().map(|(hashed_key, _)| hashed_key));
if raw_keys.iter().any(Option::is_none) {
return Ok(None);
}
let assets = raw_keys
.into_iter()
.flatten()
.zip(hashed_entries)
.map(|(raw_key, (_hashed_key, value))| {
Asset::from_id_and_value(raw_key, value).map_err(WitnessError::from)
})
.collect::<Result<Vec<_>, _>>()?;
Ok(Some(AccountVaultDetails::from_assets(assets)))
}
#[miden_instrument(
target = COMPONENT,
)]
pub(crate) fn get_storage_map_details_for_keys(
&self,
account_id: AccountId,
slot_name: StorageSlotName,
block_num: BlockNumber,
raw_keys: &[StorageMapKey],
) -> Option<Result<AccountStorageMapDetails, MerkleError>> {
let lineage = Self::storage_lineage_id(account_id, &slot_name);
let tree = self.get_tree_id(lineage, block_num)?;
let proofs = Result::from_iter(raw_keys.iter().map(|raw_key| {
let key_hashed = raw_key.hash().into();
self.forest.open(tree, key_hashed).map_err(Self::map_forest_error)
}));
Some(proofs.map(|proofs| AccountStorageMapDetails::from_proofs(slot_name, proofs)))
}
fn get_storage_map_entries(
&self,
account_id: AccountId,
slot_name: &StorageSlotName,
block_num: BlockNumber,
) -> Option<Result<Vec<(StorageMapKeyHash, Word)>, MerkleError>> {
let lineage = Self::storage_lineage_id(account_id, slot_name);
let tree = self.get_tree_id(lineage, block_num)?;
Some(self.forest.entries(tree).map_err(Self::map_forest_error).and_then(|entries| {
entries
.map(|entry| {
entry
.map(|e| (StorageMapKeyHash::from_raw(e.key), e.value))
.map_err(Self::map_forest_error)
})
.collect()
}))
}
fn num_storage_map_entries(
&self,
account_id: AccountId,
slot_name: &StorageSlotName,
block_num: BlockNumber,
) -> Option<Result<usize, MerkleError>> {
let lineage = Self::storage_lineage_id(account_id, slot_name);
let tree = self.get_tree_id(lineage, block_num)?;
Some(self.forest.entry_count(tree).map_err(Self::map_forest_error))
}
#[miden_instrument(
target = COMPONENT,
)]
pub(crate) fn get_storage_map_details_for_all_entries(
&self,
account_id: AccountId,
slot_name: StorageSlotName,
block_num: BlockNumber,
) -> Result<AccountStorageMapResult, MerkleError> {
let Some(num_entries) =
self.num_storage_map_entries(account_id, &slot_name, block_num).transpose()?
else {
return Ok(AccountStorageMapResult::NotFound);
};
if num_entries > AccountStorageMapDetails::MAX_RETURN_ENTRIES {
return Ok(AccountStorageMapResult::Details(AccountStorageMapDetails {
slot_name,
entries: StorageMapEntries::LimitExceeded,
}));
}
let Some(hashed_entries) =
self.get_storage_map_entries(account_id, &slot_name, block_num).transpose()?
else {
return Ok(AccountStorageMapResult::NotFound);
};
let raw_keys = self
.storage_map_key_cache
.get_many(hashed_entries.iter().map(|(hashed_key, _)| hashed_key));
if raw_keys.iter().any(Option::is_none) {
return Ok(AccountStorageMapResult::CannotReconstructKeysFromCache);
}
let mut entries = raw_keys
.into_iter()
.flatten()
.zip(hashed_entries)
.map(|(raw_key, (_hashed_key, value))| (raw_key, value))
.collect::<Vec<_>>();
entries.sort_by_key(|(key, _)| *key);
Ok(AccountStorageMapResult::Details(AccountStorageMapDetails::from_forest_entries(
slot_name, entries,
)))
}
pub(crate) fn get_latest_vault_root(&self, account_id: AccountId) -> Word {
let lineage = Self::vault_lineage_id(account_id);
self.forest.latest_root(lineage).unwrap_or_else(empty_smt_root)
}
pub(crate) fn get_latest_storage_map_root(
&self,
account_id: AccountId,
slot_name: &StorageSlotName,
) -> Word {
let lineage = Self::storage_lineage_id(account_id, slot_name);
self.forest.latest_root(lineage).unwrap_or_else(empty_smt_root)
}
}
impl<B: Backend> AccountStateForest<B> {
pub(crate) fn reader(&self) -> Result<AccountStateForest<B::Reader>, LargeSmtForestError> {
Ok(AccountStateForest {
forest: self.forest.reader()?,
storage_map_key_cache: self.storage_map_key_cache.clone(),
vault_key_cache: self.vault_key_cache.clone(),
})
}
pub(crate) fn compute_block_update_mutations(
&self,
block_num: BlockNumber,
account_updates: impl IntoIterator<Item = AccountPatch>,
) -> Result<PreparedAccountStateForestBlockUpdate<B>, AccountStateForestUpdateError> {
let account_patches = account_updates.into_iter().collect::<Vec<_>>();
let (batch, lineages) = self.prepare_block_update_batch(&account_patches)?;
let mutations = self.forest.compute_forest_mutations(block_num.as_u64(), batch)?;
let account_states = self.precomputed_account_states_from_mutations(
&account_patches,
&lineages,
&mutations,
)?;
Ok(PreparedAccountStateForestBlockUpdate {
account_states,
mutations,
account_patches,
})
}
fn apply_precomputed_update(
&mut self,
block_num: BlockNumber,
update: PreparedAccountStateForestBlockUpdate<B>,
) -> Result<(), LargeSmtForestError> {
let PreparedAccountStateForestBlockUpdate {
account_states: _,
mutations,
account_patches,
} = update;
self.forest.apply_mutations(mutations)?;
for patch in &account_patches {
self.cache_hashed_keys_from_patch(patch);
tracing::trace!(
target: crate::LOG_TARGET,
account_id = %patch.id(),
%block_num,
is_full_state = patch.is_full_state(),
"Updated forest with account patch"
);
}
Ok(())
}
pub(crate) fn apply_precomputed_block_update(
&mut self,
block_num: BlockNumber,
update: PreparedAccountStateForestBlockUpdate<B>,
) -> Result<(), LargeSmtForestError> {
self.apply_precomputed_update(block_num, update)?;
let number_of_pruned_blocks = self.prune(block_num);
tracing::Span::current().record("num_pruned", number_of_pruned_blocks);
Ok(())
}
fn apply_account_updates_without_pruning(
&mut self,
block_num: BlockNumber,
account_updates: impl IntoIterator<Item = AccountPatch>,
) -> Result<(), AccountStateForestUpdateError> {
let update = self.compute_block_update_mutations(block_num, account_updates)?;
self.apply_precomputed_update(block_num, update)?;
Ok(())
}
pub(crate) fn apply_rebuild_updates(
&mut self,
block_num: BlockNumber,
account_updates: impl IntoIterator<Item = AccountPatch>,
) -> Result<(), AccountStateForestUpdateError> {
self.apply_account_updates_without_pruning(block_num, account_updates)
}
pub(crate) fn prune(&mut self, chain_tip: BlockNumber) -> usize {
let cutoff_block = chain_tip
.checked_sub(HISTORICAL_BLOCK_RETENTION)
.unwrap_or(BlockNumber::GENESIS);
let before = self.forest.roots().count();
self.forest.truncate(cutoff_block.as_u64());
let after = self.forest.roots().count();
before.saturating_sub(after)
}
}
#[cfg(test)]
pub(crate) trait TestAccountStateForestExt {
fn update_account(&mut self, block_num: BlockNumber, patch: &AccountPatch);
}
#[cfg(test)]
impl<B: Backend> TestAccountStateForestExt for AccountStateForest<B> {
fn update_account(&mut self, block_num: BlockNumber, patch: &AccountPatch) {
self.apply_account_updates_without_pruning(block_num, [patch.clone()])
.expect("test account forest update should succeed");
}
}