1use std::num::NonZeroUsize;
4use std::path::Path;
5use std::sync::Arc;
6use std::sync::atomic::AtomicUsize;
7
8use arc_swap::ArcSwap;
9use miden_node_tracing::spawn::spawn_blocking_in_current_span;
10use miden_node_tracing::{ErrorReport, miden_instrument};
11use miden_node_utils::clap::StorageOptions;
12use miden_node_utils::shutdown::CancellationToken;
13use tokio::sync::{mpsc, watch};
14use tokio::task::JoinHandle;
15use tracing::Instrument;
16
17use crate::account_state_forest::AccountStateForestBackend;
18use crate::accounts::AccountTreeWithHistory;
19use crate::blocks::BlockStore;
20use crate::db::Db;
21use crate::errors::StateInitializationError;
22use crate::proven_tip::ProvenTipWriter;
23use crate::state::loader::{
24 ACCOUNT_STATE_FOREST_STORAGE_DIR,
25 ACCOUNT_TREE_STORAGE_DIR,
26 AccountForestLoader,
27 NULLIFIER_TREE_STORAGE_DIR,
28 TreeStorage,
29 TreeStorageLoader,
30 load_mmr,
31 verify_account_state_forest_consistency,
32 verify_tree_consistency,
33};
34use crate::state::writer::{WriteRequest, WriteWorker, WriterTask};
35use crate::state::{
36 BlockCache,
37 BlockWriter,
38 ProofCache,
39 ProofWriter,
40 SnapshotGuard,
41 State,
42 StateSnapshot,
43};
44use crate::{COMPONENT, DataDirectory, DatabaseOptions};
45
46async fn join_load_task<T>(
51 handle: JoinHandle<Result<T, StateInitializationError>>,
52) -> Result<T, StateInitializationError> {
53 match handle.await {
54 Ok(result) => result,
55 Err(err) => std::panic::resume_unwind(err.into_panic()),
56 }
57}
58
59const BLOCK_CACHE_CAPACITY: NonZeroUsize = NonZeroUsize::new(512).unwrap();
61
62const PROOF_CACHE_CAPACITY: NonZeroUsize = NonZeroUsize::new(512).unwrap();
64
65#[must_use = "call `start` to spawn the write worker and obtain the state"]
74pub struct LoadedState {
75 state: State,
76 writer: WriteWorker,
77 write_tx: mpsc::Sender<WriteRequest>,
78}
79
80impl LoadedState {
81 pub fn start(
99 self,
100 shutdown: CancellationToken,
101 ) -> (Arc<State>, BlockWriter, ProofWriter, WriterTask) {
102 let writer_task = tokio::spawn(self.writer.run(shutdown));
103 let state = Arc::new(self.state);
104 let block_writer = BlockWriter {
105 block_store: Arc::clone(&state.block_store),
106 write_tx: self.write_tx,
107 };
108 let proof_writer = ProofWriter { state: Arc::clone(&state) };
109 (state, block_writer, proof_writer, WriterTask(writer_task))
110 }
111}
112
113impl State {
117 #[miden_instrument(
123 target = COMPONENT,
124 )]
125 pub async fn load(
126 data_path: &Path,
127 storage_options: StorageOptions,
128 ) -> Result<LoadedState, StateInitializationError> {
129 Self::load_with_database_options(data_path, storage_options, DatabaseOptions::default())
130 .await
131 }
132
133 #[miden_instrument(
139 target = COMPONENT,
140 )]
141 pub async fn load_with_database_options(
142 data_path: &Path,
143 storage_options: StorageOptions,
144 database_options: DatabaseOptions,
145 ) -> Result<LoadedState, StateInitializationError> {
146 let data_directory = DataDirectory::load(data_path.to_path_buf())
147 .map_err(StateInitializationError::DataDirectoryLoadError)?;
148
149 let block_store = Arc::new(
150 BlockStore::load(data_directory.block_store_dir())
151 .map_err(StateInitializationError::BlockStoreLoadError)?,
152 );
153
154 let database_filepath = data_directory.database_path();
155 let db = Arc::new(
156 Db::load_with_pool_size(
157 database_filepath.clone(),
158 database_options.connection_pool_size,
159 )
160 .await
161 .map_err(StateInitializationError::DatabaseLoadError)?,
162 );
163
164 let genesis_header = db
165 .select_genesis_block_header()
166 .await?
167 .ok_or(StateInitializationError::GenesisBlockMissing)?;
168 let genesis_protocol_config_commitment = genesis_header.protocol_config_commitment();
169 if db
170 .select_protocol_config_by_commitment(genesis_protocol_config_commitment)
171 .await?
172 .is_none()
173 {
174 return Err(StateInitializationError::GenesisProtocolConfigMissing {
175 commitment: genesis_protocol_config_commitment,
176 });
177 }
178
179 let latest_block_num = db
182 .select_block_header_by_block_num(None)
183 .await?
184 .ok_or(StateInitializationError::GenesisBlockMissing)?
185 .block_num();
186
187 let apply_block_thread_priority = storage_options.apply_block_thread_priority;
188
189 #[cfg(feature = "rocksdb")]
190 let (account_storage_config, nullifier_storage_config, forest_storage_config) = (
191 storage_options.account_tree.into(),
192 storage_options.nullifier_tree.into(),
193 storage_options.account_state_forest.into(),
194 );
195 #[cfg(not(feature = "rocksdb"))]
196 let (account_storage_config, nullifier_storage_config, forest_storage_config) =
197 ((), (), ());
198
199 let mmr_task = tokio::spawn(
205 {
206 let db = Arc::clone(&db);
207 async move { load_mmr(&db).await }
208 }
209 .in_current_span(),
210 );
211 let account_tree_task = tokio::spawn(
212 {
213 let (db, path) = (Arc::clone(&db), data_path.to_path_buf());
214 async move {
215 join_load_task(spawn_blocking_in_current_span(move || {
216 TreeStorage::create(
217 &path,
218 &account_storage_config,
219 ACCOUNT_TREE_STORAGE_DIR,
220 )
221 }))
222 .await?
223 .load_account_tree(&db)
224 .await
225 }
226 }
227 .in_current_span(),
228 );
229 let nullifier_tree_task = tokio::spawn(
230 {
231 let (db, path) = (Arc::clone(&db), data_path.to_path_buf());
232 async move {
233 join_load_task(spawn_blocking_in_current_span(move || {
234 TreeStorage::create(
235 &path,
236 &nullifier_storage_config,
237 NULLIFIER_TREE_STORAGE_DIR,
238 )
239 }))
240 .await?
241 .load_nullifier_tree(&db)
242 .await
243 }
244 }
245 .in_current_span(),
246 );
247 let forest_task = tokio::spawn(
248 {
249 let (db, path) = (Arc::clone(&db), data_path.to_path_buf());
250 async move {
251 let forest = join_load_task(spawn_blocking_in_current_span(move || {
252 AccountStateForestBackend::create(
253 &path,
254 &forest_storage_config,
255 ACCOUNT_STATE_FOREST_STORAGE_DIR,
256 )
257 }))
258 .await?
259 .load_account_state_forest(&db, latest_block_num)
260 .await?;
261 verify_account_state_forest_consistency(&forest, &db).await?;
262 Ok(forest)
263 }
264 }
265 .in_current_span(),
266 );
267 let (blockchain, account_tree, nullifier_tree, forest) = tokio::try_join!(
268 join_load_task(mmr_task),
269 join_load_task(account_tree_task),
270 join_load_task(nullifier_tree_task),
271 join_load_task(forest_task),
272 )?;
273
274 verify_tree_consistency(account_tree.root(), nullifier_tree.root(), &db).await?;
278
279 let account_tree = AccountTreeWithHistory::new(account_tree, latest_block_num);
280
281 let proven_tip_init = block_store
283 .load_proven_tip()
284 .map_err(StateInitializationError::ProvenTipLoadError)?;
285 let (proven_tip, _rx) = ProvenTipWriter::new(proven_tip_init);
286
287 let (committed_tip_tx, _rx) = watch::channel(latest_block_num);
289 let committed_tip_tx = Arc::new(committed_tip_tx);
290
291 let block_cache = BlockCache::new(BLOCK_CACHE_CAPACITY);
292 let proof_cache = ProofCache::new(PROOF_CACHE_CAPACITY);
293
294 let snapshots_live = Arc::new(AtomicUsize::new(0));
296
297 let initial_snapshot = Arc::new(StateSnapshot::new(
299 nullifier_tree
300 .reader()
301 .map_err(|e| StateInitializationError::NullifierTreeIoError(e.as_report()))?,
302 blockchain.clone(),
303 account_tree.reader(),
304 forest
305 .reader()
306 .map_err(|e| StateInitializationError::AccountStateForestIoError(e.as_report()))?,
307 SnapshotGuard::new(Arc::clone(&snapshots_live), latest_block_num),
308 ));
309 let latest_snapshot = Arc::new(ArcSwap::from(initial_snapshot));
310
311 let (write_tx, write_rx) = mpsc::channel(1);
316 let block_writer = WriteWorker::new(
317 Arc::clone(&db),
318 Arc::clone(&block_store),
319 Arc::clone(&latest_snapshot),
320 Arc::clone(&committed_tip_tx),
321 block_cache.clone(),
322 write_rx,
323 nullifier_tree,
324 account_tree,
325 blockchain,
326 forest,
327 snapshots_live,
328 apply_block_thread_priority,
329 );
330 let state = Self {
331 data_directory: data_path.to_path_buf(),
332 db,
333 block_store,
334 latest_snapshot,
335 proven_tip,
336 committed_tip_tx,
337 block_cache,
338 proof_cache,
339 };
340
341 Ok(LoadedState { state, writer: block_writer, write_tx })
342 }
343
344 #[doc(hidden)]
355 pub async fn for_tests(data_path: &Path) -> (Arc<Self>, BlockWriter, ProofWriter) {
356 let (state, block_writer, proof_writer, _writer_task) =
357 Self::load(data_path, StorageOptions::default())
358 .await
359 .expect("state should load")
360 .start(CancellationToken::new());
361 (state, block_writer, proof_writer)
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use miden_node_db::sqlite::DbWriter;
368 use miden_node_utils::clap::StorageOptions;
369 use miden_node_utils::fee::{test_fee_params, test_protocol_config};
370 use miden_protocol::block::ValidatorConfig;
371 use miden_protocol::testing::random_secret_key::random_secret_key;
372
373 use super::State;
374 use crate::DataDirectory;
375 use crate::errors::{DatabaseError, StateInitializationError};
376 use crate::genesis::GenesisState;
377
378 async fn bootstrap_store(path: &std::path::Path) -> miden_protocol::Word {
379 let signer = random_secret_key();
380 let genesis = GenesisState::new(
381 Vec::new(),
382 test_fee_params(),
383 0,
384 ValidatorConfig::new(vec![signer.public_key()], 1).unwrap(),
385 test_protocol_config(),
386 )
387 .into_block()
388 .unwrap();
389 let commitment = genesis.protocol_config().to_commitment();
390 State::bootstrap(genesis, path).await.unwrap();
391 commitment
392 }
393
394 fn database_writer(path: &std::path::Path) -> DbWriter {
395 let database_path = DataDirectory::load(path.to_path_buf()).unwrap().database_path();
396 let (writer, _reader) = miden_node_db::sqlite::open(&database_path).unwrap();
397 writer
398 }
399
400 #[tokio::test]
401 async fn load_rejects_missing_genesis_protocol_config() {
402 let temp_dir = tempfile::tempdir().unwrap();
403 let commitment = bootstrap_store(temp_dir.path()).await;
404 database_writer(temp_dir.path())
405 .write::<_, DatabaseError, _>("delete genesis protocol config", move |tx| {
406 tx.execute("DELETE FROM protocol_configs WHERE commitment = ?1", &[&commitment])?;
407 Ok(())
408 })
409 .await
410 .unwrap();
411
412 let error = State::load(temp_dir.path(), StorageOptions::default())
413 .await
414 .err()
415 .expect("state load should fail");
416 assert!(matches!(
417 error,
418 StateInitializationError::GenesisProtocolConfigMissing { commitment: actual }
419 if actual == commitment
420 ));
421 }
422
423 #[tokio::test]
424 async fn load_rejects_corrupt_genesis_protocol_config() {
425 let temp_dir = tempfile::tempdir().unwrap();
426 let commitment = bootstrap_store(temp_dir.path()).await;
427 let mut bytes = miden_node_persistence::encode(&test_protocol_config());
428 bytes.push(0xff);
429 database_writer(temp_dir.path())
430 .write::<_, DatabaseError, _>("corrupt genesis protocol config", move |tx| {
431 tx.execute(
432 "UPDATE protocol_configs SET protocol_config = ?1 WHERE commitment = ?2",
433 &[&bytes, &commitment],
434 )?;
435 Ok(())
436 })
437 .await
438 .unwrap();
439
440 let error = State::load(temp_dir.path(), StorageOptions::default())
441 .await
442 .err()
443 .expect("state load should fail");
444 assert!(matches!(
445 error,
446 StateInitializationError::DatabaseError(DatabaseError::Persistence(_))
447 ));
448 }
449
450 #[tokio::test]
451 async fn state_view_returns_genesis_protocol_config() {
452 let temp_dir = tempfile::tempdir().unwrap();
453 let commitment = bootstrap_store(temp_dir.path()).await;
454
455 let loaded = State::load(temp_dir.path(), StorageOptions::default()).await.unwrap();
456 let protocol_config = loaded.state.view().get_protocol_config(commitment).await.unwrap();
457
458 assert_eq!(protocol_config, Some(test_protocol_config()));
459 }
460}