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    /// The URL of the builders to use for submitting transactions
243    #[clap(long, env = "ESPRESSO_SEQUENCER_BUILDER_URLS", value_delimiter = ',')]
244    pub builder_urls: Vec<Url>,
245
246    /// URL of the Light Client State Relay Server
247    #[clap(
248        long,
249        env = "ESPRESSO_STATE_RELAY_SERVER_URL",
250        default_value = "http://localhost:8083"
251    )]
252    #[derivative(Debug(format_with = "Display::fmt"))]
253    pub state_relay_server_url: Url,
254
255    /// Path to TOML file containing genesis state.
256    #[clap(
257        long,
258        name = "GENESIS_FILE",
259        env = "ESPRESSO_SEQUENCER_GENESIS_FILE",
260        default_value = "/genesis/demo.toml"
261    )]
262    pub genesis_file: PathBuf,
263
264    /// Path to file containing private keys.
265    ///
266    /// The file should follow the .env format, with two keys:
267    /// * ESPRESSO_SEQUENCER_PRIVATE_STAKING_KEY
268    /// * ESPRESSO_SEQUENCER_PRIVATE_STATE_KEY
269    ///
270    /// Appropriate key files can be generated with the `keygen` utility program.
271    #[clap(long, name = "KEY_FILE", env = "ESPRESSO_SEQUENCER_KEY_FILE")]
272    pub key_file: Option<PathBuf>,
273
274    /// Private staking key.
275    ///
276    /// This can be used as an alternative to KEY_FILE.
277    #[clap(
278        long,
279        env = "ESPRESSO_SEQUENCER_PRIVATE_STAKING_KEY",
280        conflicts_with = "KEY_FILE"
281    )]
282    #[derivative(Debug = "ignore")]
283    pub private_staking_key: Option<TaggedBase64>,
284
285    /// Private state signing key.
286    ///
287    /// This can be used as an alternative to KEY_FILE.
288    #[clap(
289        long,
290        env = "ESPRESSO_SEQUENCER_PRIVATE_STATE_KEY",
291        conflicts_with = "KEY_FILE"
292    )]
293    #[derivative(Debug = "ignore")]
294    pub private_state_key: Option<TaggedBase64>,
295
296    /// Add optional modules to the service.
297    ///
298    /// Modules are added by specifying the name of the module followed by it's arguments, as in
299    ///
300    /// sequencer [options] -- api --port 3000
301    ///
302    /// to run the API module with port 3000.
303    ///
304    /// To see a list of available modules and their arguments, use
305    ///
306    /// sequencer -- help
307    ///
308    /// Multiple modules can be specified, provided they are separated by --
309    #[clap(raw = true)]
310    modules: Vec<String>,
311
312    /// Url we will use for RPC communication with L1.
313    #[clap(
314        long,
315        env = "ESPRESSO_SEQUENCER_L1_PROVIDER",
316        default_value = "http://localhost:8545",
317        value_delimiter = ',',
318        num_args = 1..,
319    )]
320    #[derivative(Debug = "ignore")]
321    pub l1_provider_url: Vec<Url>,
322
323    /// Configuration for the L1 client.
324    #[clap(flatten)]
325    pub l1_options: L1ClientOptions,
326
327    /// Whether or not we are a DA node.
328    #[clap(long, env = "ESPRESSO_SEQUENCER_IS_DA", action)]
329    pub is_da: bool,
330
331    /// Peer nodes use to fetch missing state
332    #[clap(long, env = "ESPRESSO_SEQUENCER_STATE_PEERS", value_delimiter = ',')]
333    #[derivative(Debug(format_with = "fmt_urls"))]
334    pub state_peers: Vec<Url>,
335
336    /// Peer nodes use to fetch missing config
337    ///
338    /// Typically, the network-wide config is fetched from the orchestrator on startup and then
339    /// persisted and loaded from local storage each time the node restarts. However, if the
340    /// persisted config is missing when the node restarts (for example, the node is being migrated
341    /// to new persistent storage), it can instead be fetched directly from a peer.
342    #[clap(long, env = "ESPRESSO_SEQUENCER_CONFIG_PEERS", value_delimiter = ',')]
343    #[derivative(Debug(format_with = "fmt_opt_urls"))]
344    pub config_peers: Option<Vec<Url>>,
345
346    /// Exponential backoff for fetching missing state from peers.
347    #[clap(flatten)]
348    pub catchup_backoff: BackoffParams,
349
350    #[clap(flatten)]
351    pub logging: logging::Config,
352
353    #[clap(flatten)]
354    pub identity: Identity,
355
356    #[clap(flatten)]
357    pub proposal_fetcher_config: ProposalFetcherConfig,
358}
359
360impl Options {
361    pub fn modules(&self) -> Modules {
362        ModuleArgs(self.modules.clone()).parse()
363    }
364
365    pub fn private_keys(&self) -> anyhow::Result<(BLSPrivKey, StateSignKey)> {
366        if let Some(path) = &self.key_file {
367            let vars = dotenvy::from_path_iter(path)?.collect::<Result<HashMap<_, _>, _>>()?;
368            let staking = TaggedBase64::parse(
369                vars.get("ESPRESSO_SEQUENCER_PRIVATE_STAKING_KEY")
370                    .context("key file missing ESPRESSO_SEQUENCER_PRIVATE_STAKING_KEY")?,
371            )?
372            .try_into()?;
373
374            let state = TaggedBase64::parse(
375                vars.get("ESPRESSO_SEQUENCER_PRIVATE_STATE_KEY")
376                    .context("key file missing ESPRESSO_SEQUENCER_PRIVATE_STATE_KEY")?,
377            )?
378            .try_into()?;
379
380            Ok((staking, state))
381        } else if let (Some(staking), Some(state)) = (
382            self.private_staking_key.clone(),
383            self.private_state_key.clone(),
384        ) {
385            let staking = bls_over_bn254::SignKey::try_from(staking)?;
386            let state = schnorr::SignKey::try_from(state)?;
387
388            Ok((staking, state))
389        } else {
390            bail!("neither key file nor full set of private keys was provided")
391        }
392    }
393}
394
395/// Identity represents identifying information concerning the sequencer node.
396/// This information is used to populate relevant information in the metrics
397/// endpoint.  This information will also potentially be scraped and displayed
398/// in a public facing dashboard.
399#[derive(Parser, Clone, Derivative)]
400#[derivative(Debug(bound = ""))]
401pub struct Identity {
402    #[clap(long, env = "ESPRESSO_SEQUENCER_IDENTITY_COUNTRY_CODE")]
403    pub country_code: Option<String>,
404    #[clap(long, env = "ESPRESSO_SEQUENCER_IDENTITY_LATITUDE")]
405    pub latitude: Option<f64>,
406    #[clap(long, env = "ESPRESSO_SEQUENCER_IDENTITY_LONGITUDE")]
407    pub longitude: Option<f64>,
408
409    #[clap(long, env = "ESPRESSO_SEQUENCER_IDENTITY_NODE_NAME")]
410    pub node_name: Option<String>,
411    #[clap(long, env = "ESPRESSO_SEQUENCER_IDENTITY_NODE_DESCRIPTION")]
412    pub node_description: Option<String>,
413
414    #[clap(long, env = "ESPRESSO_SEQUENCER_IDENTITY_COMPANY_NAME")]
415    pub company_name: Option<String>,
416    #[clap(long, env = "ESPRESSO_SEQUENCER_IDENTITY_COMPANY_WEBSITE")]
417    pub company_website: Option<Url>,
418    #[clap(long, env = "ESPRESSO_SEQUENCER_IDENTITY_OPERATING_SYSTEM", default_value = std::env::consts::OS)]
419    pub operating_system: Option<String>,
420    #[clap(long, env = "ESPRESSO_SEQUENCER_IDENTITY_NODE_TYPE", default_value = get_default_node_type())]
421    pub node_type: Option<String>,
422    #[clap(long, env = "ESPRESSO_SEQUENCER_IDENTITY_NETWORK_TYPE")]
423    pub network_type: Option<String>,
424
425    #[clap(long, env = "ESPRESSO_SEQUENCER_IDENTITY_ICON_14x14_1x")]
426    pub icon_14x14_1x: Option<Url>,
427    #[clap(long, env = "ESPRESSO_SEQUENCER_IDENTITY_ICON_14x14_2x")]
428    pub icon_14x14_2x: Option<Url>,
429    #[clap(long, env = "ESPRESSO_SEQUENCER_IDENTITY_ICON_14x14_3x")]
430    pub icon_14x14_3x: Option<Url>,
431    #[clap(long, env = "ESPRESSO_SEQUENCER_IDENTITY_ICON_24x24_1x")]
432    pub icon_24x24_1x: Option<Url>,
433    #[clap(long, env = "ESPRESSO_SEQUENCER_IDENTITY_ICON_24x24_2x")]
434    pub icon_24x24_2x: Option<Url>,
435    #[clap(long, env = "ESPRESSO_SEQUENCER_IDENTITY_ICON_24x24_3x")]
436    pub icon_24x24_3x: Option<Url>,
437}
438
439/// get_default_node_type returns the current public facing binary name and
440/// version of this program.
441fn get_default_node_type() -> String {
442    format!("espresso-sequencer {}", env!("CARGO_PKG_VERSION"))
443}
444
445// The Debug implementation for Url is noisy, we just want to see the URL
446fn fmt_urls(v: &[Url], fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
447    write!(
448        fmt,
449        "{:?}",
450        v.iter().map(|i| i.to_string()).collect::<Vec<_>>()
451    )
452}
453
454fn fmt_opt_urls(
455    v: &Option<Vec<Url>>,
456    fmt: &mut std::fmt::Formatter,
457) -> Result<(), std::fmt::Error> {
458    match v {
459        Some(urls) => {
460            write!(fmt, "Some(")?;
461            fmt_urls(urls, fmt)?;
462            write!(fmt, ")")?;
463        },
464        None => {
465            write!(fmt, "None")?;
466        },
467    }
468    Ok(())
469}
470
471#[derive(Clone, Copy, Debug, PartialEq, Eq)]
472pub struct Ratio {
473    pub numerator: u64,
474    pub denominator: u64,
475}
476
477impl From<Ratio> for (u64, u64) {
478    fn from(r: Ratio) -> Self {
479        (r.numerator, r.denominator)
480    }
481}
482
483impl Display for Ratio {
484    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
485        write!(f, "{}:{}", self.numerator, self.denominator)
486    }
487}
488
489impl PartialOrd for Ratio {
490    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
491        Some(self.cmp(other))
492    }
493}
494
495impl Ord for Ratio {
496    fn cmp(&self, other: &Self) -> Ordering {
497        (self.numerator * other.denominator).cmp(&(other.numerator * self.denominator))
498    }
499}
500
501#[derive(Clone, Debug)]
502struct ModuleArgs(Vec<String>);
503
504impl ModuleArgs {
505    fn parse(&self) -> Modules {
506        match self.try_parse() {
507            Ok(modules) => modules,
508            Err(err) => err.exit(),
509        }
510    }
511
512    fn try_parse(&self) -> Result<Modules, clap::Error> {
513        let mut modules = Modules::default();
514        let mut curr = self.0.clone();
515        let mut provided = Default::default();
516
517        while !curr.is_empty() {
518            // The first argument (the program name) is used only for help generation. We include a
519            // `--` so that the generated usage will look like `sequencer -- <command>` which is the
520            // way these commands must be invoked due to the use of `raw` arguments.
521            let module = SequencerModule::try_parse_from(
522                once("sequencer --").chain(curr.iter().map(|s| s.as_str())),
523            )?;
524            match module {
525                SequencerModule::Storage(m) => {
526                    curr = m.add(&mut modules.storage_fs, &mut provided)?
527                },
528                SequencerModule::StorageFs(m) => {
529                    curr = m.add(&mut modules.storage_fs, &mut provided)?
530                },
531                SequencerModule::StorageSql(m) => {
532                    curr = m.add(&mut modules.storage_sql, &mut provided)?
533                },
534                SequencerModule::Http(m) => curr = m.add(&mut modules.http, &mut provided)?,
535                SequencerModule::Query(m) => curr = m.add(&mut modules.query, &mut provided)?,
536                SequencerModule::Submit(m) => curr = m.add(&mut modules.submit, &mut provided)?,
537                SequencerModule::Status(m) => curr = m.add(&mut modules.status, &mut provided)?,
538                SequencerModule::Catchup(m) => curr = m.add(&mut modules.catchup, &mut provided)?,
539                SequencerModule::Config(m) => curr = m.add(&mut modules.config, &mut provided)?,
540                SequencerModule::HotshotEvents(m) => {
541                    curr = m.add(&mut modules.hotshot_events, &mut provided)?
542                },
543                SequencerModule::Explorer(m) => {
544                    curr = m.add(&mut modules.explorer, &mut provided)?
545                },
546                SequencerModule::LightClient(m) => {
547                    curr = m.add(&mut modules.light_client, &mut provided)?
548                },
549            }
550        }
551
552        Ok(modules)
553    }
554}
555
556trait ModuleInfo: Args + FromArgMatches {
557    const NAME: &'static str;
558    fn requires() -> Vec<&'static str>;
559}
560
561macro_rules! module {
562    ($name:expr, $opt:ty $(,requires: $($req:expr),*)?) => {
563        impl ModuleInfo for $opt {
564            const NAME: &'static str = $name;
565
566            fn requires() -> Vec<&'static str> {
567                vec![$($($req),*)?]
568            }
569        }
570    };
571}
572
573module!("storage-fs", persistence::fs::Options);
574module!("storage-sql", persistence::sql::Options);
575module!("http", api::options::Http);
576module!("query", api::options::Query, requires: "http");
577module!("submit", api::options::Submit, requires: "http");
578module!("status", api::options::Status, requires: "http");
579module!("catchup", api::options::Catchup, requires: "http");
580module!("config", api::options::Config, requires: "http");
581module!("hotshot-events", api::options::HotshotEvents, requires: "http");
582module!("explorer", api::options::Explorer, requires: "http", "storage-sql");
583module!("light-client", api::options::LightClient, requires: "http", "storage-sql");
584
585#[derive(Clone, Debug, Args)]
586struct Module<Options: ModuleInfo> {
587    #[clap(flatten)]
588    options: Box<Options>,
589
590    /// Add more optional modules.
591    #[clap(raw = true)]
592    modules: Vec<String>,
593}
594
595impl<Options: ModuleInfo> Module<Options> {
596    /// Add this as an optional module. Return the next optional module args.
597    fn add(
598        self,
599        options: &mut Option<Options>,
600        provided: &mut HashSet<&'static str>,
601    ) -> Result<Vec<String>, clap::Error> {
602        if options.is_some() {
603            return Err(clap::Error::raw(
604                ErrorKind::TooManyValues,
605                format!("optional module {} can only be started once", Options::NAME),
606            ));
607        }
608        for req in Options::requires() {
609            if !provided.contains(&req) {
610                return Err(clap::Error::raw(
611                    ErrorKind::MissingRequiredArgument,
612                    format!("module {} is missing required module {req}", Options::NAME),
613                ));
614            }
615        }
616        *options = Some(*self.options);
617        provided.insert(Options::NAME);
618        Ok(self.modules)
619    }
620}
621
622#[derive(Clone, Debug, Parser)]
623enum SequencerModule {
624    /// Run an HTTP server.
625    ///
626    /// The basic HTTP server comes with healthcheck and version endpoints. Add additional endpoints
627    /// by enabling additional modules:
628    /// * query: add query service endpoints
629    /// * submit: add transaction submission endpoints
630    Http(Module<api::options::Http>),
631    /// Alias for storage-fs.
632    Storage(Module<persistence::fs::Options>),
633    /// Use the file system for persistent storage.
634    StorageFs(Module<persistence::fs::Options>),
635    /// Use a Postgres database for persistent storage.
636    StorageSql(Module<persistence::sql::Options>),
637    /// Run the query API module.
638    ///
639    /// This module requires the http module to be started.
640    Query(Module<api::options::Query>),
641    /// Run the transaction submission API module.
642    ///
643    /// This module requires the http module to be started.
644    Submit(Module<api::options::Submit>),
645    /// Run the status API module.
646    ///
647    /// This module requires the http module to be started.
648    Status(Module<api::options::Status>),
649    /// Run the state catchup API module.
650    ///
651    /// This module requires the http module to be started.
652    Catchup(Module<api::options::Catchup>),
653    /// Run the config API module.
654    Config(Module<api::options::Config>),
655
656    /// Run the hotshot events API module.
657    ///
658    /// This module requires the http module to be started.
659    HotshotEvents(Module<api::options::HotshotEvents>),
660    /// Run the explorer API module.
661    ///
662    /// This module requires the http and storage-sql modules to be started.
663    Explorer(Module<api::options::Explorer>),
664    /// Run the light client API module.
665    ///
666    /// This module provides data and proofs necessary for an untrusting light client to retrieve
667    /// and verify Espresso data from this server.
668    ///
669    /// This module requires the http and storage-sql modules to be started.
670    LightClient(Module<api::options::LightClient>),
671}
672
673#[derive(Clone, Debug, Default)]
674pub struct Modules {
675    pub storage_fs: Option<persistence::fs::Options>,
676    pub storage_sql: Option<persistence::sql::Options>,
677    pub http: Option<api::options::Http>,
678    pub query: Option<api::options::Query>,
679    pub submit: Option<api::options::Submit>,
680    pub status: Option<api::options::Status>,
681    pub catchup: Option<api::options::Catchup>,
682    pub config: Option<api::options::Config>,
683    pub hotshot_events: Option<api::options::HotshotEvents>,
684    pub explorer: Option<api::options::Explorer>,
685    pub light_client: Option<api::options::LightClient>,
686}