miden_node_store/server/
mod.rs

1use std::{
2    ops::Not,
3    path::{Path, PathBuf},
4    sync::Arc,
5};
6
7use anyhow::Context;
8use miden_node_proto::generated::store::api_server;
9use miden_node_utils::tracing::grpc::store_trace_fn;
10use tokio::net::TcpListener;
11use tokio_stream::wrappers::TcpListenerStream;
12use tower_http::trace::TraceLayer;
13use tracing::{info, instrument};
14
15use crate::{
16    COMPONENT, DATABASE_MAINTENANCE_INTERVAL, GenesisState, blocks::BlockStore, db::Db,
17    server::db_maintenance::DbMaintenance, state::State,
18};
19
20mod api;
21mod db_maintenance;
22
23/// The store server.
24pub struct Store {
25    pub listener: TcpListener,
26    pub data_directory: PathBuf,
27}
28
29impl Store {
30    /// Bootstraps the Store, creating the database state and inserting the genesis block data.
31    #[instrument(
32        target = COMPONENT,
33        name = "store.bootstrap",
34        skip_all,
35        err,
36    )]
37    pub fn bootstrap(genesis: GenesisState, data_directory: &Path) -> anyhow::Result<()> {
38        let genesis = genesis
39            .into_block()
40            .context("failed to convert genesis configuration into the genesis block")?;
41
42        let data_directory =
43            DataDirectory::load(data_directory.to_path_buf()).with_context(|| {
44                format!("failed to load data directory at {}", data_directory.display())
45            })?;
46        tracing::info!(target=COMPONENT, path=%data_directory.display(), "Data directory loaded");
47
48        let block_store = data_directory.block_store_dir();
49        let block_store =
50            BlockStore::bootstrap(block_store.clone(), &genesis).with_context(|| {
51                format!("failed to bootstrap block store at {}", block_store.display())
52            })?;
53        tracing::info!(target=COMPONENT, path=%block_store.display(), "Block store created");
54
55        // Create the genesis block and insert it into the database.
56        let database_filepath = data_directory.database_path();
57        Db::bootstrap(database_filepath.clone(), &genesis).with_context(|| {
58            format!("failed to bootstrap database at {}", database_filepath.display())
59        })?;
60        tracing::info!(target=COMPONENT, path=%database_filepath.display(), "Database created");
61
62        Ok(())
63    }
64
65    /// Serves the store's RPC API and DB maintenance background task.
66    ///
67    /// Note: this blocks until the server dies.
68    pub async fn serve(self) -> anyhow::Result<()> {
69        info!(target: COMPONENT, endpoint=?self.listener, ?self.data_directory, "Loading database");
70
71        let data_directory =
72            DataDirectory::load(self.data_directory.clone()).with_context(|| {
73                format!("failed to load data directory at {}", self.data_directory.display())
74            })?;
75
76        let block_store =
77            Arc::new(BlockStore::load(data_directory.block_store_dir()).with_context(|| {
78                format!("failed to load block store at {}", self.data_directory.display())
79            })?);
80
81        let database_filepath = data_directory.database_path();
82        let db = Db::load(database_filepath.clone()).await.with_context(|| {
83            format!("failed to load database at {}", database_filepath.display())
84        })?;
85
86        let state = Arc::new(State::load(db, block_store).await.context("failed to load state")?);
87
88        let db_maintenance_service =
89            DbMaintenance::new(Arc::clone(&state), DATABASE_MAINTENANCE_INTERVAL);
90        let api_service = api_server::ApiServer::new(api::StoreApi { state });
91
92        info!(target: COMPONENT, "Database loaded");
93
94        tokio::spawn(db_maintenance_service.run());
95        // Build the gRPC server with the API service and trace layer.
96        tonic::transport::Server::builder()
97            .layer(TraceLayer::new_for_grpc().make_span_with(store_trace_fn))
98            .add_service(api_service)
99            .serve_with_incoming(TcpListenerStream::new(self.listener))
100            .await
101            .context("failed to serve store API")
102    }
103}
104
105/// Represents the store's data-directory and its content paths.
106///
107/// Used to keep our filepath assumptions in one location.
108#[derive(Clone)]
109pub struct DataDirectory(PathBuf);
110
111impl DataDirectory {
112    /// Creates a new [`DataDirectory`], ensuring that the directory exists and is accessible
113    /// insofar as is possible.
114    pub fn load(path: PathBuf) -> std::io::Result<Self> {
115        let meta = std::fs::metadata(&path)?;
116        if meta.is_dir().not() {
117            return Err(std::io::ErrorKind::NotConnected.into());
118        }
119
120        Ok(Self(path))
121    }
122
123    pub fn block_store_dir(&self) -> PathBuf {
124        self.0.join("blocks")
125    }
126
127    pub fn database_path(&self) -> PathBuf {
128        self.0.join("miden-store.sqlite3")
129    }
130
131    pub fn display(&self) -> std::path::Display<'_> {
132        self.0.display()
133    }
134}