use std::future::Future;
use std::num::NonZeroUsize;
use std::path::Path;
use miden_crypto::merkle::mmr::Mmr;
#[cfg(feature = "rocksdb")]
use miden_large_smt_backend_rocksdb::RocksDbStorage;
use miden_node_utils::clap::RocksDbOptions;
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";
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 StorageLoader: 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;
}
#[cfg(not(feature = "rocksdb"))]
impl StorageLoader 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 StorageLoader 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))
}
}
#[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 chain_mmr = Blockchain::from_mmr_unchecked(Mmr::from(
block_commitments.iter().copied().map(BlockHeaderCommitment::word),
));
Ok(chain_mmr)
}
#[instrument(target = COMPONENT, skip_all, fields(block.number = %block_num))]
pub async fn load_smt_forest(
db: &mut Db,
block_num: BlockNumber,
) -> Result<AccountStateForest, StateInitializationError> {
use miden_protocol::account::delta::AccountDelta;
let mut forest = AccountStateForest::new();
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(forest)
}
#[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(())
}