Skip to main content

miden_node_store/state/
lifecycle.rs

1//! Store lifecycle: loading the state, starting its write worker, and stopping the store.
2
3use std::num::NonZeroUsize;
4use std::path::Path;
5use std::sync::Arc;
6use std::sync::atomic::AtomicUsize;
7
8use arc_swap::ArcSwap;
9use miden_node_utils::ErrorReport;
10use miden_node_utils::clap::StorageOptions;
11use miden_node_utils::shutdown::CancellationToken;
12use miden_node_utils::spawn::spawn_blocking_in_current_span;
13use miden_node_utils::tracing::miden_instrument;
14use tokio::sync::{mpsc, watch};
15use tokio::task::JoinHandle;
16use tracing::Instrument;
17
18use crate::account_state_forest::AccountStateForestBackend;
19use crate::accounts::AccountTreeWithHistory;
20use crate::blocks::BlockStore;
21use crate::db::Db;
22use crate::errors::StateInitializationError;
23use crate::proven_tip::ProvenTipWriter;
24use crate::state::loader::{
25    ACCOUNT_STATE_FOREST_STORAGE_DIR,
26    ACCOUNT_TREE_STORAGE_DIR,
27    AccountForestLoader,
28    NULLIFIER_TREE_STORAGE_DIR,
29    TreeStorage,
30    TreeStorageLoader,
31    load_mmr,
32    verify_account_state_forest_consistency,
33    verify_tree_consistency,
34};
35use crate::state::writer::{WriteRequest, WriteWorker, WriterTask};
36use crate::state::{
37    BlockCache,
38    BlockWriter,
39    ProofCache,
40    ProofWriter,
41    SnapshotGuard,
42    State,
43    StateSnapshot,
44};
45use crate::{COMPONENT, DataDirectory, DatabaseOptions};
46
47/// Awaits a spawned load task, forwarding its result.
48///
49/// The load tasks are never aborted, so a join error is a panic from the task; it is resumed on
50/// the caller so panics keep propagating as panics.
51async fn join_load_task<T>(
52    handle: JoinHandle<Result<T, StateInitializationError>>,
53) -> Result<T, StateInitializationError> {
54    match handle.await {
55        Ok(result) => result,
56        Err(err) => std::panic::resume_unwind(err.into_panic()),
57    }
58}
59
60/// Number of recent committed blocks held in the in-memory cache for replica subscriptions.
61const BLOCK_CACHE_CAPACITY: NonZeroUsize = NonZeroUsize::new(512).unwrap();
62
63/// Number of recent block proofs held in the in-memory cache for replica subscriptions.
64const PROOF_CACHE_CAPACITY: NonZeroUsize = NonZeroUsize::new(512).unwrap();
65
66// LOADED STATE
67// ================================================================================================
68
69/// A loaded store state whose write worker has not been started yet.
70///
71/// Returned by [`State::load`]. [`Self::start`] spawns the writer and yields the read-only
72/// [`State`] together with the write capabilities; since this is the only way to obtain a
73/// [`BlockWriter`], [`BlockWriter::apply_block`] can always make progress.
74#[must_use = "call `start` to spawn the write worker and obtain the state"]
75pub struct LoadedState {
76    state: State,
77    writer: WriteWorker,
78    write_tx: mpsc::Sender<WriteRequest>,
79}
80
81impl LoadedState {
82    /// Spawns the write worker onto the current runtime and returns the read-only state together
83    /// with the write capabilities and the writer task's handle.
84    ///
85    /// [`Arc<State>`] is the read-only view shared with every component that queries or
86    /// subscribes; it exposes no mutating methods. [`BlockWriter`] and [`ProofWriter`] are the
87    /// only handles able to mutate the store — hand them to the single task driving each write
88    /// path (block production or sync, and proof scheduling or sync respectively). The
89    /// capabilities expose no read access: tasks that both read and write receive the
90    /// [`Arc<State>`] alongside their capability.
91    ///
92    /// The writer exits once `shutdown` is cancelled or the [`BlockWriter`] (holding the only
93    /// request sender) is dropped — an in-flight block write always completes first. Awaiting the
94    /// returned handle after either event guarantees the writer has released the tree storage it
95    /// owns; a join error carries a writer panic.
96    ///
97    /// Callers without a token to cancel should pass [`CancellationToken::new`] and stop the store
98    /// via [`BlockWriter::stop`] instead of dropping and joining by hand.
99    pub fn start(
100        self,
101        shutdown: CancellationToken,
102    ) -> (Arc<State>, BlockWriter, ProofWriter, WriterTask) {
103        let writer_task = tokio::spawn(self.writer.run(shutdown));
104        let state = Arc::new(self.state);
105        let block_writer = BlockWriter {
106            block_store: Arc::clone(&state.block_store),
107            write_tx: self.write_tx,
108        };
109        let proof_writer = ProofWriter { state: Arc::clone(&state) };
110        (state, block_writer, proof_writer, WriterTask(writer_task))
111    }
112}
113
114// LOAD
115// ================================================================================================
116
117impl State {
118    /// Loads the state from the data directory.
119    ///
120    /// The loaded state owns all store data structures and exposes subscription methods for
121    /// sequencer and replica tasks. Call [`LoadedState::start`] on the result to spawn the block
122    /// writer and obtain the usable [`State`].
123    #[miden_instrument(
124        target = COMPONENT,
125    )]
126    pub async fn load(
127        data_path: &Path,
128        storage_options: StorageOptions,
129    ) -> Result<LoadedState, StateInitializationError> {
130        Self::load_with_database_options(data_path, storage_options, DatabaseOptions::default())
131            .await
132    }
133
134    /// Loads the state from the data directory using explicit database options.
135    ///
136    /// The loaded state owns all store data structures and exposes subscription methods for
137    /// sequencer and replica tasks. Call [`LoadedState::start`] on the result to spawn the block
138    /// writer and obtain the usable [`State`].
139    #[miden_instrument(
140        target = COMPONENT,
141    )]
142    pub async fn load_with_database_options(
143        data_path: &Path,
144        storage_options: StorageOptions,
145        database_options: DatabaseOptions,
146    ) -> Result<LoadedState, StateInitializationError> {
147        let data_directory = DataDirectory::load(data_path.to_path_buf())
148            .map_err(StateInitializationError::DataDirectoryLoadError)?;
149
150        let block_store = Arc::new(
151            BlockStore::load(data_directory.block_store_dir())
152                .map_err(StateInitializationError::BlockStoreLoadError)?,
153        );
154
155        let database_filepath = data_directory.database_path();
156        let db = Arc::new(
157            Db::load_with_pool_size(
158                database_filepath.clone(),
159                database_options.connection_pool_size,
160            )
161            .await
162            .map_err(StateInitializationError::DatabaseLoadError)?,
163        );
164
165        // The chain tip drives forest loading and the account tree history below; `load_mmr`'s
166        // consistency check also pins the chain MMR to this header.
167        let latest_block_num = db
168            .select_block_header_by_block_num(None)
169            .await?
170            .ok_or(StateInitializationError::GenesisBlockMissing)?
171            .block_num();
172
173        let apply_block_thread_priority = storage_options.apply_block_thread_priority;
174
175        #[cfg(feature = "rocksdb")]
176        let (account_storage_config, nullifier_storage_config, forest_storage_config) = (
177            storage_options.account_tree.into(),
178            storage_options.nullifier_tree.into(),
179            storage_options.account_state_forest.into(),
180        );
181        #[cfg(not(feature = "rocksdb"))]
182        let (account_storage_config, nullifier_storage_config, forest_storage_config) =
183            ((), (), ());
184
185        // The four structures live in independent storages and the database pool supports
186        // concurrent readers, so open and load them concurrently. Each branch is a spawned task
187        // because loading has long synchronous sections (RocksDB opens, MMR hashing, SMT top
188        // reconstruction) that would serialize if polled from a single task. Spawning is eager, so
189        // all four run from this point; the join below only collects their results.
190        let mmr_task = tokio::spawn(
191            {
192                let db = Arc::clone(&db);
193                async move { load_mmr(&db).await }
194            }
195            .in_current_span(),
196        );
197        let account_tree_task = tokio::spawn(
198            {
199                let (db, path) = (Arc::clone(&db), data_path.to_path_buf());
200                async move {
201                    join_load_task(spawn_blocking_in_current_span(move || {
202                        TreeStorage::create(
203                            &path,
204                            &account_storage_config,
205                            ACCOUNT_TREE_STORAGE_DIR,
206                        )
207                    }))
208                    .await?
209                    .load_account_tree(&db)
210                    .await
211                }
212            }
213            .in_current_span(),
214        );
215        let nullifier_tree_task = tokio::spawn(
216            {
217                let (db, path) = (Arc::clone(&db), data_path.to_path_buf());
218                async move {
219                    join_load_task(spawn_blocking_in_current_span(move || {
220                        TreeStorage::create(
221                            &path,
222                            &nullifier_storage_config,
223                            NULLIFIER_TREE_STORAGE_DIR,
224                        )
225                    }))
226                    .await?
227                    .load_nullifier_tree(&db)
228                    .await
229                }
230            }
231            .in_current_span(),
232        );
233        let forest_task = tokio::spawn(
234            {
235                let (db, path) = (Arc::clone(&db), data_path.to_path_buf());
236                async move {
237                    let forest = join_load_task(spawn_blocking_in_current_span(move || {
238                        AccountStateForestBackend::create(
239                            &path,
240                            &forest_storage_config,
241                            ACCOUNT_STATE_FOREST_STORAGE_DIR,
242                        )
243                    }))
244                    .await?
245                    .load_account_state_forest(&db, latest_block_num)
246                    .await?;
247                    verify_account_state_forest_consistency(&forest, &db).await?;
248                    Ok(forest)
249                }
250            }
251            .in_current_span(),
252        );
253        let (blockchain, account_tree, nullifier_tree, forest) = tokio::try_join!(
254            join_load_task(mmr_task),
255            join_load_task(account_tree_task),
256            join_load_task(nullifier_tree_task),
257            join_load_task(forest_task),
258        )?;
259
260        // Verify that tree roots match the expected roots from the database. This catches any
261        // divergence between persistent storage and the database caused by corruption or incomplete
262        // shutdown.
263        verify_tree_consistency(account_tree.root(), nullifier_tree.root(), &db).await?;
264
265        let account_tree = AccountTreeWithHistory::new(account_tree, latest_block_num);
266
267        // Initialize the proven tip from the block store.
268        let proven_tip_init = block_store
269            .load_proven_tip()
270            .map_err(StateInitializationError::ProvenTipLoadError)?;
271        let (proven_tip, _rx) = ProvenTipWriter::new(proven_tip_init);
272
273        // Committed-tip watch: fires after each successful apply_block.
274        let (committed_tip_tx, _rx) = watch::channel(latest_block_num);
275        let committed_tip_tx = Arc::new(committed_tip_tx);
276
277        let block_cache = BlockCache::new(BLOCK_CACHE_CAPACITY);
278        let proof_cache = ProofCache::new(PROOF_CACHE_CAPACITY);
279
280        // Shared counter of live snapshot generations, for observability.
281        let snapshots_live = Arc::new(AtomicUsize::new(0));
282
283        // Create the initial snapshot from reader views of the just-loaded trees.
284        let initial_snapshot = Arc::new(StateSnapshot::new(
285            nullifier_tree
286                .reader()
287                .map_err(|e| StateInitializationError::NullifierTreeIoError(e.as_report()))?,
288            blockchain.clone(),
289            account_tree.reader(),
290            forest
291                .reader()
292                .map_err(|e| StateInitializationError::AccountStateForestIoError(e.as_report()))?,
293            SnapshotGuard::new(Arc::clone(&snapshots_live), latest_block_num),
294        ));
295        let latest_snapshot = Arc::new(ArcSwap::from(initial_snapshot));
296
297        // Assemble the write worker. It owns the writable trees and processes write requests
298        // serially, publishing a new snapshot after each committed block. The caller runs it; it
299        // exits when the shutdown token is cancelled or the `BlockWriter` (holding the only request
300        // sender) is dropped.
301        let (write_tx, write_rx) = mpsc::channel(1);
302        let block_writer = WriteWorker::new(
303            Arc::clone(&db),
304            Arc::clone(&block_store),
305            Arc::clone(&latest_snapshot),
306            Arc::clone(&committed_tip_tx),
307            block_cache.clone(),
308            write_rx,
309            nullifier_tree,
310            account_tree,
311            blockchain,
312            forest,
313            snapshots_live,
314            apply_block_thread_priority,
315        );
316        let state = Self {
317            data_directory: data_path.to_path_buf(),
318            db,
319            block_store,
320            latest_snapshot,
321            proven_tip,
322            committed_tip_tx,
323            block_cache,
324            proof_cache,
325        };
326
327        Ok(LoadedState { state, writer: block_writer, write_tx })
328    }
329
330    /// Loads the state with default options and starts its write worker, detaching the worker
331    /// task.
332    ///
333    /// Test-only helper for tests in sibling crates that don't manage the writer's lifecycle:
334    /// the detached writer exits once the returned [`BlockWriter`] is dropped. Hidden from public
335    /// docs and not part of the stable API.
336    ///
337    /// # Panics
338    ///
339    /// Panics if the state fails to load.
340    #[doc(hidden)]
341    pub async fn for_tests(data_path: &Path) -> (Arc<Self>, BlockWriter, ProofWriter) {
342        let (state, block_writer, proof_writer, _writer_task) =
343            Self::load(data_path, StorageOptions::default())
344                .await
345                .expect("state should load")
346                .start(CancellationToken::new());
347        (state, block_writer, proof_writer)
348    }
349}