1use std::{collections::BTreeSet, env, sync::Arc};
4
5use ::light_client::{state::LightClientOptions, storage::LightClientSqliteOptions};
6use anyhow::{Context, bail};
7use clap::Parser;
8use espresso_telemetry as telemetry;
9use espresso_types::{
10 PubKey,
11 v0::traits::{EventConsumer, NullEventConsumer, PersistenceOptions, SequencerPersistence},
12};
13use futures::{channel::oneshot, future::BoxFuture};
14use hotshot_query_service::{
15 data_source::{ExtensibleDataSource, MetricsDataSource},
16 status::{HasMetrics, UpdateStatusData},
17};
18use hotshot_types::traits::{
19 metrics::{Metrics, NoMetrics},
20 network::ConnectedNetwork,
21};
22use process_metrics::ProcessMetrics;
23use url::Url;
24
25use super::{
26 ApiState, StorageState,
27 data_source::{
28 NodeStateDataSource, Provider, PruningDataSource, SequencerDataSource, provider,
29 },
30 fs, sql,
31 state::NodeApiStateImpl,
32 update::ApiEventConsumer,
33};
34use crate::{
35 api::LightClientProvider,
36 catchup::CatchupStorage,
37 context::{SequencerContext, TaskList},
38 options::PublicNodeConfig,
39 persistence,
40 request_response::data_source::Storage as RequestResponseStorage,
41 state::update_state_storage_loop,
42};
43
44#[derive(Clone, Debug)]
45pub struct Options {
46 pub http: Http,
47 pub query: Option<Query>,
48 pub submit: Option<Submit>,
49 pub status: Option<Status>,
50 pub catchup: Option<Catchup>,
51 pub config: Option<Config>,
52 pub hotshot_events: Option<HotshotEvents>,
53 pub explorer: Option<Explorer>,
54 pub light_client: Option<LightClient>,
55 pub storage_fs: Option<persistence::fs::Options>,
56 pub storage_sql: Option<persistence::sql::Options>,
57 pub public_node_config: Option<Box<PublicNodeConfig>>,
58}
59
60impl From<Http> for Options {
61 fn from(http: Http) -> Self {
62 Self {
63 http,
64 query: None,
65 submit: None,
66 status: None,
67 catchup: None,
68 config: None,
69 hotshot_events: None,
70 explorer: None,
71 light_client: None,
72 storage_fs: None,
73 storage_sql: None,
74 public_node_config: None,
75 }
76 }
77}
78
79impl Options {
80 pub fn with_port(port: u16) -> Self {
82 Http::with_port(port).into()
83 }
84
85 pub fn query_sql(mut self, query: Query, storage: persistence::sql::Options) -> Self {
87 self.query = Some(query);
88 self.storage_sql = Some(storage);
89 self
90 }
91
92 pub fn query_fs(mut self, query: Query, storage: persistence::fs::Options) -> Self {
94 self.query = Some(query);
95 self.storage_fs = Some(storage);
96 self
97 }
98
99 pub fn submit(mut self, opt: Submit) -> Self {
101 self.submit = Some(opt);
102 self
103 }
104
105 pub fn status(mut self, opt: Status) -> Self {
107 self.status = Some(opt);
108 self
109 }
110
111 pub fn catchup(mut self, opt: Catchup) -> Self {
113 self.catchup = Some(opt);
114 self
115 }
116
117 pub fn config(mut self, opt: Config) -> Self {
119 self.config = Some(opt);
120 self
121 }
122
123 pub fn public_node_config(mut self, c: PublicNodeConfig) -> Self {
127 self.public_node_config = Some(Box::new(c));
128 self
129 }
130
131 pub fn hotshot_events(mut self, opt: HotshotEvents) -> Self {
133 self.hotshot_events = Some(opt);
134 self
135 }
136
137 pub fn explorer(mut self, opt: Explorer) -> Self {
139 self.explorer = Some(opt);
140 self
141 }
142
143 pub fn light_client(mut self, opt: LightClient) -> Self {
145 self.light_client = Some(opt);
146 self
147 }
148
149 pub fn has_query_module(&self) -> bool {
151 self.query.is_some() && (self.storage_fs.is_some() || self.storage_sql.is_some())
152 }
153
154 pub async fn serve<N, P, F>(mut self, init_context: F) -> anyhow::Result<SequencerContext<N, P>>
160 where
161 N: ConnectedNetwork<PubKey>,
162 P: SequencerPersistence,
163 F: FnOnce(
164 Box<dyn Metrics>,
165 Box<dyn EventConsumer>,
166 Option<RequestResponseStorage>,
167 ) -> BoxFuture<'static, anyhow::Result<SequencerContext<N, P>>>,
168 {
169 let (send_ctx, recv_ctx) = oneshot::channel();
173 let state = ApiState::new(async move {
174 recv_ctx
175 .await
176 .expect("context initialized and sent over channel")
177 });
178 let mut tasks = TaskList::default();
179
180 #[allow(clippy::type_complexity)]
183 let (metrics, consumer, storage): (
184 Box<dyn Metrics>,
185 Box<dyn EventConsumer>,
186 Option<RequestResponseStorage>,
187 ) = if let Some(query_opt) = self.query.take() {
188 if let Some(opt) = self.storage_sql.take() {
189 self.init_with_query_module_sql(query_opt, opt, state, &mut tasks)
190 .await?
191 } else if let Some(opt) = self.storage_fs.take() {
192 self.init_with_query_module_fs(query_opt, opt, state, &mut tasks)
193 .await?
194 } else {
195 bail!("query module requested but not storage provided");
196 }
197 } else if self.status.is_some() {
198 let ds = MetricsDataSource::default();
202 let metrics = ds.populate_metrics();
203 telemetry::set_registry(Arc::new(ds.metrics().registry().clone()));
204 tasks.spawn("process_metrics", ProcessMetrics::new(ds.metrics()).run());
205 let axum_ds = Arc::new(ExtensibleDataSource::new(ds, state.clone()));
206
207 let port = self.http.port;
208 let env_vars = get_public_env_vars().unwrap_or_default();
209 let node_cfg = self.public_node_config.as_deref().cloned();
210 let modules = espresso_api::OptionalModules {
211 submit: self.submit.is_some(),
212 catchup: self.catchup.is_some(),
213 config: self.config.is_some(),
214 hotshot_events: self.hotshot_events.is_some(),
215 ..Default::default()
216 };
217 let max_connections = self.http.max_connections;
218 tasks.spawn("API server", async move {
219 let state = NodeApiStateImpl::new(axum_ds)
220 .with_env_vars(env_vars)
221 .with_public_node_config(node_cfg);
222 if let Err(e) =
223 espresso_api::serve_axum_status(port, state, modules, max_connections).await
224 {
225 tracing::error!("Axum server error: {}", e);
226 }
227 anyhow::Ok(())
228 });
229
230 if self.http.tonic_port.is_some() {
231 tracing::warn!("gRPC API not available in status-only mode");
232 }
233
234 (metrics, Box::new(NullEventConsumer), None)
235 } else {
236 let port = self.http.port;
243 let env_vars = get_public_env_vars().unwrap_or_default();
244 let node_cfg = self.public_node_config.as_deref().cloned();
245 let modules = espresso_api::OptionalModules {
246 submit: self.submit.is_some(),
247 catchup: self.catchup.is_some(),
248 config: self.config.is_some(),
249 hotshot_events: self.hotshot_events.is_some(),
250 ..Default::default()
251 };
252 let axum_ds = Arc::new(state.clone());
253 let max_connections = self.http.max_connections;
254 tasks.spawn("API server", async move {
255 let state = NodeApiStateImpl::new(axum_ds)
256 .with_env_vars(env_vars)
257 .with_public_node_config(node_cfg);
258 if let Err(e) =
259 espresso_api::serve_axum_bare(port, state, modules, max_connections).await
260 {
261 tracing::error!("Axum server error: {}", e);
262 }
263 anyhow::Ok(())
264 });
265
266 (Box::new(NoMetrics), Box::new(NullEventConsumer), None)
267 };
268
269 let ctx = init_context(metrics, consumer, storage.clone()).await?;
270 send_ctx
271 .send(ctx.clone())
272 .ok()
273 .context("API server exited without receiving context")?;
274 Ok(ctx.with_task_list(tasks))
275 }
276
277 async fn init_with_query_module_fs<N, P>(
278 &self,
279 query_opt: Query,
280 mod_opt: persistence::fs::Options,
281 state: ApiState<N, P>,
282 tasks: &mut TaskList,
283 ) -> anyhow::Result<(
284 Box<dyn Metrics>,
285 Box<dyn EventConsumer>,
286 Option<RequestResponseStorage>,
287 )>
288 where
289 N: ConnectedNetwork<PubKey>,
290 P: SequencerPersistence,
291 {
292 let ds = <fs::DataSource as SequencerDataSource>::create(
293 mod_opt,
294 provider(
295 query_opt.peers,
296 &state,
297 query_opt.light_client,
298 query_opt.light_client_db,
299 )
300 .await?,
301 false,
302 )
303 .await?;
304
305 let inner_storage = ds.inner();
307
308 tasks.spawn("process_metrics", ProcessMetrics::new(ds.metrics()).run());
309
310 let (metrics, ds) = init_query_data_source(ds, state.clone());
311
312 let port = self.http.port;
313 let ds_for_axum = ds.clone();
314 let env_vars = get_public_env_vars().unwrap_or_default();
315 let node_cfg = self.public_node_config.as_deref().cloned();
316 let modules = espresso_api::OptionalModules {
317 submit: self.submit.is_some(),
318 config: self.config.is_some(),
319 hotshot_events: self.hotshot_events.is_some(),
320 ..Default::default()
321 };
322 let max_connections = self.http.max_connections;
323 tasks.spawn("API server", async move {
324 let state = NodeApiStateImpl::new(ds_for_axum)
325 .with_env_vars(env_vars)
326 .with_public_node_config(node_cfg);
327 if let Err(e) = espresso_api::serve_axum_fs(port, state, modules, max_connections).await
328 {
329 tracing::error!("Axum server error: {}", e);
330 }
331 anyhow::Ok(())
332 });
333
334 if self.http.tonic_port.is_some() {
335 tracing::warn!("gRPC API not available with filesystem storage");
336 }
337
338 Ok((
339 metrics,
340 Box::new(ApiEventConsumer::from(ds)),
341 Some(RequestResponseStorage::Fs(inner_storage)),
342 ))
343 }
344
345 async fn init_with_query_module_sql<N, P>(
346 self,
347 query_opt: Query,
348 mod_opt: persistence::sql::Options,
349 state: ApiState<N, P>,
350 tasks: &mut TaskList,
351 ) -> anyhow::Result<(
352 Box<dyn Metrics>,
353 Box<dyn EventConsumer>,
354 Option<RequestResponseStorage>,
355 )>
356 where
357 N: ConnectedNetwork<PubKey>,
358 P: SequencerPersistence,
359 {
360 let mut provider = Provider::default();
361
362 let db_provider = mod_opt.clone().create().await?;
365 provider = provider
366 .with_block_provider(db_provider.clone())
367 .with_vid_common_provider(db_provider);
368 provider = provider.with_provider(
370 LightClientProvider::new(
371 query_opt.peers,
372 state.clone(),
373 query_opt.light_client,
374 query_opt.light_client_db,
375 )
376 .await?,
377 );
378
379 let ds = sql::DataSource::create(mod_opt.clone(), provider, false).await?;
380 let inner_storage = ds.inner();
381 tasks.spawn("process_metrics", ProcessMetrics::new(ds.metrics()).run());
382 let (metrics, ds) = init_query_data_source(ds, state.clone());
383
384 let get_node_state = {
385 let state = state.clone();
386 async move { state.node_state().await.clone() }
387 };
388 tasks.spawn(
389 "merklized state storage update loop",
390 update_state_storage_loop(ds.clone(), get_node_state),
391 );
392
393 let port = self.http.port;
394 let ds_for_axum = ds.clone();
395 let env_vars = get_public_env_vars().unwrap_or_default();
396 let node_cfg = self.public_node_config.as_deref().cloned();
397 let modules = espresso_api::OptionalModules {
398 submit: self.submit.is_some(),
399 config: self.config.is_some(),
400 explorer: self.explorer.is_some(),
401 light_client: self.light_client.is_some(),
402 hotshot_events: self.hotshot_events.is_some(),
403 ..Default::default()
404 };
405 let max_connections = self.http.max_connections;
406 tasks.spawn("API server", async move {
407 let state = NodeApiStateImpl::new(ds_for_axum)
408 .with_env_vars(env_vars)
409 .with_public_node_config(node_cfg);
410 if let Err(e) = espresso_api::serve_axum(port, state, modules, max_connections).await {
411 tracing::error!("Axum server error: {}", e);
412 }
413 anyhow::Ok(())
414 });
415
416 if let Some(tonic_port) = self.http.tonic_port {
417 let ds_for_tonic = ds.clone();
418 tasks.spawn("Tonic gRPC server", async move {
419 let state = NodeApiStateImpl::new(ds_for_tonic);
420 if let Err(e) = espresso_api::serve_tonic(tonic_port, state).await {
421 tracing::error!("Tonic gRPC server error: {}", e);
422 }
423 });
424 }
425
426 Ok((
427 metrics,
428 Box::new(ApiEventConsumer::from(ds)),
429 Some(RequestResponseStorage::Sql(inner_storage)),
430 ))
431 }
432}
433
434#[derive(Parser, Clone, Copy, Debug)]
439pub struct Http {
440 #[clap(long, env = "ESPRESSO_NODE_API_PORT", default_value = "8080")]
442 pub port: u16,
443
444 #[clap(long, env = "ESPRESSO_NODE_API_MAX_CONNECTIONS")]
450 pub max_connections: Option<usize>,
451
452 #[clap(long, env = "ESPRESSO_NODE_TONIC_PORT")]
454 pub tonic_port: Option<u16>,
455}
456
457impl Http {
458 pub fn with_port(port: u16) -> Self {
460 Self {
461 port,
462 max_connections: None,
463 tonic_port: None,
464 }
465 }
466}
467
468#[derive(Parser, Clone, Copy, Debug, Default)]
470pub struct Submit;
471
472#[derive(Parser, Clone, Copy, Debug, Default)]
474pub struct Status;
475
476#[derive(Parser, Clone, Copy, Debug, Default)]
478pub struct Catchup;
479
480#[derive(Parser, Clone, Copy, Debug, Default)]
482pub struct Config;
483
484#[derive(Parser, Clone, Debug, Default)]
486pub struct Query {
487 #[clap(long, env = "ESPRESSO_NODE_API_PEERS", value_delimiter = ',')]
489 pub peers: Vec<Url>,
490
491 #[clap(flatten)]
493 pub light_client: LightClientOptions,
494
495 #[clap(flatten)]
497 pub light_client_db: LightClientSqliteOptions,
498}
499
500#[cfg(test)]
501impl Query {
502 pub fn test() -> Self {
503 Self::default()
504 }
505}
506
507#[derive(Parser, Clone, Copy, Debug, Default)]
509pub struct State;
510
511#[derive(Parser, Clone, Copy, Debug, Default)]
513pub struct HotshotEvents;
514
515#[derive(Parser, Clone, Copy, Debug, Default)]
517pub struct Explorer;
518
519#[derive(Parser, Clone, Copy, Debug, Default)]
521pub struct LightClient;
522
523#[allow(clippy::type_complexity)]
529fn init_query_data_source<N, P, D>(
530 ds: D,
531 state: ApiState<N, P>,
532) -> (Box<dyn Metrics>, Arc<StorageState<N, P, D>>)
533where
534 N: ConnectedNetwork<PubKey>,
535 P: SequencerPersistence,
536 D: SequencerDataSource + CatchupStorage + PruningDataSource + Send + Sync + 'static,
537{
538 let metrics = ds.populate_metrics();
539 telemetry::set_registry(Arc::new(ds.metrics().registry().clone()));
540 let ds = Arc::new(ExtensibleDataSource::new(ds, state));
541 (metrics, ds)
542}
543
544fn get_public_env_vars() -> anyhow::Result<Vec<String>> {
546 #[derive(serde::Deserialize)]
547 struct PublicEnvVars {
548 variables: BTreeSet<String>,
549 }
550
551 let PublicEnvVars { variables } =
552 toml::from_str(include_str!("../../api/public-env-vars.toml"))?;
553 Ok(variables
554 .into_iter()
555 .map(|key| {
556 let value = env::var(&key).unwrap_or_default();
557 format!("{key}={value}")
558 })
559 .collect())
560}