use std::future::Future;
use std::num::NonZeroUsize;
use std::path::Path;
use miden_crypto::merkle::mmr::Mmr;
use miden_crypto::merkle::smt::{Backend, ForestInMemoryBackend};
#[cfg(feature = "rocksdb")]
use miden_crypto::merkle::smt::{ForestPersistentBackend, PersistentBackendConfig};
#[cfg(feature = "rocksdb")]
use miden_large_smt_backend_rocksdb::{RocksDbStorage, SmtStorageReader};
#[cfg(feature = "rocksdb")]
use miden_node_utils::clap::RocksDbOptions;
use miden_protocol::account::{AccountId, AccountStorageHeader, StorageSlotType};
use miden_protocol::block::account_tree::{AccountIdKey, AccountTree};
use miden_protocol::block::nullifier_tree::NullifierTree;
use miden_protocol::block::{BlockNumber, Blockchain};
#[cfg(not(feature = "rocksdb"))]
use miden_protocol::crypto::merkle::smt::MemoryStorage;
use miden_protocol::crypto::merkle::smt::{LargeSmt, LargeSmtError, SmtStorage};
use miden_protocol::{Felt, Word};
#[cfg(feature = "rocksdb")]
use tracing::info;
use tracing::instrument;
use crate::COMPONENT;
use crate::account_state_forest::AccountStateForest;
use crate::db::Db;
use crate::db::models::queries::BlockHeaderCommitment;
use crate::errors::{DatabaseError, StateInitializationError};
pub const ACCOUNT_TREE_STORAGE_DIR: &str = "accounttree";
pub const NULLIFIER_TREE_STORAGE_DIR: &str = "nullifiertree";
pub const ACCOUNT_STATE_FOREST_STORAGE_DIR: &str = "accountstateforest";
const ACCOUNT_COMMITMENTS_PAGE_SIZE: NonZeroUsize = NonZeroUsize::new(10_000).unwrap();
const NULLIFIERS_PAGE_SIZE: NonZeroUsize = NonZeroUsize::new(10_000).unwrap();
const PUBLIC_ACCOUNT_IDS_PAGE_SIZE: NonZeroUsize = NonZeroUsize::new(1_000).unwrap();
#[cfg(feature = "rocksdb")]
pub type TreeStorage = RocksDbStorage;
#[cfg(not(feature = "rocksdb"))]
pub type TreeStorage = MemoryStorage;
pub fn account_tree_large_smt_error_to_init_error(e: LargeSmtError) -> StateInitializationError {
use miden_node_utils::ErrorReport;
match e {
LargeSmtError::Merkle(merkle_error) => {
StateInitializationError::DatabaseError(DatabaseError::MerkleError(merkle_error))
},
LargeSmtError::Storage(err) => {
StateInitializationError::AccountTreeIoError(err.as_report())
},
err @ (LargeSmtError::RootMismatch { .. } | LargeSmtError::StorageNotEmpty) => {
StateInitializationError::AccountTreeIoError(err.as_report())
},
}
}
fn block_num_to_nullifier_leaf(block_num: BlockNumber) -> Word {
Word::from([Felt::from(block_num), Felt::ZERO, Felt::ZERO, Felt::ZERO])
}
pub trait TreeStorageLoader: SmtStorage + Sized {
type Config: std::fmt::Debug + std::default::Default;
fn create(
data_dir: &Path,
storage_options: &Self::Config,
domain: &'static str,
) -> Result<Self, StateInitializationError>;
fn load_account_tree(
self,
db: &mut Db,
) -> impl Future<Output = Result<AccountTree<LargeSmt<Self>>, StateInitializationError>> + Send;
fn load_nullifier_tree(
self,
db: &mut Db,
) -> impl Future<Output = Result<NullifierTree<LargeSmt<Self>>, StateInitializationError>> + Send;
}
pub trait AccountForestLoader: Backend + Sized {
type Config: std::fmt::Debug + std::default::Default;
fn create(
data_dir: &Path,
storage_options: &Self::Config,
domain: &'static str,
) -> Result<Self, StateInitializationError>;
fn load_account_state_forest(
self,
db: &mut Db,
block_num: BlockNumber,
) -> impl Future<Output = Result<AccountStateForest<Self>, StateInitializationError>> + Send;
}
#[cfg(not(feature = "rocksdb"))]
impl TreeStorageLoader for MemoryStorage {
type Config = ();
fn create(
_data_dir: &Path,
_storage_options: &Self::Config,
_domain: &'static str,
) -> Result<Self, StateInitializationError> {
Ok(MemoryStorage::default())
}
#[instrument(target = COMPONENT, skip_all)]
async fn load_account_tree(
self,
db: &mut Db,
) -> Result<AccountTree<LargeSmt<Self>>, StateInitializationError> {
let mut smt = LargeSmt::with_entries(self, std::iter::empty())
.map_err(account_tree_large_smt_error_to_init_error)?;
let mut cursor = None;
loop {
let page = db
.select_account_commitments_paged(ACCOUNT_COMMITMENTS_PAGE_SIZE, cursor)
.await?;
cursor = page.next_cursor;
if page.commitments.is_empty() {
break;
}
let entries = page
.commitments
.into_iter()
.map(|(id, commitment)| (AccountIdKey::from(id).as_word(), commitment));
let mutations = smt
.compute_mutations(entries)
.map_err(account_tree_large_smt_error_to_init_error)?;
smt.apply_mutations(mutations)
.map_err(account_tree_large_smt_error_to_init_error)?;
if cursor.is_none() {
break;
}
}
AccountTree::new(smt).map_err(StateInitializationError::FailedToCreateAccountsTree)
}
#[instrument(target = COMPONENT, skip_all)]
async fn load_nullifier_tree(
self,
db: &mut Db,
) -> Result<NullifierTree<LargeSmt<Self>>, StateInitializationError> {
let mut smt = LargeSmt::with_entries(self, std::iter::empty())
.map_err(account_tree_large_smt_error_to_init_error)?;
let mut cursor = None;
loop {
let page = db.select_nullifiers_paged(NULLIFIERS_PAGE_SIZE, cursor).await?;
cursor = page.next_cursor;
if page.nullifiers.is_empty() {
break;
}
let entries = page.nullifiers.into_iter().map(|info| {
(info.nullifier.as_word(), block_num_to_nullifier_leaf(info.block_num))
});
let mutations = smt
.compute_mutations(entries)
.map_err(account_tree_large_smt_error_to_init_error)?;
smt.apply_mutations(mutations)
.map_err(account_tree_large_smt_error_to_init_error)?;
if cursor.is_none() {
break;
}
}
Ok(NullifierTree::new_unchecked(smt))
}
}
#[cfg(feature = "rocksdb")]
impl TreeStorageLoader for RocksDbStorage {
type Config = RocksDbOptions;
fn create(
data_dir: &Path,
storage_options: &Self::Config,
domain: &'static str,
) -> Result<Self, StateInitializationError> {
let storage_path = data_dir.join(domain);
let config = storage_options.with_path(&storage_path);
fs_err::create_dir_all(&storage_path)
.map_err(|e| StateInitializationError::AccountTreeIoError(e.to_string()))?;
RocksDbStorage::open(config)
.map_err(|e| StateInitializationError::AccountTreeIoError(e.to_string()))
}
#[instrument(target = COMPONENT, skip_all)]
async fn load_account_tree(
self,
db: &mut Db,
) -> Result<AccountTree<LargeSmt<Self>>, StateInitializationError> {
let has_data = self
.has_leaves()
.map_err(|e| StateInitializationError::AccountTreeIoError(e.to_string()))?;
if has_data {
let smt = load_smt(self)?;
return AccountTree::new(smt)
.map_err(StateInitializationError::FailedToCreateAccountsTree);
}
info!(target: COMPONENT, "RocksDB account tree storage is empty, populating from SQLite");
let mut smt = LargeSmt::with_entries(self, std::iter::empty())
.map_err(account_tree_large_smt_error_to_init_error)?;
let mut cursor = None;
loop {
let page = db
.select_account_commitments_paged(ACCOUNT_COMMITMENTS_PAGE_SIZE, cursor)
.await?;
cursor = page.next_cursor;
if page.commitments.is_empty() {
break;
}
let entries = page
.commitments
.into_iter()
.map(|(id, commitment)| (AccountIdKey::from(id).as_word(), commitment));
let mutations = smt
.compute_mutations(entries)
.map_err(account_tree_large_smt_error_to_init_error)?;
smt.apply_mutations(mutations)
.map_err(account_tree_large_smt_error_to_init_error)?;
if cursor.is_none() {
break;
}
}
AccountTree::new(smt).map_err(StateInitializationError::FailedToCreateAccountsTree)
}
#[instrument(target = COMPONENT, skip_all)]
async fn load_nullifier_tree(
self,
db: &mut Db,
) -> Result<NullifierTree<LargeSmt<Self>>, StateInitializationError> {
let has_data = self
.has_leaves()
.map_err(|e| StateInitializationError::NullifierTreeIoError(e.to_string()))?;
if has_data {
let smt = load_smt(self)?;
return Ok(NullifierTree::new_unchecked(smt));
}
info!(target: COMPONENT, "RocksDB nullifier tree storage is empty, populating from SQLite");
let mut smt = LargeSmt::with_entries(self, std::iter::empty())
.map_err(account_tree_large_smt_error_to_init_error)?;
let mut cursor = None;
loop {
let page = db.select_nullifiers_paged(NULLIFIERS_PAGE_SIZE, cursor).await?;
cursor = page.next_cursor;
if page.nullifiers.is_empty() {
break;
}
let entries = page.nullifiers.into_iter().map(|info| {
(info.nullifier.as_word(), block_num_to_nullifier_leaf(info.block_num))
});
let mutations = smt
.compute_mutations(entries)
.map_err(account_tree_large_smt_error_to_init_error)?;
smt.apply_mutations(mutations)
.map_err(account_tree_large_smt_error_to_init_error)?;
if cursor.is_none() {
break;
}
}
Ok(NullifierTree::new_unchecked(smt))
}
}
impl AccountForestLoader for ForestInMemoryBackend {
type Config = ();
fn create(
_data_dir: &Path,
_storage_options: &Self::Config,
_domain: &'static str,
) -> Result<Self, StateInitializationError> {
Ok(ForestInMemoryBackend::new())
}
#[instrument(target = COMPONENT, skip_all, fields(block.number = %block_num))]
async fn load_account_state_forest(
self,
db: &mut Db,
block_num: BlockNumber,
) -> Result<AccountStateForest<Self>, StateInitializationError> {
let mut forest = AccountStateForest::from_backend(self)
.map_err(|e| StateInitializationError::AccountStateForestIoError(e.to_string()))?;
rebuild_account_state_forest(&mut forest, db, block_num).await?;
Ok(forest)
}
}
#[cfg(feature = "rocksdb")]
impl AccountForestLoader for ForestPersistentBackend {
type Config = RocksDbOptions;
fn create(
data_dir: &Path,
storage_options: &Self::Config,
domain: &'static str,
) -> Result<Self, StateInitializationError> {
let storage_path = data_dir.join(domain);
fs_err::create_dir_all(&storage_path)
.map_err(|e| StateInitializationError::AccountStateForestIoError(e.to_string()))?;
let max_open_files = usize::try_from(storage_options.max_open_fds).map_err(|_| {
StateInitializationError::AccountStateForestIoError(format!(
"invalid account state forest RocksDB max_open_fds: {}",
storage_options.max_open_fds
))
})?;
let config = PersistentBackendConfig::new(&storage_path)
.map_err(|e| StateInitializationError::AccountStateForestIoError(e.to_string()))?
.with_cache_size_bytes(storage_options.cache_size_in_bytes)
.with_max_open_files(max_open_files);
ForestPersistentBackend::load(config)
.map_err(|e| StateInitializationError::AccountStateForestIoError(e.to_string()))
}
#[instrument(target = COMPONENT, skip_all, fields(block.number = %block_num))]
async fn load_account_state_forest(
self,
db: &mut Db,
block_num: BlockNumber,
) -> Result<AccountStateForest<Self>, StateInitializationError> {
let mut forest = AccountStateForest::from_backend(self)
.map_err(|e| StateInitializationError::AccountStateForestIoError(e.to_string()))?;
if forest.lineage_count() != 0 {
return Ok(forest);
}
info!(
target: COMPONENT,
"RocksDB account state forest storage is empty, populating from SQLite"
);
rebuild_account_state_forest(&mut forest, db, block_num).await?;
Ok(forest)
}
}
#[cfg(feature = "rocksdb")]
pub fn load_smt<S: SmtStorage>(storage: S) -> Result<LargeSmt<S>, StateInitializationError> {
LargeSmt::load(storage).map_err(account_tree_large_smt_error_to_init_error)
}
#[instrument(target = COMPONENT, skip_all)]
pub async fn load_mmr(db: &mut Db) -> Result<Blockchain, StateInitializationError> {
let block_commitments = db.select_all_block_header_commitments().await?;
let mmr = Mmr::try_from_iter(block_commitments.into_iter().map(BlockHeaderCommitment::word))
.expect("loaded MMR exceeds maximum allowed size");
let chain_mmr = Blockchain::from_mmr_unchecked(mmr);
Ok(chain_mmr)
}
#[instrument(target = COMPONENT, skip_all, fields(block.number = %block_num))]
pub async fn rebuild_account_state_forest(
forest: &mut AccountStateForest<impl Backend>,
db: &mut Db,
block_num: BlockNumber,
) -> Result<(), StateInitializationError> {
use miden_protocol::account::delta::AccountDelta;
let mut cursor = None;
loop {
let page = db.select_public_account_ids_paged(PUBLIC_ACCOUNT_IDS_PAGE_SIZE, cursor).await?;
if page.account_ids.is_empty() {
break;
}
for account_id in page.account_ids {
let account_info = db.select_account(account_id).await?;
let account = account_info
.details
.ok_or(StateInitializationError::PublicAccountMissingDetails(account_id))?;
let delta = AccountDelta::try_from(account).map_err(|e| {
StateInitializationError::AccountToDeltaConversionFailed(e.to_string())
})?;
forest.update_account(block_num, &delta)?;
}
cursor = page.next_cursor;
if cursor.is_none() {
break;
}
}
Ok(())
}
#[instrument(target = COMPONENT, skip_all)]
pub async fn verify_tree_consistency(
account_tree_root: Word,
nullifier_tree_root: Word,
db: &mut Db,
) -> Result<(), StateInitializationError> {
let latest_header = db.select_block_header_by_block_num(None).await?;
let (block_num, expected_account_root, expected_nullifier_root) = latest_header
.map(|header| (header.block_num(), header.account_root(), header.nullifier_root()))
.unwrap_or_default();
if account_tree_root != expected_account_root {
return Err(StateInitializationError::TreeStorageDiverged {
tree_name: "Account",
block_num,
tree_root: account_tree_root,
block_root: expected_account_root,
});
}
if nullifier_tree_root != expected_nullifier_root {
return Err(StateInitializationError::TreeStorageDiverged {
tree_name: "Nullifier",
block_num,
tree_root: nullifier_tree_root,
block_root: expected_nullifier_root,
});
}
Ok(())
}
#[instrument(target = COMPONENT, skip_all)]
pub async fn verify_account_state_forest_consistency(
forest: &AccountStateForest<impl Backend>,
db: &mut Db,
) -> Result<(), StateInitializationError> {
let mut cursor = None;
loop {
let page = db
.select_public_account_state_roots_paged(PUBLIC_ACCOUNT_IDS_PAGE_SIZE, cursor)
.await?;
if page.accounts.is_empty() {
break;
}
for account in page.accounts {
verify_account_state_forest_record(
forest,
account.account_id,
account.vault_root,
&account.storage_header,
)?;
}
cursor = page.next_cursor;
if cursor.is_none() {
break;
}
}
Ok(())
}
fn verify_account_state_forest_record(
forest: &AccountStateForest<impl Backend>,
account_id: AccountId,
vault_root: Word,
storage_header: &AccountStorageHeader,
) -> Result<(), StateInitializationError> {
let forest_vault_root = forest.get_latest_vault_root(account_id);
if forest_vault_root != vault_root {
return Err(StateInitializationError::AccountStateForestStorageDiverged {
account_id,
slot_name: None,
forest_root: forest_vault_root,
database_root: vault_root,
});
}
for slot in storage_header.slots() {
if slot.slot_type() != StorageSlotType::Map {
continue;
}
let forest_root = forest.get_latest_storage_map_root(account_id, slot.name());
let database_root = slot.value();
if forest_root != database_root {
return Err(StateInitializationError::AccountStateForestStorageDiverged {
account_id,
slot_name: Some(slot.name().to_string()),
forest_root,
database_root,
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use miden_protocol::account::{
AccountId,
AccountStorageHeader,
StorageSlotHeader,
StorageSlotName,
StorageSlotType,
};
use miden_protocol::testing::account_id::ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE;
use super::*;
#[test]
fn account_state_forest_consistency_detects_storage_map_root_mismatch() {
let account_id = AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE)
.expect("test account ID should be valid");
let slot_name =
StorageSlotName::new("account::balances").expect("slot name should be valid");
let expected_storage_root = Word::from([1, 0, 0, 0u32]);
let storage_header = AccountStorageHeader::new(vec![StorageSlotHeader::new(
slot_name.clone(),
StorageSlotType::Map,
expected_storage_root,
)])
.expect("storage header should be valid");
let forest = AccountStateForest::new();
let error = verify_account_state_forest_record(
&forest,
account_id,
AccountStateForest::empty_smt_root(),
&storage_header,
)
.expect_err("storage map root mismatch should be detected");
assert_matches::assert_matches!(
error,
StateInitializationError::AccountStateForestStorageDiverged {
account_id: actual_account_id,
slot_name: Some(actual_slot_name),
forest_root,
database_root,
} if actual_account_id == account_id
&& actual_slot_name == slot_name.to_string()
&& forest_root == AccountStateForest::empty_smt_root()
&& database_root == expected_storage_root
);
}
#[test]
fn account_state_forest_consistency_detects_vault_root_mismatch() {
let account_id = AccountId::try_from(ACCOUNT_ID_REGULAR_PUBLIC_ACCOUNT_IMMUTABLE_CODE)
.expect("test account ID should be valid");
let expected_vault_root = Word::from([2, 0, 0, 0u32]);
let storage_header =
AccountStorageHeader::new(Vec::new()).expect("storage header should be valid");
let forest = AccountStateForest::new();
let error = verify_account_state_forest_record(
&forest,
account_id,
expected_vault_root,
&storage_header,
)
.expect_err("vault root mismatch should be detected");
assert_matches::assert_matches!(
error,
StateInitializationError::AccountStateForestStorageDiverged {
account_id: actual_account_id,
slot_name: None,
forest_root,
database_root,
} if actual_account_id == account_id
&& forest_root == AccountStateForest::empty_smt_root()
&& database_root == expected_vault_root
);
}
}