sequencer/
options.rs

1#![allow(clippy::needless_lifetimes)]
2
3use core::fmt::Display;
4use std::{
5    cmp::Ordering,
6    collections::{HashMap, HashSet},
7    fmt::{self, Formatter},
8    iter::once,
9    path::PathBuf,
10    time::Duration,
11};
12
13use anyhow::{bail, Context};
14use clap::{error::ErrorKind, Args, FromArgMatches, Parser};
15use derivative::Derivative;
16use espresso_types::{parse_duration, BackoffParams, L1ClientOptions};
17use hotshot_types::{light_client::StateSignKey, signature_key::BLSPrivKey};
18use jf_signature::{bls_over_bn254, schnorr};
19use libp2p::Multiaddr;
20use sequencer_utils::logging;
21use tagged_base64::TaggedBase64;
22use url::Url;
23
24use crate::{api, persistence, proposal_fetcher::ProposalFetcherConfig};
25
26// This options struct is a bit unconventional. The sequencer has multiple optional modules which
27// can be added, in any combination, to the service. These include, for example, the API server.
28// Each of these modules has its own options, which are all required if the module is added but can
29// be omitted otherwise. Clap doesn't have a good way to handle "grouped" arguments like this (they
30// have something called an argument group, but it's different). Sub-commands do exactly this, but
31// you can't have multiple sub-commands in a single command.
32//
33// What we do, then, is take the optional modules as if they were sub-commands, but we use a Clap
34// `raw` argument to collect all the module commands and their options into a single string. This
35// string is then parsed manually (using a secondary Clap `Parser`, the `SequencerModule` type) when
36// the user calls `modules()`.
37//
38// One slightly unfortunate consequence of this is that the auto-generated help documentation for
39// `SequencerModule` is not included in the help for this top-level type. Users can still get at the
40// help for individual modules by passing `help` as a subcommand, as in
41// `sequencer [options] -- help` or `sequencer [options] -- help <module>`. This means that IT IS
42// BEST NOT TO ADD REQUIRED ARGUMENTS TO THIS TYPE, since the required arguments will be required
43// even if the user is only asking for help on a module. Try to give every argument on this type a
44// default value, even if it is a bit arbitrary.
45#[derive(Parser, Clone, Derivative)]
46#[derivative(Debug(bound = ""))]
47pub struct Options {
48    /// URL of the HotShot orchestrator.
49    #[clap(
50        short,
51        long,
52        env = "ESPRESSO_SEQUENCER_ORCHESTRATOR_URL",
53        default_value = "http://localhost:8080"
54    )]
55    #[derivative(Debug(format_with = "Display::fmt"))]
56    pub orchestrator_url: Url,
57
58    /// The socket address of the HotShot CDN's main entry point (the marshal)
59    /// in `IP:port` form
60    #[clap(
61        short,
62        long,
63        env = "ESPRESSO_SEQUENCER_CDN_ENDPOINT",
64        default_value = "127.0.0.1:8081"
65    )]
66    pub cdn_endpoint: String,
67
68    /// The address to bind to for Libp2p (in `host:port` form)
69    #[clap(
70        long,
71        env = "ESPRESSO_SEQUENCER_LIBP2P_BIND_ADDRESS",
72        default_value = "0.0.0.0:1769"
73    )]
74    pub libp2p_bind_address: String,
75
76    /// Time between each Libp2p heartbeat
77    #[clap(long, env = "ESPRESSO_SEQUENCER_LIBP2P_HEARTBEAT_INTERVAL", default_value = "1s", value_parser = parse_duration)]
78    pub libp2p_heartbeat_interval: Duration,
79
80    /// Number of past heartbeats to gossip about on Libp2p
81    #[clap(
82        long,
83        env = "ESPRESSO_SEQUENCER_LIBP2P_HISTORY_GOSSIP",
84        default_value = "3"
85    )]
86    pub libp2p_history_gossip: usize,
87
88    /// Number of heartbeats to keep in the Libp2p `memcache`
89    #[clap(
90        long,
91        env = "ESPRESSO_SEQUENCER_LIBP2P_HISTORY_LENGTH",
92        default_value = "5"
93    )]
94    pub libp2p_history_length: usize,
95
96    /// Target number of peers for the Libp2p mesh network
97    #[clap(long, env = "ESPRESSO_SEQUENCER_LIBP2P_MESH_N", default_value = "8")]
98    pub libp2p_mesh_n: usize,
99
100    /// Maximum number of peers in the Libp2p mesh network before removing some
101    #[clap(
102        long,
103        env = "ESPRESSO_SEQUENCER_LIBP2P_MESH_N_HIGH",
104        default_value = "12"
105    )]
106    pub libp2p_mesh_n_high: usize,
107
108    /// Minimum number of peers in the Libp2p mesh network before adding more
109    #[clap(
110        long,
111        env = "ESPRESSO_SEQUENCER_LIBP2P_MESH_N_LOW",
112        default_value = "6"
113    )]
114    pub libp2p_mesh_n_low: usize,
115
116    /// Minimum number of outbound Libp2p peers in the mesh network before adding more
117    #[clap(
118        long,
119        env = "ESPRESSO_SEQUENCER_LIBP2P_MESH_OUTBOUND_MIN",
120        default_value = "2"
121    )]
122    pub libp2p_mesh_outbound_min: usize,
123
124    /// The maximum number of messages to include in a Libp2p IHAVE message
125    #[clap(
126        long,
127        env = "ESPRESSO_SEQUENCER_LIBP2P_MAX_IHAVE_LENGTH",
128        default_value = "5000"
129    )]
130    pub libp2p_max_ihave_length: usize,
131
132    /// The maximum number of IHAVE messages to accept from a Libp2p peer within a heartbeat
133    #[clap(
134        long,
135        env = "ESPRESSO_SEQUENCER_LIBP2P_MAX_IHAVE_MESSAGES",
136        default_value = "10"
137    )]
138    pub libp2p_max_ihave_messages: usize,
139
140    /// Libp2p published message ids time cache duration
141    #[clap(long, env = "ESPRESSO_SEQUENCER_LIBP2P_PUBLISHED_MESSAGE_IDS_CACHE_TIME", default_value = "10s", value_parser = parse_duration)]
142    pub libp2p_published_message_ids_cache_time: Duration,
143
144    /// Time to wait for a Libp2p message requested through IWANT following an IHAVE advertisement
145    #[clap(
146        long,
147        env = "ESPRESSO_SEQUENCER_LIBP2P_MAX_IWANT_FOLLOWUP_TIME",
148        default_value = "3s", value_parser = parse_duration
149    )]
150    pub libp2p_iwant_followup_time: Duration,
151
152    /// The maximum number of Libp2p messages we will process in a given RPC
153    #[clap(long, env = "ESPRESSO_SEQUENCER_LIBP2P_MAX_MESSAGES_PER_RPC")]
154    pub libp2p_max_messages_per_rpc: Option<usize>,
155
156    /// How many times we will allow a Libp2p peer to request the same message id through IWANT gossip before we start ignoring them
157    #[clap(
158        long,
159        env = "ESPRESSO_SEQUENCER_LIBP2P_GOSSIP_RETRANSMISSION",
160        default_value = "3"
161    )]
162    pub libp2p_gossip_retransmission: u32,
163
164    /// If enabled newly created messages will always be sent to all peers that are subscribed to the topic and have a good enough score
165    #[clap(
166        long,
167        env = "ESPRESSO_SEQUENCER_LIBP2P_FLOOD_PUBLISH",
168        default_value = "true"
169    )]
170    pub libp2p_flood_publish: bool,
171
172    /// The time period that Libp2p message hashes are stored in the cache
173    #[clap(long, env = "ESPRESSO_SEQUENCER_LIBP2P_DUPLICATE_CACHE_TIME", default_value = "20m", value_parser = parse_duration)]
174    pub libp2p_duplicate_cache_time: Duration,
175
176    /// Time to live for Libp2p fanout peers
177    #[clap(long, env = "ESPRESSO_SEQUENCER_LIBP2P_FANOUT_TTL", default_value = "60s", value_parser = parse_duration)]
178    pub libp2p_fanout_ttl: Duration,
179
180    /// Initial delay in each Libp2p heartbeat
181    #[clap(long, env = "ESPRESSO_SEQUENCER_LIBP2P_HEARTBEAT_INITIAL_DELAY", default_value = "5s", value_parser = parse_duration)]
182    pub libp2p_heartbeat_initial_delay: Duration,
183
184    /// How many Libp2p peers we will emit gossip to at each heartbeat
185    #[clap(
186        long,
187        env = "ESPRESSO_SEQUENCER_LIBP2P_GOSSIP_FACTOR",
188        default_value = "0.25"
189    )]
190    pub libp2p_gossip_factor: f64,
191
192    /// Minimum number of Libp2p peers to emit gossip to during a heartbeat
193    #[clap(
194        long,
195        env = "ESPRESSO_SEQUENCER_LIBP2P_GOSSIP_LAZY",
196        default_value = "6"
197    )]
198    pub libp2p_gossip_lazy: usize,
199
200    /// The maximum number of bytes we will send in a single Libp2p gossip message
201    #[clap(
202        long,
203        env = "ESPRESSO_SEQUENCER_LIBP2P_MAX_GOSSIP_TRANSMIT_SIZE",
204        default_value = "2000000"
205    )]
206    pub libp2p_max_gossip_transmit_size: usize,
207
208    /// The maximum number of bytes we will send in a single Libp2p direct message
209    #[clap(
210        long,
211        env = "ESPRESSO_SEQUENCER_LIBP2P_MAX_DIRECT_TRANSMIT_SIZE",
212        default_value = "20000000"
213    )]
214    pub libp2p_max_direct_transmit_size: u64,
215
216    /// The URL we advertise to other nodes as being for our public API.
217    /// Should be supplied in `http://host:port` form.
218    #[clap(long, env = "ESPRESSO_SEQUENCER_PUBLIC_API_URL")]
219    pub public_api_url: Option<Url>,
220
221    /// The address we advertise to other nodes as being a Libp2p endpoint.
222    /// Should be supplied in `host:port` form.
223    #[clap(
224        long,
225        env = "ESPRESSO_SEQUENCER_LIBP2P_ADVERTISE_ADDRESS",
226        default_value = "localhost:1769"
227    )]
228    pub libp2p_advertise_address: String,
229
230    /// A comma-separated list of Libp2p multiaddresses to use as bootstrap
231    /// nodes.
232    ///
233    /// Overrides those loaded from the `HotShot` config.
234    #[clap(
235        long,
236        env = "ESPRESSO_SEQUENCER_LIBP2P_BOOTSTRAP_NODES",
237        value_delimiter = ',',
238        num_args = 1..
239    )]
240    pub libp2p_bootstrap_nodes: Option<Vec<Multiaddr>>,
241
242    /// URL of the Light Client State Relay Server
243    #[clap(
244        long,
245        env = "ESPRESSO_STATE_RELAY_SERVER_URL",
246        default_value = "http://localhost:8083"
247    )]
248    #[derivative(Debug(format_with = "Display::fmt"))]
249    pub state_relay_server_url: Url,
250
251    /// Path to TOML file containing genesis state.
252    #[clap(
253        long,
254        name = "GENESIS_FILE",
255        env = "ESPRESSO_SEQUENCER_GENESIS_FILE",
256        default_value = "/genesis/demo.toml"
257    )]
258    pub genesis_file: PathBuf,
259
260    /// Path to file containing private keys.
261    ///
262    /// The file should follow the .env format, with two keys:
263    /// * ESPRESSO_SEQUENCER_PRIVATE_STAKING_KEY
264    /// * ESPRESSO_SEQUENCER_PRIVATE_STATE_KEY
265    ///
266    /// Appropriate key files can be generated with the `keygen` utility program.
267    #[clap(long, name = "KEY_FILE", env = "ESPRESSO_SEQUENCER_KEY_FILE")]
268    pub key_file: Option<PathBuf>,
269
270    /// Private staking key.
271    ///
272    /// This can be used as an alternative to KEY_FILE.
273    #[clap(
274        long,
275        env = "ESPRESSO_SEQUENCER_PRIVATE_STAKING_KEY",
276        conflicts_with = "KEY_FILE"
277    )]
278    #[derivative(Debug = "ignore")]
279    pub private_staking_key: Option<TaggedBase64>,
280
281    /// Private state signing key.
282    ///
283    /// This can be used as an alternative to KEY_FILE.
284    #[clap(
285        long,
286        env = "ESPRESSO_SEQUENCER_PRIVATE_STATE_KEY",
287        conflicts_with = "KEY_FILE"
288    )]
289    #[derivative(Debug = "ignore")]
290    pub private_state_key: Option<TaggedBase64>,
291
292    /// Add optional modules to the service.
293    ///
294    /// Modules are added by specifying the name of the module followed by it's arguments, as in
295    ///
296    /// sequencer [options] -- api --port 3000
297    ///
298    /// to run the API module with port 3000.
299    ///
300    /// To see a list of available modules and their arguments, use
301    ///
302    /// sequencer -- help
303    ///
304    /// Multiple modules can be specified, provided they are separated by --
305    #[clap(raw = true)]
306    modules: Vec<String>,
307
308    /// Url we will use for RPC communication with L1.
309    #[clap(
310        long,
311        env = "ESPRESSO_SEQUENCER_L1_PROVIDER",
312        default_value = "http://localhost:8545",
313        value_delimiter = ',',
314        num_args = 1..,
315    )]
316    #[derivative(Debug = "ignore")]
317    pub l1_provider_url: Vec<Url>,
318
319    /// Configuration for the L1 client.
320    #[clap(flatten)]
321    pub l1_options: L1ClientOptions,
322
323    /// Whether or not we are a DA node.
324    #[clap(long, env = "ESPRESSO_SEQUENCER_IS_DA", action)]
325    pub is_da: bool,
326
327    /// Peer nodes use to fetch missing state
328    #[clap(long, env = "ESPRESSO_SEQUENCER_STATE_PEERS", value_delimiter = ',')]
329    #[derivative(Debug(format_with = "fmt_urls"))]
330    pub state_peers: Vec<Url>,
331
332    /// Peer nodes use to fetch missing config
333    ///
334    /// Typically, the network-wide config is fetched from the orchestrator on startup and then
335    /// persisted and loaded from local storage each time the node restarts. However, if the
336    /// persisted config is missing when the node restarts (for example, the node is being migrated
337    /// to new persistent storage), it can instead be fetched directly from a peer.
338    #[clap(long, env = "ESPRESSO_SEQUENCER_CONFIG_PEERS", value_delimiter = ',')]
339    #[derivative(Debug(format_with = "fmt_opt_urls"))]
340    pub config_peers: Option<Vec<Url>>,
341
342    /// Exponential backoff for fetching missing state from peers.
343    #[clap(flatten)]
344    pub catchup_backoff: BackoffParams,
345
346    #[clap(flatten)]
347    pub logging: logging::Config,
348
349    #[clap(flatten)]
350    pub identity: Identity,
351
352    #[clap(flatten)]
353    pub proposal_fetcher_config: ProposalFetcherConfig,
354}
355
356impl Options {
357    pub fn modules(&self) -> Modules {
358        ModuleArgs(self.modules.clone()).parse()
359    }
360
361    pub fn private_keys(&self) -> anyhow::Result<(BLSPrivKey, StateSignKey)> {
362        if let Some(path) = &self.key_file {
363            let vars = dotenvy::from_path_iter(path)?.collect::<Result<HashMap<_, _>, _>>()?;
364            let staking = TaggedBase64::parse(
365                vars.get("ESPRESSO_SEQUENCER_PRIVATE_STAKING_KEY")
366                    .context("key file missing ESPRESSO_SEQUENCER_PRIVATE_STAKING_KEY")?,
367            )?
368            .try_into()?;
369
370            let state = TaggedBase64::parse(
371                vars.get("ESPRESSO_SEQUENCER_PRIVATE_STATE_KEY")
372                    .context("key file missing ESPRESSO_SEQUENCER_PRIVATE_STATE_KEY")?,
373            )?
374            .try_into()?;
375
376            Ok((staking, state))
377        } else if let (Some(staking), Some(state)) = (
378            self.private_staking_key.clone(),
379            self.private_state_key.clone(),
380        ) {
381            let staking = bls_over_bn254::SignKey::try_from(staking)?;
382            let state = schnorr::SignKey::try_from(state)?;
383
384            Ok((staking, state))
385        } else {
386            bail!("neither key file nor full set of private keys was provided")
387        }
388    }
389}
390
391/// Identity represents identifying information concerning the sequencer node.
392/// This information is used to populate relevant information in the metrics
393/// endpoint.  This information will also potentially be scraped and displayed
394/// in a public facing dashboard.
395#[derive(Parser, Clone, Derivative)]
396#[derivative(Debug(bound = ""))]
397pub struct Identity {
398    #[clap(long, env = "ESPRESSO_SEQUENCER_IDENTITY_COUNTRY_CODE")]
399    pub country_code: Option<String>,
400    #[clap(long, env = "ESPRESSO_SEQUENCER_IDENTITY_LATITUDE")]
401    pub latitude: Option<f64>,
402    #[clap(long, env = "ESPRESSO_SEQUENCER_IDENTITY_LONGITUDE")]
403    pub longitude: Option<f64>,
404
405    #[clap(long, env = "ESPRESSO_SEQUENCER_IDENTITY_NODE_NAME")]
406    pub node_name: Option<String>,
407
408    #[clap(long, env = "ESPRESSO_SEQUENCER_IDENTITY_COMPANY_NAME")]
409    pub company_name: Option<String>,
410    #[clap(long, env = "ESPRESSO_SEQUENCER_IDENTITY_COMPANY_WEBSITE")]
411    pub company_website: Option<Url>,
412    #[clap(long, env = "ESPRESSO_SEQUENCER_IDENTITY_OPERATING_SYSTEM", default_value = std::env::consts::OS)]
413    pub operating_system: Option<String>,
414    #[clap(long, env = "ESPRESSO_SEQUENCER_IDENTITY_NODE_TYPE", default_value = get_default_node_type())]
415    pub node_type: Option<String>,
416    #[clap(long, env = "ESPRESSO_SEQUENCER_IDENTITY_NETWORK_TYPE")]
417    pub network_type: Option<String>,
418}
419
420/// get_default_node_type returns the current public facing binary name and
421/// version of this program.
422fn get_default_node_type() -> String {
423    format!("espresso-sequencer {}", env!("CARGO_PKG_VERSION"))
424}
425
426// The Debug implementation for Url is noisy, we just want to see the URL
427fn fmt_urls(v: &[Url], fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
428    write!(
429        fmt,
430        "{:?}",
431        v.iter().map(|i| i.to_string()).collect::<Vec<_>>()
432    )
433}
434
435fn fmt_opt_urls(
436    v: &Option<Vec<Url>>,
437    fmt: &mut std::fmt::Formatter,
438) -> Result<(), std::fmt::Error> {
439    match v {
440        Some(urls) => {
441            write!(fmt, "Some(")?;
442            fmt_urls(urls, fmt)?;
443            write!(fmt, ")")?;
444        },
445        None => {
446            write!(fmt, "None")?;
447        },
448    }
449    Ok(())
450}
451
452#[derive(Clone, Copy, Debug, PartialEq, Eq)]
453pub struct Ratio {
454    pub numerator: u64,
455    pub denominator: u64,
456}
457
458impl From<Ratio> for (u64, u64) {
459    fn from(r: Ratio) -> Self {
460        (r.numerator, r.denominator)
461    }
462}
463
464impl Display for Ratio {
465    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
466        write!(f, "{}:{}", self.numerator, self.denominator)
467    }
468}
469
470impl PartialOrd for Ratio {
471    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
472        Some(self.cmp(other))
473    }
474}
475
476impl Ord for Ratio {
477    fn cmp(&self, other: &Self) -> Ordering {
478        (self.numerator * other.denominator).cmp(&(other.numerator * self.denominator))
479    }
480}
481
482#[derive(Clone, Debug)]
483struct ModuleArgs(Vec<String>);
484
485impl ModuleArgs {
486    fn parse(&self) -> Modules {
487        match self.try_parse() {
488            Ok(modules) => modules,
489            Err(err) => err.exit(),
490        }
491    }
492
493    fn try_parse(&self) -> Result<Modules, clap::Error> {
494        let mut modules = Modules::default();
495        let mut curr = self.0.clone();
496        let mut provided = Default::default();
497
498        while !curr.is_empty() {
499            // The first argument (the program name) is used only for help generation. We include a
500            // `--` so that the generated usage will look like `sequencer -- <command>` which is the
501            // way these commands must be invoked due to the use of `raw` arguments.
502            let module = SequencerModule::try_parse_from(
503                once("sequencer --").chain(curr.iter().map(|s| s.as_str())),
504            )?;
505            match module {
506                SequencerModule::Storage(m) => {
507                    curr = m.add(&mut modules.storage_fs, &mut provided)?
508                },
509                SequencerModule::StorageFs(m) => {
510                    curr = m.add(&mut modules.storage_fs, &mut provided)?
511                },
512                SequencerModule::StorageSql(m) => {
513                    curr = m.add(&mut modules.storage_sql, &mut provided)?
514                },
515                SequencerModule::Http(m) => curr = m.add(&mut modules.http, &mut provided)?,
516                SequencerModule::Query(m) => curr = m.add(&mut modules.query, &mut provided)?,
517                SequencerModule::Submit(m) => curr = m.add(&mut modules.submit, &mut provided)?,
518                SequencerModule::Status(m) => curr = m.add(&mut modules.status, &mut provided)?,
519                SequencerModule::Catchup(m) => curr = m.add(&mut modules.catchup, &mut provided)?,
520                SequencerModule::Config(m) => curr = m.add(&mut modules.config, &mut provided)?,
521                SequencerModule::HotshotEvents(m) => {
522                    curr = m.add(&mut modules.hotshot_events, &mut provided)?
523                },
524                SequencerModule::Explorer(m) => {
525                    curr = m.add(&mut modules.explorer, &mut provided)?
526                },
527            }
528        }
529
530        Ok(modules)
531    }
532}
533
534trait ModuleInfo: Args + FromArgMatches {
535    const NAME: &'static str;
536    fn requires() -> Vec<&'static str>;
537}
538
539macro_rules! module {
540    ($name:expr, $opt:ty $(,requires: $($req:expr),*)?) => {
541        impl ModuleInfo for $opt {
542            const NAME: &'static str = $name;
543
544            fn requires() -> Vec<&'static str> {
545                vec![$($($req),*)?]
546            }
547        }
548    };
549}
550
551module!("storage-fs", persistence::fs::Options);
552module!("storage-sql", persistence::sql::Options);
553module!("http", api::options::Http);
554module!("query", api::options::Query, requires: "http");
555module!("submit", api::options::Submit, requires: "http");
556module!("status", api::options::Status, requires: "http");
557module!("catchup", api::options::Catchup, requires: "http");
558module!("config", api::options::Config, requires: "http");
559module!("hotshot-events", api::options::HotshotEvents, requires: "http");
560module!("explorer", api::options::Explorer, requires: "http", "storage-sql");
561
562#[derive(Clone, Debug, Args)]
563struct Module<Options: ModuleInfo> {
564    #[clap(flatten)]
565    options: Box<Options>,
566
567    /// Add more optional modules.
568    #[clap(raw = true)]
569    modules: Vec<String>,
570}
571
572impl<Options: ModuleInfo> Module<Options> {
573    /// Add this as an optional module. Return the next optional module args.
574    fn add(
575        self,
576        options: &mut Option<Options>,
577        provided: &mut HashSet<&'static str>,
578    ) -> Result<Vec<String>, clap::Error> {
579        if options.is_some() {
580            return Err(clap::Error::raw(
581                ErrorKind::TooManyValues,
582                format!("optional module {} can only be started once", Options::NAME),
583            ));
584        }
585        for req in Options::requires() {
586            if !provided.contains(&req) {
587                return Err(clap::Error::raw(
588                    ErrorKind::MissingRequiredArgument,
589                    format!("module {} is missing required module {req}", Options::NAME),
590                ));
591            }
592        }
593        *options = Some(*self.options);
594        provided.insert(Options::NAME);
595        Ok(self.modules)
596    }
597}
598
599#[derive(Clone, Debug, Parser)]
600enum SequencerModule {
601    /// Run an HTTP server.
602    ///
603    /// The basic HTTP server comes with healthcheck and version endpoints. Add additional endpoints
604    /// by enabling additional modules:
605    /// * query: add query service endpoints
606    /// * submit: add transaction submission endpoints
607    Http(Module<api::options::Http>),
608    /// Alias for storage-fs.
609    Storage(Module<persistence::fs::Options>),
610    /// Use the file system for persistent storage.
611    StorageFs(Module<persistence::fs::Options>),
612    /// Use a Postgres database for persistent storage.
613    StorageSql(Module<persistence::sql::Options>),
614    /// Run the query API module.
615    ///
616    /// This module requires the http module to be started.
617    Query(Module<api::options::Query>),
618    /// Run the transaction submission API module.
619    ///
620    /// This module requires the http module to be started.
621    Submit(Module<api::options::Submit>),
622    /// Run the status API module.
623    ///
624    /// This module requires the http module to be started.
625    Status(Module<api::options::Status>),
626    /// Run the state catchup API module.
627    ///
628    /// This module requires the http module to be started.
629    Catchup(Module<api::options::Catchup>),
630    /// Run the config API module.
631    Config(Module<api::options::Config>),
632
633    /// Run the hotshot events API module.
634    ///
635    /// This module requires the http module to be started.
636    HotshotEvents(Module<api::options::HotshotEvents>),
637    /// Run the explorer API module.
638    ///
639    /// This module requires the http and storage-sql modules to be started.
640    Explorer(Module<api::options::Explorer>),
641}
642
643#[derive(Clone, Debug, Default)]
644pub struct Modules {
645    pub storage_fs: Option<persistence::fs::Options>,
646    pub storage_sql: Option<persistence::sql::Options>,
647    pub http: Option<api::options::Http>,
648    pub query: Option<api::options::Query>,
649    pub submit: Option<api::options::Submit>,
650    pub status: Option<api::options::Status>,
651    pub catchup: Option<api::options::Catchup>,
652    pub config: Option<api::options::Config>,
653    pub hotshot_events: Option<api::options::HotshotEvents>,
654    pub explorer: Option<api::options::Explorer>,
655}