Skip to main content

miden_node_store/state/
loader.rs

1//! Tree loading logic for the store state.
2//!
3//! This module handles loading and initializing the Merkle trees (account tree, nullifier tree,
4//! and SMT forest) from storage backends. It supports different loading modes:
5//!
6//! - **Memory mode** (`rocksdb` feature disabled): Trees are rebuilt from the database on each
7//!   startup.
8//! - **Persistent mode** (`rocksdb` feature enabled): Trees are loaded from persistent storage if
9//!   data exists, otherwise rebuilt from the database and persisted.
10
11use std::future::Future;
12use std::path::Path;
13
14use miden_protocol::Word;
15use miden_protocol::block::account_tree::{AccountTree, account_id_to_smt_key};
16use miden_protocol::block::nullifier_tree::NullifierTree;
17use miden_protocol::block::{BlockHeader, BlockNumber, Blockchain};
18#[cfg(not(feature = "rocksdb"))]
19use miden_protocol::crypto::merkle::smt::MemoryStorage;
20use miden_protocol::crypto::merkle::smt::{LargeSmt, LargeSmtError, SmtStorage};
21#[cfg(feature = "rocksdb")]
22use tracing::info;
23use tracing::instrument;
24#[cfg(feature = "rocksdb")]
25use {
26    miden_crypto::merkle::smt::RocksDbStorage,
27    miden_protocol::crypto::merkle::smt::RocksDbConfig,
28};
29
30use crate::COMPONENT;
31use crate::db::Db;
32use crate::errors::{DatabaseError, StateInitializationError};
33use crate::inner_forest::InnerForest;
34
35// CONSTANTS
36// ================================================================================================
37
38/// Directory name for the account tree storage within the data directory.
39pub const ACCOUNT_TREE_STORAGE_DIR: &str = "accounttree";
40
41/// Directory name for the nullifier tree storage within the data directory.
42pub const NULLIFIER_TREE_STORAGE_DIR: &str = "nullifiertree";
43
44// STORAGE TYPE ALIAS
45// ================================================================================================
46
47/// The storage backend for trees.
48#[cfg(feature = "rocksdb")]
49pub type TreeStorage = RocksDbStorage;
50#[cfg(not(feature = "rocksdb"))]
51pub type TreeStorage = MemoryStorage;
52
53// ERROR CONVERSION
54// ================================================================================================
55
56/// Converts a `LargeSmtError` into a `StateInitializationError`.
57pub fn account_tree_large_smt_error_to_init_error(e: LargeSmtError) -> StateInitializationError {
58    use miden_node_utils::ErrorReport;
59    match e {
60        LargeSmtError::Merkle(merkle_error) => {
61            StateInitializationError::DatabaseError(DatabaseError::MerkleError(merkle_error))
62        },
63        LargeSmtError::Storage(err) => {
64            StateInitializationError::AccountTreeIoError(err.as_report())
65        },
66    }
67}
68
69// STORAGE LOADER TRAIT
70// ================================================================================================
71
72/// Trait for loading trees from storage.
73///
74/// For `MemoryStorage`, the tree is rebuilt from database entries on each startup.
75/// For `RocksDbStorage`, the tree is loaded directly from disk (much faster for large trees).
76///
77/// Missing or corrupted storage is handled by the `verify_tree_consistency` check after loading,
78/// which detects divergence between persistent storage and the database. If divergence is detected,
79/// the user should manually delete the tree storage directories and restart the node.
80pub trait StorageLoader: SmtStorage + Sized {
81    /// Creates a storage backend for the given domain.
82    fn create(data_dir: &Path, domain: &'static str) -> Result<Self, StateInitializationError>;
83
84    /// Loads an account tree, either from persistent storage or by rebuilding from DB.
85    fn load_account_tree(
86        self,
87        db: &mut Db,
88    ) -> impl Future<Output = Result<AccountTree<LargeSmt<Self>>, StateInitializationError>> + Send;
89
90    /// Loads a nullifier tree, either from persistent storage or by rebuilding from DB.
91    fn load_nullifier_tree(
92        self,
93        db: &mut Db,
94    ) -> impl Future<Output = Result<NullifierTree<LargeSmt<Self>>, StateInitializationError>> + Send;
95}
96
97// MEMORY STORAGE IMPLEMENTATION
98// ================================================================================================
99
100#[cfg(not(feature = "rocksdb"))]
101impl StorageLoader for MemoryStorage {
102    fn create(_data_dir: &Path, _domain: &'static str) -> Result<Self, StateInitializationError> {
103        Ok(MemoryStorage::default())
104    }
105
106    async fn load_account_tree(
107        self,
108        db: &mut Db,
109    ) -> Result<AccountTree<LargeSmt<Self>>, StateInitializationError> {
110        let account_data = db.select_all_account_commitments().await?;
111        let smt_entries = account_data
112            .into_iter()
113            .map(|(id, commitment)| (account_id_to_smt_key(id), commitment));
114        let smt = LargeSmt::with_entries(self, smt_entries)
115            .map_err(account_tree_large_smt_error_to_init_error)?;
116        AccountTree::new(smt).map_err(StateInitializationError::FailedToCreateAccountsTree)
117    }
118
119    async fn load_nullifier_tree(
120        self,
121        db: &mut Db,
122    ) -> Result<NullifierTree<LargeSmt<Self>>, StateInitializationError> {
123        let nullifiers = db.select_all_nullifiers().await?;
124        let entries = nullifiers.into_iter().map(|info| (info.nullifier, info.block_num));
125        NullifierTree::with_storage_from_entries(self, entries)
126            .map_err(StateInitializationError::FailedToCreateNullifierTree)
127    }
128}
129
130// ROCKSDB STORAGE IMPLEMENTATION
131// ================================================================================================
132
133#[cfg(feature = "rocksdb")]
134impl StorageLoader for RocksDbStorage {
135    fn create(data_dir: &Path, domain: &'static str) -> Result<Self, StateInitializationError> {
136        let storage_path = data_dir.join(domain);
137
138        fs_err::create_dir_all(&storage_path)
139            .map_err(|e| StateInitializationError::AccountTreeIoError(e.to_string()))?;
140        RocksDbStorage::open(RocksDbConfig::new(storage_path))
141            .map_err(|e| StateInitializationError::AccountTreeIoError(e.to_string()))
142    }
143
144    async fn load_account_tree(
145        self,
146        db: &mut Db,
147    ) -> Result<AccountTree<LargeSmt<Self>>, StateInitializationError> {
148        // If RocksDB storage has data, load from it directly
149        let has_data = self
150            .has_leaves()
151            .map_err(|e| StateInitializationError::AccountTreeIoError(e.to_string()))?;
152        if has_data {
153            let smt = load_smt(self)?;
154            return AccountTree::new(smt)
155                .map_err(StateInitializationError::FailedToCreateAccountsTree);
156        }
157
158        info!(target: COMPONENT, "RocksDB account tree storage is empty, populating from SQLite");
159        let account_data = db.select_all_account_commitments().await?;
160        let smt_entries = account_data
161            .into_iter()
162            .map(|(id, commitment)| (account_id_to_smt_key(id), commitment));
163        let smt = LargeSmt::with_entries(self, smt_entries)
164            .map_err(account_tree_large_smt_error_to_init_error)?;
165        AccountTree::new(smt).map_err(StateInitializationError::FailedToCreateAccountsTree)
166    }
167
168    async fn load_nullifier_tree(
169        self,
170        db: &mut Db,
171    ) -> Result<NullifierTree<LargeSmt<Self>>, StateInitializationError> {
172        // If RocksDB storage has data, load from it directly
173        let has_data = self
174            .has_leaves()
175            .map_err(|e| StateInitializationError::NullifierTreeIoError(e.to_string()))?;
176        if has_data {
177            let smt = load_smt(self)?;
178            return Ok(NullifierTree::new_unchecked(smt));
179        }
180
181        info!(target: COMPONENT, "RocksDB nullifier tree storage is empty, populating from SQLite");
182        let nullifiers = db.select_all_nullifiers().await?;
183        let entries = nullifiers.into_iter().map(|info| (info.nullifier, info.block_num));
184        NullifierTree::with_storage_from_entries(self, entries)
185            .map_err(StateInitializationError::FailedToCreateNullifierTree)
186    }
187}
188
189// HELPER FUNCTIONS
190// ================================================================================================
191
192/// Loads an SMT from persistent storage.
193#[cfg(feature = "rocksdb")]
194pub fn load_smt<S: SmtStorage>(storage: S) -> Result<LargeSmt<S>, StateInitializationError> {
195    LargeSmt::new(storage).map_err(account_tree_large_smt_error_to_init_error)
196}
197
198// TREE LOADING FUNCTIONS
199// ================================================================================================
200
201/// Loads the blockchain MMR from all block headers in the database.
202#[instrument(target = COMPONENT, skip_all)]
203pub async fn load_mmr(db: &mut Db) -> Result<Blockchain, StateInitializationError> {
204    let block_commitments: Vec<miden_protocol::Word> = db
205        .select_all_block_headers()
206        .await?
207        .iter()
208        .map(BlockHeader::commitment)
209        .collect();
210
211    // SAFETY: We assume the loaded MMR is valid and does not have more than u32::MAX
212    // entries.
213    let chain_mmr = Blockchain::from_mmr_unchecked(block_commitments.into());
214
215    Ok(chain_mmr)
216}
217
218/// Loads SMT forest with storage map and vault Merkle paths for all public accounts.
219#[instrument(target = COMPONENT, skip_all, fields(block_num = %block_num))]
220pub async fn load_smt_forest(
221    db: &mut Db,
222    block_num: BlockNumber,
223) -> Result<InnerForest, StateInitializationError> {
224    use miden_protocol::account::delta::AccountDelta;
225
226    let public_account_ids = db.select_all_public_account_ids().await?;
227
228    // Acquire write lock once for the entire initialization
229    let mut forest = InnerForest::new();
230
231    // Process each account
232    for account_id in public_account_ids {
233        // Get the full account from the database
234        let account_info = db.select_account(account_id).await?;
235        let account = account_info.details.expect("public accounts always have details in DB");
236
237        // Convert the full account to a full-state delta
238        let delta =
239            AccountDelta::try_from(account).expect("accounts from DB should not have seeds");
240
241        // Use the unified update method (will recognize it's a full-state delta)
242        forest.update_account(block_num, &delta)?;
243    }
244
245    Ok(forest)
246}
247
248// CONSISTENCY VERIFICATION
249// ================================================================================================
250
251/// Verifies that tree roots match the expected roots from the latest block header.
252///
253/// This check ensures the database and tree storage (memory or persistent) haven't diverged due to
254/// corruption or incomplete shutdown. When trees are rebuilt from the database, they will naturally
255/// match; when loaded from persistent storage, this catches any inconsistencies.
256///
257/// # Arguments
258/// * `account_tree_root` - Root of the loaded account tree
259/// * `nullifier_tree_root` - Root of the loaded nullifier tree
260/// * `db` - Database connection to fetch the latest block header
261///
262/// # Errors
263/// Returns `StateInitializationError::TreeStorageDiverged` if any root doesn't match.
264#[instrument(target = COMPONENT, skip_all)]
265pub async fn verify_tree_consistency(
266    account_tree_root: Word,
267    nullifier_tree_root: Word,
268    db: &mut Db,
269) -> Result<(), StateInitializationError> {
270    // Fetch the latest block header to get the expected roots
271    let latest_header = db.select_block_header_by_block_num(None).await?;
272
273    let (block_num, expected_account_root, expected_nullifier_root) = latest_header
274        .map(|header| (header.block_num(), header.account_root(), header.nullifier_root()))
275        .unwrap_or_default();
276
277    // Verify account tree root
278    if account_tree_root != expected_account_root {
279        return Err(StateInitializationError::TreeStorageDiverged {
280            tree_name: "Account",
281            block_num,
282            tree_root: account_tree_root,
283            block_root: expected_account_root,
284        });
285    }
286
287    // Verify nullifier tree root
288    if nullifier_tree_root != expected_nullifier_root {
289        return Err(StateInitializationError::TreeStorageDiverged {
290            tree_name: "Nullifier",
291            block_num,
292            tree_root: nullifier_tree_root,
293            block_root: expected_nullifier_root,
294        });
295    }
296
297    Ok(())
298}