espresso_node/
options.rs

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